terraform で作成した api gateway + lambda をローカル環境で動かす手法

api gateway + lambda　の構成を作成したらローカルで動かして動作確認をしたいですよね。
今回は、terraform で作成した上記リソースをローカルで動かす方法をまとめました。
意外と癖が強くて、動くようになるまで苦戦しました。そのあたりについても書いていこうと思います。

# はじめに

AWS は公式で AWS SAM というサーバレス環境構築ツールを提供しています。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html

こちらは宣言した内容をそのままAWSにデプロイできる上、dockerを使えばローカルで動作させることも可能な優れものです。ただ、リソースを作成するために書くSAMテンプレートはなかなか癖があり、学習コストも少し高めです。
公式もそれを意識しているのか、2年ほど前に terraform で作成したコードからSAMテンプレートを作成してくれるツールが発表されました。

https://aws.amazon.com/jp/about-aws/whats-new/2022/11/aws-sam-cli-terraform-support-lambda-local-testing-debugging/

今回は、このツールを使用して API gateway + lambda の構成をローカルで動作させてみようと思います。

# 今回できるもの

今回の構成は以下のようになります。

```mermaid
graph LR
    subgraph localmachine
        apigateway --> lambda
    end
```

ちなみに以前同じ構成をServeless Frameworkを利用して作成しています。

https://qiita.com/nahiro_tus/items/b6d0ae6a554a877851da

しかしながら、Serverless Framework は v.4 からある程度の規模を超えると有償になってしまいました。

https://www.serverless.com/pricing

今後は複数クラウドに対応していて無料で使えるterraformがIaCの最有力候補になると思います。(AWS CDK や CloudFront, bicep, Pulumi, CDM などなど…。IaCの世は大戦国時代ですね。)
前置きが長くなってしまいましたが、ここからは実際に動かしてみようと思います。

# 動かし方

コード全文はこちらです。

https://github.com/Nalagami/terraform-lambda-local-sample

以下のインストトールとセットアップはできているものとするので、ここでの説明は割愛します。

- aws cli
- aws sam
- terraform

今回はlambdaをビルドするコードをshell scriptで書いているので、bashやzshでの実行を前提としています。

```bash
git clone https://github.com/Nalagami/terraform-lambda-local-sample
cd terraform-lambda-local-sample
sam build --hook-name terraform --beta-features --terraform-project-root-path ./../
## sam template の作成が始まる ##
sam local start-api --hook-name terraform
## sam template　を使用して仮想環境が起動する ##
```

ここまで実行できれば、local環境にapiが作成されるはずです。
以下のようなコメントが出ていれば成功です。

```bash
sam local start-api --hook-name terraform                                          
Skipped prepare hook. Current application is already prepared.                             
Mounting hello-terraform at http://127.0.0.1:3000/example [GET]                            
You can now browse to the above endpoints to invoke your functions. You do not need to     
restart/reload SAM CLI while working on your functions, changes will be reflected          
instantly/automatically. If you used sam build before running local commands, you will need
to re-run sam build for the changes to be picked up. You only need to restart SAM CLI if   
you update your AWS SAM template                                                           
2024-06-08 20:46:53 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:3000
2024-06-08 20:46:53 Press CTRL+C to quit
```

試しに叩いてみます。

```bash
curl -v "http://127.0.0.1:3000/example"
*   Trying 127.0.0.1:3000...
* Connected to 127.0.0.1 (127.0.0.1) port 3000
> GET /example HTTP/1.1
> Host: 127.0.0.1:3000
> User-Agent: curl/8.6.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: Werkzeug/3.0.3 Python/3.12.3
< Date: Sat, 08 Jun 2024 13:44:11 GMT
< Content-Type: application/json
< Access-Control-Allow-Origin: *
< Content-Length: 17
< Connection: close
< 
* Closing connection
Request Received!% 
```

きちんとレスポンスが返ってきてますね。

# コード解説

ここからはコード内のどの部分でローカル実行を宣言しているか解説していきます。

