NTT西日本のブログ - TECH PLAY

TECH PLAY

NTT西日本

NTT西日本 の技術ブログ

85

はじめに NTT西日本の中川です。 本記事ではフロントエンド(ブラウザ)だけで利用できるデータの保持方法とデータベースサービスを厳選してご紹介します。 本記事は、2025年12月時点の情報に基づきます。 対象読者 本記事が想定する対象読者は次の通りです。 駆け出しエンジニア フロントエンド縛りでデータの保持が必要な状況の人 データの保持に対して難しそうなイメージを持っている人 背景 プログラミングの勉強を始めたばかりの方や、簡単なアプリケーション開発に挑戦しようとしている方は、多くの場合、まず「どこにデータを保存するか」という課題に直面するのではないでしょうか。 最初はデータを保存しないシンプルなアプリから始めると思いますが、開発を進めるうちに 「データの保持」 というテーマは避けて通れない領域になってきます。 しかし、プログラミング学習を始めたばかりの段階では、本格的なデータベースを用意し、サーバーサイドの環境を構築するのは、個人開発の初期段階では少し敷居が高く感じられます。 「できるだけお金を使わずに」「できるだけ手軽に」 データを保持したいというのが本音ではないでしょうか。 私自身もデータをどう扱うかについて悩んだ経験があります。 サクッと作ってどんどん数をこなしたい、という当時の自分だったらという個人的なニーズが、今回の記事作成のきっかけです。 この記事が、皆さんのエンジニアライフに少しでも役立てば嬉しいです。 本記事で紹介するデータ保持アプローチの全体像 フロントエンド開発だけでデータを保持(ブラウザを閉じる/アプリのタブを閉じてもデータを保持すること)するには、大きく分けて以下の2つのアプローチがあります。 クライアントサイドで保持 : データがユーザーのブラウザ(デバイス)内に保存される サーバーサイドで保持 : データがサーバーに保存される 例:PaaS、BaaS上などのサーバーまたはストレージ 今回は、特に入門に適した以下の方法を解説します。前半のクライアントサイドの2つをメインに見ていきましょう。 カテゴリ 方法 データの保存場所 主な用途 クライアントサイドで保持 LocalStorage ユーザーのブラウザ内 (簡易データ) ユーザー設定、セッション情報の保持、簡易的なオフライン利用 IndexedDB ユーザーのブラウザ内 (構造化データ) 大量の構造化データの保持、本格的なオフラインアプリケーション (PWA) サーバーサイドで保持 Supabase/Firebase サーバー上のデータベース 複数ユーザー間の共有、認証・認可が必要な本格アプリケーション 1. LocalStorage LocalStorageは、JavaScriptの標準機能で利用できる、最も手軽なデータ保持方法の一つです。 データはユーザーのブラウザ内に保存されます。 特徴 事前準備 : JavaScriptの標準機能なのでブラウザだけですぐに利用できます 構造 : キーと値のシンプルなセット(Key-Valueストア) 保存形式 : 文字列 (String) のみ。オブジェクトや配列を保存するには JSON.stringify() で文字列化が必要 データ量 : 比較的小さい(5MB程度) 用途 : ユーザー設定(ダークモード/ライトモード)、セッション情報、簡易なToDoリストなど サンプルコード(データの保存と取得) // 【保存】キー 'username' に 'Nakagawa' という文字列を保存する localStorage . setItem ( "username" , "Nakagawa" ) ; console . log ( "データを保存しました" ) ; // 【取得】キー 'username' の値を取得する const storedUsername = localStorage . getItem ( "username" ) ; console . log ( "取得したデータ:" , storedUsername ) ; // 出力: 取得したデータ: Nakagawa // 【削除】キーを指定してデータを削除する localStorage . removeItem ( "username" ) ; console . log ( "データを削除しました" ) ; データの保存、取得、削除が3行ほどで完結します。 格納されたLocalStorageの値はブラウザの検証ツールで確認できます。 今回は、Google Chromeを利用して確認していきます。 検証ツールの起動方法 起動 Windows : Ctrl + Shift + I または F12 Mac : Command + Option + I 確認箇所 Applicationタブを選択 サイドバーのLocalStorageを選択すると自身のサイトのURLがあるので、それをクリック サンプルコードで登録した username が登録されていることを確認できる  補足(オブジェクトを保存する場合) オブジェクトを保存する場合は、値の保存/取得時に文字列変換↔︎JSON変換が必要なので要注意 const userSettings = { theme : "dark" , notifications : true } ; // ダークモードなどの設定を想定 // オブジェクトを文字列(JSON)に変換して保存 localStorage . setItem ( "settings" , JSON . stringify ( userSettings )) ; // 取得後、JSONを元のオブジェクトに戻す const storedSettingsString = localStorage . getItem ( "settings" ) ; const storedSettingsObject = JSON . parse ( storedSettingsString ) ; console . log ( "取得したオブジェクト:" , storedSettingsObject . theme ) ; Tips かつて広告トラッキングやコンバージョン計測などではよくCookieが使われていました。 数年前からAppleがSafariに ITP(Intelligent Tracking Prevention) と呼ばれるユーザーのウェブ上でのトラッキングを制限することを目的としたプライバシー保護機能を搭載したことで(Cookieの有効期間などへの規制が盛り込まれています)、LocalStorageが代替案として使われてきましたが、 最近、さらに要件が厳しくなってきています。 LocalStorageは意外と身近に使われている機能なので、興味があれば色々調べて見るととても面白いです。 2. IndexedDB IndexedDBは、LocalStorageよりも大容量で、本格的なデータベース機能をブラウザ内で提供してくれます。 非同期処理が前提で、リレーショナルデータベースに近い構造化データを扱えますので、 開発の勉強やちょっとした処理にも便利です。 特徴 構造 : オブジェクトストア(NoSQLのコレクションに相当) 保存形式 : JavaScriptのオブジェクトをそのまま保存可能 データ量 : 比較的大容量(数GBまで) 用途 : オフラインで動作するPWA (Progressive Web App) のキャッシュ、大量の構造化データ保持 サンプルコード(データの保存と取得) IndexedDBは複雑なため、今回はデータの追加 (Add) と取得 (Get) に焦点を当てたシンプルな例を紹介します。 // データベース名とバージョンを定義 const DB_NAME = 'MyTodoDB' ; const DB_VERSION = 1 ; const STORE_NAME = 'tasks' ; // データベースを開く(存在しない場合は作成) const request = indexedDB . open ( DB_NAME , DB_VERSION ) ; let db ; // データベースのバージョンが変わった時(初回作成時も含む)に実行される request . onupgradeneeded = ( event ) => { db = event . target . result ; // オブジェクトストアを作成(ここでは 'id' をキーとする) db . createObjectStore ( STORE_NAME , { keyPath : 'id' , autoIncrement : true }) ; } ; // 接続成功時 request . onsuccess = ( event ) => { db = event . target . result ; console . log ( 'IndexedDB接続成功' ) ; // サンプルデータの追加と取得を実行 addTask ({ text : '記事を完成させる' , priority : 'High' }) ; getAllTasks () ; } ; request . onerror = ( event ) => { console . error ( 'IndexedDBエラー:' , event . target . error ) ; } ; // 【保存】データの追加関数 function addTask ( task ) { // トランザクションを開始し、読み書きを許可 const transaction = db . transaction ([ STORE_NAME ] , 'readwrite' ) ; const store = transaction . objectStore ( STORE_NAME ) ; // オブジェクトをそのまま追加 const addRequest = store . add ( task ) ; addRequest . onsuccess = () => { console . log ( 'タスクが正常に追加されました:' , task ) ; } ; addRequest . onerror = ( e ) => { console . error ( 'タスク追加エラー:' , e . target . error ) ; } ; } // 【取得】全データの取得関数 function getAllTasks () { const transaction = db . transaction ([ STORE_NAME ] , 'readonly' ) ; const store = transaction . objectStore ( STORE_NAME ) ; const getRequest = store . getAll () ; getRequest . onsuccess = () => { console . log ( '全タスク:' , getRequest . result ) ; } ; } 格納された値はLocalStorage同様にブラウザの検証ツールで確認できます。 こちらも、Google Chromeを利用して確認していきます。 検証ツールの起動方法 起動 Windows : Ctrl + Shift + I または F12 Mac : Command + Option + I 確認箇所 Applicationタブを選択 サイドバーのIndexedDBを選択すると先ほど作成した「MyTodoDB」があるので、それをクリック tasksテーブルをクリック サンプルコードで登録した MyTodoDB と tasks が登録されていることを確認できる 実行方法の補足 ここまで紹介したブラウザで動く LocalStorage と IndexedDB は、試すだけなら実はわざわざファイルを作成する必要はありません。 結果を確認するために使っている検証ツールの「 Console 」というタブに、今回紹介しているコードをそのまま貼り付けるだけで動きます。 LocalStorageの章で紹介した内容をConsoleにコピペして実行 3. サーバーサイドにおけるデータ保持の紹介 最後に、クライアントサイドの制約(データ量の制限、複数ユーザー間での共有ができない)を超えて、本格的なアプリケーション開発に進むためのアプローチを簡単に紹介します。 BaaS (Backend as a Service) は、サーバー側の機能(データベース、認証、ストレージなど)を、APIを通じてフロントエンドから手軽に利用できるように提供するサービスです。サーバーサイドの構築作業を行わずに、高度なデータ保持を実現できます。 Supabase と Firebase SupabaseはPostgreSQLベースのリレーショナルなデータベース、FirebaseはNoSQLのリアルタイムデータベースを提供しています。どちらも無料枠があり、駆け出しのエンジニアが本格的なWebサービスを開発する際のデータ保持に適しています。 用途 : 複数ユーザー間のデータ共有、ユーザー認証・認可、リアルタイム通信が必要なチャットアプリなど。 特徴 : フロントエンドのコードから、サービスが提供する専用のライブラリ(SDK)を使って、クラウド上のデータベースに直接データの読み書きができます。 これらのサービスは、さらに次のステップに進むための強力な武器になります。 このブログでも機会があれば紹介したいなと思います。 一層本格的なアプリケーションに挑戦する際には、ぜひ調べてみてください。 まとめ 本記事では、プログラミング初心者やフロントエンド開発を主軸とする方が、手軽かつ安価にデータを保持するための主要なアプローチを解説しました。 この記事を通じて、データの保持は決して難しいものではなく、実は手軽に開発のニーズに合わせて適切なツールを選べることが伝えられていたら嬉しいです。まずはLocalStorageやIndexedDBで実際に手を動かし、開発を加速させていきましょう!💪 執筆者 中川 拓哉(NTT西日本 デジタル革新本部 デジタル改革推進部所属) NTT西日本の法人向けの顧客ポータルサイトを開発・運営しています。 TypeScript, Vue.js, GraphQL, Laravelが好きです。 参考文献 IndexedDB の使用 - Web API | MDN ウェブストレージ API - Web API | MDN 商標 「Supabase」は、Supabase Inc.(米国デラウェア州法人)の商標もしくは商標登録です 「Google Chrome」「Firebase」は、Google LLCまたはその関連会社の商標もしくは登録商標です
はじめに NTT西日本エンタープライズビジネス営業部の吉田泰隆です。 本記事ではAWSが提供する生成AIアシスタント「Amazon Q」の性能を3つの観点で検証・評価してみました。 1観点1記事、合計3つの連載となりますので、最後までお読みいただけると嬉しいです。 気になる記事からの閲覧も大歓迎です。 Amazon Qでどこまでできるのか?~1.リソースコントロール編~ (本記事) Amazon Qでどこまでできるのか?~2.セキュリティ編~ Amazon Qでどこまでできるのか?~3.トラブルシューティング編~ 本記事は2025年10月時点の情報に基づきます。 対象読者 本記事が想定する対象読者は以下の通りです。 Amazon Q を利用している人 生成 AI の実践的な活用方法に関心がある人 クラウドエンジニアとして 生成AI に代替されないスキルを知りたい人 AWS 主要サービスを日常的に扱っている人 システム検証や検証レポートを業務に活かしたい人 生成AI の限界やリスクも含めて理解したい人 背景  近年、生成 AI が急速に普及し、クラウドエンジニアリングの分野でも活用可能性が広がっています。 私自身も 生成AIに強い関心を持ち、実際にどこまで現場で役立つのかを確かめたいと考えました。 また検証を通じて、生成AI に任せられる領域と人間のエンジニアが担うべき領域を明確化し、今後の業務に活用していくことを最終目的としています。 検証概要 AWS 環境での構築や保守・運用といった具体的なシナリオに適用できるかを以下3観点から評価します。 リソースコントロール (本記事) セキュリティ トラブルシューティング 冒頭「はじめに」でも述べましたが、本記事ではリソースコントロールに焦点をあてます。 1つの観点につき難易度の異なる2つシナリオで検証します。 検証条件・補足 Amazon Q Developer Pro (バージョン q 1.13.2 )を利用します。 (Amazon Linux 2023にインストーラを利用して Q Developer CLIをインストール) Amazon Qの回答をすべて記述するとかなり長いため、要所要所で「~略~」を使って省略をしています。 リソースID等の情報保護のため、「※※※」を使ってマスキングをしています。 シナリオ①「CloudWatch アラーム作成」 内容 EC2インスタンスのCPU使用率を監視するCloudWatchアラームを作成してもらいます。 ※EC2は事前に構築しています。 基本的なリソース作成における自動補完の精度と安全性を確認します。 プロンプト EC2インスタンス(i-※※※)のCPU使用率を監視し、 80%超過時に反応するCloudWatchアラームを作成してください。通知等は不要です。 回答結果 Amazon Qの結果がこちらです。 > EC2インスタンス「i-※※※」のCPU使用率が80%を超過した時に 反応するCloudWatchアラームを作成します。 🛠️ Using tool: use_aws (trusted) ⋮ ● Running aws cli command: ~略~ ⋮ ● Completed in 2.312s > CloudWatchアラームが正常に作成されました。 作成されたアラームの詳細: • **アラーム名**: test1-1-cpu-high-alarm • **監視対象**: EC2インスタンス i-※※※ のCPU使用率 • **閾値**: 80% • **条件**: CPU使用率が80%を超過 • **評価期間**: 2回連続(10分間) • **統計**: 平均値 • **監視間隔**: 5分毎 • **タグ**: test1-1 このアラームは以下の動作をします: • 5分間隔でCPU使用率を監視 • 連続2回(10分間)CPU使用率が80%を超えた場合にアラーム状態になる • 通知アクションは設定していないため、アラーム状態の変化のみを記録 アラームの状態はAWSコンソールのCloudWatchダッシュボードで確認できます。 CloudWatchアラームの構築が問題なく完了しました。 コンソールでも確認できました。 考察 動作条件・パラメータの説明はありましたが、指定しないパラメータは生成AIによって自動補完されてしまうことがわかります。 指定したパラメータ      自動補完されたパラメータ    EC2インスタンスID 監視メトリクス 閾値 通知設定 アラーム名 評価期間 統計方法 監視間隔 タグ ハンズオンレベルなら利用しても問題ないですが、本番環境では十分な注意が必要と思いました。というのも、 生成AIが良かれと思って設定した値が、システムの要件と合致せず、重大なインシデントの見逃しに繋がる危険性があるため です。 具体的には、以下のようなリスクが考えられます。 1.意図しない監視条件による障害の見逃し 瞬間的なCPUスパイクや短時間でも負荷が続くことが問題となるシステムであれば、障害の兆候を完全に無視してしまう可能性があります。 2.運用・管理上の混乱 一般的に、作成するリソースにはシステムや案件ごとに命名規則があります。ルールから外れたリソースが自動で作成されると、誰が何のために作ったのか分からなくなり、棚卸しやコスト管理、自動化処理の妨げになるなど、運用上の大きな混乱を招きます。 もちろん、詳細パラメータを事前に指定しなかったこちら側の非でもありますが、厳密な監視要件や運用ルールが求められる本番環境では、生成AIによる設定を安易に受け入れてはいけません。利用するにしても、構築する前の第三者レビューは必須です。 シナリオ②「VPCエンドポイント作成」 内容 シナリオ①よりも、さらに複雑にしてみます。 EC2がS3にインターネットを経由している状態から、インターネットを経由しない接続に構成を変更(ゲートウェイVPCエンドポイント作成・ルートテーブルの修正)してもらいます。 ※EC2とS3バケットは事前に構築しています。 さらに、バケットポリシーを以下のように設定しました。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Sid ": " DenyListBucket ", " Effect ": " Deny ", " Principal ": " * ", " Action ": " s3:ListBucket ", " Resource ": " arn:aws:s3:::※※※ " } ] } これによって、S3オブジェクト一覧の取得をすべてのユーザーに対して拒否させ、一筋縄ではいかないようにしました。 複雑な構成変更と問題解決能力、セキュリティ考慮の自律性を確認します。 プロンプト EC2「i-※※※」がS3「※※※ 」へのアクセスをインターネット経由なしにしたいので、 エンドポイントを作ってバケットの中身のリスト表示をできるようにしてください。 回答結果 まず、以下の設定変更を実施しました。 1. S3用VPCエンドポイントの作成 2. IAMロールとポリシーの作成 3. EC2インスタンスへのIAMロールアタッチ > EC2インスタンス「i-※※※」がS3バケット「※※※」に インターネット経由なしでアクセスできるよう、VPCエンドポイントを作成します。 まず、インスタンスのVPC情報を確認してから、S3用のVPCエンドポイントを作成します。 ~略~ ● Completed in 0.965s > 次に、ルートテーブル情報を確認します。 ~略~ ● Completed in 1.1s > EC2インスタンスが配置されているサブネット(subnet-※※※)は、 パブリックルートテーブル(rtb-※※※)に関連付けられています。 S3用のVPCエンドポイントを作成します。 ~略~ ● Completed in 1.271s > VPCエンドポイントが正常に作成されました。 次に、EC2インスタンスがS3バケットにアクセスできるよう、 IAMロールを確認し、必要に応じて作成・アタッチします。 まず、インスタンスに現在アタッチされているIAMロールを確認します。 ~略~ ● Completed in 1.4s > インスタンスにIAMロールがアタッチされていないようです。 S3にアクセスするためのIAMロールを作成し、インスタンスにアタッチします。 まず、IAMロールを作成します。 ~略~ ● Completed in 0.850s > 次に、S3の読み取り権限ポリシーを作成してロールにアタッチします。 ~略~ ● Completed in 0.833s > 次に、インスタンスプロファイルを作成し、ロールを追加します。 ~略~ ● Completed in 0.834s > 最後に、インスタンスプロファイルをEC2インスタンスにアタッチします。 ~略~ ● Execution failed after 1.10s: An error occurred (InvalidParameterValue) when calling the AssociateIamInstanceProfile operation: Value (EC2-S3-Access-Profile) for parameter iamInstanceProfile.name is invalid. Invalid IAM Instance Profile name > インスタンスプロファイルのARNを使用して再試行します。 ~略~ ● Completed in 1.235s この段階で終了すれば、バケットポリシーが修正されておらず、 エンドポイント経由でのアクセスができません。 Amazon Qも解けないかと思ったのですが、、、 > 設定が完了しました。S3バケットの内容を確認してみましょう。 ~略~ > S3のAPIを正しく使用します。 🛠️ Using tool: use_aws (trusted) ⋮ ● Running aws cli command: Service name: s3api Operation name: list-objects-v2 Parameters: - bucket: "※※※" Profile name: default Region: us-east-1 Label: S3バケットの内容を確認 ⋮ ● Execution failed after 0.849s: An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User: arn:aws:iam::※※※: root is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::※※※" with an explicit deny in a resource-based policy > バケットポリシーでアクセスが拒否されているようです。バケットポリシーを確認してみましょう。 ~略~ ● Completed in 0.805s > バケットポリシーでListBucketアクションが拒否されています。 VPCエンドポイント経由でのアクセスのみを許可するようにバケットポリシーを更新します。 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /tmp/bucket-policy.json + 1: { + 2: "Version": "2012-10-17", + 3: "Statement": [ + 4: { + 5: "Sid": "AllowVPCEndpointAccess", + 6: "Effect": "Allow", + 7: "Principal": "*", + 8: "Action": [ + 9: "s3:ListBucket", + 10: "s3:GetObject" + 11: ], + 12: "Resource": [ + 13: "arn:aws:s3:::※※※", + 14: "arn:aws:s3:::※※※/*" + 15: ], + 16: "Condition": { + 17: "StringEquals": { + 18: "aws:sourceVpce": "vpce-※※※" + 19: } + 20: } + 21: }, + 22: { + 23: "Sid": "DenyDirectInternetAccess", + 24: "Effect": "Deny", + 25: "Principal": "*", + 26: "Action": "*", + 27: "Resource": [ + 28: "arn:aws:s3:::※※※", + 29: "arn:aws:s3:::※※※/*" + 30: ], + 31: "Condition": { + 32: "StringNotEquals": { + 33: "aws:sourceVpce": "vpce-※※※" + 34: } + 35: } + 36: } + 37: ] + 38: } ⋮ ↳ Purpose: VPCエンドポイント経由のアクセスを許可するバケットポリシーを作成 Creating: /tmp/bucket-policy.json ⋮ ● Completed in 0.0s ~略~ Label: VPCエンドポイント経由のアクセスを許可するバケットポリシーを設定 ⋮ ● Completed in 1.4s > 設定が完了しました。 想定した構築を実施して終了!ではなく、EC2からS3へのアクセスできるかの試験まで実施していました。 その後、バケットポリシーを更新し、課題をクリアしました。 作成したVPCエンドポイントはこちらになります。 考察 単なる構築作業で終わらず、実際の動作確認まで自動実行した点が特筆すべき点です。 エンジニアからすれば当然の工程なのですが、こちらから指示していないことを自ら実行にうつした点は正直驚きました。 また、S3バケットポリシーも「S3へのアクセスをプライベートネットワーク経由に限定したい」という要求に対して的確な回答となっているのも非常に良い点で、セキュリティを遵守する能力があることも評価できました。(セキュリティに関しては、次回投稿で焦点を当てます。) (補足)バケットポリシーの解説 第1ステートメント(4~21行目):VPCエンドポイント経由のアクセス許可 項目 内容 効果 許可(Allow) 対象者 すべてのユーザー(Principal: "*") 許可アクション s3:ListBucket:バケット内のオブジェクト一覧取得 s3:GetObject:オブジェクトの読み取り 対象リソース 指定されたS3バケットとその配下のすべてのオブジェクト 条件 アクセス元が特定のVPCエンドポイント(vpce-※※※)である場合のみ 第2ステートメント(22~36行目):直接インターネットアクセスの拒否 項目 内容 効果 拒否(Deny) 対象者 すべてのユーザー(Principal: "*") 拒否アクション すべてのアクション(Action: "*") 対象リソース 同じS3バケットとその配下のすべてのオブジェクト 条件 アクセス元が指定されたVPCエンドポイント以外の場合 リソースコントロール編まとめ 複雑な構成変更と予期しないエラーへの対応を含むシナリオでしたが、Amazon Qは「構築→テスト→エラー検知→原因分析→修正→再テスト」という一連のエンジニアリングプロセスを自律的に実行しました。これは単なる構築ツールではなく、真のアシスタント機能を具備していると考えます。 ただし、バケットポリシーの自動変更など、セキュリティに関わる重要な変更も自動実行してしまうため、権限管理や承認プロセスの整備が不可欠です。 次回予告 次はセキュリティ編です。シナリオ②のバケットポリシーでも少し関連がありますが、ベストプラクティスに準拠した設計がAmazon Qでできるのか? ぜひ、次回をお楽しみに! 免責事項 本記事は検証・評価を目的とした技術記事です。 本記事を使用して本番環境でのAmazon Qを利用したリソース変更作業を推奨するものではありません。 本記事の内容を参考にした結果生じるいかなる損害についても、筆者および所属組織は一切の責任を負いかねます。 読者の責任において、適切なリスク管理の下でご活用ください。 執筆者 吉田 泰隆(NTT西日本 エンタープライズビジネス営業部所属) 好きなAWSサービス:Route53 所有資格:AWS Certified Solutions Architect – Professional 参考資料・出典 本記事を執筆するにあたり、以下のサイトを参考にしました。 business.ntt-east.co.jp dev.classmethod.jp 商標 「AWS」は、Amazon Web Services, Inc.またはその関連会社の商標もしくは登録商標です。
NTT WEST Engineers' Blog事務局の真木です。   NTT西日本グループの公式ブログで初めてのアドベントカレンダーを実施させていただき、 多くの技術者が企画に参加し、様々な魅力的な記事を書いてくれました! 今回のアドベント記事をまとめましたので、見逃したものがあればぜひ見てみてください。 (気になる記事タイトルをクリックすると当該記事へジャンプします!) .advent-2025 table { width: 100%; border-collapse: collapse; font-size: 10px; /* 小さめ文字 */ table-layout: auto; } .advent-2025 thead th { background: linear-gradient(135deg, #d32f2f 0%, #c2185b 50%, #b71c1c 100%); color: #000; font-size: 11px !important; padding: 8px; border: 1px solid #8b0000; text-align: center; vertical-align: middle; position: relative; } /* ヘッダー下のゴールドライン(装飾) */ .advent-2025 thead th::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 3px; background: linear-gradient(90deg, #cda434, #ffd87a, #cda434); } .advent-2025 tbody td { border: 1px solid #e0e0e0; padding: 8px 10px; font-size: 11px; color: #333; vertical-align: top; word-break: break-word; vertical-align: middle; } /* 行の交互色(白×薄緑) */ .advent-2025 tbody tr:nth-child(odd) { background-color: #ffffff; } .advent-2025 tbody tr:nth-child(even) { background-color: #f0f7f0; } /* 行ホバーで少しゴールドの光沢を出す */ .advent-2025 tbody tr:hover { background: linear-gradient(0deg, rgba(255, 232, 168, 0.35), rgba(255,255,255,0.35)); } /* 「日にち」列を少し強調(🎁アイコンと赤文字) */ .advent-2025 tbody td.date { color: #b71c1c; font-weight: 600; } /* タイトルのリンク色とホバー演出(緑 → ゴールド下線) */ .advent-2025 a { color: #1b5e20; /* 深い緑 */ text-decoration: none; border-bottom: 1px dashed #a5d6a7; } .advent-2025 a:hover { color: #2e7d32; border-bottom: 2px solid #cda434; /* ゴールド下線に */ } /* 「分野」バッジ風(薄いゴールド枠+緑背景) */ .advent-2025 .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #e8f3e8; border: 1px solid #cda434; color: #2e7d32; font-size: 10px; line-height: 1.6; } /* スマホでの読みやすさ強化 */ @media (max-width: 600px) { .advent-2025 table { font-size: 11px; } .advent-2025 thead th, .advent-2025 tbody td { padding: 6px 8px; } } 日付 記事タイトル 分野 1日 Google Gemini でトークンを実践検証してみた:PoCから得られた知見のご紹介 AI 2日 イメージでざっくり理解するECDHE(楕円曲線暗号による鍵交換) セキュリティ 3日 AI評価の新しい可能性:LLM-as-a-Judgeを検証してみた AI 4日 Hardening 2025 Invisible Divide 参加レポート セキュリティ 5日 Amazon Q Developerで実現するAWSインフラ構築の自動化 AI 6日 若手エンジニアによるLT会に登壇しました! AI 裏話 7日 AWSのDTO無料枠でS3コスト削減を狙ったら、Boxの1TB/月帯域制限に気づいた話 クラウド 8日 ディジタル回路シミュレータ「SimcirJS」をひさしぶりに使ってみた サービス/プロダクト開発 9日 オープンデータから観光ダッシュボードへ データサイエンス 10日 GitHub Copilot と Playwright を活用したアプリケーションテスト効率化 DX AI 11日 AWS API MCP ServerでWebアプリをデプロイする AI システム開発 プログラミング サーバ 12日 Entra IDでSalesforceへSSO&ユーザー自動プロビジョニング!実践手順まとめ システム開発 クラウド 13日 小規模ハードニング競技環境を構築してみた セキュリティ 14日 社内で「Difyハンズオン」を開催しました! AI 裏話 15日 1000円台で筋トレの実行回数カウンターを作ってみた 裏話 16日 RAGの精度を上げる7つの方法 AI 17日 Microsoft Purview で実現できる暗号化(秘密度ラベル)とは?仕組み・設定方法・利用イメージについて セキュリティ 18日 roboflowの物体検出モデル「RF-DETR」の環境を構築してみた システム開発 プログラミング 19日 aws-nukeで安全なクリーンアップを実現! 必要なリソースを守る実践テクニック5選 クラウド 20日 AIエージェントはどこまでできる?DifyとLangChainでRAGタスクの精度評価 AI 21日 Ragas指標を用いたDify製RAGチャットボットの自動評価をやってみた AI 22日 Amazon RDS Aurora Serverless v2の利用料削減に取り組んだ話 クラウド 裏話 23日 Webブラウザでのみ設定可能な装置の管理自動化アプリをSeleniumで作ってみた DX システム開発 24日 初心者エンジニアがCrewAIを活用して情報収集を自動化してみた AI DX 25日 Tableau×生成AIをもとに考えるデータ分析と生成AIの未来 AI データサイエンス ``
はじめに NTT西日本の酒井です。本記事ではTableauという分析ツールの生成AI機能を触りながらデータ分析と生成AIの未来について考察します。内容は記事執筆時点(2025年12月16日)時点の情報に基づきます。 対象読者 この記事は以下のような人を対象に書いています。 Tableauと生成AIの連携に興味がある人 生成AIでデータ分析がどう変わるか考えたい人 背景 私はこの会社でデータ分析に関わる仕事に10年ほど携わってきました。ネットワークトラフィックの分析や社内でのデータ活用の推進、社内外のコミュニティ活性化、お客様向けのデータ活用・Tableau活用の支援などデータにまつわるさまざまな業務を行ってきました。しかし昨今では「生成AIの登場によってデータ分析やデータ可視化のスキルが生成AIに置き換わってしまうのではないか?」という言説にほんのりと不安になることがあります。そんな状況だからこそ生成AIは自分で触って使い道を考えていく必要があると考えていろいろ試してみましたので、その試行の記録と、その結果から考える生成AIとデータ分析の未来について考察したいと思います。 本記事の内容と目的 本記事ではTableauというデータ分析ツールの生成AI機能を試していきます。私のデータ分析のキャリアのほとんどにおいてTableauがメインツールだったので、最も使い慣れたこのツールの生成AI機能を試すことで生成AIがデータ分析、データ活用にどう役立つのかを考えていきたいと思います。 目次 Tableauとは?からはじまり、Tableau×生成AIのお試しをするための設定方法、実際に使ってみたレポート、考察という流れで進めます。 はじめに 対象読者 背景 本記事の内容と目的 目次 Tableauとは? Tableau MCPとは? Tableau MCPで利用可能な機能 今回の動作環境 Tableau MCPの導入方法 Tableau MCPを試してみよう! ダッシュボードの検索 ダッシュボードの使い方を教えてもらう データからビジネスの状況を教えてもらう Tableau MCPの使い所はどこか? データ分析と生成AIの未来 さいごに 商標 Tableauとは? まずTableauとはなんなのか簡単に説明します。Tableauは、使いやすい直感的なUI(ユーザーインタフェース)と豊富なビジュアル表現が特徴的なデータ分析のためのツールです。BI(Business Intelligence)ツールと呼ばれる分野のツールで、Tableau以外にもMicrosoftのPower BIや、Qlik社のQlik Sense、ウイングアーク1st社のMotionboardなどがあります。BI各社も生成AIの機能をリリースしていますが、今回はTableauの生成AI機能に絞って検証と考察をしていきたいと思います。 www.tableau.com Tableauでは本記事のテーマである生成AI機能の開発がとても活発です。2024年2月に登場した特定のKPIの動向を生成AIが要約してくれるTableau Pulseにはじまり、同じ年の夏にはデータ可視化をサポートしてくれるTableau Agentが、2025年には分析からアクションまでトータルにサポートしてくれるAIエージェントであるTableau Nextと、矢継ぎ早にさまざまな機能がリリースされています(これらの機能の詳細な説明は今回は割愛します)。 そして上記のTableauにビルトインされている機能以外にも、Tableau Langchain、Tableau MCPというオープンソースのプロジェクトも存在します。今回はこのうちのTableau MCPで何ができるのか?を試していきたいと思います。 Tableau MCPとは? 今年(2025年)の夏頃に公開されたオープンソースのプロジェクトです。生成AIに対してTableau ServerおよびTableau Cloudに格納されている情報にアクセスするためのスキルを提供します。ちなみにMCPとはModel Context Protocolの略で、Anthropic社が開発した生成AIのための標準規格のことです。生成AIが外部ツールやデータソースにアクセスするための共通プロトコルです。Tableau MCPはTableauの機能に生成AIがアクセスするための機能を提供しています。ちなみにAnthropic社はClaudeという生成AIを提供しています。 Tableau MCPについては以下のGithubリポジトリに詳しい情報があります。 github.com 前述の通りTableau Langchainという別のオープンソースプロジェクトもあるのですが、執筆時点(2025年12月16日)において最後の更新が5ヶ月前でありこちらの開発は最近は停滞している状況です。 Tableau LangchainのCommits over timeの画像 反面、Tableau MCPは執筆日(2025年12月6日)の4日前にも更新されており直近でも多くのCommitがなされています。 Tableau MCPのCommits over time おそらくTableauは注力領域をTableau LangchainからTableau MCPにシフトしているのだと思います。Tableau LangchainはLangchainというAIアプリ開発フレームワークでしか利用できませんがTableau MCPはオープン標準規格なのでClaudeやDifyなど多くのツールで利用できます。Tableau MCPに注力するのは自然な流れだと思います。 ということで本記事でもTableau MCPを使ってみて、使い道について考察をしていきたいと思います。 ちなみにTableau LangchainとTableau MCPについては、Tableauユーザー会のLT(ライトニングトーク)で私が発表した資料もありますのでよければご覧ください。 Tableau Langchain speakerdeck.com Tableau MCP(本記事はこの内容を肉付けしたものです) speakerdeck.com Tableau MCPで利用可能な機能 Tableau MCPでは以下の機能が提供されています。多いですがこの記事を読む上では覚える必要はありません。 Data Q&A:データソースの一覧やメタデータの取得やクエリを投げることができる List Datasources Get Datasource Metadata Query Datasource Workbooks:ワークブック(Excelブックのようなもの)の情報を取得できる List Workbooks Get Workbook Views:ビュー(Excelのシートのようなもの)の情報を取得できる List Views Get View Image Get View Data Pulse:Tableau Pulse関連の機能。今回は取り扱いません。 List All Metric Definitions List Metric Definitions List Metrics for Definition List Metrics List Metric Subscriptions Generate Insight Bundle Generate Pulse Insight Brief Content Exploration:Tableau ServerおよびTableau Cloud上のコンテンツ(ワークブックやデータソース)を検索できます Search Content 今の所、Tableau ServerおよびTableau Cloud上のワークブックやデータソースの情報を取得する機能が提供されていますが、ダッシュボードを作ったりグラフを作る機能は提供されていません。 Tableau MCPの機能は基本的にTableau ServerおよびTableau Cloudで提供されているAPIに依存してます。APIが提供されているものについてはTableau MCP側に機能追加される可能性がありますが、APIがないものはなかなか機能追加されないと思います。ダッシュボードやグラフの作成については現時点でAPI自体が提供されていないため実現には時間がかかりそうです(要望は多いと思うのですが)。 今回の動作環境 今回は以下の構成で検証を進めます。Tableau Cloud以外はローカル環境で実行します。 各ツールのバージョンは以下のとおりです。 macOS: Sequoia 15.5 Claude Desktop: 1.0.1768 Claudeのモデル: Opus 4.5 Tableau MCP: 1.10.3 Tableau Cloud: 2025.3.0 今回の実行環境の概要 Tableau MCPの導入方法 導入方法はとても簡単です。 1. Tableau ServerもしくはTableau Cloudを用意する 個人で準備するのは難しいと思われると思いますが、Tableau Cloudであれば検証用にDeveloperサイトを無料で開設できます。以下の記事を参考にぜひ試してみてください。簡単ですよ。 note.com 2. パーソナルアクセストークン(PAT)を作成する 以下の手順でPATを作成することができます。シークレットは1度しか表示されないのでメモ帳などにコピペしておいてください。 PATの作成方法 3. Claude DesktopにTableau MCPをインストールし各種設定をする 以下のとおりClaude Desktopの拡張機能として簡単にTableau MCPをインストールできます。 Claude Desktopの設定から拡張機能を参照する Tableauと検索するとTableau MCPが出てくる Tableau MCPをインストールする Tableau ServerもしくはTableau Cloudに接続するための設定をする PAT nameに先ほど作成したPATの名前を、PAT Valueにシークレットを入力してください。 これで準備OKです!早速試していきましょう。 Tableau MCPを試してみよう! 以下のユースケースを想定して実際に動作を確認していきたいと思います ダッシュボードの検索 ダッシュボードの使い方を教えてもらう データからビジネスの状況を教えてもらう ダッシュボードの検索 Tableau Cloudには多くのダッシュボードが掲載されていると、自分に関係のあるダッシュボードを探すのも一苦労です。そこで生成AIの力を借りましょう まずはTableau Cloudに掲載されているダッシュボードには何があるかを聞いてみましょう。 Tableau MCPを使ってダッシュボードを聞いてみた図 List Workbooksの機能を使って3つのワークブックがあることを教えてくれました。これらはTableau Cloudにデフォルトで格納されているサンプルワークブックです。それぞれどういう内容なのか知りたいのでURLも聞いてみましょう。 URLも聞いてみた図 新たにTableau MCPの機能を使うことなくURLを提示してくれました。すでにURLの情報は取得していたようです。実際にリンクをクリックすると当該ワークブックにアクセスすることができました。 では自分の興味のあるダッシュボードを探してもらいましょう。売上に関するダッシュボードがないか聞いてみます。 売上に関するダッシュボードを探してもらう するとSearch ContentとGet Workbookを使って関連するダッシュボードを探してくれました。Superstoreというワークブックに入っている具体的なビュー(ダッシュボードに当たるもの)の説明もつけてくれています。 Overviewというビューをみれば売上の全体概要が掴めそうなので、OverviewのURLを聞いてみましょう。 OverviewのURLを教えてもらう 無事教えてくれました。アクセスしてみましょう。 Overviewのダッシュボードの画面 Superstoreという架空の小売店の売り上げをまとめたダッシュボードが表示されました。自分の興味に合致するダッシュボードを探すことができました。なかなか使いやすいですね。 ダッシュボードの使い方を教えてもらう ダッシュボードの使い方も慣れるまでは難しいものです。なのでダッシュボードの使い方も生成AIに聞いてみましょう。 使い方を聞いた結果 Get View Imageという機能を使ってダッシュボードの画像を取得し内容を解析しているようです。ダッシュボードの構成を説明した上でフィルターを切り替えてみたいデータを見る方法も提示してくれています。ここまで説明してくれれば初めて見る人でも使い方をイメージしやすそうですね。 データからビジネスの状況を教えてもらう とはいえダッシュボードを触りながらデータを深ぼっていくのも慣れない人からするとハードルがあつかもしれません。なのでこのダッシュボードで使われているデータを使ってデータ分析をしてもらいましょう。架空の設定として私はCentral Regionの責任者であり、Central Regionについて他のRegionと比較した良いところ悪いところを生成AIに提示してもらおうと思います。 分析依頼の結果(1) List DatasourceとGet Datasource Metadataを使ってデータソースの基礎情報を取得しています。その後Query Datasourceにてクエリをいくつか投げて分析を行っています。どんな結果が返ってきたのでしょうか。以下がこの続きです。 分析依頼の結果(2) 売上、利益だけでなく利益率や顧客数、割引率までに着目してサマリーをまとめてくれています。また具体的にCentral Regionの良いところをいくつか示してくれています。では課題についではどうでしょうか? 分析依頼の結果(3) 割引率が高いせいで利益率が低いこと、どのサブカテゴリーの商品の利益率が特に低いのか、具体的な州についても触れてくれています。また2024年に利益が急落したという大きな変化も示してくれています。なかなか良い分析をしてくれていそうです。どう改善すれば良いのでしょうか?それも提案してくれていました。 分析依頼の結果(4) かなり具体的かつ定量的な目標も含めて提示してくれています。最近の生成AIの優秀ぶりには本当に驚かされます。 せっかくなので最終的なアクションにも繋げていきましょう。州や製品の担当者に指示を出しましょう。 メール文案作成依頼の結果 各担当者向けのメール文案を細かく作成してくれました。これは想像以上の結果でした。シンプルにClaude Opus 4.5の性能に驚きました。 メール文案作成結果の一部 かなり具体的な指示を丁寧に書いてくれています。内容のクオリティについては精査が必要とは思いますが、ダッシュボードの検索からメール文案作成までの一連の作業はこの記事を書きながら約30分で実施できました。すごいスピード感だと思います。データ分析者の仕事はなくなってしまうのか不安になってしまいますね。 Tableau MCPの使い所はどこか? これまでみた通りTableau MCPではデータ分析に詳しくない人でも、自然言語を使ってデータから気づきを得ることができそうなことが見えてきました。ではダッシュボードは不要になるのでしょうか?おそらくそうではないと思います。ダッシュボードはビジネスを実施する上での健康診断のようなもので、定期的に同じ数値を確認し自分たちのビジネスや施策が予定通りうまく進んでいるのか、課題がないのかを見つめ直すために使います。こういった数値は生成AIに都度聞くのではなくダッシュボードとしてまとめておいた方がチーム全体での統一的な見解を持つことができると思います。Aさんが聴いた結果とBさんが聴いた結果がもし異なっていたら議論が食い違ってしまいますよね。ではTableau MCPのような生成AI機能は何に使えばよいのでしょうか? 大きく二つの用途が考えられると思います。 - データに触れるきっかけを作る - ダッシュボードではわからない質問をする 前者はさきほどの「試してみよう」の章でみてきたようなゼロからデータに触れるためのきっかけを作る用途です。ダッシュボードを見たり、データ分析者に質問をするのは多少のハードルがあるものです。ダッシュボードはどこにあるんだろう、下手に触って壊したら怖い、データ分析者にこんなこと聞いて恥をかかないだろうか、そんなハードルは生成AIであれば皆無です。気軽に生成AIに気になる情報を聞いてもらってデータで何がわかるのかを掴んでもらうのにはとても良いと思います。 後者はデータ活用がより成熟してきてダッシュボードによるデータ活用ができている状態において、スピード感が求められる調査に活用できます。会議にてダッシュボードをみながら利益率の低下といった課題が見えてきました。ではその要因はなんなのか分析したいがダッシュボードで用意された切り口以外の分析はできません。Tableauに慣れた人であれば、データ可視化機能を使って別の切り口での分析を行うことも可能、普段ダッシュボードを見るだけの人にはなかなか難しいです。そんなときでも生成AIを使えばすぐに質問をして、課題のあたりをつけていくことができます。それらしい仮説がうまれたら分析ができる人により詳細な分析を依頼することもできるでしょう。 いずれにしてもデータ分析のスキルが高くない人がデータを活用する入り口になると思います。 データ分析と生成AIの未来 Tableau MCPを試してみた感じたのは「生成AIはこれまで以上に簡単にデータに触れられる新しいユーザーインタフェース」だと感じました。これはBI登場の歴史にも似ていると思います。昔は基幹システムにバンドルされている分析機能やデータ出力機能を使っていて、求められる技術力も高く一部の人しかデータ分析はできませんでした。BIの登場により簡単にデータを分析できる環境が生まれ、より多くの人がデータ分析をできるようになりました。これにより現場で業務を回している人がみずからデータを見た判断がしやすくなりました。しかし簡単になったとはいえそれでも一定の基礎リテラシーやトレーニングが必要ではありました。しかし生成AIであれば同僚や部下に質問をするように自然言語で質問することで欲しいデータを得ることができます。これであればデータ活用のハードルをかなり下げることができます。この簡単さこそが生成AIによるデータ分析が生み出す価値だと思います。 ただしこの価値を享受するには大きな壁もあります。それはデータの整備です。BIを広めていく上でもデータ整備は大きな壁でしたが、生成AIではさらに大きな障壁となります。BIを使う際は人がデータを見て誤りを検知したり、修正するフローを組んだりすることができました。しかし生成AIにそれができるでしょうか?現時点では分析者ほどの精度では実現できないと思います。そのためAIが使いやすいデータを整備することの重要性が高まります。実際にそういった議論がさまざまなところで行われています。生成AIのためにデータ整備が進めばBIを使ったデータ分析もしやすくなるので、この生成AIのブームに乗って日本中のデータ整備がすすむことを願います。 さいごに 本記事ではTableau MCPを使って生成AIとデータ活用の未来を考えました。最初の疑問であった「生成AIの登場によってデータ分析やデータ可視化のスキルが生成AIに置き換わってしまうのではないか?」についてはどうでしょうか?現時点ではすぐに置き換わることはなさそうです。ポイントはデータ整備とドメイン知識です。やはりデータの整備やメタデータ(データの各列の意味などをまとめたデータ)の整備がなされていないと生成AIも正しくデータを見ることができません。また職場それぞれにある特有の商習慣やドメイン知識をとらえるのもまだ難しいでしょう。しかし今後データの整備が進み、生成AIに職場独自の知識を与え、生成AIの精度も高まっていけば生成AIが置き換えられる領域は増えていきそうです。とはいえ今回Tableau MCPを触ってみて思ったのは不安よりもワクワクでした。やはり新しい技術が新しい価値を生み出す瞬間はワクワクします。データ分析人材として生成AIを脅威としてとらえることも重要ですが、それ以上に新たな武器として使いこなしていきたいなと思いました。生成AI時代のデータ分析者としての自分らしい強みを見つけていきたいと思います。最後までお読みいただきありがとうございました。 商標 「Tableau」「Tableau Cloud」「Tableau Server」「Tableau Pulse」「Tableau Agent」「Tableau Next」は、Salesforce, Inc.の商標または登録商標です。 「Microsoft」「Power BI」は、米国Microsoft Corporationの商標または登録商標です。 「Qlik」「Qlik Sense」は、QlikTech International ABの商標または登録商標です。 「MotionBoard」は、ウイングアーク1st株式会社の商標または登録商標です。 「Claude」は、Anthropic, PBCの商標または登録商標です。 「Mac」「macOS」は、米国Apple Inc.の商標または登録商標です。 「Dify」は、LangGenius, Inc.の商標または登録商標です。 「LangChain」は、LangChain, Inc.の商標または登録商標です。 「GitHub」は、GitHub, Inc.の商標または登録商標です。
0. はじめに 1. 対象読者 2. 執筆の背景 3. AIエージェントとは? 3-1. AIエージェントのコアコンポーネント 3-2. 代表的なAIエージェントフレームワーク 3-3. 今回活用したAIエージェント:CrewAIについて 4. AIエージェントの実装 4-1. 実装の目的 4-2. プロセスと実装アーキテクチャ 4-3. 動作環境 4-4. 構築手順 4-4-1. 事前準備 4-4-2. 必要なライブラリのインストール 4-4-3. APIキーの取得 4-4-4. コードファイルの作成と実行 4-5. 結果 4-5-1. 全体構成 4-5-2. 各AIエージェントの応答 Planning Agent(引っ越し家電プランナー) Research Agent(家電市場リサーチャー) Comparison Agent(家電最適化アナリスト) Report Agent(レポートジェネレーター) 4-5-3. 最終成果物 4-6. 評価 5. 課題 6. 今後取り組みたいこと 7. まとめ 執筆者 商標について 免責事項 参考資料・出典 0. はじめに   こんにちは。NTT西日本の武田です。 「AI」という言葉が日常に浸透し、最近では、 「AIエージェント」 という新たなキーワードを耳にする機会が増えてきました。AIエージェントは、単に質問に答えるだけでなく、自らタスクを計画し、環境と相互作用しながら自律的に働いてくれる賢いアシスタントです。 本記事では、AIエージェントに興味を持つ方を対象に、その基本的な仕組みから、簡単な実装までを解説します。 1. 対象読者   本記事が想定する対象読者は以下の通りです。 AIエージェントを使って開発をしてみたい方 CrewAIに興味がある方 2. 執筆の背景   実は、この記事を書いている私自身まだまだ経験の浅いエンジニアです。しかし、AIの急速な進展には非常に興味があり、「AIエージェント」という新しい技術を試してみたいという思いから、この開発に挑戦しました。本記事が「AIエージェントに興味はあるけれど、よく分からない」と感じている方にとって、少しでも参考になれば幸いです。 3. AIエージェントとは?   「AIエージェント」と従来の生成AIとの違いは何でしょうか? 一言でいうと、生成AIは 「受動的に答える」 のに対し、AIエージェントは 「自律的に行動」 します。 従来の生成AIは、ユーザーの質問に対して学習済みの知識から最適な答えを返すことに特化していました。これは、あくまで受動的な「応答」です。 一方、AIエージェントは、与えられた目標を達成するためにタスクを分解し、外部システムと連携しながら自律的に「行動」するインテリジェントシステムです。例えば営業支援を行うAIエージェントなら、潜在顧客の探索から情報収集、提案書作成、メール送信までの一連のワークフローを自動で実施することができます。 3-1. AIエージェントのコアコンポーネント AIエージェントは、主に以下の4つの要素で構成されています。 コンポーネント 役割 機能 LLM (Large Language Model) エージェントの「脳」として機能する中核 ユーザーの目標を理解し、論理的に推論する。 計画 目標達成のための具体的な手順を決定する 複雑な目標を小さなタスクに分割する。現在の状態と目標に基づき、次にどのツールを使うか、応答を行うかを決定する。 メモリ 過去の経験や情報を保持し、行動に活かす 短期記憶: 現在の会話やタスクの履歴を保持する。 長期記憶: 過去の知識やドキュメントを保存し、必要に応じて検索・参照する。 ツール 外部システムと連携する 外部のAPIや関数を呼び出し、LLM単体ではできない操作を実行する。 これらのコンポーネントは、以下の流れで連携し、エージェントの自律的な行動を可能にします。 3-2. 代表的なAIエージェントフレームワーク AIエージェントを効率的に開発するための代表的なフレームワークを3つご紹介します。それぞれ異なる機能と得意分野を持っているので、用途に合わせて選択してください。 フレームワーク 機能 得意分野 LangGraph   エージェント間のワークフローをグラフ構造で管理 複雑な条件分岐やループを含む、柔軟な意思決定プロセス AutoGen   複数エージェントの対話(チャット)をベースにタスクを進める 探索的な問題解決や、コード生成・実行を伴うタスク CrewAI   役割ベースのエージェントチームを簡単に構築できる 明確な役割分担とエージェント間の連携 3-3. 今回活用したAIエージェント:CrewAIについて 今回は、役割が明確なフローの実践を取組のテーマ(=「ユーザニーズに基づく家電購入リストの作成」)としたため、3-2で示したフレームワークの中でも特にCrewAIを活用して開発を行いました。CrewAIは、 複数のAIエージェントを「チーム」として編成し、役割分担しながらタスクを遂行すること を得意としています。 CrewAIの構造はシンプルです。基本的な仕組みは以下の通りです。 CrewAIは、現実世界のプロジェクトチームをモデルにしており、プロジェクトの立ち上げから、タスクの定義、エージェントへの割り当て、そしてプロセスに沿った段階的な実装を行います。以下の要素を組み合わせることで、役割ベースのエージェントチームを簡単に構築することができます。 Crew(プロジェクトチーム) 複数のエージェントをまとめる単位 Agent(チームメンバー) 役割と専門性を持つAI(例:Planner、Coder、Reviewer) Task(作業) エージェントに割り当てる作業 4. AIエージェントの実装   4-1. 実装の目的 今回の実装のきっかけは、引っ越しです。私が最近引っ越しを経験し、家電の購入を検討した際、予想以上の時間と労力がかかったため、この課題を解決したいと考えました。家電の購入は、情報収集・比較・予算調整といった複数のタスクが含まれる複雑なワークフローであると言えます。そこで、役割ベースのエージェントチームを構築できるCrewAIの特性を活かし、この問題を解決しようと考えました。 4-2. プロセスと実装アーキテクチャ 本実装では、家電購入リストの作成というワークフローを以下の4つのプロセスに分割し、プロセス(=タスク)ごとにAIエージェントを作成しました。また、各AIエージェントが互いに連携するよう構成しました。 プロセス① 要件定義: ユーザーの生活環境(部屋の広さ、家族構成、予算)に基づく必要な家電リストの作成 プロセス② 情報収集: Web検索を通じたリアルタイムな市場データ(価格、レビュー、在庫)の収集 プロセス③ 意思決定: 収集データに基づく多角的な比較と最適化 プロセス④ 成果物生成: 最終結果をレポートとして出力 上記のプロセス1〜4に対応するAIエージェントを下記のように作成しました。 エージェント名 役割 入力 出力 タスク Planning Agent 引っ越し家電プランナー ユーザーの引っ越し条件 必要な家電リスト 引っ越し条件をもとに必要な家電を洗い出し Research Agent 家電市場リサーチャー 家電リスト 市場データ 家電ごとに価格、レビュー、在庫情報をWeb検索して収集 Comparison Agent 家電最適化アナリスト 市場データ 最適な購入候補リスト 価格、性能、レビューを評価し、最適な候補を選定 Report Agent レポートジェネレーター 最適な購入候補リスト レポート 購入候補リストをレポートにまとめる 4-3. 動作環境 要件 詳細 ライブラリ crewai, crewai-tools LLM API OPENAI_API_KEY Web検索 API SERPER_API_KEY コードエディタ Visual Studio Code 4-4. 構築手順 4-4-1. 事前準備 まず、PythonとVS Codeをインストールし、プロジェクトフォルダを作成します。 crewai-appliance-project/ ├── .env └── appliance_purchase_crew.py 4-4-2. 必要なライブラリのインストール 必要なライブラリをインストールします。 pip install crewai crewai-tools python-dotenv 4-4-3. APIキーの取得 本実装では、2つのAPIを利用します。(LLM API(OpenAI)とWeb検索 API(Serper)) それぞれのウェブサイトでアカウントを作成し、APIキーを取得します。 OPENAI_API_KEY= "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" SERPER_API_KEY= "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 4-4-4. コードファイルの作成と実行 今回は、 appliance_purchase_crew.py というpythonファイルを作成し、実行しました。 4-5. 結果 今回構築したプログラムの構成と各AIエージェントの応答、最終成果物について示します。 4-5-1. 全体構成 4-2で示した4つのプロセスごとにそれぞれ専門のAIエージェントを作成し、前のタスクの出力を次のタスクの入力とすることで互いに情報を渡し、最終成果物を作成するという構成にしました。 4-5-2. 各AIエージェントの応答 Planning Agent(引っ越し家電プランナー) ユーザの条件から引越しに必要な家電を洗い出すエージェントです。 タスクを以下のように提示します。 ここでは、家電の購入条件を指定します。 AIエージェントの応答は以下です。 この出力を次のResearch Agent(家電市場リサーチャー)の入力とします。 Research Agent(家電市場リサーチャー) プランナーが作成したリストをもとに、情報収集を行うエージェントです。 Planning Agent(引っ越し家電プランナー)の出力を入力として、各家電(テレビ、洗濯機、冷蔵庫など)ごとにSerperを用いて検索します。ここでは、冷蔵庫の検索結果を示します。 Comparison Agent(家電最適化アナリスト) リサーチャーが収集した情報をもとに、最適な家電を選択するエージェントです。 Research Agent(家電市場リサーチャー)の出力を入力として、最適なモデルを選択します。 Report Agent(レポートジェネレーター) アナリストの結果をもとに、最終成果物としてレポートを作成するエージェントです。 Comparison Agent(家電最適化アナリスト)の出力を入力として、レポートを作成します。 4-5-3. 最終成果物 以上のプロセスを経て作成された最終成果物がこちらです。 4-6. 評価 本取り組みでは、「最適な家電購入リストの自動生成」を、AIエージェントが自律的に判断し、最終レポートとしてまとめることに成功しました。また、複数の専門エージェント(プランナー、リサーチャー、アナリスト、ジェネレーター)が役割ごとにタスクを分担し、連携しながら結果を導く仕組みを構築できました。 5. 課題   今回、CrewAIを用いたシステム実装の過程でいくつかの課題が明らかになりました。今後はこれらの課題を解消し、より利便性の高いシステムの構築を目指します。 課題① UIを作成していないため、ユーザビリティが低い - CLIによる実行のみで、非エンジニアには使いにくい 課題② データ品質の低さ(精度やリアルタイム性) - 検索結果の構造化が不十分で、比較ロジックが曖昧になることがある - 最終アウトプットに販売終了製品を含むなど、古い情報を取得してしまうケースがある 課題③ 最終成果物の高度化 - PDF形式への変換や、購入リンクの付与など、より実用的なレポートの生成を行う 6. 今後取り組みたいこと   今回実装したのは非常にシンプルなAIエージェントですが、この仕組みを応用することで、データ分析やマーケティングなど、さまざまなワークフローを自動化することが可能だと感じました。今後は、実業務でも活用できるAIエージェントを実装してみたいと思います。 7. まとめ   本記事では、AIエージェントの基本的な仕組みから、CrewAIを用いた簡単な実装までを解説しました。 AIエージェントは、私たちの働き方を大きく変える可能性のある、非常に興味深い技術分野です。 本記事が皆さんのAIエージェント開発の一助となれば幸いです。 ぜひ、自分だけのオリジナルAIエージェントを作成してみてください。 執筆者 武田 海渡(NTT西日本 ビジネス営業部所属) 関西エリアの広域支援(教育、防災案件)に携わっています。 商標について 本記事で言及されている製品名・サービス名は、各社の商標または登録商標です。 OpenAIは、OpenAI, Inc.の商標または登録商標です LangChainは、LangChain, Inc.の商標または登録商標です AutoGenは、Microsoft Corporationの商標または登録商標です CrewAIは、CrewAI, Inc.の商標または登録商標です 免責事項 本記事に記載された情報は、2025年12月時点での公開情報および筆者の検証・ヒアリング結果に基づくものです。 各サービスの機能、制限事項等は予告なく変更される場合があります 本記事の内容を実践される際は、必ず各サービスの最新の公式ドキュメントをご確認ください 本記事の情報に基づいて行われた意思決定や実装により生じた損害について、筆者および所属組織は一切の責任を負いかねます 参考資料・出典 www.crewai.com ai-market.jp www.nvidia.com
はじめに こんにちは、NTTフィールドテクノの福田です。 今回は、業務環境の改善のために導入した、ある装置の管理自動化をめざしたものの、Webブラウザでのみ設定可能という仕様上の制約がありましたので、Selenium(Webブラウザ操作の自動化を行うオープンソースのテストフレームワーク)を使ってなんとかした経緯を紹介する記事になります。 本記事は2025年8月時点の情報に基づきます。 対象読者 本記事が想定する対象読者は以下の通りです。 Webブラウザ操作の自動化に興味を持っている人 トラブル発生時の「現場の付け焼刃」エピソードに興味のある方 背景・目的 業務環境の改善のために、ある装置を導入することとなりました。しかし、諸般の事情から、保守者が手動で設定・管理をするには限界があることがわかりました。 そこで自動化ツールを作成しようと思ったのですが、Webブラウザ操作でのみ設定可能な特殊仕様の装置でした。 それならば、Webブラウザ操作の自動化に用いられているSeleniumを使って自動化を実現しようと試みました。 Webブラウザ操作の自動化ツールを実現するためにはさまざまな手法があるかと思いますが、今回は、私が多少開発ノウハウのあったPHPによるWebアプリとして作成することにしました。 前提条件・動作環境 本記事は以下の環境・前提条件で執筆しました。 ツールサーバ:Dockerコンテナが稼働する環境 Webサーバコンテナ:rockylinux:8.9 ミドルウェア:PHP 8.2 ライブラリ:php-webdriver Seleniumコンテナ:selenium/standalone-chrome:128.0 ポイント ツールサーバはSeleniumコンテナとWebサーバコンテナが稼働し、Webブラウザ操作自動化を実現するとともに、Webブラウザから操作ができるWebアプリを提供するサーバとします。 諸般の事情から、Webサーバコンテナとしてはphp-apacheではなくrockylinux上に環境を構築するものとしました。 実装その1~Selenium動作環境を整える~ 今回はSeleniumのDockerコンテナを利用することとしました。 コンテナの起動にはDocker Composeを使うこととし、以下のような定義としました。 docker-compose.yml selenium : image : selenium/standalone-chrome:128.0 container_name : selenium volumes : - ./hogehoge/:/home/seluser/Downloads/ shm_size : 2g environment : SE_NODE_SESSION_TIMEOUT : 600 SE_NODE_MAX_INSTANCES : 10 SE_NODE_MAX_SESSIONS : 10 SE_NODE_OVERRIDE_MAX_SESSIONS : true TZ : "Asia/Tokyo" 設定ポイント volumes: Webブラウザでダウンロードしたファイルを利用(永続化)するためホストのフォルダをマウントします。装置設定前にバックアップファイルをダウンロードすることがあるため設定しています shm_size: 2g コンテナに割り当て可能なメモリ量を増やし、Webブラウザウィンドウを多数開いたときのメモリ枯渇を防ぎます SE_NODE_MAX_INSTANCES: 10 Webブラウザ1種類あたり10ウィンドウまで開くことができるようにします(余裕を持った設定です) SE_NODE_MAX_SESSIONS: 10 全Webブラウザ合計で10ウィンドウまで開くことができるようにします(余裕を持った設定です) SE_NODE_OVERRIDE_MAX_SESSIONS: true により、MAX_SESSIONS設定をオーバーライドします 実装その2~PHPからSeleniumを操作する下地作り~ php-webdriverの公式githubページに記載のインストール方法や、その他の手法に従い、webdriverをPHPで取り扱えるようにします。 基本はComposer(PHPのライブラリ管理ツール)を使うのが一般的です。 php-webdriverのインストールが完了したら、いよいよコーディングへと移ります。 定数の定義(module_define.php) Webブラウザ操作のWAITTIMEなど、共通で何度も使うパラメータの定義とその保守性向上のために、定数を定義するPHPファイルを切り出しました。 <?php ... (省略) ... # Seleniumサーバ定義 const SELENIUM_SERVER = 'http://selenium:{port番号}/wd/hub' ; # プログラムとしての最大セッション数(サーバの10に対してここでは3と決め打ち) const SELENIUM_MAX_SESSIONS = 3 ; # その他処理関連定義 const SELENIUM_RETRY = 3 ; const SELENIUM_WAITTIME = 100000 ; const SELENIUM_TUNINGWAIT = 100000 ; const SELENIUM_TUNINGWAIT_LONG = 500000 ; const SELENIUM_TIMEOUT = 5 ; ... (省略) ... ?> ライブラリ読み込み部分抜粋(module_selenium.php) PHPファイルの先頭に以下のような記述をすることでSelenium操作用ライブラリを読み込みます。(ついでに上で作った定数定義も取り込みます) <?php ... (省略) ... # 定数の定義 require_once 'module_define.php' ; ... (省略) ... use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverExpectedCondition; use Facebook\WebDriver\WebDriverSelect; ... (省略) ... ?> Webブラウザの起動と終了(module_selenium.php) PHPからWebブラウザを操作する基本的な流れとしては PHPからSelenium上のWebブラウザを起動 各種操作 Webブラウザの終了(quit()) となります。 まずはWebブラウザを起動する関数と、終了する関数を準備しました。 <?php ... (省略) ... # Chromeを起動する function openChrome (){ try { # Chromeオプション設定 $ chromeOptions = new ChromeOptions () ; # 起動オプションを設定(詳細省略) $ chromeOptions -> addArguments ([ ]) ; $ capabilities = DesiredCapabilities :: chrome () ; $ capabilities -> setCapability ( ChromeOptions :: CAPABILITY, $ chromeOptions ) ; # Seleniumサーバの設定 $ host = SELENIUM_SERVER; # Chrome起動 $ driver = RemoteWebDriver :: create ( $ host , $ capabilities ) ; # タイムアウト設定 $ driver -> manage () -> timeouts () -> pageLoadTimeout ( SELENIUM_TIMEOUT ) ; # Chromeドライバをリターン return $ driver ; } catch ( Exception $ e ){ } } # Chromeを終了する function closeChrome ( $ driver ){ $ driver -> quit () ; } ... (省略) ... ?> 実装その3~Webブラウザ操作用の汎用関数の作成~ Webブラウザ操作を以下のように分解して、それぞれ汎用関数を作成しました。 Webブラウザは、主にHTML形式で記載された情報を整形して画面表示をしています。 HTML形式のデーター中のHTMLタグで囲まれた情報単位のことを「要素」と呼び、Seleniumではこの「要素」に対して操作を行うことができます。 ※さまざまな操作のうちの一例となります。 ※既に用意されている関数はそれを使うものとします id属性またはname属性により、操作対象の要素を選択する 要素からパラメータを取得する 要素にパラメータ(例えばテキストボックスであれば文字列)を入力する 要素をクリックする また、テキストボックスに文字を入力する場合(要素選択→要素クリア→文字列の入力)など、一部の操作はひとまとまりにしました。 汎用関数の作成(module_selenium.php)―要素をクリックする関数の例 <?php ... (省略) ... # id属性で取得したエレメントをクリックする function clickById ( $ driver , $ id ){ $ retryCount = 0 ; # リトライ回数まで繰り返す while ( true ){ try { # 要素がクリックできるようになるまで待つ $ driver -> wait ( SELENIUM_TIMEOUT ) -> until ( WebDriverExpectedCondition :: elementToBeClickable ( WebDriverBy :: id ( $ id )) , 'clickById-Timeout :' .$ id ) ; # 要素をクリックし終了 $ driver -> findElement ( WebDriverBy :: id ( $ id )) -> click () ; break ; # エラー時はリトライ試行 } catch ( Exception $ e ){ $ retryCount ++ ; if ( $ retryCount == SELENIUM_RETRY ){ throw $ e ; } usleep ( SELENIUM_WAITTIME ) ; continue ; } } } ... (省略) ... ?> ポイント SeleniumはWebブラウザ操作を自動化しているため、Webブラウザ上の表示にかなり依存する動きをします。 例えば、通信データは受信完了しているが、Webブラウザ表示がまだこれから、という状況で要素を探しても、見つからずにエラーとなる場合があります。 そのため、Webブラウザ表示を待つ処理を書くことになるのですが、これが意外と上手くいかない・・・。 (WAITTIMEを多く取れば安定に向かっていくものの、処理時間がかかりすぎる) さまざまな先人の知恵を組み合わせて、表示を待つ処理と、エラー時に3回までリトライする処理を組み合わせて、安定動作かつ高速な動作を実現しました。 実装その4~装置操作の流れをまとめた関数の作成~ 装置操作の流れをひとまとまりにした関数を作成しました。 例えば装置にログインする場合は、以下のような操作となります。 Webブラウザから装置にアクセスする name属性が"username"である要素(テキストボックス)を選択 テキストボックスに、ユーザー名文字列を入力する name属性が"password"である要素(テキストボックス)を選択 テキストボックスに、パスワード文字列を入力する name属性が"login"である要素(ボタン)を選択 ボタンをクリックする これらを、実装その3で作成した汎用関数を使って表現すると以下のようになります。 ※装置はhttps://{IPv4アドレス}で接続できるものとします。 操作用のまとめ関数の作成(module_login.php) <?php # selenium操作用汎用関数の読み込み require_once 'module_selenium.php' ; ... (省略) ... # 装置にログインする function login ( $ driver , $ ipAddress , $ user , $ password ){ try { # 接続 $ URL = 'https://' . rawurlencode ( $ ipAddress ) ; $ driver -> get ( $ URL ) ; # ユーザー名入力 setInputTextByName ( $ driver , 'username' , $ user ) ; # パスワード入力 setInputTextByName ( $ driver , 'password' , $ password ) ; # ログインボタン押下 clickByName ( $ driver , 'login' ) ; } catch ( Exception $ e ) { throw $ e ; } } ... (省略) ... ?> 設定ポイント $driver->get($URL); 「1. Webブラウザから装置にアクセスする」に対応します setInputTextByName($driver, 'username', $user); は前項で作成した汎用関数を使います。「2. name属性が"username"である要素(テキストボックス)を選択」→「3. テキストボックスに、ユーザー名文字列を入力する」に対応します setInputTextByName($driver, 'password', $password); 「4. name属性が"password"である要素(テキストボックス)を選択」→「5. テキストボックスに、パスワード文字列を入力する」に対応します clickByName($driver, 'login'); は前項で作成した汎用関数を使います。「6. name属性が"login"である要素(ボタン)を選択」→「7. ボタンをクリックする」に対応します 実装その5~操作の組み合わせ~ 実装その4にてある程度流れをまとめましたが、それをさらにまとめます。 例えば装置のネットワーク設定を変更したい場合の例としては 装置ログイン 事前バックアップ(configファイルのダウンロード) ネットワーク設定に遷移 パラメータ設定 設定保存 事後バックアップ 装置ログアウト という流れになります。 これら操作を必要な分だけまとめました。 装置の基本設定セット アカウント設定セット ネットワーク設定セット セキュリティ設定セット etc... 装置のネットワーク設定操作の組み合わせ例の図 組み合わせ例の図 できた!・・・と思いきや さて、Webブラウザ操作を自動化し、保守者がWebアプリ上からほぼワンボタンで、特定の装置の設定が完了するぞ!という状況になったと思いきや、利用者からクレームが入りました。 装置はそれを取り扱うチーム単位で、約30人分くらいのアカウント情報を登録していきますが、そのうち3~4人分についてログインできないというのです(残りの20数人は問題なし) 装置内のアカウント情報を確認すると、なんと一部のユーザー情報が欠けているばかりではなく、重複して登録されているユーザーもいる状況でした。 クレーム発生時のアカウント登録の状況 机上でプログラムを見返しても特に問題があるように見えず、一時的にSeleniumサーバの設定を変更し、動作状況を確認できるようにしてみました。 するとユーザーを新規登録した際にランダムに既存ユーザーの情報が置き換わってしまい、重複も発生するという動作が見えました。 アカウント情報が上書きされ、重複が発生する状況 どこかでSelenium操作用の関数にミスがあるのか・・・?と探しますが特に問題があるように見えず、処理速度の問題だろうか?とWAITTIMEを多めにとるなどをしても特に状況が変わらないなか、こんな情報が・・・。 「手動でユーザーをたくさん登録したときにもたまに起こるんだよね」 なるほど、ということは自動化ツールが悪いのではなく、また手動でも起こり得るということは処理が速すぎてトラブルになっているわけでもない・・・ であるならば、これは装置側のWebインターフェースに起因する可能性があるのではないか?という考えが頭をよぎりながらも、判断が必要になりました。 装置メーカーに連絡して、Webインターフェースの動作について調査する。 力技で乗り切る あるべき姿は前者だとは思いますが、メーカーとの折衝やその解決にはそれなりの期間を要することが予想できます。 今回は運用開始後であり急ぎ対応が求められるケースであったため、短期間の対処を最優先とし、いったん力技で乗り切ることで進めることにしました。 不具合の修正 アカウント登録用の関数を編集し、重複や消えてしまった(足りていない)ユーザーを対処する処理を追加し、また最終的に全員分のアカウント情報が正しくなるまで繰り返す処理としました。 アカウント登録用の関数の編集 <?php ... (省略 ― データベースからユーザーリストを取り出す処理) ... ... (省略 ― 装置のアカウント管理画面に遷移する処理) ... # 完了までループする while ( true ){ # 待機 usleep ( SELENIUM_TUNINGWAIT_LONG ) ; # ユーザー管理画面よりユーザー一覧を取得 $ users = userManagementGetUsers ( $ driver ) ; # ユーザー一覧のうち重複ユーザーを抽出 $ duplicateUsers = findDuplicateArray ( $ users ) ; # 重複ユーザーに対して削除処理を実施 foreach ( $ duplicateUsers as $ delUser ){ userManagementRevokeUser ( $ driver , $ delUser ) ; } # 待機 usleep ( SELENIUM_TUNINGWAIT_LONG ) ; # ユーザー管理画面よりユーザー一覧を取得 $ users = userManagementGetUsers ( $ driver ) ; # ユーザー一覧のうち過登録を抽出 $ overUsers = array_diff ( $ users , $ groupUsersArray ) ; # 過登録ユーザーに対して削除処理を実施 foreach ( $ overUsers as $ delUser ){ userManagementRevokeUser ( $ driver , $ delUser ) ; } # ユーザー一覧のうち不足を抽出 $ shortUsers = array_diff ( $ groupUsersArray , $ users ) ; # 不足ユーザーに対して登録処理を実施 foreach ( $ shortUsers as $ addUser ){ userManagementAddUser ( $ driver , $ addUser ) ; } # 待機 usleep ( SELENIUM_TUNINGWAIT_LONG ) ; # ユーザー管理画面よりユーザー一覧を取得 $ users = userManagementGetUsers ( $ driver ) ; # 比較 if ( count ( array_diff ( $ groupUsersArray , $ users )) == 0 ){ break ; } } ... (省略) ... ?> ポイント userManagementGetUsers は操作用まとめ関数を使い、テーブルからユーザーのリストを取得します findDuplicateArray は汎用のツール関数(記事記載外)を使い、テーブルの重複した値を抽出します userManagementRevokeUser は操作用まとめ関数を使い、ユーザーを削除します。例えば重複ユーザーを与えると、いったんそのユーザーを全件削除します(後述の足りないユーザーの再度追加でリカバリーします userManagementAddUser により足りないユーザーを再度追加します。 結果 重複ユーザーを削除する処理、過登録を削除する処理、不足分を追加する処理、の3点セットを、内部データベースと装置とで一致するまでループさせることで、アカウント登録状況のズレが解消できるようになりました。 アカウント登録状況を、Webアプリ内部DBと一致させる動作となるので応用が効きます。 ユーザーを新規登録したとき 装置を増やした際のアカウント情報流し込み 何らかの事情でWebアプリと装置とでアカウント情報がズレたとき といったケースすべて、この関数で対応可能です。 まとめ 今回は、Webブラウザでのみ設定可能な装置の管理を行う際の自動化ツールの作成を記事にしました。 また特定の条件下でユーザー登録が正しく行えない状況に対して、力技で対応した例も記事にしました。 設定はWebブラウザ利用のみ対応で、自動化にはSeleniumが必要、というのはレアケースではありますが、本記事をきっかけに興味を持ってチャレンジしていただければ幸いです。 執筆者 福田 匡志 NTTフィールドテクノ サービスマネジメント部 ネットワークサービス部門 クラウド・サーバ・アプリケーションセンタ クラウド・サーバ技術担当(技術・戦略グループ) 主な業務: サーバの開発・構築・運用に関する、高度化・自動化に向けた技術検討・研究・導入に携わっています。 マイパーパス(モットー): すべての人がITを活用し、楽しいデジタルライフを送ることのできる豊かな社会の実現のために働きます 参考資料・出典 本記事を執筆するにあたり、以下を参考にしました。 https://developer.mozilla.org/ja/docs/Glossary/Element https://github.com/php-webdriver/php-webdriver selenium/standalone-chrome - Docker Image docker-selenium 使って見た #Docker - Qiita Selenium Serverの使い方メモ #Python - Qiita PHP Seleniumでのエラー対応メモ #php-webdriver - Qiita Docker Selenium の最大セッション数の設定を docker run で指定する #Docker - Qiita PHP seleniumを使ってみよう #Selenium - Qiita 【PHP】selenium_チートシート (Facebook/webdriver) #Selenium - Qiita https://tech.kurojica.com/archives/47029/ PHP: 関数リファレンス - Manual 商標 Docker, Docker Compose, PHP, Selenium, Chrome(Google Chrome), Rocky Linux等、記載のサービス名・製品名は、各社・各グループ・各権利保有者の商標もしくは登録商標です
はじめに 株式会社ジャパン・インフラ・ウェイマークの川邉です。 当社はNTT西日本の子会社で、ドローン×画像解析AIを活用したインフラ点検を主に行っています。 本稿では、Amazon RDS の Aurora Serverless v2 の利用料削減のために調査した内容について記載しています。 対象読者 本記事が想定する対象読者は以下の通りです。 Amazon RDS の Aurora Serverless v2 を利用している 最近、RDSのコストが想定よりちょっと高いなと思っている 背景 筆者は画像解析AIの開発や実行環境としてAWSを利用しています。DBには Amazon RDS の Aurora Serverless v2 を利用しているのですが、モニタリングで各種リソースの消費状況を確認している中で、 ServerlessDatabaseCapacity の値が想定ほど下がっていないことに気付きました。 DBサーバの情報 インスタンスタイプ:Serverless v2 エンジンバージョン:8.0.mysql_aurora.3.09.0 最小容量:0.5 ACU 最大容量:256 ACU モニタリング 以下は、ほぼ使用していない状態での CPUUtilization 、 FreeableMemory 、 ServerlessDatabaseCapacity のモニタリング結果です。 CPUUtilization FreeableMemory ServerlessDatabaseCapacity 値の説明 公式ドキュメント では、それぞれの値について以下のように説明しています。 CPUUtilization このメトリクスは Aurora Serverless v2 において、プロビジョン済みの DB インスタンスとは異なる解釈がされます。Aurora Serverless v2 の場合、この値は、現在の CPU の使用量を DB クラスターの最大 ACU 値で使用可能な CPU 容量で割った割合です。Aurora はこの値を自動的にモニタリングし、DB インスタンスが CPU 容量 を使用している割合が常に大きい場合、Aurora Serverless v2 DB インスタンスをスケールアップします。 このメトリクスが 100.0 値に近づいた場合、DB インスタンスは最大 CPU 容量に達しています。クラスターの最大 ACU 設定を引き上げることを検討してください。このメトリクスがリーダー DB インスタンスで 100.0 値に近づいた場合、クラスターにリーダー DB インスタンスを追加することを検討してください。これにより、ワークロードの読み取り専用部分のワークロードをより多くの DB インスタンスに分散することで、各リーダー DB インスタンスのロードを軽減できます。 FreeableMemory この値は、Aurora Serverless v2 DB インスタンスを最大容量にスケーリングしたときに利用できる未使用のメモリ量を表します。現在の容量が最大容量を下回る 各 ACU では、この値は約 2 GiB 増加します。したがって、DB インスタンスが限りなく大きくスケールアップされるまで、このメトリクスはゼロに近づきません。 このメトリクスが 0 値に近づいた場合、DB インスタンスは可能な限りスケールアップし、使用可能なメモリの上限に近づいています。クラスターの最大 ACU 設定を引き上げることを検討してください。このメトリクスがリーダー DB インスタンスで 0 値に近づいた場合、クラスターにリーダー DB インスタンスを追加することを検討してください。これにより、ワークロードの読み取り専用部分のワークロードをより多くの DB インスタンスに分散することで、各リーダー DB インスタンスのメモリ使用量を軽減できます。 ServerlessDatabaseCapacity インスタンスレベルのメトリクスとして、現在の DB インスタンスの容量で表される ACU 値を報告します。クラスターレベルのメトリクスとして、クラスター内のすべての Aurora Serverless v2 DB インスタンスの ServerlessDatabaseCapacity 値の平均を表しています。このメトリクスは、Aurora Serverless v1 ではクラスターレベルのメトリクスに限られます。Aurora Serverless v2 では、DB インスタンスレベルとクラスターレベルで利用できます。 引っかかりポイント CPUUtilization を見ると CPU は最大 ACU の 0.3 % 程度しか利用されていないにもかかわらず、 ServerlessDatabaseCapacity は 2 ACU(≒4GiBのメモリ+対応するvCPU)が消費されている状態が続いている事が分かります。最大容量 は 256 ACU ですから、その 0.3 % は 0.768 ACU のはずです FreeableMemory を見ると 512 GBytes 程度残っています。最大容量の 256 ACU までスケールアップした時のメモリが 256 * 2 GiB = 512 GiB ですから、メモリはほとんど利用されていない事が分かります 以上の理由から、 ServerlessDatabaseCapacity は 0.7 ACU 程度の値を推移して欲しいのですが、どういう訳か、常に 2 ACU 程度が利用されており、無駄にコストがかかっていることになります。 公式ドキュメントの調査 公式ドキュメント によると、Aurora PostgreSQL の以下のパラメータについては 最大 ACU 設定に基づくメモリサイズから算出したデフォルト値を使用します と記載されています。 autovacuum_max_workers autovacuum_vacuum_cost_limit autovacuum_work_mem effective_cache_size maintenance_work_mem 上記はあくまで Aurora PostgreSQL についての記載であり、Aurora MySQL については同様の記載を見つけることはできなかったのですが、ひょっとすると、Aurora MySQL についても同様に最大 ACU 設定に応じて確保されるメモリがあるのかもしれません。 試してみた ということで、最大 ACU 設定を 16 まで落としてみた結果が以下の通りです。設定を変更した 16:30 頃を境に、各グラフが変動しているのが分かります。最大 ACU 設定に対する比率で計算される CPUUtilization と FreeableMemory の変化は想定内ですが、現在の利用状況に応じて計算されると想像していた ServerlessDatabaseCapacity が 最小 ACU 設定の 0.5 ACU 近くまで下がるという結果になりました。 CPUUtilization FreeableMemory ServerlessDatabaseCapacity まとめ 本項では Amazon RDS の Aurora Serverless v2 における最大 ACU 設定の影響について簡単に記しました。 サーバレスシステムなので「最大値の設定はあくまで過剰利用を抑制するもので、コストは実際の利用量に依存するのだろう」と思っていたらまったくそんなことはなかったという結論ですので、皆さまもご注意ください。 免責事項 本記事に記載された情報は、2025年12月時点での公開情報および筆者の検証・調査結果に基づくものです。 記事に記載されているAWS各サービスの機能、実行条件などは予告なく変更される場合があります 本記事の内容を実践される際は、必ずAWSの最新の公式ドキュメントをご確認ください 本記事の情報に基づいて行われた意思決定や実装により生じた損害について、筆者および所属組織は一切の責任を負いかねます 参考資料・出典 本記事の執筆にあたり参考としたページは以下の通りです Amazon Aurora のユーザーガイド: https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.setting-capacity.html 執筆者 川邉 隆伸 (ジャパン・インフラ・ウェイマーク開発部所属) 画像認識系AIの開発や、それらを提供するSaaS環境の構築を行っています。 商標 AWS、Amazon RDS、Aurora PostgreSQL、Aurora MySQLは Amazon Web Services, Inc. またはその関連会社の商標もしくは登録商標です
1. はじめに NTT西日本の伏尾です。 Dify 1 のようなローコードでLLMアプリケーションを作れるツールが広がり、いわゆる市民開発によるアプリケーションが手軽に作れるようになってきました。 特にRAGチャットボットによる問い合わせ対応の省力化は有力なユースケースです。 一方で、このような市民開発ツールでは作成したツールの評価については後回しになりがちです。 今回はそのような背景を踏まえ、Difyで作成したRAGチャットボットを対象として自動評価の枠組みを取り入れることを検討します。具体的には、RAGシステムの自動評価のフレームワークであるRagas 2 の指標の一部をDify上に実装することを行います。 1. はじめに 2. 想定読者 3. RAGシステムによるアウトプット品質の評価フレームワーク:Ragas 4. 実装図 5. 実装方法 5.1 Answer Relevancy(回答関連性) 5.2 Faithfulness(忠実性) 5.3 Context Recall(必要コンテキストの網羅性) 5.4 Context Precision(コンテキスト精度) 6. 動作確認 7. まとめと今後の展望 執筆者 商標 2. 想定読者 ・Difyで作成したRAGチャットボットの品質を定量的に評価してみたい方 ・エンジニアがいない現場で評価・改善のループを回したい方 3. RAGシステムによるアウトプット品質の評価フレームワーク:Ragas RagasはRAGシステムによるアウトプットをLLMを用いて自動評価するためのフレームワークです。Pythonのモジュールとして利用が可能ですが、Difyのサンドボックス環境には導入されておらず、また調べた限り追加もできませんでした。本記事では、その指標の一部をDify上で実装することで、Dify上のみで完結するようにRagasの指標による評価を行えるようにします。なお、外部ツールと連携した場合はDify上で作成したRAGシステムの評価が可能なことが この記事 で報告されています。 Ragasではいくつかのメトリクスが提案されています。本記事では、Ragasの提案論文 3 や旧バージョンのドキュメント 4 を参照し、中心的な指標として下記をリストアップしました。いずれも0から1の範囲で1に近いほど精度がよいことを示します。 Answer Relevancy(回答関連性) :回答がユーザーの入力に対してどれだけ関連して答えているかを判別する指標です。 5 Faithfulness(忠実性) :回答が与えられた検索結果にどれだけ事実的に整合しているかを示す指標です。 6 まず回答を主張に分解し、各主張が与えられた検索結果から主張可能かを判断する二段構えで実装されています。 Context Recall(必要コンテキストの網羅性) :検索結果の内容は、Reference(=理想的な回答)を回答するのに十分な内容があるかを判別する指標です。 7 Context Precision(コンテキスト精度) :検索結果のうち、Referenceに関連しているものを上位に配置しているかを判別する指標です。 8 検索結果にReferenceに関連しない情報が存在したり、検索結果の上位に関連しない情報があると値が0に近づきます。 上記の指標は精度の分析や改善の一手を考察するための指標として有用です。本記事では、Ragasを参考に上記4つのメトリクスを自動で検出できるフローを作り上げていきます。 4. 実装図 実装した評価機能付きRAGチャットボットの全体図 上図がDify上での実装になります。 ⓪はユーザー入力を受け付け知識検索を行い回答を作成する部分です。今回の評価には直接の関係はないため、知識検索と回答作成のブロックをつなげる最低限の実装にしています。 ①~④は各評価指標を作成する部分で、入力はユーザーの質問と回答、使用した検索結果、さらに事前に用意したユーザーの質問に対するReferenceを使用します。それぞれ、①Answer Relevancy(回答関連性)②Faithfulness(忠実性)③Context Recall(必要コンテキストの網羅性)④Context Precision(コンテキスト精度)を計算しています。 黒で囲ったブロックはユーザーの質問に対応するReferenceが存在する場合にそれを取得するブロックです。 最後に、ユーザーの入力への回答に追記する形で各評価値を記載し回答します。 5. 実装方法 実装にあたっては以下の記事やドキュメントを参考にしました。 RAGASのプロンプトを理解する #LLM - Qiita Ragasにおけるコンテキスト評価指標:Context RecallとContext Precision #ragas - Qiita Metrics | Ragas 5.1 Answer Relevancy(回答関連性) Answer Relevancyの計算の実装 Answer Relevancy(回答関連性)は、下記の手順で計算します。 (1)回答の情報からその回答を得るための質問を生成します。 プロンプトはRagasのコードを参考に日本語に翻訳し、下記を使用しています。Ragasの実装では複数回生成しますが、本記事では1回の生成を行う実装としています。 システムプロンプト 与えられた回答に対して、その回答に対応する質問文を生成し、さらにその回答がnoncommittalかどうかを判定してください。noncommittalであればnoncommittalを1、committal(noncommittal ではない)であれば0を与えてください。noncommittalな回答とは、はぐらかしていたり、曖昧だったり、あいまいで断定を避けている回答のことです。たとえば「わかりません」や「確かではありません」といった回答はnoncommittalに該当します。 出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください: {"properties": {"question": {"title": "Question", "type": "string"}, "noncommittal": {"title": "Noncommittal", "type": "integer"}}, "required": ["question", "noncommittal"], "title": "ResponseRelevanceOutput", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。 --------例----------- 例 1 入力: { "response": "アルバート・アインシュタインはドイツで生まれました。" } 出力: { "question": "アルバート・アインシュタインはどこで生まれましたか?", "noncommittal": 0 } 例 2 入力: { "response": "2022年以降の情報は知らないので、2023年に発明されるスマートフォンの画期的な機能についてはわかりません。" } 出力: { "question": "2023年に発明されたスマートフォンの画期的な機能は何だったでしょうか?", "noncommittal": 1 } ユーザープロンプト それでは、次の入力について同じことを行ってください。 Input: <回答> Output: (2)生成した回答全体から質問部分を抽出 パラメータ抽出ブロックを使用して与えられたJSON形式のテキストから必要なパラメータを自然言語で指定して抽出します。 パラメータ抽出の設定画面 (3)生成した質問と元の質問をエンベディング Difyにはモデル設定でエンベディングモデルを設定できますが、チャットフローやワークフローで提供されているブロックからそれらのエンベディングモデルを呼び出すことができません。したがって、ここではHTTPリクエストブロックでエンベディングモデルを直接呼び出しています。API_KEYは環境変数を使用して導入しています。 HTTPリクエストの設定画面 (4)エンベディングした二つのベクトルからAnswer Relevancy(回答関連性)のスコア)を得ます (3)で作成したブロックのコサイン類似度がスコアになります。 5.2 Faithfulness(忠実性) Faithfulnessの計算の実装 Faithfulnessの計算は2回のLLM問い合わせが必要です。 (1)与えられた回答を主張に分解する。 回答は複数の主張を組み合わせてできているため、まずは個別の主張に分解します。 システムプロンプト 質問と回答が与えられた場合、回答に含まれる各文の複雑さを分析します。各文を1つ以上の完全に理解可能な文に分解します。どの文にも代名詞が使用されていないことを確認してください。出力はJSON形式で出力してください。 出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください: {"properties": {"statements": {"description": "The generated statements", "items": {"type": "string"}, "title": "Statements", "type": "array"}}, "required": ["statements"], "title": "StatementGeneratorOutput", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。 --------例----------- 例 1 入力: { "question": "アルバート・アインシュタインとは誰で、どのような業績で最もよく知られていますか?", "answer": "彼はドイツ生まれの理論物理学者で、史上最も偉大で影響力のある物理学者の一人として広く認められています。彼は相対性理論の開発で最もよく知られていますが、量子力学理論の発展にも重要な貢献をしました。" } 出力: { "statements": [ "アルバート・アインシュタインはドイツ生まれの理論物理学者でした。", "アルバート・アインシュタインは、史上最も偉大で影響力のある物理学者の一人として認められています。", "アルバート・アインシュタインは相対性理論の開発で最もよく知られています。", "アルバート・アインシュタインは量子力学理論の発展にも重要な貢献をしました。" ] } ユーザープロンプト それでは、次の入力について同じことを行ってください。 Input: <回答> Output: (2)各主張の判定 生成した各主張が検索結果から推論できるかを判断します。推論可能なら1、そうでなければ0を出力します。 システムプロンプト あなたの課題は、与えられた文脈に基づいて、一連の文の忠実性を判断することです。各文について、文脈から直接推論できる場合は1、文脈から直接推論できない場合は0を判定として返してください。 出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください: {"$defs": {"StatementFaithfulnessAnswer": {"properties": {"statement": {"description": "the original statement, word-by-word", "title": "Statement", "type": "string"}, "reason": {"description": "the reason of the verdict", "title": "Reason", "type": "string"}, "verdict": {"description": "the verdict(0/1) of the faithfulness.", "title": "Verdict", "type": "integer"}}, "required": ["statement", "reason", "verdict"], "title": "StatementFaithfulnessAnswer", "type": "object"}}, "properties": {"statements": {"items": {"$ref": "#/$defs/StatementFaithfulnessAnswer"}, "title": "Statements", "type": "array"}}, "required": ["statements"], "title": "NLIStatementOutput", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。 --------例----------- 例 1 入力: { "context": "ジョンはXYZ大学の学生です。コンピュータサイエンスの学位取得を目指しています。今学期は、データ構造、アルゴリズム、データベース管理など、複数のコースを受講しています。ジョンは勤勉な学生で、多くの時間を勉強と課題の完了に費やしています。プロジェクトに取り組むため、図書館に遅くまでいることがよくあります。", "statements": [ "ジョンは生物学を専攻しています。", "ジョンは人工知能のコースを受講しています。", "ジョンは熱心な学生です。", "ジョンはパートタイムの仕事をしています。" ] } 出力: { "statements": [ { "statement": "ジョンは生物学を専攻しています。", "reason": "ジョンの専攻はコンピュータサイエンスと明示的に記載されています。彼が生物学を専攻していることを示す情報はありません。", "verdict": 0 }, { "statement": "ジョンは人工知能に関するコースを受講しています。", "reason": "文脈にはジョンが現在受講しているコースが記載されており、人工知能については言及されていません。したがって、ジョンがAIに関するコースを受講しているとは推測できません。", "verdict": 0 }, { "statement": "ジョンは熱心な学生です。", "reason": "文脈には、彼がかなりの時間を勉強と課題の完了に費やしていることが記載されています。さらに、彼はプロジェクトに取り組むために図書館に遅くまで残ることが多いと記載されており、これは彼の献身を示唆しています。", "verdict": 1 }, { "statement": "ジョンはパートタイムのjob.", "reason": "ジョンがパートタイムの仕事をしていることについては、文脈から何も得られていない。", "verdict": 0 } ] } 例2 入力: { "context": "光合成は、植物、藻類、および特定の細菌が光エネルギーを化学エネルギーに変換するプロセスです。", "statements": [ "アルバート・アインシュタインは天才でした。" ] } 出力: { "statements": [ { "statement": "アルバート・アインシュタインは天才でした。", "reason": "文脈と記述は無関係です。", "verdict": 0 } ] } ユーザープロンプト それでは、次の入力について同じことを行ってください。 Input: { "context": <検索結果>, "statements": <主張の生成で生成した主張のリスト> } Output: (3)Faithfulnessの計算 各主張に対する推論結果の平均がFaithfulnessの値です。 5.3 Context Recall(必要コンテキストの網羅性) Context Recallの計算の実装 (0)Referenceの取得 取得用コード ユーザーからの質問に対して事前に用意した理想的な回答を取得します。 Difyではデータセットとしてユーザーからの質問とReferenceのセットを保存できなかったためコードブロックに直接記述する形で質問とReferenceのセットを用意しました。 (1)理想的な回答が検索結果に沿っているか判断します。 理想的な回答の各主張が検索結果の検索結果に帰属するかどうかを判断します。帰属する場合は1、そうでない場合は0です。 システムプロンプト 文脈と回答が与えられます。回答に含まれる各文を分析し、その文が与えられたコンテキストに帰属するかどうかを分類します。「はい」(1)または「いいえ」(0)のいずれかの二値分類のみを使用します。理由をJSON形式で出力します。 出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください: {"$defs": {"ContextRecallClassification": {"properties": {"statement": {"title": "Statement", "type": "string"}, "reason": {"title": "Reason", "type": "string"}, "attributed": {"title": "Attributed", "type": "integer"}}, "required": ["statement", "reason", "attributed"], "title": "ContextRecallClassification", "type": "object"}}, "properties": {"classifications": {"items": {"$ref": "#/$defs/ContextRecallClassification"}, "title": "Classifications", "type": "array"}}, "required": ["classifications"], "title": "ContextRecallClassifications", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。 --------例----------- 例 1 入力: { "question": "アルバート・アインシュタインについて教えてください。", "context": "アルバート・アインシュタイン(1879年3月14日 - 1955年4月18日)はドイツ生まれの理論物理学者で、歴史上最も偉大で影響力のある科学者の一人と広く考えられています。相対性理論の考案で最もよく知られていますが、量子力学にも重要な貢献を果たし、20世紀初頭に現代物理学が成し遂げた自然に対する科学的理解の革命的な再構築において中心人物となりました。相対性理論から導き出された質量とエネルギーの等価性を示す式 E = mc2 は、「世界で最も有名な式」と呼ばれています。彼は1921年に「理論物理学への貢献、特に時空の法則の発見」によりノーベル物理学賞を受賞しました。アインシュタインは、量子論の発展における極めて重要な一歩である「光電効果」を発見しました。彼の研究は科学哲学にも影響を与えたことで知られています。1999年に英国の物理学誌『Physics World』が世界中の著名な物理学者130人を対象に行った投票で、アインシュタインは史上最高の物理学者に選ばれました。彼の知的業績と独創性により、アインシュタインは天才の代名詞となっています。", "answer":"アルバート・アインシュタインは1879年3月14日に生まれたドイツ生まれの理論物理学者であり、史上最も偉大で影響力のある科学者の一人と広く考えられています。彼は理論物理学への貢献により、1921年にノーベル物理学賞を受賞しました。彼は1905年に4本の論文を発表しました。アインシュタインは1895年にスイスに移住しました。" } 出力: { "classifications": [ { "statement": "アルバート・アインシュタインは1879年3月14日に生まれたドイツ生まれの理論物理学者であり、史上最も偉大で影響力のある科学者の一人と広く考えられています。", "reason": "アインシュタインの生年月日は文脈の中で明確に言及されています。", "attributed": 1 }, { "statement": "彼は理論物理学への貢献により1921年のノーベル物理学賞を受賞しました。", "reason": "指定された文脈には正確な文が含まれています。", "attributed": 1 }, { "statement": "彼は1905年に4本の論文を発表しました。", "reason": "指定された文脈には彼が執筆した論文についての言及はありません。", "attributed": 0 }, { "statement": "アインシュタインは1895年にスイスに引っ越しました。" "reason": "この文脈では、これを裏付ける証拠はありません。" "attributed": 0 } ] } ユーザープロンプト それでは、次の入力について同じことを行ってください。 Input: { "question": <ユーザーからの質問>, "context": <検索結果>, "answer": <回答> } Output: (2)Context Recallの計算 (1)で得た各主張の帰属するかどうかの値の平均がContext Recallになります。 5.4 Context Precision(コンテキスト精度) Context Precisionは上述した通りReferenceの内容に関係のない検索結果が含まれていたり、関係のない情報が検索結果の上位にある場合はスコアが0に近づきます。 Context Precisionは以下の数式で与えられます。 は上位 番目までの検索結果のうち、Referenceの内容に関連のあった検索結果の個数です。 は 番目の検索結果がReferenceの内容に関連があった場合は1、なかった場合は0になります。 実装上は の分母が0にならないよう、微小な定数を足すことで回避します。 Context Precisionの計算の実装 (0)Referenceの取得 Context Recallの処理と同じ。 (1)検索結果を各結果に分割し、配列に保存 知識検索ブロックの結果を受け取り、各検索結果を要素とする配列にします。 実装図 (2)各検索結果がReferenceに関連しているかどうかを判定 Referenceを回答するのに各検索結果が役立つかどうかを判定します。役立つ場合は1、そうでない場合は0です。 検索結果ごとにイテレーションを回す処理をしています。 システムプロンプト 与えられた質問、回答、文脈から、文脈が与えられた回答に至る上で役立ったかどうかを検証します。役立った場合は「1」、役に立たない場合は「0」と判定し、JSON形式で出力します。 出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください: {"properties": {"reason": {"description": "Reason for verification", "title": "Reason", "type": "string"}, "verdict": {"description": "Binary (0/1) verdict of verification", "title": "Verdict", "type": "integer"}}, "required": ["reason", "verdict"], "title": "Verification", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。 --------例----------- 例 1 入力: { "question": "アルバート・アインシュタインについて教えてください。", "context": "アルバート・アインシュタイン(1879年3月14日 - 1955年4月18日)は、ドイツ生まれの理論物理学者であり、歴史上最も偉大で影響力のある科学者の一人と広く考えられています。相対性理論の考案で最もよく知られていますが、量子力学にも重要な貢献を果たし、20世紀初頭に現代物理学が成し遂げた自然に対する科学的理解の革命的な再構築において中心人物となりました。相対性理論から導き出された質量とエネルギーの等価性の式 E = mc2 は、「世界で最も有名な式」と呼ばれています。彼は1921年に「理論物理学への貢献、特に光電効果の法則の発見」によりノーベル物理学賞を受賞しました。量子論の発展における極めて重要な一歩である「量子効果」を提唱しました。彼の研究は科学哲学への影響でも知られています。1999年に英国の物理学誌『Physics World』が世界中の著名な物理学者130人を対象に行った投票で、アインシュタインは史上最高の物理学者に選ばれました。彼の知的業績と独創性により、アインシュタインは天才の代名詞となっています。", "answer":"アルバート・アインシュタインは1879年3月14日に生まれたドイツ生まれの理論物理学者であり、史上最も偉大で影響力のある科学者の一人と広く考えられています。彼は理論物理学への貢献により、1921年にノーベル物理学賞を受賞しました。" } 出力: { "reason": "提供された文脈は、与えられた回答を導き出す上で非常に役立ちました。文脈には、アルバート・アインシュタインの生涯と貢献に関する重要な情報が含まれており、回答にも反映されています。", "verdict": 1 } 例2 入力: { "question": "2020 ICCワールドカップの優勝者は誰ですか?", "context": "2022年10月16日から11月13日までオーストラリアで開催された2022 ICC男子T20ワールドカップは、同大会の第8回大会でした。当初は2020年に開催予定でしたが、COVID-19パンデミックの影響で延期されました。決勝戦ではイングランドがパキスタンを5ウィケット差で破り、2度目のICC男子T20ワールドカップ優勝を果たしました。", "answer": "イングランド" } 出力: { "reason": "文脈は、2020 ICCワールドカップの状況を明確にするのに役立ちました。 2020年に開催予定だったが実際には2022年に開催された大会で、イングランドが優勝したことを示しています。", "verdict": 1 } 例3 入力: { "question": "世界で最も高い山は何ですか?", "context": "アンデス山脈は、南アメリカに位置する世界最長の大陸山脈です。7か国にまたがり、西半球の最高峰の多くがそびえ立っています。この山脈は、高地のアンデス高原やアマゾンの熱帯雨林など、多様な生態系で知られています。", "answer": "エベレスト山です。" } 出力: { "reason": "提供された文脈はアンデス山脈について論じていますが、アンデス山脈は印象的ですが、エベレスト山を含んでおらず、世界最高峰に関する質問にも直接関連していません。", "verdict": 0 } ユーザープロンプト それでは、次の入力について同じことを行ってください。 Input: { "question": <ユーザーからの質問>, "context": <検索結果>, "answer": <理想的な回答> } Output: (3)Context Precisionの計算 前述した式に基づき、各検索結果に対する関連するかどうかの値(0, 1)からContext Precisionを求めます。 Context Precisionの計算 6. 動作確認 実行結果 質問に対して回答した後、各指標が計算されていることが確認できました。 事前に質問と理想的な回答を整備しておけば、作成者は作成したチャットボットに質問を投げ続けるだけで、各種指標を得ることができます。 よりRagasの実装に近づけるには、事前に質問と理想的な回答のリストを保存しておき、一度の実行で用意したリストの質問すべてに対して評価を実行することが望ましいです。しかしDify上では外部から呼び出しても、応答と検索結果を別の変数として受け取ることができず実現が難しかったです。 7. まとめと今後の展望 Difyで作成したRAGチャットボットに対してRagasで使用する一部指標をDify上で実装しました。 実装を通して、Ragasで評価したい観点をどのように実装しているのか把握でき、今後のRAGチャットボットの構築や評価設計において活用できる知見が得られました。 今後の展望としては、まずRagasの評価結果と本記事で実装したDify上での評価結果を比較することでRagasの実装にどの程度合致しているか検証することが考えられます。また、Difyではワークフローを別のワークフローやチャットフローからツールとして呼び出せるため、本記事で実装した評価の部分をツール化することで、別のRAGチャットボットからでも容易に利用できる仕組みの追加を目指していきたいと考えています。 執筆者 伏尾 佳悟(NTT西日本 技術革新部) AIエンジニアとして主にLLM関連のAIプロジェクトの技術支援を担当 商標 DifyはLangGenius, Inc.の登録商標です。 Dify: 最先端のAgentic AI開発プラットフォーム ↩ ragas/src/ragas/metrics/_answer_relevance.py at main · vibrantlabsai/ragas · GitHub ↩ [2309.15217] Ragas: Automated Evaluation of Retrieval Augmented Generation ↩ Metrics | Ragas ↩ Response Relevancy - Ragas ↩ Faithfulness - Ragas ↩ Context Recall - Ragas ↩ Context Precision - Ragas ↩
1.はじめに NTT西日本の大賀です。 先日投稿された「話題のAIエージェント作ってみた( 話題のAIエージェントを作ってみた - NTT WEST Engineers' Blog )」では、エンジニアが AI エージェントを 1 から実装する方法について紹介されていました。 今回はより幅広い方が AI エージェントに触れる機会となりうるローコード/ノーコードツール「Dify」を用いて AI エージェントを構築し、その実力値を検証した内容をご紹介します。 また、OSS の「LangChain」を使ってフルスクラッチで構築した AI エージェントとの比較検証も実施しました。 ※本記事は 2025年11月時点の情報に基づきます。 2.対象読者 本記事が想定する対象読者は以下の通りです。 Dify の AI エージェント機能の実力値を知りたい方 Dify と LangChain の エージェント精度の比較に興味がある方 3.背景・目的 AI エージェントは、LLM(大規模言語モデル)が事前登録されたツールを自律的に選択・使用しながらユーザーのリクエストに対応する仕組みを指します。 AI エージェントの構築方法には大きく二つあります。 市中の OSS を使って自身でコードを書いて実装する 簡易に実装できるプラットフォーム(Dify など)を使用する OSS での構築は自由度が高い一方、スキルが求められます。 一方プラットフォームは構築のハードルが下がる代わりに機能制限が存在します。 本記事では LangChain で構築したエージェント と Dify で構築したエージェント を同じタスクで比較し、それぞれの実力値を測定、評価することを目的としました。 4.検証タスク概要 ユーザーからの質問に対し、あらかじめ格納した背景知識から関連する情報を検索し、LLM が回答を作成する RAG タスクを対象としました。 検索対象は Huggingface でRAG 評価サンプル公開されている以下 5 ドメインのドキュメント 金融/IT/製造/流通/公共 Dify 側にこれらを検索用ドキュメントとして登録 評価用 QA 約 50 問を作成し、2 種類のエージェントに投入 取得ドキュメントの正しさと生成回答の正しさを評価 【参考】 https://huggingface.co/datasets/allganize/RAG-Evaluation-Dataset-JA 5.AIエージェントの構成 5-1.Dify を用いた AI エージェントの構成 Difyエージェントアシスタント機能を利用 Dify に RAG 用ドキュメントを登録し、検索ツールとして LLM が呼び出せるように設定 LLM は Azure OpenAI(AOAI)を API 呼び出しで利用 Difyエージェント構成図 Difyエージェントアシスタントの設定画面 5-2.LangChain を用いた AI エージェントの構成 Python が動作する PC 上でエージェントロジックをコーディング RAG ツールは Dify 上に登録したものを API 経由で呼び出し、条件を Dify と統一 LLM は AOAI を同様に利用 ソースコードは以下の github にサンプルコードを置いています 【参考】 https://github.com/TakaOga199/LangchainAIagent_source LangChainエージェント構成図 6.評価方法 RAG タスクのエージェント性能を以下 2 点で評価しました。 観点1:ドキュメント検索精度  評価用QA 50 問中、回答に必要なドキュメントを正しく検索で取得できた割合。 観点2:回答生成精度  正しいドキュメント取得後、LLM が正しい回答を生成した割合。 最終精度 = ドキュメント検索精度 × 回答生成精度 7.検証結果 7-1.初回検証 まずは簡単なプロンプト(指示)をAIエージェントに与え、各AIエージェントがどれくらいの実力値なのかを確認しました。 プロンプト あなたは5つの業種ドメイン(金融、情報通信、製造、公共、流通・小売)の問い合わせに対して回答するエージェントです。 ユーザーの質問に対して適切な関連文書を検索し、検索で取得した文書の情報のみで回答を作成してください。 回答を生成した際に、ユーザーの質問に対して正しく回答が生成できているかをチェックし、問題や不足がある場合は、再度情報を検索し、回答を修正してください。 ユーザーの質問に正しい回答ができたら、ユーザーに最終回答を提供してください。 結果サンプル 結果の1例としてDifyエージェントでは不正解だったがLangChainエージェントでは正解した例を示します。 同様に5ドメインの質問を合計50問入力し正誤をまとめています。 結果サンプル 結果サマリ 50問の正誤をまとめた結果は以下の通り エージェント環境 ドキュメント検索精度 回答生成精度 最終精度 Dify エージェント 25% 36% 10%(5/50) LangChain エージェント 55% 42% 24%(12/50) 考察 エージェントの動作ログを確認すると、ドキュメントの検索時にLLMが質問文(クエリ)をキーワードに変換しており、質問の意図に沿わないドキュメントを取得している場合が多く見られた(下画像) 改善のためにプロンプトの内容を見直し検索時の挙動を具体的に記載して再検証を試みます。 回答生成についても、生成した回答の正しさを検証するタスクを明記し、より正しい回答を生成するよう変更。(7-2) クエリのキーワード化 7-2.プロンプトエンジニアリングによる精度改善 7-1で得られた質問文のキーワード化の課題改善と、正確な回答を狙って、より厳密にエージェントの動作を規定する以下のプロンプトに更新し再度検証を行いました。 プロンプト あなたは、以下の5つの業種ドメイン(金融、情報通信、製造、公共、流通・小売)に関する問い合わせに対して回答を行うエージェントです。 ユーザーからの質問に基づき、関連する情報を検索し、検索結果に基づいて正確かつ具体的な回答を提供してください。 検索先には必ず必要な情報が含まれている前提で動作してください。 また、検索が不十分で情報が見つからないという回答を避けるために、検索プロセスを徹底的に行ってください。 # 検索プロセス 1. **検索クエリの生成** - 最初に、ユーザーの質問をそのまま検索クエリとして使用してください。 2. **検索の再試行** - 適切な検索結果が得られない場合は、以下の手順で検索条件を調整してください: - 質問文から重要なキーワードを抽出する。 - 業種ドメインを明確に指定する。 - キーワードを組み合わせて異なる検索クエリを生成する。 3. **検索範囲** - 検索対象には、以下のデータベースやソースを含めてください: [ツール情報は省略] 4. **検索結果の収集** - 検索結果が得られた場合は、それに基づいて回答を生成してください。 # 回答生成 - 検索結果で得られた情報のみを使用して、ユーザーの質問に対する正確で簡潔な回答を作成してください。 - 曖昧さを排除することを目指しつつ、関連情報を補足的に探索することを許可します。 # 回答検証 - 作成した回答がユーザーの質問に対して正確であるか、また情報に不足や誤りがないかを確認してください。 - 必要に応じて再度情報を検索して回答を修正してください。 結果サマリ(検索精度のみ) エージェント環境 検索精度(前回 → 今回) Dify エージェント 25% → 52% LangChain エージェント 55% → 75% 両環境ともに検索精度の向上が確認されたが、依然としてDifyとLangChainの間に検索精度の乖離が見られた。 考察 ドキュメント検索を行うツールはDifyエージェント、LangChainエージェントで共通 LLMの動作を規定するプロンプトやモデルも共通のものを使用 両者で検索精度、回答生成精度に乖離がある理由としては主に以下が考えられます エージェントの動作を規定する動作ロジック(ソースコード)がDify、LangChain間で異なる ドキュメント検索ツールの呼び出し方法の違いが検索精度に影響を与えている 1については両エージェントのソースコードを読み解く必要があるため、今回は2の検索ツールの呼び出し方法を2エージェント間でそろえて再検証を実施しました。(7-3) 7-3.検索ツールの呼び出し方法変更 DifyではこれまでDify内のツール呼び出し機能を使用し、LangChainでは同じDify上のツールをAPI経由で呼び出して使用していました。この呼び出し方法の差が精度の差に寄与しているのかどうか確かめるため、Difyのツール呼び出し方法をLangChainに合わせてAPI呼び出しに変更し再度精度検証を行いました。 ツール呼び出し方法の変更後構成図 結果サマリ エージェント環境 ドキュメント検索精度 回答生成精度 最終精度 Dify エージェント 75% 68% 50%(25/50) LangChain エージェント 75% 69% 52%(26/50) Difyエージェントの検索精度が向上し、LangChainエージェントと同等の精度となることが確認できました。 考察 今回の検証で検索ツールの呼び出し方法が検索精度に影響を与えていたであろうことが確認できました。 Dify内のツール呼び出し機能を使用する際に、何かユーザーには見えないロジックが働いているのではないかと想像しますが、詳細には内部仕様のため分かりませんでした。 また、検索結果や回答の中身を見ると7-2で記載した「動作ロジックの差分」はそこまで大きな影響を及ぼすものではないと推測されます。(少なくとも今回のタスクの範囲では) 8. まとめ 今回はGUIベースでAIエージェントを作成できるDifyとフルスクラッチで作成したLangChainのエージェントの精度を「RAGタスク」に絞って比較評価しました。 以下のようなポイントを押さえることで「RAGタスク」の精度が向上することが確認でき、またDifyでもフルスクラッチと同等精度のエージェントを作成することができました。  1. プロンプトはステップに分解して記載し、各ステップの動作条件についても詳細に記載  2. ツールの呼び出し手法はAPI呼び出しの方が高精度(今回の検証の場合) とはいえ今回の最終結果である検索精度75%や、最終回答精度50%は仕事など正確さが求められる場面では不安が残る数字であり、引き続きの精度向上が必要と感じます。これは進捗があればまた記事にするかもしれません。 執筆者 大賀 隆寛(NTT西日本) 社内でのAI技術の導入支援を主に担当し、AIのチューニングや評価に携わっています。 商標  「Microsoft Azure」は,米国Microsoft Corporationの米国およびその他の国における登録商標または商標です。 「Dify」は,米国LangGenius, Incの米国およびその他の国における登録商標または商標です。
1. はじめに NTT西日本の桂川です。 本記事では、AWSリソースを安全にクリーンアップするためのaws-nukeの実践テクニックをご紹介します。 aws-nukeは、AWSアカウント内のリソースを一括で削除できるオープンソースツールです。 便利なツールですが、誤って意図しないリソースを削除してしまうリスクもあるため、慎重な運用が求められます。 aws-nukeを使用することで、AWSアカウント内のリソースを効率的に一括削除できるため、削除作業の効率化や不要なリソースの削除によるコスト削減を目的に導入している方も多いのではないでしょうか。 一方で、実際の運用では「セキュリティ統制やコスト管理のために保持すべきリソース」が存在するケースもあり、aws-nukeの運用に課題を感じている方もいるのではないかと思います。 そこで本記事では、こうしたリソースを保護するために活用できる「5つの実践テクニック」を解説します。 なお、本記事の内容は2025年11月時点の情報に基づいています。 < 目次 > 1. はじめに 2. 対象読者 3. 背景・目的 4. aws-nuke とは? 4.1 前提条件 4.2 考慮事項 5. 実行方法 ① CloudShellを起動します ② aws-nukeをダウンロードします ③ aws-nukeの設定ファイルを作成します ④ aws-nukeを実行します 6. 設定ファイルの基本構成 7. リソース保護に役立つ実践テクニック 7.1 ブロックリスト 7.2 タグ条件によるフィルタリング 7.3 文字列条件によるフィルタリング 7.4 時間条件によるフィルタリング 7.5 プリセット 8. まとめ 執筆者 参考資料・出典 商標  免責事項 2. 対象読者 本記事の対象読者は、以下のような方々を想定しています。 AWS運用自動化に関心があり、効率化やコスト削減を検討している方 aws-nuke導入を検討しており、基本的な使い方を知りたい方 aws-nuke運用で削除したくないリソースを除外する設定方法を知りたい方 3. 背景・目的 検証用途で使用しているAWSアカウント(以降、検証環境)があり、検証終了後に不要なコストが発生しないよう、その都度リソースの削除を行っていました。 しかし、当初はリソース削除を手作業で行っていたため、削除作業に時間が掛かる上、削除漏れが頻発するという問題がありました。 そこで、作業効率化を目的にAWSアカウント内のリソースを一括で削除することができるaws-nukeの導入を検討し始めました。 aws-nukeを活用することでAWSアカウント内のリソースを効率的に一括削除でき、削除漏れを防ぎながら作業時間を短縮することができます。 一方で、その際に課題となったのが、検証環境内の「セキュリティ統制やコスト管理のために保持すべきリソース」を削除しないように保護する方法でした。 検証環境では、セキュリティ統制やコスト管理のためにAWS CloudTrailやAWS Budgetsなどのリソースが存在しており、aws-nukeでこれらのリソースが削除されないよう保護することが求められました。 今回のようにAWSアカウント内の一部リソースを保護したい要件があり、aws-nukeの運用に課題を感じている方も多いのではないでしょうか。 そこで本記事では、リソースを保護するために活用できる「5つの実践テクニック」をご紹介したいと思います。 4. aws-nuke とは? aws-nukeは、AWSアカウント内のリソースを一括で削除できるオープンソースツールです。 現在「 rebuy-de/aws-nuke (バージョン2)」は、メンテナンスが停止されており、「rebuy-de」から派生した「 ekristen/aws-nuke (バージョン3)」への切り替えが推奨されています。 aws-nukeを導入する際は、「 ekristen/aws-nuke (バージョン3)」を 使用することをお勧めします。 「 ekristen/aws-nuke (バージョン3)」では、新機能や改善によって運用の柔軟性と効率性が向上しています。 例えば、新しく追加されたGlobalフィルター(すべてのリソースに適用できるフィルター)を活用することで、より簡単にフィルタ条件を設定することができ、運用効率を高めることが可能です。 もし、現在「 rebuy-de/aws-nuke (バージョン2)」をご利用中であれば、一度リリース内容( README.md -> What's New in Version 3 )をご確認いただくことをお勧めします。 4.1 前提条件 aws-nukeを利用するためには、以下の事前準備が必要となります。 AWSアカウントエイリアスが設定されている必要があります。 AWSアカウントエイリアスに「prod」という文字が含まれる場合、aws-nukeを実行時にエラーが発生する為、ご注意ください。 4.2 考慮事項 aws-nukeは、オープンソースツールであり、AWS公式のツールではありません。 本番環境でのご使用はお控えください。 すべてのリソースが削除されても問題のない環境で使用することが前提となります。 細心の注意を払ったとしてもフィルタリング設定の誤りやaws-nukeの不具合などにより、意図せずリソースが削除される可能性があります。 復元できないリソースが含まれる環境では、使用を避けてください。 5. 実行方法 「AWS CloudShell(以降、CloudShell)」でaws-nukeを実行する方法について紹介します。 ① CloudShellを起動します ② aws-nukeをダウンロードします 以下のコマンドは、2025年11月時点の最新バージョン(v3.61.0)をダウンロードする例です。 最新のリリースバージョンは、 公式リポジトリ で確認し、必要に応じてURLを修正してください。 wget -c https://github.com/ekristen/aws-nuke/releases/download/v3.61.0/aws-nuke-v3.61.0-linux-amd64.tar.gz ダウンロードしたファイルを展開します。 tar -zxvf aws-nuke-v3.61.0-linux-amd64.tar.gz ③ aws-nukeの設定ファイルを作成します 詳細は、「6. 設定ファイルの基本構成」を参照ください。 vim nuke-config.yml ④ aws-nukeを実行します aws-nukeは、デフォルトは dry run が実行されます。 dry run で削除対象のリソースを必ず確認してからリソース削除を実行してください。 ./aws-nuke run --config nuke-config.yml 「--no-dry-run」フラグを指定することで、リソース削除を実行することができます。 ./aws-nuke run --config nuke-config.yml --no-dry-run 6. 設定ファイルの基本構成 aws-nukeの設定ファイルについて解説します。 本章では、設定ファイルの基本構成と各セクションの概要について紹介します。 詳細は、公式マニュアルをご参照ください。 各セクションの設定例は、次章で解説します。 公式マニュアル: docs/config.md ◆ 設定ファイル例 blocklist : - 123456789012 blocklist - terms : - " production " regions : - global - ap - northeast -1 accounts : 111122223333: filters : __global__: - property : tag : env value : " prd " IAMUser : - type : contains value : " admin " resource - types : excludes : - IAMRole blocklist:削除対象から除外するAWSアカウントを指定します。 blocklist-terms:削除対象から除外するAWSアカウントのエイリアス名に含まれる文字列を指定します。 regions:対象リージョンを指定します。 accounts:対象アカウントとフィルタリング設定を指定します。 filters:削除対象から除外するリソースの条件を指定します。 resource-types:対象リソースとフィルタリング設定を指定します。 excludes:削除対象から除外するリソースの条件を指定します。 7. リソース保護に役立つ実践テクニック リソースを保護するために活用できる「5つの実践テクニック」を紹介します。 7.1 ブロックリスト ブロックリストを使用してアカウント内のリソースを保護する方法について紹介します。 ◆ 目的 意図しないリソース削除からアカウントを保護する ◆ 設定内容 指定したアカウントID(123456789012)を削除対象から除外する 指定したAWSアカウントエイリアス名(production)を含むAWSアカウントを削除対象から除外する ◆ 設定例 blocklist : - 123456789012 blocklist - terms : - " production " 「blocklist」に アカウントID や 「blocklist-terms」に AWSアカウントエイリアス名を設定することで、対象アカウントのリソースを保護することが可能です。 マルチアカウント環境では、意図しないアカウントでaws-nukeを実行してしまう可能性があるため、設定を強く推奨します。 「blocklist」には、最低でも1つのアカウントIDを設定する必要があるため、保護対象のアカウントが存在しない場合は、ダミーのアカウントIDを設定ください。 ◆ 設定例(保護対象のアカウントが存在しない場合) blocklist : - 000000000000 # ダミーのアカウント ID blocklist - terms : - " production " ◆ 実行環境のAWSアカウントIDの確認方法 CloudShellから以下のコマンドを実行することでAWSアカウントIDを確認することが可能です。 aws-nukeを実行前に実行環境のAWSアカウントIDを確認してください。 aws sts get-caller-identity 7.2 タグ条件によるフィルタリング タグ条件を使用してリソースを保護する方法について紹介します。 ◆ 目的 特定のタグが設定されたリソースを一括で保護する ◆ 設定内容 指定したタグ条件(「env」タグに「prd」を設定)に一致するすべてのリソースを削除対象から除外する ◆ 設定例 accounts : 111122223333: filters : __global__: - property : tag : env value : " prd " フィルタリング条件としてタグを設定することで、特定のタグが設定されたリソースを保護することができます。 タグ条件は、リソースタイプ毎に設定することも可能ですが、Globalフィルターとタグ条件を組み合わせることで、すべてのリソースに適用されるタグ条件を設定することが可能です。 アカウント内で特定のタグが設定されたリソースを一括で保護することが可能なため、運用効率を向上させることができます。 7.3 文字列条件によるフィルタリング 文字列条件を使用してリソースを保護する方法について紹介します。 ◆ 目的 特定のリソース名が設定されたリソースを保護する ◆ 設定内容 指定した文字列条件(リソース名に「admin」が含まれる)に一致するIAMユーザーを削除対象から除外する ◆ 設定例 accounts : 111122223333: filters : IAMUser : - type : contains value : " admin " フィルタリング条件として文字列を設定することで、特定のリソース名が設定されたリソースを保護することができます。 一部のAWSサービスではタグを設定できない場合もあるため、そのような場合に文字列条件を設定することで、特定のリソース名が設定されたリソースを保護することが可能です。 aws-nukeでは、以下の比較条件を設定することができます。 ◆ 比較条件 exact :文字列と 完全一致 することを確認します。 contains :文字列が設定された 文字列を含む ことを確認します。 glob :文字列が設定された Globパターン と一致することを確認します。 regex :文字列が設定された 正規表現 と一致することを確認します。 7.4 時間条件によるフィルタリング 時間条件を使用してリソースを保護する方法について紹介します。 ◆ 目的 最新のリソースを保護し、古いリソースを削除する ◆ 設定内容 指定した時間条件(作成日時が現在日時から過去1時間以内)に一致するS3オブジェクトを削除対象から除外する ◆ 設定例 accounts : 111122223333: filters : S3Object : - type : dateOlderThanNow property : CreationDate # フィルタリング条件として使用するプロパティを設定します。 value : -1h invert : true # フィルタリング判定結果の反転を行います。 フィルタリング条件として時間を設定することで、特定の時間より前や後に作成されたリソースを保護することができます。 「invert」を「true」に設定することで現在日時の過去1時間以内に作成された最新のS3オブジェクトは保護し、それ以外の古いS3オブジェクトを削除することができます。 下図にフィルタリング動作の詳細を示します。 「dateOlderThanNow」を設定することで作成日時が判定日時(現在日時の1時間前)より古い日時のリソースが保護されます。 「invert」を「true」を設定することでフィルタリング判定結果が反転されるため、 判定日時(現在日時の1時間前)より新しい日時のリソースが保護されます。 7.5 プリセット プリセットを使用してフィルタリング条件を管理する方法について紹介します。 ◆ 目的 フィルタリング条件を一括管理することで設定誤りの防止、管理負荷を軽減する ◆ 設定内容 共通フィルタリング条件(「env」タグに「prd」を設定)に一致する複数アカウントのすべてのリソースを削除対象から除外する ◆ 設定例 blocklist : - 123456789012 blocklist - terms : - " production " regions : - global - ap - northeast -1 accounts : 111122223333: presets : - " common " 444455556666: presets : - " common " 777788889999: presets : - " common " presets : common : filters : __global__: - property : tag : env value : " prd " プリセットを設定することで、複数アカウントのフィルタリング条件をまとめて管理することができます。 アカウント毎にフィルタリング条件の管理が不要となるため、フィルタリング設定の誤りや管理負荷を軽減することが可能です。 8. まとめ 本記事では、aws-nukeによるリソース削除を行う際にリソースを保護するために活用できる「5つの実践テクニック」をご紹介しました。 aws-nukeを利用することで、AWSアカウントのクリーンアップ作業を効率化し、運用負荷を軽減することができます。 aws-nukeは、すべてのリソースが削除されても問題のない環境で使用することが前提ですが、その中でも保護したいリソースがある場合は、本記事で紹介した方法を参考にしていただければと思います。 最後になりますが、細心の注意を払ったとしてもフィルタリング設定の誤りやaws-nukeの不具合などにより、意図せずリソースが削除される可能性があります。 aws-nukeを実行時には、dry run で削除対象のリソースを必ず確認してからリソース削除を実行してください。 また、必要に応じてバックアップや復旧手段を確保した上で、すべてのリソースが削除されても問題のない環境で必ず検証を実施してください。 復元できないリソースが含まれる環境では、使用を避けるようお願いします。 本記事が、皆様の業務改善に少しでもお役に立てれば幸いです。 執筆者 桂川 裕幸(NTT西日本 ビジネス営業本部 エンタープライズビジネス営業部所属) AWSを活用したクラウドシステムの設計・構築に携わっています。 サーバーレスアーキテクチャが好きです。 2025 Japan AWS All Certifications Engineers に選出。 参考資料・出典 本記事を執筆するにあたり、以下のサイトを参考にさせていただきました。 github.com github.com github.com 商標  「aws-nuke」は、MITライセンスで公開されているオープンソースツールです。 「AWS」、「AWS CloudShell」、「AWS CloudTrail」、「AWS Budgets」は、Amazon Web Services, Inc.またはその関連会社の商標もしくは登録商標です。 「GitHub」は、GitHub, Inc.またはその関連会社の商標もしくは登録商標です。 免責事項 本記事では、aws-nukeを使用したAWSアカウント内のリソース削除に関する設定例を紹介しました。 本記事は情報提供を目的としたものであり、動作保証を行うものではありません。 本記事の内容を実環境で利用する場合は、自己責任でお願いします。 本記事の内容を実施した結果発生する損失・損害については一切の責任を負いかねます。
はじめに 株式会社ジャパン・インフラ・ウェイマークの川邉です。 当社はNTT西日本の子会社で、ドローン×画像解析AIを活用したインフラ点検を主に行っています。 本記事では2025年3月にRoboflow社が発表したRF-DETRという物体検出モデルの環境構築を行った際に、色々と調べた結果をまとめています。記事執筆時点の最新版である1.3.0について記載していますが、発表から半年ちょっとですでに300コミットを超えているリポジトリですので、すぐに古い情報となってしまうかもしれませんが、新しく始める時のとっかかりくらいのつもりでお読みください。 対象読者 本記事が想定する対象読者は以下の通りです。 新しい物体検出モデルに興味がある人 業務で使いやすい物体検出モデルを探している人 背景 高性能で使いやすい物体検出モデルとしてはUltralytics社の提供するUltralytics YOLO™が有名ですが、ライセンスの関係で商用システムの開発には若干使いづらいものとなっています。 そんな中、2025年3月にRoboflow社がRF-DETRという新しい物体検出モデルを発表しました。公式のページによると、COCO datasetに対してmAPが60を超える高性能となっており、なおかつ、ライセンスはApache 2.0なので非常に使い勝手の良いものとなっています。 ただ、出たばかりで情報も少ないことから、実際に自前で学習/推論環境を構築しながら、できること/できないことを色々と調査してみました。 前提条件 執筆時点で最新版の 1.3.0 を使って調査しました 環境構築はWSL2(Ubuntu-22.04)上で実施しています 仮想環境にはPoetryを利用しましたが、このあたりは何でも良いと思います コード とりあえず結論からということで、最終的に動作確認した学習/開発環境のコードを以下に記載します。 pyproject.toml [tool.poetry] name = "rf_detr_test" version = "0.1.0" description = "" authors = [] [tool.poetry.dependencies] python = ">=3.10,<3.15" torch = {version = "^2.9.0", extras = ["cuda"]} torchvision = "^0.24.0" pillow = "^12.0.0" [tool.poetry.dev-dependencies] pytest = "^5.2" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" train.py import torch import rfdetr import os import argparse # 学習結果格納先のディレクトリのパスを返す def train ( dataset_dir: str , num_classes: int , epochs: int = 1000 , patience: int = 50 , batch_size: int = 4 , lr: float = 1e-4 , pretrained: bool = True , save_dir: str = './result' , size: str = 'medium' , resolution: int = 640 ) -> str : # 結果保存先ディレクトリの作成 result_root = os.path.join(save_dir, 'rf_detr_train' ) save_dir = os.path.join(result_root, 'result' ) if os.path.exists(save_dir): count = 1 while os.path.exists(save_dir): save_dir = os.path.join(result_root, f 'result_{count}' ) count += 1 os.makedirs(save_dir, exist_ok= True ) if size in ( 'nano' , 'small' , 'medium' ): if resolution % 32 != 0 : raise ValueError (f 'モデルサイズが{size}の場合、resolutionは32の倍数を指定してください' ) elif size in ( 'large' ): if resolution % 56 != 0 : raise ValueError (f 'モデルサイズが{size}の場合、resolutionは56の倍数を指定してください' ) # 利用可能なデバイスの確認 device = 'cuda' if torch.cuda.is_available() else 'cpu' # モデル初期化 model_classes = { 'medium' : rfdetr.RFDETRMedium, 'nano' : rfdetr.RFDETRNano, 'large' : rfdetr.RFDETRLarge, 'small' : rfdetr.RFDETRSmall } model = model_classes[size]( num_classes=num_classes, pretrained= True , device=device ) if size in model_classes else None if model is None : raise ValueError (f 'invalid model size: {size}' ) model.train( dataset_dir=dataset_dir, epochs=epochs, batch_size=batch_size, lr=lr, output_dir=save_dir, early_stopping= True , patience=patience, resolution=resolution, checkpoint_interval=epochs # 余分なチェックポイントをなるべく保存させない ) return save_dir if __name__ == '__main__' : parser = argparse.ArgumentParser(description= 'RF-DETRを使って学習を実施するプログラム' ) parser.add_argument( '--dataset' , required= True , help = '学習データセットの格納されているディレクトリ' ) parser.add_argument( '--size' , default= 'medium' , choices=[ 'nano' , 'small' , 'medium' , 'large' ], help = 'モデルサイズ' ) parser.add_argument( '--num_classes' , type = int , required= True , help = 'クラス数' ) parser.add_argument( '--epochs' , type = int , default= 1000 , help = '学習エポック数' ) parser.add_argument( '--patience' , type = int , default= 50 , help = 'lossが改善しなかった場合に学習を完了させるエポック数' ) parser.add_argument( '--batch_size' , type = int , default= 4 , help = 'バッチサイズ' ) parser.add_argument( '--save' , default= 'result' , help = '結果格納先のディレクトリ' ) parser.add_argument( '--resolution' , type = int , default= 640 , help = '学習時の画像サイズ' ) parser.add_argument( '--no-pretrained' , action= 'store_true' , help = '事前学習済みのデータセットを利用しない' ) args = parser.parse_args() train( dataset_dir = args.dataset, num_classes = args.num_classes, epochs = args.epochs, patience = args.patience, batch_size = args.batch_size, lr = 1e-4 , pretrained = not args.no_pretrained, save_dir = args.save, size = args.size, resolution = args.resolution ) predict.py import torch from PIL import Image import rfdetr import argparse import os def predict ( checkpoint_path: str , image_dir: str , num_classes: int , size: str = 'medium' , resolution: int = 640 , threshold: float = 0.5 ): if size in ( 'nano' , 'small' , 'medium' ): if resolution % 32 != 0 : raise ValueError (f 'モデルサイズが{size}の場合、resolutionは32の倍数を指定してください' ) elif size in ( 'large' ): if resolution % 56 != 0 : raise ValueError (f 'モデルサイズが{size}の場合、resolutionは56の倍数を指定してください' ) device = 'cuda' if torch.cuda.is_available() else 'cpu' # 推論時もnum_classesの指定は必要 # https://github.com/roboflow/rf-detr/issues/51 model_classes = { 'medium' : rfdetr.RFDETRMedium, 'nano' : rfdetr.RFDETRNano, 'large' : rfdetr.RFDETRLarge, 'small' : rfdetr.RFDETRSmall } model = model_classes[size]( device=device, num_classes=num_classes, pretrained= False , pretrain_weights=checkpoint_path ) if size in model_classes else None if model is None : raise ValueError (f 'invalid model size: {size}' ) for image_path in [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith(( '.jpg' , '.png' , '.jpeg' ))]: print (f 'load image: {image_path}' ) # 推論対象画像の読み込み image = Image.open(image_path).convert( "RGB" ) # 推論実行 detections = model.predict( image, threshold=threshold, resolution=resolution ) # 推論結果の表示 for xyxy, class_id, score in zip (detections.xyxy, detections.class_id, detections.confidence): print (xyxy, class_id, score) if __name__ == '__main__' : parser = argparse.ArgumentParser(description= 'RF-DETRを学習したモデルを使って推論を実施するプログラム' ) parser.add_argument( '--model' , '-m' , required= True , help = '学習した重みファイルのパス' ) parser.add_argument( '--image' , '-i' , required= True , help = '画像ファイルの格納されているディレクトリ' ) parser.add_argument( '--size' , '-s' , default= 'medium' , choices=[ 'nano' , 'small' , 'medium' , 'large' ], help = 'モデルサイズ' ) parser.add_argument( '--resolution' , '-r' , type = int , default= 640 , help = '推論時の画像サイズ' ) parser.add_argument( '--threshold' , '-t' , type = float , default= 0.5 , help = '推論時の閾値' ) parser.add_argument( '--num_classes' , '-n' , type = int , required= True , help = 'クラス数' ) args = parser.parse_args() predict( checkpoint_path = args.model, image_dir = args.image, size = args.size, num_classes = args.num_classes, resolution = args.resolution, threshold = args.threshold ) 構築方法 Poetryを使いましたので、pyproject.tomlの存在するディレクトリで poetry install を実行すれば、仮想環境上に必要なモジュールなどがインストールされます。 ただし、上のpyproject.tomlだけでは肝心のRF-DETRがインストールされません。私の環境ではrfdetrだけは poetry add でインストールしようとするとエラーが発生したため、 poetry run pip install rfdetr==1.3.0 で別にインストールしています。実行環境によっては、pyproject.toml内で定義してもちゃんとインストールされるのではないかと思います。 学習データのディレクトリ構成 RF-DETRの学習に関するREADMEでは以下のように記載されています。 dataset/ ├── train/ │ ├── _annotations.coco.json │ ├── image1.jpg │ ├── image2.jpg │ └── ... (other image files) ├── valid/ │ ├── _annotations.coco.json │ ├── image1.jpg │ ├── image2.jpg │ └── ... (other image files) └── test/ ├── _annotations.coco.json ├── image1.jpg ├── image2.jpg └── ... (other image files) なお、RF-DETRの内部ではCOCO JSON形式のファイルの処理に pycocotools.COCO.loadRes を利用しています。このモジュールでは 2.0.9 以降はCOCO JSONのトップレベルキーに info がないとエラーが発生するようになっているようです。単純に学習データの定義だけであれば不要な項目かもしれませんが、安全のためにつけておくと良いかと思います。 学習方法 上記コードで学習を実施する場合は、以下のように学習用データセットのパス(--dataset)とクラス数(--num_classes)を必ず付与してください(パスは環境に応じて適宜変更してください)。 poetry run python3 rf_detr_test/train.py --dataset ~/coco_dataset/ --num_classes 2 ちなみに、実際に同じデータセットでYOLOと学習過程を比較すると 学習結果が収束するまでのエポック数はYOLOよりもかなり少ない 学習に必要なGPUメモリ量はYOLOよりも多い という感じでしたので、YOLOで学習した時にGPUメモリがギリギリだった学習データセットについては、バッチ数を下げて実施したほうが良いと思います。 学習完了までのエポック数はYOLOよりもかなり少ないので、バッチ数を下げてもトータルの学習時間は短くなるはずです。 参考までに、同一の学習データセットに対してRF-DETR と YOLOv8で学習した時の学習曲線/メトリクスを記載しておきます(学習環境としてはAWSのg6.xlargeインスタンスを利用しました)。各AIモデルが標準で出力するグラフのため縦軸が一致していませんが、参考までに。 RF-DETRの学習曲線 YOLOv8の学習曲線 推論方法 以下のように推論対象の画像のディレクトリのパス(--image)、読み込むモデルのパス(--model)、クラス数(--num_classes)を指定して実行してください(パスは環境に応じて適宜変更してください)。 poetry run python3 rf_detr_test/predict.py --image ~/test_images --model ./train/checkpoint.pth --num_classes 2 コード解説 全体的な話 モデルには RFDETRNano RFDETRSmall RFDETRMedium RFDETRLarge RFDETRBase の5つのサイズが用意されています(今回のコードでは RFDETRBase は利用していませんが、使い方は基本的に同じはずです) RFDETRBase が他の4つのモデルのどこに位置するのかは良く分かりませんでした(コードには Train an RF-DETR Base model (29M parameters). との注釈がありました) 推論/学習共にモデル作成時に num_classes 引数でクラス名を指定したほうが良いようです( 参照 ) 画像サイズは resolution 引数で指定します。モデルサイズがbase、largeの場合は56の倍数、nano、small、mediumの場合は32の倍数が良いようです( 参考 )。長方形の画像を入力した場合、内部で正方形にリサイズされます(YOLOのように余白を追加しないので、学習/推論時のアスペクト比が実画像と異なる可能性があります)。 PyTorchのバージョンの話 公式リポジトリのpyproject.tomlでは、利用するPyTorchのバージョンが "torch>=1.13.0", "torchvision>=0.14.0" となっていますが、実際にPyTorch 1.13.0で動作させようとすると rfdetr/engine.py の以下の箇所でエラーが発生します def train_one_epoch ( model: torch.nn.Module, criterion: torch.nn.Module, lr_scheduler: torch.optim.lr_scheduler.LRScheduler, # <-- ここ data_loader: Iterable, optimizer: torch.optim.Optimizer, device: torch.device, epoch: int , batch_size: int , max_norm: float = 0 , ema_m: torch.nn.Module = None , schedules: dict = {}, num_training_steps_per_epoch= None , vit_encoder_num_layers= None , args= None , callbacks: DefaultDict[ str , List[Callable]] = None , ): これは、 torch.optim.lr_scheduler.LRScheduler がPyTorchの1.13.0には存在しないためです( 参考 )。初めて登場するのが2.5.0なので( 参考 )、少なくとも本コードについてはPyTorch2.5.0以降でないとエラーになると思います。 学習コードの話 model.trainという関数が用意されているので、この中で学習を実施しています ユーザ自作のコールバック関数の指定はできないようです。内部的にはエポック終了時などのイベントごとに呼ばれているコールバックが存在しているようですので、そのあたりのコードを自分で変更しても良いですし、放っておいてもそのうちに対応してくれるかもしれません。 損失関数 lossの計算は rfdetr/models/lwdetr.py の SetCriterion の中で実施されています。今のところクラスごとに重みを指定する機能はありませんが、Focal Lossを使っているのでクラス不均衡に対する強度はある程度確保されているようです(YOLOの方は分類損失の計算に torch.nn.BCEWithLogitsLoss を使用しているので、学習データのクラス不均衡の影響を受けます)。 また、学習対象の特性に応じて利用する損失関数の調整がある程度可能になっています。指定可能なパラメータは以下です( 参考 ) パラメータ 初期値 内容 aux_loss True デコーダの各層から出力される補助的な予測に対しても損失を計算します。Trueにすると最終層だけではなく中間層の出力も損失に含めるので学習が早く安定します sum_group_losses False 各グループの損失を合計して最終損失に反映します use_varifocal_loss False Focal Lossの代わりにVarifocal Lossを利用する。IoUスコアに応じて重みづけが変わるので、Focal Lossよりも位置情報に敏感になります use_position_supervised_loss False 位置情報に基づいた追加の教師あり損失を使うかどうかを制御します。DETR系では通常、Hungarian Matching後に座標回帰を行うため対象物が小さい場合や、隣接して存在する場合の精度が下がる傾向があると言われています。このオプションは座標に対する補助的な監督を強化することで、DETR系の弱点を補うようになるようです ia_bce_loss True クラス分類損失としてIoU-Aware Binary Cross Entropy Lossを使用するかどうかを指定します。IoU情報を組み込んだBCE損失で、Varifocal Lossの代替として利用可能です なお、処理の優先順位が ia_bce_loss use_position_supervised_loss use_varifocal_loss その他 となっており、なおかつ、 ia_bce_loss の初期値が True となっているので、 ia_bce_loss の値を明示的に False に指定しないと、他の損失関数は利用されません。 作成されるファイル 学習を実施するとsave_dirで指定したディレクトリ配下に以下のような重みファイルが生成されます( 参考 )。 重みファイル名 内容 checkpoint_best_regular.pth EMA(Exponential Moving Average)を使わない純粋なモデルの中で最もスコアが良かったもの。追加学習を実施する時はこちらを利用すると良いようです。 checkpoint_best_ema.pth EMAモデルで全ての過去のモデル重みを指数移動平均で平滑化したもの checkpoint_best_total.pth 最終的に一番良かったもの。基本的に推論時はこちらを利用すると良いようです また、上記のファイルに加えて、以下のタイミングで学習中のチェックポイントが保存されます( rfdetr/main.py の train 関数) if (epoch + 1 ) % args.lr_drop == 0 or (epoch + 1 ) % args.checkpoint_interval == 0 : checkpoint_paths.append(output_dir / f 'checkpoint{epoch:04}.pth' ) lr_drop の初期値は(おそらく)100なので、 checkpoint_interval をどれだけ大きく設定しても100エポックごとにcheckpointXXXX.pthというファイルが保存されます(XXXX部分は4桁の数字)。 クラウドサービスでGPUインスタンスを借りて学習を実施する場合、あまり沢山チェックポイントファイルが作成されるとストレージが圧迫されるので困るのですが、コールバックの指定などができないので今のところはWatchDog的なもので監視しながら適宜消していくしかなさそうです。 推論コードの話 学習時とモデルサイズ、クラス数( num_classes )、画像サイズ( resolution )は合わせる必要があります 検出する閾値は threshold で指定可能です predict の返り値には xyxy (BBOXのleft、top、right、bottomの座標)、 confidence (信頼度スコア)、 class_id (クラスID)が入っています。 xyxy は入力画像のスケールに戻されていますが、float型なので描画時などは注意してください 未解決の問題 コードを読んだり試したりしている時に起きた問題のうち、未解決のものをいくつかまとめました。いずれについても公式リポジトリに修正のPRを投げた or 投げる予定なので、いずれ解決してくれるとは思います。 学習時のエラー NVIDIA L4環境上で学習時に ia_bce_loss を False 、 use_position_supervised_loss を True に設定すると以下のエラーが発生しました。 return loss_map[loss](outputs, targets, indices, num_boxes, **kwargs) File "/home/ubuntu/.cache/pypoetry/virtualenvs/rf-detr-train-vhWH2Pc8-py3.10/lib/python3.10/site-packages/rfdetr/models/lwdetr.py", line 372, in loss_labels cls_iou_func_targets[pos_ind] = pos_ious_func RuntimeError: Index put requires the source and destination dtypes match, got BFloat16 for the destination and Float for the source. BFloat16対応しているGPUだと優先的にBFloat16を利用しようとするのですが、引き続きFloat型を利用している箇所があるため型不整合が起きているものと思います。 一応、 rfdetr/models/lwdetr.py の該当箇所を修正して本エラーを抑制する事は難しくないのですが、これを解消すると別のエラーが発生します。 データ拡張の少なさ 学習時のデータ拡張に関わるコードを見ると内容が以下だけなので、YOLOに比べるとバリエーションが少ない印象です。 return T.Compose([ T.RandomHorizontalFlip(), T.RandomSelect( T.SquareResize(scales), T.Compose([ T.RandomResize([ 400 , 500 , 600 ]), T.RandomSizeCrop( 384 , 600 ), T.SquareResize(scales), ]), ), normalize, ]) まとめ 本項ではRoboflowの最新物体検出モデルであるRF-DETRの学習/推論環境の構築についてまとめました。 できたばかりでまだ粗削りなところはありますが、性能面でもライセンス面でも今後に期待の物体検出モデルですのでもう少し弄ってみて、新しいことが分かったり有効な使い方が見つかった時には、また記事にまとめたいと思います。 執筆者 川邉 隆伸 (ジャパン・インフラ・ウェイマーク開発部所属) 画像認識系AIの開発や、それらを提供するSaaS環境の構築を行っています。 免責事項 本記事に記載された情報は、2025年11月時点での公開情報および筆者の検証・調査結果に基づくものです。 記事に記載されている各プログラムやモジュールの機能、実行条件などは予告なく変更される場合があります 本記事の内容を実践される際は、必ず各プログラムの最新の公式ドキュメントをご確認ください 本記事の情報に基づいて行われた意思決定や実装により生じた損害について、筆者および所属組織は一切の責任を負いかねます 参考資料・出典 本記事の執筆にあたり参考としたページは以下の通りです roboflow 公式ページ: https://blog.roboflow.com/rf-detr/ 公式GitHub: https://github.com/roboflow/rf-detr Deepwiki: https://deepwiki.com/roboflow/rf-detr/4.3-loss-functions-and-matching PyTorch Docs: https://docs.pytorch.org/docs/ 商標 Ultralytics YOLO は、Ultralytics の登録商標または商標です PyTorch は、The Linux Foundation の登録商標または商標です Ubuntu は、Canonical Ltd の登録商標または商標です AWS は、Amazon Web Services, Inc. またはその関連会社の商標もしくは登録商標です
1.はじめに NTT西日本の西川と申します。 デジタル化やクラウド化の進展に伴い、情報資産を安全に守る仕組みの重要性が高まっています。 Microsoft Purviewの情報保護機能である 秘密度ラベル は、主にMicrosoft 365上の情報資産を分類し、保護する機能です。 本記事では、秘密度ラベルによる暗号化の仕組み、設定方法、利用イメージを紹介します。 本記事は2025年11月時点の情報に基づきます。 2.対象読者 本記事が想定する対象読者は以下の通りです。 Microsoft 365を使っている方 Microsoft Purviewで実現できる暗号化について興味がある方 組織の情報資産を安全に管理したい方 3.目的 ファイルの暗号化と言われても、具体的にどのような制御が可能なのか?どのようなメリットがあるのか?というイメージが湧かない方も多いのではないでしょうか。 本記事では、Purviewで秘密度ラベルを実際に設定・動作させる様子をご紹介することで、具体的なイメージを持っていただけることをめざします。 4.Microsoft Purview 秘密度ラベルとは Purview  Information Protectionというサービス群に含まれる「秘密度ラベル」とは、情報を分類し、暗号化により保護することができる機能です。 4.1.機能概要 分類 :ファイルやメールに「社外秘」「機密」といったラベルを付けることで、分類を明示できます。 暗号化による保護 :秘密度ラベルには、暗号化設定をすることも、しないこともできます。暗号化設定を含めると、指定されたユーザー/グループ以外は閲覧できないといったアクセス制御を行うことができます。 適用対象 :SharePoint/OneDrive for Business上のドキュメント、Exchange Onlineのメール、Officeデスクトップアプリ、Teams会議とチャットをはじめとする様々なデータへ適用できます。 4.2.秘密度ラベルによる暗号化の仕組み・特徴 Microsoft Purview Information Protection 秘密度ラベルにおける暗号化の仕組みについてご紹介します。 暗号化に使われるキーは2つあります。 また、復号時には2つのキーのほかに、ユーザーごとのキーも利用されます。 コンテンツキー:ファイルなどのコンテンツを 暗号化 するための鍵。コンテンツごとに一意。AES共通鍵暗号方式。 テナントキー:コンテンツキーを 暗号化 して保護する鍵。RSA 公開鍵暗号方式。 ユーザーごとのキー:特定ユーザーにだけ 復号 できるように保護するための鍵。ユーザーごとに一意。復号時のみ利用。RSA公開鍵暗号方式。 4.2.1.暗号化の流れ 2つのキーを使って、以下の流れで暗号化されます。 ファイルの場合で説明します。 クライアントがコンテンツキーを作成し、ファイルを暗号化する クライアントがテナントキーの公開鍵を使用して、コンテンツキーおよび「誰がどのようなアクセス権限を持っているか?」という情報を暗号化する 暗号化されたコンテンツキーとアクセス権限情報は、ファイルのヘッダー部分に格納 4.2.2.復号の流れ 3つのキーを使って、以下の流れで復号されます。ファイルの場合で説明します。 ユーザー認証・認可 ファイルを開こうとしているユーザーAに対し、 Entra IDの認証・認可 が行われる 認可されると、Azure Rights Management Service(以下、Azure RMSと記載。Purview Information Protectionの暗号化を支える暗号化基盤)へアクセス可能なトークンが発行される Azure RMSによる処理 Azure RMSがテナントキーの秘密鍵を使って、コンテンツキーおよびアクセス権限情報を復号化する ※なおこの時点において、ファイル本体はコンテンツキーで暗号化されたまま ユーザーAが持つアクセス権限(閲覧、編集など)を確認 ユーザーごとのキーで再暗号化 Azure RMSは、復号化したコンテンツキーおよびアクセス権限情報を、ユーザーAの公開鍵で再暗号化する クライアントへの返却 再暗号化された情報をクライアントに渡す クライアント側で復号 クライアントはユーザーAの秘密鍵を使って復号化し、コンテンツキーを取得 コンテンツキーを使ってファイル本体を復号化する ユーザーが操作可能 許可されたアクセス権限に従い、閲覧・編集などが可能になる ※4.2.1.および4.2.2.に記載した流れは、一部を簡略化していますので、詳細は参考文献に掲載している公式ドキュメントの確認をお願いいたします。 4.2.3. 特徴:EntraIDと連携したアクセス制御 このように、Purview 秘密度ラベルによる暗号化・復号化の特徴は、 Microsoft Entra ID (ID とアクセス管理サービス。以下Entra ID と記載) と連携 できる点にあります。暗号化されたコンテンツを復号化するには、 Entra ID での認証・認可 が必要です。 すなわち、暗号化されたファイルへのアクセスは、EntraIDの機能である条件付きアクセスを使った制御が可能です。具体的には、ネットワークの場所、デバイスの準拠、多要素認証の要求などといった条件に応じたアクセス制御ができるということを意味します。 5.秘密度ラベル設定手順 ここからは、実際に秘密度ラベルを設定する手順をご紹介します。 本記事では組織内の全ユーザーは閲覧可、組織外のユーザーは閲覧不可とするようなアクセス制御を行うラベルを作成し、SharePointサイトに配置したExcelファイルの動作を確認します。 管理者側では以下の設定が必要です。 事前設定 秘密度ラベルを作成する ラベルポリシーを作成する 5.1. 事前設定 事前設定として、以下に示す設定が必要です。 なお本記事では、事前設定に関する画面キャプチャを使った紹介は省略し、次章の5.2以降について画面キャプチャを使った手順を紹介させていただきます。 SharePointとOneDriveの秘密度ラベルの有効化 SharePoint および OneDrive でファイルの秘密度ラベルを有効にする | Microsoft Learn 秘密度ラベルの共同編集の設定 暗号化されたドキュメントの共同編集を有効にする | Microsoft Learn 5.2.秘密度ラベルの作成 Microsoft Purviewポータル (https://purview.microsoft.com) へアクセス>ソリューション>Microsoft Information Protection>秘密度ラベル>[+ラベルの作成]より、新規ラベルを作成します。 名前(管理ポータルでの名称)、表示名(各ユーザーに表示される名称)、ユーザー向けの説明などを入力します。 ラベルを適用できる範囲を設定します。 今回は、ファイルについてのラベルの挙動を確認するため、画像のとおり設定します。 ラベルが付与されるとどうなるかを設定します。 アクセスの制御 ・・・暗号化のことです。特定のユーザー(組織内の全ユーザー、もしくは指定したユーザー)以外閲覧できないように制御します。 コンテンツマーキングを適用する ・・・ヘッダー/フッター/透かしとして「社外秘」「confidential」などの指定した文字列を入れることができます。 今回はアクセス制御のみを設定します。 アクセス制御(暗号化)の設定を行います。 組織内のすべてのユーザーとグループを追加する をクリックし、テナント名が追加されたことを確認します。 アクセス許可が「編集者」となっていることを確認し、保存します。 ※今回はOfficeファイルに関するラベル付けに使用するため、 なおこの例では、以下のような設定となります。 組織内の全ユーザーに対しては、編集者というアクセス許可レベルを付与 組織外のユーザーではアクセス不可となる 自動ラベル付け設定を行う場合にルールを設定できます。今回はオフのまま次へ進みます。 Teams、Microsoft 365 グループ、SharePoint サイト、Loop ワークスペースを含むコンテナーにラベルを適用する場合に設定します。今回は設定せず次へ進みます。 設定内容を確認し、[ラベルの作成]をクリックします。 以上で秘密度ラベルの作成は終了です。 5.3.ラベルポリシーの作成 秘密度ラベルをユーザーに発行します。これにより、ユーザーはコンテンツにラベルを付与できるようになります。 ラベル発行ポリシー>ラベルの発行 を選択します。 先ほど作成した秘密度ラベルを選択します。 今回は設定を変更せず次へを選択します。 ラベルを発行するユーザー、すなわちラベルを利用可能にしたいユーザーを選択します。 今回はすべてのユーザーとグループの設定のまま、次に進みます。 ラベル付けに関するポリシーの設定となります。 要件に応じてチェックを入れます。 ドキュメント作成時の既定のラベルを設定できます。暗号化のあるラベルを既定にしておくと、ファイル作成時に既定でそのラベルが適用されるようになるため、情報流出リスクの低減が期待できます。 今回は作成したラベルを選択し、次へ進みます。 メールにおいても、作成時の既定のラベルを設定できます。 今回は「なし」と設定し、次へ進みます。 以降、Teams/Outlookの会議や予定表、FabricおよびPowerBIにおける既定のラベルの設定画面となりますが、今回は利用しないため「なし」と設定し次に進んでください。 次の画面が表示されたら、ラベルポリシーに名前を設定します。 内容を確認し[送信]をクリックします。 以上で管理者側からの設定は完了となります。 画面に表示されているとおり、ユーザー側で利用できるようになるまで最大24時間かかる場合があります。 6.秘密度ラベル 利用イメージ 作成したラベルを、Sharepointサイト上のファイルへ付与した時の動作を確認していきます。ここからは、管理者の設定ではなく、一般ユーザーでの利用イメージです。 今回の設定では、以下のような動作となる想定です。 組織内のユーザーは閲覧・編集可能 組織外のユーザーは閲覧不可 6.1.既定のラベルが付与されていることを確認する 組織内のユーザーがドキュメントを作成した時に、既定のラベルが付与されるか確認します。 SharePoint上でExcelファイルを作成します。 既定の秘密度ラベルとして設定した「社外秘」が付与されていることが確認できます。 ラベル作成時に指定した、説明の内容が表示されます。 6.2. 組織内の別ユーザーが、ファイルを閲覧できることを確認する 組織内の別ユーザーアカウントでサインイン、Sharepointサイトにアクセスし、同じExcelファイルを開いてみます。 ファイルを開き、問題なく編集できることが確認できます。 6.3. 組織外のユーザーが、ファイルを閲覧できないことを確認する 6.3.1. 組織内ユーザーがファイルをダウンロード、外部に持ち出した場合 組織内ユーザーがPCへファイルダウンロード、およびメールなどで外部に送信し、外部ユーザーが受信した場合を想定してみましょう。 SharePointからWindows端末へ、該当のExcelファイルをダウンロードします。 該当のExcelファイルを外部にメールで送信し、組織外ユーザーを想定したメールボックスで受信したとします。 ファイルを開くと、アカウントのサインインが求められる表示となります。 ここで、組織外ユーザーのMicrosoftアカウントなどでサインインをしても、ファイルを開くことができません。 ※なお、組織内のユーザーでサインインができる場合については、ファイルへアクセスできます。サインインできる条件については、冒頭で記した通り、Entra IDの機能である 条件付きアクセスを利用し、制御を行う必要があります。 6.3.2. SharePoint共有リンクを生成し、外部に送信した場合 SharePointの共有リンクを生成し、閲覧権限を[すべてのユーザー]として、外部に送信した場合を想定してみましょう。 組織外のユーザーを想定したアカウントで、リンクをクリックしても、以下のような表示となり、アクセスできません。 このように秘密度ラベルによるファイル単位での暗号化を行うことで、誤送信や外部へのファイル持ち出しが発生した場合でも、組織外のユーザーからのアクセスは制御されるイメージとなります。 7.まとめ 本記事では Microsoft Purview Information Protection における 秘密度ラベルによる暗号化の仕組み、設定方法、動作イメージ について解説しました。 秘密度ラベルは、組織の情報資産における情報流出リスクの軽減が期待できる機能です。今回は SharePointに配置したファイルおよびその外部持ち出しを対象に紹介しましたが、秘密度ラベルはこれに限らず、以下のような幅広いコンテンツに適用できます。 Teams SharePoint/OneDrive for business Exchange Online Teams、Microsoft 365 グループ、SharePoint サイト、Loop ワークスペースを含むコンテナー Power BIなど そして、今回はご紹介しておりませんが、Purviewにおける自動ラベル付けポリシー、DLPポリシーなどを活用することで、組織の情報保護レベルを大きく向上させることが可能です。 今後も本ブログでは、Microsoft 365における様々な情報保護の方法やセキュリティサービスについて紹介できればと思います。 執筆者 西川 実優(NTT西日本 ビジネス営業本部 エンタープライズビジネス営業部所属) Microsoft365をはじめとしたクラウドの設計~構築に携わっています。 ミニチュアダックスフンドとお好み焼きが好きです。 Microsoft 365 Certified: Administrator Expert 参考文献 本記事を執筆するにあたり、以下のサイトを参考にしました。 秘密度ラベルの詳細 | Microsoft Learn Azure Rights Management 暗号化サービスについて説明します | Microsoft Learn Azure Rights Management サービスのしくみ: 技術的な詳細 | Microsoft Learn 秘密度ラベルとそのポリシーを作成して構成する | Microsoft Learn Azure Rights Management サービスの使用権限を構成する | Microsoft Learn 商標 「Microsoft」「Microsoft 365」「Microsoft Purview」「Azure Rights Management Service」「Microsoft Entra ID」「SharePoint」「OneDrive」「Teams」「Exchange Online」「Power BI」は、米国 Microsoft Corporation の米国およびその他の国における登録商標または商標です。 免責事項 本記事の内容は執筆時点(2025年11月)の情報に基づいており、サービスの仕様変更などにより内容が変更される可能性があります。 本記事で紹介する設定手順や使用方法は、特定の環境での動作確認に基づくものであり、すべての環境での動作を保証するものではありません。 本記事では生成AIを活用した内容が含まれており、AIによる情報の生成過程でハルシネーション(事実に基づかない情報の生成)が発生する可能性があります。記載内容の正確性については、必ず公式ドキュメントや信頼できる情報源で検証してください。
はじめに NTT西日本の青木と申します。 生成AIに関する案件を推進しております。 案件推進の中でRAGの精度検証をしましたので知見を共有できればと思いこちらの記事を執筆いたしました。 本記事では RAGの精度を向上させる7つの方法 についてご紹介いたします。 本記事は 2025年11月の情報 に基づきます。 本記事についての注意事項 本記事はRAG(Retrieval Augmented Generation)に関する 一般的な情報 をまとめたものであり、 特定のサービスや製品を対象としたものではありません。 ご利用の生成AIチャットサービスによっては、 本記事で紹介する方法をそのまま試せない場合があります。 記事内の表や比較内容は、筆者が実施した検証結果および主観に基づいており、 すべての環境・ユースケースで同様の結果を保証するものではありません。 対象読者 生成AIを用いたRAGの検証を実施している方 業務でRAGを使用しているが精度が上がらず困っている方 社内で生成AI推進を実施している方 背景・目的 2022年にGPT-3.5が搭載された対話型生成AIサービスChatGPTが登場し、2025年に至りビジネスにおいてもさまざまな用途で生成AIが使われております。 その中でも、 RAG は多く使用されている用途の一つです。 RAGとは Retrieval Augmented Generation を指し、日本語では 「検索拡張生成」 と訳されます。 生成AIに外部情報の検索を組み合わせることで、モデルが持つ情報の限界を超えた専門知識や最新情報に基づく回答を生成することが可能になります。 RAG機能を具備している生成AIチャットサービスも一般的になってきました。 RAGの利用用途 具体的なRAGの用途としては下記のようなものが挙げられます。 社内ルールとヘルプデスク 社内規定や業務マニュアルを素早く検索・参照し、問い合わせ対応を自動化することで業務を効率化 カスタマーサポート 製品情報や過去のトラブルシューティング事例をもとに、顧客の質問に即座に対応し、カスタマーサービスの質の向上 専門業務の支援 市場動向や消費者データを分析し、事業戦略に活用 生成AIの活用が進むと同時に、RAGの活用を進めているもののなかなか精度が上がらず業務に活用できない方もいるのではないかと推測します。 本記事では RAGの精度を向上させる7つの方法 についてご紹介します。 手動で試すのも、プログラムで試すのも良いかと思いますのでご検討ください。 RAGの仕組み まずは RAGの仕組み について確認していきましょう。 前述の通り「検索拡張生成」と訳されるこの技術は生成AIに外部情報の検索を組み合わせることで、モデルが持つ情報の限界を超えた専門知識や最新情報に基づく回答を生成することができる技術です。 これがどのように実現されているのかを確認しましょう。 RAGを使用した場合、しない場合の比較 上記の画像では 交通費精算方法 について生成AIに質問しています。 なお、RAGを使用した場合は、社内の交通費申請方法のマニュアルを参照できるようにしています。 RAGを使用しない場合は一般的な交通費精算について回答していますが、RAGを使用した場合は社内の交通費精算について回答することができます。 RAGを使用した場合に、行われている処理を詳しく確認していきましょう。 RAGの処理 RAGの回答生成の流れ RAGで行われている処理は上記の画像の通りになります。 ユーザーが質問をする ユーザーの質問をデータベースで検索 ユーザーの質問とデータベースの検索内容を生成AIに入力 ユーザーの質問とデータベースの検索内容をもとに生成AIが推論し、出力を生成 生成AIが回答をする 生成AIはユーザーの質問に関する情報をデータベースから取得し、それを基に回答を生成します。 次にデータベースには何が入るのかを確認しましょう。 データベースに登録される情報 データベースには社内の交通費精算に関するファイルが登録されています。 このファイルがどのようにデータベースの中に登録されているかを確認してみましょう。 データベースへの登録方法 交通費申請方法.pdfのテキストを チャンク単位 で分割 分割したチャンクを ベクトルデータ に変換 データベースに分割したチャンクと変換したベクトルデータの両方を登録 チャンク とは、大量のテキストを効率的に扱うため、適切なサイズに分割された単位です。 例として10,000文字程度の文章があったとします。 これを1,000文字ずつのチャンクに分けると合計10チャンクになるといったイメージです。 ファイルをチャンク単位で分割し、ベクトルデータに変換して、チャンクとベクトルデータの両方をデータベースに登録しています。 ベクトルデータとは何かをここでは簡単に紹介します。 ベクトルデータとは RAGにおける ベクトルデータ とはテキストや画像、音声などの非構造のデータを数値の配列(ベクトル)に変換したものです。 下記のようなイメージですね。 例:ある文書をベクトルデータへ変換した結果 -0.12, 0.83, 0.01, ..., 0.45 数値化して何が嬉しいかというと、 ベクトル空間上で近いテキスト同士は意味的にも近い ことです。 このベクトルデータは検索に使用します。 従来の検索は「キーワード検索」が主でした。 「キーワード検索」は検索ワードが検索対象に含まれている場合は有効に働きますが、意味が近いが単語としては異なるワードの検索を行うことができませんでした。 例:検索キーワード「請求日 締め日 変更」 この3単語を含むFAQやマニュアルは検索可能だが、「請求サイクル」や「支払条件の更新」など表現が違う記事はヒットしづらい ベクトルデータの検索 「ベクトル検索」 を行うことで意味が近いテキストを検索することができるようになりました。 RAGの精度とは ここまででRAGの仕組みについて説明いたしました。 一度、当初の目的の「RAGの精度を上げる」に立ち戻って考えてみましょう。 「RAGの精度を上げる」とは具体的には何を指すのでしょうか? RAGには大きく2つの精度があります。 検索精度 :ユーザーの入力に対して、正しくチャンクを取得できているかの精度 回答精度 :生成AIがチャンクを正しく理解し、ユーザーの質問に対する回答を生成できているかの精度 この2つを正しく理解せずに闇雲に精度向上のアプローチを行っても、RAGの精度を改善することはできません。 ここまでのRAGの仕組みをもとにRAGの精度を上げる7つの方法について確認していきましょう。 RAGの精度を上げる7つの方法 ここからは具体的にRAGの精度を上げるための7つの方法について確認していきましょう。 大きく7つあります。(細かく書くとキリがありませんので、大分類として記載しています。) それぞれの方法について詳しく確認していきましょう。 アプローチ 概要 稼動コスト 有効度 検索精度に寄与 生成精度に寄与 ①. 生成AIモデルの変更 より高性能なモデルへの切り替え 高 高 - ◎ ②. RAGとして登録するファイルの加工 ドキュメントの構造化、不要情報の削除 高 高 ◎ ◎ ③. チャンク分割方法の変更 セマンティック分割、固定長、文書構造ベースなど最適化 中 中〜高 ◎ △ ④. 検索方法の変更 キーワード検索、ベクトル検索、ハイブリッド検索 低 高 ◎ - ⑤. システムプロンプトの変更 指示の明確化、出力形式の指定、役割の定義 低 高 - ◎ ⑥. 生成AIパラメータの変更 Temperatureの調整 低 中 - ◎ ⑦. 質問内容の変更 質問内容を具体化 低 中 ○ ○ 稼動コスト :低<中<高(実装・運用のコストや使用料) 有効度 :低<中<高(改善効果の大きさ) 寄与度 :-(影響なし)<△<○<◎ ①. 生成AIモデルの変更 使用する生成AIモデルの変更は生成AIの回答精度に寄与します。 正しくチャンクを検索できているのに回答を正しく生成できない場合は生成AIのモデルを変更してみましょう。 特に、チャンクの内容が複雑な場合はモデルを変更することで回答できる可能性が高まります。 使用できる最新のモデルに変更してみてください。 ②. RAGとして登録するファイルの加工 RAGとして登録するファイルの加工はチャンクの検索精度、生成AIの回答精度の両方に寄与します。 この方法は 表や図を含む場合に非常に有効 です。 ファイルに表や図が含まれる場合、生成AIが正しく意味合いを理解できないことが多いです。 その場合には表や図をテキスト化し、チャンク化しましょう。 生成AIが理解しやすい形式にマークダウン形式があります。 マークダウン形式についてはこの記事では紹介しませんが、検索していただくと記載方法が多く出てきますので参考にしてみてください。 (ただし、この方法は非常に手間がかかる方法です。重要なファイルを登録したい場合に試すことを推奨します。) ③. チャンク分割方法の変更 チャンク分割方法の変更はチャンクの検索精度、生成AIの回答精度の両方に寄与します。 生成AIのアプリ上で変更できないこともありますので、ご自身で使用されているアプリをお確かめください。 チャンク分割にはさまざまな分割方法があります。 文字数分割 決められた文字数で分割する方法(例:10,000文字のテキストの場合1,000文字単位で10チャンクとなる) ページ数分割 ファイルを分割する際に1ページ毎にチャンク化し、データベースに登録 セマンティック分割 ファイル内のテキストを意味や関連性毎に分けていく手法 理想のチャンク分割方法は セマンティック分割 です。 ファイルの中で関連性がある部分毎にチャンク分割することで、検索精度、回答精度が向上します。 文字数分割やページ数分割の場合、文章が途中で区切られてしまい、生成AIに正しく情報を入力できない場合があります。 データベースに登録したチャンクの情報を確認できる場合は、関連する情報が一つのチャンクとして含まれているかを確認してください。 ④. 検索方法の変更 検索方法の変更はチャンクの検索精度に寄与します。 こちらも生成AIのアプリ上で変更できないこともありますので、ご自身で使用されているアプリをお確かめください。 検索方法としては「キーワード検索」、「ベクトル検索」が一般的です。 キーワード検索 :文字列の一致度や頻度から関連度スコアを検索して順位付けする手法 ベクトル検索 :テキストや画像などのデータを数値化し、意味がどの程度近いかを測って関連度を出し順位付けする手法 一部のクラウドサービスを使われている場合は、「キーワード検索」と「ベクトル検索」を組み合わせた「ハイブリッド検索」も使用できる場合があります。 ハイブリッド検索が最も精度が高いとされていますが、用途によっては変更することで精度が高まる場合もあります。 例えば、入力ファイルの中身を知っている場合です。 ファイルの中に何が記載されていたか、キーワードレベルで認識している場合はキーワード検索のほうが良いですね。 記載されているキーワードを含めた生成AIに入力することで、求めている情報が書かれているチャンクを取得できる可能性が高いでしょう。 逆にファイルの中身をよく知らない場合は、ベクトル検索を含めたほうが良いですね。 ⑤. システムプロンプトの変更 システムプロンプトの変更は生成AIの回答精度に寄与します。 システムプロンプトとは 生成AIへの指示文章 を指します。 生成AIに行わせるタスクの詳細を記載しましょう。 記載方法は下記のように 「役割」 、 「指示」 、 「出力形式」 を記載するようにしてください。 # 役割 あなたは経理部に所属するエキスパート社員です。 # 指示 ユーザーからの質問に対して明瞭にわかりやすく返信を行ってください。 # 出力形式 - 注釈や一般的な情報は不要です。 - まずは結論、その後に補足を行ってください。 - 回答が不明な場合はその旨を必ず出力するようにしてください。 ⑥. 生成AIパラメータの変更 生成AIに入力する際に幾つかのパラメータがあります。 生成AIのモデルによってパラメータは異なりますが、下記のようなパラメータが存在します。 パラメータ 概要 temperature 出力のランダム性を制御。0に近いほど決定的で一貫性が高く、高い値(1.0以上)ほど創造的で多様な出力になる。一般的な範囲は0.0〜2.0。 top_p nucleus samplingとも呼ばれる。累積確率がこの値に達するまでのトークンから選択。0.9なら上位90%の確率質量を持つトークンのみ考慮。範囲は0.0〜1.0。 top_k 各ステップで考慮するトークンの数を制限。上位k個の最も可能性の高いトークンのみから選択。例:top_k=50なら上位50個のトークンのみ。 max_tokens 生成する最大トークン数。出力の長さを制限し、コストやレスポンス時間を管理。 frequency_penalty 既に出現したトークンの再出力を抑制。正の値で繰り返しを減らし、負の値で繰り返しを促進。一般的な範囲は-2.0〜2.0。 presence_penalty トークンが既に出力に現れたかどうかに基づいてペナルティを適用。新しいトピックへの移行を促進。一般的な範囲は-2.0〜2.0。 stop_sequences 生成を停止させる文字列やトークンのリスト。指定した文字列が生成されると、そこで出力を終了。 seed 再現性のための乱数シード。同じseedと設定で同じ入力を与えると、類似した出力が得られる(完全な決定性は保証されない場合あり) これらのパラメータは、生成AIの出力品質や特性を細かく調整するために使用されます。用途に応じて適切に組み合わせることが重要です。 いくつかパラメータがあるのですが、ここで着目すべきは temperature です。 temperature とは生成AIの出力にどの程度の「ランダム性」を加えるかを決めるパラメータです。 temperature が低いほどランダム性が低くなり、高いほどランダム性が高くなります。 社内規定などをRAGとして登録し、使用する場合はランダム性が低い方が良いので、 temperature も低くしましょう。 生成AIのアプリの初期値は真ん中であることが多いので、意外と見逃されがちな設定になります。 ⑦. 質問内容の変更 ユーザーの質問内容を変更することでチャンクの検索精度、生成AIの回答精度の両方に寄与します。 質問内容はより具体的に記載するようにしましょう。 下記の2つを見比べてみましょう。 簡易的な質問内容 交通費の精算方法について教えて。 具体化した質問内容 交通費の精算方法について教えて。 出張で東京まで新幹線で行った。 領収書の格納先について教えて。 質問内容の変更については、人に聞くときと同様の考え方です。 ざっくりと質問するよりもより具体的に質問をしたほうが求めている回答は返ってきやすいです。 7つの方法の中で最もこの方法が試しやすい のでぜひ試してください。 まとめ 今回の記事ではRAGの仕組みとRAGの精度を上げる7つの方法について紹介しました。 RAGの精度には下記の2つがあります。 検索精度 :ユーザーの入力に対して、正しくチャンクを取得できているかの精度 回答精度 :生成AIがチャンクを正しく理解し、ユーザーの質問に対する回答を生成できているかの精度 この2つの精度を上げるためには下記の7つの方法があります。 アプローチ 概要 稼動コスト 有効度 検索精度に寄与 生成精度に寄与 ①. 生成AIモデルの変更 より高性能なモデルへの切り替え 高 高 - ◎ ②. RAGとして登録するファイルの加工 ドキュメントの構造化、不要情報の削除 高 高 ◎ ◎ ③. チャンク分割方法の変更 セマンティック分割、固定長、文書構造ベースなど最適化 中 中〜高 ◎ △ ④. 検索方法の変更 キーワード検索、ベクトル検索、ハイブリッド検索 低 高 ◎ - ⑤. システムプロンプトの変更 指示の明確化、出力形式の指定、役割の定義 低 高 - ◎ ⑥. 生成AIパラメータの変更 Temperatureの調整 低 中 - ◎ ⑦. 質問内容の変更 質問内容を具体化 低 中 ○ ○ RAGの精度が低い場合、原因は正しくチャンクを取得できているかの検索精度と生成AIがユーザーの質問に対して回答を正しくできているかの回答精度のどちらかです。 どちらに問題があるのかを見極めながら、精度向上に努めていきましょう。 まず最初に試すのは「質問内容の変更」です。 具体的な質問を行って回答が正しく返ってくるのかを確かめてみましょう。 最後まで読んでいただいて、ありがとうございました。 執筆者 青木 聡志 (NTT西日本 ビジネス営業本部エンタープライズビジネス営業部所属) AIエンジニアとして生成AI案件の推進を行っております。 商標 「ChatGPT」「GPT-3.5」は、OpenAI社の商標または登録商標です。
はじめに こんにちは。NTT西日本ルセントの福井です。 みなさんは、筋トレのときに「動画見ながらやりたいけど数をよく忘れる」「派手なエフェクトがないとモチベーションが維持できない」といったお悩みはないですか? ないですか……。 であれば、何かのタスク専用のハードウェア/ソフトウェアに興味はありませんか? ありますよね。 今回はそういった悩みや興味へのアプローチとして、 筋トレ実行回数カウンター「マッスルカウンター Mk.1」 を作ってみたのでご紹介します。 ※本記事に記載されている情報は、あくまでも筆者個人の調査・経験に基づくものであり、所属組織を代表するものではありません。 また、本記事の内容は所属組織の業務に関係のない趣味的な内容で、技術情報の共有を目的としています。 本サイトは、LEGO®/レゴ®の商標所有者であるレゴグループの承認・許可・スポンサー契約を得て運営しているものではありません 「マッスルカウンター Mk.1」 はじめに 対象読者 この記事で説明しないこと 目的 要件と仕様 技術構成 ハードウェア ハードウェア側言語 Webアプリ 実装手順 1. スイッチのはんだ付け 2. コードの書き込みとテスト 3. Webアプリの実装 4. ケースのビルド まとめ 実際に触れる・使えるモノができる達成感はひとしお 執筆者 商標 対象読者 ものづくりに興味がある方 専用のハードウェアにロマンを感じる方 Raspberry Pi Picoシリーズなどの汎用マイコンを買ったものの、放置している方 この記事で説明しないこと 開発環境の準備方法 ハードウェア部品の調達先 筋トレのメニュー 目的 私は最近筋トレを始めたのですが、1セット内で何回やったかよく忘れます。 それだけならいいのですが、ちょっとしたフラストレーションで 習慣化に失敗することも非常に得意 です。 そのような場合の個人的な解決策として「専用のハードウェアやソフトウェアを作ってしまう」というものがあります。 これはある種の特効薬で、下記の効果が見込めます。 単純に便利だから習慣化しやすい せっかく作ったものなので、使いたくなる 飽きたとしてもこうして記事を作ってしまえば誰かの役に立つ可能性がある これは、やるしかないですね。 要件と仕様 上記の目的から要件を下記のように固めました。 出来るだけコンパクトに設計する ハード側は汎用のマイコンを使い、コードはArduino言語 *1 で書く 表示器はスマートフォンとし、Webアプリ1ページ構成とする ソフトとハードを修正・分解・再利用がしやすいようにする ハード側ケースにはLEGO®ブロックとキーボード用スイッチを利用する ここから仕様は下記とします。 マイコンとスマートフォンをBluetooth又は有線で接続。マイコンをキーボードとして認識させる。 マイコンのスイッチが押されたとき、キー入力をスマートフォンに送信。 スマートフォンのブラウザで開かれたWebアプリでキー入力を解釈し、値を増やす。 技術構成 ハードウェア 汎用マイコン (この記事ではRaspberry Pi Pico W) 900~1200円前後 キースイッチ 1コ 100円以下 電池ボックス 1コ (Bluetooth利用時のみ使用) 100円以下 ケース用LEGO®ブロック (今回は手持ちのものを使用) ハードウェア側言語 Arduino言語 Webアプリ Next.js 16 React 19 Tailwind CSS 実装手順 1. スイッチのはんだ付け まず、スイッチをPico Wにはんだ付けします。 今回は位置的に接続しやすいので、GP16のピンとその近くのGNDを使います。 Pico Wのピンアサイン( 公式のドキュメント から引用) 通常、間に抵抗を挟まないと電圧が不定となってしまい、ノイズの影響を受けてしまいますが、 実はRaspberry Pi Picoシリーズを含め、最近の汎用マイコンはプルアップ抵抗を内蔵しています。 しかもコード側から指定できるので、抵抗は不要です。 スイッチはなんでもいいのですが、せっかくなのでメカニカルキーボード用のキースイッチを使います。 押下時のカチャッとした音が大きいキースイッチのほうが押したい動機づけになるのでおすすめです。 メカニカルキーボード用のキースイッチ 2. コードの書き込みとテスト 下記コードをArduino IDEからPico Wに書き込みます。 (クリックで展開) #include <KeyboardBT.h> // Bluetoothキーボードライブラリ // --- ユーザー設定 --- const int switchPin = 16 ; // スイッチを接続するピン番号 const long longPressTime = 500 ; // 500ミリ秒以上押したら「長押し」と判定 const long debounceDelay = 50 ; // 50ミリ秒のチャタリング防止時間 // --- 内部状態を管理する変数 --- int switchState; // 現在のスイッチの状態 (HIGH or LOW) int lastswitchState = HIGH; // 前回のスイッチの状態 unsigned long lastDebounceTime = 0 ; // 最後にチャタリングが検出された時刻 unsigned long switchPressTime = 0 ; // スイッチが押された時刻 boolean longPressExecuted = false ; // 長押しが既に送信されたかどうかのフラグ void setup () { Serial. begin ( 115200 ); pinMode (switchPin, INPUT_PULLUP); // Bluetoothキーボードを開始 KeyboardBT. begin ( "BT Muscle Counter" ); Serial. println ( "Bluetoothキーボードを開始しました。接続待機中..." ); delay ( 2000 ); switchState = digitalRead (switchPin); lastswitchState = switchState; } void loop () { int reading = digitalRead (switchPin); // 最後に読み取った物理状態と異なる場合チャタリングの可能性 if (reading != lastswitchState) { // デバウンスタイマーをリセット lastDebounceTime = millis (); } if (( millis () - lastDebounceTime) > debounceDelay) { // 物理状態変化検知 if (reading != switchState) { switchState = reading; // スイッチが押された瞬間 if (switchState == LOW) { switchPressTime = millis (); longPressExecuted = false ; Serial. println ( "スイッチが押されました" ); } // スイッチが離された瞬間 else { Serial. println ( "スイッチが離されました" ); // 長押し処理がされていない場合カウンタ追加キーを送信 if (longPressExecuted == false ) { Serial. println ( "短押し" ); KeyboardBT. write ('KEY_F13'); } } } } // スイッチが押され続けている場合 if (switchState == LOW) { if (( millis () - switchPressTime > longPressTime) && (longPressExecuted == false )) { Serial. println ( "長押し" ); KeyboardBT. write ('KEY_F14'); longPressExecuted = true ; // 長押し処理を実行済みにする } } // 最後に読み取った物理状態を保存 lastswitchState = reading; } このコードは以下の仕様で作りました。 スイッチを短く押した場合と長押しした場合でキー入力を出しわける 後述のWebアプリでは短押しをカウントアップ、長押しをカウントダウンとして実装します。 KeyboardBTライブラリを使う よくESP32で使われるBleKeyboardライブラリは、Pico Wではペアリングがほとんどできなかったです。 チャタリング防止のため、デバウンスタイムを入れる スイッチの押下時には、微細な振動によって接点が複数回触れてしまうことがあります(チャタリング)。 人間は運動しているとものすごく振動するので、チャタリングを防ぐためにスイッチ押下時に反応しない期間を作ります(デバウンスタイム)。 また、 KeyboardBT.h を Keyboard.h に置き換えれば有線でも動くと思います。 コード自体は標準的なものだと思いますが、重要な点が一つあります。 KeyboardBT. write ('KEY_F13'); ここです。 PCでご覧になっている方はキーボードで F13キー を探してみてください。 F13キー がなかった方 今確認していただいた通り、 F13キー とそれに続く F24キー までのキーは一般的なキーボードにはほとんどありません。 しかし、プログラム的な扱いとしては存在していて、まれに Shift+F(1~12) を押すことでそれに対応するキーを呼び出せるキーボードが存在します。 これを利用して「キー入力として認識はするが、実際に文字が入力されるわけではない」状態を作ります。 補足として、 - 後述のWebアプリができるまでのデバッグ時には通常のキーを割り当てておくと便利です。 - F1~F12キー にしない理由としては、それらのキーは何らかのショートカットに割り当てられていることが多いので、誤ってショートカットが入力されてしまう可能性を下げるためです。 - 13 の理由として、 F17キー 以降のキーは環境によっては入力できないこと、MuscleのMが13番目のアルファベットであることがあります。 F13キー があった方 ご存知の通り、 F13キー とそれに続く F24キー までのキーは一般的なキーボードにはほとんどありません。 どのようなキーボードをお使いか非常に興味があるので、もし良ければ記事末尾のシェアボタンからキーボードの情報を教えてください! 3. Webアプリの実装 WebアプリはNext.js、Tailwind CSSを使って実装しました。 メインページのコードを参考までに載せておきます。 page.tsx 'use client' ; import { useState, useEffect } from 'react' ; import Confetti from 'react-confetti' ; import { useWindowSize } from '@react-hook/window-size' ; export default function Home () { const [ count , setCount ] = useState( 0 ); const [ keyMode , setKeyMode ] = useState< 'MN' | 'F13F14' >( 'MN' ); const [ showConfetti , setShowConfetti ] = useState( false ); const [ confettiOrigin , setConfettiOrigin ] = useState( { x : 0.5 , y : 0.5 } ); const [ width , height ] = useWindowSize(); useEffect(() => { const handleKeyDown = ( event : KeyboardEvent ) => { if (keyMode === 'MN' ) { if (event. key === 'M' || event. key === 'm' ) { setCount( prev => prev + 1 ); triggerConfetti(); } else if (event. key === 'N' || event. key === 'n' ) { setCount( prev => prev - 1 ); } } else if (keyMode === 'F13F14' ) { if (event. key === 'F13' || event.code === 'F13' ) { setCount( prev => prev + 1 ); triggerConfetti(); } else if (event. key === 'F14' || event.code === 'F14' ) { setCount( prev => prev - 1 ); } } } ; window . addEventListener ( 'keydown' , handleKeyDown); return () => window . removeEventListener ( 'keydown' , handleKeyDown); } , [ keyMode ] ); const triggerConfetti = () => { const counterElement = document . getElementById ( 'counter-display' ); if (counterElement) { const rect = counterElement.getBoundingClientRect(); const x = (rect.left + rect.width / 2 ) / window . innerWidth ; const y = (rect. top + rect.height / 2 ) / window . innerHeight ; setConfettiOrigin( { x , y } ); } setShowConfetti( true ); setTimeout (() => setShowConfetti( false ), 500 ); } ; const increment = () => { setCount( prev => prev + 1 ); triggerConfetti(); } ; const decrement = () => setCount( prev => prev - 1 ); const reset = () => setCount( 0 ); const toggleKeyMode = () => setKeyMode( prev => prev === 'MN' ? 'F13F14' : 'MN' ); return ( < div className = "flex min-h-screen items-center justify-center bg-amber-100 p-4" > { showConfetti && ( < Confetti width ={ width } height= { height } colors= {[ '#FF8C00' , '#FFA500' , '#FFB84D' , '#FF7F00' , '#FFD700' , '#FFA000' ]} numberOfPieces= { 150 } gravity= { 0 . 6 } confettiSource= { { x : confettiOrigin.x * width, y : confettiOrigin.y * height, w : 10 , h : 10 , } } initialVelocityX= { 25 } initialVelocityY= { 25 } recycle= { false } tweenDuration= { 150 } opacity= { 0 . 8 } /> ) } <main className= "w-full max-w-md" > <div className= "bg-orange-300 text-white p-4 rounded-t-3xl shadow-lg shadow-white" > <div className= "text-center" > <div className= "text-2xl font-bold tabular-nums tracking-wider" > マッスルカウンター </div> </ div > </div> { /* メインカウンター部分 */ } <div className= "bg-white pt-6 shadow-2xl shadow-white border-x-2 border-orange-300" > <div className= "text-center" > <div id= "counter-display" className= "text-8xl font-bold text-orange-600 tabular-nums leading-none" > { count } </div> </ div > </div> { /* ボタン部分 */ } <div className= "bg-white p-6 rounded-b-3xl shadow-lg shadow-white border-x-2 border-b-2 border-orange-300" > <div className= "flex gap-3 mb-6" > <button onClick= { decrement } className= "flex-1 py-4 text-2xl font-bold text-red-600 bg-orange-200 hover:bg-orange-400 active:bg-orange-500 transition-all duration-200 rounded-xl shadow-sm transform active:scale-95" > − 1 </button> <button onClick= { increment } className= "flex-1 py-4 text-2xl font-bold text-white bg-green-400 hover:bg-green-500 active:bg-green-300 transition-all duration-200 rounded-xl shadow-sm transform active:scale-95" > + 1 </button> </ div > <div className= "flex flex-col gap-3" > <button onClick= { reset } className= "w-full py-3 text-lg font-semibold text-orange-700 bg-white hover:bg-orange-100 active:bg-orange-200 transition-all duration-200 rounded-lg shadow-md border-2 border-orange-300 transform active:scale-98" > リセット </button> <button onClick= { toggleKeyMode } className= "w-full py-3 text-sm font-medium text-orange-700 bg-orange-200 hover:bg-orange-100 active:bg-orange-200 transition-all duration-200 rounded-lg shadow-sm" > キーモード: { keyMode === 'MN' ? 'M/N' : 'F13/F14' } </button> < div className = "text-xs text-orange-700 text-center mt-2" > { keyMode === 'MN' ? ( <> < kbd className = "px-2 py-1 bg-orange-200 rounded font-mono" >M</ kbd > で + 1 / < kbd className = "px-2 py-1 bg-orange-200 rounded font-mono ml-1" >N</ kbd > で - 1 </> ) : ( <> < kbd className = "px-2 py-1 bg-orange-200 rounded font-mono" >F13</ kbd > で + 1 / < kbd className = "px-2 py-1 bg-orange-200 rounded font-mono ml-1" >F14</ kbd > で - 1 </> ) } </div> </ div > </div> </ main > </div> ); } 実装時に気を付けることは、キー入力の判定に KeyboardEvent.key プロパティを使用した場合、Androidでは F13~F24キー が Unidentified と判定されてしまいますが、 KeyboardEvent.code プロパティであれば正常に判定できます。 これはAndroidが想定しているキーボードレイアウトに F13~F24キー が存在しないために物理キーからのパースに失敗しているためだと思われます。 4. ケースのビルド 私はもともと、自分しか使わないようなハードウェアを制作するとき3Dプリンタでケースを作ることがよくありました。 しかし製作途中での仕様の変更や、実際に使ってみて基板の位置やスイッチの配置を変えたくなったときに、設計をやり直して印刷を待つのはかなり面倒です。 その点、決定版になるまではLEGO®ブロックを採用しておけば、苦労が少なくて済みます。 この方法でケースを作るには、まず設計ソフトであるBrickLink Studioにマイコンの3DモデルデータをPartDesigner機能でインポートします。 Pico Wであれば 公式のドキュメント で配布されています。 そのあと、基板を固定できるようにパーツを選定し、 タイルとプレートのパーツで覆えば完成です。 Studioの機能ですべて実際に存在するパーツを選定したので、既製品だけで作れました。 まとめ 実際に触れる・使えるモノができる達成感はひとしお 全体的に味付け薄目でサラッと書いてますが、ハードウェアが動く達成感はソフトウェア開発とはまた違った味わいがあります。 なんでもLLMに流れてしまう昨今、ハードウェアは人間に残されたフロンティアの一つかもしれません。 この記事で自分用ハードウェア製作に興味をもってくださる方が一人でもいれば幸いです。 執筆者 福井 凱理(NTT西日本ルセント) 商標 「Raspberry Pi」はRaspberry Pi財団の登録商標です。 「Arduino」はArduino SAの商標です。 「LEGO」「レゴ」「BrickLink」はLEGO Group.の登録商標です。 「Next.js」はVercel Inc.の商標です。 「React」は、Meta Platforms, Inc.の商標または登録商標です。 「Tailwind CSS」はTailwind Labs Inc.の商標です。 「ESP32」は、Espressif Systems社の登録商標です。 *1 : Arduino IDEとその環境で使用されるプログラミング言語を、本記事では「Arduino言語」と表記します。 公式サイトGetting Started によると、「『Arduino プログラミング言語』としても知られるArduino APIは、C/C++ 言語に基づくいくつかの関数、変数、構造体で構成されています。(筆者訳)」とあり、また リファレンス にも「Arduino programming language」とあるためです。
はじめに NTT西日本の福田です。 NTT西日本では、生成AI技術を現場で活用し業務改善につなげる取り組みを進めています。 その一環として、社内でDify®が利用できるようになりました。 しかし、どれだけ優れたツールが展開されても、 社員が使いこなせなければ意味がありません。 そこで今回、AIイベントとしてDifyを実際に触りながら理解を深めてもらうために、 生成AIで業務改善にトライ! と題してRAG(Retrieval-Augmented Generation:検索拡張生成)チャットボット構築をテーマにしたハンズオンを開催しました。 本記事では、今回のAIイベント内容をまとめ、 Difyに興味がある方や社内外で生成AI活用を進めたい方に向けてご紹介します。 目次 1. AIイベントを企画した背景 2. AIイベントの構成 3. ハンズオン|あるサービスの問い合わせRAGボットを作ってみた 4. 実践演習|“略語問題”にどう立ち向かったか 5. Advanced編|複数チャットボットの統合窓口を作る 6. AIイベントを通じて見えてきた“受講者のつまずきポイント”と学び 7. 今後の展望 おわりに 1. AIイベントを企画した背景 社内でDify®が利用できるようになりました。 ただし、 利用できるようになってもすぐに使いこなせる わけではありません。 実際には、 どのように使い始めればよいのか RAGチャットボットはどう作ればよいのか 自分の業務でどう活かせばいいのか といった疑問や不安の声が想定されます。 そのため今回、 展開タイミングに合わせて“使える状態になってもらう”こと を目的としたAIイベントを企画しました。 2. AIイベントの構成 AIイベントは 3部構成 とし、段階的にスキルを身につけられるように設計しました。 ハンズオン編  あるサービスのRAGチャットボットを作成 実践演習編  略語・俗称・省略語による回答精度低下問題を解決 Advanced編  増えすぎた複数チャットボットの統合窓口をつくる 参加者はエンジニアだけでなく、非エンジニアの方も含めた幅広い層。 初級者でも迷わず手を動かせるようにしつつ、上級者にも学びが残って「つまらない」と感じないよう、実践・Advanced課題も用意する構成に工夫しました! 「生成AIを使えるようになりたい」「チャットボットに興味がある」という声が多く、非常に活発な取り組みとなりました。 3. ハンズオン|あるサービスの問い合わせRAGボットを作ってみた 課題テーマ 「あるサービスの問い合わせ対応をするチャットボットを構築せよ」 使用した参照文書 今回のハンズオンでは、以下の社内公開資料をKnowledgeとして登録しました。 サービス申し込み受付マニュアル 付帯サービス概要ドキュメント これらの文書に基づき、問い合わせに回答できるRAGチャットボットの作成を行いました。 実施した内容 今回のハンズオン演習では、まず 「Difyとは何か」「どのように操作するのか」 を理解してもらうことを目的に、基本的な操作に重点を置きました。 アプリ作成やKnowledgeへの文書登録といった基本操作 各ブロック(ノード)がどのように連携して動作するかの理解 取り込んだ文書を使って実際に回答が返ってくる体験 といった内容を中心に進めています。 この後の「精度向上」セッションでは、ハンズオンで学んだブロック構成やチャットフローの使い方を応用し、 参加者に“自分で改善を考える”取り組みをしてもらう 流れとしました。 実際にできたもの 取り込んだ文書に基づき、 サービス申し込み手続きの説明 付帯サービスに関する案内 といった内容に回答できるチャットボットが構築できました。 参加者からは、 「自分の文書を入れるだけでここまで動くのは驚き」 という声もあり、Difyの使いやすさを感じてもらう機会になりました。 チャットフロー機能で作成したRAGアプリケーションの全体像 4. 実践演習|“略語問題”にどう立ち向かったか ハンズオンでボットを作成した後、実際にテストしてみるとすぐに問題が見えてきました。 ✔ ユーザ(オペレーター)が普段利用している略語でチャットする場合がある 例: FD(フリーダイヤル) BO(バックオーダー) 現調(現地調査) 加入電話(固定電話) しかし、こういった単語は RAGの参照文書には書かれていません 。 そのため、RAG検索がうまく機能しないケースが発生しました。 4-1. 解決アプローチ 今回の実践演習では、簡素な実装として プロンプト内に略語展開ルールをすべて記述し、 ユーザーの入力内容を変換したうえでRAGに渡す方式 を採用しました。 実装した内容 プロンプトに略語 → 正式名称の変換ルールを記載 LLMがユーザーの質問を受け取ったら まず変換を実行 「変換済みの質問」を後段のブロックに渡して検索 検索後の結果を元に最終回答を生成 実践演習の内容 4-2. Before / After Before :略語が来ると回答不能 After :ほぼ正式名称として認識できるようになり回答精度が向上 参加者からは「確かに必要になる改善ですね!」という声が多く、 実際に動作させることで処理の重要性を体感しました。 5. Advanced編|複数チャットボットの統合窓口を作る 各部署が自由にRAGボットを作れるようになった結果、 新たな課題が見えてきました。 ✔ ボットが増えすぎてどれを使えば良いかわからない そこで、 “複数チャットボットの統合窓口” を作る取り組みに挑戦しました。 5-1. 実装した検索カテゴリ 今回のAdvanced編では、 以下の3つの検索カテゴリをチャットフロー内に実装しました。 別サービスに関する検索 当社の将来構想サービスに関する検索 サービスに関する検索 これら3カテゴリに分類し、ユーザーの問い合わせ内容に応じて適切な情報源にルーティングする構成を参加者とともに構築しました。 5-2. チャットフローでの実現例 Difyのチャットフロー機能を用いて、以下の流れで動作する仕組みを構築しました。 User Input カテゴリ判定(LLM分類) Switch分岐で3カテゴリへルーティング 各カテゴリに対応するRAGチェーンを実行 回答を統合しユーザーへ返答 チャットフローを実際に触りながら構築することで、 「一つの窓口から複数情報源にアクセスできる」構成を体験できたという声をいただきました。 6. AIイベントを通じて見えてきた“受講者のつまずきポイント”と学び 今回のイベントでは、参加者の実際の質問や作業中のつまずきから、 「Difyを初めて触るときにどこが難しく感じるのか」 が具体的に見えてきました。 ここでは、受講者の困りごとを中心にまとめます。 受講者がつまずいたポイント ✔ どの文書が検索対象になるのかイメージしづらい Knowledgeに文書を入れるだけでRAGが動くものの、 「どの文章が検索されるのか」「どの記述が回答に使われるのか」が最初は掴みにくい様子でした。 ✔ ブロック(ノード)の役割が最初は混乱しやすい チャットフロー内の各ブロックが 入力を処理するのか 文書検索を行うのか 回答をまとめるのか といった役割の理解に時間が必要な参加者が多く見られました。 ✔ チャットフローの分岐処理(Switch)の考え方に慣れが必要 Advanced編で扱った「分類 → ルート分岐」は、 プログラミング経験の少ない参加者にはややハードルが高かったようです。 ✔ プロンプトの“指示の書き方”に慣れておらず、期待値とのギャップがある プロンプトは「思ったより細かく指示しないと意図どおりに動かない」場面があり、 「ここまで指示を与えなきゃならないの?」という反応も見られました。 そこから得られた学び “何が検索対象になるか”を理解すると、文書整備の重要性に気づける ブロックの役割がわかると、改善ポイントを自分で発見できるようになる 略語問題は初学者ほど気づきにくく、実務ユーザーほど重要性を実感する チャットフローの構造を一度理解すると、マルチチャットボット統合など発展的な応用に進みやすい プロンプトの作り方そのものが奥深く、それ自体をテーマに勉強会を1回開催できるレベル 参加者の声(抜粋) 「AIアプリを簡単に作れることに驚いた」 「RAGの全体像とプロセスを更に明確に把握することができた」 「是非業務でも活用し、メンバーにも展開していきたい」 「短い時間だったので、もう少し長く時間を取ってやってほしい」 受講者のつまずきから、 「まずはDifyに触れて構造を理解することが、応用への第一歩」 ということが明確になった取り組みでした。 ハンズオン実施中の様子 7. 今後の展望 今回のAIイベントを通じて、参加者の皆さんにDifyに触れていただき、 チャットボット構築の基本的な流れを体験してもらうことができました。 今後は、AIイベントで得た知識をもとに、 実際に業務の中で使ってもらうこと が重要だと考えています。 利用を進める中で寄せられた声や見えてきた課題を踏まえ、 現場でより使いやすい形へと活用方法を洗練させていきます。 また、社内向けのAIテンプレートは既に整備されているため、 これを活用するとともに、必要に応じてテンプレートの改善や活用促進を図っていきます。 今後もDifyを活用したチャットボット構築を支援しながら、 関連部とともに現場での業務改善につながる勉強会を進めていく予定です。 おわりに 今回のAIイベントは、参加者が 「実際に手を動かす」 ことを中心にしたため、 技術だけでなく実務に近い学びが得られる場になりました。 Difyがどのように使えるのか RAGのどこがつまずきポイントか 実務で活用するための工夫は何か など、現場のリアルな知見が積み上がったと思います。 今後も継続しながら、社内AI活用の支援を続けていきたいと思います。 執筆者 福田 航平(NTT西日本 技術革新部) NTT版LLM「tsuzumi®」のビジネス適用に関する検討・推進に従事。 JDLA Deep Learning for ENGINEER(2024#1)取得。 商標 「Dify」は、Dify.AI(またはその関連会社)の商標もしくは登録商標です。 「tsuzumi」は、NTTの商標もしくは登録商標です。
はじめに NTTビジネスソリューションズの衣川です。 この記事は、私が自作した小規模なハードニング競技環境の構成と設計思想を解説する記事です。 ハードニング競技とは、サーバの脆弱性を減らし、模擬的なサイバー攻撃からサーバを守る競技です。 作成したハードニング競技の概要 本記事で紹介するハードニング競技の概要は以下になります。 項目 内容 守る対象 WordPress(バージョン4.7.0)がインストールされたWebサーバ1つ 演習時間 30分/回 攻撃の種類 全6つ 攻撃方法 攻撃シナリオに沿って作成されたスクリプトを使い自動で実施 評価方法 ハッキングされていないWebページ(WordPressの固定ページと投稿)のアクセス数の累積 今回自作したハードニング競技環境では基本的なセキュリティ対策が施されていない基本的な脆弱性を扱っており、短時間でサイバー攻撃を体験できる構成にしました。 対象読者 本記事が想定する対象読者は以下を想定しています。 ハードニング競技に興味のある方 ハードニング競技環境を作成するにあたり、参考情報を探している方 本記事では詳細な構築手順や攻撃スクリプトの解説は扱っていませんので、ご了承ください。 背景 取り組みのきっかけは、Hardening Project主催のイベントで実施されたMicro Hardeningに参加したことです。 参加者としてハードニング競技をしつつも、裏側の仕組みに興味がわいてきて自分でも約30分で終わるようなハードニング競技環境を作成したいと思い、取り組み始めました。 注意事項 ここに記載されている情報は教育目的および検証を目的としたものです。 実運用環境や第三者のシステムに対して無断で使用することは、法律に違反する可能性があります。 本記事の内容を使用する際は、必ず自分が管理・所有する環境でのみ実行してください。 脆弱性のある環境を構築するため、グローバルIPアドレスを付与したサーバで構築しないでください。 本記事の内容の使用によって生じたいかなる損害・不利益・法的責任についても、執筆者は一切の責任を負いません。 本記事の内容の悪用を固く禁じます。 この注意書きをもって、執筆者は十分な注意喚起を行ったことを示します。 目次 はじめに 作成したハードニング競技の概要 対象読者 背景 注意事項 目次 1. ハードニング競技環境の全体構成 2. Webサーバの詳細設定 2-1. ユーザーの権限追加 2-2. /var/www/htmlの権限変更 2-3. ソフトウェアの設定変更 2-3-1. WordPressの設定値 2-3-2. vsftpdの設定値 3. 攻撃シナリオと対応する設定値 3-1. 不正なファイルの配置 3-1-1. 脆弱性と攻撃手法 3-1-2. 対処方法 3-1-2-1. 不要なユーザーはロックする 3-1-2-2. ユーザーのパスワードを複雑にする 3-1-2-3. 権限を必要最小限にする 3-2. 不正ユーザーの作成 3-2-1. 脆弱性と攻撃 3-2-2. 対処方法 3-2-2-1. ユーザー権限を必要最小限にする 3-2-2-2. 不審なユーザーの有無を確認する 3-3. Webページの削除 3-3-1. 脆弱性と攻撃 3-3-2. 対処方法 3-3-2-1. 不要な機能を無効にする 3-3-2-2. バックアップを取得する 3-4. Webサイトの機能停止 3-4-1. 脆弱性と攻撃 3-4-2. 対処方法 3-4-3. 補足 3-5. 窃取した情報をWebサイトに投稿 3-5-1. 脆弱性と攻撃 3-5-2. 対処方法 3-5-2-1. FTPポートの接続制限する 3-5-2-2. FTPの設定を変更する 3-5-2-3. プロセスを確認する 3-6. WordPressの脆弱性をついた改ざん 3-6-1. 脆弱性と攻撃 3-6-2. 対処方法 3-7. 攻撃シナリオのまとめ 4. 評価サーバの作成 5. おわりに 執筆者 免責事項 商標 付録 インフラの詳細設定 ネットワーク サーバ インスタンスの設定値 新規作成したユーザー 各サーバの共通設定 接続元PCの設定 利用したWordPressのプラグイン スクレイピングを実施するスクリプト スクレイピング結果の可視化 1. ハードニング競技環境の全体構成 ハードニング競技で利用するサーバはすべてAWSの東京リージョンに構築しており、主なサーバは以下です。 サーバ種別 役割・説明 Webサーバ 競技参加者が保守するサーバで、WordPressが稼働している 評価サーバ Webサーバにスクレイピングして正常稼働しているか確認する。また、稼働状況を点数にして表示する 攻撃サーバ Webサーバに攻撃をするサーバ 踏み台サーバ 上記3つのサーバにアクセスする時に経由するサーバ サーバのOSはすべてUbuntu Server 24.04 LTSです。 ハードニング競技の作成に直接関わらない以下の情報は付録にまとめています。 ネットワーク サーバ  インスタンスの設定値 新規作成したユーザー 各サーバの共通設定 2. Webサーバの詳細設定 攻撃シナリオで悪用する設定のみを抜粋して記載します。 2-1. ユーザーの権限追加 以下を実行して、 www-data ALL=(ALL) NOPASSWD: ALL を追記します。 $ sudo nano /etc/sudoers 後述する攻撃でHTTPリクエストでサーバの設定変更をするために、www-dataはパスワード無しでsudoを実行できるようにしています。 2-2. /var/www/htmlの権限変更 /var/www/html の権限を 777 に設定し、誰でも読み込み・書き込み・実行 ができるようにします。 インターネットに公開されているディレクトリに書き込みができてしまうことは大変危険で、攻撃シナリオでもOSコマンドインジェクションを実行するスクリプトを /var/www/html に配置します。 2-3. ソフトウェアの設定変更 主に攻撃で利用する以下3つのソフトウェアをインストールしました。 WordPress(バージョン4.7.0)  vsftpd zip このうち、WordPressとvsftpdに追加した設定について解説します。 2-3-1. WordPressの設定値 項目 設定値 ドメイン名 wphardening1.com 接続ポート 8081 REST API 有効化済み REST APIを有効にしたのは、バージョン4.7.0のWordPressのREST APIの脆弱性をついてWebページの改ざんを実施するためです。 また、念のため /var/www/html の所有者がwww-dataになっていることを確認します。 2-3-2. vsftpdの設定値 vsftpdをインストールした後に、以下3つの設定を追加します 設定項目 設定値 説明 listen YES FTPアクセスを受け入れる(IPv4) write_enable YES 書き込み(ファイルアップロードなど)を許可 allow_writeable_chroot NO chroot を強制しない設定にし、ホームディレクトリ外へのアクセスを許可 特に最後の ホームディレクトリ外へのアクセスを許可 を悪用して、攻撃を仕掛けます。 3. 攻撃シナリオと対応する設定値 攻撃サーバからWebサーバに実施する攻撃を全部で6つ用意しました。 指定の時間になれば、以下の表の上から順番に攻撃を仕掛けます。 No 攻撃手法 内容 3-1 不正なファイルの配置 OSコマンドインジェクションを行うスクリプトをWebサーバにアップロード 3-2 不正ユーザーの作成 バックドアとして利用するユーザーを作成 3-3 Webページの削除 WordPressの投稿を削除 3-4 Webサイトの機能停止 コマンドインジェクションまたは不正ユーザー経由で、サイトをパスワード付きzipにし閲覧不可に 3-5 窃取した情報をWebサイトに投稿 FTPで窃取したファイルの内容をWebページで公開 3-6 WordPressの脆弱性をついた改ざん WordPressのREST APIの脆弱性を悪用してWebページを改ざん 以降で、各攻撃について起点となった脆弱性と攻撃手法、そして対処方法を説明します。 3-1. 不正なファイルの配置 攻撃の起点となった脆弱性、攻撃手法、そして対処方法の概要を以下に記載します。 項目 概要 脆弱性 脆弱なパスワードと過剰な書き込み権限 攻撃手法 SSHブルートフォース→スクリプト配置 対処方法 ユーザーロック、パスワード強化、権限制限 以降で、詳細について解説します。 3-1-1. 脆弱性と攻撃手法 まずSSH接続にブルートフォース攻撃します。 成功すれば、そのアカウントを利用して、SCPでOSコマンドインジェクションを実行するためのスクリプトをWebサーバの /var/www/html に配置します。 以下の事前に作成済みのユーザーでブルートフォース攻撃が成功してしまいます。 ユーザー名 パスワード test test このユーザー名と脆弱なパスワードでは、SSH接続をブルートフォース攻撃で突破されてしまいます。 加えて、 /var/www/html にどのユーザーでも書き込みができてしまうため、スクリプトの配置も成功します。 参考までに、スクリプトの内容は以下の通りで、クエリパラメータをそのまま exec() で実行するものになっています。 <?php $ command = $ _GET [ 'cmd' ] ; exec ( $ command ) ; ?> 3-1-2. 対処方法 考えられる対処を3つ挙げます。 不要なユーザーはロックする ユーザーのパスワードを複雑にする 権限を必要最小限にする 他にもパスワード認証でSSH接続できないようにすることも考えられますが、競技参加者はパスワード認証でのみWebサーバにアクセスできることにしているのでここでは検討しません。 3-1-2-1. 不要なユーザーはロックする 今回はWebサーバにアクセスするために別のユーザーが用意されていました。 そのため、このアカウントは不要でユーザーをロックしてしまえば、この攻撃は成功しません。 以下はユーザーロックのコマンド例です。 $ sudo passwd -l [ユーザー名] 3-1-2-2. ユーザーのパスワードを複雑にする testユーザーのパスワードがtestで、脆弱なことを知っていれば、パスワードは長く複雑なものにするべきです。 以下はコマンド例です $ sudo passwd [ユーザー名] ただ、現実では勝手に変更してしまうことはリスクがあるので、利用者がいないことを確認してからになるかと思います。 3-1-2-3. 権限を必要最小限にする /var/www/html の書き込み権限が緩く、どのユーザーでも書き込みができてしまったため、ファイルの配置が成功していました。 逆にディレクトリの書き込み権限を制限することで、この攻撃は防げます。 以下は /var/www/html の権限を755に設定し、所有者以外書き込めないようにするコマンド例です $ sudo chmod 755 /var/www/html 3-2. 不正ユーザーの作成 攻撃の起点となった脆弱性、攻撃手法、そして対処方法の概要を以下に記載します。 項目 概要 脆弱性 www-dataに過剰なsudo権限 攻撃手法 スクリプト利用で不正ユーザー追加 対処方法 権限最小化、不審ユーザー確認 以降で、詳細について解説します。 3-2-1. 脆弱性と攻撃 No.1 でアップロードしたスクリプトを利用して不正なユーザーを作成します。 www-dataの権限が非常に強いことも攻撃の起点になっており、以下を実行し www-data ALL=(ALL) NOPASSWD: ALL を追記することでパスワード無しでsudoを実行できるようにしていました。 $ sudo nano /etc/sudoers すべてのコマンドを、パスワード無しで実行できるユーザーを現実には設定しないとは思いますが、必要最小限の権限が守れていないことで成功してしまう攻撃です。 3-2-2. 対処方法 以下2つが考えられます。 ユーザー権限を必要最小限にする 不審なユーザーの有無を確認する 3-2-2-1. ユーザー権限を必要最小限にする www-dataの権限が強すぎたことで攻撃が成功してしまいます。 以下で /etc/sudoers を編集し、www-dataの権限を変更することで、sudo権限で実行できるコマンドを絞ることや、パスワードを求めるように設定変更すれば、攻撃を防ぐことができます。 $ sudo nano /etc/sudoers 3-2-2-2. 不審なユーザーの有無を確認する 定期的にユーザー一覧を確認して、不審なユーザーが増えていないか確認することが重要です。 現実世界でも数か月に1回以上はアカウント・ユーザーの棚卸しすることで、攻撃に気が付けるかもしれません。 3-3. Webページの削除 攻撃の起点となった脆弱性、攻撃手法、そして対処方法の概要を以下に記載します。 項目 内容 脆弱性 REST APIが有効で不要機能露出 攻撃手法 REST APIから投稿ID取得→Webページ削除 対処方法 REST API無効化、バックアップ取得 以降で、詳細について解説します。 3-3-1. 脆弱性と攻撃 以下のREST APIから投稿IDを取得し、OSコマンドインジェクション用スクリプトを経由して削除コマンドを実行します。 http://wphardening1.com:8081/wp-json/wp/v2/posts 3-3-2. 対処方法 スクリプトが実行されてしまうことへの対処はNo.2でお伝えしたので、それ以外で以下2つ挙げます。 不要な機能を無効にする バックアップを取得する 3-3-2-1. 不要な機能を無効にする 可能な限り不要な機能(今回はREST API)は無効にするべきです。 しかし、30分の間でREST APIが有効になっていることに気が付くことは難しいため、次の対処の方が実施しやすいと思います。 3-3-2-2. バックアップを取得する 競技の開始タイミングで、 /var/www/html のバックアップを取得しておくことで、削除された後でもバックアップから復元すれば元に戻せます。 基本的なことですが、バックアップの取得と復元は大切なことです。 以下は /var/www/html を /tmp/ にコピーするコマンド例です $ sudo cp -a /var/www/html /tmp/ 今回の競技環境にはなかったですが、バックアップ用サーバがあれば、そちらに格納した方が安全です。 3-4. Webサイトの機能停止 攻撃の起点となった脆弱性、攻撃手法、そして対処方法の概要を以下に記載します。 項目 概要 脆弱性 OSコマンドインジェクションや不正ユーザー接続 攻撃手法 /var/www/html をzip化しサイト停止 対処方法 No.1〜3の攻撃を防ぐ対処 以降で、詳細について解説します。 3-4-1. 脆弱性と攻撃 OSコマンドインジェクションを行うスクリプト、または不正ユーザーのSSH接続のどちらかを起点として、 /var/www/html のディレクトリをパスワード付きzipにし、Webサイト全体が機能しないようにします。 (No.5, 6の攻撃も続くため、一定時間経過後に自動で復旧させます) 3-4-2. 対処方法 考えられる対処は以下3つですが、すべて前述のためこちらでの解説は省略します。 不正なファイルを配置させない(No.1) 不正なユーザーを作成させない(No.2) バックアップを取得しておく(No.3) 3-4-3. 補足 これまでと同じ脆弱性を利用していますが、Webサイトを機能停止させることで、サイバー攻撃を受けた衝撃を競技参加者に体験してもらいます。 これまでに競技を体験した方は、ここで一番驚いていました。 3-5. 窃取した情報をWebサイトに投稿 攻撃の起点となった脆弱性、攻撃手法、そして対処方法の概要を以下に記載します。 項目 概要 脆弱性 FTP設定の不備でホーム外にアクセス可能 攻撃手法 /etc/passwd 窃取→Webページで公開 対処方法 FTPポートの接続制限、FTPの設定変更、プロセス監視 以降で、詳細について解説します。 3-5-1. 脆弱性と攻撃 以下のFTPの設定を攻撃の起点とし、 /etc/passwd を窃取します。 設定項目 設定値 説明 allow_writeable_chroot NO chroot を強制しない設定にし、ホームディレクトリ外へのアクセスを許可 そして、No.2で作成した不正なユーザーでSSH接続しWebページにファイルの中身を書き込んで公開します。 3-5-2. 対処方法 FTPに関する対処として以下3つを挙げます。 FTPポートの接続制限する FTPの設定を変更する プロセスを確認する 3-5-2-1. FTPポートの接続制限する ファイアウォール機能で20番・21番ポートの接続を制限することで攻撃を防ぎます。 以下はufwを利用したコマンド例です。 $ sudo ufw deny 20 $ sudo ufw deny 21 ufwが有効になっていなければ、以下コマンドで有効にします。 $ sudo ufw enable 3-5-2-2. FTPの設定を変更する FTPが使われている可能性がある場合はFTPの脆弱な設定を変更することでも対応できます。 しかし、FTPの設定値に詳しくなければ難しいと思われます。 (生成AIに聞くのも一案です) 3-5-2-3. プロセスを確認する プロセスを確認して、FTPを使っていないのにプロセスが起動していれば、悪用を疑えます。 攻撃を防ぐことはできませんが、攻撃検知を早め迅速な対処につながるかもしれません。 以下はプロセスの中からftpを含む行を抽出するコマンド例です。 $ ps aux | grep ftp 3-6. WordPressの脆弱性をついた改ざん 攻撃の起点となった脆弱性、攻撃手法、そして対処方法の概要を以下に記載します。 項目 概要 脆弱性 WordPress 4.7.0 REST API脆弱性(CVE-2017-1001000) 攻撃手法 REST API悪用で認証なし改ざん 対処方法 WPアップグレード、REST API無効化、バックアップ取得 以降で、詳細について解説します。 3-6-1. 脆弱性と攻撃 Webサーバには、REST APIの脆弱性があるバージョン4.7.0のWordPressをインストールしています。 悪用する脆弱性はCVE-2017-1001000で、REST APIの機能が有効になっていれば、認証なしでコンテンツを改ざんできてしまう脆弱性です。 これを悪用し、Webページを改ざんします。 参考: CVE-2017-1001000 3-6-2. 対処方法 攻撃を防ぐには、以下3つが挙げられます。 WordPressのバージョンをアップグレードする 不要な機能(REST API)を無効にする バックアップを取得しておく しかし、1つ目のアップグレードは今回の環境ではWebサーバからインターネットに通信することは想定していないため難しく、3つ目のバックアップ取得は前述で記載済みなので、詳細は割愛します。 30分間ではREST APIが有効になっていることに気が付きにくいですが、以下サイトでは無効化する手順も記載されていましたので、参考にしてみてください。 参考: WordPress4.7.0以降で「REST API」を無効にする方法が変わっていたので試しました 3-7. 攻撃シナリオのまとめ 今回用意した6つの攻撃で、起点になったことをまとめると以下3つになります。 脆弱なパスワードのユーザー 必要以上の権限付与(ディレクトリ、ユーザー) 不要だが稼働しているサービス・機能の放置 30分の中で6つの攻撃が実施されると、準備をしていない方はほとんどの攻撃を受けてしまいます。 しかし、 攻撃を受けることを経験する という観点では、準備と対応の時間が十分ではない方がよいと考えて、あえて短時間に6つの攻撃を盛り込むようにしました。 4. 評価サーバの作成 詳細は最後の付録に記載していますが、 トップページにアクセスし、同じドメインのURLがあれば、すべてに遷移していきスクレイピングし、正常なWebページ数をカウントしていきます。 そして、カウントを累積した値が最終的な得点となります。 5. おわりに 今回の記事では、私が作成した小規模なハードニング競技環境の概要を説明しました。 脆弱性のあるサーバを構築し、その攻撃方法も検討することは以下の2点で勉強になりました。 攻撃を受けてしまった時の影響を具体的に体験できる 脆弱な設定値を検討することで、設定値の意味を理解できる 脆弱性の再現と攻撃の自動化も難しく、いかに不自然でない範囲で攻撃を自動化できるような脆弱な設定を組み込むのか、という思考も必要でした。 また、攻撃スクリプトをテストしている時に、構築している環境が何度か破損してしまい、本当の意味でバックアップの重要性を体験できました。 ハードニング環境の構築を検討されている方は定期的にバックアップ(AWSならAMIなど)を取得することをおすすめいたします。 今後、今回扱わなかった代表的な脆弱性をもりこんだハードニング競技環境も機会があれば作成・執筆したいと考えています。 執筆者 衣川 琢磨(NTTビジネスソリューションズ株式会社 バリューデザイン部) セキュリティ分野の中でも特にサイバーハイジーンの実現にむけて支援するマネージドサービスの開発と運用に携わっています。 免責事項 本記事の内容は執筆時点(2025年11月)の情報に基づいており、サービスの仕様変更等により内容が変更される可能性があります。 本記事で紹介する設定手順や使用方法は、特定の環境での動作確認に基づくものであり、すべての環境での動作を保証するものではありません。 商標 WordPress は WordPress Foundation の登録商標です。 AWS および Amazon Web Services は Amazon.com, Inc. の商標です。 付録 ハードニング環境を構築するにあたり必要ですが、ハードニング競技そのものにはあまり関わらない情報を記載します。 インフラの詳細設定 ネットワーク 種類 Name プライベートIPアドレス帯 設定・備考 VPC my-hardening-project 192.168.0.0/16 プライベートサブネット my-hardening-private-subnet 192.168.1.0/24 0.0.0.0あての通信がNATゲートウェイにいくルートテーブルを作成 パブリックサブネット my-hardening-public-subnet 192.168.2.0/24 0.0.0.0あての通信がインターネットゲートウェイにいくルートテーブルを作成 NATゲートウェイ my-hardening-nat-tmp ー 構築時に各サーバがインターネット接続できるように設置。構築後に削除 サーバ Webサーバ、攻撃サーバ、評価サーバはすべて プライベートサブネット内に構築し、直接インターネットからアクセスできないようにします。これらのサーバへ接続する際は、必ず 踏み台サーバを経由させることで、脆弱な設定を持つWebサーバがインターネットに公開されることを防ぎます。 サーバ構築時には、私が所有する Windows 11のPC(以下「接続元PC」)から、パブリックサブネット上に配置した踏み台サーバへSSH接続しました。その後、SSHのポートフォワーディング機能を利用して、プライベートサブネット内の各サーバに接続しました。 インスタンスの設定値 種類 Name プライベートIPアドレス 通信の制限 踏み台サーバ my-hardening-jmp 192.168.2.1 SSHのインバウンド通信のみを許可 Webサーバ my-hardening-web1 192.168.1.101 VPC内からのSSH, FTP, HTTP接続と、8081ポートのアクセスを許可 攻撃サーバ my-hardening-atk1 192.168.1.11 プライベートサブネットからの全通信と、VPC内からのSSH接続を許可 評価サーバ my-hardening-evl 192.168.1.201 プライベートサブネットからの全通信と、VPC内からのSSH接続を許可 新規作成したユーザー 種類 ユーザー名 パスワード 踏み台サーバ jumper jmphardening Webサーバ web-viewer webhardening Webサーバ test test 攻撃サーバ ー ー 評価サーバ viewer viewerhardening 各サーバの共通設定 パスワードでSSH接続できるようにし、以下コマンドで /etc/hosts に 192.168.1.101 wphardening1.com を追記し、WebサーバのIPアドレスとドメインを対応づけ、各Webサーバのページにアクセスする際に名前解決できるようにします。 $ sudo nano /etc/hosts 接続元PCの設定 Windows 11の 接続元PC で、管理者権限で起動させたメモ帳から、 C:\Windows\System32\drivers\etc\hosts を開き、以下を追記します。 127.0.0.1 wphardening1.com 接続元PCでSSHポートフォワードを設定すると、127.0.0.1の特定ポートがプライベートサブネット内のWebサーバへ転送され、ポートフォワード経由でプライベートサーバのWebページが表示されます。 利用したWordPressのプラグイン 脆弱性の悪用のためではなく、利便性向上のためプラグインは以下2つを導入しておきます。 WP Sitemap Page(サイトマップ作成用) Migration, Backup, Staging – WPvivid Backup & Migration(バックアップ取得用) 参考: WP Sitemap Page – WordPress プラグイン | WordPress.org 日本語 参考: Migration, Backup, Staging – WPvivid Backup & Migration – WordPress プラグイン | WordPress.org 日本語 スクレイピングを実施するスクリプト スクレイピングの大まかな処理の流れは以下です。 1. 開始時間まで待機 1. スクレイピングした結果から、遷移可能なURLを抽出 1. 遷移可能なURLがなくなるまで2を実施 1. 最初のWebページから2を実施する 以下がスクレイピングを実行するコードです。 複数台のWebサーバを同時にスクレイピングするならば、サーバの台数だけ複製し、以下3つの項目はWebサーバの情報に合わせます。 start_url:スクレイピングを開始するトップページのURL allowed_domains:評価対象で、スクレイピングをするドメイン名 output_file:スクレイピング結果を格納するファイル名 import subprocess from html.parser import HTMLParser from urllib.parse import urljoin, urlparse import datetime import time import os import config start_url = "http://wphardening1.com:8081" list_team = config.team_name_list output_file = f "titles{list_team[0]}.csv" # スクレイピングデータを格納するディレクトリ folder = "/home/ubuntu/results_curl" file_path = os.path.join(folder, output_file) # 過去のスクレイピング結果があれば削除 if os.path.isfile(file_path): try : os.remove(file_path) except FileNotFoundError : print ( "ファイルが見つかりませんでした" ) except PermissionError : print ( "削除権限がありません" ) else : print (f "{file_path} は存在しません" ) # 外部サイトを得点に入れないようにドメインを限定 allowed_domains = [ "wphardening1.com:8081" ] start_time = config.start_time end_time = config.end_time visited = set () url_list = [start_url] # スクレイピングしてパースするClass class SimpleHTMLParser (HTMLParser): def __init__ (self, base_url): super ().__init__() self.in_title = False self.title = "" self.links = [] self.base_url = base_url def handle_starttag (self, tag, attrs): if tag.lower() == "title" : self.in_title = True if tag.lower() == "a" : for name, value in attrs: if name == "href" : absolute = urljoin(self.base_url, value) if urlparse(absolute).netloc in allowed_domains: self.links.append(absolute) def handle_endtag (self, tag): if tag.lower() == "title" : self.in_title = False def handle_data (self, data): if self.in_title: self.title += data.strip() def fetch_html (url): try : res = subprocess.run( [ "curl" , "-sL" , url], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout= 10 ) return res.stdout.decode( "utf-8" , errors= "ignore" ) except Exception as e: print (f "[!] curl failed: {e}" ) return "" print ( "start time" ,start_time) print ( "end time" , end_time) while datetime.datetime.now() < end_time: # 開始時間までは待機 if (datetime.datetime.now() < start_time): print ( "wait" ) time.sleep( 30 ) continue # 遷移可能なURLがなくなればトップページから再スタート if not url_list: time.sleep( 40 ) url_list.append(start_url) visited.clear() print ( "not url_list \n restart with " ,url_list) continue # 遷移した先のWebページに記載されているURLをたどってスクレイピングをしていく time.sleep( 1 ) target_url = url_list.pop( 0 ) if target_url in visited: continue visited.add(target_url) html = fetch_html(target_url) if not html: continue parser = SimpleHTMLParser(base_url=target_url) try : parser.feed(html) except Exception as e: print (f "[Warning] HTML parse error: {e}" ) time.sleep( 60 ) if not (target_url==start_url): with open (file_path, "a" , encoding= "utf-8" ) as f: f.write(f "{parser.title or 'No Title or hacked'},{time.time()},{list_team[0]} \n " ) for link in parser.links: if link not in visited and link not in url_list: url_list.append(link) スクレイピング結果の可視化 スクレイピングで得られたデータをStreamlitで可視化します。 Streamlitはpipでインストールするのですが、Ubuntu Server 24.04 LTSではpipが使えないので、仮想環境の中でインストールします。 $ python3 -m venv venv-streamlit $ source venv-streamlit/bin/activate $ pip install streamlit 大まかな処理の流れは以下です。 全チームのスクレイピング結果をDataFrameに格納 タイトルに hacked と無いWebページ数を集計 時間推移を折れ線グラフで、最新の数値をテーブルで表示 import numpy as np import pandas as pd import time import streamlit as st import altair as alt import config import datetime import os # スクレイピング結果を格納するディレクトリ folder = "/home/ubuntu/results_curl" st.title( "獲得点数" ) # プレースホルダー作成 placeholder = st.empty() # Hardening競技の開始と終了の時間 start_time = config.start_time end_time = config.end_time while ( 1 ): # Hardeningが始まった直後はスクレイピング結果がまだない可能性があるためフラグで判断 exist_data_flag = False df = pd.DataFrame() # 参加チームの分だけデータをDataFrameに取り込む for team_name in config.team_name_list: # データの再読み込み(ファイルが更新される前提) result_scrapy_file = os.path.join(folder, f "titles{team_name}.csv" ) if os.path.exists(result_scrapy_file): exist_data_flag = True tmp_df = pd.read_csv(result_scrapy_file, names=[ "title" , "time" , "name" ]) # Wordpressページのタイトルに"hacked"とあれば加点しない tmp_df[ "valid_title" ] = tmp_df[ "title" ].apply( lambda x: 0 if "hacked" in x else 1 ).astype( int ) # UNIX TIMEを日時に変換 tmp_df[ "time" ] = tmp_df[ "time" ].apply( lambda ts: datetime.datetime.fromtimestamp(ts)) else : tmp_df = pd.DataFrame(columns=[ "title" , "time" , "name" , "valid_title" ]) df = pd.concat([df, tmp_df]) # まだスクレイピング結果がなければ以降の処理を実行しない if not (exist_data_flag): continue # "hacked"とないタイトルのページに訪れた回数の累積和(=点数)を計算 df[ "cumsum" ] = df.groupby( "name" )[ "valid_title" ].cumsum() # 各 name ごとに最新行を取得して、時刻を上書きして複製 # データ改ざんでページに到達できなくてもグラフ描画を実行するため latest_df = ( df.sort_values( "time" , ascending= False ) .groupby( "name" , as_index= False ) .head( 1 ) .copy() ) # Altairで折れ線グラフを作成 chart_data = df[[ "time" , "name" , "cumsum" ]] chart = alt.Chart(chart_data).mark_line().encode( x=alt.X( "time:T" , scale=alt.Scale(domain=[start_time, end_time])), y=alt.X( "cumsum:Q" , scale=alt.Scale(domain=[ 0 , 1000 ])), color= "name:N" , tooltip=[ "name" , "time" , "cumsum" ] ).properties(width= 1600 , height= 900 ) placeholder.altair_chart(chart, use_container_width= True ) # 表形式でも最新の点数を表示 st.table(latest_df[[ "name" , "cumsum" ]].reset_index(drop= True )) # 1分待機 time.sleep( 60 )
1.はじめに こんにちは。NTT西日本の谷本です。 今回はMicrosoft Entra IDでSalesforceへの シングルサインオン(SSO)とユーザー自動プロビジョニング の実践的な手順をまとめます。 本記事では、手順を解説するだけでなく、実際にあったエラーとその回避方法もご紹介します。 ※本記事の手順は 2025/11 記事作成時点のもので、Salesforceの無料開発者用アカウント(Developer Edition)で実施したものです。本番環境への適用に際しては、十分な検証を行った上で、ご自身の責任において実施してください。 2.対象読者 本記事が想定する対象読者は以下の通りです。 Microsoft Entra IDに興味をお持ちの方 Microsoft Entra IDを利用されている方 SalesforceとのSAML連携に興味をお持ちの方 3.検証の概要と前提条件  今回の実践手順では以下の図のようにMicrosoft Entra ID Admin Centerでの設定作業とSalesforceでの設定作業を交互に行います。  Microsoftの公式ドキュメントは以下になります。 learn.microsoft.com 本記事内容の前提条件は以下になります。 Microsoft Entra ID P1ライセンスが紐づいたユーザー Salesforceへの SSOのみの場合はMicrosoft Entra ID Freeライセンスで実装可能です。またユーザーを指定したユーザー自動プロビジョニングもMicrosoft Entra ID Freeライセンスで実装可能です。 しかし今回の検証ではグループを指定し、 グループに属するユーザーを自動プロビジョニングする ように構成しているため Microsoft Entra ID P1ライセンス が必要です。 詳細は以下の公式ドキュメントをご参照ください。 learn.microsoft.com 次のいずれかのロール   -アプリケーション管理者   -クラウドアプリケーション管理者   -アプリケーション所有者 今回の手順作成時は最も包括的な権限を持つ グローバル管理者 ロールを割り当てています。 本番環境で運用する場合は、 最小権限の原則 に従い、上記のロールを割り当てることが推奨されます。 SSO が有効な Salesforce のサブスクリプション 今回の検証では Salesforce Developer Edition を用いて実践手順を作成しております。 Salesforce Developer Editionアカウントの作成方法は4-0【事前準備】検証用のSalesforceのアカウント作成を行う。に記載しております。 またSalesforce Developer Editionの制約もありますので、5-1.Salesforceのプロファイルについてをご参照ください。 4.実践手順について 4-0.【事前準備】Salesforce Developer Editionのアカウント作成を行う 【事前準備】の作業はSalesforceで設定作業を行います。 まず以下ページより、Salesforceの無料開発者用アカウント(Developer Edition)にサインアップします。 https://www.salesforce.com/form/developer-signup/ サインアップにはメールが受け取ることが可能なメールアドレスを利用してください。 作成したアカウントでSalesforceにログインし、[設定]からテナント情報を確認します。 以下のようにアクセスし作成したSalesforceのカスタムドメイン情報を確認します。 4-1.【作業1】SalesforceアプリをMicrosoft Entra IDテナントに追加する 【作業1】の作業はMicrosoft Entra ID側で設定作業を行います。 まずMicrosoft Entra IDにアクセスし、左側のエンタープライズアプリケーションのタブから遷移し、新しいアプリケーションの追加を行います。 検索タブに「Salesforce」と入力し検索を行い、Salesforceのアプリケーションを選択します。アプリケーションに対して名前を入力し作成します。 今回はSalesforce_tanimotoと名前を付けたアプリケーションを作成しました。 以下のように作成されたことを確認します。 4-2.【作業2】SalesforceテナントのSSOを構成する。 【作業2】の作業はMicrosoft Entra ID Admin Centerで設定作業を行います。 先ほど作成したSalesforce_tanimotoに移動し、シングルサインオンの設定項目から SAML方式 を選択します。 SAMLによるシングルサインオンのセットアップの項目の①基本的なSAML構成を編集します。 基本的なSAML構成の識別子、応答URL,サインオンURLに https://"Salesforceのカスタムドメイン名" を入力します。 Salesforceのカスタムドメイン名の確認方法は4-0.【事前準備】Salesforce Developer Editionのアカウント作成を行う。を参照してください。 ②属性とクレームに関しては今回は設定を行いません。なお、この項目ではMicrosoft Entra IDとSSOさせたいシステムでユーザー名やドメイン名が異なる際にユーザー情報を紐づける設定を行います。 ③SAML証明書の フェデレーションメタデータXMLをダウンロード します。このXMLファイルを用いてSalesforce側の設定を行っていきます。 4-3.【作業3】Microsoft Entra IDテナントへのSSOを構成する。 【作業3】の作業はSalesforceで設定作業を行います。 Salesforceの [設定]-[ID]-[シングルサインオン設定] から編集を行います。 SAMLの有効化にチェックを入れて、保存します。 SAMLシングルサインオン構成の [メタデータファイルから新規作成] を選択し、Microsoft Entra IDから取得したフェデレーションメタデータXMLファイルをアップロードします。 フェデレーションメタデータXMLの情報を基にした構成が作成されます。構成の名前がデフォルトではstsになっているため今回はEntra SSOに変更しています。 [設定]-[会社の設定]-[私のドメイン]の認証設定 を編集し、ユーザー認証時に表示するログインフォームを指定します。既定ではログインフォームとシングルサインオン構成の両方が有効化されています。 今回は既定のまま保存します。 既定の設定のままだと以下のようなログイン画面が作成されます。この画面はMicrosoft 365ポータル以外からSalesforceへログインする際に表示されます。赤枠がログインフォームになっており、青枠にはEntraでSSOするためのURLが埋め込まれています。 設定画面からシングルサインオン構成のみ有効にするとMicrosoft 365ポータル以外からSalesforceへログインしようとした際に、いきなりMicrosoft Entra IDでの認証を求める画面へ遷移します。 4-4.【作業4】Salesforceアプリへのアクセス許可をユーザーに割り当てる。 【作業4】の作業はMicrosoft Entra ID Admin Centerで設定作業を行います。 Salesforce_tanimotoの画面からユーザーとグループを選択し、ユーザーまたはグループの追加を行います。 左の ユーザーとグループ からアクセス権を割り当てたいグループを選択します。グループを選択した際には、該当グループに所属しているユーザーにアクセス権、同じロールが割り当てられます。 今回はAccessPackage_tanimotoという割り当て済みメンバーシップのグループ(俗にいう静的グループ)を追加しています。 同じく左の ロールを選択してください から選択したユーザーとグループに対して、どのようなロールを割り当てるかを選択します。 ここで指定するロールは Salesforce側のプロファイル になっています。今回はChatter Free Userというプロファイルを選択しています。プロファイルの選択はSalesforce Developer Editionの制約にも関係していますので、詳細は5-1.Salesforceのプロファイルについてを参照してください。 エンタープライズアプリケーションへのアクセス権がきちんと割り当たっていれば、Microsoft 365ポータルやマイアプリポータルから確認ができます。以下の画像はM365ポータルの画面になっています。ポータルに表示されなくても、何度かページを更新すると表示されることがあるので注意が必要です。 補足になりますが、SAML連携の証明書は通常有効期限が3年ですので、運用時の注意点として証明書の期限切れ前の更新や監視が必要になります。SAML署名証明書についての詳細は以下の公式ドキュメントをご参照ください。 learn.microsoft.com 4-5.【作業5】Salesforceユーザーの自動プロビジョニングを構成する。 【作業5】の作業はMicrosoft Entra ID Admin CenterとSalesforceの両方で設定作業を行います。 Salesforce_tanimotoの画面からプロビジョニングを選択し、設定を行います。 プロビジョニングを選択すると以下の画面に遷移し、中央の作業を開始から設定を行います。 新しいUI(2025/11~)では以下の画面に遷移します。 上部の[新しい構成]からも設定できますが、今回は左のバーの[プロビジョニング]から設定を行います。 まず[プロビジョニングモード]から 自動 を選択します。 次に[管理者資格情報]の赤枠内を設定していきます。 [テナントのURL]には https://"Salesforceのカスタムドメイン名" を入力します。 [管理者パスワード]、[管理ユーザー名]にはユーザープロビジョニングを行うSalesforceテナントの管理者アカウントとパスワードを入力します。今回は Salesforce Developer Editionのアカウント作成時に作成した管理者アカウントのIDとパスワード を入力します。 最後に[管理者資格情報]の青枠で示す[シークレットトークン]の設定をしていきます。 シークレットトークンを得るためには シークレットトークンのリセット が必要になります。シークレットトークンのリセットはSalesforceで実行します。 Salesforceの管理者でログインし、Salesforceの右上のアイコンから設定を開きます。 [私の個人情報]-[私のセキュリティトークンのリセット] からセキュリティトークンのリセットを行います。 セキュリティトークンのリセットを行うと以下のようなメールが送信されます。メール中のセキュリティトークンを青枠の[シークレットトークン]に入力します。 最後に[テスト接続]を押下し、赤枠、青枠に入力した情報でアカウントプロビジョニングが可能かの検証を行います。 緑のチェックが表示されれば、入力した情報でプロビジョニングが可能な状態です。 [設定]の項目から[割り当てられたユーザーとグループのみを同期する]を選択します。この設定を行うことでエンタープライズアプリケーションのアクセス権が割り当てられたユーザーとグループのみプロビジョニングされます。 [プロビジョニング状態]をオンにし、設定を保存します。 プロビジョニング状態をオンにすることで、以下の画像のようにアクセス権が割り当てられたユーザーと、割り当てられたグループに所属するユーザーが追加されます。 Salesforceの場合、デフォルトで割り当てられているマッピングルールだけでは設定情報が足りないので、5-3.ProfileIdのマッピングエラーについてをご参照ください。 基本的に40分間隔でユーザー同期処理が発生します。ユーザー指定ではなく、動的グループなどを指定している場合は内部処理の関係でユーザー同期タイミングが少し遅れることがあるようです。 Salesforce側でも [設定]-[ユーザー]-[ユーザー] からプロビジョニングされたユーザーを確認することができます。 Microsoft Entra IDの設定でプロビジョニング済のユーザーからアプリケーションのアクセス権を削除した場合、Salesforce側では青枠のように有効のチェックが外れた状態へと更新されます。 5.各種エラーについて 手順作成に当たって対応したエラーをまとめました。 5-1.Salesforceのプロファイルについて Salesforce Developer Editionでは通常ユーザー用のプロファイルを持ったユーザーのプロビジョニングは2名までしかサポートされていません。それ以上のユーザーを作成しようとした際に以下のエラーが出力されます。今回の手順書では通常ユーザー用のプロファイルではないChatter Free Userを指定しているので人数制限はありません。 5-2.Salesforceからのプロファイル同期エラーについて Salesforce側で同じ名前のプロファイルがあるために発生しているエラーです。Microsoft Entra ID側ではSalesforceのプロファイルは一意である必要がありますが、Salesforce Developer Editionではプロファイル削除ができないので回避ができません。 以下の赤枠のプロファイル名が重複してます。 5-3.ProfileIdのマッピングエラーについて ユーザープロビジョニングに必要なProfileIDという属性情報が足りないというエラーになっています。 このエラーを回避するためにデフォルト作成されるマッピングルールにProfileID属性を追加します。 エンタープライズアプリケーションからSalesforceアプリを選択し、 [プロビジョニング]タブの[マッピング] から既定のマッピングルール(Provision Microsoft Entra ID Users)を選択し、設定を行います。 新しいUI(2025/11~)では以下の画面に遷移します。 エンタープライズアプリケーションからSalesforceアプリを選択し、[属性マッピング]から既定のマッピングルール(Provision Microsoft Entra ID Users)を選択し、設定を行います。 画面下部の[新しいマッピングの追加]に情報を入力します。 [属性の編集]より必要な情報を入力します。 まず赤枠部分の[マッピングの種類]についてです。今回はユーザーに割り当てるプロファイルがChatter Free Userのみであるので[マッピングの種類]は 定数 になっています。 実際に導入する際には、Microsoft Entra IDの属性をもとにプロファイルを振り分けることがほとんどだと思います。その場合は[マッピングの種類]で 直接 を選択し、Microsoft Entra IDのユーザー属性値を引数にしてください。また[マッピングの種類]で 式 を選択した場合、関数などを記述して複雑な処理を実行させることが可能になります。式の詳細については以下に公式リファレンスがありますので、参考にしてください。 learn.microsoft.com 次に[対象の属性]ですが、エラーの原因である ProfileID をプルダウンから選択します。 最後に青枠部分についてです。定数の場合は[定数値]にSalesforce側の プロファイルを識別するための文字列 を入力する必要があります。確認方法は後述します。 必要情報の入力ののち[OK]を選択し、設定を反映させます。 Salesforce側のプロファイルを識別するための文字列の確認方法ですが、私は該当のロールのURLで確認を行いました。 Salesforceの[設定]-[ユーザー]-[プロファイル]を選択し、割り当てたいプロファイルを選択します。 上部のURLの末尾からプロファイルを識別するための文字列を確認します。 URLの末尾が、 /page?address=%2F[プロファイルを識別するための文字列] のような構成になっています。 5-4.ユーザープロファイルの更新に関するエラーについて 一度通常ユーザープロファイルを割り当てたユーザープロファイルをChatter Free Userに上書きしようとした際に出るエラーです。   5-5.同名のユーザーに関するエラーについて 別のSalesforceテナントにて同じユーザー名が存在するというエラーになっています。 6.まとめ 本記事では、Microsoft Entra ID と Salesforce を連携させ、SSOとユーザー自動プロビジョニングを構成するための実践的な手順を紹介しました。 特に、SAML構成の設定やプロビジョニング設定における注意点、そして実際に発生したエラーとその回避方法を解説しました。 導入時には、ライセンス要件や適切なロールへの属性マッピングを設定することが重要です。 本記事が、Microsoft Entra IDとSalesforceの連携を検討されている方の参考になれば幸いです。 7.執筆者 谷本 有正 (NTT西日本 ビジネス営業部所属) 現在、教育委員会向けに、Microsoft製品を中心としたゼロトラストセキュリティの提案業務に従事しています。 8.商標 Microsoft製品に関する商標 「Microsoft Entra ID」、「Microsoft 365」は、米国Microsoft Corporationの米国およびその他の国における登録商標または商標です。 Salesforceに関する商標 「Salesforce」は、Salesforce.com, Inc.の米国およびその他の国における登録商標または商標です。
1. はじめに 2025年12月、AIエージェント(Claude Code, Gemini CLI, Cursorなど)によるAIコーディングが当たり前になる中、AWS構築の進め方も大きく変わりつつあります。 従来、AWSの構築、AWSへのアプリケーションのデプロイといえば、マネジメントコンソールを行き来したり、複雑なIaCコードを記述する必要がありました。 ですが、AIエージェントと「AWS API MCP Server」を組み合わせることで、自然言語によってインフラ操作・自動構築ができるようになります。 本記事では、「AWS API MCP Server」を解説し、Claude Codeへの「AWS API MCP Server」追加手順、サンプルプログラムを自然言語でAWSにデプロイする手順をご紹介します。 1. はじめに 2. 対象読者 3. 背景・課題 4. AWS API MCP Server とは? 5. 前提条件・事前準備 6. 実施手順 6.1 uv コマンドのインストール 6.2 AWS API MCP Server のセットアップ 6.3 AWS Documentation MCP Server のセットアップ 6.4 Claude Codeの起動 6.5 AWS API MCP Serverの動作確認 6.6 プログラムをローカルPCで実行する 6.7 このプログラムに適したAWS構成を提案してもらう 6.8 AWSリソース作成と、デプロイ 6.9 ブラウザで動作確認 6.10 後片付け、AWSリソースの削除 7. まとめ 執筆者 商標 免責事項 2. 対象読者 AIエージェントを活用してAWS環境へアプリをデプロイしたい方 AWSデプロイ作業が「難しそう」と感じる方 生成AIを使った最新のAWSデプロイを体験したい方 3. 背景・課題 アプリケーションをローカルPCで動作させた後、そのアプリをクラウドにデプロイする際のインフラ構築はアプリ実装とは異なる技術的ハードルがあります。また、AWSのようなクラウド環境では、多数のサービスの中からどのサービスをどのように連携/設計すべきなのか、初学者には難しく感じるでしょう。それから、個々のAWSサービス毎の固有用語や設定手順が多いだけでなく、書籍やWeb情報の説明通りにAWSへ実装することができず、技術的に躓くことも多いのではないかと思います。加えて、昨今のアプリは多様な技術スタックの連携によって実装されることが多く、同種のAWSサービス(※1)の中からどれを選定すべきなのか、悩まれる方も多いでしょう。 この躓きやすく悩ましいAWSデプロイについて、AIエージェントと「AWS API MCP Server」を連携することで、自然言語によるプロンプト指示で適合するAWSサービス選定しながら、AIエージェントに任せてAWSデプロイを進めることができます。 ※1 例えば、Dockerコンテナをデプロイできるサービスは、以下のように色々あります。 AWS App Runner Amazon ECS Amazon EKS Amazon Lightsail Amazon EC2 AWS Lambda AWS Batch AWS Elastic Beanstalk 4. AWS API MCP Server とは? AWS API MCP Serverは、AIエージェントとAWSサービスの間の橋渡しとして機能し、AWSリソースの作成、更新、管理を行うことができます。AIエージェントに自然言語のプロンプト指示を与えることで、AWS CLIコマンドに変換・実行されます。 AWS API MCP Server | AWS MCP Servers 例えば、「S3のリストを表示」と指示することで、AWS CLIの「aws s3 ls」コマンドに変換・実行され、S3バケット一覧が表示されます。 このとき、従来のブラウザによるAWSマネジメントコンソール操作は必要はありません。 AWS API MCP Serverの活用例 ソースコードの中身を解析した上でのAWSアーキテクチャーの提案、インフラ構築とデプロイ【本記事の記載内容】 S3のバケット一覧表示 EC2/Lambda/RDS等、AWSリソースの構築 AWSリソースの問題調査 障害発生リソースの再起動・復旧 システムの稼働状況(ヘルスチェック)の確認 バックアップ(スナップショット)の取得 アプリケーションの動作不良からインフラのログ調査と原因分析 インフラ設定とソースコードを横断したセキュリティ診断 アプリの負荷傾向に基づいたコスト分析とリソース最適化 これらのように、アプリ開発、アプリ構築、リリース後の運用/保守のあらゆる場面で、アプリケーション作業とインフラ作業を一気通貫で自律的に実行させることができます。 5. 前提条件・事前準備 本記事でご紹介する手順を行うために必要となる、動作環境の前提条件は次のとおりです。 OS macOS IDE Visual Studio Code AIエージェント Claude Code ※最新バージョンを推奨します AIモデル Opus 4.5 AWSアカウントの準備 ※本記事の手順を実施すると、AWS App Runner等の利用料金が発生します。 IAMユーザーのアクセスキーの準備 AWS CLIのインストールと認証設定 ターミナルで aws configure 等でアクセスキーを設定します。 awsコマンドが実行可能であること コマンド例 aws s3 ls Docker Desktop または Rancher Desktop などのDocker動作環境 Visual Studio Codeで作業フォルダを開いておきます 6. 実施手順 6.1 uv コマンドのインストール AWS API MCP Server を実行するために必要な uv というパッケージマネージャーをインストールします。 ターミナルで以下を実行します。 curl -LsSf https://astral.sh/uv/install.sh | sh インストールが完了したら、変更を反映させるためにターミナルを再起動するか、画面の指示に従ってパスを通してください(通常は自動で設定されますが、反映されない場合はターミナルを一度閉じて開き直すのが確実です)。 6.2 AWS API MCP Server のセットアップ Claude Code に AWS API MCP Server を追加するため、ターミナルで以下を実行します。 claude mcp add -s project \ -e AWS_REGION=ap-northeast-1 \ -e AWS_API_MCP_PROFILE_NAME=default \ -- awslabs-aws-api-mcp-server \ uvx awslabs.aws-api-mcp-server@latest コマンドの解説 コマンド/オプション 説明 claude mcp add Claude Code に新しい MCP サーバーを追加するコマンドです。 -s project プロジェクトスコープで設定を追加します(現在のプロジェクト設定ファイル .mcp.json などに保存されます)。 -e AWS_REGION=ap-northeast-1 AWSのリージョンを東京(ap-northeast-1)に指定しています。 -e AWS_API_MCP_PROFILE_NAME=default お使いのPCに設定されているAWSプロファイル( ~/.aws/credentials )の default を使用します。 default 以外のプロファイルを指定する場合は、適宜変更してください。 awslabs-aws-api-mcp-server サーバーに付ける名前です。 uvx awslabs.aws-api-mcp-server@latest uv ツールを使って、最新版の当該プログラムをダウンロード・実行します。 6.3 AWS Documentation MCP Server のセットアップ AWS API MCP Serverを利用する際、AWS Documentation MCP Server を併用することでAIエージェントの実行精度が向上します。 ターミナルで以下を実行します。 claude mcp add -s project \ -- awslabs-aws-documentation-mcp-server \ uvx awslabs.aws-documentation-mcp-server@latest コマンドの解説 コマンド/オプション 説明 claude mcp add Claude Code に新しい MCP サーバーを追加するコマンドです。 -s project プロジェクトスコープで設定を追加します(現在のプロジェクト設定ファイル .mcp.json などに保存されます)。 awslabs-aws-documentation-mcp-server サーバーに付ける名前です。 uvx awslabs.aws-documentation-mcp-server@latest uv ツールを使って、最新版の当該プログラムをダウンロード・実行します。 6.4 Claude Codeの起動 claude 新しくMCP Serverを追加すると下記画面が表示される場合があります、Enter を押下してください。 6.5 AWS API MCP Serverの動作確認 スラッシュコマンド /mcp を実行し、下記画面のように awslabs-aws-api-mcp-server と awslabs-aws-documentation-mcp-server が、 connected となっていることを確認してください。 /mcp 動作確認として、 S3の一覧を表示 という簡単なプロンプトを入力してみましょう。 S3バケット一覧が表示されれば Claude Code から AWS API MCP Server が連携され、自然言語が aws s3 ls コマンドに変換・実行されていることがわかります。 S3の一覧を表示 ※Claude Codeの画面上に Do you want to proceed? と表示された場合、適宜、表示内容を確認して Yes or No を選択してください。 ※ご利用のAWSアカウント内でS3バケット未作成の場合は表示されません。このような場合はEC2やVPCなど、存在するAWSリソースの一覧表示をお試しください。 6.6 プログラムをローカルPCで実行する 今回は FastAPI を使用したシンプルな、おみくじアプリを作成します。 デプロイするアプリケーションで使用するソースコードを以下に用意しました。 下記の2つのファイル( fortunes.py と Dockerfile )を、作業フォルダに作成してください。 fortunes.py from fastapi import FastAPI from fastapi.responses import HTMLResponse import random app = FastAPI() @ app.get ( "/" , response_class=HTMLResponse) async def read_root (): fortunes = [ "大吉" , "中吉" , "小吉" , "吉" , "末吉" , "凶" ] lucky_items = [ "金の腕時計" , "新しい文房具" , "ミントタブレット" , "文庫本" , "温かいお茶" , "折りたたみ傘" , "お気に入りの音楽" , "散歩" , "古い写真" , "甘いお菓子" , "スマートフォン" , "ハンカチ" , "スニーカー" , "観葉植物" , "マグカップ" ] selected = random.choice(fortunes) item = random.choice(lucky_items) html_content = f """ <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>おみくじ</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Shippori+Mincho:wght@400;600&display=swap" rel="stylesheet"> <style> :root {{ --sky-light: #e8f4fc; --sky-mid: #7ec8ed; --sky-accent: #4ab3e6; --blue-deep: #2196c9; --text-primary: #1a4a5e; --text-muted: #5a8fa8; }} * {{ margin: 0; padding: 0; box-sizing: border-box; }} body {{ background: var(--sky-light); background-image: radial-gradient(ellipse at 30% 0%, rgba(126, 200, 237, 0.5) 0%, transparent 50%), radial-gradient(ellipse at 70% 100%, rgba(184, 223, 245, 0.6) 0%, transparent 50%), linear-gradient(180deg, #f0f9ff 0%, var(--sky-light) 50%, #d4edfc 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; font-family: "Shippori Mincho", "Hiragino Mincho ProN", "Yu Mincho", serif; color: var(--text-primary); overflow: hidden; }} body::before {{ content: ""; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); opacity: 0.02; pointer-events: none; }} @keyframes fadeInUp {{ from {{ opacity: 0; transform: translateY(30px); }} to {{ opacity: 1; transform: translateY(0); }} }} .card {{ position: relative; text-align: center; padding: 4rem 3.5rem; background: linear-gradient(145deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 252, 255, 0.9) 100%); border: 1px solid rgba(74, 179, 230, 0.2); border-radius: 16px; min-width: 320px; backdrop-filter: blur(20px); box-shadow: 0 20px 60px rgba(33, 150, 201, 0.15), 0 8px 25px rgba(74, 179, 230, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8); animation: fadeInUp 0.8s ease-out; }} .card::before {{ content: ""; position: absolute; top: 0; left: 30%; right: 30%; height: 3px; background: var(--sky-accent); border-radius: 0 0 3px 3px; }} .title {{ font-size: 0.8rem; letter-spacing: 0.4em; color: var(--text-muted); margin-bottom: 2.5rem; text-transform: uppercase; animation: fadeInUp 0.8s ease-out 0.1s backwards; }} .result {{ font-size: 5.5rem; font-weight: 600; margin-bottom: 2.5rem; line-height: 1; color: var(--text-primary); animation: fadeInUp 0.8s ease-out 0.2s backwards; }} .divider {{ width: 50px; height: 1px; background: var(--sky-mid); margin: 0 auto 2rem; animation: fadeInUp 0.8s ease-out 0.3s backwards; }} .label {{ font-size: 0.7rem; letter-spacing: 0.25em; color: var(--text-muted); margin-bottom: 0.75rem; animation: fadeInUp 0.8s ease-out 0.4s backwards; }} .item {{ font-size: 1.2rem; margin-bottom: 3rem; color: var(--blue-deep); animation: fadeInUp 0.8s ease-out 0.5s backwards; }} .btn {{ display: inline-block; padding: 0.875rem 2rem; background: var(--sky-accent); color: #fff; font-family: inherit; font-size: 0.8rem; letter-spacing: 0.1em; text-decoration: none; border: none; border-radius: 4px; transition: background 0.2s ease; animation: fadeInUp 0.8s ease-out 0.6s backwards; }} .btn:hover {{ background: var(--blue-deep); }} </style> </head> <body> <div class="card"> <div class="title">今日の運勢</div> <div class="result">{selected}</div> <div class="divider"></div> <div class="label">ラッキーアイテム</div> <div class="item">{item}</div> <a href="/" class="btn">もう一度引く</a> </div> </body> </html> """ return HTMLResponse(content=html_content) Dockerfile FROM python:3.9-slim WORKDIR /app RUN pip install --no-cache-dir fastapi uvicorn COPY fortunes.py /app/fortunes.py CMD [ " uvicorn ", " fortunes:app ", " --host ", " 0.0.0.0 ", " --port ", " 8000 " ] ローカルでの実行手順 Docker環境が起動していることを確認し、以下のコマンドをターミナル上で実行してコンテナをビルド・起動します。 1. Dockerイメージのビルド docker build -t fortunes-app . 2. コンテナの起動 docker run -p 8080:8000 fortunes-app ※ ホスト側のポート8080を、コンテナ側のポート8000(FastAPIのデフォルト)に転送しています。 3. 動作確認 ブラウザで http://localhost:8080 にアクセスしてください。 「今日の運勢」が表示されれば成功です。 4. コンテナの停止 確認ができたら、ターミナルで Ctrl + C を押してサーバーを停止してください。 6.7 このプログラムに適したAWS構成を提案してもらう AIエージェントにAWS構成を提案してもらいましょう。 作業フォルダの fortunes.py や Dockerfile などのコードベースをClaude Codeに把握させることで、精度を高めて出力させることができます。 コードベースを把握させるためには、Claude Code上で /init コマンドを実行します /init 「Do you want to proceed? (実行しますか?)」と確認を求められるので、内容を確認して実行を許可(Enter または Yes)してください。 このFastAPIアプリをAWSで公開したい。 初心者におすすめ、コード変更無し、オートスケール、 管理が楽、コストが安い構成を提案して。 まだ実装しないでください。 このプロンプトを実行すると、おそらく「AWS App Runner」が提案されると思います。 ※ 本記事のこの後の説明を統一するため、意図して「AWS App Runner」が提案されるプロンプトとしています。 ※ 時間経過による新規AWSサービスのリリース、既存サービスの改善など、提案内容が変わる可能性があります。 私が本プロンプトを実行した結果、AWS App Runnerがおすすめされ、簡易的な構成図、概算コスト、他の選択肢との比較表が出力されました。 6.8 AWSリソース作成と、デプロイ 提案された構成(AWS App Runner)で実際にデプロイを行います。 通常、App Runnerへのデプロイを行うには、以下の手順が必要です。 ECRリポジトリの作成 Dockerイメージのビルド AWSへのログイン(ECRログイン) イメージのプッシュ App Runnerサービスの作成(IAMロール設定など含む) これらを人間が手動で行うと手数が多く大変ですが、AWS API MCP Serverを使えば、自然言語で指示するだけでClaude Codeが実行してくれます。 以下のプロンプトを入力してください。 留意点: デプロイを進めることでAWSへの課金が加算されます。 本記事の構成(1 vCPU、2 GBメモリ)における App Runner の利用料金目安(東京リージョン)は、1日(24時間)稼働させた場合、待機時間が多い場合で約0.43 USD(約65円)、常にリクエストを処理し続けた場合で約2.37 USD(約356円)程度です(2025年12月現在)。 提案してくれた App Runner の構成でデプロイを進めてください。 ECRのリポジトリ名は `fortunes-app`、App Runnerのサービス名は `fortunes-service` とします。 Claude Code は必要な手順を計画し、順次コマンドを実行していきます。 途中で「Do you want to proceed? (実行しますか?)」と確認を求められるので、内容を確認して実行を許可(Enter または Yes)してください。 実行される主な処理の流れ: ECRリポジトリの作成 : aws ecr create-repository などが実行されます。 Dockerビルドとプッシュ : ローカルでビルドしたイメージがECRにアップロードされます。 App Runnerサービスの作成 : アップロードされたイメージを使ってサービスが起動します。 ※デプロイ完了までには数分〜10分程度かかります。 処理が完了すると、Claude Code が「デプロイが完了しました」といったメッセージと共に、アプリケーションのURL( https://xxxxxxxx.ap-northeast-1.awsapprunner.com のような形式)を表示してくれます。 AWSマネジメントコンソールで確認: マネジメントコンソールからも作成されたリソースを確認できます。 App Runnerの管理画面を開くと、 fortunes-service というサービスが作成され、ステータスが Running になっていることがわかります。 6.9 ブラウザで動作確認 発行されたApp RunnerのドメインURL( https://...awsapprunner.com )に、ブラウザからアクセスしてみましょう。 ローカル実行時と同様に、「今日の運勢」画面が表示されれば、AWSへのデプロイ成功です! 6.10 後片付け、AWSリソースの削除 最後に、不要な課金が継続しないよう、作成したAWSリソースを削除して後片付けを行います。 これも自然言語で指示するだけで、Claude Codeが関連リソースを特定して削除してくれます。 作成したApp Runnerサービス、IAMロール、ECRリポジトリを削除してください。 ここでも「Do you want to proceed?」と確認を求められるので、削除対象のリソース(App Runnerサービス fortunes-service と ECRリポジトリ fortunes-app )が含まれていることをよく確認してから、実行を許可してください。 これでAWSリソースが削除されました。 7. まとめ 本記事では、AIエージェントに「AWS API MCP Server」を連携させることで、アプリやAWSの専門知識がなくても、自然言語による指示でWebアプリケーションをAWS環境にデプロイできることを解説しました。 AWS API MCP Serverを活用するメリット: 学習コストの削減 : AWSの専門用語やマネジメントコンソール操作が難しいと感じる方でも、自然言語でAWSを操ることができます。 迅速な構築とデプロイ : 対話形式でスムーズにAWS構成の提案をもらい、インフラの構築、デプロイまで進められます。 運用の効率化 : リソースの確認や削除などの運用タスクも、プロンプト入力で実施できます。 2025年12月、AIと対話しながらインフラを作り上げるスタイルは、今後の開発におけるスタンダードになっていくでしょう。AIによる技術革新を味方につけ、よりクリエイティブで価値あるアプリ開発を行うため、まずはこの一歩から始めてみてはいかがでしょうか。 執筆者 三島 映人 NTT西日本グループのサービス開発におけるテックリードやプロジェクトマネジメント、アプリケーション開発組織の技術統括、エンジニア育成を行っています。 MBA、PMI認定PMP等を保有。趣味は自作キーボードの設計。 商標 Amazon Web Services、AWS、App Runner、およびその他のAWSサービス名は、米国およびその他の諸国における Amazon.com, Inc. またはその関連会社の商標です。 Claude、Claude Code は、Anthropic PBC の商標です。 Docker は、Docker, Inc. の商標または登録商標です。 Python は、Python Software Foundation の登録商標です。 Visual Studio Code は、Microsoft Corporation の商標または登録商標です。 macOS は、Apple Inc. の商標です。 その他、記載されている会社名、製品名は、各社の登録商標または商標です。 免責事項 本記事の内容は執筆時点(2025年12月)の情報に基づいており、サービスの仕様変更等により内容が変更される可能性があります。 本記事で紹介する設定手順や使用方法は、特定の環境での動作確認に基づくものであり、すべての環境での動作を保証するものではありません。 AWS API MCP Server および AWS Documentation MCP Server は AWS Labs によって提供されるオープンソースプロジェクトであり、AWSの公式サポート対象外です。 本記事では生成AIを活用した内容が含まれており、AIによる情報の生成過程でハルシネーション(事実に基づかない情報の生成)が発生する可能性があります。記載内容の正確性については、必ず公式ドキュメントや信頼できる情報源で検証してください。
はじめに NTTビジネスソリューションズ の 迫です。 Web アプリケーション開発はスピードと品質が強く求められるようになり、テスト工程にかかる負担は増大しています。 本記事では、 GitHub Copilot と Playwright を組み合わせて、アプリケーションテストの効率を高める方法について解説します。 AI によるテスト項目の自動生成と、E2E テストの自動化がどのように生産性向上に寄与するのか、その有効性と期待できる成果をまとめています。 用語補足 GitHub Copilot GitHub が提供する AI コード補完ツールです。 自然言語による指示からコードの生成、テストケースの作成支援まで幅広く活用できます。 Playwright Microsoft が開発した E2E(End-to-End)テスト自動化フレームワークです。 複数ブラウザに対応し、UI の動作を高速かつ正確に自動で検証できます。 自動でテストシナリオを作成と実行まで行ってくれます。 対象読者 本記事は以下の方々を対象としています。 テスト工程の効率化に興味のある開発者 GitHub Copilot や Playwright の利用方法・活用メリットを知りたい方 背景と課題について 従来のテスト工程では、次のような課題が一般的です。 テスト項目はテストシナリオ・パターンなど様々なケースを想定して作成する事が必要になり、下記のような要因によって開発工程全体に対して大きい比率を占めるものとなっていると思います。 テスト工程が増大する要因 テスト項目の作成に時間がかかる(特にテスト項目を作成にあたっての設計書・ソースファイルの確認に時間がかかる) テストコードの記述量が多く、メンテナンスが大変 人手による確認・レビューが多く、漏れが発生しやすい 開発スピードにテスト工程が追いつかない これらの課題に対し、 AI と自動化ツールを組み合わせたテスト効率化 が注目されています。 GitHub Copilot によるテストコード生成と Playwright による自動テストは、品質と生産性の両立を実現してくれます。 事前の準備(GitHub Copilot を使ってサンプルのプロジェクトを作成) 本ブログでの解説用にテスト対象となる簡易的なアプリケーションを準備します。 GitHub Copilot に対し「簡単な Web アプリケーションを作成して」といったプロンプトを与えるだけで、コードが自動生成されます。今回は3択クイズを出してくれるアプリを用意しました。このモックアプリはログインしクイズに挑戦できる簡単なアプリケーションになっています。GitHub Copilotを使用する際のメリットを下記に列挙します。 GitHub Copilotを使用する際のメリット GitHub Copilot は自然言語の説明から UI や API のサンプルコードを生成可能 早期にテスト対象を準備し、テスト作成を並行できる モックアプリを短時間で用意できるため、教育・検証用途にも有効 サンプルのプロジェクト 作業の前提条件 以降の作業操作は、Visual Studio Code(以下、VS Code)を利用しています。 GitHub Copilot は、Agentモードを使用しています。 1.テスト環境の準備(Playwright のインストール) まずはテスト環境の準備を行います。 Playwright のインストールを実施します。今回本ブログで使用するPlaywrightの特徴を下記に記載いたします。 Playwright の特徴 ブラウザソフト:Chromium / Firefox / WebKit をサポート 画面操作の自動化が容易(クリック、入力、遷移など) スクリーンショット・動画・ログ取得が可能 CI/CD(GitHub Actions など)と連携しやすい インストール手順(playwright) npm install npx playwright install Playwrightインストール 2.テスト項目の作成 - 正常系テスト(GitHub Copilot による自動生成) 正常な操作手順を想定したテスト項目を作成します(3択クイズ画面を操作するイメージ)。 GitHub Copilot はテスト実施したい内容(ユーザーの操作など)を仕様や動作イメージを伝えるだけで、Playwright のテストコードを作成してくれます。期待できる効果と具体的な指示の例は下記です。 GitHub Copilot による自動生成で期待できる効果 テストケースのひな型作成が高速化 コーディングミスの防止 テストの観点漏れを減らせる 複数テストの量産が容易になる GitHub Copilotへの指示例 テストを行いたい操作をフォーム画面のオブジェクト名(ログインボタンなど)を指定して日本語で指示できます。 運用マニュアルのイメージでテスト項目が作れるので作成漏れの対策にもなります。 テスト項目作成のプロンプト 作成されたテスト項目TypeScript(.ts)ファイル 正常系テストのテストコードTypeScript(.ts)ファイル 作成された TypeScript(.ts)形式のテストコードは E2E テストを自動化するためのシンプルなブラウザ操作スクリプトであり、内容は自由に修正・追記できるため、自動生成後もテストを進めながら追加のテストロジックを柔軟に拡張できます。 例えばボタンクリックして戻りを3秒待つなど簡単な構文で追加や修正は可能です。 await page.click('.choice-btn[data-answer="1"]'); await page.waitForSelector('.result-message', { timeout: 3000 }); 3.テストの実行 生成したテストは Playwright のコマンドで実行できます。 npx playwright test TypeScript(.ts)[ファイルを指定] [playwright のパラメータを措定] ターミナル実行例 結果検証 Playwright はテストの成功・失敗だけでなく、結果の詳細レポートを生成します。具体的には下記の項目を詳細レポートの中で確認が可能になります。生成されるのはHTMLレポートのレポートで、とても見やすく、正常/異常をすぐに確認できます。 詳細レポートの項目 スクリーンショット 実行ログ ステップごとの処理状況 エラー発生時のトレース テストレポート(個別結果) テストでエラーが発生すると「Errors」に不具合箇所が表示され、テスト結果とソースファイルを同時に確認できるため効率的で、修正後もそのまま再テストできることから単体テストから結合テストまでの時間短縮に役立ちます。 4.テスト項目の作成 - アプリケーションの網羅性テストを実施したい場合(GitHub Copilot による自動生成) 簡単な正常系だけでなく、アプリケーション開発ではロジックの網羅性も確認の観点の一つです。本来ステートメントの分岐を網羅するには相当な数の時間と労力が必要です。そこでプロンプトを工夫して以下の内容で指示をします。 プロンプト(網羅テスト) 機能単位にテストケースの洗い出しとスクリプトが作成されます。 この作業だけでも人間が作業実施した場合と比較すると大幅な効率化に繋がります。 作成されたテストパターン 内容を検証してみます。 ログイン機能の場合、「通常のログインID時:IDが空白:IDが最大文字」 など正しい条件で作成されているのが確認できます。 5.テスト項目の作成 - セキュリティテストを実施したい場合(GitHub Copilot による自動生成) 上記の正常系テストだけでなく、セキュリティ観点のテスト項目も GitHub Copilotに生成させることができます。セキュリティテストはテスト観点の洗い出しも大変ですが、クロスサイトスクリプティング(XSS)など自分でコーディングしたりパターンを洗い出すだけでも相当な工数がかかります。今回はプロンプトを工夫して以下の内容で指示をします。 セキュリティテスト テスト項目(セキュリティテスト) Open Web Application Security Project(OWASP)に記載されている上位の脆弱性についてテスト項目 を作成依頼しました。TypeScript(.ts)ファイルの中には数百ラインのテストコードが出来上がりました。 参考)OWASPとは Webアプリケーションのセキュリティ向上を目的とした、非営利・オープンコミュニティの国際的プロジェクトです。 6.テスト実行(結果) セキュリティテストの結果です。いくつかのテストでセキュリティに関する修正のアドバイスが出力されました。 テスト結果(スクリーンショット) 上記の画面(上部)にXSSの証跡が確認できます。テストは様々なパターンで実行されます。TypeScript(.ts)ファイルの中に以下の構文をいれると該当コードの箇所でスクリーンショット(エビデンス)を自動保存してくれます。 スクリーンショット await page.screenshot({ path: 'screenshot.png' }); 結果検証 不正な入力(XSS)が適切にサニタイズされているか、認証が必要なページへ不正アクセスできないか、そして想定外のUI表示が発生していないかを確認します。 Playwright のログ・スクリーンショットにより、問題の再現性と解析精度が向上します。 テスト結果(一覧) テスト結果(個別) 期待できる効果 セキュリティテストの観点を自動的に補完することで、人手では見落としやすい脆弱性を早期に発見でき、結果として全体的な品質向上にもつながります。 まとめ GitHub Copilot と Playwright を組み合わせることで、従来と同程度以上の品質を担保しつつテスト工程を自動生成ができ、開発の効率化を大幅に向上できます。具体的なメリットとしては下記を挙げることができます。 テスト工程自動化のメリット テスト項目作成の高速化 テストコード品質の向上 自動実行・検証による作業負荷の削減 開発スピードと品質の両立 AI と自動テストの活用は今後ますます重要になると考えられます。 同時に開発者自身が Playwright の基礎を理解していないと、 生成されたコードの意味がわからないまま運用するリスクもあるので自身のスキルアップもすすめながら開発ツールの利活用を行う事が大切になると思います。 執筆者 迫 正宏(NTTビジネスソリューションズ(株) バリューデザイン部 システム開発部門) 第一種情報処理技術者、第二種電気工事士、AWS Certified Solutions Architect - Associateなど 商標 GitHub および GitHub Copilot は、GitHub, Inc. の登録商標または商標です。 Microsoft および Playwright は、Microsoft Corporation の登録商標または商標です。 Firefox は、Mozilla Foundation の登録商標です。 Chromium は、Google LLC の商標です。 WebKit は、Apple Inc. の商標です。 Java は、Oracle および/またはその関連会社の登録商標です。 ECMAScript は、ECMA International によって標準化された仕様です。 JavaScript は、商標ではなく、ECMAScript をベースとしたプログラミング言語の名称です。 Visual Studio Code は、米国 Microsoft Corporation の商標または登録商標です。 タイトル画像:ImageFXで生成。Googleが提供する高性能画像生成AIです。(※ImageFX、GoogleはGoogle LLCの商標または登録商標です) その他記載されている会社名、製品名は、それぞれの所有者の登録商標または商標です。
こんにちは。NTT西日本の谷です。 近年、データドリブンな意思決定の重要性が高まる中、「どのようにデータを整形し、分析できる状態にするか」が課題となっています。今回は、モダンなデータ分析基盤の構築手法として注目されている dbt(data build tool) を使い、日本観光振興会が公開している観光来訪者数のオープンデータを整形・可視化する方法をご紹介します。 この記事で得られること 本記事は ハンズオン形式 で、実際に手を動かしながらパイプライン構築を体験できる内容になっています。 ✅ 実践的なdbtスキル : dbt Projects on Snowflakeを使った実データ加工のノウハウ ✅ 分析用データモデリングの知見 : スタースキーマによるデータ設計手法の理解と適応 ✅ Tableauダッシュボードの構築スキル : 観光データを可視化したTableauダッシュボード ✅ データ品質を担保するテスト実装力 : dbt testを活用した、データ品質保証(QA)の組み込み方法 想定読者 データエンジニア・データアナリスト志望の方 SQLは書けるが、dbtは初めての方 モダンデータスタックに興味がある方 実際のデータパイプラインを構築してみたい方 所要時間 全体 : 約2〜3時間 環境構築 : 30分 データ準備とアップロード : 20分 dbtプロジェクトの実装 : 60〜90分 データ品質テストの実装 : 10分 Tableauでの可視化 : 30分 前提条件 SQLの基本的な知識(SELECT、JOIN、GROUP BYなど) Webブラウザ(Snowflakeはブラウザ上で完結します) データのダウンロード先(日本観光振興会サイトからCSVをダウンロード) データ可視化に利用しているTableau Desktopはトライアル版を利用しています(Tableau Public Desktop EditionではSnowflakeとの接続はできません) 💡 ヒント : dbt Projects on Snowflakeを初めて使う方向けに、各ステップに「確認ポイント」を設けているので、進捗を確認しながら進められます。 この記事で得られること 想定読者 所要時間 前提条件 1. 使用する技術の概要 dbt(data build tool)とは データモデリング時の課題とdbtによる解決 Snowflakeとは Tableauとは 2. 全体アーキテクチャ データモデルの設計思想 3. 環境構築 3.1 Snowflakeアカウントの準備 3.2 Snowflake上でデータベースとスキーマを作成 3.3 dbt Projects on Snowflakeの有効化 3.3.1 dbt Projects on Snowflakeのセットアップ手順 3.3.2 dbt_project.yml設定 4. データの準備とアップロード 4.1 観光データのダウンロード 4.2 Snowflakeへのデータアップロード 4.2.1 dbt Projects on Snowflakeでのseedアップロード 4.2.2 dbtコマンドを実行してseedをロード 5. dbtプロジェクトの実装 5.1 プロジェクト構成 5.2 マクロの実装:複数テーブルの統合 5.3 ステージング層の実装 5.4 マート層の実装 5.4.1 ディメンションテーブル1:日付マスター 5.4.2 ディメンションテーブル2:地域マスター 5.4.3 ファクトテーブル:観光来訪者数 5.5 スキーマ定義(schema.yml) 5.6 モデルの実行 6. データ品質テストの実装 6.1 カスタムテストマクロ 6.2 テストの実行 7. Tableauでの可視化 7.1 Tableauのインストール 7.2 Snowflakeへの接続 7.3 データソースの設定 7.4 観光データを利用したワークシート(グラフなど)の作成 7.4.1 例1:都道府県別観光者数 7.4.2 例2:都道府県別ヒートマップ 7.4.3 例3:地図表示 7.5 ダッシュボードの作成 8. まとめ 実現できたこと 今後の改善案 参考リンク 執筆者 商標について 免責事項 1. 使用する技術の概要 今回構築するデータ基盤では、公益社団法人日本観光振興協会が公開している デジタル観光統計オープンデータ を使用し、下記の技術スタックで分析基盤を構築します。 データ加工 :dbt データ蓄積 :Snowflake データ可視化 :Tableau dbt(data build tool)とは dbtは、データウェアハウス内のデータ加工に特化したツールです。従来のETLツールと異なり、 ELT(Extract, Load, Transform) のTに焦点を当てています。 データ分析をする際に欲しいデータを作るために、SQLを使いデータマートを作ることも多いと思いますが、データ加工に関する領域は作った人にしかわからない属人化がしやすい領域となりがちです。そのような課題の解決やその他にもチームや組織でデータを管理する人にとって便利な機能が多数搭載されていることから、近年注目されています。 データモデリング時の課題とdbtによる解決 課題 従来の問題点 dbtによる解決策 処理内容の不透明性 最終的なデータマートを作成するまでにどのような処理を行ったのかわかりにくい。複雑なSQLが何層にも重なり、全体像が見えない。 データリネージの自動生成 :モデル間の依存関係を視覚的に表示。どのテーブルがどのテーブルから作られているか一目で把握できる。 仕様変更への対応困難 データソースの中身が変わるなど仕様変更が発生した際に、影響範囲の特定と修正が大変。どこを修正すればいいのか分からない。 モジュール化とref()関数 :テーブル参照を名前で管理。上流のテーブル名を変更しても、下流のSQLを自動で追従。依存関係も自動解決。 属人化とナレッジの分散 人によって参照しているDBやテーブル、データ加工のロジックが異なる。担当者が変わるとメンテナンス不可能に。 Gitによるバージョン管理 :すべてのSQLをコードとして管理。変更履歴を追跡でき、チーム全体で共有可能。 データ品質の担保 データの異常値や欠損があっても気づけない。分析結果の信頼性が低下。 テスト機能 :not_null、unique、範囲チェックなどを自動実行。データ品質を継続的に保証。 ドキュメントの陳腐化 手動で書いたドキュメントが実装と乖離。「ドキュメントが古くて信用できない」状態に。 自動ドキュメント生成 :SQLから自動的にドキュメントを生成。常に最新の状態を維持。 Snowflakeとは クラウドネイティブなデータウェアハウスサービスです。コンピュートとストレージが分離されており、必要に応じてリソースをスケールできます。 最近Snowflake上でdbtを利用できる「dbt Projects on Snowflake」がGAとなったため、今回はこの機能を利用します。 Tableauとは BIツールの一つで、視覚的にデータ分析・可視化ができるツールです。直感的な操作性と様々なデータソースとの接続を簡単に行うことができます。今回はTableau Desktop(有償版)を使っていますが、Snowflakeに接続可能な他のBIツールに置き換えることも可能です。 2. 全体アーキテクチャ 今回構築するデータ基盤の全体像は以下のとおりです。今回は公益社団法人日本観光振興協会が公開している デジタル観光統計オープンデータ を使用します。 データ処理の流れは下記の通りです。 [日本観光振興会オープンデータ (CSV)] ↓ [Snowflake (データレイク層)] ↓ [dbt変換処理] ↓ ┌─────────────┐ │ ステージング層 │ ← データクレンジング、基本的な型変換 └─────────────┘ ↓ ┌─────────────┐ │ マート層 │ ← ディメンション・ファクトテーブル └─────────────┘ ↓ [Tableau] ← ダッシュボード・可視化 データモデルの設計思想 今回は「集計クエリのパフォーマンス向上」「BIツールでの分析が容易」「データの重複を削減」という観点から、 スタースキーマ を採用しました。スタースキーマは、中心となるファクトテーブル(トランザクションデータ)と、それを取り巻くディメンションテーブル(マスターデータ)で構成されます。それぞれのテーブルは下記のとおりです。 ※今回dim_datesで作成するディメンションをBIで利用しませんが、データの系統(データリネージ)を確認するために作成しています。 ディメンションテーブル(マスターデータ) dim_dates :日付マスター(年月情報) dim_regions :地域マスター(都道府県、市区町村、地方ブロック情報) ファクトテーブル(トランザクションデータ) fact_tourism_monthly :月次観光来訪者数 3. 環境構築 ⏱️ 所要時間 : 約30分 🎯 このセクションのゴール : - Snowflakeアカウントを作成し、ログインできる状態にする - データベース、ウェアハウス、スキーマを作成する - dbt Projects on Snowflakeプロジェクトを立ち上げる 3.1 Snowflakeアカウントの準備 まず初めにSnowflake環境を準備するため、30日間のトライアルに申し込みます。 今回はAWSの東京リージョン&Enterpriseエディションを選択しました。 手順 : Snowflakeの公式サイト からトライアルアカウントを作成 アカウント作成後、以下の情報をメモ(後で使用します) アカウント識別子(例: abc12345.ap-northeast-1.aws ) ユーザー名 パスワード 📝 重要 : アカウント識別子は後でTableau接続時に必要になるため、必ずメモしておきましょう。 ⚠️ Snowflakeトライアルの留意点 : - 期間 : 30日間の無料トライアル - クレジットカード : 登録時にクレジットカード情報は 不要 です - 無料クレジット : $400分の無料クレジットが付与されます - 自動課金なし : トライアル終了後、自動的に有料プランに移行することはありません(クレジットカードなどの支払い設定を行っている場合は課金されるためご注意ください) - リージョン選択 : 日本からのアクセスの場合、AWS東京リージョン(ap-northeast-1)を推奨 - エディション : Standardエディションでも実施可能ですが、一部機能制限がある場合があるため Enterpriseエディション以上 が推奨です - コスト管理 : ウェアハウスを使い終わったら停止する習慣をつけると、無料クレジットを節約できます ✅ 確認ポイント : [ ] Snowflakeのログイン画面にアクセスできる [ ] ユーザー名とパスワードでログインできる [ ] Snowflakeのホーム画面が表示される 3.2 Snowflake上でデータベースとスキーマを作成 SnowflakeのWebコンソールにログインし、ワークスペースを利用してSQLを実行します。 手順 : 左側メニューから「プロジェクト」→「ワークスペース」を選択 「+ 新規作成」→「SQLワークシート」をクリック プロジェクトからワークスペースを選択 新規作成からSQLファイルを選ぶ 以下のSQLをコピーしてワークシートに貼り付け ⚠️ 注意 : 今回は検証用のため ACCOUNTADMIN ロールを利用していますが、実運用時は適切な権限を持つ専用ロールの設定を行う必要があります。 💡 ヒント : AUTO_SUSPEND = 60 を設定することで、60秒間未使用の場合にウェアハウスが自動停止し、コストを削減できます。小規模な開発では X-SMALL で十分です。 -- 設定にAccountAdminのロールを利用 use role accountadmin; -- データベース作成 CREATE DATABASE tourism_analytics; -- ウェアハウス作成(dbtで使用) CREATE WAREHOUSE dbt_wh WITH WAREHOUSE_SIZE = ' X-SMALL ' AUTO_SUSPEND = 60 AUTO_RESUME = TRUE ; -- スキーマ作成 USE DATABASE tourism_analytics; CREATE SCHEMA DATA; ワークシート上部の青色三角ボタン横の「▽」から「すべて実行」を選択 SQLをワークシートにコピー&ペースト 青色三角の横の「▽」から全て実行し、結果が「Schema DATA successfully created.」になる ✅ 確認ポイント : [ ] すべてのSQL文が正常に実行される(緑色のチェックマークが表示される) [ ] 画面下部の結果欄に「Database TOURISM_ANALYTICS successfully created」と表示される [ ] 「Warehouse DBT_WH successfully created」と表示される [ ] 「Schema DATA successfully created」と表示される [ ] 左側のデータベース一覧に「TOURISM_ANALYTICS」が表示される 🔧 トラブルシューティング : エラーが出た場合は、すでに同じ名前のデータベースやウェアハウスが存在する可能性があります。その場合は、先に DROP DATABASE IF EXISTS tourism_analytics; を実行してください。 3.3 dbt Projects on Snowflakeの有効化 今回は dbt Projects on Snowflake を使用します。これにより、ローカル環境にdbtをインストールする必要がなく、Snowflake上でdbtを直接実行できます。 💡 ヒント : 実運用ではGit連携して運用することが多いですが、今回は学習目的のため、Snowflake上で完結する方法で進めます。 3.3.1 dbt Projects on Snowflakeのセットアップ手順 手順 : Snowflakeの左側メニューから「プロジェクト」→「ワークスペース」を選択 「+ 新規作成」をクリックし、「dbtプロジェクト」を選択 以下の情報を入力: プロジェクト名 : dbt_Tourism_Project ロール : ACCOUNTADMIN ウェアハウス : DBT_WH (先ほど作成したもの) データベース : TOURISM_ANALYTICS スキーマ : DATA 「作成」ボタンをクリック dbtプロジェクトを作成 プロジェクト名、ロール、ウェアハウス、データベース、スキーマを設定 ✅ 確認ポイント : [ ] dbtプロジェクトが作成され、エディタ画面が表示される [ ] 左側に models/ 、 macros/ 、 seeds/ などのフォルダが自動生成される [ ] サンプルファイル( my_first_dbt_model.sql など)が含まれている 📝 補足 : これで自動的にdbtを利用するために必要なフォルダやファイルが準備できました。まずはサンプルファイルで動作確認してから、実際のデータモデルを作成していきます。 3.3.2 dbt_project.yml設定 dbt_project.ymlはdbtプロジェクトの中核となる設定ファイルです。 本ファイルを設定することで、「プロジェクト全体の設定を一元管理できる」「チームメンバー全員が同じ設定で開発できる」「モデルの構造化ルールを統一できる」といったメリットがあります。 このファイルで以下の内容を定義します。 dbt_project.ymlの役割: プロジェクトの基本情報 プロジェクト名やバージョンの定義 dbtの設定バージョンの指定 ディレクトリ構造の定義 SQLモデルの配置場所( models/ ) seedファイルの配置場所( seeds/ ) テストの配置場所( tests/ ) マクロの配置場所( macros/ ) モデルごとの動作設定 マテリアライゼーション方法(table、view、incremental等) 出力先スキーマの指定 タグやドキュメント設定 プロジェクト設定: 📁 ファイル : dbt_project.yml # dbt_project.yml の基本設定 name : 'dbt_Tourism_Project' version : '1.0.0' config-version : 2 profile : 'dbt_Tourism_Project' model-paths : [ "models" ] seed-paths : [ "seeds" ] test-paths : [ "tests" ] macro-paths : [ "macros" ] # seedの設定 seeds : dbt_Tourism_Project : +schema : raw # モデルの設定 models : dbt_Tourism_Project : staging : +materialized : table +schema : staging mart : +materialized : table +schema : mart 各設定項目の説明: 項目 説明 name プロジェクト名。dbt内部でプロジェクトを識別するために使用 version プロジェクトのバージョン config-version dbt_project.ymlの設定形式バージョン(現在は2が推奨) profile データベース接続情報を定義したプロファイル名 model-paths SQLモデルファイルを格納するディレクトリ seed-paths CSVファイル(seed)を格納するディレクトリ test-paths カスタムテストを格納するディレクトリ macro-paths マクロ(共通関数)を格納するディレクトリ seeds 配下 seedファイルの設定(出力先スキーマ等) models 配下 モデルごとの設定(マテリアライゼーション、スキーマ等) +materialized テーブルの作成方法( table =物理テーブル、 view =ビュー) +schema モデルまたはseedを作成するスキーマ名 プロジェクトの実行: projectの設定が終わったら、一度dbtを実行してみます。 「Completed successfully」という表示がされたら、うまく動いています。 実はdbtプロジェクトを作成した際、modelsフォルダー内にSQLが書かれたファイルが格納されており、このファイルを参照し設定したStagingスキーマ内にテーブルとビューが作成されています。 DAGのタブを選択すると作成したテーブル同士の関係性(リネージ)も確認することができます。 青色の実行ボタン横のコマンド選択で「実行」を選択し、青色のボタンを押下 Stagingスキーマにテーブルとビューが作成 4. データの準備とアップロード ⏱️ 所要時間 : 約20分 🎯 このセクションのゴール : - 日本観光振興会から観光データ(CSV)をダウンロードする - dbtのseed機能を使ってSnowflakeにデータをアップロードする - データが正しくロードされたことを確認する 4.1 観光データのダウンロード 日本観光振興会の観光統計サイト から、観光来訪者数のCSVデータをダウンロードします。今回は都道府県別、市区町村別のデータを2021年1月から2025年10月までをデータソースとします。 💡 ヒント : ダウンロードしたファイルが異なる場合、後のコード上で一部修正が必要になります。 手順 : 日本観光振興会の観光統計サイト 上記リンクにアクセス 「都道府県別」と「市区町村別」のデータをそれぞれダウンロード 最低でも2〜3ヶ月分のデータを用意すると、後の分析で傾向が見やすくなります ファイル名を以下の形式に変更(dbtでの処理がしやすくなります): 都道府県: pref_YYYYMM.csv (例: pref_202301.csv ) 市区町村: city_YYYYMM.csv (例: city_202301.csv ) 💡 ヒント : ファイル名の命名規則を統一することで、後のマクロ処理が簡単になります。 データの構造例 : year,month,region_category,data_category,prefecture_code,prefecture_name,region_code,region_name,visitor_count 2023,1,市区町村,観光来訪者数,1,北海道,1101,札幌市,1500000 2023,1,市区町村,観光来訪者数,13,東京都,13101,千代田区,2500000 主要カラムの説明 : - year , month : 年月 - region_category : 「都道府県」または「市区町村」 - prefecture_code : 都道府県コード(1〜47) - region_code : 地域コード - visitor_count : 観光来訪者数 4.2 Snowflakeへのデータアップロード dbtでは、小規模なCSVファイルを seed として管理できます。これは参照データやテストデータに最適です。 📚 dbt seedとは? : CSV形式のデータをテーブルとしてデータウェアハウスにロードする機能です。バージョン管理が可能で、dbtコマンドで簡単にロードできます。 seedのメリット : - バージョン管理が可能(CSVをGitで管理) - dbtコマンドで簡単にロード可能 - 開発環境と本番環境で同じデータを使用できる 4.2.1 dbt Projects on Snowflakeでのseedアップロード 今回は都道府県と市区町村のデータがそれぞれあるので、整理してアップロードします。 手順 : dbtプロジェクト画面で seeds/ フォルダを右クリック 「新しいフォルダ」を選択し、 pref を作成 同様に city フォルダも作成 各フォルダにCSVファイルをアップロード: seeds/pref/ 配下 pref_202301.csv pref_202302.csv (ダウンロードした月数分) seeds/city/ 配下 city_202301.csv city_202302.csv (ダウンロードした月数分) seeds内にPrefとCityのサブディレクトリを作成 seeds内にCSVファイルを保存 プロジェクト構造(例): tourism_analytics/ ├── seeds/ │ ├── pref/ │ │ ├── pref_2023_01.csv │ │ └── pref_2023_02.csv │ │ └── pref_XXXX_XX.csv │ └── city/ │ ├── city_2023_01.csv │ └── city_2023_02.csv │ └── city_XXXX_XX.csv ├── models/ ├── macros/ └── dbt_project.yml 4.2.2 dbtコマンドを実行してseedをロード 「シード」コマンドを実行することで、CSVファイルが STAGING_RAW スキーマにテーブルとして作成されます。(5分程度時間がかかります) SQLシートでデータが格納されているかチェックしておきます。 実行結果の確認: -- アップロードされたテーブルを確認 SHOW TABLES IN tourism_analytics.staging_raw; -- データの中身を確認 SELECT * FROM tourism_analytics.staging_raw.city202301 LIMIT 10 ; SELECT * FROM tourism_analytics.staging_raw.pref202301 LIMIT 10 ; STAGING_RAWスキーマにデータがロードされる 5. dbtプロジェクトの実装 ⏱️ 所要時間 : 約60〜90分 🎯 このセクションのゴール : - マクロを使った複数テーブルの統合処理を実装する - ステージング層でデータクレンジングを行う - マート層でディメンションテーブルとファクトテーブルを作成する - スキーマ定義とテストを設定する 5.1 プロジェクト構成 この章から本格的にデータ加工を行っていきます。dbt Projects on Snowflake上でのプロジェクト構成は以下のようになります。 複数のファイルを作成し、適切なフォルダに保存する必要があるため保存場所に注意が必要です。 dbt_Tourism_Project/ (dbt Projects on Snowflakeプロジェクト) ├── models/ │ ├── staging/ # ステージング層 │ │ ├── stg_city_tourism_from_seeds.sql │ │ ├── stg_pref_tourism_from_seeds.sql │ │ └── schema.yml │ └── mart/ # マート層 │ ├── dim_dates.sql │ ├── dim_regions.sql │ ├── fact_tourism_monthly.sql │ └── schema.yml ├── macros/ # 共通処理 │ ├── union_seed_tables.sql │ └── get_tourism_data_tests.sql ├── seeds/ # CSVデータ │ ├── pref/ # 都道府県データ │ │ ├── pref_2023_01.csv │ │ └── pref_2023_02.csv │ │ └── pref_20XX_XX.csv │ └── city/ # 市区町村データ │ ├── city_2023_01.csv │ └── city_2023_02.csv │ └── city_202X_XX.csv └── dbt_project.yml # プロジェクト設定 5.2 マクロの実装:複数テーブルの統合 観光データは月ごとに別々のCSVファイルとして提供されるため、これらを統合する必要があります。dbtの マクロ機能 を使うと、動的にテーブルを結合できます。 マクロを利用することにより、「新しい月のデータを追加しても、コードを変更する必要がない」「DRY原則(Don't Repeat Yourself)に従った実装」「メンテナンス性の向上」といったメリットがあります。 このマクロは以下を実現します。 1. 特定のパターン(例: city_* )に一致するテーブルを自動検索 2. すべてのテーブルを UNION ALL で結合 3. ソースファイル名を追跡 重要:seedのフォルダ構造とテーブル名の関係 seedファイルは seeds/pref/ と seeds/city/ に分けて格納していますが、Snowflake上では フォルダ構造に関わらず すべて同じスキーマ( STAGING_RAW )にフラットに作成されます。 例: - seeds/city/city_2023_01.csv → STAGING_RAW.city202301 テーブル - seeds/pref/pref_2023_01.csv → STAGING_RAW.pref_202301 テーブル したがって、マクロでは schema_pattern='STAGING_RAW' を指定し、 city_* や pref_* のようなテーブル名パターンで検索することで、該当するすべてのseedテーブルを取得できます。 📁 ファイル : macros/union_seed_tables.sql -- macros/union_seed_tables.sql {% macro union_seed_tables(prefix) %} {# 実際のテーブルリストを定義 #} {%- if prefix == ' CITY ' -%} {%- set table_list = [ ' CITY202101 ' , ' CITY202201 ' , ' CITY202301 ' , ' CITY202401 ' , ' CITY202501 ' , ' CITY202502 ' , ' CITY202503 ' , ' CITY202504 ' , ' CITY202505 ' , ' CITY202506 ' , ' CITY202507 ' , ' CITY202508 ' , ' CITY202509 ' , ' CITY202510 ' ] -%} {%- elif prefix == ' PREF ' -%} {%- set table_list = [ ' PREF202101 ' , ' PREF202201 ' , ' PREF202301 ' , ' PREF202401 ' , ' PREF202501 ' , ' PREF202502 ' , ' PREF202503 ' , ' PREF202504 ' , ' PREF202505 ' , ' PREF202506 ' , ' PREF202507 ' , ' PREF202508 ' , ' PREF202509 ' , ' PREF202510 ' ] -%} {%- endif -%} {# UNION ALLクエリを生成 #} {% for table_name in table_list %} {% if loop . first %} -- Union all {{ prefix }} seed tables {% else %} UNION ALL {% endif %} SELECT year, month, region_category, data_category, {%- if prefix == ' CITY ' %} prefecture_code, prefecture_name, {% endif -%} region_code, region_name, visitor_count, ' {{ table_name }} ' AS source_file, CURRENT_TIMESTAMP () AS processed_at FROM {{ source( ' data_raw ' , table_name) }} {% endfor %} {% endmacro %} マクロのポイント: schema_pattern='STAGING_RAW' : seedテーブルが格納されている STAGING_RAW スキーマを明示的に指定 table_pattern=prefix + '%' : city や pref で始まるテーブルをパターンマッチで検索 relation.identifier : 元のseedファイル名(テーブル名)を記録 5.3 ステージング層の実装 ステージング層では、以下の処理を行います。 - データのクレンジング(NULL、異常値のチェック) - 日付フィールドの生成 - データ品質フラグの付与 ステージング層を作ることにより、「生データと分析用データを分離」「データ品質の問題を早期に発見」「後続の処理で綺麗なデータのみを扱える」といったメリットがあります。 📁 ファイル : models/staging/stg_city_tourism_from_seeds.sql -- models/staging/stg_city_tourism_from_seeds.sql {{ config( materialized= ' table ' , schema= ' Staging ' ) }} WITH combined_city_data AS ( -- マクロを呼び出して複数テーブルを統合(プレフィックスをCITYに変更) {{ union_seed_tables( ' CITY ' ) }} ), enriched_data AS ( SELECT year AS visit_year, month AS visit_month, region_category, data_category, prefecture_code, prefecture_name, region_code, region_name, visitor_count, source_file, processed_at, -- 日付フィールドの作成(分析で使いやすくするため) DATE (year || ' - ' || LPAD (month, 2 , ' 0 ' ) || ' -01 ' ) AS visit_date, -- 年月文字列の作成(ソートキーとして便利) year || LPAD (month, 2 , ' 0 ' ) AS year_month, -- データ品質フラグ CASE WHEN visitor_count IS NULL OR visitor_count < 0 THEN ' invalid_count ' WHEN year < 2020 OR year > YEAR( CURRENT_DATE ()) THEN ' invalid_date ' WHEN month < 1 OR month > 12 THEN ' invalid_month ' ELSE ' valid ' END AS data_quality_flag FROM combined_city_data ) SELECT visit_year, visit_month, visit_date, year_month, region_category, data_category, prefecture_code, prefecture_name, region_code, region_name, visitor_count, data_quality_flag, source_file, processed_at FROM enriched_data WHERE data_quality_flag = ' valid ' -- 有効なデータのみを後続処理に渡す ORDER BY visit_year, visit_month, prefecture_code, region_code 同様に、都道府県データ用のステージングモデル( stg_pref_tourism_from_seeds.sql )も実装します。 ファイル名: models/staging/stg_pref_tourism_from_seeds.sql 変更点: {{ union_seed_tables(' PREF ') }} 都道府県データからは以下のカラムを削除: - prefecture_code - prefecture_name 都道府県データには元々これらのカラムが存在しないため、SELECTから除外しました。 📁 ファイル : models/staging/stg_pref_tourism_from_seeds.sql -- models/staging/stg_pref_tourism_from_seeds.sql {{ config( materialized= ' table ' , schema= ' Staging ' ) }} WITH combined_pref_data AS ( -- マクロを呼び出して複数テーブルを統合 {{ union_seed_tables( ' PREF ' ) }} ), enriched_data AS ( SELECT year AS visit_year, month AS visit_month, region_category, data_category, region_code, region_name, visitor_count, source_file, processed_at, -- 日付フィールドの作成 DATE (year || ' - ' || LPAD (month, 2 , ' 0 ' ) || ' -01 ' ) AS visit_date, -- 年月文字列の作成 year || LPAD (month, 2 , ' 0 ' ) AS year_month, -- データ品質フラグ CASE WHEN visitor_count IS NULL OR visitor_count < 0 THEN ' invalid_count ' WHEN year < 2020 OR year > YEAR( CURRENT_DATE ()) THEN ' invalid_date ' WHEN month < 1 OR month > 12 THEN ' invalid_month ' ELSE ' valid ' END AS data_quality_flag FROM combined_pref_data ) SELECT visit_year, visit_month, visit_date, year_month, region_category, data_category, region_code, region_name, visitor_count, data_quality_flag, source_file, processed_at FROM enriched_data WHERE data_quality_flag = ' valid ' ORDER BY visit_year, visit_month, region_code Stagingテーブルの元となるデータの保存先を定義します。 📁 ファイル : models/staging/sources.yml # models/staging/sources.yml version : 2 sources : - name : data_raw database : tourism_analytics schema : data_raw tables : - name : CITY202101 - name : CITY202201 - name : CITY202301 - name : CITY202401 - name : CITY202501 - name : CITY202502 - name : CITY202503 - name : CITY202504 - name : CITY202505 - name : CITY202506 - name : CITY202507 - name : CITY202508 - name : CITY202509 - name : CITY202510 - name : PREF202101 - name : PREF202201 - name : PREF202301 - name : PREF202401 - name : PREF202501 - name : PREF202502 - name : PREF202503 - name : PREF202504 - name : PREF202505 - name : PREF202506 - name : PREF202507 - name : PREF202508 - name : PREF202509 - name : PREF202510 5.4 マート層の実装 マート層では、ビジネスロジックに基づいた分析用のテーブルを作成します。 5.4.1 ディメンションテーブル1:日付マスター まずは日付マスターを作成します。日付マスターを作成することにより、「データが存在しない月も含めて、連続した時系列データを表現できる」「年月の計算が容易になる」「BIツールでの時系列分析が簡単になる」といったメリットがあります。 📁 ファイル : models/mart/dim_dates.sql -- models/mart/dim_dates.sql {{ config( materialized= ' table ' , schema= ' mart ' ) }} WITH RECURSIVE date_spine AS ( -- 開始日 SELECT DATE ( ' 2021-01-01 ' ) as date_month UNION ALL -- 月を1つずつ増やす(再帰的に2030年12月まで) SELECT DATEADD(month, 1 , date_month) FROM date_spine WHERE date_month < DATE ( ' 2030-12-31 ' ) ), date_dimension AS ( SELECT date_month as date_key, YEAR(date_month) as year, MONTH(date_month) as month, CURRENT_TIMESTAMP () as created_at FROM date_spine ) SELECT * FROM date_dimension ORDER BY date_key 5.4.2 ディメンションテーブル2:地域マスター 次に地域マスターを作成します。その中で、地方ブロックを追加し、別の切り口での分析ができるようにします。 他にも様々な分析の切り口を作ることも考えられますが、今回は地域ブロックだけにします。 📁 ファイル : models/mart/dim_regions.sql -- models/mart/dim_regions.sql {{ config( materialized= ' table ' , schema= ' mart ' ) }} WITH city_regions AS ( -- 市区町村データから地域情報を取得 SELECT DISTINCT region_code, region_name, region_category, prefecture_code, prefecture_name FROM {{ ref( ' stg_city_tourism_from_seeds ' ) }} WHERE region_code IS NOT NULL AND region_name IS NOT NULL ), pref_regions AS ( -- 都道府県データから地域情報を取得 SELECT DISTINCT region_code, region_name, region_category, NULL as prefecture_code, region_name as prefecture_name FROM {{ ref( ' stg_pref_tourism_from_seeds ' ) }} WHERE region_code IS NOT NULL AND region_name IS NOT NULL ), combined_regions AS ( SELECT * FROM city_regions UNION SELECT * FROM pref_regions ), enriched_regions AS ( SELECT -- 複合キーとしてregion_idを作成 region_code || ' _ ' || region_category as region_id, region_code, region_name, region_category, prefecture_code, prefecture_name, -- 都道府県+市区町村名の列を追加 CASE WHEN region_category = ' 市区町村 ' THEN prefecture_name || region_name ELSE region_name -- 都道府県の場合は都道府県名のみ END as full_region_name, -- 地方ブロック分類(ビジネスロジック) CASE WHEN region_code IN ( 1 ) THEN ' 北海道 ' WHEN region_code IN ( 2 , 3 , 4 , 5 , 6 , 7 ) THEN ' 東北 ' WHEN region_code IN ( 8 , 9 , 10 , 11 , 12 , 13 , 14 ) THEN ' 関東 ' WHEN region_code IN ( 15 , 16 , 17 , 18 , 19 , 20 ) THEN ' 中部 ' WHEN region_code IN ( 21 , 22 , 23 , 24 ) THEN ' 東海 ' WHEN region_code IN ( 25 , 26 , 27 , 28 , 29 , 30 ) THEN ' 関西 ' WHEN region_code IN ( 31 , 32 , 33 , 34 , 35 ) THEN ' 中国 ' WHEN region_code IN ( 36 , 37 , 38 , 39 ) THEN ' 四国 ' WHEN region_code IN ( 40 , 41 , 42 , 43 , 44 , 45 , 46 , 47 ) THEN ' 九州・沖縄 ' ELSE ' その他 ' END as region_block, CURRENT_TIMESTAMP () as created_at, -- 重複排除用:region_id(region_code + region_category)ごとに番号を付与 ROW_NUMBER() OVER (PARTITION BY region_code, region_category ORDER BY region_name) as row_num FROM combined_regions ), deduplicated_regions AS ( -- 重複を排除:各region_idの最初の1件のみを取得 SELECT region_id, region_code, region_name, region_category, prefecture_code, prefecture_name, full_region_name, region_block, created_at FROM enriched_regions WHERE row_num = 1 ) SELECT region_id, region_code, region_name, region_category, prefecture_code, prefecture_name, full_region_name, region_block, created_at FROM deduplicated_regions ORDER BY region_code, region_category 5.4.3 ファクトテーブル:観光来訪者数 次にファクトテーブルとして、観光来訪者数テーブルを作成します。 これまでのデータをJOINして非正規化することで、Tableauなどの BIツールから直接利用しやすいデータマート を構築できます。具体的には「BIツールでの計算を削減し、クエリパフォーマンスが向上」「よく使われる項目を事前に結合しておくことで、分析者の手間を削減」といったメリットがあります。 📁 ファイル : models/mart/fact_tourism_monthly.sql {{ config( materialized= ' table ' , schema= ' mart ' ) }} WITH tourism_facts AS ( -- 市区町村データ SELECT visit_date as date_key, region_code, visit_year, visit_month, region_category, data_category, visitor_count, source_file, processed_at FROM {{ ref( ' stg_city_tourism_from_seeds ' ) }} WHERE data_quality_flag = ' valid ' UNION ALL -- 都道府県データ SELECT visit_date as date_key, region_code, visit_year, visit_month, region_category, data_category, visitor_count, source_file, processed_at FROM {{ ref( ' stg_pref_tourism_from_seeds ' ) }} WHERE data_quality_flag = ' valid ' ) SELECT tf.date_key, tf.region_code, tf.visit_year, tf.visit_month, tf.region_category, tf.data_category, tf.visitor_count, -- 地域情報との結合(非正規化して高速化) dr.region_name, dr.prefecture_name, dr.full_region_name, dr.region_block, -- 日付情報との結合(dim_datesを参照) dd.year as dim_year, dd.month as dim_month, tf.source_file, tf.processed_at, CURRENT_TIMESTAMP () as mart_created_at FROM tourism_facts tf LEFT JOIN {{ ref( ' dim_regions ' ) }} dr ON tf.region_code = dr.region_code AND tf.region_category = dr.region_category LEFT JOIN {{ ref( ' dim_dates ' ) }} dd ON tf.date_key = dd.date_key ORDER BY date_key, region_category, region_code 5.5 スキーマ定義(schema.yml) dbtでは、 schema.yml でテーブルの構造とテストを定義します。 スキーマを定義しておくことで、「データの意味が明確になる(ドキュメントとして機能)」「テストを実行してデータ品質を保証できる」「チーム開発時の理解が容易になる」というメリットがあります。 📁 ファイル : models/mart/schema.yml # models/mart/schema.yml version : 2 models : - name : dim_regions description : "地域マスターテーブル(都道府県・市区町村情報)" columns : - name : region_id description : "地域ID(region_code + region_category の組み合わせ)" tests : - not_null - unique - name : region_code description : "地域コード" tests : - not_null - name : region_name description : "地域名(市区町村名または都道府県名)" tests : - not_null - name : region_category description : "地域区分(都道府県/市区町村)" tests : - not_null - accepted_values : values : [ '都道府県' , '市区町村' ] - name : prefecture_code description : "都道府県コード(都道府県の場合はNULL)" - name : prefecture_name description : "都道府県名" tests : - not_null - name : full_region_name description : "完全な地域名(都道府県名+市区町村名、または都道府県名のみ)" tests : - not_null - name : region_block description : "地方ブロック" tests : - not_null - name : created_at description : "レコード作成日時" tests : - not_null - name : dim_dates description : "日付マスターテーブル" columns : - name : date_key description : "日付キー" tests : - not_null - unique - name : year description : "年" tests : - not_null - name : month description : "月" tests : - not_null - accepted_values : values : [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ] - name : created_at description : "レコード作成日時" tests : - not_null - name : fact_tourism_monthly description : "観光データファクトテーブル(月次)" columns : - name : date_key description : "日付キー" tests : - not_null - name : region_code description : "地域コード" tests : - not_null - name : visit_year description : "訪問年" tests : - not_null - name : visit_month description : "訪問月" tests : - not_null - accepted_values : values : [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ] - name : region_category description : "地域区分(都道府県/市区町村)" tests : - not_null - accepted_values : values : [ '都道府県' , '市区町村' ] - name : data_category description : "データ区分" tests : - not_null - name : visitor_count description : "訪問者数" tests : - not_null - name : region_name description : "地域名" - name : prefecture_name description : "都道府県名" - name : region_block description : "地方ブロック" - name : source_file description : "ソースファイル名" tests : - not_null - name : processed_at description : "処理日時" tests : - not_null - name : mart_created_at description : "マート作成日時" tests : - not_null 5.6 モデルの実行 dbt Projects on Snowflake上ですべてのモデルを実行します。 実行方法: Snowflakeのdbtプロジェクト画面で「実行」ボタンをクリック dbtは依存関係を自動的に解決し、正しい順序でモデルを実行します。 seeds → staging → mart 実行結果の確認: -- マート層のテーブルを確認 SHOW TABLES IN tourism_analytics.data_mart; -- ファクトテーブルのデータを確認 SELECT * FROM tourism_analytics.data_mart.fact_tourism_monthly LIMIT 10 ; -- 地域マスターを確認 SELECT * FROM tourism_analytics.data_mart.dim_regions LIMIT 10 ; データマートの完成 6. データ品質テストの実装 ⏱️ 所要時間 : 約10分 🎯 このセクションのゴール : - カスタムテストマクロを実装する - データ品質チェックを実行する - データリネージを確認する dbtの強力な機能の一つが テスト機能 です。データパイプラインの信頼性を高めるために、Snowflake上でdbtのテストを実行し、カスタムテストを実装します。 テストの内容をコードで定義することで、「ビジネスルールに基づいた検証ができる」「データの異常を早期に発見できる」「データパイプラインの信頼性が向上する」「データ品質を継続的に保証できる」といったメリットがあります。 今回のデータでは福島県の桧枝岐村が旧字体の檜枝岐村と同じコードで登録されているため、dim_regions.sqlのREGION_CODEで重複エラーが出ていました。 そのため重複がある場合は、各リージョンIDの最初の市区町村名のみを取得する制約を入れることができました。 6.1 カスタムテストマクロ 📁 ファイル : macros/get_tourism_data_tests.sql -- macros/get_tourism_data_tests.sql {% macro test_valid_prefecture_codes() %} -- 都道府県コードが1〜47の範囲内かチェック select * from {{ ref( ' stg_city_tourism_from_seeds ' ) }} where region_code not between 1 and 47 {% endmacro %} {% macro test_reasonable_visitor_counts() %} -- 訪問者数が妥当な範囲内かチェック select * from {{ ref( ' stg_city_tourism_from_seeds ' ) }} where visitor_count > 100000000 -- 1億人超は異常値 or visitor_count < 0 {% endmacro %} {% macro test_data_completeness() %} -- データの完全性チェック(月間レコード数) select visit_year, visit_month, count (*) as record_count from {{ ref( ' stg_city_tourism_from_seeds ' ) }} group by visit_year, visit_month having count (*) < 100 -- 月間100件未満は不完全データの可能性 {% endmacro %} 6.2 テストの実行 dbt Projects on Snowflake上でテストを実行します。 dbtテストの実行 データリネージの確認: 依存関係グラフで、どのテーブルがどのテーブルに依存しているかを視覚的に確認できます。これにより、データフローの全体像を把握できます。 DATA_STAGINGからDATA_MARTに含まれるデータのリネージ図 7. Tableauでの可視化 ⏱️ 所要時間 : 約30分 🎯 このセクションのゴール : - Tableau Desktopをインストールする - SnowflakeのデータマートにTableauから接続する - 都道府県別の観光データを可視化する - インタラクティブなダッシュボードを作成する 7.1 Tableauのインストール 今回は Tableau Desktop を使用します。無料で利用可能なTableau Public Desktop EditionではSnowflakeとの接続ができないため、Tableau Desktopの 14日間無料トライアル版 を利用します。 💡 ヒント : Tableau Desktopではなく、Tableau Public Desktop Edition を利用する場合は、Snowflake上のデータソースをCSV等でダウンロードする必要があります。Tableau Desktopの有償版とほぼ同じ機能が利用できますが、作成したビジュアライゼーションは公開され、ローカル保存はできません。業務用途では有償版のTableau Desktopの使用を検討してください。 ⚠️ Tableau Desktopトライアルの留意点 : - 期間 : 14日間の無料トライアル - メールアドレス : トライアル申込時にメールアドレスの登録が必要です - クレジットカード : 登録時にクレジットカード情報は 不要 です - 自動課金なし : トライアル終了後、自動的に有料プランに移行することはありません - 機能制限なし : トライアル版でも製品版と同じ全機能を利用できます インストール手順 : Tableau Desktop 公式サイト にアクセス 登録フォームに以下の情報を入力: メールアドレス 名前 国/地域 「無料トライアル版をダウンロード」ボタンをクリックしてインストーラーをダウンロード ダウンロードした実行ファイル( .exe )をダブルクリックしてインストールを開始 インストールウィザードに従って進め、完了後にTableau Desktopを起動 📝 補足 : インストールには数分かかる場合があります。 7.2 Snowflakeへの接続 Tableau DesktopからSnowflakeに接続し、作成したデータマートを利用します。 手順 : Tableau Desktopを起動 左側のメニューから「サーバーへ」→「Snowflake」を選択 Snowflake用のドライバーのダウンロードが求められるため、利用しているOSに合わせてドライバーをインストール(ドライバーインストール後Tableauを再起動し、2を再度実施) Snowflake接続情報を入力: サーバー : <アカウント識別子>.snowflakecomputing.com 例: abc12345.ap-northeast-1.aws.snowflakecomputing.com アカウント識別子はSnowflakeのアカウント詳細から確認できます(後述) ウェアハウス : dbt_wh (大文字小文字は区別されません) データベース : tourism_analytics スキーマ : data_mart ユーザー名 :Snowflakeのユーザー名 パスワード :Snowflakeのパスワード 「サインイン」ボタンをクリック ⚠️ 注意 : サーバー名には <アカウント識別子>.snowflakecomputing.com の形式を使用してください。 https:// や末尾の / は不要です。 ファイルから新規作成を行い、データソースを選択 接続先としてSnowflakeに接続 Snowflakeのログインに必要な情報を入力 サーバーのURLが不明な場合はアカウント詳細を確認 アカウント詳細の例 7.3 データソースの設定 FACT_TOURISM_MONTHLY テーブルをキャンバスにドラッグ ドラッグし、更新するとデータが表示 7.4 観光データを利用したワークシート(グラフなど)の作成 作成したデータマートを活用して、観光来訪者数の傾向を把握するためのビジュアライゼーションを作成します。以下では、都道府県別の観光者数の比較、月次の変化パターン、地理的な分布を視覚化します。 7.4.1 例1:都道府県別観光者数 新しいワークシートを作成 列に「合計(Visitor Count)」をドラッグ 行に「Region Block」(地方ブロック)、「Prefecture Name」(都道府県名)をドラッグ フィルターに「Date Key」(年)を追加し2025年など特定の年を選択 表示をしてみると、Region Blockかその他のものが多数含まれていることが分かります。 これは市区町村側ごとのデータマートもまとめて同じデータマートに含まれており、都道府県名でグループ化するとダブルカウントされています。 Region Block内で、その他のデータが含まれていることが分かる そのため、その他を右クリックし除外することで重複を排除します。 また、フィルター内の「Date Key」(年)を右クリックして、フィルターを表示しておくことでグラフに表示する年を選べるようにしておきます。 その他の除外とフィルターの表示 今回「Region Block」(地方ブロック)ごとではなく、都道府県全体でデータを降順に並べたいため、「Region Block」(地方ブロック)は削除し、降順に並べておきます。 また、ラベルに「合計(Visitor Count)」をドラッグしておくことで棒グラフ横でも数値が見えるようにしておきます。 都道府県別観光者数の棒グラフ表示 7.4.2 例2:都道府県別ヒートマップ 新しいワークシートを作成 列に「Prefecture Name」(都道府県名)をドラッグ 行に「Visit Month」(月)をドラッグ 表示形式をヒートマップに変更 先ほどと同様に都道府県別の訪問者数グラフを作成(Region Blockでその他を除外) 表示形式をヒートマップに変更 VisitCountをラベルにドラッグし、訪問者数を表示&フィルタとして年(DataKey)を追加 7.4.3 例3:地図表示 Tableauは地図表示にも優れており、都道府県名を元に訪問者数を簡単に地図表示で表現することも可能です。 新しいワークシートを作成 「Prefecture Name」(都道府県名)を右クリックし、「地理的役割」から「都道府県」を選択 「Prefecture Name」(都道府県名)を真ん中のフィールドへドラッグ マークの形をマップへ変更 マークカードに「Visitor Count」を色として追加 Prefecture Nameを右クリックし「地理的役割」から「都道府県」を選択 Prefecture Nameを真ん中にドラッグし、マークの形をマップへ変更 Region Blockでその他のデータをフィルタで除外する 7.5 ダッシュボードの作成 複数のワークシートを組み合わせてダッシュボードを作成します。 「ダッシュボード」→「新しいダッシュボード」 作成したワークシート「都道府県別観光者数」「都道府県別観光者数(マップ)」をドラッグ&ドロップ 「都道府県別観光者数(マップ)」を浮動に変更し、タイトル表示を非表示にする 「DateKey(年)」フィルタを選択し、適応先をマップにも反映させる ダッシュボードを新規作成し、シートをドラッグ&ドロップ マップを浮動形式に変更し、タイトルを非表示に変更 DateKey(年)フィルタの適応先を変更 マップにもフィルタを反映させる 地図上での数値と棒グラフの数値が連動(フィルタが両方のグラフに反映) 8. まとめ 今回は dbt、Snowflake、Tableau を活用し、観光オープンデータの分析基盤を構築しました。 チームなど複数人でデータ分析を行う場合、今回のようにデータパイプラインを整備し、処理内容が他の人にも分かりやすく、再利用可能な状態にしておくことは非常に重要だと考えています。 まだ完全自動化に向けて改善の余地が多く残されているため、今後も継続的に改善を進めていきたいと思います。 実現できたこと データ加工の透明性 SQLでバージョン管理されたデータパイプライン いつでも過去の変更を追跡可能 データ品質の保証 自動テストによる異常検知 スケーラブルな基盤 データ量が増えても対応可能 新しい月のデータ追加が容易(seedを追加するだけ) 分析の民主化 BI ツールで誰でも分析可能 SQL を書かなくても可視化できる 今後の改善案 パッケージの実装 :現状、月ごとにファイルが増えるたびにコードの書き換えが必要なため、dbt-labs/dbt_utilsなどdbt処理を便利にするパッケージの実装を検討 CI/CDパイプライン :GitHub Actions等で自動テスト・デプロイ アラート機能 :異常値検知時にSlack通知 パフォーマンス最適化 :クラスタリングキーやマテリアライズドビューの活用 参考リンク dbt公式ドキュメント Snowflake公式ドキュメント 日本観光振興会 観光統計 モダンなデータ分析基盤の構築に、ぜひこの技術スタックを試してみてください! 執筆者 谷 清隆 (NTT西日本 ビジネス営業本部 エンタープライズビジネス営業部所属) データエンジニアとして、民間企業や自治体のデータ利活用戦略の構想策定から基盤構築まで幅広く担当しています。 商標について 「Snowflake」は、Snowflake Inc.の米国およびその他の国における商標または登録商標です。 「Tableau」は、Salesforce, Inc.の米国およびその他の国における商標または登録商標です。 「dbt」は、dbt Labs, Inc.の商標または登録商標です。 「Amazon Web Services」、「AWS」は、Amazon.com, Inc.またはその関連会社の米国およびその他の国における商標または登録商標です。 その他、本記事に記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。 免責事項 本記事の内容は執筆時点(2025年12月)の情報に基づいており、サービスの仕様変更等により内容が変更される可能性があります。 本記事で紹介する設定手順や使用方法は、特定の環境での動作確認に基づくものであり、すべての環境での動作を保証するものではありません。 Snowflakeの無料トライアルには利用期限・無料クレジット枠がありますが、クレジットカードなどの支払い設定を行っている場合、期限後は課金が発生する可能性があります。利用規約をご確認の上、ご使用ください。 本記事で使用している日本観光振興会のオープンデータは、公開時点のものであり、データの正確性や完全性について保証するものではありません。