RDS: マネージドPostgreSQLの設計判断とセキュリティグループチェーンの完成
前章で、TasukuはFargate上でコンテナを起動する実行基盤を手に入れました。ただし、起動したタスクはまだ何のデータも扱っていません。タスク管理SaaSである以上、タスクの内容やユーザー情報をどこかに保存する場所が必要です。この章では、その保存先となるマネージドデータベース、RDS for PostgreSQLを構築します。第4章4.7節で予告したセキュリティグループのチェーンも、この章で完成します。
9.1. マネージドデータベースの責任分界
RDS (Relational Database Service) は、PostgreSQLをはじめとするリレーショナルデータベースエンジンをマネージドサービスとして提供する仕組みです。第1章で予告した通り、TasukuはこのRDSでPostgreSQLを動かします。
もう1つの選択肢が、EC2インスタンスに自分でPostgreSQLをインストールして運用する方法です。この方法では、OSのパッチ適用、PostgreSQL自体のマイナーバージョンアップ、バックアップの取得と保管、障害発生時の切り替えを、すべて自分で構築し維持する必要があります。RDSはこれらの管理作業を代行し、バックアップの取得・ソフトウェアパッチの適用・障害の自動検知と復旧をAWS側が担います。1 同期セカンダリインスタンスへのフェイルオーバーによる高可用性も、RDSが提供する仕組みの1つです。1 Tasukuの規模でこれらの運用を自前で構築する理由はないため、RDSを選びます。
RDSが代行しないのは、テーブル設計・クエリの書き方・アクセス制御・インスタンスサイズの選定です。この章で扱うのは主にインスタンスサイズの選定 (9.4節) とアクセス制御 (9.5節・9.7節) です。テーブル設計とクエリはTasukuのアプリケーションコードの範疇のため、第1章1.3節の境界宣言通りこの連載では扱いません。
9.2. PostgreSQLのバージョン選定
PostgreSQLにはメジャーバージョンが複数あり、RDSでは執筆時点でバージョン18から14までが現行サポート対象です。2 各メジャーバージョンには、RDS上での標準サポート終了日が設定されています。2 2022年にリリースされたバージョン14は2027年2月に標準サポートが終了し、2024年にリリースされたバージョン17は2030年2月まで続きます。2
この章ではPostgreSQL 17を採用します。執筆時点 (2026年7月) でRDSへのリリースから1年半ほどが経ち、標準サポート終了まで4年近くの猶予があります。最新のバージョン18はリリースからまだ日が浅く、実績が薄いバージョンです。バージョン選定にAWSの公式な推奨基準があるわけではなく、これはこの連載での執筆判断です。
9.3. サブネットグループ: private-dataサブネットの再利用
RDSインスタンスをVPC内に配置するには、DBサブネットグループを用意する必要があります。DBサブネットグループは、RDSインスタンスをどのサブネットに置くかを指定する入れ物で、最低でも2つのアベイラビリティゾーンにまたがるサブネットを含まなければなりません。3 満たさない場合、RDSインスタンスの作成自体がエラーになります。3
第4章で作成したprivate-dataサブネット (2つのアベイラビリティゾーンにまたがる10.0.20.0/24・10.0.21.0/24) は、最初からRDS用として設計されており、この2AZ要件をそのまま満たします。この章では新しいサブネットを作らず、このサブネットグループをそのまま使います。
9.4. インスタンスクラス、ストレージ、Multi-AZ
RDSのインスタンスクラスには、汎用・メモリ最適化・バーストパフォーマンスなど複数の系統があります。4 この章ではdb.t4g.microを採用します。
db.t4gはArmベースのAWS Graviton2プロセッサで動くバーストパフォーマンス系のインスタンスクラスで、4 PostgreSQL 17を含む現行のすべてのバージョンをサポートします。5 第8章のFargateでcpu・memoryを最小構成 (256・512MiB) から始めたのと同じ考え方で、db.t4g.microから始めます。
Tインスタンス (db.t2・t3・t4g) について、「開発・テスト環境専用で本番では非推奨」という注意書きを見かけることがあります。これはAurora向けのガイドに明記されている内容です。6 Aurora以外の標準的なRDSのガイドでは、この注意書きはdb.t2にのみ付いており、db.t3・db.t4gには付いていません。7 Tasukuが使うのはAuroraでなく標準のRDSのため、db.t4g.microを本番環境の出発点として使うことに支障はありません。
db.t4g.microのメモリは1GiBです。8 Tasukuの想定規模 (数十テナント、同時アクセス数百ユーザー程度、第8章8.2節) でこれが十分かどうかは、実際にapplyして負荷をかけてみるまでわかりません。Fargateは第10章のAuto Scalingでタスクの数を負荷に応じて動的に調整しますが、cpu・memoryの値自体は第8章の決め打ちのまま連載を通じて見直しません。RDSのインスタンスクラスも同様に、見直す章はこの連載の計画にありません。実運用で不足が見えたときにインスタンスクラスの変更で対応する前提の値だと理解してください。
ストレージにはgp3を使い、サイズは最小値の20GiBとします。9 暗号化は作成時に有効にします。RDSインスタンスの暗号化は、作成後の既存インスタンスに追加できません。10 暗号化されたスナップショットのコピーを作り、そこから新しいインスタンスを復元するしかありません。10 最初から有効にしておけば、この作り直しを避けられます。
最後に、Multi-AZを有効にします。第4章で、1つのアベイラビリティゾーンが利用不可になってもサービスを継続する構成にすると決めています。RDSだけシングルAZのままでは、この前提がデータベースの層で崩れます。Multi-AZを有効にすると、RDSは別のアベイラビリティゾーンに同期レプリカのスタンバイインスタンスを自動的に用意し、障害発生時にフェイルオーバーします。11 この方式は「Multi-AZ DB instanceデプロイメント」と呼ばれ、11 読み取り可能なスタンバイを2台持つ別方式の「Multi-AZ DB clusterデプロイメント」とは違い、12 インスタンスクラスに制限がありません。12 db.t4g.microのままMulti-AZ化できるのはこのためです。
9.5. マスターパスワードとSecrets Manager統合
RDSインスタンスにはマスターユーザーが必要です。このパスワードの管理方法には、Terraformのコードに直接書く方法と、AWS Secrets Managerに管理を委ねる方法があります。この章では後者を使います。
manage_master_user_passwordを有効にすると、RDSがパスワードを生成し、Secrets Manager上のシークレットとしてライフサイクル全体を管理します。13 シークレットは既定で7日ごとに自動的にローテーションされます。14 生成されたシークレットには、少なくともユーザー名とパスワードのキーが含まれることを、AWS公式ドキュメントの例から確認できます。15 接続先のホスト名・ポート番号・データベース名は、シークレットの中身に頼らずTerraformのoutputsから取得する設計にします。
この統合の有効化に必要な権限は、カスタムのKMSキーを使わない場合、kms:DescribeKey・secretsmanager:CreateSecret・secretsmanager:TagResourceの3つです。16 第3章で設定したterraform実行者の権限にはPowerUserAccessが含まれています。このポリシーが除外するのはiam:*・organizations:*・account:*の3つの名前空間だけで、17 上の3つの権限はいずれもこの除外に該当しません。第8章8.5節でECRの実行ロール権限に自前のポリシーが必要になったのとは違い、この統合には追加のIAMポリシーが要りません。
9.6. パラメータグループ
PostgreSQLのエンジン設定 (パラメータ) を変更するには、専用のDBパラメータグループを作成する必要があります。パラメータグループは、対応するエンジンとメジャーバージョンの組み合わせ (family) を1つ持ちます。18 この章ではpostgres17というfamilyでパラメータグループを新設します。
最初に検討したのは、SSL接続を強制するrds.force_sslパラメータを明示的に1へ変更することでした。ただし、PostgreSQL 15以降ではこのパラメータの既定値がすでに1 (オン) です。19 Tasukuが採用するPostgreSQL 17でも同様のため、変更する必要はありません。パラメータグループでは変更でなく、9.9節の動作確認で既定値どおり有効になっていることを確かめます。
パラメータグループで実際に変更するのはlog_min_duration_statementです。このパラメータは既定では設定されておらず、20 値をミリ秒単位で指定すると、それ以上かかったSQL文をログに記録します。1000 (1秒) を設定し、遅いクエリを検知できるようにします。このパラメータが即座に反映されるか、DBインスタンスの再起動を要するかを明記した一次情報は見つかりませんでした。9.9節の動作確認で反映を確認し、反映されていなければ再起動が必要になる場合があります。
9.7. セキュリティグループ: チェーンの完成
第4章4.7節のセキュリティグループチェーンの図では、RDS用のSGが「ECSタスク用SGのみ」からのingressを許可する形で予告されていました。ここでその通りに作り、第6章のALB、第8章のECSタスクに続いて、この連載3つ目の明示的なSGを新設します。
ingressは、第8章で作ったECSタスク用SGからの5432番のみを許可します。PostgreSQLの既定ポートです。
egressは一切定義しません。RDSインスタンス自身が外部への接続元として動作しない限り、egressのルールは実質的に意味を持ちません。21 加えて、AWSはVPC内に新しいセキュリティグループを作ると、既定でegressの全許可ルールを自動的に追加します。22 Terraformのaws_security_groupリソースは、egressを明示的に指定しない場合、この自動生成されたルールを検出して削除します。23 つまり、egressのルールを1つも書かなければ、最終的な状態はegressが1件もない全egress拒否になります。第6章のALB用SGの「意図的に空」も、同じ仕組みの上に成立しています。
最後に、第8章で作ったECSタスク用SGに、RDS用SGへの5432番egressを追加します。第8章時点のegressは443番のみだったため、この章でセキュリティグループチェーンが完成します。
9.8. Terraform実装
ここまでの内容をrds.tfという新しいファイルに書きます。DB名・マスターユーザー名にはname_prefixをそのまま使えません。PostgreSQLのdb_nameは1〜63文字の英数字とアンダースコアのみ、master_usernameは1〜16文字の英数字とアンダースコアのみで、いずれもハイフンを含められないためです。24 db_nameにはname_prefixのハイフンをアンダースコアに変換した値を使い、master_usernameにはvar.project(既定値tasuku、ハイフンを含まない)からこの変換を経由せず直接組み立てた値を使います。なお、db_nameを指定しても、それとは別にpostgresという名前の既定データベースが常に作成されます。24 Tasuku用のデータベースだけが存在するわけではありません。
locals {
db_identifier_safe = replace(local.name_prefix, "-", "_")
}
resource "aws_db_subnet_group" "main" {
name = "${local.name_prefix}-db-subnet-group"
description = "RDS用サブネットグループ。第4章のprivate-dataサブネット (2AZ) を使用"
subnet_ids = [for k in local.private_data_subnet_keys : aws_subnet.this[k].id]
tags = {
Name = "${local.name_prefix}-db-subnet-group"
}
}
9.7節で決めたセキュリティグループのチェーンです。
resource "aws_security_group" "rds" {
name = "${local.name_prefix}-rds-sg"
description = "RDS用SG。ECSタスク用SGからのingressのみ許可"
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-rds-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "rds_from_ecs_tasks" {
security_group_id = aws_security_group.rds.id
referenced_security_group_id = aws_security_group.ecs_tasks.id
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
description = "ECSタスク用SG (第8章) からのPostgreSQL接続のみ許可"
}
resource "aws_vpc_security_group_egress_rule" "ecs_tasks_to_rds" {
security_group_id = aws_security_group.ecs_tasks.id
referenced_security_group_id = aws_security_group.rds.id
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
description = "RDS用SGへのPostgreSQL接続 (run-taskでのpsql疎通確認、第10章のアプリ本体でも使用)"
}
9.6節のパラメータグループです。
resource "aws_db_parameter_group" "postgres17" {
name = "${local.name_prefix}-postgres17"
family = "postgres17"
description = "Tasuku用PostgreSQL 17パラメータグループ。スロークエリログ (1秒以上) を有効化"
parameter {
name = "log_min_duration_statement"
value = "1000"
}
tags = {
Name = "${local.name_prefix}-postgres17"
}
}
RDSインスタンス本体です。9.4節・9.5節で決めた設計をまとめて反映します。
resource "aws_db_instance" "main" {
identifier = "${local.name_prefix}-db"
engine = "postgres"
engine_version = "17"
instance_class = "db.t4g.micro"
allocated_storage = 20
storage_type = "gp3"
storage_encrypted = true
db_name = local.db_identifier_safe
username = "${var.project}_admin"
manage_master_user_password = true
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
parameter_group_name = aws_db_parameter_group.postgres17.name
multi_az = true
backup_retention_period = 7
deletion_protection = true
# 固定identifierのfinal_snapshot_identifierはdestroy再実行時に名前衝突するため、
# 最終スナップショットは作らない (deletion_protectionで誤操作防止は別途担保)
skip_final_snapshot = true
tags = {
Name = "${local.name_prefix}-db"
}
}
backup_retention_periodは明示的に7を指定しています。Terraformでこの引数を省略すると既定値は0 (自動バックアップ無効) になり、25 AWSのAPI・CLI経由の既定値である1日とも、コンソール経由で作成した場合の既定値である7日とも異なります。25 コンソールの既定値と同じ7日を明示しました。deletion_protectionも既定はfalseのため、26 本番運用を想定してtrueにしています。
skip_final_snapshotはtrueにし、削除時の最終スナップショットを作りません。27 falseにするとfinal_snapshot_identifierで指定した名前のスナップショットが作成されますが、27 固定の文字列をこの値に使うと、destroyしてから同じコードでもう一度apply・destroyしたときに、前回作成済みの同名スナップショットと衝突します。誤操作からの保護はdeletion_protectionがすでに担っているため、この章ではシンプルにtrueを選びます。
最後に、動作確認で使うpsqlクライアント同梱の専用タスク定義を追加します。Tasukuのアプリイメージはまだ存在しないため、第8章のhello-worldと同じく、Docker公式のpostgresイメージで代用します。28 実行ロール・タスクロール・クラスタ・ロググループは第8章のものを再利用し、ログストリームのprefixだけ変えます。
resource "aws_ecs_task_definition" "psql_check" {
family = "${local.name_prefix}-psql-check"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}
container_definitions = jsonencode([
{
name = "psql-check"
image = "${aws_ecr_repository.app.repository_url}:psql-check"
essential = true
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.region
"awslogs-stream-prefix" = "psql-check"
}
}
}
])
tags = {
Name = "${local.name_prefix}-psql-check"
}
}
後続の章が参照する値をoutputs.tfに追加します。
output "rds_endpoint" {
description = "RDSインスタンスの接続エンドポイント (address:port形式、第10章のアプリ接続文字列で参照)"
value = aws_db_instance.main.endpoint
}
output "rds_address" {
description = "RDSインスタンスのホスト名のみ (ポート番号を含まない、run-taskのpsql接続コマンドで参照)"
value = aws_db_instance.main.address
}
output "rds_db_name" {
description = "RDSインスタンスの初期データベース名 (第10章のアプリ接続文字列で参照)"
value = aws_db_instance.main.db_name
}
output "rds_master_user_secret_arn" {
description = "マスターパスワードを管理するSecrets ManagerシークレットのARN (run-task疎通確認・第10章のアプリでの参照用)"
value = aws_db_instance.main.master_user_secret[0].secret_arn
}
output "rds_port" {
description = "RDSインスタンスのポート番号"
value = aws_db_instance.main.port
}
output "rds_security_group_id" {
description = "RDS用SGのID (第10章で参照する可能性あり)"
value = aws_security_group.rds.id
}
output "psql_check_task_definition_arn" {
description = "psql疎通確認用タスク定義のARN (run-taskで参照)"
value = aws_ecs_task_definition.psql_check.arn
}
9.9. 動作確認
terraform fmt -check -recursive -diffとterraform validateを実行し、構文とプロバイダスキーマの整合を確認します。この章の検証もvalidateまでで、planやapplyは対象に含めません。
実際にapplyして確認する場合の手順です。まず、postgres:17-alpineイメージをDocker HubからpullしてECRへ再ホストします。第8章8.9節のhello-worldと同じ手順です。
aws ecr get-login-password --region ap-northeast-1 \
| docker login --username AWS --password-stdin <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com
docker pull postgres:17-alpine
docker tag postgres:17-alpine <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:psql-check
docker push <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:psql-check
次に、Secrets Managerからマスターパスワードを取得します。JSONからパスワードだけを取り出すためjqコマンドを使います。事前にインストールしておいてください。
PGPASSWORD=$(aws secretsmanager get-secret-value \
--secret-id <rds_master_user_secret_arnの値> \
--query SecretString --output text | jq -r .password)
取得したパスワードをコンテナの環境変数に注入し、run-taskでpsqlの単発タスクを起動します。--network-configurationのsubnets・securityGroupsは、第8章8.9節と同じく明示します。省略するとVPCの既定セキュリティグループが使われてしまい、第4章で全閉鎖したそのSGでは疎通できません。接続先のホスト名にはrds_endpointではなくrds_addressを使います。rds_endpointはポート番号を含むaddress:port形式のため、そのままではpsqlの-hオプションに渡せません。
aws ecs run-task \
--cluster tasuku-dev-cluster \
--task-definition tasuku-dev-psql-check \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<private-appサブネットID>],securityGroups=[<ecs_tasks SGのID>],assignPublicIp=DISABLED}" \
--overrides "{\"containerOverrides\":[{\"name\":\"psql-check\",\"environment\":[{\"name\":\"PGPASSWORD\",\"value\":\"$PGPASSWORD\"}],\"command\":[\"psql\",\"-h\",\"<rds_addressの値>\",\"-U\",\"tasuku_admin\",\"-d\",\"tasuku_dev\",\"-c\",\"SELECT version(); SHOW rds.force_ssl; SHOW log_min_duration_statement;\"]}]}"
検証は2段階です。まずaws ecs describe-tasksでlastStatusがSTOPPEDになり、containers[].exitCodeが0であることを確認します。タスクが正常に終了しただけでは、実際にPostgreSQLへ接続できたかはわかりません。続けてaws logs tail /ecs/tasuku-dev-appを実行し、CloudWatch Logsに記録されたSELECT version()の実際の出力、rds.force_sslがonであること、log_min_duration_statementが1000であることを確認します。この2段階を経て、初めて疎通確認が完了したと言えます。
この章のRDSインスタンスも、これまでと同じ検証水準です。実際にAWSへapplyしてこの手順を実行した結果は保証しません。
本章のまとめ
- RDSはバックアップ・パッチ適用・障害検知と復旧をAWS側が代行するマネージドサービスで、Tasukuはテーブル設計・アクセス制御・インスタンスサイズの選定を担います
- PostgreSQL 17を採用しました。標準サポート終了まで4年近くの猶予があり、最新バージョンほど実績が浅くないという執筆判断です
- インスタンスクラスはdb.t4g.microから始めます。「Tインスタンスは開発・テスト専用」という注意書きはAurora向けのガイダンスで、標準RDSには同じ制約がないことを確認しました
- ストレージはgp3・20GiB・暗号化有効、Multi-AZも有効にし、第4章で決めたアベイラビリティゾーン障害への耐性をデータベースの層まで一貫させました
deletion_protectionを有効にして誤操作による削除を防ぎ、skip_final_snapshotはtrueにして固定文字列の識別子による名前衝突を避けました- マスターパスワードはSecrets Manager統合 (
manage_master_user_password) に任せ、第8章のような追加IAMポリシーは不要でした - パラメータグループでは、PostgreSQL 15以降で既定オンの
rds.force_sslを変更せず、log_min_duration_statementのみ明示的に変更しました - RDS用SGを新設し、第4章で予告されたALB→ECS→RDSのセキュリティグループチェーンを完成させました
- 動作確認は、psqlクライアント同梱のタスクによるrun-taskで行い、exitCodeとCloudWatch Logsの実際の出力の両方を確認する2段階の検証にしました
次に読む
- 第 10 章: ECS 後編: web・apiサービスの公開、Secrets Managerからのアプリへの秘密情報注入
- 第 8 章: ECS 前編: ECSのクラスタ・タスク定義、実行ロールとタスクロール、ECR
- 連載の目次: 全 12 章の構成と読み進め方