MENU

問い合わせ


    【Laravel+Vue.jsで作る管理画面をECSで動かす 第8回】RDS for MySQL と秘密情報:パスワードをコードに書かない

    管理画面のデータを置くデータベースを、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.tfDB サブネットグループ、パラメータグループ(文字コード・タイムゾーン・スロークエリ)、RDS for MySQL 8.4
    secrets.tfSSM Parameter Store の SecureString(APP_KEY の入れ物)
    iam.tfECS のタスク実行ロールと、秘密情報を読むための最小限のポリシー
    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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      コメント一覧 (1件)

      目次