<2018/03/08追記>
2018年3月、Lambda@Edgeを使った画像リサイズのソリューションがAWSの公式ブログにて紹介されました。
- Amazon CloudFront & Lambda@Edge で画像をリサイズする - Amazon Web Services ブログ
今後サーバレスな画像リサイズ処理について検討する場合は、上のブログ記事を参照されることをお勧めいたします。
</追記>
2016年11月に Amazon API Gatewayがバイナリデータに対応したので、Amazon CloudFront、AWS Lambda、Amazon S3を組み合わせて、フルマネージドな画像変換(リサイズ)APIを作ってみる。
やりたいこと
URL中のパスで指定したファイルを、クエリパラメータで指定したサイズ(今回は幅のみ指定)に変換して返す。
例えば HTML中にimgタグで
<img src="https://resizer.example.com/sample.png?w=50">
とか書くだけで望んだ画像を望んだサイズで(sample.pngを幅50で)表示したい。
こうすれば数あるサイズのデバイスに対応するのにいちいちサイズ別画像を生成しておかなくてもよくてハッピー。
例
元画像
https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/sample.png
幅50に変換
https://d3rdcsp6kyxueg.cloudfront.net/sample.png?w=50
幅200に変換
https://d3rdcsp6kyxueg.cloudfront.net/sample.png?w=200
いい感じ
ざっくり処理概要
- Lambda
- S3から画像をとってきて変換してBase64で返す
- S3
- 元画像置き場
- API Gateway
- Content-Type, Acceptヘッダがimage/*なときにバイナリを扱えるよう設定
- Lambdaから返ってきたBase64 stringをバイナリにして返す
- CloudFront
- 今回はキャッシュ目的でなく、 API GatewayへのリクエストにContent-Type, Acceptヘッダを付加する ために使う
- もちろんキャッシュしてもいい
アーキテクチャ
作り方
※ CloudFront の Distribution 作成後、有効になるまで数十分かかる可能性があるので注意。
Amazon S3
特に複雑なことはせず、読み込み可能な元画像を置いておく。今回は
https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/sample.png
を用意。
AWS Lambda
パラメータ w
と filename
を受け取り、 S3の元画像を取得し、imagemagickで変換して、Base64にして返す。
LambdaのBlueprint s3-get-object
を元にして作った。
Step-by-step instruction
1. Lambdaのマネジメントコンソールで Create a Lambda Function
または Lambda関数の作成
ボタンをクリック
2. Select blueprint(設計図の選択)
で s3-get-object
と入力し、Node.jsのものを選択
3. Configure triggers(トリガーの設定)
で S3 っぽいものが出ているかも知れないが、 Remove(削除)
ボタンで消す(今回のトリガーはあとでAPI Gatewayを設定するので)
4. Lambdaファンクション名は好きな名前でよいが ImageResizer
あたりにしておく。ソースコードのエディタ部分に ↓の ImageResizer.js
をコピペする
5. コード中の key
と bucket
を自分が作ったバケット名とアップロードしたファイルのパスに合わせる
6. エディタの下にある Lambda function handler and role(Lambda 関数ハンドラおよびロール)
で、 Role name(ロール名)
に ImageResizerLambdaRole
とかそれっぽい名前をつける
7. 次へ
8. Create function(関数の作成)
ボタンをクリック
コード
このサンプルは最低限の処理になっていますが、
誰でもどんなサイズでも好き勝手に画像生成されてはたまらん、という方は
パラメータのValidationなどを入れることを考慮 するとよいです。
'use strict';
const im = require('imagemagick');
const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });
exports.handler = (event, context, callback) => {
const key = "demos/ImageResizer/img/" + event.filename;
const bucket = "tsukada-aws";
const params = {
Bucket: bucket,
Key: key,
};
s3.getObject(params, (err, data) => {
if (err) {
console.log(err);
const message = `Error getting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
console.log(message);
callback(message);
} else {
console.log(data.Body);
console.log('CONTENT TYPE:', data.ContentType);
im.resize({
srcData: data.Body,
format: "png",
width: event.w
}, function(err, stdout, stderr) {
if (err) {
callback('resize failed', err);
} else {
console.log("resized!");
callback(null, new Buffer(stdout, 'binary').toString('base64'))
}
});
}
});
};
今回は最低限 width
を変更しているだけだが、画像品質やその他もろもろに対応しようと思えばできる。
IAM Roleの設定
LambdaにはS3にアクセス可能(read only)なIAM Roleを設定する。
Create new role from template(s)
で S3 object read-only permissions
を選ぶ。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::*"
}
]
}
↑これと、デフォルトのLog出力用ポリシー↓の二つが付いている状態になる。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:ap-northeast-1:************:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:************:log-group:/aws/lambda/ImageResizer:*"
]
}
]
}
テスト
こんな感じで w
と filename
を渡すようにテストイベントを設定して Save and Testし、
succeededでBase64な文字列が返ってきていればたぶん大丈夫。
API Gateway
Step-by-step Instruction
1. API GatewayのマネジメントコンソールでCreate API(APIの作成)
をクリック
2. API Name
に Image Resizer
と入れ Create API(APIの作成)
ボタンをクリック
3. 画面左の Binary Support(バイナリサポート)
を開き、 Edit(編集)
ボタンをクリックし image/*
と入力、 Save(保存
する
4. 画面左の Resources(リソース)
を開き、 Actions(アクション)
ボタンを押して Create resource(リソースの作成)
を選択
5. Configure as proxy resource(プロキシリソースとして設定)
にチェックを入れ、 Resource Name(リソース名)
に filename
、 Resource Path(リソースパス)
に {filename+}
と入力し、 Enable API Gateway CORS(API Gateway CORS を有効にする)
にチェックを入れ Create Resource(リソースの作成)
ボタンをクリック
6. 勝手にできた ANY
メソッドは今回必要ないので Actions
> Delete Method
7. 代わりに GET
メソッドを作りたいので Actions
> Create Method
> GET
を選択しチェックマークをクリック
8. Integration Type(統合タイプ)
は Lambda Function Proxy(Lambda関数プロキシ)
を選択、 Lambda Region(Lambda リージョン)
と Lambda Function(Lambda関数)
で自分が作成した Lambda ファンクションを選んで Save(保存)
ボタンをクリック
9. Lambda 関数に権限を追加する
ダイアログが表示されたら OK
的なボタンをクリック
10. Integration Request(統合リクエスト)
を開き、 Use Lambda Proxy Integration(Lambdaプロキシ統合の使用)
のチェックを外す(ホントはLambda Proxy Integrationにして、レスポンスヘッダなどもLambda関数内で生成したほうが手堅い。。)
11. Body Mapping Templates(本文マッピングテンプレート)
を開き、 When there are no templates defined (recommended)(テンプレートが定義されていない場合 (推奨))
を選択、 Add mapping template(マッピングテンプレートを追加)
をクリック、 Content-Type
に image/png
と入力してチェックマークをクリック
12. ↓の記述 を参考にマッピングテンプレート内にJSONを記入、保存する
13. Actions(アクション)
> Deploy API(API のデプロイ)
を選択
14. Deployment Stage(デプロイされるステージ)
に demo
と入力し Deploy(デプロイ)
ボタンクリック
15. 表示される Invoke URL(URLの呼び出し)
をメモ
バイナリサポートの設定
image/*
を設定
詳しい仕様はAPI Gateway公式ドキュメントを参照。
プロキシリソースとGETメソッドの作成
今回はパスパラメータとして自由にファイル名を受け取りたいので、/{filename+}
としてAPIにリソースを作成する。
また、最終的には s3-ap-northeast-1.amazonaws.com
なドメインからAjaxで叩くので、CORSも有効にする。
プロキシリソースとして作成すると ANY
メソッドが勝手にできるが、今回は GET
だけできればいいので ANY
は消して GET
を作ることにする。
GET
はさっき作ったLambdaファンクションにつなげておく。
Lambdaに渡す引数のマッピングテンプレートを作成
「統合リクエスト > 本文マッピングテンプレート」からこんな感じで設定する。
テンプレート本文はこう。
{
"w": "$input.params('w')",
"filename": "$input.params('filename')"
}
今回、無駄にpng, jpg, gifに対応したのでそれぞれ設定。
この辺はマネジメントコンソールだけでやると冗長な感じですが、 Swagger とか使ってうまいこと重複排除するといいと思います。
APIのテスト(デプロイ前)
GET
> メソッドリクエストの設定で、「URL クエリ文字列パラメータ」に w
、「HTTP リクエストヘッダー」に Content-Type
Accept
を設定すると、API Gatewayのテスト機能で API Gateway ↔ Lambda の結合テスト的なことができる。
(が、設定していないので割愛)
APIのデプロイ
今回は demo
ステージにデプロイしました。
https://kmdc2eae46.execute-api.ap-northeast-1.amazonaws.com/demo
というInvoke URL=エンドポイントが作られます。
APIのテスト(デプロイ後)
curl -v --request GET -H "Accept: image/*" -H "Content-Type: image/png" 'https://kmdc2eae46.execute-api.ap-northeast-1.amazonaws.com/demo/sample.png?w=150' > sample.png
これで指定したサイズのsample.pngがローカルに落ちてくればOK.
ここまでの問題点と解決のアプローチ
これで API Gateway ↔ Lambda ↔ S3 な画像リサイズAPIができた。
しかし、このままHTML中に
<img src="https://kmdc2eae46.execute-api.ap-northeast-1.amazonaws.com/demo/sample.png?w=150">
とか書いても画像は表示されない。
なぜなら imgタグからのHTTPリクエストには Accept
ヘッダと Content-Type
ヘッダが含まれず、API Gatewayのバイナリサポート機能が使えないから。
そこで今回はCloudFrontを使って強引に(?)解決してみる。
CloudFront
問題点は Accept
ヘッダと Content-Type
ヘッダが API Gateway アクセス時のHTTPリクエストに含まれていないこと。
CloudFrontには Custom Origin Header
という機能があるので、今回はそれを使ってAPI GatewayへのHTTPリクエストに欲しいヘッダを追加してみる。
Step-by-step Instruction
1. CloudFrontのマネジメントコンソールから、 Create Distribution
をクリック
2. Web
の方の Get Started
をクリック
3. Origin Domain Name
に API Gateway のデプロイ後にメモした Invoke URL
のドメイン(xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
みたいなやつ)を入力
4. Origin Path
に /demo
と入力
5. Origin Protocol Policy
を HTTPS Only
に
6. Origin Custom Headers
に Header Name Accept
: Value image/jpeg,image/png,image/gif
, Header Name Content-Type
: Value image/jpeg,image/png,image/gif
の2行を入力
7. Object Caching
を Customize
にして、 Default TTL
を 0
にして キャッシュを無効化(テストしやすくするため)
8. Query String Forwarding and Caching
を Forward all, cache based on all
に( ?w=50
のクエリストリングをOriginへの転送対象に含める)
9. Create Distribution
ボタンをクリック
10. 待つ
CloudFrontディストリビューションとOriginの作成
こんな感じの設定で作りました。
Distribution
Origin
- Origin Domain Name に API Gateway でデプロイ済みAPIのドメイン名
- Origin Path に
/
+ API Gateway でデプロイしたステージ名 - Origin SSL Protocol は HTTPS Only
-
Origin Custom Headersに以下二点をエントリ
-
Accept
=image/png,image/jpeg,image/gif
-
Content-Type
=image/png,image/jpeg,image/gif
-
動作確認
これで、CloudFront を通して API Gateway にアクセスすれば Accept
ヘッダと Content-Type
ヘッダが付加された状態でHTTPリクエストが送られ、API Gateway がバイナリを扱ってくれるはず。
CloudFrontの作成・設定後は State が In Progress から Enabled になるまでしばらくかかるので待つ。
Enabled になったら、ブラウザで
https://**************.cloudfront.net/sample.png?w=150
にアクセスしてみて画像が落ちてくることを確認する。
(ドメイン名部分はさっき作ったディストリビューション情報で確認)
画像が落ちてくるんじゃなくてHTML上で確認したい場合は
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<img src="https://**************.cloudfront.net/sample.png?w=150">
</body>
</html>
とか書いてローカルで開いてみて画像が表示されるか見てみる。
表示されれば成功、便利API完成。
注意点 & 補足
パラメータのValidationについて
Lambdaのサンプルのところで少し書きましたが、実運用時に誰でも好きなサイズの画像を生成し放題では困る、という場合はパラメータのValidationを考慮するのがよいです。
API Gatewayのペイロードサイズ制限
API Gatewayがリクエストあたりで処理できるペイロードサイズは最大10 MB なので、それを超える大きな画像はこのAPIでは扱えません。
(ただ、多くのWebサイトではそうそう10MBの画像をほいほい表示したりしてないと思います)
CloudFront カスタムオリジンヘッダの使い方
カスタムオリジンヘッダには様々な使い道が考えられますが、典型的にはオリジンへのアクセスを制限することなどがあります。
利用前には公式ドキュメントをご参照ください。
API Gatewayの前にCloudFrontを置くことについて
今回、CloudFrontの提供する機能を使いたいために、API Gatewayの前段にCloudFrontを通しています。
しかし、当然ながらCloudFrontを置くことによってPOPが増えるため、レイテンシの増加を考慮の上で利用する必要があります。
AWS Black Belt Online Seminar 2016 Amazon API Gateway
CloudFrontのOriginとBehaviour
今回はCloudFrontを 1 Distribution - 1 Origin で設定していますが、実運用時はもう少し複雑な設定をしたくなることが多いと思います。
例えば /img
/css
/js
以下は S3 に向けて、それらへのリクエストにはカスタムオリジンヘッダーは付けず、 /api
または api.example.com
だけを API Gateway に向ける、など。
そういった要件を満たしたい場合、CloudFrontディストリビューションをマルチオリジンで設定し、ビヘイビアで振り分け先と条件を指定することで実現できます。
Amazon CloudFront » 開発者ガイド » ウェブディストリビューションの使用 » キャッシュ動作の設定
AWS Black Belt Tech シリーズ 2016 - Amazon CloudFront
デバッグ
ログ
Lambda、API Gateway、CloudFrontはそれぞれログを出すことができます。(API Gateway、CloudFrontは要設定)
Amazon CloudFront » 開発者ガイド » アクセスログ
特に、開発作業中はLambdaとAPI Gatewayのログが必要になることが多いかと思います。
その二つのログはCloudWatch Logsで参照できます。
AWS X-Ray
re:Invent 2016で発表されたAWS X-Rayも、デバッグには有用です。2016年12月15日現在、まだプレビュー中で、誰でもサインアップすることができます。
Amazon Web Services ブログ AWS X-Ray – 分散アプリケーションの内部を見る
免責
この記事は個人の見解であり、所属する企業や団体とは関係ございません。