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?

閉域内に Bedrock と MCP を使った Streamlit アプリを ECS Express モードでデプロイする

Posted at

生成 AI をガバメントクラウドのようなインターネット接続のない閉域の AWS 環境で手軽に使いたいとずっと考えていて、みのるんさん (@minorun365) の AI エージェント関係のハンズオン記事を閉域環境で動かせないか試行錯誤していたのですが、こちらのハンズオン記事を参考に、ハンズオンと比べるとだいぶ不完全ですが、閉域で Bedrock と MCP を使う Streamlit アプリ(チャットボット)を動かすことができました。

Streamlit アプリは簡単に Docker コンテナと周辺インフラ一式をまとめてデプロイできる ECS Express モードを使用しました。備忘録として、主に AWS のインフラ視点で構築手順をメモしたいと思います。

閉域内で ECS Express モードに Bedrock を使うアプリをデプロイする際の考慮ポイント

ECR の VPC エンドポイント経由で Docker イメージを取得

ECS Express モードでは ECR から Docker イメージをプルしてデプロイしなければなりません。

今回は閉域環境にデプロイするので、ECR への VPC エンドポイントを作る必要があります。

Bedrock のモデルアクセスも VPC エンドポイント経由で接続

Bedrock のモデルへのアクセスも VPC エンドポイント経由となります。

MCP サーバーを uvx で実行するため PyPI に閉域でアクセスするには CodeArtifact を使う

uvx で MCP サーバーをダウンロードする際、デフォルトではインターネット経由で PyPI に接続しますが、今回は閉域環境のためそれができません。

CodeArtifact には PyPI や NPM をアップストリームリポジトリにする機能があるので、CodeArtifact に VPC エンドポイント経由でアクセスし、MCP サーバーをダウンロードできるようにします。

ecs.region.on.aws に対する名前解決が必要

ECS Express モードでは、HTTPS 証明書と ALB の設定まで自動で行ってくれます。自動で作られた設定に対する URL に閉域環境内からアクセスするためには、閉域環境内のクライアントで当該 URL のドメインを名前解決できる必要があります。

ハンズオンの構成で再現できなかったもの

Cognito と ALB を連携したユーザー認証

Cognito が対応している VPC エンドポイントの関係で恐らくこれが出来ないと思われます。(ユーザーのログイン画面には VPC エンドポイント経由ではアクセスできない。)

API Gateway を前段に置くことで閉域から Cognito の機能を呼び出すことはできるので、やるとしたら認証画面を自分で作り込むしかないと思われます。

インターネット接続を前提としている MCP サーバーは正常に動作しない

MCP サーバー自体は uvx で閉域環境内に立てるのですが、MCP サーバー自体の機能にインターネット接続が必要な場合、閉域環境では正常に動作しません。

そのため、ハンズオンでは AWS Documentation MCP Server を連携ツールにしていますが、閉域環境では search_documentation が動作しませんでした。

そこで今回は、インターネット接続が不要な MCP サーバーとして、公式が動作テスト用として公開している Fetch MCP Server を連携ツールにしました。

Fetch MCP Server は指定した URL の内容の処理ができる MCP サーバーなので、イントラ内部の Web コンテンツの要約などに使えるのではないかと思います。

それでは早速、構築手順を見ていきます。

Streamlit アプリ on ECS Express モードの構成図

今回構築する Streamlit アプリ on ECS Express モードの構成図は以下のとおりです。

ECS Express モードでプライベートサブネットに必要なリソースを自動でデプロイします。また、ECS の Docker イメージのデプロイや Streamlit アプリの動作に必要な Bedrock などの VPC エンドポイントを同じプライベートサブネットに作成しています。

ECSExpressMode.drawio.png

Streamlit アプリの Docker イメージをビルド

Streamlit アプリの本体をローカル環境で開発する

みのるんさんのハンズオン記事 のコードを以下のとおり一部修正しました。

  • uvx が参照する PyPI の URL を CodeArtifact の VPC エンドポイントとなるようにした
  • CodeArtifact の PyPI リポジトリへ ECS のコンテナからアクセスするのに必要な認証情報を boto3 で取得し、CodeArtifact の VPC エンドポイントにアクセスする URL へセットするようにした
  • ガバメントクラウドを想定して、推論プロファイルは東京・大阪リージョンにのみクロスリージョン推論するモデルの「Amazon Nova 2 Lite」に変更
  • MCP サーバーをインターネット接続が不要な別の MCP サーバーに変更
import asyncio
import boto3
import streamlit as st
from strands import Agent
from strands.tools.mcp import MCPClient
from mcp import stdio_client, StdioServerParameters

# CodeArtifact の認証設定
CODEARTIFACT_DOMAIN = "CodeArtifact のドメイン名"
AWS_ACCOUNT_ID = "CodeArtifact の AWS アカウント ID"
REGION = "ap-northeast-1" # CodeArtifact のリポジトリがあるリージョン
REPOSITORY_NAME = "pypi-store"

