はじめに
こんにちは。株式会社ファンリードの森 龍太です。
いつもは活動名Martimでやらせてもらっていますが、今回Jr.Championに選出いただきました!
この記事は「2026 Japan AWS Jr. Champions 真夏のQiitaリレー」の20日目の記事となります。
過去の投稿(リンク集)はこちらからご覧ください! https://qiita.com/ys-yoshida/private/6f7c7f85155a993e2c86
さて、今回は社内で生成AI(ChatGPTなど社外のAIサービス)にプロンプトを貼る前に、個人情報を自動でマスキングしてくれるツール「Prompt Masking Tool」を個人的に作ってみたので、その中身と作りながらハマったところを共有していきます!
本記事の情報は2026年8月20日時点のものです。個人的に調べ・作ったものであり、業務として導入されたものではありません。
皆さん、こんな経験はありませんか?
「このエラーログ、ChatGPTに聞きたいけど社員名とメアドが入ってるな……」
「顧客情報をマスクしてから貼るの、毎回手作業で面倒くさい」
「そもそも何をマスクすればいいのか、毎回考えるのがしんどい」
私自身、まさにこの状況に困って作ったのが今回のツールです。
目次
「Prompt Masking Tool」とは
入力したテキストのPIIをAmazon Bedrock Guardrailsでマスキングして表示するだけのツールです。氏名・メールアドレス・電話番号・住所・クレジットカード番号といった個人情報や、会社名のような固有名詞を自動で検出し、[NAME]のような識別子タグに置き換えて表示してくれます。
ポイントは、AIとの会話機能を一切持たないことです。マスキング後のテキストは自分でコピーして、使いたいAIチャットに貼り付けて使う、という前提の設計にしています。
(ここにマスキング前後の画面のスクショ)
機能としてはシンプルに絞っていて、実質2つしかありません。
-
マスキング設定の編集:画面上の「Guardrail設定」パネルから、マスキングのタグ名(
[NAME]のような識別子)と対象パターン(正規表現)を、プログラミング知識無しで追加・編集・削除できる - マスキング前後の表示:テキストを貼り付けて「マスキングする」ボタンを押すと、マスキング前後を並べて表示する。氏名・メールアドレス・電話番号・住所・年齢・クレジットカード番号(Bedrock Guardrails標準のPII検知)や、会社名などカスタム登録した固有名詞が対象
補足として、マスキング後のテキストはワンクリックでコピー可能で、アプリ自体はローカルPC上でのみ動作(127.0.0.1限定)し、外部やLANからはアクセスできません。
このリポジトリはGitHubで公開しており、.envやprivate-notes.mdにあたる環境固有の値(AWSアカウント・Guardrail ID等)を自分の環境に合わせて設定すれば、誰でも同じものをそのまま使えます。もともとは自社での利用を想定して作りましたが、コード自体には会社固有のロジックは無いので、「プロンプトを送る前にPIIだけマスキングしたい」というニーズがあれば、他社の方に使ってもらっても構わないという前提で公開しています。
できないこと・あえてやらなかったこと
- AIとの会話(Claudeなどへの送信)はしない
- 複数人でのログイン管理・利用履歴の保存は無い
- 「契約書の内容」のような、パターンでは検知できない意味的な情報の検知はしていない(Bedrock GuardrailsのDenied Topics機能を使えば理論上は可能だが未着手)
→ つまり、「マスキングして終わり」の割り切ったツールです。
なぜこの形になったか:要件転換の話
実はこのツール、最初からこのシンプルな形だったわけではありません。個人的に一番おもしろかったのがこの経緯です。
最初に手を付けたのは、Bedrock Guardrailsでマスキング → Amazon Bedrock経由のClaude(Converse API)に送信して会話する、というチャットアプリでした。複数セッション管理、簡易ログイン、DynamoDBでの会話履歴の永続化まで、それなりに作り込みました。
ところが、ある程度形になったところで「実際の運用は、マスキング済みのテキストをChatGPTなど別のAIサービスに手動で貼り付けて使う」という使われ方が実態であり、そもそもAIとの会話機能は要らないということになりました。
この時点でチャット・ログイン・セッション管理・DB連携のコードを全部削除し、「マスキングして表示するだけ」のシンプルな構成に作り直しています。
「会話させたいのか、前処理・後処理だけしたいのか」で設計がまるっきり変わる、というのは、個人的にかなり実感の伴った学びでした。最初に「何を作るか」を確定させないまま作り始めると、後戻りのコストが想像以上に大きくなります(この一連の作り直し、正直かなり長かったです……)。
構成
構成はいたってシンプルです。
| コンポーネント | 役割 |
|---|---|
Streamlitアプリ(app.py) |
テキスト入力、マスキング実行・表示、Guardrail設定パネル |
| Bedrock Guardrails | PIIマスキング本体(ApplyGuardrail)。設定変更はUpdateGuardrail
|
DB・認証基盤・常時稼働サーバー・ロードバランサーは無し。各自のPCでバッチファイルをダブルクリックしてローカル起動する運用にしています。
ここが少し分かりにくいポイントなのですが、アプリ本体はローカルでしか動かないのに、サーバーを立てる必要がありません。理由は、マスキングのタグ名・対象パターンといった「設定」自体が、ローカルではなくAWS側のBedrock Guardrailsリソースに保存されているためです。チームの全員が同じGuardrail ID(同じGuardrailリソース)を向けてアプリを起動していれば、誰か1人が「Guardrail設定」パネルで設定を変更した時点で、その変更はGuardrailリソース側に反映され、他のメンバーのアプリからも次回以降同じ設定として見えます。アプリはローカル、設定は共有という分担にしたことで、チームで使う場合でも専用サーバーを構築・維持する必要が無い構成にできました。
アプリ専用のIAMユーザーを分離し、bedrock:ApplyGuardrailなどの最小権限のみ付与しています。AWSアカウントIDやGuardrail IDなど環境固有の値は、リポジトリには含めず利用者ごとに手元で管理する形にしています。
つまずいたところダイジェスト
開発中に発生した問題を、カテゴリ別にまとめます。特にBedrock Guardrails周りは、ドキュメントだけでは分からない実装の癖が複数あり、実機で検証して初めて気づいたものが多かったです。
個人的な検証環境(Windows + ローカルPC)での話であり、全ての環境で同じ現象が起きるとは限りません。
AWS認証・権限まわり
| 事象 | 原因・対応 |
|---|---|
InvalidClientTokenId |
メインIAMユーザーの認証情報が失効。新しいアクセスキーを発行し直して解決 |
| MFA必須ポリシーで全操作拒否 |
BlockNonMFARequestsにより、Bedrockを含むほぼ全操作が実行不可。アプリ専用の新規IAMユーザーを作成し、ポリシー対象外にすることで解決。常時稼働するアプリが人間のMFA操作を求められるのは運用上成立しないため、専用ユーザーの分離は必須だった |
| 権限不足の段階的な発覚 |
iam simulate-principal-policyで検証しながらbedrock:ApplyGuardrail等を都度追加。なおbedrock:Converseはbedrock:InvokeModelで権限がカバーされており別途追加不要、と公式ドキュメントで確認 |
| Marketplaceのエラー | Bedrock上のAnthropicモデルは初回利用時にAWS Marketplace経由の自動サブスクライブが走る仕様。該当権限をaws:CalledViaLast: bedrock.amazonaws.com条件付きで追加して解決(この機能自体は後にAI会話機能ごと撤去) |
ValidationException(オンデマンド呼び出し不可) |
生のfoundation-model IDではなく推論プロファイルIDの指定が必要だった |
Bedrock Guardrails特有の仕様
Guardrailを作ってPII種別を設定したのに、ApplyGuardrailをsource=INPUTで呼んでも全くマスキングされない(action: NONEが返る)という現象に遭遇しました。
原因は、PII設定にはactionという古い/簡易フィールドとは別に、inputAction/inputEnabled/outputAction/outputEnabledという入出力を個別制御するフィールドがあり、これを明示しないと入力側(INPUT)のマスキングがデフォルトで無効になるという仕様でした。
# NG: inputAction/inputEnabledを指定しないと source=INPUT で機能しない
{"type": "EMAIL", "action": "ANONYMIZE"}
# OK
{
"type": "EMAIL",
"action": "ANONYMIZE",
"inputAction": "ANONYMIZE",
"inputEnabled": True,
"outputAction": "ANONYMIZE",
"outputEnabled": True,
}
なお、この入出力個別制御自体は2025年4月のBedrock Guardrailsのアップデートで追加された機能とのことで、公式ドキュメントにも記載はあるものの、実際にAPIレスポンスを見て初めて「あ、明示しないとダメなんだ」と腑に落ちた、という感覚でした(地味にここで結構な時間を溶かしました……)。
正規表現の誤検知
会社名の略称を単体でマスキング対象にしようとしたところ、実機検証で以下が判明しました。
- 単語境界付きの正規表現は、日本語がそのまま隣接する文(「◯◯の担当者」のような形)には全くマッチしない(Pythonの正規表現は日本語も「単語文字」とみなすため、英字と日本語の間に境界が生まれない)
- 境界無しにすると、無関係な英単語まで誤ってマスキングしてしまう
→ つまり、「会社名だと明確に分かる文脈のみにパターンを限定する」形で解決しました。
環境構築・細かいもの
ここから先は正直「あるある」に近いですが、一応記録として残しておきます。
- Streamlitがlocalhost実行を「開発中」とみなし、
c/rキーに開発者向けショートカットを割り当てていたため、コピーのつもりで押したCtrl+Cの「C」だけ拾われてキャッシュクリア確認ダイアログが出る現象が発生。toolbarMode = "minimal"設定で解決 - 日本語ファイル名のバッチファイルが実行できない、
startがcmd.exeの組み込みコマンドと衝突する、といった問題があり、最終的にバッチファイルの中身・ファイル名は全てASCII文字に統一 - PowerShellのワンライナーをバッチファイルに埋め込むと引用符が壊れる問題があり、ロジックを別の
.ps1ファイルに切り出して解決 - 保存完了の
st.toast()通知がst.rerun()のタイミングと重なって一瞬で消える問題があり、session_stateにフラグを立てて次の描画冒頭で表示する方式に変更
Bedrock Guardrailsのコスト感
参考までに、Bedrock GuardrailsのPII/機密情報フィルターは**$0.10 / 1,000 text units**(1 text unit = 最大1,000文字)です。カスタム正規表現によるフィルターは無料のため、会社名のような固有名詞をカスタム登録で検知する分には追加コストはかかりません。個人の検証利用の範囲では、実質ほとんど気にならないレベルの金額でした。
今後の課題
正直まだ完成形ではないので、今分かっている宿題を並べておきます。
- なりすまし対策: 現状ログイン機能は撤去しているため、画面を開ける人なら誰でもGuardrail設定を変更できる状態。変更履歴を残したい・編集権限を絞りたい場合は、簡易ログイン+管理者制限の再導入が必要
- 意味的な検知: 「契約書の内容」等、正規表現では検知できない情報分類は未対応
- Docker配布の完成: Dockerfile・起動スクリプトは作成済みだが、実機でのビルド・動作確認は途中
- 会社名マスキングの網羅性: 現状は登録した固有名詞のみが対象で、未知の会社名は検知できない
おわりに
生成AIに何かを貼り付ける前の「ちょっと待って、これ個人情報入ってないか?」を、毎回自分の目視だけに頼らずに済むようにしたい、というのが今回のツールの出発点でした。作りながら何度も設計を作り直すことになりましたが、結果的に「マスキングだけに徹する」というシンプルな形に落ち着いたのは、個人的には良い経験だったと思っています。
同じようにAWS Bedrock Guardrailsを使ってみようとしている方の参考になれば嬉しいです。
それでは!よいAWSライフを!