前回の記事で、単純な静的サイトのオートデプロイフローまで完了した後の続き。
後編
「問い合わせフォームもサーバレスでDevOps!」
- 認証 AWS Cognito
- 処理 AWS Lambda
- 通知 AWS SES
を利用して、もちろんサーバレスで。
Gibhubへのstaging/masterへのpush のみで、
CircleCI経由で各種環境へ自動デプロイを実現。
#前提
前回同様の続きなので、
s3site.proudit.jp -> 本番サイト
st.s3site.proudit.jp -> ステージングサイト
が、すでにGithub+CircleCiにてデプロイ可能である前提で進める。
#構築内容
- Cognito準備
- フォーム/JavaScript準備
- Lambda準備
- SES準備
- CicriCIデプロイ処理修正
- オートデプロイテスト
#Cognito準備
CognitoはWebアクセスしたユーザに対して、時限で一時的、限定的なAWS権限を付与することが可能。
今回は**「フォーム入力データを指定S3バケットにUPする」**権限を付与する為に構築
###フォームデータを一時的にUPする為のS3バケットを作成する。
公開用 s3site-form-data
ステージング用 st-s3site-form-data
このままだと、一時的な権限であってもどこからでも際限なくデータUPされかねないので、指定サイト外からのアクセス制限をCORS(Cross-Origin Resource Sharing)機能で実施。
各バケットプロパティより、「▼アクセス許可」-> 「CORS設定の追加」
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>
###AWS Cognito Identify-Poolの作成
AWSコンソールより、Cognitoを選択。
***「Create New identity pool」***より、新規Poolを作成。
pool名は任意でOK
今回は認証無しユーザへ一時権限を付与するので、
「Enable access to Unauthenticated identities」にもチェックを入れる。
あとはそのまま作成でOK。
詳細を表示させて、Unauthenticatedの方のロール名を覚えておく。
(このロール名は自動で命名されるが、自分で任意のものも指定可能)
作成後の画面で、**[Get AWS Credentials]**に表示される「IdentityPoolID」も控えておく。(赤ラインの部分)もちろん後から確認も可能。
###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 -> ポリシーのアタッチ
上記ポリシーをアタッチする。
ここまでで 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>
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。
###Lambda関数の(仮)作成
lambdaの関数を作成する。
ここで(仮)としているのは、関数の中身自体は後ほどCircleCIにてデプロイするので、このタイミングでは器を作成するだけという意味。
Lambda -> Create a Lambda functions
Select blueprintにて
Filter に [S3]と入力し、
S3-get-objedtを選択
Event source type を [S3]
Bucket を [s3site-form-data]
Event Type を [Put]
Name s3site-form
Discprition Product
Runtime Node.jp
Code部分は特にこの時点では編集しなくていい。
Role は作成しておいた mycorpsite-lambda-role を指定する。
Event sources は Enable now としておく。
上記と同様の流れで、下記の部分を変更した、ステージング用の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であることが望ましい)
登録したアドレスに承認メールが届くので承認し、
下記の通りverifiedとなることを確認する。
以上で、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]を追加アタッチする。
###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秒程で完了するはず。
処理中error/failerが出て処理が中断した場合はデプロイ失敗となる。
その場合は、適宜エラー内容から判断し、修正する。
Parmission系はIAM設定や、タイポなどをチェック。
無事「success」となったら、ステージングサイトURLにてフォーム入力チェック。
http://st.s3site.proudit.jp/
※(IP制限が入っているので、許可IPからは閲覧出来ないが、正しい挙動)
入力したら、Send Messageをクリック。
ブラウザポップアップで、「問い合わせ完了」と出れば無事S3へUP出来ている。
lambdaが無事デプロイされていれば、ワンテンポ遅れて、メールが届くはずだ。
stagingブランチで一連の表示、挙動を含め、レビューする。
問題なければ、masterブランチへマージすることで、本番へデプロイされる。
本番サイトからの問い合わせフォームも同様に無事メールが届くことを確認する。
#雑感
サーバレス+自動デプロイ、DevOpsをテーマにどこまでやれるかを試した。
サーバレスが故に、関連する様々なサービスとの連携が必須。
それぞれのIAMによる権限調整は、関連処理を熟知が必要。
と、形に持って行くまでのハードルは少々高いが、その分のメリットは十分あると感じた。
- HTML/javascript/lambda の連携のデプロイを可能な限りオートメーション化
- レビューしたStagingソースをそのままMasterへ反映
- 完全サーバレス
この3点を抑えることで、驚くほどデプロイサイクルが早まるのではないかと思う。
ぜひお試しあれ。