やーまんぶろぐ

気が向いた時にだけ書くブログ

Serverless Web Application Workshop をCLIでやってみた Lab 4: Product Update Blog

サーバーレスアーキテクチャ Advent Calendar 2016 の25日目の記事です。

「Serverless Web Application Workshop をCLIでやってみた Lab 3: Administrative Interface」 の続きになります。
yamano3201.hatenablog.jp

大枠はこちらを参考。
qiita.com

README記載のコンソール作業をCLIで置き換えて構築を確認していくというものです。
github.com

ワークショップは全部で4つあります。

  • Lab 1: Static Website Hosting
  • Lab 2: Beta Sign-up Mailing List
  • Lab 3: Administrative Interface
  • Lab 4: Product Update Blog

今回は最後のLab 4: Product Update Blog について書いていきます。

やること

管理者がアップデートできるブログ機能の構築。ブログ記事を保存するテーブルをDynamoDBで作成し、記事一覧を取得するLambdaと記事をポストをするLambdaとIAM roleを作成し、API Gateway(GET/POST resource)でLambda Proxy連携とCognito user pool認証を設定してウェブアプリから呼び出せるAPI endpointを作成してウェブアプリ側に埋め込む。ウェブアプリで管理者としてログイン後、記事のポストと最新記事一覧が表示される事を確認。

ブログ記事を保存するためのテーブルをAmazon DynamoDBで作成

$ export BLOG_TABLE=$(aws dynamodb create-table --table-name Wildrydes_Posts --attribute-definitions AttributeName=Blog,AttributeType=S AttributeName=Timestamp,AttributeType=N --key-schema AttributeName=Blog,KeyType=HASH AttributeName=Timestamp,KeyType=RANGE --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 | jq -r '.TableDescription.TableArn')

ブログ記事を保存するLambdaと、ブログ記事一覧を取得するLambdaを作成

  • ロジックはREADMEに書かれているコードをコピーします。
  • DynamoDBにブログ記事を保存するコードと、ブログ記事一覧を取得するコードになっています
  • コードはzip化して渡します。
$ cat << EOF > index.js
var aws = require('aws-sdk');
var dynamodb = new aws.DynamoDB({});

