LoginSignup
3
1

More than 3 years have passed since last update.

CloudFormation (S3+サムネイル画像)

Last updated at Posted at 2019-06-09

概要

S3に画像登録したら、サムネイル画像を作成、CloudFront(CDN)で配信する環境を作成してみる。

※ S3バケットの「 origin 」フォルダに画像(jpg/png)をアップロードしたら、
 「 thumb 」フォルダに80x80px以内にリサイズしたサムネイル画像を作成する。

前提条件

  • macOS
  • Docker Toolbox ( Docker for mac )

ファイル構成

下記のようなファイルを作成します。

ファイル構成
opt
 ├ dis       <- サムネイル画像生成コード(Node.js)のパッケージ(zip)の格納フォルダ
 │ ├ dist/thumbnail-function.v***.zip
 │    ... 
 │ 
 ├ docker
 │ ├ aws-cli
 │ │  ├ cmd
 │ │  │ ├ code.sh
 │ │  │ └ deploy.sh
 │ │  └ Dockerfile
 │ │
 │ └ lambda
 │    ├ cmd
 │    │ └ thumbnail.sh
 │    └ Dockerfile
 │
 ├ src
 │  ├ thumbnail-function
 │  │  └ index.js
 │  └ template.yml
 │
 └ docker-compose.yml

aws-cliコンテナの構築ファイル

Dockerfile作成 (aws-cli)

aws-cliコマンドが使えるコンテナを構築する。

./docker/aws-cli/Dockerfile
FROM python:3.6

ARG pip_installer="https://bootstrap.pypa.io/get-pip.py"
ARG awscli_version="1.16.168"

# install aws-cli
RUN pip install awscli==${awscli_version}

# install sam
RUN pip install --user --upgrade aws-sam-cli
ENV PATH $PATH:/root/.local/bin

# install command.
RUN apt-get update && apt-get install -y less vim

# copy aws command.
COPY ./docker/aws-cli/cmd/code.sh /root/code.sh
COPY ./docker/aws-cli/cmd/lambda.sh /root/lambda.sh

# copy source code.
COPY ./src /src

WORKDIR /root

コマンド作成 (code.sh)

lambda関数に登録するプログラムをアップするS3バケットを作成し、アップロードするshellスクリプト

./docker/aws-cli/cmd/code.sh
#!/bin/bash

source /root/.bashrc

if [ -z "${CODE_BUCKET}" ]; then
    echo -e "環境変数「CODE_BUCKET」が定義されていません\n"
    exit 1
fi
if [ -z "$CODE_VERSION" ]; then
    echo -e "環境変数「CODE_VERSION」が定義されていません\n"
    exit 1
fi

create() {
    aws s3 mb s3://${CODE_BUCKET}
}
upload() {
    aws s3 cp /dist/thumbnail-function.${CODE_VERSION}.zip s3://${CODE_BUCKET}/
}
clear() {
    rm -f /dist/thumbnail-function.${CODE_VERSION}.zip
}

# 引数(オプション)
while getopts ":-:" opt; do
    case "$opt" in
        -)
            case "${OPTARG}" in
                create)
                    create
                    upload
                    clear
                    exit 0 ;;
                upload)
                    upload
                    clear
                    exit 0 ;;
                *) ;;
            esac ;;
    esac
done

echo -e "Usage: code.sh [--create] [--upload]\n"
exit 1

コマンド作成 (deploy.sh)

CloudFormationを実行するshellスクリプト

./docker/aws-cli/cmd/deploy.sh
#!/bin/bash

source /root/.bashrc

if [ -z "${STACK_NAME}" ]; then
    echo "環境変数「STACK_NAME」が定義されていません\n"
    exit 1
fi
if [ -z "${IMAGE_BUCKET}" ]; then
    echo "環境変数「IMAGE_BUCKET」が定義されていません\n"
    exit 1
fi
if [ -z "${CODE_BUCKET}" ]; then
    echo -e "環境変数「CODE_BUCKET」が定義されていません\n"
    exit 1
