以下の内容はhttps://tech.timee.co.jp/entry/2026/03/30/155108より取得しました。


Terraform + Aurora Data APIでDBユーザー作成をIaC化した話

はじめに

こんにちは。プラットフォームエンジニアリングチームに所属している徳富(@yannKazu1)です。

新規プロダクトを立ち上げるとき、インフラ構築って意外とやることが多いですよね。その中でも地味にめんどくさいのがDBユーザーの作成と権限付与。手動でやると「あ、権限つけ忘れた」「このユーザー名スペルミスってない?」みたいなヒヤリハットが発生しがちです。

今回は、この作業をTerraformでIaC化した話を書いていきます。

背景:ボイラープレートでインフラ構築を爆速にしている

弊社ではTerraformのボイラープレートと、それをもとにインフラを構築するためのDevinへの指示プロンプトをセットで管理しているリポジトリがあります。

新規プロダクトのインフラが必要になったら、このリポを使ってDevinにお願いするだけ。数時間もあれば、AWSアカウントの作成、VPC・ECS・Auroraなどの基盤構築、Terraform実行に必要なIAMロール・バックエンド・CI/CDワークフローの設定に加え、Datadog設定、監査設定、ログ基盤の作成まで、必要なインフラがひととおり立ち上がります。

このボイラープレートを整備するにあたって目指したのは、「Devinにお願いするだけで、新規プロダクトのインフラを簡単に作れる状態」でした。ところが、DBユーザーの作成だけはどうしても手動作業が残ってしまっていました

せっかくDevinに投げれば数時間でインフラができるのに、DBユーザーだけは人間が踏み台経由でDBに繋いで CREATE USER して GRANT して……とやらないといけない。これだとボイラープレートの意味が薄れてしまいますし、手動オペレーションにはミスのリスクもあります。

ここをTerraformでIaC化できれば、ボイラープレートにサクッと組み込めて、Devinに任せるだけでインフラ構築が完結するようになります。

なぜTerraform?

DBユーザーの管理といえばAnsibleを使うパターンも考えました。ただ、弊社のインフラは基本的にTerraformで一元管理しているので、できればTerraformの中で完結させたい。ツールが増えると学習コストも運用コストも増えますし、ボイラープレートにAnsibleのステップを追加するよりも、Terraformモジュールとして組み込む方がシンプルです。

というわけで、Terraformでなんとかする方向で調査を進めました。

Aurora Data APIという選択肢

弊社ではAurora MySQLを使用しており、バージョン3.07以降でRDS Data APIに対応しています。

Data APIは、HTTPSエンドポイント経由でSQLを実行できるAPIです。従来のようにVPC内から直接DBに接続する必要がなく、AWS CLIやSDKからサクッとSQLを叩けます。

これを terraform_data リソースの local-exec プロビジョナーと組み合わせれば、Terraformの中からDBユーザーを作成できるというわけです。

モジュールの実装

実際に作ったTerraformモジュールを紹介します。

DBユーザーの作成・削除

resource "terraform_data" "db_user" {
  input = {
    rds_cluster_arn    = var.rds_cluster_arn
    rds_secret_arn     = var.rds_secret_arn
    database_name      = var.database_name
    username           = var.username
    ssm_parameter_name = var.ssm_parameter_name
  }

  provisioner "local-exec" {
    command = <<-EOT
      PASSWORD=$(aws ssm get-parameter --name "${self.input.ssm_parameter_name}" --with-decryption --query 'Parameter.Value' --output text)
      aws rds-data execute-statement \
        --resource-arn "${self.input.rds_cluster_arn}" \
        --secret-arn "${self.input.rds_secret_arn}" \
        --database "${self.input.database_name}" \
        --sql "CREATE USER IF NOT EXISTS '${self.input.username}'@'%' IDENTIFIED BY '$PASSWORD'"
    EOT
  }

  provisioner "local-exec" {
    when = destroy

    command = <<-EOT
      aws rds-data execute-statement \
        --resource-arn "${self.input.rds_cluster_arn}" \
        --secret-arn "${self.input.rds_secret_arn}" \
        --database "${self.input.database_name}" \
        --sql "DROP USER IF EXISTS '${self.input.username}'@'%'"
    EOT
  }
}

ポイントは以下のとおりです。

  • terraform_data リソースを使って、local-exec プロビジョナーでData API経由のSQLを実行
  • CREATE USER IF NOT EXISTS で冪等性を担保
  • when = destroy のプロビジョナーで、terraform destroy 時にユーザーを自動削除
  • パスワードはSSM Parameter Storeから取得(後述の工夫で安全に管理)

権限の付与・取り消し

resource "terraform_data" "db_grant" {
  for_each = { for idx, grant in var.grants : idx => grant }

  depends_on = [terraform_data.db_user]

  input = {
    rds_cluster_arn = var.rds_cluster_arn
    rds_secret_arn  = var.rds_secret_arn
    database_name   = var.database_name
    username        = var.username
    privileges      = each.value.privileges
    grant_database  = coalesce(each.value.database, var.database_name)
    grant_table     = coalesce(each.value.table, "*")
  }

  provisioner "local-exec" {
    command = <<-EOT
      aws rds-data execute-statement \
        --resource-arn "${self.input.rds_cluster_arn}" \
        --secret-arn "${self.input.rds_secret_arn}" \
        --database "${self.input.database_name}" \
        --sql "GRANT ${self.input.privileges} ON ${self.input.grant_database}.${self.input.grant_table} TO '${self.input.username}'@'%'"
    EOT
  }

  provisioner "local-exec" {
    when = destroy

    command = <<-EOT
      aws rds-data execute-statement \
        --resource-arn "${self.input.rds_cluster_arn}" \
        --secret-arn "${self.input.rds_secret_arn}" \
        --database "${self.input.database_name}" \
        --sql "REVOKE ${self.input.privileges} ON ${self.input.grant_database}.${self.input.grant_table} FROM '${self.input.username}'@'%'" || true
    EOT
  }
}

