社内でクイズ番組的なノリを実現したくて、簡易的なWebアプリケーションを Lambda でととのえる。

  • 13
    Like
  • 0
    Comment

アドベントカレンダーですので、箸休め記事を張り切って書いていきたいと思います! @mochizukikotaro です。

日常の風景

弊社ではときどき社内で、すごい小さなプログラミング勉強会が実施されます。クイズを2、3個出すような短いものなので、『つぶやき勉強会』と呼んでいます。先輩エンジニアにひねくれた問題をだして、間違えを誘いだしヒンシュクを買ったりすることができます。

だいたい下の図のようなイメージです。

IMG_1990.JPG

これは、これで楽しいのですが。毎回、

司会 : 「これは何だとおもいますか?では、ラーメンマンさん!」
:man_with_gua_pi_mao_tone1: : 「んー false」
司会 : 「なるほどー。では、ミスターカーメンさん!」
:construction_worker: : 「ん、ん、.. true」
司会 : 「ははーん。では..(以下同文」

という会話を繰り返すのに、すこしだけ飽きてきてしまいました。

ふと、

クイズ番組みたいにやりたい。

と思いました。テレビでやってるクイズ番組的な...解答者は手元のパネルに記載して、司会者の合図で「それでは一斉に解答オープン!」的なやつ。

今回は、それらしきものを S3 + Lambda + API Gateway で 簡易的でクローズド使用なWebアプリケーションとして作ってみようとしたお話になります。

できあがりイメージ

下の gif は解答者側の画面になります。流れは以下のようなものです。

  • 問題を読む
  • 解答と名前を入力して
  • 送信
  • しばらく待ってから結果ボタンを押すと
  • (この待ち時間中に、admin画面側で出題者が ○ × を付けています)
  • 全員の解答結果が ○ × つきでオープン

qr_demo.gif

つまり、ひとことで言うと、

はじめに夢見たクイズ番組とは、まあ、違います。

クイズ番組のことは忘れてください。

これは、なんとなくクイズに答えて、みんなの解答がまとめて見れる、そんなWebアプリケーションです。

(WebSocket とかを考えたのですが、Lambda を触ってみたいという気持ちが主なモチベーションだったので細かいことは無視しました。)

ここからレシピ紹介

まず、アーキテクチャー図は以下のようなイメージです。

Architecture.png

S3 に html とか js ファイルを置いて、静的ウェブホスティングによりユーザーからアクセス可能にします。そして、ユーザーの解答や名前情報を保有するのは DB をつかわずに、JSON ファイルでまかないました。その json ファイルも S3 に置いておきます。

ユーザーのアクションで js のイベントを発火させ ajax で API Gateway を叩き Lambda function を呼び出して、それが json ファイルを読んだり、アップデートしたりして情報を更新していきます。

ここから大枠の流れとソースコードをステップバイステップで書きたいと思います。ぼくがハマったポイントを添えて。(ただ、ちょっと複雑でうまくお伝えできるか...)

ステップ

  1. まずは S3 に html を配置してアクセスできるようにする。
  2. Lambda に Function を用意する
  3. API Gateway にリソースを作って Lambda と連結する

対象読者

  • Lambda 使ったことない
  • S3 でWebホスティングしたことない
  • API Gateway という名前をはじめて聞いた
  • Node.js を普段書かないけど、JavaScrip は書いている
  • CORS なにそれ?
  • つまり私

Step1: まずは S3 に html を配置してアクセスできるようにする。

  • 適当な名前でバケットを作成(便宜的に examplebucket とします)
  • index.html と error.html を適当に用意してアップロード
  • プロパティ -> 静的ウェブホスティング -> ホスティングを有効にする

hosting.png

バケットポリシー
{
  "Version":"2012-10-17",
  "Statement":[{
    "Sid":"PublicReadGetObject",
        "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::examplebucket/*"
      ]
    }
  ]
}
  • これでエンドポイントにアクセスできるようになりました。

ここまでは、問題ないと思います。 :grinning:

Step2: Lambda に Function を用意する

作り方はとても簡単。Lambda のページに行き。
「Create a Lambda function」 -> 「Blank Function」を選んで -> 「Next」ボタン。とすれば、設定画面までたどり着きます。

スクリーンショット 2016-12-23 16.23.26.png

ここで、適当にファンクション名をつけたらやることは3つです。

  • IAMユーザーをつくって key を用意する
  • IAMロールつくっておく
  • コード書く

