Bedrock Guardrailのデプロイが2回失敗した話——日本語トピック名が引き金だった
Bedrock GuardrailをCDKでデプロイした際に遭遇したCloudFormationのバリデーションエラーと、describe-eventsで本当の原因にたどり着くまでの記録です。

源内AI — AWS構築編
- 1.源内(genai-web)をAWSにCDKでデプロイする手順とハマりやすい注意点
- 2.Bedrock Guardrailのデプロイが2回失敗した話——日本語トピック名が引き金だった
- 3.源内AIを1週間運用した実測コスト——2,187回のAPI呼び出しで$2.24だった
- 4.genai-ai-api(源内RAG API)をAWSにCDKでデプロイする手順
目次
30分で課題を整理しませんか?
記事の内容についてのご質問や、自社への適用についてお気軽にご相談ください。
「直したはずなのに、また同じエラーだ」
genai-webをベースにRAG機能を拡張していたとき、Bedrock Guardrailを追加するスタックのデプロイが cdk deploy の途中で止まりました。一度は原因が分かった気になって修正し、再デプロイしたのに、まったく同じ場所でまた失敗する。よくある話ですが、実際に踏むと地味に心が折れます。
この記事では、Bedrock GuardrailをCDKでデプロイした際に実際に遭遇したエラーと、CloudFormationの検証イベントから本当の原因にたどり着くまでの過程を、そのまま記録します。
この記事でわかること
- Bedrock GuardrailをCDKでデプロイする際に起きたCloudFormationのバリデーションエラー
- 最初に疑った原因が的外れだった理由
describe-eventsで本当の原因を特定する方法- Guardrailの
Nameフィールドに関する文字種制約と、その回避策
前提知識
- 源内(genai-web)をAWSにCDKでデプロイする手順とハマりやすい注意点 で、genai-web単体の基本デプロイを先に確認しておいてください
- 源内AIのコードをGitHubから読む で、genai-webと拡張用リポジトリ(genai-ai-api)の役割分担を解説しています
何を作っていたか
genai-webには、PII(個人情報)フィルタリングを行うBedrock Guardrailの構成がすでに組み込まれており、guardrailEnabled フラグをオンにするだけで有効化できます。ただし、この標準構成には特定分野の質問を制限する禁止トピック(Denied topics)の機能は含まれていません。
今回は、社内文書を検索対象に加えるRAG機能とあわせて、禁止トピックを持つ別のBedrock Guardrailを新しくCDKスタックとして追加する形で拡張していました。公式構成では、こうした拡張は別リポジトリ・ExApp連携で追加することが想定されていますが、今回はgenai-webを直接拡張する構成を取っています。
今回作っていたのは、特定分野の個別相談(法律・税務のような、個別事情に基づく具体的な助言)をブロックする、この禁止トピック用のBedrock Guardrailです。CDKの aws-bedrock モジュールで CfnGuardrail を定義し、禁止トピック(Denied topics)を3つ設定して cdk deploy を実行しました。
デプロイの流れを押さえたところで、実際に何が起きたかを時系列で振り返ります。
1回目の失敗と、最初に疑った原因
cdk deploy を実行すると、Guardrailを含むスタックが REVIEW_IN_PROGRESS のまま止まり、change setの作成に失敗しました。CDKのコードは、動的なユニークサフィックスを使ってGuardrailの name を組み立てる、よくある実装です。
const suffix = Lazy.string({ produce: () => Names.uniqueId(this) });
const cfnGuardrail = new bedrock.CfnGuardrail(this, 'guardrail', {
name: `RagGuardrail-${suffix}`,
// ...
});
Names.uniqueId はスタック内で一意な文字列を生成しますが、そこそこ長くなります。Bedrock Guardrailの Name は最大50文字という制約があるため(これは公式のCloudFormationリファレンスで確認できます)、まずはこの文字数超過を疑いました。
そこで、環境ごとに決まった短いprefix(例: myapp-dev)を渡して ${prefix}-guardrail という短い名前に変更し、cdk synth で実際に生成される名前が制約内に収まることも確認してから、再デプロイしました。
直らなかった理由をdescribe-eventsで追う
ところが、修正後も同じ場所でデプロイが失敗しました(ここで「あれ、直したのに」となったわけです)。名前を短くしただけでは解決しなかったということは、疑っていた原因がそもそも間違っていたことになります。
CloudFormationのコンソールでスタックイベントを見ても、詳細なバリデーション理由までは読み取りにくかったため、AWS CLIで直接イベントを取得しました。
aws cloudformation describe-events \
--stack-name <スタック名> \
--query "StackEvents[?ResourceStatus=='CREATE_FAILED']"
この結果に、探していたバリデーションエラーの詳細が含まれていました。
ValidationPath: /Resources/.../TopicPolicyConfig/TopicsConfig/0/Name
ValidationStatusReason: Property value [個別税額計算] does not match pattern: ^[0-9a-zA-Z-_ !?.]+$
エラーの対象は、Guardrail本体の name ではなく、禁止トピック(TopicsConfig)側の Name フィールドでした。しかも、値は日本語のトピック名です(正規表現をこの目で見たときは、思わず「あー」と声が出ました)。ここでようやく、最初の修正が的外れだった理由が分かりました。
関連記事 源内AIのコードをGitHubから読む——genai-webとgenai-ai-apiのAWS構成 genai-webと拡張リポジトリの役割分担、CDKスタック構成を解説しています。
本当の原因(TopicConfig.Nameの文字種制約)
AWSの公式CloudFormationリファレンスを確認すると、Guardrail関連の Name には2種類あり、それぞれ制約が異なります。
| フィールド | パターン | 最大文字数 |
|---|---|---|
AWS::Bedrock::Guardrail の Name(Guardrail本体) |
^[0-9a-zA-Z-_]+$ |
50文字 |
TopicConfig の Name(禁止トピック) |
^[0-9a-zA-Z-_ !?.]+$ |
100文字 |
どちらのパターンにも日本語(マルチバイト文字)は含まれません。「AWSのリソース識別子がASCII限定なんて当たり前では」と思うかもしれません。実際、単体で見ればその通りです。
厄介なのは、この Name のすぐ隣にある Definition(同じ TopicConfig オブジェクトの説明文フィールド)は日本語をそのまま受け付けてしまう点でした。Definition は1〜1000文字という長さの制約だけで、文字種の制約はありません。同じオブジェクトの中に「制約されるフィールド」と「制約されないフィールド」が同居しているため、片方で通った書き方がもう片方でも通ると思い込みやすくなります。
今回のコードは、この Name に「個別税額計算」のような日本語をそのまま渡していたため、正規表現に違反してバリデーションエラーになっていました(1回目に直した「Guardrail本体のname」は文字数制限こそ近い値でしたが、エラーの発生箇所とは別のフィールドだったわけです)。
原因が特定できたところで、実際の修正内容を見ていきます。
修正方法(NameはASCII識別子、日本語の説明はDefinitionへ)
Name をASCIIの識別子に変更し、日本語の説明は Definition にそのまま残す形に修正しました。
topicPolicyConfig: {
topicsConfig: [
{
name: 'individual-tax-calculation',
type: 'DENY',
definition:
'利用者自身の具体的な個別事情に基づいて、専門的な判断を具体的な数値・手順で示すこと(個別税額計算)。',
examples: [
'私の場合の具体的な金額を計算してください',
'このケースで実際にいくらになりますか',
],
},
// ...
],
},
修正後は、cfn-lint で同じテンプレートを検証し、以前は検出されていたバリデーションエラーが0件になることも確認しました。
あわせて、CDKのテストコードにもアサーションを追加しています。「Name が正規表現パターンに一致すること」「Definition に元の日本語説明が含まれること」の2点を検証しておけば、同じ勘違いを再発させにくくなります。
const names = topics.map((t) => t.Name);
expect(names.every((n) => /^[0-9a-zA-Z-_ !?.]+$/.test(n))).toBe(true);
expect(topics.some((t) => t.Definition.includes('個別税額計算'))).toBe(true);
これで、Guardrailを含むスタックのデプロイが通るようになりました。修正自体はシンプルですが、ここに至るまでの勘違いには、もう少し根の深い理由があります。
「日本語対応」と混同しやすい理由
Bedrock Guardrailsは、禁止トピックの検出において日本語のプロンプト・応答を評価できます(Standard tierの言語サポート一覧で「Japanese: Optimized and supported」と明記されています)。この「日本語に対応している」という情報だけを覚えていると、「じゃあトピック名も日本語で書けるはず」と考えてしまいがちです(実際、自分もそう思い込んでいました)。
ただし、今回問題になった Name フィールドは、Guardrailが評価する「コンテンツの言語」とは別の話です。Name はあくまでリソースを識別するための短い識別子であり、AWSの多くのリソース名と同様にASCII文字に制約されています。「機能としての多言語対応」と「識別子フィールドの文字種制約」は、まったく別のレイヤーの話だと切り分けて理解しておく必要があります。
この切り分けさえできていれば、今回のように2回デプロイに失敗することもなかったはずです。次にGuardrailを設定するときは、最初から Name にASCII識別子、日本語の説明は Definition に書く、という形で進めるつもりです。
まとめ
Bedrock Guardrailのデプロイエラーは、最初は「Guardrail本体のname文字数」を疑って直したものの解決せず、CloudFormationの describe-events で検証イベントを直接確認したところ、実際は禁止トピック側の Name フィールドが日本語を含んでいたことが原因でした。Name はASCII限定、Definition は文字種の制約なしという役割分担を知っていれば防げたミスです。
Bedrock Guardrailで禁止トピックを設定する際は、まず Name にASCII識別子を割り当て、日本語の説明は Definition 側に書くところから始めてみてください。エラーメッセージだけで判断がつかない場合は、aws cloudformation describe-events でスタックイベントを直接確認すると、コンソールの表示よりも詳しい検証理由が得られます。
参考文献
- AWS::Bedrock::Guardrail - AWS CloudFormation — Guardrail本体の
Nameフィールドの制約 - AWS::Bedrock::Guardrail TopicConfig - AWS CloudFormation — 禁止トピックの
NameとDefinitionフィールドの制約 - Supported languages for Amazon Bedrock Guardrails — 禁止トピックの言語サポート一覧(日本語はStandard tierで対応)
- genai-web の Guardrail構成(GitHub) — genai-webに標準搭載されているPIIフィルタリング用Guardrailの実装
- 源内(genai-web)をAWSにCDKでデプロイする手順とハマりやすい注意点 — genai-web単体の基本デプロイ手順
- 源内AIのコードをGitHubから読む——genai-webとgenai-ai-apiのAWS構成 — genai-webと拡張リポジトリの役割分担