Aurora PostgreSQL IAM認証 — 人間がログインしないDB管理をTerraform+RDS Proxyで実現

目次

Aurora PostgreSQL — 人間がログインしないDB管理 完全ガイド

📚 AWS Aurora PostgreSQL 実践ガイド — この記事について

  • Aurora PostgreSQL — 人間がログインしないDB管理 完全ガイド(この記事)

本記事は単独の完全ガイドです。Terraform + IAM認証 + RDS Proxy を組み合わせた「パスワードなし・ログインなし」のDB管理を一から解説します。

公開日: 2026-04-14
難易度: 中級〜上級
所要時間: 約120分
タグ: AWS, Aurora, PostgreSQL, Terraform, IAM認証, RDS Proxy, Lambda, IaC

📋 この記事で学ぶこと

  • 従来のDB管理の問題点と「人間がログインしない」アーキテクチャのメリット
  • Aurora PostgreSQLへの8つのアクセス手法を徹底比較
  • Terraform cyrilgdn/postgresql providerでスキーマ・ロール・権限をIaC管理
  • IAM認証(パスワードレス)でAurora PostgreSQLに接続する仕組み
  • RDS Proxyでコネクションプーリングとフェイルオーバーを透過的に処理
  • Lambda + psycopg2 でDDLを自動実行するサーバーレスパターン

1. なぜ「人間がログインしないDB管理」なのか

1-1. 従来のDB管理が抱える問題

多くの現場では、Aurora PostgreSQLを含むRDBMSの管理を「人間が直接ログインして操作する」方法で行っています。しかしこのアプローチには、セキュリティ・運用・コンプライアンスの観点から深刻な問題があります。

問題1: パスワードの共有と管理コスト

従来の構成では、DBのマスターパスワードやアプリ用パスワードをチームで共有することが一般的です。

問題具体的な影響
パスワード共有誰がいつログインしたか追跡不能。退職者の認証情報が残留
ローテーション困難パスワード変更時に全アプリの設定変更が必要。ダウンタイムリスク
シークレット管理.envファイルやSlackでのパスワード共有。情報漏洩リスク
監査証跡の欠如「誰が何をしたか」のログが取れない。コンプライアンス違反

問題2: 踏み台サーバーによる属人的な運用

多くの現場では「踏み台EC2 → SSH → psql」というオペレーションフローが定着しています。

[開発者PC] → SSH → [踏み台EC2] → psql → [Aurora PostgreSQL]

この構成の問題点:

  • 属人化: 踏み台サーバーのIPアドレス、SSHキー、接続手順がドキュメントに散在
  • セキュリティリスク: 踏み台サーバー自体が攻撃対象になる。SSHキーの漏洩リスク
  • 変更追跡不可能: psqlで実行したSQLはCloudTrailに残らない
  • スケール困難: 開発者が増えるたびにSSHキー発行・管理が必要

問題3: 手動DDLによる一貫性の欠如

「スキーマを変更したいとき」に開発者がpsqlで直接DDLを実行するケースは珍しくありません。

-- 開発者Aがログインして手動実行(ステージング環境に反映済み)
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;

-- 本番環境への展開を忘れていた...

この問題は:
本番/ステージングの乖離: 誰かが手動で変更を加えると環境間で差分が生まれる
レビュープロセスの欠如: コードと異なり、DBへの手動変更はPull Requestでのレビューがない
ロールバックが困難: 誤ったDDLをロールバックするには再度手動で修正が必要


1-2. 理想の形: Infrastructure as Code + IAM認証

本記事が目指すアーキテクチャは 「人間がAurora PostgreSQLに直接ログインしない」 構成です。

理念

❌ 従来: 人間 → psql → Aurora(パスワード認証・手動操作)
✅ 理想: コード → Terraform/Lambda → Aurora(IAM認証・自動実行)

具体的な解決策

課題解決策
パスワード共有IAM認証でパスワードレス接続。パスワード自体を持たない
スキーマ手動変更Terraform postgresql providerでDDLをIaC化。Git管理・PR必須
踏み台サーバーRDS Proxy + VPC Lambdaで接続。SSHキー不要
監査証跡の欠如CloudTrail + CloudWatch Logsで全接続・操作を記録
環境間の乖離Terraformのterraform planで差分を可視化。applyで一致させる

1-3. 本記事のゴール

本記事では以下を実現します:

  1. Aurora PostgreSQL Serverless v2 をVPC内に構築
  2. RDS Proxy をフロントに配置してコネクション管理
  3. IAM認証 を有効化し、パスワードなしで接続
  4. Terraform cyrilgdn/postgresql provider でスキーマ・ロール・権限を宣言的管理
  5. Lambda + psycopg2 でDDLを自動実行するサーバーレスパターンを実装
  6. 全リソースを Terraform IaC で管理し、git push だけで環境を再現可能に
💡 この記事のコンセプト
「人間がDB管理をするのではなく、コードがDB管理をする。人間はコードをレビューする。」
すべての変更はGitHubのPull Requestを通じ、Terraformが自動適用します。

2. DB管理手法 徹底比較

Aurora PostgreSQLへのアクセス・管理方法は多岐にわたります。本セクションでは8つの代表的な手法を整理し、本記事でのハンズオン対象選定理由を説明します。

2-1. 8つの方法の概要

A. Terraform postgresql provider(cyrilgdn/postgresql)

Terraform HashiCorp Registryで公開されている cyrilgdn/postgresql プロバイダー(v1.26.0、2025年9月)を使い、Aurora PostgreSQLのスキーマ・ロール・権限をTerraformリソースとして宣言的に管理します。

  • 特徴: 19種類のリソース(postgresql_database/postgresql_schema/postgresql_role/postgresql_grant等)
  • IAM認証: scheme=awspostgres + aws_rds_iam_auth=true でパスワードレス接続対応(v1.25.0〜)
  • ユースケース: 初期スキーマ構築・ロール管理・権限管理をIaCで完結させたい場合

B. Flyway(CodeBuild / Fargate)

Java製のDBマイグレーションツール「Flyway」をAWSサービス上で実行するパターン。AWS公式ブログでもCodePipeline + CodeBuildの構成が紹介されています。

  • 特徴: SQLスクリプトをバージョン番号管理(V1.0__init.sql, V2.0__add_column.sql)
  • IAM認証: 直接サポートなし(Secrets Manager経由のパスワード認証が一般的)
  • ユースケース: 継続的なスキーママイグレーションをCI/CDパイプラインに組み込みたい場合

C. Lambda + psycopg2

Python製のPostgreSQL接続ライブラリ psycopg2 をLambda LayerとしてパッケージングしたLambda関数からAuroraにアクセスするパターン。

  • 特徴: boto3.client('rds').generate_db_auth_token() でIAM認証トークンを生成し接続
  • ユースケース: 軽量DDL実行・スキーマ初期化・EventBridge Schedulerでの定期実行

D. RDS Data API

AuroraにHTTPS経由でSQLを実行できるAPI。VPCへの接続不要でVPC外からも利用可能。

  • 特徴: DDL対応、トランザクション対応、IAM認証(rds-data:ExecuteStatement権限)
  • 制約: ENUM型非対応、結果サイズ上限1MB、T系インスタンス非対応
  • ユースケース: VPC外からの軽量DB操作・開発環境でのデバッグ

E. IAM データベース認証

Aurora PostgreSQLクラスターレベルでIAM認証を有効化し、トークンベースのパスワードレス接続を実現する基盤機能。他の方法(A/C/F)と組み合わせて使用します。

  • 仕組み: IAMポリシーで rds-db:connect 権限を付与 → 15分間有効のトークンを生成 → パスワード代わりに使用
  • RDS Proxy連携: アプリ→RDS Proxy間はIAM認証、RDS Proxy→Aurora間はSecrets Manager

F. SSM Session Manager + ECS Run Task

SSM Session ManagerのPort Forwarding機能でEC2経由のポートフォワードを行い、ローカルPCからpsqlやpgAdminで接続するパターン、またはECS Fargateでpsqlコンテナを一時実行するパターン。

  • SSM: aws ssm start-session --document-name AWS-StartPortForwardingSessionToRemoteHostlocalhost:15432 で接続
  • ユースケース: 開発者のad-hocなDB参照(踏み台EC2サーバー不要)

G. DMS / SCT(Database Migration Service)

異種DB間のスキーマ変換・データ移行ツール。同一エンジン(PostgreSQL → Aurora PostgreSQL)のスキーマ管理には不適切で、既存DBからの初期移行時のみ利用します。本記事の対象外です。

H. その他のパターン

  • GitHub Actions OIDC: OIDC → IAMロール → generate_db_auth_token → Aurora接続。CI/CDパイプラインからパスワードなしでDB操作
  • Secrets Manager 自動ローテーション: ローテーションLambdaをカスタマイズしてスキーマ変更も実行(非推奨)
  • AWSコンソール Query Editor: Data APIを使ったGUI操作。開発・デバッグ用途

2-2. 比較表

方法ユースケースVPC内接続IAM認証Terraform統合スキーマ変更難易度推奨度
A. TF postgresql provider初期構築・IaC管理要(直接接続)✅ awspostgres✅ 19リソース✅ DDL管理★★★☆☆★★★★★
B. Flyway (CodeBuild/Fargate)バージョン管理マイグレーション❌(Secrets Manager)△ null_resource✅ SQLスクリプト★★★★☆★★★★☆
C. Lambda + psycopg2軽量DDL・スキーマ初期化要(VPC Lambda)✅ generate_db_auth_token△ aws_lambda✅ 任意SQL★★☆☆☆★★★★☆
D. RDS Data APIVPC外からの軽量操作不要(HTTPS)✅ IAM△ null_resource✅ DDL可★☆☆☆☆★★★☆☆
E. IAM DB Authパスワードレス基盤✅ 基盤機能✅ RDS設定★★☆☆☆★★★★★
F. SSM / ECS Run Task開発者ad-hoc操作EC2経由✅ SSM★★☆☆☆★★★☆☆
G. ECS Run Task + psql長時間マイグレーション★★★☆☆★★★☆☆
H. GitHub Actions OIDCCI/CDパイプラインOIDC経由★★★☆☆★★★☆☆

2-3. 本記事でのハンズオン対象

本記事では以下を選定しました。

メインハンズオン: A + E + RDS Proxy(Terraform + IAM認証 + RDS Proxy)

[Terraform実行環境]
↓(IAM認証 / awspostgres scheme)
[RDS Proxy] → [Aurora PostgreSQL Serverless v2]
↑
[Lambda / アプリ](IAM認証 / generate_db_auth_token)

選定理由:
IaCに最も忠実: スキーマ・ロール・権限がすべてTerraform stateで管理され、terraform planで差分を可視化できる
パスワードレス: IAM認証によりパスワード自体が存在しない。ローテーション不要
「人間がログインしない」コンセプトに最合致: コードのみがDBを操作し、全変更がGit管理される
RDS Proxy: コネクションプーリングによるスケーラビリティ、フェイルオーバーの高速化

サブハンズオン: C(Lambda + psycopg2 + IAM認証)

Terraform providerでは管理できないインデックス作成やデータシードのユースケースをLambdaで補完します。Phase 4〜5で実装します。

解説のみ(ハンズオン手順なし):

  • D(RDS Data API): VPC不要で最も手軽だが制約が多い。概要とQuery Editor紹介のみ
  • B(Flyway on CodeBuild): Java依存・Enterprise版が必要なケースあり。アーキテクチャ概要のみ
  • F(SSM Port Forwarding): 開発者のデバッグ用。コマンド例のみ
  • H(GitHub Actions OIDC): CI/CD応用パターンとして紹介

3. アーキテクチャ概要

3-1. 全体構成

本記事で構築するシステムの全体像を示します。

全体アーキテクチャ

ネットワーク構成

システム全体はVPC内のプライベートサブネットに配置します。インターネットからAurora PostgreSQLへの直接アクセスは行いません。

ap-northeast-1(東京リージョン)
└── VPC (10.0.0.0/16)
 ├── パブリックサブネット (10.0.1.0/24, 10.0.2.0/24)
 │└── NAT Gateway(Lambda → VPCエンドポイント用)
 └── プライベートサブネット (10.0.11.0/24, 10.0.12.0/24)
  ├── Aurora PostgreSQL Serverless v2(マルチAZ)
  ├── RDS Proxy
  └── Lambda関数(VPC Lambda)

コンポーネント一覧

コンポーネントサービス役割
データベースAurora PostgreSQL Serverless v2メインDB(0.5〜128 ACU自動スケール)
コネクション管理RDS Proxyコネクションプーリング・IAM認証フロント
スキーマ管理Terraform postgresql providerDDL(スキーマ/ロール/権限)のIaC化
DDL実行Lambda + psycopg2インデックス作成等のサーバーレス実行
認証IAM Database Authenticationパスワードレス接続の基盤
シークレット管理Secrets ManagerRDS Proxy→Aurora間のマスターパスワード格納
VPC接続VPCエンドポイントSTS/Secrets Manager/RDSへのプライベート接続

3-2. IAM認証フロー

IAM認証は「パスワードの代わりに15分間有効なトークンを使う」仕組みです。

IAM認証フロー

フローの詳細

Terraform postgresql provider(スキーマ管理時):

1. Terraform実行環境(IAMロール: terraform_pg_role)
2. generate_db_auth_token(scheme=awspostgres が自動処理)
3. RDS Proxy エンドポイントに接続(TLS必須)
4. RDS Proxy → Aurora PostgreSQL(Secrets Manager認証)
5. DDLリソースを作成/更新/削除

Lambda + psycopg2(DDL実行時):

import boto3, psycopg2

# IAM認証トークン生成(15分間有効)
client = boto3.client('rds', region_name='ap-northeast-1')
token = client.generate_db_auth_token(
 DBHostname=rds_proxy_endpoint,
 Port=5432,
 DBUsername='iam_user',
 Region='ap-northeast-1'
)

# パスワード代わりにトークンを使用
conn = psycopg2.connect(
 host=rds_proxy_endpoint,
 port=5432,
 user='iam_user',
 password=token,
 dbname='mydb',
 sslmode='require'
)

RDS Proxy の役割:

接続区間認証方式補足
アプリ/Lambda → RDS ProxyIAM認証(トークン)TLS必須
RDS Proxy → AuroraSecrets Manager(パスワード)自動ローテーション対応

RDS Proxyを経由することで、アプリ側はIAM認証のみを意識すればよく、Aurora側のパスワード管理はRDS Proxyが吸収します。


3-3. マイグレーションフロー

スキーマ変更の流れを示します。

マイグレーションフロー

Terraformによるスキーマ変更フロー

① 開発者がpostgresql.tf を編集
② git push → Pull Request作成
③ CIで terraform plan を実行(差分レビュー)
④ レビュー承認 → main ブランチにマージ
⑤ CD(GitHub Actions等)で terraform apply 自動実行
⑥ postgresql provider が RDS Proxy 経由でDDLを実行
⑦ terraform state に変更後の状態を記録

Lambdaによるマイグレーションフロー(インデックス・データシード等)

① EventBridge Scheduler(毎日実行)またはStep Functions
② Lambda関数(Python 3.12 + psycopg2)を起動
③ Lambda が RDS Proxy 経由でIAM認証接続
④ migration_versionテーブルで未適用マイグレーションを確認
⑤ 未適用のDDL/DMLを冪等に実行(IF NOT EXISTS等)
⑥ CloudWatch Logsに実行結果を記録

3-4. 各コンポーネントの役割詳細

VPC

プライベートサブネット内にAurora・RDS Proxy・Lambdaを配置し、インターネットからの直接アクセスを遮断します。セキュリティグループでポート5432の通信を制御します。

セキュリティグループ:
- aurora_sg: ポート5432 ← rds_proxy_sg, lambda_sg のみ許可
- rds_proxy_sg: ポート5432 ← lambda_sg, terraform_sg のみ許可
- lambda_sg: アウトバウンドのみ(STS, Secrets Manager, RDS Data API)

Aurora PostgreSQL Serverless v2

最小0.5 ACU(Aurora Capacity Unit)から最大128 ACUまで自動スケールします。IAM認証有効化により、マスターパスワードを知っている人間でも、IAM権限なしでは接続できなくなります。

resource "aws_rds_cluster" "aurora_pg" {
  engine  = "aurora-postgresql"
  engine_mode= "provisioned"
  engine_version= "16.4"
  iam_database_authentication_enabled = true  # IAM認証を有効化

  serverlessv2_scaling_configuration {
 min_capacity = 0.5
 max_capacity = 4.0
  }
}

RDS Proxy

Aurora PostgreSQLのフロントに配置するフルマネージドのコネクションプロキシです。

  • コネクションプーリング: Lambda関数が並列実行されても、Aurora側のコネクション数を抑制
  • フェイルオーバー高速化: Aurora障害時のフェイルオーバーを66%高速化
  • IAM認証必須(REQUIRED): IAM認証なしの接続を完全拒否

