この記事は CodeChrysalis Advent Calendar 2019 の記事です。
Serverless FrameworkでDynamoDBを構築することについての記事と[Serverless FrameworkでCognitoを構築する]ことについての記事もぜひ御覧ください。
はじめに
Serverless Framework はYAMLでサーバサイドの環境構築ができる、Infrastructure As Code の真骨頂です。確かにAWS等のコンソールでもポチポチクリックしながら環境構築はできますが、
- 変更の管理ができない
- 画面でポチポチクリックするのが本当に面倒(Lazy...)
なのでServerless Framework の CLI でコマンド一発で全てが構築されるのはとても気持ちが良い。今回はその中でもAWSのCognitoをどのように構築するかを紹介します。
この説明の前提は、データベースはDynamoDBをすでにServerless Framework(CloudFormation)で構築しているものとし、また認証はCognitoを使うこととします。
まずはYAML
早速YAMLを紹介します。
a
service: cc-hello-api
plugins:
- serverless-webpack
- serverless-domain-manager
custom:
stage: ${opt:stage, self:provider.stage}
webpack:
webpackConfig: ./webpack.config.js
includeModules:
packagePath: ./package.json
forceExclude:
- aws-sdk
domains:
dev: dev-api.cc.io
prod: api.cc.io
customDomain:
domainName: ${self:custom.domains.${self:custom.stage}}
basePath: "v1"
stage: ${self:custom.stage}
certificateName: "*.cc.io"
createRoute53Record: false
provider:
name: aws
runtime: nodejs12.x
stage: dev
profile: cc-admin
region: ap-northeast-1
functions:
hello:
handler: hello.main
events:
- http:
path: hello
method: get
cors: true
authorizer: aws_iam
resources:
Outputs:
ApiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:custom.stage}-ApiGatewayRestApiId
ApiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
Export:
Name: ${self:custom.stage}-ApiGatewayRestApiRootResourceId
b
service: cc-users-api
plugins:
- serverless-webpack
- serverless-dotenv-plugin
custom:
stage: ${opt:stage, self:provider.stage}
dotenv:
path: ./.env
environment: ${file(./env.yml):${self:custom.stage}, file(./env.yml):default}
webpack:
webpackConfig: ./webpack.config.js
includeModules:
packagePath: ./package.json
forceExclude:
- aws-sdk
provider:
name: aws
runtime: nodejs12.x
stage: dev
profile: cc-admin
region: ap-northeast-1
apiGateway:
restApiId:
"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
restApiRootResourceId:
"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiRootResourceId
environment:
USERS_DYNAMODB_TABLE: ${file(../../database/serverless.yml):custom.usersTableName}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- "Fn::ImportValue": ${self:custom.stage}-UsersDynamoDbTableArn
functions:
createUser:
handler: createUser.main
events:
- http:
path: users
method: post
cors: true
authorizer: aws_iam
getUser:
handler: getUser.main
events:
- http:
path: users/{userId}
method: get
cors: true
authorizer: aws_iam
request:
parameters:
paths:
userId: true
listUsers:
handler: listUsers.main
events:
- http:
path: users
method: get
cors: true
authorizer: aws_iam
request:
parameters:
querystrings:
userEmail: false
paginationToken: false
limit: false
updateUser:
handler: updateUser.main
events:
- http:
path: users/{userId}
method: put
cors: true
authorizer: aws_iam
request:
parameters:
paths:
userId: true
deleteUser:
handler: deleteUser.main
events:
- http:
path: users/{userId}
method: delete
cors: true
authorizer: aws_iam
request:
parameters:
paths:
userId: true
デプロイの順番
Serverless Framework で Cognito を構築することについての記事でも触れたのですが、Serverless Frameworkでマイクロサービスをデプロイするときはデプロイする順番を気にする必要があります。
前述の記事でApiGatewayRestApiId
という値を使っているのですが、API Gatewayをデプロイした結果を参照させる必要があるので、まずはAPI Gatewayをデプロイします。そしてそれが終わった後にCognitoと各エンドポイントをデプロイします。
それぞれの要素を解説
serverless.yml - 1
1つ目のLambdaのデプロイです。これを通してAPI Gatewayもデプロイされます。
plugins:
- serverless-webpack
- serverless-domain-manager
プラグインを2つ使っています。この説明はNode.jsを使う前提としていて、一つはWebpackを使うためのプラグインと、カスタムドメインを使うためのプラグインです。後者については使わない場合はAWSのデフォルトのドメインが提供されますが、ご自身で取得したドメインを使いたい場合はこのプラグインが使えます。
webpack:
webpackConfig: ./webpack.config.js
includeModules:
packagePath: ./package.json
forceExclude:
- aws-sdk
WebpackのConfigがどこにあるのか、Package.jsonはどこにあるのかを設定しています。
forceExclude
でaws-sdk
を指定して点について説明します。aws-sdk
はpackage.json
の中では通常devDependencies
に置かれていると思います。なぜならLambdaにすでにインクルードされているからです。しかしそれはAWS特有の話で、Serverless Frameworkは他のクラウドプラットフォームにも対応している性質からそれは気づけないので、この設定が必要になります。原文はこちら。
domains:
dev: dev-api.cc.io
prod: api.cc.io
customDomain:
domainName: ${self:custom.domains.${self:custom.stage}}
basePath: "v1"
stage: ${self:custom.stage}
certificateName: "*.cc.io"
createRoute53Record: false
ここはドメインの設定です。開発環境と本番環境のそれぞれを設定しています。basePath
とはdomains
で設定したURIの続きを何にするかです。certificateName
はAWSのACMで取得した証明書を指しています。SSLで接続させるので電子証明書が必要になるからです。DNSについてはRoute53を使っていないのなら、createRoute53Record
はfalse
とします。
runtime: nodejs12.x
つい最近(2019/12/24時点)ですが、Node.jsのVer.12世代がLambdaでサポートされました。
functions:
hello:
handler: hello.main
events:
- http:
path: hello
method: get
cors: true
authorizer: aws_iam
ここでLambdaです。hello
はLambda関数の名前、handler
はどこに処理が書いてあるかのファイル名と関数名を書いています。hello.main
はhello
がファイル名、main
が関数名です。
events
に関しては詳しくはServerless Frameworkのドキュメントに詳細がありますが、この例だと、path
はURI、method
はHTTPメソッド、cors
はcors
を使うかどうかです。
authorizer
はデフォルトで良ければaws_iam
です。なにかカスタマイズした場合は、そのリソースを指定します。
resources:
Outputs:
ApiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:custom.stage}-ApiGatewayRestApiId
ApiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
Export:
Name: ${self:custom.stage}-ApiGatewayRestApiRootResourceId
そして忘れずにOutputsを設定することによって、他のデプロイでもAPI Gatewayを参照できるようにします。
serverless.yml - 2
service: cc-users-api
plugins:
- serverless-webpack
- serverless-dotenv-plugin
custom:
stage: ${opt:stage, self:provider.stage}
dotenv:
path: ./.env
webpack:
webpackConfig: ./webpack.config.js
includeModules:
packagePath: ./package.json
forceExclude:
- aws-sdk
provider:
name: aws
runtime: nodejs12.x
stage: dev
profile: cc-admin
region: ap-northeast-1
apiGateway:
restApiId:
"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
restApiRootResourceId:
"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiRootResourceId
environment:
CC_DYNAMODB_TABLE: ${file(../../database/serverless.yml):custom.ccTableName}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- "Fn::ImportValue": ${self:custom.stage}-CcDynamoDbTableArn
functions:
createUser:
handler: createUser.main
events:
- http:
path: users
method: post
cors: true
authorizer: aws_iam
getUser:
handler: getUser.main
events:
- http:
path: users/{userId}
method: get
cors: true
authorizer: aws_iam
request:
parameters:
paths:
userId: true
listUsers:
handler: listUsers.main
events:
- http:
path: users
method: get
cors: true
authorizer: aws_iam
request:
parameters:
querystrings:
userEmail: false
paginationToken: false
limit: false
updateUser:
handler: updateUser.main
events:
- http:
path: users/{userId}
method: put
cors: true
authorizer: aws_iam
request:
parameters:
paths:
userId: true
deleteUser:
handler: deleteUser.main
events:
- http:
path: users/{userId}
method: delete
cors: true
authorizer: aws_iam
request:
parameters:
paths:
userId: true
さて実際のエンドポイントのデプロイです。
plugins:
- serverless-webpack
- serverless-dotenv-plugin
今回はもちろんWebpackとserverless-dotenv-plugin
を入れています。環境変数を使うためにdotenv
を使います。
dotenv:
path: ./.env
これによって環境変数ファイルを読み込んでいます。
apiGateway:
restApiId:
"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
restApiRootResourceId:
"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiRootResourceId
ここでAPI Gatewayを参照することによって、API Gatewayと関連付けます。関連付けないとまた他のAPI Gatewayが立ち上がることになり、それをIAMが参照しないことになるので、このエンドポイントは誰もコールできなくなりますのでご注意。
environment:
CC_DYNAMODB_TABLE: ${file(./database/serverless.yml):custom.ccTableName}
これはServerless Framework で DynamoDB を構築することについての記事でデプロイしているDynamoDBのテーブル名を参照しています。プログラムの中で使いたい意図です。
このようにしてすべてのファイルでテーブル名をハードコーディングしなくとも良いようにできます。${file(./database/serverless.yml)
これがファイル自体を参照するための文法です。
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- "Fn::ImportValue": ${self:custom.stage}-CcDynamoDbTableArn
IAMです。DynamoDBに接続させるためにはIAMで許可する必要があります。これもServerless Framework で DynamoDB を構築することについての記事で説明したとおり、デプロイしたテーブルをOutputsに設定しているので、"Fn::ImportValue"
でArnを取得できるようにしています。
listUsers:
handler: listUsers.main
events:
- http:
path: users
method: get
cors: true
authorizer: aws_iam
request:
parameters:
querystrings:
userEmail: false
paginationToken: false
limit: false
クエリパラメータを使う場合は
parameters:
querystrings:
userEmail: false
paginationToken: false
limit: false
を設定します。false
になっているのは必須ではないという意味です。
またパス(/users/{userId}
)を使う場合は
parameters:
paths:
userId: true
を使います。true
になっているのは必須という意味です。
さいごに
どのようにAPI Gateway、Lambdaをデプロイするのか、またserverless-domain-manager
やserverless-dotenv-plugin
に触れてきました。次はCodeBuildのを説明したいと思います。