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

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

# 日常の風景

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

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

![IMG_1990.JPG](https://qiita-image-store.s3.amazonaws.com/0/45822/77d3314e-2325-d026-48f2-120d0bbc6d74.jpeg)

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

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

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

ふと、

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

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

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


# できあがりイメージ

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

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


![qr_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/45822/84f463fe-e88c-3b91-af33-ee8b5c906da0.gif)


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

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

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

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


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


# ここからレシピ紹介

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

![Architecture.png](https://qiita-image-store.s3.amazonaws.com/0/45822/dd9bf59f-ed74-d4ad-af8e-d4f1a01073af.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](https://qiita-image-store.s3.amazonaws.com/0/45822/c5bd0fd8-0c0e-869d-dd12-6e4366a43a71.png)


- このタイミングでエンドポイントにアクセスすると403になります。
- アクセス許可 -> バケットポリシーを設定する必要がります。
- [匿名ユーザーへの読み取り専用アクセス許可の付与](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-2) を参考に以下を追加

```json:バケットポリシー
{
  "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](https://qiita-image-store.s3.amazonaws.com/0/45822/0c919c0a-e637-4457-c90e-7a191b191184.png)

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

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

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

まず、Lambda の編集画面はそのままにしておいて、IAM に行きます。「ユーザーを追加」ボタンをクリックして、適当にユーザー名を入れて、アクセスの種類で「プログラムによるアクセス」をチョイスして「次のステップ」に進みます。そして、「既存のポリシーを直接アタッチ」で「AmazonS3FullAccess」をアタッチします。![スクリーンショット 2016-12-23 15.28.46.png](https://qiita-image-store.s3.amazonaws.com/0/45822/0bd364b4-d493-f9f2-9d20-ba10c3482926.png)

あとは、道なりに進めば

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

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

### IAMロールをつくっておく

[実行ロール (IAM ロール) を作成する](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example-create-iam-role.html) この辺を参考にロールを作成してください。 :innocent:

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

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

```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](https://qiita-image-store.s3.amazonaws.com/0/45822/60160ad5-f480-cc16-9d1d-4763a99ad6e6.png)

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


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

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

![スクリーンショット 2016-12-23 16.28.10.png](https://qiita-image-store.s3.amazonaws.com/0/45822/8771e88b-bfab-abb4-8e30-81efcc425ab5.png)

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

保存してください。

残る手順は**２つほど**です。

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

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

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

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

![スクリーンショット 2016-12-23 16.41.08.png](https://qiita-image-store.s3.amazonaws.com/0/45822/d923965c-9df3-9d94-4c61-16eaace452a4.png)

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

![スクリーンショット 2016-12-23 16.43.46.png](https://qiita-image-store.s3.amazonaws.com/0/45822/2887035c-897c-1d41-c147-b98fccad7c76.png)


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

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

### Lambda function の別例

```js: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](https://qiita-image-store.s3.amazonaws.com/0/45822/d16a27b7-db7f-a050-7d70-0adec55397cf.png)


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

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

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

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

![スクリーンショット 2016-12-23 18.37.38.png](https://qiita-image-store.s3.amazonaws.com/0/45822/c35b1c28-7691-1a61-9f44-3248cb0b3197.png)


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

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

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

```js: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問だけ解答できるという貧弱仕様状態なので、引き続き手を加えて、育てて行きたいと思います。）

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

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