Terraform postgresql provider

cyrilgdn/postgresql プロバイダーのv1.26.0(2025年9月リリース)を使用します。

terraform {
  required_providers {
 postgresql = {
source  = "cyrilgdn/postgresql"
version = "~> 1.26"
 }
  }
}

provider "postgresql" {
  scheme = "awspostgres"  # IAM認証用スキーム
  host= aws_db_proxy.main.endpoint
  port= 5432
  database  = "postgres"
  username  = "terraform_user"
  aws_rds_iam_auth= true  # IAMトークン自動生成
  aws_rds_iam_region = "ap-northeast-1"
  superuser = false
}

Lambda(psycopg2)

Python 3.12ランタイムのLambda関数に psycopg2 をLayerとして追加します。VPC内に配置し、RDS Proxy経由でIAM認証接続します。


3-5. コスト概算

本記事の構成を1日稼働した場合の概算コスト(東京リージョン、2026年4月時点):

リソース単価1日の概算コスト
Aurora PostgreSQL Serverless v2(最小0.5 ACU)$0.12/ACU時間$1.44/日
RDS Proxy$0.015/VCPUh(Auroraの0.5 vCPU相当)$0.18/日
NAT Gateway(データ転送なし)$0.062/時間$1.49/日
Lambda(月10万回実行以内)無料枠内$0
Secrets Manager(1シークレット)$0.40/月$0.01/日
合計約$3.12/日(約470円)
⚠️ コスト注意
Aurora Serverless v2 は「最小ACU = 0.5」でも、クラスターが起動している間は課金が発生します。ハンズオン完了後は必ず terraform destroy でリソースを削除してください。特に RDS Proxy は Aurora クラスターが停止していても課金されます。

4. 前提条件と環境準備

4-1. 必要な環境

⚠️ ハンズオン開始前に確認すること

  • AWSアカウント: Aurora, RDS Proxy, Lambda, IAM, VPC, Secrets Manager の操作権限が必要
  • Terraform: バージョン 1.5 以上(terraform version で確認)
  • AWS CLI v2: aws --versionaws-cli/2.x.x であることを確認
  • AWS認証情報: aws configure 済み、またはIAMロールが設定済み
  • Docker(任意): psycopg2 Layerをローカルビルドする場合に必要
  • 対象リージョン: ap-northeast-1(東京)を前提に手順を記載

ツールのバージョン確認

# Terraform バージョン確認(1.5以上が必要)
terraform version
# Terraform v1.9.x

# AWS CLI v2 確認
aws --version
# aws-cli/2.x.x Python/3.x.x ...

# Python(Lambda開発用)
python3 --version
# Python 3.12.x

# Docker(任意: psycopg2 Layer ビルド用)
docker --version
# Docker version 27.x.x

4-2. IAM権限の確認

ハンズオンを実行するIAMユーザー/ロールには以下の権限が必要です。

{
  "Effect": "Allow",
  "Action": [
 "rds:*",
 "rds-db:connect",
 "secretsmanager:*",
 "iam:*",
 "lambda:*",
 "ec2:*",
 "logs:*",
 "sts:GetCallerIdentity"
  ],
  "Resource": "*"
}
💡 最小権限の原則
上記はハンズオン用の広めの権限です。本番環境では各リソースに応じた最小権限を設計してください。Terraformが必要とする権限の詳細は iam.tf の作成時に説明します。

4-3. 作業ディレクトリ構成

本記事のTerraformコードは以下の構成で管理します。

aurora-no-human-login/
├── terraform/
│├── providers.tf # AWSプロバイダー + PostgreSQLプロバイダー
│├── variables.tf # 変数定義
│├── outputs.tf# 出力値
│├── vpc.tf # VPC/サブネット/NAT Gateway/セキュリティグループ
│├── aurora.tf # Aurora PostgreSQL Serverless v2 + Secrets Manager
│├── rds_proxy.tf # RDS Proxy + IAM認証設定
│├── iam.tf # IAMロール/ポリシー(Terraform用・Lambda用)
│├── postgresql.tf# スキーマ/ロール/権限(cyrilgdn/postgresql)
│├── lambda.tf # マイグレーションLambda
│└── terraform.tfvars# 変数値(gitignore推奨)
├── lambda/
│├── migration/
││├── handler.py  # Lambda関数コード
││└── requirements.txt  # psycopg2-binary
│└── layer/
│ └── build.sh # psycopg2 Layer ビルドスクリプト
└── README.md

作業ディレクトリを作成します:

mkdir -p aurora-no-human-login/terraform
mkdir -p aurora-no-human-login/lambda/migration
mkdir -p aurora-no-human-login/lambda/layer
cd aurora-no-human-login
git init

4-4. cyrilgdn/postgresql provider について

本記事のメインハンズオンで使用する cyrilgdn/postgresql プロバイダーの概要を事前に把握しておきましょう。

# providers.tf(最小構成)
terraform {
  required_version = ">= 1.5"
  required_providers {
 aws = {
source  = "hashicorp/aws"
version = "~> 5.0"
 }
 postgresql = {
source  = "cyrilgdn/postgresql"
version = "~> 1.26"
 }
  }
}

主要リソース(本記事で使用するもの)

リソース名用途
postgresql_databaseデータベース作成
postgresql_schemaスキーマ作成(名前空間分離)
postgresql_roleロール/ユーザー作成
postgresql_grant権限付与(GRANT)
postgresql_default_privilegesデフォルト権限設定
postgresql_extension拡張機能追加(pgcrypto, uuid-ossp等)

IAM認証での接続設定(scheme=awspostgres)

cyrilgdn/postgresql v1.25.0 以降、scheme=awspostgres を指定することでIAM認証トークンを自動生成して接続できます。パスワードを一切コードに記述する必要がありません。

provider "postgresql" {
  scheme = "awspostgres"  # ← GoCloud経由のIAM認証
  host= "<RDS Proxyエンドポイント>"
  port= 5432
  database  = "postgres"
  username  = "terraform_user"
  aws_rds_iam_auth= true
  aws_rds_iam_region = "ap-northeast-1"
  # password は不要!IAMトークンが自動使用される
  superuser = false
  sslmode= "require"
}

4-5. コスト警告

⚠️ 課金リソースの注意事項

  • Aurora Serverless v2: 最小 0.5 ACU × $0.12/時間 = 約$1.44/日。停止しても削除しない限り課金継続
  • RDS Proxy: Aurora クラスターの vCPU 数 × $0.015/時間。Aurora を停止していても課金
  • NAT Gateway: 稼働時間 × $0.062/時間 = 約$1.49/日(データ転送除く)
  • Secrets Manager: $0.40/シークレット/月(30日間の無料トライアルあり)

ハンズオン完了後は必ず terraform destroy を実行してすべてのリソースを削除してください。


5. Phase 1: VPC + Aurora PostgreSQL + RDS Proxy 構築(コンソール)

Aurora PostgreSQL クラスターを「人間がログインしないDB管理」に適した構成で構築します。このフェーズでは AWSコンソールを使い、VPC/ネットワーク基盤から Aurora クラスター・Secrets Manager・RDS Proxy までをセットアップします。

ハンズオンの進め方
Phase 1(Section 5)はコンソールで手動構築します。Phase 3 以降(Section 7)で同じリソースを Terraform コードに落とし込みます。まずコンソールで全体像を把握し、その後 IaC へ移行する流れです。

Phase 1-1: VPC・ネットワーク基盤の作成

Aurora を VPC のプライベートサブネットに配置し、外部から直接アクセスできない構成を作ります。

1. VPC の作成

  1. AWSコンソール → VPC → 左メニュー「Your VPCs」→ 「Create VPC」をクリック
  2. 以下の設定を入力します:
項目設定値
VPC only を選択(「VPC and more」ではなく単体作成)
Name tagaurora-db-vpc
IPv4 CIDR block10.0.0.0/16
IPv6 CIDR blockNo IPv6 CIDR block
TenancyDefault
  1. Create VPC」をクリック

2. サブネットの作成

VPC ダッシュボード → 「Subnets」→ 「Create subnet

Public Subnet A(NAT Gateway 配置用)

項目設定値
VPC IDaurora-db-vpc(上で作成した VPC)
Subnet nameaurora-public-1a
Availability Zoneap-northeast-1a
IPv4 subnet CIDR block10.0.0.0/24

Add new subnet」をクリックして続けて追加:

Public Subnet C

項目設定値
Subnet nameaurora-public-1c
Availability Zoneap-northeast-1c
IPv4 subnet CIDR block10.0.64.0/24

Add new subnet」→ Private Subnet A

項目設定値
Subnet nameaurora-private-1a
Availability Zoneap-northeast-1a
IPv4 subnet CIDR block10.0.128.0/24

Add new subnet」→ Private Subnet C

項目設定値
Subnet nameaurora-private-1c
Availability Zoneap-northeast-1c
IPv4 subnet CIDR block10.0.192.0/24

4 つのサブネットをまとめて作成します。「Create subnet」をクリック。

3. Internet Gateway の作成とアタッチ

  1. VPC ダッシュボード → 「Internet gateways」→ 「Create internet gateway
  2. Name tag: aurora-igw → 「Create internet gateway
  3. 作成後に「Attach to a VPC」が表示されます → aurora-db-vpc を選択して「Attach internet gateway

4. NAT Gateway の作成

Public Subnet からプライベートサブネット内の Lambda がアウトバウンド通信できるように NAT Gateway を配置します。

  1. VPC ダッシュボード → 「NAT gateways」→ 「Create NAT gateway
  2. 設定:
項目設定値
Nameaurora-nat-1a
Subnetaurora-public-1a (Publicサブネットに配置)
Connectivity typePublic
Elastic IP allocation ID「Allocate Elastic IP」をクリックして新規割り当て
  1. Create NAT gateway」をクリック
NAT Gateway は約 2〜3 分で Available になります。次のルートテーブル設定の前に Status が「Available」になるまで待ちましょう。

5. ルートテーブルの設定

パブリック用ルートテーブル(IGW 経由)

  1. VPC ダッシュボード → 「Route tables
  2. aurora-db-vpc に紐づくデフォルトルートテーブルを選択(Name が空のもの)
  3. Actions」→「Edit routes」→「Add route
  4. Destination: 0.0.0.0/0
  5. Target: aurora-igw(先ほど作成した Internet Gateway)
  6. Save changes
  7. Subnet associations」タブ → 「Edit subnet associations
  8. aurora-public-1aaurora-public-1c を選択 → 「Save associations

プライベート用ルートテーブル(NAT 経由)

  1. Create route table」→ Name: aurora-private-rt、VPC: aurora-db-vpc
  2. Create route table」→ 「Edit routes」→ 「Add route
  3. Destination: 0.0.0.0/0
  4. Target: aurora-nat-1a(先ほど作成した NAT Gateway)
  5. Save changes
  6. Subnet associations」→ aurora-private-1aaurora-private-1c を追加 → 「Save associations

Phase 1-2: セキュリティグループの作成

Aurora・RDS Proxy・Lambda の間の通信を最小権限で制御する 3 つのセキュリティグループを作ります。

作成順序に注意: RDS Proxy SG が Aurora SG を参照し、Lambda SG が RDS Proxy SG を参照するため、aurora-sg → rds-proxy-sg → lambda-sg の順で作成します。

VPC ダッシュボード → 「Security groups」→ 「Create security group

セキュリティグループ 1: aurora-sg(Aurora PostgreSQL 用)

項目設定値
Security group nameaurora-sg
DescriptionAurora PostgreSQL security group
VPCaurora-db-vpc

Inbound rules(後で追加): RDS Proxy SG 作成後に設定

Outbound rules(デフォルト): すべて許可

Create security group

セキュリティグループ 2: rds-proxy-sg(RDS Proxy 用)

項目設定値
Security group namerds-proxy-sg
DescriptionRDS Proxy security group
VPCaurora-db-vpc

Inbound rules(後で追加): Lambda SG 作成後に設定

Outbound rules:

TypeProtocolPortDestination
Custom TCPTCP5432aurora-sg のセキュリティグループ ID

Create security group

セキュリティグループ 3: lambda-sg(Lambda 用)

項目設定値
Security group namelambda-sg
DescriptionLambda migration function security group
VPCaurora-db-vpc

Inbound rules: なし(Lambda は受信しない)

Outbound rules:

TypeProtocolPortDestination
Custom TCPTCP5432rds-proxy-sg のセキュリティグループ ID
HTTPSTCP4430.0.0.0/0(STS/Secrets Manager へのアクセス)

Create security group

セキュリティグループのルールを更新

aurora-sg の Inbound rules を追加:

  1. aurora-sg を選択 → 「Edit inbound rules」→ 「Add rule
TypeProtocolPortSource
Custom TCPTCP5432rds-proxy-sg のセキュリティグループ ID
Custom TCPTCP5432lambda-sg のセキュリティグループ ID
  1. Save rules

rds-proxy-sg の Inbound rules を追加:

  1. rds-proxy-sg を選択 → 「Edit inbound rules」→「Add rule
TypeProtocolPortSource
Custom TCPTCP5432lambda-sg のセキュリティグループ ID
  1. Save rules

Phase 1-3: Aurora PostgreSQL Serverless v2 クラスターの作成

IAM DB Authentication は作成時に有効化が必要です。後から有効化すると既存接続が一時中断されます。作成ウィザードの途中に必ずチェックボックスをオンにしてください。
  1. AWSコンソール → RDS → 「Create database
  2. 設定を順番に入力します:

① Standard create を選択

② Engine options

項目設定値
Engine typeAurora (PostgreSQL Compatible)
Engine versionAurora PostgreSQL 16.x(最新の 16 系を選択)

③ Templates

項目設定値
TemplateProduction

④ Settings

項目設定値
DB cluster identifieraurora-no-human-login-cluster
Master usernamepostgres
Credentials managementSelf managed
Master password任意の強いパスワード(後で Secrets Manager に登録する)

⑤ Cluster storage configuration

項目設定値
Storage typeAurora Standard

⑥ Instance configuration

項目設定値
DB instance classServerless v2
Minimum capacity0.5 ACU
Maximum capacity16 ACU
Serverless v2 の課金について: 最小 0.5 ACU に設定すると、アクティブでない時間帯は自動でスケールダウンします。ハンズオン完了後はクラスターを削除してコスト管理しましょう。

⑦ Availability & durability

項目設定値
Multi-AZ deploymentDon’t create an Aurora Replica(ハンズオン用途のため)

⑧ Connectivity

項目設定値
Virtual private cloud (VPC)aurora-db-vpc
DB subnet groupCreate new DB subnet group」を選択
Public accessNo
VPC security groupChoose existingaurora-sg を選択(default は削除)
Availability Zoneap-northeast-1a

⑨ Database authentication

ここが最重要設定です。「Database authentication options」セクションで「Password and IAM database authentication」を選択してください。IAM DB Authentication が有効化されていないと、後の手順でトークンベース認証ができません。
項目設定値
Database authenticationPassword and IAM database authentication ← 必須

⑩ Monitoring

項目設定値
Enable Enhanced monitoringチェックを外す(ハンズオン用途では不要)

⑪ Additional configuration

項目設定値
Initial database nameappdb
DB cluster parameter groupdefault.aurora-postgresql16
Backup retention period1 day(ハンズオン用途)
Enable deletion protectionチェックを外す(後の削除を容易にするため)
  1. Create database」をクリック

Aurora クラスターの作成には約 5〜10 分かかります。Status が「Available」になるまで次のステップに進みます。


Phase 1-4: Secrets Manager シークレットの作成

RDS Proxy がマスターユーザーの認証情報を安全に管理するため、Secrets Manager にシークレットを登録します。

  1. AWSコンソール → Secrets Manager → 「Store a new secret
  2. 設定:

① Secret type

項目設定値
Secret typeCredentials for Amazon RDS database
Usernamepostgres
PasswordAurora 作成時に設定したマスターパスワード
Database上で作成した aurora-no-human-login-cluster を選択

Next

② Configure secret

項目設定値
Secret nameaurora-no-human-login/master
DescriptionMaster credentials for Aurora aurora-no-human-login-cluster

Next

③ Configure rotation(ローテーション設定)

項目設定値
Automatic rotationEnable automatic rotation
Rotation schedule30 days
Rotation functionCreate a new Lambda function
Lambda function nameSecretsManagerAuroraPostgreSQLRotation
自動ローテーションを有効にしておくのがベストプラクティスです。RDS Proxy は Secrets Manager の最新の認証情報を自動的に取得するため、ローテーションしてもアプリへの影響はありません。ハンズオン目的では無効でも構いませんが、本番環境では必ず有効化してください。

Next」→「Store」をクリック


Phase 1-5: RDS Proxy の作成

RDS Proxy を介することで、コネクションプーリングと IAM 認証の一元管理が実現します。

  1. AWSコンソール → RDS → 左メニュー「Proxies」→ 「Create proxy
  2. 設定:

