こしあん
2025-02-11

Terraform+SAMによるLambdaのローカルテスト


43{icon} {views}

Terraform構成を維持しつつSAMでローカルテストを行い、動作確認後にTerraformで本番デプロイする流れを紹介。local_fileリソースを用いてSAMテンプレートを生成し、sam local invokeで検証してからフルリソースをデプロイする手順を試してみた。

はじめに

  • 普段Terraformを使っているが、SAMちょっと使ってみたところ、ローカルテスト周りの機能が充実してたので併用できないかと模索
  • デプロイはTerraformでやるものの、ローカルテストの段階でSAMを使って成功したらTerraformでデプロイができたら理想だな……というので思いついた苦肉の策。もっといい方法あるかもしれない

方針

  • TerraformでLambdaを定義すると同時に、local_fileリソースでSAMのテンプレートを生成させる
  • terraform applyでテスト前にLambdaがデプロイされてはあんまり美味しくないので、テスト用のSAMテンプレートだけ先にデプロイするようにする。このときリソースの依存関係に依存関係に注意する。
  • うまく依存関係を切り離してしまえば、terraform apply -target=local_file.sam_templateでSAMテンプレート生成だけapplyできる
  • あとはsam local invoke <function_name> --event <test_json>でテストできる

具体的にはコード見て

コード

  • SAMテンプレートをヒアドキュメントで作るという苦肉の策笑
  • CloudFormation→Terraformのツールは見つかったが、逆は見つからなかったので頭の悪いやり方になった。もしそれ用のライブラリがあれば、CIと組み合わせれば結構スマートにできそう。
  • Lambda関数と依存関係切り離すために、localsで設定値を持っておく
##############################
# Lambda 用実行ロール
##############################

resource "aws_iam_role" "lambda_role" {
  name = "lambda-execution-role"

  assume_role_policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "lambda.amazonaws.com"
        },
        "Action" : "sts:AssumeRole"
      }
    ]
  })
}

# Lambda 用基本実行ポリシー(CloudWatch Logs 出力用)
resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

##############################
# Lambda 関数作成
##############################

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = "${path.module}/.cache/lambda_function.zip"
}

# SAMテンプレートを先にapplyする関係でLambdaとの依存関係を切る
locals {
  lambda_functions = {
    AddLambda = {
      function_name = "AddLambda"
      handler       = "lambda_function.lambda_handler"
      runtime       = "python3.12"
      timeout       = 5
    }
  }
}


resource "aws_lambda_function" "add_function" {
  function_name    = local.lambda_functions["AddLambda"].function_name
  role             = aws_iam_role.lambda_role.arn
  handler          = local.lambda_functions["AddLambda"].handler
  runtime          = local.lambda_functions["AddLambda"].runtime
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  timeout          = local.lambda_functions["AddLambda"].timeout
}

##############################
# SAM用テンプレート 関数作成
##############################
resource "local_file" "sam_template" {
  content = <<-EOF
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  ${local.lambda_functions["AddLambda"].function_name}:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./         # Lambdaコードが存在するディレクトリを指定
      Handler: ${local.lambda_functions["AddLambda"].handler}
      Runtime: ${local.lambda_functions["AddLambda"].runtime}
      Timeout: ${local.lambda_functions["AddLambda"].timeout}
EOF

  filename = "${path.module}/template.yml"
}

lambda_function.py

足し算のLambda。入力のa+bを計算

import json

def lambda_handler(event, context):
    """
    eventに含まれる 'a' と 'b' の数値を足し合わせるLambda関数。
    event例: { "a": 10, "b": 20 }
    """
    try:
        # eventからaとbの値を取得
        a = event.get("a")
        b = event.get("b")

        # aまたはbが渡されていない場合は例外を発生させる
        if a is None or b is None:
            raise ValueError("入力に 'a' と 'b' の両方を指定してください。")

        # 値が文字列の場合も考慮して数値に変換(整数または浮動小数点)
        a = float(a)
        b = float(b)

        # 数値の足し算
        result = a + b

    except Exception as e:
        # エラーが発生した場合はエラーメッセージと共にHTTPステータスコード400を返す
        return {
            "statusCode": 400,
            "body": json.dumps({
                "error": str(e)
            })
        }

    # 正常終了時は計算結果を返す(HTTPステータスコード200)
    return {
        "statusCode": 200,
        "body": json.dumps({
            "result": result
        })
    }

