TerraformでRDSをプロビジョニングする方法|aws_db_instanceとサブネットグループ・パラメータグループの実践設計

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > TerraformでRDSをプロビジョニングする方法|aws_db_instanceとサブネットグループ・パラメータグループの実践設計
「TerraformでRDSを追加しようとしたら、サブネットグループ・パラメータグループ・セキュリティグループと関連リソースが多くてどこから手をつけるべきかわからない」
こうした声はよく聞きます。RDSはEC2やVPCと比べてリソース間の依存関係が多く、サブネットグループの設定漏れやセキュリティグループの許可ポートミスで apply 直後にエラーになるケースが後を絶ちません。

この記事では、aws_db_instance を中心に、aws_db_subnet_group・aws_db_parameter_group・aws_security_group を組み合わせたRDSのプロビジョニング手順を実践解説します。MySQLを題材に、削除保護・マルチAZ・自動バックアップ・ストレージ暗号化といった本番環境の必須設定も含めてカバーします。

動作確認環境: Terraform 1.8.5 / AWS Provider 5.50.0(ap-northeast-1リージョン、MySQL 8.0.35)

この記事のポイント

・aws_db_instanceの前にサブネットグループとセキュリティグループの定義が必要
・aws_db_parameter_groupで文字コード・タイムゾーンをコードで管理できる
・本番環境ではdeletion_protection=trueとskip_final_snapshot=falseを必ず設定する
・storage_encrypted=trueでディスク暗号化をTerraformで管理できる(作成後に変更不可)
・terraform applyは完了まで最大20分かかるためCI/CDのタイムアウト設定に注意
・apply後はterraform outputとAWS CLIでRDSエンドポイントと状態を必ず確認する


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜRDSをTerraformで管理するのか

AWSコンソールからRDSを手作業で構築すると、パラメータグループの設定値・サブネットグループのAZ構成・セキュリティグループのingressルールが別々の画面に散らばります。後からどの設定が本番と開発で異なるのかを把握しにくく、「なぜかstgで動いてprodで動かない」問題の温床になります。

Terraformで管理することで次の3つの問題が解消されます。

設定の再現性:terraform applyを実行すれば同じ構成のRDSを何度でも作れる
差分の可視化:terraform planでパラメータ変更の影響範囲を事前に確認できる
レビュー可能性:GitのPRでRDSの設定変更をチームでレビューできる

RDSはリソース間の依存関係が明確に決まっているため、作成順序を把握しておくことが安定した apply の前提条件になります。

1. セキュリティグループ:aws_db_instanceに紐づける。3306番ポートへの許可元を限定する
2. aws_db_subnet_group:RDSを配置するサブネットの集合。2つ以上のAZが必須
3. aws_db_parameter_group:文字コード・タイムゾーンなどのMySQLパラメータを管理
4. aws_db_instance:上記3つを参照してMySQLインスタンスを作成する

Terraformはリソース間の参照(aws_db_subnet_group.main.name 等)を解析して自動的に作成順序を決定します。手順を意識しなくても apply は通りますが、エラーの切り分けには依存関係の理解が必須です。次のセクションで具体的な設定手順を解説します。

aws_db_instanceの基本設定|依存リソースの作成順序

1. aws_db_subnet_groupでサブネットグループを作成する

RDSを作成するには必ず事前にサブネットグループが必要です。サブネットグループは「RDSを配置できるサブネットの集合」で、マルチAZ構成ではサブネットグループに2つ以上のAZのサブネットを含める必要があります。

# subnet_group.tf resource "aws_db_subnet_group" "main" { name = "main-db-subnet-group" subnet_ids = [ aws_subnet.private_a.id, # ap-northeast-1a のプライベートサブネット aws_subnet.private_c.id, # ap-northeast-1c のプライベートサブネット ] tags = { Name = "main-db-subnet-group" Environment = "production" } }

subnet_ids には必ず2つ以上のAZのサブネットIDを渡してください。同一AZのサブネットを2つ指定しても「少なくとも2つのアベイラビリティゾーンが必要」というエラーになります。RDSは必ずプライベートサブネットに配置し、インターネットから直接到達できない設計にしてください。

2. セキュリティグループを定義する

RDS用のセキュリティグループでは、アクセス元(アプリケーションサーバーのセキュリティグループ)からのみ3306番ポートを許可します。0.0.0.0/0 の全開放は絶対に避けてください。

