TECH PLAY

NTT西日本

NTT西日本 の技術ブログ

78

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の無料トライアルには利用期限・無料クレジット枠がありますが、クレジットカードなどの支払い設定を行っている場合、期限後は課金が発生する可能性があります。利用規約をご確認の上、ご使用ください。 本記事で使用している日本観光振興会のオープンデータは、公開時点のものであり、データの正確性や完全性について保証するものではありません。
はじめに 対象読者 背景・目的 SimcirJSとは まずは触ってみよう JKフリップフロップ回路 まず、フリップフロップとは JKフリップフロップとは クロックとは カウンタ回路を作る 3進カウンタへの道 どうやって3進に 強制リセット なければ作る!? リセット付きJK-FF 登録のやり方・・・ httpdサーバ 自作部品の登録 自分で追加した回路を使って組み上げる 3進カウンタ 動作例 さいごに まとめ 執筆者 参考資料・出典 SimcirJSを利用できるサイト ライセンス はじめに NTT西日本の ふるかわ といいます。 本記事は、入社XX年(ナイショ)のベテラン(?)技術者が、ひさしぶりにディジタル回路シミュレータを触ってみたくなった顛末記のようなものです。 本記事は2025年11月時点の情報に基づいています。 対象読者 まわりの記事と比較すると、すこし異色かも知れません。 本記事は、ディジタル回路に興味があるひと、「コンピュータの中身ってどうやって動いてるんだっけ」ということをちょっと覗いてみたいひと、などに、ごく簡単な導入ができたらいいな、と思って書き記すものです。 あまり、お勉強お勉強にならないように気を付けますが、代わりに(?)学術的に厳密でもないかも知れないので、入門編の読み物としてサラっと読んでみていただけるとありがたいです。 背景・目的 あまりこれと言った背景はないのですが、なぜか無性に「3進カウンタ」が作りたくなりました。 0→1→2→0→1→2→… と、みっつの値を繰り返すカウンタです。野球の「ストライク」(または「アウト」)のカウンタをイメージしてもらうとよろしいかと思います。 ディジタル回路の特性上、2 n 進カウンタ(2進、4進、8進、…)はとても簡単に作れるのですがそれ以外のカウンタは、ちょっと面倒だったりします。 こんなとき、約四半世紀前に大学生をしていたころなら、巨大なディジタル回路実験盤を持ち出して大変な思いをしていましたが、イマドキそんなのはシミュレータを使えば簡単にシミュレーションできるようになっています。 しかもありがたいことに無償で使えるものもありますから、これを活用するのが便利です。ということで、ひさびさの利用になる「SimcirJS」というシミュレータを使ってみることにしました。 SimcirJSとは HTML5とJavaScriptでできたディジタル回路シミュレータで、MITライセンスで公開されています。 ブラウザから利用できるため、インストール不要で使い始めることができます。高専・大学等の授業でも多く利用されているようで、教材等が公開されているものもあります。 わたしも、いくつかのサイトを参考にさせていただきました。それらは、文末の参考資料の節にまとめます。 まずは触ってみよう SimcirJSの作者さんのサイト「Kazuhiko Arase's Workplace」 https://kazuhikoarase.github.io/simcirjs/index.html にアクセスすれば、すぐに利用することができます。 SimcirJSの画面例 左側から回路の要素を選んで、右側にドラッグアンドドロップし、 SimcirJSの操作例 回路の端子同士をドラッグ操作でつなぐと結線されます。 SimcirJSの結線操作例 このとき注意事項は、回路の右側の端子(白)は出力で、いくつも分岐させられますが、 回路の左側の端子(橙)は入力になるので、分岐はできません。 「DC」は直流電源(Direct Current)、「LED」は発光ダイオード(Light-Emitting Diode)なので、このふたつを結線すると、「LEDが光った」ことになり、 画面上ではLEDの表示が黒丸から赤丸に変わります。 SimcirJS動作例、DCとLEDを結線するとLEDが点灯する 試しに、ディジタル回路らしく、AND回路とOR回路の動きを確かめておきましょう。 どちらも入力をふたつ・出力をひとつ持つ回路ですが、 AND回路は入力が両方とも1のときだけ出力も1に、 逆にOR回路は入力が両方とも0のときだけ出力も0に、 それぞれなるような回路です。 ここで、0とは電圧なし、1は電圧ありのことなので、「DC」回路とトグルスイッチ(「Toggle」)を使って、次のような回路を組んでみましょう。 SimcirJSの動作例 AND回路とOR回路 「トグルスイッチ」とは、部屋の電気(照明)のスイッチのように、スイッチから指を離しても切り替わった状態が維持されるようなスイッチのことです。 「Toggle」の上にある「PushOn」「PushOff」のスイッチは、指で押している間だけON(またはOFF)になるようなスイッチです。 このような動作確認のときは、指を離しても切り替わったままになっている方が便利なので、トグルスイッチを利用するのがよいでしょう。 トグルスイッチをONにすれば「DC」とつながって電圧あり(つまり入力1)、OFFなら電圧なし(入力0)になりますから、 ANDとORの動きを確かめることができました。 JKフリップフロップ回路 まず、フリップフロップとは フリップフロップ回路とは、1bit (2値) の状態を保持できる回路のことです。 「フリップフロップ」(flip-flop) とは、シーソーがあっちこっちに傾くときの擬音だそうで、 その名の通り、シーソーを思い浮かべていただくのがいちばん分かりやすいと思います。 いま、なにも乗っていないシーソーが左に傾いているとします。なにもしなければ左に傾いているままです。 そこで右側にだけ荷物をのせると、今度は右に傾きます。ここまでは当たり前なのですが、 ミソはその次で、右に傾いたあとで、荷物を降ろすとどうなるでしょうか。そうです、荷物を降ろしても シーソーは右に傾いたままになります。次にもういちど左に荷物を載せて、左に傾けると、 その荷物を降ろした後もシーソーは左に傾いたままになります。 これはどういうことかというと、シーソーは、「最後に荷物が載った状態(降ろす直前の状態)」を「保持している」 ということになります。言い換えると「最後に荷物が載った側がどちらか」を「覚えている」とも言えます。 この「右」と「左」を「0」と「1」に読み替えると、シーソーは1bitのメモリ装置である、と言えることになります。 フリップフロップ回路もシーソーと同じように、最後にかけられた信号の状態を保持するような回路です。 JKフリップフロップとは フリップフロップ回路には、いくつかの種類があります。 その中のひとつであるJKフリップフロップ回路(以下 JK-FF) は、JとKのふたつの値とクロック(CLK)、合計3つの入力端子を持っています。 また、出力としてはQと~Qのふたつの端子を持っています。「~Q」は、「Qの逆」という意味で、Q=0なら~Q=1、Q=1なら~Q=0という関係です。 まずはふたつの値、JとKについて説明します。 (J,K)=(1,0)のときは、フリップフロップ回路の状態には「1」がセットされます。右に傾いた状態です。このときフリップフロップ回路の出力も「1」になります。 (J,K)=(0,1)のときは、同様に値0がセットされます。左に傾いた状態です。フリップフロップ回路の出力は「0」になります。 (J,K)=(0,0)のとき、つまり入力が何もないときは、前の状態を保持します。シーソーどちらにも荷物がない状態なので、 最後に傾いた方を覚えています。フリップフロップ回路の出力は、直前の出力が継続されます。 次にJK-FFのいちばんの特徴である、(J,K)=(1,1)のときの動きです。シーソーに喩えると、「両側に荷物を載せた状態」 とも言える、特殊な入力です。シーソーであれば、右にも左にも傾かず、水平に釣り合ってしまいます。ディジタル回路では、 これはすこしマズそうです。0でも1でもないものは存在させられないですから。なので、JK-FFでは、(J,K)=(1,1)のときは、 「いまの状態を反転させる」という特別な動きを定義しています。いまが1なら0をセット、いまが0なら1をセット、 というように動きます。したがって、フリップフロップからの出力も、直前の出力と逆のものに切り替えられます。 これを利用することで、JK-FFの状態を0→1→0→1…と順に変更していくことができます。 なにかに使えそうです。 ただしひとつ注意すべき点があります。(J,K)=(1,1)の状態をずっと続けるとどうなるでしょうか。 これもシーソーに喩えると、シーソーが右に左にバタバタしてしまいそうです。「ボタンを押したらシーソーが 左右に倒れる装置」があったとして、そのボタンをポチ・ポチ・…と押しているときはいいのですが、 「長押し」をしてしまうとよくなさそうです。さらに付け加えると、「長押し」でなかったとしても、 ポチっと1回だけ押したつもりがバタバタっと左右に2回分の動きになってしまうかも知れません。 これはまずいです。 最後に出力Q・~Qですが、これはJKーFFの状態により、0または1のどちらかが出力されます。 JK-FFの状態が0のときはQ=0が、JK-FFの状態が1のときはQ=1がそれぞれ出力されます。 先に述べたように(J,K)=(1,1)が入力され、JK-FFの状態が0→1→0→1…と切り替わるとき、 出力Qの値も、Q=0→1→0→1…と切り替わります。 クロックとは そこでJK-FFにもうひとつ、クロック(CLK)という入力を付けます。 クロックというのは、「CPUの最大クロックは5.4GHz」などと言っている、このクロックと同じものです。 クロックを喩えるなら、「メトロノーム」を思い浮かべてください。音楽の時間に使う、カチコチとリズムを取る、あれです。 ディジタル回路のさまざまな動作を、メトロノームがカチコチとリズムを刻んだ瞬間だけ動作するように制御します。 (J,K)=(1,1)の入力をずっと続けていても、メトロノームがカチコチと刻んだタイミングだけ、シーソーが倒れるように すればよい、というアイディアです。この「メトロノームがカチコチと刻んだタイミング」のことを、「クロックタイミング」といいます。クロックタイミングが1秒間に何回訪れるかを示すのが「クロック周波数」です。 ですので、クロック周波数が速いと、クロックタイミングがより頻繁に訪れ、よってシーソーの倒れる頻度が高くなります。 「CPUのクロック周波数が速いと計算能力も高い」ということと、なんとなくですが結び付きそうです。 ちなみにクロック波形には通常は矩形波(方形波)を入力しますが、矩形波の立ち上がりまたは立ち下がり どちらかの瞬間をクロックタイミングとするケースが多いです。SimcirJSのJK-FFでは、 立ち下がりの瞬間をクロックタイミングとしています。PushOnボタン操作でいうと、押して、離した瞬間が クロックタイミングになります。(これ、実は後の伏線になっています。) クロック波形の例 カウンタ回路を作る 前置きが長くなりましたが、このJK-FFを使って、カウンタ回路を作ることができます。 クロック入力付きのJK-FF回路は、SimcirJSに初めから登録されているので、これを利用しましょう。 SimcirJSのJKフリップフロップ回路 JK-FFの「(J,K)=(1,1)のときはいまの状態を反転させる」を利用すると、 J=K=1のとき、クロックタイミングごとに 状態がOFF→ON→OFF→ON…と変化していきます。 状態が変化するので、あわせて出力も0→1→0→1…と変化します。 これは、見方を変えると、入力をJ=K=1に固定しておくと、クロックが入るごとに状態が変わることになります。 さらに言い換えると、クロックが入った回数を、1回だけ数えている、ということになります。 記憶できる値が0か1かの1bitなので、すぐに桁あふれ(オーバーフロー)を起こしてしまいますが 動作としては「カウンタ」と言えるものができます。「1bitのカウンタ」です。 SimcirJSでJK-FFで1bitカウンタを作る ただ、数えられる値があまりに少なすぎて、「カウンタ」というイメージがあまり湧きません。 どうにかして、もう少し多くの値を数えられるようにならないでしょうか。 桁あふれ(オーバーフロー)してしまうのが問題なので、「桁上がり」の仕組みを作れば、桁数を増やして 数えられる値を増やしていけそうです。 桁上がりの仕組みなので、0→1になって、次に1→0になるときに、上位桁をひとつ繰り上げればよいことになります。 というのはよいのですが、実際問題として「1→0になるとき」を検出するのは、どうすればよいでしょうか。 1→0になる、ON→OFFになる、矩形波が立ち下がる、…。そうなんです、実は、JK-FFの出力(Q)を、 そのまま次のJK-FFのCLKに入れるだけでいいんです。「立ち下がりがクロックタイミングになっている」のは この応用を見越した伏線だったのです。非常に巧妙な設計ですね。 ということで、こういう回路を組んでみます。実際どうなるか SimcirJSでJK-FFで2bitカウンタを作る …と、2bitのカウンタができあがりました。2bitなので4値、つまり0→1→2→3→0→1→2→3→…、 と数えられます。同じように後ろに後ろにJK-FFをつなげていくと、3bit、4bit、…のカウンタが作れます。 冒頭で「2 n 進カウンタはとても簡単に作れる」と記載しましたが、このとおりです。 3進カウンタへの道 どうやって3進に さて、欲しいのは3進カウンタでした。つまり0→1→2→0→1→2→… と繰り返してほしいのです。 ぱっと思いつくのは、3より大きな最小の2 n 数である4進カウンタを使って、 0→1→2→3→0→1→2→3→…となるところの「余計なもの」つまり「→3」の部分を削る方法です。 「カウンタ状態が3のときにも、表示上は2と同じにする」という方法もありますが、 この場合0→1→2→2→0→1→2→2→…という、ちょっとコレジャナイ感のカウンタができてしまうので却下とします。 強制リセット 「→3 の部分を削る」と書きましたが、「→3」の瞬間で状態をリセットしてしまうと、→3が削られることになります。 JK-FFの状態を無理やりリセットできると便利そうです。 市販の実際のICチップでは、リセット(クリア)のついたJK-FFがいろいろ販売されています。 例えばテキサスインスツルメンツ社 SN74LS73AN、や、東芝社 TC74HC107AF などがそうです。 ですが、SimcirJSで用意されているJK-FFには、リセット端子はありません。 なければ作る!? 筆者は実家が町工場でして、幼少の頃より「なければ作る、壊れたら直す」の精神で生きてきました(若干誇張)。 「リセット付きJK-FF」も、ANDやORを組み合わせれば作れますので、なければ作るの精神で臨んでみましょう。 ちなみに元から用意されているJK-FFも、実はANDやORの組み合わせで作られています。JK-FFのパーツを ダブルクリックすると SimcirJSの標準準備のJK-FFの中身 のように、登録されている回路が出てきます。高級言語のライブラリ関数のようなものですね。 なので、同じように「リセット付きJK-FF」をひとつ自作して、登録してしまいましょう。 リセット付きJK-FF ということで、もとのJK-FFの中身を参考にしながら、リセット付きJK-FFの回路を作りました。 SimcirJSでのリセット付きJKフリップフロップ構成例 リセット付きJKフリップフロップの回路図例 これを解説しているとちょっと長くなりそうなので、すいませんが、ポイントだけにします。 SimcirJS標準のJK-FFの回路にリセット(RST)入力端子を付け足して、ここに入力があった際には、両方の「RS-FF」にリセット入力(~S,~R)=(1,0)が入るようにしました(青破線部) (このとき、SimcirJS標準搭載のRS-FFは、入力が(S,R)ではなく、(~S,~R)であることに注意) その他の部分はSimcirJS標準のJK-FFの回路を基にしていますが、「3入力NAND」回路を、2入力AND回路とNOT回路に分解しています(赤破線部) この通り作るか、またはJSONコードを載せておくので、使ってみてください。 リセット付きJK-FFのJSONコードを表示 { "width":800, "height":400, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"DC","id":"dev0","x":16,"y":144,"label":"DC"}, {"type":"Toggle","id":"dev1","x":80,"y":88,"label":"Toggle","state":{"on":false}}, {"type":"PushOn","id":"dev2","x":80,"y":144,"label":"PushOn"}, {"type":"PushOn","id":"dev3","x":80,"y":304,"label":"PushOn"}, {"type":"Toggle","id":"dev4","x":80,"y":208,"label":"Toggle","state":{"on":false}}, {"type":"AND","id":"dev5","x":208,"y":112,"label":"AND"}, {"type":"AND","id":"dev6","x":208,"y":184,"label":"AND"}, {"type":"AND","id":"dev7","x":264,"y":112,"label":"AND"}, {"type":"NOT","id":"dev8","x":328,"y":112,"label":"NOT"}, {"type":"NOT","id":"dev9","x":328,"y":184,"label":"NOT"}, {"type":"RS-FF","id":"dev10","x":384,"y":144,"label":"RS-FF"}, {"type":"NAND","id":"dev11","x":472,"y":112,"label":"NAND"}, {"type":"NAND","id":"dev12","x":472,"y":184,"label":"NAND"}, {"type":"NOT","id":"dev13","x":288,"y":320,"label":"NOT"}, {"type":"AND","id":"dev14","x":344,"y":320,"label":"AND"}, {"type":"OR","id":"dev15","x":312,"y":272,"label":"OR"}, {"type":"NOT","id":"dev16","x":472,"y":320,"label":"NOT"}, {"type":"AND","id":"dev17","x":536,"y":320,"label":"AND"}, {"type":"LED","id":"dev18","x":424,"y":32,"label":"LED"}, {"type":"LED","id":"dev19","x":576,"y":32,"label":"LED"}, {"type":"NOT","id":"dev20","x":208,"y":32,"label":"NOT"}, {"type":"OR","id":"dev21","x":512,"y":272,"label":"OR"}, {"type":"RS-FF","id":"dev22","x":552,"y":144,"label":"RS-FF"}, {"type":"AND","id":"dev23","x":264,"y":184,"label":"AND"}, {"type":"In","id":"dev24","x":144,"y":88,"label":"J"}, {"type":"In","id":"dev25","x":144,"y":144,"label":"CLK"}, {"type":"In","id":"dev26","x":144,"y":208,"label":"K"}, {"type":"In","id":"dev27","x":144,"y":304,"label":"RST"}, {"type":"Out","id":"dev28","x":632,"y":96,"label":"Q"}, {"type":"Out","id":"dev29","x":632,"y":192,"label":"~Q"} ], "connectors":[ {"from":"dev1.in0","to":"dev0.out0"}, {"from":"dev2.in0","to":"dev0.out0"}, {"from":"dev3.in0","to":"dev0.out0"}, {"from":"dev4.in0","to":"dev0.out0"}, {"from":"dev5.in0","to":"dev24.out0"}, {"from":"dev5.in1","to":"dev25.out0"}, {"from":"dev6.in0","to":"dev25.out0"}, {"from":"dev6.in1","to":"dev26.out0"}, {"from":"dev7.in0","to":"dev5.out0"}, {"from":"dev7.in1","to":"dev22.out1"}, {"from":"dev8.in0","to":"dev7.out0"}, {"from":"dev9.in0","to":"dev23.out0"}, {"from":"dev10.in0","to":"dev15.out0"}, {"from":"dev10.in1","to":"dev14.out0"}, {"from":"dev11.in0","to":"dev20.out0"}, {"from":"dev11.in1","to":"dev10.out0"}, {"from":"dev12.in0","to":"dev10.out1"}, {"from":"dev12.in1","to":"dev20.out0"}, {"from":"dev13.in0","to":"dev27.out0"}, {"from":"dev14.in0","to":"dev9.out0"}, {"from":"dev14.in1","to":"dev13.out0"}, {"from":"dev15.in0","to":"dev8.out0"}, {"from":"dev15.in1","to":"dev27.out0"}, {"from":"dev16.in0","to":"dev27.out0"}, {"from":"dev17.in0","to":"dev12.out0"}, {"from":"dev17.in1","to":"dev16.out0"}, {"from":"dev18.in0","to":"dev10.out0"}, {"from":"dev19.in0","to":"dev22.out0"}, {"from":"dev20.in0","to":"dev25.out0"}, {"from":"dev21.in0","to":"dev11.out0"}, {"from":"dev21.in1","to":"dev27.out0"}, {"from":"dev22.in0","to":"dev21.out0"}, {"from":"dev22.in1","to":"dev17.out0"}, {"from":"dev23.in0","to":"dev22.out0"}, {"from":"dev23.in1","to":"dev6.out0"}, {"from":"dev24.in0","to":"dev1.out0"}, {"from":"dev25.in0","to":"dev2.out0"}, {"from":"dev26.in0","to":"dev4.out0"}, {"from":"dev27.in0","to":"dev3.out0"}, {"from":"dev28.in0","to":"dev22.out0"}, {"from":"dev29.in0","to":"dev22.out1"} ] } 説明していませんでしたが、SimcirJSの回路は、全部JSONで表記が可能です。 ワークエリアを Ctrl+クリック すると表示が切り替わります。もういちどCtrl+クリックすると元の表示に戻ります。 (Mac系マシンご利用の場合は Command+クリック です) SimcirJSの画面例 JSONコード表示 登録のやり方・・・ さて登録だ、と思ったのですが、登録方法は「simcir-library.js ファイルに回路定義した内容を追記する」とのこと。 ということは、サイト上のSimcirJSの設定ファイルの編集権限が必要、ということになります。 つまり、誰かが立てたサイト上のSimcirJSを使わせてもらってるのではだめで、 自分でhttpdを立てて、自らSimcirJSをインストールせよ、ということになります。 httpdサーバ 筆者はたまたま手元にESXi環境があり、LinuxOSの仮想マシンが稼働していたので、ここにさっとhttpdを立てて、SimcirJSを置いてみました。 サイトさえ立てばよく、必ずしもLinuxである必要はありませんので、手元にLinux環境がない場合は、Windowsのローカル環境にhttpdを 立てるやり方でも大丈夫です。 SimcirJSの入手方法については、いま https://kazuhikoarase.github.io/simcirjs/index.html を利用しているなら、画面上の方にダウンロードボタンがあります。 SimcirJSのダウンロード または https://github.com/kazuhikoarase/simcirjs/archive/master.zip からダウンロードできます(同じリンク先です)。 インストールといっても./configure も make も不要で、ダウンロードしたZIPファイルを展開し、 そのファイル群を httpd の DocumentRoot 下のどこかにコピーするだけの簡単インストールです。 そのまま展開するとフォルダ名が「simcirjs-master」になりますが、ちょっと長いのでフォルダ名を「simcirjs」とし、 DocumentRoot直下の、/var/www/html/simcirjs/ に、一式をコピーしました。 あと、そのままでは index.html ファイルがなく、アクセスが少し面倒なので、sample.html をそのままコピーして index.html を作っておきました。 これで、自PC上のブラウザから、自前のSimcirJSが利用できるようになりました。 自作部品の登録 自作部品の登録は先は [DocumentRoot]/simcirjs/simcir-library.js のファイルです。先ほど作った「リセット付きJK-FF」を いちばん最後に追記しておきます。追記後の simcir-library.js を掲載しますが、ちょっと長いので、スクロールしてご覧ください。 部品の名称は「JK-FF-RST」としました。 // // SimcirJS - library // // Copyright (c) 2014 Kazuhiko Arase // // URL: http://www.d-project.com/ // // Licensed under the MIT license: // http://www.opensource.org/licenses/mit-license.php // // includes following device types: // RS-FF // JK-FF // T-FF // D-FF // 8bitCounter // HalfAdder // FullAdder // 4bitAdder // 2to4BinaryDecoder // 3to8BinaryDecoder // 4to16BinaryDecoder simcir.registerDevice('RS-FF', { "width":320, "height":160, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"NAND","id":"dev0","x":184,"y":32,"label":"NAND"}, {"type":"NAND","id":"dev1","x":184,"y":80,"label":"NAND"}, {"type":"In","id":"dev2","x":136,"y":24,"label":"~S"}, {"type":"In","id":"dev3","x":136,"y":88,"label":"~R"}, {"type":"Out","id":"dev4","x":232,"y":32,"label":"Q"}, {"type":"Out","id":"dev5","x":232,"y":80,"label":"~Q"}, {"type":"PushOff","id":"dev6","x":88,"y":24,"label":"PushOff"}, {"type":"PushOff","id":"dev7","x":88,"y":88,"label":"PushOff"}, {"type":"DC","id":"dev8","x":40,"y":56,"label":"DC"} ], "connectors":[ {"from":"dev0.in0","to":"dev2.out0"}, {"from":"dev0.in1","to":"dev1.out0"}, {"from":"dev1.in0","to":"dev0.out0"}, {"from":"dev1.in1","to":"dev3.out0"}, {"from":"dev2.in0","to":"dev6.out0"}, {"from":"dev3.in0","to":"dev7.out0"}, {"from":"dev4.in0","to":"dev0.out0"}, {"from":"dev5.in0","to":"dev1.out0"}, {"from":"dev6.in0","to":"dev8.out0"}, {"from":"dev7.in0","to":"dev8.out0"} ] } ); simcir.registerDevice('JK-FF', { "width":480, "height":240, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"RS-FF","id":"dev0","x":216,"y":112,"label":"RS-FF"}, {"type":"RS-FF","id":"dev1","x":344,"y":112,"label":"RS-FF"}, {"type":"NAND","numInputs":3,"id":"dev2","x":168,"y":80,"label":"NAND"}, {"type":"NAND","numInputs":3,"id":"dev3","x":168,"y":144,"label":"NAND"}, {"type":"NAND","id":"dev4","x":296,"y":80,"label":"NAND"}, {"type":"NAND","id":"dev5","x":296,"y":144,"label":"NAND"}, {"type":"NOT","id":"dev6","x":168,"y":24,"label":"NOT"}, {"type":"In","id":"dev7","x":120,"y":64,"label":"J"}, {"type":"In","id":"dev8","x":120,"y":112,"label":"CLK"}, {"type":"In","id":"dev9","x":120,"y":160,"label":"K"}, {"type":"Out","id":"dev10","x":424,"y":80,"label":"Q"}, {"type":"Out","id":"dev11","x":424,"y":144,"label":"~Q"}, {"type":"Toggle","id":"dev12","x":72,"y":64,"label":"Toggle"}, {"type":"PushOn","id":"dev13","x":72,"y":112,"label":"PushOn"}, {"type":"Toggle","id":"dev14","x":72,"y":160,"label":"Toggle"}, {"type":"DC","id":"dev15","x":24,"y":112,"label":"DC"} ], "connectors":[ {"from":"dev0.in0","to":"dev2.out0"}, {"from":"dev0.in1","to":"dev3.out0"}, {"from":"dev1.in0","to":"dev4.out0"}, {"from":"dev1.in1","to":"dev5.out0"}, {"from":"dev2.in0","to":"dev1.out1"}, {"from":"dev2.in1","to":"dev7.out0"}, {"from":"dev2.in2","to":"dev8.out0"}, {"from":"dev3.in0","to":"dev8.out0"}, {"from":"dev3.in1","to":"dev9.out0"}, {"from":"dev3.in2","to":"dev1.out0"}, {"from":"dev4.in0","to":"dev6.out0"}, {"from":"dev4.in1","to":"dev0.out0"}, {"from":"dev5.in0","to":"dev0.out1"}, {"from":"dev5.in1","to":"dev6.out0"}, {"from":"dev6.in0","to":"dev8.out0"}, {"from":"dev7.in0","to":"dev12.out0"}, {"from":"dev8.in0","to":"dev13.out0"}, {"from":"dev9.in0","to":"dev14.out0"}, {"from":"dev10.in0","to":"dev1.out0"}, {"from":"dev11.in0","to":"dev1.out1"}, {"from":"dev12.in0","to":"dev15.out0"}, {"from":"dev13.in0","to":"dev15.out0"}, {"from":"dev14.in0","to":"dev15.out0"} ] } ); simcir.registerDevice('T-FF', { "width":320, "height":160, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"JK-FF","id":"dev0","x":168,"y":48,"label":"JK-FF"}, {"type":"In","id":"dev1","x":120,"y":32,"label":"T"}, {"type":"In","id":"dev2","x":120,"y":80,"label":"CLK"}, {"type":"Out","id":"dev3","x":248,"y":32,"label":"Q"}, {"type":"Out","id":"dev4","x":248,"y":80,"label":"~Q"}, {"type":"Toggle","id":"dev5","x":72,"y":32,"label":"Toggle"}, {"type":"PushOn","id":"dev6","x":72,"y":80,"label":"PushOn"}, {"type":"DC","id":"dev7","x":24,"y":56,"label":"DC"} ], "connectors":[ {"from":"dev0.in0","to":"dev1.out0"}, {"from":"dev0.in1","to":"dev2.out0"}, {"from":"dev0.in2","to":"dev1.out0"}, {"from":"dev1.in0","to":"dev5.out0"}, {"from":"dev2.in0","to":"dev6.out0"}, {"from":"dev3.in0","to":"dev0.out0"}, {"from":"dev4.in0","to":"dev0.out1"}, {"from":"dev5.in0","to":"dev7.out0"}, {"from":"dev6.in0","to":"dev7.out0"} ] } ); simcir.registerDevice('D-FF', { "width":540, "height":200, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"In","id":"dev0","x":128,"y":24,"label":"D"}, {"type":"In","id":"dev1","x":168,"y":128,"label":"CLK"}, {"type":"NOT","id":"dev2","x":176,"y":64,"label":"NOT"}, {"type":"NAND","id":"dev3","x":224,"y":32,"label":"NAND"}, {"type":"NAND","id":"dev4","x":224,"y":96,"label":"NAND"}, {"type":"RS-FF","id":"dev5","x":272,"y":64,"label":"RS-FF"}, {"type":"NOT","id":"dev6","x":296,"y":128,"label":"NOT"}, {"type":"NAND","id":"dev7","x":352,"y":32,"label":"NAND"}, {"type":"NAND","id":"dev8","x":352,"y":96,"label":"NAND"}, {"type":"RS-FF","id":"dev9","x":400,"y":64,"label":"RS-FF"}, {"type":"Out","id":"dev10","x":480,"y":32,"label":"Q"}, {"type":"Out","id":"dev11","x":480,"y":96,"label":"~Q"}, {"type":"Toggle","id":"dev12","x":80,"y":24,"label":"Toggle"}, {"type":"PushOn","id":"dev13","x":80,"y":128,"label":"PushOn"}, {"type":"DC","id":"dev14","x":32,"y":72,"label":"DC"} ], "connectors":[ {"from":"dev0.in0","to":"dev12.out0"}, {"from":"dev1.in0","to":"dev13.out0"}, {"from":"dev2.in0","to":"dev0.out0"}, {"from":"dev3.in0","to":"dev0.out0"}, {"from":"dev3.in1","to":"dev1.out0"}, {"from":"dev4.in0","to":"dev1.out0"}, {"from":"dev4.in1","to":"dev2.out0"}, {"from":"dev5.in0","to":"dev3.out0"}, {"from":"dev5.in1","to":"dev4.out0"}, {"from":"dev6.in0","to":"dev1.out0"}, {"from":"dev7.in0","to":"dev5.out0"}, {"from":"dev7.in1","to":"dev6.out0"}, {"from":"dev8.in0","to":"dev6.out0"}, {"from":"dev8.in1","to":"dev5.out1"}, {"from":"dev9.in0","to":"dev7.out0"}, {"from":"dev9.in1","to":"dev8.out0"}, {"from":"dev10.in0","to":"dev9.out0"}, {"from":"dev11.in0","to":"dev9.out1"}, {"from":"dev12.in0","to":"dev14.out0"}, {"from":"dev13.in0","to":"dev14.out0"} ] } ); simcir.registerDevice('8bitCounter', { "width":320, "height":420, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"T-FF","id":"dev0","x":184,"y":16,"label":"T-FF"}, {"type":"T-FF","id":"dev1","x":184,"y":64,"label":"T-FF"}, {"type":"T-FF","id":"dev2","x":184,"y":112,"label":"T-FF"}, {"type":"T-FF","id":"dev3","x":184,"y":160,"label":"T-FF"}, {"type":"T-FF","id":"dev4","x":184,"y":208,"label":"T-FF"}, {"type":"T-FF","id":"dev5","x":184,"y":256,"label":"T-FF"}, {"type":"T-FF","id":"dev6","x":184,"y":304,"label":"T-FF"}, {"type":"T-FF","id":"dev7","x":184,"y":352,"label":"T-FF"}, {"type":"Out","id":"dev8","x":264,"y":16,"label":"D0"}, {"type":"Out","id":"dev9","x":264,"y":64,"label":"D1"}, {"type":"Out","id":"dev10","x":264,"y":112,"label":"D2"}, {"type":"Out","id":"dev11","x":264,"y":160,"label":"D3"}, {"type":"Out","id":"dev12","x":264,"y":208,"label":"D4"}, {"type":"Out","id":"dev13","x":264,"y":256,"label":"D5"}, {"type":"Out","id":"dev14","x":264,"y":304,"label":"D6"}, {"type":"Out","id":"dev15","x":264,"y":352,"label":"D7"}, {"type":"In","id":"dev16","x":120,"y":16,"label":"T"}, {"type":"In","id":"dev17","x":120,"y":112,"label":"CLK"}, {"type":"PushOn","id":"dev18","x":72,"y":112,"label":"PushOn"}, {"type":"DC","id":"dev19","x":24,"y":16,"label":"DC"}, {"type":"Toggle","id":"dev20","x":72,"y":16,"label":"Toggle"} ], "connectors":[ {"from":"dev0.in0","to":"dev16.out0"}, {"from":"dev0.in1","to":"dev17.out0"}, {"from":"dev1.in0","to":"dev16.out0"}, {"from":"dev1.in1","to":"dev0.out0"}, {"from":"dev2.in0","to":"dev16.out0"}, {"from":"dev2.in1","to":"dev1.out0"}, {"from":"dev3.in0","to":"dev16.out0"}, {"from":"dev3.in1","to":"dev2.out0"}, {"from":"dev4.in0","to":"dev16.out0"}, {"from":"dev4.in1","to":"dev3.out0"}, {"from":"dev5.in0","to":"dev16.out0"}, {"from":"dev5.in1","to":"dev4.out0"}, {"from":"dev6.in0","to":"dev16.out0"}, {"from":"dev6.in1","to":"dev5.out0"}, {"from":"dev7.in0","to":"dev16.out0"}, {"from":"dev7.in1","to":"dev6.out0"}, {"from":"dev8.in0","to":"dev0.out0"}, {"from":"dev9.in0","to":"dev1.out0"}, {"from":"dev10.in0","to":"dev2.out0"}, {"from":"dev11.in0","to":"dev3.out0"}, {"from":"dev12.in0","to":"dev4.out0"}, {"from":"dev13.in0","to":"dev5.out0"}, {"from":"dev14.in0","to":"dev6.out0"}, {"from":"dev15.in0","to":"dev7.out0"}, {"from":"dev16.in0","to":"dev20.out0"}, {"from":"dev17.in0","to":"dev18.out0"}, {"from":"dev18.in0","to":"dev19.out0"}, {"from":"dev20.in0","to":"dev19.out0"} ] } ); simcir.registerDevice('HalfAdder', { "width":320, "height":160, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"Toggle","id":"dev0","x":96,"y":80,"label":"Toggle"}, {"type":"DC","id":"dev1","x":48,"y":56,"label":"DC"}, {"type":"AND","id":"dev2","x":192,"y":80,"label":"AND"}, {"type":"XOR","id":"dev3","x":192,"y":32,"label":"XOR"}, {"type":"In","id":"dev4","x":144,"y":32,"label":"A"}, {"type":"In","id":"dev5","x":144,"y":80,"label":"B"}, {"type":"Out","id":"dev6","x":240,"y":32,"label":"S"}, {"type":"Out","id":"dev7","x":240,"y":80,"label":"C"}, {"type":"Toggle","id":"dev8","x":96,"y":32,"label":"Toggle"} ], "connectors":[ {"from":"dev0.in0","to":"dev1.out0"}, {"from":"dev2.in0","to":"dev4.out0"}, {"from":"dev2.in1","to":"dev5.out0"}, {"from":"dev3.in0","to":"dev4.out0"}, {"from":"dev3.in1","to":"dev5.out0"}, {"from":"dev4.in0","to":"dev8.out0"}, {"from":"dev5.in0","to":"dev0.out0"}, {"from":"dev6.in0","to":"dev3.out0"}, {"from":"dev7.in0","to":"dev2.out0"}, {"from":"dev8.in0","to":"dev1.out0"} ] } ); simcir.registerDevice('FullAdder', { "width":440, "height":200, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"In","id":"dev0","x":120,"y":32,"label":"Cin"}, {"type":"In","id":"dev1","x":120,"y":80,"label":"A"}, {"type":"In","id":"dev2","x":120,"y":128,"label":"B"}, {"type":"Toggle","id":"dev3","x":72,"y":32,"label":"Toggle"}, {"type":"Toggle","id":"dev4","x":72,"y":80,"label":"Toggle"}, {"type":"Toggle","id":"dev5","x":72,"y":128,"label":"Toggle"}, {"type":"DC","id":"dev6","x":24,"y":80,"label":"DC"}, {"type":"HalfAdder","id":"dev7","x":168,"y":104,"label":"HalfAdder"}, {"type":"HalfAdder","id":"dev8","x":248,"y":56,"label":"HalfAdder"}, {"type":"OR","id":"dev9","x":328,"y":104,"label":"OR"}, {"type":"Out","id":"dev10","x":376,"y":104,"label":"Cout"}, {"type":"Out","id":"dev11","x":376,"y":48,"label":"S"} ], "connectors":[ {"from":"dev0.in0","to":"dev3.out0"}, {"from":"dev1.in0","to":"dev4.out0"}, {"from":"dev2.in0","to":"dev5.out0"}, {"from":"dev3.in0","to":"dev6.out0"}, {"from":"dev4.in0","to":"dev6.out0"}, {"from":"dev5.in0","to":"dev6.out0"}, {"from":"dev7.in0","to":"dev1.out0"}, {"from":"dev7.in1","to":"dev2.out0"}, {"from":"dev8.in0","to":"dev0.out0"}, {"from":"dev8.in1","to":"dev7.out0"}, {"from":"dev9.in0","to":"dev8.out1"}, {"from":"dev9.in1","to":"dev7.out1"}, {"from":"dev10.in0","to":"dev9.out0"}, {"from":"dev11.in0","to":"dev8.out0"} ] } ); simcir.registerDevice('4bitAdder', { "width":280, "height":480, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"FullAdder","id":"dev0","x":120,"y":72,"label":"FullAdder"}, {"type":"FullAdder","id":"dev1","x":120,"y":136,"label":"FullAdder"}, {"type":"FullAdder","id":"dev2","x":120,"y":200,"label":"FullAdder"}, {"type":"FullAdder","id":"dev3","x":120,"y":264,"label":"FullAdder"}, {"type":"In","id":"dev4","x":40,"y":80,"label":"A0"}, {"type":"In","id":"dev5","x":40,"y":128,"label":"A1"}, {"type":"In","id":"dev6","x":40,"y":176,"label":"A2"}, {"type":"In","id":"dev7","x":40,"y":224,"label":"A3"}, {"type":"In","id":"dev8","x":40,"y":272,"label":"B0"}, {"type":"In","id":"dev9","x":40,"y":320,"label":"B1"}, {"type":"In","id":"dev10","x":40,"y":368,"label":"B2"}, {"type":"In","id":"dev11","x":40,"y":416,"label":"B3"}, {"type":"Out","id":"dev12","x":200,"y":72,"label":"S0"}, {"type":"Out","id":"dev13","x":200,"y":120,"label":"S1"}, {"type":"Out","id":"dev14","x":200,"y":168,"label":"S2"}, {"type":"Out","id":"dev15","x":200,"y":216,"label":"S3"}, {"type":"Out","id":"dev16","x":200,"y":280,"label":"Cout"}, {"type":"In","id":"dev17","x":40,"y":24,"label":"Cin"} ], "connectors":[ {"from":"dev0.in0","to":"dev17.out0"}, {"from":"dev0.in1","to":"dev4.out0"}, {"from":"dev0.in2","to":"dev8.out0"}, {"from":"dev1.in0","to":"dev0.out1"}, {"from":"dev1.in1","to":"dev5.out0"}, {"from":"dev1.in2","to":"dev9.out0"}, {"from":"dev2.in0","to":"dev1.out1"}, {"from":"dev2.in1","to":"dev6.out0"}, {"from":"dev2.in2","to":"dev10.out0"}, {"from":"dev3.in0","to":"dev2.out1"}, {"from":"dev3.in1","to":"dev7.out0"}, {"from":"dev3.in2","to":"dev11.out0"}, {"from":"dev12.in0","to":"dev0.out0"}, {"from":"dev13.in0","to":"dev1.out0"}, {"from":"dev14.in0","to":"dev2.out0"}, {"from":"dev15.in0","to":"dev3.out0"}, {"from":"dev16.in0","to":"dev3.out1"} ] } ); simcir.registerDevice('2to4BinaryDecoder', { "width":400, "height":240, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"AND","numInputs":3,"id":"dev0","x":280,"y":24,"label":"AND"}, {"type":"AND","numInputs":3,"id":"dev1","x":280,"y":72,"label":"AND"}, {"type":"AND","numInputs":3,"id":"dev2","x":280,"y":120,"label":"AND"}, {"type":"NOT","id":"dev3","x":192,"y":48,"label":"NOT"}, {"type":"AND","numInputs":3,"id":"dev4","x":280,"y":168,"label":"AND"}, {"type":"NOT","id":"dev5","x":192,"y":96,"label":"NOT"}, {"type":"In","id":"dev6","x":192,"y":176,"label":"OE"}, {"type":"In","id":"dev7","x":128,"y":48,"label":"D0"}, {"type":"In","id":"dev8","x":128,"y":96,"label":"D1"}, {"type":"Toggle","id":"dev9","x":80,"y":48,"label":"Toggle"}, {"type":"Toggle","id":"dev10","x":80,"y":96,"label":"Toggle"}, {"type":"DC","id":"dev11","x":32,"y":96,"label":"DC"}, {"type":"Out","id":"dev12","x":328,"y":24,"label":"A0"}, {"type":"Out","id":"dev13","x":328,"y":72,"label":"A1"}, {"type":"Out","id":"dev14","x":328,"y":120,"label":"A2"}, {"type":"Out","id":"dev15","x":328,"y":168,"label":"A3"}, {"type":"Toggle","id":"dev16","x":80,"y":144,"label":"Toggle"} ], "connectors":[ {"from":"dev0.in0","to":"dev3.out0"}, {"from":"dev0.in1","to":"dev5.out0"}, {"from":"dev0.in2","to":"dev6.out0"}, {"from":"dev1.in0","to":"dev7.out0"}, {"from":"dev1.in1","to":"dev5.out0"}, {"from":"dev1.in2","to":"dev6.out0"}, {"from":"dev2.in0","to":"dev3.out0"}, {"from":"dev2.in1","to":"dev8.out0"}, {"from":"dev2.in2","to":"dev6.out0"}, {"from":"dev3.in0","to":"dev7.out0"}, {"from":"dev4.in0","to":"dev7.out0"}, {"from":"dev4.in1","to":"dev8.out0"}, {"from":"dev4.in2","to":"dev6.out0"}, {"from":"dev5.in0","to":"dev8.out0"}, {"from":"dev6.in0","to":"dev16.out0"}, {"from":"dev7.in0","to":"dev9.out0"}, {"from":"dev8.in0","to":"dev10.out0"}, {"from":"dev9.in0","to":"dev11.out0"}, {"from":"dev10.in0","to":"dev11.out0"}, {"from":"dev12.in0","to":"dev0.out0"}, {"from":"dev13.in0","to":"dev1.out0"}, {"from":"dev14.in0","to":"dev2.out0"}, {"from":"dev15.in0","to":"dev4.out0"}, {"from":"dev16.in0","to":"dev11.out0"} ] } ); simcir.registerDevice('3to8BinaryDecoder', { "width":360, "height":440, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"In","id":"dev0","x":24,"y":144,"label":"D0"}, {"type":"In","id":"dev1","x":24,"y":192,"label":"D1"}, {"type":"In","id":"dev2","x":24,"y":240,"label":"D2"}, {"type":"In","id":"dev3","x":24,"y":304,"label":"OE"}, {"type":"NOT","id":"dev4","x":72,"y":240,"label":"NOT"}, {"type":"AND","id":"dev5","x":120,"y":248,"label":"AND"}, {"type":"AND","id":"dev6","x":120,"y":296,"label":"AND"}, {"type":"2to4BinaryDecoder","id":"dev7","x":184,"y":144,"label":"2to4BinaryDecoder"}, {"type":"2to4BinaryDecoder","id":"dev8","x":184,"y":224,"label":"2to4BinaryDecoder"}, {"type":"Out","id":"dev9","x":296,"y":32,"label":"A0"}, {"type":"Out","id":"dev10","x":296,"y":80,"label":"A1"}, {"type":"Out","id":"dev11","x":296,"y":128,"label":"A2"}, {"type":"Out","id":"dev12","x":296,"y":176,"label":"A3"}, {"type":"Out","id":"dev13","x":296,"y":224,"label":"A4"}, {"type":"Out","id":"dev14","x":296,"y":272,"label":"A5"}, {"type":"Out","id":"dev15","x":296,"y":320,"label":"A6"}, {"type":"Out","id":"dev16","x":296,"y":368,"label":"A7"} ], "connectors":[ {"from":"dev4.in0","to":"dev2.out0"}, {"from":"dev5.in0","to":"dev4.out0"}, {"from":"dev5.in1","to":"dev3.out0"}, {"from":"dev6.in0","to":"dev2.out0"}, {"from":"dev6.in1","to":"dev3.out0"}, {"from":"dev7.in0","to":"dev0.out0"}, {"from":"dev7.in1","to":"dev1.out0"}, {"from":"dev7.in2","to":"dev5.out0"}, {"from":"dev8.in0","to":"dev0.out0"}, {"from":"dev8.in1","to":"dev1.out0"}, {"from":"dev8.in2","to":"dev6.out0"}, {"from":"dev9.in0","to":"dev7.out0"}, {"from":"dev10.in0","to":"dev7.out1"}, {"from":"dev11.in0","to":"dev7.out2"}, {"from":"dev12.in0","to":"dev7.out3"}, {"from":"dev13.in0","to":"dev8.out0"}, {"from":"dev14.in0","to":"dev8.out1"}, {"from":"dev15.in0","to":"dev8.out2"}, {"from":"dev16.in0","to":"dev8.out3"} ] } ); simcir.registerDevice('4to16BinaryDecoder', { "width":440, "height":360, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"In","id":"dev0","x":32,"y":56,"label":"D0"}, {"type":"In","id":"dev1","x":32,"y":104,"label":"D1"}, {"type":"In","id":"dev2","x":32,"y":152,"label":"D2"}, {"type":"In","id":"dev3","x":32,"y":200,"label":"D3"}, {"type":"In","id":"dev4","x":32,"y":264,"label":"OE"}, {"type":"NOT","id":"dev5","x":80,"y":200,"label":"NOT"}, {"type":"AND","id":"dev6","x":136,"y":208,"label":"AND"}, {"type":"AND","id":"dev7","x":136,"y":256,"label":"AND"}, {"type":"3to8BinaryDecoder","id":"dev8","x":208,"y":32,"label":"3to8BinaryDecoder"}, {"type":"3to8BinaryDecoder","id":"dev9","x":208,"y":184,"label":"3to8BinaryDecoder"}, {"type":"BusOut","id":"dev10","x":320,"y":88,"label":"BusOut"}, {"type":"BusOut","id":"dev11","x":320,"y":184,"label":"BusOut"}, {"type":"Out","id":"dev12","x":376,"y":128,"label":"A0"}, {"type":"Out","id":"dev13","x":376,"y":184,"label":"A1"} ], "connectors":[ {"from":"dev5.in0","to":"dev3.out0"}, {"from":"dev6.in0","to":"dev5.out0"}, {"from":"dev6.in1","to":"dev4.out0"}, {"from":"dev7.in0","to":"dev3.out0"}, {"from":"dev7.in1","to":"dev4.out0"}, {"from":"dev8.in0","to":"dev0.out0"}, {"from":"dev8.in1","to":"dev1.out0"}, {"from":"dev8.in2","to":"dev2.out0"}, {"from":"dev8.in3","to":"dev6.out0"}, {"from":"dev9.in0","to":"dev0.out0"}, {"from":"dev9.in1","to":"dev1.out0"}, {"from":"dev9.in2","to":"dev2.out0"}, {"from":"dev9.in3","to":"dev7.out0"}, {"from":"dev10.in0","to":"dev8.out0"}, {"from":"dev10.in1","to":"dev8.out1"}, {"from":"dev10.in2","to":"dev8.out2"}, {"from":"dev10.in3","to":"dev8.out3"}, {"from":"dev10.in4","to":"dev8.out4"}, {"from":"dev10.in5","to":"dev8.out5"}, {"from":"dev10.in6","to":"dev8.out6"}, {"from":"dev10.in7","to":"dev8.out7"}, {"from":"dev11.in0","to":"dev9.out0"}, {"from":"dev11.in1","to":"dev9.out1"}, {"from":"dev11.in2","to":"dev9.out2"}, {"from":"dev11.in3","to":"dev9.out3"}, {"from":"dev11.in4","to":"dev9.out4"}, {"from":"dev11.in5","to":"dev9.out5"}, {"from":"dev11.in6","to":"dev9.out6"}, {"from":"dev11.in7","to":"dev9.out7"}, {"from":"dev12.in0","to":"dev10.out0"}, {"from":"dev13.in0","to":"dev11.out0"} ] } ); // // この下を追記 // simcir.registerDevice('JK-FF-RST', { "width":800, "height":400, "showToolbox":false, "toolbox":[ ], "devices":[ {"type":"DC","id":"dev0","x":16,"y":144,"label":"DC"}, {"type":"Toggle","id":"dev1","x":80,"y":88,"label":"Toggle","state":{"on":false}}, {"type":"PushOn","id":"dev2","x":80,"y":144,"label":"PushOn"}, {"type":"PushOn","id":"dev3","x":80,"y":304,"label":"PushOn"}, {"type":"Toggle","id":"dev4","x":80,"y":208,"label":"Toggle","state":{"on":false}}, {"type":"AND","id":"dev5","x":208,"y":112,"label":"AND"}, {"type":"AND","id":"dev6","x":208,"y":184,"label":"AND"}, {"type":"AND","id":"dev7","x":264,"y":112,"label":"AND"}, {"type":"NOT","id":"dev8","x":328,"y":112,"label":"NOT"}, {"type":"NOT","id":"dev9","x":328,"y":184,"label":"NOT"}, {"type":"RS-FF","id":"dev10","x":384,"y":144,"label":"RS-FF"}, {"type":"NAND","id":"dev11","x":472,"y":112,"label":"NAND"}, {"type":"NAND","id":"dev12","x":472,"y":184,"label":"NAND"}, {"type":"NOT","id":"dev13","x":288,"y":320,"label":"NOT"}, {"type":"AND","id":"dev14","x":344,"y":320,"label":"AND"}, {"type":"OR","id":"dev15","x":312,"y":272,"label":"OR"}, {"type":"NOT","id":"dev16","x":472,"y":320,"label":"NOT"}, {"type":"AND","id":"dev17","x":536,"y":320,"label":"AND"}, {"type":"LED","id":"dev18","x":424,"y":32,"label":"LED"}, {"type":"LED","id":"dev19","x":576,"y":32,"label":"LED"}, {"type":"NOT","id":"dev20","x":208,"y":32,"label":"NOT"}, {"type":"OR","id":"dev21","x":512,"y":272,"label":"OR"}, {"type":"RS-FF","id":"dev22","x":552,"y":144,"label":"RS-FF"}, {"type":"AND","id":"dev23","x":264,"y":184,"label":"AND"}, {"type":"In","id":"dev24","x":144,"y":88,"label":"J"}, {"type":"In","id":"dev25","x":144,"y":144,"label":"CLK"}, {"type":"In","id":"dev26","x":144,"y":208,"label":"K"}, {"type":"In","id":"dev27","x":144,"y":304,"label":"RST"}, {"type":"Out","id":"dev28","x":632,"y":96,"label":"Q"}, {"type":"Out","id":"dev29","x":632,"y":192,"label":"~Q"} ], "connectors":[ {"from":"dev1.in0","to":"dev0.out0"}, {"from":"dev2.in0","to":"dev0.out0"}, {"from":"dev3.in0","to":"dev0.out0"}, {"from":"dev4.in0","to":"dev0.out0"}, {"from":"dev5.in0","to":"dev24.out0"}, {"from":"dev5.in1","to":"dev25.out0"}, {"from":"dev6.in0","to":"dev25.out0"}, {"from":"dev6.in1","to":"dev26.out0"}, {"from":"dev7.in0","to":"dev5.out0"}, {"from":"dev7.in1","to":"dev22.out1"}, {"from":"dev8.in0","to":"dev7.out0"}, {"from":"dev9.in0","to":"dev23.out0"}, {"from":"dev10.in0","to":"dev15.out0"}, {"from":"dev10.in1","to":"dev14.out0"}, {"from":"dev11.in0","to":"dev20.out0"}, {"from":"dev11.in1","to":"dev10.out0"}, {"from":"dev12.in0","to":"dev10.out1"}, {"from":"dev12.in1","to":"dev20.out0"}, {"from":"dev13.in0","to":"dev27.out0"}, {"from":"dev14.in0","to":"dev9.out0"}, {"from":"dev14.in1","to":"dev13.out0"}, {"from":"dev15.in0","to":"dev8.out0"}, {"from":"dev15.in1","to":"dev27.out0"}, {"from":"dev16.in0","to":"dev27.out0"}, {"from":"dev17.in0","to":"dev12.out0"}, {"from":"dev17.in1","to":"dev16.out0"}, {"from":"dev18.in0","to":"dev10.out0"}, {"from":"dev19.in0","to":"dev22.out0"}, {"from":"dev20.in0","to":"dev25.out0"}, {"from":"dev21.in0","to":"dev11.out0"}, {"from":"dev21.in1","to":"dev27.out0"}, {"from":"dev22.in0","to":"dev21.out0"}, {"from":"dev22.in1","to":"dev17.out0"}, {"from":"dev23.in0","to":"dev22.out0"}, {"from":"dev23.in1","to":"dev6.out0"}, {"from":"dev24.in0","to":"dev1.out0"}, {"from":"dev25.in0","to":"dev2.out0"}, {"from":"dev26.in0","to":"dev4.out0"}, {"from":"dev27.in0","to":"dev3.out0"}, {"from":"dev28.in0","to":"dev22.out0"}, {"from":"dev29.in0","to":"dev22.out1"} ] } ); すると、ツールエリアのいちばん下に、「JK-FF-RST」が追加されています。 SimcirJSの画面例 ツールエリアに自作部品 JK-FF-RST が追加されている これで、リセット付きJKフリップフロップが使えるようになりました。 ちなみに、このJK-FF-RSTをダブルクリックすると、ちゃんと中身が確認できます。 自分で追加した回路を使って組み上げる 自分で追加した回路を使って、目的の「3進カウンタ」を組み上げていきましょう。 といっても、使い方はこれまでの操作と変わりません。他の回路と同じようにワークエリアにドラッグアンドドロップし、結線するだけです。 3進カウンタ 0→1→2→3→0→1→2→3→…となるところの「→3」の瞬間でリセットしたいので、 「3」の状態、すなわち両方のJK-FFが「1」(ON)のときにリセットを入れればよさそうです。 両方が1のときだけ1になればいいので、AND回路が使えそうです。両方のJK-FFの出力QをAND回路に入力し、そのAND回路出力をRST端子につなぎこめば よさそうです。 なのですが、実は、両方のJK-FFのRST端子にそのまま入れてしまうと、うまく動きません。 3進カウンタの回路例と動作例 うまく動かないケース 両方同じタイミングで入れてしまうと、両方のJK-FFが同時に 0 になった瞬間に、下位側のJK-FFの1→0の 立ち下がり信号で上位側JK-FFが再度 1 に遷移してしまうためです。 対策として、上位側にリセットを入れるタイミングを少し遅らせます。バッファ(BUF)を入れるか、 ロジック上は意味のない「NOT-NOT」を通して上位側のリセットに入れます。 3進カウンタの回路例と動作例 うまく動くケース これで、思っていた通り動くようになりました。 動作例 ということで3進カウンタができたので、4進カウンタと合わせて、野球のカウンタ(BSO)のようにしてみました。 ついでに「4bit7seg」という便利な回路もあるので、あわせて組み込んでみました。LEDの点灯数と数字で ボールカウント・アウトカウントを表示してくれます。B/S/Oのそれぞれのボタンを押すごとに、B/S/Oが ひとつずつカウントされていきます。 SimcirJSで作成した野球カウンタ(BSO)の例 この回路も示しておきます。 野球のBSOカウンタの回路図 さいごに 本物の野球のカウンタのように利用するのであれば、次のような機能もあるとよいのか、と思います。 Bが3→0に戻るとき(フォアボール)、Sも0に戻す、Oはそのまま Sが2→0に戻るとき(三振)、Bも0に戻す。Oを増やしたい気がするが、「振り逃げ」のケースがあり得るので、Oはそのまま Oが2→0に戻るとき(チェンジ)、BとSも0に戻す どれも、リセット入力を利用することで実現可能です。その意味では、B用の4進カウンタも、「JK-FF」ではなく「JK-FF-RST」で 組んでおくべきでした。上記を満たすためには、どんな条件のときに、どのJK-FF-RSTにリセット信号を入れればよいか、 またそのときも、今回と同じようなタイミング調整(NOT-NOT回路)の必要があるか、興味のある方は、考えてみて、 SimcirJSで試してみてください。 まとめ SimcirJSを使うと、ディジタル回路のシミュレーションが簡単にできます すでにあるものを利用することもできますが、自分で立てたhttpd上へのインストールも、とても簡単にできます 自作の回路を登録するには、すこし手間はかかりますが、決して難しくはありません このよう簡単に試すことができるシミュレータで、ディジタル回路に興味を持っていただけるとありがたいです。 筆者自身はディジタル回路をさわるのはひさびさで、かなり勘が鈍ってしまっていました。たまにはさわっておかないとダメですね。 執筆者 ふるかわ やすゆき (古川 靖之) NTT西日本 バリューデザイン部 所属 ふだんは、情報機器開発に携わっています。以前はビジネスフォンの開発も行っていました。 前職では法人営業SEで、自治体様を担当していました。その前は安全帽装備で建設現場に入っていたこともあります。もっと前はアプリ開発をしていたこともあります。ハード(電気電子)・ソフト(情報)・筐体(機械)をひととおり経験している、弊社では少し珍しいタイプの人間です。 技術士(電気電子部門、情報工学部門、機械部門、経営工学部門、総合技術監理部門) 参考資料・出典 本記事の作成にあたり参考にしたもの、そもそも自身でSimcirJSを使うために参照したものを紹介します。 SimcirJSを利用できるサイト 作者さんのサイト : https://kazuhikoarase.github.io/simcirjs/ 作者さんのサイトです。ダウンロードもここからできますし、このサイト上でSimcirJSを利用することもできます。 論理回路シミュレータ SimcirJS の使い方 : https://lecture.ecc.u-tokyo.ac.jp/JOHZU/joho/Y2016/LogicSimulator/LogicSimulator/01.html SimcirJSの使い方の説明とともに、実際に利用できるページも用意されています。 ワークエリアが広くとってあります。 スク解ホーム : https://kaisaba.f5.si/html/simcirjs-master/index.html 赤色以外のLED(緑色・青色)や、3入力以上の論理ゲート回路が準備されています。 また、作成した回路のPCへの保存や、PCに保存したファイルからの読み出しができるようにされています。 ライセンス SimcirJSはMITライセンスで公開されています。本記事に、配布コードの一部を引用した部分があることから、 SimcirJSのライセンス全文を掲載します。 MIT License Copyright (c) 2014 Kazuhiko Arase Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
はじめに 株式会社ジャパン・インフラ・ウェイマーク(以下、JIW)の森田です。 JIWでは、ドライブレコーダーやドローンで撮影した動画・画像に対してAIを活用し、 道路のひび割れ・道路構造物・サビなど、インフラ点検の支援を行っています。 あるプロジェクトでは、約300TBのデータをAmazon S3に長期保管していました。 最大の論点は「 月額コストをどう圧縮するか 」です。 この課題に向き合う中で、複数のストレージサービスやクラウド事業者のプログラムを調査しました。 本記事では、その過程で見えてきた 状況別の現実的な対応案 各サービスの特性を理解するうえで押さえておきたいポイント を共有します。 対象読者 本記事は、主に次のような方を想定しています。 Boxへのデータ移行を検討している方 数十〜数百TBクラスの長期保管データのコスト最適化を検討している方 DTO(Data Transfer Out)無料化の流れやEU Data Actの影響が気になっている方 背景・目的 データの特性 インフラ点検業務では、次のような特性を持つデータを大量に抱えることになります。 すぐには消せない(いつか使う可能性がある) 平常時のアクセス頻度は低い いざ使うとなったら、即時〜数分以内にはアクセスしたい 性質としては、 いわゆる「コールドストレージ」 に分類されるストレージ要件です。 調査した選択肢 この課題に取り組む中で、以下の選択肢を検討・調査しました。 Boxの「無制限ストレージ」への移行検討 欧州データ法(EU Data Act)を契機とした DTO(Data Transfer Out)無料化のトレンド [1] [2] [3] [4] AWSの中だけで S3ストレージコストを削減する方法 [8] AWS以外のストレージサービス を選択肢に含める方法 [10] [11] [12] 前提と用語整理 想定シナリオ 今回の検討対象となったデータの前提は以下です。 項目 内容 データ総量 約300TB 特性 削除できない/アクセス頻度は低い/アクセス時は即時性が必要 保存先 Amazon S3(複数バケット) 主な課題 月額ストレージコストの圧縮 制約 将来の参照可能性を維持すること この前提から、候補となるのは大きく次の3つです。 S3ストレージクラスの見直し(Glacier系など) [8] 他社クラウド/専用オブジェクトストレージへの退避 [10] [11] DTO無料/減額プログラムの活用 [1] [4] [5] [12] DTO(Data Transfer Out)とは DTOは、 クラウドからインターネットや他クラウド、オンプレ環境へデータを出すときに発生する転送料金 を指します。 AWSでは「リージョンからインターネットへの送信」に対してGBあたりの料金が設定されており、 これがいわゆる 「出口コスト」 としてスイッチングの障壁になりがちです。 [6] Box移行検討でわかった「帯域制限」の重要性 Boxの「Unlimited storage」を調査してわかったこと Boxのエンタープライズ系プランでは、「 Unlimited storage(ストレージ無制限) 」が提供されています。 300TBクラスのデータを抱える立場から見ると、非常に魅力的に見えます。 しかし、詳細を調べていく中で、 帯域(アップロード+ダウンロード)のFair Use制限 の存在が見えてきました。 [9] BoxのFair Use Policyでは、次のように定義されています。 [9] Each individual or User is limited to a total usage of 1 TB of Bandwidth per month . [9] つまり、 1ユーザーあたり月間1TBまで がFair Useの目安です。 単一アカウント(=1ユーザー)で300TBを移行しようとすると、 理論上300ヶ月(25年) かかる計算になります。 実際には、 複数ユーザーで並列にアップロードする Box側と個別に相談し、一時的な帯域増強や例外対応を打診する といった現実的な選択肢がありますが、今回のように 「短期間で300TB規模を一気に移行したい」 ケースでは、 ストレージ容量だけでなく帯域制限も設計上の制約になる 事前に移行計画をきちんと相談しておかないと、「終わらない移行」になり得る ということが分かりました。 大容量データ移行時の事前確認ポイント 今回の調査を通じて、大容量データの移行検討時には、少なくとも以下の観点を事前確認する必要があると感じました。 チェック項目 見るべきポイント ストレージ容量 「無制限」と書いてあっても、その単位(ユーザー単位/テナント単位など)を確認 帯域(Bandwidth) 月間アップロード+ダウンロードの上限。ユーザー単位/組織単位のFair Useの有無 ドキュメントの場所 日本語ページだけでなく、 英語の規約・Fair Use Policy本文 を確認する [9] 回避余地 契約オプションやサポート経由で帯域を増やせるか/一時的な緩和が可能か EU Data ActとDTO無料化の動き EU Data Actのクラウドスイッチング規定 欧州データ法(EU Data Act、Regulation (EU) 2023/2854)は、データ利活用の促進とともに、 クラウドロックインの緩和 を重要な目的の1つとして掲げています。 [1] [2] 特に「クラウドスイッチング」に関する条文(Chapter VI)では、 スイッチング手数料(Switching charges)やDTOを含むエグレス課金の扱い が段階的に規定されています。 [1] [3] [4] タイムラインをざっくり整理すると、以下のイメージです。 [1] [4] 日付 内容 2025年9月12日 Data Actの多くの規定が適用開始(クラウドスイッチング関連義務もここから適用) 〜2027年1月11日 スイッチング手数料は「コストベース」のみ許容(原価回収レベルまで) 2027年1月12日以降 スイッチング手数料の禁止(DTOを含むエグレス課金による 追加利益 は認められない方向性) この流れを受けて、主要クラウドベンダーは DTO無料化プログラム マルチクラウド間転送の無償化(例:Google CloudのData Transfer Essentials) [12] など、 「スイッチングしやすい」料金体系 を相次いで打ち出しています。 [5] [12] AWSのDTO無料プログラム(公式情報ベース) AWSは2024年3月5日の公式ブログで、 他クラウドやオンプレ環境へ移行する場合にDTOを無料とするプログラム を発表しています。 [5] 主なポイントを整理すると、以下の通りです。 対象 AWSリージョンからインターネットへのDTO(他クラウド/オンプレへの移行目的) [5] 基本ルール すべての顧客に対して、 月間100GBまでのDTO無料枠 が恒久的に提供されている(AWS Free Tierの一部) [6] [7] CloudFrontからのDTOも、 月間1TBまで無料 の枠がある(こちらも恒久) [6] [7] 追加で無料枠を拡大したい場合 「移行のための一時的なトラフィック」であること AWSサポートに連絡し、 移行計画(対象サービス・データ量・期間など)を共有したうえで申請 する [5] 承認された場合、 対象DTO分の料金がクレジットで相殺される [5] 2025年9月30日のアップデート時点の情報では: 移行に伴うDTO無料プログラムの利用は、 原則90日以内に移行を完了 させることが前提 [5] 90日を超える場合は、 AWSサポートへの事前連絡が必須 「月間100GBを超えるDTOでなければサポート申請できない」という条件は撤廃済み このプログラムは、 すべてのリージョンのAWS顧客を対象 としており、EU Data Actの方向性に沿った施策と説明されています。 [1] [5] まとめると、DTO無料プログラムを活用する際には、以下の点を意識する必要があります。 公式ブログやFAQに記載された共通条件を確認し、最新の情報を把握すること [5] [6] [7] 自社の移行計画(対象データ量・期間・移行先など)を明文化し、その前提で無理のないスケジュールを組むこと 疑問点があれば早めにAWSサポートや営業担当に相談し、適用可否や運用条件を確認すること また、これらを踏まえたうえで、 少し余裕のあるスケジュール で移行計画を立てておくことが重要です。 AWSの中でS3コストを減らす選択肢 DTO無料で「外に出る」前に、 「まずAWS内でどこまで最適化できるか」 を検討する価値は高いです。 300TBクラスでも効いてきそうな代表的な施策をピックアップします。 [8] ストレージクラスの見直し 代表的なS3ストレージクラスをざっくり整理すると、次のようなイメージです。 [8] クラス名 用途イメージ 特徴(ざっくり) S3 Standard 頻繁アクセス 高性能だが単価は最も高いクラス S3 Intelligent-Tiering アクセスパターンが読めない アクセス頻度に応じて自動で階層を切替、取り出し料金なし S3 Standard-IA / One Zone-IA たまにしか読まない 保存単価は安いが、取り出し料金あり S3 Glacier Instant / Flexible Retrieval 年に数回程度のアクセス 取り出しレイテンシあり。保存単価はさらに安価 S3 Glacier Deep Archive ほとんど読まない長期アーカイブ TB単価が最安クラス。取り出しに数時間+取り出し料金あり 特にS3 Glacier Deep Archiveは、S3 Standardと比べて GB単価が1/20程度 と、 オーダーが一つ以上違うレベル で安価です。 [8] そのため、 「消せないがほぼ読まない」データ については、 アクセス要件を満たす範囲でDeep Archiveに寄せるだけで、月額コストを大きく削減できるケースがあります。 [8] S3 Intelligent-Tieringの検討ポイント S3 Intelligent-Tieringは、アクセスパターンが読めないデータに対して便利な選択肢です。 S3が自動でアクセス頻度を監視し、適切なストレージ層に移動してくれるため、運用負荷を抑えつつコスト最適化ができます。 [8] 一方で、 大量の画像ファイルなど、オブジェクト数が非常に多い場合 は、次の点に注意が必要です。 Intelligent-Tieringでは、 128KB以上のオブジェクトに対して オブジェクト監視・自動階層化の料金が発生する オブジェクト数が極端に多い場合、この 監視・自動化料金 が無視できない金額になることがある 128KB未満のオブジェクトは自動階層化の対象外 で、常にFrequent Access相当の料金で課金される(監視料金はかからない) 今回のような「動画・画像データが大量にある」ケースでは、次のような判断基準を設けました。 アクセスパターンが明確なデータ ライフサイクルルールで明示的にGlacier系へ移行 アクセスパターンが不明確で、かつファイルサイズが大きめ(数MB以上) Intelligent-Tieringの利用を検討 小さなファイルが大量にあるデータ 後述の オブジェクト集約(s3tarなど) でまとめてからアーカイブする案を検討 Storage Lensとライフサイクルルール S3 Storage Lensを有効化すると、アカウント全体のストレージ利用状況を俯瞰できます。 [8] 代表的な使い方は次のフローです。 Storage Lensで「コストインパクトが大きいバケット」を特定 対象バケットに対して、ライフサイクルルールを設計・適用 例: 最終アクセスから90日経過 → Glacier 365日以上アクセスなし → Deep Archive 古いオブジェクトバージョンを一定期間で削除 未完了マルチパートアップロードを一定日数で破棄 「古いデータから順に安い階層へ落としていく仕組み」 を組み込んでおくことで、 将来のS3コストの暴騰を抑えやすくなります。 [8] オブジェクト集約(s3tarなど) 大量の小さなファイルをそのままS3に置いている場合、 PUT/LISTリクエスト回数 オブジェクトメタデータの管理コスト も無視できなくなってきます。 例えば、 s3tar でファイルをまとめて1オブジェクトにし、Glacierに移行するアーキテクチャ を採用する方法があります。 「ファイル単位でピンポイントに復旧したい」という要件が薄いアーカイブ用途であれば、 一定期間ごとにまとめてTAR化 → Glacier系へ退避 といったパターンも有力な選択肢になります。 AWSを使わない場合のストレージ選択肢 DTO無料プログラムやEU Data Actの影響により、 他社クラウド/専用オブジェクトストレージを候補に含める現実味 も増しています。 [1] [4] [5] [12] ここでは、「どのベンダーが良いか」ではなく、 カテゴリごとの選択肢の整理 にフォーカスします。 他メガクラウドのアーカイブ層 Microsoft AzureやGoogle Cloudなど、他のメガクラウドもS3 Glacierに相当するアーカイブ向けストレージを提供しています。 Azure Blob Storage : Hot / Cool / Cold / Archive など Google Cloud Storage : Standard / Nearline / Coldline / Archive など さらにGoogle Cloudは、 EU/UK向けに「Data Transfer Essentials」という無償のマルチクラウド転送サービス を提供開始しています。 [12] EU Data Actの原則(マルチクラウド/スイッチング容易性)に応える位置づけ EU/UKの顧客向けに、特定条件を満たすマルチクラウド間転送の エグレス料金を0円 にする [12] メリット 大手クラウドのコンプライアンス・認証を活用しやすい 既存システムとの連携資料・SDKが豊富 注意点 AWSからDTOを出す際は、 AWS側DTO無料プログラムとの組み合わせ を検討 [5] クラウドごとに料金・取り出し制約・リージョン制限が異なる [8] [12] 専用オブジェクトストレージ(Backblaze B2 / Wasabiなど) S3互換API+シンプルな料金体系 を特徴とする専用オブジェクトストレージも、有力な選択肢です。 Backblaze B2の例 Backblaze B2 Cloud Storageの価格表では、おおよそ以下の条件が提示されています。 [10] ストレージ: 約$6 / TB / 月 「Free egress up to 3x storage」 ルール 「平均月間ストレージ量の3倍までのダウンロードは無料」 それを超えた分は$0.01/GB Wasabiの例 Wasabi Hot Cloud Storageは、「 No fees for egress or API requests 」を打ち出しています。 [11] ストレージ料金はTBあたりの固定単価(例:$6.99/TB/月) egress・APIリクエストは無料 代わりに、最低保持期間(90日)や最低利用容量などの条件がある [11] ざっくり比較イメージ サービス egressポリシーの特徴 備考 AWS S3(通常利用) DTO課金あり(100GB/月の無料枠+DTO無料プログラムあり) DTO無料プログラムは「移行時向け」 [5] [6] [7] Backblaze B2 月間平均ストレージ量の3倍までegress無料 S3互換API・シンプルな従量課金 [10] Wasabi egress / APIリクエストともに無料 最低保持期間や最低容量などに注意 [11] メリット 料金モデルがシンプルで予測しやすい S3互換APIのため、既存ツール/SDKをそのまま流用しやすい 注意点 データレジデンシ(どの国・地域にデータが置かれるか) 自社のコンプライアンス・セキュリティ要件との整合 サービスの継続性、サポート体制、ベンダーの事業規模 [10] [11] まとめ:状況別の選択肢 ここまでの内容を踏まえ、データ容量とアクセスパターン別に、現実的なアプローチを整理します。 状況別の推奨アプローチ データ容量 アクセス頻度 必要なアクセス速度 推奨アプローチ 備考 〜1TB 低頻度 即時〜数分 Box移行を検討 帯域制限(1TB/月/ユーザー)内で移行可能。複数ユーザー並列も選択肢 [9] 〜1TB 低頻度 12時間以内でOK S3 Glacier Deep Archive AWS内で完結。TB単価は最安クラス [8] 1TB〜100TB ほぼアクセスなし 12〜48時間でOK S3 Glacier Deep Archive まずAWS内最適化を優先 [8] 1TB〜100TB 月数回程度 数分〜数時間 S3 Intelligent-Tiering または S3 Glacier Flexible Retrieval ファイルサイズが大きい(数MB以上)場合はIntelligent-Tieringも有力 [8] 100TB以上 ほぼアクセスなし 数日でもOK DTO無料プログラム+オンプレ(HDD/NAS) カスタマーサポートに相談。審査通過が前提 [5] 100TB以上 ほぼアクセスなし 即時〜数時間 DTO無料プログラム+他クラウド B2(3倍egress無料)、Wasabi(egress無料)など [5] [10] [11] 100TB以上 月数回程度 数分〜数時間 S3 Glacier Flexible Retrieval AWS内での最適化を優先。Expedited/Standard/Bulk取得を使い分け [8] 小容量データ(〜1TB)の場合の追加オプション 1TB未満のデータであれば、より柔軟な選択肢もとりやすくなります。 Box移行 帯域制限内で移行しやすく、Unlimited storageプランなら追加ストレージ費用無しで収容可能 [9] DTO無料プログラムの活用 月間100GBのDTO無料枠と組み合わせれば、 数ヶ月に分けて段階的に退避 することも現実的 [6] [7] オンプレ保存(HDD/NAS) DTO無料プログラムが承認されれば、ローカル保存に切り替えたうえで、バックアップ戦略を自前で設計することも可能 [5] 各アプローチの重要ポイント 1. AWS内での最適化(第一優先) Storage Lensで現状分析 して、コストインパクトの大きいバケットを特定 [8] ライフサイクルルール で自動的にGlacier系へ移行 オブジェクト集約(s3tar) で小さなファイルをまとめてからアーカイブ まずは「 AWS内でやり切る 」ことで、DTOコストや移行リスクを最小化 [5] [8] 2. Box移行を検討する場合 帯域制限(1TB/月/ユーザー) を事前に把握 [9] データ量に応じて、 複数ユーザーでの並列化や分割移行 を設計 数十TB〜の移行では、 移行期間がボトルネック になる可能性が高い 3. DTO無料プログラムを利用する場合 早い段階で AWSカスタマーサポートに相談 [5] 移行先(他クラウド/オンプレ)、データ量、期間を含む 移行計画を明文化 公開情報はアップデートされるため、 最新の公式ドキュメントを随時確認 する [5] [6] [7] 国内での適用事例は、現時点では多くないと考えられるため、 検証的に小さな案件から試す のも選択肢 4. 他社ストレージを選択する場合 データレジデンシ (どの国・地域にデータが置かれるか) コンプライアンス要件(業法・顧客契約・社内規程) との整合性 ベンダーの サービス継続性・サポート体制・事業規模 [10] [11] [12] 共通して重要なポイント 「無制限ストレージ」には帯域制限が存在する可能性が高い 容量だけでなく、 どれだけの速度/期間で移行できるか を必ず確認する [9] S3 Intelligent-Tieringは万能ではない 小さいオブジェクトが大量にある場合、監視料金や128KB未満オブジェクトの扱いに注意 [8] DTO無料プログラムは「事前相談」が前提 公式条件に加え、自社のケースに関する運用条件を確認し、 書面レベルで認識合わせ しておく [5] [6] [7] まずはAWS内での最適化をやり切る Storage Lens/ライフサイクル/Glacier系/s3tarといったベーシックな施策でも、 TB単価を桁レベルで落とせる ケースがある [8] 執筆者について 森田 賢徳 株式会社ジャパン・インフラ・ウェイマーク 開発部 開発担当 ソフトウェアチーム 担当業務 Waymark Noteのフルスタック開発(iOS/Swift) Raspberry Piを用いたドローン連携制御によるリアルタイムAI開発 道路等のひび割れ・パッチ等検出のAI開発 その他、インフラ点検に関する多様な開発業務 免責事項 本記事に記載された情報は、 2025年11月時点 での公開情報および筆者の検証・ヒアリング結果に基づくものです。 各サービスの料金体系、機能、制限事項等は予告なく変更される場合があります 本記事の内容を実践される際は、必ず各サービスの 最新の公式ドキュメント をご確認ください [1] [5] [8] [9] [10] [11] [12] 特にAWS DTO無料プログラムの適用条件は、 個別の状況・審査・時期によって異なる可能性 があります [5] 本記事の情報に基づいて行われた意思決定や実装により生じた損害について、筆者および所属組織は一切の責任を負いかねます 商標について 本記事で言及されている製品名・サービス名は、各社の商標または登録商標です。 Amazon Web Services、AWS、Amazon S3、Amazon S3 Glacier、Amazon CloudFront、AWS Direct Connectは、Amazon.com, Inc.またはその関連会社の商標です Microsoft、Azureは、米国Microsoft Corporationの米国およびその他の国における登録商標または商標です Google Cloud、Google Cloud Storageは、Google LLCの商標です Boxは、Box, Inc.の登録商標です Backblaze、Backblaze B2は、Backblaze, Inc.の商標です Wasabiは、Wasabi Technologies, Inc.の商標です Raspberry Piは、Raspberry Pi Foundationの商標です その他、本文中に記載されている会社名、製品名、サービス名等は、各社の商標または登録商標です 本記事は特定の製品やサービスを推奨するものではなく、 技術的な調査結果・設計観点の共有 を目的としています。 参考リンク 本文中で触れた主な公開情報へのリンクです(いずれも2025年11月時点で参照)。 EU Data Act関連 EU Data Act 公式テキスト(Regulation (EU) 2023/2854) https://eur-lex.europa.eu/eli/reg/2023/2854/oj/eng European Commission「Data Act」概要ページ https://digital-strategy.ec.europa.eu/en/policies/data-act Taylor Wessing「EU Data Act FAQs」 https://www.taylorwessing.com/de/global-data-hub/2025/eu-data-act---understanding-the-issues/gdh---eu-data-act-faqs Deloitte「Cloud switching under the EU Data Act」 https://www.deloitte.com/dl/en/services/legal/perspectives/cloud-switching-eu-data-act.html AWS DTO関連 AWS DTO無料プログラム(Free data transfer out to internet when moving out of AWS) https://aws.amazon.com/blogs/aws/free-data-transfer-out-to-internet-when-moving-out-of-aws/ AWS Global Network FAQs(DTO 100GB無料枠 等) https://aws.amazon.com/about-aws/global-infrastructure/global-network/faqs/ AWS Free Tier における DTO 100GB 無料拡張 https://aws.amazon.com/blogs/aws/aws-free-tier-data-transfer-expansion-100-gb-from-regions-and-1-tb-from-amazon-cloudfront-per-month/ Amazon S3 Pricing https://aws.amazon.com/s3/pricing/ Box関連 Box Fair Use Policy https://www.box.com/legal/fairusepolicy その他ストレージサービス Backblaze B2 Cloud Storage Pricing https://www.backblaze.com/cloud-storage/pricing Wasabi Hot Cloud Storage Pricing https://wasabi.com/pricing Google Cloud「Data Transfer Essentials」 https://cloud.google.com/blog/products/networking/new-for-the-uk-and-eu-no-cost-multicloud-data-transfer-essentials
はじめに NTT西日本の吉田・﨑野・杉本です。 本記事では、先日開催されたアジアクエスト株式会社との共催イベントに参加したので、 そのイベント内容、登壇を通じて得られた学びや交流について報告します。 対象読者 AWSのAIサービス(Amazon Q Developer、Amazon Bedrock AgentCoreなど)の実践的な活用例を知りたい方 NTT西日本やアジアクエスト社のエンジニアの取り組みに興味がある方 若手エンジニアの技術的なチャレンジや成長事例に興味がある方 社外との共催イベントやLT登壇の経験談を知りたい方 背景・目的 生成AIの分野では、日々新しいサービスや機能がリリースされており、 それらの実用性を確かめるため、社内では検証が活発に行われています。 そして今回、アジアクエスト社との共催によるLT大会に登壇する機会を得て、 若手エンジニアによるAWSのAIサービスの技術検証の成果を発表しました。 イベント概要 イベント名 【NTT西日本共催】若手エンジニアによるLT会~AWSのAIサービスを使ったチャレンジ~ 開催日時 2025/10/28(火) 19:00 ~ 20:30 場所 オンライン イベントURL asiaquest.connpass.com イベント詳細 body { padding: 20px; } h1 { margin-bottom: 30px; } table { width: 800px; border-collapse: collapse; margin-bottom: 30px; } th, td { border: 1px solid #000; padding: 10px; } th { width: 150px; text-align: center; } td { width: 650px; text-align: left; } タイトル Amazon Qでできる限界とは? 発表者 NTT西日本 吉田 泰隆 概要 Amazon Q Developerがエンジニアの実務でどこまで活用できるのかを明らかにするため、「リソース操作」「セキュリティ遵守」「トラブルシューティング」の3つの評価軸で検証を実施しました。具体的には、Amazon CloudWatchアラーム作成やVPCエンドポイント構築などの実践的なシナリオを通じて、Amazon Q Developerの能力と限界、そしてエンジニアが担うべき役割について明らかにしました。その結果、問題の初期診断・ベストプラクティス提案は生成AI(Amazon Q Developer)に任せる一方で、設計方針の決定・リスク評価・承認判断・最終責任は我々エンジニアが担うべきということがわかりました。 タイトル Q Developerで実現するAWSインフラ構築の自動化 発表者 NTT西日本 﨑野 風音 概要 インフラ構築から運用に至るまでのプロセスにおいて、Amazon Q Developerの活用の幅について検証しました。具体的には、一般的な Web アプリケーション構成の構築から、構築後の運用フェーズにおけるセキュリティ強化(WAF に適用されたルールの見直し・最適化)までの一連の作業を、Amazon Q Developer との対話を通じて実施できるかを試みました。特にログの分析においては人間が時間をかけて実施する内容を瞬時にアウトプットしてくれるので、活用の幅も広いのではないかと感じています。逆に構築フェーズにおいては、各種パラメータを詳細に指定しなければAmazon Q Developerが勝手に判断する範囲も増えてしまうこともあるように、まだまだ実案件で活用するのは難しいように感じました。 タイトル Amazon Bedrock AgentCoreとは? 発表者 NTT西日本 杉本 脩 概要 2025年10月に正式公開されたAmazon Bedrock AgentCoreについて、プレビュー版を活用して機能調査と技術検証を実施しました。具体的には、AWSが提供する他の生成AIサービスとの比較検討や、スターターツールキットを用いたAIエージェントの作成・デプロイに挑戦しました。これらの検証から、Amazon Bedrock AgentCoreは生成AIアプリケーションを作りたい開発者向けのサービスであり、Amazon Bedrock Agentに比べてコーディングを必要とする反面、自由度が高く、Pythonコード数行でAIエージェントのロジックとインフラ設定をデプロイ・管理できるという優位性を確認できました。 タイトル Kiroのハッカソンに参加した話 発表者 アジアクエスト社 秋山 直輝 概要 Kiroのハッカソンに参加した体験談をメインに、Kiroの紹介、ハッカソンの紹介やKiroを使った開発の流れ、Kiroを使ってみた感想としてよかったところ(context機能やSteering機能など)、微妙だったところについて発表しました。また、ハッカソン参加に当たってのライセンス対応や動画作成の体験談の紹介なども行いました。 タイトル Amazon Q Developer CLIでインフラ+アプリ開発にチャレンジ 発表者 アジアクエスト社 安藤 遼河 概要 Amazon Q Developer CLI を使って取り組んだインフラ構築とWebアプリケーション開発について登壇しました。登壇者自らは直接開発作業を行わずに、Amazon Q Developer CLI に与える指示のみでどこまで開発が可能であるか挑戦しました。生成AIに対する指示の出し方や開発の進め方など、実際に直面した失敗談を交えながら、その解決策として取り入れたドキュメント整備、段階的なレビューの工夫のナレッジ共有を行いました。 印象に残った内容・学び 吉田 検証については、生成AI(Amazon Q Developer)のスピード感と正確性に驚きました。 その一方で、エンジニアが果たすべき役割も見えたので、これからも成長していきたいなと思いました。 また他の皆さんの発表を聞くことで、多くの刺激を受けました。今回の発表で満足することなく、これからもインプットとアウトプットのサイクルは続けていきたいです。 あと、オンラインだと参加者のみなさんのリアクションが少しわかりにくかったので、次回は対面イベントにも参加・企画したいなと思います。 﨑野 今回、LT大会への参加は初めての経験でしたが、とても良い機会となりました。 登壇を通じて、聞き手に伝わりやすい構成の重要性を改めて実感しました。 また、質疑応答の時間では多くの方から質問をいただき、発表内容に興味を持って聞いていただけたことを実感できて嬉しかったです。 杉本 今回の技術検証を通して感じたことは、AIエージェント開発の手軽さです。AIエージェント開発と聞くと、 「AWSコンソールで複雑な設定が必要で、デプロイが大変そう…」というイメージがありました。 しかし、Amazon Bedrock AgentCoreを使うことで、数行のコマンドと5分程度のコーディングでAIエージェントを作成できました。 また、コーディングの際に使用したStrands Agentは、開発者が書いたPython関数をそのままツールとして利用できます。 どの質問に対して、どのツールを呼び出し、その結果をどう解釈して最終回答を生成するか、という一連の流れがコードを通じて追えるため、ブラックボックス感が薄れました。 交流 今回の勉強会は、普段の業務で関わる機会の少ないメンバーと一緒に登壇しました。 発表を進める中で、AWSの設計や構築に取り組む際にそれぞれが抱えている課題や悩みに共感し、 それらの課題をどのようにAIで支援できるかについて共有したことで、新たな視点や学びを得ることができました。 特に、Amazon Q Developerの検証結果を共有した場面では、 「自分はこういう動きになりました!」 「このケースはどう対応しました?」 といった会話が生まれ、 互いの知見や工夫をその場で交換できたことが印象に残っています。 同じテーマに向き合う仲間として、実践的なノウハウを共有できた時間はとても楽しく、刺激的でした。 まとめ 今回のLT大会では「AWSのAIサービスを使ったチャレンジ」というテーマのもと、アプリ開発やインフラ構築など、登壇者それぞれが独自の視点でAIを活用しており、非常に多様性に富んだ興味深い発表が続きました。 今後、AIは単なるツールとしてではなく、開発や運用のパートナーとして活用していくことが重要です。 AWSのAIサービスは、アイデアをすばやく形にする力を持っており、これからの時代のイノベーションを加速させる鍵になると感じました。 今回のLT大会を通じて、AIの可能性を再確認するとともに、私たち自身がその可能性をどう引き出していくかが問われていると強く感じました。 執筆者 吉田泰隆 2023年に新卒でNTT西日本に入社 AWSを中心とした詳細設計·構築案件や生成AIに関する技術検証を担当 﨑野 風音 2023年に新卒でNTT西日本に入社 自治体様向けのMicrosoft365設計·構築やDX推進案件に従事 杉本 脩 2024年に新卒でNTT西日本に入社 AWSを中心としたインフラの提案·設計·構築案件に従事 商標 「AWS、Amazon Q Developer、Amazon Bedrock AgentCore、Kiro」は、Amazon Web Services, Inc.またはその関連会社の商標もしくは登録商標です。
はじめに 対象読者 背景 前提条件・動作環境 Amazon Q Developerとは? 検証内容 1. Amazon Q Developer for CLIによるWebアプリケーション構成の開発 2. Amazon Q Developer for CLIによる構成のチェックとAWS CloudFormationテンプレート作成 3. Amazon Q Developer for CLIによるAWS WAFログの分析とWeb ACLの作成 苦労したポイント まとめ 執筆者 商標  参考資料・出典 はじめに NTT西日本の﨑野 風音です。 本記事では、AWSの生成AIサービス「Amazon Q Developer for CLI」を使って、どこまでインフラ構築を自動化できるのかを検証しました。 実際に試した結果や、うまくいった点・苦労した点をまとめています。 本記事は2025年10月時点の情報に基づきます。 対象読者 本記事が想定する対象読者は以下の通りです。 AWS、生成AIサービスを勉強している人 生成AIの活用方法に困っている人 生成AIを活用したAWSインフラ構築について知りたい人 背景 本検証はアジアクエスト社共催の外部登壇(AWSの生成AIを活用した取組)に向けて実施しました。 登壇の模様は後日別の記事として公開予定ですので、あわせて読んでいただけるとありがたいです。 また、テーマとしてAmazon Q Developer for CLIを選んだ背景としては、 以下にあげるような課題をAmazon Q Developer for CLIを活用することで解決できると感じたためです。 インフラ構築の手順は個人依存で標準化が難しい AWSサービスは多岐に渡り、学習コストが高い 構築時にヒューマンエラーによるリスクが高い 手作業による構築に時間がかかる 運用におけるリソース最適化に割く時間がない 前提条件・動作環境 本検証はWindows環境で実施しています。 Windows環境におけるAmazon Q Developer for CLIのインストールについては以下の記事を参考に実施しております。 1 Amazon Q Developer for CLIを使って、AWSのリソースをAIで作成⇒調査⇒構成図作成までやってみて、構築作業を楽にしよう | コラム | クラウドソリューション|サービス|法人のお客さま|NTT東日本 記事中のIPアドレス、FQDN、ID等はマスクしています。Amazon Q Developerの実際の応答では左記情報含め出力されます。 Amazon Q Developerとは? Amazon Q Developerとは、AWSが提供する開発者向けに設計された生成AIアシスタントです。 AWSの開発作業を効率化するためのツールになります。 AWSのサービスを使ったコードの自動生成やAWSの公式ドキュメントやベストプラクティスの検索、トラブルシューティング支援等をチャット形式で質問することができます。 aws.amazon.com Amazon Q Developerは以下3つの方法で利用可能です。 Amazon Q Developer for CLIとして利用 コマンドラインで自然言語を使ってAWS操作やコード生成をしたい際に便利です。 AWSマネジメントコンソールから利用 AWSマネジメントコンソール右側に表示されるQアイコンから利用できます。画面のコンテキストを理解して最適な回答や操作方法を提案してくれます。 IDEプラグインとして利用 Visual Studio CodeのようなIDEにプラグインを追加することでコード補完やリファクタリング等をAmazon Q Developerがサポートしてくれます。 今回は一つ目のAmazon Q Developer for CLIをご紹介します。 ※現在、Amazon Q DeveloperはKiro CLIに統合されています。 詳細はこちらの記事をご覧ください。 aws.amazon.com 検証内容 本検証は大きく分けて以下の3つを実施しました。 Amazon Q Developer for CLIによるWebアプリケーション構成の開発 Amazon Q Developer for CLIによる構成のチェックとCloudFormationテンプレート作成 Amazon Q Developer for CLIによるAWS WAFログの分析とWeb ACLの作成 全体の流れは以下のようになります。 手順1~3が「1. Amazon Q Developer for CLIによるWebアプリケーション構成の開発」 手順4~6が「2. Amazon Q Developer for CLIによる構成のチェックとCloudFormationテンプレート作成」 手順7,8が「3. Amazon Q Developer for CLIによるAWS WAFログの分析とWeb ACLの作成」に対応しています。 1. Amazon Q Developer for CLIによるWebアプリケーション構成の開発 このセクションでは、Webアプリケーション構成をAmazon Q Developer for CLIとの対話によって構築していく手順を紹介します。 始めに、以下のような命令を実施しました。 ※EC2インスタンス:Amazon EC2 インスタンス 「 EC2インスタンスを1台パブリックサブネットに起動してください。簡単なゲームアプリを構築してください。」 ※VPC等の作成もAmazon Q Developer for CLIを活用しましたが、ここでは割愛しています。 Amazon Q Developer for CLIの応答はこちら。 ちなみに、指定しなかった設定項目はAmazon Q Developer for CLIが自動で設定します。 また、同一アカウント内にAmazon Virtual Private Cloud(以下、VPC)が複数ある場合は、どのVPC(指定がある場合はサブネットまで)でインスタンスを起動するかを指定してあげてください。 アクセスも確認できたので、次は以下の命令を実施しました。 ※ALB:Application Load Balancer 「ALBをパブリックサブネットに配置し、EC2インスタンスはマルチAZ配置で、かつプライベートサブネットで起動してください。」 Amazon Q Developer for CLIの応答はこちら。 こちらも問題なくアクセスを確認できました。 2. Amazon Q Developer for CLIによる構成のチェックとAWS CloudFormationテンプレート作成 セクション1の操作を進め、以下の構成を構築しました。 使用されているAWSリソース自体はこちらの意図した通りとなったので、ベストプラクティスと照らし合わせて構成評価を実施しました。 応答はこちら。 ご覧の通り、エンタープライズレベルで求められる要件と比較すると改善が求められる結果となりました。 構築時に詳細なパラメーターを指定することである程度の改善は見込めるかもしれません。 また、以下の通り、Amazon Q Developer for CLIを活用することでAWS CloudFormationテンプレートの作成も確認できました。 もちろん、そのまま本番運用せず、セキュリティ・コスト・可用性等の観点でのレビューが必須です。 ここまでを振り返っての個人的な感想です。 厳密な構築が求められない検証(触ってみた程度)であればAmazon Q Developer for CLIは有効です。 実案件に応用する場合は、「Amazon Q Developer for CLIで簡易構築→AWS CloudFormationテンプレート作成→手動チェック」が必須だと感じました。 3. Amazon Q Developer for CLIによるAWS WAFログの分析とWeb ACLの作成 ここからは構築後のフェーズにおけるリソース最適化へのAmazon Q Developer for CLI活用事例になります。 今回はAWS WAFログの分析から設定ルールをブラッシュアップできないかを検証しました。 ※事前にAWS WAFのログ記録機能を有効化し、Amazon Kinesis Data Firehose経由でAmazon S3に保存する必要があります。 始めに、以下のような命令を実施しました。 「過去1か月間のトラフィックを分析し、AWS WAFに追加すべきルールを提案してください。 分析の際には部分サンプルから判断せず、完全なデータを活用してください。 」 左上がAWS WAFダッシュボードの表示、左下がAmazon Athenaでのクエリ結果(過去一か月間のブロックイベント数の多い上位10個のIPアドレスを表示)、右側がAmazon Q Developer for CLIによる応答結果です。 ご覧の通り、分析結果が一致していることが確認できます。 ※ちなみに、Amazon Athenaで用いたSQLもAmazon Q Developer for CLIで作成しました。 上記結果から、実際のトラフィック状況とAmazon Q Developer for CLIの分析結果に認識齟齬がないことが確認できたので、AWS WAFルールのブラッシュアップを依頼した結果がこちら。 ルールの自動作成と、作成理由の説明まで実施してくれました。 問題なければルールのアタッチまで実施可能です。 苦労したポイント ここまで、「Amazon Q Developer for CLIは便利ですよ!なんでもできますよ!」といったような紹介に見えるかもしれませんが、私が検証を実施する中で苦労したポイントも多々ありました。 そちらを以下に紹介します。 出力の揺らぎ 抽象的な指示では意図しない出力結果が出てしまうことも 詳細なパラメーターの指定や、分析対象期間を明確化することが大切です。 エラーハンドリング 権限不足等のエラーが発生しやすいです。対応方針を事前に明確化しておくと開発がスムーズに進みます。(エラー時に停止するか、必要な権限は取得させて続行するか) エラーハンドリングを指定しないと動作しないアプリが誕生したり、なんてこともありました。 サンプリング 「3. Amazon Q Developer for CLIによるAWS WAFログの分析とWeb ACLの作成」セクションで紹介したトラフィック状況の分析ですが、分析当初はAmazon Q Developer for CLIの応答結果と実際のトラフィック状況に大きな差異が見られていました。 対話を繰り返す中で、 「Amazon Q Developer for CLIがサンプリングを実施したうえで応答を出力していた」 ことが判明しました。 そこで、 サンプリングを実施せず、完全なデータを活用するように 命令をしたところ、正しい結果が出力されました。 まとめ Amazon Q Developer for CLIは、AWS構築のスピードを大幅に向上させるツールです。 特に、検証やPoCでは非常に有効ですが、本番環境では生成結果のレビューとベストプラクティス適用が必須です。 また、構築後のレビューや運用改善にも活用できる可能性があります。 今後は、より高度な構成やCI/CDとの連携、セキュリティ強化の自動化など、さらに実践的な活用方法を検証していきます。 執筆者 﨑野 風音 (NTT西日本 ビジネス営業部所属) M365案件の提案、設計、構築、運用に携わっています。 また、社内向けのAWS勉強会や資格取得支援施策等を実施しています。 商標  「AWS」「Amazon Q Developer」「Amazon Q Developer for CLI」「Kiro CLI」「Amazon EC2」「ALB」「AWS CloudFormation」「AWS WAF」「Amazon Kinesis Data Firehose」「Amazon S3」は、Amazon Web Services,Inc.またはその関連会社の商標もしくは登録商標です。 その他、記載されている会社名、製品名は、各社の登録商標または商標です。 参考資料・出典 本記事を執筆するにあたり、以下のサイトを参考にしました。 Amazon Q Developer for CLIを使って、AWSのリソースをAIで作成⇒調査⇒構成図作成までやってみて、構築作業を楽にしよう | コラム | クラウドソリューション|サービス|法人のお客さま|NTT東日本 ↩
はじめに NTTビジネスソリューションズの衣川です。 2025年度のハードニング競技会『 Hardening 2025 Invisible Divide 』に参加したので、体験記を書きたいと思います。 ハードニング競技会は、ECサイトなどのビジネスシステムに潜む脆弱性への堅牢化能力を総合的に競うコンペティションで、Hardening Projectが毎年開催しています。 具体的には、応募した人の中からチームを組み、約20台の脆弱な設定がされたサーバーをサイバー攻撃から守りつつ、ECサイトを正常稼働させ得られた売上や、ミッションというお題をクリアすることで得られるポイントなどで最終的な順位が決まります。 今回参加してきた2025年度版『Hardening 2025 Invisible Divide』の開催概要は以下の公式ページに記載されています。 開催概要/募集要項 – Hardening 2025 Invisible Divide | Hardening Project 対象読者 本記事が想定する対象読者は以下の通りです。 ハードニング競技会を聞いたことはあり興味もあるが、具体的にどんなことをするのかイメージできない方 ハードニング競技会に参加予定で、準備やイベント中の段取りの情報収集したい方 背景 以下2つが、今回参加しようと思った動機になります。 社内で開催されたMini Hardeningに参加し、得られる学びが多かったことから、より大規模な競技会に興味がわいた テクニカルだけでなくビジネス(売上確保や顧客対応など)への対応も求められることが業務にも活かせると考えた スケジュール 競技会に参加するにあたり、実施したことを以下に記載します。 日付 実施内容 詳細 8/18 申し込み締め切り 応募動機などを記載 8/22 応募結果発表 当選連絡とチームメンバー発表 8/22 - 10/7 準備期間 チームメンバーで準備を進める 10/5 競技会資料の公開  資料は100ページ越え  10/7 沖縄現地入りし、最終調整 メンバー全員が対面で集合 10/8 Hardening Day 競技会 本番! 10/9 Analysis Day 発生したインシデントを整理 10/10 Softening Day 準備期間からHardening Dayまでの取り組みを各チームで発表 チームメンバー 全8人のチームメンバーはほぼ全員初対面で、インフラ企業や法律事務所など様々な業界の方に加え、学生の方もおられるなど多様なバックグラウンドをもったチーム構成になりました。 私は普段、IT環境を健全で安全な状態に保つための「衛生管理」であるサイバーハイジーンの実現にむけたお客様の支援をしています。 他のメンバーも基本的にITに関わりのあるメンバーがそろっていましたが、競技経験としては、約10年前のハードニング競技会に参加された方が1名で小規模なハードニング競技会への参加経験者が私を含めて2名という構成でした。 経験者が少なく競技会の準備という観点では大変でしたが、その分フラットな関係で話し合えたことはよかったと感じています。 まず最初にメンバーのやりたいことをもとに準備と当日の役割分担のためにビジネスチームとテクニカルチームに分け、私はテクニカルチームに所属しました。 準備 毎週定例会を実施して、チームメンバーで分担して準備を進めました。 (約1か月半の準備期間で打ち合わせや演習を9回実施していました) 全部で70個以上のタスクをこなしたのですが、以下にビジネスチームとテクニカルチームのそれぞれについて3つ抜粋しています。 ■ビジネスチーム ECサイトの販売戦略(在庫補充のタイミングなどをチーム内で検討) 個人情報漏洩時の対応整理(競技会中に個人情報が漏洩してしまうことを想定) 特定商取引法による表記の作成(ECサイトを利用するので) Market Placeの製品選定 社内規程類の作成 ※ Market Place(以下、MP)とは、ハードニング競技会で参加チームがスポンサー企業の製品やサービスを利用できる仕組みです。 MarketPlaceで診断ツールや支援サービスを購入することで、チームは不足する技術やリソースを補えます。 ■テクニカルチーム 初期パスワードから変更するパスワードの検討(複雑だけれど、すべて同じにしてしまわないように工夫) コマンドチートシートの作成(後ほど詳細を説明) インシデント対応フロー整理と模擬演習での改善(後ほど詳細を説明) ファイアウォールのポリシー検討(ホワイトリストで通すべき通信を選定) 脆弱な設定の洗い出し(競技会資料が公開されてから実施) この中から私がメインで担当した以下の2つを紹介したいと思います。 コマンドチートシートの作成 インシデント対応フロー整理と模擬演習での改善 コマンドチートシートの作成 Linuxユーザーのパスワード変更やデータベースバックアップなどのコマンド自体は生成AIで出すことはできるものの、それが正しく動作するか検証し、整理していました。 この検証をしていく中で、Linux・データベース・WordPressなどさまざまなツールのセキュリティ対策を経験でき、良い経験になるとともに、セキュリティは幅広い知識が求められることも再認識しました。 また、「ハードニングファン★リファクタ」に記載されていたLL改ざん検知※も動作確認してみました。 (※LL改ざん検知:Linuxで利用されるLLコマンド(ls -l のエイリアス)を活用したファイルの改ざん検知をする手法) 改ざん検知ができるOSSは導入ミスを考慮して利用せずに、LL改ざん検知を本番で利用させていただきました。 ハードニングファン★リファクタ インシデント対応フロー整理と模擬演習での改善 インシデントが起きた時の対応を、メンバーの役割分担から整理しました。 しかし、対応時間が限られていることと、複数のインシデントの同時発生が想定されることから、厳密なフローの整理と迅速で柔軟なインシデント対応の間のトレードオフに悩まされました。 そこで、以下2つの演習を実施して、作成したフローが実運用に使えるかどうかを検証しました。 自作したHardening競技環境を活用したチーム内での演習 インシデント対応のシナリオを作成し、机上演習 自作したHardening競技環境は、WordPressがインストールされたWebサーバー1つを守るハードニング競技で、詳細については本ブログにて後日掲載予定です。 実際に演習を実施していくと、複数のツールを使う余裕がないことや、複数のインシデントが同時に発生した時に全体の状況を確認しづらくなることなどがわかり、フローや報告方法など具体的な手順を改善していきました。 前日 チームメンバー全員が前日入りして最後のミーティングをしました。 特に、競技資料が公開されたのが数日前ということもあり、資料の読み込みや設定値の最終確認などを実施していました また、当日の段取りや気を付けるべきことなど「経験者でないとわからないこと」を相談員の方に相談していました。 「相談員」は競技参加者をサポートしてくださる方々で、以下に詳細が記載されています。 NECセキュリティブログ - Hardening 2025 Invisible Divide 参加記 Hardening Day この日が本番の競技会の日で、沖縄空手会館で9時から実施されました。 事前説明のあとに競技が開始されたのですが、競技会中はインシデントの連続でした。 以下に2つ抜粋して紹介したいと思います。 ECサイトが稼働していない まず、ECサイトが稼働していない時間が多かったです。 下図はすべてのECサイトが起動していない時の画像です。 (Took a Nap(昼寝中)はECサイトが稼働していないことを表現しています) 全てのECサイトが稼働していない時のダッシュボード(主催者から写真掲載の許可をいただいております) 他のインシデント対応で人手が不足しており対応が遅くなりましたが、振り返ると売上に直結するショップの対応を優先しておくべきでした。 事前にインシデント対応の優先順位づけのルールを決め、お客様影響があるインシデントを最優先しようと決めていたのですが、いざインシデントが起きると十分に意識できなくなってしまいました。 個人情報の漏洩 競技の中盤に、対応が漏れていたサーバーからディレクトリリスティングによって個人情報が漏洩してしまいました。 チームのメンバーの中で対象のサーバーOSに慣れている人がおらず、対応に苦慮しました。 (設定変更をいくつも試したのですが、どれも上手くいきませんでした) 最終的には、WAFで当該ディレクトリへのアクセスを禁止することで何とか対応できましたが、もし現実で起きたら、と思うと恐ろしかったです。 上層部からの呼び出し 競技会の設定として、ある架空の企業の社員として競技会に臨むのですが、その企業の上層部にインシデントの対応状況を聞かれる(詰められる)場面がありました。 私は直接対応しなかったのですが、対応したリーダーのテンションが下がっており、なかなか厳しい状況だったようです。   MP利用時の不手際 MPでWAFの購入権を獲得できたので、さっそく購入しようと思ったのですが、競技会環境への接続に手間取り、結果として導入できたのは11時ごろになってしまいました しかし、この後に一部メンバーがWordPressなどのCMSの管理画面にアクセスできなくなりました。 最終的には管理画面への通信をWAFでブロックしてしまっていたのですが、WAFの導入前後で正常性確認をすることで、攻撃をうけたのか、自分たちの設定ミスなのか、切り分けできる状態を事前に作るべきでした。 普段の業務なら注意できていることも慌てている時にはおろそかになってしまうことを身をもって体感しました。 ちなみに、別のネットワークからならアクセスできることをチーム内で共有したことで解決の糸口が見つかり、些細な事でも共有する密なコミュニケーションの重要性を再認識させられました。 Analysis Day この日はHardening Dayで受けた攻撃や自分たちの取り組みを振り返る日として設けられていました。 競技会当日は時間がなく共有できなかった個人の取り組みの共有や、MPで購入したWAFを提供してくださったベンダーの方からどのような攻撃を受けていたのかをログをもとに説明など有意義な振り返りになりました。 Softening Day 最終日に行われたSoftening Dayでは、主に以下4つが行われました。 各チームの取り組みの発表 運営側からの競技会の説明 スポンサー賞 順位発表 私たちのチームは総合順位が12チーム中7位でしたが、技術点は1位でした。 (技術点が1位でしたが、総合順位が中位だったことは後述します) 自チームの結果発表(主催者から写真掲載の許可をいただいております) 技術点が1位だったことをチームメンバーと振り返りをした時に以下2つが挙がりました。 初動対応によるPWと権限変更 ファイアウォールの詳細な設定(ホワイトリストで制御) 特に2つ目のファイアウォールについては設定値の確認も入念に実施していたので、当日効果を発揮してくれていたのであれば嬉しいです。 振り返り 総括として、学んだことや今後に活かしたいことを記載します よかった点 チームビルディング ハードニング競技会で最も重要だと感じたのはチームビルディングです。 準備から当日の競技会、最後の振り返りまでチームメンバーとのコミュニケーションが本当に大切でした。 リーダーが率先してチームビルディングに取り組んでくれたことで、定例会でのコミュニケーションのしやすさに加え、チャットへはリアクションをすることや他のメンバーのタスクも一緒に考える、など風通しがよく、準備を進めやすかったです。 私個人としては、競技会中はチームは2つの場所に分かれて作業するのですが、お互いの現状を把握するために走って移動しており、歩数計が約1万歩になっていました。 演習の重要性の再認識 一度作成したインシデント対応フローやチーム内での報告方法を、模擬環境での演習や机上演習で利用することで、明らかになった改善点を多く修正できました。 それでも本番の競技会では、インシデント対応フローが上手く機能していないこともあったので、事前準備の段階で改善していたため本番でも一定の機能を果たせたのだと考えています。 インシデント対応や作業手順などは継続的にテストし、改善していくことが重要だと身をもって実感しました。 啓発点 ビジネスを考慮した判断 競技会では売上を増やすための対応が後手に回った結果、売上が増えず技術点は高いが順位は中位にとどまりました。 私含めてテクニカルチーム側が、インシデント対応のフローの優先順位に則って売上が上がっていないことへの対処を優先できていなかったことが原因だと考えています。 この反省は普段の業務に活かせると考えており、ビジネスとテクニカルが両方ともお互いのことを理解し、自分の取り組みが何につながるのかを把握することがチームの目標達成には重要だと感じました。 技術的スキルの不足 Softening Dayで他のチームの発表を聞いていると、OSSを活用したサーバー一括管理や複数サーバーをたてた模擬環境でのテストなど、技術的に高度な取り組みをしているチームもありました。 今の私ではそこまで手が回らなかった・技術的に本番で使えていたか怪しいと感じ、その差を痛感したことで自分も技術力を高めたいというモチベーションにもつながりました。 おわりに ここまで記載したとおり本当に多くの学びを得られたのは、実務で滅多に起こりえないセキュリティインシデントを(疑似的に)経験できるイベントだったからこそだと感じています。 今回、このような素晴らしい競技会を企画してくださった運営の皆様はじめ競技会に関わってくださった皆様、そしてチームメンバーに感謝申し上げます。 執筆者 衣川 琢磨(NTTビジネスソリューションズ株式会社 バリューデザイン部) セキュリティ分野の中でも特にサイバーハイジーンの実現にむけて支援するマネージドサービスの開発と運用に携わっています。 商標 WordPressは WordPress Foundation の登録商標です。 Linuxは Linus Torvalds 氏の登録商標です NECは 日本電気株式会社(NEC Corporation) の登録商標です。
1.はじめに 本記事では、LLM-as-a-Judge(AIによる要約評価手法)を紹介し、検証結果を共有します。 対象読者:AI活用や自然言語処理、要約評価に関心のある方 1.はじめに 2.背景・目的:LLMによる要約評価の課題と改善の可能性 3.評価手法の解説:BLEU/ROUGEとLLM-as-a-Judgeの違い BLEU/ROUGE(単語一致ベース) LLM-as-a-Judge(意味理解ベース) 4.検証の概要:LLMモデルと要約評価の実験条件 5.評価算定方法:LLM-as-a-Judgeによるスコアリング手法 評価指標 評価スコア 6.検証結果と考察:AI評価の精度比較 評価の平均スコア 人の目とLLM-as-a-Judgeの評価結果比較 誤りのある要約5件 個別の評価結果 考察ポイント 7.まとめ:LLM-as-a-Judgeで要約評価を効率化する未来 執筆者 商標 2.背景・目的:LLMによる要約評価の課題と改善の可能性 私の業務では、AI技術を活用して社内外のビジネス課題を解決しています。特にニーズが高いのが LLMによる文章要約 で、要約業務の効率化や要約データ活用へのニーズが増えています。 しかし、従来の評価手法(BLEUやROUGE)は単語一致ベースで、人間の評価と乖離が大きく、結局は人手評価が主体でした。複数人で評価する場合、主観によるばらつきが避けられません。 そこで、 LLMに評価を委ねる「LLM-as-a-Judge」 に注目し、人に近い評価ができるか検証しました。 3.評価手法の解説:BLEU/ROUGEとLLM-as-a-Judgeの違い 従来の評価手法BLEU/ROUGEの限界と、より人間に近い判断を目指すLLM-as-a-Judgeの特徴を解説します。 BLEU/ROUGE(単語一致ベース) 参照文とどれだけ似ているかをチェックする、シンプルな評価方法です。 単語やフレーズの一致率を数値化 メリット:定量評価が可能 デメリット: 言い換えを正しく評価できない 文脈や自然さを無視 参照文が必須 LLM-as-a-Judge(意味理解ベース) AIが文章の文脈・論理・自然さを判断し、参照文なしで評価できる新手法です。 LLMが文章の意味・文脈・自然さを総合判断 メリット: 「自然さ」「論理性」など複数観点で評価 人間の評価に近い傾向を示す可能性がある 参照文不要で創作やQAにも対応 デメリット: 評価基準の不透明性(評価の説明ができない場合が多い) モデル選定やプロンプト設計によって精度が変動 高性能LLMを使用する場合はコストがかかる 4.検証の概要:LLMモデルと要約評価の実験条件 コールセンターの会話データを用いて、以下の条件でLLM-as-a-Judgeの評価精度を検証します。 元データ:コールセンター会話のダミーデータ 評価対象:元データをLLMで要約したデータ5件(誤りのある要約5件) 評価LLM:オンプレミス対応OSSモデル(gpt-oss-120b/20b) 5.評価算定方法:LLM-as-a-Judgeによるスコアリング手法 論文に基づく指標で、AIが要約の誤りと重大度を判定し、減点方式で品質を評価します。 評価指標 LLM-as-a-Judge関連論文 *1 を参考に、以下の指標を設定します。 Issue Type (誤り種別)  Syntactic Label (文法的ラベル) Severity (重大度)  [補足事項] ・ 誤り種別の具体的な項目と重大度の重みづけ点数は論文で明記されていないため、本検証で独自に設定 ・ 文法的ラベルは論文に記載されている指標で設定 評価スコア 評価スコアは、論文に記載されている次の算出式により、100点を基準に減点方式で計算します。 スコアの算出式 スコア = (1 - (減点合計 ÷ 単語数)) × 100 [補足事項] ・ スコアは 0~100 点の範囲で評価し、負の値は 0 点として扱います。 ・ 要約の長さによる影響を調整するため、減点合計を単語数で割って正規化しています。 ・ 減点合計が非常に大きく、減点合計 ÷ 単語数 が 1 を超える場合、スコアは負となり 0 点で算出されます。この場合、全スコアの正規化には該当しません。 スコア算出の例 CRITICAL(致命的)数:3件 MAJOR(重要)数:1件 MINOR(軽微)数:0件 単語数:70 減点合計: CRITICAL:3 × 10点 = 30点 MAJOR:1 × 5点 = 5点 MINOR:0 × 1点 = 0点 合計:35点 スコア:(1 - 35÷70)× 100 =50点 6.検証結果と考察:AI評価の精度比較 gpt-oss-120bとgpt-oss-20bを用いて検証を行い、誤りのある要約の評価結果を比較しました。 評価の平均スコア gpt-oss-120bとgpt-oss-20bの平均スコアを比較した結果、gpt-oss-120bは人間の評価に近い傾向を示しましたが、gpt-oss-20bは要約の誤りを検出できませんでした。 LLM-as-a-Judgeで評価した平均スコア ※100点より低い(誤りを判別して減点している)ほど人の判断に近い LLM 誤りありの要約 gpt-oss-120b 68点 gpt-oss-20b 100点 gpt-oss-120b : 人間の評価と近い傾向があり、68点評価 gpt-oss-20b:誤りを検出できず、100点評価 以降は、誤りのある要約を適切に減点できた gpt-oss-120b の評価結果の例を紹介します。 人の目とLLM-as-a-Judgeの評価結果比較 誤りのある要約の判定結果は以下のとおりです。すべての要約で誤りが指摘され、そのうち3件は人の目で見た誤りと一致しました。総合的には、概ね人の目に近い評価結果となっています。 誤りのある要約5件 No 人の目で見た誤り判定 LLM-as-a-Judgeの誤り判定(抜粋) LLM-as-a-Judgeと人の目の判定比較 1 回答に欠損があるため ・画像の条件 原文では 「画像は文字や顔写真がはっきりと確認できること」が条件と明示されているが、要約ではこの条件が省略 されている。 LLM-as-a-Judgeは、人の目と 同じ観点を指摘 2 事実と異なる内容が含まれる ・電話の利便性を再確認 オペレーターが電話の利便性を再確認したという記述は元の文に存在せず 、事実と異なる。 同上 3 回答に欠損があるため ・mailの設定 確認 メールクライアントがGmailである旨が記載されておらず 、重要情報が欠落している。 同上 4 回答に欠損があるため ・連絡手順 ・返信時間の詳細 要約では「 返信時間を説明した」だけで数値情報が欠落 している。 LLM-as-a-Judgeは、人の目と 同じ観点の一部を指摘 5 回答に欠損があるため ・郵送先 ・通知方法 ・公式サイト活用案内 【概要】書類送付先「▲▲サービスセンター解約係」の 住所情報が欠落 しており、手続きに必要な重要情報が欠落しているため。 同上 誤りのある要約5件すべてで、LLM-as-a-Judgeも誤りを指摘。 3件(No1、2、3)は、人の目の指摘事項と一致。 残り2件(No4、5)は、人の目では複数の観点で指摘していたが、LLM-as-a-Judgeはその内の一部を指摘。 個別の評価結果 個別の評価結果の一つ(例:上記のNo2)を紹介します。No2-1では人間の評価と同じ指摘ができていますが、No2-2~2-4では、人間の評価では誤りとされなかった点をLLMが指摘しており、求める要約の粒度によっては、LLMへの追加指示などの対策が必要であることが分かりました。 LLM-as-a-Judgeの評価スコア:50点 * 要約文書 * 人の目で見た判定結果 * LLM-as-a-Judgeの評価結果 No Issue_type Syntactic_label Severity Applicable_parts Reason 2-1 Inaccuracy Intrinsic Predicate CRITICAL 【概要】…電話の利便性を再確認し… → 原文ではオペレーターが電話の利便性を再確認したという記述はなく、顧客が「電話が一番安心感がある」と述べただけであるため、内容が誤っている。 オペレーターが電話の利便性を再確認したという記述は元の文に存在せず 、事実と異なる。 2-2 Omission Number&Time MAJOR 【概要】…混雑時間帯やチャット・メール対応を案内… → 原文では「平日の午前10時~11時、午後2時~4時」と具体的な時間帯が示されているが、要約で省略されている。 重要な具体的情報(混雑しない時間帯)が欠落している。 2-3 Omission Attribute MAJOR 【概要】…混雑時間帯やチャット・メール対応を案内… → 原文では「AIチャットは公式サイト・▲▲サービスの問い合わせページから利用でき、FAQ等に即時対応できる」ことが説明されているが、要約に記載がない。 代替手段としてのAIチャットの利用方法・範囲が欠落している。 2-4 Omission Number&Time MAJOR 【概要】…チャット・メール対応を案内… → 原文では「メールは▲▲サービスの問い合わせフォームから送信し、1~3営業日以内に返信される」旨が示されているが、要約に抜けている。 メール問い合わせの手順と返信期限が欠落している。 Inaccuracy Intrinsic(内的不正確性)が1件判定されており(No2-1)、人の目と同じ誤りを指摘。 Omission(省略)が3件判定されているが(No2-2~2-4)、詳細レベルの情報不足を指摘しており、求める要約の粒度によって判断が異なる指摘。 考察ポイント 検証の結果、LLM-as-a-Judgeによる評価は人に近い判断が可能であり、評価を数値化することで公平性の向上が期待できることが分かりました。さらに、要約の粒度を明示することで判定誤差を減らせるほか、使用するLLMモデルの選定によって精度をさらに改善できる可能性があります。 LLM-as-a-Judgeは人間に近い評価を示す傾向がある 実務で導入する際には、評価観点を明確に定義し、基準を統一することで、より安定した評価が得られる可能性があります。   評価を数値化できるため、主観の排除により公平性の向上が期待される スコアリングをダッシュボード化(可視化)することで、改善ポイントを迅速に把握できる可能性があります。 要約粒度の指定(プロンプトチューニング)で精度改善の余地あり プロンプトチューニングで「要約の対象情報」「省略可能な情報」を明示的に指示し、ユースケースごとにテンプレート化することで、Omission(省略)判定の不要な誤差を減らせる可能性があります。 LLMの違いが評価精度に大きく影響 モデル選定時には、パラメータ数だけでなく、必要に応じて社内データで追加学習を行うことで、評価精度を改善できる可能性があります。 7.まとめ:LLM-as-a-Judgeで要約評価を効率化する未来 今回は、LLM-as-a-Judgeの検証結果をご紹介しました。LLM-as-a-Judgeは、人間の判断に近い結果を得られる可能性があり、要約評価の効率化と品質向上に有望な手法であることが示されました。今後は、プロンプトチューニングや高性能LLMの活用により、さらに精度を高め、文書評価業務全体の効率化を目指します。 執筆者 倉田裕紀(NTT西日本 技術革新部 AI技術担当) AIエンジニアとして、社内外のAIプロジェクトを技術支援 商標 「GPT」は、OpenAI OpCo, LLC の商標もしくは登録商標です。 *1 : A Survey on LLM-as-a-Judge
はじめに NTTビジネスソリューションズの平田です。 インターネットで安全に情報をやり取りするために欠かせない技術のひとつがTLS(Transport Layer Security)です。TLSでは、通信を暗号化するための共通鍵を安全に共有する仕組みとしてECDHE(Elliptic Curve Diffie-Hellman Ephemeral、楕円曲線暗号による鍵交換)が利用されています。本記事ではECDHEの仕組みをざっくりとしたイメージで解説します。 なお、この解説は直感的な理解を意図したものです。わかりやすさを優先したため、厳密さは不十分である点にご留意ください。 本記事は2025年11月時点の情報に基づきます。 対象読者 本記事が想定する対象読者は以下の通りです。 情報処理安全確保支援士の学習をされている方 ECDHEによる鍵交換の仕組みを知りたい方 背景 NTTグループでは 専門性を重視する人事制度 が導入され、NTT西日本グループにおいても社員個々人の専門性がこれまで以上に重視されるようになりました。社員の専門性向上のために、会社側から各種の研修、通信教育、オンライン学習プラットフォームのアカウント提供などの サポート があります(所属会社により多少の差異があります)。そのほかにも社内講師による勉強会やハンズオンなどが活発に開催されています。その一つとして情報処理安全確保支援士の対策講座を筆者が講師として開催し、約50名に参加いただきました。 講座の中で 2025年春試験(午後問2) に関連し、楕円曲線暗号(Elliptic Curve Cryptography, ECC)を利用した鍵交換(ECDHE)の説明がわかりやすかったと好評だったので、Engineers' Blogの記事として再編集して掲載します。 TLSによる暗号化 TLS(Transport Layer Security)では、通信データの暗号化に共通鍵暗号方式が用いられ、共通鍵(本記事ではトラフィック鍵と呼びます)は公開鍵暗号方式を用いて事前に安全に共有されます。 送信者から受信者に送られるデータは、送信者側で暗号化され、受信者側で復号されます。この暗号化と復号には、送信者と受信者が共有する同一のトラフィック鍵が使用されます。 トラフィック鍵による暗号化と復号 通信経路を流れるデータは暗号化されているため、第三者が通信を盗聴したとしても、データの内容を解読できません。しかし、第三者がトラフィック鍵を入手し通信を盗聴した場合にはデータを解読できてしまい、その結果情報漏えい(機密性の侵害)が発生する可能性があります。 そのため、トラフィック鍵は送信者と受信者以外には知られないよう、暗号化通信の開始前に安全な方法で共有する必要があります。その際に利用するのが鍵交換です。鍵交換の際には送信者と受信者の間で通信が行われますが、仮にこの通信が盗聴されたとしても、トラフィック鍵が漏えいすることはありません。この安全性は、楕円曲線暗号が基盤とする「楕円曲線上の離散対数問題の計算困難性」によって保証されます。 楕円曲線暗号の仕組み 楕円軌道上に既知の基準点Gがあります。この楕円軌道上でGをn回加算する(n倍)すると、2G、3G、…と楕円軌道上を進みます。図の2G~6Gがそれにあたり、2周目以降も楕円軌道上のどこかに位置します(7G以降も存在)。 楕円軌道 秘密鍵a(自然数)を決めGをa回加算した位置をAとすると、Aは楕円軌道上に存在します。このとき A = aG と表すことができ、Aが公開鍵になります。 a(秘密鍵)からA(公開鍵)は簡単に求めることができますが、A(公開鍵)からa(秘密鍵)を求める逆算( a = A/G )は非常に難しく、総当たり以外に効率的にaを見つける方法が知られていません。 この「逆算の難しさ」が楕円曲線暗号の安全性の根拠です。 楕円曲線暗号を利用した鍵交換 送信者αさんと、受信者βさんが鍵交換を行う手順を説明します。ここで交換する「鍵」とは、トラフィック鍵のもとになる値です。 ①基準点Gの決定 αさんとβさんは基準点Gを共有します。(実際にはプロトコルとしてGがあらかじめ決まっており、お互いGを知っています) ①基準点Gの決定 ②秘密鍵と公開鍵のペアを生成 楕円曲線暗号を使ってそれぞれ一時的な秘密鍵と公開鍵のペアを生成します。具体的には次の手順です。 αさんは秘密鍵aを決定します。aとGから公開鍵Aを生成します( A = aG )。 βさんは秘密鍵bを決定します。bとGから公開鍵Bを生成します( B = bG )。 ②秘密鍵と公開鍵のペアを生成 ③公開鍵の送信 αさんは自身の公開鍵Aをβさんに送信します。同様に、βさんは自身の公開鍵Bをαさんに送信します。 ③公開鍵の送信 ④共通の値Kを算出 αさんは受信したBと自身の秘密鍵aを掛け算し、Kを求めます。K = aB = a(bG) = abGです。 同様にβさんは受信したAと自身の秘密鍵を掛け算し、Kを求めます。K = Ab = (aG)b = abGです。 ④共通の値Kを算出 この計算によって双方が同じ値Kを算出でき、結果としてKを共有できます。このKをもとに、トラフィック鍵を生成します。 仮に第三者がこの鍵交換の通信(③)を盗聴しAまたはBを入手したとしても、Kを計算することはできません。前述の通り、公開鍵(AやB)から秘密鍵(aやb)を逆算することが難しいからです。 まとめ ECDHEの仕組みをざっくりとしたイメージとして解説しました。厳密な解説ではない点はご容赦ください。 通信相手と同じ値を算出できる仕組みを初めて理解した際は、そのシンプルさに驚いたものです。ブラウザでリンク先を気軽にクリックする度に、このような鍵交換が行われていることを思い出していただければ幸いです。 補足 直感的に理解いただけるよう楕円を利用して解説しましたが、楕円曲線と楕円は本質的に異なるものです。実際に利用される楕円曲線( y 2 = x 3 + ax + b )は閉じない曲線です(「Ω」に似た形状で https://ja.wikipedia.org/wiki/%E6%A5%95%E5%86%86%E6%9B%B2%E7%B7%9A#/media/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB:ECClines-3.svg の右側の図です)。 曲線を用いた解説のため実数をイメージされるかもしれませんが、実際には自然数による離散的な値を利用するのでマス目上の点の集合です。具体的なイメージは https://graui.de/code/elliptic2/ で確認できます。G、nG、A、Bがこのマス目上の点にあたります。 G、2G、3G、…、と等間隔で楕円軌道上の位置が変化するような解説を行いましたが、実際の楕円曲線上では等間隔ではありません。 執筆者  平田賀一(NTTビジネスソリューションズ(株)バリューデザイン部 システム開発部門) PaaS/SaaSの開発・運用、社内案件のコンサル、エンジニア育成、NTT WEST Engineers' Blog運営などに携わっています。 技術士(情報工学部門)/CISSP/情報処理安全確保支援士 参考資料・出典 本記事を執筆するにあたり、以下のサイトを参考にしました。 ja.wikipedia.org gaiax-blockchain.com qiita.com graui.de