表題の通りです。
地味に苦戦したので書きます。
結論のコードだけ見たい方は、こちらから。
前提
CloudflareのDNSをTerraformで管理したいのですが、そのためのAPI TokenもTerraformで管理したいという状態です。
大まかな方針としては、TerraformでAPI Tokenを作成するためのAPI Tokenを手動で作り、それを使ってAPI TokenをTerraformから作成します。Terraformから作成するAPI TokenはDNSのWriteとAPI Tokenの意図せぬ変更を検知出来るようにAPI Tokenのreadを付与します。一番最初に手動で作ったtokenはrevokeします。(かなりガバガバですが許してください。)
API Tokenとは
Cloudflareには、APIを使う際に主に3種類のクレデンシャルが存在します。それぞれ、Account API Token, User API Token, (Global) API Keyです。(Global) API Keyは権限などの制限が出来ず非推奨化されています。User API Tokenはユーザーに紐づくトークンであるのに対して、Account API Tokenはアカウントに紐づきます。
便宜的に User API Token と読んでいますが、Cloudflareのドキュメント上では、User API Token は API Token と書かれています。ドキュメントを読むときは Account API Token と User API Token を混同しないように気を付けてください。
以下ではAccount API Tokenを管理する前提で進めますが、おそらくUser API Tokenでもそこまで大差はないと思います。
Account API Tokenを取得する
TerraformからAccount API Tokenを作成するための一時的なAccount API Tokenを取得します。一時的なものなのでこちらはWebUIから手動で。
ダッシュボードのトップページから、 「アカウントの管理」>「アカウントAPIトークン」を選択。

その後は「トークンを作成する」ボタンを押して、「追加のトークンを作成」というテンプレートを使うと楽だと思います。作成したらトークンが表示されるのでどっかにメモ。
プロフィールから発行出来るAPIトークンはUser API Tokenです。間違えないように注意してください。
Terraform の諸々の設定
下記ドキュメントを見ながらproviderとか諸々の設定します。
私は下記のようにしました。
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5"
}
}
}
provider "cloudflare" {
}
variable "account_id" {
description = "cloudflare account id"
type = string
sensitive = true
}
同時に下記の環境変数もセットします。
export CLOUDFLARE_API_TOKEN=hoge # 先ほどメモしたトークン
export TF_VAR_account_id=fuga # アカウントID
アカウントIDは下記の記事を参考にして取得したものをいれてください。
policyの設定
さて、ドキュメントによるとAccount API Tokenを作るには下記のような感じで出来るようです。下記はドキュメントの例を参考に簡略化したものです。
resource "cloudflare_account_token" "example_account_token" {
account_id = "023e105f4ecef8ad9ca31a8372d0c353"
name = "readonly token"
policies = [{
effect = "allow"
permission_groups = [{
id = "c8fed203ed3043cba015a93ad1616f1f"
}, {
id = "82e64a83756745bbbb1c9c2701bf816b"
}]
resources = {
foo = "string"
}
}]
}
permission_groups の id の取得
permission_groupsのidを設定したいところなのですが、こちら文字列をべた書きが分かりにくいのでどうにかData Sourceから取得したいです。
私はcloudflare_account_api_token_permission_groups_listというものを使って下記のようにしました。
data "cloudflare_account_api_token_permission_groups_list" "dns_write" {
account_id = var.account_id
name = urlencode("DNS Write")
scope = "com.cloudflare.api.account.zone"
}
data "cloudflare_account_api_token_permission_groups_list" "account_api_tokens_read" {
account_id = var.account_id
name = urlencode("Account API Tokens Read")
scope = "com.cloudflare.api.account"
}
cloudflare_account_api_token_permission_groupsというData Sourceで出来そうに見えますが、なぜか出来ないので注意してください。ドキュメントについてはissueも上がっています。
nameやscopeは下記から確認できます。nameはURLエンコードをする必要があるのですが、直接書くと読みにくいためurlencode()を使用しています。
上記のページに記載がないものもある(実際にAccount API Tokens Readは載っていませんでした)ので注意してください。
確実なのは、CloudflareのAPIを叩いて確認することです。前述の環境変数が入っている状態なら、下記コマンドで取得できます。
curl https://api.cloudflare.com/client/v4/accounts/$TF_VAR_account_id/tokens/permission_groups -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
idの取得はone(data.cloudflare_account_api_token_permission_groups_list.dns_write.result).idのように行う想定です。
resources の設定
さて、いまのところ下記のような感じですが、この状態だと resources の設定がなく怒られてしまいます。
data "cloudflare_account_api_token_permission_groups_list" "dns_write" {
account_id = var.account_id
name = urlencode("DNS Write")
scope = "com.cloudflare.api.account.zone"
}
data "cloudflare_account_api_token_permission_groups_list" "account_api_tokens_read" {
account_id = var.account_id
name = urlencode("Account API Tokens Read")
scope = "com.cloudflare.api.account"
}
resource "cloudflare_account_token" "account_api_token" {
account_id = var.account_id
name = "test"
policies = [ {
effect = "allow"
permission_groups = [ {
id = one(data.cloudflare_account_api_token_permission_groups_list.dns_write.result).id
}]
},{
effect = "allow"
permission_groups = [ {
id = one(data.cloudflare_account_api_token_permission_groups_list.account_api_tokens_read.result).id
} ]
} ]
}
resourcesはpolicyを適用する先を指定するものです。詳しくは下記のドキュメントを参考にしてみてください。
結果的に下記のようになりました。
resource "cloudflare_account_token" "account_api_token" {
account_id = var.account_id
name = "test"
policies = [ {
effect = "allow"
permission_groups = [ {
id = one(data.cloudflare_account_api_token_permission_groups_list.dns_write.result).id
}]
resources = jsonencode({
"com.cloudflare.api.account.${var.account_id}" = {
"com.cloudflare.api.account.zone.*" = "*"
}
})
},{
effect = "allow"
permission_groups = [ {
id = one(data.cloudflare_account_api_token_permission_groups_list.account_api_tokens_read.result).id
} ]
resources = jsonencode({
"com.cloudflare.api.account.${var.account_id}" = "*"
})
} ]
}
terraform-provider-cloudflareのv5.13.0で破壊的な変更が入り、resourcesにはjsonencodeが必要になりました。
完成版
一応まとめて置いておきます
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5"
}
}
}
provider "cloudflare" {
}
variable "account_id" {
description = "cloudflare account id"
type = string
sensitive = true
}
data "cloudflare_account_api_token_permission_groups_list" "dns_write" {
account_id = var.account_id
name = urlencode("DNS Write")
scope = "com.cloudflare.api.account.zone"
}
data "cloudflare_account_api_token_permission_groups_list" "account_api_tokens_read" {
account_id = var.account_id
name = urlencode("Account API Tokens Read")
scope = "com.cloudflare.api.account"
}
resource "cloudflare_account_token" "account_api_token" {
account_id = var.account_id
name = "test"
policies = [ {
effect = "allow"
permission_groups = [ {
id = one(data.cloudflare_account_api_token_permission_groups_list.dns_write.result).id
}]
resources = jsonencode({
"com.cloudflare.api.account.${var.account_id}" = {
"com.cloudflare.api.account.zone.*" = "*"
}
})
},{
effect = "allow"
permission_groups = [ {
id = one(data.cloudflare_account_api_token_permission_groups_list.account_api_tokens_read.result).id
} ]
resources = jsonencode({
"com.cloudflare.api.account.${var.account_id}" = "*"
})
} ]
}
参考リンク