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・自動バックアップ・ストレージ暗号化・RDS IAM認証といった本番環境の必須設定も含めてカバーします。

動作確認環境: 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はインスタンス作成後に変更できないため最初から有効にする
・iam_database_authentication_enabled=trueでパスワード不要のIAM認証接続が実現できる
・lifecycleブロックでパラメータグループの無停止差し替えとエンジンバージョンのdrift防止を両立できる
・apply後はterraform outputとAWS CLIでRDSエンドポイントと稼働状態を必ず確認する


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

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

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

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

設定の再現性:terraform applyを実行すれば同じ構成のRDSを何度でも作れる
差分の可視化:terraform planでパラメータ変更の影響範囲を事前に確認できる
レビュー可能性:GitのPRでRDSの設定変更をチームでレビューできる
ドリフト検出:コンソールからの手動変更をterraform planで即座に検知できる

「コンソールで急いでパラメータを変更したがコードに反映し忘れた」——この状態をTerraformは自動的に検知してくれます。次のapplyでコンソールの手動変更が上書きされてしまうため、設定変更はすべてコードを通す習慣が自然と身につきます。

既存のRDSをTerraform管理下に取り込む場合は terraform import を使います。新規にVPCから構築する手順はTerraformでVPCとEC2を構築する記事を先に参照してください。RDSはリソース間の依存関係が明確に決まっているため、作成順序を把握しておくことが安定した apply の前提条件になります。依存関係を図で確認してから作業に入ってください。

# RDSリソースの依存関係 aws_vpc | +-- aws_subnet (private_a, private_c) | | | +-- aws_db_subnet_group ← RDSを配置するサブネットの集合 | +-- aws_security_group (rds) ← 3306番ポートへのアクセス制御 | +-- aws_db_parameter_group ← 文字コード・タイムゾーン等の設定 | +-- aws_db_instance ← MySQLインスタンス本体(最後に作成)

Terraformはリソース間の参照(aws_db_subnet_group.main.name 等)を解析して自動的に作成順序を決定します。手順を意識しなくても 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インスタンスを作成する

次のセクションで具体的な設定手順を解説します。

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

0. 既存VPCをdata sourceで参照する

VPCとサブネットがすでに存在する環境では、resource ブロックで新規作成するのではなく data ブロックで参照するのが実務では一般的です。タグ名で既存VPCを特定し、プライベートサブネットのIDを動的に取得できます。

# data.tf:既存VPCとプライベートサブネットをdata sourceで参照する data "aws_vpc" "main" { tags = { Name = "main-vpc" } } data "aws_subnets" "private" { filter { name = "vpc-id" values = [data.aws_vpc.main.id] } tags = { Tier = "private" } }

サブネットにTierタグが付いていない場合は、サブネットIDを変数で直接渡す設計に変えてください。RDSは必ずプライベートサブネットに配置し、インターネットから直接到達できない設計にします。

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つのアベイラビリティゾーンが必要」というエラーになります。既存VPCをdata sourceで参照している場合は data.aws_subnets.private.ids をそのまま渡せます。

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 egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "rds-sg" } } # ingressルールは aws_security_group_rule で分離して定義する resource "aws_security_group_rule" "rds_from_app" { type = "ingress" from_port = 3306 to_port = 3306 protocol = "tcp" # アプリケーションサーバーのSGからのみ許可 source_security_group_id = aws_security_group.app.id security_group_id = aws_security_group.rds.id description = "MySQL from app servers" }

セキュリティグループのingressルールを aws_security_group_rule で分離して定義すると、複数のアプリサーバーSGから許可する場合に差分管理がしやすくなります。CIDRブロックではなく source_security_group_id でアプリサーバーのSGを直接指定するのが正しい設計です。VPC CIDR(10.0.0.0/16 等)で許可すると同一VPC内の意図しないリソースからも接続可能になります。

セキュリティグループとあわせて、aws_db_instance 側でも publicly_accessible = false(デフォルト値)を明示的に指定することをおすすめします。デフォルト値であっても明示することで「意図的にインターネット非公開にしている」という意図がコードに残ります。

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 # インターネットから直接接続させない(デフォルトfalseだが明示する) publicly_accessible = false # 本番必須設定 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 # AWSがマイナーバージョンを自動パッチしてもplan差分が出ないようにする lifecycle { ignore_changes = [engine_version] } tags = { Name = "main-mysql" Environment = "production" } } output "rds_endpoint" { value = aws_db_instance.main.endpoint sensitive = false }

lifecycle { ignore_changes = [engine_version] } を追加しておくと、AWSがマイナーバージョンを自動適用(例: 8.0.35 → 8.0.36)した際に terraform plan でノイズが出なくなります。メジャーバージョンアップはコードを意図的に変更して管理する運用です。

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

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