① Proxy configuration

項目設定値
Proxy identifieraurora-no-human-login-proxy
Engine familyPostgreSQL
Require Transport Layer Securityチェックを入れる (TLS 必須)
Idle client connection timeout1800 seconds(30 分)

② Target group configuration

項目設定値
Databaseaurora-no-human-login-cluster
Connection pool maximum connections100(ハンズオン用途)

③ Connectivity

項目設定値
Secrets Manager secretaurora-no-human-login/master
IAM roleCreate a new IAM role → 名前: aurora-rds-proxy-role
IAM authenticationRequired ← 重要
Subnetsaurora-private-1aaurora-private-1c を選択
Security groupsrds-proxy-sg
RDS Proxy の IAM 認証を Required にすると、IAM トークンなしの接続が拒否されます。開発環境では Allowed も選択できますが、本番環境では Required を推奨します。Required に設定することで、パスワードによる直接接続を完全に防ぎます。

④ Additional configuration

項目設定値
Enable enhanced loggingチェックを入れる(トラブルシューティング用)
  1. Create proxy」をクリック

RDS Proxy の作成には約 5〜10 分かかります。Status が「Available」になったら Phase 2 に進みます。

RDS Proxy エンドポイントの確認: Proxy 詳細画面の「Endpoint」に表示される値(例: aurora-no-human-login-proxy.proxy-xxxx.ap-northeast-1.rds.amazonaws.com)を後の手順で使用します。メモしておきましょう。

6. Phase 2: IAM 認証設定と DB ユーザー作成

Phase 1 で構築した Aurora クラスターに IAM 認証用の DB ユーザーを作成し、パスワードレス接続の基盤を完成させます。

このフェーズだけ一時的にマスターユーザーでログインします。IAM 認証用ユーザーを作成したら、以降は人間が直接 DB にアクセスしません。マスターユーザーの認証情報は Secrets Manager にのみ保管し、人間が覚える必要はありません。

Phase 2-1: VPC 内からの初回接続

Aurora はプライベートサブネットに配置しているため、VPC 内のリソースからしかアクセスできません。Cloud9(または EC2)を使ってプライベートサブネット内から接続します。

Cloud9 環境の準備

  1. AWSコンソール → Cloud9 → 「Create environment
  2. 設定:
項目設定値
Nameaurora-admin-env
Environment typeNew EC2 instance
Instance typet3.small
PlatformAmazon Linux 2023
Network settingsaurora-db-vpc / aurora-public-1a
  1. Create」→ Cloud9 IDE が起動するまで待つ(約 2 分)
Cloud9 の代わりに SSM Session Manager を使う場合: プライベートサブネットの EC2 に SSM エージェントが入っていれば、以下のコマンドでポートフォワードできます。

aws ssm start-session \
  --target i-xxxxxxxxxxxxxxxxx \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{"host":["<aurora-writer-endpoint>"],"portNumber":["5432"],"localPortNumber":["15432"]}'

その後 localhost:15432 で psql 接続できます。

psql のインストールと Aurora への接続

Cloud9 ターミナルで以下を実行します:

# PostgreSQL クライアントのインストール
sudo dnf install -y postgresql15

# Aurora Writer エンドポイントへの接続
# (RDS コンソール → Databases → aurora-no-human-login-cluster → Connectivity & security タブで確認)
AURORA_ENDPOINT="aurora-no-human-login-cluster.cluster-xxxx.ap-northeast-1.rds.amazonaws.com"

psql -h $AURORA_ENDPOINT -U postgres -d appdb

パスワードを求められたら、Phase 1-3 で設定したマスターパスワードを入力します。

接続確認:

SELECT version();
-- PostgreSQL 16.x (Aurora PostgreSQL ...)  が表示される

Phase 2-2: IAM 認証用 DB ユーザーの作成

PostgreSQL の rds_iam ロールを付与したユーザーだけが IAM トークンでログインできます。

-- Terraform 実行環境用 IAM ユーザーの作成
CREATE USER iam_terraform_user;
GRANT rds_iam TO iam_terraform_user;

-- Lambda 関数用 IAM ユーザーの作成
CREATE USER iam_lambda_user;
GRANT rds_iam TO iam_lambda_user;

-- 確認: rds_iam ロールが付与されているか確認
SELECT usename, usesuper, memberof
FROM pg_user
JOIN pg_auth_members ON pg_user.usesysid = pg_auth_members.member
JOIN pg_roles ON pg_auth_members.roleid = pg_roles.oid
WHERE pg_roles.rolname = 'rds_iam';

期待される出力:

  usename | usesuper | memberof
-------------------+----------+---------
 iam_terraform_user |  false| {rds_iam}
 iam_lambda_user |  false| {rds_iam}
(2 rows)
-- psql を終了
\q
これが最後のパスワードログインです。psql を終了したら、以降は IAM トークンで接続します。マスターユーザー(postgres)のパスワードは Secrets Manager に保管され、RDS Proxy が内部的に使用するだけです。

Phase 2-3: IAM ポリシーの作成

rds-db:connect アクションを IAM ポリシーで許可します。

  1. AWSコンソール → IAM → 「Policies」→ 「Create policy
  2. JSON」タブを選択して以下を入力:

まず Aurora クラスターのリソース ID を確認します:

  1. AWSコンソール → RDSaurora-no-human-login-cluster を選択
  2. Configuration」タブ → 「Resource ID」の値をコピー(例: cluster-XXXXXXXXXXXXXXXXX
{
  "Version": "2012-10-17",
  "Statement": [
 {
"Effect": "Allow",
"Action": "rds-db:connect",
"Resource": [
  "arn:aws:rds-db:ap-northeast-1:{ACCOUNT_ID}:dbuser:{CLUSTER_RESOURCE_ID}/iam_terraform_user",
  "arn:aws:rds-db:ap-northeast-1:{ACCOUNT_ID}:dbuser:{CLUSTER_RESOURCE_ID}/iam_lambda_user"
]
 }
  ]
}

{ACCOUNT_ID} は自身の AWS アカウント ID(12桁)、{CLUSTER_RESOURCE_ID} は上で確認したリソース ID に置き換えます。

  1. Next」→ Policy name: aurora-iam-db-access → 「Create policy
RDS Proxy 経由での接続について: RDS Proxy を使う場合、Resource ARN の dbuser 部分は RDS Proxy のリソース ID ではなく Aurora クラスターのリソース ID を使います。RDS Proxy は IAM トークンを検証した後、内部的に Secrets Manager の認証情報で Aurora に接続します。

Phase 2-4: IAM ロールの作成

Terraform 実行環境と Lambda 関数それぞれに IAM ロールを作成し、aurora-iam-db-access ポリシーをアタッチします。

aurora-terraform-role(Terraform 実行環境用)

  1. IAM → 「Roles」→ 「Create role
  2. Trusted entity type: AWS serviceEC2
  3. Add permissions: aurora-iam-db-access を検索してアタッチ
  4. Role name: aurora-terraform-role
  5. Create role
EC2 に IAM ロールをアタッチする場合は EC2 コンソール → インスタンスを選択 → 「Actions」→「Security」→「Modify IAM role」で設定します。GitHub Actions や CloudShell から実行する場合は OIDC 連携や IAM Identity Center を使います。

aurora-lambda-role(Lambda 実行用)

  1. IAM → 「Roles」→ 「Create role
  2. Trusted entity type: AWS serviceLambda
  3. Add permissions:
  4. aurora-iam-db-access
  5. AWSLambdaVPCAccessExecutionRole(VPC Lambda 実行に必要)
  6. Role name: aurora-lambda-role
  7. Create role

Phase 2-5: IAM トークンの生成テスト

RDS Proxy エンドポイントに対してトークンを生成できるか確認します。Cloud9 ターミナルで実行します。

# RDS Proxy エンドポイントを変数に設定
PROXY_ENDPOINT="aurora-no-human-login-proxy.proxy-xxxx.ap-northeast-1.rds.amazonaws.com"

# IAM 認証トークンの生成
TOKEN=$(aws rds generate-db-auth-token \
  --hostname $PROXY_ENDPOINT \
  --port 5432 \
  --username iam_terraform_user \
  --region ap-northeast-1)

# トークンの先頭 80 文字を確認
echo "Token (first 80 chars): ${TOKEN:0:80}..."

期待される出力例:

Token (first 80 chars): aurora-no-human-login-proxy.proxy-xxxx.ap-northeast-1.rds.amazonaws.com:5432/...

トークンは hostname:port/?Action=connect&DBUser=...&X-Amz-Credential=... 形式の署名付き URL です。

トークン生成に失敗する場合: Cloud9 インスタンスの IAM ロールに rds:connect 権限がない可能性があります。Cloud9 コンソール → 環境設定 → IAM ロールを確認するか、Cloud9 の AWS Managed Temporary Credentials を使用してください。

Phase 2-6: パスワードレス接続テスト

生成したトークンを使って RDS Proxy 経由で Aurora に接続します。

# SSL 証明書のダウンロード(ap-northeast-1)
wget https://truststore.pki.rds.amazonaws.com/ap-northeast-1/ap-northeast-1-bundle.pem

# IAM トークン(再生成)
PROXY_ENDPOINT="aurora-no-human-login-proxy.proxy-xxxx.ap-northeast-1.rds.amazonaws.com"
TOKEN=$(aws rds generate-db-auth-token \
  --hostname $PROXY_ENDPOINT \
  --port 5432 \
  --username iam_terraform_user \
  --region ap-northeast-1)

# パスワードレス接続
PGPASSWORD=$TOKEN psql \
  "host=$PROXY_ENDPOINT \
port=5432 \
user=iam_terraform_user \
dbname=appdb \
sslmode=require \
sslrootcert=ap-northeast-1-bundle.pem"

接続に成功したら以下が表示されます:

psql (15.x, server 16.x)
SSL connection (protocol: TLSv1.3, cipher: ..., bits: 256, compression: off)
Type "help" for help.

appdb=>
-- 現在のユーザーとバージョンを確認
SELECT current_user, version();

期待される出力:

 current_user | version
--------------------+---------
 iam_terraform_user | PostgreSQL 16.x ...
(1 row)
接続成功!これ以降、このユーザーはパスワードなしで接続できます。トークンは 15 分で期限切れになりますが、アプリ・Lambda は接続時に自動で再生成します。マスターパスワードはどこにも存在しない状態で Aurora への接続が実現しました。

Phase 2-7: RDS Proxy 経由の接続確認と設定まとめ

ここまでの設定を整理します。

設定済みリソース一覧

リソース名前/値備考
VPCaurora-db-vpcCIDR: 10.0.0.0/16
Public Subnet 1aaurora-public-1a10.0.0.0/24
Public Subnet 1caurora-public-1c10.0.64.0/24
Private Subnet 1aaurora-private-1a10.0.128.0/24
Private Subnet 1caurora-private-1c10.0.192.0/24
Aurora Clusteraurora-no-human-login-clusterPostgreSQL 16, Serverless v2
RDS Proxyaurora-no-human-login-proxyIAM: Required
Secrets Manageraurora-no-human-login/master自動ローテーション
IAM Policyaurora-iam-db-accessrds-db:connect
IAM Role (TF)aurora-terraform-roleEC2 用
IAM Role (Lambda)aurora-lambda-roleLambda 用
DB Useriam_terraform_userrds_iam ロール付き
DB Useriam_lambda_userrds_iam ロール付き

接続フローのまとめ

Client (Lambda/Terraform)
 ↓ generate_db_auth_token (AWS STS)
 ↓ 15分有効なIAMトークン取得
 ↓ SSL接続 (user=iam_*_user, password=token)
RDS Proxy
 ↓ IAMトークンを検証
 ↓ Secrets Managerからmaster認証情報取得
 ↓ 接続プール管理
Aurora PostgreSQL
Phase 2 完了!Aurora への直接パスワードログインの仕組みが完全に排除されました。次の Phase 3 では、Terraform postgresql provider を使ってスキーマ・ロール・権限を IaC で管理します。

トラブルシューティング: Phase 1-2 よくあるエラー

エラー: PAM authentication failed for user "iam_terraform_user"

原因: RDS Proxy が IAM 認証モードなのに、psql コマンドに SSL 設定が不足している。

対処: sslmode=require を必ず指定する。

# NG: SSL なし
psql "host=$PROXY_ENDPOINT user=iam_terraform_user dbname=appdb"

# OK: SSL あり
PGPASSWORD=$TOKEN psql "host=$PROXY_ENDPOINT user=iam_terraform_user dbname=appdb sslmode=require"

エラー: FATAL: IAM authentication is not allowed for user "iam_terraform_user"

原因: Aurora クラスターの IAM DB Authentication が無効。

対処: RDS コンソール → クラスター → 「Modify」→ 「Database authentication」→「Password and IAM database authentication」に変更して Apply。

エラー: connection timeout または could not connect to server

原因: セキュリティグループのルールが誤っている、またはサブネットの設定ミス。

対処:
1. lambda-sg の Outbound: rds-proxy-sg へのポート 5432 が許可されているか確認
2. rds-proxy-sg の Inbound: lambda-sg からのポート 5432 が許可されているか確認
3. RDS Proxy が aurora-private-1aaurora-private-1c の両サブネットにデプロイされているか確認

エラー: The provided token has expired

原因: IAM トークンの有効期限(15 分)が切れた。

対処: トークンを再生成してから接続する。アプリケーションコードでは、接続前に毎回 generate_db_auth_token を呼び出すのがベストプラクティスです。

# 毎回トークンを再生成(スクリプト化するとよい)
TOKEN=$(aws rds generate-db-auth-token \
  --hostname $PROXY_ENDPOINT \
  --port 5432 \
  --username iam_terraform_user \
  --region ap-northeast-1)

7. Phase 3: Terraform postgresql provider でスキーマ管理

このPhaseでは、cyrilgdn/postgresql Terraform providerを使い、AuroraのDB・スキーマ・ロール・権限・拡張機能をコードで宣言的に管理します。これにより、terraform plan でスキーマ変更差分が可視化され、Pull Requestレビューと組み合わせたガバナンスが実現します。

ステップ内容
7-1cyrilgdn/postgresql provider の設定
7-2postgresql_database — データベース作成
7-3postgresql_role — ロール/ユーザー作成
7-4postgresql_schema — スキーマ作成
7-5postgresql_grant — 権限付与(GRANT)
7-6postgresql_default_privileges — デフォルト権限
7-7postgresql_extension — 拡張機能
7-8terraform apply でスキーマ管理
7-9スキーマ変更の差分管理

7-1. cyrilgdn/postgresql provider の設定

cyrilgdn/postgresql は、PostgreSQL リソース(DB・スキーマ・ロール・権限)を Terraform リソースとして管理するサードパーティproviderです。AWS IAM認証に対応しており、scheme = "awspostgres"aws_rds_iam_auth = true を指定することでパスワードなしのIAM認証が自動的に使用されます。

providers.tf に追加

terraform {
  required_providers {
 aws = {
source  = "hashicorp/aws"
version = "~> 5.0"
 }
 postgresql = {
source  = "cyrilgdn/postgresql"
version = "~> 1.26.0"
 }
  }
}

provider "postgresql" {
  scheme = "awspostgres"
  host= aws_db_proxy.main.endpoint
  port= 5432
  database  = "postgres"
  username  = "iam_terraform_user"
  superuser = false

  aws_rds_iam_auth= true
  aws_rds_iam_region = "ap-northeast-1"
  # IAMロールARN(Terraform実行環境のロール)
  # aws_rds_iam_provider_role_arn = var.terraform_role_arn  # assume role使用時
}
💡 ポイント: scheme と aws_rds_iam_auth の組み合わせ
scheme = “awspostgres”aws_rds_iam_auth = true の組み合わせが重要です。
このprovider設定でパスワードなしのIAM認証トークンが自動的に生成・使用されます。username には Section 6 で作成した iam_terraform_user を指定してください。

前提: Terraform実行環境のIAMロールに必要な権限

Terraform CLIを実行するIAMロール(または実行ユーザー)には、以下のポリシーが必要です。

# iam.tf (抜粋)
resource "aws_iam_policy" "terraform_pg_connect" {
  name = "terraform-pg-connect"

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect= "Allow"
Action= "rds-db:connect"
Resource = "arn:aws:rds-db:ap-northeast-1:${data.aws_caller_identity.current.account_id}:dbuser:${aws_db_proxy.main.id}/iam_terraform_user"
 }]
  })
}
⚠️ 注意: terraform apply 実行環境はVPC内が前提
cyrilgdn/postgresql providerterraform apply 時に Aurora(またはRDS Proxy)へ直接TCP接続します。
CI/CD環境(CodeBuild、GitHub Actions self-hosted runner等)では、必ずVPC内(プライベートサブネット)から実行してください。ローカル開発環境の場合は SSM Session Manager Port Forwarding でVPNトンネルを確立してから実行します。

7-2. postgresql_database — データベース作成

