🚀

Lambda × Terraform で始めるトイル削減:準備 編

に公開

🚀 Lambda × Terraform で始めるトイル削減:準備 編

インフラ運用における日々の面倒な作業(=トイル)を減らすために、AWS Lambda と Terraform を活用して、インフラとコードの自動化を進めていきます。

本記事では、TypeScript で書かれた AWS Lambda 関数を Terraform でデプロイする構成を作成し、Makefile でビルドとデプロイの操作を統一管理します。

Lambda 関数の実装には TypeScript を採用することで型安全性を確保し、ビルドには esbuild を使うことで高速かつ軽量な成果物の作成を可能にしています。


🧰 事前準備:nodenv / tfenv の導入(macOS + Homebrew)

本プロジェクトでは、Node.js と Terraform のバージョン管理にそれぞれ nodenvtfenv を使用します。

✅ 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!" を返す関数

title=lambda/src/hello.ts
export const sayHello = (): string => {
  return "Hello, world!";
};

Lambda のハンドラ

title=lambda/src/index.ts
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を配置する。

title=lambda/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "outDir": "../dist"
  },
  "include": ["src"]
}
title=lambda/build.mjs
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 にデプロイせずにローカル環境で実行を確認できるように、ローカルで実行できる構成を作ります。

title=lambda/test.mjs
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 で実装します。

title=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 ポリシー

を作成します。

title=terraform/backend.tf
terraform {
  backend "s3" {
    bucket = "my-terraform-state-bucket"
    key    = "ltprep/terraform.tfstate"
    region = "ap-northeast-1"
  }
}
title=terraform/main.tf
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")
}
title=terraform/provider.tf
provider "aws" {
  region  = var.aws_region
  profile = var.aws_profile
}
title=terraform/variables.tf
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 planterraform apply の実行時に自動的に読み込まれる変数ファイルです。 terraform ディレクトリに配置してください。

以下はその設定例です。aws_profile には Lambda, ロール、ポリシーが書き込み可能なプロファイル名を指定してください。
AWS CLI が使用するプロファイルの設定は このあたり: AWS CLIのプロファイルの作り方と使用方法 を参照してください。

aws_profile = "dev"
aws_region  = "ap-northeast-1"

💡 terraform.tfvars は個人や環境ごとの認証情報を含む場合があるため、 .gitignore に追加してコミット対象から除外することを推奨します。


📘 Github

terraform.tfvars 以外のファイルは以下のリポジトリに置きました。

https://github.com/mtakahashi-ivi/lambda-terraform-prep

🏃 使い方

📝 初回セットアップ

  • 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.tfvarsterraform ディレクトリに配置, 設定済みであることを確認してください。

初回のインフラ構築、またはコード変更後のデプロイでは、以下のプロセスを実行してください。

インフラ構築プランの確認

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!\"}"}

インフラの削除

destroyapply で構築した範囲のインフラの削除ができます。

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.
Inventit Tech

Discussion