fi
if [ -z "$CODE_VERSION" ]; then
    echo -e "環境変数「CODE_VERSION」が定義されていません\n"
    exit 1
fi

YAML="template.yml"
SRC="/src/${YAML}"
FILE="/root/${YAML}"

if [ -e "${FILE}" ]; then
    rm -f ${FILE}
fi

cp ${SRC} ${FILE}
sed -i "s/<image-bucket>/${IMAGE_BUCKET}/g" ${FILE}
sed -i "s/<code-bucket>/${CODE_BUCKET}/g" ${FILE}
sed -i "s/<code-version>/${CODE_VERSION}/g" ${FILE}

aws cloudformation deploy \
    --template-file ${FILE} \
    --stack-name "${STACK_NAME}" \
    --capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM

lambdaコンテナの構築ファイル

Dockerfile作成 (lambda)

lambda関数をコンパイル&パッケージングするコンテナを構築する。

./docker/lambda/Dockerfile
FROM amazonlinux

WORKDIR /tmp

# install the dependencies.
RUN yum -y install gcc-c++ && yum -y install findutils tar zip gzip && \
    touch ~/.bashrc && chmod +x ~/.bashrc && \
    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.5/install.sh | bash && \
    source ~/.bashrc && nvm install 8.10

# copy aws command.
COPY ./docker/lambda/cmd/thumbnail.sh /root/thumbnail.sh

# copy source code.
RUN mkdir -p /src && chmod -x /src
COPY ./src /src

WORKDIR /root

コマンド作成 (thumbnail.sh)

npm installを実行し、アプリケーションをzip圧縮するスクリプト

./docker/lambda/cmd/thumbnail.sh
#!/bin/bash

source /root/.bashrc

if [ -z "$CODE_VERSION" ]; then
    echo -e "環境変数「CODE_VERSION」が定義されていません\n"
    exit 1
fi

compile() {
    mkdir -p /tmp/thumbnail-function/
    cd /tmp/thumbnail-function/
    cp /src/thumbnail-function/index.js .
    npm init -f -y;
    npm install async gm --save;
    npm install --only=prod
}
package() {
    mkdir -p /dist
    cd /tmp/thumbnail-function/
    file="/dist/thumbnail-function.${CODE_VERSION}.zip"
    zip -FS -q -r "${file}" *
    echo "export: ${file}"
}

while getopts ":-:" opt; do
    case "$opt" in
        -)
            case "${OPTARG}" in
                package)
                    compile
                    package
                    exit 0 ;;
                *) ;;
            esac ;;
    esac
done

echo -e "Usage: thumbnail.sh --package\n"
exit 1

lambda関数の作成

lambda関数で実行するNode.jsのスクリプトを作成する

./src/thumbnail-function/index.js
/**
 * AWSドキュメント - Amazon S3 で AWS Lambda を使用する
 * https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html
 */
'use strict';
console.log('Loading function');

// dependencies
var async = require('async');
var AWS = require('aws-sdk');
var gm = require('gm')
    .subClass({ imageMagick: true }); // Enable ImageMagick integration.
var util = require('util');

// constants
var MAX_WIDTH  = 80;
var MAX_HEIGHT = 80;

// get reference to S3 client
var s3 = new AWS.S3();

