コンテンツにスキップ

基盤

Overview

基盤は、Cloudflare の zone、Workers の custom domain、Terraform の state を置く R2 のバケットを、Terraform のコードで持つ。
Worker script と静的アセットの deploy は Wrangler が持ち、この文書の対象に入らない。
全体の構成は Design Doc にある。
ADR-0005: Workers と Terraform と Wrangler の分担

Scope

持つものは次のとおり。

  • infra/bootstrap/ と infra/cloudflare/ の分け方
  • R2 の state backend の設定
  • deploy と apply の流れ
  • ドキュメントサイトの Access を Terraform で持たないことと、custom domain を作る順序

持たないものは次のとおり。

Design

deploy と apply の流れ

次の図は、Worker の deploy の流れと、運用者が手で起動する apply の流れが、別の credential で分かれていることを示す。

flowchart TD
    dmanual["運用者が develop から手動で起動"] --> ci["deploy workflow"]
    ci -->|"deploy 用の token"| wd["wrangler deploy"]
    wd --> worker["Worker script + assets"]
    check["develop の check workflow の成功"] --> cci["deploy-catalog workflow"]
    cci -->|"deploy 用の token"| cwd["wrangler deploy(packages/ui)"]
    cwd --> catalog["カタログの Worker + assets"]
    check --> pci["deploy-playground workflow"]
    pci -->|"deploy 用の token"| pwd["wrangler deploy(apps/playground)"]
    pwd --> playground["プレイグラウンドの Worker + assets"]
    check --> vci["deploy-preview workflow"]
    vci -->|"deploy 用の token"| vwd["wrangler deploy --env preview(apps/web)"]
    vwd --> preview["プレビューの Worker + assets"]
    check --> dci["deploy-docs workflow"]
    dci -->|"deploy 用の token"| dwd["wrangler deploy(apps/docs-site)"]
    dwd --> docs["ドキュメントサイトの Worker + assets"]
    manual["運用者が手動で起動(mode=plan、次に mode=apply)"] --> env["environment protection rules"]
    env -->|"apply 用の token"| tf["terraform plan / apply"]
    tf --> base["Zone / custom domain"]
    ci -. "触らない" .-x base
    cci -. "触らない" .-x base
    pci -. "触らない" .-x base
    vci -. "触らない" .-x base
    dci -. "触らない" .-x base

Worker のコードの変更は、通常の pull request に含まれる。
deploy と apply を起動する条件と、apply を plan と apply の 2 回に分けて起動する手順は context/operations.md にある。
deploy の workflow は、apply の workflow と違い、environment の承認を挟まない。
変更を止める手段は、pull request のレビューと自動のテストに頼る。
develop には branch protection を掛けていないので、pull request を通すのは慣行であり、仕組みでは強制しない。
deploy 用の token は Workers Scripts の権限を持つので、custom domain の apex のレコードにも届きうる。deploy の workflow がそれを触らないのは、wrangler deploy の振る舞いによる。
どの Worker もハンドラを持たないので、誤った変更が及ぶ範囲は静的アセットの誤った配信に限られる。

Wrangler の設定ファイルは wrangler.jsonc にする。
Cloudflare は新しいプロジェクトに JSONC を勧めており、一部の新しい機能は JSON の形式でしか使えない。

bootstrap と本体

ディレクトリ 対象 state の置き場 実行する人
infra/bootstrap/ state 用の R2 のバケット 手元(commit しない) 運用者が手元で 1 回
infra/cloudflare/ zone、Workers の custom domain R2(infra/bootstrap/ が作る) apply の workflow か手元

infra/bootstrap/ を CI から apply しない。state を手元に置く前提が成り立たなくなる。
CI は infra:check で、fmt の確認、init(backend なし)、validate だけを実行する。

zone は Terraform で作らない。
Cloudflare Registrar がドメインを取得したときに作った zone を、zone.tf の import ブロックが、state にまだないときに取り込む。
cloudflare_zone には prevent_destroy を付ける。

cloudflare_workers_custom_domain が、サイトの apex、カタログの design、プレイグラウンドの play、ドキュメントサイトの docs のレコードを管理するので、DNS のレコードは書かない。

ドキュメントサイトの Access

docs.<zone> の Access のアプリケーションとポリシーは、Terraform で持たない。
運営者がダッシュボードで設定する。
ADR-0032: アクセスの認証

  • Terraform は、custom domain の docs だけを持つ。Terraform では、Access と custom domain を作る順序を制御しない。
  • custom domain は、Access より先に作ってよい。Access を掛けるまでは、ドキュメントサイトを誰でも開ける。手順は apps/docs-site/README.md にある。
  • Access は docs.<zone> のホスト名にだけ掛かる。Worker の workers_dev と preview_urls を false にして、ほかの URL を閉じる。

state backend

R2 は S3 と互換だが、AWS ではない。
s3 backend には STS、IAM、metadata API を前提にする箇所があるので、その確認を切る。

backend "s3" {
  key    = "cloudflare/main/terraform.tfstate"
  region = "auto"

  skip_credentials_validation = true # STS がない
  skip_region_validation      = true # auto は AWS のリージョンの名前ではない
  skip_requesting_account_id  = true # IAM、STS、metadata API がない
  skip_s3_checksum            = true # R2 はチェックサムへの対応が限られる
  use_path_style              = true
  use_lockfile                = true # state のロックを S3 の側のオブジェクトで取る
}

