Closed9

Terraform で Cloudflare R2 の Token, Access Key ID, Secret Access Key を管理する

9sako69sako6

(未来からのまとめ)

  • Terraform で Cloudflare の Token, Access Key ID, Secret Access Key は作れる
  • 公式ドキュメントには書いていないが、トークンのスコープも絞れる

やりたいこと

Cloudflare R2 のバケットに CORS 設定をしたい場合、Cloudflare Provider では実現できず、AWS Provider を使う必要があります。

AWS Provider を使って Cloudflare R2 を管理する場合、Access Key ID, Secret Access Key を発行して以下のように provider に渡す必要があります。
これらトークンも Terraform で発行することができます(後述)。

公式ドキュメントの例の抜粋:

modules/r2/main.tf
provider "aws" {
  region = "us-east-1"

  access_key = <R2 Access Key>
  secret_key = <R2 Secret Key>

  skip_credentials_validation = true
  skip_region_validation      = true
  skip_requesting_account_id  = true

  endpoints {
    s3 = "https://<account id>.r2.cloudflarestorage.com"
  }
}
9sako69sako6

Cloudflare Provider には cloudflare_api_token リソースが用意されており、API Token を発行できます。

https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/api_token

API Token を発行できるのはわかるものの、Access Key ID, Secret Access Key はどうやって作るんだと思っていたら、どうやらこのリソースの id が Access Key ID, 発行された API Token を sha256 したものが Secret Access Key でした。

9sako69sako6

例:

module/cloudflare_account_token/outputs.tf
output "token" {
  value       = cloudflare_api_token.default.value
  sensitive   = true
  description = "Token value"
}

output "access_key_id" {
  value       = cloudflare_api_token.default.id
  description = "Access Key ID"
}

output "secret_access_key" {
  value       = sha256(cloudflare_api_token.default.value)
  sensitive   = true
  description = "Secret Access Key"
}

output "name" {
  value       = cloudflare_api_token.default.name
  description = "Name of the API Token"
}

module/cloudflare_account_token/main.tf
terraform {
  required_version = "~> 1.8.0"

  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.36"
    }
  }
}

resource "cloudflare_api_token" "default" {
  name = var.token_name
  policy {
    permission_groups = var.permission_groups

    resources = {
      "com.cloudflare.api.account.${var.cloudflare_account_id}" = "*"
    }
  }
}
module/cloudflare_account_token/variables.tf
variable "cloudflare_account_id" {
  description = "The Cloudflare account ID"
  type        = string
  sensitive   = true
}

variable "token_name" {
  description = "The name of the token"
  type        = string
}

variable "permission_groups" {
  description = "The permission groups for the token. https://developers.cloudflare.com/fundamentals/api/reference/permissions/"
  type        = set(string)
}

9sako69sako6

WIP

発行したトークンをどうやって provider に渡すかですが、私は R2 用 module と Token 用 module をそれぞれ用意して、R2 用 module の中に provider "aws" を書いています。

main.tf
// e.g.:
data "cloudflare_api_token_permission_groups" "all" {}

module "cloudflare_r2_token" {
  source = "../../../modules/cloudflare_account_token"

  token_name = "terraform-app-r2-management"
  permission_groups = [
    data.cloudflare_api_token_permission_groups.all.account["Workers R2 Storage Write"],
    data.cloudflare_api_token_permission_groups.all.account["Workers R2 Storage Read"],
  ]
  cloudflare_account_id = data.sops_file.secrets.data["cloudflare.account_id"]
}

module "object_storage" {
  source = "../../../modules/cloudflare_r2"

  cloudflare_r2_token             = module.cloudflare_r2_token.token
  cloudflare_r2_access_key_id     = module.cloudflare_r2_token.access_key_id
  cloudflare_r2_secret_access_key = module.cloudflare_r2_token.secret_access_key
}

modules/cloudflare_r2/main.tf
// e.g.:
provider "aws" {
  region = "APAC"

  access_key = var.cloudflare_r2_access_key_id
  secret_key = var.cloudflare_r2_secret_access_key

  skip_requesting_account_id  = true
  skip_credentials_validation = true
  skip_region_validation      = true

  endpoints {
    s3 = "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com"
  }
}
9sako69sako6

