アドベントカレンダーですので、箸休め記事を張り切って書いていきたいと思います! @mochizukikotaro です。
日常の風景
弊社ではときどき社内で、すごい小さなプログラミング勉強会が実施されます。クイズを2、3個出すような短いものなので、『つぶやき勉強会』と呼んでいます。先輩エンジニアにひねくれた問題をだして、間違えを誘いだしヒンシュクを買ったりすることができます。
だいたい下の図のようなイメージです。
これは、これで楽しいのですが。毎回、
司会 : 「これは何だとおもいますか?では、ラーメンマンさん!」
: 「んー false」
司会 : 「なるほどー。では、ミスターカーメンさん!」
: 「ん、ん、.. true」
司会 : 「ははーん。では..(以下同文」
という会話を繰り返すのに、すこしだけ飽きてきてしまいました。
ふと、
クイズ番組みたいにやりたい。
と思いました。テレビでやってるクイズ番組的な...解答者は手元のパネルに記載して、司会者の合図で「それでは一斉に解答オープン!」的なやつ。
今回は、それらしきものを S3 + Lambda + API Gateway で 簡易的でクローズド使用なWebアプリケーションとして作ってみようとしたお話になります。
できあがりイメージ
下の gif は解答者側の画面になります。流れは以下のようなものです。
- 問題を読む
- 解答と名前を入力して
- 送信
- しばらく待ってから結果ボタンを押すと
- (この待ち時間中に、admin画面側で出題者が ○ × を付けています)
- 全員の解答結果が ○ × つきでオープン
つまり、ひとことで言うと、
はじめに夢見たクイズ番組とは、まあ、違います。
クイズ番組のことは忘れてください。
これは、なんとなくクイズに答えて、みんなの解答がまとめて見れる、そんなWebアプリケーションです。
(WebSocket とかを考えたのですが、Lambda を触ってみたいという気持ちが主なモチベーションだったので細かいことは無視しました。)
ここからレシピ紹介
まず、アーキテクチャー図は以下のようなイメージです。
S3 に html とか js ファイルを置いて、静的ウェブホスティングによりユーザーからアクセス可能にします。そして、ユーザーの解答や名前情報を保有するのは DB をつかわずに、JSON ファイルでまかないました。その json ファイルも S3 に置いておきます。
ユーザーのアクションで js のイベントを発火させ ajax で API Gateway を叩き Lambda function を呼び出して、それが json ファイルを読んだり、アップデートしたりして情報を更新していきます。
ここから大枠の流れとソースコードをステップバイステップで書きたいと思います。ぼくがハマったポイントを添えて。(ただ、ちょっと複雑でうまくお伝えできるか...)
ステップ
- まずは S3 に html を配置してアクセスできるようにする。
- Lambda に Function を用意する
- API Gateway にリソースを作って Lambda と連結する
対象読者
- Lambda 使ったことない
- S3 でWebホスティングしたことない
- API Gateway という名前をはじめて聞いた
- Node.js を普段書かないけど、JavaScrip は書いている
- CORS なにそれ?
- つまり私
Step1: まずは S3 に html を配置してアクセスできるようにする。
- 適当な名前でバケットを作成(便宜的に examplebucket とします)
- index.html と error.html を適当に用意してアップロード
- プロパティ -> 静的ウェブホスティング -> ホスティングを有効にする
- このタイミングでエンドポイントにアクセスすると403になります。
- アクセス許可 -> バケットポリシーを設定する必要がります。
- 匿名ユーザーへの読み取り専用アクセス許可の付与 を参考に以下を追加
{
"Version":"2012-10-17",
"Statement":[{
"Sid":"PublicReadGetObject",
"Effect":"Allow",
"Principal": "*",
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::examplebucket/*"
]
}
]
}
- これでエンドポイントにアクセスできるようになりました。
ここまでは、問題ないと思います。
Step2: Lambda に Function を用意する
作り方はとても簡単。Lambda のページに行き。
「Create a Lambda function」 -> 「Blank Function」を選んで -> 「Next」ボタン。とすれば、設定画面までたどり着きます。
ここで、適当にファンクション名をつけたらやることは3つです。
- IAMユーザーをつくって key を用意する
- IAMロールつくっておく
- コード書く
IAMユーザーをつくっておく
まず、Lambda の編集画面はそのままにしておいて、IAM に行きます。「ユーザーを追加」ボタンをクリックして、適当にユーザー名を入れて、アクセスの種類で「プログラムによるアクセス」をチョイスして「次のステップ」に進みます。そして、「既存のポリシーを直接アタッチ」で「AmazonS3FullAccess」をアタッチします。
あとは、道なりに進めば
- アクセスキーID
- シークレットアクセスキー
を取得できます。大切に管理してください。
IAMロールをつくっておく
実行ロール (IAM ロール) を作成する この辺を参考にロールを作成してください。
Lambda 編集ページの続きにもどります
まず、コードはこんな感じです。node.js です。
'use strict';
var AWS = require('aws-sdk');
var AWS_ACCESS_KEY_ID = {取得したアクセスキーID};
var AWS_SECRET_ACCESS_KEY = {取得したシークレットアクセスキー};
var S3_BUCKET_NAME = 'examplebucket';
var FILE_NAME = 'example.json'; // 適当に json 書いて S3 にアップしておいてください
exports.handler = (event, context, callback) => {
AWS.config.update({
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
region: 'ap-northeast-1'
});
var s3 = new AWS.S3();
var getParams = {
Bucket: S3_BUCKET_NAME,
Key: FILE_NAME
};
s3.getObject(getParams, function(err, data){
if (err) {
console.log(err);
} else {
callback(null, data.Body.toString());
}
});
}
そして、Lambda 作成ページの下の方にある「Exsinting role」に上で作成したロールを入れる。
あとは道なりで Lambda function の作成は完了です。
Step3: API Gateway にリソースを作って Lambda と連結する
API Gateway のページにいき適当な名前で新規 API を作成してください。そうしましたら、アクションボタンから適当にメソッドを追加して、ポチポチと先ほどの Lambda function を選択します。
(上キャプチャはすぐにメソッド追加しちゃってますが、アクションボタンからリソースも簡単につくれます。これで任意のパスに GET とか POST とかできるようになります。)
保存してください。
残る手順は2つほどです。
- 統合リクエストのマッピングテンプレート設定
- CORSの有効化とAPIデプロイ
安心してください。後者はボタンポチポチです。(CORSとかの概念がわかっていない僕はとてもハマリましたが。)
統合リクエストのマッピングテンプレート設定
※ここの設定は、API Gateway に POST する場合、そのデータにアクセスするための対応です。なので、上の Lambda function の例とは直接絡みません。 下に別の例を記載します。
上キャプチャの通り「統合リクエスト」へ進みます。そして、下キャプチャのように、Content-Type や テンプレートを書きます。
{
"body" : $input.json('$')
}
上のように書くことで、API Gateway に対してポストしたデータを Lambda function 側で、event.body としてオブジェクトのように扱うことができます。(これを知るまでは、どこを彷徨っているのかも分からない状態でした。
Lambda function の別例
exports.handler = (event, context, callback) => {
// (略)
var obj = event.body; // こんな風に POST データにアクセスできる
var jsonStr = JSON.stringify(obj);
var putParams = {
Bucket: S3_BUCKET_NAME,
Key: FILE_NAME,
Body: jsonStr,
ContentType: 'application/json; charset=utf-8',
ACL: 'public-read'
};
s3.putObject(putParams, function(err, data){
if (err) {
callback(null, err);
} else {
callback(null, jsonStr);
}
});
}
CORS の有効化と API デプロイ
ポチ、ポチ、ポチで完了です。
S3 エンドポイントから API Gateway エンドポイントへのアクセスが走るので、CORS を設定しないと、XMLHttpRequest cannot load
となります。(最初ぼくは、JSONPをどうやったらつくれるんだろうとしばらく彷徨っていました。
あ、デプロイするときに、ステージを聞かれるので、適当に prod などのステージ名をつけてみてください。
これで下キャプチャのダッシュボードからエンドポイントを確認できます。このエンドポイントに向かって後で ajax 叩きます。
以上で、設定完了です。
ajax 部分: S3 に置いてある HTML で発火する JS を紹介します
全部はまだ公開できる状態ではないので、部分的ですが、雰囲気をお伝えするために。
「挑戦する!」ボタンを押したら、API Gateway に解答と名前をポストする部分を記載します。
var API_BASE_URL = {上で用意した API Gateway のエンドポイント};
var challengePromise = function(){
var nickname = $('#nickname').val();
var answer = $('#answer').val();
var defer = $.Deferred();
var str = JSON.stringify({ "nickname": nickname, "answer": answer });
$.ajax({
type: 'POST',
url: API_BASE_URL + '/challenge', // challenge というリソースを用意してた場合
contentType: 'application/json',
data: str,
success: defer.resolve,
error: defer.reject
});
return defer.promise();
}
$('#challenge').on('click',function(){
challengePromise().done(function(data){
$('#form_area').remove();
document.cookie = "quiz_room_id=" + JSON.parse(data).quiz_room_id;
appendWaitingArea();
});
challengePromise().fail(function(r,s,e){
console.log('失敗');
})
});
おわりに感想
最近、サーバーレスという言葉をよく聞くので、どんなものか知りたくて、Lambda + API Gateway を触ってみました。ネタとしてクイズ番組アプリを作成しはじめたのですが、慣れないので、重複コードは沢山できるし、アクセス権限をコントロールしきれてないし、Lambda のデプロイについてはまだ勉強してないので、いちいちコピペしているし、大変です 。CORS のこと分からないし、DynamoDB 使ったことないし(未だ使ってないけど)、途中で WebSocket したいと思って、AWS IoT に手を出そうとして調べ始めたら心がバキバキに折れるし、大変です 。でも、サーバーを立ち上げないで、こういったことができて、面白さに少し触れることができた気がしました 。
(結局アプリはととのったんだか、ととのっていないんだか... いまのところ1問だけ解答できるという貧弱仕様状態なので、引き続き手を加えて、育てて行きたいと思います。)
引き続き勉強していきたいと思います。
ありがとうございました!