古いterraform でAWSLambdaをあげたときにはまった話

AWSsharp - High performance Node.js image processing を使ったNodeを動かすためにLambdaを使っていました。sharp部分をLayerにして実際のコードをLambda関数というごく当たり前の構成にしてました
それをNode.js18に上げようとしたのですが、たぶん Error expanding plan for AWS Lambda Provisioned Concurrency · Issue #14267 · hashicorp/terraform-provider-aws だと思われるバグに遭遇して、いったんはS3のサンプルにあるようにLayerを使わずにやろうと。サンプル:チュートリアル: Amazon S3 トリガーを使用してサムネイル画像を作成する - AWS Lambda

結論

ちゃんとバージョンアップをしてLayerを真面目に使うか、DockerをLambdaで動かせばいいのでは

はまりポイント: null_resource, archive_fileの扱いを熟知してなかった

最初package-lock.jsonとかコードを変えたらトリガーすればいいやん。と思って、こういうコードをを書いてたんです。
terraform planすると差分でるし、terraform applyするとうまくいくのでよかったよかった。
ただ、次の作業をしようとしたときに、CI(GitHub Actions)上で「あれ?差分がでるぞ?」となって調査。
自分の手元のPCでもplanすると差分がでる。 更になぜかラムダ関数のzipの中身にnode_modulesがない。

これは理解がたりてなかったんですが、 data.archive_filedepends_onnull_resource をしてるけど、triggerが走ってない(npm ci)が走ってない状態でzipファイルを作っていたんです。なので、そりゃぁ差分がでるよね。
これを調べている間にローカルPCでplan実行していたのですが、そもそも null_resource が動いている気配がない。これapplyのときしか local-exec がうごかいないんですね。よくよく考えたらplan時に副作用あったらこまるからそうか。
だから、巷で流れている 「null_resource ので○○しました」って、planが考慮されてない気がする。applyとplanがずれるなら何のためのplanやねん。
(まぁ実際にplanは成功してapplyして失敗することはあるんだけど)

resource "null_resource" "test" {
  triggers = {
    change = join("", [
      filebase64sha256("${path.root}/lambda/index.js"),
      filebase64sha256("${path.root}/lambda/package.json"),
      filebase64sha256("${path.root}/lambda/package-lock.json")
    ])
  }

  provisioner "local-exec" {
    working_dir = "./lambda"
    command     = "npm ci" 
  }
}

data "archive_file" "test" {
  type        = "zip"
  output_path = "function.zip"
  source_dir  = "${path.root}/lambda"
  depends_on  = [null_resource.test]
}

どう対応したの?

考え方としたら、aws_lambda_functionsource_code_hash をかえなければよい。つまり作成されるzipファイルのsha256をbase64エンコードした結果が同じであればよいです。
なので、 data "external" をつかって常にzipファイルを生成するようにしました。ただ、これだと毎回zipファイルの結果が異なります。
なので、その中のシェルでzipファイルの中身も全て同じ時刻にしました(この時点でもうダメ過ぎる)。 もちろんファイルの中身が変わればzipファイルの中身もかわるのでそれにも対応してる。 という、ワークアラウンドでなんとかしようかなぁと思います

#!/bin/sh

# 結果のjqだけほしいので全部 /dev/null 2>&1にしている
set -eu
export NODE_ENV=production
timestamp='2023-11-01T00:00:00'

# install module
npm ci > /dev/null 2>&1
rm -rf node_modules/sharp
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm ci --arch=x64 --platform=linux --libc=glibc sharp > /dev/null 2>&1

# function.zipを何回生成されても同じファイルになるようにtimestampを統一する
# find node_modules -type f -print | xargs -I{} touch -d "${timestamp}" {}
# find node_modules -type d -print | xargs -I{} touch -d "${timestamp}" {}
find node_modules \( -type f -o -type d \) -print0 | xargs -0 -I{} touch -d "${TIMESTAMP}" {}
touch -d "${timestamp}" index.js

zip -r -X function.zip index.js node_modules > /dev/null 2>&1
touch -d "${timestamp}" function.zip
FILE_SHA256=$(openssl dgst -binary -sha256 < function.zip | openssl base64)
mv function.zip ..
jq -n --arg code_hash "$FILE_SHA256" '{"code_hash":$code_hash}'
data "external" "make" {
  working_dir = "./lambda"
  program = ["sh", "make.sh"]
}

resource "aws_lambda_function" "test" {
  function_name    = "test"
  role             = aws_iam_role.lambda.arn
  filename         = "function.zip"
  handler          = "index.handler"
  runtime          = "nodejs18.x"
  source_code_hash = data.external.make.result.code_hash # ここを変えたくない
}

追記

find を2回かいてるのださいなーと思ってMan page of FIND見直してたら -o でor検索できるのか。みんな思うところは同じなんだな