postgresql_database リソースでアプリケーション用のデータベースを作成します。

postgresql.tf

resource "postgresql_database" "app_db" {
  name  = "app_database"
  owner = postgresql_role.app_owner.name
  encoding = "UTF8"
  lc_collate  = "C"
  lc_ctype = "C"
  connection_limit  = -1
  allow_connections = true
}
パラメータ説明
nameapp_database作成するDB名
ownerapp_owner_roleDBオーナーロール(後述の 7-3 で定義)
encodingUTF8文字コード
lc_collateCソート順(Cは最速)
lc_ctypeC文字分類(Cは最速)
connection_limit-1無制限(-1)
💡 ヒント: lc_collate = “C” を推奨する理由
lc_collate = "C" はロケール依存のソートを行わないため、パフォーマンスが最速です。
日本語テキストのソートが必要な場合のみ "ja_JP.UTF-8" を検討してください。ただしAurora PostgreSQLではサポートするロケールに制限がある点に注意してください。

7-3. postgresql_role — ロール/ユーザー作成

PostgreSQLのロール設計は「ロール(権限の束)」と「ログインユーザー(IAMユーザー)」を分離するのがベストプラクティスです。アプリケーションコードから直接ロールにログインするのではなく、IAMユーザー(例: iam_lambda_user)に対してロールをGRANTして使います。

# アプリ書き込みロール
resource "postgresql_role" "app_writer" {
  name = "app_writer_role"
  login= false  # ロールとして使用(直接ログイン不可)
  connection_limit = -1
}

# アプリ読み取り専用ロール
resource "postgresql_role" "app_reader" {
  name = "app_reader_role"
  login= false
  connection_limit = -1
}

# アプリオーナーロール(DB所有者・スキーマ作成権限)
resource "postgresql_role" "app_owner" {
  name = "app_owner_role"
  login= false
  createdb= true
  connection_limit = -1
}
💡 ヒント: login = false のロールの使い方
login = false のロールは直接ログインできません。
Lambda用IAMユーザー(iam_lambda_user)やTerraform用IAMユーザー(iam_terraform_user)に対して GRANT ROLE で付与して使います。これにより「IAMユーザー単位の認証」と「PostgreSQL権限の管理」を分離できます。

IAMユーザーへのロール付与

iam_lambda_userapp_writer_role を付与する例です。postgresql_grantobject_type = "role" を使います。

# iam_lambda_user に app_writer_role を付与
resource "postgresql_grant" "lambda_writer_role" {
  database = postgresql_database.app_db.name
  role  = "iam_lambda_user"
  object_type = "role"
  privileges  = []
  # NOTE: object_type = "role" の場合、privileges は使用されない
  # GRANT ROLE は postgresql_grant ではなく
  # postgresql_grant_role リソースを使うことを推奨
}

# 推奨: postgresql_grant_role を使う場合
# resource "postgresql_grant_role" "lambda_writer" {
#role = "iam_lambda_user"
#grant_role = postgresql_role.app_writer.name
# }
⚠️ 注意: IAMユーザーはTerraformの外で作成済みである必要がある
iam_lambda_useriam_terraform_user は Section 6 の手順(CREATE USER ... WITH LOGIN + GRANT rds_iam)で事前に作成済みである必要があります。
cyrilgdn/postgresql provider はこれらのIAMユーザーをTerraform管理外のリソースとして参照します。

7-4. postgresql_schema — スキーマ作成

postgresql_schema リソースでアプリケーション用のスキーマを作成し、ロールごとのUSAGE権限をインラインで設定します。

resource "postgresql_schema" "app" {
  name  = "app"
  database = postgresql_database.app_db.name
  owner = postgresql_role.app_owner.name

  policy {
 usage = true
 role  = postgresql_role.app_writer.name
  }

  policy {
 usage = true
 role  = postgresql_role.app_reader.name
  }
}
パラメータ説明
nameスキーマ名(app
database対象DB(app_database
ownerスキーマオーナー(app_owner_role
policy.usageUSAGEを付与するか
policy.role権限付与対象のロール
💡 ヒント: publicスキーマとの使い分け
PostgreSQL 15以降、public スキーマへのデフォルト権限は廃止されました。
アプリケーション用に専用スキーマ(この例では app)を作成し、明示的に権限管理することを強く推奨します。search_pathapp に設定することで、app. プレフィックスなしでテーブルにアクセスできます。

7-5. postgresql_grant — 権限付与(GRANT)

postgresql_grant リソースでスキーマ・テーブルへの権限を付与します。object_type ごとに別リソースを定義する必要があります。

# app_writer ロールにスキーマ使用権限
resource "postgresql_grant" "writer_schema" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_writer.name
  schema= postgresql_schema.app.name
  object_type = "schema"
  privileges  = ["USAGE", "CREATE"]
}

# app_reader ロールにスキーマ使用権限
resource "postgresql_grant" "reader_schema" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_reader.name
  schema= postgresql_schema.app.name
  object_type = "schema"
  privileges  = ["USAGE"]
}

# app_writer ロールにテーブル権限(読み書き)
resource "postgresql_grant" "writer_table" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_writer.name
  schema= postgresql_schema.app.name
  object_type = "table"
  privileges  = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

# app_reader ロールにテーブル参照権限
resource "postgresql_grant" "reader_table" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_reader.name
  schema= postgresql_schema.app.name
  object_type = "table"
  privileges  = ["SELECT"]
}

object_type ごとの権限マッピング

object_type利用可能な privileges用途
schemaUSAGE, CREATEスキーマへのアクセス許可
tableSELECT, INSERT, UPDATE, DELETE, TRUNCATEテーブル操作
sequenceUSAGE, SELECT, UPDATEシーケンス操作
functionEXECUTE関数実行
databaseCONNECT, TEMPORARY, CREATEDB接続
⚠️ 注意: postgresql_grant の変更はdestroyが発生する場合がある
privileges を変更すると、Terraformは既存の GRANTREVOKE してから再 GRANT します(destroy + create)。
本番環境でテーブル権限を変更する場合は、terraform plan で差分を事前確認し、アプリケーションへの影響を考慮してから terraform apply を実行してください。

7-6. postgresql_default_privileges — デフォルト権限

postgresql_grant はすでに存在するテーブルへの権限付与です。後から作成されるテーブルにも自動的に権限を付与するには postgresql_default_privileges を使います。

# app_owner が作成する新規テーブルに自動で app_writer の権限を付与
resource "postgresql_default_privileges" "writer_tables" {
  database = postgresql_database.app_db.name
  schema= postgresql_schema.app.name
  owner = postgresql_role.app_owner.name
  object_type = "table"
  role  = postgresql_role.app_writer.name
  privileges  = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

# app_owner が作成する新規テーブルに自動で app_reader の権限を付与
resource "postgresql_default_privileges" "reader_tables" {
  database = postgresql_database.app_db.name
  schema= postgresql_schema.app.name
  owner = postgresql_role.app_owner.name
  object_type = "table"
  role  = postgresql_role.app_reader.name
  privileges  = ["SELECT"]
}

# シーケンスへのデフォルト権限(SERIAL/BIGSERIAL型使用時)
resource "postgresql_default_privileges" "writer_sequences" {
  database = postgresql_database.app_db.name
  schema= postgresql_schema.app.name
  owner = postgresql_role.app_owner.name
  object_type = "sequence"
  role  = postgresql_role.app_writer.name
  privileges  = ["USAGE", "SELECT", "UPDATE"]
}
🚨 重要: postgresql_default_privileges を設定しないと権限漏れが発生する
postgresql_default_privileges を設定しないと、後から作成したテーブルには権限が自動付与されません。
Lambdaマイグレーション(Section 8)や null_resource + local-exec で新規テーブルを作成するたびに、手動で GRANT を実行する必要が生じます。必ずデフォルト権限を設定してください。

SQLでの確認方法

terraform apply 後、以下のSQLでデフォルト権限が正しく設定されているか確認できます。

-- デフォルト権限の確認
SELECT
  pg_get_userbyid(d.defaclrole) AS "owner",
  pg_get_userbyid(acl.grantee) AS "grantee",
  acl.privilege_type,
  d.defaclobjtype AS "object_type"
FROM
  pg_default_acl d,
  aclexplode(d.defaclacl) AS acl
WHERE
  d.defaclnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'app');

7-7. postgresql_extension — 拡張機能

Aurora PostgreSQLで利用可能な拡張機能をTerraformで管理します。

# UUID生成(uuid_generate_v4() 関数)
resource "postgresql_extension" "uuid_ossp" {
  name  = "uuid-ossp"
  database = postgresql_database.app_db.name
  schema= "public"
}

# 暗号化(crypt()、pgp_sym_encrypt() 等)
resource "postgresql_extension" "pgcrypto" {
  name  = "pgcrypto"
  database = postgresql_database.app_db.name
  schema= "public"
}

Aurora PostgreSQL でよく使われる拡張機能一覧

