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ヘッダを付加する ために使う
- もちろんキャッシュしてもいい
アーキテクチャ
作り方
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
を元にして作った。
コード
このサンプルは最低限の処理になっていますが、
誰でもどんなサイズでも好き勝手に画像生成されてはたまらん、という方は
パラメータの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
バイナリサポートの設定
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リクエストに欲しいヘッダを追加してみる。
CloudFrontディストリビューションとOriginの作成
こんな感じの設定で作りました。
Distribution
Origin
- Origin Domain Name に API Gateway でデプロイ済みAPIのドメイン名
- Origin Path に
/
+ API Gateway でデプロイしたステージ名 - Origin SSL Protocol は HTTPS Only
-
Origin Custom Headersに以下二点をエントリ
-
Accept
=image/*
-
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 カスタムオリジンヘッダの使い方
http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.htmlが、典型的にはhttp://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.html#forward-custom-headers-restrict-accessことなどがあります。
利用前には公式ドキュメントをご参照ください。
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 – 分散アプリケーションの内部を見る
免責
この記事は個人の見解であり、所属する企業や団体とは関係ございません。