👾
Terraform x Lambda (TypeScript) のおれおれディレクトリ構成
何の記事?
Terraform が大好きで仕方ないのですが、Lambdaと組み合わせる際に毎回ディレクトリ構成を試行錯誤してました。
あくまで「こういうパターンもあるよね」で誰かの参考になればと思います。
ディレクトリ構成
ESbuild を使ってTypeScriptをトランスパイル & バンドルします。
- lambdaフォルダはバンドルまでが責務
- awsフォルダ内でバンドルした資材の移動とデプロイ
-
aws/module/lambda
内のdata archive_file
を使ってバンドル結果を取得する
-
📁ROOT
├─ 📁lambda : Node.js のプロジェクト
│ ├─ 📁functions
│ │ └─ 📁<関数名> : Lambdaにデプロイする関数ごとのフォルダ
│ │ ├─ 📁bundle : ESBuildのバンドル結果置場
│ │ ├─ 📁src : ソースコード
│ │ │ └─ main.ts: Lambdaの本処理。modulesのインポートと処理の実装。
│ │ └─ index.ts : Lambdaのハンドラ。main.tsをインポートするだけ
│ ├─ 📁modules : 関数間で共有するユーティリティ
│ └─ package.json
├─ 📁aws : AWSプロバイダ向けTerraformのプロジェクト
│ ├─ 📁modules : AWSサービスごとにモジュール化
│ │ └─ 📁lambda
│ │ └─ main.tf
│ ├─ main.tf : モジュールを参照する
│ └─ provider.tf
└─ .env
Makefile
make init
で my-sample
関数向けディレクトリ構成を作成する
.PHONY: init clean
ROOT_DIR=$(shell pwd)
LAMBDA_DIR=lambda
FUNCTION_NAME=my-sample
init:
# .env ファイル
touch $(ROOT_DIR)/.env
# Lambda 関連
mkdir -p $(ROOT_DIR)/$(LAMBDA_DIR)/functions/$(FUNCTION_NAME)/bundle
mkdir -p $(ROOT_DIR)/$(LAMBDA_DIR)/functions/$(FUNCTION_NAME)/src
touch $(ROOT_DIR)/$(LAMBDA_DIR)/functions/$(FUNCTION_NAME)/src/main.ts
touch $(ROOT_DIR)/$(LAMBDA_DIR)/functions/$(FUNCTION_NAME)/index.ts
mkdir -p $(ROOT_DIR)/$(LAMBDA_DIR)/modules
cd $(LAMBDA_DIR) && npm init -y
npm install --save-dev typescript ts-node @types/node esbuild fs
npx tsc --init
# terraform関連
mkdir -p $(ROOT_DIR)/aws/modules/cloud-watch
mkdir -p $(ROOT_DIR)/aws/modules/lambda-role
mkdir -p $(ROOT_DIR)/aws/modules/lambda
touch $(ROOT_DIR)/aws/modules/lambda/variable.tf
touch $(ROOT_DIR)/aws/modules/lambda/main.tf
touch $(ROOT_DIR)/aws/modules/lambda/output.tf
touch $(ROOT_DIR)/aws/main.tf
touch $(ROOT_DIR)/aws/provider.tf
clean:
rm -rf $(ROOT_DIR)/$(LAMBDA_DIR) $(ROOT_DIR)/aws $(ROOT_DIR)/.env
lambdaフォルダ
my-sample 関数を例として作成します。
index.ts
Lambda のハンドラ向けの抽象レイヤ。
ESBuild のエントリーポイントとして扱う。
main に実際の処理を実装し、ここでは何もしない。
/lambda/functions/my-sample/index.ts
import { main } from "./src/main";
export const handler = (event: any, context: any): Promise<any> => {
console.log("event: %j", event);
const result = main(event, context);
console.log("result: %j", result);
return result;
};
build.mjs
npm run build 関数名
で実行するファイル。
npm run build my-sample
→ /lambda/functions/my-sample/index.js を生成する
"scripts": {
"build": "node ./build.mjs"
},
/lambda/build.mjs
import fs from "fs";
import esbuild from "esbuild";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const __filename = fileURLToPath(import.meta.url);
export const __dirname = path.dirname(__filename);
(async (dirname) => {
console.log(dirname);
const srcDir = path.resolve(__dirname, `./functions/${dirname}`);
const distDir = path.resolve(__dirname, `./functions/${dirname}/bundle`);
const entryPoint = path.resolve(srcDir, "index.ts");
const tsconfig = path.resolve(__dirname, "./tsconfig.json");
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
console.log("Build: ", entryPoint);
// ESBuild でビルドする
const res = await esbuild.build({
entryPoints: [entryPoint],
bundle: true,
minify: true,
platform: "node",
// target: "node22", 必要に応じてver指定
outfile: path.resolve(distDir, "index.js"),
format: "cjs",
});
console.log("%o", res);
return distDir;
})(process.argv.slice(2));
Terraform フォルダ
main.tf
/aws/main.tf
locals {
function_name = "my-sample"
}
# Policy は別途作成してください
resource "aws_iam_role" "function_my_sample" {
path = "/${local.function_name}/"
name = "${local.function_name}_lambda-role-${local.function_name}"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
# Lambdaの共通モジュールに関数名とソースコードを渡す
module "function_my_sample" {
source = "./modules/lambda"
function_name = local.function_name
function_dir = "../lambda/functions/${local.function_name}"
lambda_role_arn = aws_iam_role.function_my_sample.arn
}
module/lambdaフォルダ
適宜分割してください
/aws/module/lambda/main.tf
variable "function_name" {
type = string
}
variable "function_dir" {
type = string
validation {
condition = fileexists("${var.function_dir}/bundle/index.js")
error_message = "Directory not found. Before Applying, Run 'npm bundle ${var.function_dir}'."
}
}
variable "lambda_role_arn" {
type = string
}
# バンドルのZip化
# --------------------------
data "archive_file" "package" {
type = "zip"
source_file = "${var.function_dir}/bundle/index.js"
output_path = "${var.function_dir}/bundle/package.zip"
}
# lambda function 本体
# --------------------------
resource "aws_lambda_function" "function" {
function_name = "${var.function_name}"
filename = data.archive_file.package.output_path
runtime = "nodejs20.x"
memory_size = 512
timeout = 60
role = var.lambda_role_arn
handler = "index.handler"
publish = true
# Zipを連係する
source_code_hash = data.archive_file.package.output_base64sha256
tags = {
Name = "${var.function_name}"
generated_by = "terraform"
}
}
output "function_arn" {
value = aws_lambda_function.function.arn
}
Discussion