拡張機能名用途Terraform resource
uuid-osspUUID生成(uuid_generate_v4()postgresql_extension.uuid_ossp
pgcrypto暗号化・ハッシュpostgresql_extension.pgcrypto
pg_stat_statementsクエリ統計(パフォーマンス分析)postgresql_extension.pg_stat_statements
pg_trgm部分一致検索(LIKE最適化)postgresql_extension.pg_trgm
postgis地理情報(GIS)postgresql_extension.postgis
vectorpgvector(AI埋め込みベクトル)postgresql_extension.vector
💡 ヒント: Aurora PostgreSQLでサポートされる拡張機能
すべての拡張機能がAurora PostgreSQLで利用できるわけではありません。
サポート一覧は AWS公式ドキュメント を参照してください。pg_stat_statements は Aurora のパラメータグループで shared_preload_libraries に追加が必要です。

7-8. terraform apply でスキーマ管理

provider設定とリソース定義が完了したら、terraform initterraform planterraform apply の順で実行します。

初回実行手順

cd terraform/

# プロバイダーをダウンロード(cyrilgdn/postgresql を含む)
terraform init

# 差分確認(何が作成・変更・削除されるか確認)
terraform plan

# 適用
terraform apply

terraform plan の出力例

Terraform will perform the following actions:

  # postgresql_database.app_db will be created
  + resource "postgresql_database" "app_db" {
+ name = "app_database"
+ owner= "app_owner_role"
+ encoding= "UTF8"
+ lc_collate = "C"
+ lc_ctype= "C"
+ connection_limit = -1
 }

  # postgresql_schema.app will be created
  + resource "postgresql_schema" "app" {
+ name  = "app"
+ database = "app_database"
+ owner = "app_owner_role"

+ policy {
 + role  = "app_writer_role"
 + usage = true
  }

+ policy {
 + role  = "app_reader_role"
 + usage = true
  }
 }

  # postgresql_role.app_writer will be created
  + resource "postgresql_role" "app_writer" {
+ name = "app_writer_role"
+ login= false
+ connection_limit = -1
 }

  # postgresql_grant.writer_table will be created
  + resource "postgresql_grant" "writer_table" {
+ database = "app_database"
+ role  = "app_writer_role"
+ schema= "app"
+ object_type = "table"
+ privileges  = ["DELETE", "INSERT", "SELECT", "UPDATE"]
 }

  # postgresql_extension.uuid_ossp will be created
  + resource "postgresql_extension" "uuid_ossp" {
+ name  = "uuid-ossp"
+ database = "app_database"
+ schema= "public"
 }

Plan: 12 to add, 0 to change, 0 to destroy.

実行ログの確認

# apply後、tfstateで管理対象リソースを確認
terraform state list | grep postgresql

# 出力例:
# postgresql_database.app_db
# postgresql_default_privileges.reader_tables
# postgresql_default_privileges.writer_sequences
# postgresql_default_privileges.writer_tables
# postgresql_extension.pgcrypto
# postgresql_extension.uuid_ossp
# postgresql_grant.reader_schema
# postgresql_grant.reader_table
# postgresql_grant.writer_schema
# postgresql_grant.writer_table
# postgresql_role.app_owner
# postgresql_role.app_reader
# postgresql_role.app_writer
# postgresql_schema.app
💡 ヒント: terraform apply の実行順序は自動解決される
Terraformはリソース間の依存関係(depends_on や参照)を自動解析し、正しい順序で作成します。
たとえば postgresql_schema.apppostgresql_role.app_owner を参照しているため、ロールが先に作成されてからスキーマが作成されます。明示的な depends_on の記述は原則不要です。

7-9. スキーマ変更の差分管理

Terraform postgresql providerはスキーマ(Schema)・ロール・権限・拡張機能の宣言的管理に対応していますが、テーブルレベルのDDL(ALTER TABLE等)には対応していません。テーブル変更は Section 8 のLambdaマイグレーション、またはnull_resource + local-exec で実施します。

対応パターン: テーブルの管理方法

操作手段Terraform対応
スキーマ作成・変更postgresql_schema✅ 対応
ロール作成・変更postgresql_role✅ 対応
権限付与・変更postgresql_grant✅ 対応(変更はdestroy+create)
拡張機能管理postgresql_extension✅ 対応
テーブル作成postgresql_function + null_resource⚠️ 間接対応
カラム追加Lambda(Section 8)または null_resource❌ 直接不可
インデックス追加Lambda(Section 8)または null_resource❌ 直接不可

null_resource + local-exec を使ったカラム追加の例

Terraformからpsqlコマンドを実行してALTER TABLEを行う代替パターンです。IAM認証トークンを動的に生成して使用します。

# 一例: usersテーブルに email_verified カラムを追加
resource "null_resource" "add_email_verified_column" {
  # トリガー: この値が変わるたびに再実行される
  triggers = {
 migration_version = "v1_add_email_verified"
  }

  provisioner "local-exec" {
 command = <<-EOF
# IAM認証トークンを生成(有効期間15分)
TOKEN=$(aws rds generate-db-auth-token \
  --hostname ${aws_db_proxy.main.endpoint} \
  --port 5432 \
  --region ap-northeast-1 \
  --username iam_terraform_user)

# psql で ALTER TABLE 実行
PGPASSWORD="$TOKEN" psql \
  "host=${aws_db_proxy.main.endpoint} \
port=5432 \
dbname=app_database \
user=iam_terraform_user \
sslmode=require" \
  -c "ALTER TABLE app.users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE;"
 EOF
  }
}
🚨 注意: Terraform postgresql provider は ALTER TABLE(カラム追加・削除)に非対応
Terraform postgresql provider はスキーマ・ロール・権限・拡張機能の管理に特化しており、テーブルレベルのDDL(CREATE TABLEALTER TABLE・インデックス等)には対応していません。
カラム変更は Lambdaマイグレーション(Section 8) または null_resource + local-exec + psql コマンドで実施します。大規模なスキーマ変更管理が必要な場合は Flyway on CodeBuild(Section 11-2)の導入も検討してください。

terraform plan による差分可視化

スキーマ変更をGitHubでレビューするワークフロー例です。

# 1. ブランチを切る
git checkout -b feat/add-app-schema-v2

# 2. postgresql.tf を変更(例: 新しいロールを追加)
# resource "postgresql_role" "app_analytics" { ... }

# 3. terraform plan で差分確認
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary | jq '.resource_changes[] | {address, action: .change.actions}'

# 出力例:
# {
#"address": "postgresql_role.app_analytics",
#"action": ["create"]
# }

# 4. Pull Requestを作成(terraform plan の出力をPRコメントに貼付)
git add postgresql.tf
git commit -m "feat: add app_analytics role for reporting"
git push origin feat/add-app-schema-v2
# → GitHub PR作成 → レビュー → マージ → terraform apply
💡 ポイント: terraform plan をGitHub PRコメントに自動投稿する
GitHub Actionshashicorp/setup-terraform アクションと github-script を組み合わせることで、PR作成時に terraform plan の出力を自動コメントできます。
これにより、レビュアーはコード差分とインフラ差分を同時に確認でき、「意図しない削除」や「権限の変更」を事前に発見できます。CI/CDとの統合はSection 11-4(GitHub Actions OIDC連携)で詳しく解説します。

Section 7 まとめ

リソース役割
postgresql_databaseアプリケーション用DB作成
postgresql_roleロール定義(writer/reader/owner)
postgresql_schemaスキーマ作成 + USAGE権限
postgresql_grant既存オブジェクトへの権限付与
postgresql_default_privileges新規作成オブジェクトへの自動権限付与
postgresql_extension拡張機能の有効化
null_resource + local-execTerraform非対応DDL(ALTER TABLE等)の代替手段

次のSection 8では、Lambda + psycopg2 を使ったDDL実行(CREATE TABLECREATE INDEX等)と冪等マイグレーションの実装を解説します。

8. Phase 4: Lambda + psycopg2 で DDL 実行

Phase 3 では Terraform postgresql provider を使い、スキーマ・ロール・権限を IaC で管理しました。しかし Terraform provider が対応していない操作(ALTER TABLE、CREATE INDEX、データ投入、条件付き DDL)は別の手段が必要です。Phase 4 では Lambda 関数 + psycopg2 を使い、IAM 認証経由でこれらの DDL を実行します。

8-1. Lambda + psycopg2 パターンの概要

Terraform postgresql provider では以下の操作を直接管理できません:

操作理由
ALTER TABLE(カラム追加・削除・型変更)Terraform state との競合が発生しやすい
CREATE INDEXprovider がインデックス管理リソースを未提供
INSERT / UPDATE などのデータ投入DDL ではなく DML のため対象外
IF NOT EXISTS チェック後の条件付き DDLTerraform のべき等性モデルと相性が悪い

これらを Lambda 関数から psycopg2 経由で実行することで、Terraform では管理しにくい操作を補完します。

Terraform provider との役割分担
Terraform postgresql provider → 宣言的なスキーマ・ロール・権限管理(CREATE DATABASE / SCHEMA / ROLE / GRANT)
Lambda + psycopg2 → 動的な DDL 実行(ALTER TABLE、インデックス作成、データ投入)
この 2 つを組み合わせることで、IaC による一元管理と柔軟な運用変更の両立が可能です。

8-2. psycopg2 Lambda Layer の作成

psycopg2 は C 拡張ライブラリを含むため、Lambda 実行環境(Amazon Linux 2023)に合わせた専用バイナリが必要です。ローカル環境で pip install psycopg2-binary しても Lambda では動作しません。Docker を使ってビルドします。

手順:

① Docker でバイナリをビルド

# 作業ディレクトリ作成
mkdir -p lambda-migration/layer/python

# Lambda 実行環境(Amazon Linux 2023)と同じコンテナでビルド
docker run --rm \
  -v $(pwd)/lambda-migration/layer:/output \
  public.ecr.aws/lambda/python:3.12 \
  pip install psycopg2-binary -t /output/python/

# ビルド結果を確認
ls lambda-migration/layer/python/

② ZIP ファイル作成

cd lambda-migration/layer
zip -r psycopg2-layer.zip python/
cd ../..

# ファイルサイズ確認(5〜10 MB 程度)
ls -lh lambda-migration/layer/psycopg2-layer.zip

③ Lambda Layer として登録

aws lambda publish-layer-version \
  --layer-name psycopg2-aurora \
  --zip-file fileb://lambda-migration/layer/psycopg2-layer.zip \
  --compatible-runtimes python3.12 \
  --region ap-northeast-1

出力例:

{
 "LayerVersionArn": "arn:aws:lambda:ap-northeast-1:123456789012:layer:psycopg2-aurora:1",
 "LayerArn": "arn:aws:lambda:ap-northeast-1:123456789012:layer:psycopg2-aurora",
 "Version": 1
}

LayerVersionArn の値を控えておきます。後続の Lambda 関数作成で使用します。

psycopg2-binary を使用する理由
psycopg2(C ライブラリ依存)と psycopg2-binary(バイナリ同梱)の違いは、ビルド時の依存関係にあります。Lambda 環境では libpq などの C ライブラリが利用できないため、psycopg2-binary を使用します。また、Docker ビルドは Lambda の実行環境(Amazon Linux 2023)と同じ OS でビルドするため、バイナリ互換性が保証されます。ローカル Mac/Windows 環境での pip install は OS が異なるためネイティブバイナリが動作しません。

8-3. Lambda 関数コードの作成

Lambda 関数は以下の役割を担います:

  1. IAM 認証トークンを生成(毎回新規生成、15 分間有効)
  2. RDS Proxy 経由で Aurora PostgreSQL に接続
  3. 未適用のマイグレーションを検出して実行
  4. べき等性を確保(schema_migrations テーブルで管理)

関数コード:

ローカルに lambda-migration/lambda_function.py を作成します。

import boto3
import psycopg2
import os
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def get_connection():
 """IAM 認証トークンで Aurora に接続(RDS Proxy 経由)"""
 rds_client = boto3.client('rds', region_name=os.environ['AWS_REGION'])

 # IAM 認証トークンを生成(毎接続前に新規生成することが重要)
 token = rds_client.generate_db_auth_token(
  DBHostname=os.environ['RDS_PROXY_HOST'],
  Port=5432,
  DBUsername=os.environ['DB_USER'],
  Region=os.environ['AWS_REGION']
 )

 conn = psycopg2.connect(
  host=os.environ['RDS_PROXY_HOST'],
  port=5432,
  user=os.environ['DB_USER'],
  password=token,
  database=os.environ['DB_NAME'],
  sslmode='require'  # IAM 認証では TLS 必須
 )
 return conn


def lambda_handler(event, context):
 """べき等なマイグレーション実行ハンドラー"""
 logger.info("Migration Lambda started")

 conn = get_connection()
 try:
  with conn.cursor() as cur:
# バージョン管理テーブルの作成(べき等: 既存なら何もしない)
cur.execute("""
 CREATE TABLE IF NOT EXISTS schema_migrations (
  version VARCHAR(255) PRIMARY KEY,
  applied_at TIMESTAMPTZ DEFAULT NOW(),
  applied_by VARCHAR(255) DEFAULT current_user
 )
""")
conn.commit()

# 未適用マイグレーションの取得と実行
migrations = get_pending_migrations(cur)
logger.info(f"Pending migrations: {len(migrations)}")

for migration in migrations:
 apply_migration(cur, migration)
 logger.info(f"Applied migration: {migration['version']}")
 conn.commit()  # マイグレーションごとにコミット

  return {
'statusCode': 200,
'body': f'Applied {len(migrations)} migrations successfully'
  }

 except Exception as e:
  conn.rollback()
  logger.error(f"Migration failed: {e}")
  raise

 finally:
  conn.close()


def get_pending_migrations(cur):
 """適用済みマイグレーション一覧を取得し、未適用分を返す"""
 cur.execute("SELECT version FROM schema_migrations")
 applied = {row[0] for row in cur.fetchall()}
 logger.info(f"Already applied: {applied}")

 all_migrations = [
  {
'version': 'v001_create_users_table',
'sql': """
 CREATE TABLE IF NOT EXISTS app.users (
  idUUIDPRIMARY KEY DEFAULT gen_random_uuid(),
  emailVARCHAR(255) UNIQUE NOT NULL,
  name VARCHAR(255),
  created_at TIMESTAMPTZ  DEFAULT NOW(),
  updated_at TIMESTAMPTZ  DEFAULT NOW()
 )
"""
  },
  {
'version': 'v002_create_orders_table',
'sql': """
 CREATE TABLE IF NOT EXISTS app.orders (
  idUUID  PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID  REFERENCES app.users(id) ON DELETE CASCADE,
  amount  DECIMAL(10, 2) NOT NULL CHECK (amount >= 0),
  status  VARCHAR(50) DEFAULT 'pending',
  created_at TIMESTAMPTZ DEFAULT NOW()
 )
"""
  },
  {
'version': 'v003_add_index_users_email',
'sql': "CREATE INDEX IF NOT EXISTS idx_users_email ON app.users(email)"
  },
  {
'version': 'v004_add_index_orders_user_id',
'sql': "CREATE INDEX IF NOT EXISTS idx_orders_user_id ON app.orders(user_id)"
  },
  {
'version': 'v005_add_index_orders_created_at',
'sql': "CREATE INDEX IF NOT EXISTS idx_orders_created_at ON app.orders(created_at DESC)"
  },
 ]

 return [m for m in all_migrations if m['version'] not in applied]


def apply_migration(cur, migration):
 """マイグレーションを実行し、バージョンテーブルに記録"""
 cur.execute(migration['sql'])
 cur.execute(
  """
  INSERT INTO schema_migrations (version)
  VALUES (%s)
  ON CONFLICT (version) DO NOTHING
  """,
  (migration['version'],)
 )
べき等性の確保方法
schema_migrations テーブルで適用済みバージョンを管理することで、同じ Lambda を何度実行しても安全です。CREATE TABLE IF NOT EXISTSCREATE INDEX IF NOT EXISTS も DDL レベルでのべき等性を保証します。マイグレーションごとにコミットすることで、途中失敗時のリカバリーも容易になります。

8-4. Lambda 関数のデプロイ(コンソール)

① Lambda 関数の ZIP 作成

cd lambda-migration
zip -j migration-function.zip lambda_function.py

② Lambda コンソールで関数を作成

  1. AWS マネジメントコンソールで Lambda を開く
  2. 関数の作成」をクリック
  3. 一から作成」を選択し、以下を入力:
  4. 関数名: aurora-migration
  5. ランタイム: Python 3.12
  6. アーキテクチャ: x86_64
  7. デフォルトの実行ロールの変更」を展開:
  8. 既存のロールを使用する」を選択
  9. 既存のロール: aurora-lambda-role(Phase 2 で作成)
  10. 関数の作成」をクリック

③ コードのアップロード

  1. 関数の「コード」タブを開く
  2. アップロード元」→「.zip ファイル」を選択
  3. migration-function.zip をアップロード
  4. 保存」をクリック

④ VPC 設定

  1. 設定」タブ→「VPC」を選択
  2. 編集」をクリック
  3. 以下を設定:
  4. VPC: aurora-db-vpc(Phase 1 で作成)
  5. サブネット: プライベートサブネット 2 つ(ap-northeast-1aap-northeast-1c
  6. セキュリティグループ: lambda-sg
  7. 保存」をクリック

⑤ 一般設定(タイムアウト変更)

  1. 設定」タブ→「一般設定」→「編集
  2. タイムアウト: 50 秒(デフォルト 3 秒では不足)
  3. メモリ: 256 MB
  4. 保存」をクリック

⑥ 環境変数の設定

  1. 設定」タブ→「環境変数」→「編集
  2. 環境変数を追加」で以下を設定:
キー
RDS_PROXY_HOST{RDS Proxy エンドポイント}(Phase 1 で記録)
DB_USERiam_lambda_user
DB_NAMEapp_database
  1. 保存」をクリック
AWS_REGION は設定不要
AWS_REGION は Lambda 実行環境が自動で設定する予約済み環境変数です。手動で追加しようとするとエラーになります。コード内では os.environ['AWS_REGION'] でそのまま参照できます。

⑦ Layer の追加

  1. コード」タブの最下部にある「レイヤー」セクションを開く
  2. レイヤーの追加」をクリック
  3. カスタムレイヤー」を選択
  4. カスタムレイヤー: psycopg2-aurora
  5. バージョン: 1(8-2 で登録したもの)
  6. 追加」をクリック

8-5. EventBridge Scheduler での定期実行(任意)

マイグレーション Lambda を定期的に自動実行する場合は、EventBridge Scheduler を使います。べき等性が確保されているため、複数回実行しても問題ありません。

Terraform コード(lambda.tf に追記):

# EventBridge Scheduler 用 IAM ロール
resource "aws_iam_role" "scheduler" {
  name = "aurora-migration-scheduler-role"

  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Principal = { Service = "scheduler.amazonaws.com" }
Action = "sts:AssumeRole"
 }]
  })
}

resource "aws_iam_role_policy" "scheduler_invoke" {
  name = "invoke-migration-lambda"
  role = aws_iam_role.scheduler.id

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect= "Allow"
Action= "lambda:InvokeFunction"
Resource = aws_lambda_function.migration.arn
 }]
  })
}

# EventBridge Scheduler(毎日 AM 3:00 に実行)
resource "aws_scheduler_schedule" "migration" {
  name = "aurora-migration-schedule"
  group_name = "default"

  flexible_time_window {
 mode = "OFF"
  }

  # 毎日 JST 3:00(UTC 18:00)
  schedule_expression = "cron(0 18 * * ? *)"
  schedule_expression_timezone = "Asia/Tokyo"

  target {
 arn= aws_lambda_function.migration.arn
 role_arn = aws_iam_role.scheduler.arn
  }
}
EventBridge Scheduler でのべき等実行
マイグレーション Lambda はべき等性が確保されているため、複数回実行しても安全です。schema_migrations テーブルで適用済みバージョンをチェックするため、すでに適用済みの場合は 0 件の変更で正常終了します。定期実行を設定しておくと、新しいマイグレーションを追加した際に自動で適用されます。

9. Phase 5: 動作確認とトラブルシューティング

Lambda 関数のデプロイが完了したら、動作確認を行います。Terraform apply の確認、Lambda のテスト実行、CloudWatch Logs の確認の順に進めます。

9-1. Terraform apply の動作確認

Phase 3 で作成した Terraform コード(postgresql provider)の適用結果を確認します。

確認コマンド:

# 全リソースの状態確認
terraform state list

# 出力値の確認
terraform output

出力例:

aurora_cluster_endpoint = "aurora-cluster.cluster-xxxx.ap-northeast-1.rds.amazonaws.com"
rds_proxy_endpoint= "aurora-proxy.proxy-xxxx.ap-northeast-1.rds.amazonaws.com"

DB 内の状態確認(CloudShell または SSM Port Forwarding 経由):

-- 作成されたデータベース一覧
SELECT datname FROM pg_database WHERE datname = 'app_database';

-- スキーマ一覧
SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'app';

-- ロール一覧(IAM 認証ユーザーを含む)
SELECT rolname, rolcanlogin FROM pg_roles
WHERE rolname LIKE '%app%' OR rolname LIKE '%iam%'
ORDER BY rolname;

-- 権限確認
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee IN ('app_readonly', 'app_readwrite', 'iam_lambda_user')
ORDER BY grantee, table_schema, table_name;

期待する出力:

 datname
-----------
 app_database
(1 row)

 schema_name
-------------
 app
(1 row)

 rolname  | rolcanlogin
----------------------+-------------
 app_readonly| t
 app_readwrite  | t
 iam_lambda_user| t
 iam_terraform_user| t
(4 rows)

9-2. Lambda 関数のテスト実行

Lambda コンソールからテスト:

  1. Lambda コンソールで aurora-migration 関数を開く
  2. テスト」タブを選択
  3. テストイベントを作成」:
  4. イベント名: migration-test
  5. テンプレート: hello-world
  6. イベント JSON: {} のまま
  7. 保存」→「テスト」をクリック

初回実行の期待結果:

{
  "statusCode": 200,
  "body": "Applied 5 migrations successfully"
}

2 回目以降(べき等性確認):

{
  "statusCode": 200,
  "body": "Applied 0 migrations successfully"
}

2 回目以降は全マイグレーションが適用済みとして検出され、0 件の変更で正常終了します。

9-3. CloudWatch Logs での確認

ログの確認手順:

  1. Lambda コンソール → 「モニタリング」タブ
  2. CloudWatch のログを表示」をクリック
  3. 最新のログストリームを開く

期待するログ出力(初回):

INFO Migration Lambda started
INFO Already applied: set()
INFO Pending migrations: 5
INFO Applied migration: v001_create_users_table
INFO Applied migration: v002_create_orders_table
INFO Applied migration: v003_add_index_users_email
INFO Applied migration: v004_add_index_orders_user_id
INFO Applied migration: v005_add_index_orders_created_at

DB でのテーブル確認:

-- マイグレーション適用履歴
SELECT version, applied_at, applied_by
FROM schema_migrations
ORDER BY applied_at;

-- テーブル一覧
SELECT tablename, tableowner
FROM pg_tables
WHERE schemaname = 'app'
ORDER BY tablename;

-- インデックス一覧
SELECT indexname, tablename, indexdef
FROM pg_indexes
WHERE schemaname = 'app'
ORDER BY tablename, indexname;

期待する出力:

  version| applied_at  | applied_by
------------------------------------+-------------------------------+--------------
 v001_create_users_table| 2026-04-14 09:00:01.123+00 | iam_lambda_user
 v002_create_orders_table  | 2026-04-14 09:00:01.456+00 | iam_lambda_user
 v003_add_index_users_email| 2026-04-14 09:00:01.789+00 | iam_lambda_user
 v004_add_index_orders_user_id| 2026-04-14 09:00:02.012+00 | iam_lambda_user
 v005_add_index_orders_created_at| 2026-04-14 09:00:02.234+00 | iam_lambda_user
(5 rows)

 tablename | tableowner
-----------+--------------
 orders | iam_lambda_user
 users  | iam_lambda_user
(2 rows)

9-4. よくあるエラーと対処法

エラー 1: IAM 認証トークン期限切れ

FATAL: PAM authentication failed for user "iam_lambda_user"

原因: IAM 認証トークンの有効期限(15 分)が切れている。Lambda 関数内でトークンをキャッシュしている場合に発生します。

対処法:

トークンは毎接続前に生成すること
Lambda 関数のグローバルスコープ(ハンドラー外)でトークンを生成すると、同一コンテナの再利用時に期限切れトークンが使われます。get_connection() 関数内で毎回 generate_db_auth_token() を呼び出す設計にしてください。コネクションプールは使わず、ハンドラー実行ごとに接続・切断する設計が安全です。
# NG: グローバルスコープでトークン生成(期限切れになる)
token = rds_client.generate_db_auth_token(...)  # Lambda コンテナ再利用時に15分で無効化

