Terraform で Cloudflare Workers をデプロイし、tfstate を R2 に配置してみた

Terraform で Cloudflare Workers をデプロイし、tfstate を R2 に配置してみた

Cloudflare Workers を Terraform でデプロイし、tfstate を R2 に置いてみました。R2 の Access Key と Workers 用 API Token という2つの認証情報が必要だったことや、S3 互換の backend 設定まで、実装全体をご紹介します。
2026.08.23

はじめに

クラウド事業本部、あきやまです。

最近、実務で CloudFront を触る機会が増えました。同じ CDN 周辺の話として Cloudflare もよく聞くので、Workers と R2 を Terraform から使ってみることにしました。今回は、簡易なアプリのデプロイと、tfstate のリモート管理まで実施しています。

環境

項目
OS macOS
Terraform 1.14 系(required_version = ">= 1.14"
Cloudflare Provider v5.23.0
デプロイ手段 ローカルから terraform apply
state R2(S3 互換 backend)
公開 URL *.workers.dev(カスタムドメインなし)

結論

Terraform の apply で Worker をデプロイできました。workers.dev 上で HTML と JSON が返るところまで確認しています。

作成したリソースは次の 3 つです。

リソース 役割
cloudflare_worker Worker 本体。workers.dev とログ設定
cloudflare_worker_version worker.mjs のアップロード
cloudflare_workers_deployment その version にトラフィック 100%

cloudflare_workers_deployment が、アップロードしたコードを実際のトラフィックに載せる工程です。ここまで通ったので、デプロイ完了としています。

tfstate は R2 に置きました。R2 に専用の Terraform backend はありません。公式どおり backend "s3" の endpoint を R2 に向けます。鍵は R2 の Access Key ですが、Terraform が読む環境変数名は AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY です。今回初めて知ったのは、この remote backend が AWS の S3 API と互換である、という点です。

Worker 本体の操作には、それとは別に CLOUDFLARE_API_TOKEN が必要です。R2 のキーだけでは apply 時に認証ヘッダ不足で落ちます。

やってみた

Step 1: 最小の Worker を用意する

構成は次です。アプリと Terraform を分けています。

src/
├── app/worker.mjs      # Worker
└── terraform/          # IaC

Worker は HTML と JSON だけです。GET / で HTML、GET /api/hello で JSON を返します。

src/app/worker.mjs
export default {
  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/api/hello") {
      return Response.json({
        message: "Hello from Cloudflare Workers",
        path: url.pathname,
      });
    }

    return new Response(html(), {
      headers: {
        "content-type": "text/html; charset=utf-8",
      },
    });
  },
};

ストレージ(KV / D1 / R2 のアプリ利用)は入れていません。今回の目的は Terraform だけで Worker をデプロイすることです。tfstate の置き場に R2 を使っています。

Step 2: Terraform で Worker / Version / Deployment を定義する

Cloudflare Provider v5 では、スクリプトを 1 リソースで上げ切るより、Worker・Version・Deployment を分ける形が公式の IaC 例に近いです。今回もそれに合わせました。

src/terraform/main.tf
resource "cloudflare_worker" "app" {
  account_id = var.account_id
  name       = var.worker_name

  observability = {
    enabled            = true
    head_sampling_rate = 1
    logs = {
      enabled         = true
      invocation_logs = true
      persist         = true
    }
  }

  subdomain = {
    enabled          = true
    previews_enabled = true
  }
}

resource "cloudflare_worker_version" "app" {
  account_id         = var.account_id
  worker_id          = cloudflare_worker.app.id
  compatibility_date = "2026-08-23"
  main_module        = "worker.mjs"

  modules = [{
    name         = "worker.mjs"
    content_type = "application/javascript+module"
    content_file = "${path.module}/../app/worker.mjs"
  }]
}

resource "cloudflare_workers_deployment" "app" {
  account_id  = var.account_id
  script_name = cloudflare_worker.app.name
  strategy    = "percentage"

  versions = [{
    percentage = 100
    version_id = cloudflare_worker_version.app.id
  }]
}

Provider はトークンを HCL に書きません。環境変数 CLOUDFLARE_API_TOKEN を使います。

src/terraform/providers.tf
provider "cloudflare" {}

Step 3: R2 を手で作り、S3 backend にする

tfstate 用バケットは、この Terraform では作りません。先に Dashboard で R2 バケットを作ります。

