AWS Lambda@Edge + CloudFront でサーバレス画像リサイズサーバ構築
AWS Lambda@Edge + CloudFront でサーバレス画像リサイズサーバ構築:
AWS Lambda@Edge と CloudFront の組み合わせを使って画像リサイズを動的に行える環境を構築するメモ。
環境変数を埋め込むために
ServerlessFramework の環境を用意します。
ここで Lambda の設定と権限を作ります。
コードに埋め込みたい環境変数はこちらで管理します。
画像のリサイズには sharp ライブラリを利用します。また S3 からのファイルダウンロードは
まず CloudFront へのリクエスト時に URL をパースしてあげます。ここで行っているのは大きく2点。
storage.js で正規化されたリクエスト URL を受け取って実際のレスポンスを返却します。すでに S3 にリサイズファイルのキャッシュがある場合はそれをそのまま返します。なければ S3 からファイルを取得して Sharp でリサイズ後に S3 に保存します。
Lambda@Edge は npm パッケージをバンドリしてアップロードする必要があります。Sharp ライブラリはネイティブライブラリなので Mac などでインストールしたパッケージは Lambda では利用できません。そこで Docker を使って Sharp だけ置き換えるようにします。
これでようやく Lambda@Edge のデプロイが完了しました。なんか長いですね。あと一息です。
CloudFront のコンソールから「Behaviors」を開き作成します。
Lambda Function ARN はバージョンも含めた完全なものを指定する必要があります。
これですべての設定は完了です。おつかれさまでした。
AWS Lambda@Edge と CloudFront の組み合わせを使って画像リサイズを動的に行える環境を構築するメモ。
前提
- ServerlessFramework 利用
- リサイズは Sharp を利用
- 画像オリジンサーバは S3 を利用
- webp に対応している UA は webp 変換
- エンドポイント URL: https://example.com/sample/hoge.jpg?size=100x50
- S3 オリジンに保存されているパス: example-bucket/sample/hoge.jpg
- リサイズファイル保持パス: example-bucket/100x50/jpg/sample/hoge.jpg
手順
ServerlessFramework
$ mkdir sample $ cd sample $ npm init $ npm install -g serverless $ npm install --save serverless-plugin-embedded-env-in-code
serverless-plugin-embedded-env-in-code
というプラグイン作ったので、これを利用します。ServerlessFramework の環境を用意します。
$ sls create -t aws-nodejs --name cloudfront-resize
serverless.yml
ここで Lambda の設定と権限を作ります。service: cloudfront-edge package: individually: true exclude: - node_modules/** - lambda_modules/** provider: name: aws runtime: nodejs8.10 region: us-east-1 memorySize: 128 timeout: 5 role: LambdaEdgeRole logRetentionInDays: 30 stage: ${opt:stage, 'development'} profile: ${self:custom.profiles.${self:provider.stage}} plugins: - serverless-plugin-embedded-env-in-code custom: profiles: development: profile-name production: profile-name otherfile: environment: development: ${file(./conf/development.yml)} production: ${file(./conf/production.yml)} functions: storage: handler: storage.redirect resize: handler: resize.perform embedded: files: - resize.js - resize-func.js variables: File_Original_Url: ${self:custom.otherfile.environment.${self:provider.stage}.File_Original_Url} Cache_S3_Bucket: ${self:custom.otherfile.environment.${self:provider.stage}.Cache_S3_Bucket} timeout: 20 memorySize: 512 package: include: - node_modules/** resources: Resources: LambdaEdgeRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com - edgelambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: ${opt:stage}-serverless-lambdaedge PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - logs:DescribeLogStreams Resource: 'arn:aws:logs:*:*:*' - Effect: "Allow" Action: - "s3:PutObject" - "s3:GetObject" - "s3:PutObjectAcl" Resource: - "arn:aws:s3:::example-bucket-name/*"
conf/development.yml
コードに埋め込みたい環境変数はこちらで管理します。File_Original_Url: https://s3.amazonaws.com/example-bucket-name Cache_S3_Bucket: example-bucket-name
Lambda@Edge
画像のリサイズには sharp ライブラリを利用します。また S3 からのファイルダウンロードは request-promise
を利用します。S3 SDK の getObject
を使っても良いのですが、S3 以外のオリジンサーバを利用することを想定して通常の HTTP リクエストにしています。$ npm install --save sharp request-promise
storage.js
まず CloudFront へのリクエスト時に URL をパースしてあげます。ここで行っているのは大きく2点。-
?size=100x50
のクエリストリングをパースして、定義済みのサイズに一番近いものに正規化する - webp 対応のブラウザの場合は webp に変換するように URL を修正
'use strict' const querystring = require('querystring') const variables = { allowedDimension: [ { w: 128, h: 128 }, { w: 256, h: 256 }, { w: 512, h: 512 }, { w: 1024, h: 1024 }, { w: 2048, h: 2048 }, { w: 3072, h: 3072 }, { w: 4096, h: 4096 } ], defaultDimension: { w: 128, h: 128 }, variance: 20 } const getExt = url => { const match = url.match(/\.((gif|jpg|jpeg|png)+)/) if (match) { return match[1] } return '' } const balanceSize = size => { let [width, height] = size.split('x') let matchFound = false let variancePercent = (variables.variance / 100) for (let dimension of variables.allowedDimension) { let minWidth = dimension.w - (dimension.w * variancePercent) let maxWidth = dimension.w + (dimension.w * variancePercent) if (width >= minWidth && width <= maxWidth) { width = dimension.w if (height) { height = dimension.h } matchFound = true break } } if (!matchFound) { width = variables.defaultDimension.w height = variables.defaultDimension.h } return [width, height] } const normalizedExt = (ext, headers) => { const accept = headers['accept'] ? headers['accept'][0].value : '' if (accept.includes('webp')) { url.push('webp') } else { url.push(ext) } } module.exports.redirect = (event, context, callback) => { const request = event.Records[0].cf.request const ext = getExt(request.uri) const params = querystring.parse(request.querystring) const originalUri = request.uri let url = [] if (params.size) { const [width, height] = balanceSize(params.size) url.push(width + 'x' + height) url.push(normalizedExt(ext, headers)) } else { url.push('original') url.push(ext) } url.push(originalUri) const rewriteUri = '/' + url.join('/') request.uri = rewriteUri callback(null, request) }
resize.js
storage.js で正規化されたリクエスト URL を受け取って実際のレスポンスを返却します。すでに S3 にリサイズファイルのキャッシュがある場合はそれをそのまま返します。なければ S3 からファイルを取得して Sharp でリサイズ後に S3 に保存します。'use strict' const AWS = require('aws-sdk') const S3 = new AWS.S3({ signatureVersion: 'v4' }) const Sharp = require('sharp') const request = require('request-promise') const download = (url) => { return request({ url: url, encoding: null }) } const resize = (body, format, width, height) => { return Sharp(body) .resize(width, height) .toFormat(format) .toBuffer() .then(buffer => { return buffer }) } const save = (body, format, key) => { return S3.putObject({ Body: body, Bucket: process.env.Cache_S3_Bucket, ContentType: 'image/' + format, CacheControl: 'max-age=31536000', Key: key, StorageClass: 'STANDARD', ACL: 'public-read' }).promise() } const setResponse = response => { response.status = 200 response.body = body.toString('base64') response.bodyEncoding = 'base64' response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/' + format }] response.headers['cache-control'] = [{ key: 'Cache-Control', value: 'max-age=31536000' }] return response } module.exports.perform = (event, context, callback) => { let response = event.Records[0].cf.response const request = event.Records[0].cf.request if (response.status === '404') { const match = request.uri.match(/^\/(.+?)\/(.+?)\/(.+)$/) const size = match[1] const format = match[2] const key = match[3] const originalUrl = `${process.env.File_Original_Url}${key}` if (size === 'original' || format === 'gif') { download(originalUrl).then(body => { save(body, format, request.uri).then(() => { response = setResponse(response) callback(null, response) }) }) } else { resizeFunc.download(originalUrl).then(body => { let [width, height] = size.split('x') width = width !== 'undefined' ? width : undefined height = height !== 'undefined' ? height : undefined resize(body, format, width, height).then(body => { save(body, format, request.uri).then(() => { response = setResponse(response) callback(null, response) }) }) }) } } else { callback(null, response) } }
デプロイ
Lambda@Edge は npm パッケージをバンドリしてアップロードする必要があります。Sharp ライブラリはネイティブライブラリなので Mac などでインストールしたパッケージは Lambda では利用できません。そこで Docker を使って Sharp だけ置き換えるようにします。
Sharp を Lambda で使う
$ mkdir lambda_modules $ cd lambda_modules $ npm init $ npm install --save sharp request-promise serverless-plugin-embedded-env-in-code $ rm -rf node_modules/sharp $ docker run -v "$PWD":/var/task lambci/lambda:build-nodejs8.10 npm install $ rm -rf ../node_modules && mv ./node_modules ../
ServerlessFramework デプロイ
$ sls deploy -v --stage development --aws-profile profile-name
CloudFront の設定
CloudFront のコンソールから「Behaviors」を開き作成します。- Path Pattern:
/sample/*
- Lambda Function Associations:
- Origin Response: resize.js
- Viewer Request: storage.js
Lambda Function ARN はバージョンも含めた完全なものを指定する必要があります。
これですべての設定は完了です。おつかれさまでした。
コメント
コメントを投稿