kakakakakku blog

Weekly Tech Blog: Keep on Learning!

正式リリースになった AWS SAM CLI の Terraform サポート機能を試す

2023年9月5日に AWS SAM CLI の Terraform サポート機能が GA (正式リリース)になった👏

Amazon API Gateway や AWS Lambda 関数などサーバーレス関連のコンポーネントは Terraform で統一的に管理しつつも,AWS SAM CLI の開発支援機能(sam local invoke コマンドや sam local start-api コマンドでローカルデバッグ)は使いたい❗️という場面はあって非常に便利な組み合わせだと思う.

aws.amazon.com

実際にどういう開発体験なのかを確認するために AWS ブログに載っていたサンプルを試してみる \( 'ω')/

aws.amazon.com

検証環境

今回は macOS 上で SAM CLI 1.97.0(最新)と Terraform 1.5.7(最新)を使う.

$ sam --version
SAM CLI, version 1.97.0

$ terraform version
Terraform v1.5.7
on darwin_arm64

GitHub リポジトリ確認

サンプルコードは GitHub aws-samples/aws-sam-terraform-examples リポジトリの ga ディレクトリにある❗️

github.com

ga ディレクトリには3種類のサンプルがあって,今回は Amazon API Gateway の REST API 前提の api_gateway_v1 を使う📝

$ tree ga -L 1
ga
├── README.md
├── api_gateway_v1
├── api_gateway_v2
└── api_gateway_v2_tf_cloud

4 directories, 1 file

さらに ga/api_gateway_v1 ディレクトリ配下のファイルをまとめた.tf-resources ディレクトリに main.tf など Terraform コードがあって,src ディレクトリに AWS Lambda 関数コード(今回は Python 実装)があって,events ディレクトリに AWS Lambda 関数 auth の動作確認に使うイベント情報がある👌

$ tree ga/api_gateway_v1
ga/api_gateway_v1
├── events
│   └── auth.json
├── src
│   ├── auth
│   │   ├── app.py
│   │   └── requirements.txt
│   └── responder
│       ├── app.py
│       └── requirements.txt
└── tf-resources
    ├── api.tf
    ├── functions.tf
    ├── main.tf
    ├── samconfig.yaml
    └── variables.tf

6 directories, 10 files

事前準備

サンプルコードを確認したところ,Python 3.9 の AWS Lambda 関数になっていた.せっかくなら最新の Python 3.11 で動作確認をしておきたくて ga/api_gateway_v1/tf-resources/functions.tf を修正した.

