管理画面のデータを置くデータベースを、AWS のマネージドサービス RDS for MySQL で用意します。あわせて、DB のパスワードや Laravel の APP_KEY といった秘密情報を「どこに置き、どう渡すか」を決めます。
「DB のパスワードが設定ファイルや引き継ぎ資料に平文で書かれていて、誰が知っているのか分からない。退職者が出るたびに不安になる…」
結論から言うと、DB のパスワードは RDS 自身に生成・保管させ(Secrets Manager で管理)、APP_KEY などは SSM Parameter Store に Terraform の外から登録し、ECS には「読んでよい秘密情報の場所」だけを IAM で許可します。こうすると、パスワードの文字列は Terraform のコードにも state にも Git にも出てきません。
前回(第7回:Terraform の土台とネットワーク)では、VPC と private サブネット、RDS 用のセキュリティグループを作りました。今回はそこに RDS を置き、秘密情報の置き場所と読み取り権限を定義します。
第7回と同じく、執筆環境には AWS アカウントがないため、terraform apply は行っていません。構文の確認(validate)と、AWS に接続しない形での plan までを確認しています。
RDS と秘密情報で作るもの
ファイル(infra/terraform/) | 内容 |
|---|---|
rds.tf | DB サブネットグループ、パラメータグループ(文字コード・タイムゾーン・スロークエリ)、RDS for MySQL 8.4 |
secrets.tf | SSM Parameter Store の SecureString(APP_KEY の入れ物) |
iam.tf | ECS のタスク実行ロールと、秘密情報を読むための最小限のポリシー |
variables.tf / outputs.tf | インスタンスクラス・Multi-AZ・バックアップ日数などの変数、接続先やシークレットの ARN の出力 |
秘密情報の置き場所は、性質で2つに分けました。
| 秘密情報 | 置き場所 | 理由 |
|---|---|---|
| DB のマスターユーザーのユーザー名・パスワード | Secrets Manager(RDS が管理) | RDS がパスワードを生成し、定期的に自動で変更(ローテーション)までしてくれる |
APP_KEY など、自動で変えないもの | SSM Parameter Store(SecureString) | 自動ローテーションが不要なら、構成と費用の構造が単純 |
RDS for MySQL 8.4 を private サブネットに置く
パスワードを RDS に管理させる
infra/terraform/rds.tf(DB インスタンスの抜粋)
resource "aws_db_instance" "main" {
identifier = "${local.name_prefix}-mysql"
engine = "mysql"
engine_version = "8.4" # マイナーバージョンは作成時点の既定版。以後は自動マイナーアップグレードで更新
instance_class = var.db_instance_class
db_name = "laravel"
username = "admin_user"
# パスワードは RDS が生成して Secrets Manager に保存する。Terraform には値が出てこない
manage_master_user_password = true
# ストレージ:gp3、暗号化あり、容量が足りなくなったら上限まで自動で拡張
storage_type = "gp3"
allocated_storage = var.db_allocated_storage
max_allocated_storage = var.db_max_allocated_storage
storage_encrypted = true
# ネットワーク:private サブネット、ECS タスクからの 3306 だけを許可した SG
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
multi_az = var.db_multi_az
parameter_group_name = aws_db_parameter_group.mysql.name
# バックアップとメンテナンス(時刻は UTC。日本時間の深夜 3〜4 時と、月曜 4〜5 時)
backup_retention_period = var.db_backup_retention_days
backup_window = "18:00-19:00"
maintenance_window = "sun:19:30-sun:20:30"
auto_minor_version_upgrade = true
copy_tags_to_snapshot = true
# エラーログとスロークエリログを CloudWatch Logs へ
enabled_cloudwatch_logs_exports = ["error", "slowquery"]
# 誤削除の防止。削除するときは最後にスナップショットを取る
deletion_protection = var.db_deletion_protection
skip_final_snapshot = false
final_snapshot_identifier = "${local.name_prefix}-mysql-final"
# (apply_immediately と tags は省略)
}
manage_master_user_password = true が今回の要点です。password を指定する代わりにこれを付けると、RDS がパスワードを生成し、Secrets Manager のシークレットに保存します。Terraform は「どのシークレットに入っているか(ARN)」だけを知っていて、パスワードの文字列を扱いません。第7回で触れたように、Terraform の変数でパスワードを渡すと state にも平文で残るため、この方法を選びました。
📰 出典:Amazon RDS と AWS Secrets Manager によるパスワード管理(Amazon RDS ユーザーガイド)
そのほかの設定の意図は次のとおりです。
publicly_accessible = falseと private サブネット:インターネットから DB に直接つながる経路を作りません。接続できるのは、第7回のセキュリティグループで許可した ECS のタスクだけです。storage_encrypted = true:保存データを暗号化します。作成後に暗号化の有無は変えられないため、最初から有効にします。backup_retention_period:自動バックアップの保持日数です。この日数の範囲で、任意の時点の状態に復元できます(ポイントインタイムリカバリ)。deletion_protectionと最終スナップショット:terraform destroyや画面の操作で誤って DB を消さないための保護です。engine_version = "8.4":マイナーバージョンを固定せず、作成時点の既定版を使います。2025年8月時点で RDS for MySQL が提供する 8.4 系のどの版になるかは、AWS アカウントがないため確認できていません。
パラメータグループ:文字コードとタイムゾーン
infra/terraform/rds.tf(パラメータグループの抜粋)
resource "aws_db_parameter_group" "mysql" {
name = "${local.name_prefix}-mysql84"
family = "mysql8.4"
# 文字コード:絵文字も保存できる utf8mb4。照合順序は Laravel の既定(config/database.php)に合わせる
parameter {
name = "character_set_server"
value = "utf8mb4"
}
parameter {
name = "collation_server"
value = "utf8mb4_unicode_ci"
}
# タイムゾーン:アプリ(config/app.php の timezone)と同じ UTC にそろえる。画面表示や締め処理の日付は日本時間に変換して扱う
parameter {
name = "time_zone"
value = "UTC"
}
# (slow_query_log = 1、long_query_time = 1 は省略)
}
タイムゾーンは、日本向けのシステムでも UTC にそろえました。第5回で、アプリは日時を UTC で保存し、締め処理などの「今日」だけを日本時間で判定するようにしています。DB だけを日本時間にすると、MySQL 側で自動的に入る日時(例:失敗したジョブの記録日時)と、アプリが保存する日時で基準がずれてしまいます。「どこを日本時間で扱うか」をアプリ側に集めておく方が、後から混乱しにくくなります。
APP_KEY は SSM Parameter Store に Terraform の外から登録する
infra/terraform/secrets.tf
# アプリの秘密情報(APP_KEY など):SSM Parameter Store の SecureString に置く
#
# Terraform は「入れ物」だけを作り、値は Terraform の外で登録する。
# aws ssm put-parameter --name /admin/stg/APP_KEY --type SecureString --overwrite --value "base64:..."
# Terraform が入れる初期値はダミーで、以後の値の変更は無視する(ignore_changes)。
# こうすると、本当の値がコード・tfvars・state のどこにも残らない。
resource "aws_ssm_parameter" "app_secret" {
for_each = var.app_secret_names
name = "/${var.project}/${var.environment}/${each.key}"
description = "${each.key} for ${local.name_prefix} (value is set outside Terraform)"
type = "SecureString"
value = "set-outside-terraform"
lifecycle {
ignore_changes = [value]
}
}
APP_KEY は、Laravel がセッションや Cookie の暗号化に使う鍵です。漏れると、暗号化されたデータを読まれたり、改ざんされたりするおそれがあります。
Terraform で作るのは「/admin/stg/APP_KEY という名前の SecureString」という入れ物だけで、値にはダミーの文字列を入れます。lifecycle { ignore_changes = [value] } を付けたので、あとから AWS CLI などで本当の値に書き換えても、Terraform はそれを元に戻しません。本当の値の登録は、権限を持つ担当者が次のように行います。
# 鍵を生成して表示する(.env は書き換えない)。表示された値は画面共有やチャットに貼らない
docker compose run --rm --no-deps app php artisan key:generate --show
# 表示された値を登録する(シェルの履歴に残さない方法で渡すのが望ましい)
aws ssm put-parameter --name /admin/stg/APP_KEY --type SecureString --overwrite --value "<生成した値>"
APP_KEY を途中で変えると、ログイン中のセッションが無効になり、APP_KEY で暗号化して保存したデータがあれば読めなくなります。環境ごとに一度生成したら、むやみに変えない値として扱います。
ECS から読むための IAM ポリシーを最小限にする
ECS のタスクを起動するとき、ECS は「タスク実行ロール」(=ECS がタスクの準備に使う権限)で、イメージの取得、ログの送信、そして秘密情報の取得を行います。取得した値は、コンテナの環境変数として渡されます(タスク定義の secrets。第9回で書きます)。
infra/terraform/iam.tf(秘密情報を読むポリシー)
data "aws_iam_policy_document" "ecs_task_execution_secrets" {
# RDS が管理する DB のマスターユーザーのシークレット(このシークレットだけ)
statement {
sid = "ReadDatabaseSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = [aws_db_instance.main.master_user_secret[0].secret_arn]
}
# SSM Parameter Store のアプリの秘密情報(この環境のパラメータだけ)
statement {
sid = "ReadAppParameters"
actions = ["ssm:GetParameters"]
resources = [for p in aws_ssm_parameter.app_secret : p.arn]
}
}
resource "aws_iam_role_policy" "ecs_task_execution_secrets" {
name = "read-secrets"
role = aws_iam_role.ecs_task_execution.id
policy = data.aws_iam_policy_document.ecs_task_execution_secrets.json
}
resources に *(すべて)を書かず、今回作った DB のシークレットと、この環境のパラメータの ARN だけを指定しています。こうしておけば、別の環境や別のシステムの秘密情報は、このロールからは読めません。イメージの取得とログの送信は、AWS 管理ポリシー AmazonECSTaskExecutionRolePolicy を付けて許可しています。
どちらの秘密情報も、AWS が用意した既定の暗号化キー(aws/secretsmanager、aws/ssm)で暗号化しているため、KMS の復号の権限を追加する必要はありません。独自の暗号化キー(カスタマー管理キー)を使う場合は、kms:Decrypt の許可も必要になります。
📰 出典:Amazon ECS タスク実行 IAM ロール(Amazon ECS デベロッパーガイド)
アプリ自身が AWS のサービス(SQS など)を使うための「タスクロール」は、タスク実行ロールとは別に作ります(第9回・第10回)。役割ごとにロールを分けると、どちらかの権限が漏れたときの影響を小さくできます。
動作確認の方法
cd infra/terraform
docker run --rm -v $PWD:/w -w /w hashicorp/terraform:1.11.4 fmt -check -recursive
docker run --rm -v $PWD:/w -w /w hashicorp/terraform:1.11.4 init -backend=false
docker run --rm -v $PWD:/w -w /w hashicorp/terraform:1.11.4 validate
grep -rin password .
筆者の環境では、次のことを確認しました。
fmt -checkは差分なし、validateはSuccess! The configuration is valid.grep -rin passwordの結果は、manage_master_user_passwordの設定とコメント・説明文だけで、パスワードの文字列は含まれない- 第7回と同じく AWS に接続しない形で
planを実行し、Plan: 42 to add(第7回の35件に、DB サブネットグループ・パラメータグループ・RDS・SSM パラメータ・IAM ロールとポリシーの7件が追加)。RDS はpublicly_accessible = false、manage_master_user_password = true、storage_encrypted = true、deletion_protection = trueで作成予定になっている - SSM パラメータの値は、plan の表示でも秘密情報として伏せられる
state にパスワードが入らないことは、この設計(RDS 側での生成)によるもので、実際に apply した state での確認はしていません。パラメータグループの値(time_zone = "UTC" など)が RDS に受け付けられるかも、AWS に接続しての確認はできていません。読者の AWS アカウントでは、terraform plan で内容を確認してから apply してください。
つまずきやすい点・セキュリティ上の注意
- パスワードの自動ローテーションと、動いているタスクの関係:RDS が管理するシークレットは、既定で7日ごとにパスワードが自動で変わります。一方、ECS がコンテナに渡す値はタスクの起動時点のものです。起動したままのタスクは、ローテーション後に新しい接続を作ろうとすると認証に失敗するおそれがあります。ローテーションの周期に合わせてタスクを入れ替える(再デプロイする)、周期を業務に合わせて変更する、などの運用を決めておく必要があります。この連載のサンプルでは、AWS 上でこの動きを確認できていません。
- アプリがマスターユーザーで接続している:説明を簡単にするため、連載ではマスターユーザーをそのままアプリの接続に使います。本番では、必要な権限だけを持つアプリ用の DB ユーザーを作るのが望ましい形です。
apply_immediately = falseの意味:インスタンスクラスの変更など、再起動を伴う設定変更は次のメンテナンス時間まで反映されません。すぐ反映したいときは、影響を確認したうえで一時的に変更します。- リードレプリカとの組み合わせ:RDS がパスワードを管理する設定では、リードレプリカ(読み取り専用の複製)の作成がサポートされていません(2025年8月時点の AWS ドキュメントの記載)。将来リードレプリカを使う予定があれば、設計時に確認します。
- SSM の値を登録する人を限定する:
ssm:PutParameterの権限を持つ人は、APP_KEYを書き換えられます。誰が秘密情報を登録・閲覧できるかを IAM で絞ります。
発注者向けメモ:DB は「止まってよい時間」と「戻せる範囲」で費用が決まる
RDS の費用と復旧力は、主に次の設定で大きく変わります。見積もりに Multi-AZ やバックアップの項目がある場合は、何を基準に決めたのかを確認してください。
| 設定 | 何が変わるか | 決めるための問い |
|---|---|---|
| Multi-AZ | 障害時に別の AZ の待機系へ自動で切り替わる。費用は一般的に単一構成のおおむね2倍 | 障害時に何時間まで止まってよいか |
| バックアップの保持日数 | 何日前までの任意の時点に戻せるか | 誤操作やデータの破損に、何日後まで気づく可能性があるか |
| インスタンスクラス・ストレージ | 処理能力と月額 | 利用者数・データ量の見込み |
「何時間止まってよいか」「何日前まで戻せればよいか」は技術ではなく業務の判断です。これを先に決めておくと、検証環境は単一構成・短い保持日数、本番は Multi-AZ・長めの保持日数、のように根拠のある構成と見積もりになります。
秘密情報の管理についても、あわせて確認しておくと安心です。
- 「DB のパスワードや暗号化キーは、どこに保管し、誰が見られますか?退職者が出たときはどうしますか?」
- 「障害やデータの誤削除があった場合、どの時点まで、どのくらいの時間で戻せますか?復元の手順は試したことがありますか?」
- 「検証環境と本番で、Multi-AZ やバックアップの設定はどう違いますか?」
- 「パスワードの自動変更は有効ですか?変更されたときにシステムが止まらない仕組みになっていますか?」
まとめと次回予告
第8回では、RDS for MySQL 8.4 と秘密情報の置き場所を定義しました。
- RDS は private サブネットに置き、
publicly_accessible = false、暗号化、削除保護、自動バックアップを有効にする - DB のパスワードは
manage_master_user_passwordで RDS に生成・管理させ、Terraform のコードと state に出さない APP_KEYは SSM Parameter Store の SecureString に Terraform の外から登録し、ignore_changesで上書きしない- ECS のタスク実行ロールには、対象のシークレットとパラメータの ARN だけを読む権限を与える
次回は「ECR・ECS on Fargate・ALB:管理画面をインターネットに公開する」です。第6回のイメージを置く ECR、nginx と php-fpm のタスク定義、HTTPS の ALB を作り、今回の秘密情報をタスク定義の secrets でコンテナに渡します。
この連載の記事一覧
この記事は連載「Laravel+Vue.jsで作る管理画面をECSで動かす」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第1回】全体像と開発環境:Laravel 12+Vue 3 スターターキットを Docker で動かす
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第2回】顧客マスタの一覧と登録:FormRequest でバリデーションする
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第3回】ロールと Policy:誰が何をできるかをコードで決める
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第4回】案件管理と一覧画面の作り込み:検索・並べ替え・ページング
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第5回】請求と非同期処理:キューとスケジューラをローカルで動かす
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第6回】本番用コンテナイメージを作る:マルチステージ Dockerfile
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第7回】Terraform の土台とネットワーク:state 管理と VPC
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第8回】RDS for MySQL と秘密情報:パスワードをコードに書かない(この記事)
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第9回】ECR・ECS on Fargate・ALB:管理画面をインターネットに公開する
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第10回】キューワーカーとスケジューラを ECS で動かす
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第11回】GitHub Actions で CI/CD:OIDC でキーを持たずにデプロイ
- 【Laravel+Vue.jsで作る管理画面をECSで動かす 第12回】運用編:ログ・監視・スケーリング・切り戻し










コメント
コメント一覧 (1件)
[…] 前回(第8回:RDS for MySQL と秘密情報)では、RDS と秘密情報の置き場所、秘密情報を読むためのタスク実行ロールを作りました。今回はそれらをタスク定義から参照します。 […]