IAMユーザーをつくっておく

まず、Lambda の編集画面はそのままにしておいて、IAM に行きます。「ユーザーを追加」ボタンをクリックして、適当にユーザー名を入れて、アクセスの種類で「プログラムによるアクセス」をチョイスして「次のステップ」に進みます。そして、「既存のポリシーを直接アタッチ」で「AmazonS3FullAccess」をアタッチします。スクリーンショット 2016-12-23 15.28.46.png

あとは、道なりに進めば

  • アクセスキーID
  • シークレットアクセスキー

を取得できます。大切に管理してください。

IAMロールをつくっておく

実行ロール (IAM ロール) を作成する この辺を参考にロールを作成してください。 :innocent:

Lambda 編集ページの続きにもどります

まず、コードはこんな感じです。node.js です。

Lambda_function_S3のjsonファイルを読むサンプル
'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」に上で作成したロールを入れる。

スクリーンショット 2016-12-23 16.14.55.png

あとは道なりで Lambda function の作成は完了です。 :grinning:

Step3: API Gateway にリソースを作って Lambda と連結する

API Gateway のページにいき適当な名前で新規 API を作成してください。そうしましたら、アクションボタンから適当にメソッドを追加して、ポチポチと先ほどの Lambda function を選択します。

スクリーンショット 2016-12-23 16.28.10.png

(上キャプチャはすぐにメソッド追加しちゃってますが、アクションボタンからリソースも簡単につくれます。これで任意のパスに GET とか POST とかできるようになります。)

保存してください。

残る手順は2つほどです。

  • 統合リクエストのマッピングテンプレート設定
  • CORSの有効化とAPIデプロイ

安心してください。後者はボタンポチポチです。(CORSとかの概念がわかっていない僕はとてもハマリましたが。)

統合リクエストのマッピングテンプレート設定

※ここの設定は、API Gateway に POST する場合、そのデータにアクセスするための対応です。なので、上の Lambda function の例とは直接絡みません。 下に別の例を記載します。

スクリーンショット 2016-12-23 16.41.08.png

上キャプチャの通り「統合リクエスト」へ進みます。そして、下キャプチャのように、Content-Type や テンプレートを書きます。

スクリーンショット 2016-12-23 16.43.46.png

テンプレート
{
"body" : $input.json('$')
}

上のように書くことで、API Gateway に対してポストしたデータを Lambda function 側で、event.body としてオブジェクトのように扱うことができます。(これを知るまでは、どこを彷徨っているのかも分からない状態でした。 :astonished:

Lambda function の別例

Lambda_function_S3にプットする例
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 デプロイ

スクリーンショット 2016-12-23 17.01.28.png

ポチ、ポチ、ポチで完了です。

S3 エンドポイントから API Gateway エンドポイントへのアクセスが走るので、CORS を設定しないと、XMLHttpRequest cannot load となります。(最初ぼくは、JSONPをどうやったらつくれるんだろうとしばらく彷徨っていました。 :astonished:

あ、デプロイするときに、ステージを聞かれるので、適当に prod などのステージ名をつけてみてください。

これで下キャプチャのダッシュボードからエンドポイントを確認できます。このエンドポイントに向かって後で ajax 叩きます。

スクリーンショット 2016-12-23 18.37.38.png

以上で、設定完了です。 :grinning:

ajax 部分: S3 に置いてある HTML で発火する JS を紹介します

全部はまだ公開できる状態ではないので、部分的ですが、雰囲気をお伝えするために。
「挑戦する!」ボタンを押したら、API Gateway に解答と名前をポストする部分を記載します。

index.js_部分的な紹介
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 のデプロイについてはまだ勉強してないので、いちいちコピペしているし、大変です :astonished:。CORS のこと分からないし、DynamoDB 使ったことないし(未だ使ってないけど)、途中で WebSocket したいと思って、AWS IoT に手を出そうとして調べ始めたら心がバキバキに折れるし、大変です :astonished:でも、サーバーを立ち上げないで、こういったことができて、面白さに少し触れることができた気がしました :slight_smile:

(結局アプリはととのったんだか、ととのっていないんだか... いまのところ1問だけ解答できるという貧弱仕様状態なので、引き続き手を加えて、育てて行きたいと思います。)

引き続き勉強していきたいと思います。

ありがとうございました!