exports.handler = function(event, context, callback) {
    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
    var srcBucket = event.Records[0].s3.bucket.name;
    // Object key may have spaces or unicode non-ASCII characters.
    var srcKey    =
        decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
    var dstBucket = srcBucket + "/thumb";
    var dstKey    = srcKey.substr(7);

    // Sanity check: validate that source and destination are different buckets.
    if (srcBucket == dstBucket) {
        callback("Source and destination buckets are the same.");
        return;
    }

    // Infer the image type.
    var typeMatch = srcKey.match(/\.([^.]*)$/);
    if (!typeMatch) {
        callback("Could not determine the image type.");
        return;
    }
    var imageType = typeMatch[1];
    if (imageType != "jpg" && imageType != "png") {
        callback('Unsupported image type: ${imageType}');
        return;
    }

    // Download the image from S3, transform, and upload to a different S3 bucket.
    async.waterfall([
            function download(next) {
                // Download the image from S3 into a buffer.
                s3.getObject({
                        Bucket: srcBucket,
                        Key: srcKey
                    },
                    next);
            },
            function transform(response, next) {
                gm(response.Body).size(function(err, size) {
                    // Infer the scaling factor to avoid stretching the image unnaturally.
                    var scalingFactor = Math.min(
                        MAX_WIDTH / size.width,
                        MAX_HEIGHT / size.height
                    );
                    var width  = scalingFactor * size.width;
                    var height = scalingFactor * size.height;

                    // Transform the image buffer in memory.
                    this.resize(width, height)
                        .toBuffer(imageType, function(err, buffer) {
                            if (err) {
                                next(err);
                            } else {
                                next(null, response.ContentType, buffer);
                            }
                        });
                });
            },
            function upload(contentType, data, next) {
                // Stream the transformed image to a different S3 bucket.
                s3.putObject({
                        Bucket: dstBucket,
                        Key: dstKey,
                        Body: data,
                        ContentType: contentType
                    },
                    next);
            }
        ], function (err) {
            if (err) {
                console.error(
                    'Unable to resize ' + srcBucket + '/' + srcKey +
                    ' and upload to ' + dstBucket + '/' + dstKey +
                    ' due to an error: ' + err
                );
            } else {
                console.log(
                    'Successfully resized ' + srcBucket + '/' + srcKey +
                    ' and uploaded to ' + dstBucket + '/' + dstKey
                );
            }

            callback(null, "message");
        }
    );
};

docker-compose.yml作成

下記の内容で作成します。

docker-compose.yml
version: '3'
services:
  aws-cli:
    container_name: 'aws-cli'
    image: aws-s3/aws-cli
    build:
      context: ./
      dockerfile: ./docker/aws-cli/Dockerfile
    tty:true
    environment:
      AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
      AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
      AWS_DEFAULT_REGION: ap-northeast-1
      AWS_DEFAULT_OUTPUT: json

  lambda:
    container_name: 'lambda'
    image: local/lambda
    build:
      context: ./
      dockerfile: docker/lambda/Dockerfile
    volumes:
      - ./dist:/dist

template.yml作成

CloudFormationの設定ファイルを作成します。
※ 「<image-bucket>,<code-version>,<code-version>」は、shellスクリプト内で置換される