# 推論プロファイルが日本国内に限定されているモデルを選択
PROFILE_ID = "jp.amazon.nova-2-lite-v1:0"

# フロントエンドを描画
st.title("Strands MCPエージェント お試し版")
st.text("MCPサーバーを使って、Nova Liteがあなたの質問に答えます!")
prompt = st.chat_input("質問を入力")

if prompt:
    # ユーザーのプロンプトを表示
    with st.chat_message("user"):
        st.markdown(prompt)

    # エージェントの応答を表示
    with st.chat_message("assistant"):
        with st.spinner("考え中…"):

            # CodeArtifact から PyPI のあるリポジトリを使うための
            # 認証トークンを取得する設定
            sdk_client = boto3.client("codeartifact", region_name=REGION)
            code_artifact_token = sdk_client.get_authorization_token(
                domain=CODEARTIFACT_DOMAIN,
                domainOwner=AWS_ACCOUNT_ID,
            )

            # uvx がデフォルトの PyPI の URL ではなく、
            # CodeArtifact の VPC エンドポイントへ接続するようにする設定
            uv_index_url = (
                "https://aws:"
                + code_artifact_token["authorizationToken"]
                + "@"
                + CODEARTIFACT_DOMAIN
                + "-"
                + AWS_ACCOUNT_ID
                + ".d.codeartifact."
                + REGION
                + ".amazonaws.com/pypi/"
                + REPOSITORY_NAME
                + "/simple/"
            )

            # MCPクライアント作成
            # args に --default-index を指定することで、
            # PyPI の URL ではなく指定した URL からパッケージをダウンロードできる
            # 元のハンズオン記事では AWS Documentation MCP Server を使っているが、
            # インターネット接続がない環境では正常に動作しないため、
            # 閉域環境でも動作する Fetch MCP Server を使うようにした。
            client = MCPClient(
                lambda: stdio_client(
                    StdioServerParameters(
                        command="uvx",
                        args=[
                            "--default-index",
                            uv_index_url,
                            "mcp-server-fetch",
                        ],
                    )
                )
            )

            with client:
                # エージェント作成
                agent = Agent(
                    model=PROFILE_ID,
                    system_prompt="思考も回答も日本語で行ってください。",
                    tools=client.list_tools_sync(),
                )

                # ストリーミング表示の準備
                container = st.container()
                state = {
                    "text_holder": container.empty(),
                    "buffer": "",
                    "shown_tools": set(),
                }

                # Strandsをストリーミング実行する非同期関数を定義
                async def run_stream():
                    async for event in agent.stream_async(prompt):
                        current_tool = event.get("current_tool_use", {})
                        tool_id = current_tool.get("toolUseId")
                        tool_name = current_tool.get("name")

                        # ツール実行を検出して表示
                        if (
                            tool_id
                            and tool_name
                            and tool_id not in state["shown_tools"]
                        ):
                            state["shown_tools"].add(tool_id)
                            if state["buffer"]:
                                state["text_holder"].markdown(state["buffer"])
                                state["buffer"] = ""
                            container.info(f"🔧 **{tool_name}** ツールを実行中...")
                            state["text_holder"] = container.empty()

                        # テキストを抽出して表示
                        if event.get("data"):
                            state["buffer"] += event["data"]
                            state["text_holder"].markdown(state["buffer"] + "")

                    # 最終表示
                    if state["buffer"]:
                        state["text_holder"].markdown(state["buffer"])

                # 非同期関数を実行
                asyncio.run(run_stream())

Dockerfile の内容

Dockerfile は みのるんさんのハンズオン記事 をそのまま真似しました。

一箇所だけ、Mac で Docker イメージをビルドするとアーキテクチャが ARM64 になるのですが、ECS Express Mode だとデフォルトでは x86_64 のため、Dockerfile でアーキテクチャを指定するよう変更しています。(Mac の Docker Desktop はこの辺りをうまいことやってくれます。)

FROM --platform=linux/x86_64 python:3.13

# 作業ディレクトリ
WORKDIR /app

# 依存パッケージをインストール
RUN pip install uv mcp streamlit strands-agents strands-agents-tools

# アプリ本体をコピー
COPY . /app

# 公開ポート
EXPOSE 80

# Streamlitを起動
CMD ["streamlit", "run", "app.py", "--server.port=80", "--server.address=0.0.0.0"]

作業用ローカル環境で Docker イメージのビルド

作業用ローカル環境の Docker でイメージをビルドし、tar でアーカイブしてローカルに保存します。

$ docker build -t simple-streamlit-chatbot:latest .
docker save simple-streamlit-chatbot:latest > ~/simple-streamlit-chatbot.tar