--- a/ga/api_gateway_v1/tf-resources/functions.tf
+++ b/ga/api_gateway_v1/tf-resources/functions.tf
@@ -5,7 +5,7 @@ module "lambda_function_responder" {
   source_path   = "../src/responder/"
   function_name = "responder"
   handler       = "app.open_handler"
-  runtime       = "python3.9"
+  runtime       = "python3.11"
   create_sam_metadata = true
   publish       = true
   allowed_triggers = {
@@ -23,6 +23,6 @@ module "lambda_function_auth" {
   source_path   = "../src/auth/"
   function_name = "authorizer"
   handler       = "app.handler"
-  runtime       = "python3.9"
+  runtime       = "python3.11"
   create_sam_metadata = true
 }

しかし AWS Provider 側で Python 3.11 がサポートされてなく,sam build コマンドの実行時に以下のエラーが出てしまう🔥

Error: expected runtime to be one of [nodejs nodejs4.3 nodejs6.10 nodejs8.10 nodejs10.x nodejs12.x nodejs14.x nodejs16.x java8 java8.al2 java11 python2.7 python3.6 python3.7 python3.8 python3.9 dotnetcore1.0 dotnetcore2.0 dotnetcore2.1 dotnetcore3.1 dotnet6 nodejs4.3-edge go1.x ruby2.5 ruby2.7 provided provided.al2 nodejs18.x python3.10 java17], got python3.11

そこで AWS Provider の最新を使えるようにga/api_gateway_v1/tf-resources/main.tf を修正して terraform init -upgrade コマンドを実行しておく.

--- a/ga/api_gateway_v1/tf-resources/main.tf
+++ b/ga/api_gateway_v1/tf-resources/main.tf
@@ -2,7 +2,7 @@ terraform {
   required_providers {
     aws = {
       source  = "hashicorp/aws"
-      version = "~> 4.16"
+      version = "~> 5.17"
     }
   }

準備完了👏

さっそく試す: sam buildsam local invoke

sam build コマンド

Terraform コードや設定など細かいことは後ほど確認するとして,まずは試してみる❗️

ga/api_gateway_v1/tf-resources ディレクトリに移動して,Terraform 変数にリージョンを指定して(今回は東京リージョンを使う),あとは使い慣れた sam build コマンドを実行する.しかし今回は --hook-name オプションで terraform を指定して実行する点に注意💡

実行すると Terraform の local-exec Provisioner の実行ログが大量に出てきて,不安になる(Ctrl+C を押したくなる)けどそのまま待ってると Build Succeeded という表示が出て完了する.

$ cd ga/api_gateway_v1/tf-resources

$ export TF_VAR_aws_region=ap-northeast-1

$ sam build --hook-name terraform --terraform-project-root-path ../
Running Prepare Hook to prepare the current application
Executing prepare hook of hook "terraform"

(中略)

module.lambda_function_auth.null_resource.archive[0] (local-exec):
module.lambda_function_responder.null_resource.archive[0] (local-exec):

(中略)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

sam local invoke コマンド

そして使い慣れた sam local invoke コマンドに --hook-name terraform オプションを指定して実行すると,public.ecr.aws/lambda/python:3.11-rapid-x86_64 イメージを使って AWS Lambda 関数を実行できる.今回は responder を実行したら Hello TF World って返ってきた \( 'ω')/ うおお〜

$ sam local invoke --hook-name terraform 'module.lambda_function_responder.aws_lambda_function.this[0]'
Skipped prepare hook. Current application is already prepared.
Invoking app.open_handler (python3.11)
Local image was not found.
Removing rapid images for repo public.ecr.aws/sam/emulation-python3.11
Building image.............................................................................................................................................................................................................................................................................................................................................................................................................................
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4 Version: $LATEST
END RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4
REPORT RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4  Init Duration: 0.74 ms    Duration: 1551.46 ms  Billed Duration: 1552 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Hello TF World\", \"location\": \"xxx.xxx.xxx.xxx\"}"}%

せっかくだから ga/api_gateway_v1/src/responder/app.py を修正して,もう一度 sam build コマンドと sam local invoke コマンドを実行すると,今度は Hello kakakakakku World って返ってきた \( 'ω')/ うおお〜

$ sam build --hook-name terraform --terraform-project-root-path ../

$ sam local invoke --hook-name terraform 'module.lambda_function_responder.aws_lambda_function.this[0]'
Skipped prepare hook. Current application is already prepared.
Invoking app.open_handler (python3.11)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798 Version: $LATEST
END RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798
REPORT RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798  Init Duration: 0.57 ms    Duration: 1201.38 ms  Billed Duration: 1202 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Hello kakakakakku World\", \"location\": \"xxx.xxx.xxx.xxx\"}"}%

そして同じように auth も実行できた \( 'ω')/ うおお〜

$ sam local invoke --hook-name terraform 'module.lambda_function_auth.aws_lambda_function.this[0]' -e ../events/auth.json
Skipped prepare hook. Current application is already prepared.
Invoking app.handler (python3.11)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a Version: $LATEST
END RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a
REPORT RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a  Init Duration: 0.52 ms    Duration: 269.49 ms   Billed Duration: 270 ms    Memory Size: 128 MB    Max Memory Used: 128 MB
{"principalId": "user|a1b2c3d4", "policyDocument": {"Version": "2012-10-17", "Statement": [{"Action": "execute-api:Invoke", "Effect": "Allow", "Resource": ["arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/*/*"]}]}, "context": {"number": 1, "bool": true, "passed": "from authorizer"}}%

また sam local invoke コマンドの引数に実行する AWS Lambda 関数の Terraform リソース ID を指定する必要があるけど,指定なしで実行すると候補を表示してくれるようになってて親切設計だった👏

$ sam local invoke --hook-name terraform

Skipped prepare hook. Current application is already prepared.
Error: You must provide a function logical ID when there are more than one functions in your template. Possible options in your template: ['module.lambda_function_auth.aws_lambda_function.this[0]', 'module.lambda_function_responder.aws_lambda_function.this[0]']

ちなみに Terraform と AWS SAM CLI の連携に関しては以下のドキュメントに詳しく載っている📝 現時点だと最新情報は日本語に翻訳されてなく,まだ GA 前のドキュメントになっているため,英語を確認する必要がある.

docs.aws.amazon.com

samconfig ファイル

AWS SAM CLI でよく使う samconfig にも対応していて,例えば以下のように samconfig.toml を設定できる.

version = 0.1
[default]
[default.global]
[default.global.parameters]
hook_name = "terraform"
skip_prepare_infra = true
[default.build]
[default.build.parameters]
terraform_project_root_path = "../"

すると sam build コマンドと sam local invoke コマンドのオプションを減らせてシンプルに実行できるようになる👏

$ sam build
$ sam local invoke 'module.lambda_function_responder.aws_lambda_function.this[0]'

ちなみに2023年7月頃から AWS SAM CLI で YAML 形式の samconfig もサポートされててサンプルには samconfig.yaml が含まれた.

version: 0.1
default:
  global:
    parameters:
      hook_name: terraform
      skip_prepare_infra: true
  build:
    parameters:
      terraform_project_root_path: ../

さっそく試す: sam local start-api

今度は sam local start-api コマンドを実行して API の動作確認をする❗️

$ sam local start-api --hook-name terraform
Skipped prepare hook. Current application is already prepared.

AWS SAM CLI does not guarantee 100% fidelity between authorizers locally
and authorizers deployed on AWS. Any application critical behavior should
be validated thoroughly before deploying to production.

Testing application behaviour against authorizers deployed on AWS can be done using the sam sync command.

Mounting responder at http://127.0.0.1:3000/open [GET]
Mounting responder at http://127.0.0.1:3000/secure [GET]

(中略)

2023-09-19 22:00:00 Press CTRL+C to quit

すぐに localhost:3000/open API と /secure API を呼び出せた \( 'ω')/ うおお〜

ちなみに /secure API は Amazon API Gateway の Lambda オーソライザーを使ったアクセス制御になっているため,HTTP ヘッダーに myheader: 123456789 を設定した場合にレスポンスが返るように実装されている.

$ curl http://localhost:3000/open
{"message": "Hello kakakakakku World", "location": "xxx.xxx.xxx.xxx"}

$ curl http://localhost:3000/secure
{"message":"Unauthorized"}

$ curl http://localhost:3000/secure --header 'myheader: 123456789'
{"message": "Hello kakakakakku World", "location": "xxx.xxx.xxx.xxx"}

$ curl http://localhost:3000/secure --header 'myheader: IamInvalid'
{"message":"Unauthorized"}

Terraform コード

うまく実行できたので,次は Terraform コードを確認する❗️

module "lambda_function_responder" {
  source        = "terraform-aws-modules/lambda/aws"
  version       = "~> 6.0"
  timeout       = 300
  source_path   = "../src/responder/"
  function_name = "responder"
  handler       = "app.open_handler"
  runtime       = "python3.11"
  create_sam_metadata = true
  publish       = true
  allowed_triggers = {
    APIGatewayAny = {
      service    = "apigateway"
      source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
    }
  }
}

module "lambda_function_auth" {
  source        = "terraform-aws-modules/lambda/aws"
  version       = "~> 6.0"
  timeout       = 300
  source_path   = "../src/auth/"
  function_name = "authorizer"
  handler       = "app.handler"
  runtime       = "python3.11"
  create_sam_metadata = true
}

まず functions.tf を確認すると,AWS Provider の aws_lambda_function ではなく AWS Lambda Terraform module を使っていた.AWS ブログにも書いてある通り,あくまでこれは Terraform コードをシンプルに書くために使われていて,もちろん AWS SAM CLI の Terraform サポートは AWS Provider でも使える👌

github.com

そして AWS Lambda Terraform module を使うことでシンプルに書ける例として create_sam_metadata = true があって,これは Terraform の null_resource を使って sam metadata リソースを自動的に作ってくれる.sam metadata は AWS Lambda 関数のアーティファクトがどこにあるかを伝えるために必要で,さらに AWS Lambda 関数の ZIP ファイルを作るビルド処理にも依存している.sam metadata に関しては以下のドキュメントに載っている❗️

docs.aws.amazon.com

また AWS Lambda Terraform module の main.tf にある null_resource.sam_metadata_aws_lambda_functionpackage.tf にある null_resource.archive を読むと Terraform の local-exec Provisioner を使ってビルドをしてる流れ(sam build コマンドを実行するとログが大量に出てくる部分)など理解できるからおすすめ〜👀

以下のドキュメントには AWS Provider の aws_lambda_function を使ったサンプルも載っていた💡

docs.aws.amazon.com

まとめ

今月 GA になった AWS SAM CLI の Terraform サポート機能をさっそく試してみたけど良かった❗️

さっそく今の現場で実戦投入してみようと思う \( 'ω')/