やーまんぶろぐ

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

Serverless Web Application Workshop をCLIでやってみた Lab 3: Administrative Interface

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

「Serverless Web Application Workshop をCLIでやってみた Lab2: Beta Sign-up Mailing List」 の続きになります。
yamano3201.hatenablog.jp

大枠はこちらで紹介されています。
qiita.com

コンソールでの手順はREADMEに記載されています。
github.com

CLIに置き換えてみたものをメモしていきます。
作業はMacで、リージョンはバージニアを想定して書いてます。

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

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

今回はLab 3: Administrative Interfaceについて書いていきます。

やること

管理インターフェイスの作成。Cognito user poolを使って管理者を定義し、DynamoDBに記録されている登録ユーザ一覧を取得するLambdaとIAM roleを作成し、API Gateway(GET resource)でLambda Proxy連携とCognito user pool認証を設定してウェブアプリから呼び出せるAPI endpointを作成してウェブアプリ側に埋め込む。ウェブアプリで管理者としてログイン後、登録ユーザ一覧が表示される事を確認。

Cognito user poolを使って管理者を作成

  • まずはuser poolを作成します。
  • user poolに紐づく管理者を作成します。ユーザネームとパスワードで認証できます。
  • 後半でAPIGatewayのAuthorizerにuser poolを設定することで、ログインされたユーザのみAPIを呼び出せるように認可の設定をすることができます。
  • パスワードには大文字、数字、記号を入れて8文字以上の文字列を指定します。
$ export USER_POOL=$(aws cognito-idp create-user-pool --pool-name Wildrydes_Admin --admin-create-user-config AllowAdminCreateUserOnly=true | jq -r '.UserPool.Id')
$ export USER_POOL_CLIENT=$(aws cognito-idp create-user-pool-client --user-pool-id ${USER_POOL} --client-name web | jq -r '.UserPoolClient.ClientId')
$ aws cognito-idp admin-create-user --user-pool-id ${USER_POOL} --username yamano1023@gmail.com --temporary-password XXXXXX

LambdaとDynamoDBにアクセス可能なIAMロールの作成

  • LambdaとDynamoDBにアクセスできるIAMロールを作成しておきます。
  • ロールを作成してからポリシーをアタッチします。
$ export ASSUME_ROLE=assume_role.json
$ cat << EOF > ${ASSUME_ROLE}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
$ export LAMBDA_DYNAMODB_ROLE=$(aws iam create-role --role-name LambdaDynamoDBRole --assume-role-policy-document file://${ASSUME_ROLE} | jq -r  .'Role.Arn')
$ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess --role-name LambdaDynamoDBRole
$ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole --role-name LambdaDynamoDBRole

DynamoDBから登録ユーザ一覧を取得するLambdaを作成

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

exports.handler = (event, context, callback) => {
 var params = {
   TableName: "Wildrydes_Emails"
 };

 dynamodb.scan(params, function(err, data) {
   if (!err) {
     var emails = [];
     data.Items.forEach((item) => emails.push(item.Email.S));

     context.succeed({
       statusCode: '200',
       headers: { 'Access-Control-Allow-Origin': '*'},
       body: JSON.stringify({ Emails: emails })
     });
   } else {
     context.fail(err);
   }
 });  
};
EOF

$ zip -r get_all_emails.zip index.js
$ export GET_ALL_EMAILS=$(aws lambda create-function --region us-east-1 --function-name GetAllEmails --role ${LAMBDA_DYNAMODB_ROLE} --runtime nodejs4.3 --handler index.handler --zip-file fileb://get_all_emails.zip | jq -r '.FunctionArn’)

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

リソース作成

  • rest apiを作成します。
  • user poolを設定したAuthorizerを作成します。
  • emailsのリソースを作成します。emailsがそのままパスになります。
  • 今回のようにパスがルート直下の場合は、ルートIDを指定して作成します。
$ export API=$(aws apigateway create-rest-api --name Wildrydes | jq -r '.id')
$ export ACCOUNT=$(aws sts get-caller-identity | jq -r ".Account")
$ export AUTHORIZER=$(aws apigateway create-authorizer --rest-api-id ${API} --name Wildrydes_Admin --type COGNITO_USER_POOLS --identity-source 'method.request.header.Authorization' --provider-arns arn:aws:cognito-idp:us-east-1:${ACCOUNT}:userpool/${USER_POOL} | 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 emails | jq -r '.id')

OPTIONSメソッド作成

  • CORSを有効化するためにemailsリソースにOPTIONSメソッドを作成します。これを設定しないとブラウザからアクセスすることができないので注意が必要です。
  • メソッドリクエスト、統合リクエスト、統合レスポンス、メソッドレスポンスの4つを作成します。
    • リソースに対するメソッド (OPTIONS) を作成します。
    • MOCKタイプで統合リクエストを作成します。
    • メソッドレスポンスのレスポンスヘッダーにAccess-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Originを追加します。
    • 統合レスポンスでヘッダーのマッピングをします。
$ 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": "'"'"'*'"'"'"}'

GETメソッド作成

  • リソースに対するメソッド (GET) を作成します。
  • 認証の設定でuser poolを設定したAuthorizerを設定します。
  • Lambda 関数を GET メソッドの送信先に設定します。
  • prodステージにAPI をデプロイします。
  • API Gateway が Lambda 関数を呼び出すためのアクセス権限を付与します。ステージごとにアクセスを絞りたい場合は*をprodに変更します。
$ aws apigateway put-method --rest-api-id ${API} --resource-id ${RESOURCE} --http-method GET --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 GET --integration-http-method POST --type AWS_PROXY --uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${GET_ALL_EMAILS}/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 apigateway create-deployment --rest-api-id ${API} --stage-name prod
$ aws lambda add-permission --function-name GetAllEmails --statement-id prod --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:us-east-1:${ACCOUNT}:${API}/*/GET/emails"

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

  • アプリコードは用意されたものを使います。
  • userPoolIdとuserPoolClientId、apiUrlを今回のものに書き換えてからsyncします。
  • /adminにアクセスしてログインします。
  • Cognito user poolを使って作った管理者でログインして、「View Emails」からDynamoDBから登録ユーザ一覧を取得して表示します。
$ cp lab2/scripts/config.js lab3/scripts
$ sed -i -e "s/userPoolId: \'\'/userPoolId: \'${USER_POOL}'/" lab3/scripts/config.js
$ sed -i -e "s/userPoolClientId: \'\'/userPoolClientId: \'${USER_POOL_CLIENT}'/" lab3/scripts/config.js
$ sed -i -e "s/apiUrl: \'\'/apiUrl: \'https:\/\/${API}.execute-api.us-east-1.amazonaws.com\/prod\'/" lab3/scripts/config.js
$ aws s3 sync lab3/ s3://${BUCKET_NAME} 
$ open https://$(cat cloudfront.json | jq -r '.Distribution.DomainName')/admin

最後に

CORSエラーの解消にけっこう時間を消費してしまいました。。
けっきょくLambda 関数を呼び出すためのアクセス権限が抜けていたのが原因でした。Lambda側の設定だったので見逃していました。

最後の1つも頑張ります。