🛼

TerraformとlambrollでGoのLambdaをデプロイする

に公開3

背景

(HCP) Terraform を使ってLambdaをデプロイするには、zipを何かしらの方法で作る必要がある。
色んな記事を当たると、Provisioners の "local_exec" を使う方法が多く見つかり、それを使えば確かにterraform apployコマンドだけでbuildとzip化、Lambdaのデプロイができます。

しかし、Terraformの公式には、

Important: Use provisioners as a last resort. There are better alternatives for most situations. Refer to Declaring Provisioners for more details.

と書かれていて、provisionersは最終手段で大抵の場合もっと良い方法がある。(あまり使わない方がいい?)とされています。
https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec

また、HCP Terraform ではランタイム環境に自力で npm などのbuild(コンパイル)に必要な環境をインストールする方法もあるにはありそうなのですが力技感があります。

参考:
https://zenn.dev/progate/articles/56b62bef96a8d2

そこで、

  • Lambdaの定義はTerraformでする
  • Lambdaのデプロイはlambrollでする

という役割に分ける方法を試したいと思いました。lambrollはGitHub Actionsも用意されているのでGitHub Actionsとの相性もいいです。

lambrollとは?という内容はこちらを参照ください。
https://zenn.dev/fujiwara/articles/fujiwara-ware-2024-lambroll

ざっくりというとecspressoのLambda版です。

やってみる

Terraformの定義

大枠はClaudeに作ってもらったのですが、大事なこととすれば、null_resource に依存する形でzipファイルを生成しているところです。こうすることで Go のコードがない状態でも terraform apply に成功することができ、Lambdaの作成ができます。(もちろん動作するとエラーになります)

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_iam_role" "lambda_role" {
  name = "go-lambda-function-role"

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

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_lambda_function" "function" {
  lifecycle {
    ignore_changes = [
      filename,
      source_code_hash,
    ]
  }
  function_name    = "go-lambda-function"
  filename         = data.archive_file.dummy.output_path
  role             = aws_iam_role.lambda_role.arn
  handler          = "bootstrap"  # Go AL2ランタイムではハンドラーは使用されませんが必要
  runtime          = "provided.al2"  # Go AL2用のカスタムランタイム
  timeout          = 30
  environment {
    variables = {
      EXAMPLE_VAR = "example_value"
    }
  }
}

data "archive_file" "dummy" {
  type        = "zip"
  output_path = "${path.module}/dummy.zip"
  source {
    content  = "dummy"
    filename = "bootstrap"
  }
  depends_on = [
    null_resource.main
  ]
}

resource "null_resource" "main" {}

デプロイするGoのコードを準備

今回の本題ではないので解説等はないです。

package main

import (
        "context"

        "github.com/aws/aws-lambda-go/lambda"
)

type MyEvent struct {
        Name string `json:"name"`
}

type MyResponse struct {
        Message string `json:"message"`
}

func HandleRequest(ctx context.Context, event MyEvent) (MyResponse, error) {
        return MyResponse{
                Message: "Hello, " + event.Name,
        }, nil
}

func main() {
        lambda.Start(HandleRequest)
}

lambroll によるデプロイ準備

install:

$ brew install fujiwara/tap/lambroll

Lambdaの読み込み(事前にAWS CLIが使えるようにセットアップ):

$ lambroll init --function-name go-lambda-function --region=ap-northeast-1

function.jsonが作成される

 % cat function.json
{
  "Architectures": [
    "arm64"
  ],
  "Environment": {
    "Variables": {
      "EXAMPLE_VAR": "example_value"
    }
  },
  "EphemeralStorage": {
    "Size": 512
  },
  "FunctionName": "go-lambda-function",
  "Handler": "bootstrap",
  "LoggingConfig": {
    "LogFormat": "Text",
    "LogGroup": "/aws/lambda/go-lambda-function"
  },
  "MemorySize": 128,
  "Role": "arn:aws:iam::111111111111:role/go-lambda-function-role",
  "Runtime": "provided.al2",
  "SnapStart": {
    "ApplyOn": "None"
  },
  "Timeout": 30,
  "TracingConfig": {
    "Mode": "PassThrough"
  }
}

これでデプロイする準備が整いました。

lambrollによるデプロイ

goのbuild:
bootstrap という名前の成果物にする必要があります。(handlerの定義と合わせる)

GOOS=linux GOARCH=arm64 go build -o bootstrap .

lambroll deploy:

% lambroll deploy  --region=ap-northeast-1
2025/04/17 09:09:43 [info] lambroll v1.2.2
2025/04/17 09:09:43 [info] loading Function from function.json
2025/04/17 09:09:43 [info] starting deploy function go-lambda-function
2025/04/17 09:09:43 [info] creating zip archive from .
2025/04/17 09:09:44 [info] zip archive wrote 5583207 bytes
2025/04/17 09:09:44 [info] updating function configuration
2025/04/17 09:09:44 [info] updating function configuration ...
2025/04/17 09:09:44 [info] State:Active LastUpdateStatus:Successful
2025/04/17 09:09:44 [info] updating function configuration accepted. waiting for LastUpdateStatus to be successful.
2025/04/17 09:09:44 [info] State:Active LastUpdateStatus:InProgress
2025/04/17 09:09:44 [info] waiting for LastUpdateStatus Successful
2025/04/17 09:09:45 [info] State:Active LastUpdateStatus:Successful
2025/04/17 09:09:45 [info] updating function configuration successfully
2025/04/17 09:09:45 [info] updating function code ...
2025/04/17 09:09:45 [info] State:Active LastUpdateStatus:Successful
2025/04/17 09:09:47 [info] updating function code accepted. waiting for LastUpdateStatus to be successful.
2025/04/17 09:09:47 [info] State:Active LastUpdateStatus:InProgress
2025/04/17 09:09:47 [info] waiting for LastUpdateStatus Successful
2025/04/17 09:09:48 [info] State:Active LastUpdateStatus:InProgress
2025/04/17 09:09:48 [info] waiting for LastUpdateStatus Successful
2025/04/17 09:09:50 [info] State:Active LastUpdateStatus:InProgress
2025/04/17 09:09:50 [info] waiting for LastUpdateStatus Successful
2025/04/17 09:09:54 [info] State:Active LastUpdateStatus:Successful
2025/04/17 09:09:54 [info] updating function code successfully
2025/04/17 09:09:54 [info] deployed version 9
2025/04/17 09:09:54 [info] updating alias set current to version 9
2025/04/17 09:09:54 [info] alias updated

以上で、Lambdaのデプロイをすることができました。

疑問点

--src="." とすればbuildも一緒にしてくれるかと思ったがうまく動作しなかった

lambroll deploy --src="." --region=ap-northeast-1

を実行するとbootstrapファイルがない状態でもdeployコマンドに成功しました。
しかし、Lambdaを実行するとEntryPointがない(bootstrapがない)とエラーになってしまいました。そこで、事前にbuildする方法でやってみたらうまくいったのですが、 go build コマンドを叩かなくても lambroll deploy だけでデプロイできるのか知っている方いたら教えて欲しいです。

実際の運用時の考慮点

環境変数やTimeout値をTerraform側かLambroll側のどちらでするか決める必要がある

例えば、TerraformでTimeout値を30秒に変更しても、function.jsonにTimeout値が3秒と書かれていてそのままデプロイしたら3秒に書き変わってしまいます。

Terraform側で管理する場合:

方法1:--ignoreをつけることでfunctions.json側の設定をignoreすることができます。

lambroll deploy --region=ap-northeast-1 --ignore=".Timeout, .Environment"

方法2: lambroll deploy 前に毎回 lambroll init して読み込む

--force-overwriteオプションがある
https://github.com/fujiwara/lambroll/issues/376

lambroll側で管理する場合:

function.jsonに環境変数やTimeout値を定義します。
方法はシンプルなのですが、dev,stg,prdなど複数の環境にデプロイする場合は、環境変数や設定を変えたいケースもあると思います。

方法1:function.jsonをfunction-dev.jsonなど環境ごとに分割する
方法2:.envファイルを作成し、 --envfile=ENVFILE で読み込む

などで対応するのがいいかと思います。

参考

調べると多くの記事があるのですが、個人的にはこの記事が参考になりました。
https://techblog.szksh.cloud/create-empty-lambda-by-terraform/

まとめ

TerraformとlambrollでGoのLambdaをデプロイする方法を紹介してみました。
CDKだとNodejsFunctionのようにコンパイル+zip化+デプロイを内包して実行してくれるモジュールもありますが、(HCP) Terraformだと実行環境にnodeやgoがなかったりするので仕組みを考える必要があります。

lambrollのやっていることはシンプルなので多くの場合はzipコマンドとaws cliで代替できると思います。ですが、個人的にはcliの呼び出しを増やし過ぎたくないのと、aliasやfunction-urlなどlambdaの細かい機能を使うシーンにも対応できるlambrollは良いツールだなと思いました。

以上、この記事が誰かの役に立つと幸いです。

Discussion

fujiwarafujiwara

記事ありがとうございます!(lambrollの作者です)

go build コマンドを叩かなくても lambroll deploy だけでデプロイできるのか知っている方いたら教えて欲しいです

lambrollの--srcはzipを作る時のディレクトリを指定するもので、go(に限らず)のビルドなどは行いません。zipに入れるファイルのビルド方法は言語や環境によってまちまちなので、そこにはlambrollは関心を持たないようにしています

tkg216tkg216

ありがとうございます!!
確かにそこをやってしまうとツールのコンセプトから外れる感じがしますね

Terraformでlambdaの設定は全て管理する場合、--ignoreでパラメータを指定するか、initで設定を読み込む方法があるのかなと思ったのですが、functionだけをデプロイするオプションがあるといいなと思いました(issueとしてあげたほうが良いかもしれないですね)

fujiwarafujiwara

functionだけをデプロイするオプション

API的にはUpdateFunctionConfigurationをskipしてUpdateFunctionCodeを実行したい、ということですよね。比較的容易にできそうな気はするのでちょっと見てみますね。
issueいただけると嬉しいです!