パスワードの渡し方は複数あります。

# 方法1: -varオプションで直接渡す(CI/CD環境ではシークレット変数から渡す) $ terraform apply -var="db_password=MySecretPass123!" # 方法2: terraform.tfvarsファイルに書く(必ず.gitignoreに追加すること) # terraform.tfvars db_password = "MySecretPass123!" $ terraform apply # .tfvarsを自動的に読み込む # 方法3: 環境変数で渡す(variable名にTF_VAR_を付ける) $ export TF_VAR_db_password="MySecretPass123!" $ terraform apply

terraform.tfvars を使う場合は、必ず .gitignore に追加してパスワードがリポジトリに入らないようにしてください。本番環境でパスワードのローテーション管理が必要な場合は、AWS Secrets Manager と組み合わせる設計が有効です。

# Secrets Managerからパスワードを自動取得する設計(本番推奨) data "aws_secretsmanager_secret_version" "db_password" { secret_id = "prod/rds/main-mysql/password" } resource "aws_db_instance" "main" { # ... password = jsondecode(data.aws_secretsmanager_secret_version.db_password.secret_string)["password"] }

Secrets Managerを使う場合、パスワードをコードやCI/CD変数で管理する必要がなくなります。ローテーション設定(自動30日更新等)はマネジメントコンソールまたは aws_secretsmanager_secret_rotation リソースで設定できます。CI/CD環境ではシークレット変数(GitHub ActionsのSecrets等)から -var または環境変数で渡すのが一般的な設計です。

実際に 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_parameter_group.main: Creation complete after 2s [id=main-mysql80] 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: 4 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 = "collation_server" value = "utf8mb4_unicode_ci" } 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に影響する) } # 最大接続数(インスタンスクラスのメモリ量に応じて調整する) parameter { name = "max_connections" value = "200" } # InnoDBバッファプールサイズ(静的パラメータのためpending-reboot) parameter { name = "innodb_buffer_pool_size" value = "134217728" # 128MB。db.t3.microの場合の目安 apply_method = "pending-reboot" } # リードレプリカ・binlogレプリケーションを使う場合に必要 parameter { name = "binlog_format" value = "ROW" apply_method = "pending-reboot" } # パラメータグループを差し替える場合に新しいグループを先に作成する lifecycle { create_before_destroy = true } tags = { Name = "main-mysql80" } }

lifecycle { create_before_destroy = true } を追加しておくと、パラメータグループの name を変更した際(例: mysql8.0系→mysql8.4系へのメジャーバージョンアップ)に新しいグループが先に作られてから古いグループが削除されます。aws_db_instance がパラメータグループを参照している状態で先に削除しようとするとエラーになるため、このオプションは実務上ほぼ必須です。

max_connections のデフォルト値はインスタンスクラスのメモリ量から自動計算されます(db.t3.microでは約85)。アプリケーションの同時接続数が多い場合は明示的に設定してください。ただし大きくしすぎるとメモリを圧迫するため、接続プールの最大数と合わせて設計してください。

collation_server を utf8mb4_unicode_ci に合わせておくと、文字コードと照合順序の不整合によるエラーを防げます。binlog_format = "ROW" はリードレプリカを追加する予定がある場合やデータ変更の監査ログが必要な場合に設定します。

innodb_buffer_pool_size はInnoDBがデータキャッシュに使うメモリです。本番環境では搭載メモリの50~70%を目安に設定します。db.t3.microの2GBメモリに対して128MBという設定は小さめですが、RDS側のOSやその他プロセスとのメモリ競合を考慮した保守的な値です。

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」を確認してから設定してください。適用状態は AWS CLI で確認できます。

# パラメータグループの変更が反映済みかどうかを確認 $ aws rds describe-db-instances --db-instance-identifier main-mysql --query 'DBInstances[0].DBParameterGroups' [ { "DBParameterGroupName": "main-mysql80", "ParameterApplyStatus": "in-sync" } ] # "pending-reboot"が表示された場合はメンテナンスウィンドウ後または手動再起動で反映される

ParameterApplyStatus が "pending-reboot" になっている場合、Terraformのapplyではなくコンソールまたは AWS CLI での手動再起動が必要です。パラメータグループを変更した後に apply を実行すると、RDSインスタンスの再起動が必要かどうかがこのコマンドで確認できます。

実務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段階が「事故を防ぐフェンス」として機能します。

さらに強固な保護が必要な場合は lifecycle ブロックの prevent_destroy を組み合わせます。