```HCL:terraform/lambda.tf
~~~省略~~~

resource "null_resource" "sam_metadata_aws_lambda_function_hello_terraform" {
  triggers = {
    resource_name        = "aws_lambda_function.hello_lambda"
    resource_type        = "ZIP_LAMBDA_FUNCTION"
    original_source_code = "${local.lambda_src_path}"
    built_output_path    = "${local.building_path}/${local.lambda_code_filename}"
  }
  depends_on = [
    null_resource.build_lambda_function
  ]
}

~~~省略~~~
```

AWS SAMがLambda関数を識別するために書かれている設定です。
ここで使われている`null_resource`はterraformの構文でサポートされていないリソースを管理する際に宣言します。AWS SAMは`sam_metadata_aws_lambda_function_hello_terraform`が宣言されていた場合、ここの設定を読み取ってSAMテンプレートを作成します。
また、`depends_on`にはLambdaのzipファイル作成用の`null_resource`が宣言されています。

```HCL:terraform/lambda.tf
~~~省略~~~
resource "null_resource" "build_lambda_function" {
  triggers = {
    build_number = "${timestamp()}" # TODO: calculate hash of lambda function. Mo will have a look at this part
  }
  provisioner "local-exec" {
    command = substr(pathexpand("~"), 0, 1) == "/" ? "./py_build.sh \"${local.lambda_src_path}\" \"${local.building_path}\" \"${local.lambda_code_filename}\" Function" : "powershell.exe -File .\\PyBuild.ps1 ${local.lambda_src_path} ${local.building_path} ${local.lambda_code_filename} Function"
  }
}
~~~省略~~~
```

lambdaのファイルをzipファイルにするための`null_resource`です。
`triggers` が　timestampになっているため、`terraform plan`や`terraform apply`の際は必ず実行されます。

```bash:terraform/py_build.sh
#!/bin/bash

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0

src_code=$1
build_path=$2
output_name=$3
resource_type=$4

[[ -z "$src_code" ]]      && echo "ERROR: src_code is not defined"      && exit 1
[[ -z "$build_path" ]]    && echo "ERROR: build_path is not defined"    && exit 1
[[ -z "$output_name" ]]   && echo "ERROR: output_name is not defined"   && exit 1
[[ -z "$resource_type" ]] && echo "ERROR: resource_type is not defined" && exit 1

echo "building ${resource_type} ${src_code} into ${build_path}"

temp_path=${build_path}/tmp_building
if [[ "${resource_type}" == "Layer" ]]; then
  temp_path=${build_path}/tmp_building/python
  echo "new path ${temp_path}"
fi

pwd
mkdir -p ${build_path}
rm -rf ${build_path}/*
mkdir -p ${build_path}/tmp_building
mkdir -p ${temp_path}
cp -r $src_code/* ${temp_path}
pip install -r ${temp_path}/requirements.txt -t ${temp_path}/.
pushd ${build_path}/tmp_building/ && zip -r $output_name . && popd
mv "${build_path}/tmp_building/${output_name}" "${build_path}/$output_name"
rm -rf ${build_path}/tmp_building˜
```

Lambdaでにアップロードするpythonのzipファイルを作成するためのshellscriptです。
やっていることは、`requirements.txt`に書かれているパッケージをダウンロードして、ソースコードと一緒にzipファイルへ圧縮しているだけです。
公式のリポジトリにpowershell版もあるので、windowsを利用している方は参考にしてください。


# まとめ

terraformで作成したリソースをローカルで動作させる方法をまとめました。ちなみにLambda→DynamoDBの通信もできるみたいですが、その際はAWS環境のDynamoDBを見に行く挙動となりました。そのため、DynamoDB local　でローカルDBを作成しつつ、コードでboto3のエンドポイントを切り替えれば、ローカルだけで動かすこともできそうでした。
ただ、ここまでやるならAWS SAMを勉強した方が早い気もするので、使い所が難しいと感じました。

# 参考

https://dev.classmethod.jp/articles/2022-11-aws-sam-cli-terraform-support-lambda-local-testing-debugging/

https://github.com/aws/aws-sam-cli/issues/4724