テスト用JSON

テスト用JSONとして、tests/test_a.json(正常系)、tests/test_b.json(異常系)をおいておく。

{
    "a": 5,
    "b": 3
}
{
    "a": -1,
    "c": 10
}

試した結果

SAMテンプレートだけ出力する。警告ズラズラ出てくるがテンプレート生成の1リソースしかデプロイされない。

terraform apply -target=local_file.sam_template

Plan: 1 to add, 0 to change, 0 to destroy.

│ Warning: Resource targeting is in effect

│ You are creating a plan with the -target option, which means that the result of this plan may not represent all of
│ the changes requested by the current configuration.

│ The -target option is not for routine use, and is provided only for exceptional situations such as recovering from
│ errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.

SAMでテストを行う。Dockerが使える環境が必要で、WindowsならこれはWSL側でやったほうがいいかもしれない。

sam local invoke AddLambda --event tests/test_a.json

sam local invoke AddLambda --event tests/test_b.json

出力は以下のようになる。

# 正常系
$ sam local invoke AddLambda --event tests/test_a.json
No current session found, using default AWS::AccountId
Invoking lambda_function.lambda_handler (python3.12)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.12-rapid-x86_64.

Mounting .../SAMLocalTest as /var/task:ro,delegated, inside runtime container
START RequestId: 90b51c89-5905-4c9f-bb26-f7095264063c Version: $LATEST
END RequestId: 1d82badc-8985-40f2-96f4-da9ca726d4e6
REPORT RequestId: 1d82badc-8985-40f2-96f4-da9ca726d4e6  Init Duration: 0.03 ms  Duration: 172.31 ms     Billed Duration: 173 ms Memory Size: 128 MB     Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"result\": 8.0}"}

# 異常系
$ sam local invoke AddLambda --event tests/test_b.json
No current session found, using default AWS::AccountId
Invoking lambda_function.lambda_handler (python3.12)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.12-rapid-x86_64.

Mounting .../SAMLocalTest as /var/task:ro,delegated, inside runtime container
START RequestId: f4246c15-923c-4947-aaec-f7b7d2fd7409 Version: $LATEST
END RequestId: d618997f-61e9-4cf5-9f5c-73ca7084bd94
REPORT RequestId: d618997f-61e9-4cf5-9f5c-73ca7084bd94  Init Duration: 0.02 ms  Duration: 57.30 ms      Billed Duration: 58 ms  Memory Size: 128 MB     Max Memory Used: 128 MB
{"statusCode": 400, "body": "{\"error\": \"\\u5165\\u529b\\u306b 'a' \\u3068 'b' \\u306e\\u4e21\\u65b9\\u3092\\u6307\\u5b9a\\u3057\\u3066\\u304f\\u3060\\u3055\\u3044\\u3002\"}"}

テスト終わったらめでたく全体をデプロイ

terraform apply

公式側ではこんなツールもあるが

SAM×Terraformだったらこんなツールもあるらしい。ただSAMがメインっぽいんでちょっとユースケース違うかな。

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

local_execがいっぱい出てくるのが大丈夫かこれ感はある。

考察

まあだいぶキワモノ感あるけど、こういうこともできなくはないという話。SAMテンプレートが自動でできるようになったら結構いい感じになるかもしれない(要するにCloudFormationの自動生成ができればいい?)。

こういうのLLM得意な気がするけど、LLMが作ったSAMテンプレートが必ずしも動く保証がないというのが悩ましいところ。LLMのテンプレート生成がミスってるのか、テストがミスってるのか問題が起きちゃうので。



Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です