[CloudFront + EC2 + YOLOv8] WebSocket でリアルタイム物体セグメンテーションする Web アプリを AWS で構築してみました

[CloudFront + EC2 + YOLOv8] WebSocket でリアルタイム物体セグメンテーションする Web アプリを AWS で構築してみました

YOLOv8を使ったリアルタイム物体セグメンテーションのWebアプリをAWS上に構築してみました。ブラウザのカメラ映像やMP4動画をリアルタイムで処理し、推論結果をブロードキャスト配信する仕組みをご紹介します。
2026.07.05

1 はじめに

製造ビジネステクノロジー部の平内(SIN)です。

YOLOv8 を使ったリアルタイム物体セグメンテーションの Web アプリを AWS 上に構築してみました。

映像を送信する方法は、以下の3種類です。

  • ブラウザのカメラ映像(リアルタイム)
  • MP4動画の再生映像(リアルタイム)
  • MP4動画ファイルの送信(バッチ処理)

サーバー側では、受け取った映像をYOLOv8 セグメンテーションモデルで処理し、結果をそのまま配信しています。

最初の動作している様子をご確認ください。Webカメラの映像が、殆どリアルタイムで配信されていることを確認できると思います。

https://www.youtube.com/watch?v=kBBQsDmQhgU

使用したモデルは、以前に作成した、「きのこの山」と「たけのこの里」でファインチューニングしたものです。

https://dev.classmethod.jp/articles/yolov8-instance-segmentation/

本記事のサンプルコードは GitHub で公開しています。

GitHub: aws-yolov8-realtime-segmentation

2 システム構成

構成です。

フロントエンドは React で作成し、CloudFront + S3(OAC)で配信しています。Cognito 認証には AWS Amplify ライブラリ(@aws-amplify/ui-react)を使用しています。ブラウザからの映像は WebSocket 経由で EC2 に送信し、YOLOv8 推論後の結果フレームをブロードキャストで返す構成です。

EC2で動作するアプリは、Docker イメージとして作成され、起動時にECRからダウンロードして使用されています。

001

スタック 主なリソース
配信 CloudFront S3(React Frontend)+ EC2(WebSocket/API)
認証 Cognito User Pool streamers グループ / viewers グループ
推論 EC2 + Docker YOLOv8-seg、FastAPI、WebSocket サーバー
バッチ処理 S3 + SQS MP4 アップロード → キュー → バッチ推論

3 構築手順

(1) CDK によるインフラ構築

最初にCDKでインフラを構築します。

git clone https://github.com/furuya02/aws-yolov8-realtime-segmentation.git
cd aws-yolov8-realtime-segmentation/cdk
pnpm install
pnpm cdk bootstrap
pnpm cdk deploy

002

cdk deploy が完了したら、出力された CloudFrontUrl / EcrRepoUri / UserPoolId などを控えます。この時点では ECR にイメージが存在しないため、コンテナは起動しません。

(2) Docker イメージのビルドと ECR への push

Mac 上で docker buildx を使って linux/amd64 向けにビルドし、ECR に push したあと、EC2 を自動再起動します。EC2 が起動すると systemd が ECR から pull してコンテナを起動します(初回のDocker buildは、時間がかかります)。

bash scripts/ecr_build_push.sh

003

(3) フロントエンドのデプロイ

React アプリをビルドして S3 にアップロードし、CloudFront のキャッシュを無効化します。

bash scripts/deploy_frontend.sh

004

(4) Cognito ユーザーの作成

映像を送れる streamer と視聴のみの viewer を Cognito のグループで分けています。

POOL_ID=<UserPoolId>
aws cognito-idp admin-create-user \
  --user-pool-id $POOL_ID \
  --username streamer@example.com \
  --user-attributes Name=email,Value=streamer@example.com Name=email_verified,Value=true \
  --message-action SUPPRESS
aws cognito-idp admin-set-user-password \
  --user-pool-id $POOL_ID --username streamer@example.com \
  --password "YourPassword1!" --permanent
