Whats'?
AWS SAMで管理するAWS Lambda関数を、単一AWSアカウント内の複数回(別環境として)デプロイするのにはどうすればよいのかな?ということで、試してみました。
AWS CloudFormationと複数環境
AWS SAMは、AWS CloudFormationの拡張です。
AWS SAM は AWS CloudFormation の拡張であるため、AWS CloudFormation の信頼性の高いデプロイ機能を利用できます。
このため、AWS SAMで管理している内容を複数の環境に適用するには、AWS CloudFormationの考え方に従うのが妥当でしょう。
AWS CloudFormationのドキュメントによると、テンプレートを複数環境向けに利用するには、パラメーターやマッピング、条件セクションで環境間の差異を吸収していくようです。
たとえば、開発、テスト、本番の環境を作成して、本番環境に実装する前に変更をテストすることができます。テンプレートを再利用可能にするには、パラメーター、マッピング、および条件セクションを使用してスタックの作成時にカスタマイズできるようにします。
AWS CloudFormation ベストプラクティス / テンプレートを再利用して複数の環境にスタックを複製する
このあたりですね。
また、AWS SAMではパラメーターの設定は、--parameter-overrides
(設定ファイルの場合はparameter_overrides
)で行うようです。
これらを利用して、AWS SAMで管理するAWS Lambda関数を、複数環境にデプロイしてみます。
やってみること
今回は、以下の内容をお題にしてみます。
- AWS SAMアプリケーションのデプロイ先を開発環境(
development
)と本番環境(production
)の2つに対して行う - アプリケーション自体は、AWS SAMのQuick StartからHello World Example(Node.js)をベースにして作成
- AWS Lambda関数の名前は固定し、どの環境向けの関数なのかを名前でわかるようにする(※)
- ひとつのAWS SAMテンプレートで、複数環境にデプロイできるようにカスタマイズ
- 各環境で異なる構成で動作していることがわかりやすいように、アプリケーション内で環境変数を参照するようにする
- 参照する環境変数は、AWS SAMテンプレート内で環境別の値を設定する
※ … AWS Lambda関数名を固定しない(FunctionName
を指定しない)場合は一意の名前が生成されるので、このお題の意味があまりないかもですが
環境
今回の環境は、こちらです。
$ sam --version
SAM CLI, version 1.37.0
$ aws --version
aws-cli/2.4.11 Python/3.8.8 Linux/5.4.0-94-generic exe/x86_64.ubuntu.20 prompt/off
$ node --version
v14.18.3
$ npm --version
6.14.15
なお、AWSのクレデンシャルは今回は環境変数に指定しているものとします。
$ export AWS_ACCESS_KEY_ID=.....
$ export AWS_SECRET_ACCESS_KEY=.....
$ export AWS_DEFAULT_REGION=ap-northeast-1
AWS SAMアプリケーションを作成する
まずは、AWS SAMアプリケーションを作成します。sam init
でアプリケーション名はsam-hello-world
、テンプレートはhello-world
、ランタイムはNode.js 14.xとします。
$ sam init \
--name sam-hello-world \
--runtime nodejs14.x \
--app-template hello-world \
--package-type Zip
ディレクトリ内へ移動。
$ cd sam-hello-world
ディレクトリ内の構成は、こんな感じです。
$ tree
.
├── README.md
├── events
│ └── event.json
├── hello-world
│ ├── app.js
│ ├── package.json
│ └── tests
│ └── unit
│ └── test-handler.js
└── template.yaml
4 directories, 6 files
ポイントとなるファイルだけ表示しておきましょう。
AWS Lambda関数。
// const axios = require('axios')
// const url = 'http://checkip.amazonaws.com/';
let response;
/**
*
* Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
* @param {Object} event - API Gateway Lambda Proxy Input Format
*
* Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
* @param {Object} context
*
* Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
* @returns {Object} object - API Gateway Lambda Proxy Output Format
*
*/
exports.lambdaHandler = async (event, context) => {
try {
// const ret = await axios(url);
response = {
'statusCode': 200,
'body': JSON.stringify({
message: 'hello world',
// location: ret.data.trim()
})
}
} catch (err) {
console.log(err);
return err;
}
return response
};
AWS SAMテンプレート。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-hello-world
Sample SAM Template for sam-hello-world
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello-world/
Handler: app.lambdaHandler
Runtime: nodejs14.x
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: get
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
ここから、少し変更してみます。
AWS SAMテンプレートには、FunctionName
と環境変数(Environment
)を追加します。
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello-world/
Handler: app.lambdaHandler
Runtime: nodejs14.x
FunctionName: HelloWorldFunction
Architectures:
- x86_64
Environment:
Variables:
APPLICATION_MESSAGE: World
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: get
Environment
にAPPLICATION_MESSAGE
という環境変数を追加しました。
AWS Lambda関数は、追加されたAPPLICATION_MESSAGE
環境変数を参照するように修正します。
exports.lambdaHandler = async (event, context) => {
try {
// const ret = await axios(url);
response = {
'statusCode': 200,
'body': JSON.stringify({
message: `Hello ${process.env['APPLICATION_MESSAGE']}`,
// location: ret.data.trim()
})
}
} catch (err) {
console.log(err);
return err;
}
return response
};
では、ビルドして
$ sam build
デプロイしてみます。
この時、最初から環境別を意識した形での実行にします。まずは開発環境(development
)向けにしましょう。
$ sam deploy --guided
入力内容はこんな感じにしました。
Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: development-sam-hello-world
AWS Region [ap-northeast-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: n
HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
Save arguments to configuration file [Y/n]: y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]: development
ここから生成される値。
Deploying with following values
===============================
Stack name : development-sam-hello-world
Region : ap-northeast-1
Confirm changeset : True
Disable rollback : False
Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp
Capabilities : ["CAPABILITY_IAM"]
Parameter overrides : {}
Signing Profiles : {}
変更内容を確認して、進めます。
CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation LogicalResourceId ResourceType Replacement
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add HelloWorldFunctionHelloWorldPermissionProd AWS::Lambda::Permission N/A
+ Add HelloWorldFunctionRole AWS::IAM::Role N/A
+ Add HelloWorldFunction AWS::Lambda::Function N/A
+ Add ServerlessRestApiDeployment47fc2d5f9d AWS::ApiGateway::Deployment N/A
+ Add ServerlessRestApiProdStage AWS::ApiGateway::Stage N/A
+ Add ServerlessRestApi AWS::ApiGateway::RestApi N/A
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:[AWSアカウントID]:changeSet/samcli-deploy1642156897/137a0b76-fb85-433c-a389-7c43a9b40456
デプロイが終わりました。
2022-01-14 19:43:01 - Waiting for stack create/update to complete
CloudFormation events from stack operations
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus ResourceType LogicalResourceId ResourceStatusReason
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS AWS::IAM::Role HelloWorldFunctionRole -
CREATE_IN_PROGRESS AWS::IAM::Role HelloWorldFunctionRole Resource creation Initiated
CREATE_COMPLETE AWS::IAM::Role HelloWorldFunctionRole -
CREATE_IN_PROGRESS AWS::Lambda::Function HelloWorldFunction -
CREATE_IN_PROGRESS AWS::Lambda::Function HelloWorldFunction Resource creation Initiated
CREATE_COMPLETE AWS::Lambda::Function HelloWorldFunction -
CREATE_IN_PROGRESS AWS::ApiGateway::RestApi ServerlessRestApi -
CREATE_IN_PROGRESS AWS::ApiGateway::RestApi ServerlessRestApi Resource creation Initiated
CREATE_COMPLETE AWS::ApiGateway::RestApi ServerlessRestApi -
CREATE_IN_PROGRESS AWS::Lambda::Permission HelloWorldFunctionHelloWorldPermissionProd -
CREATE_IN_PROGRESS AWS::ApiGateway::Deployment ServerlessRestApiDeployment47fc2d5f9d -
CREATE_IN_PROGRESS AWS::Lambda::Permission HelloWorldFunctionHelloWorldPermissionProd Resource creation Initiated
CREATE_IN_PROGRESS AWS::ApiGateway::Deployment ServerlessRestApiDeployment47fc2d5f9d Resource creation Initiated
CREATE_COMPLETE AWS::ApiGateway::Deployment ServerlessRestApiDeployment47fc2d5f9d -
CREATE_IN_PROGRESS AWS::ApiGateway::Stage ServerlessRestApiProdStage -
CREATE_IN_PROGRESS AWS::ApiGateway::Stage ServerlessRestApiProdStage Resource creation Initiated
CREATE_COMPLETE AWS::ApiGateway::Stage ServerlessRestApiProdStage -
CREATE_COMPLETE AWS::Lambda::Permission HelloWorldFunctionHelloWorldPermissionProd -
CREATE_COMPLETE AWS::CloudFormation::Stack development-sam-hello-world -
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
アクセスして確認してみます。
$ curl https://[REST API ID].execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"Hello World"}
OKですね。
この時に作成されたsamconfig.toml
は、以下のようになりました。
version = 0.1
[development]
[development.deploy]
[development.deploy.parameters]
stack_name = "development-sam-hello-world"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp"
s3_prefix = "development-sam-hello-world"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
samconfig.toml
が生成されたので、以降は--config-env
を指定することでこの環境用の設定を利用することができます。
$ sam deploy --config-env development
この環境名で管理される項目一式のことを、テーブルと呼ぶみたいです。
では、このまま本番環境(production
)用のものも進めてみます。
$ sam deploy --guided
先ほどはdevelopment
だった部分を、production
にして実行。
Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: production-sam-hello-world
AWS Region [ap-northeast-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: n
HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
Save arguments to configuration file [Y/n]: y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]: production
〜省略〜
Deploying with following values
===============================
Stack name : production-sam-hello-world
Region : ap-northeast-1
Confirm changeset : True
Disable rollback : False
Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp
Capabilities : ["CAPABILITY_IAM"]
Parameter overrides : {}
Signing Profiles : {}
変更内容はこちら。
CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation LogicalResourceId ResourceType Replacement
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add HelloWorldFunctionHelloWorldPermissionProd AWS::Lambda::Permission N/A
+ Add HelloWorldFunctionRole AWS::IAM::Role N/A
+ Add HelloWorldFunction AWS::Lambda::Function N/A
+ Add ServerlessRestApiDeployment47fc2d5f9d AWS::ApiGateway::Deployment N/A
+ Add ServerlessRestApiProdStage AWS::ApiGateway::Stage N/A
+ Add ServerlessRestApi AWS::ApiGateway::RestApi N/A
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
そして、このまま進めるとデプロイに失敗します。
CloudFormation events from stack operations
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus ResourceType LogicalResourceId ResourceStatusReason
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS AWS::IAM::Role HelloWorldFunctionRole -
CREATE_IN_PROGRESS AWS::IAM::Role HelloWorldFunctionRole Resource creation Initiated
CREATE_COMPLETE AWS::IAM::Role HelloWorldFunctionRole -
CREATE_IN_PROGRESS AWS::Lambda::Function HelloWorldFunction -
CREATE_FAILED AWS::Lambda::Function HelloWorldFunction HelloWorldFunction already exists in stack
arn:aws:cloudformation:ap-northeast-1:[AWSアカウントID]
:stack/development-sam-hello-
world/4f593cc0-7529-11ec-b829-0e71e619494f
ROLLBACK_IN_PROGRESS AWS::CloudFormation::Stack production-sam-hello-world The following resource(s) failed to
create: [HelloWorldFunction]. Rollback
requested by user.
DELETE_COMPLETE AWS::Lambda::Function HelloWorldFunction -
DELETE_IN_PROGRESS AWS::IAM::Role HelloWorldFunctionRole -
ROLLBACK_COMPLETE AWS::CloudFormation::Stack production-sam-hello-world -
DELETE_COMPLETE AWS::IAM::Role HelloWorldFunctionRole -
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Error: Failed to create/update the stack: production-sam-hello-world, Waiter StackCreateComplete failed: Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" we matched expected path: "ROLLBACK_COMPLETE" at least once
これは、すでに同一AWSアカウント内に同じ名前のAWS Lambda関数が存在しているからですね。
今回はAWS Lambda関数名を指定しつつ、この事象を回避しようといお題になっています。
なお、この時点でsamconfig.toml
の内容はこのようになっています。
version = 0.1
[development]
[development.deploy]
[development.deploy.parameters]
stack_name = "development-sam-hello-world"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp"
s3_prefix = "development-sam-hello-world"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
[production]
[production.deploy]
[production.deploy.parameters]
stack_name = "production-sam-hello-world"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp"
s3_prefix = "production-sam-hello-world"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
いったん、これらのスタックを削除しておきましょう。
## 開発環境用のリソースを削除
$ sam delete --config-env development
## 本番環境用のリソースを削除
$ sam delete --config-env production
複数環境に対応させる
では、AWS CloudFormationのベストプラクティスに習って、複数環境に対応できるようにAWS SAMテンプレートを修正していきます。
AWS CloudFormation ベストプラクティス / テンプレートを再利用して複数の環境にスタックを複製する
まず、環境の切り替えにはパラメーターを使うことにします。Environment
というパラメーターを定義しました。
Parameters:
Environment:
Type: String
AllowedValues:
- development
- production
Environment
に指定した値をキーにして、マッピングを作成します。
Mappings:
EnvironmentConfig:
development:
FunctionNamePrefix: Dev
ApplicationMessage: "World[development]"
production:
FunctionNamePrefix: Prd
ApplicationMessage: "World[production]"
今回は、AWS Lambda関数名に付与するPrefixとAWS Lambda関数に設定する環境変数の2つにしました。
これを、AWS SAMテンプレート内のAWS Lambda関数のリソース定義に設定します。
FunctionName: !Join
- ""
- - !FindInMap
- EnvironmentConfig
- !Ref Environment
- FunctionNamePrefix
- "HelloWorldFunction"
Architectures:
- x86_64
Environment:
Variables:
APPLICATION_MESSAGE: !FindInMap
- EnvironmentConfig
- !Ref Environment
- ApplicationMessage
FindInMap
関数の使い方は、マッピングのドキュメントを見るとわかると思います。
Join
関数は文字列連結ですね。
AWS SAMテンプレート全体としては、このようになりました。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-hello-world
Sample SAM Template for sam-hello-world
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
Parameters:
Environment:
Type: String
AllowedValues:
- development
- production
Mappings:
EnvironmentConfig:
development:
FunctionNamePrefix: Dev
ApplicationMessage: "World[development]"
production:
FunctionNamePrefix: Prd
ApplicationMessage: "World[production]"
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello-world/
Handler: app.lambdaHandler
Runtime: nodejs14.x
FunctionName: !Join
- ""
- - !FindInMap
- EnvironmentConfig
- !Ref Environment
- FunctionNamePrefix
- "HelloWorldFunction"
Architectures:
- x86_64
Environment:
Variables:
APPLICATION_MESSAGE: !FindInMap
- EnvironmentConfig
- !Ref Environment
- ApplicationMessage
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: get
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
次に、samconfig.toml
での環境と、AWS SAMテンプレートで環境を表すパラメーターを紐付けます。
これには、parameter_overrides
を使うことにします。
開発環境向けのものであれば、以下のように指定します。
parameter_overrides = [
"Environment=development"
]
全体としては、こうなりました。
version = 0.1
[development]
[development.deploy]
[development.deploy.parameters]
stack_name = "development-sam-hello-world"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp"
s3_prefix = "development-sam-hello-world"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
parameter_overrides = [
"Environment=development"
]
[production]
[production.deploy]
[production.deploy.parameters]
stack_name = "production-sam-hello-world"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp"
s3_prefix = "production-sam-hello-world"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
parameter_overrides = [
"Environment=production"
]
設定変更したので、sam build
します。
$ sam build
あとは、--config-env
を指定して環境別にデプロイします。
$ sam deploy --config-env development
実行時に、parameter_overrides
の内容が入るようになります。
Deploying with following values
===============================
Stack name : development-sam-hello-world
Region : ap-northeast-1
Confirm changeset : True
Disable rollback : False
Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp
Capabilities : ["CAPABILITY_IAM"]
Parameter overrides : {"Environment": "development"}
Signing Profiles : {}
デプロイ後、確認。
$ curl https://[開発環境のREST API ID].execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"Hello World[development]"}
本番環境用のものもデプロイ。
$ sam deploy --config-env production
parameter_overrides
で、本番環境の名前が入るようになっています。
Deploying with following values
===============================
Stack name : production-sam-hello-world
Region : ap-northeast-1
Confirm changeset : True
Disable rollback : False
Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-1f8pzvbjpaczp
Capabilities : ["CAPABILITY_IAM"]
Parameter overrides : {"Environment": "production"}
Signing Profiles : {}
今度はデプロイが成功するので、動作確認もできます。
$ curl https://[本番環境のREST API ID].execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"Hello World[production]"}
アプリケーションが取得している環境変数の内容も、開発環境のものと異なるものになりました。
関数名の確認。
$ aws lambda list-functions --query 'Functions[].FunctionName' | grep HelloWorld
"PrdHelloWorldFunction",
"DevHelloWorldFunction",
OKですね。
確認できたので、最後に削除して終了。
$ sam delete --config-env development
$ sam delete --config-env production
これで、AWS SAMで管理するAWS Lambda関数を、単一のAWSアカウント内の複数環境にデプロイすることができました。