./src/template.yml
AWSTemplateFormatVersion: 2010-09-09
Description: "create image thumnail."
Resources:

  # Lambda関数にセットするRole(役割)を定義する
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          Effect: "Allow"
          Principal:
            Service:
              - "lambda.amazonaws.com"
          Action:
            - "sts:AssumeRole"
      Path: "/lambda-role/"
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
        - "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"

  # サムネイル画像を作るLambda関数を定義する
  ThumbLambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        S3Bucket: "<code-bucket>"
        S3Key: "thumbnail-function.<code-version>.zip"
      Handler: "index.handler"
      Runtime: "nodejs8.10"
      MemorySize: "192"
      Timeout: "25"
      Role: !GetAtt LambdaExecutionRole.Arn

  # S3BucketからLambda関数を実行する許可を与える
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt
        - ThumbLambdaFunction
        - Arn
      Action: 'lambda:InvokeFunction'
      Principal: s3.amazonaws.com
      SourceAccount: !Ref 'AWS::AccountId'
      SourceArn: arn:aws:s3:::<image-bucket>

  # S3Bucket作成
  ImageBucket:
    Type: "AWS::S3::Bucket"
    DeletionPolicy: "Retain"
    Properties:
      AccessControl: "PublicRead"
      BucketName: !Sub <image-bucket>
      NotificationConfiguration:
        LambdaConfigurations:
          - Event: "s3:ObjectCreated:*"
            Filter:
              S3Key:
                Rules:
                  - Name: "prefix"
                    Value: "origin/"
                  - Name: "suffix"
                    Value: ".png"
            Function: !GetAtt
              - ThumbLambdaFunction
              - Arn
          - Event: "s3:ObjectCreated:*"
            Filter:
              S3Key:
                Rules:
                  - Name: "prefix"
                    Value: "origin/"
                  - Name: "suffix"
                    Value: ".jpg"
            Function: !GetAtt
              - ThumbLambdaFunction
              - Arn

  # S3Bucketのポリシーを定義する
  ImageBucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Sub ${ImageBucket}
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject
            Effect: "Allow"
            Principal: "*"
            Resource: !Sub arn:aws:s3:::${ImageBucket}/*
          - Action:
              - s3:PutObject
            Effect: "Allow"
            Principal:
              AWS: !GetAtt LambdaExecutionRole.Arn
            Resource: !Sub arn:aws:s3:::${ImageBucket}/*
          - Action:
              - s3:GetObject
            Effect: "Allow"
            Principal:
              AWS: !GetAtt LambdaExecutionRole.Arn
            Resource: !Sub arn:aws:s3:::${ImageBucket}/*

  # CloudFront
  MyDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - DomainName: <image-bucket>.s3.amazonaws.com
            Id: myS3Origin
            S3OriginConfig: {}
        Enabled: 'true'
        Comment: distribution for content delivery
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          TargetOriginId: myS3Origin
          ForwardedValues:
            QueryString: 'true'
            Cookies:
              Forward: 'none'
          ViewerProtocolPolicy: allow-all
          MinTTL: '100'
          SmoothStreaming: 'false'
          Compress: 'true'
        PriceClass: PriceClass_All
        ViewerCertificate:
          CloudFrontDefaultCertificate: 'true'

Outputs:
  ImageBucket:
    Value: !Ref ImageBucket
    Export:
      Name: !Sub "${AWS::StackName}-ImageBucket"

  MyDistribution:
    Value: !Ref MyDistribution
    Export:
      Name: !Sub "${AWS::StackName}-MyDistribution"

サーバ構築

下記コマンドにてdockerコンテナのイメージを構築します。

ターミナル
$ docker-compose build

環境設定

下記のように環境変数を定義します。
※ ここで定義した環境変数は、自動的にdockerコンテナに取り込まれます

ターミナル
$ export AWS_ACCESS_KEY_ID='xxxxxxxxxxxxx'
$ export AWS_SECRET_ACCESS_KEY='xxxxxxxxxxxxxxxxxx'
$ export STACK_NAME='sample-s3-thumb'
$ export CODE_BUCKET='sample-s3-thumb-func'
$ export IMAGE_BUCKET='sample-s3-thumb'
$ export CODE_VERSION='v1'

デプロイ

CloudFormationでStackを登録してAWSの各リソースを自動生成する。

ターミナル
$ export CODE_VERSION='v1'
  docker-compose build && \
  docker-compose run --rm lambda /root/thumbnail.sh --package && \
  docker-compose run --rm aws-cli /root/code.sh --create && \
  docker-compose run --rm aws-cli /root/deploy.sh

動作確認

# S3 - オリジナル画像
https://******.s3-ap-northeast-1.amazonaws.com/origin/sample.png

# S3 - サムネイル画像 (80x80px以内)
https://******.s3-ap-northeast-1.amazonaws.com/thumb/sample.png

# CloudFront - オリジナル画像
https://**********.cloudfront.net/origin/sample.png

# CloudFront - サムネイル画像 (80x80px以内)
https://**********.cloudfront.net/thumb/sample.png

メンテナンス

lamda関数の更新

JavaScriptを変更して、CloudFormationでStackを更新し、lambdaのリソースを再構築する。

ターミナル
# ↓ 最新バージョン+1を指定する
$ export CODE_VERSION='v2' && \
  docker-compose build && \
  docker-compose run --rm lambda /root/thumbnail.sh --package && \
  docker-compose run --rm aws-cli /root/code.sh --upload && \
  docker-compose run --rm aws-cli /root/deploy.sh

参考サイト

以上

3
1
1

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
3
1