aws cognito-idp admin-add-user-to-group \
  --user-pool-id $POOL_ID --username streamer@example.com \
  --group-name streamers

005

006

(5) GPU / CPU 切り替え

このシステムは、サーバー側で使用するインスタンスを切り替えることができます。

bash scripts/switch_to_gpu.sh   # g5.2xlarge に変更する
bash scripts/switch_to_cpu.sh   # t3.large に戻す

EC2 が起動するたびに systemd が GPU の有無を自動検出してコンテナの起動モードを切り替えます。手動操作は不要です。

(6) 後片付け

cd cdk
pnpm cdk destroy 

4 CDK 実装のポイント

GitHub: cdk/lib/stack.ts

(1) ECR pre-push パターン

Docker イメージは、予めビルドされたものをECRに配置し、EC2 起動時に docker pull する仕組みです。

007

ecr_build_push.shは、Mac上で、Docker イメージをbuild・pushし、EC2を再起動します。ECRのURLとインスタンスIDは、CloudFormationから自動的に取得して動作します。

GitHub: scripts/ecr_build_push.sh

# scripts/ecr_build_push.sh(要点)
docker buildx build \
  --platform linux/amd64 \
  -f docker/Dockerfile \
  -t "${ECR_REPO_URI}:latest" \
  --push \
  .

docker_start_auto.shは、EC2起動時に、systemdから起動され、ECR をpullしてコンテナを起動します。(初回は ECR が空なのでコンテナが起動しません。)

GitHub: scripts/docker_start_auto.sh

# scripts/docker_start_auto.sh(要点)
if ! docker pull "${ECR_REPO_URI}:latest" 2>&1; then
  echo "ECRにイメージなし → 起動スキップ"
  exit 0
fi

(2) CloudFront の構成

1 つの CloudFront ディストリビューションで 3 つの Behavior(フロントエンド・WebSocket・API) を定義しています。

