いわさです。
AWS でサーバーレスアプリケーションを構築する際に、SAM を使った IaC を導入する場合があります。
最近では AWS Application Composer という SAM で記述されたテンプレートを GUI エディターを使った視覚的に構築することが出来るサービスも登場しました。
CloudFormation でリソース数が多い場合にテンプレートを分割する場合があります。
この考え方は SAM も同様で、規模が大きくなった場合にテンプレート分割戦略を採ることが出来ます。
私がよく見るパターンは次のようなマイクロサービスでのサービスごとにテンプレートを分割するパターンです。
Best practices for organizing larger serverless applications | AWS Compute Blog より引用
上記記事ではテンプレートを分割した場合に共通コンポーネントをどう管理するかについて言及されていますが、それ以外に API Gateway の REST API を使う場合にどこで API Gateway リソースを管理すべきかという課題が出てきます。
本日は API Gateway を含むサーバーレスアプリケーションのテンプレートを分割する場合の API Gateway リソース定義方法を整理したので紹介します。
1 つの関数、1 つのテンプレートのパターン
これが最も最小の API Gateway + Lambda の構成だと思います。
SAM の利用を開始し始めた時点では次のような実装が多いのではないでしょうか。
% tree .
.
├── hello_world
│ └── app.py
├── samconfig.toml
└── template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
この場合はテンプレート上に関数(AWS::Serverless::Function
)のみしか登場しませんが、デプロイしてみると API Gateway も生成されています。
これは上記ハイライト部分で、API Gateway の ID を指定していないため、デフォルトで REST API が自動作成され Lambda 統合も構成ます。
API Gateway がデフォルトの設定になるので、カスタムしたい場合はAWS::Serverless::Api
を定義して関数側でRestApiId
プロパティで参照します。
2 つの関数、1 つのテンプレートのパターン
続いて先程のテンプレートにもう 1 つ関数を追加した場合です。
次のように同一テンプレート内で関数定義を追加します。
% tree .
.
├── hello_world
│ └── app.py
├── hello_world2
│ └── app.py
├── samconfig.toml
└── template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: hoge0410func1
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
HelloWorldFunction2:
Type: AWS::Serverless::Function
Properties:
FunctionName: hoge0410func2
CodeUri: hello_world2/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello2
Method: get
先程と同様に RestApiId は明示的に指定していないので同一テンプレート内であれば同じ REST API に統合される形となります。
それぞれの関数を別テンプレートで定義しつつイベントソースを指定してみる
先程の 2 つの関数を定義してあるテンプレートを分割してみます。
関数としての分割自体は簡単だと思いますが、API Gateway をどうしたら良いか悩みますね。
ここではまず先程と同様にイベントソースを使ってみます。
% tree .
.
├── hello_world
│ └── app.py
├── hello_world2
│ └── app.py
├── samconfig.toml
├── template1.yaml
└── template2.yaml
template1.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: hoge0410func1
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
template2.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Resources:
HelloWorldFunction2:
Type: AWS::Serverless::Function
Properties:
FunctionName: hoge0410func2
CodeUri: hello_world2/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello2
Method: get
上記のような場合は、同じスタック名の場合は API Gateway の定義が上書きされ、別のスタックにした場合はそれぞれ別の API Gateway となります。
カスタムドメインを構成して複数の API Gateway を使う場合は利用出来そうですが、それでもカスタムドメインやマッピングなどの共通部分をどこで定義するという課題は出てきます。
別テンプレートで API Gateway を作成し、外部から Rest API ID 渡す
通常の CloudFormation の分割であれば、別のスタックで API Gateway リソースをデプロイして、そのリソース ID をそれぞれの関数テンプレートに引き渡す構成を採ると思います。
以下のような形で外部から API の ID を設定してみましょう。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Parameters:
ApiId:
Type: string
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: hoge0410func1
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
RestApiId: !Ref ApiId
Path: /hello
Method: get
実は SAM ではこの使い方はサポートされていません。
AWS::Serverless::Function
のイベントソースの設定値であるRestApiId
の仕様として外部から ID を設定することが出来ません。
ビルド時に次のように同一テンプレート内での ID でなければならないというエラーが発生します。
% sam build -t template1.yaml
Starting Build use cache
Error: [InvalidResourceException('HelloWorldFunction', "Event with id [HelloWorld] is invalid. RestApiId must be a valid reference to an 'AWS::Serverless::Api' resource in same template.")] ('HelloWorldFunction', "Event with id [HelloWorld] is invalid. RestApiId must be a valid reference to an 'AWS::Serverless::Api' resource in same template.")
Traceback:
File "/opt/homebrew/Cellar/aws-sam-cli/1.74.0/libexec/lib/python3.8/site-packages/click/core.py", line 1055, in main
rv = self.invoke(ctx)
File "/opt/homebrew/Cellar/aws-sam-cli/1.74.0/libexec/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/opt/homebrew/Cellar/aws-sam-cli/1.74.0/libexec/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
return ctx.invoke(self.callback, **ctx.params)
:
この制限事項はドキュメントにも次のように記述されています。
RestApiId
- RestApi リソースの識別子です。これには、指定されたパスとメソッドを持つオペレーションが含まれている必要があります。通常、このテンプレートで定義される AWS::Serverless::Api リソースを参照するように設定されます。
- このプロパティを定義しない場合は、AWS SAM が生成された OpenApi ドキュメントを使用してデフォルトの AWS::Serverless::Api リソースを作成します。そのリソースには、RestApiId を指定しない同じテンプレート内の Api イベントによって定義されるすべてのパスとメソッドの和集合が含まれます。
- これは、別のテンプレートで定義された AWS::Serverless::Api リソースを参照できません。
別テンプレートで API Gateway を定義する場合は OpenAPI Specification(OAS) を使う
イベントソースのRestApiId
は利用出来ないということがわかりました。
別の API 定義方法として DefinitionBody に OAS を指定することが出来ます。
API Gateway では言語拡張されており OAS 上で Lambda 統合を定義することも出来ます。
ここでは事前に関数を含むテンプレートをデプロイしておき、その ARN を取得して API Gateway に引き渡します。
template1.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: hoge0410func1
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Outputs:
HelloWorldFunction:
Value: !GetAtt HelloWorldFunction.Arn
template3.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Parameters:
Function1Arn:
Type: String
Function2Arn:
Type: String
Resources:
HogeApi:
Type: AWS::Serverless::Api
Properties:
Name: hoge0410api
StageName: hoge0410stage
DefinitionBody:
openapi: '3.0'
info: {}
paths:
/hello1:
get:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function1Arn}/invocations
responses: {}
/hello2:
get:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function2Arn}/invocations
responses: {}
今回はデプロイ時にパラメータを直接指定しました。
デプロイしてみます。
% sam deploy -t template3.yaml --parameter-overrides 'Function1Arn=arn:aws:lambda:ap-northeast-1:123456789012:function:hoge0410func1 Function2Arn=arn:aws:lambda:ap-northeast-1:123456789012:function:hoge0410func2'
Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-b2ke18r5t3j
A different default S3 bucket can be set in samconfig.toml
Or by specifying --s3-bucket explicitly.
Deploying with following values
===============================
Stack name : sam-app
Region : ap-northeast-1
Confirm changeset : True
Disable rollback : False
Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-b2ke18r5t3j
Capabilities : ["CAPABILITY_IAM"]
Parameter overrides : {"Function1Arn": "arn:aws:lambda:ap-northeast-1:123456789012:function:hoge0410func1", "Function2Arn": "arn:aws:lambda:ap-northeast-1:123456789012:function:hoge0410func2"}
Signing Profiles : {}
Initiating deployment
=====================
File with same data already exists at b85950bf435450e6727ebc5557b783c7.template, skipping upload
:
Successfully created/updated stack - sam-app in ap-northeast-1
次のように単一の API Gateway で複数の関数を関連付けしつつ、テンプレートを分けることが出来ました。
ちなみに、API Gateway は OAS じゃなければ設定出来ない詳細パラメータも結構多いので、OAS で定義出来るものは寄せたほうが良いと私は思っています。
利用出来る拡張機能の一覧については次を参考にしてください。
さいごに
本日は AWS SAM でテンプレートを分割する場合の API Gateway と Lambda の定義方針を整理してみました。
結論としては、API Gateway 管理用のテンプレートを用意し、API 定義(DefinitionBody)内で Lambda 統合を定義して、関数はそれぞれのテンプレートで管理しても OK、とするのが良いだろうということでまとめました。
API Gateway を本格的に使用すると OpenAPI 拡張機能を使ってカスタマイズするシーンがどうしても出てきがちなので、その点からも最初から OAS に寄せたほうが安全かなという温度感です。
また、サービスごとに API Gateway を定義してカスタムドメインでまとめる方法も考えられますが、API Gateway に紐づくオーソライザーなどのオブジェクト管理や使用量プランなど考慮しなければいけないことが出てきます。
そのあたりの課題が解決出来そうであればサービスごとに API Gateway を分割する方法も採用出来そうという温度感です。その場合でもカスタムドメインなどの共通部分はどこで管理するかというのは考える必要がありますが RestApiId の制限事項は回避出来そうですね。