AWS Lambda@Edge + CloudFront でサーバレス画像リサイズサーバ構築

AWS Lambda@Edge + CloudFront でサーバレス画像リサイズサーバ構築:

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 
これでようやく Lambda@Edge のデプロイが完了しました。なんか長いですね。あと一息です。


CloudFront の設定

CloudFront のコンソールから「Behaviors」を開き作成します。

  • Path Pattern: /sample/*
  • Lambda Function Associations:

    • Origin Response: resize.js
    • Viewer Request: storage.js


AWS_CloudFront_Management_Console.png


Lambda Function ARN はバージョンも含めた完全なものを指定する必要があります。

これですべての設定は完了です。おつかれさまでした。

コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)