1. Qiita
  2. 投稿
  3. CloudFront

API Gatewayでサーバレスな画像リサイズAPIを作る

  • 83
    いいね
  • 0
    コメント

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ヘッダを付加する ために使う
    • もちろんキャッシュしてもいい

アーキテクチャ

アーキテクチャ

作り方

Amazon S3

特に複雑なことはせず、読み込み可能な元画像を置いておく。今回は
https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/sample.png
を用意。

AWS Lambda

パラメータ wfilename を受け取り、 S3の元画像を取得し、imagemagickで変換して、Base64にして返す。
LambdaのBlueprint s3-get-object を元にして作った。

コード

このサンプルは最低限の処理になっていますが、
誰でもどんなサイズでも好き勝手に画像生成されてはたまらん、という方は
パラメータの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

バイナリサポートの設定

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リクエストに欲しいヘッダを追加してみる。

CloudFrontディストリビューションとOriginの作成

こんな感じの設定で作りました。

Distribution

Kobito.KK1PQp.png

Origin

Kobito.apySkX.png

  • 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 カスタムオリジンヘッダの使い方

カスタムオリジンヘッダには様々な使い道が考えられますが、典型的にはオリジンへのアクセスを制限することなどがあります。
利用前には公式ドキュメントをご参照ください。

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

免責

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