resource "aws_db_instance" "main" { # ... deletion_protection = true lifecycle { # terraform destroyをHCLレベルでブロックする(deletion_protectionとの二重防壁) prevent_destroy = true # AWSのマイナーバージョン自動パッチによるplan差分を抑制する ignore_changes = [engine_version] } }

prevent_destroy = true が設定されている場合、terraform destroy を実行すると「Error: Instance cannot be destroyed」とHCLレベルでエラーになります。deletion_protection はAWS API側の制御、prevent_destroy はTerraform実行レベルの制御であり、二重の安全弁として機能します。ignore_changes = [engine_version] と組み合わせることで、削除保護とドリフト抑制を1つのlifeycleブロックにまとめて管理できます。

2. skip_final_snapshotとfinal_snapshot_identifierの設定

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

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

# environments/prod.tfvars skip_final_snapshot = false backup_retention_period = 7 # environments/dev.tfvars skip_final_snapshot = true backup_retention_period = 1 # 本番環境に適用する場合 $ terraform apply -var-file="environments/prod.tfvars"

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が自動的にスタンバイを新しいプライマリとして解決します。

マルチAZと読み取り負荷分散を両立したい場合は aws_db_instance に replicate_source_db を指定したリードレプリカを追加します。リードレプリカはフェイルオーバーの対象にはなりませんが、参照クエリをリードレプリカのエンドポイントに向けることでプライマリの負荷を下げられます。

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を暗号化ありに変えるには、スナップショットから暗号化済みの新しいインスタンスを作成し直す必要があります。本番環境では最初から有効にしてください。

5. Performance InsightsとEnhanced Monitoringを有効にする

本番環境では、クエリの遅延原因を特定するために Performance Insights を有効にすることをおすすめします。Enhanced Monitoring と組み合わせることで、CPU・メモリ・I/Oのリアルタイム監視も可能になります。

# rds.tf(既存のaws_db_instanceに追加する設定) resource "aws_db_instance" "main" { # ... 既存の設定 ... # Performance Insights(クエリ単位の遅延分析) performance_insights_enabled = true performance_insights_retention_period = 7 # 7日間無料。31日・731日は有料 # Enhanced Monitoring(OSレベルのメトリクス収集) monitoring_interval = 60 # 秒単位(0で無効、60が標準) monitoring_role_arn = aws_iam_role.rds_monitoring.arn } # Enhanced Monitoring用のIAMロール resource "aws_iam_role" "rds_monitoring" { name = "rds-monitoring-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "monitoring.rds.amazonaws.com" } Action = "sts:AssumeRole" }] }) } resource "aws_iam_role_policy_attachment" "rds_monitoring" { role = aws_iam_role.rds_monitoring.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole" } # Enhanced Monitoringのログ保持期間(CloudWatch Logs) resource "aws_cloudwatch_log_group" "rds_enhanced_monitoring" { name = "RDSOSMetrics" retention_in_days = 30 }

Performance Insights の保存期間は7日間が無料です。monitoring_interval を 0 以外に設定する場合は監視用IAMロールが必須です。monitoring_interval を設定しているのに monitoring_role_arn が抜けていると apply がエラーになるため、セットで設定してください。

CloudWatch Logs の RDSOSMetrics ロググループは Enhanced Monitoring を有効にすると自動作成されますが、aws_cloudwatch_log_group で明示的に管理することでログ保持期間をコードで制御できます。

6. RDS IAM認証でパスワード管理を不要にする

iam_database_authentication_enabled = true を設定すると、MySQLのパスワードの代わりにAWS IAMトークンでRDSに接続できます。EC2やLambdaからIAMロールで接続する構成では、パスワードのローテーション管理が不要になります。

resource "aws_db_instance" "main" { # ... iam_database_authentication_enabled = true } # アプリサーバーのIAMロールにRDS接続権限を付与 resource "aws_iam_policy" "rds_iam_auth" { name = "rds-iam-auth-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = "rds-db:connect" Resource = "arn:aws:rds-db:ap-northeast-1:123456789012:dbuser/main-mysql/appuser" }] }) }

IAM認証を使う場合、MySQLにはIAM認証専用のDBユーザーを作成する必要があります。接続の実行コマンドは以下のとおりです。

# IAMトークンを取得してMySQLに接続する TOKEN= mysql -h main-mysql.c9akciq32a3n.ap-northeast-1.rds.amazonaws.com -u appuser --password=$TOKEN --enable-cleartext-plugin appdb

IAMトークンの有効期限は15分です。アプリケーション側では接続時に毎回トークンを生成する実装が必要になります。パスワード管理のコストと実装コストのトレードオフを考慮して採用を判断してください。

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, @@collation_server;" Enter password: +-------------+------------------------+--------------------+ | @@time_zone | @@character_set_server | @@collation_server | +-------------+------------------------+--------------------+ | Asia/Tokyo | utf8mb4 | utf8mb4_unicode_ci | +-------------+------------------------+--------------------+ # ポートレベルでの疎通確認(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.

原因1:aws_db_subnet_group の subnet_ids に同一AZのサブネットを2つ指定している
原因2:subnet_ids に存在しないサブネットIDを記載している(スペルミス・環境の取り違え)

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

# サブネットのAZを確認する $ aws ec2 describe-subnets --subnet-ids subnet-aaa111 subnet-bbb222 --query 'Subnets[*].[SubnetId,AvailabilityZone]' --output table

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 は別物)

原因1:engine_version が "8.0.35" なのに family = "mysql8.4" と指定している
原因2:Aurora MySQLに対して family = "mysql8.0"(非Aurora)を指定している

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

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

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

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

GitHub Actions を使っている場合はジョブレベルの timeout-minutes: 90 も合わせて設定してください。Terraformの timeouts だけ延ばしてもCI側がジョブを打ち切ると apply が中断されます。

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

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

原因1:RDSのセキュリティグループにアプリサーバーのSGが登録されていない
原因2:アプリサーバーのセキュリティグループIDとTerraformのリソース参照がずれている
原因3:RDSがプライベートサブネットにあるのに、アプリサーバーから異なるVPCを経由しようとしている

# アプリサーバー(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での許可設定(source_security_group_id = aws_security_group.app.id)を使っている場合、SGのIDが正しく参照されているかを確認するのが最速の切り分けです。

5. DBInstanceIdentifierが既に存在するエラー

一度作成したRDSをコンソールから手動で削除した後に terraform apply を実行すると、Terraformのstateには記録が残っているため「既に存在する」エラーが発生するケースがあります。逆に、既存のRDSをTerraform管理下に取り込む場合は terraform import を使います。

$ terraform apply Error: creating RDS DB Instance (main-mysql): DBInstanceAlreadyExists: DB Instance already exists. # 既存RDSをTerraform管理下に取り込む場合はimportを使う $ terraform import aws_db_instance.main main-mysql # コンソールで手動削除済みでstateだけ残っている場合はstateから削除してから再apply $ terraform state rm aws_db_instance.main $ terraform apply

terraform state rm はstateからリソースの記録を消すだけでAWS上のリソースには触れません。手動削除済みのRDSのstateを消したい場合に使います。Terraform 1.5以降では import ブロックをHCLで書けるため、既存リソースの取り込みはよりコードベースで管理しやすくなっています。

本記事のまとめ

TerraformでRDSをプロビジョニングする設計のポイントをまとめます。
やりたいこと 使うリソース・設定
既存VPCをコードで参照する data "aws_vpc" "main" { tags = { Name = "..." } }
RDSのサブネットグループを定義する resource "aws_db_subnet_group" { subnet_ids = [...] }
RDS専用セキュリティグループを作る resource "aws_security_group_rule" { from_port = 3306 }
MySQLインスタンスを作成する resource "aws_db_instance" { engine = "mysql" }
文字コード・タイムゾーンを設定する resource "aws_db_parameter_group" { parameter { name = "time_zone" } }
最大接続数を設定する aws_db_parameter_group { parameter { name = "max_connections" } }
パラメータグループを無停止で差し替える aws_db_parameter_group { lifecycle { create_before_destroy = true } }
誤削除を二重防止する aws_db_instance { deletion_protection = true, lifecycle { prevent_destroy = true } }
マイナーバージョン自動パッチのdriftを抑制する aws_db_instance { lifecycle { ignore_changes = [engine_version] } }
マルチAZフェイルオーバーを有効にする aws_db_instance { multi_az = true }
ストレージを暗号化する aws_db_instance { storage_encrypted = true }
Performance Insightsを有効にする aws_db_instance { performance_insights_enabled = true }
IAM認証でパスワード管理を不要にする aws_db_instance { iam_database_authentication_enabled = true }
Secrets Managerでパスワードを管理する data "aws_secretsmanager_secret_version" { secret_id = "prod/rds/..." }
apply後にRDS状態を確認する aws rds describe-db-instances --db-instance-identifier main-mysql
aws_db_instance は依存リソースが多いぶん、作成順序を事前に把握してHCLを整理することが安定運用の鍵です。パスワード管理には sensitive 変数またはSecrets Managerを使い、deletion_protection・skip_final_snapshot = false・storage_encrypted = true のセットを本番環境の最低ラインとして設定してください。パラメータグループには lifecycle { create_before_destroy = true } を、aws_db_instance には lifecycle { ignore_changes = [engine_version] } を加えておくと、バージョンアップ時のplan差分ノイズとdestroyエラーを事前に防げます。

次に読む記事:
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人材の育成に取り組んでいる。

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