# OK: ハンドラー内または接続関数内で毎回生成
def get_connection():
 token = rds_client.generate_db_auth_token(...)  # 毎回新規生成
 return psycopg2.connect(password=token, ...)

エラー 2: セキュリティグループの設定ミス

[Errno 110] Connection timed out

原因: Lambda のセキュリティグループ(lambda-sg)から RDS Proxy のセキュリティグループ(rds-proxy-sg)へのポート 5432 通信が許可されていない。

対処法:

# lambda-sg の ID を確認
aws ec2 describe-security-groups \
  --filters Name=group-name,Values=lambda-sg \
  --query 'SecurityGroups[0].GroupId' \
  --output text

# rds-proxy-sg の ID を確認
aws ec2 describe-security-groups \
  --filters Name=group-name,Values=rds-proxy-sg \
  --query 'SecurityGroups[0].GroupId' \
  --output text

# rds-proxy-sg に inbound ルールを追加(lambda-sg からのポート 5432)
aws ec2 authorize-security-group-ingress \
  --group-id <rds-proxy-sg-id> \
  --protocol tcp \
  --port 5432 \
  --source-group <lambda-sg-id>
セキュリティグループのルール確認
RDS Proxy は Aurora クラスターのセキュリティグループとは別のセキュリティグループを持ちます。Lambda → RDS Proxy → Aurora の通信経路で、それぞれのセキュリティグループにインバウンドルールが必要です:
rds-proxy-sg: lambda-sg からのポート 5432 を許可
aurora-sg: rds-proxy-sg からのポート 5432 を許可

エラー 3: RDS Proxy の IAM 認証拒否

SSL connection required for IAM authentication

原因: psycopg2.connect()sslmode='require' が設定されていない。IAM 認証では TLS が必須です。

対処法: 接続コードに sslmode='require' を追加します。

conn = psycopg2.connect(
 host=os.environ['RDS_PROXY_HOST'],
 port=5432,
 user=os.environ['DB_USER'],
 password=token,
 database=os.environ['DB_NAME'],
 sslmode='require'  # 必須: IAM 認証には TLS が必要
)

エラー 4: Terraform postgresql provider 接続エラー

Error: Error connecting to PostgreSQL server
  on postgresql.tf line 1, in provider "postgresql":
  Configuration for provider "postgresql" is invalid

原因: Terraform 実行環境の IAM ロールに rds-db:connect 権限がない。

対処法:

# Terraform 実行ロールのポリシーを確認
aws iam list-attached-role-policies --role-name aurora-terraform-role

# 権限がない場合はポリシーをアタッチ
aws iam attach-role-policy \
  --role-name aurora-terraform-role \
  --policy-arn arn:aws:iam::{ACCOUNT_ID}:policy/aurora-iam-db-access

aurora-iam-db-access ポリシーの内容(Phase 2 で作成):

{
 "Version": "2012-10-17",
 "Statement": [{
  "Effect": "Allow",
  "Action": "rds-db:connect",
  "Resource": "arn:aws:rds-db:ap-northeast-1:{ACCOUNT_ID}:dbuser:*/*"
 }]
}

エラー 5: VPC Lambda タイムアウト(STS API 疎通不可)

Task timed out after 300.00 seconds

ログには以下のようなメッセージ:

botocore.exceptions.EndpointConnectionError: Could not connect to the endpoint URL: "https://sts.ap-northeast-1.amazonaws.com/"

原因: VPC 内の Lambda が STS API(IAM 認証トークン生成に必要)に接続できない。NAT Gateway もなく、VPC エンドポイントも未設定の場合に発生します。

対処法: STS 用の VPC エンドポイントを作成します。

# VPC ID を取得
VPC_ID=$(aws ec2 describe-vpcs \
  --filters Name=tag:Name,Values=aurora-db-vpc \
  --query 'Vpcs[0].VpcId' \
  --output text)

# プライベートサブネット ID を取得
SUBNET_IDS=$(aws ec2 describe-subnets \
  --filters Name=vpc-id,Values=$VPC_ID Name=tag:Type,Values=private \
  --query 'Subnets[*].SubnetId' \
  --output text | tr '\t' ',')

# STS VPC エンドポイント作成
aws ec2 create-vpc-endpoint \
  --vpc-id $VPC_ID \
  --service-name com.amazonaws.ap-northeast-1.sts \
  --vpc-endpoint-type Interface \
  --subnet-ids $SUBNET_IDS \
  --security-group-ids <lambda-sg-id>
VPC Lambda に必要な VPC エンドポイント一覧
NAT Gateway を使わない場合、以下の VPC エンドポイントが必要です:
com.amazonaws.ap-northeast-1.sts — IAM 認証トークン生成
com.amazonaws.ap-northeast-1.secretsmanager — Secrets Manager 参照(必要な場合)
com.amazonaws.ap-northeast-1.logs — CloudWatch Logs 書き込み
com.amazonaws.ap-northeast-1.lambda — Lambda 関数の呼び出し(別 Lambda を呼ぶ場合)

9-5. CloudWatch Metrics でのモニタリング

Lambda + Aurora の動作状態を確認するために、以下のメトリクスを監視します。

Lambda メトリクス(CloudWatch → Lambda → aurora-migration):

メトリクス確認内容正常値の目安
Duration実行時間初回: 5〜15 秒、2 回目以降: 1〜3 秒
Errorsエラー数0
Throttlesスロットリング数0
ConcurrentExecutions同時実行数1(マイグレーションは直列実行推奨)

RDS Proxy メトリクス(CloudWatch → RDS → aurora-proxy):

メトリクス確認内容正常値の目安
DatabaseConnectionsDB 接続数Lambda 実行中: 1、アイドル: 0
ClientConnectionsクライアント接続数Lambda 実行中: 1
QueryCountクエリ実行数マイグレーション数 + α
FailedConnections失敗接続数0

Aurora メトリクス(CloudWatch → RDS → aurora-cluster):

メトリクス確認内容正常値の目安
ServerlessDatabaseCapacityACU(処理能力単位)0.5〜2 ACU(アイドル〜軽負荷)
DatabaseConnectionsDB 接続数通常 1〜3(RDS Proxy 経由)
CPUUtilizationCPU 使用率通常 10% 以下
CloudWatch Alarm の設定推奨
本番環境では以下のアラームを設定することを推奨します:
Lambda Errors > 0 → SNS 通知(マイグレーション失敗の即時検知)
RDS Proxy FailedConnections > 0 → SNS 通知(IAM 認証失敗の検知)
Aurora CPUUtilization > 80% → SNS 通知(リソース不足の検知)

9-6. 動作確認完了の確認チェックリスト

Phase 4〜5 の動作確認が完了したら、以下の項目をすべて確認してください。

Terraform postgresql provider(Phase 3):

  • [ ] terraform state list で postgresql_* リソースが表示される
  • [ ] app_database データベースが作成されている
  • [ ] app スキーマが作成されている
  • [ ] app_readonlyapp_readwriteiam_lambda_useriam_terraform_user ロールが存在する
  • [ ] GRANT 権限が正しく設定されている(information_schema.role_table_grants で確認)

Lambda + psycopg2(Phase 4〜5):

  • [ ] Lambda 関数 aurora-migration がデプロイされている
  • [ ] VPC 設定(プライベートサブネット + lambda-sg)が正しい
  • [ ] 環境変数(RDS_PROXY_HOSTDB_USERDB_NAME)が設定されている
  • [ ] Layer(psycopg2-aurora:1)が追加されている
  • [ ] テスト実行で Applied 5 migrations successfully が返る
  • [ ] 2 回目のテスト実行で Applied 0 migrations successfully が返る(べき等性)
  • [ ] CloudWatch Logs にマイグレーション実行ログが出力されている
  • [ ] schema_migrations テーブルに 5 件のレコードが存在する
  • [ ] app.usersapp.orders テーブルが作成されている
  • [ ] インデックスが 3 件作成されている(pg_indexes で確認)

Section 10: Phase 6 — Terraform IaC 全リソース

Section 5〜9のコンソール・手動ハンズオンで構築した全リソースを、Terraform で再現可能な IaC として整理します。terraform apply 一発で Aurora PostgreSQL Serverless v2 + RDS Proxy + IAM認証 + Lambdaマイグレーション環境を構築できます。

📋 この章の前提

Terraform 1.5以上・AWS CLI v2設定済み・必要なIAM権限(VPC/RDS/Lambda/IAM/SecretsManager の作成・管理権限)が必要です。コンソールハンズオン(Section 5〜9)を先に読んでおくと各リソースの役割を理解しやすくなります。


10-1. ディレクトリ構成

プロジェクトディレクトリ terraform/ を作成し、以下の構成にします。

terraform/
├── providers.tf # Providerバージョン固定 + PostgreSQL Provider設定
├── variables.tf # 変数定義
├── vpc.tf # ネットワーク基盤(VPC/サブネット/NAT/SG/VPCエンドポイント)
├── aurora.tf # Aurora PostgreSQL Serverless v2 + Secrets Manager
├── rds_proxy.tf # RDS Proxy(IAM認証 REQUIRED)
├── iam.tf # IAMロール/ポリシー(RDS Proxy用・Lambda用・Terraform PG用)
├── postgresql.tf# cyrilgdn/postgresql provider(DB/スキーマ/ロール/権限)
├── lambda.tf # マイグレーションLambda + psycopg2 Layer + EventBridge Scheduler
└── outputs.tf# 出力値
mkdir terraform
cd terraform

10-2. providers.tf — Provider設定

terraform {
  required_version = ">= 1.5.0"
  required_providers {
 aws = {
source  = "hashicorp/aws"
version = "~> 5.0"
 }
 postgresql = {
source  = "cyrilgdn/postgresql"
version = "~> 1.26.0"
 }
  }
}

provider "aws" {
  region = var.aws_region
}

# PostgreSQL Providerは RDS Proxy エンドポイント経由で IAM認証接続
# Aurora クラスターが起動済み・RDS Proxyが作成済みの状態で初めて有効になる
provider "postgresql" {
  scheme = "awspostgres"
  host= aws_db_proxy.main.endpoint
  port= 5432
  database  = "postgres"
  username  = "iam_terraform_user"
  superuser = false
  aws_rds_iam_auth= true
  aws_rds_iam_region = var.aws_region
}
⚠️ postgresql provider の初期適用について

providers.tfprovider "postgresql" ブロックは、Aurora クラスターおよび RDS Proxy の作成後でないと接続できません。初回デプロイは -target オプションで段階的に適用してください(10-9 デプロイ手順を参照)。


10-3. variables.tf — 変数定義

variable "aws_region" {
  description = "AWSリージョン"
  default  = "ap-northeast-1"
}

variable "project_name" {
  description = "プロジェクト名(リソース名プレフィックス)"
  default  = "aurora-no-human-login"
}

variable "vpc_cidr" {
  description = "VPC CIDR ブロック"
  default  = "10.0.0.0/16"
}

variable "db_name" {
  description = "Aurora クラスターのデータベース名"
  default  = "app_database"
}

variable "aurora_master_username" {
  description = "Aurora マスターユーザー名"
  default  = "postgres"
  sensitive= true
}

variable "aurora_master_password" {
  description = "Aurora マスターパスワード(terraform.tfvars で管理)"
  sensitive= true
}
⚠️ aurora_master_password の管理

aurora_master_passwordterraform.tfvars に記載し、.gitignore に追加してください。本番環境では AWS Secrets Manager または HashiCorp Vault 経由での管理を推奨します。

terraform.tfvars.gitignore に追加すること):

aurora_master_password = "YourSecurePassword123!"

.gitignore:

*.tfvars
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl

10-4. vpc.tf — ネットワーク基盤

VPC・サブネット(パブリック×2 / プライベート×2)・NAT Gateway・セキュリティグループ・VPCエンドポイントを定義します。

# ─────────────────────────────────────────
# データソース: AZリスト
# ─────────────────────────────────────────
data "aws_availability_zones" "available" {
  state = "available"
}

