はじめに
AWS CloudFormationの拡張機能であるAWS SAMを使って、APIGatewayとLambdaを管理するユースケースは多いと思います。SAMのメリットのひとつとしては、少ない記述量で済むことが挙げられると思います。
さて、このSAM。APIGatewayとLambdaを素直に管理するのには良いのですが、プロジェクトが成熟したときに出てくるかもしれない、このような要望には少々難しいところがあります。
私「いやー。AWS謹製のSAM最高。」
SRE「そういえば、Lambdaにエイリアスを作成して、APIGatewayからはエイリアス指定のLambdaを呼び出すようにできたりしますかー?デプロイするタイミングで最新バージョンに向いてしまうのはアレなので。update-alias
でエイリアスのバージョンを上げるようにしたいです。」
私「なるほどです。できると思います!」
... 数時間後 ...
私「SAMの記述ではできなくないかい...?」
Versioningなし & AliasなしのLambda
template.yaml
GET /hello
というAPIを作成するtemplateを例として書いていきます。
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Lambda handler for API Gateway.
Globals:
Function:
Runtime: go1.x
Timeout: 30
Resources:
ApiGateway:
Type: AWS::Serverless::Api
Properties:
EndpointConfiguration: REGIONAL
Name: MyAPI
StageName: prod
MyLambdaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: myFunction
CodeUri: functions/hello/
Handler: app
Events:
GetHelloApi:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /hello
Method: GET
はい、これだけです。
template.yamlをvalidationする
$ sam validate
template.yaml is a valid SAM Template
注意点として、sam validate
は、あくまでテンプレートの構文チェックをしてくれるだけです。つまり、記述内容がデプロイ先のAWS環境と整合性がとれているかといったチェックまではしてくれません。
ですので、初回は、試行錯誤しながら、CloudFormationから吐かれるエラーを潰しながらデプロイしていきました。
build & deploy
$ GOOS=linux GOARCH=amd64 go build -o functions/hello/app ./functions/hello
$ sam package --template-file template.yaml --output-template-file packaged.yaml --s3-bucket $DEPLOYMENT_TARGET_BUCKET
$ sam deploy --template-file packaged.yaml --stack-name $STACK_NAME --debug --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset
AWS APIGatewayのマネジメントコンソールで確認
GET /hello
がmyFunction関数を呼び出すように設定されました。
Versioningあり & AliasありのLambda
追加要件はざっくりこのような感じです。
- Lambda関数のバージョニングをする。
- Lambda関数には、開発用と本番用のエイリアスを用意する。
- 開発用エイリアスは常に最新バージョンを参照する
- 本番用エイリアスは手動でエイリアスの張替えをする。デプロイする度にバージョン変更されたくない
- APIGatewayからは本番用エイリアスのLambdaを呼び出す。
APIGatewayから呼び出されるLambdaをエイリアス指定しようとすると厳しい現実が突きつけられました。
TypeAWS::Serverless::Function
ではLambda関数を指定するんですが、FunctionName
にはARNの指定ができないため、エイリアスを指し示すLambda関数の指定ができません。
結論、ほぼSAMの記述(AWS::Serverless
)は使えずにCloudFormationの記述方法で記載することになりました。
template.yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Lambda handler for API Gateway.
Globals:
Function:
Runtime: go1.x
Timeout: 30
Resources:
# ApiGateway:
# Type: AWS::Serverless::Api
# Properties:
# EndpointConfiguration: REGIONAL
# Name: MyAPI
# StageName: dev
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
EndpointConfiguration:
Types: [ REGIONAL ]
Name: MyAPI
ApiGatewayResourceHello:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt ApiGateway.RootResourceId
PathPart: 'hello'
RestApiId: !Ref ApiGateway
ApiGatewayMethodGetHello:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
HttpMethod: GET
ResourceId: !Ref ApiGatewayResourceHello
RestApiId: !Ref ApiGateway
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri: !Join [ '' , [ 'arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/', !GetAtt MyLambdaFunction.Arn, ':PROD', '/invocations' ] ]
ApiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
DependsOn: [ ApiGatewayMethodGetHello ]
Properties:
RestApiId: !Ref ApiGateway
ApiGatewayStage:
Type: AWS::ApiGateway::Stage
Properties:
DeploymentId: !Ref ApiGatewayDeployment
RestApiId: !Ref ApiGateway
StageName: prod
MyLambdaFunctionAliasProd:
Type: AWS::Lambda::Alias
Properties:
FunctionName: !GetAtt MyLambdaFunction.Arn
FunctionVersion: $LATEST
Name: PROD
MyLambdaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: myFunction
CodeUri: functions/hello/
Handler: app
AutoPublishAlias: DEV
# Events:
# GetHelloApi:
# Type: Api
# Properties:
# RestApiId: !Ref ApiGateway
# Path: /hello
# Method: GET
MyLambdaFunctionPermission:
Type: AWS::Lambda::Permission
DependsOn: MyLambdaFunctionAliasProd
Properties:
FunctionName: !Join [ '' , [ !GetAtt MyLambdaFunction.Arn, ':PROD' ] ]
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Join [ '' , [ 'arn:aws:execute-api:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref ApiGateway, '/*/GET/hello' ] ]
結果、あらたな要件に対応したこともあるのですが、step数が27→63stepと倍以上になりました。
いかにSAMが暗黙的に色々なリソースを作成してくれていたか。SAMへのありがたみを感じると同時にCloudFormationの辛みを感じる経験でした。
build & deploy
さきほどと同様にdeployすると、Cloudformationでエラーになります。
ApiGatewayResourceHello
リソースを作成する際に、CREATE_FAILED
になってしまいました。
Another resource with the same parent already has this name: hello (Service:AmazonApiGateway; Status Code: 409; Error Code: ConflictException; RequestID: f36f9b8b-8275-4fba-bc73-0d3920203333; Proxy:null)
これはつまり、stack内で/helloというリソースが競合しているとのこと。
MyLambdaFunction
リソースに定義していたPath: /hello
は削除しているのに競合になってしまうので、一度stackを削除しました。
AWS APIGatewayのマネジメントコンソールで確認
GET /hello
がPRODエイリアスのmyFunction関数を呼び出すように設定される。
AWS Lambdaのマネジメントコンソールで確認
PRODエイリアスのLambdaに向けてトリガーが設定されている。
まとめ
-
一度stackを削除してデプロイするということは、「リソース削除→新規作成」される挙動になるため、API IDが変わってしまいます。API IDが変わってしまうということは、APIGatewayのエンドポイントに変更が生じてしまうということです。
https://{API_ID}.execute-api.{REGION}.amazonaws.com/{STAGE}/{PATH}
-
要件次第ですが、すべてをSAMで対応することはできないと考えたほうが良いです。VersioningやAlias周りを細かく設定したいのであれば、現時点では素のCloudFormationで書くのが良さそうです。
-
SAMを書いているのか、CloudFormationを書いているのか迷子になることがありました。そんなときはリソースのTypeを見て、
AWS::Serverless::
と定義していればSAMなので、「今は(SAM|CloudFormation)を書いている」と確認してました。
参考