for_each で権限セットを柔軟に定義できるようにしています。DMLとDDLを分けて付与したい、みたいなケースにも対応可能です。

使い方

モジュールの呼び出しはこんな感じです。

module "db_user_app" {
  source = "../../modules/db_user"

  rds_cluster_arn    = module.aurora_main.cluster_arn
  rds_secret_arn     = module.aurora_main.cluster_master_user_secret[0].secret_arn
  database_name      = "hoge_db"
  username           = "hoge_app_user"
  ssm_parameter_name = aws_ssm_parameter.app_db_password.name

  depends_on = [
    aws_ssm_parameter.app_db_password,
    module.aurora_main,
  ]

  grants = [
    {
      # DML権限: データの読み書き
      privileges = "SELECT, INSERT, UPDATE, DELETE"
    },
    {
      # DDL権限: マイグレーション実行用(テーブル作成・変更・削除、インデックス操作)
      privileges = "CREATE, ALTER, DROP, INDEX, REFERENCES"
    }
  ]
}

DMLとDDLの権限を分けて書けるのが個人的に気に入っています。どの権限を付与しているかが一目でわかりますし、将来的に読み取り専用ユーザーを追加したいときもモジュールを再利用するだけでOKです。

パスワードをstateに残さない工夫

ここが今回一番こだわったポイントです。

Terraformでパスワードを扱うとき、普通にやるとstateファイルにパスワードが平文で残ってしまいます。sensitive = true をつけても、plan出力でマスクされるだけでstateには書き込まれてしまう。これはセキュリティ的によろしくないですよね。

そこで活用したのが、Terraform 1.10で導入された ephemeral リソースと、Terraform 1.11で導入された value_wo(write-only引数) です。

ephemeral "random_password" "app_db" {
  length  = 32
  special = false
}

resource "aws_ssm_parameter" "app_db_password" {
  name             = "/${var.app_name}/${var.env}/db/app_user_password"
  type             = "SecureString"
  value_wo         = ephemeral.random_password.app_db.result
  value_wo_version = 1
}

ephemeral リソースとは

Terraform 1.10で登場した新しいリソースタイプです。通常の resourcedata と違い、planやstateに一切値が保存されません。毎回のplan/apply時に一時的に生成され、使い終わったら破棄されます。

ephemeral "random_password" を使うことで、パスワードの生成結果がstateに残ることを防げます。従来の resource "random_password" だと、生成したパスワードがstateファイルにそのまま記録されてしまっていたので、これは大きな改善です。

value_wo(write-only引数)とは

Terraform 1.11で導入されたwrite-only引数の仕組みです。aws_ssm_parameter リソースの value_wo を使うと、SSM Parameter Storeへの書き込みは行われますが、その値がTerraformのstateやplanファイルに保存されることはありません。

value_wo_version は変更検知のためのバージョン番号です。パスワードをローテーションしたい場合はこの値をインクリメントすれば、次のapply時に新しいパスワードが生成・設定されます。逆にバージョンを変えなければ、ephemeral が毎回新しいパスワードを生成しても、SSM Parameter Storeの値は更新されません。

この2つを組み合わせることで、パスワードの生成から保存まで、一度もstateファイルにパスワードが記録されないフローが実現できました。

現時点での制約:パスワードの更新には対応していない

ここまで読んで「パスワードのローテーションはどうするの?」と思った方もいるかもしれません。正直に言うと、このモジュールはパスワードの更新(ALTER USER)には対応していません

理由はシンプルで、Terraformのプロビジョナーには when = createwhen = destroy しかなく、when = update が存在しないからです。つまり、リソースの入力値が変わったときに「更新用のSQLを実行する」ということができません。

terraform_datainput が変わると、Terraformは古いリソースをdestroyしてから新しいリソースをcreateする(replace)動きになります。DBユーザーの場合、これは DROP USERCREATE USER という流れになるので、パスワード変更だけしたいケースには少々大げさです。

この when = update の追加については、HashiCorpのGitHubリポジトリにIssueが上がっています(hashicorp/terraform#35825)。いつか実装されれば、パスワードローテーションもこのモジュールの中でスマートに対応できるようになるはずです。それまでは、パスワードの更新が必要になった場合は手動対応か、別の仕組みで対応する必要があります。

とはいえ、新規プロダクト立ち上げ時の初期ユーザー作成という用途においては、create/destroyだけで十分に機能しています。

まとめ

やったことをまとめると以下のとおりです。

  • Aurora MySQLのData API(3.07以降で利用可能)を使って、Terraformの terraform_data + local-exec でDBユーザーの作成・権限付与をIaC化
  • Terraform 1.10の ephemeral リソースと1.11の value_wo を活用して、パスワードをstateに残さないセキュアな構成を実現
  • これらをモジュール化してボイラープレートに組み込み、新規プロダクトのインフラ構築をさらにスムーズに
  • プロビジョナーに when = update がないため、パスワードの更新には現時点では未対応

Terraformの ephemeralvalue_wo はまだ比較的新しい機能なので、知らない方も多いかもしれません。パスワード管理で困っている方はぜひ試してみてください。

参考リンク




以上の内容はhttps://tech.timee.co.jp/entry/2026/03/30/155108より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14