元々、tfstate 等 Terraform 必要な情報を管理するためにアプリケーション用とは別のルートモジュール management をもっていた。
その management であらかじめ Cloud Secret Manager に各種 R2 トークンを入れておく。

data "cloudflare_api_token_permission_groups" "all" {}

resource "cloudflare_api_token" "r2_write" {
  name = "terraform-demo-r2"

  policy {
    permission_groups = [
      data.cloudflare_api_token_permission_groups.all.account["Workers R2 Storage Write"],
      data.cloudflare_api_token_permission_groups.all.account["Workers R2 Storage Read"],
    ]

    resources = {
      "com.cloudflare.api.account.${var.cloudflare_account_id}" = "*"
    }
  }
}

resource "google_project_service" "secret_manager" {
  service = "secretmanager.googleapis.com"
}

resource "google_secret_manager_secret" "cloudflare_r2_access_key_id" {
  secret_id = "cloudflare_r2_access_key_id"

  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "cloudflare_r2_access_key_id" {
  secret = google_secret_manager_secret.cloudflare_r2_access_key_id.id

  secret_data = cloudflare_api_token.r2_write.id
}

resource "google_secret_manager_secret" "cloudflare_r2_secret_access_key" {
  secret_id = "cloudflare_r2_secret_access_key"

  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "cloudflare_r2_secret_access_key" {
  secret = google_secret_manager_secret.cloudflare_r2_secret_access_key.id

  secret_data = sha256(cloudflare_api_token.r2_write.value)
}

resource "google_secret_manager_secret" "cloudflare_r2_token" {
  secret_id = "cloudflare_r2_token"

  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "cloudflare_r2_token" {
  secret = google_secret_manager_secret.cloudflare_r2_token.id

  secret_data = cloudflare_api_token.r2_write.value
}

9sako69sako6

ルートモジュール側から Secret Manager の値で Provider を定義して、無事に module から Provider 定義を消すことができた。

maiin.tf
data "google_secret_manager_secret_version" "cloudflare_r2_access_key_id" {
  version = "latest"
  secret  = "cloudflare_r2_access_key_id"
}

data "google_secret_manager_secret_version" "cloudflare_r2_secret_access_key" {
  version = "latest"
  secret  = "cloudflare_r2_secret_access_key"
}

provider "aws" {
  region = "APAC" # Asia-Pacific

  access_key = data.google_secret_manager_secret_version.cloudflare_r2_access_key_id.secret_data
  secret_key = data.google_secret_manager_secret_version.cloudflare_r2_secret_access_key.secret_data

  skip_requesting_account_id  = true
  skip_credentials_validation = true
  skip_region_validation      = true

  endpoints {
    s3 = "https://${data.sops_file.secrets.data["cloudflare.account_id"]}.r2.cloudflarestorage.com"
  }
}
9sako69sako6

R2 トークンのスコープを絞る方法

data "cloudflare_api_token_permission_groups" "all" {}

resource "cloudflare_api_token" "demo" {
  name = var.token_name
  policy {
    permission_groups = [
      data.cloudflare_api_token_permission_groups.all.account["Workers R2 Storage Write"],
      data.cloudflare_api_token_permission_groups.all.account["Workers R2 Storage Read"],
    ]

    resources = {
      "com.cloudflare.api.account.${var.cloudflare_account_id}" = "*"
    }
  }
}

上記の方法でトークンを作成すると、All buckets, Admin Read & Write の権限をもつトークンが生成されます。

例えば、特定のバケットに対して、Object Read only のトークンを作るには以下のようにします。

data "cloudflare_api_token_permission_groups" "all" {}

resource "cloudflare_api_token" "demo" {
  name = var.token_name
  policy {
    permission_groups = [
      data.cloudflare_api_token_permission_groups.all.r2["Workers R2 Storage Bucket Item Read"],
    ]

    resources = {
      "com.cloudflare.edge.r2.bucket.${var.cloudflare_account_id}_default_${var.bucket_name}" = "*"
    }
  }
}

参考

https://github.com/Cyb3r-Jak3/terraform-cloudflare-r2-api-token/tree/main

このスクラップは6ヶ月前にクローズされました