🐪

Terraform で AWS Lambda をデプロイしようとする際にぶつかる現実

に公開1

はじめに

AWSリソースを管理する IaC ツールの中でも高いシェア率を誇っている Terraform で Lambda 関数をデプロイをしようとすると、sls deploycdk deploy などと同じメンタルモデルで「 terraform apply すれば OK! 」というわけにはいかない という気付きがあり、その内容について書き残しておこうと思いました。

Lambda のデプロイに必要なことをおさらい

ここでは、TypeScript(実行環境=Nodejs)、Go(実行環境=Amazon Linux) などのビルドが必要なプログラミング言語を前提として書いています。(Python などでも依存モジュールのダウンロードなどが必要なのでおおよそ変わらないはずです)

そもそも、Lambda をデプロイする方法としては、

  • (ZIP形式)ZIP化してアップロードする
  • (ZIP形式)S3にアップロードして経由でアップロードする
  • (コンテナ形式)コンテナイメージを作成して ECR などのコンテナレジストリを使ってデプロイする
    の大きく3パターンあります。

それぞれの方法の特徴は、S3 を経由することで 50 MB を超えるZIPファイルをアップロードすることができ、コンテナイメージを使うと圧縮前 250 MB というサイズ制限に引っ掛からなくなることや コンテナをそのまま動かせるメリットなどがあります。

ここでは最も一般的であろう ZIP化してアップロードしてデプロイ方式 についてのみを対象とします。

