見出し画像

AWS ECS/Bedrock 素振り


下記記事をなぞってみる

Bedrock有効化

リクエストを出す。会社や利用目的を書かされるが適当に記載する。まとめて複数のモデルに対してリクエストをしておけばOK。

モデルへのアクセス権を依頼

数分でアクセス権が付与された

モデルへのアクセス権付与完了

DynamoDBテーブル作成

bedrock_simple_chat テーブルを作成。
パーティションキーは session_id とする。
キャパシティーの設定では「オンデマンド」を指定。

テーブル作成作業
作成結果

メモ
最初は session_id というカラムをパーティションキーにしていたが、これだとStreamlitからDynamoDBに繋ぎ込んだ際にカラムを認識できなかった。
DynamoDBで SessionId と指定して、 Streamlit 側で session_id と指定するとうまく紐づいた。

ECRレジストリ作成

bedrock-simple-chat というリポジトリを作成

リポジトリ作成作業

アプリ開発

VSCodeで開発。

# rye を使ってPython環境を整備
rye init --script bedrock_simple_chat

# ディレクトリ移動
cd bedrock_simple_chat

# セットアップ
rye sync

rye add boto3==1.34.87
rye add streamlit==1.33.0
rye add langchain==0.2.0
rye add langchain-aws==0.1.4
rye add langchain-community==0.2.0

rye sync

# プロジェクト動作確認
rye run bedrock-simple-chat # アンダーバーで初期化したはずだが、勝手にハイフンになるので注意

main.py

# Pyhton外部モジュールのインポート
import streamlit as st
from langchain_aws import ChatBedrock
from langchain_community.chat_message_histories import DynamoDBChatMessageHistory
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# セッションIDを定義
if "session_id" not in st.session_state:
    st.session_state.session_id = "session_id"

# セッションに会話履歴を定義
if "history" not in st.session_state:
    st.session_state.history = DynamoDBChatMessageHistory(
        table_name="bsc_db", session_id=st.session_state.session_id
    )

# セッションにLangChainの処理チェーンを定義
if "chain" not in st.session_state:
    # プロンプトを定義
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "絵文字入りでフレンドリーに会話してください"),
            MessagesPlaceholder(variable_name="messages"),
            MessagesPlaceholder(variable_name="human_message"),
        ]
    )

    # チャット用LLMを定義
    chat = ChatBedrock(
        model_id="anthropic.claude-3-5-sonnet-20240620-v1:0",
        model_kwargs={"max_tokens": 4000}, streaming=True,
    )

    # チェーンを定義
    st.session_state.chain = prompt | chat

# タイトルを画面表示
st.title("Bedrockと話そう!")

# 履歴クリアボタンを画面表示
if st.button("履歴クリア"):
    st.session_state.history.clear()

# メッセージを画面表示
for message in st.session_state.history.messages:
    with st.chat_message(message.type):
        st.markdown(message.content)

# チャット入力欄を定義
if prompt := st.chat_input("何でも話してね!"):
    # ユーザーの入力をメッセージに追加
    with st.chat_message("user"):
        st.markdown(prompt)

    # モデルの呼び出しと結果の画面表示
    with st.chat_message("assistant"):
        response = st.write_stream(
            st.session_state.chain.stream(
                {
                    "messages": st.session_state.history.messages,
                    "human_message": [HumanMessage(content=prompt)],
                },
                config={"configurable": {"session_id": st.session_state.session_id}},
            )
        )

    # 会話を履歴に追加
    st.session_state.history.add_user_message(prompt)
    st.session_state.history.add_ai_message(response)

streamlit への PATH を通しておく

# 作業ディレクトリ内の、streamlitが存在するパスをPATHに追加
export PATH=$PATH:/home/XXXXXXXX/dev/bedrock_simple_chat/.venv/bin/

動作確認

streamlit run main.py

これだけで動いてしまった…
Bedrock(Claude)に接続もできている。実行ユーザーのAWSアクセスキーを用いて繋ぎ込みを行っているのか。

画面

※上記コマンドを実行した環境・ユーザーについては確かに以下の通りなので、多分それを使っているんだろう。

  • aws cli コマンドがインストール済み

  • aws configure でアクセスキーが設定済み(権限も高め)

Dockerイメージ化

dockerはインストール済みであること。
参考にさせてもらった記事
https://sogo.dev/posts/2023/11/rye-with-docker

DockerFile

ARG PYTHON_VERSION=3.12.5aaaaaa
FROM python:${PYTHON_VERSION}-slim-bookworm

RUN echo "Using Python version: ${PYTHON_VERSION}"