# security_group.tf resource "aws_security_group" "rds" { name = "rds-sg" description = "Security group for RDS MySQL" vpc_id = aws_vpc.main.id ingress { from_port = 3306 to_port = 3306 protocol = "tcp" # アプリケーションサーバーのSGからのみ許可 security_groups = [aws_security_group.app.id] description = "MySQL from app servers" } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "rds-sg" } }

CIDRブロックではなく security_groups でアプリサーバーのSGを直接指定するのが正しい設計です。VPC CIDR(10.0.0.0/16 等)で許可すると同一VPC内の意図しないリソースからも接続可能になります。

3. aws_db_instanceでMySQLを起動する

サブネットグループとセキュリティグループが揃ったら aws_db_instance を定義します。

# rds.tf resource "aws_db_instance" "main" { identifier = "main-mysql" engine = "mysql" engine_version = "8.0.35" instance_class = "db.t3.micro" allocated_storage = 20 storage_type = "gp3" db_name = "appdb" username = "admin" password = var.db_password # variables.tfで定義したsensitive変数 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.main.name # 本番必須設定 deletion_protection = true skip_final_snapshot = false final_snapshot_identifier = "main-mysql-final-snapshot" backup_retention_period = 7 backup_window = "02:00-03:00" maintenance_window = "mon:03:00-mon:04:00" # ストレージ暗号化(本番環境では必須・作成後に変更不可) storage_encrypted = true tags = { Name = "main-mysql" Environment = "production" } }

password には必ず sensitive 変数を使い、コードに平文で書き込まないようにしてください。

# variables.tf variable "db_password" { description = "RDS admin password" type = string sensitive = true # terraform planとログに値が表示されなくなる }

実際に terraform apply を実行するとRDSの作成が始まります。完了までの実際の出力例を見てみましょう。

$ terraform apply -var="db_password=MySecretPass123!" aws_db_subnet_group.main: Creating... aws_db_subnet_group.main: Creation complete after 4s [id=main-db-subnet-group] aws_security_group.rds: Creating... aws_security_group.rds: Creation complete after 3s [id=sg-0a1b2c3d4e5f67890] aws_db_instance.main: Creating... aws_db_instance.main: Still creating... [10s elapsed] aws_db_instance.main: Still creating... [1m0s elapsed] # ... (RDSの作成は通常5分~20分かかる) aws_db_instance.main: Creation complete after 8m42s [id=main-mysql] Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformによるRDS・VPC・EC2の構成設計からモジュール分離・環境分離まで、実機を使ったハンズオン形式のセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

パラメータグループで文字コードとタイムゾーンをコード化する

1. aws_db_parameter_groupを定義する

AWSが提供するデフォルトのパラメータグループは変更不可です。カスタム設定(文字コードやタイムゾーン)が必要な場合は aws_db_parameter_group を作成してから aws_db_instance に紐づけます。

# parameter_group.tf resource "aws_db_parameter_group" "main" { name = "main-mysql80" family = "mysql8.0" # engineとengine_versionに対応したfamilyを指定 description = "Custom parameter group for MySQL 8.0" parameter { name = "character_set_server" value = "utf8mb4" } parameter { name = "character_set_client" value = "utf8mb4" } parameter { name = "time_zone" value = "Asia/Tokyo" apply_method = "immediate" # 再起動なしで即時反映 } parameter { name = "slow_query_log" value = "1" } parameter { name = "long_query_time" value = "1" # 1秒以上のクエリをスロークエリとして記録 } parameter { name = "general_log" value = "0" # 本番では無効(全クエリ記録はI/Oに影響する) } tags = { Name = "main-mysql80" } }

2. apply_methodの違い(immediateとpending-reboot)

パラメータの apply_method には2種類あります。

immediate:apply直後に反映。time_zoneやslow_query_logなど動的パラメータに使用できる
pending-reboot:次のメンテナンスウィンドウまたは手動再起動後に反映。innodb_buffer_pool_sizeなどの静的パラメータに必要

静的パラメータを immediate にしてもエラーにはなりませんが反映されません。AWSドキュメントのパラメータ一覧で「Dynamic/Static」を確認してから設定してください。パラメータグループを変更した後に apply を実行すると、RDSインスタンスの再起動が必要かどうかが terraform plan の出力に表示されます。

実務Tips|削除保護・自動バックアップ・マルチAZ・暗号化

1. 本番環境の削除保護設定

terraform destroy を実行しても本番DBが消えないよう、deletion_protection = true は必須です。削除保護が有効な状態で destroy しようとすると次のエラーになります。

