どうもこんにちは。
今回はTerraformを使って、PythonのLambdaレイヤーと、そのレイヤーを使うLambda関数をまとめてデプロイしてみます。
Lambdaレイヤーは、依存ライブラリや共通処理を関数本体から分けられる便利な仕組みですが、最初は「ZIPはどこで作るのか」「S3を用意すべきか」「レイヤーを関数にどう設定するのか」が少し分かりにくいです。
この記事では、ローカルのソースコードをTerraformでZIP化し、レイヤーとLambda関数を同時に作る最小構成を扱います。検証済みのコードは本プロジェクトにあります。
この記事は誰向け?
この記事は以下のような方向けに書いています。
- Terraformを初めて使う方
- LambdaレイヤーをTerraformでデプロイしたい方
- Pythonの共通処理を複数Lambdaで使い回したい方
- STGと本番で同じ構成を使いたい方
- TerraformとAWS CDKのどちらを選ぶか迷っている方
Githubリポジトリ
この検証でやりたいこと
今回のゴールは次の4点です。
- TerraformでLambdaレイヤーを作る
- 作成したレイヤーをLambda関数へ関連付ける
- レイヤーと関数の単体テスト、およびTerraformの構成テストを実行する
- STG/本番へ同じコードから安全にデプロイできるようにする
構成は次のとおりです。
app.pyはfrom shared.message import greetingとしてレイヤーのモジュールをimportします。LambdaのPythonレイヤーはpython/配下のファイルを/opt/pythonに展開するため、このディレクトリ構成にしています。
前提
今回の環境は次のとおりです。
- Terraform 1.7以上
- Python 3以上(単体テスト用)
- AWS CLIとAWS認証情報
- Lambda、Lambdaレイヤー、IAM Roleを作成・更新できるIAM権限
- デプロイリージョン:
ap-northeast-1
AWS認証情報は、ローカルアクセスキーをソースコードへ書かず、AWS CLIのプロファイルまたはIAM Identity Center (SSO)を使うのがおすすめです。
aws configure sso
aws sso login --profile my-profile
export AWS_PROFILE=my-profile
0. プロジェクト構成
最終的な構成は以下です。
.
├── main.tf
├── variables.tf
├── versions.tf
├── outputs.tf
├── Makefile
├── src/
│ ├── function/
│ │ └── app.py
│ └── layer/
│ └── python/
│ └── shared/
│ ├── __init__.py
│ └── message.py
└── tests/
├── lambda.tftest.hcl
├── test_app.py
└── test_layer.py
src/layer/の下のpythonディレクトリが重要です。
Pythonランタイムでレイヤーを使う場合、この階層でZIP化する必要があります。
レイヤーで指定したロジックは、app.pyなどの呼び出し元のメインファイルでimport shared.messageと記述することで動きます。
1. Terraformをインストールする
macOSではHashiCorp公式のHomebrew tapからインストールできます。
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform version
Linux/Windowsのインストール方法は、Terraform公式ドキュメントを参照してください。
Terraformは最初にterraform initを実行します。このコマンドは、AWSを操作するhashicorp/awsやZIPを作るhashicorp/archiveのようなProviderをダウンロードします。
terraform init
初期化後に作られる.terraform.lock.hclはProviderのバージョンを固定するファイルです。チームやCIで同じバージョンを使うため、Git管理対象に入れます。一方、.terraform/、terraform.tfstate、生成したbuild/はGit管理しません。
2. Terraformは何をしているのか
Terraformは、HCLという設定言語で「AWS上にどういう状態を作りたいか」を書くIaCツールです。
今回なら次の流れでコマンドや操作を実行していきます。
基本的なデプロイフローとして、terraform planで「何が作成・変更・削除されるか」を確認してから、terraform applyするのが基本です。
terraform.tfstateにはリソースの対応関係や値が保存されるため、漏えいしない場所に保管します。
3.レイヤーとLambdaのZIPを作る
Terraformのarchive_fileデータソースを使うと、外部のZIP作成スクリプトなしでディレクトリをZIPにできます。
main.tfではレイヤー用と関数用にそれぞれZIPを作ります。
locals {
layer_name = "${var.function_name}-shared"
}
data "archive_file" "layer" {
type = "zip"
source_dir = "${path.module}/src/layer"
output_path = "${path.module}/build/${local.layer_name}.zip"
}
data "archive_file" "function" {
type = "zip"
source_dir = "${path.module}/src/function"
output_path = "${path.module}/build/${var.function_name}.zip"
}
source_code_hashにZIPのハッシュを渡すことで、ソースコードが変わったときにTerraformが更新を検知できます。
source_code_hash = data.archive_file.layer.output_base64sha256
S3を使うべき?
今回のような小さなレイヤーと関数なら、TerraformからLambda APIへ直接ZIPを送る方式を選びます。S3バケット、アップロード権限、ライフサイクル管理が不要で、構成がシンプルだからです。
次の場合はS3を使う構成を検討します。
- ZIPが直接アップロードの上限(50 MB)を超える
- CIで作った同じ成果物を複数環境・複数プロジェクトへ配布したい
- 成果物を長期間保管し、デプロイ元を明確にしたい
- 依存ライブラリが大きく、レイヤーをビルド専用環境で作る必要がある
特にネイティブ依存を含むPythonレイヤーは、Lambdaと同じLinux環境でビルドする必要があります。この場合はCIやDockerでZIPを作り、S3へ置く方式が扱いやすくなります。
4. Lambdaレイヤーをデプロイする
レイヤーはaws_lambda_layer_versionで作成します。
resource "aws_lambda_layer_version" "shared" {
layer_name = local.layer_name
description = "Shared Python utilities for ${var.function_name}."
filename = data.archive_file.layer.output_path
source_code_hash = data.archive_file.layer.output_base64sha256
compatible_runtimes = [var.runtime]
lifecycle {
create_before_destroy = true
}
}
レイヤーはバージョン付きで公開されます。create_before_destroyを付けると、新しいレイヤーバージョンを公開してから古いものを破棄する順序になります。
レイヤーの中身は次のシンプルな共通関数です。
def greeting(name: str) -> str:
normalized_name = name.strip() or "world"
return f"Hello, {normalized_name}!"
5. Lambda関数をデプロイし、レイヤーを設定する
Lambdaが実行するときのIAM RoleもTerraformで作ります。今回はCloudWatch Logsへの出力だけが必要なので、AWS管理ポリシーAWSLambdaBasicExecutionRoleを付与しています。
resource "aws_iam_role_policy_attachment" "basic_execution" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
関数側ではlayersに、作成したレイヤーのARNを渡します。
resource "aws_lambda_function" "app" {
function_name = var.function_name
filename = data.archive_file.function.output_path
source_code_hash = data.archive_file.function.output_base64sha256
handler = "app.lambda_handler"
role = aws_iam_role.lambda.arn
runtime = var.runtime
layers = [aws_lambda_layer_version.shared.arn]
depends_on = [aws_iam_role_policy_attachment.basic_execution]
}
layers = [aws_lambda_layer_version.shared.arn]が、レイヤーと関数をつなぐ行です。レイヤーARNが変わればTerraformは関数側の設定も更新します。
関数はレイヤーのモジュールをimportしています。
import json
from shared.message import greeting
def lambda_handler(event, _context):
name = event.get("name", "world")
return {
"statusCode": 200,
"headers": {"content-type": "application/json"},
"body": json.dumps({"message": greeting(name)}),
}
ここまでできたら、実際の変更を確認してデプロイします。
terraform fmt -recursive
terraform validate
terraform plan
terraform apply
デプロイ後はAWS CLIから呼び出せます。
aws lambda invoke \
--function-name "$(terraform output -raw lambda_function_name)" \
--cli-binary-format raw-in-base64-out \
--payload '{"name":"Terraform"}' \
response.json
cat response.json
レスポンス内のmessageがHello, Terraform!なら、Lambda関数がレイヤーの関数を呼べています。
6. テストを実行する
このプロジェクトには2種類のテストがあります。
| テスト | コマンド | 確認すること |
|---|---|---|
| Python単体テスト | make test |
レイヤーの関数そのもの、およびLambdaハンドラのレスポンス |
| Terraformテスト | terraform test |
Lambdaのlayersに公開したレイヤーARNが設定されること |
Pythonテストは標準ライブラリのunittestだけを使います。
make test
tests/test_layer.pyはレイヤーのgreeting関数を直接テストします。名前の整形、空白の除去、空文字時の既定値を確認しています。
tests/test_app.pyはレイヤーと同じPythonパスを通してLambdaハンドラを読み込み、レイヤーをimportできることも確認します。
TerraformテストはモックAWS Providerを使うため、AWS認証情報も実リソース作成も不要です。
terraform test
CIでは、最低でも以下をpull requestごとに実行すると安心です。
terraform fmt -check -recursive
terraform validate
terraform test
make test
7. STGと本番を分けてデプロイする
環境を分けるときに重要なのは、リソース名とTerraform stateを両方分けることです。同じstateをSTGと本番で共有すると、片方のapplyがもう片方の構成を変更してしまいます。
まず環境別の変数ファイルを用意します。
# environments/stg.tfvars
aws_region = "ap-northeast-1"
function_name = "terraform-layer-demo-stg"
tags = {
Environment = "stg"
ManagedBy = "Terraform"
}
# environments/prod.tfvars
aws_region = "ap-northeast-1"
function_name = "terraform-layer-demo-prod"
tags = {
Environment = "prod"
ManagedBy = "Terraform"
}
小さな構成では、Terraform workspaceを使ってstateを分離できます。
# STG
terraform workspace new stg
terraform workspace select stg
terraform plan -var-file=environments/stg.tfvars
terraform apply -var-file=environments/stg.tfvars
# 本番
terraform workspace new prod
terraform workspace select prod
terraform plan -var-file=environments/prod.tfvars
terraform apply -var-file=environments/prod.tfvars
ローカルbackendでもworkspaceごとにstateは分かれます。ただしこれは導入しやすい最小構成です。チームや本番運用では、stateをローカルに置かず、アクセス制御・暗号化・ロックを設定したリモートbackendを使います。環境ごとにAWSアカウントや権限が大きく異なる場合は、workspaceだけに頼らず、環境ごとのディレクトリ・バックエンド設定・CI/CDパイプラインも分けます。
さらに安全性を上げるなら、STGと本番でAWSアカウント自体を分け、AWS_PROFILEも分けます。
AWS_PROFILE=my-stg-profile terraform apply -var-file=environments/stg.tfvars
AWS_PROFILE=my-prod-profile terraform apply -var-file=environments/prod.tfvars
本番では次のようなことを運用ルールにすると事故を減らせます。
-
terraform apply前に保存済みのterraform plan -out=tfplanをレビューする - 本番のapplyはCI/CDからだけ実行する
- Terraform stateをGitに入れない
- IAM権限をSTG/本番で分離する
- 本番アカウントへの人手の管理者アクセスを最小化する
8. TerraformとAWS CDKはどちらを選ぶ?
AWS CDKもTerraformもInfrastructure as Codeのツールです。ただしデプロイの仕組みと得意な場面が異なります。
| 観点 | Terraform | AWS CDK |
|---|---|---|
| 記述言語 | HCL | TypeScript、Python、Java、C#、Goなどから自由に選択 |
| デプロイ先 | AWS、Azure、GCP、SaaSなど複数Provider | AWS |
| デプロイ基盤 | Terraform stateを使いProviderがAPIを操作 | CloudFormationテンプレートを生成してデプロイ |
| 再利用 | Module | Construct |
| AWSの抽象化 | リソースを比較的そのまま記述 | 高レベルConstructによる便利な既定値がある |
| 導入時の準備 | Provider初期化、state設計 | CDK CLI、言語ランタイム、通常はbootstrap |
Terraformを選びやすい
Terraformを選びやすいのは、次のような場合かなと思います。
- AWS以外のクラウドやGitHub、Datadogなども同じIaCで管理したい
- HCLでインフラ設定を宣言的に読みやすく管理したい
- 既にTerraform Moduleや運用基盤がある
AWS CDKを選びやすい
AWS CDKを選びやすいのは、次のような場合です。
- AWS専用のシステムで、アプリと同じTypeScript/Pythonで書きたい
- Constructを使って、複数AWSリソースの定型構成を短く書きたい
- CloudFormationのスタック管理やロールバックを中心に運用したい
今回のようなLambdaとレイヤーの最小構成なら、どちらでも十分に実装できます。マルチクラウドや複数SaaSをまたぐ構成ならTerraform、AWSのConstructを積極的に使いたいアプリチームならCDK、という基準から始めると選びやすいです。
まとめ
今回はTerraformでLambdaレイヤーとLambda関数をデプロイしました。
-
archive_fileでソースディレクトリをZIP化できる - 小さなZIPはTerraformから直接デプロイするとシンプル
-
aws_lambda_layer_version.shared.arnをLambdaのlayersに設定する - レイヤー単体、Lambda単体、Terraformの配線を分けてテストできる
- 環境別tfvarsとworkspaceでSTG/本番のリソース名とstateを分離する
- 本番ではリモートstate、CI/CD、別AWSアカウントを組み合わせる
レイヤーに外部ライブラリを追加する、EventBridgeやAPI GatewayからLambdaを呼ぶ、といった構成にもこのまま発展させられます。