作業用ローカル環境から Docker イメージファイルを S3 へアップロード

作業用ローカル環境から保存した Docker イメージファイルを S3 へアップロードします。

$ aws s3 cp ~/simple-streamlit-chatbot.tar s3://アップロード先の S3 バケット名 --profile プロファイル名

以降は AWS 環境(全て東京リージョンとします)側での作業となります。

ECS Express モードを使うための VPC エンドポイントの作成

ECS Express モードの使用には ECR が必要なため、ECR へのインターフェース型の VPC エンドポイントを作成します。また、CloudWatch Logs も使うため、以下の VPC エンドポイントを作成します。

  • com.amazonaws.ap-northeast-1.ecr.dkr
  • com.amazonaws.ap-northeast-1.ecr.api
  • com.amazonaws.ap-northeast-1.logs

その他に、ECS とは直接関係ありませんが、この後の作業に S3 への VPC エンドポイント(も必要なので作成します。

ECR にプライベートリポジトリを作成する

ECR のマネジメントコンソールから ECR のプライベートレジストリに任意の名前空間とリポジトリ名でプライベートリポジトリを作成します。

スクリーンショット 2026-02-04 23.15.37.png

プライベートレジストリのアクセス許可に特定の VPC エンドポイントからのみのアクセスを許可する設定をすることで、インターネット経由でのプライベートレジストリへのアクセスを禁止します。

ECR のマネジメントコンソールから「プライベートレジストリ」→「許可」に進み、以下のような JSON を編集します。

{
  "Sid": "DenyExceptSpecificVPCEndpoint",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "ecr:*",
  "Resource": "*",
  "Condition": {
    "StringNotEquals": {
      "aws:SourceVpce": [
        "com.amazonaws.ap-northeast-1.ecr.api の VPC エンドポイント ID",
        "com.amazonaws.ap-northeast-1.ecr.dkr の VPC エンドポイント ID"
      ]
    }
  }
}

Docker イメージを ECR プライベートリポジトリへプッシュする

作業用 EC2 の設定

閉域 VPC から ECR へ Docker イメージをプッシュするため、この作業は VPC 内の EC2 インスタンスから実施します。

閉域 VPC のプライベートサブネットに適当な EC2 インスタンスを起動します。ここでは Amazon Linux 2023 でインスタンスを起動しました。

IAM ロールのアタッチ

このインスタンスには、S3 にアクセス可能で、以下の ECR のアクションが許可されたポリシーを割り当てた IAM ロールをアタッチしておきます。

  • ecr:GetAuthorizationToken
  • ecr:InitiateLayerUpload
  • ecr:UploadLayerPart
  • ecr:CompleteLayerUpload
  • ecr:ListImages
  • ecr:PutImage
  • ecr:DescribeImages
  • ecr:BatchCheckLayerAvailability

Docker のインストール

作業用の EC2 インスタンスに、Docker をインストールします。Amazon Linux 2023 だと次のとおりです。なお、このインスタンスはプライベートサブネットにあるためインターネット接続ができませんが、事前に S3 の VPC エンドポイントを作成すれば dnf からパッケージのダウンロードが可能です。

$ sudo dnf update
$ sudo dnf install docker
$ sudo systemctl start docker
$ sudo systemctl enable docker
$ sudo usermod -aG docker ec2-user

Docker イメージのロード

S3 から先ほどアップロードした tar ファイルになっている Docker イメージを S3 の VPC エンドポイント経由でダウンロードし、Docker にロードします。

$ aws s3 cp s3://アップロード先の S3 バケット/simple-streamlit-chatbot.tar .
$ docker load < simple-streamlit-chatbot.tar

Docker イメージを ECR へプッシュする

まずは Docker イメージの ID を dokcker image コマンドで確認し、名前を ECR に作成したリポジトリと合わせて変更します。

$ docker tag イメージ ID ECR のある AWS アカウント ID.dkr.ecr.ap-northeast-1.amazonaws.com/test01/simple-streamlit-chatbot:latest

作業用 EC2 インスタンスの Docker クライアントを VPC エンドポイント経由で ECR に認証します。AWS CLI から次のコマンドを実行します。

$ aws ecr get-login-password | docker login --username AWS --password-stdin ECR のある AWS アカウント ID.dkr.ecr.ap-northeast-1.amazonaws.com

ログインに成功したら、Docker イメージを ECR リポジトリにプッシュします。

$ docker push ECR のある AWS アカウント ID.dkr.ecr.ap-northeast-1.amazonaws.com/test01/simple-streamlit-chatbot:latest

Docker イメージを ECR のリポジトリへプッシュできたら、次は Streamlit アプリを動かすための環境を設定します。

CodeArtifact に PyPI をアップストリームとするリポジトリを作成する

CodeArtifact のインターフェースエンドポイントを作成

プライベートサブネットに CodeArtifact のインターフェース型 VPC エンドポイントを作成します。

必要なエンドポイントは以下のとおりです。

  • com.amazonaws.ap-northeast-1.codeartifact.api
  • com.amazonaws.ap-northeast-1.codeartifact.repositories

リポジトリのアップストリームを PyPI Store に設定

CodeArtifact マネジメントコンソールから「リポジトリの作成」に進み、アップストリームリポジトリに「pypi-store」を選択します。

スクリーンショット 2025-10-26 10.14.25.png

適当なドメイン名を設定します。

スクリーンショット 2025-10-26 10.16.14.png

閉域環境から CodeArtifact の VPC エンドポイント経由で PyPI のパッケージをプルすることができるようになりました。

Bedrock の VPC エンドポイントを作成

今回作成する Streamlit アプリで必要な Bedrock の VPC エンドポイントは以下のとおりです。

  • com.amazonaws.ap-northeast-1.bedrock-runtime

Route 53 インバウンドエンドポイントの設定

既に設定済みのことが多いと思いますが、閉域のオンプレミス環境などから閉域 AWS 環境の名前解決をするためには、DNS リゾルバーサーバーと Route 53 インバウンドエンドポイントを連携する必要があります。

Route 53 インバウンドエンドポイントの設定は以前に記事を書きましたので良かったら参考にしてください。

以上で Streamlit アプリを ECS Express モードで動かす準備は全て完了です。早速 ECS Express モードで Streamlit アプリをデプロイしてみましょう。

ECS Express モードのデプロイ

ECS のマネジメントコンソールから「Express モード」へ進みます。

「イメージ URI」には ECR にプッシュした Docker イメージを選択します。

スクリーンショット 2026-02-01 16.27.25.png

ECS タスクに割り当てる IAM ロールの作成

「タスク実行ロール」「インフラストラクチャロール」は自動的に作成してくれます。

「タスクロール」は、ECS タスクから実行させたい処理に必要な権限の設定された IAM ロールを割り当てます。

ここでは事前に、CodeArtifact へアクセスできるよう AWS 管理ポリシーの「AWSCodeArtifactReadOnlyAccess」と、以下のとおり Bedrock の Amazon Nova 2 Lite モデルを実行するのに必要なポリシーを設定した IAM ロールを作成しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowBedrockInvokeModel",
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream"
            ],
            "Resource": [
                "arn:aws:bedrock:ap-northeast-1::foundation-model/amazon.nova-2-lite-v1:0",
                "arn:aws:bedrock:ap-northeast-3::foundation-model/amazon.nova-2-lite-v1:0",
                "arn:aws:bedrock:ap-northeast-1:AWS アカウント ID:inference-profile/jp.amazon.nova-2-lite-v1:0"
            ]
        }
    ]
}

