TypeScript と AWS CDK 初さわりが Organization 下アカウントの Bedrock モデル呼び出しログを有効化 & 集約してみた際のメモ


Bedrock (というより LLM 全般に言えること) は利用者が自由に文章を入力でき、出力が確率的に出てくる単語の羅列、という何が入出力されるか予測がつかないサービスの特性上、予防的統制が非常に難しい印象であるサービスです。

プロンプトによる調整、ガードレールによる出力抑制といった手段もありますが、絶対に防げると言い切れない以上、発見的統制もまた重要です。

......という大義名分のもと、少し時間の余裕もあって AWS CDK を触る機会だと思ったので、タイトルのことを試した際のメモになります。

## 実装前に読んだ資料、試したこと

### BlackBelt Online Seminar

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_AWS-CDK-Basic-1-Overview_0731_v1.pdf

### AWS CDK Workshop

https://cdkworkshop.com/ja/20-typescript.html

今回に限らず、ある AWS サービスを始めて触る際には BlackBelt を読んで、公式ワークショップを一通り試すのがいいと思います。「完全に理解した」状態まで持っていけます。作りたいものを実装しているうちに「全然わからん」になれます。

### L2 コンストラクトだけで VPC + ECS + Auto Scaling + ALB Web サーバー構成

Hello World や ToDo アプリみたいなものだと思って作成

### JavaScript Primer

https://jsprimer.net/

### サバイバル TypeScript

https://typescriptbook.jp/

JavaScript を触ったのは 9 年前が最後、TypeScript は RSS に流れてくる記事ぐらいしか見たことがなかったので、一通り読んでまとまった知識を得ます。

実装中は ググる -> 技術ブログやサンプル見つける -> 使われてる機能に関する CDK のレファレンス読む でなんとかします。

## アーキテクチャ図

![architecture.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102816/190c3098-a442-e66e-d5c4-5b54c0eb69f2.png)

S3 へのログ配信よる方法で実装してます。ログ配信は同一リージョン/アカウントのみサポートとなるため、S3 レプリケーションでログ集約アカウントへの転送を行います。

図に記載はないですが、メンバーアカウント側の S3 バケットはライフサイクルポリシーで 1 日経過後にオブジェクトの期限切れ、および期限切れオブジェクト削除を行っています。

StackSet の作成自体を CloudFormation (以下: CFn と略) で行うため、図のように

1. CDK より StackSet を作成する CFn スタックを管理アカウントで作成。これにはスタックインスタンスへ展開される Template が含まれる
2. その StackSet からメンバーアカウントへ展開したい Template をもとにしたオペレーション実行
3. 各メンバーアカウントにてスタック作成

と入れ子実行されます。管理アカウントで CFn スタックが失敗した際にロールバックしようとしても、スタックインスタンスの状態が OUTDATED の場合、そちらを解決しないと再び失敗するので注意が必要です。

CFn ではモデル呼び出しログの有効化が未実装なので、Lambda カスタムリソースで対応してます。

## 実装について

https://github.com/m-ochi-4/aws-cdk-bedrock-logging

### 環境ごとのパラメタ管理

別組織へ同機能を展開することを考慮してます。`cdk/env/map.ts` に、環境名とマップするパラメタオブジェクトを作成して、cdk コマンド実行時、コンテキストで環境名を渡すことによって実現してます。

公開リポジトリではファイル名: `map.example.ts` でコミットしているので、実際に動かす場合は .example の部分を削除してお使いください。

### ベストプラクティスから外れている件 (リソース名にプリフィックスとテンプレート分割)

リソース名のベストプラクティスは、コンストラクトの ID に分かりやすい名前を付けてリソース名は CDK 生成に任せる、です。
なのですが SCP でメンバーアカウントからの削除を制御する際、ABAC できないリソースに対しては、リソース名に特定の文字列を含ませ、それを対象にする RBAC が必要です。今回は S3 バケットがそれになります。
なのでやむなくプリフィックスをつけています。

スタック分割については、集約アカウント向けテンプレートとログ有効化アカウント向けテンプレートで内容やデプロイタイミングが大きく変わること、両環境を同時にデプロイした際、ロールバックが発生したときの対応の煩雑さより敢えて分離してます。

