もう二度とlambda layer作成をミスりたくない！！【terraform】

# はじめに
Lambda Layerを何度か作成しているのですが、
毎回zipのディレクトリ構造を間違えて「Cannot find module XXX」エラーに遭遇します。

ディレクトリ構造を間違えないよう、Terraformで良い感じに工夫してみました。

## （参考）Lambda Layerとは
（引用）補助的なコードやデータを含む .zip ファイルアーカイブです。
複数のlambdaで共有したい関数や、
lambdaのデプロイパッケージに入れたくない依存ライブラリなどを別途用意できる機能です。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/chapter-layers.html

別途用意されたzipは、Lambda実行環境でlambda関数実行前に解凍されます。
lambda関数は解凍後の関数ソースやライブラリを使用することができます。

## （参考）Lambda Layer作成時の落とし穴
Lambda Layerの作成時にZIPファイルをアップロードできますが、
ZIPファイル内のディレクトリ構造が正しくないと、Lambda関数は解凍後の関数ソースやライブラリを利用できません。

nodejsの場合、node_modulesを含むzipファイルは「XXX.zip/nodejs/node_modules/...」の形式をとる必要があります。
毎回これを忘れてしまう・・・

このように、Lambda Layer作成時は、zipファイルの構造を気にする必要があります。
以下に言語ごとのディレクトリ指定がまとめられています

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/packaging-layers.html#packaging-layers-paths

## TerraformでLambda Layer作成時にディレクトリ構造をチェックする
コードに書けば忘れないはずなので、Terraformに毎回validationしてもらおうと思います。
### コード紹介
以下のようなmoduleを作成してみました。
```
.
└── lambda_layer
    ├── input.tf
    ├── layer.tf
    └── output.tf
```
<details><summary>input.tf</summary><div>

```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)
}

```
</div></details>


<details><summary>layer.tf</summary><div>

```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
}
```
</div></details>


<details><summary>output.tf</summary><div>

```output.tf
output "layer_name" {
  value = aws_lambda_layer_version.main.layer_name
}

output "arn" {
  value = aws_lambda_layer_version.main.arn
}
```
</div></details>

### コード解説
ポイントはinput.tfのvalidation部分です。
```input.tf
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化しようとしているディレクトリの構造をチェックしています。

https://developer.hashicorp.com/terraform/language/functions/fileset

#### ```(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になります。

### モジュールの呼び方
モジュールブロックで呼び出します。
```main.tf
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の参照方法
```index.js
// 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のような扱いで用いることで、多少は軽減できてうれしいです。