はじめに
以下のブログを参考に SAM を Terraform に組み込んでみたところ、
色々苦労することになったので備忘録を残します
実行環境
- Terraform: 1.0.6
- Terraform providers:
- aws: 3.58.0
- local: 2.1.0
- null: 3.1.0
- SAM: 1.24.1
背景
AWS Lambda のデプロイに昔は apex を使っていましたが、メンテナンスされなくなってしまいました
昔は他に便利なツールもなかったため、
Docker コンテナで依存ライブラリをインストールしてから圧縮するスクリプトを自前で組みました
それを Terraform から呼んで関数毎にビルド・デプロイする方法です
自前実装で長く運用してきたものの、
やはり Lambda 周りが無駄に複雑になってしまっているため、
公式の Lambda Layer や SAM を使って正しくデプロイしようと考えました
長さ制限
最初にぶち当たったのは長さ制限でした
既存システムの Lambda 関数は数が非常に多く、まずそれらを SAM 用に書き下すだけでも大変でした
ようやく全て書いて実行すると、早速エラーが発生
template_body に指定できる長さの上限が 51,200 バイトだったために、実行できなかったのです
そこで、まずは API Gateway から呼び出される Lambda とそれ以外(定時実行など)を分離しました
それでもやはり API の関数が多い、、、
元は GET や POST などのメソッド毎に別関数にしていたのですが、
パス毎、メソッド毎だと、どうしても多くなってしまいます
というわけで、 GET でも POST でも同じパスに来たら同じ関数で処理するようにして、
関数内でメソッドを判別して処理を振り分けるようにしました
def handle(event, _):
if event["httpMethod"] == "GET":
...
if event["httpMethod"] == "POST":
...
これで関数が少なくなり、とりあえず SAM の実行まではできるようになりましたが、、、
Common Layer
元々、データベースアクセスやロギングなどの共通関数は
ビルド時に各関数のディレクトリー内にコピーしていました
最初は「これはしょうがないか、、、」と思ってシェルスクリプトでコピーを組んでいたのですが、
Lambda Layer なるものの存在を初めて知りました
しかも、依存パッケージもこちらに入れられるという、、、
テンプレートに以下のように Layer を定義し、各関数から参照します
Layer 用のディレクトリ(以下の例では ../sam-common/
) に
共通関数の各 Python ファイルと requirements.txt を入れておけば、
あとは SAM が上手くやってくれます
CommonLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: !Sub ${ServiceName}-api-common-layer
Description: common layer for API
ContentUri: "../sam-common/"
CompatibleRuntimes:
- python3.8
Metadata:
BuildMethod: python3.8
XXXFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${ServiceName}-xxx
CodeUri: functions/xxx/
Role: !GetAtt XXXFunctionRole.Arn
Layers:
- !Ref CommonLayer
昔からこれがあれば苦労しなかったのに、、、
同時実行エラー
上記の問題により、 SAM のテンプレートは API と Others に分かれました
しかし、それぞれの SAM を個別に実行するとエラーにならないのに、
なぜか Terraform で実行すると謎のエラーが、、、
OSError: [Errno 66] Directory not empty:...
しばらくはさっぱり理解できませんでしたが、実行状況を眺めてようやく理解できました
Terraform はできるだけ効率的に処理するため、
並列実行可能な箇所は同時に動かしてしまうのです
null_resource から SAM のビルドを実行しているため、
Terraform からは並列実行できないものとは見なされません
SAM が API 用と Others 用で同時に動いた結果、
同じキャッシュディレクトリーを参照し、エラーになっていたのです
並列実行させないためには、依存性を入れて必ず順序を守らせるしかありません
Others 用が終わってから、 API 用が動くようにしました
resource "null_resource" "sam-api-build" {
...
depends_on = [
null_resource.sam-others-build,
...
]
}
ただし、これには問題があって、 Others 用の変更があった場合、 API 用も無駄に動いてしまうのです
どうしても、これは回避できません
本番用とステージング用がある場合、ステージング用を本番用に依存させることで、ステージング適用時はステージングのみ、本番適用時は本番+ステージングが動くようになります
逆にするとステージング適用時に本番の方も動いてしまうので注意
おわりに
他にも細かいエラーが色々あり、結構時間がかかってしまいました
依存性を入れたり、結局、むしろ前より複雑になってしまったのでは、、、、
SAM は単体で運用するならベストですが、 Terraform との相性はあまり良くなさそうです
Layer に関しては1番の収穫だったので、今後は SAM はともかく Layer は必ず使います