手順は次です。

  1. Dashboard の R2 でバケットを作成する(例: handson-tfstate
  2. R2 API Token を発行する(Object Read and Write)
  3. Access Key ID と Secret Access Key を控える

backend は空の s3 ブロックにして、実体は backend.hcl に分けました。鍵は HCL に書かず環境変数にします。

src/terraform/backend.tf
terraform {
  backend "s3" {}
}
src/terraform/backend.hcl.example
bucket = "handson-tfstate"
key    = "cloudflare-handson/terraform.tfstate"
region = "auto"

skip_credentials_validation = true
skip_region_validation      = true
skip_requesting_account_id  = true
skip_metadata_api_check     = true
skip_s3_checksum            = true

endpoints = {
  s3 = "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com"
}

公式の Remote R2 backend と同じ考え方です。R2 は S3 API 互換なので、S3 クライアントの接続先だけ R2 に変えます。

手元では次を実行しました。

cd src/terraform
cp backend.hcl.example backend.hcl
# Account ID を埋める

export AWS_ACCESS_KEY_ID="<R2 Access Key ID>"
export AWS_SECRET_ACCESS_KEY="<R2 Secret Access Key>"

terraform init -backend-config=backend.hcl

backend.hcl は gitignore しています。

Step 4: ハマったところ

R2 のキーだけでは Worker を作れない

R2 接続後に apply すると、次で落ちました。

POST ".../accounts/.../workers/workers": 400 Bad Request
Missing X-Auth-Key, X-Auth-Email or Authorization headers

AWS_* は tfstate 用です。Cloudflare API には届きません。Worker 用に API Token(Account > Workers Scripts > Edit)を発行し、同じシェルで次を設定して解消しました。

export CLOUDFLARE_API_TOKEN="<Workers 用 API Token>"

整理すると、認証は 2 系統 です。役割が違うので、片方では足りません。

環境変数 中身 誰が読むか 用途
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY R2 の Access Key ID / Secret Access Key Terraform の backend "s3"(AWS SDK) tfstate の読み書き
CLOUDFLARE_API_TOKEN Cloudflare API Token Cloudflare Provider Worker の作成・更新

tfstate 側(R2 + S3 backend)

Cloudflare 公式の Remote R2 backend は、R2 専用 backend ではなく backend "s3" を使います。R2 が S3 API 互換 だからです。PutObject / GetObject など、S3 と同じ操作でオブジェクトを扱えます。

接続先だけ AWS ではなく R2 に向けます。endpoint は次の形です。

https://<Account ID>.r2.cloudflarestorage.com

鍵は AWS の IAM ではなく、R2 の Access Key ID / Secret Access Key です。Dashboard の R2 → Manage R2 API tokens で、Object Read & Write のトークンを発行するとこの 2 値が取れます。公式手順も同じです。

Terraform 側の読み方は S3 backend の仕様です。access_key は HCL に書くこともできますが、同じ値を環境変数 AWS_ACCESS_KEY_ID からも取ります。secret_keyAWS_SECRET_ACCESS_KEY です。S3 backend の中が AWS SDK なので、環境変数名は AWS の決まりのままです。AWS アカウントは不要です。

公式の R2 backend 例では、AWS 固有の検査を避けるため次も付けます。R2 には STS や IMDS が無いためです。

  • region = "auto"
  • skip_credentials_validation
  • skip_metadata_api_check
  • skip_region_validation
  • skip_requesting_account_id
  • skip_s3_checksum

HashiCorp は、認証情報を HCL に直書きせず環境変数にするのを推奨しています。HCL や -backend-config に書くと、.terraform 配下や plan ファイルに残るためです。今回は backend.hcl には bucket と endpoint だけ書き、鍵は export AWS_* にしました。

Worker 側(Cloudflare API)

Worker の作成は Cloudflare API です。Provider は Create API token で発行した Token を使い、環境変数は CLOUDFLARE_API_TOKEN です。公式の R2 backend ページでも、provider は $CLOUDFLARE_API_TOKEN から取る、と書いてあります。

権限は今回 Account > Workers Scripts > Edit です。R2 の Access Key にはこの権限が無いので、Worker API には Authorization ヘッダが付きません。先の 400 はそれが原因でした。

図にすると次です。

terraform apply
    ├── backend "s3"
    │     AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
    │     → https://<Account ID>.r2.cloudflarestorage.com
    │     → tfstate の読み書き
    └── provider "cloudflare"
          CLOUDFLARE_API_TOKEN
          → https://api.cloudflare.com
          → Worker / Version / Deployment

2 本とも Cloudflare の資格情報ですが、製品も API も別です。名前の AWS_ だけが、S3 互換クライアント由来です。

Step 5: apply してデプロイする

plan では 3 リソースの追加になりました。yes で apply した出力は次です。

Plan: 3 to add, 0 to change, 0 to destroy.

cloudflare_worker.app: Creation complete after 0s
cloudflare_worker_version.app: Creation complete after 1s
cloudflare_workers_deployment.app: Creation complete after 0s

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

worker_id   = "(Worker の ID)"
worker_name = "handson-hello"

cloudflare_workers_deployment まで作成できたので、Worker はデプロイ済みです。Dashboard の Workers に handson-hello が出て、https://handson-hello.<workers.dev のサブドメイン>.workers.dev で応答します。

ブラウザでは次を確認しました。

  • GET / … HTML(「Terraform でデプロイした Worker です。」)
  • GET /api/hello … JSON

apply 後、R2 バケットに cloudflare-handson/terraform.tfstate が載りました。init 直後はオブジェクトが空のことがあります。デプロイの実体が state に残るのは apply 後です。

注意点と制約

  • R2 バケットは先に手で作る。tfstate の保存先バケットを、同じ構成の初回 apply で作ることはできません。backend の初期化時点で、そのバケットがすでに存在する必要があるためです。
  • 鍵が 2 種類ある。R2 Access Key と Workers 用 API Token は別物です。片方だけでは足りません。
  • backend.hclterraform.tfvars はコミットしない。Account ID や実行時の値が入ります。
  • workers.dev は検証向き。本番なら Custom Domain や Route を別途足します。今回は入れていません。
  • R2 の Access Key をシェル履歴に残さない方がよいです。漏れたら Dashboard で失効してください。

まとめ

  • Terraform Provider v5 の Worker / Version / Deployment で、最小の Worker をデプロイできました
  • workers.dev で HTML と /api/hello の JSON が返るところまで確認しました
  • tfstate は公式どおり R2 + backend "s3" に置きました
  • AWS_* は R2 用、CLOUDFLARE_API_TOKEN は Worker 用、と役割が分かれます

「Wrangler なしで Worker を IaC してデプロイしたい」という場合の足がかりになれば幸いです。

参考

この記事をシェアする

関連記事