# ─────────────────────────────────────────
# VPC
# ─────────────────────────────────────────
resource "aws_vpc" "main" {
  cidr_block  = var.vpc_cidr
  enable_dns_support= true
  enable_dns_hostnames = true

  tags = {
 Name = "${var.project_name}-vpc"
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# サブネット(パブリック × 2)
# ─────────────────────────────────────────
resource "aws_subnet" "public_a" {
  vpc_id= aws_vpc.main.id
  cidr_block  = "10.0.1.0/24"
  availability_zone = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true

  tags = {
 Name = "${var.project_name}-public-a"
 Project = var.project_name
  }
}

resource "aws_subnet" "public_c" {
  vpc_id= aws_vpc.main.id
  cidr_block  = "10.0.2.0/24"
  availability_zone = data.aws_availability_zones.available.names[1]
  map_public_ip_on_launch = true

  tags = {
 Name = "${var.project_name}-public-c"
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# サブネット(プライベート × 2)
# ─────────────────────────────────────────
resource "aws_subnet" "private_a" {
  vpc_id= aws_vpc.main.id
  cidr_block  = "10.0.10.0/24"
  availability_zone = data.aws_availability_zones.available.names[0]

  tags = {
 Name = "${var.project_name}-private-a"
 Project = var.project_name
  }
}

resource "aws_subnet" "private_c" {
  vpc_id= aws_vpc.main.id
  cidr_block  = "10.0.11.0/24"
  availability_zone = data.aws_availability_zones.available.names[1]

  tags = {
 Name = "${var.project_name}-private-c"
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# Internet Gateway
# ─────────────────────────────────────────
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
 Name = "${var.project_name}-igw"
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# NAT Gateway(パブリックサブネット a に配置)
# ─────────────────────────────────────────
resource "aws_eip" "nat" {
  domain = "vpc"

  tags = {
 Name = "${var.project_name}-nat-eip"
 Project = var.project_name
  }
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id  = aws_subnet.public_a.id

  tags = {
 Name = "${var.project_name}-nat"
 Project = var.project_name
  }

  depends_on = [aws_internet_gateway.main]
}

# ─────────────────────────────────────────
# ルートテーブル(パブリック)
# ─────────────────────────────────────────
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
 cidr_block = "0.0.0.0/0"
 gateway_id = aws_internet_gateway.main.id
  }

  tags = {
 Name = "${var.project_name}-rt-public"
 Project = var.project_name
  }
}

resource "aws_route_table_association" "public_a" {
  subnet_id= aws_subnet.public_a.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_c" {
  subnet_id= aws_subnet.public_c.id
  route_table_id = aws_route_table.public.id
}

# ─────────────────────────────────────────
# ルートテーブル(プライベート)
# ─────────────────────────────────────────
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
 cidr_block  = "0.0.0.0/0"
 nat_gateway_id = aws_nat_gateway.main.id
  }

  tags = {
 Name = "${var.project_name}-rt-private"
 Project = var.project_name
  }
}

resource "aws_route_table_association" "private_a" {
  subnet_id= aws_subnet.private_a.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private_c" {
  subnet_id= aws_subnet.private_c.id
  route_table_id = aws_route_table.private.id
}

# ─────────────────────────────────────────
# セキュリティグループ: Aurora
# ─────────────────────────────────────────
resource "aws_security_group" "aurora" {
  name  = "${var.project_name}-aurora-sg"
  description = "Aurora PostgreSQL SG — RDS Proxy and Lambda only"
  vpc_id= aws_vpc.main.id

  tags = {
 Name = "${var.project_name}-aurora-sg"
 Project = var.project_name
  }
}

resource "aws_security_group_rule" "aurora_ingress_proxy" {
  type= "ingress"
  from_port = 5432
  to_port= 5432
  protocol  = "tcp"
  security_group_id  = aws_security_group.aurora.id
  source_security_group_id = aws_security_group.rds_proxy.id
  description  = "Allow from RDS Proxy"
}

resource "aws_security_group_rule" "aurora_ingress_lambda" {
  type= "ingress"
  from_port = 5432
  to_port= 5432
  protocol  = "tcp"
  security_group_id  = aws_security_group.aurora.id
  source_security_group_id = aws_security_group.lambda.id
  description  = "Allow from Lambda migration"
}

resource "aws_security_group_rule" "aurora_egress" {
  type  = "egress"
  from_port= 0
  to_port  = 0
  protocol = "-1"
  cidr_blocks = ["0.0.0.0/0"]
  security_group_id = aws_security_group.aurora.id
}

# ─────────────────────────────────────────
# セキュリティグループ: RDS Proxy
# ─────────────────────────────────────────
resource "aws_security_group" "rds_proxy" {
  name  = "${var.project_name}-rds-proxy-sg"
  description = "RDS Proxy SG — Lambda and Terraform PG provider"
  vpc_id= aws_vpc.main.id

  tags = {
 Name = "${var.project_name}-rds-proxy-sg"
 Project = var.project_name
  }
}

resource "aws_security_group_rule" "rds_proxy_ingress_lambda" {
  type= "ingress"
  from_port = 5432
  to_port= 5432
  protocol  = "tcp"
  security_group_id  = aws_security_group.rds_proxy.id
  source_security_group_id = aws_security_group.lambda.id
  description  = "Allow from Lambda"
}

resource "aws_security_group_rule" "rds_proxy_egress" {
  type  = "egress"
  from_port= 0
  to_port  = 0
  protocol = "-1"
  cidr_blocks = ["0.0.0.0/0"]
  security_group_id = aws_security_group.rds_proxy.id
}

# ─────────────────────────────────────────
# セキュリティグループ: Lambda
# ─────────────────────────────────────────
resource "aws_security_group" "lambda" {
  name  = "${var.project_name}-lambda-sg"
  description = "Lambda migration function SG"
  vpc_id= aws_vpc.main.id

  tags = {
 Name = "${var.project_name}-lambda-sg"
 Project = var.project_name
  }
}

resource "aws_security_group_rule" "lambda_egress" {
  type  = "egress"
  from_port= 0
  to_port  = 0
  protocol = "-1"
  cidr_blocks = ["0.0.0.0/0"]
  security_group_id = aws_security_group.lambda.id
}

# ─────────────────────────────────────────
# VPCエンドポイント: STS(IAM認証トークン生成)
# ─────────────────────────────────────────
resource "aws_vpc_endpoint" "sts" {
  vpc_id  = aws_vpc.main.id
  service_name  = "com.amazonaws.${var.aws_region}.sts"
  vpc_endpoint_type= "Interface"
  subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_c.id]
  security_group_ids  = [aws_security_group.lambda.id]
  private_dns_enabled = true

  tags = {
 Name = "${var.project_name}-sts-endpoint"
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# VPCエンドポイント: Secrets Manager
# ─────────────────────────────────────────
resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id  = aws_vpc.main.id
  service_name  = "com.amazonaws.${var.aws_region}.secretsmanager"
  vpc_endpoint_type= "Interface"
  subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_c.id]
  security_group_ids  = [aws_security_group.lambda.id]
  private_dns_enabled = true

  tags = {
 Name = "${var.project_name}-sm-endpoint"
 Project = var.project_name
  }
}

10-5. aurora.tf — Aurora PostgreSQL Serverless v2

# ─────────────────────────────────────────
# DB サブネットグループ
# ─────────────────────────────────────────
resource "aws_db_subnet_group" "main" {
  name = "${var.project_name}-subnet-group"
  subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_c.id]

  tags = {
 Name = "${var.project_name}-subnet-group"
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# Aurora クラスター(Serverless v2)
# ─────────────────────────────────────────
resource "aws_rds_cluster" "main" {
  cluster_identifier= "${var.project_name}-cluster"
  engine= "aurora-postgresql"
  engine_version = "16.4"
  engine_mode = "provisioned"
  database_name  = var.db_name
  master_username= var.aurora_master_username
  master_password= var.aurora_master_password
  db_subnet_group_name = aws_db_subnet_group.main.name
  vpc_security_group_ids  = [aws_security_group.aurora.id]
  skip_final_snapshot  = true
  deletion_protection  = false
  storage_encrypted = true

  # IAM データベース認証を有効化(パスワードレス接続の基盤)
  iam_database_authentication_enabled = true

  serverlessv2_scaling_configuration {
 min_capacity = 0.5
 max_capacity = 16
  }

  tags = {
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# Aurora クラスターインスタンス(Writer)
# ─────────────────────────────────────────
resource "aws_rds_cluster_instance" "writer" {
  cluster_identifier = aws_rds_cluster.main.id
  instance_class  = "db.serverless"
  engine = aws_rds_cluster.main.engine
  engine_version  = aws_rds_cluster.main.engine_version
  identifier= "${var.project_name}-writer"

  tags = {
 Project = var.project_name
  }
}

# ─────────────────────────────────────────
# Secrets Manager(マスター認証情報)
# RDS Proxy が参照するシークレット
# ─────────────────────────────────────────
resource "aws_secretsmanager_secret" "aurora_master" {
  name  = "${var.project_name}/master"
  recovery_window_in_days = 0  # ハンズオン用: 即時削除を許可

  tags = {
 Project = var.project_name
  }
}

resource "aws_secretsmanager_secret_version" "aurora_master" {
  secret_id = aws_secretsmanager_secret.aurora_master.id
  secret_string = jsonencode({
 host  = aws_rds_cluster.main.endpoint
 port  = 5432
 username = var.aurora_master_username
 password = var.aurora_master_password
 dbname= var.db_name
  })
}

10-6. rds_proxy.tf — RDS Proxy(IAM認証 REQUIRED)

# ─────────────────────────────────────────
# RDS Proxy
# ─────────────────────────────────────────
resource "aws_db_proxy" "main" {
  name = "${var.project_name}-proxy"
  debug_logging = false
  engine_family = "POSTGRESQL"
  idle_client_timeout = 1800
  require_tls= true
  role_arn= aws_iam_role.rds_proxy.arn
  vpc_security_group_ids = [aws_security_group.rds_proxy.id]
  vpc_subnet_ids= [aws_subnet.private_a.id, aws_subnet.private_c.id]

  auth {
 auth_scheme = "SECRETS"
 description = "Aurora master credentials via Secrets Manager"
 iam_auth = "REQUIRED"  # IAM認証を強制(パスワード直接接続を禁止)
 secret_arn  = aws_secretsmanager_secret.aurora_master.arn
  }

  tags = {
 Project = var.project_name
  }

  depends_on = [aws_rds_cluster_instance.writer]
}

# ─────────────────────────────────────────
# RDS Proxy デフォルトターゲットグループ
# ─────────────────────────────────────────
resource "aws_db_proxy_default_target_group" "main" {
  db_proxy_name = aws_db_proxy.main.name

  connection_pool_config {
 connection_borrow_timeout = 120
 max_connections_percent= 100
 max_idle_connections_percent = 50
  }
}

# ─────────────────────────────────────────
# RDS Proxy ターゲット(Aurora クラスター)
# ─────────────────────────────────────────
resource "aws_db_proxy_target" "main" {
  db_cluster_identifier = aws_rds_cluster.main.id
  db_proxy_name= aws_db_proxy.main.name
  target_group_name  = aws_db_proxy_default_target_group.main.name
}

10-7. iam.tf — IAMロール / ポリシー

data "aws_caller_identity" "current" {}

# ─────────────────────────────────────────
# IAMロール: RDS Proxy 用(Secrets Manager読み取り)
# ─────────────────────────────────────────
resource "aws_iam_role" "rds_proxy" {
  name = "${var.project_name}-rds-proxy-role"

  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Principal = { Service = "rds.amazonaws.com" }
Action = "sts:AssumeRole"
 }]
  })

  tags = {
 Project = var.project_name
  }
}

resource "aws_iam_policy" "rds_proxy_secrets" {
  name = "${var.project_name}-rds-proxy-secrets-policy"

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect= "Allow"
Action= ["secretsmanager:GetSecretValue"]
Resource = aws_secretsmanager_secret.aurora_master.arn
 }]
  })
}

resource "aws_iam_role_policy_attachment" "rds_proxy_secrets" {
  role = aws_iam_role.rds_proxy.name
  policy_arn = aws_iam_policy.rds_proxy_secrets.arn
}

# ─────────────────────────────────────────
# IAMロール: Lambda マイグレーション用
# ─────────────────────────────────────────
resource "aws_iam_role" "lambda_migration" {
  name = "${var.project_name}-lambda-migration-role"

  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
 }]
  })

  tags = {
 Project = var.project_name
  }
}

# VPC Lambda 基本実行権限
resource "aws_iam_role_policy_attachment" "lambda_vpc_execution" {
  role = aws_iam_role.lambda_migration.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

# Aurora IAM認証: rds-db:connect 権限
resource "aws_iam_policy" "aurora_iam_connect_lambda" {
  name = "${var.project_name}-lambda-aurora-connect-policy"

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Action = ["rds-db:connect"]
Resource = [
  "arn:aws:rds-db:${var.aws_region}:${data.aws_caller_identity.current.account_id}:dbuser:${aws_rds_cluster.main.cluster_resource_id}/iam_lambda_user"
]
 }]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_aurora_connect" {
  role = aws_iam_role.lambda_migration.name
  policy_arn = aws_iam_policy.aurora_iam_connect_lambda.arn
}

# ─────────────────────────────────────────
# IAMロール: Terraform postgresql provider 用
# terraform apply 実行者のロールに rds-db:connect を付与
# ─────────────────────────────────────────
resource "aws_iam_policy" "aurora_iam_connect_terraform" {
  name = "${var.project_name}-terraform-pg-connect-policy"

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Action = ["rds-db:connect"]
Resource = [
  "arn:aws:rds-db:${var.aws_region}:${data.aws_caller_identity.current.account_id}:dbuser:${aws_rds_cluster.main.cluster_resource_id}/iam_terraform_user"
]
 }]
  })
}

# terraform apply 実行ユーザー/ロールにアタッチ
# 実際の環境では適切なユーザーまたはロールにアタッチしてください
resource "aws_iam_role_policy_attachment" "terraform_pg_connect" {
  role = aws_iam_role.lambda_migration.name  # 本番では専用ロールを使用
  policy_arn = aws_iam_policy.aurora_iam_connect_terraform.arn
}

10-8. postgresql.tf — スキーマ / ロール / 権限管理(cyrilgdn/postgresql)

📋 適用タイミング

このファイルは Aurora クラスターおよび RDS Proxy が稼働済みの状態でのみ適用できます。また、IAM認証用 DB ユーザー iam_terraform_user が事前に作成されている必要があります(Section 6 参照)。

# ─────────────────────────────────────────
# データベース作成
# ─────────────────────────────────────────
resource "postgresql_database" "app_db" {
  name  = var.db_name
  owner = "postgres"
  encoding = "UTF8"
  lc_collate  = "en_US.UTF-8"
  lc_ctype = "en_US.UTF-8"
  connection_limit  = -1
  allow_connections = true
}

# ─────────────────────────────────────────
# ロール定義(アプリケーション用)
# ─────────────────────────────────────────

# オーナーロール(DDL実行・テーブル所有)
resource "postgresql_role" "app_owner" {
  name = "app_owner"
  login= false
  inherit = true
  connection_limit = -1
}

# ライタロール(INSERT/UPDATE/DELETE)
resource "postgresql_role" "app_writer" {
  name = "app_writer"
  login= false
  inherit = true
  connection_limit = -1
}

# リーダーロール(SELECT のみ)
resource "postgresql_role" "app_reader" {
  name = "app_reader"
  login= false
  inherit = true
  connection_limit = -1
}

# IAM認証ユーザー(Lambdaアプリ用)— app_writer を継承
resource "postgresql_role" "iam_lambda_user" {
  name = "iam_lambda_user"
  login= true
  inherit = true
  connection_limit = 10
  roles= [postgresql_role.app_writer.name]
}

# IAM認証ユーザー(読み取り専用アプリ用)— app_reader を継承
resource "postgresql_role" "iam_readonly_user" {
  name = "iam_readonly_user"
  login= true
  inherit = true
  connection_limit = 10
  roles= [postgresql_role.app_reader.name]
}

# ─────────────────────────────────────────
# スキーマ作成
# ─────────────────────────────────────────
resource "postgresql_schema" "app" {
  name = "app"
  owner= postgresql_role.app_owner.name
  database= postgresql_database.app_db.name
  drop_cascade  = false
}

# ─────────────────────────────────────────
# 権限付与(GRANT)
# ─────────────────────────────────────────

# app_writer: スキーマ USAGE + CREATE
resource "postgresql_grant" "writer_schema" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_writer.name
  schema= postgresql_schema.app.name
  object_type = "schema"
  privileges  = ["USAGE", "CREATE"]
}

# app_reader: スキーマ USAGE
resource "postgresql_grant" "reader_schema" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_reader.name
  schema= postgresql_schema.app.name
  object_type = "schema"
  privileges  = ["USAGE"]
}

# app_writer: 既存テーブルへの DML 権限
resource "postgresql_grant" "writer_tables" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_writer.name
  schema= postgresql_schema.app.name
  object_type = "table"
  privileges  = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

# app_reader: 既存テーブルへの SELECT 権限
resource "postgresql_grant" "reader_tables" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_reader.name
  schema= postgresql_schema.app.name
  object_type = "table"
  privileges  = ["SELECT"]
}

# app_writer: シーケンスへの権限(SERIAL/BIGSERIAL カラム用)
resource "postgresql_grant" "writer_sequences" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_writer.name
  schema= postgresql_schema.app.name
  object_type = "sequence"
  privileges  = ["USAGE", "SELECT", "UPDATE"]
}

# ─────────────────────────────────────────
# デフォルト権限(将来作成されるオブジェクトへの自動付与)
# ─────────────────────────────────────────

# app_owner が作成したテーブルを app_writer が自動で DML 可能
resource "postgresql_default_privileges" "writer_tables" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_writer.name
  owner = postgresql_role.app_owner.name
  schema= postgresql_schema.app.name
  object_type = "table"
  privileges  = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

# app_owner が作成したテーブルを app_reader が自動で SELECT 可能
resource "postgresql_default_privileges" "reader_tables" {
  database = postgresql_database.app_db.name
  role  = postgresql_role.app_reader.name
  owner = postgresql_role.app_owner.name
  schema= postgresql_schema.app.name
  object_type = "table"
  privileges  = ["SELECT"]
}

# ─────────────────────────────────────────
# 拡張機能(Extension)
# ─────────────────────────────────────────
resource "postgresql_extension" "uuid_ossp" {
  name  = "uuid-ossp"
  database = postgresql_database.app_db.name
  schema= "public"
}

resource "postgresql_extension" "pgcrypto" {
  name  = "pgcrypto"
  database = postgresql_database.app_db.name
  schema= "public"
}

10-9. lambda.tf — マイグレーション Lambda

# ─────────────────────────────────────────
# Lambda レイヤー: psycopg2
# ─────────────────────────────────────────
# 事前にビルドしたpsycopg2のzipを lambda_layer/ に配置してください
# ビルド方法: Section 8-2 参照
data "archive_file" "psycopg2_layer" {
  type  = "zip"
  source_dir  = "${path.module}/../lambda_layer/psycopg2"
  output_path = "${path.module}/.terraform/psycopg2_layer.zip"
}

resource "aws_lambda_layer_version" "psycopg2" {
  layer_name = "${var.project_name}-psycopg2"
  filename= data.archive_file.psycopg2_layer.output_path
  source_code_hash = data.archive_file.psycopg2_layer.output_base64sha256
  compatible_runtimes = ["python3.12"]
  description= "psycopg2-binary for PostgreSQL connection"
}

# ─────────────────────────────────────────
# Lambda 関数コードのアーカイブ
# ─────────────────────────────────────────
data "archive_file" "lambda" {
  type  = "zip"
  source_dir  = "${path.module}/../lambda_src"
  output_path = "${path.module}/.terraform/lambda_migration.zip"
}

# ─────────────────────────────────────────
# Lambda 関数: マイグレーション
# ─────────────────────────────────────────
resource "aws_lambda_function" "migration" {
  function_name = "${var.project_name}-migration"
  role = aws_iam_role.lambda_migration.arn
  filename= data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  handler = "migration.lambda_handler"
  runtime = "python3.12"
  timeout = 300
  memory_size= 256
  layers  = [aws_lambda_layer_version.psycopg2.arn]

  vpc_config {
 subnet_ids= [aws_subnet.private_a.id, aws_subnet.private_c.id]
 security_group_ids = [aws_security_group.lambda.id]
  }

  environment {
 variables = {
RDS_PROXY_HOST = aws_db_proxy.main.endpoint
DB_USER  = "iam_lambda_user"
DB_NAME  = var.db_name
AWS_REGION_NAME = var.aws_region
 }
  }

  tags = {
 Project = var.project_name
  }

  depends_on = [
 aws_iam_role_policy_attachment.lambda_vpc_execution,
 aws_iam_role_policy_attachment.lambda_aurora_connect,
  ]
}

# ─────────────────────────────────────────
# EventBridge Scheduler: 定期マイグレーション
# ─────────────────────────────────────────
resource "aws_iam_role" "scheduler" {
  name = "${var.project_name}-scheduler-role"

  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Principal = { Service = "scheduler.amazonaws.com" }
Action = "sts:AssumeRole"
 }]
  })
}

resource "aws_iam_role_policy" "scheduler_lambda" {
  name = "invoke-migration-lambda"
  role = aws_iam_role.scheduler.id

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect= "Allow"
Action= ["lambda:InvokeFunction"]
Resource = aws_lambda_function.migration.arn
 }]
  })
}

