Amazon ECS Express Mode を利用して WebSocket アプリをALBで公開してみた

Amazon ECS Express Mode を利用して WebSocket アプリをALBで公開してみた

2025年11月リリースの「Amazon ECS Express Mode」を使用し、WebSocket対応Nginxアプリをデプロイを検証しました。コンソール数クリックで環境が完成し、自動構築されたALBで追加設定なくWebSocket通信に成功。デプロイ手順からダッシュボードでのメトリクス確認までを解説します。
2025.11.22

2025年11月21日、Amazon ECS Express Mode がリリースされました。

https://aws.amazon.com/jp/blogs/aws/build-production-ready-applications-without-infrastructure-complexity-using-amazon-ecs-express-mode/

本番環境ですぐに使えるコンテナアプリケーションを、数クリック、または1つのコマンドで構築できるというこの機能を、早速試す機会がありましたので紹介します。

事前準備

コンテナイメージ作成、登録

ECS Express Mode にデプロイする コンテナイメージを用意しました。

今回、WebSocketエコーサーバを含む、Nginxイメージを作成。 ECRにPushしました。

Nginxテストイメージの作成、登録スクリプト
#!/bin/bash
set -e

REGION="ap-northeast-1"
ACCOUNT_ID="********"
REPO_NAME="websocket-nginx"
IMAGE_URI="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO_NAME}:latest"

echo "=== 1. WebSocketサーバーを作成 ==="
cat > ws-server.js << 'EOF'
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', ws => {
  ws.on('message', (msg, isBinary) => {
    const data = isBinary ? msg : msg.toString();
    ws.send(data);
  });
});
EOF

echo "=== 2. package.jsonを作成 ==="
cat > package.json << 'EOF'
{"dependencies":{"ws":"^8.14.2"}}
EOF

echo "=== 3. Nginx設定を作成 ==="
cat > nginx.conf << 'EOF'
events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        root /usr/share/nginx/html;
        index index.html;

        location / {
            try_files $uri $uri/ =404;
        }

        location /ws {
            proxy_pass http://127.0.0.1:8080;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
        }
    }

    server {
        listen 8081;

        location / {
            return 200 'WebSocket Echo\n';
            add_header Content-Type text/plain;
        }
    }
}
EOF

echo "=== 4. 起動スクリプトを作成 ==="
cat > start.sh << 'EOF'
#!/bin/sh
node /app/ws-server.js &
nginx -g 'daemon off;'
EOF

echo "=== 5. HTMLテストページを作成 ==="
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Test</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; }
        #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; margin: 10px 0; }
        input, button { padding: 10px; margin: 5px 0; }
        input { width: 70%; }
        button { width: 25%; }
        .sent { color: blue; }
        .received { color: green; }
        .status { color: red; }
    </style>
</head>
<body>
    <h1>WebSocket Echo Test</h1>
    <div id="status" class="status">Disconnected</div>
    <div id="messages"></div>
    <input id="input" type="text" placeholder="Type message..." disabled>
    <button id="send" disabled>Send</button>
    <button id="connect">Connect</button>

    <script>
        let ws;
        const messages = document.getElementById('messages');
        const input = document.getElementById('input');
        const status = document.getElementById('status');
        const sendBtn = document.getElementById('send');
        const connectBtn = document.getElementById('connect');

        function log(msg, className) {
            const div = document.createElement('div');
            div.className = className;
            div.textContent = msg;
            messages.appendChild(div);
            messages.scrollTop = messages.scrollHeight;
        }

        connectBtn.onclick = () => {
            const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
            const wsUrl = `${protocol}//${location.host}/ws`;

            ws = new WebSocket(wsUrl);

            ws.onopen = () => {
                status.textContent = 'Connected';
                status.style.color = 'green';
                input.disabled = false;
                sendBtn.disabled = false;
                connectBtn.disabled = true;
                log('Connected to ' + wsUrl, 'status');
            };

            ws.onmessage = (e) => {
                log('← ' + e.data, 'received');
            };

            ws.onclose = () => {
                status.textContent = 'Disconnected';
                status.style.color = 'red';
                input.disabled = true;
                sendBtn.disabled = true;
                connectBtn.disabled = false;
                log('Disconnected', 'status');
            };

            ws.onerror = (e) => {
                log('Error: ' + e, 'status');
            };
        };

        sendBtn.onclick = () => {
            if (ws && input.value) {
                ws.send(input.value);
                log('→ ' + input.value, 'sent');
                input.value = '';
            }
        };

        input.onkeypress = (e) => {
            if (e.key === 'Enter') sendBtn.click();
        };
    </script>
