Closed8

Terraform で Provider がない Bunny CDN を設定する

9sako69sako6

問題

Cloudflare R2 にあるリソースを Bunny CDN で配信したい。
Bunny CDN には API が存在するが、Terraform Provider はメンテナンスされているものがない。
(bunny.net の API のページは充実しててめっちゃいいな...)

方針

今回使いたいのは Pull Zone というリソースのみ。
Custom Provider を作るのはコストに見合わないと思ったので、terraform_data リソースの provisioner で API を叩いて設定していく。

9sako69sako6

必要なこと

デフォルトでどこまで設定されるかわからないが、必要なもの。

  • Pull Zone の作成
  • Origin の設定
    • Origin URL
    • Verify Origin SSL Certificate -> on
    • Follow Redirects -> on
    • Forward Host Header -> off
  • Pricing
    • Choose Tier -> Standard Tier
    • Pricing Zones -> Asia & Oceania
  • Security
    • Block Root Path Access -> on
    • Block POST Requests -> on
    • Allowed Referrers
    • Block Direct Url File Access
  • S3 Authentication
    • Enable AWS S3 Authentication
    • AWS Key
    • AWS Secret
    • AWS Region Name
9sako69sako6

Cloudflare R2 のバケットは作成済みで、カスタムドメインを設定してある。Terraform で R2 にカスタムドメインを設定するのにはかなり苦労したので下記にぶら下げておく。

https://zenn.dev/9sako6/scraps/43a97140093a2c

スパイクとして、手動で Bunny CDN と R2 の接続がうまくいくことは検証済みなので、新しいプロジェクトで Terraform で設定を再現していく。

9sako69sako6

module の構成。

.
├── main.tf
├── outputs.tf
├── requests
│   └── add_pull_zone.json.tftpl
├── state
│   └── pull_zone.json
└── variables.tf

provisioner の削除時に Pull Zone の ID が必要なので、state/pull_zone.json に保存して受け渡しています。ただし、初回の実行時には pull_zone.json が存在しないので、ダミーファイルを作る必要があります。

main.tf
locals {
  request_body = templatefile("${path.module}/requests/add_pull_zone.json.tftpl", {
    name              = var.pull_zone_name
    origin_url        = var.origin_url
    aws_key           = var.aws_key
    aws_secret        = var.aws_secret
    aws_region        = var.aws_region
    allowed_referrers = var.allowed_referrers
  })
}

data "local_file" "pull_zone" {
  filename = "${path.module}/state/pull_zone.json"
}

// API reference: https://docs.bunny.net/reference/pullzonepublic_add
resource "terraform_data" "pull_zone" {
  triggers_replace = [
    md5(local.request_body),
  ]

  input = {
    api_key             = var.api_key
    pull_zone_file_path = data.local_file.pull_zone.filename
  }

  provisioner "local-exec" {
    on_failure = fail
    when       = create
    command    = <<-EOT
      curl -f -X POST \
        --url https://api.bunny.net/pullzone \
        -H 'AccessKey: ${var.api_key}' \
        -H 'accept: application/json' \
        -H 'content-type: application/json' \
        -d '${local.request_body}' | jq '{id: (.Id | tostring), cdnFqdn: .Hostnames[0].Value}' > ${data.local_file.pull_zone.filename}
    EOT
  }

  provisioner "local-exec" {
    when       = destroy
    on_failure = fail
    command    = <<-EOT
      curl -f -X DELETE \
        --url "https://api.bunny.net/pullzone/${jsondecode(file(self.input.pull_zone_file_path)).id}" \
        -H 'AccessKey: ${self.input.api_key}'
    EOT
  }
}

data "external" "pull_zone" {
  program    = ["cat", data.local_file.pull_zone.filename]
  depends_on = [terraform_data.pull_zone]
}

resource "terraform_data" "pull_zone_ssl" {
  triggers_replace = [
    data.external.pull_zone.result.id,
    data.external.pull_zone.result.cdnFqdn
  ]

  provisioner "local-exec" {
    on_failure = fail
    when       = create
    command    = <<-EOT
      curl -f -X POST \
        --url https://api.bunny.net/pullzone/${data.external.pull_zone.result.id}/setForceSSL \
        -H 'AccessKey: ${var.api_key}' \
        -H 'accept: application/json' \
        -H 'content-type: application/json' \
        -d '{"ForceSSL": true, "Hostname": "${data.external.pull_zone.result.cdnFqdn}"}'
    EOT
  }
}


state/pull_zone.json
{
  "id": "1234567",
  "cdnFqdn": "dummy-test.b-cdn.net"
}
variables.tf
variable "pull_zone_name" {
  description = "The name and hostname of your Pull Zone where your files will be accessible"
  type        = string
}

variable "api_key" {
  description = "Bunny.net API Key"
  type        = string
  sensitive   = true
}

variable "origin_url" {
  description = "The origin URL of the object storage bucket"
  type        = string
}

variable "aws_key" {
  description = "The AWS access key used to authenticate the requests"
  type        = string
  sensitive   = true
}

variable "aws_secret" {
  description = "The AWS secret key used to authenticate the requests"
  type        = string
  sensitive   = true
}

variable "aws_region" {
  description = "The region name of your AWS S3 bucket used to authenticate the requests"
  type        = string
}

variable "allowed_referrers" {
  description = "The set of allowed referrers fqdn"
  type        = set(string)
}
outputs.tf
output "cdn_url" {
  description = "The Bunny CDN URL"
  value       = "https://${data.external.pull_zone.result.cdnFqdn}"
}

output "pull_zone_id" {
  description = "The Bunny CDN pull zone ID"
  value       = data.external.pull_zone.result.id
}

requests/add_pull_zone.json.tftpl
{
  "OriginUrl": "${origin_url}",
  "Name": "${name}",
  "FollowRedirects": true,
  "BlockRootPathAccess": true,
  "BlockPostRequests": true,
  "BlockNoneReferrer": true,
  "DisableCookies": true,
  "AllowedReferrers": ${jsonencode(allowed_referrers)},
  "AccessControlOriginHeaderExtensions": [
    "eot",
    "ttf",
    "woff",
    "woff2",
    "css"
  ],
  "AWSSigningEnabled": true,
  "AWSSigningRegionName": "${aws_region}",
  "AWSSigningKey": "${aws_key}",
  "AWSSigningSecret": "${aws_secret}",
  "EnableTLS1": true,
  "EnableTLS1_1": true,
  "VerifyOriginSSL": true,
  "EnableLogging": true,
  "EnableGeoZoneUS": false,
  "EnableGeoZoneEU": false,
  "EnableGeoZoneASIA": true,
  "EnableGeoZoneSA": false,
  "EnableGeoZoneAF": false
}
9sako69sako6

トークンの Terraform 管理

AWS Key, AWS Secret は共に cloudflare_api_token リソースを使えば作成できます。
Bunny CDN には、オリジンのバケットに対する Read Only の権限をもつトークンを作成して設定しました。

トークンの Terraform 管理についてはこちら:
https://zenn.dev/link/comments/d2781e3b171b7d

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