TerraformでやってきたLambdaデプロイの試行錯誤の歴史と行き着いた雑な運用

この記事は[Wanoグループ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/wano-group)  / 7日目の記事になります。。

terraformユーザーの皆様は Lambda や StepFunctions などのデプロイどうしてますか?
プロジェクト構成によっていろんなパターンがあるかと思いますが、少し前までは結構悩んでるユーザーが多かったように記憶しています。 
**特に弊社ではデプロイ間の依存や順番、リポジトリ構成の都合で大いに悩みました。**
弊社でいままで使用/検討したものを振り返りつつ、プロジェクトで固まってきた運用を紹介します。

# いままでにやってきた/検討したデプロイ

現行関わっているプロジェクトの特徴として、**アプリ/ビジネスロジック系リポジトリとIaC専用リポジトリが別である** ということがあります。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85375/88e0e4a9-4fec-961d-bbd2-e55d21d6619f.png)


運用が決まるまでの苦しみや試行錯誤もこの要件にまつわるものである点にご留意ください。   


## 1. そのままterraformのLambdaモジュールを使い、IaCリポジトリ側に置く (local code)
愚直にterraformのlambdaモジュールを学んでいくと、こうなると思います。
ハッシュ値やzip関数などを使ってローカルのアプリケーションコードを常に同梱するパターンです。

```hcl
data "archive_file" "layer_zip" {
  type        = "zip"
  source_dir  = "lambda"
  output_path = "lambda.zip"
}

resource "aws_lambda_function" "test_lambda" {
  filename         = "lambda.zip"
  function_name    = local.lambda_name
  role             = aws_iam_role.iam_for_lambda.arn
  handler          = "index.handler"
  runtime          = "nodejs12.x"
  timeout          = 40
  source_code_hash = data.archive_file.layer_zip.output_base64sha256
}

```

### Pros
- 常にTerraform経由でIaCできるので、IAMや物理id/permission/event当てなど、他terraformリソースとの連携が簡単
-  IaCの経路がこれだけなのでわかりやすい

### Cons
- アプリケーションのコードが別リポジトリだったりすると、ビルドやCIの経路に悩むことになる
  
  
### 検討したこと  
アラート通知やインフラ監視など、**ビジネスロジックそのものとあまり関係のない**lambdaコードなどは弊社でもこのパターンを 今も採用しており、先述のIaCリポジトリの中にコードの実体も同梱しています。
**弊社で問題になったのはアプリ/ビジネスロジック系リポジトリとIaC専用リポジトリが別であるパターンです。**
Web3層アプリ程度ならInfra/アプリのリポジトリ分離の思想はうまくいっていたのですが、LambdaやStepFunctionsで実現できるような「ビジスロジック/アプリケーション寄り」のインフラコンポーネントを置くときに、どこでデプロイされるべきか悩み始めました。  
特にLambdaやStepFunctionsの初回デプロイは「中身のロジック」と一緒でなくてはならないため、インフラリポジトリでLambdaをデプロイするときには同時にコードもなければいけません。  



## 2. そのままterraformのLambdaモジュールを使い、IaCリポジトリ側に置く (s3/docker)
上記と同じパターンで、Lambdaのロジック置き場として s3 object/dockerを指定する場合です。

```hcl
resource "aws_lambda_function" "image_resize_api" {
  function_name = local.lambda_name
  s3_bucket     = local.lambda_code_bucket
  s3_key        = "image_resize_api/lambda.zip"     
  role          = module.s3_image_resize_api_role.iam_role_arn
  handler       = "main"
  runtime       = "go1.x"
  timeout       = 40
  memory_size   = 6000
}
```

### Pros
- S3のobject更新とコード更新の分離が簡単
- 常にTerraform経由でIaCできるので、IAMや物理id/permission/event当てなど、他terraformリソースとの連携が簡単

###  Cons
  * アプリケーションのコードが別リポジトリだったりすると、ビルドやCIの経路に悩むことになる(同じ)
  * 別にS3リポジトリ/ECR設定が要る
  * 初回デプロイにS3オブジェクトや少なくとも少なくとも一つのdockerimageの実体が必要
  * 別リポジトリのCIでs3 object/docker image の更新や ``` aws lambda update-function-code ``` が必要

### 検討したこと  
はじめのパターンより分離が可能になりました。初回デプロイで必ずコード実体としてのS3 Objectが必要なのがいまいちなくらい、です。 Lambdaのビジネスロジックが更新された場合、更新を伝えてあげるスクリプトが別途アプリのCI/CD時に必要になります。


## 3. 旧apexやlambrollなどのLambda専用デプロイツールを使い、アプリのデプロイとサイクルを合わせる

