1. はじめに
こんにちは!Sakitsuです。
前回は、超初心者としてAWS Fargateを使って無事「hello-world」を実行することができました!
今回はさらにステップアップして、AWSのECSとBedrockを活用し、サーバレスで会話AIチャットボットを構築してみたいと思います。
参考資料
下記 みのるん様の記事を参考に、今回のアーキテクチャーを考えました!
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. システム構成と全体像
システムの全体像は下記の図の通りです。
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」 |
![]() |
ポリシーは下記を追加: 「AmazonEC2ContainerRegistryFullAccess」 「AmazonBedrockFullAccess」 「AmazonDynamoDBFullAccess」 |
![]() |
ロール名を入力して作成 | ![]() |
コンテナ用のIAMロール(タスクロール)
下記手順でコンテナ用IAMロール(タスクロール)を作成しました。
ECSからBedrockやDynamoDBへアクセスするために必要です。
手順内容 | 参考画像 |
---|---|
IAM>ロール>ロールを作成 信頼されたエンティティタイプ: AWSサービス ユースケース:Elastic Container Service Task |
![]() |
ポリシーは下記を追加: 「AmazonBedrockFullAccess」 「AmazonDynamoDBFullAccess」 |
![]() |
ロール名: ecs-task-roleを入力して作成 | ![]() |
4.1 AWS Bedrockの有効化と権限設定
なぜ有効化が必要か?
AWSのBedrockサービスは、初期状態では誰でも自由に使えるわけではありません。
利用したいBedrockモデル(例:Anthropic Claude, Amazon Titan, AI21など)ごとに、
「利用申請・有効化」が必要です。
今回は「Claude 3.5 Sonnet」モデルを使いたいので、
下記の手順でアクセス権があるかどうかを確認しました。
有効化手順
手順内容 | 参考画像 |
---|---|
Amazon Bedrock > Bedrock configurations(メニューバーの一番下) > モデルアクセス | ![]() |
「Claude 3.5 Sonne」を選択し、 「リクエスト可能」をクリックし、 「モデルアクセスをリクエスト」でアクセスを申請できる |
![]() |
会社情報などの情報の入力が必要ですので、適当に入力しても大丈夫 | ![]() |
2、3分経ったらアクセス権が付与された | ![]() |
4.2 イメージ作成用EC2の準備
4.2.1 EC2の作成
手順内容 | 参考画像 |
---|---|
EC2 >インスタンス >インスタンスを起動 名前入力:BedRock_Test AMI選択:Amazon Linux 2023 AMI |
![]() |
キーペアを選択(新規作成も可) サブネット:Public Subnet 1a パブリック IP の自動割り当て:有効化 セキュリティグループ:EC2用セキュリティグループ |
![]() |
IAMインスタンスプロフィール:EC2用のIAMロールを選択 その他はデフォルト |
![]() |
4.2.2 必要パッケージのインストール
下記コマンドを実行し、必要なパッケージをインストールします。
「Complete!」と表示されれば成功です。
①まず、既存パッケージをすべて最新に更新します。
sudo yum update -y
②また、「python3」と「git」をインストールも必要です。
sudo yum install python3 git -y
③最後は、boto3のインストールです。
boto3はAWSのサービス(Bedrockなど)をPythonから操作するための公式SDKです。
Bedrock APIをPythonで使うには「boto3」が必須です。
sudo yum install python3-pip -y
pip3 install boto3
4.3 DynamoDBのテーブルの作成
下記手順でDynamoDBテーブルを作成しました。
手順内容 | 参考画像 |
---|---|
DynamoDB >テーブル >テーブルの作成 | ![]() |
下記のように設定: テーブル名:chat_sessions パーティションキー:session_id、文字列 ソートキー-オプション:timestamp、数値 |
![]() |
5. ハンズオン
5.1 Bedrock API を使った会話AIの構築
5.1.1 ディレクトリ構成
EC2にログオンし、下記のような構成でディレクトリを作成しました。
bedrock_chatbot/
├── app.py
├── templates/
│ └── chat.html
├── requirements.txt
5.1.2 requirements作成
fastapi==0.111.0
uvicorn==0.29.0
boto3==1.34.87
jinja2==3.1.3
5.1.3 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作成
<!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 &
下記のような結果が出たら、成功です。
上記の結果によると、
〇 サーバプロセス(プロセスID:3740)が起動し、アプリケーションの初期化が完了。
〇 8080番ポートでサーバが立ち上がり、外部ネットワークからアクセスできる。
5.2.3 ウェブアクセス確認
では、実際にウェブサイトへアクセスしてみましょう!
http:// <EC2インスタンスのパブリックIP> :8080/ ←こちらにアクセスして、下記のほうを画面が表示されました。
5.3 DockerイメージのPush
ここからはコンテナ化のステップです。
作成したアプリをDockerイメージ化し、AWS ECRへPushします。
5.3.1 ECRの作成
下記の手順でECRを作成しました。
手順内容 | 参考画像 |
---|---|
リポジトリ名に「bedrock-test」を入れ、それ以外の項目はデフォルトとする | ![]() |
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のコマンドをbedrock_chatbot/ディレクトリの配下で実行 | ![]() |
コマンド実行結果の一例 |
![]() ![]() |
ECRの画面でイメージが表示されましたら成功 | ![]() |
5.4 ECS Fargateでのデプロイ手順
5.4.1 クラスター作成
手順内容 | 参考画像 |
---|---|
Amazon Elastic Container Service > クラスター > 「クラスターの作成」 | ![]() |
クラスター名を入力し、「AWS Fargate(サーバーレス)」を選択 その他の設定はデフォルトのまま |
![]() |
クラスターが表示されましたら成功 | ![]() |
5.4.2 タスク定義作成
上記のステップで、タスク定義が正常に作成され、このタスク定義を使用して、サービスをデプロイしたり、タスクを実行したりできます。
5.4.3 サービス作成
ここでは、先ほど作成したタスク定義をもとに、Fargate(サーバーレス)で2つのコンテナを常時稼働させ、ALB(ロードバランサー)でアクセスできるように構成します。
5.4.5 ALBのサブネット修正
上記「5.4.3 サービス作成」で作成されたALBは、
タスクと同じプライベートサブネットに配置されてしまうので、インタネットからはアクセス不能ですので、
ALBをパブリックサブネットへ移動する必要があります。
手順内容 | 参考画像 |
---|---|
EC2> ロードバランサー > 作成されたロードバランサー > アクション > サブネットの編集 | ![]() |
右図のように、ECSで作成されたALBは、プライベートサブネットに置かれている | ![]() |
右図のように、サブネットをパブリックサブネットへ変更 | ![]() |
5.4.6 動作確認
最後に実際にアプリが正しく動いているか確認しましょう。
手順内容 | 参考画像 |
---|---|
ECSに紐づくALBのDNS名でアクセスし、画面が表示されれば成功 | ![]() |
送信してみれば、AIとチャットはできます | ![]() |
DynamoDBの画面からも、チャットの履歴を確認できる DynamoDB > テーブル > 作成されたテーブル > テーブルアイテムの探索 |
![]() |
チャット履歴はここから確認できる | ![]() |
6.まとめ&今後について
今回はAWS BedrockとECS Fargateを使い、サーバーレスな会話AIチャットボットを構築する一連の流れを体験しました。
まだまだ初心者なので、手順や設定が不十分な部分、分かりづらい箇所もあったかもしれません。
もし今後、動作しないポイントや設定ミスなど気づいた点があれば、その都度修正し、より分かりやすい記事にアップデートしていきたいと思います。