lambdazip providerでnode_moduleを含むzipをLambdaにデプロイする

terraformでaws_lambda_functionarchive_fileを使ってLambdaをデプロイする方法がある。

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "lambda.js"
  output_path = "lambda_function_payload.zip"
}

resource "aws_lambda_function" "test_lambda" {
  filename         = data.archive_file.data.archive_file.nyan.output_path.output_path
  function_name    = "lambda_function_name"
  role             = aws_iam_role.iam_for_lambda.arn
  handler          = "index.handler"
  source_code_hash = data.archive_file.lambda.output_base64sha256
  runtime          = "nodejs18.x"
}

しかし、node_modulesと相性が悪くて、npm iの実行タイミングが難しかったり、terraformのCIで毎回差分が出たりする。

リソースの方のarchive_filenull_resourceを組み合わせれば出来るかもしれないが、archive_file (Resource)はすでに非推奨で、やり方を考えるのにも手間がかかる。

なのでzipファイルのハッシュ値はtfstateに保持しつつ、ソースコードやpackage.jsonが変わったらnpm iとzipファイルの再作成を行うterraform providerを作った。

github.com

使い方

以下のようなファイル構成だったとして

./
|-- lambda/
|   |-- index.js
|   |-- node_modules/
|   |-- package-lock.json
|   `-- package.json
`-- main.tf

main.tfは次のようになる。

terraform {
  required_providers {
    lambdazip = {
      source  = "winebarrel/lambdazip"
      version = ">= 0.5.0"
    }
  }
}

data "lambdazip_files_sha256" "triggers" {
  files = ["lambda/*.js", "lambda/*.json"]
}

resource "lambdazip_file" "app" {
  base_dir      = "lambda"
  sources       = ["**"]
  excludes      = [".env"]
  output        = "lambda.zip"
  before_create = "npm i"
  triggers      = data.lambdazip_files_sha256.triggers.map
}

resource "aws_lambda_function" "app" {
  filename         = lambdazip_file.app.output
  function_name    = "my_func"
  role             = aws_iam_role.lambda_app_role.arn
  handler          = "index.handler"
  source_code_hash = lambdazip_file.app.base64sha256
  runtime          = "nodejs20.x"
}

resource "aws_iam_role" "lambda_app_role" {
  name = "lambda-app-role"

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

resource "aws_iam_role_policy_attachment" "lambda_app_role" {
  role       = aws_iam_role.lambda_app_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
  • lambdazip_file.app作成時にはnpm iが実行されてからzipファイルが作成される
  • その後はtriggers属性に含まれるファイルを変更した場合にzipファイルが再作成されLamadaがデプロイされる
    • node_modulesやlambda.zipを削除しても、base64sha256属性は変わらないのデプロイはされない
  • CIでterraformを実行する場合も、node_modulesの有無にかかわらず不必要な差分がでることはない

おまけ: Goのデプロイ

GoをLambdaにデプロイする場合は、archive_fileとnull_resourceだけで出来るかもしれないが、lambdazipを使うとよりシンプルにかけると思う。

./
|-- lambda/
|   |-- go.mod
|   |-- go.sum
|   `-- main.go
`-- main.tf
data "lambdazip_files_sha256" "triggers" {
  files = ["lambda/*.go", "lambda/go.mod", "lambda/go.sum"]
}

resource "lambdazip_file" "app" {
  base_dir      = "lambda"
  sources       = ["bootstrap"]
  output        = "lambda.zip"
  before_create = "GOOS=linux GOARCH=amd64 go build -o bootstrap main.go"
  triggers      = data.lambdazip_files_sha256.triggers.map
}

resource "aws_lambda_function" "app" {
  filename         = lambdazip_file.app.output
  function_name    = "my_func"
  role             = aws_iam_role.lambda_app_role.arn
  handler          = "my-handler"
  source_code_hash = lambdazip_file.app.base64sha256
  runtime          = "provided.al2023"
}