bucket の名前と endpoint は account ID を含むので、commit しない。
bucket の名前は -backend-config で、endpoint は AWS_ENDPOINT_URL_S3 で渡す。

state の key に scope を含める(cloudflare/main/terraform.tfstate)。
後から兄弟の state を足すときに、既存の state の名前を変えずに済む。

バケットのバージョニングを設定しない理由と、state を失ったときの戻し方は context/operations.md にある。

Interface

置くファイル

  • infra/bootstrap/provider.tf、r2_state_bucket.tf、vars.tf: state 用のバケット
  • infra/cloudflare/provider.tf: provider と state backend
  • infra/cloudflare/zone.tf: zone の取り込み
  • infra/cloudflare/workers_domain.tf: Workers の custom domain。サイトの apex(site)、カタログの design.<zone>(catalog)、プレイグラウンドの play.<zone>(playground)、ドキュメントサイトの docs.<zone>(docs)
  • infra/cloudflare/vars.tf: 変数。Worker の名前の worker_name、catalog_worker_name、playground_worker_name、docs_worker_name を持つ。ほかに account_id、zone_id、zone_name を持つ。
  • apps/web/wrangler.jsonc: サイトの Worker の設定。env.preview に、プレビューの Worker(fukuemon-dev-preview)の設定を置く。プレビューの Worker は custom domain を持たず、workers.dev の URL で公開する
  • packages/ui/wrangler.jsonc: カタログの Worker の設定。workers_dev と preview_urls を切り、design.<zone> だけで公開する
  • apps/playground/wrangler.jsonc: プレイグラウンドの Worker の設定。workers_dev と preview_urls を切り、play.<zone> だけで公開する
  • apps/docs-site/wrangler.jsonc: ドキュメントサイトの Worker の設定。workers_dev と preview_urls を切り、docs.<zone> だけで公開する

変数の値は、手元では各ディレクトリの terraform.tfvars に置き、commit しない。
CI では、apply の workflow が、TF_VAR_account_id、TF_VAR_zone_id、TF_VAR_zone_name を渡す。
値は secret と variable から取る。
worker_name、catalog_worker_name、playground_worker_name、docs_worker_name と、bootstrap の location はデフォルト値を持つ。

実行するコマンドとチェック

  • pnpm run infra:init、infra:plan、infra:apply: infra/cloudflare/ を対象にする
  • pnpm run infra:bootstrap:init、infra:bootstrap:apply: infra/bootstrap/ を対象にする
  • pnpm run infra:check: terraform fmt -check と terraform validate を、backend を無効にして実行する。credential は要らない
  • pnpm run check:worker-name: 4 つの Worker の名前が、wrangler.jsonc と vars.tf で一致するかを確認する。
    • apps/web/wrangler.jsonc の name と worker_name のデフォルト値を比べる
    • packages/ui/wrangler.jsonc の name と catalog_worker_name のデフォルト値を比べる
    • apps/playground/wrangler.jsonc の name と playground_worker_name のデフォルト値を比べる
    • apps/docs-site/wrangler.jsonc の name と docs_worker_name のデフォルト値を比べる
    • プレビューの Worker は custom domain を持たず、Terraform の変数に名前が無いので、比べない
  • pnpm run check:deploy: サイト、プレビュー、カタログ、プレイグラウンド、ドキュメントサイトの wrangler deploy --dry-run を実行する。プレビューは、サイトの build の出力に noindex を足した複製を読む。カタログは pnpm run build:storybook の出力を、プレイグラウンドは apps/playground の build の出力を、ドキュメントサイトは apps/docs-site の build の出力を読む
  • pnpm run check:closed-urls: apps/docs-site/scripts/check-closed-urls.sh を実行する。ドキュメントサイトの Worker の workers_dev と preview_urls が false であることと、_headers のすべてのパスに noindex があることを確認する。wrangler.jsonc のほかに wrangler の設定ファイル(wrangler.json、.wrangler/deploy/config.json)が無いことと、route と routes が無いことも確認する。package.json の deploy が、引数の無い wrangler deploy であることと、predeploy と postdeploy が無いことも確認する。custom domain は Terraform で作る(ADR-0005)。deploy の workflow は apply を通らないので、Access の外の URL を開く変更を、この確認で止める。deploy-docs workflow も、配信の前にこの確認を実行する
  • check の workflow(.github/workflows/check.yml)が、infra:check と、上の 3 つを含む pnpm run check を実行する
  • workflow は次の 6 つ
    • apply: .github/workflows/apply.yml
    • deploy: .github/workflows/deploy.yml
    • deploy-catalog: .github/workflows/deploy-catalog.yml
    • deploy-playground: .github/workflows/deploy-playground.yml
    • deploy-preview: .github/workflows/deploy-preview.yml
    • deploy-docs: .github/workflows/deploy-docs.yml

Verification

  • cloudflare_workers_custom_domain が、wrangler deploy を済ませた Worker へ結び付くことを、初回の apply で確認した。https://fukuemon.dev が Worker を配信した。
  • skip_s3_checksum を含む上の backend の設定で、R2 への state の読み書きと use_lockfile のロックが通ることを、同じ apply で確認した。
  • 初回の plan は、cloudflare_zone.site について、account.id を sensitive として扱う差分だけを出した。apply した後に plan を実行すると、差分が残らなかった。
  • context/operations.md の token の権限の表のとおりで、初回の deploy と初回の apply が通った。