6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWSコンテナ入門】ECS&BedRockで会話AIチャットボットを構築してみた!

Posted at

1. はじめに

こんにちは!Sakitsuです。
前回は、超初心者としてAWS Fargateを使って無事「hello-world」を実行することができました!

今回はさらにステップアップして、AWSのECSとBedrockを活用し、サーバレスで会話AIチャットボットを構築してみたいと思います。

作成例:
last-6.png

参考資料

下記 みのるん様の記事を参考に、今回のアーキテクチャーを考えました!

2. AWS Bedrockとは?

AWSのホームページ「Amazon Bedrock」にはこう書いてあります:

Amazon Bedrock は、単一の API を通じて AI21 Labs、Anthropic、Cohere、DeepSeek、Luma、Meta、Mistral AI、poolside (近日リリース予定)、Stability AI、TwelveLabs (近日リリース予定)、Writer および Amazon などの先駆的な AI 企業からの高性能な基盤モデル (FM) の幅広い選択肢を提供するフルマネージドサービスであり、セキュリティ、プライバシー、責任ある AI を備えた生成 AI アプリケーションを構築するために必要な一連の幅広い機能を提供します。

今回は、このBedrockで提供されているAnthropicの「Claude」モデルを使って、会話AIチャットボットを作成してみます。

3. システム構成と全体像

システムの全体像は下記の図の通りです。

Bedrock.drawio.png

4. 事前準備

4.0 VPC、サブネットなどのAWS環境準備

VPC、サブネットなど
今回は、パブリックサブネットにALBと作業用EC2を置き、プライベートサブネットにWebサーバを配置します。
 アベイラビリティゾーン1aと1cに、それぞれパブリックサブネットとプライベートサブネットを作成しました。

AWSサービス IP 備考
VPC 192.168.0.0/16
Public Subnet 1a 192.168.10.0/24 ルートテーブルで外部向け通信は IGW を指定
Public Subnet 1c 192.168.20.0/24 ルートテーブルで外部向け通信は IGW を指定
Private Subnet 1a 192.168.30.0/24 ルートテーブルで外部向け通信は NAT を指定
Private Subnet 1c 192.168.40.0/24 ルートテーブルで外部向け通信は NAT を指定

ルートテーブルについて

ルートテーブル 送信先 ターゲット 備考
Public Route Table 0.0.0.0/0 IGW(インターネットゲートウェイ) Public Subnetと関連付け
Private Route Table 0.0.0.0/0 NATゲートウェイ  Private Subnetと関連付け

セキュリティグループについて
EC2用セキュリティグループ

ルール タイプ  プロトコル ポート範囲 ソース 備考
インバウンドルール SSH TCP 22 プレフィックスリスト名: com.amazonaws.ap-northeast-1.ec2-instance-connect EC2 Instance ConnectのIPアドレス範囲からのSSHを許可
インバウンドルール HTTP TCP 80 0.0.0.0/0 Webサーバ公開テスト用

ECS用セキュリティグループ

ルール タイプ   プロトコル ポート範囲 ソース/送信先 備考
インバウンドルール カスタム TCP TCP 8080 0.0.0.0/0
インバウンドルール HTTP TCP 80 0.0.0.0/0
アウトバウンドルール すべてのトラフィック すべて すべて 0.0.0.0/0

EC2用のIAMロール
下記手順でEC2用のIAMロールを作成しました。
今回はPublicサブネットにEC2を作成したため、インターネットゲートウェイ(IGW)経由で「ECR」へ直接イメージをPushできます。
もしEC2をプライベートサブネットに置く場合は、別途「VPCエンドポイント」が必要です。

手順内容 参考画像 
IAM>ロール>ロールを作成
信頼されたエンティティタイプ →「AWSのサービス」
ユースケース →「EC2」
iam-1.png
ポリシーは下記を追加:
「AmazonEC2ContainerRegistryFullAccess」
「AmazonBedrockFullAccess」
「AmazonDynamoDBFullAccess」
iam-2.png
ロール名を入力して作成 iam-3.png

コンテナ用のIAMロール(タスクロール)
下記手順でコンテナ用IAMロール(タスクロール)を作成しました。
ECSからBedrockやDynamoDBへアクセスするために必要です。

手順内容 参考画像 
IAM>ロール>ロールを作成
信頼されたエンティティタイプ: AWSサービス
ユースケース:Elastic Container Service Task
ecs-4.png
ポリシーは下記を追加:
「AmazonBedrockFullAccess」
「AmazonDynamoDBFullAccess」
ecs-4.png
ロール名: ecs-task-roleを入力して作成 ecs-3.png

