Serverless FrameworkとS3で超簡単な投票システムを作った話

# はじめに

社内でいつも通り仕事をしていると、約1週間で簡単な投票アプリを作ってくれないか？というオファーを受け、構成を考えているときに「Serverless Frameworkを使えばいいじゃん」と神のお告げが降ってきましたので、触ってみることにしました。

## 最終目標

スマホから投票できて、最終的にCSVでまとまった投票データを吐き出す

## 構成はこんな感じ

青枠の部分がServerlessFramework

![serverless-img.PNG](https://qiita-image-store.s3.amazonaws.com/0/95875/43d4405a-ae33-26fc-2a85-626c3f2c9c2f.png)



## 大まかな流れ

1. S3に投票フォームをアップロードしてバケットごとWeb公開する
2. フォームの投票内容をAPIGateWayに向かってPOSTする
3. APIGateWayがLambdaをキックする
4. Lambdaが受け取ったJSONをDynamoDBに書き込む
5. 完了！


## さあ、はじめてみようか

書いていく前に･･･今回ServerlessFrameworkを触るにあたって@hiroshik1985様の[とことんサーバーレス①：Serverless Framework入門編](http://qiita.com/hiroshik1985/items/6d979ff1afb56953b62e)を参考にさせていただきました。

### ServerlessFrameworkのインストール

npmで入れちゃいます

```
$ npm install -g serverless
```

### AWS Credentialsを設定する

ServerlessFramework用のIAMを作成して、AWSConfigureに登録します

```
$ aws configure
AWS Access Key ID [None]: 先ほど作成したIAMのアクセスキー
AWS Secret Access Key [None]: 先ほど作成したIAMのシークレットアクセスキー
Default region name [None]: ap-northeast-1
Default output format [None]: ENTER
```

### プロジェクト作成

serverless用のディレクトリを作成し、そのディレクトリの中で下記のコマンド実行
今回はnode.jsで
※serverlessコマンドはインストール時に用意されるslsエイリアスを使うと便利です

```
$ sls create --template aws-nodejs --name vote
```

```
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.8.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"
```

こんなのが表示されたら成功。なんかかっこいい･･･
これでディレクトリ内にhandler.jsやserverless.ymlが作成される

### はじめてのデプロイ

デプロイのコマンドはこちら

```
$ sls deploy -v
```

特に怒られれなければデプロイ成功です。AWSのコンソール画面ですでにLambda等が立ち上がっていると思います

### Lambdaファンクションを書くよ

Lambdaファンクションは基本的にhandler.jsに書きます。
たとえばこんな風に

```handler.js
import AWS from 'aws-sdk'

AWS.config.update({region: 'ap-northeast-1'})

const db = new AWS.DynamoDB.DocumentClient()

export const register = (event, context, callback) => {
    const body = JSON.parse(event.body)
    const params = {
        TableName: "names",
        Key: {
            id: body.employeeNumber
        },
        UpdateExpression: "set #Group1 = :vote1, #Group2 = :vote2, #Group3 = :vote3",
        ExpressionAttributeNames: {
            "#Group1": "voteGroup1",
            "#Group2": "voteGroup2",
            "#Group3": "voteGroup3"
        },
        ExpressionAttributeValues: {
            ":vote1": body.voteGroup1,
            ":vote2": body.voteGroup2,
            ":vote3": body.voteGroup3
        },
        ReturnValues: "UPDATED_NEW"
    }



    try {
        db.update(params, (error, data) => {
            if (error) {
                callback(null, {
                    statusCode: 400,
                    headers:{ "Access-Control-Allow-Origin" : "*" },
                    body: JSON.stringify({message: 'Failed.', error: error, params: params})
                })
            }
            callback(null, {
                statusCode: 200,
                headers:{ "Access-Control-Allow-Origin" : "*" },
                body: JSON.stringify({message: 'Succeeded!', params: params})
            })
        })
    } catch (error) {
        callback(null, {
            statusCode: 400,
            headers:{ "Access-Control-Allow-Origin" : "*" },
            body: JSON.stringify({message: 'Failed.', error: error, params: params})
        })
    }
}
```

フォームからPOSTメソッドでJSON形式のデータをDynamoDBに格納します
送られてくるJSONデータは{"employeeNumber": "001", "voteGroup1": "1", "voteGroup2": "2", "voteGroup3": "3"}のような形で送られてくることを想定しています

### そのほか設定を書くよ

serverless.ymlにDynamoDBやLambdaなどの設定を書きます
serverlessFrameworkの設定ファイルみたいなものです
たとえばこんな風に

```serverless.yml

service: vote

provider:
  name: aws
  runtime: nodejs4.3
  stage: dev
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: "Allow"
      Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/*"
      Action:
        - "dynamodb:*"

plugins:
  - serverless-webpack
  
// LambdaFunction
functions:
  register:
    handler: handler.register
    events:
      - http:
         path: names
         method: post
         cors: true

// DynamoDB
resources:
  Resources:
    hello:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: names
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

これで再度デプロイするとLambdaファンクションが設定され、DynamoDBにテーブルが作成されます

### 試しに実行だ

デプロイ後に表示されるAPIGateWayのエンドポイントに対してcurlでリクエストを送信します
※「 https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names 」はAPIGateWayのエンドポイントです

```

curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"employeeNumber": "001", "voteGroup1": "1", "voteGroup2": "2", "voteGroup3": "3"}'  https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names
```

問題なくリクエストが送信されれば、DynamoDBにテータが格納されていると思います

### フォームの作成

あまり手の込んだフォームをコーディングしている時間がなかったので、シンプルにまとめました
CSSは「Material Design Lite」というCSSフレームワークを使用し、AjaxでAPIGatewayのエンドポイントに対してHTTPリクエストを投げています

```index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>投票アプリ</title>
    <script type="text/javascript" src="jquery-3.1.1.min.js"></script>
    <script type="text/javascript" src="vote.js"></script>
    <script type="text/javascript" src="mdl/material.min.js"></script>
    <link rel="stylesheet" href="mdl/material.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="common.css">
</head>
<body>

<!-- Always shows a header, even in smaller screens. -->
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
    <header class="mdl-layout__header">
        <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title">投票アプリ</span>
            <!-- Add spacer, to align navigation to the right -->
            <div class="mdl-layout-spacer"></div>
        </div>
    </header>
    <main class="mdl-layout__content">
        <div id="page-content" class="mdl-grid">
            <div class="mdl-cell mdl-cell--12-col">
                社員番号:<br>
                <select name="employee_number" class="employee-number">
                    <option value="">選択してください</option>
                    <option value="001">001</option>
                    <option value="002">002</option>
                    <option value="003">003</option>
                    <option value="004">004</option>
                    <option value="005">005</option>
                    <option value="006">006</option>
                </select>
                <span class="font-red">※必須</span>
            </div>
            <div class="mdl-cell mdl-cell--12-col">
                投票するグループを<span class="font-red">3つ</span>選択してください<span class="font-red">※必須</span>
                <br>
                1位:
                <select name="vote_group1" class="vote-group1">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
                <br>
                2位:
                <select name="vote_group2" class="vote-group2">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
                <br>
                3位:
                <select name="vote_group3" class="vote-group3">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
            </div>
            <div class="mdl-cell mdl-cell--12-col">
                <button id="vote-button"
                        class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">投票
                </button>
            </div>

            <div class="mdl-cell mdl-cell--12-col">
                <textarea id="response" disabled></textarea>
            </div>

        </div>
    </main>
</div>
</body>
</html>
```

```common.css

.font-red {
    color: red;
    font-weight: bold;
    font-size: 16px;
}
```

```vote.js

$(function () {
    $("#response").html("Response Values");

    /**
     * 投票ボタンを押したときの処理
     */
    $("#vote-button").click(function () {
        var url = 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names';

        var employeeNumber = $(".employee-number").val();
        if (employeeNumber === "") {
            alert("社員番号は必須です");
            return;
        }

        /**
         * 選択肢の登録数のバリデーション
         */
        var voteGroup = {
            group1: $(".vote-group1").val(),
            group2: $(".vote-group2").val(),
            group3: $(".vote-group3").val()
        };

        var count = 0;
        $.each(voteGroup, function (key, val) {
            if (val !== "") {
                count++;
            }
        });
        if (count < 3) {
            alert("グループを3つ選択してください");
            return
        }

        /**
         * 重複チェック
         */
        if(voteGroup.group1 === voteGroup.group2){
            alert("同じクループは選択できません");
            return
        }
        if(voteGroup.group1 === voteGroup.group3){
            alert("同じクループは選択できません");
            return
        }
        if(voteGroup.group2 === voteGroup.group3){
            alert("同じクループは選択できません");
            return
        }

        var JsonData = {
            employeeNumber: employeeNumber,
            voteGroup1: voteGroup.group1,
            voteGroup2: voteGroup.group2,
            voteGroup3: voteGroup.group3
        };

        alert(JSON.stringify(JsonData));

        $.ajax({
            type: 'post',
            url: url,
            data: JSON.stringify(JsonData),
            contentType: 'application/JSON',
            dataType: 'JSON',
            scriptCharset: 'utf-8',
            success: function (data) {
                window.location.href = "thankYou.html";
            },
            error: function (data) {

                // Error
                alert("error");
                alert(JSON.stringify(data));
                $("#response").html(JSON.stringify(data));
            }
        });
    });

});
```

これらのファイルをWeb公開したS3バケットにアップロードし、S3のエンドポイントにアクセスし画面を確認します
あとはフォームの項目を入力し、「投票ボタン」をクリックすると･･･
無事成功すればDynamoDBにPOSTしたJSONが追加されています
また要望により、同じ社員番号で投票すると内容を更新できるようにしています
Web公開したS3のエンドポイントに対してドメインを向けてやるとさらにいい感じですね。
S3に独自ドメインを登録する方法は@Ichiro_Tsuji様の[独自ドメインを使ってAmazon S3で静的Webサイトをホストする](http://qiita.com/Ichiro_Tsuji/items/c174d580587a622d3358)を参照してください


## さいごに

いかんせん1週間とはいえ通常業務の合間を縫ってやっていたので、かなり雑な処理になっていると思います(汗)
しかも最後のCSV吐き出しはコンソールから生成するというなんともいけてない感じ･･･
次回いつ使うかわからないけど、改善に励んでいこうとおもっております！

ちなみにServerlessFrameworkで作成した環境を消去するときは下記を実行すればよいみたいです

```

sls remove
```

もっといい感じになったらまた記事書き直すんだ･･･