Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

はじめに

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

最終目標

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

構成はこんな感じ

青枠の部分がServerlessFramework

serverless-img.PNG

大まかな流れ

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

さあ、はじめてみようか

書いていく前に・・・今回ServerlessFrameworkを触るにあたって@hiroshik1985様のとことんサーバーレス①:Serverless Framework入門編を参考にさせていただきました。

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サイトをホストするを参照してください

さいごに

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

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

sls remove

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

is_ryo
(IoTチョットワカル)フロントエンドエンジニア。 Vue.js / AWS / GraphQL / Serverless
https://is-ryo.com
acall
ACALLは、「Life in Work and Work in Life for Happiness」をVISIONとして、どこでも安心・安全・快適なワークスタイルを実現するスマートオフィスプラットフォーム「WorkstyleOS」を開発・提供しています。オフィスワークとリモートワークのベストミックスを通じて、人々の「くらし」と「はたらく」を自由にデザインできる世界を目指します。
https://www.workstyleos.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away