4.1 AWS Bedrockの有効化と権限設定

なぜ有効化が必要か?

AWSのBedrockサービスは、初期状態では誰でも自由に使えるわけではありません。
利用したいBedrockモデル(例:Anthropic Claude, Amazon Titan, AI21など)ごとに、
「利用申請・有効化」が必要です。

今回は「Claude 3.5 Sonnet」モデルを使いたいので、
下記の手順でアクセス権があるかどうかを確認しました。

有効化手順

手順内容 参考画像
Amazon Bedrock > Bedrock configurations(メニューバーの一番下) > モデルアクセス Bedrock-access-01.png
「Claude 3.5 Sonne」を選択し、
「リクエスト可能」をクリックし、
「モデルアクセスをリクエスト」でアクセスを申請できる
Bedrock-access-02.png
会社情報などの情報の入力が必要ですので、適当に入力しても大丈夫 Bedrock-access-03.png
2、3分経ったらアクセス権が付与された Bedrock-access-04.png

4.2 イメージ作成用EC2の準備

4.2.1 EC2の作成

手順内容 参考画像
EC2 >インスタンス >インスタンスを起動
名前入力:BedRock_Test
AMI選択:Amazon Linux 2023 AMI
ec2-start.png
キーペアを選択(新規作成も可)
サブネット:Public Subnet 1a
パブリック IP の自動割り当て:有効化
セキュリティグループ:EC2用セキュリティグループ
ec2-start-2.png
IAMインスタンスプロフィール:EC2用のIAMロールを選択
その他はデフォルト
ec2-start-3.png

4.2.2 必要パッケージのインストール

下記コマンドを実行し、必要なパッケージをインストールします。
「Complete!」と表示されれば成功です。

①まず、既存パッケージをすべて最新に更新します。

sudo yum update -y

参考画像:
ec2-install-1.png

②また、「python3」と「git」をインストールも必要です。

sudo yum install python3 git -y

参考画像:
ec2-install-2.png

③最後は、boto3のインストールです。

boto3はAWSのサービス(Bedrockなど)をPythonから操作するための公式SDKです。
Bedrock APIをPythonで使うには「boto3」が必須です。

sudo yum install python3-pip -y
pip3 install boto3

参考画像:
ec2-install-3.png

4.3 DynamoDBのテーブルの作成

下記手順でDynamoDBテーブルを作成しました。

手順内容 参考画像
DynamoDB >テーブル >テーブルの作成  DynamoDB-1.png
下記のように設定:
テーブル名:chat_sessions
パーティションキー:session_id、文字列
ソートキー-オプション:timestamp、数値
DynamoDB-2.png

5. ハンズオン

5.1 Bedrock API を使った会話AIの構築

5.1.1 ディレクトリ構成
EC2にログオンし、下記のような構成でディレクトリを作成しました。

bedrock_chatbot/ディレクトリの構造
bedrock_chatbot/
├── app.py
├── templates/
   └── chat.html
├── requirements.txt

5.1.2 requirements作成

requirements.txtの中身
fastapi==0.111.0
uvicorn==0.29.0
boto3==1.34.87
jinja2==3.1.3

5.1.3 app.py作成

app.pyの中身
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from jinja2 import Environment, FileSystemLoader
import boto3
import json
import time
import os
import uuid

app = FastAPI()
env = Environment(loader=FileSystemLoader('templates'))

# AWS設定
REGION = os.environ.get("AWS_DEFAULT_REGION", "ap-northeast-1")
BEDROCK_MODEL_ID = "anthropic.claude-3-5-sonnet-20240620-v1:0"
DYNAMODB_TABLE = "chat_sessions"

bedrock = boto3.client("bedrock-runtime", region_name=REGION)
dynamodb = boto3.resource("dynamodb", region_name=REGION)
table = dynamodb.Table(DYNAMODB_TABLE)

def save_message(session_id, role, msg):
    table.put_item(Item={
        "session_id": session_id,
        "timestamp": int(time.time()*1000),
        "role": role,
        "message": msg
    })

def get_history(session_id):
    res = table.query(
        KeyConditionExpression="session_id = :s",
        ExpressionAttributeValues={":s": session_id},
        ScanIndexForward=True
    )
    return [{"role": item["role"], "message": item["message"]} for item in res.get("Items", [])]

