195
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[2018/03/08追記]API Gatewayでサーバレスな画像リサイズAPIを作る

Last updated at Posted at 2016-12-18

<2018/03/08追記>
2018年3月、Lambda@Edgeを使った画像リサイズのソリューションがAWSの公式ブログにて紹介されました。


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に変換

幅50
https://d3rdcsp6kyxueg.cloudfront.net/sample.png?w=50

幅200に変換

幅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
を用意。

Step-by-step instruction 1. ↑の sample.png をダウンロード 2. s3のマネジメントコンソールでバケットを作成 `yourname-image-resizer` 的な名前で 3. ダウンロードした sample.png をアップロード 4. アップロードしたファイルを `make public` あるいは `公開する` 5. ![公開するボタン](https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/make_public.png) 6. ↑の `Link` をブラウザで開ける(=パブリックアクセスできる)ことを確認する

AWS Lambda

パラメータ wfilename を受け取り、 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などを入れることを考慮 するとよいです。

ImageResizer.js
'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:*"
            ]
        }
    ]
}

テスト

Kobito.aL6374.png

こんな感じで wfilename を渡すようにテストイベントを設定して Save and Testし、

Kobito.sJse6F.png

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. [↓の記述](http://qiita.com/akitsukada/items/e6d8fe68c49973d1edf6#lambda%E3%81%AB%E6%B8%A1%E3%81%99%E5%BC%95%E6%95%B0%E3%81%AE%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90) を参考にマッピングテンプレート内にJSONを記入、保存する 13. `Actions(アクション)` > `Deploy API(API のデプロイ)` を選択 14. `Deployment Stage(デプロイされるステージ)` に `demo` と入力し `Deploy(デプロイ)` ボタンクリック 15. 表示される `Invoke URL(URLの呼び出し)` をメモ

バイナリサポートの設定

image/* を設定

Kobito.dHLqPf.png

詳しい仕様はAPI Gateway公式ドキュメントを参照

プロキシリソースとGETメソッドの作成

今回はパスパラメータとして自由にファイル名を受け取りたいので、/{filename+} としてAPIにリソースを作成する。
また、最終的には s3-ap-northeast-1.amazonaws.com なドメインからAjaxで叩くので、CORSも有効にする。
Kobito.A1pSGo.png

プロキシリソースとして作成すると ANY メソッドが勝手にできるが、今回は GET だけできればいいので ANY は消して GET を作ることにする。

Kobito.eEeiz2.png

GET はさっき作ったLambdaファンクションにつなげておく。

Kobito.Efybdx.png

Lambdaに渡す引数のマッピングテンプレートを作成

「統合リクエスト > 本文マッピングテンプレート」からこんな感じで設定する。
Kobito.p6Sv53.png

テンプレート本文はこう。

{
 "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

Kobito.KK1PQp.png

Origin

Kobito.EOuJqN.png

  • 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が増えるため、レイテンシの増加を考慮の上で利用する必要があります。

Kobito.0htJIz.png
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は要設定)

AWS Lambda » Developer Guide » Lambda 関数 » 概要: Lambda 関数のビルド » プログラミングモデル » プログラミングモデル (Node.js) » ログ作成 (Node.js)

Amazon API Gateway » 開発者ガイド » Amazon API Gateway での API のデプロイ » Amazon API Gateway でステージに API をデプロイする » ステージの設定

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 – 分散アプリケーションの内部を見る

免責

この記事は個人の見解であり、所属する企業や団体とは関係ございません。

195
144
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
195
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?