1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudflareのContainersで遊んでみる

1
Last updated at Posted at 2025-12-21

はじめに

2025年にpublic betaとしてリリースされたContainers。

概要としてはCloudflareのグローバルネットワーク上でDockerコンテナをネイティブに実行するためのプラットフォームという理解でしたが、まだ触ったことがなかったのでこの機会にContainersを使ったアプリケーションをデプロイしてみたいと思います。

何を作るか

とはいうものの、何を作ろう・・・おそらくCloudflare Containersが本領を発揮するのは、「重めの処理」+「リアルタイム性」のあるユースケースだろう、、、どうせなら"エッジで動いている感"が分かるアプリがいい、、、ということでウェブカメラの映像をリアルタイムで加工するアプリにいきつきました。

色々リサーチをしてみたところ、OpenCVという「コンピュータビジョン(画像・映像の解析)」のためのオープンソースライブラリを見つけました。C++ネイティブ依存ライブラリのようで、Workers単体で動かすのは無理。さらにPythonからOpenCVの機能をサクッと呼び出せるということで、実験にはちょうど良さそうです。

アプリのアーキテクチャ概要

Cloudflareの Workers と Containers を組み合わせて、「Webカメラ映像をリアルタイムで加工 → ブラウザへ返す」というのを目標にします。実はいくつか試作を作ったのですが、レイテンシーが高くなってしまい人に見せられるレベルのアプリケーションにするのに少し苦労しました。ChatGPTとバイブコーディングしながら構成をいじっていったので、元々の構成と比較的うまくいった構成二つ記載しておきます。

元々の構成要素

  • Workers: Webフロント配信、ルーティング、WebSocket中継
  • Containers (Python + FastAPI): 重い処理(OpenCV)をコンテナ内で実行
  • OpenCV: 映像の加工(エッジ検出、グレースケール、アニメーション化等)
  • WebSocket: ブラウザ ⇄ Containers(映像フレーム)のリアルタイム送受信

これが大まかな方向性ですがレイテンシー回避のために、以下のように構成を肉付け。

バージョンアップした構成要素

