概要
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コマンドが使えるコンテナを構築する。
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スクリプト
# !/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スクリプト
# !/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関数をコンパイル&パッケージングするコンテナを構築する。
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圧縮するスクリプト
# !/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のスクリプトを作成する
/**
* 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作成
下記の内容で作成します。
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スクリプト内で置換される
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
参考サイト
- AWSドキュメント - Amazon S3 で AWS Lambda を使用する
- S3の画像アップロードをトリガーにサムネイル画像を自動生成する
- S3にcreateObjectをトリガーにLambdaを起動するCloudformationテンプレート
- LambdaでS3にアップロードされが画像をサムネイルにしてみた
- CloudFrontでマルチオリジンとCache Behavior設定してみた
以上