resource "aws_scheduler_schedule" "migration_trigger" {
  name = "${var.project_name}-migration-schedule"
  schedule_expression = "rate(1 day)"
  schedule_expression_timezone = "Asia/Tokyo"

  flexible_time_window {
 mode = "OFF"
  }

  target {
 arn= aws_lambda_function.migration.arn
 role_arn = aws_iam_role.scheduler.arn

 input = jsonencode({
action = "migrate"
 })
  }
}

10-10. outputs.tf — 出力値

output "aurora_writer_endpoint" {
  description = "Aurora クラスター Writer エンドポイント"
  value = aws_rds_cluster.main.endpoint
}

output "aurora_reader_endpoint" {
  description = "Aurora クラスター Reader エンドポイント"
  value = aws_rds_cluster.main.reader_endpoint
}

output "aurora_cluster_resource_id" {
  description = "Aurora クラスターリソースID(IAMポリシーで使用)"
  value = aws_rds_cluster.main.cluster_resource_id
}

output "rds_proxy_endpoint" {
  description = "RDS Proxy エンドポイント(アプリケーション接続先)"
  value = aws_db_proxy.main.endpoint
}

output "migration_lambda_arn" {
  description = "マイグレーション Lambda ARN"
  value = aws_lambda_function.migration.arn
}

output "migration_lambda_name" {
  description = "マイグレーション Lambda 関数名"
  value = aws_lambda_function.migration.function_name
}

output "secrets_manager_arn" {
  description = "Aurora マスター認証情報 Secrets Manager ARN"
  value = aws_secretsmanager_secret.aurora_master.arn
}

10-11. デプロイ手順

Aurora クラスターと cyrilgdn/postgresql provider の依存関係があるため、2段階で適用します。

Phase A: AWS インフラ基盤の構築

cd terraform/

# 初期化(プロバイダーのダウンロード)
terraform init

# プラン確認(postgresql リソースを除外)
terraform plan \
  -var="aurora_master_password=YourSecurePassword123!" \
  -target=aws_vpc.main \
  -target=aws_rds_cluster.main \
  -target=aws_rds_cluster_instance.writer \
  -target=aws_db_proxy.main \
  -target=aws_lambda_function.migration

# 適用(VPC/Aurora/RDS Proxy/Lambda)
terraform apply \
  -var="aurora_master_password=YourSecurePassword123!" \
  -target=aws_vpc.main \
  -target=aws_rds_cluster.main \
  -target=aws_rds_cluster_instance.writer \
  -target=aws_db_proxy.main \
  -target=aws_lambda_function.migration
⚠️ RDS Proxy の作成待ち

RDS Proxy の作成には 5〜10分かかります。aws_db_proxy リソースのステータスが available になるまで待ってから Phase B に進んでください。

Phase B: postgresql provider でスキーマ適用

# IAM認証ユーザーを事前に作成(Section 6 の手順)
# その後、postgresql リソースを適用
terraform apply -var="aurora_master_password=YourSecurePassword123!"

全リソース削除

terraform destroy -var="aurora_master_password=YourSecurePassword123!"

Section 11: 他の方法(概要解説)

本記事のメインハンズオンでは Terraform postgresql provider + IAM認証 + RDS Proxy を採用しましたが、実際の運用では要件に応じて複数の方法を組み合わせるケースもあります。以下に代表的な4つの方法を概説します。


11-1. RDS Data API — VPC接続不要のSQL実行

概要: HTTPS経由でSQLを実行できるマネージドAPIです。Lambda関数やCI/CDパイプラインからVPC設定なしでAuroraにアクセスできます。

アーキテクチャ:

クライアント(Lambda / CI/CD / コンソール)
 ↓ HTTPS + IAM認証(rds-data:ExecuteStatement)
RDS Data API エンドポイント
 ↓
Aurora PostgreSQL

AWSコンソール(Query Editor)での使用方法:

  1. RDS コンソール → クラスター選択 → 「クエリエディター」タブ
  2. データベースに接続(シークレット選択 or パスワード入力)
  3. SQLを直接入力・実行

AWS CLI での実行例:

aws rds-data execute-statement \
  --resource-arn "arn:aws:rds:ap-northeast-1:123456789:cluster:aurora-cluster" \
  --secret-arn "arn:aws:secretsmanager:ap-northeast-1:123456789:secret:aurora/master" \
  --database "app_database" \
  --sql "SELECT table_name FROM information_schema.tables WHERE table_schema = 'app'"

Terraform連携 (null_resource + local-exec):

resource "null_resource" "create_schema_via_data_api" {
  triggers = {
 schema_version = "v1"
  }

  provisioner "local-exec" {
 command = <<-EOF
aws rds-data execute-statement \
  --resource-arn "${aws_rds_cluster.main.arn}" \
  --secret-arn "${aws_secretsmanager_secret.aurora_master.arn}" \
  --database "${var.db_name}" \
  --sql "CREATE SCHEMA IF NOT EXISTS app"
 EOF
  }

  depends_on = [aws_rds_cluster_instance.writer]
}

主な制約:

制約内容
ENUM型非対応
結果セットサイズ1MB上限
DDL + DML の順序DDL実行が前のDMLをコミット
トランザクションBeginTransaction/CommitTransaction APIで別途管理が必要

推奨ユースケース: 開発・デバッグ用のad-hocクエリ、VPC外からの軽量DDL実行


11-2. Flyway on CodeBuild — バージョン管理マイグレーション

概要: Javaベースのマイグレーションツール「Flyway」をAWS CodeBuild上で実行し、SQL スクリプトをバージョン管理します。V001__init.sql, V002__add_index.sql のように連番管理されたスクリプトを順番に実行します。

アーキテクチャ:

GitHub / CodeCommit
 ↓ Push
CodePipeline
 ↓ Source Stage
CodeBuild(Flyway Docker イメージ)
 ├─ Secrets Manager から認証情報取得
 └─ Aurora PostgreSQL(VPC内)
  ↓ V001, V002, ... を順番に適用

buildspec.yml の例:

version: 0.2

phases:
  pre_build:
 commands:
- DB_HOST=$(aws secretsmanager get-secret-value --secret-id aurora-no-human-login/master --query SecretString --output text | jq -r .host)
- DB_PASS=$(aws secretsmanager get-secret-value --secret-id aurora-no-human-login/master --query SecretString --output text | jq -r .password)
  build:
 commands:
- |
  docker run --rm \
 -v $(pwd)/migrations:/flyway/sql \
 flyway/flyway:10 \
 -url="jdbc:postgresql://${DB_HOST}:5432/app_database" \
 -user=postgres \
 -password="${DB_PASS}" \
 migrate

メリット / デメリット:

メリットデメリット
バージョン管理SQL スクリプトの実行履歴をDB管理Java依存
ロールバックEnterprise版のみ undo migration 対応Community版は undo 不可
IAM認証非対応(Secrets Managerのパスワード認証)パスワードレス化不可
CI/CD統合CodePipeline/GitHub Actionsと容易に統合Docker環境が必要

参考: AWS公式ブログ — Automate database object deployments in Amazon Aurora using AWS CodePipeline


11-3. SSM Session Manager Port Forwarding — 開発者のad-hocアクセス

概要: AWS Systems Manager Session Manager を使って、踏み台サーバー(Bastion)なしに開発者のローカルマシンからAurora PostgreSQLへ接続します。EC2インスタンスをSSMエージェント経由で中継します。

接続コマンド:

# ローカルポート 15432 → Aurora エンドポイント:5432 へ転送
aws ssm start-session \
  --target i-xxxxxxxxxxxxxxxxx \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{
 "host": ["aurora-no-human-login-cluster.cluster-xxxxxx.ap-northeast-1.rds.amazonaws.com"],
 "portNumber": ["5432"],
 "localPortNumber": ["15432"]
  }'

別ターミナルから接続:

# psql で接続
psql -h localhost -p 15432 -U postgres -d app_database

# pgAdmin や TablePlus の接続先
# Host: localhost
# Port: 15432
# Database: app_database

必要条件:

  • EC2インスタンスに SSM エージェントがインストール済み(Amazon Linux 2023はデフォルトでインストール)
  • EC2に AmazonSSMManagedInstanceCore ポリシーをアタッチ
  • 開発者ローカルに AWS CLI + Session Manager Plugin インストール
# Session Manager Plugin のインストール(macOS)
brew install --cask session-manager-plugin

推奨ユースケース: 開発者のデータ確認・デバッグ・スキーマ変更のテスト。自動化ワークフローには不向き。


11-4. GitHub Actions OIDC連携 — CI/CDパイプラインからのパスワードレス接続

概要: GitHub Actions の OIDC(OpenID Connect)を使ってAWS IAMロールを一時的に引き受け、generate-db-auth-token でAurora IAM認証トークンを生成してDB操作を実行します。シークレットのローテーション不要でセキュリティを維持できます。

GitHub Actions ワークフロー例:

name: DB Migration

on:
  push:
 branches: [main]
 paths:
- 'migrations/**'

permissions:
  id-token: write
  contents: read

jobs:
  migrate:
 runs-on: ubuntu-latest
 steps:
- uses: actions/checkout@v4

- name: Configure AWS Credentials(OIDC)
  uses: aws-actions/configure-aws-credentials@v4
  with:
 role-to-assume: arn:aws:iam::123456789012:role/github-actions-aurora-role
 aws-region: ap-northeast-1

- name: Generate RDS Auth Token
  id: token
  run: |
 TOKEN=$(aws rds generate-db-auth-token \
--hostname ${{ vars.RDS_PROXY_ENDPOINT }} \
--port 5432 \
--username iam_lambda_user \
--region ap-northeast-1)
 echo "token=${TOKEN}" >> $GITHUB_OUTPUT

- name: Run DB Migration
  env:
 PGPASSWORD: ${{ steps.token.outputs.token }}
 PGSSLMODE: require
  run: |
 psql \
-h ${{ vars.RDS_PROXY_ENDPOINT }} \
-U iam_lambda_user \
-d app_database \
-f migrations/V$(date +%Y%m%d)__migration.sql

IAMロール信頼ポリシー(GitHub Actions OIDC用):

{
  "Version": "2012-10-17",
  "Statement": [{
 "Effect": "Allow",
 "Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
 },
 "Action": "sts:AssumeRoleWithWebIdentity",
 "Condition": {
"StringEquals": {
  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
 }
  }]
}

メリット:
– GitHub Secretsにパスワードを保存不要
– IAM認証トークンは15分で自動失効(漏洩リスク低)
– GitHub Actions の OIDC は追加費用なし


Section 12: クリーンアップ

ハンズオン終了後は不要なリソースを削除してください。Aurora Serverless v2 は起動中のインスタンスに対して時間課金が発生します。

12-1. Terraform で構築したリソースの削除

Terraform で構築した場合は terraform destroy で一括削除できます。依存関係は自動解決されます。

削除前の確認(dry-run)

cd terraform/
terraform plan -destroy -var="aurora_master_password=YourSecurePassword123!"
Plan: 0 to add, 0 to change, 38 to destroy.

  # aws_db_proxy.main will be destroyed
  # aws_lambda_function.migration will be destroyed
  # aws_rds_cluster.main will be destroyed
  # aws_rds_cluster_instance.writer will be destroyed
  # aws_secretsmanager_secret.aurora_master will be destroyed
  # ...

削除の実行

terraform destroy -var="aurora_master_password=YourSecurePassword123!"

確認プロンプトで yes を入力してください。

⚠️ Auroraクラスター削除の待ち時間

Aurora クラスターの削除には 5〜10分かかります。削除完了前に同名のクラスターを再作成しようとするとエラーになります。terraform destroy 完了を確認してから再構築してください。

12-2. コンソールで構築したリソースの手動削除

コンソールハンズオン(Section 5〜6)で手動作成したリソースは以下の順序で削除します。

削除順序(依存関係を考慮):

順序リソース注意点
1EventBridge Scheduler(スケジュール)Lambda より先に削除
2Lambda 関数(マイグレーション)
3Lambda レイヤー(psycopg2)
4RDS ProxyAurora より先に削除必須
5Aurora クラスターインスタンスクラスターより先に削除
6Aurora クラスタースナップショット不要なら「最終スナップショットをスキップ」
7Secrets Manager シークレット即時削除: 「復旧期間なしで削除」を選択
8IAMポリシー / ロール順不同
9NAT GatewayEIP 解放より先に削除
10EIP(Elastic IP)NAT Gateway削除後に解放
11VPCエンドポイント
12VPCサブネット・SGは VPC削除で自動削除

12-3. 部分削除(特定リソースのみ)

Terraform での部分削除は -target オプションで対象リソースを指定します。

# RDS Proxy のみ削除(コスト削減 + Aurora は残す場合)
terraform destroy \
  -var="aurora_master_password=YourSecurePassword123!" \
  -target=aws_db_proxy.main \
  -target=aws_db_proxy_default_target_group.main \
  -target=aws_db_proxy_target.main
⚠️ -target の注意事項

-target での部分削除は Terraform State と実際のインフラに不整合が生じる可能性があります。本番環境での使用は最小限にし、通常のクリーンアップには terraform destroy(全体削除)を使用してください。


Section 13: まとめ

13-1. 本記事で実現したこと

本記事では「人間がログインしないDB管理」をコンセプトに、Aurora PostgreSQL の完全 IaC 化・パスワードレス化を実現しました。

構築したアーキテクチャのポイント:

課題解決策実装方法
パスワード共有のリスクIAM データベース認証iam_database_authentication_enabled = true + rds_iam ロール
踏み台サーバー不要RDS Proxy 経由の接続集約aws_db_proxy (IAM_AUTH = REQUIRED)
スキーマのドリフト防止Terraform で宣言的管理cyrilgdn/postgresql provider
コネクション枯渇RDS Proxy によるプーリングmax_connections_percent / idle 制御
マイグレーションの自動化Lambda + EventBridge Scheduler冪等DDL + 日次スケジュール実行
監査証跡CloudWatch Logs + IAMロールLambda実行ログ + IAMポリシー単位の制御

13-2. 手法選定フローチャート

運用シナリオに合わせた手法の選び方:

🔀 DB管理手法 選定フロー

初期スキーマ構築・ロール管理・権限管理
Terraform postgresql provider(IaC で宣言的管理、変更差分が見える)

ALTER TABLE / インデックス追加 / データ投入
Lambda + psycopg2 + IAM認証(柔軟なDDL実行、冪等性を保証)

バージョン管理された複雑なマイグレーション
Flyway on CodeBuild(SQL スクリプトのバージョン管理、CI/CD統合)

開発者のデバッグ・データ確認(ad-hoc)
SSM Session Manager Port Forwarding(踏み台不要、一時的なローカル接続)

CI/CDパイプラインからのDB操作
GitHub Actions OIDC + IAM認証(シークレット不要、最小権限の一時認証)

VPC外から素早くSQL確認・開発
RDS Data API + Query Editor(VPC設定不要、コンソールから即実行)

13-3. セキュリティのベストプラクティス

本記事を通じて実践したセキュリティ原則:

最小権限の原則:
– Lambda には iam_lambda_user のみの rds-db:connect 権限
– リーダーには app_reader ロール(SELECT のみ)
– ライターには app_writer ロール(DML のみ、DDL不可)

パスワードレス化:
– RDS Proxy の iam_auth = "REQUIRED" でパスワード直接接続を禁止
– IAM認証トークンは15分で自動失効

暗号化:
storage_encrypted = true(KMSによる保存データの暗号化)
require_tls = true(転送データの暗号化)

13-4. 応用と次のステップ

マルチアカウント構成:
– Resource Access Manager(RAM)でサブネットを共有
– IAM ロールの cross-account trust で別アカウントの Lambda からアクセス

Blue/Green デプロイメント:
– Aurora Blue/Green Deployments でゼロダウンタイムのメジャーバージョンアップ
– Terraform で aws_rds_cluster_blue_green_update ブロックを使用(AWS Provider 5.x)

Flyway 統合:
– Terraform で基盤を構築し、アプリケーション層のマイグレーションは Flyway に委譲
null_resource + local-exec で terraform apply 後に自動的に Flyway を実行


関連記事: Terraform基礎 — AWS IaC入門ハンズオン ▶


13-5. シリーズリンク


本記事は「AWS ハンズオン TechBlog」の Aurora PostgreSQL 完全ガイドです。

関連シリーズ:
AWS Step Functions 入門 — コンソールとTerraformで学ぶハンズオン
ECS × Step Functions 入門 — CSVバッチをFargateタスクでジョブ化するハンズオン
Step Functions エラーハンドリング完全ガイド
Step Functions 入出力データフロー制御完全ガイド
Step Functions Callbackパターン完全ガイド
Step Functions Distributed Map完全ガイド
Terraform基礎 — AWS IaC入門ハンズオン
IAM Identity Center × SSM Session Manager 本番運用完全ガイド — 本記事の汎用横展開版 (EC2/ECS/RDS/RDP 4接続 + Permission Set 6パターン)


Section 14: 参考リンク

AWS 公式ドキュメント

Terraform Registry

AWS 公式ブログ

GitHub / コミュニティ