はじめに
社内システムで最初 ServerlessFramework を使用しておりましたが、AWS SAMに移行しようという話になりました。AWS SAMへ移行するにあたり幾つか詰まった箇所を紹介します。
- テンプレートの分割方法
- テンプレートを跨いでRestApiIdが使用できない
- 他スタックの出力値をテンプレート内で参照すると、参照先のスタックが更新できない
1. テンプレートの分割方法について
ServerlessFrameworkでLambdaのAPIを作成する際に、大まかにLambda+APIGateway、VPCのリソースを作成していました。ServerlessFrameworkの時はCloudFormationのリソース上限に触れないようにserverless-plugin-split-stacksというプラグインを使い、分割数だけ決めて自動で分割していました。
今回の移行では、自動分割はないのでテンプレートファイルを3つの構成に分けることにしました。それぞれで作成するリソースとフォルダ構成は下記になります。
| ファイル名 | 役割 |
|---|---|
| template.yaml | 各テンプレートを纏める |
| template-vpc.yaml | ルートテーブルやエンドポイントなどのVPCリソース作成 |
| template-lambda.yaml | Lambdaとロググループの作成 |
| template-api.yaml | APIGatewayの作成とLambda紐づけ、LambdaのPermission作成 |
src
├─ handlers/
│ ├─ login/
│ │ └─ src/
│ │ └─ login.py
│ ├─ purchase
│ └─ calculation
├─ .samignore
├─ samconfig.toml
├─ template.yaml
├─ template-vpc.yaml
├─ template-lambda.yaml
└─ template-api.yaml
2. テンプレートを跨いでRestApiIdが使用できない
AWS SAMの記述でLambdaを作成する場合、TypeでAWS::Serverless::Functionを設定し、RestApiIdを指定すればApiGatewayとの紐づけが可能です。
しかし、公式ドキュメントのRestApiIdのパラメータ説明に下記記載があり、別テンプレートで作成したAPIGatewayのIDを設定できないことを知りました。
RestApiId
This cannot reference an AWS::Serverless::Api resource defined in another template.
(これは、別のテンプレートで定義された AWS::Serverless::Api リソースを参照できません。)
そのため、template-lambda.yamlで作成したLambdaのArnをOutPutに出力し、template-api.yamlでAPIGatewayの作成と紐づけを行うようにしました。※下記ソースは一部記載を省いております
Resources:
LambdaStack:
Type: AWS::Serverless::Application
Properties:
Location: template-lambda.yaml
Parameters:
Service: service
Stage: develop
Region: ap-northeast-1
ApiStack:
Type: AWS::Serverless::Application
Properties:
Location: template-api.yaml
Parameters:
Service: service
Stage: develop
Region: ap-northeast-1
Origin: "*"
LoginLambdaArn: !GetAtt LambdaStack.Outputs.LoginLambdaArn
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: lambda-stack
Parameters:
Service:
Type: String
Description: サービス名
Stage:
Type: String
Description: 環境名
Region:
Type: String
Description: AWSリージョン
LogRetentionInDays:
Type: Number
Description: CloudWatchにおけるログの保持日数
LogGroupPrefix:
Type: String
Default: /aws/lambda
Description: CloudWatchロググループのプレフィックス
Globals:
Function:
Timeout: 30
Runtime: python3.14
MemorySize: 512
Architectures:
- x86_64
Tracing: Active
Environment:
Variables:
STAGE: !Ref Stage
Resources:
Login:
Type: AWS::Serverless::Function
Properties:
CodeUri: handlers/
Handler: login/src/login.handler
FunctionName: !Sub ${Stage}-common-login
Role: !GetAtt defaultRole.Arn
LoggingConfig:
LogGroup: !Sub ${LogGroupPrefix}/${Stage}-login
LoginLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub ${LogGroupPrefix}/${Stage}-login
RetentionInDays: !Ref LogRetentionInDays
Outputs:
LoginLambdaArn:
Value: !GetAtt Login.Arn
Export:
Name: !Sub ${Stage}-LoginLambdaArn
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: api-stack
Parameters:
Service:
Type: String
Description: The name of the service being deployed
Stage:
Type: String
Description: 環境名
Region:
Type: String
Description: AWSリージョン
Origin:
Type: String
Description: CORSのAllow-Origin設定値
LoginLambdaArn:
Type: String
Resources:
ApiGateway:
Type: AWS::Serverless::Api
Properties:
Name: !Sub ${Stage}-${Service}
StageName: !Ref Stage
EndpointConfiguration:
Type: EDGE
OpenApiVersion: 3.0.2
Cors:
AllowMethods: "'GET,POST,PUT,OPTIONS'"
AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,X-Amzn-Trace-Id'"
AllowOrigin: !Sub "'${Origin}'"
DefinitionBody:
openapi: '3.0'
info:
title: !Sub ${Stage}-${Service}
version: '1.0'
paths:
/login:
post:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LoginLambdaArn}/invocations
LoginPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Sub ${LoginLambdaArn}
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/*"
template-api.yamlでAPIGatewayをOpenAPIの形式で作成しており、paths配下に/loginというURIを定義しAPIとLambdaの紐づけをしています。上記サンプルでは1つしか記載していないですが、URIを追加する際はpathsに記載していけば問題ありませんでした。
また、APIGatewayとLambdaの紐づけを自力で行っている為、APIGatewayからLambdaを実行する権限がありません。そのため、Permissionの設定も追加が必要です。
3. 他スタックの出力値をテンプレート内で参照すると、参照先のスタックが更新できない
この社内システムではLambda Layerを作成するスタックと、Lambdaを作成するスタックを分離させています。
-
service-api-layer-stack: Lambda Layerを作成 -
service-api-stack: Lambda関数を作成(上記Layerを使用)
そのため、template.yaml内で!ImportValue を用いて、service-api-layer-stackで作成したレイヤーのARNを参照するようにしていました。
LambdaStack:
Type: AWS::Serverless::Application
Properties:
Location: template-lambda.yaml
Parameters:
ApiLayerArn: !ImportValue
Fn::Sub: develop-ApiLayerLambdaLayerArn
しかし、この記載方法でデプロイ後、service-api-layer-stackの更新を行おうとしたら以下のエラーが発生しました。
Update canceled. Cannot update export develop-ApiLayerLambdaLayerArn as it is in use by service-api-stack.
CloudFormationの仕様により、Export値を使用している間は、Export元のスタックを更新できません。レイヤー自体は定期的に更新があるため、更新できなくなることは問題です。
そのため、やり方を変更しました。samコマンド実行前にレイヤーのARNを取得し、sam deployコマンドの実行引数で渡すようにして解決しました。template.yaml側では!Ref ApiLayerArn と記載して参照できました。
API_LAYER_ARN=$(aws cloudformation describe-stacks \
--stack-name "service-api-layer-stack" \
--query 'Stacks[0].Outputs[?OutputKey==`develop-ApiLayerLambdaLayerArn`].OutputValue' \
--output text)
sam build
sam deploy --config-env develop \
--no-confirm-changeset \
--no-fail-on-empty-changeset \
--s3-bucket ${API_BUCKET} \
--parameter-overrides \
"ParameterKey=Stage,ParameterValue=develop" \
"ParameterKey=ApiBucket,ParameterValue=${API_BUCKET}" \
"ParameterKey=ApiLayerArn,ParameterValue=${API_LAYER_ARN}"
参考サイト
テンプレートの分割:
OpenAPIについて: