はじめに
Lambda Layerを何度か作成しているのですが、
毎回zipのディレクトリ構造を間違えて「Cannot find module XXX」エラーに遭遇します。
ディレクトリ構造を間違えないよう、Terraformで良い感じに工夫してみました。
(参考)Lambda Layerとは
(引用)補助的なコードやデータを含む .zip ファイルアーカイブです。
複数のlambdaで共有したい関数や、
lambdaのデプロイパッケージに入れたくない依存ライブラリなどを別途用意できる機能です。
別途用意されたzipは、Lambda実行環境でlambda関数実行前に解凍されます。
lambda関数は解凍後の関数ソースやライブラリを使用することができます。
(参考)Lambda Layer作成時の落とし穴
Lambda Layerの作成時にZIPファイルをアップロードできますが、
ZIPファイル内のディレクトリ構造が正しくないと、Lambda関数は解凍後の関数ソースやライブラリを利用できません。
nodejsの場合、node_modulesを含むzipファイルは「XXX.zip/nodejs/node_modules/...」の形式をとる必要があります。
毎回これを忘れてしまう・・・
このように、Lambda Layer作成時は、zipファイルの構造を気にする必要があります。
以下に言語ごとのディレクトリ指定がまとめられています
TerraformでLambda Layer作成時にディレクトリ構造をチェックする
コードに書けば忘れないはずなので、Terraformに毎回validationしてもらおうと思います。
コード紹介
以下のようなmoduleを作成してみました。
.
└── lambda_layer
├── input.tf
├── layer.tf
└── output.tf
input.tf
######################################
## terraform.tfvarsから変数取得
######################################
locals {
required_path_pattern = "/nodejs/node_modules/"
}
variable "layer_name" {}
variable "dir_path" {
type = string
description = "Path of directory to be zip"
validation {
condition = (length(fileset(var.dir_path, "/nodejs/node_modules/**/*")) > 0 ) || (length(fileset(var.dir_path, "/lib/**/*.js")) > 0 )
error_message = "The directory path must contain a directory matching the pattern /nodejs/node_modules/**/* or /lib/**/*.js."
}
}
variable "runtimes" {
type = list(string)
}
layer.tf
# zipを作成
data "archive_file" "main" {
type = "zip"
output_file_mode = "0666"
source_dir = "${var.dir_path}"
output_path = "${var.dir_path}.zip"
}
resource "aws_lambda_layer_version" "main" {
filename = data.archive_file.main.output_path
layer_name = var.layer_name
source_code_hash = filebase64sha256(data.archive_file.main.output_path)
compatible_runtimes = var.runtimes
}
output.tf
output "layer_name" {
value = aws_lambda_layer_version.main.layer_name
}
output "arn" {
value = aws_lambda_layer_version.main.arn
}
コード解説
ポイントはinput.tfのvalidation部分です。
variable "dir_path" {
type = string
description = "Path of directory to be zip"
validation {
condition = (length(fileset(var.dir_path, "/nodejs/node_modules/**/*")) > 0 ) || (length(fileset(var.dir_path, "/lib/**/*.js")) > 0 )
error_message = "The directory path must contain a directory matching the pattern /nodejs/node_modules/**/* or /lib/**/*.js."
}
}
fileset関数を使用し、zip化しようとしているディレクトリの構造をチェックしています。
(length(fileset(var.dir_path, "/nodejs/node_modules/**/*")) > 0 )
部分
ライブラリをlayer化したい場合は以下の構造である必要があります。
.
└── axios.zip
└── nodejs
└── node_modules
├── axios/
├── ~ 略 ~
├── proxy-from-env/
└──.package-lock.json
${var.dir_path}/nodejs/node_modules/**/*
上記ディレクトリにファイルが存在しない場合、validation errorになります。
(length(fileset(var.dir_path, "/lib/**/*.js")) > 0 )
部分
関数をlayer化したい場合は以下の構造である必要があります。(厳密には違うのですが、シンプルさのためにその整理で・・・)
.
└── function.zip
└── lib
└── func.js
${var.dir_path}/lib/**/*.js
上記ディレクトリにファイルが存在しない場合、validation errorになります。
モジュールの呼び方
モジュールブロックで呼び出します。
module "function_layer" {
source = "./modules/lambda_layer"
layer_name = "function_layer"
dir_path = "../libs/functions"
runtimes = ["nodejs20.x"]
}
Lambda Layerに適していない形式のフォルダをLambda Layer化しようとすると、planまたはapply時に以下のエラーが発生します。
$ terraform apply
╷
│ Error: Invalid value for variable
│
│ on main.tf line 41, in module "function_layer":
│ 41: dir_path = "../../spike/function"
│ ├────────────────
│ │ var.dir_path is "../../spike/function"
│
│ The directory path must contain a directory matching the
│ pattern /nodejs/node_modules/**/* or /lib/*.js.
│
│ This was checked by the validation rule at
│ modules/lambda_layer/input.tf:11,3-13.
╵
lambda関数でのlayerの参照方法
// lib/functions.jsを参照する場合
const hello = require('/opt/lib/functions');
// node_modulesへはパスが通っている
const axios = require('axios');
exports.handler = async (event, context) => {
/*
何らかの処理
*/
};
おわりに
Lambda Layerは便利な機能なのですが、毎回zip化のところでストレスを感じていました。
Terraformのvalidation設定をlintのような扱いで用いることで、多少は軽減できてうれしいです。