AWSで構築する生成AIサービスとCloudWatch Application Signalsの監視① 環境構築編

:::note info  
[３行まとめ]  
・Bedrock + EKS + S3でRAG構成の生成AIサービス環境を構築  
・CloudWatch Application Signalsを有効化して監視  
・本記事では環境構築手順までを記載  
:::

# はじめに
tsuzuki(@melktzk)です。本記事が初投稿です。  
本記事ではAWSを活用して生成AIサービスを提供する環境を構築しつつ、[2024年6月に正式リリースされたAmazon CloudWatch Application Signals](https://aws.amazon.com/jp/about-aws/whats-new/2024/06/amazon-cloudwatch-application-signals-application-monitoring/)を利用してどのような監視ができるのか、実際に確認してみた内容を紹介していきます。 

生成AIサービスはAWSが公開している[multi-tenant-chatbot-using-rag-with-amazon-bedrock](https://github.com/aws-samples/multi-tenant-chatbot-using-rag-with-amazon-bedrock)を利用したチャットボットWebアプリケーションを構築します。構築の際に実行環境の違いなどに依存した要注意ポイントがいくつかあったので、まずは環境構築編として生成AIサービス環境の構築とCloudWatch Application Signalsを有効化するところまでの手順について紹介します。

# 構成
## 生成AIサービス環境
構築する生成AIサービス環境のインフラレイヤは、

* コンテナ化されたアプリケーションをEKSのクラスター上で管理
* ユーザからのチャット入力情報に対して、S3に格納したドキュメントをベースにBedrockで回答を生成するRAG構成
  * デフォルトではテナントA向けにはSagemakerについてのQAのCSVデータ、テナントB向けにはAmazon EMRについてのQAのCSVデータが保管されている
* チャットの履歴をDynamoDBに保存
* マルチテナントなアプリとしてCognitoでユーザ管理
* NLBで各テナントのコンテナへルーティング

のような構成になっています。全体のイメージは以下の図のとおりです。

![aws-genai-app-architecture01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/786e6d45-bd76-fe13-a062-3f806c5ef477.png)

上記図のとおり、本環境ではCloud9を利用して環境構築及びEKS操作などを実施しています。[新規作成したAWSアカウントなど環境によってはCloud9が利用不可](https://aws.amazon.com/jp/blogs/devops/how-to-migrate-from-aws-cloud9-to-aws-ide-toolkits-or-aws-cloudshell/)であることご注意ください。


また、マルチテナントやRAGとしてのふるまいを実現するためのコンテナ構成・アプリケーションレイヤの概要としては、

* [SaaS Amazon EKS Reference Architecture ](https://github.com/aws-samples/aws-saas-factory-eks-reference-architecture)を基にした、[Istio Service Mesh](https://istio.io/latest/about/service-mesh/)によるマルチテナントのルーティング
* テナントごとにKubernetes Namespace（名前空間）が分離
* チャットボットのPodは、チャット入力インターフェースの"Chatbot"アプリコンテナと、BedrockへのAPIなど実行する"RAG-API"アプリコンテナ（サイドカー）の２つのコンテナで構成
* BedrockのEmbeddingsモデル（デフォルトではamazon.titan-embed-text-v1）を使用して、S3のテキストデータをベクトル埋め込み変換
* [Facebook AI Similarity Search (FAISS)](https://github.com/facebookresearch/faiss)を使用して、インデックス作成とユーザ入力情報の類似性検索の実行
* 扱うデータサイズが小さいデモユースケース向けな環境として、作成したインデックス（バイナリ形式）をコンテナアプリ初期実行時にインメモリにロードする構成で応答速度を向上
* Bedrockのテキスト生成モデル（デフォルトではanthropic.claude-instant-v1）を使用して、ユーザ入力情報＋類似性検索結果の内容からユーザへの応答生成

のような構成になっています。以下の図は、ユーザのアクセス・チャット入力から応答生成までのフローイメージです。

> ![Chat-conversation-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/b2354d40-d7e6-f35d-57ef-2d1a1c18813b.png)
> 画像出典元：https://aws.amazon.com/jp/blogs/containers/build-a-multi-tenant-chatbot-with-rag-using-amazon-bedrock-and-amazon-eks/

詳細は[AWS公式ブログ記事 （Build a multi-tenant chatbot with RAG using Amazon Bedrock and Amazon EKS）](https://aws.amazon.com/jp/blogs/containers/build-a-multi-tenant-chatbot-with-rag-using-amazon-bedrock-and-amazon-eks/)で紹介されているのでそちらを参照ください。

## 監視環境

前述の生成AIサービス環境に対してCloudWatch Application Signalsを有効化して、SLO管理機能や自動計測などについて確認していきます。

![aws-genai-monitoring-architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/02b645b9-82ed-1810-c543-03a51acbf2c5.png)

上記図のとおり、CloudWatch Application Signalsは監視対象からメトリクスやトレース情報を収集して、サービスマップの表示やSLOの作成・管理が可能になります。また、CloudWatch AlarmやCloudWatch Synthetics, CloudWatch RUMなどの機能と連携することも可能ですが、今回の構築では対象外としています。

## クライアント環境

本環境では、以下の図のとおりクライアントからのアクセスがサブドメインで振り分けされる[ドメイン駆動型ルーティング](https://aws.amazon.com/jp/blogs/news/tenant-routing-strategies-for-saas-applications-on-aws/)になっています。

> ![High-level-Architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/52def995-d92d-db7d-b2c9-8b78c6de0f79.png)
> 画像出典元：https://aws.amazon.com/jp/blogs/containers/build-a-multi-tenant-chatbot-with-rag-using-amazon-bedrock-and-amazon-eks/

ただし、あくまでデモユースケース用のサンプルコードであるため、デフォルト構成ではAmazon Route 53によるDNS設定などは含まれていません。したがって、独自ドメインの取得・登録を実施してサンプルコードに記載されているデフォルトドメイン(*.example.com)を修正・環境構築するか、デフォルトドメインのままアクセスするクライアントのローカルhosts情報を書き換えるなどの対応が必要になります。

また、デフォルト構成ではNLBがパブリック公開されてしまう状態となっていました。

上記より本環境では、下記図のように別途VPC上にクライアント用のインスタンス (Windows)を構築してそのhostsを修正してアクセスさせるようにしました。さらにNLBにSecurity GroupでIP制限するように修正、クライアント側のNAT GW経由でのみアクセス可能とするように修正しています。

![aws-genai-client-architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/6219fc06-8401-2154-76d6-c94dba57c65a.png)

NLBのIP制限について、先ほど記載したように本環境のNLBはIstioで制御されており、後から個別にセキュリティグループ設定を追加・修正しても自動で元の状態に戻ってしまうため、その修正方法についても今回記載していきます。

# 環境構築
基本的に本環境は[multi-tenant-chatbot-using-rag-with-amazon-bedrock](https://github.com/aws-samples/multi-tenant-chatbot-using-rag-with-amazon-bedrock)で用意されている各スクリプトと手順に従って構築していきます。注意すべきポイントを先にまとめつつ、構築手順について記載していきます。

## ポイントまとめ
* [Cloud9サイズ](#2-cloud9環境構築)
* スクリプト/fileごとの修正
    * [deploy-els.sh](#deploy-ekssh)
        * AZ設定、インスタンスサイズ
        * EKSバージョン
    * [setup.sh](#setupsh)
        * kubectlバージョン
        * Istioバージョン
    * [configure-istio.sh](#configure-istiosh)
        * Envoyバージョン
    * [proxy-protocol-envoy-filter.yaml](#proxy-protocol-envoy-filteryaml)
        * Istioバージョンアップに伴う修正
* [アクセスエントリ](#13-アクセスエントリ)
* 必要に応じて実施
    * [IP制限](#14-ip制限)
* [環境削除や構築の再実行](#15-削除スクリプト)

## 手順
### 1. Bedrock有効化
本環境では東京リージョンに構築していくため、東京リージョンで利用可能なモデルを有効化します。

![001-bedrock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/a3fd333f-30d8-92de-2be1-686440e5b2b7.png)

デフォルトで必要なのは`anthropic.claude-instant-v1`
と`amazon.titan-embed-text-v1`だけですが、有効化しただけでは料金特にかからないのでここではすべてのモデルを有効化しています。

有効化するモデルのプロバイダーによっては追加で情報入力する必要があるので適宜入力します。

![002-bedrock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/d7d19346-b5c3-10d2-d5ce-0df63a32339d.png)

内容確認してリクエスト実行後、数分程度でアクセス権が付与されます。

![003-bedrock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/d59444fa-8a71-500a-8acf-d26cc902ec4f.png)

### 2. Cloud9環境構築
スクリプトや各種コマンド実行していく環境としてCloud9を構築していきます。

:::note warn
Cloud9を利用できない環境では、[SageMaker StudioやEC2利用など代替案](https://repost.aws/ja/questions/QUiHHHrRawTC6pF5gWOTeNyA/aws-cloud-9-%E3%81%AE%E4%BB%A3%E6%9B%BF%E6%A1%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)を検討する必要がありますが、本記事では対象外としています。
:::

ここではインスタンスタイプ`t3a.small`、プラットフォーム`Amazon Linux 2`を選択、他はデフォルト設定で立ち上げています。

![004-c9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/7bb5e4d2-5fe4-e6fc-1510-04ac75a0c211.png)

デフォルト状態のCloud9だと、アプリのイメージビルドの際にストレージ容量が足りずにエラーとなる場合があります。[AWS公式手順](https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/move-environment-resize.html)を参考にストレージサイズを変更していきます。Cloud9のIDEを開き、以下のスクリプトを作成します。

<details><summary>Cloud9リサイズ用スクリプト</summary>

```sh:resize.sh
#!/bin/bash

# Specify the desired volume size in GiB as a command line argument. If not specified, default to 20 GiB.
SIZE=${1:-20}

# Get the ID of the environment host Amazon EC2 instance.
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
INSTANCEID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/instance-id 2> /dev/null)
REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/placement/region 2> /dev/null)

# Get the ID of the Amazon EBS volume associated with the instance.
VOLUMEID=$(aws ec2 describe-instances \
  --instance-id $INSTANCEID \
  --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \
  --output text \
  --region $REGION)

# Resize the EBS volume.
aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE

# Wait for the resize to finish.
while [ \
  "$(aws ec2 describe-volumes-modifications \
    --volume-id $VOLUMEID \
    --filters Name=modification-state,Values="optimizing","completed" \
    --query "length(VolumesModifications)"\
    --output text)" != "1" ]; do
sleep 1
done

# Check if we're on an NVMe filesystem
if [[ -e "/dev/xvda" && $(readlink -f /dev/xvda) = "/dev/xvda" ]]
then
# Rewrite the partition table so that the partition takes up all the space that it can.
  sudo growpart /dev/xvda 1
# Expand the size of the file system.
# Check if we're on AL2 or AL2023
  STR=$(cat /etc/os-release)
  SUBAL2="VERSION_ID=\"2\""
  SUBAL2023="VERSION_ID=\"2023\""
  if [[ "$STR" == *"$SUBAL2"* || "$STR" == *"$SUBAL2023"* ]]
  then
    sudo xfs_growfs -d /
  else
    sudo resize2fs /dev/xvda1
  fi

else
# Rewrite the partition table so that the partition takes up all the space that it can.
  sudo growpart /dev/nvme0n1 1

# Expand the size of the file system.
# Check if we're on AL2 or AL2023
  STR=$(cat /etc/os-release)
  SUBAL2="VERSION_ID=\"2\""
  SUBAL2023="VERSION_ID=\"2023\""
  if [[ "$STR" == *"$SUBAL2"* || "$STR" == *"$SUBAL2023"* ]]
  then
    sudo xfs_growfs -d /
  else
    sudo resize2fs /dev/nvme0n1p1
  fi
fi

```
</details>


スクリプトを実行してストレージサイズを変更します。ここでは30GiBとしています。

```bash
$ chmod +x resize.sh
$ 
$ ./resize.sh 30

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        954M     0  954M   0% /dev
tmpfs           963M     0  963M   0% /dev/shm
tmpfs           963M  456K  962M   1% /run
tmpfs           963M     0  963M   0% /sys/fs/cgroup
/dev/nvme0n1p1   30G  6.9G   24G  23% /
tmpfs           193M     0  193M   0% /run/user/1000
```

次に、各種EKSのコマンドなど実行可能にするために、Cloud9のインスタンスのロールを修正します。IAMロール管理画面に移動し、EC2用のロールを作成していきます。

![005-c9role.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/08845074-e593-0399-c8cd-96bf358c90c7.png)

許可ポリシーでは`AdministratorAccess`を選択します。

![006-c9role.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/fc07b572-3609-4550-89b8-47d21564016c.png)

ロール名を`Cloud9AdminRole`とし、必要に応じてタグを追加してロール作成します。

![007-c9role.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/8d6d265b-e1d3-99e0-8656-6e966dbf0358.png)

ロール作成が完了したら、Cloud9のデフォルトロールの無効化と作成したロールのアタッチを実施していきます。

Cloud9のIDEに移動し、画面右上の歯車アイコンから設定画面を開きます。
設定画面の`AWS Settings`の`Credentials`の項目を左にスライドして`AWS Managed Temporary Credentials`をオフにします。これでデフォルトのロールの無効化ができます。

![008-cloud9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/5c1b6fed-bc8c-9bf5-4bf5-9914d8e7000e.png)

次にEC2インスタンスの管理画面に移動して、Cloud9のインスタンスを右クリックし、メニューから`Security`>`IAMロールの変更`をクリックします。

![009-cloud9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/039c8738-2b7f-0e88-5f12-a15256f4f8ef.png)

作成したロール`Cloud9AdminRole`を選択して更新します。

![010-cloud9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/0864ddee-b6ff-1e60-6a2b-ed09584b8d01.png)

### 3. スクリプト取得

Cloud9のIDEに移動して、ターミナルからGitクローンを実施してコードを取得します。

```bash
$ git clone https://github.com/aws-samples/multi-tenant-chatbot-using-rag-with-amazon-bedrock.git
Cloning into 'multi-tenant-chatbot-using-rag-with-amazon-bedrock'...
remote: Enumerating objects: 79, done.
remote: Counting objects: 100% (79/79), done.
remote: Compressing objects: 100% (69/69), done.
remote: Total 79 (delta 15), reused 60 (delta 7), pack-reused 0 (from 0)
Receiving objects: 100% (79/79), 93.87 KiB | 5.21 MiB/s, done.
Resolving deltas: 100% (15/15), done.
$ 
$ cd multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ 
```

### 4. スクリプト修正

取得した各ファイルを修正していきます。

#### ①deploy-eks.sh

EKSクラスターをデプロイするスクリプト`deploy-eks.sh`の、以下の箇所を修正します。
* EKSバージョン
* EKSクラスターのAZ設定、インスタンスサイズ設定

デフォルトではEKSのバージョンが`1.27`となっており、[標準サポート外となってしまっている](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/kubernetes-versions.html)ため変更します。ここでは、`1.30`としています。

```diff_shell
- export EKS_CLUSTER_VERSION="1.27"
+ export EKS_CLUSTER_VERSION="1.30"
```

クラスターのAZを指定する箇所で、アカウント依存でAZのbが利用できない場合があるので削除します。

```diff_shell
- availabilityZones: ["${AWS_REGION}a", "${AWS_REGION}b", "${AWS_REGION}c"]
+ availabilityZones: ["${AWS_REGION}a", "${AWS_REGION}c"]
```

ノードとしてデプロイするEC2インスタンスのインスタンスタイプが指定されている箇所で、東京リージョンではAZ依存で`t3a`が利用できない場合があるのでこれも削除します。

```diff_shell
-   instanceTypes: ["t3a.medium", "t3.medium"]
+   instanceTypes: ["t3.medium"]
```

<details><summary>修正後のdeploy-eks.sh全体は以下のとおり</summary>

```sh:multi-tenant-chatbot-using-rag-with-amazon-bedrock/deploy-eks.sh
#!/usr/bin/bash
source ~/.bash_profile

test -n "$EKS_CLUSTER_NAME" && echo EKS_CLUSTER_NAME is "$EKS_CLUSTER_NAME" || echo EKS_CLUSTER_NAME is not set

export EKS_CLUSTER_VERSION="1.30"
echo "Deploying Cluster ${EKS_CLUSTER_NAME} with EKS ${EKS_CLUSTER_VERSION}"

cat << EOF > ${YAML_PATH}/cluster-config.yaml
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${EKS_CLUSTER_NAME}
  region: ${AWS_REGION}
  version: "${EKS_CLUSTER_VERSION}"
iam:
  withOIDC: true
  serviceAccounts:
  - metadata:
      name: aws-load-balancer-controller
      namespace: kube-system
    wellKnownPolicies:
      awsLoadBalancerController: true
availabilityZones: ["${AWS_REGION}a", "${AWS_REGION}c"]
managedNodeGroups:
- name: nodegroup
  desiredCapacity: 3
  instanceTypes: ["t3.medium"]
  volumeEncrypted: true
  ssh:
    allow: false
cloudWatch:
  clusterLogging:
    enableTypes: ["*"]
secretsEncryption:
  keyARN: ${MASTER_ARN}
EOF

eksctl create cluster -f ${YAML_PATH}/cluster-config.yaml

aws eks update-kubeconfig --name=${EKS_CLUSTER_NAME}

# Associate an OIDC provider with the EKS Cluster
echo "Associating an OIDC provider with the EKS Cluster"
eksctl utils associate-iam-oidc-provider \
--region=${AWS_REGION} \
--cluster=${EKS_CLUSTER_NAME} \
--approve

export OIDC_PROVIDER=$(aws eks describe-cluster \
                      --name ${EKS_CLUSTER_NAME} \
                      --query "cluster.identity.oidc.issuer" \
                      --output text)

echo "Installing AWS Load Balancer Controller"

kubectl apply -k "github.com/aws/eks-charts/stable/aws-load-balancer-controller/crds?ref=master"

helm repo add eks https://aws.github.io/eks-charts

helm repo update

# Setting AWS Load Balancer Controller Version
export VPC_ID=$(aws eks describe-cluster \
                --name ${EKS_CLUSTER_NAME} \
                --query "cluster.resourcesVpcConfig.vpcId" \
                --output text)

helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=${EKS_CLUSTER_NAME} \
  --set serviceAccount.create=false \
  --set region=${AWS_REGION} \
  --set vpcId=${VPC_ID} \
  --set serviceAccount.name=aws-load-balancer-controller

kubectl -n kube-system rollout status deployment aws-load-balancer-controller

export OIDC_PROVIDER=$(aws eks describe-cluster \
                      --name ${EKS_CLUSTER_NAME} \
                      --query "cluster.identity.oidc.issuer" \
                      --output text)

export OIDC_ID=$(echo $OIDC_PROVIDER | awk -F/ '{print $NF}')

echo "Creating S3 Access Role in IAM"
export S3_ACCESS_ROLE=${EKS_CLUSTER_NAME}-s3-access-role-${RANDOM_STRING}
export ENVOY_IRSA=$(
envsubst < iam/s3-access-role-trust-policy.json | \
xargs -0 -I {} aws iam create-role \
              --role-name ${S3_ACCESS_ROLE} \
              --assume-role-policy-document {} \
              --query 'Role.Arn' \
              --output text
)
echo "Attaching S3 Bucket policy to S3 Access Role"
aws iam attach-role-policy \
    --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/s3-envoy-config-access-policy-${RANDOM_STRING} \
    --role-name ${S3_ACCESS_ROLE}

echo ${ENVOY_IRSA}
echo "export ENVOY_IRSA=${ENVOY_IRSA}" \
    | tee -a ~/.bash_profile

TENANTS="tenanta tenantb"
for t in $TENANTS
do
  export NAMESPACE="${t}-ns"
  export SA_NAME="${t}-sa"

  echo "Creating DynamoDB / Bedrock Access Role in IAM"
  export CHATBOT_ACCESS_ROLE=${EKS_CLUSTER_NAME}-${t}-chatbot-access-role-${RANDOM_STRING}
  export CHATBOT_IRSA=$(
  envsubst < iam/chatbot-access-role-trust-policy.json | \
  xargs -0 -I {} aws iam create-role \
                --role-name ${CHATBOT_ACCESS_ROLE} \
                --assume-role-policy-document {} \
                --query 'Role.Arn' \
                --output text
  )
  echo "Attaching S3 Bucket and DynamoDB policy to Chatbot Access Role"
  aws iam attach-role-policy \
      --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/s3-contextual-data-access-policy-${t}-${RANDOM_STRING} \
      --role-name ${CHATBOT_ACCESS_ROLE}
      
  aws iam attach-role-policy \
      --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/dynamodb-access-policy-${t}-${RANDOM_STRING} \
      --role-name ${CHATBOT_ACCESS_ROLE}
done

```
</details>

#### ②setup.sh

初期設定用スクリプト`setup.sh`の、以下の箇所を修正します。
* kubectlバージョン
* Istioバージョン

:::note warn
利用するBedrockのモデルを変更したい場合もここでsetup.shの修正が必要です。本記事では対象外としていますが、環境構築後に変更したくなった場合はsetup.shとは別のアプリケーション用コードを修正してコンテナ再デプロイなどの作業が必要になるため注意してください。
:::

[①deploy-eks.sh](#deploy-ekssh)で変更したEKSバージョンに合わせて、kubectlバージョンも修正します。EKSバージョンとの対応は[kubectl と eksctl のセットアップ](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/install-kubectl.html)を参照。

```diff_shell
- export KUBECTL_VERSION="1.27.1/2023-04-19"
+ export KUBECTL_VERSION="1.30.0/2024-05-12"
```

同様に[①deploy-eks.sh](#deploy-ekssh)で変更したEKSバージョンに合わせて、Istioのバージョンも修正します。EKSバージョンとの対応は[Support status of Istio releases](https://istio.io/latest/docs/releases/supported-releases/#support-status-of-istio-releases)を参照。

```diff_shell
- export ISTIO_VERSION="1.18.3"
+ export ISTIO_VERSION="1.22.3"
```

<details><summary>修正後のsetup.shの全体は以下のとおり</summary>

```sh:multi-tenant-chatbot-using-rag-with-amazon-bedrock/setup.sh
#!/usr/bin/env bash
. ~/.bash_profile

export TEXT2TEXT_MODEL_ID=anthropic.claude-instant-v1
export EMBEDDING_MODEL_ID=amazon.titan-embed-text-v1
export BEDROCK_SERVICE=bedrock-runtime
echo "export TEXT2TEXT_MODEL_ID=${TEXT2TEXT_MODEL_ID}" \
    | tee -a ~/.bash_profile
echo "export EMBEDDING_MODEL_ID=${EMBEDDING_MODEL_ID}" \
    | tee -a ~/.bash_profile
echo "export BEDROCK_SERVICE=${BEDROCK_SERVICE}" \
    | tee -a ~/.bash_profile

export KUBECTL_VERSION="1.30.0/2024-05-12"

if [ "x${KUBECTL_VERSION}" == "x" ]
then
  echo "################"
  echo "Please specify a version for kubectl"
  echo "################"
  exit
fi

export EKS_CLUSTER_NAME="multitenant-chatapp"

if [ "x${EKS_CLUSTER_NAME}" == "x" ]
then
  echo "################"
  echo "Please specify a name for the EKS Cluster"
  echo "################"
  exit
fi

echo "export EKS_CLUSTER_NAME=${EKS_CLUSTER_NAME}" | tee -a ~/.bash_profile

export ISTIO_VERSION="1.22.3"

if [ "x${ISTIO_VERSION}" == "x" ]
then
  echo "################"
  echo "Please specify a version for Istio"
  echo "################"
  exit
fi

echo "export ISTIO_VERSION=${ISTIO_VERSION}" \
    | tee -a ~/.bash_profile

echo "Installing helper tools"
sudo yum -q -y install jq bash-completion
sudo amazon-linux-extras install -q -y python3.8 2>/dev/null >/dev/null
python3.8 -m pip install -q -q --user botocore
python3.8 -m pip install -q -q --user boto3

echo "Uninstalling AWS CLI 1.x"
sudo pip uninstall awscli -y

echo "Installing AWS CLI 2.x"
curl --silent --no-progress-meter \
    "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \
    -o "awscliv2.zip"
unzip -qq awscliv2.zip
sudo ./aws/install --update
PATH=/usr/local/bin:$PATH
/usr/local/bin/aws --version
rm -rf aws awscliv2.zip

CLOUD9_EC2_ROLE="Cloud9AdminRole"

AWS=$(which aws)

echo "---------------------------"
${AWS} sts get-caller-identity --query Arn | \
  grep ${CLOUD9_EC2_ROLE} -q && echo "IAM role valid. You can continue setting up the EKS Cluster." || \
  echo "IAM role NOT valid. Do not proceed with creating the EKS Cluster or you won't be able to authenticate.
  Ensure you assigned the role to your EC2 instance as detailed in the README.md"
echo "---------------------------"

export AWS_REGION=$(curl --silent --no-progress-meter \
                    http://169.254.169.254/latest/dynamic/instance-identity/document \
                    | jq -r '.region')
export AWS_DEFAULT_REGION=$AWS_REGION

export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)

export RANDOM_STRING=$(cat /dev/urandom \
                | tr -dc '[:alpha:]' \
                | fold -w ${1:-20} | head -n 1 \
                | cut -c 1-8 \
                | tr '[:upper:]' '[:lower:]')

echo "export RANDOM_STRING=${RANDOM_STRING}" | tee -a ~/.bash_profile

echo "Installing kubectl"
sudo curl --silent --no-progress-meter --location -o /usr/local/bin/kubectl \
  https://s3.us-west-2.amazonaws.com/amazon-eks/${KUBECTL_VERSION}/bin/linux/amd64/kubectl

sudo chmod +x /usr/local/bin/kubectl

kubectl version --client=true

echo "Installing bash completion for kubectl"
kubectl completion bash >>  ~/.bash_completion
. /etc/profile.d/bash_completion.sh
. ~/.bash_completion

echo "Installing eksctl"
curl --silent --no-progress-meter \
    --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" \
    | tar xz -C /tmp

sudo mv -v /tmp/eksctl /usr/local/bin

echo "eksctl Version: $(eksctl version)"

echo "Installing bash completion for eksctl"
eksctl completion bash >> ~/.bash_completion
. /etc/profile.d/bash_completion.sh
. ~/.bash_completion

test -n "$AWS_REGION" && echo AWS_REGION is "$AWS_REGION" || echo AWS_REGION is not set
echo "export ACCOUNT_ID=${ACCOUNT_ID}" | tee -a ~/.bash_profile
echo "export AWS_REGION=${AWS_REGION}" | tee -a ~/.bash_profile
echo "export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}" | tee -a ~/.bash_profile
aws configure set default.region ${AWS_REGION}
aws configure get default.region

export YAML_PATH=yaml
echo "export YAML_PATH=yaml" | tee -a ~/.bash_profile
[ -d ${YAML_PATH} ] || mkdir ${YAML_PATH}

export ENVOY_CONFIG_BUCKET="envoy-config-${RANDOM_STRING}"
aws s3 mb s3://${ENVOY_CONFIG_BUCKET}

if [[ $? -eq 0 ]]
then
    echo "export ENVOY_CONFIG_BUCKET=${ENVOY_CONFIG_BUCKET}" \
        | tee -a ~/.bash_profile
fi

# Creating S3 Bucket Policy for Envoy Dynamic Configuration Files
echo "Creating S3 Bucket Policy for Envoy Dynamic Configuration Files"
envsubst < iam/s3-envoy-config-access-policy.json | \
xargs -0 -I {} aws iam create-policy \
              --policy-name s3-envoy-config-access-policy-${RANDOM_STRING} \
              --policy-document {}

# Creating DynamoDB and Bedrock access policy for chatbot app
TENANTS="tenanta tenantb"
for t in $TENANTS
do
  export TENANT=${t}
  
  echo "Creating Contextual Data S3 Bucket for ${t}"
  aws s3 mb s3://contextual-data-${t}-${RANDOM_STRING}
  
  if [ "${t}" == "tenanta" ]
  then
      aws s3 cp data/Amazon_SageMaker_FAQs.csv s3://contextual-data-${t}-${RANDOM_STRING}
  elif [ "${t}" == "tenantb" ]
  then
      aws s3 cp data/Amazon_EMR_FAQs.csv s3://contextual-data-${t}-${RANDOM_STRING}
  fi
  
  echo "S3 access policy for ${t}"
  envsubst < iam/s3-contextual-data-access-policy.json | \
  xargs -0 -I {} aws iam create-policy \
                --policy-name s3-contextual-data-access-policy-${t}-${RANDOM_STRING} \
                --policy-document {}

  echo "DynamoDB and Bedrock access policy for ${t} chatbot app"
  envsubst < iam/dynamodb-access-policy.json | \
  xargs -0 -I {} aws iam create-policy \
                --policy-name dynamodb-access-policy-${t}-${RANDOM_STRING} \
                --policy-document {}
done

# Ingest Data to FAISS Index
source ~/.bash_profile
pip3.8 install -q -q --user -r data_ingestion_to_vectordb/requirements.txt
python3.8 data_ingestion_to_vectordb/data_ingestion_to_vectordb.py

echo "Creating Chatbot ECR Repository"
export ECR_REPO_CHATBOT=$(aws ecr create-repository \
  --repository-name ${EKS_CLUSTER_NAME}-${RANDOM_STRING}-chatbot \
  --encryption-configuration encryptionType=KMS)
export REPO_URI_CHATBOT=$(echo ${ECR_REPO_CHATBOT}|jq -r '.repository.repositoryUri')
export REPO_CHATBOT=$(echo ${ECR_REPO_CHATBOT}|jq -r '.repository.repositoryName')

echo "Creating rag-api ECR Repository"
export ECR_REPO_RAGAPI=$(aws ecr create-repository \
  --repository-name ${EKS_CLUSTER_NAME}-${RANDOM_STRING}-rag-api \
  --encryption-configuration encryptionType=KMS)
export REPO_URI_RAGAPI=$(echo ${ECR_REPO_RAGAPI}|jq -r '.repository.repositoryUri')
export REPO_RAGAPI=$(echo ${ECR_REPO_RAGAPI}|jq -r '.repository.repositoryName')

echo "export ECR_REPO_CHATBOT=${REPO_CHATBOT}" | tee -a ~/.bash_profile
echo "export REPO_URI_CHATBOT=${REPO_URI_CHATBOT}" | tee -a ~/.bash_profile
echo "export ECR_REPO_RAGAPI=${REPO_RAGAPI}" | tee -a ~/.bash_profile
echo "export REPO_URI_RAGAPI=${REPO_URI_RAGAPI}" | tee -a ~/.bash_profile

echo "Building Chatbot and RAG-API Images"
sh image-build/build-chatbot-image.sh
docker rmi -f $(docker images -a -q) &> /dev/null
sh image-build/build-rag-api-image.sh
docker rmi -f $(docker images -a -q) &> /dev/null

echo "Installing helm"
curl --no-progress-meter \
    -sSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

helm version --template='Version: {{.Version}}'; echo

rm -vf ${HOME}/.aws/credentials

echo "Generating a new key"
ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa <<< y

export EC2_KEY_NAME=${EKS_CLUSTER_NAME}-${RANDOM_STRING}
aws ec2 import-key-pair --key-name ${EC2_KEY_NAME} --public-key-material fileb://~/.ssh/id_rsa.pub
echo "export EC2_KEY_NAME=${EC2_KEY_NAME}" | tee -a ~/.bash_profile

echo "Creating KMS Key and Alias"
export KMS_KEY_ALIAS=${EKS_CLUSTER_NAME}-${RANDOM_STRING}
aws kms create-alias --alias-name alias/${KMS_KEY_ALIAS} \
    --target-key-id $(aws kms create-key --query KeyMetadata.Arn --output text)
echo "export KMS_KEY_ALIAS=${KMS_KEY_ALIAS}" | tee -a ~/.bash_profile

export MASTER_ARN=$(aws kms describe-key --key-id alias/${KMS_KEY_ALIAS} \
    --query KeyMetadata.Arn --output text)
echo "export MASTER_ARN=${MASTER_ARN}" | tee -a ~/.bash_profile

```
</details>

#### ③configure-istio.sh

Istioの設定スクリプト`configure-istio.sh`の、以下の箇所を修正します。
* Envoyバージョン

[②setup.sh](#setupsh)で変更したIstioのバージョンに合わせてEnvoyのDockerイメージバージョン設定を修正します。対応するバージョンは[Supported Envoy Versions](https://istio.io/latest/docs/releases/supported-releases/#supported-envoy-versions)と[envoyproxy/envoy](https://hub.docker.com/r/envoyproxy/envoy)を参照してください。

```diff_shell
-         image: envoyproxy/envoy:v1.27.0
+         image: envoyproxy/envoy:v1.31-latest
```

<details><summary>修正後のconfigure-istio.shの全体は以下のとおり</summary>

```sh:multi-tenant-chatbot-using-rag-with-amazon-bedrock/configure-istio.sh
#!/usr/bin/bash
. ~/.bash_profile

# Directory for generated certs
mkdir certs

echo "Creating Root CA Cert and Key"
openssl req -x509 -sha256 -nodes -days 365 \
  -newkey rsa:2048 \
  -subj '/O=Cluster 1 CA/CN=ca.example.com' \
  -keyout certs/ca_example_com.key \
  -out certs/ca_example_com.crt

echo "Creating Cert and Key for Istio Ingress Gateway"
openssl req \
  -newkey rsa:2048 -nodes \
  -subj "/O=Cluster 1/CN=*.example.com" \
  -keyout certs/example_com.key \
  -out certs/example_com.csr

openssl x509 -req -days 365 \
  -set_serial 0 \
  -CA certs/ca_example_com.crt \
  -CAkey certs/ca_example_com.key \
  -in certs/example_com.csr \
  -out certs/example_com.crt

echo "Creating TLS secret for Istio Ingress Gateway"
kubectl create -n istio-ingress secret generic credentials \
  --from-file=tls.key=certs/example_com.key \
  --from-file=tls.crt=certs/example_com.crt

echo "Creating namespaces"
kubectl create namespace llm-demo-gateway-ns
kubectl create namespace envoy-reverse-proxy-ns
kubectl create namespace tenanta-oidc-proxy-ns
kubectl create namespace tenantb-oidc-proxy-ns
kubectl create namespace tenanta-ns
kubectl create namespace tenantb-ns

echo "Enabling sidecar injection in namespaces"
kubectl label namespace envoy-reverse-proxy-ns istio-injection=enabled
kubectl label namespace tenanta-oidc-proxy-ns istio-injection=enabled
kubectl label namespace tenantb-oidc-proxy-ns istio-injection=enabled
kubectl label namespace tenanta-ns istio-injection=enabled
kubectl label namespace tenantb-ns istio-injection=enabled

kubectl get namespace -L istio-injection

echo "Applying STRICT mTLS Policy on all application namespaces"
cat << EOF > ${YAML_PATH}/strictmtls.yaml
---
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: strict-mtls
spec:
  mtls:
    mode: STRICT
EOF
kubectl -n tenanta-ns apply -f ${YAML_PATH}/strictmtls.yaml
kubectl -n tenantb-ns apply -f ${YAML_PATH}/strictmtls.yaml
kubectl -n envoy-reverse-proxy-ns apply -f ${YAML_PATH}/strictmtls.yaml

kubectl -n tenanta-ns get PeerAuthentication
kubectl -n tenantb-ns get PeerAuthentication
kubectl -n envoy-reverse-proxy-ns get PeerAuthentication

rm -rf ${YAML_PATH}/strictmtls.yaml

echo "Deploying Istio Gateway resource"
cat << EOF > ${YAML_PATH}/llm-demo-gateway.yaml
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: llm-demo-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: credentials
        minProtocolVersion: TLSV1_2
        maxProtocolVersion: TLSV1_3
      hosts:
        - 'tenanta-ns/*'
        - 'tenantb-ns/*'
EOF
kubectl -n llm-demo-gateway-ns apply -f ${YAML_PATH}/llm-demo-gateway.yaml

rm -rf ${YAML_PATH}/llm-demo-gateway.yaml

# Copying Envoy Dynamic Config files to S3 bucket
echo "Copying Envoy Dynamic Config files to S3 bucket"
aws s3 cp envoy-config/envoy.yaml s3://${ENVOY_CONFIG_BUCKET}
aws s3 cp envoy-config/envoy-lds.yaml s3://${ENVOY_CONFIG_BUCKET}
aws s3 cp envoy-config/envoy-cds.yaml s3://${ENVOY_CONFIG_BUCKET}

echo "Deploying Envoy Reverse Proxy"
export DOLLAR='$'
cat << EOF > ${YAML_PATH}/envoy-reverse-proxy.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::${ACCOUNT_ID}:role/${EKS_CLUSTER_NAME}-s3-access-role-${RANDOM_STRING}
  name: envoy-reverse-proxy-sa
---
apiVersion: v1
kind: Service
metadata:
  name: envoy-reverse-proxy
  labels:
    app: envoy-reverse-proxy
spec:
  selector:
    app: envoy-reverse-proxy
  ports:
  - port: 80
    name: http
    targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: envoy-reverse-proxy
  labels:
    app: envoy-reverse-proxy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: envoy-reverse-proxy
  minReadySeconds: 60
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app: envoy-reverse-proxy
      annotations:
        eks.amazonaws.com/skip-containers: "envoy-reverse-proxy"
    spec:
      serviceAccountName: envoy-reverse-proxy-sa
      initContainers:
      - name: envoy-reverse-proxy-bootstrap
        image: public.ecr.aws/aws-cli/aws-cli:2.13.6
        volumeMounts:
        - name: envoy-config-volume
          mountPath: /config/envoy
        command: ["/bin/sh", "-c"]
        args:
          - aws s3 cp s3://${DOLLAR}{ENVOY_CONFIG_S3_BUCKET}/envoy.yaml /config/envoy;
            aws s3 cp s3://${DOLLAR}{ENVOY_CONFIG_S3_BUCKET}/envoy-lds.yaml /config/envoy;
            aws s3 cp s3://${DOLLAR}{ENVOY_CONFIG_S3_BUCKET}/envoy-cds.yaml /config/envoy;
        env:
        - name: ENVOY_CONFIG_S3_BUCKET
          value: ${ENVOY_CONFIG_BUCKET}
      containers:
      - name: envoy-reverse-proxy
        image: envoyproxy/envoy:v1.31-latest
        args: ["-c", "/config/envoy/envoy.yaml"]
        imagePullPolicy: Always
        ports:
          - containerPort: 8000
        volumeMounts:
        - name: envoy-config-volume
          mountPath: /config/envoy
      volumes:
      - name: envoy-config-volume
        emptyDir: {}
EOF
kubectl -n envoy-reverse-proxy-ns apply -f ${YAML_PATH}/envoy-reverse-proxy.yaml

rm -rf ${YAML_PATH}/envoy-reverse-proxy.yaml

echo "Adding Istio External Authorization Provider"
cat << EOF > ${YAML_PATH}/auth-provider.yaml
---
apiVersion: v1
data:
  mesh: |-
    accessLogFile: /dev/stdout
    defaultConfig:
      discoveryAddress: istiod.istio-system.svc:15012
      proxyMetadata: {}
      tracing:
        zipkin:
          address: zipkin.istio-system:9411
    enablePrometheusMerge: true
    rootNamespace: istio-system
    trustDomain: cluster.local
    extensionProviders:
    - name: rev-proxy
      envoyExtAuthzHttp:
        service: envoy-reverse-proxy.envoy-reverse-proxy-ns.svc.cluster.local
        port: "80"
        timeout: 1.5s
        includeHeadersInCheck: ["authorization", "cookie"]
        headersToUpstreamOnAllow: ["authorization", "path", "x-auth-request-user", "x-auth-request-email"]
        headersToDownstreamOnDeny: ["content-type", "set-cookie"]
EOF
kubectl -n istio-system patch configmap istio --patch "$(cat ${YAML_PATH}/auth-provider.yaml)"
kubectl rollout restart deployment/istiod -n istio-system

rm -rf ${YAML_PATH}/auth-provider.yaml

echo "Configuring AuthorizationPolicy on Istio Ingress Gateway"
kubectl apply -f ${YAML_PATH}/chatbot-auth-policy.yaml
rm -rf ${YAML_PATH}/chatbot-auth-policy.yaml
```
</details>

#### ④proxy-protocol-envoy-filter.yaml

Istio Ingress Gatewayの設定`istio-proxy-v2-config/proxy-protocol-envoy-filter.yaml`の、以下を修正します。
* Istioバージョンアップに伴う修正

[Istioの1.18.xから1.19.xへのバージョンアップで下位互換性のない変更](https://istio.io/latest/news/releases/1.19.x/announcing-1.19/upgrade-notes/)がされています。そのため、Istio 1.18.xが前提の元設定ではNLBとの疎通にエラーが発生します。本エラーに対応するために[istioのissues#39868](https://github.com/istio/istio/issues/39868)と[Envoy Filter](https://istio.io/latest/docs/reference/config/networking/envoy-filter/)の情報を参考に、以下のように修正します。

```diff_yaml
-         listener_filters:
-         - name: envoy.filters.listener.proxy_protocol
-         - name: envoy.filters.listener.tls_inspector
+         listener_filters:
+         - name: envoy.filters.listener.proxy_protocol
+           typed_config:
+             '@type': type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol
+         - name: envoy.filters.listener.tls_inspector
+           typed_config:
+             '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
```

<details><summary>修正後のproxy-protocol-envoy-filter.yamlの全体は以下のとおり</summary>

```yaml:multi-tenant-chatbot-using-rag-with-amazon-bedrock/istio-proxy-v2-config/proxy-protocol-envoy-filter.yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: proxy-protocol
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
  - applyTo: LISTENER
    patch:
      operation: MERGE
      value:
        listener_filters:
        - name: envoy.filters.listener.proxy_protocol
          typed_config:
            '@type': type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol
        - name: envoy.filters.listener.tls_inspector
          typed_config:
            '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
      
```
</details>

以上で設定の修正は完了です。

### 5. setupスクリプトの実行

ここからは、各スクリプトを実行して実際に環境構築を実施していきます。Cloud9のIDEのターミナルで以下のように実行します。

```bash
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x setup.sh
$ ./setup.sh
```
ここでは、環境変数の定義や依存関係ライブラリの取得、各リソース用ポリシー作成、S3バケット作成、ECRリポジトリ作成、アプリ用のイメージビルドなど実施されます。

<details><summary>setup.shの実行ログ例（アカウントIDなどはマスク）</summary>

```sh
$ ./setup.sh
export TEXT2TEXT_MODEL_ID=anthropic.claude-instant-v1
export EMBEDDING_MODEL_ID=amazon.titan-embed-text-v1
export BEDROCK_SERVICE=bedrock-runtime
export EKS_CLUSTER_NAME=multitenant-chatapp
export ISTIO_VERSION=1.22.3
Installing helper tools
Package jq-1.5-1.amzn2.0.2.x86_64 already installed and latest version
Package 1:bash-completion-2.1-6.amzn2.noarch already installed and latest version
Uninstalling AWS CLI 1.x
WARNING: Skipping awscli as it is not installed.
Installing AWS CLI 2.x
You can now run: /usr/local/bin/aws --version
aws-cli/2.18.3 Python/3.12.6 Linux/5.10.226-214.879.amzn2.x86_64 exe/x86_64.amzn.2
---------------------------
IAM role valid. You can continue setting up the EKS Cluster.
---------------------------
export RANDOM_STRING=XXXXXXXXX
Installing kubectl
Client Version: v1.30.0-eks-036c24b
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Installing bash completion for kubectl
Installing eksctl
‘/tmp/eksctl’ -> ‘/usr/local/bin/eksctl’
eksctl Version: 0.191.0
Installing bash completion for eksctl
AWS_REGION is ap-northeast-1
export ACCOUNT_ID=XXXXXXXXXXX
export AWS_REGION=ap-northeast-1
export AWS_DEFAULT_REGION=ap-northeast-1
ap-northeast-1
export YAML_PATH=yaml
make_bucket: envoy-config-XXXXXXXXX
export ENVOY_CONFIG_BUCKET=envoy-config-XXXXXXXXX
Creating S3 Bucket Policy for Envoy Dynamic Configuration Files
{
    "Policy": {
        "PolicyName": "s3-envoy-config-access-policy-XXXXXXXXX",
        "PolicyId": "ANPAW3MECN2KS3CVJ6QYV",
        "Arn": "arn:aws:iam::XXXXXXXXXXX:policy/s3-envoy-config-access-policy-XXXXXXXXX",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2024-10-10T03:42:35+00:00",
        "UpdateDate": "2024-10-10T03:42:35+00:00"
    }
}
Creating Contextual Data S3 Bucket for tenanta
make_bucket: contextual-data-tenanta-XXXXXXXXX
upload: data/Amazon_SageMaker_FAQs.csv to s3://contextual-data-tenanta-XXXXXXXXX/Amazon_SageMaker_FAQs.csv
S3 access policy for tenanta
{
    "Policy": {
        "PolicyName": "s3-contextual-data-access-policy-tenanta-XXXXXXXXX",
        "PolicyId": "ANPAW3MECN2KROO6LLHW5",
        "Arn": "arn:aws:iam::XXXXXXXXXXX:policy/s3-contextual-data-access-policy-tenanta-XXXXXXXXX",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2024-10-10T03:42:40+00:00",
        "UpdateDate": "2024-10-10T03:42:40+00:00"
    }
}
DynamoDB and Bedrock access policy for tenanta chatbot app
{
    "Policy": {
        "PolicyName": "dynamodb-access-policy-tenanta-XXXXXXXXX",
        "PolicyId": "ANPAW3MECN2K7BF3377KO",
        "Arn": "arn:aws:iam::XXXXXXXXXXX:policy/dynamodb-access-policy-tenanta-XXXXXXXXX",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2024-10-10T03:42:42+00:00",
        "UpdateDate": "2024-10-10T03:42:42+00:00"
    }
}
Creating Contextual Data S3 Bucket for tenantb
make_bucket: contextual-data-tenantb-XXXXXXXXX
upload: data/Amazon_EMR_FAQs.csv to s3://contextual-data-tenantb-XXXXXXXXX/Amazon_EMR_FAQs.csv
S3 access policy for tenantb
{
    "Policy": {
        "PolicyName": "s3-contextual-data-access-policy-tenantb-XXXXXXXXX",
        "PolicyId": "ANPAW3MECN2KQGH66AYF7",
        "Arn": "arn:aws:iam::XXXXXXXXXXX:policy/s3-contextual-data-access-policy-tenantb-XXXXXXXXX",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2024-10-10T03:42:47+00:00",
        "UpdateDate": "2024-10-10T03:42:47+00:00"
    }
}
DynamoDB and Bedrock access policy for tenantb chatbot app
{
    "Policy": {
        "PolicyName": "dynamodb-access-policy-tenantb-XXXXXXXXX",
        "PolicyId": "ANPAW3MECN2K4OFTGGLJ2",
        "Arn": "arn:aws:iam::XXXXXXXXXXX:policy/dynamodb-access-policy-tenantb-XXXXXXXXX",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2024-10-10T03:42:49+00:00",
        "UpdateDate": "2024-10-10T03:42:49+00:00"
    }
}
documents:loaded:size=153
Documents:after split and chunking size=154
vector_db:created=<langchain.vectorstores.faiss.FAISS object at 0x7f7b28df9b20>::
S3 Bucket: $contextual-data-tenanta-XXXXXXXXX
documents:loaded:size=260
Documents:after split and chunking size=265
vector_db:created=<langchain.vectorstores.faiss.FAISS object at 0x7f7b2b764970>::
S3 Bucket: $contextual-data-tenantb-XXXXXXXXX
Creating Chatbot ECR Repository
Creating rag-api ECR Repository
export ECR_REPO_CHATBOT=multitenant-chatapp-XXXXXXXXX-chatbot
export REPO_URI_CHATBOT=XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/multitenant-chatapp-XXXXXXXXX-chatbot
export ECR_REPO_RAGAPI=multitenant-chatapp-XXXXXXXXX-rag-api
export REPO_URI_RAGAPI=XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/multitenant-chatapp-XXXXXXXXX-rag-api
Building Chatbot and RAG-API Images
WARNING! Your password will be stored unencrypted in /home/ec2-user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
{
    "imageIds": [],
    "failures": [
        {
            "imageId": {
                "imageDigest": "None"
            },
            "failureCode": "InvalidImageDigest",
            "failureReason": "Invalid request parameters: image digest should satisfy the regex '[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+'"
        }
    ]
}
[+] Building 1.1s (14/14) FINISHED                                                                                                                          docker:default
 => [internal] load build definition from Dockerfile-app                                                                                                              0.0s
 => => transferring dockerfile: 1.34kB                                                                                                                                0.0s
 => [internal] load metadata for public.ecr.aws/docker/library/python:3.11.4-slim                                                                                     1.0s
 => [internal] load .dockerignore                                                                                                                                     0.0s
 => => transferring context: 2B                                                                                                                                       0.0s
 => [internal] load build context                                                                                                                                     0.0s
 => => transferring context: 10.66kB                                                                                                                                  0.0s
 => [installer-image 1/5] FROM public.ecr.aws/docker/library/python:3.11.4-slim@sha256:17d62d681d9ecef20aae6c6605e9cf83b0ba3dc247013e2f43e1b5a045ad4901               0.0s
 => CACHED [stage-1 2/5] RUN DEBIAN_FRONTEND=noninteractive apt-get -qq update -y 2>/dev/null >/dev/null &&     DEBIAN_FRONTEND=noninteractive apt-get -qq upgrade -  0.0s
 => CACHED [stage-1 3/5] WORKDIR /home/streamlit/app                                                                                                                  0.0s
 => CACHED [installer-image 2/5] WORKDIR /app                                                                                                                         0.0s
 => CACHED [installer-image 3/5] RUN DEBIAN_FRONTEND=noninteractive apt-get -qq update -y 2>/dev/null >/dev/null &&     DEBIAN_FRONTEND=noninteractive apt-get -qq i  0.0s
 => CACHED [installer-image 4/5] ADD app/* ./                                                                                                                         0.0s
 => CACHED [installer-image 5/5] RUN pip install --user --upgrade -q -q pip && pip install --user -q -q -r requirements.txt                                           0.0s
 => CACHED [stage-1 4/5] COPY --chown=streamlit:streamlit --from=installer-image /root/.local /home/streamlit/.local/                                                 0.0s
 => CACHED [stage-1 5/5] COPY --chown=streamlit:streamlit app/*.py /home/streamlit/app/                                                                               0.0s
 => exporting to image                                                                                                                                                0.0s
 => => exporting layers                                                                                                                                               0.0s
 => => writing image sha256:6d652c08d9893c9f6d7c70720d838bf73c51217e02b5a110486db89141e880d4                                                                          0.0s
 => => naming to XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/multitenant-chatapp-XXXXXXXXX-chatbot:latest                                                        0.0s
The push refers to repository [XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/multitenant-chatapp-XXXXXXXXX-chatbot]
3df3f95fe208: Pushed 
8e79ce1f95ac: Pushed 
d53056c1cb0a: Pushed 
90adf92c730f: Pushed 
84aede4a88e1: Pushed 
3e5ac6427acb: Pushed 
c01ec0304ec8: Pushed 
5b60283f3630: Pushed 
511780f88f80: Pushed 
latest: digest: sha256:b1ee3d2543c5e9248a49ace700a44bf909fff15793eeb288c05e76467b79b976 size: 2210
WARNING! Your password will be stored unencrypted in /home/ec2-user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
{
    "imageIds": [],
    "failures": [
        {
            "imageId": {
                "imageDigest": "None"
            },
            "failureCode": "InvalidImageDigest",
            "failureReason": "Invalid request parameters: image digest should satisfy the regex '[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+'"
        }
    ]
}
[+] Building 0.5s (14/14) FINISHED                                                                                                                          docker:default
 => [internal] load build definition from Dockerfile-api                                                                                                              0.1s
 => => transferring dockerfile: 1.38kB                                                                                                                                0.0s
 => [internal] load metadata for public.ecr.aws/docker/library/python:3.11.4-slim                                                                                     0.2s
 => [internal] load .dockerignore                                                                                                                                     0.0s
 => => transferring context: 2B                                                                                                                                       0.0s
 => [internal] load build context                                                                                                                                     0.0s
 => => transferring context: 8.28kB                                                                                                                                   0.0s
 => [installer-image 1/5] FROM public.ecr.aws/docker/library/python:3.11.4-slim@sha256:17d62d681d9ecef20aae6c6605e9cf83b0ba3dc247013e2f43e1b5a045ad4901               0.0s
 => CACHED [stage-1 2/5] RUN DEBIAN_FRONTEND=noninteractive apt-get -qq update -y 2>/dev/null >/dev/null &&     DEBIAN_FRONTEND=noninteractive apt-get -qq upgrade -  0.0s
 => CACHED [stage-1 3/5] WORKDIR /home/ragapi/app                                                                                                                     0.0s
 => CACHED [installer-image 2/5] WORKDIR /app                                                                                                                         0.0s
 => CACHED [installer-image 3/5] RUN DEBIAN_FRONTEND=noninteractive apt-get -qq update -y 2>/dev/null >/dev/null &&     DEBIAN_FRONTEND=noninteractive apt-get -qq i  0.0s
 => CACHED [installer-image 4/5] ADD api/requirements.txt ./                                                                                                          0.0s
 => CACHED [installer-image 5/5] RUN pip install --upgrade -q -q pip &&     pip install --user --upgrade -q -q pip && pip install --user -q -q -r requirements.txt &  0.0s
 => CACHED [stage-1 4/5] COPY --chown=ragapi:ragapi --from=installer-image /root/.local /home/ragapi/.local/                                                          0.0s
 => CACHED [stage-1 5/5] COPY --chown=ragapi:ragapi api/app /home/ragapi/app/                                                                                         0.0s
 => exporting to image                                                                                                                                                0.0s
 => => exporting layers                                                                                                                                               0.0s
 => => writing image sha256:cecec0cd243eba12af73f0102f9a517c876e94dc3d87e9e1738daaf5ca19f12a                                                                          0.0s
 => => naming to XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/multitenant-chatapp-XXXXXXXXX-rag-api:latest                                                        0.0s
The push refers to repository [XXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/multitenant-chatapp-XXXXXXXXX-rag-api]
add10cf807e1: Pushed 
7c9245b7838b: Pushed 
b9b0d28a5952: Pushed 
1b15e06f6043: Pushed 
84aede4a88e1: Pushed 
3e5ac6427acb: Pushed 
c01ec0304ec8: Pushed 
5b60283f3630: Pushed 
511780f88f80: Pushed 
latest: digest: sha256:53c434e46783d96c43d4848d9feadf9631a5be22dc1afe027a3671f20358cf9b size: 2209
Installing helm
Helm v3.16.1 is already latest
Version: v3.16.1
Generating a new key
Generating public/private rsa key pair.
Your identification has been saved in /home/ec2-user/.ssh/id_rsa.
Your public key has been saved in /home/ec2-user/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:XXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ec2-user@ip-172-31-25-176.ap-northeast-1.compute.internal
The key's randomart image is:
+---[RSA 2048]----+
+----[SHA256]-----+
{
    "KeyFingerprint": "XXXXXXXXXXXXXXXXXXXX",
    "KeyName": "multitenant-chatapp-XXXXXXXXX",
    "KeyPairId": "key-XXXXXXXXXXXXXXX"
}
export EC2_KEY_NAME=multitenant-chatapp-XXXXXXXXX
Creating KMS Key and Alias
export KMS_KEY_ALIAS=multitenant-chatapp-XXXXXXXXX
export MASTER_ARN=arn:aws:kms:ap-northeast-1:XXXXXXXXXXX:key/XXXXXXXXXXXXXXXXXXXXXXXXXXXXX

```
</details>

### 6. DynamoDBテーブル作成

Cloud9のIDEで新しくターミナルウィンドウを開き、以下のとおり実行します。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x create-dynamodb-table.sh
$ ./create-dynamodb-table.sh
```

ここでは、チャット履歴用テーブルなどが作成されます。

<details><summary>create-dynamodb-table.shの実行ログ例</summary>

```sh:
$ ./create-dynamodb-table.sh
Creating DynamoDB table Sessions_tenanta_XXXXXXX
Creating DynamoDB table ChatHistory_tenanta_XXXXXXX
Creating DynamoDB table Sessions_tenantb_XXXXXXX
Creating DynamoDB table ChatHistory_tenantb_XXXXXXX
```
</details>

### 7. EKSクラスター作成

以下のとおり実行します。このスクリプトの実行には完了まで30分程度かかることがあります。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x deploy-eks.sh
$ ./deploy-eks.sh
```

ここでは、EKSクラスターの構築やロールの作成、OIDCプロバイダーのクラスターとの関連付け、クラスターへのAWS Load Balancer Controllerのデプロイなどが実行されます。

<details><summary>deploy-eks.shの実行ログ例</summary>

```sh
$ ./deploy-eks.sh
EKS_CLUSTER_NAME is multitenant-chatapp
Deploying Cluster multitenant-chatapp with EKS 1.30
2024-10-10 03:50:08 [ℹ]  eksctl version 0.191.0
2024-10-10 03:50:08 [ℹ]  using region ap-northeast-1
2024-10-10 03:50:09 [ℹ]  subnets for ap-northeast-1a - public:192.168.0.0/19 private:192.168.64.0/19
2024-10-10 03:50:09 [ℹ]  subnets for ap-northeast-1c - public:192.168.32.0/19 private:192.168.96.0/19
2024-10-10 03:50:09 [ℹ]  nodegroup "nodegroup" will use "" [AmazonLinux2023/1.30]
2024-10-10 03:50:09 [ℹ]  using Kubernetes version 1.30
2024-10-10 03:50:09 [ℹ]  creating EKS cluster "multitenant-chatapp" in "ap-northeast-1" region with managed nodes
2024-10-10 03:50:09 [ℹ]  1 nodegroup (nodegroup) was included (based on the include/exclude rules)
2024-10-10 03:50:09 [ℹ]  will create a CloudFormation stack for cluster itself and 0 nodegroup stack(s)
2024-10-10 03:50:09 [ℹ]  will create a CloudFormation stack for cluster itself and 1 managed nodegroup stack(s)
2024-10-10 03:50:09 [ℹ]  if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=ap-northeast-1 --cluster=multitenant-chatapp'
2024-10-10 03:50:09 [ℹ]  Kubernetes API endpoint access will use default of {publicAccess=true, privateAccess=false} for cluster "multitenant-chatapp" in "ap-northeast-1"
2024-10-10 03:50:09 [ℹ]  configuring CloudWatch logging for cluster "multitenant-chatapp" in "ap-northeast-1" (enabled types: api, audit, authenticator, controllerManager, scheduler & no types disabled)
2024-10-10 03:50:09 [ℹ]  default addons vpc-cni, kube-proxy, coredns were not specified, will install them as EKS addons
2024-10-10 03:50:09 [ℹ]  
2 sequential tasks: { create cluster control plane "multitenant-chatapp", 
    2 sequential sub-tasks: { 
        5 sequential sub-tasks: { 
            1 task: { create addons },
            wait for control plane to become ready,
            associate IAM OIDC provider,
            2 sequential sub-tasks: { 
                create IAM role for serviceaccount "kube-system/aws-load-balancer-controller",
                create serviceaccount "kube-system/aws-load-balancer-controller",
            },
            update VPC CNI to use IRSA if required,
        },
        create managed nodegroup "nodegroup",
    } 
}
2024-10-10 03:50:09 [ℹ]  building cluster stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:50:09 [ℹ]  deploying stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:50:39 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:51:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:52:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:53:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:54:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:55:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:56:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:57:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:58:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:59:09 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-cluster"
2024-10-10 03:59:11 [!]  recommended policies were found for "vpc-cni" addon, but since OIDC is disabled on the cluster, eksctl cannot configure the requested permissions; the recommended way to provide IAM permissions for "vpc-cni" addon is via pod identity associations; after addon creation is completed, add all recommended policies to the config file, under `addon.PodIdentityAssociations`, and run `eksctl update addon`
2024-10-10 03:59:11 [ℹ]  creating addon
2024-10-10 03:59:12 [ℹ]  successfully created addon
2024-10-10 03:59:12 [ℹ]  creating addon
2024-10-10 03:59:12 [ℹ]  successfully created addon
2024-10-10 03:59:13 [ℹ]  creating addon
2024-10-10 03:59:13 [ℹ]  successfully created addon
2024-10-10 04:01:14 [ℹ]  building iamserviceaccount stack "eksctl-multitenant-chatapp-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2024-10-10 04:01:14 [ℹ]  deploying stack "eksctl-multitenant-chatapp-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2024-10-10 04:01:14 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2024-10-10 04:01:44 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2024-10-10 04:02:40 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2024-10-10 04:02:40 [ℹ]  created serviceaccount "kube-system/aws-load-balancer-controller"
2024-10-10 04:02:41 [ℹ]  deploying stack "eksctl-multitenant-chatapp-addon-vpc-cni"
2024-10-10 04:02:41 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-addon-vpc-cni"
2024-10-10 04:03:11 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-addon-vpc-cni"
2024-10-10 04:03:11 [ℹ]  updating addon
2024-10-10 04:03:22 [ℹ]  addon "vpc-cni" active
2024-10-10 04:03:22 [ℹ]  building managed nodegroup stack "eksctl-multitenant-chatapp-nodegroup-nodegroup"
2024-10-10 04:03:22 [ℹ]  deploying stack "eksctl-multitenant-chatapp-nodegroup-nodegroup"
2024-10-10 04:03:22 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-nodegroup-nodegroup"
2024-10-10 04:03:52 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-nodegroup-nodegroup"
2024-10-10 04:04:23 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-nodegroup-nodegroup"
2024-10-10 04:05:44 [ℹ]  waiting for CloudFormation stack "eksctl-multitenant-chatapp-nodegroup-nodegroup"
2024-10-10 04:05:44 [ℹ]  waiting for the control plane to become ready
2024-10-10 04:05:45 [✔]  saved kubeconfig as "/home/ec2-user/.kube/config"
2024-10-10 04:05:45 [ℹ]  no tasks
2024-10-10 04:05:45 [✔]  all EKS cluster resources for "multitenant-chatapp" have been created
2024-10-10 04:05:45 [✔]  created 0 nodegroup(s) in cluster "multitenant-chatapp"
2024-10-10 04:05:45 [ℹ]  nodegroup "nodegroup" has 3 node(s)
2024-10-10 04:05:45 [ℹ]  node "ip-192-168-0-181.ap-northeast-1.compute.internal" is ready
2024-10-10 04:05:45 [ℹ]  node "ip-192-168-38-32.ap-northeast-1.compute.internal" is ready
2024-10-10 04:05:45 [ℹ]  node "ip-192-168-44-200.ap-northeast-1.compute.internal" is ready
2024-10-10 04:05:45 [ℹ]  waiting for at least 3 node(s) to become ready in "nodegroup"
2024-10-10 04:05:45 [ℹ]  nodegroup "nodegroup" has 3 node(s)
2024-10-10 04:05:45 [ℹ]  node "ip-192-168-0-181.ap-northeast-1.compute.internal" is ready
2024-10-10 04:05:45 [ℹ]  node "ip-192-168-38-32.ap-northeast-1.compute.internal" is ready
2024-10-10 04:05:45 [ℹ]  node "ip-192-168-44-200.ap-northeast-1.compute.internal" is ready
2024-10-10 04:05:45 [✔]  created 1 managed nodegroup(s) in cluster "multitenant-chatapp"
2024-10-10 04:05:46 [ℹ]  kubectl command should work with "/home/ec2-user/.kube/config", try 'kubectl get nodes'
2024-10-10 04:05:46 [✔]  EKS cluster "multitenant-chatapp" in "ap-northeast-1" region is ready
Updated context arn:aws:eks:ap-northeast-1:XXXXXXXXXXX:cluster/multitenant-chatapp in /home/ec2-user/.kube/config
Associating an OIDC provider with the EKS Cluster
2024-10-10 04:05:48 [ℹ]  IAM Open ID Connect provider is already associated with cluster "multitenant-chatapp" in "ap-northeast-1"
Installing AWS Load Balancer Controller
customresourcedefinition.apiextensions.k8s.io/ingressclassparams.elbv2.k8s.aws created
customresourcedefinition.apiextensions.k8s.io/targetgroupbindings.elbv2.k8s.aws created
eks already exists with the same configuration, skipping
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "eks" chart repository
...Successfully got an update from the "oauth2-proxy" chart repository
...Successfully got an update from the "istio" chart repository
Update Complete. ⎈Happy Helming!⎈
NAME: aws-load-balancer-controller
LAST DEPLOYED: Thu Oct 10 04:05:55 2024
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
AWS Load Balancer controller installed!
Waiting for deployment "aws-load-balancer-controller" rollout to finish: 0 of 2 updated replicas are available...
deployment "aws-load-balancer-controller" successfully rolled out
Creating S3 Access Role in IAM
Attaching S3 Bucket policy to S3 Access Role
arn:aws:iam::XXXXXXXXXXX:role/multitenant-chatapp-s3-access-role-XXXXXXX
export ENVOY_IRSA=arn:aws:iam::XXXXXXXXXXX:role/multitenant-chatapp-s3-access-role-XXXXXXX
Creating DynamoDB / Bedrock Access Role in IAM
Attaching S3 Bucket and DynamoDB policy to Chatbot Access Role
Creating DynamoDB / Bedrock Access Role in IAM
Attaching S3 Bucket and DynamoDB policy to Chatbot Access Role
```
</details>

### 8. Istio Service Meshのデプロイ

[7. EKSクラスター作成](#7-eksクラスター作成)の完了後、Cloud9のIDEで新しくターミナルウィンドウを開き、以下のとおり実行します。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x deploy-istio.sh
$ ./deploy-istio.sh
```

ここでは、Istio Ingress GatewayのデプロイなどIstio Service Meshの構築が実行されます。

<details><summary>deploy-istio.shの実行ログ例</summary>

```sh
master-role:~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock (main) $ ./deploy-istio.sh
Installing Istio with Ingress Gateway (NLB)
istio already exists with the same configuration, skipping
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "oauth2-proxy" chart repository
...Successfully got an update from the "eks" chart repository
...Successfully got an update from the "istio" chart repository
Update Complete. ⎈Happy Helming!⎈
namespace/istio-system created
namespace/istio-ingress created
NAME: istio-base
LAST DEPLOYED: Thu Oct 10 04:27:31 2024
NAMESPACE: istio-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Istio base successfully installed!

To learn more about the release, try:
  $ helm status istio-base -n istio-system
  $ helm get all istio-base -n istio-system
NAME: istiod
LAST DEPLOYED: Thu Oct 10 04:27:33 2024
NAMESPACE: istio-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
istiod successfully installed!

To learn more about the release, try:
  $ helm status istiod -n istio-system
  $ helm get all istiod -n istio-system

Next steps:
  * Deploy a Gateway: https://istio.io/latest/docs/setup/additional-setup/gateway/
  * Try out our tasks to get started on common configurations:
    * https://istio.io/latest/docs/tasks/traffic-management
    * https://istio.io/latest/docs/tasks/security/
    * https://istio.io/latest/docs/tasks/policy-enforcement/
  * Review the list of actively supported releases, CVE publications and our hardening guide:
    * https://istio.io/latest/docs/releases/supported-releases/
    * https://istio.io/latest/news/security/
    * https://istio.io/latest/docs/ops/best-practices/security/

For further documentation see https://istio.io website
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
istio-base      istio-system    1               2024-10-10 04:27:31.844081791 +0000 UTC deployed        base-1.22.3     1.22.3     
istiod          istio-system    1               2024-10-10 04:27:33.398846873 +0000 UTC deployed        istiod-1.22.3   1.22.3     
Creating Istio Ingress Gateway, associating an internet-facing NLB instance
with Proxy v2 protocol and cross-AZ loadbalancing enabled
Error: INSTALLATION FAILED: context deadline exceeded
NAME                    NAMESPACE       REVISION        UPDATED                                 STATUS  CHART           APP VERSION
istio-ingressgateway    istio-ingress   1               2024-10-10 04:27:47.107675455 +0000 UTC failed  gateway-1.22.3  1.22.3     
NAME     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                                 AGE
istiod   ClusterIP   10.100.8.164   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   5m15s
NAME                     READY   STATUS    RESTARTS   AGE
istiod-d56968787-kdxbz   1/1     Running   0          5m16s
NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                                      AGE
istio-ingressgateway   LoadBalancer   10.100.133.141   <pending>     15021:32338/TCP,80:32063/TCP,443:31846/TCP   5m4s
NAME                                    READY   STATUS    RESTARTS   AGE
istio-ingressgateway-6b644dc8cb-9tdwc   1/1     Running   0          5m5s
Status of Load Balancer multitenant-chatapp-nlb: "active"
Enabling Proxy v2 protocol processing on Istio Ingress Gateway
error: error parsing istio-proxy-v2-config/proxy-protocol-envoy-filter.yaml: error converting YAML to JSON: yaml: line 15: did not find expected '-' indicator
Warning: EnvoyFilter exposes internal implementation details that may change at any time. Prefer other APIs if possible, and exercise extreme caution, especially around upgrades.
envoyfilter.networking.istio.io/ingressgateway-settings created
```
上記は[④proxy-protocol-envoy-filter.yaml](#proxy-protocol-envoy-filteryaml)のYaml修正時にインデント等を誤っていたためエラーが出た例。Yaml修正して直接下記の様にコマンド再実行して無事完了。
```sh
$ kubectl -n istio-ingress apply -f istio-proxy-v2-config/proxy-protocol-envoy-filtamlya
Warning: EnvoyFilter exposes internal implementation details that may change at any time. Prefer other APIs if possible, and exercise extreme caution, especially around upgrades.
envoyfilter.networking.istio.io/proxy-protocol created
```

</details>

### 9. Cognitoユーザープールの作成

Cloud9のIDEで新しくターミナルウィンドウを開き、以下のとおり実行します。ここではtenantaとtenantbの2つのテナント用のCognitoユーザープールを作成します。スクリプト実行時に、各テストユーザーに設定されるパスワードが要求されるのでそれぞれ入力が必要です。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x deploy-userpools.sh
$ ./deploy-userpools.sh
```
ここでは、ユーザープールの作成に加えて、次の手順でデプロイされるテナントごとのプロキシの設定ファイルの生成なども実行されます。

<details><summary>deploy-userpools.shの実行ログ例</summary>

```sh
Creating User Pool for tenanta
Adding Custom Attributes to User Pool
Creating User Pool Client for tenanta
Setting User Pool Client Properties for tenanta
Creating User Pool Domain for tenanta
Creating user1@tenanta.com in tenanta_chatbot_example_com_XXXXXXX
Enter a Password for user1@tenanta.com:   #パスワード設定が要求されるので入力
Setting User Custom Attributes for user1@tenanta.com
Creating AuthN Policy for tenanta
Creating AuthZ Policy for tenanta
Creating oauth2-proxy Configuration for tenanta

Creating User Pool for tenantb
Adding Custom Attributes to User Pool
Creating User Pool Client for tenantb
Setting User Pool Client Properties for tenantb
Creating User Pool Domain for tenantb
Creating user1@tenantb.com in tenantb_chatbot_example_com_XXXXXXX
Enter a Password for user1@tenantb.com:   #パスワード設定が要求されるので入力
Setting User Custom Attributes for user1@tenantb.com
Creating AuthN Policy for tenantb
Creating AuthZ Policy for tenantb
Creating oauth2-proxy Configuration for tenantb

Creating AuthorizationPolicy on Istio Ingress Gateway
```
</details>

### 10. Istio Ingress Gatewayの設定

Cloud9のIDEで新しくターミナルウィンドウを開き、以下のとおり実行します。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x configure-istio.sh
$ ./configure-istio.sh
```
ここでは、以下が実施されます。
* 自己署名のルートCA証明書とキーの生成
* ルートCA署名のIstio Ingress Gateway証明書生成
* Istio Ingress Gatewayのクレデンシャル作成
* Gateway、Envoy リバース プロキシ、OIDCプロキシ、テナントのKubernetes名前空間の作成
* Istio Gatewayリソースのデプロイ
* Envoyリバースプロキシのデプロイ
* oauth2-proxyのデプロイ
* Istio外部認証プロバイダー定義の追加

<details><summary>configure-istio.shの実行ログ例</summary>

```sh
$ ./configure-istio.sh
Creating Root CA Cert and Key
Generating a 2048 bit RSA private key
............................................................................................................+++
....................+++
writing new private key to 'certs/ca_example_com.key'
-----
Creating Cert and Key for Istio Ingress Gateway
Generating a 2048 bit RSA private key
....+++
..................+++
writing new private key to 'certs/example_com.key'
-----
Signature ok
subject=/O=Cluster 1/CN=*.example.com
Getting CA Private Key
Creating TLS secret for Istio Ingress Gateway
secret/credentials created
Creating namespaces
namespace/llm-demo-gateway-ns created
namespace/envoy-reverse-proxy-ns created
namespace/tenanta-oidc-proxy-ns created
namespace/tenantb-oidc-proxy-ns created
namespace/tenanta-ns created
namespace/tenantb-ns created
Enabling sidecar injection in namespaces
namespace/envoy-reverse-proxy-ns labeled
namespace/tenanta-oidc-proxy-ns labeled
namespace/tenantb-oidc-proxy-ns labeled
namespace/tenanta-ns labeled
namespace/tenantb-ns labeled
NAME                     STATUS   AGE   ISTIO-INJECTION
default                  Active   54m   
envoy-reverse-proxy-ns   Active   13s   enabled
istio-ingress            Active   22m   
istio-system             Active   22m   
kube-node-lease          Active   54m   
kube-public              Active   54m   
kube-system              Active   54m   
llm-demo-gateway-ns      Active   14s   
tenanta-ns               Active   9s    enabled
tenanta-oidc-proxy-ns    Active   11s   enabled
tenantb-ns               Active   8s    enabled
tenantb-oidc-proxy-ns    Active   10s   enabled
Applying STRICT mTLS Policy on all application namespaces
peerauthentication.security.istio.io/strict-mtls created
peerauthentication.security.istio.io/strict-mtls created
peerauthentication.security.istio.io/strict-mtls created
NAME          MODE     AGE
strict-mtls   STRICT   4s
NAME          MODE     AGE
strict-mtls   STRICT   3s
NAME          MODE     AGE
strict-mtls   STRICT   4s
Deploying Istio Gateway resource
gateway.networking.istio.io/llm-demo-gateway created
Copying Envoy Dynamic Config files to S3 bucket
upload: envoy-config/envoy.yaml to s3://envoy-config-XXXXXXX/envoy.yaml
upload: envoy-config/envoy-lds.yaml to s3://envoy-config-XXXXXXX/envoy-lds.yaml
upload: envoy-config/envoy-cds.yaml to s3://envoy-config-XXXXXXX/envoy-cds.yaml
Deploying Envoy Reverse Proxy
serviceaccount/envoy-reverse-proxy-sa created
service/envoy-reverse-proxy created
deployment.apps/envoy-reverse-proxy created
Adding Istio External Authorization Provider
configmap/istio patched
deployment.apps/istiod restarted
Configuring AuthorizationPolicy on Istio Ingress Gateway
authorizationpolicy.security.istio.io/cluster1-auth-policy created
```
</details>

### 11. アプリケーションのデプロイ

Cloud9のIDEで新しくターミナルウィンドウを開き、以下のとおり実行します。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x deploy-tenant-services.sh
$ ./deploy-tenant-services.sh
```

ここでは、各テナント用のアプリコンテナのデプロイが実行されます。

<details><summary>deploy-tenant-services.shの実行ログ例</summary>

```sh
$ ./deploy-tenant-services.sh
oauth2-proxy already exists with the same configuration, skipping
Deploying tenanta services ...
-> Deploying chatbot service
serviceaccount/tenanta-sa created
deployment.apps/chatbot created
service/chatbot created
Applying Frontend Authentication Policy for tenanta
requestauthentication.security.istio.io/frontend-jwt-auth created
Applying Frontend Authorization Policy for tenanta
authorizationpolicy.security.istio.io/frontend-authz-pol created
-> Deploying VirtualService to expose chatbot via Ingress Gateway
virtualservice.networking.istio.io/chatbot created
Deploying OIDC Proxy for tenanta
NAME: oauth2-proxy
LAST DEPLOYED: Thu Oct 10 04:52:35 2024
NAMESPACE: tenanta-oidc-proxy-ns
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
To verify that oauth2-proxy has started, run:

  kubectl --namespace=tenanta-oidc-proxy-ns get pods -l "app=oauth2-proxy"
Deploying tenantb services ...
-> Deploying chatbot service
serviceaccount/tenantb-sa created
deployment.apps/chatbot created
service/chatbot created
Applying Frontend Authentication Policy for tenantb
requestauthentication.security.istio.io/frontend-jwt-auth created
Applying Frontend Authorization Policy for tenantb
authorizationpolicy.security.istio.io/frontend-authz-pol created
-> Deploying VirtualService to expose chatbot via Ingress Gateway
virtualservice.networking.istio.io/chatbot created
Deploying OIDC Proxy for tenantb
NAME: oauth2-proxy
LAST DEPLOYED: Thu Oct 10 04:52:47 2024
NAMESPACE: tenantb-oidc-proxy-ns
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
To verify that oauth2-proxy has started, run:

  kubectl --namespace=tenantb-oidc-proxy-ns get pods -l "app=oauth2-proxy"

```
</details>


ここまでで、生成AIサービス環境構築のための基本的な手順は完了です。

### 12. hosts情報の出力

次に、アプリケーションにアクセスするための情報を出力させます。
Cloud9のIDEで新しくターミナルウィンドウを開き、以下のとおり実行します。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x hosts-file-entry.sh
$ ./hosts-file-entry.sh
```

ここで出力されたhosts情報を利用してクライアントからアプリケーションにアクセスします。後程使うので出力された内容をコピーしておきます。

<details><summary>hosts-file-entry.shの実行ログ例</summary>

```sh
$ ./hosts-file-entry.sh
Status of Load Balancer multitenant-chatapp-nlb: active
You can update your hosts file with the following entries:
---------------------------------------------------------
XX.XXX.XXX.XXX  tenanta.example.com
XX.XXX.XXX.XXX  tenantb.example.com
XX.XXX.XX.XX    tenanta.example.com
XX.XXX.XX.XX    tenantb.example.com
```
</details>

### 13. アクセスエントリ

クラスター構築後、AWS管理コンソールから対象EKSクラスターを確認しようとしても、以下の様に権限がなくてアクセスできない状態となっています。`アクセスエントリの作成`から設定していきます。

![013-accessentry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/1c0bfc8c-0745-6eea-c434-3a827a18a54c.png)

[アクセスポリシーをアクセスエントリに関連付ける](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/access-policies.html)を参考に設定を進めていきます。
追加するポリシーは`AmazonEKSClusterAdminPolicy`としています。

![015-accessentry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/fea48a43-b40d-82b8-5bf3-9ac3bfe98a86.png)

`IAMプリンシパルARN`が現在コンソールにアクセスしているロールと一致することを確認し実行します。

![014-accessentry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/3fc23634-a9e4-42d5-fa5f-e737b14070dd.png)

設定完了後、EKSクラスター内のリソースが閲覧可能になっていることを確認します。

![016-accessentry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/0ce92594-395b-ea04-5492-dd83655a287c.png)

### 14. IP制限

本手順は構築するアプリケーションへのアクセスにIP制限が必要な場合に参考にしてください。

ロードバランサーをデプロイするマニフェストに設定を追加・再デプロイすることで、永続的な設定としてIP制限を行います。まず、ロードバランサーを構築するサービスを特定します。
Cloud9のIDEで新しくターミナルウィンドウを開き、以下のようにコマンド実行して確認していきます。

```sh
$ kubectl get namespace
NAME                     STATUS   AGE
amazon-cloudwatch        Active   67d
default                  Active   67d
envoy-reverse-proxy-ns   Active   67d
istio-ingress            Active   67d
istio-system             Active   67d
kube-node-lease          Active   67d
kube-public              Active   67d
kube-system              Active   67d
llm-demo-gateway-ns      Active   67d
tenanta-ns               Active   67d
tenanta-oidc-proxy-ns    Active   67d
tenantb-ns               Active   67d
tenantb-oidc-proxy-ns    Active   67d
```

名前空間の一覧を表示させ、Istioに関連しそうなものを探します。次に以下の様にサービス一覧を取得して対象サービスを探します。

```sh
$ kubectl -n istio-ingress get svc
istio-ingressgateway   LoadBalancer   XX.XXX.XXX.XXX   multitenant-chatapp-nlb-XXXXXXXXXXXXXXXXX.elb.ap-northeast-1.amazonaws.com   15021:32338/TCP,80:32063/TCP,443:31846/TCP   36d
```

`istio-ingressgateway`がロードバランサーを構成しているサービスであることが確認できました。本手順では構築時のマニフェストが作成されていないので、ここではAWS管理コンソールの`EKS > クラスター > multitenant-chatapp > Service > istio-ingress/istio-ingressgateway`からコピーして作成します。

![011-istio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/e1a7eaa4-345b-7fe8-7945-83e243ae25b7.png)

rawビューで全体を表示・コピーし、Cloud9のIDEでマニフェスト用のJsonファイルを新規で作成します。Jsonファイル作成したら、["Amazon EKS の LoadBalancer タイプのサービスの CIDR IP アドレスを制限する方法を教えてください。"](https://repost.aws/ja/knowledge-center/eks-cidr-ip-address-loadbalancer)を参考に、以下２つを追加します。

```diff_json
// ... 略
    "annotations": {
      "meta.helm.sh/release-name": "istio-ingressgateway",
      "meta.helm.sh/release-namespace": "istio-ingress",

// ... 略

+       "service.beta.kubernetes.io/aws-load-balancer-target-group-attributes": "preserve_client_ip.enabled=true"
    },
// ... 略
```

```diff_json
// ... 略
  "spec": {

// ... 略

+     "loadBalancerSourceRanges": "<クライアントIPアドレス>/32"
  }
}
```

作成したマニフェストで以下のとおり更新実行してIP制限完了です。

```
$ kubectl apply -f ＜作成したJsonファイル名＞ 
```
更新実施後、対象ロードランサーのセキュリティグループ設定が更新されていることが確認できます。

![lb-ip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/b4fb8aa9-c6ff-075b-0241-5c4b70310abd.png)

### 15. 削除スクリプト

生成AIサービス環境全体を削除する場合は以下の様にして削除実施します。また、環境構築時に設定ミスでエラー発生して再構築したくなったような場合も、各構築用スクリプトは基本的に冪等性を持っていないため、いったん削除スクリプトで全体削除してからやり直すことをお勧めします。

```sh
$ cd ~/environment/multi-tenant-chatbot-using-rag-with-amazon-bedrock
$ chmod +x cleanup.sh
$ ./cleanup.sh
```

VPC削除に失敗する場合は、直接依存関係のあるリソースを削除して、スタックの再削除で対応します。


# 動作確認
監視設定を有効化する前に、構築した生成AIアプリにアクセスできるか確認していきます。
クライアント環境の構築手順については本記事では省略しますが、本環境と同様にEC2インスタンスをクライアントとして構築する場合のポイントを記載しておきます。

* クライアント構築するVPCとEKSクラスターのVPCは分ける
  * それぞれのルーティングや削除スクリプトに影響でないように
* プライベートサブネット内へのRDP環境構築は[EC2 Instance Connect Endpointの記事](https://aws.amazon.com/jp/blogs/compute/secure-connectivity-from-public-to-private-introducing-ec2-instance-connect-endpoint-june-13-2023/)など参照
* Windows Server利用の場合はブラウザアクセスできるように[Windows サーバーで Internet Explorer ESC をオフにする方法](https://learn.microsoft.com/ja-jp/previous-versions/troubleshoot/browsers/security-privacy/enhanced-security-configuration-faq#windows-------internet-explorer-esc---------)を参照

生成AIアプリにアクセスするために、[12. hosts情報の出力](#12-hosts情報の出力)で保存しておいたドメイン情報をクライアント（Windows）ローカルのhostsに追加します。

![017-hosts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/20ed4cd3-0872-cc48-baaa-a07aff3ef129.png)

クライアントでブラウザを開き、以下どちらかのURLにアクセスします。

```
https://tenanta.example.com/
https://tenantb.example.com/
```
ここではテナントAのURLにアクセスします。自己署名証明書関連で警告が出ますが画面下の`"Continuity to tenanta.example.com (unsafe)"`からアクセスします。

![018-access.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/675a9fdc-bccb-05ce-5ef3-71d009729b60.png)

Cognitoの認証画面に遷移するので、テナントA用のユーザ`user1@tenanta.com`と[9. Cognitoユーザープールの作成](#9-cognitoユーザープールの作成)で設定したパスワードでサインインします。

![019-access.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/80ef11be-87f4-5842-3273-3ed8af119499.png)

生成AIアプリ画面が表示されました。

![020-access.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/ad98d1c8-2806-c679-08c2-e2c20b668179.png)

適当に質問してみます。SageMaker QA（テナントA向けのドキュメント）と関係のない内容でもある程度回答してくれるようですね。

![021-access.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/112efd6a-d0b3-783c-640c-a71fea9b09a6.png)


# 監視有効化

[独自のサービスを使用してAmazon EKSクラスターでApplication Signalsを有効にする](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Signals-Enable-EKS-Console.html)を参考にApplication Signalsを有効化していきます。

## ワーカーノードの権限設定
EKSクラスターのワーカーノードの情報を各サービスに送信できるように、ワーカーノードのIAMロールに以下のポリシーを追加します。

```
--policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
--policy-arn arn:aws:iam::aws:policy/AWSXRayWriteOnlyAccess
```
ワーカーノードのEC2インスタンスにアタッチされているロールをEC2管理画面等から特定し、上記ポリシーをアタッチします。

![024-nodeiamrole.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/37f62295-8788-94ee-0a11-484162741aaf.png)

![025-nodeiamrole.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/2d454e5e-29fd-5c74-27b8-a423cdac5809.png)

## アドオン追加
EKSクラスターにCloudWatch Application Signals用のアドオンをインストールしていきます。
対象EKSクラスターのアドオン追加設定画面から、`Amazon CloudWatch Observability EKS`アドオンを選択します。

![026-adon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/20297180-4102-ec0e-0eb4-f702c2db13d2.png)

![027-adon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/ca60b5ea-5ea4-fce9-8f45-03056044a464.png)

設定したEKSバージョンに合わせてバージョンを設定し、IAMロールは`未設定`のままで追加実行します。`未設定`だと対象インスタンスにアタッチされているロールをそのまま使う設定となっています。

![028-adon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/87bdef02-e169-d3b2-b162-ddcf918c205c.png)

設定数分後にアドオンが有効化が完了します。

![029-adon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/b21c447f-2a67-206a-585a-de1ec3965393.png)

## CloudWatch Application Signals有効化
CloudWatch管理画面に移動し、`Application Signalsを有効にする`を実行します。

![022-appsignals.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/b05ec19f-bbfa-b803-88eb-624225f67427.png)

プラットフォームをEKS選択、EKSクラスターは構築した`multitenant-chatapp`を選択します。

![030-signals.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/417296b3-e388-0287-fae1-69ccc6e9b8d3.png)

セットアップオプションはコンソールを選択し、監視対象サービスを選んでいきます。

![031-signals.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/ff5cdb21-7f03-6e8e-23ba-b328d12e308a.png)

各テナントごとのサービスを選択し、対応するアプリの言語を選択していきます。

![032-signals.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/e85485b6-b9d2-c065-0ade-8e7da9910509.png)

今回のアプリはPythonで実装されているため、Pythonを選択して設定完了します。

![033-signals.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/0566f7ed-5068-2d63-4673-7efc986ba894.png)

以上で各設定手順は完了です。

# 可視化確認
監視設定完了して数分後、再度クライアントから生成AIへのアクセス・チャット実行など実施すると、Application Signalsのサービス画面に以下の様にサービスが検出されます。

![034-map.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/d8e091f1-c20a-867b-f89e-2f9e98c7945a.png)

サービスマップも確認すると、それっぽいのが出ていますね。チャット履歴を保存するためのDynamoDBとのやりとり部分は可視化されるようです。

![035-map.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/5be6ba88-93c7-8d86-cd9c-29a492cfa1b9.png)

ただし、[ここで紹介されているような](https://aws.amazon.com/jp/blogs/mt/improve-amazon-bedrock-observability-with-amazon-cloudwatch-appsignals/)Bedrockとのやりとり部分などは可視化されませんでした。このあたりについては、次回記事で深堀していきたいと思います。

SLO管理機能についても確認してみます。SLO作成画面から生成AIアプリであるchatbotサービスを選択します。

![036-slo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/835aaad4-bd54-9078-3e83-e130e7cb8b75.png)

監視対象とするオペレーションでは、本環境ではクライアントからのアクセス(Get)については設定できそうです。BedrockへのAPI実行などはどうやら`InternalOperation`にくくられてしまっているようですね。

![037-slo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/2731ab8d-5f7b-4448-fd9f-4d9b574a7c09.png)

監視条件としては、可用性やレイテンシが選択可能で、それぞれの達成目標率や警告の閾値なども設定できます。

![038-slo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/88c72fd7-6018-ee59-54ae-9f8210ea0164.png)

SLO作成すると、直近の状況が可視化されます。

![039-slo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940015/b91b2fdd-bf04-87e0-5ea2-caf42469e333.png)

アプリケーション側の実装に手を入れなくても、CloudWatch Application Signalsを有効化するだけでこれら可視化・SLO管理ができることが確認できました。

# おわりに
本記事では、AWSを活用して生成AIサービス環境（[multi-tenant-chatbot-using-rag-with-amazon-bedrock](https://github.com/aws-samples/multi-tenant-chatbot-using-rag-with-amazon-bedrock)）の構築手順をメインにまとめつつ、
CloudWatch Application Signalsでどのよう監視が可能かを確認してみました。

単純にCloudWatch Application Signalsを有効化するだけでは、あまり細かい粒度での可視化はできませんでしたが、かなりお手軽にEKSクラスター環境におけるサービス全体のレイテンシやエラー状況を可視化・SLO管理が可能となることが確認できました。

今回はあくまでEKSベースの環境であり、あまりゼロベースでこれからEKS環境を利用していく、というユースケースは少ないかもしれませんが何かしらの参考となれば幸いです。

次回はchatbotのアプリケーションコードにも手を入れつつ、もっと生成AIサービスの監視として詳細な監視ができないかどうかの深堀をしていきたいと思います。