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

GitHubCircleCIAWSlambdacognito

Written by Takeshi Oguma (小熊 健) Posted on 2015/12/03



前回の記事で、単純な静的サイトのオートデプロイフローまで完了した後の続き。

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

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

前提

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

構築内容

  1. Cognito準備
  2. フォーム/JavaScript準備
  3. Lambda準備
  4. 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設定の追加」

スクリーンショット 2015-12-02 17.59.15.png

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 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>

スクリーンショット 2015-12-02 18.00.51.png

本番/ステージング、それぞれのバケットに設定するのを忘れずに。

AWS Cognito Identify-Poolの作成

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

スクリーンショット 2015-12-02 18.23.31.png

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

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

スクリーンショット 2015-12-02 18.24.04.png

 
 

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

スクリーンショット 2015-12-02 18.25.57.png

Cognito IAM設定

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

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

ポリシー名 mycorpsite-cognito

{
    "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 -> ポリシーのアタッチ
上記ポリシーをアタッチする。

スクリーンショット 2015-12-03 00.22.38.png

 

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

フォーム・javascriptの準備

既存index.html内のフォーム部分を下記に変更

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

上記項目は、html / javascript / lambda の3つを適宜変更することで要件に合わせて修正、増減可能。
 
まずは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>
 
 

 
修正後のフォーム画面
スクリーンショット 2015-12-03 02.50.58.png

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

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

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送付権限の2つ)

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

ポリシー名 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送付権限)
の2つでOK。

スクリーンショット 2015-12-03 00.56.08.png

Lambda関数の(仮)作成

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

Lambda -> Create a Lambda functions 

Select blueprintにて
Filter に [S3]と入力し、
S3-get-objedtを選択
 
スクリーンショット 2015-12-03 00.59.42.png

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

スクリーンショット 2015-12-03 01.00.10.png

Name  s3site-form
Discprition  Product
Runtime  Node.jp

スクリーンショット 2015-12-03 01.32.04.png

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

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

スクリーンショット 2015-12-03 01.01.10.png

Event sources は Enable now としておく。

スクリーンショット 2015-12-03 01.32.29.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であることが望ましい)

スクリーンショット 2015-12-03 01.43.46.png

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

スクリーンショット 2015-12-03 01.44.03.png

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

CircleCIからlambdaへのデプロイ準備

CircleCIからlambda関数をupdateする為の権限を付与する必要がある。

CircleCIには前編のS3サイトへのオートデプロイ時に、s3-deploy-userの権限を付与されている状態であるので、このユーザへポリシーを追加する。

IAM設定

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

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

ポリシー名 lambda-deploy

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

{
    "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]を追加アタッチする。

スクリーンショット 2015-12-03 02.04.56.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/

 

$ 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にて登録したものを指定する。

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のデプロイ定義を修正する。

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秒程で完了するはず。
スクリーンショット 2015-12-03 09.09.26.png

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

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

無事「success」となったら、ステージングサイトURLにてフォーム入力チェック。
http://st.s3site.proudit.jp/

(IP制限が入っているので、許可IPからは閲覧出来ないが、正しい挙動)

スクリーンショット 2015-12-03 09.07.01.png

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

スクリーンショット 2015-12-03 09.07.10.png

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


スクリーンショット 2015-12-03 09.03.18.png

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

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

スクリーンショット 2015-12-03 09.42.36.png

雑感

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

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

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