def ask_bedrock(session_id, user_input):
    # 履歴メッセージを取得
    history = get_history(session_id)  # [{'role': 'user', 'message': ...}, ...]
    # 今回のユーザー入力を履歴に追加
    history.append({'role': 'user', 'message': user_input})
    # Claude互換フォーマットに変換
    messages = []
    for msg in history:
        messages.append({
            "role": "user" if msg["role"] == "user" else "assistant",
            "content": msg["message"]
        })

    body = {
        "anthropic_version": "bedrock-2023-05-31",
        "messages": messages,
        "max_tokens": 1024,
        "temperature": 0.7,
        "top_p": 0.9
    }
    resp = bedrock.invoke_model(
        modelId=BEDROCK_MODEL_ID,
        body=json.dumps(body),
        accept="application/json",
        contentType="application/json"
    )
    result = json.loads(resp["body"].read())
    return result["content"][0]["text"]

@app.get("/", response_class=HTMLResponse)
async def chat_ui(request: Request, session: str = None):
    if not session:
        # UUID生成してリダイレクト
        new_session = str(uuid.uuid4())
        return RedirectResponse(url=f"/?session={new_session}")
    history = get_history(session)
    template = env.get_template("chat.html")
    return template.render(history=history, session=session)

@app.post("/send")
async def send_message(request: Request, session: str = Form('default'), message: str = Form(...)):
    # ユーザーの発言を保存
    save_message(session, "user", message)
    # Claudeへ問い合わせ
    ai_reply = ask_bedrock(session, message)
    # AIの返答を保存
    save_message(session, "assistant", ai_reply)
    # 履歴を取得
    history = get_history(session)
    # テンプレートを描画して返す
    template = env.get_template("chat.html")
    html_content = template.render(history=history, session=session)
    return HTMLResponse(content=html_content)

@app.post("/clear")
async def clear_session(request: Request, session: str = Form(...)):
    # 生成新session_id
    new_session = str(uuid.uuid4())
    # 新しいセッションIDでチャットページにリダイレクト
    return RedirectResponse(url=f"/?session={new_session}", status_code=303)

5.1.4 chat.html作成

