はじめに
aws.amazon.com
こちらの記事を参考に画像をリサイズするLambda@Edgeを作ろうとしたが環境構築がCloudFormationを使って0の状態から作成されており、すでに本番環境で動いてるS3やcloudfrontに対して適用するのにGUIからやりたかったのでこれを実際にGUIで設定しようとしたらどうなるのか?というのをやってみました。
一部、元の記事の設定ではうまく動かない部分やハマった部分についても記述します。もしかしたらうまく設定できない方がいれば何かの参考になるかもしれません。
CloudFormationのテンプレートを読み解く
yamlファイルは元の記事を見てもらうとして、やってることは以下のようなことになります。
- Lambda@Edge用のIAMRoleを作る
- S3のバケットを用意し、1で作ったIAMRoleに権限付与する
- viewer-requestとorigin-responseのlambda関数を作成する。
- CloudFrontを用意する。2で作ったS3をオリジンにし、3で作ったlambda関数をlambdaEdge関数として適用する
ただ今回はGUIでやっていくので多少順番を入れ替えて以下のような作業工程で完成を目指します。
- Lambda@Edge用のIAMRoleを作る
- S3のバケットを用意し、1で作ったIAMRoleに権限付与する
- 2で作ったS3をオリジンとするCloudFrontを用意する
- viewer-requestとorigin-responseのlambda関数を作成する
- lambdaEdge関数にデプロイする
これらを順番にGUIで設定していけば画像をリサイズするLamda@Edgeが完成し
cloudfrontドメイン/path?d=100x100
のようなURLで画像ファイルにアクセスすると100x100のリサイズされた画像が表示されるようになります。
順番に設定してみましょう。
1. Lambda@Edge用のIAMRoleを作る
EdgeLambdaRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" - "edgelambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/service-role/" ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
CloudFormationでいうところこの部分を作成します
IAMRoleの管理画面から、カスタム信頼ポリシーを選択し
信頼関係に
- edgelambda.amazonaws.com
- lambda.amazonaws.com
の二つを追加したIAMRoleを作成します
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "edgelambda.amazonaws.com", "lambda.amazonaws.com" ] }, "Action": "sts:AssumeRole" } ] }
IAMRoleは以上です。このIAMRoleをS3やlambda関数に使っていきます。
2. S3のバケットを用意し、手順1で作ったIAMRoleに権限付与する
ImageBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref ImageBucket PolicyDocument: Statement: - Action: - s3:GetObject Effect: Allow Principal: "*" Resource: !Sub arn:aws:s3:::${ImageBucket}/* - Action: - s3:PutObject Effect: Allow Principal: AWS: !GetAtt EdgeLambdaRole.Arn Resource: !Sub arn:aws:s3:::${ImageBucket}/* - Action: - s3:GetObject Effect: Allow Principal: AWS: !GetAtt EdgeLambdaRole.Arn Resource: !Sub arn:aws:s3:::${ImageBucket}/*
CloudFormationでいうところこの部分を作成します。
S3バケットを作成し
PutObjectを1で作ったIAMRoleに権限付与し、
GetObjectとListBucketは全体公開します。
ListBucketの権限はCloudFormation側にはないのですが、これを追加しておかないと後々うまく動かない部分があるので付けておいたほうが良いです。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::XXXXXX:role/EdgeLambdaRole" // 手順1で作成したIAMRole }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::lambdaedge-test/*" }, { "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::lambdaedge-test/*" }, { "Effect": "Allow", "Principal": "*", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::lambdaedge-test" } ] }
3. 2で作ったS3をオリジンとするCloudFrontを用意する
MyDistribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Origins: - DomainName: !Sub ${ImageBucket}.s3.amazonaws.com Id: myS3Origin S3OriginConfig: {} Enabled: 'true' Comment: distribution for content delivery DefaultRootObject: index.html DefaultCacheBehavior: TargetOriginId: myS3Origin LambdaFunctionAssociations: - EventType: 'viewer-request' LambdaFunctionARN: !Ref ViewerRequestFunctionVersion - EventType: 'origin-response' LambdaFunctionARN: !Ref OriginResponseFunctionVersion ForwardedValues: QueryString: 'true' QueryStringCacheKeys: - d Cookies: Forward: 'none' ViewerProtocolPolicy: allow-all MinTTL: '100' SmoothStreaming: 'false' Compress: 'true' PriceClass: PriceClass_All ViewerCertificate: CloudFrontDefaultCertificate: 'true'
CloudFormationでいうところこの部分を作成します。
先にキャッシュポリシーを作成します。
キャッシュキーの設定のクエリ文字列に「d」を設定すれば他はデフォルトでOKです。
CloudFrontを作成します。
オリジンを2で作成したS3を選択します。
キャッシュポリシーを先ほど作成したキャッシュポリシーを選択します。
以上でCloudFrontの設定は完了です。
ためしにS3に画像をアップロードしてCloudFrontのドメインで表示されればOKです。
4. viewer-requestとorigin-responseのlambda関数を作成し、lambdaEdge関数にデプロイする
※lambda関数はus-east-1のリージョンに作成します
ViewerRequestFunction: Type: AWS::Serverless::Function Properties: CodeUri: s3://<code-bucket>/viewer-request-function.zip Handler: index.handler Runtime: nodejs6.10 MemorySize: 128 Timeout: 1 Role: !GetAtt EdgeLambdaRole.Arn ViewerRequestFunctionVersion: Type: "AWS::Lambda::Version" Properties: FunctionName: !Ref ViewerRequestFunction Description: "A version of ViewerRequestFunction" OriginResponseFunction: Type: AWS::Serverless::Function Properties: CodeUri: s3://<code-bucket>/origin-response-function.zip Handler: index.handler Runtime: nodejs6.10 MemorySize: 512 Timeout: 5 Role: !GetAtt EdgeLambdaRole.Arn OriginResponseFunctionVersion: Type: "AWS::Lambda::Version" Properties: FunctionName: !Ref OriginResponseFunction Description: "A version of OriginResponseFunction"
CloudFormationでいうところこの部分を作成します。
どちらもzipにしたソースコードをS3にアップロードしてそれをlambda関数にしていますが
Viewer-Request関数に関しては直接ソースコードを記述しても問題ありません。
Origin-Response関数もS3にアップしてますが直接zipをアップロードしてもデプロイ可能なので好みの方法でデプロイしてください。
注意点としては、S3のバケットを両方とも正しく設定することと、decodeURIしてrequest.uriの値を変数にいれておかないと、S3にアップされたファイル名にマルチバイト文字が含まれているとエラーになります。
// read the required path. Ex: uri /images/100x100/webp/image.jpg let path = decodeURI(request.uri); //decodeURIしておく
5. Lambda@Edgeへデプロイする
Viewer-Request関数とOrigin-Response関数が作成できたらCloudFrontのlambdaEdgeにデプロイします。
Viewer-Request関数のLambda@Edgeへのデプロイ
Lambdaの右上のActionボタンから「Lambda@Edgeへのデプロイ」を選択します。
Distributionに作成したCloudFrontを選択し、CloudFront eventにViewer requestを選択しデプロイします。
Origin-Response関数のLambda@Edgeへのデプロイ 同様の手順でOrigin-Response関数もLambda@Edgeへのデプロイします。
デプロイがうまく行っていれば、CloudFront側のビヘイビアの設定をみるとLambda@Edgeが紐づいているのがわかります。
以上ですべての設定が完了です。
S3に画像をアップロードして
cloudfrontドメイン/path?d=100x100
cloudfrontドメイン/path?d=200x200
クエリパラメーターをつけたURLにアクセスし、リサイズされた画像が表示されれば完成です。
その他補足
GUIで設定していく上でいくつかハマった点やおまけの情報をいくつか紹介します。
Lambda@Edgeへのデプロイでエラーになる場合
Lambda@Edgeへのデプロイで以下のようなエラーが表示された場合
IAMRoleが正しいか確認しましょう
Lambda@Edgeの関数をテストしたいとき
Viewer-Request関数の場合
cloudfront-access-request-in-response のテンプレートを選択しuriとquerystringの部分を修正する
{ //省略 "clientIp": "2001:cdba::3257:9652", "uri": "/images/test-image.jpg", //s3のファイルのパス "querystring": "d=200x200", //リサイズの指定 "method": "GET" //省略 }
これでテストを実行し、成功するかどうかでチェックできます。
Origin-Response関数の場合
cloudfront-access-request-in-response
のテンプレートを選択しuri,querystring,responseのstatusを変更します
S3のimages/test-image.jpg
のファイルがあると想定して
{ "Records": [ // 省略 "clientIp": "2001:cdba::3257:9652", "uri": "/images/200x200s/webp/test-image.jpg", //viewer-requestのfunctionが返してくるpath "querystring": "d=200x200", //リサイズの指定 "method": "GET" }, "response": { "status": "404", // 404にする "statusDescription": "Not Found", //省略 ] }
この設定でテスト実行した結果
S3のパスimages/test-image.jpg
の画像がリサイズされたものが
/images/200x200s/webp/test-image.jpg
に作成されていれば成功しています。
CloudFrontのLambda@Edgeの関数のURLにアクセスしてエラーになる場合
AccessDeniedが表示される場合
S3のバケットポリシーにs3:ListBucketの権限が付与されているか確認しましょう。
存在しない画像URLにアクセスした時に404が帰る状態になっていないとLambda@Edgeの関数がうまく動きません。
アスペクト比を保ったままリサイズしたい
現在の実装だと正方形に切り取ることしかできないのでアスペクト比を保ったままリサイズしようとするとOrigin-Response関数に少し手を加えないといけません。
具体的にはクエリパラメーターのmodeというキーの値を元にS3から取得した元画像をどのように切り取るかを決定する処理を追加します。
以下のように修正します。
exports.handler = (event, context, callback) => { let response = event.Records[0].cf.response; //check if image is not present if (response.status == 404) { let request = event.Records[0].cf.request; let params = querystring.parse(request.querystring); // if there is no dimension attribute, just pass the response if (!params.d) { callback(null, response); return; } // read the dimension parameter value = width x height and split it by 'x' let dimensionMatch = params.d.split("x"); // read the required path. Ex: uri /images/100x100/webp/image.jpg let path = decodeURI(request.uri); // read the S3 key from the path variable. // Ex: path variable /images/100x100/webp/image.jpg let key = path.substring(1); // parse the prefix, width, height and image name // Ex: key=images/200x200/webp/image.jpg let prefix, originalKey, match, width, height, mode, requiredFormat, imageName; let startIndex; try { match = key.match(/(.*)\/(\d+)x(\d+)([sivh]?)\/(.*)\/(.*)/); prefix = match[1]; width = parseInt(match[2], 10); height = parseInt(match[3], 10); mode = match[4] ? match[4] : "s"; // correction for jpg required for 'Sharp' requiredFormat = match[5] == "jpg" ? "jpeg" : match[5]; imageName = match[6]; originalKey = prefix + "/" + imageName; } catch (err) { // no prefix exist for image.. match = key.match(/(\d+)x(\d+)([sivh]?)\/(.*)\/(.*)/); width = parseInt(match[1], 10); height = parseInt(match[2], 10); mode = match[3] ? match[3] : "s"; // correction for jpg required for 'Sharp' requiredFormat = match[4] == "jpg" ? "jpeg" : match[4]; imageName = match[5]; originalKey = imageName; } // get the source image file S3.getObject({ Bucket: BUCKET, Key: originalKey }).promise() // perform the resize operation .then(data => { let sharpConfig = {}; /* * mode * - s(square) : 中央を正方形に切り取り(デフォルト) * - i(inside) : アスペクト比を変えずに指定の正方形サイズに収まる様に切り取り * - v(vertical) : アスペクト比を変えずに、縦にサイズを合わせる * - h(horizontal) : アスペクト比を変えずに、横にサイズを合わせる */ switch(mode){ case 'i': sharpConfig.fit = 'inside'; sharpConfig.width = width; sharpConfig.height = height; break; case 'v': sharpConfig.height = height; break; case 'h': sharpConfig.width = width; break; default: sharpConfig.width = width; sharpConfig.height = height; } return Sharp(data.Body) .resize(sharpConfig) .toFormat(requiredFormat) .toBuffer(); }) .then(buffer => { // save the resized object to S3 bucket with appropriate object key. S3.putObject({ Body: buffer, Bucket: BUCKET, ContentType: 'image/' + requiredFormat, CacheControl: 'max-age=31536000', Key: key, StorageClass: 'STANDARD' }).promise() // even if there is exception in saving the object we send back the generated // image back to viewer below .catch(() => { console.log("Exception while writing resized image to bucket")}); // generate a binary response with resized image response.status = 200; response.body = buffer.toString('base64'); response.bodyEncoding = 'base64'; response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/' + requiredFormat }]; callback(null, response); }) .catch( err => { console.log("Exception while reading source image :%j",err); }); } // end of if block checking response statusCode else { // allow the response to pass through callback(null, response); } };
修正後デプロイした後動作確認します。
元画像
d=300x300でアクセス
d=300x300&mode=i でアクセス
以上のようにリサイズされていれば成功です。
最後に
長い文章になってしまい、やはりLambda@Edgeを手動で設定するのは大変ということがわかりました。
大人しくCloudFormationを勉強したいと思います。
GreenSnapではエンジニアを募集中です。サーバーサイド、iOS、Androidエンジニアはもちろんのこと
これから作成するデータ基盤とそれを活用できるデータサイエンティストの方も募集しています。
一人ひとりが幅広くいろんな技術領域を担当しているので、特定の技術だけを担当するより多くの技術に触れたい方や挑戦したい方は是非お声がけください。