Aurora PostgreSQL — 人間がログインしないDB管理 完全ガイド【Terraform + IAM認証 + 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/terraformmkdir -p aurora-no-human-login/lambda/migrationmkdir -p aurora-no-human-login/lambda/layercd aurora-no-human-logingit 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, memberofFROM pg_userJOIN pg_auth_members ON pg_user.usesysid = pg_auth_members.memberJOIN pg_roles ON pg_auth_members.roleid = pg_roles.oidWHERE 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 aclWHERE  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.binaryterraform 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.tfgit 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/layerzip -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 boto3import psycopg2import osimport logginglogger = 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 conndef 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-migrationzip -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_rolesWHERE rolname LIKE '%app%' OR rolname LIKE '%iam%'ORDER BY rolname;-- 権限確認SELECT grantee, table_schema, table_name, privilege_typeFROM information_schema.role_table_grantsWHERE 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 startedINFO Already applied: set()INFO Pending migrations: 5INFO Applied migration: v001_create_users_tableINFO Applied migration: v002_create_orders_tableINFO Applied migration: v003_add_index_users_emailINFO Applied migration: v004_add_index_orders_user_idINFO Applied migration: v005_add_index_orders_created_at

DB でのテーブル確認:

-- マイグレーション適用履歴SELECT version, applied_at, applied_byFROM schema_migrationsORDER BY applied_at;-- テーブル一覧SELECT tablename, tableownerFROM pg_tablesWHERE schemaname = 'app'ORDER BY tablename;-- インデックス一覧SELECT indexname, tablename, indexdefFROM pg_indexesWHERE 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 terraformcd 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 + CREATEresource "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: スキーマ USAGEresource "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.endpointDB_USER  = "iam_lambda_user"DB_NAME  = var.db_nameAWS_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 = <<-EOFaws 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 ↓ PushCodePipeline ↓ Source StageCodeBuild(Flyway Docker イメージ) ├─ Secrets Manager から認証情報取得 └─ Aurora PostgreSQL(VPC内)  ↓ V001, V002, ... を順番に適用

buildspec.yml の例:

version: 0.2phases:  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 Migrationon:  push: branches: [main] paths:- 'migrations/**'permissions:  id-token: write  contents: readjobs:  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入門ハンズオン


Section 14: 参考リンク

AWS 公式ドキュメント

Terraform Registry

AWS 公式ブログ

GitHub / コミュニティ