### カスタムリソース用 Lambda の LogGroup 作成について

SAM で Lambda Function リソースを作成するときは、セットで LogGroup も定義すると削除時に環境を綺麗にできるのでよく行ってます。

同じノリでカスタムリソース用の Lambda へそれをすると

リソース Delete 時の Lambda 実行 -> Lambda 削除 -> LogGroup 削除 -> 実行された Lambda のログが非同期配信によって保存

となった際、CFn 管理外で LogGroup が再作成され、再度スタックを作成する際、すでに LogGroup が存在する旨のエラーが発生します。特に、検証中は定義しない方がいいと思います。

### S3 オブジェクトの削除

実装のトライ & エラーでメンバーアカウント向けスタックを削除する際、Bedrock モデル呼び出しログの設定検証用オブジェクトも自動で削除したかったので実装してます。

L2 コンストラクトとやらで CFn にない便利機能を使えば S3 のオブジェクト削除も行えるのですが、実体である Lambda カスタムリソース作成のために、どうにかして `cdk-[修飾子]-asserts-[アカウント ID]-[リージョン]` を参照させる必要があります。
まだ自分は浅瀬にいるので、愚直に Lambda で再実装してます。

### カスタムリソースをメンバーアカウントの操作から保護する設定

メンバーアカウントからの設定変更を拒否したい Lambda カスタムリソースを StackSet に含める際、関連リソースの変更を SCP 等で拒否することはもちろん、他にも考慮すべきことが増えます。

- カスタムリソース用 Lambda を StackSets 実行ロール以外から実行させないポリシー
- 同 Lambda の実行ロールをそれ以外から AssumeRole させないポリシー

を最低限しないと、これらによって SCP の保護をバイパスされてしまいます。(下図の X 操作)

![bypass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102816/405da828-232d-9dda-21dc-065cbb5d3e5d.png)


前者は SCP で Invoke に関するアクションを Deny で対応してます。Bedrock ログ配信の設定を制限する SCP を`cdk/assets/scp.json` で残しています。

後者は信頼ポリシーの `Condition.StringEquals.SourceArn` で該当 Lambda からのみ Allow することで対応可能です。もちろん SCP でも対応できますが、ポリシー長が最大 5,120 bytes とリソース固有の設定を記述するには制約が厳しいので、SCP 側での信頼ポリシー変更拒否と合わせることで対応しています。

### ABAC のデバッグ

Condition の `aws:TagKeys` が case-sensitive なのに対し、`aws:ResourceTag/${タグ名}` が case-insensitive なので、 UpperCamel で `Audit: true` タグはつけることができ、`aws:ResourceTag/audit` が条件である SCP の制御対象になります。
メンバーアカウントからデバッグ用の空関数へそれをつけることで動作確認できます。

## 所感

自動補完、型の検証、些細なパラメタ内容の確認なら型定義ファイルへ移動して docstring を確認、至れり尽くせりです。

また、パラメタも TypeScript で管理できる点は非常にいいなと感じてます。

CFn が中規模くらいになるとテンプレート分割をどうしてもしたくなるのですが、そうなると「あれはアプリも参照したいから ParametaStore、これはスタック間依存を明示したいから ImportValue で対応、環境ごとに変わるパラメタは --parameter-overrides オプションと json で......」と、地味ながら考えごとが増えます。

CDK だと、テンプレート分割は Construct で対応でき、パラメタに関しては(いい感じにできれば)このファイルに環境ごとのパラメタがまとまっているため、環境再現とか調整とかはここだけ見ればオッケーというのはかなり気が楽です。

## 今後の改善点

### テストの実行

今回は小規模なお試しだからいいやとなってしまいましたが、プロダクトに投入する場合はそうもいかないです。
SnapShot テストぐらいはしておくと、変更するたびの不安が和らぎそうです。

### StackSet テンプレートの S3 アップロード

本実装では CFn テンプレートの文字列を TemplateBody に渡してますが、52,100 bytes の制限があるので、大規模なテンプレート展開は難しいです。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-stackset.html#aws-resource-cloudformation-stackset-properties

Asset コンストラクトで各 CFn テンプレートをアップロードできれば解決できそうです。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_assets.Asset.html