Terraform + Aurora Data APIでDBユーザー作成をIaC化した話¶
チェック¶
- [ ] 本文を確認した
- [ ] 概要を確認した
- [ ] タグを確認した
- [ ]
inbox/直下へ移行した
概要¶
新規プロダクトの AWS インフラ構築を Terraform と Devin でかなり自動化している一方、Aurora MySQL の DB ユーザー作成と権限付与だけが手作業で残っていたため、Aurora Data API と Terraform の terraform_data を使って IaC 化した記事。
CREATE USER、GRANT、DROP USER、REVOKE を Terraform から実行し、パスワードは SSM Parameter Store に置く。
さらに Terraform 1.10 の ephemeral values と AWS Provider 6 系の write-only attributes を使い、DB パスワードを Terraform state に残さない工夫も紹介している。
本文¶
Timee では、新規プロダクトを立ち上げる際に必要な AWS アカウント、VPC、ECS、Aurora、IAM、バックエンドアプリケーション、CI/CD、Datadog、監査ログなどの雛形構築を自動化している。 Devin を使ってかなりの部分を生成できるようになったが、最後に手作業として残っていたのが DB ユーザーの作成と権限付与だった。
従来は踏み台サーバーに入って SQL を実行していた。
CREATE USER 'app_user'@'%' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'%';
この作業は頻度が低くても、手作業である以上、作業漏れ、権限の差分、監査しづらさが残る。 そこで、既存のインフラ管理と同じ Terraform に寄せ、DB ユーザー作成もコードで管理する方針を取っている。
Terraform を選んだ理由¶
DB ユーザー作成は Ansible やスクリプトでも実装できる。 しかし、対象の Aurora や SSM Parameter Store、IAM、ネットワークなどはすでに Terraform で管理されている。 ユーザー作成だけ別ツールに分けると、どの環境にどのユーザーがあるのか、何に依存しているのかが追いづらくなる。
Aurora MySQL 3.07 以降では RDS Data API が使える。
Data API を使えば、踏み台サーバーや直接の MySQL 接続なしで、AWS API 経由で SQL を実行できる。
Terraform から local-exec で aws rds-data execute-statement を呼び出すことで、DB ユーザー作成を Terraform のライフサイクルに組み込める。
DBユーザー作成リソース¶
実装の中心は terraform_data。
terraform_data は、外部コマンド実行やローカルな副作用を Terraform の管理対象として扱いたいときに使える。
DB ユーザー作成では、Aurora クラスター ARN、Secrets Manager 上のマスターユーザー secret ARN、DB 名、ユーザー名、パスワードを保持する SSM Parameter 名を input に持たせる。
作成時には SSM Parameter Store からパスワードを復号して取り出し、Data API で CREATE USER IF NOT EXISTS を実行する。
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
}
}
破棄時には DROP USER IF EXISTS を実行する。
これにより、Terraform 上でユーザーリソースを削除したときに DB 上のユーザーも削除される。
権限付与リソース¶
ユーザー作成とは別に、権限付与も terraform_data で扱う。
権限は grants のリストとして受け取り、for_each で GRANT 文を実行する。
resource "terraform_data" "db_grant" {
for_each = {
for idx, grant in var.grants : idx => grant
}
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, "*")
}
depends_on = [terraform_data.db_user]
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
}
}
depends_on でユーザー作成後に権限付与されるようにしている。
破棄時は REVOKE を実行するが、すでにユーザーや権限が消えている可能性もあるため、失敗しても全体破棄が止まりすぎないように || true を付けている。
権限は DML と DDL を分けて渡せる。 たとえばアプリケーション用ユーザーに通常の読み書き権限と、必要に応じた DDL 権限を別々に定義できる。
モジュール利用例¶
記事で紹介されている利用イメージは、Aurora モジュールと SSM Parameter を作ったあと、DB ユーザーモジュールを呼び出す形。
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
grants = [
{
privileges = "SELECT, INSERT, UPDATE, DELETE"
},
{
privileges = "CREATE, ALTER, DROP, INDEX, REFERENCES"
}
]
depends_on = [
aws_ssm_parameter.app_db_password,
module.aurora_main
]
}
これで、DB ユーザー、パスワードの取得元、権限付与、Aurora への依存関係が Terraform の中に残る。 新規環境を作るときも、踏み台から SQL を手で流す手順を省ける。
パスワードを state に残さない¶
Terraform でパスワードを扱うときの重要な問題は state。
sensitive = true を付けても plan や apply の表示がマスクされるだけで、値自体は state に保存される。
記事では、Terraform 1.10 の ephemeral values と Terraform 1.11 / AWS Provider 6 系の write-only attributes を使って、生成したパスワードを state に保存しない方法を紹介している。
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 の評価中だけ値を持ち、state に残さない。
value_wo は AWS Provider 側の write-only attribute で、SSM Parameter へ値を書き込むが state には保持しない。
これにより、DB パスワードを Terraform state から漏らさない構成にできる。
制約と割り切り¶
この実装には制約がある。
terraform_data の provisioner は create/destroy には使いやすいが、update 用の provisioner はない。
input が変わるとリソースの置き換えになり、DB ユーザーの場合は DROP USER と CREATE USER が走る可能性がある。
パスワード変更やローテーションをこのモジュールだけで安全に扱うには向かない。
つまり、この仕組みは初期ユーザー作成と権限付与の自動化に向いている。 パスワードローテーションは別の運用、たとえば Secrets Manager のローテーションや専用の変更手順で扱う方が安全。
読み替え¶
この記事は、Terraform から DB 内部の状態を扱うときの実務的な割り切りが参考になる。
すべてを Terraform provider として美しく実装するのではなく、Data API と terraform_data を使って「初期作成に必要な副作用」を管理対象にする。
同時に、state に秘匿情報を残さないための Terraform 新機能も組み合わせている。 IaC 化の目的は、すべてを Terraform に閉じ込めることではなく、手作業を減らし、依存関係と監査可能性を上げ、危険な値を state に残さないことにある。
要点¶
- Aurora MySQL 3.07+ の Data API を使うと、踏み台なしで SQL を AWS API 経由で実行できる。
terraform_dataとlocal-execでCREATE USER、GRANT、DROP USER、REVOKEを Terraform に組み込める。- DB パスワードは SSM Parameter Store に置き、Terraform 実行時に取得する。
sensitive = trueだけでは state への保存は防げない。- Terraform 1.10 の ephemeral values と AWS Provider の write-only attributes を使うと、生成パスワードを state に残さず SSM に書ける。
- provisioner は update に弱いため、パスワードローテーションまでこの方式で無理に扱わない。