はじめに
RDSをTerraformで構築するとき、「とりあえず動くDB」を作るのは難しくありません。しかし、実際に運用に耐える構成にしようとすると、以下のような問いに一つずつ答えていく必要があります。
- マスターパスワードは誰がどう管理するのか
- 暗号化は必要か、どのキーを使うのか
- 障害調査のためにどんな監視が要るのか
- アプリケーション層からの接続をどう限定するのか
- メンテナンス(再起動を伴う変更)をいつ・どこまで自動化するのか
- バックアップと削除保護をどう設定するか
本記事では、ECS上で動くアプリケーションのバックエンドとして使うPostgreSQL用RDSモジュールを例に、これらの設計判断を構築を進める順番で解説します。前提として、VPCとプライベートサブネットは構築済み、ECS側のセキュリティグループIDも変数として受け取れる状態とします。
対象リソースは以下の6種類です。
| # | リソースタイプ | 役割 |
|---|---|---|
| 1 | aws_db_subnet_group |
RDS配置先のサブネットグループ |
| 2 | aws_db_instance |
PostgreSQL本体 |
| 3 | aws_db_parameter_group |
スロークエリログ・監査ログの出力設定 |
| 4 | aws_iam_role |
Enhanced Monitoring用ロール |
| 5 | aws_iam_role_policy_attachment |
Enhanced Monitoring権限付与 |
| 6 | aws_security_group |
RDS用セキュリティグループ |
1. サブネットグループを決める
RDSインスタンスを作る前に、まず「どのサブネットに配置するか」を決める必要があります。DBはインターネットから直接到達できてはいけないため、プライベートサブネットのIDのリストを受け取り、aws_db_subnet_groupにまとめます。
resource "aws_db_subnet_group" "this" {
name = "${var.project}-${var.env}-rds-subnet-group"
subnet_ids = var.subnet_ids
description = "RDS subnet group for ${var.project} ${var.env}"
tags = {
Name = "${var.project}-${var.env}-rds-subnet-group"
}
}
複数AZにまたがるサブネットを指定しておくと、後述するmulti_azをtrueに切り替えるだけでマルチAZ構成に移行できます。最初からマルチAZ用のサブネット設計をしておくことが、後の変更コストを下げるポイントです。
2. インスタンス本体の基本スペックを決める
サブネットが決まったら、次はインスタンス本体です。エンジン・バージョン・インスタンスクラス・ストレージサイズなど、コストと性能に直結する部分を変数化しておきます。
resource "aws_db_instance" "this" {
identifier = var.identifier
engine = var.engine
engine_version = var.engine_version
instance_class = var.instance_class
allocated_storage = 20
max_allocated_storage = var.max_allocated_storage
storage_type = "gp3"
db_name = var.database_name
username = var.master_username
db_subnet_group_name = aws_db_subnet_group.this.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
port = 5432
# ...後続セクションで追加する設定が続く
}
storage_typeはgp3を選択しています。旧世代のgp2と比べてIOPS・スループットをストレージ容量から切り離して個別にチューニングできるため、性能要件が変化しても容量を増やさずに調整できる余地があります。
publicly_accessible = falseは明示しておくことが重要です。デフォルト挙動に頼らず、意図を宣言的にコードへ残すことで、レビュー時に設定意図がすぐ伝わります。
3. マスターパスワードの管理方法を決める
パスワードをどう扱うかは、DB設計の中でも特に事故が起きやすいポイントです。.tfvarsに平文で書いてしまう、GitHubにコミットしてしまうといった事故を構造的に防ぐには、Secrets Manager連携機能を使うのが有効です。
manage_master_user_password = true
これをtrueにすると、AWSがランダムなパスワードを生成し、Secrets Managerのシークレットとして自動管理してくれます。Terraformのstateにも平文のパスワードは記録されません。
このシークレットはJSON形式で保存され、usernameとpasswordの2キーを持ちます。ECSタスク定義など、他のリソースからこの値を参照する際は、ARNの末尾に:password::を付与して特定のキーだけを取り出す必要があります。この点は次回の記事(ECSモジュール編)で詳しく扱います。
シークレットのARNは、後段の「Outputsの設計」で他モジュールに渡せるようにします。
4. 保管時の暗号化を有効にする
DBインスタンス作成時にしか設定できない項目の代表格が、保管時暗号化です。後から有効化することができないため、最初から入れておきます。
# RDS.3(FSBP): 保管時暗号化。デフォルトのAWS管理KMSキー(aws/rds)を使用する
storage_encrypted = true
AWS Security HubのFoundational Security Best Practices(FSBP)には、RDSインスタンスの暗号化を求めるチェック項目(RDS.3)があります。独自のKMSキーを持たない場合でも、AWS管理キー(aws/rds)を使うだけでこの項目は満たせます。コード内にコントロールIDをコメントで残しておくと、後で「なぜこの設定が必要か」を追跡しやすくなります。
5. 監視を仕込む(Enhanced Monitoring・ログエクスポート)
DBが遅い、接続できないといった障害が起きたとき、CloudWatchの標準メトリクスだけでは原因特定に時間がかかることがあります。ここでは2種類の監視を追加します。
5-1. Enhanced Monitoring
OS・プロセスレベルのメトリクスを取得するには、専用のIAMロールが必要です。RDSサービス(monitoring.rds.amazonaws.com)がAssumeRoleできるロールを作り、AWS管理ポリシーをアタッチします。
resource "aws_iam_role" "rds_enhanced_monitoring" {
name = "${var.project}-${var.env}-rds-enhanced-monitoring-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "monitoring.rds.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = {
Name = "${var.project}-${var.env}-rds-enhanced-monitoring-role"
}
}
resource "aws_iam_role_policy_attachment" "rds_enhanced_monitoring" {
role = aws_iam_role.rds_enhanced_monitoring.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
}
このロールのARNを、インスタンス側の設定で紐付けます。
# RDS.6(FSBP): Enhanced Monitoring。60秒間隔でOS/プロセスレベルのメトリクスを取得する
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_enhanced_monitoring.arn
60秒間隔は、コストと粒度のバランスを取った値です。より細かい調査が必要な場合は最短1秒まで短縮できますが、その分メトリクス量とコストが増えます。
5-2. ログのCloudWatch Logsエクスポート
スロークエリやエラーログを追跡したい場合、DBログをCloudWatch Logsにエクスポートしておくと、Logs Insightsでの横断検索が可能になります。
# RDS.9(FSBP): DBログをCloudWatch Logsへエクスポートする(PostgreSQLエンジンの対応ログタイプ)
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
PostgreSQLエンジンでエクスポート可能なログタイプはpostgresqlとupgradeの2種類です。エンジンによって指定できる値が異なる点は注意が必要です。
ただし、この設定だけでは転送先が用意されるだけで、ログの中身は空に近い状態です。何をログへ出力するかは、次に説明するパラメータグループ側の設定に依存します。
5-3. パラメータグループでログの中身を作り込む(スロークエリログ・監査ログ)
デフォルトのパラメータグループでは、スロークエリログは無効(log_min_duration_statement = -1)、監査ログの出力機能も存在しません。これらを有効にするには、専用のaws_db_parameter_groupを作成し、インスタンスに紐付けます。
resource "aws_db_parameter_group" "this" {
name = "${var.project}-${var.env}-pg-params"
family = "postgres17" # engineのメジャーバージョンに合わせて指定する
# スロークエリログ: 指定ミリ秒以上かかったクエリを記録する
parameter {
name = "log_min_duration_statement"
value = "1000"
}
# 監査ログ: pgaudit拡張をプリロードする(static。再起動が必要)
parameter {
name = "shared_preload_libraries"
value = "pgaudit"
apply_method = "pending-reboot"
}
# 監査対象: DDL(スキーマ変更)とWRITE(更新系)を記録する
parameter {
name = "pgaudit.log"
value = "ddl,write"
}
tags = {
Name = "${var.project}-${var.env}-pg-params"
}
}
familyはエンジンのメジャーバージョンに合わせる必要があります(例: PostgreSQL 17系ならpostgres17)。バージョンアップ時にはパラメータグループのfamilyも追従して見直す対象になります。
shared_preload_librariesはstaticパラメータで、値の変更に再起動を伴います。apply_method = "pending-reboot"を明示しておくと、terraform apply時にインスタンスの即時再起動ではなく、次回メンテナンスウィンドウまたは手動再起動まで変更が保留されることをコード上で表現できます。運用中のDBに対しては、この保留動作を理解した上で再起動タイミングを別途コントロールする必要があります。
pgaudit.logのddl,writeは、テーブル定義の変更(DDL)とデータの追加・更新・削除(WRITE)を監査対象とする設定です。読み取り(read)まで含めるとログ量が大きく増えるため、監査要件と運用コストのバランスで対象範囲を決めます。
作成したパラメータグループは、インスタンス側で紐付けます。
resource "aws_db_instance" "this" {
# ...(他の設定は前セクションの通り)
parameter_group_name = aws_db_parameter_group.this.name
}
6. 接続元を限定するセキュリティグループを設計する
DB用のセキュリティグループは、「誰からの接続を許可するか」を最小限に絞り込むことが目的です。ここではアプリケーションが動くECSタスクのセキュリティグループからのみ、PostgreSQLのポート(5432)を許可します。
resource "aws_security_group" "rds" {
name = "${var.project}-${var.env}-rds-sg"
description = "Security group for rds"
vpc_id = var.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.ecs_sg_id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project}-${var.env}-rds-sg"
}
}
ここではシンプルさを優先してインラインのingress/egressブロックを使っていますが、SGルールを別のリソース(aws_vpc_security_group_ingress_ruleなど)として切り出す設計も選択肢にあります。1つのSGに対してインラインルールと個別リソースを混在させると、Terraformの実行のたびに差分が検出され続ける「パーペチュアルディフ」と呼ばれる問題が起きるため、どちらか一方の方式に統一することが重要です。この点は別記事で詳しく扱います。
7. メンテナンスウィンドウと自動マイナーバージョンアップを制御する
RDSはデフォルトで、AWSが選んだ時間帯にマイナーバージョンの自動アップグレードを実行します。アプリケーションのトラフィックが多い時間帯に予期しない再起動が走ると、可用性に影響が出るため、実行タイミングと可否を明示的にコントロールします。
maintenance_window = "sun:18:00-sun:19:00"
auto_minor_version_upgrade = false
maintenance_windowはddd:hh24:mi-ddd:hh24:mi(UTC)形式で指定します。上記の例は、UTCの日曜18:00〜19:00、JSTでは月曜3:00〜4:00にあたります。トラフィックが少ない時間帯を選ぶのが基本方針です。バックアップウィンドウ(backup_window)を別途指定する場合は、この時間帯と重複しないようにする必要があります。
auto_minor_version_upgradeはfalseにしておくと、マイナーバージョンの自動適用が無効になります。予期しないタイミングでのエンジン変更を防げる一方、セキュリティパッチも自動では適用されなくなるため、マイナーバージョンの追従は手動またはCI/CDのメンテナンス手順に組み込む前提になります。検証環境ではtrueにして追従負荷を下げ、本番環境ではfalseにして変更タイミングを制御する、という環境ごとの切り替えも選択肢です。
なお、メジャーバージョンアップグレードはengine_versionの変更によって行われ、maintenance_windowの対象にはなりません。マイナー・メジャーで扱いが異なる点は区別しておく必要があります。
8. バックアップ・可用性・削除保護のポリシーを決める
運用フェーズを見据えて、バックアップと削除に関するポリシーを明示します。
skip_final_snapshot = true
final_snapshot_identifier = "${var.identifier}-final-snapshot"
deletion_protection = false
multi_az = false
backup_retention_period = var.backup_retention_period
検証環境ではskip_final_snapshot = true・deletion_protection = falseとして、破棄と再作成のサイクルを速くしています。一方でbackup_retention_periodはデフォルトを0にせず、明示的に7日など具体的な値を設定します。Terraform/AWSのデフォルト挙動では未設定時に自動バックアップが無効化されるため、意図せずバックアップなし運用になることを防ぐ狙いです。
本番環境へ昇格する際は、deletion_protection = true、skip_final_snapshot = false、multi_az = trueへ切り替えることになります。環境ごとに変数を分けておけば、この切り替えは値の変更だけで完結します。
multi_azとフェイルオーバーの関係
multi_az = falseの間は、別AZにスタンバイインスタンスが存在しないため、AZ障害やインスタンス障害が起きても「フェイルオーバー」という事象自体が発生しません。単一インスタンスの再起動・復旧を待つ形になり、ダウンタイムはMulti-AZ構成より長くなります。
multi_az = trueに切り替えると、プライマリに障害が起きた際にスタンバイへ自動的に切り替わるようになります。この切り替わりをRDSイベント通知やCloudWatchアラームで検知する仕組みは、Multi-AZ化とセットで初めて意味を持ちます。後続の監視設定を検討する際は、この前提を踏まえておく必要があります。
9. ストレージの自動拡張を設定する
初期のストレージサイズを小さめに見積もっていても、max_allocated_storageを設定しておけば、使用量が増えた際に自動でスケールアップします。
max_allocated_storage = var.max_allocated_storage
初期割り当て(allocated_storage)に近づくと自動的に拡張されるため、手動での容量監視・拡張作業を減らせます。上限値は運用コストとのバランスで決めます。
10. Outputsを設計する ― 他モジュールとの接続点
RDSモジュール単体では意味を持ちません。エンドポイント・ポート・シークレットARNを出力し、アプリケーション実行基盤側のモジュールから参照できるようにします。
output "db_instance_endpoint" {
value = aws_db_instance.this.endpoint
}
output "db_instance_id" {
value = aws_db_instance.this.id
}
output "rds_endpoint" {
description = "The connection endpoint for the RDS instance"
value = aws_db_instance.this.address
}
output "rds_port" {
description = "The port for the RDS instance"
value = aws_db_instance.this.port
}
output "rds_secret_arn" {
description = "The ARN of the Secrets Manager secret created by AWS for the RDS master user"
value = aws_db_instance.this.master_user_secret[0].secret_arn
}
rds_secret_arnが、手順3で触れたSecrets Manager連携の出力です。この値をアプリケーション実行基盤モジュールに渡すことで、コンテナ側は環境変数経由でパスワードを直接扱わずに済みます。
まとめ
読み手の構築順に沿って、以下の流れでRDSモジュールを組み立てました。
- 配置先(サブネットグループ)を決める
- インスタンスの基本スペックを決める
- マスターパスワードの管理方式を決める(Secrets Manager連携)
- 保管時暗号化を有効にする(作成後に変更不可なため最初に設定)
- 監視(Enhanced Monitoring・ログエクスポート)を仕込む
- パラメータグループでスロークエリログ・監査ログの中身を作り込む
- 接続元を最小権限のセキュリティグループで絞る
- メンテナンスウィンドウと自動マイナーバージョンアップを制御する
- バックアップ・可用性・削除保護のポリシーを環境ごとに決める
- ストレージ自動拡張を設定する
- 他モジュールへの接続点としてOutputsを設計する
各ステップはAWS Security HubのFSBPチェック項目(RDS.3、RDS.6、RDS.9)とも対応しており、「なぜこの設定が必要か」を後から追跡できるようにコメントを残しておくと、運用フェーズでの見直しがしやすくなります。