プライベートサブネットでネットワーク設定をカスタマイズ

「その他の設定」から「ネットワーク設定をカスタマイズ」をチェックし、VPC、サブネット、セキュリティグループを選択します。

スクリーンショット 2026-02-01 16.29.07.png

スクリーンショット 2026-02-01 11.20.46.png

ECS Express Mode は ALB、証明書、セキュリティグループなどを自動的に作成してくれるのですが、サブネットを「プライベートサブネット」とすることで、閉域の設定にしてくれます。

ECS のデプロイ

以上の設定が終わったら「作成」をするだけで ECS と ALB、証明書といった周辺のインフラが全て自動でデプロイされます。

スクリーンショット 2026-02-05 1.03.06.png

「アプリケーション URL」が ECS Express モードで自動作成された ALB のエンドポイントとなっているので、Web ブラウザからアクセスすると、デプロイした Docker イメージから起動したコンテナで Streamlit アプリが実行されます。

Streamlit アプリの動作確認

Web ブラウザから先ほどの ALB のエンドポイントへアクセスしてみます。

期待どおりのチャットボットの画面が表示され、対話も成功しています。

スクリーンショット 2026-02-04 17.41.50.png

Fetch MCP Server を使って任意の URL の内容を取得してみます。適当な LAN 内の URL(L2 スイッチの管理画面)を指定してみました。

スクリーンショット 2026-02-04 17.43.09.png

しっかり指定した URL の内容を解析できました。

まとめ

ECS Express モードを使うと、閉域環境でも簡単に Docker コンテナをデプロイできました。

Bedrock を使ったアプリも、VPC エンドポイントを設定することで閉域環境でも使えます。

ただし、MCP サーバーと連携する場合、MCP サーバーの機能がインターネット接続を前提としている場合は正常に動作しません。MCP サーバーが閉域環境で完結する方法を考える必要があります。

一部の制限事項はありますが、ガバメントクラウドのような閉域環境でも AI エージェントを簡単に使えるようになるのは使い道が広がっていいですね。

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?