chat.htmlの中身
<!DOCTYPE html>
<html>
<head>
    <title>AIチャットボット</title>
    <style>
        body { font-family: sans-serif; margin: 40px; }
        .bubble { border-radius: 8px; padding: 10px; margin: 8px 0; max-width: 60%; }
        .user { background: #d7eafd; margin-left: auto; text-align: right; }
        .assistant { background: #f1ebc6; margin-right: auto; text-align: left; }
    </style>
</head>
<body>
    <h2>AIチャットボット with Bedrock @Made by Kin</h2>
    <form action="/send" method="post">
        <input type="hidden" name="session" value="{{ session }}">
        <input type="text" name="message" style="width:60%" autofocus autocomplete="off">
        <button type="submit">送信</button>
    </form>
    <form method="post" action="/clear">
    <input type="hidden" name="session" value="{{ session }}">
    <button type="submit">Clear</button>
    </form>
    <div>
        {% for item in history %}
            <div class="bubble {{ item.role }}">
                <b>{{ "あなた" if item.role=="user" else "AI" }}:</b> {{ item.message }}
            </div>
        {% endfor %}
    </div>
</body>
</html>

5.2 アプリケーションの起動&ウェアサイト公開テスト

上記作成したら、ウェアサイト公開テストしましょう!

5.2.1 必要な依存パッケージのダウンロード

下記コマンドを、bedrock_chatbot/の配下で実行。
Pythonプロジェクトの必要なライブラリ(依存パッケージ)を一括インストールするコマンドです。

pip3 install -r requirements.txt

5.2.2 サーバの立ち上げ

次に、
下記コマンドを、bedrock_chatbot/の配下で実行。

uvicorn app:app --host 0.0.0.0 --port 8080 &

下記のような結果が出たら、成功です。
app-start-1.png
上記の結果によると、
〇 サーバプロセス(プロセスID:3740)が起動し、アプリケーションの初期化が完了。
〇 8080番ポートでサーバが立ち上がり、外部ネットワークからアクセスできる。

5.2.3 ウェブアクセス確認

では、実際にウェブサイトへアクセスしてみましょう!
http:// <EC2インスタンスのパブリックIP> :8080/ ←こちらにアクセスして、下記のほうを画面が表示されました。

web-test-4.png

メッセージを送信してみたら、
webtest-2.png
ちゃんと返してくれました!

5.3 DockerイメージのPush

ここからはコンテナ化のステップです。
作成したアプリをDockerイメージ化し、AWS ECRへPushします。

5.3.1 ECRの作成

下記の手順でECRを作成しました。

手順内容 参考画像
リポジトリ名に「bedrock-test」を入れ、それ以外の項目はデフォルトとする ecr-1.png

5.3.2 Dockerのインストール

EC2インスタンスにて下記コマンドを実行し、Dockerのインストールします。

sudo dnf update -y
sudo dnf install docker -y
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
newgrp docker

5.3.3 Dockerfileの作成

bedrock_chatbot/ディレクトリに、以下の内容で「Dockerfile」を作成

FROM python:3.11-slim

WORKDIR /app

# 依存パッケージインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリ本体とテンプレートをコピー
COPY app.py .
COPY templates/ ./templates/

EXPOSE 8080

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]

5.3.4 Dockerイメージの作成&Push

下記の手順で Dockerイメージを作成し、Pushしました。

手順内容 参考画像
ECRの画面で、「プッシュコマンドを表示」をクリック ecr-2.png
ECRのコマンドをbedrock_chatbot/ディレクトリの配下で実行 ecr-3.png
コマンド実行結果の一例 ecr-4.pngecr-5.png
ECRの画面でイメージが表示されましたら成功 ecr-6.png

5.4 ECS Fargateでのデプロイ手順

5.4.1 クラスター作成

手順内容 参考画像 
Amazon Elastic Container Service > クラスター > 「クラスターの作成」 claster-01.png
クラスター名を入力し、「AWS Fargate(サーバーレス)」を選択
その他の設定はデフォルトのまま
claster-02.png
クラスターが表示されましたら成功 claster-03.png

5.4.2 タスク定義作成

手順内容 参考画像 
Amazon Elastic Container Service > タスク定義 > 「新しいタスク定義の作成」 taskdifine-01.png
タスク定義ファミリーの名前を入力
起動タイプ:AWS Fargate
ecs-1.png
タスクサイズについて、今回はコスト削減のため最小のものを
CPU:.25 vCPU、メモリ:.5GB
taskdifine-03.png
タスクロールは作成されたECS用のRoleを選択 ecs-5.png
コンテナ詳細にてコンテナ名を入力
イメージURI欄に Step5.3.4 でECRへPushしたイメージのURIを入力
ポートマッピングの欄に「コンテナポート」→ 8080、「プロトコル」→ tcp を入力
ecs-6.png

上記のステップで、タスク定義が正常に作成され、このタスク定義を使用して、サービスをデプロイしたり、タスクを実行したりできます。

5.4.3 サービス作成

ここでは、先ほど作成したタスク定義をもとに、Fargate(サーバーレス)で2つのコンテナを常時稼働させ、ALB(ロードバランサー)でアクセスできるように構成します。

手順内容 参考画像 
クラスターの画面に戻り、サービスの画面で新規サービスを作成する ecs-7.png
タスク定義ファミリー:タスク定義作成のステップで作成したものを選択 ecs-8.png
コンピューティングオプションについては、今回は起動タイプ(Farget)を使用 service-02.png
必要なタスク数:2 service-03.png
今回はプライベートサブネットにECSを置く
ECS用のセキュリティグループを選択し、パブリック IP有効化をOFFにする
ecs-9.png
ロードバランサー(ALB)をここで新規作成
ロードバランサー名を入力し、リスナーも新規作成する
alb-1.pngalb-2.png
ターゲットグループも新規作成
ターゲットグループ名を入力し、その他の設定はデフォルト
alb-3.png
「作成」ボタンを押す
作成に数分かかりそうです。

5.4.5 ALBのサブネット修正

上記「5.4.3 サービス作成」で作成されたALBは、
タスクと同じプライベートサブネットに配置されてしまうので、インタネットからはアクセス不能ですので、
ALBをパブリックサブネットへ移動する必要があります。

手順内容 参考画像 
EC2> ロードバランサー > 作成されたロードバランサー > アクション > サブネットの編集 alb-6.png
右図のように、ECSで作成されたALBは、プライベートサブネットに置かれている alb-4.png
右図のように、サブネットをパブリックサブネットへ変更 alb-5.png

5.4.6 動作確認

最後に実際にアプリが正しく動いているか確認しましょう。

手順内容 参考画像 
ECSに紐づくALBのDNS名でアクセスし、画面が表示されれば成功 last-2.png
送信してみれば、AIとチャットはできます last-3.png
DynamoDBの画面からも、チャットの履歴を確認できる
DynamoDB > テーブル > 作成されたテーブル > テーブルアイテムの探索
last-4.png
チャット履歴はここから確認できる last-5.png

6.まとめ&今後について

今回はAWS BedrockとECS Fargateを使い、サーバーレスな会話AIチャットボットを構築する一連の流れを体験しました。
まだまだ初心者なので、手順や設定が不十分な部分、分かりづらい箇所もあったかもしれません。

もし今後、動作しないポイントや設定ミスなど気づいた点があれば、その都度修正し、より分かりやすい記事にアップデートしていきたいと思います。

6
1
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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?