$ terraform destroy Error: RDS DB Instance is protected from deletion. with aws_db_instance.main, on rds.tf line 1, in resource "aws_db_instance" "main": Please disable deletion protection before deleting.

意図的に削除する場合は deletion_protection = false に変更して terraform apply を先に実行してから destroy します。この2段階が「事故を防ぐフェンス」として機能します。

2. skip_final_snapshotとfinal_snapshot_identifierの設定

skip_final_snapshot = false(デフォルト)の場合、terraform destroy を実行するとRDS削除前に最終スナップショットが自動作成されます。final_snapshot_identifier には一意の名前を設定してください(省略するとエラーになります)。

開発環境では毎回スナップショットが作成されるとコストがかかるため、skip_final_snapshot = true にするケースもあります。環境ごとに var-file で制御する設計が一般的です。

3. マルチAZ設定でフェイルオーバーを実現する

本番環境では multi_az = true を設定します。1行追加するだけですが、コスト(インスタンス料金が約2倍)と引き換えに単一AZ障害時の自動フェイルオーバーが有効になります。

resource "aws_db_instance" "main" { # ... 既存の設定 ... multi_az = true # スタンバイレプリカを別AZに自動配置 } output "rds_endpoint" { value = aws_db_instance.main.endpoint sensitive = false }

マルチAZではスタンバイに直接接続することはできません。アプリケーションのDB接続先は endpoint 属性(Writerエンドポイント)1つだけです。フェイルオーバー発生時もエンドポイントは変わらず、DNSが自動的にスタンバイを新しいプライマリとして解決します。

4. ストレージ暗号化をTerraformで管理する

本番環境ではストレージ暗号化を有効にすることがセキュリティ要件として求められるケースがほとんどです。storage_encrypted = true を設定するだけでAWS管理のKMSキーを使った暗号化が有効になります。特定のカスタマー管理キーを使う場合は kms_key_id で指定します。

resource "aws_db_instance" "main" { # ... storage_encrypted = true # カスタマー管理キーを使う場合(省略するとAWS管理のaws/rdsキーが使われる) # kms_key_id = aws_kms_key.rds.arn }

注意: storage_encrypted は RDS インスタンス作成後に変更できません。暗号化なしで作成したRDSを暗号化ありに変えるには、スナップショットから暗号化済みの新しいインスタンスを作成し直す必要があります。本番環境では最初から有効にしてください。

apply後のRDS接続確認|terraform outputとAWS CLI

apply が完了しても、実際にアプリケーションからRDSに接続できるかを確認するまでが運用の基本です。RDSの場合は「インスタンス状態の確認」と「エンドポイントの取得」の2段階で確認します。

1. terraform outputでエンドポイントを確認する

# applyで出力された値を確認 $ terraform output rds_endpoint main-mysql.c9akciq32a3n.ap-northeast-1.rds.amazonaws.com:3306 # outputが定義されていない場合はstateから直接取得 $ terraform state show aws_db_instance.main | grep endpoint endpoint = "main-mysql.c9akciq32a3n.ap-northeast-1.rds.amazonaws.com:3306"

2. AWS CLIでRDSの状態を確認する

# RDSインスタンスの状態確認(availableになっていれば接続可能) $ aws rds describe-db-instances \ --db-instance-identifier main-mysql \ --query 'DBInstances[0].[DBInstanceStatus,Endpoint.Address,MultiAZ,StorageEncrypted]' \ --output table -------------------------------------------------------------------- | DescribeDBInstances | +-----------+----------------------------------------------+------+-----------+ | available | main-mysql.c9akciq32a3n.ap-northeast-1.rds.amazonaws.com | True | True | +-----------+----------------------------------------------+------+-----------+ # RDSのイベント履歴を確認(障害時の最初の調査ポイント) $ aws rds describe-events \ --source-identifier main-mysql \ --source-type db-instance \ --duration 60 \ --query 'Events[*].[EventCategories[0],Message,Date]' \ --output table

3. アプリサーバーからMySQL接続を確認する

# アプリサーバー(EC2)に接続して疎通確認 $ mysql -h main-mysql.c9akciq32a3n.ap-northeast-1.rds.amazonaws.com \ -u admin -p appdb \ -e "SELECT @@time_zone, @@character_set_server;" Enter password: +-------------+------------------------+ | @@time_zone | @@character_set_server | +-------------+------------------------+ | Asia/Tokyo | utf8mb4 | +-------------+------------------------+ # ポートレベルでの疎通確認(mysqlコマンドなしで確認したい場合) $ nc -zv main-mysql.c9akciq32a3n.ap-northeast-1.rds.amazonaws.com 3306 Connection to main-mysql.c9akciq32a3n.ap-northeast-1.rds.amazonaws.com 3306 port [tcp/mysql] succeeded!

mysqlコマンドが見つからない場合は yum install mysql か apt install mysql-client でインストールしてから実行してください。接続できない場合は次のセクションのトラブル対処法を参照してください。

「InvalidParameterCombination」などのapplyエラーと対処法

1. サブネットグループのAZが1つしかない場合

マルチAZを有効にしようとしたとき、または一部のインスタンスタイプで複数AZのサブネットが要求されるときに発生します。

$ terraform apply Error: InvalidParameterCombination: Cannot specify a DB subnet group for DB instances in VPC. # またはサブネットグループのAZが不足している場合: Error: DB subnet group "main-db-subnet-group" is invalid because it must have at least 2 availability zones.

対処法: aws_db_subnet_group の subnet_ids に2つ以上のAZのサブネットIDが含まれているか確認してください。

2. パラメータグループのfamilyが一致しない場合

$ terraform apply Error: InvalidParameterValue: Could not find parameter group family "mysql8.0" for parameter group "main-mysql80" # familyの名前は厳密に確認する(例: mysql8.0, mysql8.4, aurora-mysql8.0 は別物)

対処法: engine_version と family が対応しているか確認してください。MySQL 8.0 系は family = "mysql8.0"、MySQL 8.4 系は family = "mysql8.4" になります。

3. applyが途中でタイムアウトする場合

RDSの作成は最大20分かかります。CI/CDパイプラインのデフォルトタイムアウトが短いと apply が中断されます。Terraform側では timeouts ブロックで延長できます。

resource "aws_db_instance" "main" { # ... timeouts { create = "40m" # デフォルトは40m。CI側も合わせて延長する update = "80m" # マルチAZ変更などは時間がかかる delete = "40m" } }

4. セキュリティグループの設定ミスで接続できない場合

apply は正常終了したのに mysql コマンドで接続できない場合、セキュリティグループかサブネットのルーティングが原因のほとんどです。

# アプリサーバー(EC2)のセキュリティグループIDを確認 $ aws ec2 describe-instances \ --instance-ids i-0abc12345def \ --query 'Reservations[0].Instances[0].SecurityGroups[*].GroupId' \ --output text sg-0app1234567 # RDSのセキュリティグループのingressルールを確認 $ aws ec2 describe-security-groups \ --group-ids sg-0rds1234567 \ --query 'SecurityGroups[0].IpPermissions' # 確認ポイント: # - from_port/to_portが3306か # - UserIdGroupPairs に sg-0app1234567 が含まれているか

CIDRではなくSGでの許可設定(security_groups = [aws_security_group.app.id])を使っている場合、SGのIDが正しく参照されているかを確認するのが最速の切り分けです。

本記事のまとめ

TerraformでRDSをプロビジョニングする設計のポイントをまとめます。
やりたいこと 使うリソース・設定
RDSのサブネットグループを定義する resource "aws_db_subnet_group" { subnet_ids = [...] }
RDS専用セキュリティグループを作る resource "aws_security_group" { ingress { from_port = 3306 } }
MySQLインスタンスを作成する resource "aws_db_instance" { engine = "mysql" }
文字コード・タイムゾーンを設定する resource "aws_db_parameter_group" { parameter { name = "time_zone" } }
誤削除を防止する aws_db_instance { deletion_protection = true }
マルチAZフェイルオーバーを有効にする aws_db_instance { multi_az = true }
ストレージを暗号化する aws_db_instance { storage_encrypted = true }
apply後にRDS状態を確認する aws rds describe-db-instances --db-instance-identifier main-mysql
aws_db_instance は依存リソースが多いぶん、作成順序を事前に把握してHCLを整理することが安定運用の鍵です。パスワード管理には sensitive 変数を使い、deletion_protection・skip_final_snapshot = false・storage_encrypted = true のセットを本番環境の最低ラインとして設定してください。

次に読む記事:
Linuxのポート確認コマンド(ss・lsof)|RDSバックエンドアプリのLISTEN状態を確認する
digコマンドでDNS名前解決を確認する|RDSエンドポイントが正しく解決されるか検証する
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformによるRDS・VPC・EC2の構成設計からパラメータグループ・削除保護・マルチAZ設計まで、実機を使ったハンズオン形式のセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。