こんにちは、岩橋聡吾です。
やってみよう!AWSでWEBサーバー環境構築、久しぶりの続編です。
第4回は「AWS Lambda」「Amazon API Gateway」を使ったサーバーレスな画像リサイズAPIをクラウド上に構築して見たいと思います。
アーキテクチャ・設計概要
今回は以下のようなアーキテクチャで、Clientが指定した任意のサイズの画像を返却するAPIを構築していきます。
◉S3:
クラウドストレージ。
[設計概要] オリジナル画像の置き場を作成。
◉Lambda:
最低限のプログラムのみでアプリケーションの運用が可能なサーバーレスプラットフォーム。他のAWSサービスと連携が可能。
[設計概要] S3から画像データを受け取り、リサイズを実施、それをBase64形式にして返却されるよう作成。
◉API Gateway:
APIの玄関。手軽に柔軟で拡張的なAPIベースの構築が可能。
[設計概要] Lambdaから受け取ったBase64形式の画像データをバイナリに変換して返却されるよう作成。
◉CloudFront:
初回アクセス時に返却したコンテンツをキャッシュ。次回以降は有効期限内であればキャッシュよりコンテンツを返却。
[設計概要] 受けたリクエストにヘッダーを設定してバックエンドに渡すようなリクエスト窓口を作成。
この4つのサービスを連携させて、
https://xxx.xxx.xxx/{ファイル名}?width={横幅}
のような形で呼び出しができるAPIを構築していきます。
S3
S3(Amazon Simple Storage Service)はインターネット用のクラウドストレージです。よくご存知の方も多いと思いますので細かな説明は割愛して、早速設定していきたいと思います。
今回は、リサイズ元のオリジナル画像の置き場を作ります。
バケット作成
コンソールからS3のページに遷移をして、「+パケットを作成する」ボタンをクリック、モーダルが表示されます。
を設定して「作成」ボタンをクリックします。
オリジナル画像セット
検証用に先ほど作ったバケットに直接画像をアップします(neko.jpg)。
因みに今回はこのねこちゃんの画像をアップしました。
Lambda
AWS Lambda はサーバーをプロビジョニングしたり管理しなくてもコードを実行できるコンピューティングサービスで、最低限のプログラムのみでアプリケーションの運用が可能です。他のAWSサービスのイベントを受け取り、ここで設定した関数をキックさせたりできるので、これまで以上にAWS上でバリエーション豊かなアーキテクチャーを実現することができます。
今回は、リクエストから「画像名」と「指定サイズ」受け取り、S3より「オリジナル画像」を取得し「指定サイズ」に加工し返却するような関数を設定します。
関数の外枠作成
コンソールからLambdaのページに遷移をして、「関数の作成」ボタンをクリックします。
・名前
・ランタイム:C#, Go, Java, Node.js, Python から選択
・ロール:「テンプレートから新しいロールを作成」を選択
・ロール名
・ポリシーテンプレート:「S3 オブジェクトの読み取り専用アクセス権限」を追加
を設定して「関数の作成」ボタンをクリックします。
これでLambdaとS3の連携が設定(LambdaとCloudWatchLogsとの連携はデフォルトで設定されます)され、関数の外枠ができました。
関数の中身作成
【関数コード】
・コード エントリ タイプ:「コードをインラインで編集」を選択
・ランタイム(今回はNode.js 6.10としました)
・ハンドラ(デフォルト)
・index.js(今回は以下の内容で作成)
'use strict'; const imgmgck = require('imagemagick'); const aws = require('aws-sdk'); const s3 = new aws.S3({ apiVersion: '2006-03-01' }); exports.handler = (event, context, callback) => { // S3にあるオリジナル画像情報 const s3OrgImageFile = { Bucket: 'resize-s3-images', // S3で作ったバケット Key: event.filename, // リクエストで指定された画像名 }; // リクエストで指定された画像横幅 const width = event.width; // S3にあるオリジナル画像取得 s3.getObject(s3OrgImageFile, (err, data) => { if (err) { // 取得失敗 console.log('取得失敗:' + err); throw err; } else { // 取得成功 console.log('取得成功'); // リサイズ実施 imgmgck.resize({ srcData: data.Body, width: width }, function(err, stdout, stderr) { if (err) { // リサイズ失敗 console.log('リサイズ失敗:' + err); throw err; } else { // リサイズ成功 console.log('リサイズ成功'); callback(null, new Buffer(stdout, 'binary').toString('base64')); } }); } }); };
【基本設定】
・メモリ(MB)(今回は256MBとしました)※大きさに応じて課金額が変わります
・タイムアウト(デフォルト)
右上の「保存」ボタンをクリックします。
テスト
Lambdaでは作成した関数をクラウド上でテストすることが可能です。
先ずはテストを設定していきます。
右上の「テスト」ボタンをクリック、モーダルが表示されます。
・イベントテンプレート:「API Gateway Authorizer」を選択
・イベント名(今回はFromApiGatewayとしました)
(イベント内容は以下)
{ "authorizationToken": "incoming-client-token", "methodArn": "arn:aws:execute-api:[region]:[account_id]:[restApiId]/[stage]/[method]/[resourcePath]", "type": "TOKEN", "filename": "neko.jpg", "width": "300" }
「作成」ボタンをクリックします。
これでテストの設定ができました。右上のプルダウンより「FromApiGateway」を選択、「テスト」ボタンをクリックしテストを実施します。
上手くいったようです、無事Base64形式の画像データが返却されました。
API Gateway
Amazon API Gateway はスケーラブルなAPIの玄関を作るのに優れています。また簡単に、保守、監視、保護などが行えます。Lambdaとの相性も良く組み合わせることで、サーバーレスなAPIの構築が可能です。
ここでは、リクエストを受け付ける為のエンドポイントとその受け渡し先(先ほど作ったLambda関数)を設定していきます。
APIの外枠作成
コンソールからAPI Gatewayページに遷移をして、「+APIの作成」ボタンをクリックします。
・API名(今回はresize-s3-imagesとしました)
・エンドポイントタイプ:「エッジ最適化」を選択
を設定して「APIの作成」ボタンをクリックします。これでAPIの大枠ができました。
次にリソース(APIのエンドポイント)を作っていきます。
リソースの「/」上で、「アクション」をクリック、「リソースの作成」を選択します。
・プロキシーリソースとして設定:チェックなし
・リソース名
・リソースパス:波括弧でパラメータ定義(リクエスト受け手のLambdaに渡すパラメータ、 今回は{filename}としました)
を設定して「リソースの作成」ボタンをクリックします。これでリソース(APIのエンドポイント)ができました。
API諸設定
【メソッド】
メソッドを作成します。
直前で作ったリソース「/{filename}」上で、「アクション」をクリックし「メソッドの作成」を選択、さらに表示されたプルダウンより「GET」を選択します。
・結合タイプ:「Lambda 関数」を選択
・Lambda プロキシ統合の使用:チェックなし
・Lambda リージョン:前述のLambdaで設定したものと同リージョンを指定
・Lambda 関数:前述のLambdaで設定したものを指定
・デフォルトタイムアウトの使用:チェック
を設定して「保存」ボタンをクリックします。
【統合リクエスト】
以下のような形でAPIの呼び出しを想定している為、{ファイル名}・{横幅}に当たる部分のパラメータをLambdaに渡すように設定する必要があります。
https://xxx.xxx.xxx/{ファイル名}?width={横幅}
メソッドの実行画面で「統合リクエスト」をクリック、本文マッピングテンプレートを開きます。
・リクエスト本文のパススルー:「テンプレートが定義されていない場合 (推奨)」を選択
(Content-Typeは image/png,image/jpeg,image/gif としてテンプレート内容は以下)
{ "filename": "$input.params('filename')", "width": "$input.params('width')" }
を設定して「保存」ボタンをクリックします。
【統合レスポンス】
LambdaからはBase64形式で画像データが返却されるので、ここでバイナリデータに変換する必要があります。
メソッドの実行画面で「統合レスポンス」をクリックします。
を設定して「保存」ボタンをクリックします。
デプロイとローカル確認
【デプロイ】
デプロイをして公開します。
「アクション」をクリックし「APIのデプロイ」を選択、モーダルが表示されます。
・デプロイされるステージ:「新しいステージ」を選択
・ステージ名(今回はproductionとしました)
を設定して「デプロイ」ボタンをクリックします。
【ローカル確認】
ショルダーメニューから「ステージ」を選択、「production」をクリックします。「URLの呼び出し」を取得できます。
ローカルのターミナル上でcurlコマンド使ってリサイズ画像取得の確認できます。
$ curl --request GET "Accept: image/jpeg" -H "Content-Type: image/jpeg" https://zwhfujd9te.execute-api.us-west-2.amazonaws.com/production/neko.jpg?width=300 > neko_resized.jpg
以下のようなリサイズ画像が取得できていれば成功です。
CloudFront
CloudFrontはAWSクラウド上のCDN(コンテンツデリバリーネットワーク)です。こちらは第3回で紹介させて頂いたので細かな説明は割愛します。
何故CloudFrontが必要か
さて、何故今回のアーキテクチャーにCloudFrontが必要になるのでしょうか。
https://zwhfujd9te.execute-api.us-west-2.amazonaws.com/production/neko.jpg?width=300
上はここまでに作成してきたAPIのURLです。これをimgタグにセットすれば(もしくは直接ブラウザで叩けば)使える!と思われるかもしれませんが、上手くいきません。この場合以下のようなレスポンスが返されます。
imgタグにセットするだけでは、リクエストヘッダーに「Content-Type:image/xxx」「Accept:image/xxx」がセットされない為、API Gateway側の「統合リクエスト」の「本文マッピングテンプレート」でサポートしていないヘッダーとなり「Unsupported Media Type」が返却されます。
では、API Gateway側の「統合リクエスト」の「本文マッピングテンプレート」を調整すればいいのでは?と考えました。が…確かにこの場合、そのパケットはLambdaにたどり着けるのですが、今度は「統合レスポンス」でバイナリ変換がされなくなります。これは「統合レスポンス」がバイナリ変換するかどうかを、リクエストヘッダー「Content-Type:image/xxx」「Accept:image/xxx」を見て決めている為です。
そこでCloudFrontの登場です。CloudFrontからOrigin(今回でいうと「zwhfujd9te.execute-api.us-west-2.amazonaws.com」)へパケットを転送する際に、カスタムヘッダーを設定できる機能があります。これを使ってリクエストに強制的に「Content-Type:image/xxx」「Accept:image/xxx」のヘッダーをつけてあげると上手くいきそうです。やってみましょう。
CloudFrontにカスタムヘッダーをつけて設定
コンソールからCloudFrontのページに遷移をして、「Create Distribution」ボタンをクリックします。
Web(上)の方の「Get Started」ボタンをクリックします。
【Origin Settings】
・Origin Domain Name:「zwhfujd9te.execute-api.us-west-2.amazonaws.com」を選択
・Origin ID(今回はresize-imagesとしました)
・Origin Protocol Policy:「HTTPS Only」を選択
・Origin Custom Headers:Accept・Content-Type共にimage/png,image/jpeg,image/gifを設定 ※これが一番やりたかったこと
【Default Cache Behavior Settings】
・Query String Forwarding and Caching:「Forward all, cache based on all」を選択 ※これを設定しないとGETパラメータをOriginへ渡せません
を設定して「Create Distribution」ボタンをクリックします。
これでCloudFrontの設定が完了しました。今作ったDistributionを見てみましょう。
上の画面の「General」タブで、配布された「Domain Name」が確認できます。これを使ってブラウザで以下のようなURLを叩いて画像が表示されるかを確認します。
https://d298y89zsryi7u.cloudfront.net/production/neko.jpg?width=300
上手くいきました(設定したDistributionが反映されるまで数分かかる場合があります)。URLのwidthを任意の値に変えても取得することが確認できました。CloudFrontのキャッシュ機能の説明については割愛していますが、Lambdaでの画像リサイズは決して軽い処理ではないのでCloudFrontで上手くキャッシュができるようにチューニングすることをお薦めします。
これで、サーバーレスな画像リサイズAPIの完成です。
第4回目の最後に
今回のやってみようAWSシリーズでは、「Lambda」を使ったサーバーレスな画像リサイズAPIをクラウド上に構築手順をざっとご紹介しましたが、AWSではまだまだ多くのインフラサービスが提供されており、そのうちのどれもが実際のWEBサービス運営に役立つ強力な武器となります。今後もこのようなシリーズ記事を通じて皆さんに有益な情報を提供できればと思います。
またこのシリーズでこれまでインフラと縁遠かった方でもAWSに対する理解が少しでも深まっていれば幸いです。
ご清覧頂きありがとうございました。
Wedding Parkでは一緒に技術のウエディングパークを創っていくエンジニアを募集しています。
興味のある方はぜひ一度気軽にオフィスに遊びにきてください。
◉シリーズ
・やってみよう!AWSでWEBサーバー環境構築(シリーズ第1回)
・やってみよう!AWSでWEBサーバー環境構築(シリーズ第2回)
・やってみよう!AWSでWEBサーバー環境構築(シリーズ第3回)
◉筆者のおすすめ記事
・【DB設計入門|ER図|MySQL】コンビニレシートから学ぶ!データモデリング手法
・【機械学習入門|Python|scikit-learn】結局何ができる?cheat-sheetから解説してみる篇