3
2

More than 1 year has passed since last update.

Terraform + SAM で苦労した話

Last updated at Posted at 2021-10-27

はじめに

以下のブログを参考に 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 は必ず使います

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2