Cloudflare Workers

  • Webフロント(HTML / JS)の配信
  • /api/* ルーティング制御
  • WebSocket UpgradeをContainersに透過的に転送
  • PoP(colo)やRay IDの取得

Cloudflare Containers(Python + FastAPI)

  • WebSocketサーバとして常駐
  • OpenCVを用いたリアルタイム画像処理
  • 状態(選択中のフィルター)をセッション内で保持

OpenCV

  • エッジ検出、グレースケール、軽量cartoonフィルターなど
  • C++実装のライブラリをPython経由で利用

WebSocket

  • ブラウザ ⇄ Containers 間の双方向通信
  • 制御メッセージ(JSON)と映像フレーム(binary)を混在
┌──────────────────────────┐
│      Browser             │
│    (Web Camera)          │
└───────────┬──────────────┘
            │ WebSocket (binary / JSON)
            ▼
┌──────────────────────────┐
│ Cloudflare Workers (Edge)│
│--------------------------│
│ - HTML/JS delivery       │
│ - /api/* routing         │
│ - WebSocket upgrade      │
│ - PoP / Ray ID info      │
└───────────┬──────────────┘
            │ WebSocket (passthrough)
            ▼
┌──────────────────────────┐
│ Cloudflare Containers    │
│   (Python + FastAPI)     │
│--------------------------│
│ - WebSocket server       │
│ - Session state          │
│ - OpenCV processing      │
└───────────┬──────────────┘
            │
            ▼
┌──────────────────────────┐
│ OpenCV (C++ backend)     │
│--------------------------│
│ - Edge/Gray/Cartoon      │
│ - Fast image processing  │
└──────────────────────────┘

データの流れ

1. ブラウザ

  • Webカメラ映像を <canvas> に描画
  • JPEGに変換し、Blob → ArrayBuffer としてバイナリ送信
  • フィルター変更時はJSONメッセージ({ type: "mode" })を送信

2. WebSocket(/api/ws)

  • ブラウザは /api/ws に接続
  • WorkersはWebSocket UpgradeをそのままContainersに転送
  • Workers自身は映像データを処理しない

3. Containers(/ws)

  • FastAPI WebSocketが接続を受け付け
  • binaryフレームをOpenCVで加工
  • 加工後フレームをJPEG binaryとして即時返却
  • ping / pongによりRTT(往復遅延)を計測

4. ブラウザ

  • 受信したJPEG binaryを <img> に反映
  • FPS / RTT / PoP 情報をUIに表示
  • Edge上で処理されていることを視覚的に確認可能

実際にコードに落とし込んでみる

1. Cloudflare Containers 側(Python + FastAPI + OpenCV)

まずは映像処理の中心となるコンテナアプリから。Cloudflare Containers 上では、FastAPI を WebSocket サーバとして常駐させ、OpenCV を用いたリアルタイム画像処理を行います。dataURL(base64)ではなく、JPEG のバイナリ送信を採用して大幅にレイテンシが改善しました。

コンテナ内 WebSocket 実装(抜粋)

app.py
# app.py(Containers 内)
import json
import cv2
import numpy as np
from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()

    mode = "edges"  # 現在選択中のフィルター

    try:
        while True:
            msg = await websocket.receive()

            # ---- 制御メッセージ(JSON)----
            if msg.get("text") is not None:
                payload = json.loads(msg["text"])
                if payload.get("type") == "mode":
                    mode = payload.get("mode", mode)
                elif payload.get("type") == "ping":
                    # RTT 計測用
                    await websocket.send_text(
                        json.dumps({
                            "type": "pong",
                            "ts": payload.get("ts")
                        })
                    )
                continue

            # ---- 映像フレーム(binary)----
            if msg.get("bytes") is None:
                continue

            img_array = np.frombuffer(msg["bytes"], np.uint8)
            frame = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
            if frame is None:
                continue

            processed = apply_filter(frame, mode)

            _, encoded = cv2.imencode(".jpg", processed, [int(cv2.IMWRITE_JPEG_QUALITY), 60])
            await websocket.send_bytes(encoded.tobytes())

    except WebSocketDisconnect:
        pass

2. Dockerfile(Cloudflare Containers 用イメージ)

次に、Containers 上で FastAPI + OpenCV アプリを実行するためのDockerfile です。Cloudflare Containers では、コンテナ内アプリが 0.0.0.0:8080 で待ち受けていないと起動確認に失敗するようでした。
また、OpenCV はネイティブ依存を持つため、最低限の OS ライブラリを明示的にインストールするのが安全のようです。

Dockerfile
# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# OpenCV (headless) が必要とする最小限の依存関係
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgl1 \
    libglib2.0-0 \
 && rm -rf /var/lib/apt/lists/*

# Python 依存関係
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリ本体
COPY app.py .

# Cloudflare Containers はこのポートを監視する
EXPOSE 8080

# FastAPI を 0.0.0.0:8080 で起動(必須)
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]

requirements.txt(例)

txt
fastapi==0.115.6
uvicorn[standard]==0.32.1
numpy==2.1.3
opencv-python-headless==4.10.0.84

3. Workers 側(WebSocket ルーティング & Containers 連携)

Cloudflare Workers は、このアプリケーションにおいてフロント配信と Edge ルーティング制御を担当します。Workers 自身は映像データを処理せず、WebSocket Upgrade を そのまま Containers に転送する役割になっています。

src/index.ts
// src/index.ts(Workers)
import { Container } from "@cloudflare/containers";

export interface Env {
  VIDEO_CONTAINER: DurableObjectNamespace;
  ASSETS: Fetcher;
}

// Containers を Durable Object として定義
export class VideoContainer extends Container<Env> {
  defaultPort = 8080;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url);

    // ---- Edge 情報取得 ----
    if (url.pathname === "/api/edge-info") {
      return new Response(
        JSON.stringify({
          colo: req.cf?.colo ?? "unknown",
          ray: req.headers.get("cf-ray") ?? "unknown",
        }),
        { headers: { "content-type": "application/json" } }
      );
    }

    // ---- Containers warm-up ----
    if (url.pathname === "/api/health") {
      const instance = env.VIDEO_CONTAINER.getByName("video");
      return instance.fetch("http://container/health");
    }

    // ---- WebSocket Upgrade ----
    if (url.pathname.startsWith("/api/ws")) {
      const instance = env.VIDEO_CONTAINER.getByName("video");

      // /api/ws → /ws にパスを書き換え
      const innerUrl = new URL(req.url);
      innerUrl.pathname = "/ws";

      // Upgrade ヘッダを保持して転送
      const containerReq = new Request(innerUrl.toString(), {
        method: req.method,
        headers: req.headers,
        body: req.body,
      });

      return instance.fetch(containerReq);
    }

    // ---- UI 配信 ----
    return env.ASSETS.fetch(req);
  },
};

4. フロントエンド(Webカメラ → WebSocket → 映像表示)

最後に、ブラウザ側の実装です。ブラウザでは Web カメラ映像を canvas に描画し、JPEG に変換した バイナリデータを WebSocket 経由で送信します。フィルター切り替えや RTT 計測などの制御メッセージはJSON として別途送信します。

public/script.js
<script>
const video = document.getElementById("video");
const processedImg = document.getElementById("processed");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const filterSelect = document.getElementById("filter");

let ws = null;
let sendInterval = null;

// WebSocket 初期化
function initWebSocket() {
  const protocol = location.protocol === "https:" ? "wss" : "ws";
  ws = new WebSocket(`${protocol}://${location.host}/api/ws`);
  ws.binaryType = "arraybuffer";

  ws.onopen = () => {
    console.log("WebSocket connected");
    sendMode();
    startSendingFrames();
  };

  ws.onmessage = (event) => {
    // 映像フレーム(JPEG binary)
    const blob = new Blob([event.data], { type: "image/jpeg" });
    processedImg.src = URL.createObjectURL(blob);
  };

  ws.onclose = () => {
    console.log("WebSocket disconnected, retrying...");
    stopSendingFrames();
    setTimeout(initWebSocket, 3000);
  };
}

// 映像フレーム送信(binary)
function startSendingFrames() {
  if (sendInterval) return;

  sendInterval = setInterval(() => {
    if (!ws || ws.readyState !== WebSocket.OPEN) return;

    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

    canvas.toBlob(
      (blob) => {
        if (!blob) return;
        blob.arrayBuffer().then((buffer) => {
          ws.send(buffer);
        });
      },
      "image/jpeg",
      0.5
    );
  }, 150);
}

function stopSendingFrames() {
  if (sendInterval) {
    clearInterval(sendInterval);
    sendInterval = null;
  }
}

// フィルター変更(JSON 制御メッセージ)
function sendMode() {
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
  ws.send(JSON.stringify({
    type: "mode",
    mode: filterSelect.value
  }));
}

filterSelect.addEventListener("change", sendMode);
</script>

Cloudflare にデプロイしてみる

Cloudflare Containers を使う場合、ローカル環境に Docker のセットアップが実質必須になります。デプロイ時に Cloudflare が Dockerfile を元にコンテナイメージをビルドするため、Workers のように「単体ファイルだけ」で完結しません。

パソコンにDockerが入っていなければここからインストール。

Workersのローカル開発環境の準備も完了済みである前提です。

また、専用の SDK 追加も要ります。

npm install @cloudflare/containers

実装手順は公式ドキュメントが参考になります:

wrangler.jsonc の設定を確認

wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "realtime-video",
  "main": "src/index.ts",
  "compatibility_date": "2025-12-17",

  "assets": {
    "directory": "./public",
    "binding": "ASSETS"
  },

  "containers": [
    {
      "class_name": "VideoContainer",
      "image": "./video-demo/Dockerfile",
      "max_instances": 5
    }
  ],

  "durable_objects": {
    "bindings": [
      {
        "name": "VIDEO_CONTAINER",
        "class_name": "VideoContainer"
      }
    ]
  },

  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["VideoContainer"]
    }
  ]
}

containers.image のパスが間違っていると、Docker build が失敗します

デプロイ実行

もろもろ準備ができたら、Deployしてみます!

npx wrangler deploy

スクリーンショット 2025-12-20 15.45.14.png

ちなみにCloudflare Containers を初めてデプロイした際、思ったより時間がかかるなと感じました。これはドキュメントに説明がありました。

When you run wrangler deploy, the following things happen:

  • Wrangler builds your container image using Docker.
  • Wrangler pushes your image to a Container Image Registry that is automatically integrated with your Cloudflare account.
  • Wrangler deploys your Worker, and configures Cloudflare's network to be ready to spawn instances of your container

The build and push usually take the longest on the first deploy. Subsequent deploys are faster, because they reuse cached image layers.

諸々の処理は一度行われるとキャッシュされるので、2回目以降のデプロイは早くなるようです。

デプロイ完了! :airplane:

スクリーンショット 2025-12-20 21.11.23.png

デモはしばらくこちらで公開しておきます。
https://real-timevideo.soonchang.me/

ソースコードはGithubで。
https://github.com/soonbig/cloudflare-containers-realtime-video

管理画面で何が見えるか

① メトリクス画面(リソース使用状況)

スクリーンショット 2025-12-20 21.21.37.png

ここで分かること

メモリ使用量

  • 今回のOpenCV + FastAPI構成でも、P50 約100MiB前後
  • 256MiBの制限内で十分動作していることが分かる

CPU使用率

  • フレーム送信時のみスパイク
  • 常時高負荷ではなく「イベントドリブン」な挙動

ネットワーク帯域

  • WebSocketでJPEGフレームを送っているため、ここが最も増える

② 設定画面(リソース仕様・イメージ情報)

スクリーンショット 2025-12-20 21.22.58.png

この画面で確認できること

利用中のコンテナイメージ

  • registry.cloudflare.com/...
  • wrangler deploy 時に自動ビルドされたもの

割り当てリソース

  • vCPU:1/16
  • メモリ:256MiB
  • ディスク:2000MiB

ちなみに、メモリ256MiBは一番安いインスタンスです。
スクリーンショット 2025-12-21 11.08.45.png

作成日時 / 更新日時

③ Logs 画面

スクリーンショット 2025-12-20 21.22.38.png

④ インスタンス画面

スクリーンショット 2025-12-20 21.21.53.png

この画面で確認できること

  • 起動中のインスタンス一覧
  • インスタンス名(video, warmup)
  • 起動ロケーション
    • Delhi, India
    • Houston, United States
  • 同時起動数(例:2 / 5)

なぜ起動ロケーションが日本ではないのかと疑問に思いますが、ドキュメントにはこう記載されていました。

When a Container instance is requested with this.ctx.container.start, the nearest free container instance will be selected from the pre-initialized locations. This will likely be in the same region as the external request, but may not be. Once the container instance is running, any future requests will be routed to the initial location.

Cloudflare Containersの起動ロケーションは、ユーザーの物理的な場所ではなく、Cloudflareが事前に準備した「空いているコンテナインスタンス」から最も近いものが選択されるようです。

まとめ

Cloudflare Containersを実際に触ってみて、管理画面を見ながら強く感じたのは、Containersは動いているアプリケーションであるという点です。リソース使用量はWorkers単体利用だとそれほど意識しなかったり、「コールドスタート」の概念が出てくるのも意外でした。OSを気にすることもWorkers単体だとあまりありません。

Cloudflareのドキュメントでも触れられていますが、Containersはリクエストに応じて起動・停止される実行モデルなので、コールドスタートが発生するようです。

以上、Containersで遊んでみた記録でした。Containersの利用はWorkers Paid Planが必須ですので、皆様もクレジットカードご登録の上遊んでみてください!参考までに価格情報はこちら:https://developers.cloudflare.com/containers/pricing/

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?