Cloudflare Workers Containers 上に k3s (Kubernetes) の single-node クラスタを立てて、実際に Pod をデプロイして遊べるようにする実験プロジェクト。
- クラスタ全部が 1 個の Cloudflare Container の中に閉じている(Firecracker microVM)
- ブラウザ / curl / 手元の
kubectlの 3 経路で操作できる - 公開エンドポイントは
/services/<name>/の Ingress ベース、内部管理系は/manage/配下に分離 - 認証は Cloudflare Access(Service Token)で前段ガード
- 追加の VM / K8s コントロールプレーンサービスに一切課金しない(Workers Paid のみ)
背景メモ:
- 詳しい経緯・失敗・意思決定は
PROGRESS.md - 作業する Claude Code 用の指針は
CLAUDE.md - Ingress のサンプルは
docs/examples/
デプロイ後に手に入るもの:
| URL | 用途 |
|---|---|
https://<worker>.workers.dev/health |
生存確認 |
https://<worker>.workers.dev/services/<name>/* |
クラスタに kubectl apply した Ingress ルールに沿って backend Service にルーティング |
https://<worker>.workers.dev/manage/dashboard/ |
Kubernetes Dashboard |
https://<worker>.workers.dev/manage/apiserver/* |
Kubernetes API を丸ごと reverse-proxy(kubectl で叩ける) |
https://<worker>.workers.dev/manage/kubectl?args=... |
ブラウザで直接 kubectl を実行 |
https://<worker>.workers.dev/manage/status |
k3s サマリ (pods, nodes) |
新しいサービスを公開したいとき: kubectl apply -f my-ingress.yaml(テンプレは docs/examples/)を叩くだけで /services/<name>/* から届くようになる。status.py などのアプリコードを触る必要は無い。
Cloudflare Worker ──► Container (Firecracker microVM, standard-2 = 1 vCPU / 6 GiB / 12 GB)
│
├─ Python status.py (PID 1, port 8080)
│ ├─ /health, /manage/*, /services/*
│ ├─ /manage/apiserver → k3s apiserver reverse-proxy (HTTP + WebSocket)
│ ├─ /manage/dashboard → Kubernetes Dashboard reverse-proxy
│ └─ /services/* → Traefik ClusterIP:80 (verbatim)
│
├─ svcproxy (Go)
│ Services + EndpointSlices を polling → 各 ClusterIP を lo に追加
│ → TCP splice で pod endpoint に転送 (kube-proxy の代替)
│
└─ k3s server (all-in-one)
├─ control plane: kube-apiserver + controller-manager + scheduler + SQLite
├─ node: kubelet + containerd
└─ pods (containerd 上の nested container):
├─ coredns
├─ metrics-server
├─ local-path-provisioner
├─ kubernetes-dashboard (skip-login)
├─ traefik (Ingress Controller)
└─ ... (kubectl apply したもの)
kube-proxy が 入っていないのがポイント。CF Container の kernel は netfilter モジュールが軒並み built-in に無く、内蔵の kube-proxy がどのモードでも動かない。かわりに container/svcproxy/ の Go 実装が Services を watch して userspace で TCP をリレーしている。
- Cloudflare Workers Paid プラン(Containers が Paid 限定機能)
- ドキュメント: https://developers.cloudflare.com/containers/pricing/
- 24/7 稼働 & standard-2 で 月 $50 前後(詳細は下の「料金の目安」)
- Node.js 22 以上、npm、Docker(
wrangler deployがローカルビルドで使う) - macOS or Linux
- (任意)Cloudflare Access — 誰にも見られたくない場合
# 1. clone してあなたの手元へ
git clone <this-repo> my-k3s
cd my-k3s
# 2. 依存インストール
npm install
# 3. Cloudflare にログイン
npx wrangler login
# 4. wrangler.jsonc の name を変える (worker のサブドメイン名になる)
# "name": "my-k3s" などに書き換え
# 5. deploy (初回は Docker ビルドで数分)
npx wrangler deploy
# 6. 動作確認 (deploy 完了時に表示された URL に対して)
curl https://my-k3s.<your-subdomain>.workers.dev/health
# {"ok":true}初回は k3s + system pod の起動で 1-2 分かかる。/manage/status を叩けば apiserver / pod の状況が見える。
生の URL を晒すと /manage/kubectl?args=... から誰でもクラスタを触れてしまう。CF Access で Service Token を要求する構成に:
- CF ダッシュボード → Zero Trust → Access → Applications で Self-hosted Application を追加
- Application domain:
my-k3s.<your-subdomain>.workers.dev - Policy:
Service Authタイプで一つ作り、下で発行する Service Token に紐付ける
- Application domain:
- Zero Trust → Access → Service Auth → Service Tokens で Create Service Token
- 発行された
Client IDとClient Secretをメモ
- 発行された
.env.exampleを.envにコピーして値を書き込む(またはmise.toml/ シェル profile へ):CF_ACCESS_CLIENT_ID=... CF_ACCESS_CLIENT_SECRET=...- 以後、curl は
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"を毎回付ける。
/manage/dashboard/→ Kubernetes Dashboard(skip-login で入れる)/manage/status→ pod / node 一覧の JSON/services/<name>/→ 自分でデプロイしたアプリ
curl -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
'https://my-k3s.<subdomain>.workers.dev/manage/kubectl?args=get+pods+-A'bin/access-proxy.py を立ち上げて、そこに向けて kubectl を叩く。
# 環境変数を設定 (.env をシェルで export するか、mise 経由で)
export K3S_CF_UPSTREAM=my-k3s.<subdomain>.workers.dev
export CF_ACCESS_CLIENT_ID=... # CF Access を使う場合だけ
export CF_ACCESS_CLIENT_SECRET=...
# 別ターミナルで proxy を起動
python3 bin/access-proxy.py
# → https://127.0.0.1:8087 で listen
# トークンを発行 (cluster-admin。8h 有効)
TOKEN=$(curl -s -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
"https://$K3S_CF_UPSTREAM/manage/kubectl?args=create+token+kubectl-user+-n+kube-system+--duration=8h" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["stdout"].strip())')
# kubectl から叩く
kubectl --server=https://127.0.0.1:8087/manage/apiserver \
--token="$TOKEN" \
--insecure-skip-tls-verify=true \
get nodeskubectl exec / logs -f / port-forward などの WebSocket 系操作も通る。
# 例: nginx
kubectl create deployment nginx --image=nginx --port=80
kubectl expose deployment nginx --port=80
# public に出す (StripPrefix Middleware + Ingress)
kubectl apply -f docs/examples/ingress-nginx.yaml
# ブラウザで https://my-k3s.<subdomain>.workers.dev/services/nginx/docs/examples/README.md に汎用テンプレあり。
Workers Paid ベース料金 $5/月 に加えて、Container の稼働時間分:
| 構成 | 月額目安(Workers Paid 込み) |
|---|---|
standard-2 24/7 (現在のデフォルト、onActivityExpired no-op で常時起動) |
≈ $50 |
standard-2 1 日 2 時間だけ (onActivityExpired オーバーライドを外して sleep 復活) |
≈ $10 |
standard-1 24/7 (4 GiB 版) |
≈ $38 |
basic 24/7 (1 GiB。dashboard/voicevox は無理) |
≈ $12 |
Container 停止中は課金されない。「試すだけ」なら sleep 復活 (src/index.ts の onActivityExpired オーバーライドを削除) がお得。
詳しくは https://developers.cloudflare.com/containers/pricing/。
課金を止めたいときは Cloudflare のダッシュボードから:
- Workers & Pages → 該当 Worker を Delete
- Zero Trust → Access → Applications / Service Tokens を掃除
または CLI で:
npx wrangler deletewrangler deployがNo changes to be made: image は変わってないという表示だが、container/manifests/*.yamlを編集した時は build cache に引っかかることがある。container/DockerfileのRUN echo "cache-buster: ..."の値を bump して再 deploy。/manage/*が 302 で*.cloudflareaccess.comに飛ぶ: CF Access ヘッダを付け忘れている。.envを export しているか確認。kubectlが401:create tokenして発行した SA トークンは 1〜8h で失効する。再発行する。- Pod が CrashLoopBackOff: 大概はメモリ不足。
wrangler.jsoncのinstance_typeをstandard-3(8 GiB)やstandard-4(12 GiB)に上げる。 /services/<name>/が 404: Ingress ルールが無い、または backend service 名が間違っている。kubectl get ingress -Aとkubectl get svc -Aを突き合わせる。
MIT。詳細は LICENSE。