ZIP化してアップロードしてデプロイ方式 は以下のステップが必要です。

  1. ビルドする
  2. ビルド成果物をZIP化する
  3. ZIPファイルをアップロードする( aws lambda update-function-code --function-name funcA --zip-file fileb://lambda.zip 相当)

Terraform でも terraform apply だけで Lambda の更新をしたい!

そもそも Terraform は Lambda デプロイの何をしてくれるか?

Terraform の aws_lambda_function リソースでは主に以下のことをすることができます

  • ZIPファイル を入力として Lambda 関数を作成・更新する
    • aws lambda update-function-code 相当
  • Lambda 関数の設定(メモリ、タイムアウト、環境変数、IAMロールなど)を管理する
    • aws lambda update-function-configuration 相当

公式ドキュメント にあるサンプルのように ZIPファイルを指定する必要があります。

data "archive_file" "example" {
  type        = "zip"
  source_file = "${path.module}/lambda/index.js"
  output_path = "${path.module}/lambda/function.zip"
}

# Lambda function
resource "aws_lambda_function" "example" {
  filename         = data.archive_file.example.output_path
  function_name    = "example_lambda_function"
  role             = aws_iam_role.example.arn
  handler          = "index.handler"
  source_code_hash = data.archive_file.example.output_base64sha256

  runtime = "nodejs20.x"

  environment {
    variables = {
      ENVIRONMENT = "production"
      LOG_LEVEL   = "info"
    }
  }

  tags = {
    Environment = "production"
    Application = "example"
  }
}

つまり、ビルドとZIPファイル化については Terraform の外で実行する必要があります。

local-execという選択肢

Terraform だけで Lambda をデプロイする方法はないかと探していると、 local-exec を使う方法が見つかりました。詳細は以下の記事に書かれています。

  provisioner "local-exec" {
    command = "GOARCH=amd64 GOOS=linux go build -o ${local.golang_binary_local_path} ${local.golang_codedir_local_path}/*.go"
  }
  provisioner "local-exec" {
    command = "zip -j ${local.golang_zip_local_path} ${local.golang_binary_local_path}"
  }

のように、 local-exec を定義することで terraform apply コマンドにビルドやZIP化を内包することができます。

しかし、Terraform のドキュメントには、

と書かれており、 local-exec の利用は一時的な回避策とすべきであるようです。

HCP Terraform(Terraform Cloud) 上での選択肢

local-exec はローカル環境の node や go の実行環境を使う方法手段になりますが、クラウド上で terraform apply 等を行う HCP Terraform では少し勝手が違ってきます。

以下のドキュメントのように npm install する方法の紹介例などはあるので HCP Terraform 上でもビルド&ZIP化をする方法はあるかもしれません。しかし、この方法も開発時の回避策として紹介されているもので、このアプローチはエラーが起きやすいため、しばしば推奨されないとあります。HCP Terrraform 自体が Node.js や Go言語の実行環境を提供しているわけではないので相性が悪く思います。

結論: Terraform では、 terraform apply だけで Lambda 関数を継続的に更新していくことは難しい

では、どうするのか?

1つの解決方針としては、Terraform で Lambda の定義を管理し、Lambda関数の更新は別の方法で実現するという方針です。

具体的にどういうステップになるかというと以下です。

  1. terraform apply でひとまず Lambda をデプロイしておく(dummyもしくは最小のコード)
  2. GitHub Actions などの CI/CD 環境でビルドする
  3. GitHub Actions などの CI/CD 環境でビルドしたものをZIP化する
  4. GitHub Actions などの CI/CD 環境でZIPファイルを使って Lambda 関数を更新する

Lambda 関数の更新をする手段の選択肢としては、

  • AWS CLI
  • lambroll
  • aws-actions/aws-lambda-deploy (GitHub Actions)
    などがあります。 Lambda 関数の更新だけに限定するか、 Lambda の設定更新も行うかは自由度があります。lambroll については、少し記事を書いたことがあるので参考にしてください。

aws-actions/aws-lambda-deploy は、2025/08 に AWS がリリースした Lambda 関数デプロイ用のGitHub Action です。このモジュールがリリースした背景にはこの記事で取り上げている課題を解決しようとするものがあるのかもしれません。

各ステップの詳細なイメージ

最初にデプロイする terraform のコードは以下のようなものになります。

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" {}

ここでは、dummyのZIPファイルを作成していますが、ビルドした成果物を用意しておいても構いません。GitHub Actions の定義は以下のようなものになります。

    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Setup Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.25'
        
    - name: Install dependencies
      run: |
        go mod download
        
    - name: Build Go application for Lambda
      run: |
        GOOS=linux GOARCH=arm64 go build -o bootstrap main.go
        
    - name: Create deployment package
      run: |
        zip lambda-function.zip bootstrap
        
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ""
        role-session-name: ""
        aws-region: ""
        
    - name: Update Lambda function
      run: |
        aws lambda update-function-code \
          --function-name my-lambda-function \
          --zip-file fileb://lambda-function.zip

この構成の良さ

Lambda の設定などの定義の管理と Lambda コードの管理が別々になるため、ライフサイクルがそれぞれ独立したものになります。Lambda の設定を更新したい頻度と Lambda の本体のソースコードを変更したい頻度は異なることが多いと思うため、 Lambda 自体の定義と Lambda のコード変更タイミングでデプロイできるため責務を明確に分けたい場合はメリットになる構成だと思います。

Lambda ではない別のサービスの例だと、 ECS のクラスタの定義と ECS サービス(タスク)の更新を分離するために Terraform と ecspresso を組み合わせて使うというよく見られる構成と捉えることもできるかと思います。

しかし、サーバーレス、Function as a Service としての Lambda というサービスの特徴やちょっとした処理を Lambda に任せたいシーンなどで考えるとやや too much というか煩雑だなあと個人的には感じました。

まとめ

このように、Terraform で Lambda 関数を継続的にデプロイしていく仕組みを作ろうとすると、 terraform apply だけでは完結せず、 Terraform による AWS リソースの管理と Lambda 関数デプロイに責務を分ける必要が出てきます。

もし、そんなことはせず、 Terraform × Lambda でももっと簡単にデプロイできるいい方法を知っている方がいたら教えてください。

以上、Terraform で AWS Lambda を扱う際にぶつかる現実について紹介しました。どなたかのお役に立てれば幸いです。

Discussion