問い合わせフォームもサーバレスでDevOps!　(Github,CircleCI,AWS lambda/cognito/ses)　後編

前回の[記事](http://qiita.com/toguma/items/a3c833e42c2469142ca4)で、単純な静的サイトのオートデプロイフローまで完了した後の続き。

後編
「問い合わせフォームもサーバレスでDevOps!」

 - 認証 AWS Cognito
 - 処理 AWS Lambda
 - 通知 AWS SES

を利用して、もちろん**サーバレス**で。
Gibhubへのstaging/masterへのpush　のみで、
CircleCI経由で各種環境へ自動デプロイを実現。


#前提
前回同様の続きなので、
s3site.proudit.jp -> 本番サイト
st.s3site.proudit.jp ->　ステージングサイト
が、すでに**Github+CircleCiにてデプロイ可能**である前提で進める。

#構築内容

 1. Cognito準備
 2. フォーム/JavaScript準備
 2. Lambda準備
 3. SES準備
 5. CicriCIデプロイ処理修正
 6. オートデプロイテスト

#Cognito準備
CognitoはWebアクセスしたユーザに対して、時限で一時的、限定的なAWS権限を付与することが可能。
今回は**「フォーム入力データを指定S3バケットにUPする」**権限を付与する為に構築

###フォームデータを一時的にUPする為のS3バケットを作成する。

公開用　s3site-form-data
ステージング用　st-s3site-form-data

このままだと、一時的な権限であってもどこからでも際限なくデータUPされかねないので、指定サイト外からのアクセス制限をCORS(Cross-Origin Resource Sharing)機能で実施。


各バケットプロパティより、「▼アクセス許可」-> 「CORS設定の追加」

<img width="645" alt="スクリーンショット 2015-12-02 17.59.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/5e2a2583-fa34-a489-f376-f9b227571633.png">


CORS構成エディターより、下記に変更。

```xml:cors設定例(公開サイト用）
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>http://s3site.proudit.jp</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>
```

```xml:cors設定例(ステージングサイト用）
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>http://st.s3site.proudit.jp</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>
```


<img width="799" alt="スクリーンショット 2015-12-02 18.00.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/cba73c91-f368-a54f-48aa-698c67441b1f.png">
---
**本番/ステージング、それぞれのバケットに設定するのを忘れずに。**


###AWS Cognito Identify-Poolの作成

AWSコンソールより、Cognitoを選択。
***「Create New identity pool」***より、新規Poolを作成。
pool名は任意でOK

<img width="607" alt="スクリーンショット 2015-12-02 18.23.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/7d42d2cd-e23d-a0ec-77c4-49f0de146a6a.png">


今回は認証無しユーザへ一時権限を付与するので、
「Enable access to Unauthenticated identities」にもチェックを入れる。
あとはそのまま作成でOK。
　




詳細を表示させて、Unauthenticatedの方のロール名を覚えておく。
（このロール名は自動で命名されるが、自分で任意のものも指定可能）

<img width="739" alt="スクリーンショット 2015-12-02 18.24.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/09977e08-7502-a4cb-b911-11bf70cdbd77.png">

　
　



作成後の画面で、**[Get AWS Credentials]**に表示される「IdentityPoolID」も控えておく。(赤ラインの部分）もちろん後から確認も可能。


<img width="715" alt="スクリーンショット 2015-12-02 18.25.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/a3b4bf60-95e8-600a-0fd1-84d3e552f0d7.png">

###Cognito IAM設定
Cognitoで一時的にユーザへ与える「S3バケットへのPUT権限」を作成

IAM -> ポリシー -> ポリシーの作成 -> 独自のポリシーを作成

ポリシー名　mycorpsite-cognito

```js:IAM
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::s3site-form-data/*",
                "arn:aws:s3:::st-s3site-form-data/*"
            ]
        }
    ]
}
```
　
Cognito Pool作成時に自動で生成されたRoleの内、Unauthのロールに上記ポリシーをアタッチする。

IAM -> ロール -> Cognito_mycorpsiteidpoolUnauth_Role　-> ポリシーのアタッチ
上記ポリシーをアタッチする。

<img width="733" alt="スクリーンショット 2015-12-03 00.22.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/2809b706-a01e-374d-5a1a-87195fe4e29f.png">

　

ここまでで Cognitoの準備完了。
　


#フォーム・javascriptの準備
既存index.html内のフォーム部分を下記に変更

問い合わせフォーム データ仕様
・名前 (text)
・メールアドレス (email)
・問い合わせ内容 (textarea)

上記項目は、html / javascript / lambda　の3つを適宜変更することで要件に合わせて修正、増減可能。
　
まずはhtml部分の編集。

```html:index.html抜粋

・・・・
formタグ部分を下記と入れ替え
・・・・
<form class="form-horizontal">
	<fieldset>
	  <div class="control-group">
		<div class="controls">
                      <input type="text" class="input-xlarge" name="name"  id="name" placeholder="NAME">
		</div>
	  </div>
	   <div class="control-group">
		<div class="controls">
                      <input type="email" class="input-xlarge" name="mail" id="mail" placeholder="user@example.com">
		</div>
	  </div>
	   <div class="control-group">
		<div class="controls">
                      <textarea class="input-xlarge" rows="10" name="contents" id="contents" placeholder="Messages"></textarea>
		</div>
	  </div>
		<input onClick="uploadFile();" type="button" value="Send Message"  class="btn btn-large btn-primary" style="color: #a2a3a3;background-color: #fff;margin-top:30px;" />
	</fieldset>
 </form>
・・・・
・・
・

・
・・
・・・・
下記AWS/Form用のjavascriptを追加
・・・・
<!-- Form Script  -->
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.2.19.min.js"></script>
<script src="js/form.js"></script>
　
　
```
　
修正後のフォーム画面
<img width="1093" alt="スクリーンショット 2015-12-03 02.50.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/fad8d01c-de7a-d9ca-f63f-a0f43ec838eb.png">


formデータupload用のjavascriptファイルをjs/form.jsとして下記内容で追加。

**Cognito IdentityPoolを作成した時に控えた「IdentityPoolId」と差し替えること**
　

```js:js/form.js
var $id = function(id) { return document.getElementById(id); };
AWS.config.region = "ap-northeast-1";
AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "ap-northeast-1:00000000-0000-0000-0000-000000000000"}); ##ここに控えておいたPoolIdを記載
AWS.config.credentials.get(function(err) {
    if (!err) {
        console.log("Cognito Identify Id: " + AWS.config.credentials.identityId);
    }
});

function uploadFile() {
    AWS.config.region = 'ap-northeast-1';
    var url = location.href;

    var s3BucketName = "REPLACE-DATA-BACKET";

    var now = new Date();
    var obj = {"name":$id("name").value, "mail":$id("mail").value ,"contents":$id("contents").value, "date": now.toLocaleString(), "url": url };
    var s3 = new AWS.S3({params: {Bucket: s3BucketName}});
    var blob = new Blob([JSON.stringify(obj, null, 2)], {type:'text/plain'});
    s3.putObject({Key: "uploads/" +now.getTime()+".txt", ContentType: "text/plain", Body: blob, ACL: "public-read"},
    function(err, data){
    if(data !== null){
    alert("お問い合わせ完了致しました");
		console.log('data:' + data);
    }
    else{
        alert("Upload Failed" + err.message);
    }
    });
}
```
　
なお、
javascript内の s3BucketNameは、フォーム内容をUPするバケット「s3site-form-data」「st-s3site-form-data」などを直接指定する箇所だ。

ここをあえて、
**"REPLACE-DATA-BACKET"**
としてしてあることに注意。

これは、最終的に、CircleCIでデプロイするときに、ブランチに応じて本番用、ステージング用を動的に置き換える為の準備である。


#Lambda準備

今回のLambdaの処理概要
「指定のS3バケットにデータがUPされたら、内容を整形して、指定のメールアドレスへ送付する」
ここでいう指定のS3バケットとは前述で作成したs3site-form-dataなどを指す。

###IAMの設定
lambda用のIAMロールを先に準備しておく。
（必要なポリシーはログ出力権限とSES送付権限の２つ）

IAM -> ポリシー -> ポリシーの作成 -> 独自のポリシーを作成

ポリシー名　mycorpsite-lambda

```js:mycorpsite-lambda
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

```

　

IAM -> ロール -> 新しいロールの作成

ロール名 mycorpsite-lambda-role

アタッチするポリシーは先ほど作成した
・mycorpsite-lambda　（ログ権限）
と
・AmazonSESFullAccess　（SES送付権限）
の２つでOK。

<img width="758" alt="スクリーンショット 2015-12-03 00.56.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/d8561bca-83e5-488c-eb87-969e35e44c3c.png">


###Lambda関数の(仮)作成
lambdaの関数を作成する。
ここで（仮）としているのは、関数の中身自体は後ほどCircleCIにてデプロイするので、このタイミングでは器を作成するだけという意味。

Lambda -> Create a Lambda functions　

Select blueprintにて
Filter に [S3]と入力し、
S3-get-objedtを選択
　
<img width="932" alt="スクリーンショット 2015-12-03 00.59.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/598d3525-7ad2-3048-4659-c9184162e851.png">


Event source type を　[S3]
Bucket を　[s3site-form-data]
Event Type を [Put]

<img width="926" alt="スクリーンショット 2015-12-03 01.00.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/dc522cc8-162b-44f8-0857-fbe36ccbcde7.png">

Name 　s3site-form
Discprition 　Product
Runtime　　Node.jp

<img width="860" alt="スクリーンショット 2015-12-03 01.32.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/e76903d5-66f4-f1e5-1c3b-c0704e6ffd4f.png">


Code部分は特にこの時点では編集しなくていい。

Role　は作成しておいた　**mycorpsite-lambda-role**　を指定する。

<img width="976" alt="スクリーンショット 2015-12-03 01.01.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/432dedca-2c5a-8635-2be3-1d81241c6bb4.png">


Event sources　は　Enable now　としておく。

<img width="688" alt="スクリーンショット 2015-12-03 01.32.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/77d42141-7d11-ca02-cc01-79441b27250e.png">


　　


上記と同様の流れで、下記の部分を変更した、ステージング用のlambda関数も作成しておく。

・Name 　st-s3site-form
・Description　Staging
・Bucket　st-s3site-form-data
　
　　

#SES準備
SESは東京リージョンにはない為、バージニアかオレゴンかEUのリージョンを選択する必要がある。
ここではバージニア(us-east-1)で作成しておく。
SESはメールマガジンなど大量配信時には設定項目は多々あるが、今回のように自身の管理内のアドレスを登録するのは至って簡単だ。

SES(バージニア） -> Email Adresses -> Verify a New Email Address　
フォームから送信するアドレスを指定。（自身が受け取れるEmailであることが望ましい）
 
<img width="602" alt="スクリーンショット 2015-12-03 01.43.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/5a763442-73d7-a794-97dd-c4a8b27d4fc8.png">

登録したアドレスに承認メールが届くので承認し、
下記の通りverifiedとなることを確認する。

<img width="805" alt="スクリーンショット 2015-12-03 01.44.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/ba71e628-f716-5470-8b7a-8ea7ac3bfc7a.png">

以上で、SESの準備は完了。


#CircleCIからlambdaへのデプロイ準備
CircleCIからlambda関数をupdateする為の権限を付与する必要がある。

CircleCIには[前編](http://qiita.com/toguma/items/a3c833e42c2469142ca4)のS3サイトへのオートデプロイ時に、s3-deploy-userの権限を付与されている状態であるので、このユーザへポリシーを追加する。

###IAM設定
まずはlambdaデプロイ用ポリシーを作成。

IAM -> ポリシー -> ポリシーの作成 -> 独自のポリシーを作成

ポリシー名　lambda-deploy

PassRoleで指定するARNはlambda実行時に作成したroleのARNを指定。
(ここでは**mycorpsite-lambda-role**)

```js:lambda-deploy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "lambda:CreateFunction",
                "lambda:GetFunction",
                "lambda:UpdateFunctionConfiguration",
                "lambda:UpdateFunctionCode",
                "lambda:UpdateEventSourceMapping",
                "lambda:CreateEventSourceMapping",
                "lambda:ListEventSourceMappings"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": [
                "arn:aws:iam::[自身のAWS-ID]:role/mycorpsite-lambda-role"
            ]
        }
    ]
}
```

IAM -> ユーザー -> s3-deploy-user -> ポリシーのアタッチ
作成したポリシー[lambda-deploy]を追加アタッチする。

<img width="768" alt="スクリーンショット 2015-12-03 02.04.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/364d7a54-a4e3-7a38-8ebe-8665e3d9505d.png">


###lambda関数の本体準備

lambda関数の配置ルール
・Gitリポジトリ管理のトップディレクトリ直下の「lambda」ディレクトリ内に置く
　→htmlディレクトリと同列に配置
・lambda関数本体は　「index.js」
・lambdaが利用するnodejs関数を lambda/node_modules/　以下に置く
　→今回はaws-sdkのみ利用。

[Gitrepo]/
　　　--README.md
　　　--circle.yml
　　　--html/
　　　　　　---index.html
　　　　    ---js/
　　　　　　---etc..
**　　　--lambda/**
**　　　　　　---index.js**
**　　　　　　---node_modules/**

　

```bash:
$ cd [PathTo GitRepo]
$ mkdir lambda
$ mkdir lambda/node_modules
$ npm install aws-sdk
　通常homedirのnode_modules配下にインストールされる。

$ cp -r ~/node_modules/aws-sdk lambda/node_modules/.
$ vi lambda/index.js
$ ls lambda/
index.js	node_modules

```
 　
lambda関数本体

Destination:{ToAddresses:[]}
のアドレスには、SESにて登録したものを指定する。

```js:lambda/index.js
console.log("Loading event")
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var ses = new aws.SES({apiVersion: '2010-12-01', region: 'us-east-1' });
exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;
    s3.getObject({Bucket: bucket, Key: key},
        function(err, data) {
            if (err){
                context.done('error', 'error getting file' + err);
            } else {
                console.log('data:' + data);
                var message = JSON.parse(data.Body);
                console.log('message:' + message);
                var eParams = {
                        Destination: {
                            ToAddresses: ["info@proudit.jp"]
                        },
                        Message: {
                            Body: {
                                Text: {
                                    Data: "mail：" + message.mail+ "\n" + "subject："+ message.name + "\n" + "contents："+ message.contents
                                }
                            },
                            Subject: {
                                Data: "HPからお問い合わせがありました。" + "From:" + message.url
                            }
                        },
                        Source: "info@proudit.jp"
                        };

                        console.log('===SENDING EMAIL===');
                        var email = ses.sendEmail(eParams, function(err, data){
                            if(err){
                                console.log("===EMAIL ERR===");
                                console.log(err);
                                context.done(null, 'ERR'); 
                            }else {
                                console.log("===EMAIL SENT===");
                                console.log(data);
                                context.done(null, 'SUCCESS');
                            }
                });
                console.log("EMAIL CODE END");
            }
        }
    );

};
```



#デプロイ準備

CiecleCIのデプロイ定義を修正する。

```yaml:circl.yml
machine:
  timezone:
    Asia/Tokyo

dependencies:
    override:
        - sudo pip install awscli
    post:
        - aws configure set region ap-northeast-1

test:
  override:
    - echo "Nothing to do here"

deployment:
  production: # just a label; label names are completely up to you
    branch: master
    commands:
      - sed -i -e "s/REPLACE-DATA-BACKET/s3site-form-data/g" html/js/form.js
      - aws s3 sync html/ s3://s3site.proudit.jp/ --delete
      - cd lambda/ && zip -r lambda.zip ./*
      - aws lambda update-function-code --function-name s3site-form --zip-file fileb://./lambda/lambda.zip --publish
  staging:
    branch: staging
    commands:
      - sed -i -e "s/REPLACE-DATA-BACKET/st-s3site-form-data/g" html/js/form.js
      - aws s3 sync html/ s3://st.s3site.proudit.jp/ --delete
      - cd lambda/ && zip -r lambda.zip ./*
      - aws lambda update-function-code --function-name st-s3site-form --zip-file fileb://./lambda/lambda.zip --publish
```


以上で、長かった準備が完了。

#デプロイテスト
ここで、stagingブランチへadd/commit/push　する。

例によってCircleCiが反応し、自動でデプロイ処理を始める。
1分〜1分30秒程で完了するはず。
<img width="890" alt="スクリーンショット 2015-12-03 09.09.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/0e6fc72e-0181-4547-dd09-9773775253ba.png">


処理中error/failerが出て処理が中断した場合はデプロイ失敗となる。
その場合は、適宜エラー内容から判断し、修正する。

Parmission系はIAM設定や、タイポなどをチェック。

無事「success」となったら、ステージングサイトURLにてフォーム入力チェック。
[http://st.s3site.proudit.jp/](http://st.s3site.proudit.jp)  
※**(IP制限が入っているので、許可IPからは閲覧出来ないが、正しい挙動）**


<img width="852" alt="スクリーンショット 2015-12-03 09.07.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/b3f886cc-deab-b901-0284-4f7ca1d1506b.png">

入力したら、Send Messageをクリック。
ブラウザポップアップで、「問い合わせ完了」と出れば無事S3へUP出来ている。

<img width="543" alt="スクリーンショット 2015-12-03 09.07.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/7fa4b5d6-5d70-480f-a608-138d789a9df5.png">


lambdaが無事デプロイされていれば、ワンテンポ遅れて、メールが届くはずだ。

----
<img width="592" alt="スクリーンショット 2015-12-03 09.03.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/ef8d72ba-dd69-11d2-c5a6-58bc680c9d13.png">



stagingブランチで一連の表示、挙動を含め、レビューする。
問題なければ、masterブランチへマージすることで、本番へデプロイされる。

　
本番サイトからの問い合わせフォームも同様に無事メールが届くことを確認する。

<img width="608" alt="スクリーンショット 2015-12-03 09.42.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/75ad7de0-3fd5-a302-b702-1b2bf10614e3.png">


#雑感

サーバレス＋自動デプロイ、DevOpsをテーマにどこまでやれるかを試した。

サーバレスが故に、関連する様々なサービスとの連携が必須。
それぞれのIAMによる権限調整は、関連処理を熟知が必要。
と、形に持って行くまでのハードルは少々高いが、その分のメリットは十分あると感じた。


- HTML/javascript/lambda の連携のデプロイを可能な限りオートメーション化
- レビューしたStagingソースをそのままMasterへ反映
- 完全サーバレス

この3点を抑えることで、驚くほどデプロイサイクルが早まるのではないかと思う。
ぜひお試しあれ。