</body>
</html>
EOF

echo "=== 6. Dockerfileを作成 ==="
cat > Dockerfile << 'EOF'
FROM node:alpine
RUN apk add --no-cache nginx && mkdir -p /usr/share/nginx/html
WORKDIR /app
COPY package.json .
RUN npm install
COPY ws-server.js .
COPY nginx.conf /etc/nginx/nginx.conf
COPY index.html /usr/share/nginx/html/
COPY start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 80
CMD ["/start.sh"]
EOF

echo "=== 7. Dockerイメージをビルド ==="
docker build -t ${REPO_NAME} .

echo "=== 8. ECRにログイン ==="
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com

echo "=== 9. イメージにタグを付ける ==="
docker tag ${REPO_NAME}:latest ${IMAGE_URI}

echo "=== 10. ECRにプッシュ ==="
docker push ${IMAGE_URI}

echo "=== 完了 ==="
echo "イメージURI: ${IMAGE_URI}"

IAMロール

Express Mode のコンソールウィザード内にある「新しいロールの作成」を利用して、以下のロールを作成しました。

  • タスク実行ロール: ecsTaskExecutionRole

ecsTaskExecutionRole作成

  • インフラストラクチャロール: ecsInfrastructureRoleForExpressServices

ecsInfrastructureRoleForExpressServices作成

セットアップ

Express モードのコンソールから「作成」を実施しました。

事前に準備した ECRイメージURIを指定し、ロールを選択するだけの非常にシンプルな手順でした。

Expressモード設定

CPUやメモリなどのオプション設定はデフォルトのままとしました。

その他の設定は省略

進捗

デプロイが開始すると、リソースタブで進捗が確認できます。

 デプロイ開始

8分程度で全てのリソースが「アクティブ」となり、デプロイ完了となりました。

デプロイ完了後のリソース

動作確認

アプリケーション動作

アプリケーション URLにブラウザでアクセスを試みました。

今回の検証用アプリは WebSocket を使用したデモですが、以下の点が問題なく動作することを確認しました。

  • HTTP 接続: 静的 HTML フォームの取得
  • WebSocket 接続: メッセージの送受信

Webソケットブラウザテスト

Express Mode で自動構築された ALB が、特別な設定なしで WebSocket 通信(Upgrade ヘッダー)を正しく通していることが確認できました。

デプロイの確認

「サービスの更新」から、アプリケーションのデプロイ挙動を確認しました。

デプロイ1

新しいバージョンの ECR イメージを指定して「更新」を実施すると、バックグラウンドでデプロイが開始されました。

デプロイ2

イメージの選択画面は以下の通りです。

イメージを選択

更新開始後、10分程度でデプロイ完了となりました。

デプロイ進行中

デフォルトのデプロイ方式はローリングアップデートでした。 新しいタスクのヘルスチェック成功後、一定の並行稼働期間を経て古いタスクが停止される、安全な挙動が確認できました。

デプロイ進捗

ダッシュボード

Express サービスの概要ページに表示されているタブを確認してみました。

オブザーバビリティ

CPU、メモリ使用率と、ロードバランサのメトリクスが確認できました。 グラフにデプロイ実施のタイミングが反映されているため、デプロイに起因する変化に気づきやすい表示でした。

オブザーバビリティ

ログ

ECS タスクのアプリケーションログの確認ができました。

ログ

リソース

Express サービスが利用する AWS サービス(ECS や ALB)のダッシュボードへの導線となる一覧が表示されました。

リソース

まとめ

App Runner のような手軽さで、中身は標準的な ECS を利用開始することが可能になりました。

ロードバランサーとして通常の ALB を複数のサービスで利用することができるため、固定コストを 1 台の ELB 料金に抑制しつつ、ALB のリスナールール、メトリクス、ログなど高度な機能を活用できます。

シンプルなワークロードであれば、まず Express Mode で立ち上げ、必要に応じて詳細設定を実施。 さらにカスタマイズが必要となった段階で、通常の ECS 運用へシームレスに移行(卒業)できるため、将来的なロックインの心配もありません。

CloudFormation などの IaC サポートや、Express Mode で作成されたリソースのカスタマイズ、監視用のサイドカーコンテナの追加などについても、引き続き確認してみたいと思います。

この記事をシェアする

FacebookHatena blogX

関連記事