LoginSignup
12
9

More than 5 years have passed since last update.

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

Posted at

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 はバージョンも含めた完全なものを指定する必要があります。

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

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9