ブラウザ → https://xxx.cloudfront.net

               ├── /          → S3(React アプリ)
               ├── /api/*     → EC2:8080(REST API)
               └── /ws        → EC2:8765(WebSocket)

Behavior (/ → S3)

React アプリの静的ファイル(HTML / JS / CSS)を S3 から配信します。

Behavior /api/* → EC2:8080(REST API)

S3 の署名付き URL 取得(/api/presign)など EC2 の REST API への通信に使います。

Behavior /ws → EC2:8765(WebSocket)

映像の送受信に使う WebSocket の通信です。WebSocket の接続確立は HTTP の Upgrade: websocket ヘッダーを使ったハンドシェイクで始まります。ALL_VIEWER_EXCEPT_HOST_HEADER によってこのヘッダーも EC2 に転送されるため、CloudFront 越しでも WebSocket が正常に機能します。/api/* と異なりパスの書き換えは不要です。

X-Origin-Verify(Confused Deputy 対策)

CloudFront が EC2 に転送するとき、秘密のヘッダーを付与しています。EC2 の Python サーバーはこのヘッダーが一致しない場合は 403 を返します。これにより EC2 の IP アドレスを直接叩いて CloudFront の認証を迂回する攻撃を防いでいます。

(3) セキュリティグループの設計

009

EC2 のポートを CloudFront エッジ IP のみに絞るため、CloudFront マネージドプレフィックスリスト(pl-58a04531)を使用します。ポート 8080 と 8765 を個別に 2 ルール設定すると、プレフィックスリストの max entries(55)× 2 = 110 スロットとなり、SG のデフォルト上限(60)を超えてしまいます。

ポート範囲指定(8080〜8765)を使って 1 ルールに集約することで、55 スロットに収めました。

const cfPLId = 'pl-58a04531'; // CloudFront Managed Prefix List (ap-northeast-1)
sg.addIngressRule(
  ec2.Peer.prefixList(cfPLId),
  ec2.Port.tcpRange(8080, 8765),
  'CloudFront PL (8080-8765)'
);

5 サーバー実装のポイント(Python)

GitHub: server/main.py

FastAPI(ポート 8080)と WebSocket サーバー(ポート 8765)を 1 プロセスで動かし、supervisord で管理します。

GitHub: docker/Dockerfile

FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
RUN pip3 install --no-cache-dir \
    torch==2.6.0+cu124 torchvision==0.21.0+cu124 \
    --index-url https://download.pytorch.org/whl/cu124
RUN pip3 install --no-cache-dir \
    ultralytics websockets boto3 "fastapi[standard]" uvicorn \
    "python-jose[cryptography]" requests opencv-python-headless
COPY server/main.py /app/main.py
COPY models/best.pt /app/best.pt
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]

GitHub: docker/supervisord.conf

[program:server]
command=python3 /app/main.py
autorestart=true

(1) Cognito JWT 検証

WebSocket と FastAPI の両方で Cognito の JWT を検証します。WebSocket はクエリパラメータ(?token=...)でトークンを受け取り、HTTP ヘッダーが使えない制約を回避しています。

def verify_token(token: str) -> dict:
    jwks = get_jwks()   # Cognito の公開鍵を取得(キャッシュ)
    header = jwt.get_unverified_header(token)
    key = next((k for k in jwks["keys"] if k["kid"] == header["kid"]), None)
    return jwt.decode(token, key, algorithms=["RS256"], audience=APP_CLIENT_ID)

streamers グループのメンバーのみフレーム送信を許可し、viewers はブロードキャストの受信のみです。

(2) Confused Deputy 対策(X-Origin-Verify)

CloudFront の「Origin にカスタムヘッダーを付与する」機能を使い、EC2 側で検証します。CloudFront 経由以外のリクエスト(攻撃者が別の CloudFront ディストリビューションを立てて転送するケースなど)を弾けます。

ORIGIN_VERIFY_SECRET = os.getenv("ORIGIN_VERIFY_SECRET", "")

def _verify_origin_header(x_origin_verify: str | None = Header(None)):
    if ORIGIN_VERIFY_SECRET and x_origin_verify != ORIGIN_VERIFY_SECRET:
        raise HTTPException(status_code=403, detail="Forbidden")

api = FastAPI(dependencies=[Depends(_verify_origin_header)])

ORIGIN_VERIFY_SECRET が空の場合は検証をスキップするため、開発環境でも動作します。

(3) YOLOv8 推論とマスク描画

推論結果を使用して、オーバーレイ上にポリゴンを塗りつぶし、元のフレームを50:50で合成しています。

def process_frame(frame: np.ndarray) -> np.ndarray:
    results = model(frame, verbose=False)[0]
    if results.masks is None:
        return frame
    overlay = frame.copy()
    for i, mask_xy in enumerate(results.masks.xy):
        cls   = int(results.boxes.cls[i])
        name  = results.names[cls]
        color = CLASS_COLORS.get(name, COLORS[cls % len(COLORS)])
        pts   = np.array(mask_xy, dtype=np.int32)
        cv2.fillPoly(overlay, [pts], color)
        # ...ラベル描画
    return cv2.addWeighted(frame, 0.5, overlay, 0.5, 0)

6 CPU / GPU 自動切り替えの仕組み

008

GitHub: scripts/docker_start_auto.sh

スクリプトを使用して、t3.large(CPU)と g5.2xlarge(GPU)を切り替えると EC2 が再起動します。

GPUを有効にしてコンテナを起動するには、 --gpus all のスイッチが必要ですが、CPUにこれを付けるとnvml error: driver not loaded で起動できません。

そこで、boot 時に nvidia-smi で GPU を検出し、--gpus all の有無を決めてコンテナを 毎回作り直す systemd サービスを設定しています。

GitHub: scripts/yolov8-seg.service

[Unit]
Description=YOLOv8-seg container (auto CPU/GPU switch)
Requires=docker.service
After=docker.service network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/docker_start_auto.sh
ExecStop=/usr/bin/docker rm -f yolov8-seg

[Install]
WantedBy=multi-user.target
# scripts/docker_start_auto.sh(要点)
GPU_FLAG=""
if nvidia-smi -L 2>/dev/null | grep -q '^GPU'; then
  GPU_FLAG="--gpus all"
  echo "GPU 検出 → GPU モードで起動"
else
  echo "GPU なし → CPU モードで起動"
fi

docker rm -f yolov8-seg 2>/dev/null || true
docker run -d --name yolov8-seg ${GPU_FLAG} \
  --env-file /etc/yolov8-seg.env \
  -p 8765:8765 -p 8080:8080 \
  "${ECR_REPO_URI}:latest"

Docker イメージは CPU/GPU 共通の 1 つだけです。PyTorch は GPU がなければ自動でCPU にフォールバックします。

7 フロントエンド実装のポイント

Cognito の認証部分は AWS Amplify ライブラリで実装しています。Amplify.configure() で User Pool を設定し、<Authenticator> コンポーネントがログイン UI を提供します。サインイン後は fetchAuthSession() で JWT トークンを取得し、WebSocket 接続や API 呼び出し時に渡します。

GitHub: frontend/src/main.tsx

// Cognito の設定を環境変数から読み込んで初期化
Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId:       import.meta.env.VITE_COGNITO_USER_POOL_ID,
      userPoolClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,
    },
  },
})

GitHub: frontend/src/App.tsx

// JWT トークンを取得し Cognito グループを確認
fetchAuthSession().then(s => {
  const idToken = s.tokens?.idToken?.toString() ?? ''
  setToken(idToken)
  setIsStreamer(decodeGroups(idToken).includes('streamers'))
})

GitHub: frontend/src/VideoCanvas.tsx

(1) カメラ映像の送信

getUserMedia でカメラ映像を取得し、100ms 間隔でフレームを JPEG エンコードして WebSocket に送ります。

const startInterval = useCallback((videoEl: HTMLVideoElement) => {
  intervalRef.current = window.setInterval(() => {
    if (!videoEl.videoWidth) return
    cap.getContext('2d')!.drawImage(videoEl, 0, 0)
    if (isStreamer && readyStateRef.current === ReadyState.OPEN) {
      const b64 = cap.toDataURL('image/jpeg', 0.9).split(',')[1]
      sendRef.current(JSON.stringify({ type: 'frame_in', data: b64 }))
    }
  }, 100)
}, [isStreamer])

(2) 推論結果の描画

サーバーからブロードキャストされた frame メッセージを受け取り、Canvas に描画します。推論前の入力フレームと推論結果を左右に並べて表示します。

useEffect(() => {
  if (!lastMessage?.data) return
  const { type, data } = JSON.parse(lastMessage.data as string)
  if (type !== 'frame') return
  const img = new Image()
  img.onload = () => {
    const c = canvasRef.current
    if (!c) return
    if (c.width !== img.width || c.height !== img.height) {
      c.width = img.width; c.height = img.height
    }
    c.getContext('2d')?.drawImage(img, 0, 0)
  }
  img.src = `data:image/jpeg;base64,${data}`
}, [lastMessage])

8 まとめ

YOLOv8 のリアルタイムセグメンテーションを、ブラウザだけで操作できる Web アプリとして AWS 上に構築しました。

今回の取り組みで、意識したのは以下の点です。

  • ECR pre-push パターン: Docker イメージのビルドを cdk deploy から分離することで、インフラ変更とアプリ変更を独立して管理できるようになりました。Dockerfile を変えても cdk deploy が不要で、Mac のキャッシュが効くため 2 回目以降は数分で反映できます。
  • systemd による CPU/GPU 自動切り替え: boot 時に GPU を検出してコンテナを作り直す設計にすることで、インスタンスタイプの変更だけで CPU / GPU を切り替えられます。イメージは 1 つだけで済み、管理がシンプルとなりました。
  • CloudFront PL + X-Origin-Verify の 2 層防御: SG でリクエスト元を CloudFront エッジ IP に絞りつつ、カスタムヘッダーで Confused Deputy 攻撃を防ぎます。EC2 は、パブリックサブネットに置いていますが、直接アクセスはブロックされます。

今回の実装が、WebSocket によるリアルタイム映像配信や、サーバーサイドでの推論のたたき台になれば嬉しいです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事