exports.handler = (event, context, callback) => {
 var body = JSON.parse(event.body);
 var params = {
   TableName: 'Wildrydes_Posts',
   Item: {
     Blog: { S: 'product-updates' },
     Timestamp: { N: body.Timestamp },
     Title: { S: body.Title },
     Body: { S: body.Body }
   }
 };

 dynamodb.putItem(params, (err, data) => {
   if (!err) {
      context.succeed({
       statusCode: '200',
       headers: { 'Access-Control-Allow-Origin': '*'},
       body: JSON.stringify({})
      });
   } else {
      context.fail(err);
   }
 });
};
EOF
$ zip -r create_post.zip index.js
$ export ACCOUNT=$(aws sts get-caller-identity | jq -r ".Account")
$ export CREATE_POST=$(aws lambda create-function --region us-east-1 --function-name CreatePost --role arn:aws:iam::${ACCOUNT}:role/LambdaDynamoDBRole --runtime nodejs4.3 --handler index.handler --zip-file fileb://create_post.zip | jq -r '.FunctionArn')
$ cat << EOF > index.js
var aws = require('aws-sdk');
var dynamodb = new aws.DynamoDB({});

exports.handler = (event, context, callback) => {
 var params = {
   TableName: 'Wildrydes_Posts',
   ScanIndexForward: false,
   KeyConditions: {
        Blog: {
            ComparisonOperator: 'EQ',
            AttributeValueList: [ { S: 'product-updates' } ],
        }
   }
 };

 dynamodb.query(params,(err, data) => {
   if (!err) {
     var posts = [];

     data.Items.forEach((item) => {
       posts.push({
         Timestamp: item.Timestamp.N,
         Title: item.Title.S,
         Body: item.Body.S
       })             
     });

     context.succeed({
       statusCode: '200',
       headers: { 'Access-Control-Allow-Origin': '*'},
       body: JSON.stringify({ Posts: posts })
     });
   } else {
     context.fail(err);
   }
 });  
};
EOF
$ zip -r get_all_posts.zip index.js
$ export GET_ALL_POSTS=$(aws lambda create-function --region us-east-1 --function-name GetAllPosts --role arn:aws:iam::${ACCOUNT}:role/LambdaDynamoDBRole --runtime nodejs4.3 --handler index.handler --zip-file fileb://get_all_posts.zip | jq -r '.FunctionArn')

Lambda連携とCognito user pool認証を設定したAPI Gateway作成

リソース作成

  • Wildrydes APIはすでに作成されているので、そこにpostsリソースを作成します。
$ export API=$(aws apigateway get-rest-apis | jq -r '.items | map(select(.name == "Wildrydes"))' | jq -r '.[].id') 
$ export ROOT_ID=$(aws apigateway get-resources --rest-api-id ${API} | jq -r '.items | map(select(.path == "\/"))' | jq -r '.[].id')
$ export RESOURCE=$(aws apigateway create-resource --rest-api-id ${API} --parent-id ${ROOT_ID} --path-part posts | jq -r '.id')

OPTIONSメソッド作成

  • CORSを有効化するためにemailsリソースにOPTIONSメソッドを作成します。
$ aws apigateway put-method --rest-api-id ${API} --resource-id ${RESOURCE} --http-method OPTIONS --authorization-type NONE --no-api-key-required --request-parameters '{}'
$ aws apigateway put-integration --rest-api-id ${API} --resource-id ${RESOURCE} --http-method OPTIONS --integration-http-method POST  --type MOCK --request-templates '{ "application/json": "{\"statusCode\": 200}" }' --passthrough-behavior WHEN_NO_MATCH
$ aws apigateway put-method-response --rest-api-id $API --resource-id $RESOURCE --http-method OPTIONS --status-code 200 --response-models '{"application/json": "Empty"}' --response-parameters "method.response.header.Access-Control-Allow-Headers=false,method.response.header.Access-Control-Allow-Methods=false,method.response.header.Access-Control-Allow-Origin=false"
$ aws apigateway put-integration-response --rest-api-id ${API} --resource-id ${RESOURCE} --http-method OPTIONS --status-code 200 --response-templates '{"application/json": ""}' --response-parameters '{"method.response.header.Access-Control-Allow-Headers": "'"'"'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"'"'" , "method.response.header.Access-Control-Allow-Methods": "'"'"'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"'"'", "method.response.header.Access-Control-Allow-Origin": "'"'"'*'"'"'"}'

POSTメソッド作成

  • postsリソースに対するPOSTメソッドを作成します。
  • 認証の設定でuser poolを設定したAuthorizerを設定します。
  • Lambda のCreatePost関数を POSTメソッドの送信先に設定します。
$ export AUTHORIZER=$(aws apigateway get-authorizers --rest-api-id ${API} | jq -r '.items | map(select(.name == "Wildrydes_Admin"))' | jq -r '.[].id')
$ aws apigateway put-method --rest-api-id ${API} --resource-id ${RESOURCE} --http-method POST --authorization-type COGNITO_USER_POOLS --authorizer-id ${AUTHORIZER} --no-api-key-required
$ aws apigateway put-integration --rest-api-id ${API} --resource-id ${RESOURCE} --http-method POST --integration-http-method POST --type AWS_PROXY --uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${CREATE_POST}/invocations" --content-handling CONVERT_TO_TEXT
$ aws apigateway put-integration-response --rest-api-id ${API} --resource-id ${RESOURCE} --http-method POST --status-code 200 --response-templates '{"application/json": ""}'
$ aws apigateway put-method-response --rest-api-id ${API} --resource-id ${RESOURCE} --http-method POST --status-code 200 --response-models '{"application/json": "Empty"}'
$ aws lambda add-permission --function-name CreatePost --statement-id prod --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:us-east-1:${ACCOUNT}:${API}/*/POST/posts"

GETメソッド作成

  • postsリソースに対するGETメソッドを作成します。
  • Lambda のGetAllPosts関数をGETメソッドの送信先に設定します。
  • prodステージにAPI を再デプロイします。
$ aws apigateway put-method --rest-api-id ${API} --resource-id ${RESOURCE} --http-method GET --authorization-type "NONE" --no-api-key-required
$ aws apigateway put-integration --rest-api-id ${API} --resource-id ${RESOURCE} --http-method GET --integration-http-method POST --type AWS_PROXY --uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${GET_ALL_POSTS}/invocations" --content-handling CONVERT_TO_TEXT
$ aws apigateway put-integration-response --rest-api-id ${API} --resource-id ${RESOURCE} --http-method GET --status-code 200 --response-templates '{"application/json": ""}'
$ aws apigateway put-method-response --rest-api-id ${API} --resource-id ${RESOURCE} --http-method GET --status-code 200 --response-models '{"application/json": "Empty"}'
$ aws lambda add-permission --function-name GetAllPosts --statement-id prod --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:us-east-1:${ACCOUNT}:${API}/*/GET/posts"
$ aws apigateway create-deployment --rest-api-id ${API} --stage-name prod

ワークショップ用に用意されたlab4のファイルをS3にsync

  • アプリコードは用意されたものをsyncします。
$ aws s3 sync lab4/ s3://${BUCKET_NAME}
$ open https://$(cat cloudfront.json | jq -r '.Distribution.DomainName')/admin

動作確認

  • ログインしてNew Blog Postに進みます。
  • TitleとBodyを記入してPOSTボタンをクリックします。
  • ブログページにリダイレクトされ、作成した投稿が表示されていることを確認します。

最後に

これで全4つのServerless Web Application Workshopが終わりになります。
コンソールから自動で作られてた設定を一つ一つCLIで確認できたので良い勉強になりました。
今回のように構築全てをCLIで行うのは現実的ではないので、中身を理解できたらCloudFormationなどの他の仕組みで構築するのが良いと思います。