Lambda × Terraform で始めるトイル削減:準備 編
🚀 Lambda × Terraform で始めるトイル削減:準備 編
インフラ運用における日々の面倒な作業(=トイル)を減らすために、AWS Lambda と Terraform を活用して、インフラとコードの自動化を進めていきます。
本記事では、TypeScript で書かれた AWS Lambda 関数を Terraform でデプロイする構成を作成し、Makefile でビルドとデプロイの操作を統一管理します。
Lambda 関数の実装には TypeScript を採用することで型安全性を確保し、ビルドには esbuild を使うことで高速かつ軽量な成果物の作成を可能にしています。
🧰 事前準備:nodenv / tfenv の導入(macOS + Homebrew)
本プロジェクトでは、Node.js と Terraform のバージョン管理にそれぞれ nodenv
と tfenv
を使用します。
✅ nodenv のインストール(Node.js 22.14.0)
brew install nodenv
echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(nodenv init -)"' >> ~/.bashrc
source ~/.bashrc
nodenv install 22.14.0
nodenv local 22.14.0
✅ tfenv のインストール(Terraform 1.11.3)
brew install tfenv
tfenv install 1.11.3
tfenv use 1.11.3
📁 プロジェクト構成(lambda-terraform-prep)
lambda-terraform-prep/
├── Makefile
├── README.md
├── .node-version
├── .terraform-version
├── lambda/ # Lambda にデプロイするコード
│ ├── src/
│ │ ├── hello.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── build.mjs
│ └── test.mjs
├── dist/
├── index.zip
├── terraform/ # インフラの設定
│ ├── main.tf
│ ├── backend.tf
│ ├── variables.tf
│ ├── provider.tf
│ └── outputs.tf
🔧 使用ツールとバージョン
ツール | バージョン | 管理方法 |
---|---|---|
Node.js | 22.14.0 | nodenv |
Terraform | 1.11.3 | tfenv |
Shell | bash | - |
.node-version
と .terraform-version
により、各ツールのバージョンはプロジェクト単位で自動切り替えされます。
🧠 Lambda 関数(TypeScript)
この関数は AWS Lambda のハンドラー関数で、HTTP リクエストに対して 200 ステータスと "Hello, world!"
を返すシンプルなレスポンスを生成します。JSON 形式の文字列としてレスポンスボディを返しており、API Gateway などと連携する際の基本形となります。
処理の本体と、lambda のハンドラを分離して実装することで lambda 依存部分と非依存部分を分離します。
"Hello, world!"
を返す関数
export const sayHello = (): string => {
return "Hello, world!";
};
Lambda のハンドラ
import { sayHello } from "./hello";
export const handler = async (): Promise<{ statusCode: number; body: string }> => {
return {
statusCode: 200,
body: JSON.stringify({ message: sayHello() }),
};
};
🔧 TypeScript / esbuild 設定
TypeScript をトランスパイルして、dist ディレクトリに index.js
としてバンドル、ミニファイ済みのJSを配置する。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"outDir": "../dist"
},
"include": ["src"]
}
import { build } from "esbuild";
import { rmSync } from "fs";
rmSync("dist", { recursive: true, force: true });
build({
entryPoints: ["src/index.ts"],
bundle: true,
platform: "node",
target: "node22",
outfile: "../dist/index.js",
format: "cjs",
sourcemap: true,
minify: true,
treeShaking: true
}).catch(() => process.exit(1));
🧪 ローカルテスト
lambda にデプロイせずにローカル環境で実行を確認できるように、ローカルで実行できる構成を作ります。
import { handler } from '../dist/index.js';
const main = async () => {
const result = await handler();
console.log('handler returned:', result);
};
main();
make test-local
🛠️ Makefile によるビルド・デプロイ管理
npm でもコマンドを実行可能ですが、依存関係をつけてコマンドを実行しようとすると途端に記述が見づらくなるため、複雑なビルド処理は Makefile で実装します。
LAMBDA_DIR=lambda
DIST_DIR=dist
ZIP_NAME=index.zip
.PHONY: build zip zip-prune zip-package plan apply clean test-local
install-node-modules:
cd $(LAMBDA_DIR) && npm install
build: install-node-modules
cd $(LAMBDA_DIR) && node build.mjs
zip: clean zip-prune zip-package
zip-prune: build
cd $(LAMBDA_DIR) && npm prune --omit=dev
$(DIST_DIR)/package:
mkdir -p $(DIST_DIR)/package
zip-package: $(DIST_DIR)/package
cp $(DIST_DIR)/*.js $(DIST_DIR)/package/
cd $(DIST_DIR)/package && zip -r ../../$(ZIP_NAME) .
plan:
cd terraform && terraform plan
apply:
cd terraform && terraform apply
destroy:
cd terraform && terraform destroy
test-local: build
node $(LAMBDA_DIR)/test.mjs
clean:
rm -rf $(DIST_DIR) $(ZIP_NAME)
☁️ Terraform 構成
terraform の状態保存に S3 を使います。任意のバケットを作成し、my-terraform-state-bucket
をそのバケット名に置き換えてください。
- Lambda
- IAM ロール
- IAM ポリシー
を作成します。
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "ltprep/terraform.tfstate"
region = "ap-northeast-1"
}
}
resource "aws_iam_role" "rl-ltprep-lambda-exec" {
name = "rl-ltprep-lambda-exec"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRole",
Principal = {
Service = "lambda.amazonaws.com"
},
Effect = "Allow"
}]
})
}
resource "aws_iam_role_policy_attachment" "pl-ltprep-lambda-exec" {
role = aws_iam_role.rl-ltprep-lambda-exec.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "lm-ltprep-hello" {
function_name = "lm-ltprep-hello"
runtime = "nodejs22.x"
role = aws_iam_role.rl-ltprep-lambda-exec.arn
handler = "index.handler"
filename = "${path.module}/../index.zip"
source_code_hash = filebase64sha256("${path.module}/../index.zip")
}
provider "aws" {
region = var.aws_region
profile = var.aws_profile
}
variable "aws_profile" {
type = string
description = "Terraform 実行時に使用する AWS CLI プロファイル名"
}
variable "aws_region" {
type = string
default = "ap-northeast-1"
description = "AWS リージョン"
}
terraform.tfvars の例
terraform.tfvars
は、terraform plan
や terraform apply
の実行時に自動的に読み込まれる変数ファイルです。 terraform
ディレクトリに配置してください。
以下はその設定例です。aws_profile
には Lambda, ロール、ポリシーが書き込み可能なプロファイル名を指定してください。
AWS CLI が使用するプロファイルの設定は このあたり: AWS CLIのプロファイルの作り方と使用方法 を参照してください。
aws_profile = "dev"
aws_region = "ap-northeast-1"
💡
terraform.tfvars
は個人や環境ごとの認証情報を含む場合があるため、.gitignore
に追加してコミット対象から除外することを推奨します。
📘 Github
terraform.tfvars
以外のファイルは以下のリポジトリに置きました。
🏃 使い方
📝 初回セットアップ
-
terraform/backend.tf
に記載した S3 バケット(my-terraform-state-bucket
等)にアクセスできるプロファイル(以下の例ではinit-profile
)を指定して terrafrom の初期化を行なってください。AWS CLI が使用するプロファイルの設定は このあたり: AWS CLIのプロファイルの作り方と使用方法 を参照してください。 -
backend.tf
に記載されているbucket = "my-terraform-state-bucket"
のバケット名を、利用している環境の適切なバケットの名に変更してください。通常の運用ではこのコードは環境に合わせた状態でリポジトリにコミットします。
terraform -chdir=terraform init -backend-config="profile=init-profile"
結果
$ terraform -chdir=terraform init -backend-config="profile=init-profile"
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v5.94.1...
- Installed hashicorp/aws v5.94.1 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
生成される .terraform.lock.hcl
はリポジトリにコミットしてください。
関数のローカルテスト
make test-local
結果
$ make test-local
cd lambda && npm install
up to date, audited 4 packages in 334ms
found 0 vulnerabilities
cd lambda && node build.mjs
node lambda/test.mjs
handler returned: { statusCode: 200, body: '{"message":"Hello, world!"}' }
λ Lambda のデプロイ
make zip
結果
$ make zip
rm -rf dist index.zip
cd lambda && npm install
up to date, audited 4 packages in 316ms
found 0 vulnerabilities
cd lambda && node build.mjs
cd lambda && npm prune --omit=dev
up to date, audited 1 package in 103ms
found 0 vulnerabilities
mkdir -p dist/package
cp dist/*.js dist/package/
cd dist/package && zip -r ../../index.zip .
adding: index.js (deflated 38%)
🏭 インフラの構築と Lambda コードのデプロイ
- インフラの構築前に
terraform.tfvars
がterraform
ディレクトリに配置, 設定済みであることを確認してください。
初回のインフラ構築、またはコード変更後のデプロイでは、以下のプロセスを実行してください。
インフラ構築プランの確認
make plan
結果
$ make plan
cd terraform && terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_iam_role.rl-ltprep-lambda-exec will be created
+ resource "aws_iam_role" "rl-ltprep-lambda-exec" {
+ arn = (known after apply)
+ assume_role_policy = jsonencode(
{
+ Statement = [
+ {
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ Service = "lambda.amazonaws.com"
}
},
]
+ Version = "2012-10-17"
}
)
: #省略✂︎
+ inline_policy (known after apply)
}
# aws_iam_role_policy_attachment.pl-ltprep-lambda-exec will be created
+ resource "aws_iam_role_policy_attachment" "pl-ltprep-lambda-exec" {
+ id = (known after apply)
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ role = "rl-ltprep-lambda-exec"
}
# aws_lambda_function.lm-ltprep-hello will be created
+ resource "aws_lambda_function" "lm-ltprep-hello" {
+ architectures = (known after apply)
+ arn = (known after apply)
+ code_sha256 = (known after apply)
+ filename = "./../index.zip"
+ function_name = "lm-ltprep-hello"
+ handler = "index.handler"
+ id = (known after apply)
+ invoke_arn = (known after apply)
+ last_modified = (known after apply)
+ memory_size = 128
+ package_type = "Zip"
: #省略✂︎
}
Plan: 3 to add, 0 to change, 0 to destroy.
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
インフラの構築とLambdaコードのデプロイ(初回)
make apply
結果
$ make apply
cd terraform && terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
: #省略✂︎ この辺りは plan と同じ出力
Plan: 3 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes #yes を入力
aws_iam_role.rl-ltprep-lambda-exec: Creating...
aws_iam_role.rl-ltprep-lambda-exec: Creation complete after 2s [id=rl-ltprep-lambda-exec]
aws_iam_role_policy_attachment.pl-ltprep-lambda-exec: Creating...
aws_lambda_function.lm-ltprep-hello: Creating...
aws_iam_role_policy_attachment.pl-ltprep-lambda-exec: Creation complete after 1s [id=rl-ltprep-lambda-exec-20250406092031657800000001]
aws_lambda_function.lm-ltprep-hello: Still creating... [10s elapsed]
aws_lambda_function.lm-ltprep-hello: Creation complete after 15s [id=lm-ltprep-hello]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
インフラの構築とLambdaコードのデプロイ(コード変更後)
apply
でコードのみがデプロイされます。
make apply
結果
$ make apply
cd terraform && terraform apply
aws_iam_role.rl-ltprep-lambda-exec: Refreshing state... [id=rl-ltprep-lambda-exec]
aws_iam_role_policy_attachment.pl-ltprep-lambda-exec: Refreshing state... [id=rl-ltprep-lambda-exec-20250406092031657800000001]
aws_lambda_function.lm-ltprep-hello: Refreshing state... [id=lm-ltprep-hello]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_lambda_function.lm-ltprep-hello will be updated in-place
~ resource "aws_lambda_function" "lm-ltprep-hello" {
id = "lm-ltprep-hello"
~ last_modified = "2025-04-06T09:20:40.229+0000" -> (known after apply)
~ source_code_hash = "FDU7bK1Y9xF5PQLp+Bs4ei+HPNKz0jwlv4vfQcmFYIw=" -> "IGW2JKvW0BalZb8Skp+3mbkQ/5YEBOGHReSEtJTiFzE="
tags = {}
# (27 unchanged attributes hidden)
# (3 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_lambda_function.lm-ltprep-hello: Modifying... [id=lm-ltprep-hello]
aws_lambda_function.lm-ltprep-hello: Modifications complete after 6s [id=lm-ltprep-hello]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
λ 🏃 Lambda の実行
aws --profile dev \
lambda invoke \
--function-name function:lm-ltprep-hello \
--cli-binary-format raw-in-base64-out \
response.json
結果
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
$ cat response.json
{"statusCode":200,"body":"{\"message\":\"Hello, world!\"}"}
インフラの削除
destroy
で apply
で構築した範囲のインフラの削除ができます。
make destroy
結果
$ make destroy
cd terraform && terraform destroy
aws_iam_role.rl-ltprep-lambda-exec: Refreshing state... [id=rl-ltprep-lambda-exec]
aws_iam_role_policy_attachment.pl-ltprep-lambda-exec: Refreshing state... [id=rl-ltprep-lambda-exec-20250406092031657800000001]
aws_lambda_function.lm-ltprep-hello: Refreshing state... [id=lm-ltprep-hello]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# aws_iam_role.rl-ltprep-lambda-exec will be destroyed
- resource "aws_iam_role" "rl-ltprep-lambda-exec" {
- arn = "arn:aws:iam::369569311955:role/rl-ltprep-lambda-exec" -> null
- assume_role_policy = jsonencode(
{
- Statement = [
- {
- Action = "sts:AssumeRole"
- Effect = "Allow"
- Principal = {
- Service = "lambda.amazonaws.com"
}
},
]
- Version = "2012-10-17"
:
Plan: 0 to add, 0 to change, 3 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes #yes を入力
aws_iam_role_policy_attachment.pl-ltprep-lambda-exec: Destroying... [id=rl-ltprep-lambda-exec-20250406092031657800000001]
aws_lambda_function.lm-ltprep-hello: Destroying... [id=lm-ltprep-hello]
aws_lambda_function.lm-ltprep-hello: Destruction complete after 0s
aws_iam_role_policy_attachment.pl-ltprep-lambda-exec: Destruction complete after 1s
aws_iam_role.rl-ltprep-lambda-exec: Destroying... [id=rl-ltprep-lambda-exec]
aws_iam_role.rl-ltprep-lambda-exec: Destruction complete after 1s
Destroy complete! Resources: 3 destroyed.
Discussion