RUN apt update && \
    apt install --no-install-recommends -y curl && \
    apt clean && \
    rm -rf /var/lib/apt/lists/*

ARG USERNAME=ryeuser
RUN useradd ${USERNAME} --create-home
USER ${USERNAME}

WORKDIR /home/${USERNAME}/app

ENV RYE_HOME=/home/${USERNAME}/.rye
ENV PATH=${RYE_HOME}/shims:${PATH}

RUN curl -sSf https://rye.astral.sh/get | RYE_NO_AUTO_INSTALL=1 RYE_INSTALL_OPTION="--yes" bash

RUN --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=requirements.lock,target=requirements.lock \
    --mount=type=bind,source=requirements-dev.lock,target=requirements-dev.lock \
    --mount=type=bind,source=.python-version,target=.python-version \
    --mount=type=bind,source=README.md,target=README.md \
    rye sync --no-dev --no-lock

ENV PATH=.venv/bin/:${PATH}

COPY . .

CMD ["/bin/bash", "-c", ". .venv/bin/activate && streamlit run ./src/bedrock_simple_chat/main.py"]

.dockerignore

.venv

ビルド

docker build . -t bedrock-simple-chat:latest --build-arg PYTHON_VERSION=$(cat .python-version)

コンテナ起動
http://localhost:8501 でアクセスできるようになるはず。

docker run -it -p 8501:8501 bedrock-simple-chat

起動したコンテナ内でターミナルを使ってコマンドを叩きたければ👇️

docker run -it bedrock-simple-chat /bin/bash

ECRへプッシュ

# DockerでECRにログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin アカウントID.dkr.ecr.ap-northeast-1.amazonaws.com

# コンテナへ「最新版」タグを付与
docker tag bedrock-simple-chat:latest 191846745499.dkr.ecr.ap-northeast-1.amazonaws.com/bedrock-simple-chat:latest

# コンテナをECRへプッシュ(登録)
docker push 191846745499.dkr.ecr.ap-northeast-1.amazonaws.com/bedrock-simple-chat:latest

インフラ周りの設定

VPC

コンテナとALBを配置するVPCを作成
ECSにおいてはAZが2つ必要らしい!

VPCエンドポイント作成

VPC内に含むことができないサービスのためにVPCエンドポイントを作成することでAWSネットワーク内で通信を完結できるようにする。

DynamoDB用のエンドポイント

エンドポイントを作成する。「タイプ」は「Gateway」を選択
ルートテーブルはVPC作成時に作成されたprivateサブネットのものを指定

VPCエンドポイントのGateway型とInterface型の違い
こちらを参考に
S3とDynamoDBのみ、Gateway型をサポートしていて、それ以外は概ねInterface型らしい。
すごく簡潔な解説はこちら

  • Gateway型

    • S3, DynamoDBに対してよりセキュアに(インターネット経由せずに)接続したいときに使う

    • VPC外からの接続ができない(VPCの中から、VPC外のS3やDynamoDBに繋ぐときに飲み使われる)

    • ルートテーブルにルートを追加

    • パブリックIPでの接続

    • 課金なし

  • Interface型

    • VPC外から接続可能

    • プライベートIPで接続が可能

    • ENIを利用してVPC内部にサービスのエンドポイントが立ち上がります。そのため、ネットワークACLでローカルの通信のみに制限しても影響を受けることがありません。(?)

    • 課金あり

そもそもルートてブルって…?
こちらを参照

→VPC内(サブネット内)の通信を振り分けるルールの一覧
 外部からのアクセスに対し、VPC内のどのリソースとやり取りさせるかの割り振りを行う。

→ルートテーブルはサブネットに関連付けて利用します。
 1つのサブネットに対して1つのルートテーブルが紐づけられる。


ECR, CloudWatch, Betrock用のエンドポイント

インターフェイズ型でエンドポイントを作成する。下記4種で作成
いずれも、privateサブネットを選択。セキュリティグループはデフォルト

  • com.amazonaws.ap-northeast-1.ecr.api(ECR用)

  • com.amazonaws.ap-northeast-1.ecr.dkr(ECR用)

  • com.amazonaws.ap-northeast-1.logs(CloudWatch用)

  • com.amazonaws.ap-northeast-1.bedrock-runtime(Bedrock用)

作成結果。計6種。

ALB用セキュリティグループ作成

コンテナ用IAMロール作成

ECSのタスクロール用のIAMロールを作成する。許可するポリシーは2つ

  • AmazonBedrockFullAccess

  • AmazonDynamoDBFullAccess

ECSのクラスター&サービス作成

ECSの用語・概念はこちらを参考に

・クラスター
 タスクとサービスを実行する基盤です
・サービス
 ECSクラスター内で、タスクを実行し管理します
・タスク
 タスク定義に基づいてコンテナを起動します

タスク定義

前段作業で作成したロールを他スクロールとして指定

コンテナー
コンテナーのポートは8501を指定。コンテナーイメージはECR上のイメージURIを指定。

クラスター
デフォルトの内容で作成

こちらでも言及されている通り、クラスター作成処理を実行時、エラーになることがある(なった)。
メッセージに記載のリンクからCloudFormationスタックに飛び、「再試行」で今度はクラスター作成成功した。

サービス作成

作成したクラスターの中にサービスを作成する

ネットワーキングでは、作成したVPC、サブネット(プライベート2つ)
パブリックIPはオフ

ロードバランシングでは新規にALBを作成

ALB追加設定

クラスターのサービス作成時に新設したALBに追加設定を行う。
ALBはプライベートサブネットに配置されるので、パブリックサブネットに移動する。

ALBに、前段で作成したセキュリティグループ(自身のPCからのアクセス許可のために作成したもの)を追加する

接続テスト

やったー😄

AWS インフラ周り構成まとめ

理解が甘いと思っているので誤りもありそうだけど、ひとまず自分の理解を記載。
ECS利用の際はマルチAZが必須と書いてあったのでAZは2つ作った。
ALBもこれに合わせて(?)、両AZのパブリックサブネットに配置。
ECSのサービスデプロイ(と合わせて設定するネットワーキング)についてもプライベートサブネット2つを指定。
これって、2つのAZ上にECSサービスを配置して、両ECSサービスにインターネットからアクセスできるようにALBを2つ配置した、という理解。
間違ってる?ECSサービスやALB自体は1つずつだけど、ネットワークが2種類あるというだけ?
こういうレベルで自分は理解できていないんだな…というのがわかっただけでも良しとしよう。


いいなと思ったら応援しよう!