[apex  (メンテされていない)](https://github.com/apex/apex) や [lambroll](https://github.com/fujiwara/lambroll) など専用ツールを使っていたこともあります。
これらはlambda関数そのものをデプロイするものであり、``` remote state ```が読み込み可能だったりすることが特徴です。  

### Pros
* Lambda関数「以外」との分離が可能
* ローカルのtfやterraform remote state が使える

### Cons
* 実行permissionなどで結局インフラリポジトリ側でLambda本体への参照が必要になる

### 検討したこと
これも結局インフラリポジトリが分かれている弊社プロジェクトならではの事情になるのですが、Lambdaそのもののデプロイ自体もterraformにやらせたほうが何かと都合がよかったのもあり、採用しなくなりました。  
具体的には、LambdaのARNが必要なコンポーネントを宣言しておきたいときです。  

```hcl
resource "aws_cloudwatch_event_target" "sfn_event_to_lambda_1" {
  rule = aws_cloudwatch_event_rule.fail_event.name
  target_id = "${local.env}_sfn_fail_to_runtime_error"
  arn       = aws_lambda_function.litetask.arn

  input_transformer {
    input_paths = {
      "detail" = "$.detail"
    }
    input_template = <<-JSON
{
  "task_type" : "/on_failed",
  "payload" : {
    "detail" : <detail>
  }
}
JSON
  }
}

```

これ系はLambdaのARNが初回デプロイ時点で存在しなくとも特にエラーが出ないものも多いので、目をつむれるといえばつむれるのですが、そうでもないものがあると急にインフラが先にあるべきか、**Lambdaが先にあるべきか....のデプロイ順序が難しくなる**ので、その辺りの判断がけっこうめんどくさいです。  
計画的/明確に

- iamだけのデプロイ層やイベント層をIaCのデプロイサイクル上分けている
- コンポーネントをつなぐ層をIaCのデプロイサイクル上分けている

パターンだと有効かもしれません。  


## 4. 部分的に SAM や ServerlessFramework などのサーバレス/アプリ層寄りのIaCツールをつかい、アプリのデプロイとサイクルを合わせる
別のIaCを使うパターンです。  

### Pros
- S3のobject更新とコード更新の分離が簡単
- 得意なものがあるツールに縦割りで任せられる


###  Cons
- インフラリポジトリ側で宣言したものとの受け渡しが大変
- ツールが分散する


### 検討したこと  
「アプリ/ビジネスロジックよりのコンポーネントってstatefulなAWS Componentとわけておきたいよね」
みたいな肌感が最初からあると、これは有効かもしれません。
得意なことは得意なツールに任せる。
ただしこれも結局デプロイ単位の綿密な設計がいる話ですので、なかなか使いどころが難しかったです。
また、IaCツールが分かれると、**自動払い出しされるタイプのARNなどでいちいちParameter Storeを介した受け渡し方法を考えなきゃいけなかったりして、**数が増えるとなかなか管理が難しかったりします。  


# 採用しているパターン

## 5. そのままterraformのLambdaモジュールを使ってIaCリポジトリ側で管理 + ignore_changes + 別途 aws-cli

結局たどりついた雑なパターンが以下になります。

IaCリポジトリ側

```hcl
resource "aws_lambda_function" "image_resize_api" {
  function_name = "api_server"
  s3_bucket     = "${local.env}_terraform_lambda_source" // dummy.実際には他のアプリ側リポジトリのCIで更新
  s3_key        = "terraform_dummy_lambda/lambda.zip"     //  dummy.実際には他のアプリ側リポジトリのCIで更新
  role          = module.s3_image_resize_api_role.iam_role_arn
  handler       = "main"
  runtime       = "go1.x"
  timeout       = 40
  memory_size   = 6000


  // 実際には他のアプリ側リポジトリのCIで更新するのでignore
  lifecycle {
    ignore_changes = [
      s3_bucket, s3_key
    ]
  }
}

```

アプリのリポジトリ側

``` makefile:アプリ側Makefile

PROJECT_ROOT=${PWD}/../../../
build:
	mkdir -p .build/api_server
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
	    go build -ldflags "-X main.version=$(git describe --tags --abbrev=0) \
	    -X main.revision=$(git rev-parse --short HEAD)" \
	    -o .build/api_server/main ${PROJECT_ROOT}/cmd/api_server
deploy:
	cd ${PWD}/.build/api_server && zip lambda.zip main && \
	aws lambda update-function-code --function-name api_server --zip-file fileb://lambda.zip


```



(1) あらかじめ共通のダミーS3 Objectを決めておく
(2) s3_bucket, s3_keyはignore_changeに入れる
(3) lambdaや周辺IaCコードは先に初回デプロイ(エラーが出ない)
(4) アプリのロジックを持っている側のリポジトリのCIでs3_bucket, s3_keyやコードを更新

### Pros
- インフラだけ先にデプロイできる
- アプリのコード更新くらいならterraform側のデプロイがいらない
- だいたい対応できる


###  Cons
- s3 objectのzip化などのコードはアプリ側CIで書かないといけない
- ダミーのオブジェクト自体はプロジェクトのはじめに決めでおかないといけない(若干かっこわるい)
- 環境変数など外部依存が増えるときはIaCリポジトリ側を更新しないとダメ
- ignore_changesがあるterraformならではの手法?

### 検討したこと
要は初回デプロイでいったりきたりするのをなるべくやめたかったのがメインなので、アプリのロジック/コードは別リポジトリのCIに完全に任せきり、**Lambda関数そのものはterraformで構成管理に集中させます。**
IaC側リポジトリの構成管理の更新にLambdaのコードが巻き込まれないように、s3objectやdocker imageタグ指定まわりには```ignore_changes ```を利用しています。 
また、アプリ側CIでは zip化/update function-code のコードが必要になりますが、まあ一度書いちゃえば慣れるものなので、特別なツールを使わずシェルで更新しています。  
  
  
  
ちなみに、AWS StepFunctionsでかなりビジネスロジック寄りのStateMachineを書くときも、この ``` ignore_changes```を使って**ダミーコードの初回デプロイと他リポジトリでのロジック更新**を分離しています。Lambdaに限らず「一概にインフラとは言えない!」という気持ちになるコンポーネントを使う際にはわりと便利な仕組みだなあと感じています。


# まとめ
terraform x lambda x polyrepoなデプロイの試行錯誤の一端をお見せしました。  
既存プロジェクトの事情によってこのへんの縦割り/横割りの構成管理の思想ってけっこうバラバラかとは思いますが、「これが最強だ」みたいなのはどんどん知っていきたいですね。