株式会社豆蔵のブログ - TECH PLAY

TECH PLAY

株式会社豆蔵

株式会社豆蔵 の技術ブログ

110

これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第16日目の記事です。 1. はじめに # 9月に Kiroを使った仕様駆動開発の記事 を執筆してから、約3か月経ちました。 その間に、生成AIを取り巻くエコシステムやツールは驚くべき速度で進化を続けています。 最近話題のGeminiも、リリース当初と比べると、業務利用を視野に入れられるレベルまで急速に成熟していると感じています。 今回はGitHub が提供する仕様駆動開発(Spec-Driven Development; SDD)のツール「 Spec Kit 」と、Model Context Protocol(MCP)に対応した AI クライアントである Gemini CLI を組み合わせて、インフラストラクチャを生成する開発手法を試してみます。 前回の記事では「Kiro × Claude」で SDD + Terraform を扱いました。 本記事ではこれを「Spec Kit × Gemini」に置き換えたうえで、特にビジネスの現場では重要となるコスト最適化・コスト把握を AWS Pricing MCP Server を使って行えるようにします。 これらを組み合わせることで、設計思想(Why)から具体的な実装(What)、そしてコードとアーキテクチャの品質保証までを一気通貫で行う、インフラ構築フローをSpec Kitでどこまで実現できるのかを検証していきます。 2. 記事のゴール # 本記事のゴールは次の通りです。 Spec Kit を使いこなせるようになる GitHub Spec Kit の 4 段階プロセス(Specify, Plan, Tasks, Implement)に沿って、インフラの仕様を定義し、具体的なコード(IaC)に落とし込む流れを体験します。 SDD を踏まえたインフラ構築フローを理解する 設計思想(Why)から実装(What)、そして品質保証までを一気通貫で行う、AI を活用した新しいインフラ構築のやり方を学びます。 コストを含めた品質保証を実現する方法を知る AWS Pricing MCP Server を組み合わせ、Terraform コードから構築されるインフラのランニングコストを見積もりつつ、仕様レベルでコスト制約を織り込むアプローチを理解します。 実践的なインフラを構築できるようになる 上記フローを通じて、S3 と CloudFront を用いた静的ウェブサイトと、そのアクセス状況を可視化できる CloudWatch メトリクスを持つ、実践的なリソースを構築できる状態を目指します。 3. 事前知識 / 前提 # 本記事は、次のような読者を想定しています。 対象読者 インフラ構築・運用や、クラウド環境の設計に興味がある方 SDD(Spec-Driven Development)や IaC(特に Terraform)に興味がある方 生成 AI を用いた開発フローを、インフラ領域でも試してみたい方 本記事で扱わないこと GitHub Spec Kit 自体のインストール手順や細かな設定方法 SDDにおける細かいプロンプト指示の内容 MCP Server(例: AWS Pricing MCP Server)のセットアップ方法の詳細 生成 AI(Gemini など)のアカウント作成や各種設定の手順 Terraform や AWS のコードの解説・公開 実施環境 Mac (M1) / macOS Sequoia 15.5 [1] VSCode: 1.107.0 Gemini CLI: 0.20.2 Gemini 2.5 系列モデル(Flash / Pro など) Spec Kit v0.0.90 これらの前提知識・環境は事前に整っているものとして、本記事では「どう組み合わせて活用するか」という観点に焦点を当てます。 4. 「Vibe-Coding」の限界と見過ごされるコスト # AIコーディングエージェントの能力は日々向上していますが、その利用方法の多くは、開発者の曖昧な指示(Vibe)に依存した「Vibe-Coding」に留まっています。 このアプローチでは、コードがコンパイルしなかったり、開発者の意図を正確に反映しなかったりといった、目先の生成エラーに注目が集まりがちです。 とりわけ深刻なのは、一見正常に動作しているコードが、Vibe-Coding によって大量に生まれてしまう点です。 これらのコードは、次のような「見過ごされがちなコスト」を内包しています。 属人化とブラックボックス化 生成時のプロンプトや前提条件が十分に残らないため、「なぜその設計になっているのか(Why)」がコードから読み取れません。 結果として、他の開発者には触りづらいブラックボックスとなり、修正・改善のたびに大きな認知コストが発生します。 アーキテクチャの一貫性低下 その場その場の Vibe-Coding で機能追加を続けると、システム全体のアーキテクチャが当初の設計意図から徐々にずれていきます。 IaC の場合、環境ごとに微妙に異なる定義が増え、「どれが正なのか」を後から整理・リファクタリングするコストが跳ね上がります。 レビュー・監査コストの増大 レビュー担当者は、コードを読むたびに「そもそも何のための変更か?」を会話履歴やコミットメッセージから逆算する必要があります。 セキュリティやコンプライアンスの観点では、どのコードがどの前提・ポリシーに基づいて生成されたかが追いきれず、チェックに時間がかかります。 このように、Vibe-Codingは目先の生産性を一時的に向上させるかもしれませんが、長期的な保守性や品質、セキュリティの観点では、むしろコストを増大させる要因になりえます。 5. 仕様駆動開発(SDD)の哲学 # Vibe-Codingがもたらす長期的な課題に対し、GitHubは公式ブログでSpec Kitを発表し、新たな開発パラダイムとして「仕様駆動開発(SDD)」を提唱しました。 GitHubは、その哲学について次のように述べています。 "That’s why we’re rethinking specifications — not as static documents, but as living, executable artifacts that evolve with the project. Specs become the shared source of truth. When something doesn’t make sense, you go back to the spec; when a project grows complex, you refine it; when tasks feel too large, you break them down." (だからこそ私たちは仕様を再考しています。それは静的なドキュメントではなく、プロジェクトと共に進化する生きた、実行可能な成果物として。仕様は共有された真実の源となります。何か理解できないことがあれば仕様に戻り、プロジェクトが複雑になればそれを洗練させ、タスクが大きすぎると感じたら分解するのです。) この引用が示すエッセンスは、次の2つの重要な概念に集約されます。 仕様は「生きた実行可能な成果物」である 一度作られて陳腐化する静的なドキュメントではなく、プロジェクトの進化に合わせて更新され続け、ツールやAIがコードを生成・テスト・検証する際に直接利用する「実行可能」なファイルそのもの、という考え方です。 仕様は「信頼できる唯一の情報源」である コードがどう動作すべきかを定義した「契約」として機能します。開発者は迷ったときに立ち戻る原点となり、AIエージェントにとっては振る舞いを決定する上での絶対的な拠り所となります。 このように仕様と実装の一貫性を強く保つことこそが、Vibe-Codingの混沌に秩序をもたらし、AIの力を最大限に引き出す鍵である、というのがGitHubの示す方向性です。 6. GitHub Spec Kitの役割 # Spec Kit は、この SDD を AI コーディングエージェント(GitHub Copilot、Gemini CLI、Claude Code など)のワークフローに取り込むための オープンソースのツールキット です。 /speckit.specify :仕様(Spec)を作成する /speckit.plan :技術スタックやアーキテクチャ方針を整理する /speckit.tasks :作業単位に分解したタスクリストを作る /speckit.implement :タスクに基づいて実装を進める といったコマンドを通じて、「仕様 → 計画 → タスク → 実装」の流れを構造化し、AI に対しても一貫したコンテキストを与えられるようにします。 7. Spec Kitの構造化された開発ワークフロー # Spec Kit の大きなメリットは、「どの生成 AI を使うか」に依存しすぎず、共通のワークフローで開発を進められる点です。 現実には、複数の生成 AI を同時に利用することはコスト面で難しい場合も多く、組織として利用できる AI が限定されるケースもありますが、Spec Kit のワークフローはそうした制約の中でも再利用しやすい構造になっています。 Spec Kitは、AIとの協調作業を円滑にするため、次の4段階の明確なプロセスを定義しています。各フェーズは、特定のコマンドを通じて実行されます。 1. Specify(仕様作成)― WhatとWhyの定義 /speckit.specify コマンドを使用し、技術スタックではなく、 ユーザーのジャーニー、期待される成果、成功基準 に焦点を当てて仕様(Spec)を定義します。 仕様は、利用者や要件が変わるたびに更新される「生きた成果物」として扱われ、インフラの構成もこの Spec を起点に設計されます。 2. Plan(計画作成)― 制約の組み込み /speckit.plan コマンドで技術的な方向性を決定します。 技術スタック、アーキテクチャ、コンプライアンス要件、性能目標 など、エンタープライズ制約を AI エージェントに提供し、 包括的な技術実装計画を plan.md として生成させます。ここで MCP Server からの制約も統合されます。 3. Tasks(タスク分解)― レビュー可能な単位へ /speckit.tasks コマンドを使用し、Plan に基づき、 独立して実装・テスト可能な小さな作業単位 (レビュー可能な差分)に分解します。 依存関係や並列実行可能なタスク [P] が明示されることで、「どこから手を付けるべきか」が分かりやすくなります。 4. Implement(実装と検証)― 焦点を絞った変更 /speckit.implement コマンドで、エージェントがタスクリストに従って実装します。開発者は大規模なコードの塊ではなく、 特定の問題を解決する 焦点の絞られた変更 をレビューします。これにより、AI が生成した変更を人間が理解・検証しやすい単位に保つことができます。 7.1. Spec KitとKiroのプロセス対応 # Spec Kit と AWS Kiro はどちらも SDD を支援するツールですが、フェーズ名や成果物の切り方が少し異なります。本記事では、おおよそ次の対応関係を意識して解説します。 観点 Spec Kit のフェーズ / コマンド 主な成果物(Spec Kit) 参考: Kiro のプロセス / ファイル例 要件・意図の明確化 Specify / /speckit.specify プロダクトの目的・前提・スコープなどを整理した仕様ファイル(例: spec.md ) Requirements フェーズ / requirements.md 実現方式の検討 Plan / /speckit.plan アーキテクチャ方針・技術選定・インフラ構成などをまとめた計画ファイル(例: plan.md ) Design フェーズ / design.md 作業単位への分解 Tasks / /speckit.tasks 実装・テスト可能な単位に分解されたタスクリスト(例: tasks.md ) Tasks / Implementation のタスク一覧 実装の実行・検証 Implement / /speckit.implement タスクリストを基にしたコード・テスト・設定ファイル等の変更 Implementation フェーズ全体(フックやタスク実行) ※ Kiro は「Requirements → Design → Implementation」の 3 フェーズ構成で、Implementation の中にタスク実行やコード生成が含まれます。一方、Spec Kit は Tasks と Implement を分離することで、「タスク分解」と「実装実行」を明示的に分けている点が特徴です。 8. AWS Pricing MCP Server によるコスト制約の実現 # 本章では、 AWS Pricing MCP Server の役割と、それを Spec Kit のワークフロー、とくに Implement フェーズのあとに統合することで、コストの観点からどのように制約をかけられるか解説します。 8.1. Model Context Protocol (MCP) の役割 # MCP および MCP Registry を利用することで、Gemini CLI などの MCP 対応クライアントから AWS Pricing MCP Server といった外部サービスに接続できます。 Spec Kit の「Spec / Plan / Tasks / Implement」というワークフローの後にMCPを追加することで、外部ツールからの制約や推奨事項(本記事では主にコスト情報)を自然に統合します。 8.2. AWS Pricing MCP Serverによるコスト把握 # AWS Pricing MCP Server は、AWS の料金情報にアクセスするための MCP Server です。 Gemini CLI から呼び出すことで、サービス種別・リージョン・利用想定などを入力し、概算の料金を問い合わせることができます。 Spec Kit の文脈では、Implement フェーズで生成された Terraform コードやアーキテクチャ案に対してコストを問い合わせることで、 どの構成が月額コストを抑えられそうか トラフィック増加時にどの程度コストがスケールしそうか どのリソースがコストのボトルネックになりそうか といった観点を、AI エージェント経由で評価させることができます。 8.3. Implementフェーズ後のコスト評価への統合 # /speckit.implement コマンドや、Implement フェーズで Terraform コードが生成・デプロイされたあとに、AWS Pricing MCP Server を呼び出し、実際のリソース構成に基づくコスト情報や制約・推奨事項を取得します。 これにより、パフォーマンスや可用性などの非機能要件に加え、想定トラフィックに対するランニングコストの見積もりを踏まえて、アーキテクチャやコードをリファインするループを回せるようになります。 Implement実行後は次のような流れで進めます。 リソース構成からコストを試算する パフォーマンスとコストのバランスを評価する 必要に応じてPlan/Specを修正する レイヤ構造として見ると、次のような関係になります。 flowchart LR subgraph Workflow["Spec Kit ワークフロー"] S["spec.md<br>What / Why"] P["plan.md<br>How + 制約反映"] T["tasks.md"] I["implement.md / 構築"] S --> P --> T --> I end subgraph Artifact["成果物"] A["Artifact(Terraform コードなど)"] end subgraph MCP["MCP サーバ群"] M1["AWS Pricing MCP Server"] end I -->|Apply| A I -->|コスト確認| M1 M1 -->|コスト最適化| I 9. 実践:CloudFrontとS3で構築する静的サイトとアクセス可視化 # 本章では、Spec Kit と Gemini CLI を用いて、S3 と CloudFront による静的ウェブサイトを構築し、そのアクセス状況が CloudWatch で可視化されるまでを追います。 まずはSDDで静的ウェブサイトを構築し、その後、CloudWatch でアクセス状況を可視化できるダッシュボードの作成という、仕様を追加します。 ここでのゴールは、単にインフラを構築するだけでなく、CloudWatch コンソールでリクエスト数のグラフが動くのを確認するところまでを実践することです。 これにより、意図(Spec)が実際の運用(Monitoring)に繋がる流れまでを意識するためです。 flowchart LR subgraph Client["クライアント"] U[ユーザー/ブラウザ] end subgraph AWS["AWS インフラ(Terraform で構築)"] CF[CloudFront ディストリビューション] S3[S3 バケット(静的サイト)] CW[CloudWatch メトリクス] end U -->|HTTPS リクエスト| CF CF -->|オリジン要求| S3 CF -->|アクセス状況を送信| CW 9.1. Gemini.mdの設定と、specify init - プロジェクトの作成 # specify init <PROJECT_NAME> でプロジェクトを作成します。 作成と同時に、次のような階層でファイルが作成されます。 原則、生成されるファイルはすべて英語となっているため、次のプロンプトと Gemini.md を設定することを推奨します。 # Gemini.md ## 使用言語 日本語を利用してください。 英語の場合は日本語に訳してください。 # プロンプト .specify配下のtemplatesの各ファイルを、日本語に直してください。 +---.gemini | \---commands | speckit.analyze.toml | speckit.checklist.toml | speckit.clarify.toml | speckit.constitution.toml | speckit.implement.toml | speckit.plan.toml | speckit.specify.toml | speckit.tasks.toml | speckit.taskstoissues.toml | +---.specify | +---scripts | | \---bash | | check-prerequisites.sh | | common.sh | | create-new-feature.sh | | setup-plan.sh | | update-agent-context.sh | | | \---templates | agent-file-template.md | checklist-template.md | plan-template.md | spec-template.md | tasks-template.md (その他多数) 9.2. Constitution ― 「規約」を定義 # まず、 /speckit.constitution を実行します。 Gemini では「憲法」と訳されますが、個人的には訳し方が好みではなかったので、「規約」とするようにこの constitution も設定しました。 簡単な指示を出すと、 <workspace-name>/.specify/memory/constitution.md が作成されます。 constitution は「このプロジェクトでは何を必須(/許容)にするか」を先に固めるためのもので、後続のSpec/ Plan/Tasksの品質を安定させる土台になります。 ここでは実際に作成した constitution.md から、特に重要な原則だけ抜粋します。 (読みやすさのため一部要約) # spec-kit-entry-s3-static-site 規約 ## Core Principles 1. セキュリティ: 最小権限(開発環境はアクセスログ取得不要) 2. 状態管理: S3などのリモートバックエンド(ロックは必須としない) 3. IaCのモジュール化: 機能単位で独立モジュール 4. 命名規則: 一貫した命名規則を必須化 5. 静的解析とテスト: `tfsec` / `tflint` + MCPチェック必須 7. ドキュメントの言語: 日本語で記述 9. コード内言語: 変数の `description` 等も日本語 9.3. Specify ― 「何を」作るかを定義 # まず spec.md を作成し、「セキュアで高速な静的ウェブサイト」という目的を定義します。 成功基準としては、例えば次のような条件を明記します。 CloudFront のエンドポイントにアクセスすると、静的コンテンツが HTTPS で配信されること spec.md は(特に非機能要件まで含めると)それなりの分量になるので、 ここでは「静的サイトを S3 + CloudFront でホスティングする」例として、実際に作成した spec.md から雰囲気が伝わる箇所だけ抜粋します。 (読みやすさのため一部要約) # 機能仕様書: S3静的サイトホスティング ## ユーザーシナリオとテスト *(必須)* ### ユーザーストーリー1 - S3バケットのプロビジョニング (優先度: P1) - 受け入れ: 静的サイトホスティングが有効なS3バケットが作成される ### ユーザーストーリー2 - CloudFrontによるコンテンツ配信 (優先度: P2) - 受け入れ: CloudFront URLへHTTPSアクセスすると `index.html` が表示される ## 機能要件 (以下、FR) - FR-003: S3バケットへの直接のパブリックアクセスはブロックされる - FR-005: CloudFrontはオリジンアクセス制御(OAC)を使用してS3にアクセスする ## 成功基準 *(必須)* - SC-001: CloudFront URL経由で `index.html` がHTTPSで5秒以内に表示される 9.4. Plan ― 「どう」作るかを計画 # /speckit.plan 実行時には、技術スタック(S3, CloudFront, OAC)やセキュリティ・運用要件など、主に非機能要件とアーキテクチャ制約を与えます。 この段階では、まだ具体的な Terraform コードが存在しないため、AWS Pricing MCP Server を直接呼び出して厳密なコスト試算は行いません。 代わりに、トラフィック量の見込みやざっくりとした予算感などを Spec / Plan に書き込み、後続の Implement フェーズで生成されるコードに対してコスト評価する前提を整えます。 これにより生成される plan.md には、例えば次のような方針が盛り込まれます。 S3 バケットはプライベートとし、直接のパブリックアクセスは許可しない CloudFront からのみアクセスできるよう、OAC(Origin Access Control)を利用する Plan は「どう作るか」をレビュー可能な形に落とし込むのが目的なので、チェックリストや構成の方針が(ある程度の粒度で)明文化されます。こちらも plan.md から抜粋します。 ( specs/<feature>/ のようなディレクトリの配置方針もここに書かれます)。 # 実装計画書: S3静的サイトホスティング ## 規約チェック - [x] 1. セキュリティ: 計画は最小権限の原則に準拠しているか?(S3パブリックアクセスを禁止し、OACを利用) - [x] 2. 状態管理: 対象環境に対して、ロック付きのリモート状態管理が構成されているか?(Terraform Cloud/S3バックエンドを前提) - [x] 3. IaCのモジュール化: 設計は再利用可能なモジュールを促進しているか?(s3, cloudfrontでモジュール分割を計画) ## プロジェクト構造 specs/001-s3-cloudfront-site/ ├── plan.md ├── spec.md └── checklists/requirements.md IaCプロジェクトとして、以下のTerraform標準レイアウトを採用します。 構造の決定: 上記のTerraformモジュール構成を採用し、環境(`environments`)と再利用可能なコンポーネント(`modules`)を明確に分離します。これにより、憲法の「モジュール化」の原則を遵守し、将来的な環境追加(例: 本番環境)にも容易に対応できます。 ```text . ├── environments/ │ └── dev/ # 開発環境用のルートモジュール │ ├── main.tf │ ├── variables.tf │ ├── outputs.tf │ └── backend.tf # リモートバックエンド設定 │ └── modules/ ├── s3-static-site/ # S3バケットと関連リソースを管理するモジュール │ ├── main.tf │ ├── variables.tf │ └── outputs.tf │ └── cloudfront-cdn/ # CloudFrontディストリビューションを管理するモジュール ├── main.tf ├── variables.tf └── outputs.tf 9.5. Tasks ― 実装タスクへ分解 # Plan に基づき、Gemini が具体的なタスクリストを生成します。例としては次のようなタスクが含まれます。 S3 バケットの作成と基本ポリシーの設定 CloudFront ディストリビューションの作成(オリジンに S3 を指定) OAI(Origin Access Identity)の作成と、S3 バケットポリシーへの組み込み 依存関係を考慮しながらタスクが並べられることで、Terraform 実装の順序も明確になります。 以下が、実際に作成された tasks.md からの抜粋です。 (フェーズ分割、依存関係、独立したテスト観点などがセットで出てくるのが特徴です。必要に応じて要約しています) # タスク: S3静的サイトホスティング > `[P]` が付いたタスクは、並列に実行できます。 ## フェーズ1: セットアップ - [x] T001 [P] `environments/dev` ディレクトリを作成する - [x] T002 [P] `modules/s3-static-site` ディレクトリを作成する - [x] T003 [P] `modules/cloudfront-cdn` ディレクトリを作成する ## フェーズ3: ユーザーストーリー1 (P1) - [x] T010 [US1] S3バケット + Website設定 + パブリックアクセスブロックを定義する ## フェーズ4: ユーザーストーリー2 (P2) - [x] T015 [US2] OAC と CloudFront ディストリビューションを定義する ## フェーズ5: ユーザーストーリー3 - TerraformによるIaC化 (優先度: P3) 目標: インフラ定義の品質と再現性を保証するためのプロセスを確立します。 独立したテスト: `terraform plan` が差分なく完了すること、静的解析ツールが警告を出さないことを確認できます。 9.6. Implement ― 実装と動作確認 # Gemini が各タスクを Terraform コードとして実装し、デプロイを行います。 デプロイ後、CloudFront のエンドポイントに数回アクセスし、静的コンテンツが HTTPS で配信されることを確認します。 この確認作業をもって、最初の仕様で定義した成功基準が満たされたことを検証します。 さらに、生成された Terraform コードやデプロイされたリソース構成を入力として、AWS Pricing MCP Server を呼び出し、ランニングコストの概算を取得します。 これにより、 想定していた予算内に収まりそうか ボトルネックになりそうなサービスはどこか といった観点を踏まえて、Plan / Tasks / Implement を再度見直すフィードバックループを回すことができます。 9.7. 要件・リソースの追加 # さて、前項で静的サイトの作成・公開は無事に完了しました。 ここからは、サイトのアクセス状況を CloudWatch で可視化する要件を追加してみます。 追加する仕様(成功基準の例)は、次のようになります。 アクセス後、CloudWatch の CloudFront メトリクスでリクエスト数がカウントされること これに対応して、Plan / Tasks には例えば次のタスクを追加します。 CloudWatch で確認するためのメトリクスやログ設定の有効化 Implement フェーズでは、これらのタスクに基づいて Terraform コードを拡張し、再度デプロイを行います。 デプロイ後、CloudFront のエンドポイントにアクセスし、CloudWatch の CloudFront メトリクス画面で「Requests」グラフにデータが反映されることを確認します。 この確認作業をもって、追加した仕様が満たされたことを検証します。 10. SDD が特に有効な 3 つのシナリオ # Spec Kit の背景にある Spec-Driven Development は、特に次の 3 パターンで役立つと GitHubの公式ブログ で提示しています。 Greenfield(ゼロからの新規開発) これから新しくシステムを作る場面では、「コードから考える」のではなく「意図と制約から考える」ことが重要です。 仕様(Spec)をきちんと書き、Plan / Tasks / Implement を順に進めることで、アーキテクチャの迷子になりにくくなります。 Feature work(既存システムへの機能追加) 既存のインフラやシステムに対して機能追加を行う場合は、影響範囲の洗い出しや既存ポリシー・命名規約との整合といった点を、Spec / Plan フェーズで明文化しておくことで、レビューやロールバックがしやすくなります。 仕様駆動開発が最も威力を発揮する分野だと GitHub 側が推しています。 Legacy modernization(レガシーの近代化) 手作業やスクリプトで運用されているレガシーシステムをモダンなアーキテクチャや IaC に移行する際、暗黙知になっている仕様や制約を Spec / Plan として言語化しながら段階的に置き換えていくのに、SDD は相性が良いとされています。 11. 利用してみた感想 # 実際に Spec Kit を Kiro と比較しながら使ってみて、次のような違いを感じました。 レビューの厳格さ Spec Kit は、仕様・計画・タスク・実装の各段階でこまめに人間にレビューを求めてきます。AI がまとめた内容を読み込みながら一歩ずつ進めるスタイルのため、手戻りは少ない一方で、進行のテンポはややゆっくりになります。 Kiro の進め方 Kiro は、AI 側がある程度まとめた状態まで一気に進めてから人間にレビューを返してくる印象で、少ないやり取りでテンポよく進めたい場合に向いています。 一方で、今回の検証では次のような点も気になりました。 Constitution(規約)を完全には守らず、たまにルールから外れた提案をしてしまうことがある Kiro と比べると、SDD の各フェーズ(Requirements / Design / Implementation)の境界が曖昧になりやすい これらは、Spec Kit と組み合わせる AI エージェントの実装やプロンプト設計にも依存する部分が大きいと感じています。 総じて、 ゼロから新しいシステムをじっくり設計したい(Greenfield) チームで仕様・タスクまでをきちんとレビューしながら進めたい といったケースでは、Spec Kit の方がフィットしやすいと感じました。 一方で、 まずは SDD を軽めに体験してみたい AWS プロダクト中心の構成で、Kiro のテンプレートやガイドに乗って進めたい といった場合は、Kiro を選ぶ方が導入のハードルは低いと思います。 12. 実行可能な仕様の実現例 # 本記事で扱った S3 静的サイトの例では、次のような形で「実行可能な仕様」が実現されます。 spec.md ユースケース・制約・非機能要件をまとめたドキュメント plan.md Spec で定義した要件や、非機能要件・アーキテクチャ制約を整理した方針 使用する AWS サービスや Terraform モジュール構成の決定 tasks.md 実装・テスト・監査タスクに分解された ToDo リスト Terraform コード これらの成果物から導かれた、具体的な IaC 実装 Implement 後に AWS Pricing MCP Server を用いてコスト評価する対象 重要なのは、 コード単体ではなく、「仕様 → 計画 → タスク → コード」の流れ全体が一貫していること です。Spec Kit は、この流れを AI を介して半自動化しつつも、人間がレビューしやすい単位に分割してくれる点に価値があります。 13. 今後の発展と課題 # 最後に、今後の展望と課題を簡単に整理します。 13.1. 期待できる発展 # IDE / エディタとの連携強化 現在はAI内でコマンドを叩くのに対し、VS Code などで /speckit.* コマンドを直接叩けるようになれば、より自然なワークフローになるはずです。 複数実装案の比較 同じ Spec / Plan から複数の実装パターン(例:S3 単体 / S3 + CloudFront / S3 + CloudFront + WAF)を生成し、コスト・運用性・セキュリティなどで比較する、といった使い方も考えられます。 大規模プロジェクトへの適用 マイクロサービス群や複雑なネットワーク構成など、より大きなスコープでの Spec-Driven Development への展開も今後のテーマになりえます。 13.2. 現時点での課題・注意点 # MCP エコシステムの成熟度 MCP や各 MCP Server はまだ進化の真っ最中であり、仕様変更やバージョンアップの影響を受けやすい段階です。 セキュリティと権限管理 MCP Server の実装やデプロイ方法によっては、資格情報の取り扱いやアクセス制御に注意が必要です。 「人間のレビュー」をどう組み込むか 最終的な責任は人間にあります。Spec / Plan / Tasks / コードの各段階で、どのようなレビューを行うかをチームとして設計することが重要です。 14. 最後に # 本記事で扱った内容から、大事なポイントを一言で表すと、 「コードが起点」から「仕様・意図が起点」へと軸足を移す ということだと考えています。 コードはあくまで「仕様と意図を具現化した結果」であり、AI や MCP Server はその橋渡しを支援する存在として捉えると、Spec-Driven Development のコンセプトが理解しやすくなるはずです。 また、今回 Spec Kit を利用してみて、SDD を支援するツールそのものだけでなく、組み合わせる AI や MCP Server によって得意な領域やワークフローが変わることも実感しました。今後も、インフラ構築や運用の現場での適用例を増やしながら、より良い組み合わせ方を探っていきたいと思います。 仕様駆動開発という考え方自体は、特にインフラ構築において長期的な保守性と品質を支える重要なアプローチだと感じています。 本記事が、読者の皆さまが自分たちのプロジェクトで SDD や Spec Kit、そして AI / MCP を試してみるきっかけになれば幸いです。 執筆時点では、Windows上のGitBashでGemini CLIをnpmインストールできるのですが、文字化けを起こしたり色々とトラブルがあります。 これはWindows環境では、Gemini CLIが内部的にPowerShellを利用していることに起因するためです。Windows環境ではデフォルトのターミナルをPowerShellにすることで、それらのトラブルは解消します。 ↩︎
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第12日目の記事です。 --> Information 記事の内容に不正確な記述が含まれていたため 2025/12/15 に追記・修正を行いました はじめに # ビジネスソリューション事業部の塚野です。現在、私がアサインされている案件ではパブリッククラウドとして Google Cloud を利用しています。 もともとは AWS を業務や個人開発で使っており、インフラ構成もサービス選定も「AWS の考え方」を土台にしてきました。しかし案件アサインに際し Google Cloud を本格的に学びはじめ、その設計思想やネットワークモデルが AWS と大きく異なることに気づきました。 キャッチアップの際には、AWS の常識をそのまま Google Cloud に当てはめて混乱した経験があり、両者を横並びで比較した体系的な資料が欲しいと強く感じました。 そこで本記事では、AWS 上に用意したシンプルなデモアプリケーションを題材にしながら、 「同じ構成を Google Cloud で実現するとどうなるのか?」 という観点で、Google Cloud のプロダクトや設計思想を AWS と比較しながら紹介していければと思います。 また、本記事では AWS ユーザーを対象とし、AWS ユーザーが Google Cloud を理解する際の指針になることを目指すため、AWS 各サービスの説明は行いません。 デモアプリの構成 # 本記事で考えるデモアプリは、簡単な ToDo アプリであるとします。 典型的な三層 Web アプリケーションに、非構造化データを保存するためのオブジェクトストレージを加えた構成です。 フロントエンドには Next / Nuxt の SSR(Server-Side Rendering) を採用し、 フロントエンドとバックエンド API を同一サーバー上でホストできる構成とします。 フロントエンド: Next / Nuxt などで構築された Web UI(SSR によりサーバー側でレンダリング) バックエンド API:タスクの CRUD を提供する REST API 永続化層:RDBMS(ユーザー・タスク・プロジェクトなどのデータ) ファイルストレージ:ユーザーがアップロードするアイコン画像や添付ファイルなどを保存 本記事では AWS と Google Cloud の比較のためごく簡単なアプリケーションを題材とします。そのため高度な機械学習(Google Cloud の強みはここもあったりしますが)、メッセージング、DNS、イベント駆動アーキテクチャなどは含めず、 ネットワーキング コンピューティング データベース オブジェクトストレージ といったクラウドの基本要素にフォーカスしています。 また、実運用で考えるべきマルチAZ(マルチゾーン)やマルチリージョンな冗長化構成についても考慮しません。 AWS における構成 # まずは AWS でのデモアプリの構成です。 アプリを構築するリージョンは東京リージョン(ap-northeast-1)とします。 リージョン内にVPCを1つ作成し、VPC 内には AZ を1つ含めます。 その AZ 内にパブリックサブネットとプライベートサブネットを配置します。 さらに、Next / Nuxt の SSR やバックエンド API をホストするためのコンピューティングサービスとして EC2 をプライベートサブネット内に配置し、Auto Scaling グループを構成します。 パブリックサブネットには Application Load Balancer(ALB) を配置し、 インターネットからのトラフィックを Auto Scaling グループへルーティングします。 永続化層には、EC2 と同じプライベートサブネット内に Amazon RDS(MySQL / PostgreSQL) を配置します。 ユーザーアップロード用のファイルは Amazon S3 バケット に保存し、 プライベートサブネット内の EC2 から S3 にアクセスします。 なお、実際には次のようなネットワーク構成が必要です EC2 の外向き(Outbound)通信には NAT Gateway が必要 EC2 から S3 を安全に利用するには VPC エンドポイント(Gateway Endpoint for S3)が必要 ですが今回は構成をシンプルに保つため図からは省略しています。 また、NAT Gateway は個人利用ではコストがかかるため、EC2 をパブリックサブネットに置く構成も現実的です。しかし、AWS と Google Cloud の比較を明確にするため、本記事では EC2 をプライベートに置く構成としています。 Google Cloud における構成 # 続いて、同じデモアプリケーションを Google Cloud 上で構成した場合を見ていきます。 見た目は AWS よりもスッキリしています。 VPC やロードバランサなど、AWS でもよく見かけるリソース名が Google Cloud にも登場しますが、その配置のされ方を見ると 「おや?」 と感じる部分があるかもしれません。 この違いこそが、AWS と Google Cloud の思想的な差分であり、 以降の章ではこれらのリソースの役割や設計上の特徴を詳しく比較していきます。 ネットワーク編 # まずはネットワークリソースから見ていきます。 ここで扱うのは以下のリソースです。 VPC サブネットとゾーン ロードバランサ VPC # まず、AWS と Google Cloud で大きく異なるのが VPC のスコープです。 AWS では VPC はリージョンに紐づくリージョナルリソースです。 AZ をまたいでの構成は可能ですが、リージョンをまたいで1つの VPC の構成はできません。 一方、 Google Cloud では VPC はグローバルリソース です。 そのため、1つの VPC の中に複数リージョンを含めることができ、AWS と比べてマルチリージョンなネットワークを容易に構成できます。 AWS と比較したときの Google Cloud の大きな特徴として、グローバルリソースを前提にした設計思想があります。 Google Cloud はまず グローバルな VPC を作成し、その中でリージョンやAWSのAZに当たるゾーンといったパーティションを切っていくというトップダウンの構成をとります。 一方 AWS は VPC をリージョンごとに作成し、必要があればリージョン間を接続してマルチリージョン化します。つまりリージョンを基本単位としたボトムアップ型のアプローチです。 この設計思想の違いこそが、AWS ユーザーが Google Cloud に触れたときに最初に戸惑うポイントといえます。 サブネットとゾーン # 次に、サブネットとゾーンの扱いについて触れます。 Google Cloud では、作成した VPC に対してサブネットを追加しますが、AWS と異なり サブネットはリージョン単位のリソース です。 AWS のようにサブネット=AZ 単位ではありません。 さらに、Google Cloud のサブネットにはパブリック/プライベートの区別がありません。 サブネットに割り当てるルート(デフォルトルートをインターネットゲートウェイに向けるかどうか)と、ファイアウォールルールによってパブリック/プライベートの性質が決まります。 また、「ゾーン」は、Google Cloud における AWS の AZ に相当する最小単位のデータセンター群です。 AWS 同様リージョンを分ける単位として機能しますが、サブネットがどこに紐づくかが異なります。 AWSの場合、サブネットは AZ 単位で作られますが、Google Cloudの場合、逆にサブネット内にゾーンが存在します。 以上Google Cloud の構成を整理すると、次のようになります。 サブネットはリージョン全体に広がるため、リージョン内のすべてのゾーンから利用可能 アプリケーションをデプロイするゾーンはサブネット内で選択する パブリック/プライベートの区別はルートとファイアウォールで制御する このため、AWS の AZ ごとにサブネットを作り冗長化するモデルに比べ、 Google Cloud はサブネット構成がシンプルで、ゾーン冗長を実現しやすいのが特徴です。 ロードバランサ # ロードバランサ(LB)の設計思想も AWS と Google Cloud で異なります。 ここではL7のLBのみ言及します。AWSにおけるL7 LB はALBですが、これはリージョンリソースで各リージョンごとにエンドポイントが異なります。 したがって、グローバルに公開したい場合は Route 53 や CloudFront を併用する必要があります。 一方、Google CloudにおけるL7 LBは 外部 HTTP(S) ロードバランサ です。これにもいろいろ種類があるのですが [1] 、グローバルに公開したい場合は、 グローバルリソース として提供される LB を選択可能です。 AWS でグローバル LB を構成しようとすると複数サービスを組み合わせる必要がありますが、Google Cloud では LB 単体でグローバル公開ができる点が大きな違いです。 なお、インターネット接続の扱いも AWS と Google Cloud では少し異なります。 AWS では VPC を外部に公開するために Internet Gateway(IGW)を明示的に作成し、パブリックサブネットのルートテーブルに紐づける必要があります。一方、Google Cloud では IGW に相当するリソースをユーザーが直接意識することなく、外部 HTTP(S) ロードバランサ などの外部向けリソースを作成すると、自動的に Google のエッジネットワークがインターネットとの入口として機能します。 そのため、Google Cloud では「どこをインターネットに公開するか」を細かいネットワーク構成として指定することが少なく、AWS に比べて抽象度が高いという特徴があります。 ネットワークまとめ # ここまでを踏まえると、デモアプリのネットワーク構成は以下のようになります。 グローバルリソースとして VPC を1つ作成し、その中に東京リージョン(asia-northeast1)を含める。 東京リージョン内にリージョンサブネットを作成する。 リージョンサブネット内の特定のゾーン内(今回の場合はゾーンA)にコンピューティングやDBを配置する L7 LBはグローバルリソースとして配置する Google Cloudのネットワーク構成図 リソース AWS のスコープ Google Cloud のスコープ VPC リージョン単位 グローバルリソース サブネット AZ単位 リージョン単位 ゾーン(AZ 相当) 1つのリージョンに複数の AZ。サブネットと 1 対 1 サブネットの内部に存在。サブネットはゾーンをまたぐ ロードバランサ(LB) ALB/NLB は リージョン単位 外部 HTTP(S) LB は グローバル リソースとして配置可能 インターネット接続 IGW を VPC にアタッチし、ルートテーブルで制御 Google の外部エッジネットワーク(LB 経由)を入口として利用 コンピューティング編 # 続けて、コンピューティングサービスについて見ていきます。 アプリケーションを実際に実行するサーバーをどのように冗長化し、スケールさせるかは AWS と Google Cloud の大きな比較ポイントの一つです。 AWS では、EC2 インスタンスでフロント/バックエンドサーバーをホストし、EC2インスタンスを Auto Scaling グループに配置しています。これにより、一定条件下でインスタンス数を自動的に増減させることが可能です。 Google Cloud において EC2 にあたるコンピューティングサービスとして Google Compute Engine(GCE) があります。 GCE でも EC2 と同様に仮想マシンを利用してアプリケーションをホストしますが、スケーリングや冗長化の仕組みが少し異なります。Google Cloud では、GCE インスタンスを Managed Instance Group(MIG) にまとめることで、スケールイン/アウトや自己修復などを自動化できます。 ここで AWS と比べて特徴的なのは、Google Cloud では MIG をリージョン単位で作成できる という点です。 AWS の Auto Scaling グループが複数 AZ にまたがることで高可用性を実現するのと似ていますが、MIG は「リージョン MIG」として構成することで、リージョン内の複数ゾーンにコンピューティングリソースが自動的に分散配置されます。 これは前章で触れた「サブネットがリージョン単位」である Google Cloud のネットワーク設計にも関わっています。 AZをまたがるように配置可能なAWSのAuto Scalingグループ(左)とリージョン単位で作成可能なGoogle Cloud リージョナルMIG(右) 今回のデモアプリでは構成をシンプルに保つため、GCE インスタンスは asia-northeast1 の中の 1 つのゾーン(例:asia-northeast1-a)に配置しています。しかし実運用を想定する場合は、リージョン MIG を使って複数ゾーンにまたがって配置するのが定石です。複数ゾーンにまたがることで単一ゾーン障害に対する耐性を持たせることができます。 AWS の Auto Scaling グループと Google Cloud の MIGは、どちらもインスタンス数のスケーリングに加え、起動定義のバージョン管理、ローリングアップデートといった機能を備えています。 データベース、ストレージ編 # お次はデモアプリで利用するデータベースとストレージについて見ていきます。 デモアプリでは、ユーザーやタスクを保存するための RDB と、ユーザーがアップロードする画像ファイルなどを保存するオブジェクトストレージを利用しています。 AWS では、RDB には Amazon RDS を、ストレージには Amazon S3 を利用しています。EC2 で動くアプリケーションは、プライベートサブネットの内部から RDS に接続し、ユーザーがアップロードしたファイルを S3 に保存します。 一方、Google Cloud で同じアプリケーションを構築する場合、対応するプロダクトは Cloud SQL と Cloud Storage になります。 Cloud SQL は MySQL や PostgreSQL をマネージドで提供するサービスで [2] 、Cloud Storage は S3 と同様にオブジェクトストレージとして利用できます。 ここまでは AWS とさほど大きな違いはありませんが、ネットワーク構成に踏み込むと、Google Cloud らしい特徴が見えてきます。 まずは DB サービスの比較です。 RDS が VPC 内のサブネットにインスタンスを持つのに対し、Cloud SQL のインスタンスはユーザー管理の VPC 内には存在しません。Cloud SQL のインスタンスは Google の管理ネットワーク内にある VPC に存在します。 Cloud SQL への接続は Public IP を付与しパブリックネットワーク経由でアクセスする方法と、プライベートネットワーク経由でGoogle Cloud内部から接続する方法があります。 本記事でのデモアプリでは、プライベートなVPC内に配置した GCE からアクセスする構成を想定するため、プライベートネットワークアクセスで Cloud SQL に接続する方法をとることとします。 プライベートなアクセス方法は本アプリでは Private Service Connect(PSC)という接続方法をとることとし [3] 、VPC 内の PSC エンドポイント経由で Cloud SQL インスタンスへ接続をおこないます。 Cloud SQL では RDS のように冗長化構成をとる場合でも、レプリカをどのサブネットに置くかといった細かいネットワーク設計を利用者が意識する必要はなく、リージョンの指定のみで冗長化・レプリカ配置は Google がよしなにやってくれます。 このようにCloud SQL は内部構造がユーザーに隠蔽されている点や、冗長化方式の抽象化という観点では、AWS の Amazon Aurora に近い部分もあります。ただし、個人開発や小規模な Web アプリケーションではコスト面や構成のシンプルさから RDS が選択されるケースも多く、Amazon Aurora は多機能であるため、本記事では純粋な RDB サービスの比較対象として RDS を扱っています。 Amazon RDS はサブネットに配置され(左)、Cloud SQL インスタンスの実体はユーザー管理のサブネットにはなく Google によって管理される(右) オブジェクトストレージについても似た特徴があります。 AWS の場合、EC2 がプライベートサブネットにある場合は NAT Gateway または S3 用の VPC エンドポイント(Gateway Endpoint)を明示的に用意して、外向きトラフィックをどう流すかを設計する必要があります。 Google Cloud の Cloud Storage では、GCE などのインスタンスからアクセスする際にはサブネット単位で「限定公開の Google アクセス(Private Google Access)」を有効にすることで、外部 IP を持たないプライベートサブネットのインスタンスからでも Google Cloud の内部ネットワーク経由で Cloud Storage の API にアクセスできます。この場合、インターネットに公開されたパブリック IP を経由せずに通信が完結します。 こうした差異から見えてくるのは、AWS と Google Cloud のネットワーク設計に対する考え方の違いです。 AWS では、利用者がネットワークの経路やセキュリティを比較的細かく指定することが前提となっており、VPC・サブネット・NAT・VPC エンドポイントといった多くのリソースを組み合わせる必要があります。 対して Google Cloud は、Google 自身が世界規模で運用してきたネットワークを前提とし、ユーザーが意識しなくても安全で効率的な通信が成立するように抽象化されています。結果として、アプリケーション開発者は「どのネットワーク経路を通すか」よりも「どのサービスを使うか」に集中しやすくなります。 まとめ # 今回、同じデモアプリを題材に AWS と Google Cloud のアーキテクチャを比較してみました。 Google Cloud は、VPC やロードバランサがグローバルであるように、グローバルなネットワークや抽象化を前提としたクラウドです。 Cloud SQL のように内部構造をユーザーに見せず、シンプルな設定だけで安全に利用できるサービスが多いため、構成は AWS よりもスッキリまとまります。ただし、抽象化が強いぶん「どこからどこまでが自分の設計領域か」が直感的に理解しづらい場面もあります。 一方 AWS は、VPC・サブネット・AZ といった ネットワーク境界が明確で、ボトムアップで積み上げる構成を取りやすいのが特徴です。 インターネット接続やプライベートアクセスをユーザーが明示的に選択できるため、要件に応じて細かくコントロールできる点は大きな利点です。筆者としても、この「スコープが見えやすい構造」は理解しやすく、設計上の安心感があります。 どちらが優れているという話ではなく、Google Cloud はシンプルで抽象化されたモデル、AWS は構造が明確でコントロールしやすいモデルという違いがあります。 どちらのクラウドもそれぞれの強みがあり、アプリケーションの規模や要件に応じて最適解は変わります。本記事が、AWS を使っている方が Google Cloud を理解するきっかけになれば幸いです。 これがさらにグローバルかリージョナルリソースとして配置するか、パブリックインターネットからのトラフィックを負荷分散するか Google Cloud 内部のトラフィックを分散するかで種類が分かれます。L3/4 の LB についても同様に複数種類あってややこしいです。 何を言っているんだぜ?という感じですが、ロードバランサについてはこちらの記事で詳しく解説がなされています( AWS側の目線から理解する、Google Cloud ロードバランサの世界 )。どうしてこうなったのか。 ↩︎ RDBMS のマネージドサービスとして他に Cloud Spanner がありますが、個人開発や小規模開発ではオーバーキル気味なので今回はCloud SQLを選択します。また、Amazon Aurora のように Google Cloud にも独自データベースである AlloyDB があります。こちらは PostgreSQL のみ互換性があります。 ↩︎ Cloud SQLへのプライベートな接続方法には、Cloud SQL インスタンスに Private IP を割り当て、VPC ピアリング で接続をおこなう Private Service Access(PSA) と、ユーザー管理の VPC 内にエンドポイントを作成し、 エンドポイント経由で Cloud SQL インスタンスへ Google Cloud の内部ネットワークを通じて接続する Private Service Connect(PSC) が選択できます(ややこしい名前ですね…)。 接続方法はユースケースに合わせて PSA・PSC どちらも選択可能ですが、本記事では「VPC 内からマネージドサービスへプライベートに接続する」という構造をシンプルに示すことができる PSC によるプライベート接続を例に挙げました。 ↩︎
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第11日目の記事です! はじめに # この記事は、 Amazon Q Developerと人間の協働による実験的な取り組み です。 最初からネタばらしですが、 「この記事、ほとんどAIが書いています。」 AIを“使う側”から“協働する側”へ 。Amazon Q Developerで開発と執筆の常識が変わると思います。 何をするか: VSCodeでAmazon Q Developerをセットアップ 実際のコード作成・改善を体験 AIとの対話で技術記事を共同執筆 特徴的な点: 記事の大部分をAmazon Q Developerが執筆 コードサンプルやテストもAIが生成 人間は企画・構成・画像挿入を担当 読者が得られるもの: Amazon Q Developerの実用的な使い方 AI協働による効率的な開発手法 技術文書作成でのAI活用の可能性 それでは、Amazon Q Developerと人間の協働の旅を始めましょう。 1. 開発環境 # 今回の協働作業では、Amazon Q Developer を VSCode から使用します。 VSCode で Amazon Q Developer を使うには以下の環境が必要です。 VSCode 本体: v1.85.0 以上 サインイン用のアカウント 個人利用: AWS Builder ID (AWSアカウント不要) 会社利用: IAM Identity Center (AWSアカウント必要) ※ 会社のAWS環境で使うなら、権限やライセンスは管理者に確認しておくと安心です。 2. VSCode への拡張機能インストール # VSCode を起動します 左パネルの Extensions(拡張機能) を開きます 検索欄に Amazon Q と入力し、以下の拡張機能を選択してインストールします 以下のようなアイコンがVSCodeに表示されていれば、インストールは完了です ※ただし、まだ Amazon Q Developer へのログインが行われていないため、アイコンが赤くなっています 3. サインイン(認証) # VSCode 下部の Amazon Q アイコン をクリックし、「Sign in to get started」を選択します 「サインインオプション」から用途に合わせて選択します 個人(Personal account)の場合 → 事前に「Builder ID」を取得しておきます 会社(Company account)の場合 → 事前に「IAM Identity Center」で登録しておきます (今回は「会社の場合」で進めます) IAM Identity Centerのアカウント情報を設定します StartURL:AWS access portal URLを設定します Region :サービスのリージョンを設定します 続けて外部のAWSサイトに誘導されるので、ブラウザでログインします 以下の画面が表示されたら、アクセスを許可します VSCodeに戻ります。 下の画面のように「Amazon Q」と表示されていればサインイン完了です 4. 基本的な使い方 # チャットで質問 # Amazon Qパネルで質問や指示を入力します。 試しに「ここで何ができますか?」と質問してみます。 Amazon Q から「できることリスト」の回答が得られました。 コード補完 # ソースコードを作成していく場合、プログラムを書かずにコメントに意図を書いておくと、Amazon Qがインラインでコードを提案してくれます。 例1: 日本語コメントからクラス生成 # ユーザー情報を管理するクラスを作成 class User: def __init__(self, name, email): self.name = name self.email = email def get_display_name(self): return f"{self.name} ({self.email})" 例2: 関数の処理内容をコメントで指定 # CSVファイルからユーザーデータを読み込む関数 import csv def load_users_from_csv(filename): users = [] with open(filename, 'r', encoding='utf-8') as file: reader = csv.DictReader(file) for row in reader: user = User(row['name'], row['email']) users.append(user) return users 使い方のコツ: コメントは具体的に書く(「データを処理」より「CSVファイルから読み込み」) 日本語でも英語でも対応 Alt+C(Option+C)で手動補完も可能 5. コード作成・修正を試行 # では実際に、Amazon Q Developerでのコード作成・修正を体験してみましょう。 サンプルファイル(example.py)について # まず、今回使用するサンプルファイル example.py について説明します。このファイルは、ユーザー情報を管理するシンプルなPythonプログラムです。 example.py の機能は以下です。 ユーザー情報(名前、メールアドレス)を管理するUserクラス CSVファイルからユーザーデータを読み込む機能 メールアドレスでユーザーを検索する機能 ユーザーリストをJSON形式で保存する機能 ソースコードのひな型としてコメント部分だけを記述します。 # ユーザー情報を管理するクラスを作成 # CSVファイルからユーザーデータを読み込む関数 # リストの中から特定の条件に合うユーザーを検索する関数 # ユーザーリストをJSON形式で保存する関数 すると Amazon Q が以下のようなソースコードを提案してきます。 example.py のソースコード: # ユーザー情報を管理するクラスを作成 class User: def __init__(self, name, email): self.name = name self.email = email def get_display_name(self): return f"{self.name} ({self.email})" # CSVファイルからユーザーデータを読み込む関数 import csv def load_users_from_csv(filename): users = [] with open(filename, 'r', encoding='utf-8') as file: reader = csv.DictReader(file) for row in reader: user = User(row['name'], row['email']) users.append(user) return users # リストの中から特定の条件に合うユーザーを検索する関数 def find_user_by_email(users, email): for user in users: if user.email == email: return user return None # ユーザーリストをJSON形式で保存する関数 import json def save_users_to_json(users, filename): user_data = [] for user in users: user_data.append({ 'name': user.name, 'email': user.email }) with open(filename, 'w', encoding='utf-8') as file: json.dump(user_data, file, ensure_ascii=False, indent=2) print(f"Saved {len(users)} users to {filename}") このコードは構文的には正しく動作しますが エラーハンドリングやテストコードがない 実際に実行するメイン処理がない など、改善の余地があります。 これらの問題をAmazon Q Developerを使って解決していきます。 5.1 既存コードの理解 # まず、example.pyファイルを開いて、Amazon Qにコードの内容を質問し、Amazon Q がコードを正しく理解しているかを確認します。 Amazon Qへ以下のように指示を出します。 @example.py このファイルは何をするコードですか? Amazon Qが以下のような回答をしてくれます。 example.pyは、ユーザー情報を管理するためのシンプルなPythonプログラムです。 主な機能 1. Userクラス - ユーザー情報(名前とメールアドレス)を管理 - get_display_name(): 名前とメールアドレスを組み合わせた表示名を返す 2. CSVファイル読み込み - load_users_from_csv() - CSVファイルからユーザーデータを読み込んでUserオブジェクトのリストを作成 3. ユーザー検索 - find_user_by_email() - メールアドレスを指定して特定のユーザーを検索 4.JSON形式での保存 - save_users_to_json() - ユーザーリストをJSON形式でファイルに保存 使用例 このプログラムは、CSVファイルからユーザーデータを読み込み、必要に応じてユーザーを検索し、最終的にJSON形式で保存するといったユーザーデータの変換・管理処理に使用できます。 ただし、現在のコードにはエラーハンドリングやメイン処理がないため、実際に動作させるには追加の実装が必要です。 5.2 コードの改善提案 # 既存のコードに対して改善提案を求めてみましょう。 Amazon Qへ以下のように指示を出します。 @example.py このコードを改善する方法はありますか? Amazon Qから以下のような提案が得られます。 はい、example.pyには多くの改善点があります。以下のような改善方法をお勧めします: 1. エラーハンドリングの追加 現在の問題: ファイルが存在しない場合やCSVの形式が不正な場合にエラーで停止 改善案: try-except文でエラーを適切に処理 2. 型ヒントの追加 現在の問題: 引数や戻り値の型が不明確 改善案: from typing import List, Optional を使用して型を明示 3. docstringの追加 現在の問題: 関数の説明がコメントのみ 改善案: 各関数に適切なdocstringを追加 4. importの整理 現在の問題: import文が関数の直前に散らばっている 改善案: ファイル冒頭にまとめて配置 5. データ検証の追加 現在の問題: CSVデータの妥当性チェックなし 改善案: 必須フィールドの存在確認やメールアドレス形式の検証 6. 設定の外部化 現在の問題: エンコーディングなどがハードコード 改善案: 定数として定義 7. ログ機能の追加 現在の問題: print文のみでログ管理が不十分 改善案: loggingモジュールの使用 8. テスト可能な設計 現在の問題: メイン処理がなく、テストしにくい 改善案: if __name__ == "__main__": ブロックの追加 9. パフォーマンスの改善 現在の問題: 大量データ処理時の効率性 改善案: リスト内包表記の使用、メモリ効率の向上 これらの改善により、より堅牢で保守性の高いコードになります。特にエラーハンドリングと型ヒントの追加は優先度が高い改善点です。 5.3 実際にコードを修正 # エラーハンドリングの追加 CSVファイル読み込み関数にエラーハンドリングを追加してみます。 修正前のコード(example.py の一部)は以下です。 def load_users_from_csv(filename): users = [] with open(filename, 'r', encoding='utf-8') as file: reader = csv.DictReader(file) for row in reader: user = User(row['name'], row['email']) users.append(user) return users Amazon Qへ以下のように指示を出します。 load_users_from_csv関数にファイルが存在しない場合のエラーハンドリングを追加してください コードが以下のように修正されました。 def load_users_from_csv(filename): users = [] try: with open(filename, 'r', encoding='utf-8') as file: reader = csv.DictReader(file) for row in reader: user = User(row['name'], row['email']) users.append(user) except FileNotFoundError: print(f"エラー: ファイル '{filename}' が見つかりません") return [] except Exception as e: print(f"エラー: ファイル読み込み中に問題が発生しました: {e}") return [] return users Amazon Q が直接ソースコードを修正 し、私(人間)はソースコードに一切触れていません。 新機能の追加 ユーザーの年齢情報を管理する機能を追加してみます。 Amazon Qへ以下のように指示を出します。 Userクラスに年齢(age)フィールドを追加し、年齢で検索する関数も作成してください 以下のようにコードが追加されました。 # ユーザー情報を管理するクラスを作成 class User: def __init__(self, name, email, age=None): self.name = name self.email = email self.age = age def get_display_name(self): age_info = f" (年齢: {self.age})" if self.age else "" return f"{self.name} ({self.email}){age_info}" # 年齢範囲でユーザーを検索する関数 def find_users_by_age_range(users, min_age, max_age): """指定された年齢範囲のユーザーを検索する""" result = [] for user in users: if user.age and min_age <= user.age <= max_age: result.append(user) return result 5.4 テストコードの生成 # テストコードを追加してみます。 Amazon Qへ以下のように指示を出します。 @example.py の関数をテストするためのテストコードを作成してください 生成されたテストコード(test_example.py)は以下です。 import unittest import tempfile import os import json import csv from example import User, load_users_from_csv, find_user_by_email, find_users_by_age_range, save_users_to_json class TestUserManagement(unittest.TestCase): def test_user_creation_without_age(self): """年齢なしでのユーザー作成テスト""" user = User("田中太郎", "tanaka@example.com") self.assertEqual(user.name, "田中太郎") self.assertEqual(user.email, "tanaka@example.com") self.assertIsNone(user.age) def test_user_creation_with_age(self): """年齢ありでのユーザー作成テスト""" user = User("田中太郎", "tanaka@example.com", 30) self.assertEqual(user.name, "田中太郎") self.assertEqual(user.email, "tanaka@example.com") self.assertEqual(user.age, 30) def test_get_display_name_without_age(self): """年齢なしでの表示名テスト""" user = User("田中太郎", "tanaka@example.com") expected = "田中太郎 (tanaka@example.com)" self.assertEqual(user.get_display_name(), expected) def test_get_display_name_with_age(self): """年齢ありでの表示名テスト""" user = User("田中太郎", "tanaka@example.com", 30) expected = "田中太郎 (tanaka@example.com) (年齢: 30)" self.assertEqual(user.get_display_name(), expected) def test_find_user_by_email(self): """メールアドレスでのユーザー検索テスト""" users = [ User("田中太郎", "tanaka@example.com"), User("佐藤花子", "sato@example.com") ] # 存在するユーザーの検索 found = find_user_by_email(users, "tanaka@example.com") self.assertIsNotNone(found) self.assertEqual(found.name, "田中太郎") # 存在しないユーザーの検索 not_found = find_user_by_email(users, "yamada@example.com") self.assertIsNone(not_found) def test_find_users_by_age_range(self): """年齢範囲でのユーザー検索テスト""" users = [ User("田中太郎", "tanaka@example.com", 30), User("佐藤花子", "sato@example.com", 25), User("鈴木次郎", "suzuki@example.com", 35), User("山田三郎", "yamada@example.com") # 年齢なし ] # 25-32歳の範囲で検索 result = find_users_by_age_range(users, 25, 32) self.assertEqual(len(result), 2) self.assertEqual(result[0].name, "田中太郎") self.assertEqual(result[1].name, "佐藤花子") # 該当者なしの範囲で検索 result_empty = find_users_by_age_range(users, 40, 50) self.assertEqual(len(result_empty), 0) def test_save_users_to_json(self): """JSON保存機能のテスト""" users = [ User("田中太郎", "tanaka@example.com", 30), User("佐藤花子", "sato@example.com") # 年齢なし ] # 一時ファイルを作成してテスト with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as temp_file: temp_filename = temp_file.name try: save_users_to_json(users, temp_filename) # ファイルが作成されたことを確認 self.assertTrue(os.path.exists(temp_filename)) # ファイル内容を確認 with open(temp_filename, 'r', encoding='utf-8') as file: data = json.load(file) self.assertEqual(len(data), 2) # 年齢ありのユーザー self.assertEqual(data[0]['name'], "田中太郎") self.assertEqual(data[0]['email'], "tanaka@example.com") self.assertEqual(data[0]['age'], 30) # 年齢なしのユーザー self.assertEqual(data[1]['name'], "佐藤花子") self.assertEqual(data[1]['email'], "sato@example.com") self.assertNotIn('age', data[1]) # 年齢が設定されていない場合は含まれない finally: # 一時ファイルを削除 if os.path.exists(temp_filename): os.unlink(temp_filename) def test_load_users_from_csv_file_not_found(self): """存在しないCSVファイルの読み込みテスト""" result = load_users_from_csv("nonexistent_file.csv") self.assertEqual(result, []) def test_load_users_from_csv_valid_file(self): """有効なCSVファイルの読み込みテスト""" # 一時CSVファイルを作成 csv_data = [ ['name', 'email', 'age'], ['田中太郎', 'tanaka@example.com', '30'], ['佐藤花子', 'sato@example.com', ''] # 年齢なし ] with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv', encoding='utf-8', newline='') as temp_file: temp_filename = temp_file.name writer = csv.writer(temp_file) writer.writerows(csv_data) try: users = load_users_from_csv(temp_filename) self.assertEqual(len(users), 2) # 年齢ありのユーザー self.assertEqual(users[0].name, "田中太郎") self.assertEqual(users[0].email, "tanaka@example.com") self.assertEqual(users[0].age, 30) # 年齢なしのユーザー self.assertEqual(users[1].name, "佐藤花子") self.assertEqual(users[1].email, "sato@example.com") self.assertIsNone(users[1].age) finally: # 一時ファイルを削除 if os.path.exists(temp_filename): os.unlink(temp_filename) if __name__ == '__main__': unittest.main() 5.5 実行可能なメイン処理の追加 # example.py にmain処理が無いので追加します。 Amazon Qへ以下のように指示を出します。 @example.py に実際に動作確認できるメイン処理を追加してください 追加されたメイン処理は以下です。 if __name__ == "__main__": # サンプルデータでテスト print("=== ユーザー管理システムのテスト ===") # ユーザー作成 users = [ User("田中太郎", "tanaka@example.com", 30), User("佐藤花子", "sato@example.com", 25), User("鈴木次郎", "suzuki@example.com", 35), User("山田三郎", "yamada@example.com") # 年齢なし ] # 全ユーザー表示 print("\n登録ユーザー:") for user in users: print(f" {user.get_display_name()}") # メールアドレスで検索 print("\nメール検索テスト:") found_user = find_user_by_email(users, "sato@example.com") if found_user: print(f" 見つかりました: {found_user.get_display_name()}") # 年齢範囲で検索 print("\n年齢検索テスト (25-32歳):") age_filtered_users = find_users_by_age_range(users, 25, 32) for user in age_filtered_users: print(f" {user.get_display_name()}") # JSON保存 print("\nJSON保存テスト:") save_users_to_json(users, "sample_users.json") # 存在しないCSVファイルの読み込みテスト print("\nCSVファイル読み込みテスト(エラーハンドリング確認):") csv_users = load_users_from_csv("nonexistent.csv") print(f"読み込み結果: {len(csv_users)}件のユーザー") print("\nテスト完了!") 5.6 Amazon Q Developerの活用ポイント # Amazon Q の 効率的な使い方 のポイントは以下です。 @ファイル名 でファイル全体を参照 具体的な指示を出す(「エラーハンドリングを追加」など) 段階的に機能を追加していく テストコードも一緒に生成してもらう このように、Amazon Q Developerを使うことで、既存コードの理解から改善、新機能追加、テスト作成まで効率的に行えます。 6. まとめ # Amazon Q Developer for VSCode を使うことで以下が期待できます。 作業効率向上 : 定番の開発環境であるVSCodeを使うことで作業効率向上 コード理解 : 既存コードの動作や構造を素早く把握 コード改善 : エラーハンドリングや型ヒントなどの品質向上 機能追加 : 新しい機能を段階的に実装 テスト作成 : 自動的にテストコードを生成 実行確認 : メイン処理を追加して動作検証 日本語での指示にも対応しているため、自然な言葉でコード作成・修正ができます。 7. この記事の舞台裏 ― Amazon Q Developerとの協働執筆 # この記事は Amazon Q Developer との対話を通じて作成されました。 記事執筆の過程をご紹介します。 7.1 記事作成プロセス # 人間(筆者)の役割: 記事の構成や方向性の決定 スクリーンショット画像の撮影・挿入 最終的な内容の確認・調整 Amazon Q Developerの役割: 各章の詳細な文章作成 コードサンプルの生成 テストコードの作成 技術的な説明文の執筆 以下は、VSCode上で記事とコードを同時に執筆している作業風景です。 このように記事を執筆しながら、コード作成・修正を同時に行っています。 人間が内容の確認を行い、Amazon Q とレビューを繰り返し、Amazon Q が記事・コードの修正を行ってくれます。 7.2 協働執筆の効果 # 今回、Amazon Q Developer を VSCode上で実行し、以下のような効果を実感できました。 効率性の向上: 定番開発環境による作業効率向上 記事の骨格作成時間を大幅短縮 技術的な詳細説明を素早く生成 コードサンプルとテストを同時作成 品質の向上: 一貫した文体と構成 実際に動作するコードサンプル 包括的なテストカバレッジ 創造性の発揮: 人間は全体設計と創造的な部分に集中 AIは詳細な実装と文章作成を担当 両者の強みを活かした分業 この記事の作成プロセス自体が、Amazon Q Developerの実用性を証明しています。 Amazon Q Developer は単なるコーディング支援ツールではなく、技術者の創造的な作業全般をサポートする強力なパートナーです。 ぜひ様々な場面で活用してみてください。 この記事は Amazon Q Developer との協働により作成されました。 img { border: 1px gray solid; }
本記事は 豆蔵デベロッパーサイトアドベントカレンダー2025 第10日目の記事です。 0. はじめに # ロボット制御や画像処理の分野では、最適化問題を解く必要に迫られる場面が多々あります。 最適化問題といっても、線形計画法や組合せ最適化など、その種類や解法は多岐にわたります。 その中でも、実用上特によく扱われるのが「最小二乗問題」です。 これは、下記のような目的関数 F ( x ) F(\boldsymbol{x}) F ( x ) を最小化するパラメータ x \boldsymbol{x} x を求める問題です。 (なお、一般的に x \boldsymbol{x} x はベクトルとなります。) min ⁡ x F ( x ) = 1 2 ∑ i ∥ r i ( x ) ∥ 2 \min_{\boldsymbol{x}} F(\boldsymbol{x}) = \frac{1}{2} \sum_{i} \| r_i(\boldsymbol{x}) \|^2 x min ​ F ( x ) = 2 1 ​ i ∑ ​ ∥ r i ​ ( x ) ∥ 2 ここで、上式の r i ( x ) r_i(x) r i ​ ( x ) は残差(Residual)といい、観測されたデータ(=実測値) y i y_i y i ​ と予測値(=理論値) f i ( x ) f_i(x) f i ​ ( x ) との差を意味します。 r i ( x ) = y i − f i ( x ) r_i(\boldsymbol{x}) = y_i - f_i(\boldsymbol{x}) r i ​ ( x ) = y i ​ − f i ​ ( x ) 今回は、このような非線形最小二乗問題を効率的に解くためのGoogle社製ソルバ " Ceres Solver "について紹介します。 1. 本記事の環境について # 本記事ではUbnutu 24.04を対象とします。 筆者の環境ではWSL2を利用していますが、純正のUbuntuでも問題ありません。 --> Information 本記事ではUbuntu環境でライブラリを使用していますが、Windowsでも動作します。 詳しくは下記リンク先をご覧ください。 http://ceres-solver.org/installation.html#windows 2. 環境構築 # CeresSolverを使用するための前準備です。少し長いですがお付き合いください。 必要なライブラリのインストール # まずは、Ceres Solverのビルドに必要なツールをインストールします。 下記を1行ずつ実施して、インストールしてください。 # apt update(パスワード入力を求められます) sudo apt update && sudo apt upgrade -y # ビルドツール sudo apt install build-essential # Git sudo apt install git # CMake sudo apt install cmake # google-glog + gflags sudo apt install libgoogle-glog-dev libgflags-dev # Use ATLAS for BLAS & LAPACK sudo apt install libatlas-base-dev # Eigen3 sudo apt install libeigen3-dev # SuiteSparse (optional) sudo apt install libsuitesparse-dev ワークスペース作成 # 次に、任意の場所にワークスペースを作成しましょう。 今回はホームディレクトリに ceres_solver_ws というディレクトリを作成します。 mkdir ~/ceres_solver_ws --> Information 一般的なLinux環境での"~"(チルダ)は、ホームディレクトリを意味します。 ホームディレクトリの絶対パスは /home/${ユーザ名}/ です。 CeresSolverライブラリのクローン # ワークスペースが作成出来たら、CeresSolverのソースをクローンしましょう。 クローンする場所は任意ですが、今回は external ディレクトリ内にクローンしておきます。 この時、Submoduleについても再帰的に取得する必要があるため、 --recursive オプションを付与する必要がある点に注意してください。 cd ~/ceres_solver_ws mkdir external cd external git clone --recursive https://github.com/ceres-solver/ceres-solver --> Information サブモジュールも再帰的に取得するコマンドとして、--recurse-submodulesオプションも存在するようです。 git clone --recurse-submodules ${repo-url} CeresSolverライブラリのビルド&インストール # クローンが出来たら、CMakeを使用してビルドします。 # CeresSolverのソースに移動 cd ceres-solver # ビルド用のディレクトリを作成 mkdir build # ビルドシステムの生成(out-of-sourceビルド) cmake -S . -B build # ビルド実行 cmake --build build --> ビルドシステム生成時にエラーが発生する場合 コマンド実施に下記のエラーが発生した場合は、build-essentialパッケージをaptでインストールしてください。 CMake Error at CMakeLists.txt:33 (project): No CMAKE_CXX_COMPILER could be found. Tell CMake where to find the compiler by setting either the environment variable "CXX" or the CMake cache entry CMAKE_CXX_COMPILER to the full path to the compiler, or to the compiler name if it is in the PATH. -- Configuring incomplete, errors occurred! --> 並列ビルドによる高速化 ビルドには少し時間がかかります。下記コマンドで並列ジョブを使用して高速化可能です。 nproc コマンドは「システムが利用可能なCPUコア数を取得する」コマンドです。 この出力結果が $(nproc) と置換されます。 cmake --build build -- -j$(nproc) 下記のように達成率が100%となるようなログが出力されれば、ビルド成功です。 [ 99%] Built target robot_pose_mle [ 99%] Building CXX object examples/sampled_function/CMakeFiles/sampled_function.dir/sampled_function.cc.o [ 99%] Linking CXX executable ../../bin/sampled_function [ 99%] Built target sampled_function [ 99%] Building CXX object examples/slam/pose_graph_2d/CMakeFiles/pose_graph_2d.dir/pose_graph_2d.cc.o [ 99%] Linking CXX executable ../../../bin/pose_graph_2d [ 99%] Built target pose_graph_2d [ 99%] Building CXX object examples/slam/pose_graph_3d/CMakeFiles/pose_graph_3d.dir/pose_graph_3d.cc.o [100%] Linking CXX executable ../../../bin/pose_graph_3d [100%] Built target pose_graph_3d 最後に、ビルドした生成物をインストールします。 インストール先のデフォルトは /usr/local です。 # ビルドした生成物をインストールする sudo cmake --install build インストールが完了したら、念のために下記のコマンドを実施しておきましょう。 source ~/.bashrc 3. 簡単な最適化計算を解いてみる # CMakeLists.txtとソースファイルの作成 # では、CeresSolverを実際に使用して最適化問題を解いてみましょう。 まずは,ワークスペース ~/ceres-solver_ws に戻り、 CMakeLists.txt ファイルを作成します。 cd ~/ceres_solver_ws touch CMakeLists.txt CMakeLists.txt には下記を記述します。 エディタは自由ですが、今回はVisual Studio Codeを使用しています。 (WSLとの相性も良いのでVSCodeはおすすめ!) # CMakeの最低バージョン指定 cmake_minimum_required(VERSION 3.14) # プロジェクト名定義 project(ceres-solver-sample) # 実行ファイルをビルドディレクトリ直下に出力するように設定 # (不要な場合は削除してください) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # 依存パッケージ指定 find_package(Ceres REQUIRED) # サブディレクトリ追加 add_subdirectory(src) 次に, src ディレクトリを作成します。 src ディレクトリ内にも CMakeLists.txt を作成します。 cd ~/ceres-solver_ws mkdir src cd src touch CMakeLists.txt src/CMakeLists.txt には下記のように記述します。 # simple-ols add_executable(simple-ols simple-ols.cpp) target_link_libraries(simple-ols absl::log_initialize Ceres::ceres) 最後に,計算を記述するC++ファイル( simple-ols.cpp )を作成します。 先述した CMakeLists.txt に記載したファイル名と合致するようにしてください。 touch simple-ols.cpp 最終的なファイル構造は下記のようになります。 ceres_solver_ws/ ├── CMakeLists.txt └── src/ ├── CMakeLists.txt └── simple-ols.cpp 最適化計算の実装 # 作成した simple-ols.cpp に処理を記述しましょう。 今回のお題としては,下式で定義された関数 f ( x ) f(x) f ( x ) の値を最小化する x x x を求めてみます。 f ( x ) = 1 2 ( 5 − x ) 2 f(x) = \frac{1}{2} (5 - x)^2 f ( x ) = 2 1 ​ ( 5 − x ) 2 x = 5 x=5 x = 5 で最小値を取ることは火を見るよりも明らかですが,これをプログラムで求めてみましょう。 実装コードは下記です。 simple-ols.cpp #include <ceres/ceres.h> #include <glog/logging.h> using ceres::AutoDiffCostFunction; using ceres::CostFunction; using ceres::Problem; using ceres::Solver; /// @brief 残差の構造体 /// @remark 最適化の対象を()演算子にて記述します struct CostFunctor { template <typename T> bool operator()(const T* const x, T* residual) const { // 今回の最適化対象式 residual[0] = T(5.0) - x[0]; return true; } }; /// @brief メイン関数 int main(int argc, char** argv) { // 初期値の定義 double initial_x = 1.0; double x = initial_x; // コスト関数の定義 CostFunction* cost_function = new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(); // 最適化問題の定義 Problem problem; problem.AddResidualBlock(cost_function, nullptr, &x); // 残差ブロックを追加 problem.SetParameterLowerBound(&x, 0, 0.0); // 入力パラメータの下限値設定 problem.SetParameterUpperBound(&x, 0, 10.0); // 入力パラメータの上限値設定 // 計算オプションの定義 ceres::Solver::Options options; options.linear_solver_type = ceres::DENSE_QR; // 密行列でのQR分解を使用する options.minimizer_progress_to_stdout = true; // 進捗出力を有効化 options.max_num_iterations = 10; // 最大反復回数 // 計算結果の定義 Solver::Summary summary; // 最適化計算を行う ceres::Solve(options, &problem, &summary); // 計算結果の出力 // 計算結果は変数xに格納されます std::cout << summary.BriefReport() << std::endl; std::cout << "x: " << initial_x << " -> " << x << std::endl; return 0; } プログラムの詳細 # コードの中で重要な部分について説明します。 コスト定義 # /// @brief コスト構造体 /// @remark 最適化の対象を()演算子にて記述します struct CostFunctor { template <typename T> bool operator()(const T* const x, T* residual) const { // 今回の最適化対象式 residual[0] = T(5.0) - x[0]; return true; } }; 今回の最適化の対象となる式を記述します。 () 演算子の中に計算式を記述する必要があります。 テンプレートTには、最適化計算時に使用される型が使用されます。 具体的には、 ceres::Jet 型というデータ型が使用されます。 double型などを使用する場合は、T型でキャストする必要がある点に注意してください コスト関数の定義 # // コスト関数の定義 CostFunction* cost_function = new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(); 上記にはコスト関数の定義を行っています。今回は計算で自動微分(Automatic Differentiation)を使用するように設定しています。 また、テンプレート引数のそれぞれの意味は下記の通りです。 第1引数: コスト関数の構造体型 第2引数: 誤差パラメータの次元数 第3引数: 最適化パラメータの次元数 今回のお題では、誤差パラメータの次元(= f ( x ) f(x) f ( x ) の次元)はスカラーのため 1 、最適化パラメータ x x x の次元は同じくスカラー量のため 1 となります。 最適化問題の定義 # // 最適化問題の定義 Problem problem; problem.AddResidualBlock(cost_function, nullptr, &x); // 残差ブロックを追加 problem.SetParameterLowerBound(&x, 0, 0.0); // 入力パラメータの下限値設定 problem.SetParameterUpperBound(&x, 0, 10.0); // 入力パラメータの上限値設定 最適化問題を定義します。 また、入力パラメータの上限値、下限値もここで設定します。 AddResifualBlockメソッドの第2引数は損失関数(Loss Function)を定義できます。 本記事では詳細を割愛しますが、詳細は下記リンク先をご覧ください。 http://ceres-solver.org/nnls_tutorial.html#robust-curve-fitting ビルドと実行 # CMakeを使用して作成したプログラムをビルドします。 cd ~/ceres_solver_ws cmake -S . -B bin cmake --build bin ビルドが成功したら、下記のコマンドで実施してみましょう。 ./bin/simple-ols これを実施すると、下記のようなログが出力されます。 iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time 0 8.000000e+00 0.00e+00 4.00e+00 0.00e+00 0.00e+00 1.00e+04 0 1.35e-05 4.40e-05 1 7.998400e-08 8.00e+00 4.00e-04 0.00e+00 1.00e+00 3.00e+04 1 8.09e-05 1.88e-04 2 8.886518e-17 8.00e-08 1.33e-08 4.00e-04 1.00e+00 9.00e+04 1 3.24e-05 2.39e-04 Ceres Solver Report: Iterations: 3, Initial cost: 8.000000e+00, Final cost: 8.886518e-17, Termination: CONVERGENCE x: 1 -> 5 最終行のログより、最適入力値は 5 であると計算できました。 また、最適入力値の場合の残差コスト( Final cost )は 8.886518e-17 であり、ほぼ0であることも計算できました。 入力パラメータの上下限値を変えてみる # 次に、入力値の範囲を 1~3 に変更して計算してみましょう。 下記の★印部分の値を 10 から 3 に変更します。 // 最適化問題の定義 Problem problem; problem.AddResidualBlock(cost_function, nullptr, &x); // 残差ブロックを追加 problem.SetParameterLowerBound(&x, 0, 0.0); // 入力パラメータの下限値設定 problem.SetParameterUpperBound(&x, 0, 3.0); // 入力パラメータの上限値設定(★) これを再度ビルドして実行してみると、最適入力は x = 3 x=3 x = 3 、残差コストは 2.000000e+00 となります。 指定した制約内でコストが最小となるような最適入力が求められることが確認できます。 参考のため、計算ログを下記に記します。 iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time 0 8.000000e+00 0.00e+00 2.00e+00 0.00e+00 0.00e+00 1.00e+04 0 1.21e-05 4.23e-05 1 2.000000e+00 6.00e+00 0.00e+00 0.00e+00 7.50e-01 1.14e+04 1 6.80e-05 1.66e-04 Ceres Solver Report: Iterations: 2, Initial cost: 8.000000e+00, Final cost: 2.000000e+00, Termination: CONVERGENCE x: 1 -> 3 4. 4自由度平面マニピュレータの逆運動学を数値的に解いてみる # 3章では入力パラメータとコストがともにスカラー量である最適化問題を解くサンプルを紹介しました。 本章では、より難しい非線形最適化問題のお題として「4自由度平面マニピュレータの逆運動学」を解いてみましょう。 4自由度平面マニピュレータとは # 下図のような4つの関節を持つ平面マニピュレータを対象に、逆運動学計算を実装してみます。 パラメータについては下記の通りとします。 各リンク長は L i L_i L i ​ とする 各関節角度の回転角度を θ i \theta_i θ i ​ とする i = 1 , 2 , 3 , 4 i=1,2,3,4 i = 1 , 2 , 3 , 4 各関節の回転正方向は「反時計回り(ccw)」とする ロボットの先端(点P)のX座標を x p x_p x p ​ 、Y座標を y p y_p y p ​ とする ロボットの先端の姿勢角度(半直線CPとX軸のなす角)を ϕ \phi ϕ とする 順運動学計算 # 対象ロボットの各軸角度 θ \boldsymbol{\theta} θ と先端位置 p \boldsymbol{p} p の関係を考えてみましょう。 ここで、それぞれのベクトルの定義は下記とします。 θ = [ θ 1 θ 2 θ 3 θ 4 ] \boldsymbol{\theta} = \begin{bmatrix} \theta_1 \\ \theta_2 \\ \theta_3 \\ \theta_4 \end{bmatrix} θ = ​ θ 1 ​ θ 2 ​ θ 3 ​ θ 4 ​ ​ ​ p = [ x p y p ϕ ] \boldsymbol{p} = \begin{bmatrix} x_p \\ y_p \\ \phi \end{bmatrix} p = ​ x p ​ y p ​ ϕ ​ ​ 上図での原点Oから点Aまでのベクトル O A ⃗ \vec{OA} O A は下記のように記述できます。 O A ⃗ = [ l 1 cos ⁡ θ 1 l 1 sin ⁡ θ 1 ] \vec{OA} = \begin{bmatrix} l_1 \cos \theta_1 \\ l_1 \sin \theta_1 \end{bmatrix} O A = [ l 1 ​ cos θ 1 ​ l 1 ​ sin θ 1 ​ ​ ] これと同様に、点Aから点B、点Bから点C、点Cから点Pまでのベクトルはそれぞれ下記のようになります。 A B ⃗ = [ l 2 cos ⁡ ( θ 1 + θ 2 ) l 2 sin ⁡ ( θ 1 + θ 2 ) ] \vec{AB} = \begin{bmatrix} l_2 \cos(\theta_1 + \theta_2) \\ l_2 \sin(\theta_1 + \theta_2) \end{bmatrix} A B = [ l 2 ​ cos ( θ 1 ​ + θ 2 ​ ) l 2 ​ sin ( θ 1 ​ + θ 2 ​ ) ​ ] B C ⃗ = [ l 3 cos ⁡ ( θ 1 + θ 2 + θ 3 ) l 3 sin ⁡ ( θ 1 + θ 2 + θ 3 ) ] \vec{BC} = \begin{bmatrix} l_3 \cos(\theta_1 + \theta_2 + \theta_3) \\ l_3 \sin(\theta_1 + \theta_2 + \theta_3) \end{bmatrix} B C = [ l 3 ​ cos ( θ 1 ​ + θ 2 ​ + θ 3 ​ ) l 3 ​ sin ( θ 1 ​ + θ 2 ​ + θ 3 ​ ) ​ ] C P ⃗ = [ l 4 cos ⁡ ( θ 1 + θ 2 + θ 3 + θ 4 ) l 4 sin ⁡ ( θ 1 + θ 2 + θ 3 + θ 4 ) ] \vec{CP} = \begin{bmatrix} l_4 \cos(\theta_1 + \theta_2 + \theta_3 + \theta_4) \\ l_4 \sin(\theta_1 + \theta_2 + \theta_3 + \theta_4) \end{bmatrix} C P = [ l 4 ​ cos ( θ 1 ​ + θ 2 ​ + θ 3 ​ + θ 4 ​ ) l 4 ​ sin ( θ 1 ​ + θ 2 ​ + θ 3 ​ + θ 4 ​ ) ​ ] これらを合わせると、XY平面上での点Pの座標は下記のようになります。 (式が長くなるためベクトル表記のままとしています) [ x p y p ] = O A ⃗ + A B ⃗ + B C ⃗ + C P ⃗ \begin{bmatrix} x_p \\ y_p \end{bmatrix} = \vec{OA} + \vec{AB} + \vec{BC} + \vec{CP} [ x p ​ y p ​ ​ ] = O A + A B + B C + C P また、ロボット先端の姿勢角度 ϕ \phi ϕ は下記のようになります。 ϕ = θ 1 + θ 2 + θ 3 + θ 4 \boldsymbol{\phi} = \theta_1 + \theta_2 + \theta_3 + \theta_4 ϕ = θ 1 ​ + θ 2 ​ + θ 3 ​ + θ 4 ​ 上式2つを合わせ、関節角度ベクトル θ \boldsymbol{\theta} θ から先端位置ベクトル p \boldsymbol{p} p への写像を f f f と定義すると、下式のように表現できます。 p = f ( θ ) \boldsymbol{p} = f(\boldsymbol{\theta}) p = f ( θ ) これがロボットの順運動学計算時に使用する式となります。 逆運動学計算 # 一方、逆運動学(Inverse Kinematics)は文字通り順運動学の逆を意味します。 つまり、 f f f の逆関数(=先端位置 p \boldsymbol{p} p から関節角度 θ \boldsymbol{\theta} θ への写像)を求める操作となります。 θ = f − 1 ( p ) \boldsymbol{\theta} = f^{-1}(\boldsymbol{p}) θ = f − 1 ( p ) 一般的に順運動学よりも逆運動学計算の方が計算量が多くなります。 ロボットの機構次第では解析的に解くことも可能ですが、一般的に自由度が多くなるほど逆運動学計算の難易度は難しくなります。 今回はこの計算をCeresSolverを使って数値的に解いてみます。 コード実装 # ディレクトリとCMakeLists.txtの作成 # srcディレクトリ直下に本問題を解くためのファイルを格納するディレクトリを作成します。 ディレクトリ名は 4dof-ik とします。 cd ~/ceres_solver_ws/src mkdir 4dof-ik cd 4dof-ik touch CMakeLists.txt 作成した 4dof-ik ディレクトリもサブディレクトリとして登録されるように、srcディレクトリ直下の CMakeLists.txt に下記を記載します。(最終行を追加してください) # simple-ols add_executable(simple-ols simple-ols.cpp) target_link_libraries(simple-ols absl::log_initialize Ceres::ceres) # 4dof-ikディレクトリをサブディレクトリとして登録する add_subdirectory(4dof-ik) データ構造体の定義 # まずはデータ構造をまとめるための構造体を定義します。 4dof-ik ディレクトリ内に、位置と姿勢をまとめた Pose 構造体と、ロボットの機構パラメータをまとめた KinematicsParameters 構造体を作成します。 cd ~/ceres_solver_ws/src/4dof-ik touch Pose.hpp touch KinematicsParameters.hpp 下記のように記述します。 Pose.hpp /// @brief 姿勢 struct Pose { /// @brief X座標 double x; /// @brief Y座標 double y; /// @brief 先端角度 double phi; Pose(double x_, double y_, double phi_) : x(x_), y(y_), phi(phi_){} }; KinematicsParameters.hpp /// @brief 機構パラメータ (リンク長) struct KinematicParameters { /// @brief 第1リンク長 double L1; /// @brief 第2リンク長 double L2; /// @brief 第3リンク長 double L3; /// @brief 第4リンク長 double L4; /// @brief コンストラクタ /// @param l1 第1リンク長 /// @param l2 第2リンク長 /// @param l3 第3リンク長 /// @param l4 第4リンク長 KinematicParameters(double l1, double l2, double l3, double l4) : L1(l1), L2(l2), L3(l3), L4(l4) {} }; 最適化計算の実装 # 次に、逆運動学計算を解くためのメインプログラムを作成します。 メインプログラム作成 touch 4dof-ik.cpp 最終的なファイル構造は下記のようになります。 ceres_solver_ws/ ├── CMakeLists.txt └── src/ ├── CMakeLists.txt ├── simple-ols.cpp └── 4dof-ik/ ├── CMakeLists.txt ├── 4dof-ik.cpp ├── KinematicsParameters.hpp └── Pose.hpp 4dof-ik.cpp の実装は下記の通りです。 4dof-ik.cpp #include <iostream> #include <ceres/ceres.h> #include <ceres/rotation.h> #include <cmath> #include "Pose.hpp" #include "KinematicsParameters.hpp" /// @brief 順運動学を行う /// @tparam T データ型 /// @param[in] kp 機構パラメータ /// @param[in] theta 関節角度ベクトル(配列) /// @param[out] x X座標 /// @param[out] y Y座標 /// @param[out] phi 先端角度 template <typename T> void compute_forward_kinematics(const KinematicParameters& kp, const T* const theta, T& x, T& y, T& phi) { x = T(kp.L1) * cos(theta[0]) + T(kp.L2) * cos(theta[0] + theta[1]) + T(kp.L3) * cos(theta[0] + theta[1] + theta[2]) + T(kp.L4) * cos(theta[0] + theta[1] + theta[2] + theta[3]); y = T(kp.L1) * sin(theta[0]) + T(kp.L2) * sin(theta[0] + theta[1]) + T(kp.L3) * sin(theta[0] + theta[1] + theta[2]) + T(kp.L4) * sin(theta[0] + theta[1] + theta[2] + theta[3]); phi = theta[0] + theta[1] + theta[2] + theta[3]; } /// @brief コスト struct IKCostFunction { Pose target_pose; KinematicParameters kp; /// @brief コンストラクタ /// @param pose 先端位置・姿勢 /// @param param 機構パラメータ IKCostFunction(const Pose& pose, const KinematicParameters& param) : target_pose(pose), kp(param) { } template<typename T> bool operator()(const T* const theta, T* residuals) const { T x, y, phi; compute_forward_kinematics(kp, theta, x, y, phi); residuals[0] = T(target_pose.x) - x; // X座標誤差 residuals[1] = T(target_pose.y) - y; // Y座標誤差 residuals[2] = T(target_pose.phi) - phi; // 姿勢角度誤差 return true; }; }; /// @brief メイン関数 int main(int argc, char** argv) { // 機構パラメータの定義 double l1 = 1.5; double l2 = 1.5; double l3 = 1.0; double l4 = 1.0; // 目標位置・姿勢の定義 double x_target = 3.2; double y_target = 0.8; double phi_target = -M_PI_4; // 初期値の設定 double theta[4] = {0.0, 0.0, 0.0, 0.0}; KinematicParameters kp(l1, l2, l3, l4); Pose target_pose{ x_target, y_target, phi_target }; // 最適化問題の定義 ceres::Problem problem; problem.AddResidualBlock( // 誤差次元数は3、最適化変数次元数は4 new ceres::AutoDiffCostFunction<IKCostFunction, 3, 4>( new IKCostFunction(target_pose, kp) ), nullptr, theta ); // 上下限値の適用(-pi ~ piとする) for (int i = 0; i < 4; i++) { problem.SetParameterLowerBound(theta, i, -M_PI); problem.SetParameterUpperBound(theta, i, M_PI); } // ソルバ設定 ceres::Solver::Options options; options.linear_solver_type = ceres::DENSE_QR; options.minimizer_progress_to_stdout = true; options.max_num_iterations = 100; // 最大反復回数 options.function_tolerance = 1e-6; // 収束判定の閾値 // 最適化計算を解く ceres::Solver::Summary summary; ceres::Solve(options, &problem, &summary); // 計算結果の出力 std::cout << summary.BriefReport() << std::endl; if (summary.termination_type == ceres::CONVERGENCE) { std::cout << "✅ IK Solution Found! (Final Cost: " << summary.final_cost << ")\n"; } else { std::cout << "❌ IK Failed to Converge.\n"; } // 求まった関節角度の出力 std::cout << "theta[0] = " << theta[0] << std::endl; std::cout << "theta[1] = " << theta[1] << std::endl; std::cout << "theta[2] = " << theta[2] << std::endl; std::cout << "theta[3] = " << theta[3] << std::endl; // 求めた関節角度でどの位置・姿勢になるかを確かめる std::cout << "Check Forward kinematics calculation:" << std::endl; double x_result, y_result, phi_result; compute_forward_kinematics(kp, theta, x_result, y_result, phi_result); std::cout << "x = " << x_result << std::endl; std::cout << "y = " << y_result << std::endl; std::cout << "phi = " << phi_result << std::endl; return 0; } CMakeLists.txtに追加する # 4dof-ik ディレクトリ内のCMakeLists.txtに下記を記述します。 4dof-ik/CMakeLists.txt # 4dof-ik add_executable(4dof-ik 4dof-ik.cpp) target_link_libraries(4dof-ik absl::log_initialize Ceres::ceres) ビルド・実行 # ワークスペースに移動してビルドします。 cd ~/ceres_solver_ws cmake --build bin 実行してみます。 ./bin/4dof-ik 実行結果は下記のようになると思います。 (イテレーション結果は割愛します) Ceres Solver Report: Iterations: 7, Initial cost: 2.248425e+00, Final cost: 4.808292e-20, Termination: CONVERGENCE ✅ IK Solution Found! (Final Cost: 4.80829e-20) theta[0] = -0.414376 theta[1] = 1.25568 theta[2] = 0.609086 theta[3] = -2.23579 Check Forward kinematics calculation: x = 3.2 y = 0.8 phi = -0.785398 最終コストはほぼ0のため、指定した目標位置・姿勢を満たす関節角度を見つけることが出来ました! また、検算結果も正しそうです! 5. おわりに # 本記事では、CeresSolverを使用して最適化問題を解くプログラムを紹介しました。 本記事で紹介したサンプル以外にも、公式ページでは多くのサンプルプログラムが提供されています。 興味のある方は確認してみてください。 http://ceres-solver.org/nnls_tutorial.html#non-linear-least-squares また、今回作成したプログラムは、下記のリポジトリで公開しています。 https://github.com/hayat0-ota/CeresSolver_tutorial
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第9日目の記事です。 先月の11月に、 Spring Boot 4 と Spring Framework 7 がリリースされました。 Spring Boot 4ではnull安全性(Null-Safety)の強化が図られ、 JSpecify が標準採用されました。 そこで今回は、Spring Boot 4によるJSpecifyについて記事にしたいと思います。 JSpecifyとは # JSpecifyは、Javaにおけるnull安全性を標準化するための仕様と、そのアノテーションを提供するオープンソースプロジェクトです。 Javaプログラムのnull安全性を高め、異なるツールやライブラリ間での互換性の問題を解決するための共通的なルールブックのようなものと捉えていただければよいと思います。 JSR-305 の停滞やnull安全性に関したアノテーションの乱立を背景に、2021年にGoogleがJSpecifyプロジェクトを立ち上げ、JetBrainsやMeta、Sonarなどの複数の組織と協力して開発を進めてきました。 JSpecifyの1.0.0版がリリースされたのは2024年の7月です。 その後、Spring Boot 4.0.0-M2の リリースノート では、JSpecify準拠のアノテーションをコードベースに導入することが発表されました。 Spring Boot 3系でのnull安全性 # 筆者の勝手な想像ですが、Spring Boot 3系でJSpecifyを利用されていた開発プロジェクトは少ないのではないでしょうか。 筆者が関わってきた開発プロジェクトで、JSpecifyを利用したことは一度もありませんでした。また、筆者のまわりでもJSpecifyを利用された話は聞いたことがありません。 そのため、このタイミングでJSpecifyがくるとは、まったく予想していなかったというのが正直なところです。 では、Spring Boot 3系でのnull安全性はどのように実装していたかと言いますと、筆者はSpring Frameworkから提供されるアノテーションを利用していました。 基本的には、Spring Frameworkの @NonNullApi と @Nullable の組み合わせで利用することがほとんどでした。 まず、 package-info.java のパッケージ宣言に @NonNullApi アノテーションを付与し、そのパッケージ配下のクラス等でnullを許容するメソッドの戻り値や引数に @Nullable アノテーションを付与していました。 package-info.java @org.springframework.lang.NonNullApi package com.mamezou.blog.batch.util; // nullを空文字へ変換する。 public static String nullToEmpty(@Nullable String value) { return value == null ? "" : value; } // 空文字をnullへ変換する。 @Nullable public static String emptyToNull(@Nullable String value) { return (value == null || value.isEmpty()) ? null : value; } この他にも @NonNullFields アノテーションや @NonNull アノテーションが提供されていますが、主にはこの組み合わせで利用することが多かったと記憶しています。 JSpecifyの導入 # JSpecifyの利用方法を説明する前に、プロジェクトへのJSpecifyの導入について簡単に触れておきたいと思います。 Spring Boot 3系でJSpecifyを導入する際には、次のようにMavenプロジェクトの pom.xml にJSpecifyの依存関係を追加する必要がありました。 pom.xml <dependency> <groupId>org.jspecify</groupId> <artifactId>jspecify</artifactId> <version>1.0.0</version> </dependency> ですが、Spring Boot 4ではJSpecifyが標準採用されたため、個別にこのような依存関係を追加する必要がありません。 Spring BootのStarterライブラリが含まれていれば、JSpecifyを利用することができます。 記事の執筆に向けて作成したMavenプロジェクトの依存ライブラリを確認してみると、次のとおりとなりました。 $ mvn dependency:tree -Dincludes=org.jspecify:jspecify [INFO] --- dependency:3.9.0:tree (default-cli) @ mamezou-blog-batch --- [INFO] com.mamezou.blog:mamezou-blog-batch:jar:1.0.0 [INFO] \- org.springframework.boot:spring-boot-h2console:jar:4.0.0:compile [INFO] \- org.springframework.boot:spring-boot:jar:4.0.0:compile [INFO] \- org.springframework:spring-core:jar:7.0.1:compile [INFO] \- org.jspecify:jspecify:jar:1.0.0:compile なお、今回は ApplicationRunner を用いたCLIベースのバッチアプリケーションとしたため、Spring MVCなどのWeb開発に関したStarterライブラリは含んでおりません。 --> Information Spring Boot 4にて、Spring MVCのStarterライブラリの名称が変更されました。 Spring MVCのStarterライブラリは、Spring Boot 3系では spring-boot-starter-web でしたが、Spring Boot 4では spring-boot-starter-webmvc となります。 Spring Initializr などからプロジェクトを作成する際は特に気にすることもありませんが、直接 pom.xml や build.gradle を編集する際はご注意ください。 JSpecifyの利用 # それでは、JSpecifyが提供するアノテーションについて説明していきたいと思います。 JSpecifyにおけるnull安全性のアノテーションは、次の4つを利用します。 No. アノテーション 説明 1 @Nullable null許容であることを示すアノテーション。 2 @NonNull null非許容であることを示すアノテーション。 3 @NullMarked 一律でnull非許容とするためのアノテーション。 4 @NullUnmarked null安全性チェックの対象外とするアノテーション。 Nullable と NonNull # メソッドの戻り値や引数にnullを許容する場合は、 @Nullable アノテーションを使用します。 一方で、nullを非許容とする場合は、 @NonNull アノテーションを使用します。 利用方法としては、Spring Frameworkから提供されるアノテーションと同じです。 Spring Frameworkのアノテーションと比較して大きく異なるのが、JSpecifyの @Nullable と @NonNull は、アノテーションの定義における @Target に ElementType.TYPE_USE が指定されている点です。 org.springframework.lang.Nullable @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @CheckForNull @Deprecated(since = "7.0") @TypeQualifierNickname public @interface Nullable org.jspecify.annotations.Nullable @Documented @Target({ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Nullable これにより、JSpecifyの @Nullable と @NonNull は、型が使用されているあらゆるところにアノテーションを付与することができます。 たとえば、次のような @Nullable の指定は、Spring Frameworkのアノテーションではビルドエラーとなりますが、JSpecifyのアノテーションではエラーになりません。 @NonNull public List<@Nullable String> getValues { // ----- <中略> ----- // } この場合、戻り値となる List の要素にnullが含まれてもよいことを示しています。 List そのものはnull非許容となります。 NullMarked # @NullMarked は、Spring Frameworkの @NonNullApi や @NonNullFields と同様となり、 package-info.java のパッケージ宣言に付与することで、パッケージ内のクラス等を一括でnull非許容として扱うことができます。 つまり、 @NullMarked を使用すれば、null非許容とする箇所に @NonNull を指定する必要がなくなりますね。 package-info.java @org.jspecify.annotations.NullMarked package com.mamezou.blog.batch.util; // @NonNull ← これは不要 public static String nullToEmpty(@Nullable String value) { return value == null ? "" : value; } 先述の「 Spring Boot 3系でのnull安全性 」のように、JSpecifyにおいても @NullMarked と @Nullable の組み合わせでnull安全性を実現することができます。 NullUnmarked # 最後に @NullUnmarked ですが、Spring Frameworkにこれ相応のアノテーションは存在せず、JSpecify特有のアノテーションとなります。 @NullUnmarked は、null安全性チェックの対象外とするためのアノテーションです。 次のようなメソッドは本来、戻り値と引数に @Nullable を付与する必要がありますが、 @NullUnmarked をクラスに付与することでnull安全性チェックの対象外とすることができます。 @NoArgsConstructor(access = AccessLevel.PRIVATE) @NullUnmarked public final class StringUtils { public static String emptyToNull(String value) { return (value == null || value.isEmpty()) ? null : value; } } @NullMarked と同様、 @NullUnmarked も package-info.java のパッケージ宣言に付与することができます。 しかし、パッケージ宣言に @NullUnmarked を指定してしまうと、null安全性チェックの対象外のスコープを広めてしまうため、パッケージ宣言は @NullMarked を基本とし、必要に応じてクラスまたはメソッドの単位に @NullUnmarked を指定するのがよいと筆者は考えます。 アノテーションの対応関係 # null安全性に関して、Spring Frameworkのアノテーションと比較しながら、JSpecifyのアノテーションについて説明してきました。 これらのアノテーションの対応関係をまとめると、下表のとおりとなります。 No. Spring Boot 4(JSpecify) Spring Boot 3系(Spring Framework) 1 org.jspecify.annotations.Nullable org.springframework.lang.Nullable 2 org.jspecify.annotations.NonNull org.springframework.lang.NonNull 3 org.jspecify.annotations.NullMarked org.springframework.lang.NonNullApi 4 org.jspecify.annotations.NullMarked org.springframework.lang.NonNullFields 5 org.jspecify.annotations.NullUnmarked 該当なし ここでひとつ、残念なお知らせがあります。 Spring Boot 4のリリースと併せて、Spring Frameworkから提供される @Nullable 、 @NonNull 、 @NonNullApi 、および @NonNullFields は非推奨( @Deprecated )となりました。 これらのJavadocを参照すると、すべてJSpecifyのアノテーションへの移行を促しています。 つまり、Spring Frameworkのアノテーションを利用している開発プロジェクトでは、Spring Bootのバージョンアップに伴ってJSpecifyのアノテーションへの置き換えが必要となります。 すでにSpring Boot 4へのバージョンアップが計画されている開発プロジェクトや、数年先にリリースを控えている開発プロジェクトでは、少なからず影響があるのではないでしょうか。 最後に # というわけでして、簡単ですがSpring Boot 4に標準採用されました「JSpecify」について説明させていただきました。 さらに詳細な利用方法については、JSpecify公式の ユーザーガイド などを参照いただきたく存じます。 Spring FrameworkやJakartaプロジェクト、JetBrains、Lombokなど、null安全性のアノテーションが乱立する中、JSpecifyプロジェクトによる標準化に向けた動きそのものは正しい取り組みと感じています。 しかし、その一方でSpring Bootを用いたアプリケーション開発において、Spring Frameworkのアノテーションが非推奨となることは予想できなかったですし、冒頭でも述べたとおりJSpecifyが巻き返してくるとは思ってもみませんでした。 Spring Bootのアップデートに伴い、Spring Frameworkにおけるnull安全性のアノテーションが非推奨となっても、アプリケーションの動作に直接影響することはないと思います。 ですが、非推奨のまま利用し続けるのは、ちょっと!ちょっとちょっと!という想いです。 また、多くの開発プロジェクトでも、非推奨のものを利用するな!が基本原則として定められていることと推察します。 ですので、これに関してはSpring Bootをアップデートする際、Spring FrameworkのアノテーションをJSpecifyのアノテーションに置き換えるというのが正しい対応と言えるでしょう。 今回は、JSpecifyのnull安全性といった少し地味目のネタではありましたが、最後までご覧いただき本当にありがとうございました。
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第8日目の記事です。 ヘキサゴナルアーキテクチャ(Ports & Adapters)って、なんとなくわかった気はするけど、どこか腑に落ちないところありませんか?私の場合はだいたい次の3点でした。 「依存は外側 → 内側」と言うけれど 入力ポートと実装で依存が逆向きに見える のはなぜ?それっていいの? 入力のアダプタはポートを実装していない のに 出力のアダプタはポートを実装している のはなにか気持ち悪い そもそも、 オニオンアーキテクチャと何が違うの? 今回の記事は、これらのモヤモヤを整理したときのメモを例を交えながら説明していきたいと思います。 1. 説明の例に使うヘキサゴナル構成 # 最初に今回の記事は次に示す「教科書的なヘキサゴナル」なパッケージ構造をもつ Spring Boot のTODOアプリを例に行います。 com.example.todohex ├─ TodoHexApplication … @SpringBootApplication │ ├─ domain … ドメインモデル(純粋Java) │ └─ Task.java │ ├─ application │ ├─ port │ │ ├─ in … 入力ポート(UseCase IF) │ │ │ ├─ CreateTaskUseCase.java │ │ │ └─ GetTaskUseCase.java │ │ └─ out … 出力ポート(Repo/Gateway IF) │ │ ├─ SaveTaskPort.java │ │ └─ LoadTaskPort.java │ └─ service … ユースケース実装 │ └─ TaskService.java │ ├─ adapter │ ├─ in │ │ └─ web … RESTアダプタ(入力側) │ │ ├─ TaskController.java │ │ ├─ TaskRequest.java │ │ └─ TaskResponse.java │ └─ out │ └─ persistence … 永続化アダプタ(出力側) │ ├─ TaskEntity.java │ ├─ SpringDataTaskRepository.java │ └─ TaskPersistenceAdapter.java └─ ... 2. 依存が逆になところがあるけどいいの? # では早速最初のモヤモヤへ。どこが「逆向きに見える」かですが、 教科書的に UseCase と Service を導出すると依存関係 [1] が逆になります。例をもとに図にすると次のように赤線の依存関係が右から左になっています。 一方でヘキサゴナルアーキテクチャに対する世の中的な解説では「モジュールの依存は外側 → 内側」といった説明がたくさん出てきます。ここで「えっ、 port と service の依存関係って外側から内側の反対に向いているけどこれっていいの?」というモヤモヤが沸いてきます。 そこで、いったん原典に立ち戻りヘキサゴナルアーキテクチャの提唱者であるアリスター・コバーン自身は何といっているかを振り返ると、彼が原典といえる 元記事 でいっていることは大ざっぱにいうと以下のようなことです。 アプリケーションは Port を通して外部と会話する その Port のプロトコルは「アプリケーションの API という形をとる」 ここでいう「API」は、メソッド呼び出しでもいいし, HTTP でも, メッセージングのプロトコルでもなんでも良い、というかなり抽象的なレベルの話をしていて、少なくとも原典では、 「入力ポートを インタフェースと実装クラス に分けろ」 「モジュールの依存矢印は 必ず外→内に向けろ 」 といった Java 的な「お作法レベル」のことはいっていません。また、最近の彼の スライド版 では型付き言語向けに “required interface” を宣言しよう Port の宣言用フォルダを用意しよう くらいはいっていますが、 依存の矢印ルールそのものには踏み込んでいません。 --> 結論:依存を外側から内側へは単なる都市伝説 アリスター・コバーンは依存の方向についてはなにもいっていません。むしろ、ポートにはインタフェースを出せといっているので、ポートとその実装の依存関係が逆になるのは自然です。この話しは「依存は外側のリングから内側のリングへだけ許可すべし」と明確にいっているクリーンアーキテクチャ [2] と同じコンテキストでヘキサゴナルアーキテクチャが語られることから生まれた都市伝説ではないかと思います。 ただ、 port.in と service を 1 つの「アプリケーション・コアの塊」として見てしまえば、 adapter.in → (port.in+service) → domain という「外→内」の構図となるので、いわゆる、クリーンアーキテクチャの1種とみても問題ないとも思います 3. アダプタってポートを実装しないの? # 次のモヤモヤはこれです。 in 側の adapter(Controller 等)は port.in を実装していない(赤の依存) out 側の adapter(DB や外部API)は port.out を実装している(青の依存) 言葉だけではわかりづらいので図で表すと次のようになっています。 同じアダプタでもポートを実装したりしなかったり、そもそも左右で対称じゃなく、なんとなく気持ち悪いですよね。というか正直これってホントにあってるの?と思ったりしたのは私だけでしょうか? 疑問があったら 原典 ということでコバーンがなんといっているか再度みてみましょう。 彼は Hexagonal を Ports & Adapters とも呼びますが、ここでいう Port と Adapter は「役割の名前」になっています。 Port: 「何のための会話か」を表す 論理的な接点 Adapter: その Port を、特定の技術(HTTP / CLI / DB / メール / ファイル…)に 接続する変換器 そして彼の スライド ではPort を Driving Ports(アプリケーションを「駆動する」側) Driven Ports(アプリケーションが「駆動される」側) に分けて説明しています。 この観点で見ると、 Driving Port 側 Adapter(UI / REST / Batch …)は「Port の定義に従ってコールする クライアント 」 Driven Port 側 Adapter(DB / メール / 外部API …)は「Port の定義を満たして処理する サーバ 」 になるため、 in 側の adapter が port を 実装していない out adapter が port を 実装 している という 非対称さは実は自然 なものとなります。 --> 結論:アダプタとポートは役割名で構文パターンのことではない Port / Adapter という名前は「入力=implements、出力=implements」という構文パターンのことではなく、 「会話の目的を表す窓口」と「外界との変換器」という役割を指している だけ。そう捉えると、実装有無の非対称さはそこまで気にならなくなります。 4. オニオンアーキテクチャと何が違うの? # 最後のモヤモヤはこれです。 結局、ヘキサゴナルとオニオンって、何がどう違うの? では、その違いが分かるようにそれぞれの全体構造をみてみたいと思います。 まずオニオンアーキテクチャ # オニオンアーキテクチャをラフに描くと次のような感じになります。 オニオンアーキテクチャの主眼は(今回の図からは分かりづらいですが、、) ドメインを中心に同心円状に層を作り 依存は外側 → 内側になるようにして ドメインを守る といったところになります。 次にヘキサゴナルアーキテクチャの構造 # 対してヘキサゴナル(Ports & Adapters)は、 境界(Port)にフォーカスしたアーキテクチャ といえます。 こうして2つを並べてみると、ヘキサゴナルアーキテクチャは構造的にオニオンアーキテクチャの application 部分とその境界まわりを port.in / port.out と adapter.in / adapter.out に細かく分解し、 入出力の境界(どこから入って、どこへ出ていくか)を強調したもの と見ることができます。 つまり一言でいうと: オニオンアーキテクチャ:層(Layer)で内側を守るアーキテクチャ ヘキサゴナルアーキテクチャ:ポートとアダプタで境界を強調するアーキテクチャ で、目指しているゴール自体はどちらも ドメイン中心 外界(UI/DB/外部システム)からの独立 テスタビリティ向上 といったところでかなり近いです。 --> 結論:ヘキサゴナルはオニオンの上級バージョン(ともいえる) 構造的な観点では「ヘキサゴナル=オニオンの application+境界部分を、port と adapter に分解して“入出力の境界を強調したバージョン”」といえます。 ただし、オニオンアーキテクチャは外側から内側に向かって層を成していく構造をその特徴にしているのに対して、ヘキサゴナルアーキテクチャは図の青線の関係が表すように外→内→外となる構造にすることで「どこから入って、どこへ出ていくか」を構造として強調することをその特徴としています。よって、もともとのコンセプトは異なるものとなります。 5. おわりに # ヘキサゴナルアーキテクチャにすることで確かにクリーンなアーキテクチャを実現することはできますが、それにはコストが掛かります。コバーン自身も スライド の中で次のことをいっています。 各 Port ごとにフィールドや DI 設定が増える 型付き言語では Port 用のインタフェースやフォルダ構成が必要 Configurator(構成ルート)の設計が必要 つまり、ヘキサゴナルアーキテクチャはきれいさと引き換えにクラスやインタフェースが増えます。個人的にドメインを分離したいだけならオニオンアーキテクチャでも十分なことも多いと思っています。 良いものが常に良い訳ではありません。自分たちが必要としているものはなにか?を考え、それにフィットするアーキテクチャを選択するのがアーキテクチャ設計では重要になります。 UML的に「依存関係」といった場合、2者間の一時的な関係(dependency)を指しますが、ここではUML的な意味ではなく、単なる使う使われる関係にあるものを「依存関係」という用語で表しています ↩︎ 筆者はボブおじさんがいっているクリーンアーキテクチャは「クリーンなアーキテクチャはかくあるべき!」的なコンセプトをいっているだけで、クリーンアーキテクチャなんていうアーキテクチャは存在しないと思っている派です。ですので、記事の中のクリーンアーキテクチャはドメインが技術詳細から分離・隔離されている「クリーンなアーキテクチャ」の意味で使用しています ↩︎
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第5日目の記事です。 はじめに # 先月26日 UMTP 主催の Modeling Forum 2025 が開催されました。 https://umtp-japan.org/event-seminar/mf2025/76554 今年は「AI時代のシン・データモデリングとは?」というテーマで多くの講演やパネルディスカッションが行われました。 筆者も当日 Zoom 参加して視聴させていただきました。この記事ではいくつかの講演の内容と感想をお伝えしたいと思います。 --> Information Modeling Forum 2025の講演やパネルディスカッションの模様は UMTP の YouTube チャンネル で視聴できます。 Modeling Forum を開催する UMTP (UMLモデリング推進協議会)は、UML モデリング技能認定試験を通したモデリング技術の普及活動、分析力・発想力・想像力強化のための「モデルベース思考法」の普及活動を行っています。UMTP は豆蔵の羽生田が会長を務めています。 https://umtp-japan.org/greeting 豆蔵デベロッパーサイトでは UMTP 認定試験の記事も公開していますので、取得を目指す方は参考にしていただければと思います。 /blogs/2023/12/11/umtp_l3_challenge/ 開催宣言 # UMTP 会長の羽生田さんから「UMTPシン・モデリング宣言」が発表されました。 当日発表された UMTP シン・モデリング宣言:講演資料のスクリーンショットから これからの時代の基本的なリテラシーは「よみ・かき・AI・モデリング」である。 と宣言し、見える化・仮説検証とアジャイル・AI との対話・モデルベース開発などでモデリングが果たす役割が謳われています。最後にリベラルアーツとしてのモデリングということで、モデリングを基本的なリテラシーとして位置付けています。 基調講演: 科学的思考におけるモデル:説明、創造、そして概念工学へ # 東京大学大学院情報学環・学際情報学府の植原亮准教授が基調講演をされました。 --> 講演の YouTube 動画と資料のリンクです。 MM2025-01 基調講演:科学的思考におけるモデル:説明、創造、そして概念工学へ MF2025 基調講演 資料 - UMTP 特定非営利活動法人UMLモデリング推進協議会 植原先生の著書「科学的思考入門」では、「第5章 科学的に説明するとはどういうことか」という章でモデルのことを扱っているそうです [1] 。 科学的思考入門 (講談社現代新書 2765) | 植原 亮 |本 | 通販 | Amazon 講演では、科学者が探求する分野としての「狭い意味での科学」と「広い意味での科学(ふだん使いの科学)」を定義し、それぞれにおいて使われるモデルの特徴が解説されました。前者の例としては遺伝子の分子モデルのように、モデルがメカニズムの説明・理解に直結しているもの、後者の例としては鉄道の路線図のように実際には複雑な現実をヒトの頭で扱いやすくするものが挙げられ、いずれも抽象化(abstraction)と理想化(idealization)が二大特徴であることが説明されました。 後半には概念工学の説明がありました。概念工学は、①分析と評価 → ②改訂 → ③社会実装 というプロセスで概念を扱うということで、自由意志や創造性などの概念の改訂について説明されていました。 感想: 概念工学というものは初耳学だった。ソフトウェアエンジニアリングのモデルは特定の業務の問題を解くということに特化しているけれども、分析と検証を繰り返し洗練(進化?)させていくところは似てるかも。 久々に大学の教養課程の講義を聴いているような気持ちになった(小並感)。 技術講演 AI readyデータ整備のためのデータモデリング # 株式会社データアーキテクト 代表取締役 真野正さんの講演です。 --> 講演の YouTube 動画は以下にあります。 MF2025-02 技術講演1:AI readyデータ整備のためのデータモデリング 「実践的データモデリング入門」の著者の方です(DB Magazine 懐かしい)。 実践的デ-タモデリング入門 (DB Magazine SELECTION) | 真野 正 |本 | 通販 | Amazon 企業のデータを生成 AI に与えて活用する上でデータマネージメントが重要であることを延べ、エンタープライズ・データモデリングをどう進めるかということがテーマの講演でした。 特に企業が使用するデータが、SoR の構造化データだけでなく、非構造データも増えている点、システムをまたがって整合性のあるデータアーキテクチャを構築する必要性とその進め方が解説されていました。 今後の展望としては、AIエージェントがデータアーキテクチャの維持、品質の担保を担い、データマネージメントは AI エージェントによる自律的なプロセスによって運用されるような世界観が提示されていました。 感想: 現実の AI 導入プロジェクトでも、AI にデータを食わせれば素晴らしい結果が得られると期待する人と、その前にデータを整備しないとガベージ・イン・ガベージアウトだよねという冷静派のせめぎ合い。これは、AI じゃなく BI の時も同じことが言われており、今後も重要課題であり続けるんだろうな。そもそもデータが膨大でシステムの数も多いので、AI に整理を手伝ってもらわないと無理だよなあ。 生成AI時代のドメインモデリング ― OOPとFPを超えて # 株式会社ウルフチーフ 代表取締役 川島義隆さんの講演です。 --> 講演の YouTube 動画は以下にあります。 MM2025-03 技術講演2:生成AI時代のドメインモデリング ― OOPとFPを超えて --> Information 川島さん Cosense を愛用されているようで、本講演で紹介された話題もまとめられています。 https://scrapbox.io/kawasima/ ドメインモデリングのレベルとして、概念・仕様・実装を区別し、仕様モデルの構成要素を以下のように定義しています。 仕様ドメインモデルは、小路の抽象レベルで記述された業務の データ 振る舞い で構成される そして、Liskov の手続き抽象に基づく仕様記述の書き方が紹介されました。 入力(データ抽象) 出力(データ抽象) requires: 入力が全域的 [2] でない場合に、その条件を書く modifies: 書き換えられる入力を書く effects: 使用される入力についての振る舞いを書く https://scrapbox.io/kawasima/%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E8%A8%98%E8%BF%B0%E3%83%9F%E3%83%8B%E8%A8%80%E8%AA%9E そして、仕様モデル駆動設計についてのお話。 アウトサイドイン開発 : 業務コアの概念がわからないまま、画面駆動/テーブル駆動で設計・開発する → 配線プログラミングになりがち インサイドアウト開発 : ①仕様モデル書く(人が) ②仕様モデルに沿った Presentation モデル(画面)を書く(AI が) ②仕様モデルに沿った Persistence モデル(DB)を書く(AI が) https://scrapbox.io/kawasima/%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%80%81%E5%A4%96%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8B%E3%81%8B%3F%E5%86%85%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8B%E3%81%8B%3F 感想 DbC に似てる。実装言語でよい設計を頑張るのではなく、人は仕様記述に徹して、仕様を AI にレビューしてもらって、AI に実装言語でコードを書いてもらうのは筋がよさそう。実装言語を意識せず仕様を書ける人が強い世界。 仕様記述言語欲しいね。詳細設計書という名の、バグがあっても気づけないドキュメントを延々と書き続けている SIer は今だにいるので、そういうのが駆逐できたらいいなあ。 川島さんの仕様モデル駆動設計の本出たら買おう。 Excelデータ分析で学ぶディメンショナルモデリング~アジャイルデータモデリングへ向けて~ # 株式会社風音屋代表取締役ゆずたそ(横山翔)さんによる講演です。 --> 講演の YouTube 動画は以下にあります。 MF2025-04 技術講演3:Excelデータ分析で学ぶディメンショナルモデリング~アジャイルデータモデリングへ向けて~ 科学専門書) | ローレンス・コル, ジム・スタグニット, 打出紘基, 佐々木江亜, 土川稔生, 濱田大 ディメンショナルモデリングの話でした。ファクトとディメンションをどのように定義するか、業務内容がピボットし続ける時代だからこそ、アジャイルにデータ整備をするの大事というお話をされていました。 さらにデータサイエンティストはお高いが、データ分析は AI エージェントにかなりのアウトプットを期待できるようになっている。信頼できるデータソースを整備すれば、AI エージェントにやらせると安いし早いというお話もされてました。 感想 真野さんのエンタープライズデータモデリングの話にも似てるけど、アジャイルデータモデリングというところが特徴。 データ整備ができれば、データ分析は、AI エージェントにやってもらえばいいというのは分かる。 こんにちは!データモデリング # エークリッパー・インク代表 羽生章洋さんの講演です。 --> 講演の YouTube 動画は以下にあります。 MF2025-05 技術講演4:こんにちは!データモデリング 「楽々ERDレッスン」の羽生さんです。新しく要件定義の本を書かれたそうです。 こんにちは!要件定義①【情報活用とデータベース編】 (ビジネス×IT企画) 単行本(ソフトカバー) AI の登場によって、これまで IT 化の対象にできなかった領域についても低コストでアプリが作れるようになった。人間が上流工程を行うフロントローディングの時代。上流工程・要件定義が一層大事になっている。 IPA のデジタルスキル標準では、データベース設計をすべてのビジネスパーソンが理解すべきリテラシーとして位置付けているそうです。 羽生さんは現在デジタル人材のリテラシー向上支援をされていて、これまで IT の仕事をしてない人が「お前デジタル詳しいだろ、DX 担当な」と任命されて悩む、そういう人が多いそうです。 感想 DX 人材って不足してるんやなあ(棒)。 要件定義の中心にモデルを置きLLMが出力した要件に責任をもつ # 株式会社バリューソース 代表取締役社長 神崎善司さんの講演です。 --> 講演の YouTube 動画は以下にあります。 MF2025-06 技術講演5:要件定義の中心にモデルを置きLLMが出力した要件に責任をもつ 神崎さんは元豆蔵で筆者が入社した頃の上司でした。モデルベースのビジネスとシステムの可視化手法である RDRA を提唱して実践されています。 https://www.rdra.jp/%E3%83%9B%E3%83%BC%E3%83%A0 最近のプロジェクトでは AI エージェントに要件定義は全部任せるように舵を切ったそうです。AI が出力した要件定義を可視化して人間が検証するという段階になっているそうです。 AI にどうやって適切なコンテキストを用意し、AI の出力をいかに理解し軌道修正するかということが課題になっているとのことでした。 要件の可視化ツールとして RDRA Graph を使用して説明しておられました。 RDRA - RDRAGraphツール 感想 神崎さん AI エージェント使いこなしてるなあ。 RDRA のビューアの動きが面白くて、内容が入ってこない。 さいごに # この後の講演やパネルディスカッションは離脱してしまい聴けませんでしたが [3] 、視聴したどの講演も興味深く、AI エージェント時代を迎えてモデリングの重要性はさらに高くなっていることを確信できる内容でした。個人的には川島さんの講演がヒットでした。 Vibe Coding などの AI エージェントとの協調作業においては言語運用能力の重要性は認知されていると思いますが、AI とモデルを共有することで、AI の出力の質が高くなったり、認識齟齬が減ったりするのであればモデリングに取り組む価値はあるのではないでしょうか。 仕様駆動開発という言葉は出てきてますが、モデルベース仕様駆動開発の時代が来るのかもしれません。 本書は、「ふだん使い」の科学を身につけようというコンセプトの一般向け実用書に擬態した科学哲学の入門書とのことです。 ↩︎ 全域性:取りうる入力の全てに対して 対応する出力が存在する(ふるまいが関数として定義できる) ↩︎ パネルディスカッションの YouTube 動画も こちら に公開されています。 ↩︎
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第4日目の記事です。 はじめに # アジャイルグループの石田です。 近年、生成AIの進化は目覚ましく、私たちの働き方を大きく変えようとしています。豆蔵デベロッパーサイトでも、設計、実装、テストといったシステム開発の各工程で生成AIを活用する記事が豊富に投稿されています。(ご興味があれば #生成AI や #AIエージェント のタグもご覧ください)。 一方で、スクラムマスターとしてスクラムのプロセス自体にAIをどう活用するか、というテーマはまだ発展途上で、具体的な実践例も多くはありません。 そこで本連載では、全3回にわたり、スクラムマスターがAIとどう向き合い、チームを支援できるかのヒントを、私自身の実践を交えながら探っていきます。 連載の構成は以下の通りです。 第1回:導入(本記事) 第2回:透明性 第3回:検査・適応 第1回となる本記事では、スクラムガイド拡張パックで言及されている「人工知能」の考え方をベースに、スクラムにおけるAI活用の全体像と基本的な考え方をご紹介します。 スクラムガイド拡張パックとAI # スクラムガイド拡張パック は、2020年版スクラムガイドの包括的な副読本として作成され、2025年6月に公開された文書です。有志により日本語化もされています。 この拡張パックでは、変化し続ける市場や技術に対応するため、人工知能(AI)が「拡張パックにおけるスクラムの役割」のひとつとして定義されています。これは、AIが単なる技術トレンドではなく、スクラムの実践を強化する重要な要素として認識されていることを示しています。 AIがスクラムを強化する可能性については、下記のように紹介されています。 AIは以下を通じてスクラムを強化する可能性がある: 経験的プロセス制御:AI駆動の分析により、透明性・検査・適応が改善される。 認知的拡張:AIにより、人間のスクラムチームメンバーは戦略的・創造的・倫理的な検討に集中できる。 継続的な価値適応:AIは、リアルタイムのユーザーフィードバックとトレンドに基づき、プロダクトバックログアイテムの更新と再優先順位付けを行うことができる。 システム洞察:AIは隠れた相互依存関係を特定し、データに基づいた意思決定を改善する。 この連載では、特にスクラムマスターの役割と深く関わる一つ目の項目に焦点を当てます。 経験的プロセス制御:AI駆動の分析により、透明性・検査・適応が改善される。 (原文:Empirical Process Control: AI-driven analytics improve transparency, inspection, and adaptation.) スクラムマスターの重要な責務は、チームがスクラムの三本柱である「透明性・検査・適応」を実践できるよう支援することです。AIは、この経験的プロセス制御を、これまでになかったレベルで強化する可能性を秘めています。 まとめと次回予告 # スクラムガイド拡張パックの役割の中の「スクラムマスター」には、下記のユニークな一文が記載されています。 スクラムマスターは人間でなければならない。 (原文:The Scrum Master must be human.) これは、チームとの協働や複雑な人間関係の調整といった、人間にしか果たせない役割の重要性を示唆しています。AIはあくまでスクラムマスターの能力を拡張する「強力なパートナー」であり、私たち人間はより本質的な課題に集中できるようになります。 スクラムガイドの精神を元にスクラムに関するアドバイスをAIスクラムマスターからもらうことはできるかもしれませんが、決してスクラムマスターの仕事を代替するものではなく、その役割を強化し、チームの能力を最大限に引き出すためのパートナーである必要があります。 本記事では、AIをスクラムに「導入」するための第一歩として、その全体像と可能性を提示しました。 次回は、スクラムの三本柱の一つである「透明性」をテーマに、AIという新たな武器をどう活用し、プロジェクトの「見える化」を行うか、具体的な実践例を交えて探ります。
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第 3 日目の記事です! はじめに # Anthropicの公式ページをみたことがありますか? https://www.anthropic.com/ 配色の雰囲気が美しくて大変好きです。 さらに、最近 Claude Code とペアプログラミングをすることが多くなり、マークダウンファイルをプレビュー表示することが非常に多くなりました。要件定義やADR、技術記事の執筆などなど。 せっかく高頻度でマークダウンファイルを扱うなら、ぼくの好きなスタイルでプレビューさせたいものです。Anthropic の公式ページのような、読みやすく美しいプレビュー環境を構築できれば、執筆作業もより快適になるはず! しかし、一から自分でスタイリングを考えるのは非常に大変。。。 カラーパレット、タイポグラフィ、コンポーネントスタイルなど、考えるべき要素が多く、時間もかかります。 そんな折、Anthropic から Agent Skills という機能がリリースされました。 Agent Skills は、Claude の機能を拡張するための仕組みで、Claude に特定の作業を行う「スキル(技能)」を与えるためのツールセットです。 さらに、Anthropic が提供している公式の skills brand-guidelines なら、Anthropic のテーマを基盤に色々なスタイリングをしてくれるとのこと。これなら、自分で一から考える必要がなく、公式の洗練されたデザインを簡単に活用できるのではないかと考えました。 この記事では、Claude の Agent Skills を活用して、Markdown Preview Enhanced 拡張機能に Anthropic のブランドガイドラインを適用する方法を解説します。 この記事のゴール # この記事を読むことで、以下のことができるようになります。 Claude の Agent Skills について理解できる Agent Skills とは何か、どのように有効化して使うのかがわかります brand-guidelines スキルの使い方がわかる Anthropic のブランドガイドラインを基盤に、色々なスタイリングをしてくれるスキルです Markdown Preview Enhanced との連携方法がわかる VSCode の拡張機能と Anthropic のブランドガイドラインを組み合わせる手順を理解できます スタイル適用前 # スタイル適用前のプレビュー表示例は以下のとおりです。 スタイル適用前のプレビュー表示例 スタイル適用後 # スタイル適用後のプレビュー表示例は以下のとおりです。 スタイル適用後のプレビュー表示例 前提条件 # この記事では、以下の環境を前提としています。 使用環境 # OS : MacOS, Windows 11 VSCode : 最新版(執筆時点: 1.106.2 ) Claude アカウント : claude.ai にアクセス可能 必要な基礎知識 # VSCode の基本的な操作方法(ファイルの開き方、設定の変更方法など) claude.ai の基本的な使い方(チャット機能の利用経験) Markdown の基礎知識(見出し、リスト、コードブロックなど) 実施手順詳細 # 0. 実施手順で使用する主要な概念 # Agent Skills とは Agent Skills は、Claude の機能を拡張するための仕組みで、Claude に特定の作業を行う「スキル(技能)」を与えるためのツールセットです。この手順では、brand-guidelines スキルを使用するために Agent Skills を有効化する必要があります。Agent Skills は実験的な機能で、Pro プラン以上のユーザーが利用できます。 利用可能な公式スキル Anthropic が提供している公式スキルには、以下のようなものがあります: 最新のスキル一覧は Anthropic の公式スキルリポジトリ で確認できます。 ✅ドキュメント系スキル スキル名 説明 docx Word 文書の作成・編集(変更履歴、書式設定対応) pdf PDF の操作(抽出、作成、結合、フォーム処理) pptx PowerPoint プレゼンテーションの作成(レイアウト、テンプレート、チャート対応) xlsx Excel スプレッドシートの作成(数式、書式設定、データ分析) ✅クリエイティブ・デザイン系スキル スキル名 説明 brand-guidelines Anthropic の公式ブランドカラーと Typography をアーティファクトに適用(この記事で使用) theme-factory 10種類のプリセットテーマまたはカスタムテーマでアーティファクトをスタイリング canvas-design デザイン哲学を使用した .png / .pdf 形式のビジュアルアート作成 algorithmic-art p5.js を使用したジェネラティブアート作成(シード付きランダム、フローフィールド、パーティクルシステム) slack-gif-creator Slack のサイズ制限に最適化されたアニメーション GIF 作成 ✅開発・技術系スキル スキル名 説明 artifacts-builder React、Tailwind CSS、shadcn/ui を使用した複雑な HTML アーティファクト構築 mcp-builder 外部 API やサービスを統合する高品質な MCP サーバー作成ガイド webapp-testing Playwright を使用したローカル Web アプリケーションのテスト ✅エンタープライズ・コミュニケーション系スキル スキル名 説明 internal-comms ステータスレポート、ニュースレター、FAQ などの社内コミュニケーション文書作成 ✅メタスキル(スキル作成用) スキル名 説明 skill-creator Claude の機能を拡張する効果的なスキル作成ガイド template-skill 新しいスキル作成の出発点となる基本テンプレート スキルの呼び出し方法 Agent Skills を呼び出すには、チャットにて自然言語でスキル名を指定して依頼します: # 基本的な呼び出し方 brand-guidelines スキルを使って、○○を作成してください # スキル名を明示的に指定 skill: brand-guidelines を使用して、CSS ファイルを生成してください Claude がスキルを認識すると、必要な処理を自動的に実行してくれます。 brand-guidelines スキルとは brand-guidelines スキルは、Anthropic の公式テーマファイルを生成するための Agent Skills です。このスキルを使用することで、Anthropic の公式 UI で使用されているカラーパレットや Typography を含む CSS ファイルを生成できます。 生成される CSS ファイルの内容 生成される CSS ファイルには、以下のような要素が含まれています: カラーパレット : プライマリカラー、背景色、テキスト色など Typography : フォントファミリー、サイズ、行間など コンポーネントスタイル : ボタン、リンク、コードブロックなど ダークモード対応 : Light テーマと Dark テーマの両方に対応 Markdown Preview Enhanced とは Markdown Preview Enhanced は、VSCode の拡張機能です。 VSCode には標準で Markdown プレビュー機能が搭載されていますが、この拡張機能を使用する理由は以下の通りです: カスタム CSS の適用が容易 : 独自の CSS ファイルを簡単に読み込めます 豊富な機能 : 数式、図表、目次など、標準プレビューにない機能が多数あります 高いカスタマイズ性 : 細かい設定変更が可能です カスタムスタイル適用に関連する機能として、以下の特徴があります: style.less の編集 : 拡張機能専用のスタイルファイルを持っており、そこから外部 CSS を読み込めます リアルタイムプレビュー : スタイルの変更が即座に反映されます 複数のプレビューモード : さまざまな形式でのエクスポートにも対応しています この手順では、brand-guidelines スキルで生成した CSS ファイルを、Markdown Preview Enhanced の style.less ファイルから読み込むことで、Anthropic の公式テーマを適用します。 1. Agent Skills の有効化 # https://support.claude.com/ja/articles/12512180-claude%E3%81%A7%E3%82%B9%E3%82%AD%E3%83%AB%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B claude.ai にアクセス Claude の新規チャット画面 設定画面を開く 機能 > コード実行とファイル作成 を有効化 同画面にて、利用するスキル(brand-guidelines)を有効化 有効化の確認 claude.ai のチャット画面にて、利用可能なスキルを質問してみましょう。 2. Markdown Preview Enhanced 拡張機能のインストール # VSCode の拡張機能マーケットプレイスから、Markdown Preview Enhanced をインストールします。 https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced 3. brand-guidelines スキルによるスタイル作成 # claude.ai のチャット画面にて、brand-guidelines スキルを使用して、CSS ファイルを生成するよう依頼します。 プロンプトは適当ですが、例えば以下の内容で依頼してみました。 vscodeでmarkdownファイルをプレビューした際のスタイルにanthropicの公式テーマを適用したい。 プレビューにはmarkdown preview enhanced 拡張機能を利用する想定。 カスタムcssファイルを生成してほしい。 出力結果は以下のとおりです。 4. 生成されたスタイルの確認 # 今回生成された CSS ファイルは以下のとおりです。 anthropic-markdown-preview.css /* * Anthropic Brand Theme for Markdown Preview Enhanced * VS Code Extension Custom CSS * * Installation: * 1. Open VS Code Settings (Cmd/Ctrl + ,) * 2. Search for "Markdown Preview Enhanced: Style" * 3. Add the path to this file, or * 4. Copy this content to ~/.mume/style.less (or style.css) */ /* ============================================ Import Google Fonts ============================================ */ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap'); /* ============================================ CSS Variables - Anthropic Brand Colors ============================================ */ :root { /* Main Colors */ --anthropic-dark: #141413; --anthropic-light: #faf9f5; --anthropic-mid-gray: #b0aea5; --anthropic-light-gray: #e8e6dc; /* Accent Colors */ --anthropic-orange: #d97757; --anthropic-blue: #6a9bcc; --anthropic-green: #788c5d; /* Typography */ --font-heading: 'Poppins', Arial, sans-serif; --font-body: 'Lora', Georgia, serif; --font-code: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace; } /* ============================================ Base Styles ============================================ */ .markdown-preview.markdown-preview { font-family: var(--font-body); font-size: 16px; line-height: 1.7; color: var(--anthropic-dark); background-color: var(--anthropic-light); padding: 2rem 3rem; max-width: 900px; margin: 0 auto; } /* ============================================ Headings - Poppins Font ============================================ */ .markdown-preview h1, .markdown-preview h2, .markdown-preview h3, .markdown-preview h4, .markdown-preview h5, .markdown-preview h6 { font-family: var(--font-heading); font-weight: 600; color: var(--anthropic-dark); margin-top: 1.5em; margin-bottom: 0.5em; line-height: 1.3; } .markdown-preview h1 { font-size: 2.25rem; font-weight: 700; border-bottom: 3px solid var(--anthropic-orange); padding-bottom: 0.4em; margin-top: 0; } .markdown-preview h2 { font-size: 1.75rem; border-bottom: 2px solid var(--anthropic-light-gray); padding-bottom: 0.3em; } .markdown-preview h3 { font-size: 1.4rem; color: var(--anthropic-dark); } .markdown-preview h4 { font-size: 1.2rem; color: var(--anthropic-mid-gray); } .markdown-preview h5, .markdown-preview h6 { font-size: 1rem; color: var(--anthropic-mid-gray); text-transform: uppercase; letter-spacing: 0.05em; } /* ============================================ Paragraphs and Body Text ============================================ */ .markdown-preview p { margin: 1em 0; text-align: justify; hyphens: auto; } /* ============================================ Links ============================================ */ .markdown-preview a { color: var(--anthropic-orange); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s ease, color 0.2s ease; } .markdown-preview a:hover { color: var(--anthropic-blue); border-bottom-color: var(--anthropic-blue); } .markdown-preview a:visited { color: var(--anthropic-green); } /* ============================================ Lists ============================================ */ .markdown-preview ul, .markdown-preview ol { margin: 1em 0; padding-left: 1.5em; } .markdown-preview li { margin: 0.4em 0; line-height: 1.6; } .markdown-preview ul li::marker { color: var(--anthropic-orange); } .markdown-preview ol li::marker { color: var(--anthropic-blue); font-weight: 600; } /* Nested lists */ .markdown-preview ul ul, .markdown-preview ol ol, .markdown-preview ul ol, .markdown-preview ol ul { margin: 0.3em 0; } /* Task lists */ .markdown-preview input[type="checkbox"] { accent-color: var(--anthropic-orange); margin-right: 0.5em; } /* ============================================ Blockquotes ============================================ */ .markdown-preview blockquote { margin: 1.5em 0; padding: 1em 1.5em; border-left: 4px solid var(--anthropic-orange); background-color: var(--anthropic-light-gray); color: var(--anthropic-dark); font-style: italic; border-radius: 0 8px 8px 0; } .markdown-preview blockquote p { margin: 0.5em 0; } .markdown-preview blockquote p:first-child { margin-top: 0; } .markdown-preview blockquote p:last-child { margin-bottom: 0; } /* Nested blockquotes */ .markdown-preview blockquote blockquote { border-left-color: var(--anthropic-blue); margin: 1em 0; } /* ============================================ Code - Inline and Blocks ============================================ */ .markdown-preview code { font-family: var(--font-code); font-size: 0.9em; } /* Inline code */ .markdown-preview :not(pre) > code { background-color: var(--anthropic-light-gray); color: var(--anthropic-dark); padding: 0.2em 0.4em; border-radius: 4px; border: 1px solid var(--anthropic-mid-gray); } /* Code blocks */ .markdown-preview pre { background-color: var(--anthropic-dark); color: var(--anthropic-light); padding: 1.25em 1.5em; border-radius: 8px; overflow-x: auto; margin: 1.5em 0; border: 1px solid var(--anthropic-mid-gray); } .markdown-preview pre code { background: none; border: none; padding: 0; color: inherit; font-size: 0.875em; line-height: 1.6; } /* ============================================ Syntax Highlighting (Custom Theme) ============================================ */ .markdown-preview pre .hljs-keyword, .markdown-preview pre .hljs-selector-tag, .markdown-preview pre .hljs-built_in { color: var(--anthropic-orange); } .markdown-preview pre .hljs-string, .markdown-preview pre .hljs-attr { color: var(--anthropic-green); } .markdown-preview pre .hljs-number, .markdown-preview pre .hljs-literal { color: var(--anthropic-blue); } .markdown-preview pre .hljs-comment { color: var(--anthropic-mid-gray); font-style: italic; } .markdown-preview pre .hljs-function, .markdown-preview pre .hljs-title { color: var(--anthropic-blue); } .markdown-preview pre .hljs-variable, .markdown-preview pre .hljs-params { color: var(--anthropic-light); } .markdown-preview pre .hljs-type, .markdown-preview pre .hljs-class { color: var(--anthropic-orange); } /* ============================================ Tables ============================================ */ .markdown-preview table { width: 100%; border-collapse: collapse; margin: 1.5em 0; font-size: 0.95em; } .markdown-preview table th, .markdown-preview table td { padding: 0.75em 1em; text-align: left; border: 1px solid var(--anthropic-light-gray); } .markdown-preview table th { font-family: var(--font-heading); font-weight: 600; background-color: var(--anthropic-dark); color: var(--anthropic-light); border-color: var(--anthropic-dark); } .markdown-preview table tr:nth-child(even) { background-color: var(--anthropic-light-gray); } .markdown-preview table tr:hover { background-color: rgba(217, 119, 87, 0.1); } /* ============================================ Horizontal Rules ============================================ */ .markdown-preview hr { border: none; height: 2px; background: linear-gradient( 90deg, var(--anthropic-orange), var(--anthropic-blue), var(--anthropic-green) ); margin: 2em 0; border-radius: 1px; } /* ============================================ Images ============================================ */ .markdown-preview img { max-width: 100%; height: auto; border-radius: 8px; margin: 1em 0; box-shadow: 0 4px 12px rgba(20, 20, 19, 0.1); } /* ============================================ Footnotes ============================================ */ .markdown-preview .footnotes { margin-top: 3em; padding-top: 1.5em; border-top: 2px solid var(--anthropic-light-gray); font-size: 0.9em; color: var(--anthropic-mid-gray); } .markdown-preview .footnote-ref { color: var(--anthropic-orange); font-weight: 600; } /* ============================================ Definition Lists ============================================ */ .markdown-preview dl { margin: 1em 0; } .markdown-preview dt { font-family: var(--font-heading); font-weight: 600; color: var(--anthropic-dark); margin-top: 1em; } .markdown-preview dd { margin-left: 1.5em; color: var(--anthropic-mid-gray); } /* ============================================ Keyboard Keys ============================================ */ .markdown-preview kbd { font-family: var(--font-code); font-size: 0.85em; padding: 0.2em 0.5em; background-color: var(--anthropic-light-gray); border: 1px solid var(--anthropic-mid-gray); border-radius: 4px; box-shadow: 0 2px 0 var(--anthropic-mid-gray); } /* ============================================ Mark / Highlight ============================================ */ .markdown-preview mark { background-color: rgba(217, 119, 87, 0.3); color: var(--anthropic-dark); padding: 0.1em 0.3em; border-radius: 2px; } /* ============================================ Abbreviations ============================================ */ .markdown-preview abbr { text-decoration: underline dotted var(--anthropic-blue); cursor: help; } /* ============================================ Selection ============================================ */ .markdown-preview ::selection { background-color: rgba(106, 155, 204, 0.3); color: var(--anthropic-dark); } /* ============================================ Scrollbar Styling ============================================ */ .markdown-preview ::-webkit-scrollbar { width: 8px; height: 8px; } .markdown-preview ::-webkit-scrollbar-track { background: var(--anthropic-light-gray); border-radius: 4px; } .markdown-preview ::-webkit-scrollbar-thumb { background: var(--anthropic-mid-gray); border-radius: 4px; } .markdown-preview ::-webkit-scrollbar-thumb:hover { background: var(--anthropic-orange); } /* ============================================ Print Styles ============================================ */ @media print { .markdown-preview.markdown-preview { background-color: white; color: black; padding: 0; } .markdown-preview a { color: var(--anthropic-dark); text-decoration: underline; } .markdown-preview pre { border: 1px solid var(--anthropic-mid-gray); background-color: var(--anthropic-light-gray); color: var(--anthropic-dark); } } /* ============================================ Mermaid Diagrams ============================================ */ .markdown-preview .mermaid { background-color: var(--anthropic-light); padding: 1em; border-radius: 8px; text-align: center; } /* ============================================ Math (KaTeX/MathJax) ============================================ */ .markdown-preview .katex, .markdown-preview .MathJax { font-size: 1.1em; } .markdown-preview .katex-display, .markdown-preview .MathJax_Display { margin: 1.5em 0; padding: 1em; background-color: var(--anthropic-light-gray); border-radius: 8px; overflow-x: auto; } /* ============================================ Admonitions / Callouts ============================================ */ .markdown-preview .admonition, .markdown-preview .callout { margin: 1.5em 0; padding: 1em 1.5em; border-radius: 8px; border-left: 4px solid; } .markdown-preview .admonition.note, .markdown-preview .callout.note { background-color: rgba(106, 155, 204, 0.1); border-left-color: var(--anthropic-blue); } .markdown-preview .admonition.warning, .markdown-preview .callout.warning { background-color: rgba(217, 119, 87, 0.1); border-left-color: var(--anthropic-orange); } .markdown-preview .admonition.tip, .markdown-preview .callout.tip { background-color: rgba(120, 140, 93, 0.1); border-left-color: var(--anthropic-green); } /* ============================================ TOC (Table of Contents) ============================================ */ .markdown-preview .md-toc { background-color: var(--anthropic-light-gray); padding: 1.5em; border-radius: 8px; margin: 1.5em 0; } .markdown-preview .md-toc-content { font-family: var(--font-heading); } .markdown-preview .md-toc a { color: var(--anthropic-dark); } .markdown-preview .md-toc a:hover { color: var(--anthropic-orange); } 5. Markdown Preview Enhanced への適用 # コマンドパレットにて、 Markdown Preview Enhanced: Customize CSS (Global) を実行 表示された style.less ファイルに、生成された CSS ファイルの内容を追記し保存 6. 動作確認と微調整 # プレビューに適用されたスタイルを確認して、気になる点があれば微調整します。 スタイルの微調整は、style.less ファイルを直接編集することで実施可能です。 あるいは、claude.ai のチャット画面にて、style.less ファイルの内容を変更するよう依頼もできます。 ぼくはフォントを少し変更しました。 --> Information Markdown Preview Enhanced には、拡張機能の設定画面からプレビューテーマを指定する機能があります( Markdown Preview Enhanced: Preview Theme / Markdown-preview-enhanced: Code Block Theme 設定)。この設定と style.less の内容が競合すると、意図したスタイルが適用されない場合があります。 もしスタイルが思い通りに反映されない場合は、これらの設定を変更してみてください。 まとめ # この記事では、Claude の Agent Skills を活用して、Markdown プレビューに Anthropic のスタイルを適用する方法を解説しました。 実現できたこと # 今回の設定により、以下のことが実現できました: Agent Skills の理解 : Claude の Agent Skills を有効化し、使いこなす方法を学びました brand-guidelines スキルの活用 : 公式スタイルの CSS ファイルを簡単に生成できるようになりました 統一された執筆環境 : Anthropic の公式スタイルを適用したプレビュー環境を構築しました さらなる発展:カスタムスキルの作成 # この記事では公式の brand-guidelines スキルを使用しましたが、別の公式スキル skill-creator を使えば、自分だけのオリジナルスキルを作成することもできます。 例えば、以下のようなカスタムスキルを作成できます: 自社のブランドガイドラインに基づいた CSS 生成スキル 特定のフレームワーク向けのコンポーネント生成スキル プロジェクト固有のドキュメントテンプレート生成スキル skill-creator スキルの使い方は、claude.ai のチャットで以下のように依頼するだけです。 skill-creator スキルを使って、○○を行うカスタムスキルを作成してください 詳細は Anthropic の公式スキルリポジトリ を参照してください。公式の skill-creator や template-skill を参考にすることで、効果的なスキルを作成できます。 なお、skill-creator を利用する場合は、brand-guidelines スキルを有効化したように、skill-creator スキルを有効化することをお忘れなく! 最後までお読みいただき、ありがとうございました。この記事が、あなたの執筆環境をより快適にする助けになれば幸いです。🎨✨
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第 2 日目の記事です! はじめに # 書籍 Think Again 僕がとても好きな書籍です。 この書籍では、 考え直す ことの重要性について書かれています。 本書の目次を以下に示しました。 Part1: 自分の考えを再考する方法 Part2: 相手に再考を促す方法 Part3: 学び、再考し続ける社会・組織を創造する方法 Part4: 結論 再考することの重要性を、 Part1 では自己に、 Part2 では他者に、 Part3 ではチーム・組織に焦点を当てて説明されています。 この記事では、 Part1 でとても印象的な下記内容について紹介したいです。 再考サイクルとは何か? 対比となる過信サイクルとは何か? 再考サイクルを継続するために必要なことは何か? 「化石化した知識」を後生大事にしていないか? # 情報技術の進歩に伴い、人の知識もどんどん増加しています。 本書によると、 1950 年を見てみると、医療の知識は 1900 年から 50 年の歳月をかけて倍増した。 それが 1980 年までには 7 年ごとのペースで、そして 2010 年までにはその半分の歳月で倍増するようになった。 とのこと。 従って、社会の変化に伴い、私たちは今まで当たり前と思っていたことをより頻繁に再考する必要があリます。 だけどなかなかそれができない。。 牧師、検察官、政治家 - 誰もが持つ 3 つの思考モード # なぜ自分の知識や見解を再考できないのか。 それは、無意識に切り替わる以下 3 つの思考モードが原因とのこと。 牧師モード: 自分の信念がぐらついている時、理想を守り確固としたものにするため、他者を説教しようとする。 検察官モード: 他者の推論に矛盾を感じれば、相手の間違いを明らかにするため論拠を探す。 政治家モード: 多くの人を味方につけたい時は、支持層の是認を獲得するためにキャンペーンやロビー活動を行う そして、これらの思考モードは互いに 循環 してしまいます。 具体的なシチュエーションを考えると、こんな感じでしょうか 👇 シチュエーション SNS 経由で知り合った「投資アドバイザー」から勧められた暗号資産投資。 「いま始めれば月利 20 %、限定枠あり」と言われ、初期資金を投入した男性。 月利 20 % はヤバいですね。。相場を逸脱しています。。 牧師モード # 自尊心が確信を生む これは自分が見つけた正しい投資法だ。 自分の考えが正しいことをちゃんと説明すれば、みんなも納得するはず。 真実はすでに見つかっていると考える。 いかに正しいかを証明するために、他者を説得しようとする。 検察官モード # 確信がバイアスを生む 仕組みを理解していない周りが間違っている。 現にこんなに利益が出ている。 相手の指摘を"無知"や"矛盾"として否定する。 自分の考えを肯定する情報しか見えなくなる。 政治家モード # バイアスが是認を生む みんながやっているから、間違いない。 反対している人は、仕組みを理解していない無知な人たちだ。 同じ考えの人たちに囲まれて安心する。 反対意見は"無知な人の意見"として無視する。 本書では、 この循環を 過信サイクル と呼んでいます。 過信サイクルは、以下に没頭するあまり 自分の見解が間違っているかもしれないと再考しなくなってしまう 点で危険です。 自分の信念を貫くこと 他者の間違いを指摘すること 多くの支持を獲得すること 仮説、実験、結果、検証 - 科学者の思考モード # では、暗号資産投資で過信サイクルに陥った男性は、どのように考えれば良かったのか。 例えば、 月利 20 % の根拠は何かと考えるべきだった。 暗号資産投資に関するより多くの見解を集めるべきだった。 多額の資金を投じる前に、少額で実験してみるべきだった。 などでしょうか。 抽象化すると、 科学者の思考モード が必要だったということです。 科学者の思考モード は、以下の 4 つの要素で構成されています。 仮説: 自分の考えを言語化する。 例: 暗号資産投資は、月利 20 % の利益を生む。 実験: 自分の考えを実験する。 例: 少額で投資してみる。 結果: 実験から得られた結果を観察する。 例: 投資額と利益額を記録する。 検証: 結果をもとに仮説を検証する。 例: 月利 20 % の根拠は何か? 他の投資法と比較してどうか? この 4 つの要素を繰り返すことができれば、過信サイクルから抜け出して、 自分の考えを再考できるようになるはずです。 再考プロセスの循環 - 再考サイクル # 科学者の思考モードによる再考プロセスにも、過信サイクルと同じような循環があるそうです。 本書ではこの循環を 再考サイクル と呼び、以下の 4 つの要素で整理しています。 謙虚さ: 知的に謙虚である。 無知を自覚する。 懐疑: 自分の考えに疑問を持つ。 「本当にこれで良いのか?」と問い直す。 好奇心: 自分が持たない情報を知りたいと思う。 見えなかった視点や情報にワクワクする。 発見: 疑問の先に、新しい視点や情報を手にする。 自分の想定より世界が広かったことを知る。 発見 により「学ぶべきこと、発見すべきことはまだたくさんある」と自覚することで、 謙虚さ を保つことができるようになります。 --> Information 人が再考しない理由 これまでのやり方と異なる そんなのできっこない 自分の経験と異なる 複雑すぎて考えたくない 過信サイクル vs 再考サイクル # 過信サイクルと再考サイクルは、以下のように対比されます。 過信サイクル 自尊心 → 確信 → バイアス → 是認 graph LR A[**自尊心**<br>自分の考えこそが<br>正しい] B[**確信**<br>疑う余地はないと<br>思い込む] C[**バイアス**<br>都合の良い情報しか<br>見えなくなる] D[**是認**<br>賛同してくれる人だけを<br>信じる] A --> B B --> C C --> D D --> A style A fill:#fdf5e6,stroke:#d2691e,stroke-width:2px style B fill:#f0f8ff,stroke:#4682b4,stroke-width:2px style C fill:#e6ffe6,stroke:#2e8b57,stroke-width:2px style D fill:#f5f5ff,stroke:#6a5acd,stroke-width:2px 再考サイクル 謙虚さ → 懐疑 → 好奇心 → 発見 graph LR A[**謙虚さ**<br>自分の無知を認める] B[**懐疑**<br>今の見解に疑問を持つ] C[**好奇心**<br>もっと知りたいという衝動] D[**発見**<br>新しい視点に気づく] A --> B B --> C C --> D D --> A style A fill:#fdf5e6,stroke:#d2691e,stroke-width:2px style B fill:#f0f8ff,stroke:#4682b4,stroke-width:2px style C fill:#e6ffe6,stroke:#2e8b57,stroke-width:2px style D fill:#f5f5ff,stroke:#6a5acd,stroke-width:2px また、同じ状況に対しても、自分がどちらのサイクルにいるかで思考や行動が変わります。 過信サイクル 再考サイクル 自分の考えを変える 心の弱わさの表れ 知的誠実さの表れ 他者に説得される 負けた気になる 真実に 1 歩近づいた 考えを変える根拠 他人の反応 理性や証拠 再考サイクルを継続するために必要なこと - 自信に満ちた謙虚さ # 過信サイクルに陥らず、再考サイクルを継続するためにはどうすれば良いか。 本書では、 バランスの取れた自信と謙虚さ が重要だと述べられています。 自信 自己信頼度 自分をどのくらい信頼しているか 注意 ! 自分のやり方をどれほど確信しているかではない 謙虚さ しっかりした知識や能力、つまり自分の過ちや不確実さを認識する力 注意 ! 自信を控えめに持つことではない ここで重要なのは、 自信と謙虚さは両立する ということです。 自信を持つことは、謙虚さを失うこと 謙虚さを持つことは、自信を失うこと ではありません。 将来の目標に達するのに十分な能力が備わっていると自信を持ちながら、 そのための正しい手段は何かと現在の自分に問う謙虚さを持つことは可能です。 つまり、自己の能力を信じながら、自分の解決方法が正しくない可能性、問題自体を誤解している可能性を認める。 そこから疑問が生じれば、既存の知識を再評価するようになり、ほどほどの自信があれば、新しい知識を追い求めることができるようになります。 自分の知識に対してではなく、自分の学ぶ能力を強く信じている状態 この状態が、 自信に満ちた謙虚さ を持った状態です。 おわりに - ソフトウェアエンジニアとしての知識と再考サイクル # ソフトウェアエンジニアは、自分の知識を武器にする職業だと思います。技術的な判断や設計の選択において、これまでに培ってきた知識や経験が意思決定の基盤となります。 最近、 リファクタリング 既存のコードを安全に改善する(第2版) 単体テストの考え方/使い方 現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法 良いコード/悪いコードで学ぶ設計入門-保守しやすい 成長し続けるコードの書き方 等々を読み、どんな設計が良い設計なのか、どんなコードが良いコードなのかを学びました。 特に、変更に強いシステムを開発するためには、手続き型のプログラミングではなく、ドメインモデルをしっかり設計してオブジェクト指向でプログラミングすることが重要だと感じました。 これらの書籍の内容に非常に納得できましたが、同時にある懸念も抱きました。それは、これら書籍の内容に矛盾する知識を排除してしまうのではないかということです。 例えば、単体テストの考え方には「ロンドン学派」と「古典学派」という対立したアプローチが存在するそうです。 このどちらか一方を「正解」として学び、もう一方を「間違い」として排除してしまうと、正解と考える学派以外の知識を受け入れられなくなってしまいそうです。 絶対的な正解があるわけではなく、場面場面に合わせた最適な手段があるだけだと思います。 自分の持つ知識をこの世の正解と捉えてしまうと、自分の持つ知識以外はすべて間違いと考えてしまいかねません。これはまさに過信サイクルの初期フェーズ、 自尊心 の始まりです。 自分の持つ知識はあくまで数ある手段の一つであり、具体的な場面における最適な手段かどうかはわからないと考えることで、再考サイクルを回すことができるのではと思います。 謙虚さを持って自分の知識に疑問を持ち、異なるアプローチや思想に好奇心を持ち、新しい視点を発見することで、より良い判断ができるようになるのではないでしょうか。
これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第1日目の記事です。 はじめに # 昨年9月に Tauri 2.0 の RC を触って記事を書いていました。 /blogs/2024/09/22/try-tauri-v2-rc/ 当時も2年ぶりぐらいに Tauri を触ったのですが、この時からまた1年以上が経ってしまいました。月日が経つの早いですね。 Tauri 2.0 は昨年10月に正式リリースされ、現在のバージョンは 2.9.3 です。そろそろ熟成されてきた頃ではないかと考え、Electron から移植してみる PoC を思いついた次第です。 移植するアプリ # これまでは、ちょっとした SPA 的なアプリを動かす程度のことしかやってこなかったので、もう少し実用的なアプリで試そうと思いました。 例によって拙作の Electron 製の野良 Cosense(Scrapbox) アプリを題材にさせていただきます。 https://github.com/kondoumh/sbe sbe の操作イメージのスクリーンショットです。タブ UI が特徴で、Cosense のページをタブで開いて表示・編集できるのと、独自の管理画面やプロジェクトのページ一覧などの UI も利用可能です。 移植といっても長年メンテしているアプリなので意外とコードベースも大きく機能も多いのでピンポイントでフィーチャーを実装してみて Electron との違いを噛み締めてみるという試みです。 今回の PoC での移植結果の出来上がりのスクリーンショットです。 最初のタブで開いたページの履歴とお気に入りが表示され、次のタブでページ一覧が開きます。ここでは任意の Cosense プロジェクトのページ一覧を表示可能です。履歴やお気に入りのリンクをクリックすると別ウィンドウでページを開きます。 後述しますが、タブ内での Cosense ページ表示はできなかったので PoC では妥協して別ウィンドウ表示としました。お気に入りへの追加はコンテキストメニューから可能です。 作成したコード全体をご紹介すると膨大になってしまうため、リポジトリは記事の終わりに掲載します。記事中のコードスニペットで雰囲気を掴んでいただければと思います。 使用したソフトウェアのバージョンなど # 今回は、以下のような構成で PoC を行いました。 Rust 1.19.1 Tauri 2.9.3 Vite 6.0.3 Vue 3.5.13 sbe では Vuetify を使っていましたが、シンプルにするため、Vue と CSS だけで作成しました。最初に vanilla テンプレートを使ってプロジェクトを作成し、後から必要なものをインストールしました。 mkdir sbe-tauri-poc && cd sbe-tauri-poc npm create tauri-app@latest . --template vanilla-ts マルチビュー、タブ UI # sbe では、複数の Cosense ページや独自の画面 をタブ UI で表示しています。以下のように、Electron の WebContentsView で Scrapbox のページを表示し、複数の WebContentsView を Vue(Vuetify) で実装したタブで切り替えるようにしています。 WebContentsView は BaseWindow に埋め込まれます。複数の WebContentsView を重ねて表示やタイル表示もできますし、API により Z 軸上の順序を入れ替え可能です。レンダラープロセスの Vuetiry のタブクリックイベントをメインプロセスに通知して WebContentsView の Z order を入れ替えることでタブ切り替えを実現しています。 --> Information Electron の WebContentsView の簡単なサンプルを GitHub の mamezou-tech オーガニゼーションで公開しています。 https://github.com/mamezou-tech/electron-example-browserview WebContentsView を使用するアプリの構造については以下の記事を参照してください。 /blogs/2024/08/28/electron-webcontentsview-app-structure/ 実際には sbe は BaseWindow + WebContentsView ではなく、BrowserWindow + BrowserView で実装しています。BrowserView は現在 WebContentsView の SIM として提供されているため、実質 WebContentsView による実装となっています。 一方、Tauri の WebView は埋め込みをサポートしておらず、独立ウィンドウとして表示する方法しかありません。sbe のような UI を実装するには、単独の WebView 内に iframe を使ってサイトを表示する方法が考えられます。 Cosense サイトを iframe で表示しようとすると以下のようなエラーになります。 Refused to load https://scrapbox.io/ because it does not appear in the frame-ancestors directive of the Content Security Policy. Cosense は Content Security Policy (CSP) によって iframe 内での表示を制限しているようです。 そこで、Cosense 自体のタブ内表示は諦め、ページ毎に独立したウィンドウを WebView で起動することにしました。ただし、sbe で実装している管理画面やプロジェクトのページ一覧のような UI はタブで本体の WebView で表示することとしました。 Rust 側で WebView をウィンドウ表示するコマンドを作成しました。 src-tauri/src/lib.rs(抜粋) #[tauri::command] async fn create_webview_window(app: tauri::AppHandle, url: String, label: String) -> Result<(), String> { let webview_url = WebviewUrl::External(url.parse().map_err(|e| format!("Invalid URL: {}", e))?); let window = WebviewWindowBuilder::new(&app, &label, webview_url) .title("Scrapbox") .inner_size(1200.0, 800.0) .min_inner_size(800.0, 600.0) .center() .resizable(true) .visible(false) .build() .map_err(|e| e.to_string())?; // Show window after it's fully initialized window.show().map_err(|e| e.to_string())?; Ok(()) } これを Vue の UI から invoke で呼び出します。 App.vue(抜粋) const reopenWindow = async (window: RecentWindow) => { try { const windowId = `reopen-${Date.now()}`; await invoke('create_webview_window', { url: window.url, label: windowId }); errorMessage.value = ""; } catch (error) { console.error('Failed to reopen window:', error); errorMessage.value = `ウィンドウの再起動に失敗しました: ${error}`; } }; デスクトップがウィンドウだらけになってしまいますが、ひとまずマルチビューアプリの土台はできました。 WebView でのナビゲーションの検出と Rust → フロントエンド通知 # sbe では閲覧した Cosense ページの履歴を記録していますが、これは Electron の webContents のイベントを捕捉して実装しています。Cosense サイト内での遷移は did-navigate-in-page イベントで捕捉できます。 Electron のコード - main.mjs(抜粋) function handleLinkEvent(view) { view.webContents.on('will-navigate', (e, url) => { // リンクを開く処理 }); view.webContents.on('did-start-navigation', async (e, url, isInPlace) => { const currentUrl = view.webContents.getURL(); // 遷移開始時の処理 }); view.webContents.on('did-navigate-in-page', async (e, url) => { // サイト内遷移の処理(ヒストリへの保存など) }); view.webContents.on('update-target-url', (e, url) => { // リンクのマウスオーバー時の処理 }); } Tauri の Rust 用 API では on_navigation や on_page_load というメソッドがあり、Web ページの取得開始やロード完了を捕捉できます。しかしこのハンドラーでは同一サイト内のページ遷移は検出できないようです。Cosense サイト内でのページ遷移をリアルタイムに捕捉するには、JavaScript を WebView に埋め込んでイベントをトラッキングする必要があります。そのため WebView を起動する際に initialization_script でトラッキング用のスクリプトを埋め込みます。 Tauri WebView でのスクリプト注入 - src-tauri/src/lib.rs(抜粋) #[tauri::command] async fn create_webview_window(app: tauri::AppHandle, url: String, label: String) -> Result<(), String> { let webview_url = WebviewUrl::External(url.parse().map_err(|e| format!("Invalid URL: {}", e))?); let window = WebviewWindowBuilder::new(&app, &label, webview_url) .title("Scrapbox") .inner_size(1200.0, 800.0) .min_inner_size(800.0, 600.0) .center() .resizable(true) .visible(false) .initialization_script(include_str!("../scripts/navigation-tracker.js")) .build() .map_err(|e| e.to_string())?; window.show().map_err(|e| e.to_string())?; Ok(()) } 短いスクリプトは initialization_script の中にインラインで書けますが、可読性や IDE での作業効率化のために別ファイルで作成しロードする方がよいでしょう。 以下のスクリプトでは、trackNavigation 関数を用意し、変更を検出したら Tauri の invoke コマンドを通じて Rust 側に送信しています。ブラウザの進む・戻るイベントをリッスンして通知。history.pushState / history.replaceState をキャプチャーして SPA のナビゲーションを通知しています。また、MutationObserver を使ってタイトル変更を検知するようにしています。これにより、Cosense のようなモダンな SPA の画面遷移をトラッキングできるようになります。 注入するスクリプト - navigation-tracker.js let currentUrl = window.location.href; let currentTitle = document.title || window.location.hostname || 'Untitled'; // Function to track navigation function trackNavigation(source = 'unknown') { const url = window.location.href; const title = document.title || window.location.hostname || 'Untitled'; // Skip if no change if (url === currentUrl && title === currentTitle) return; console.log('Navigation tracked (' + source + '):', title, '→', url); // Update state currentUrl = url; currentTitle = title; // invoke Tauri command if (window.__TAURI__ && window.__TAURI__.core) { window.__TAURI__.core.invoke('track_navigation', { windowLabel: window.navigationTrackerLabel, url: url, title: title }).then(result => { console.log('Track navigation success:', result); }).catch(err => { console.error('Failed to track navigation:', err); }); } else { console.error('Tauri API not available'); } } // Track initial page load trackNavigation('initialization'); // Listen for forward/back event window.addEventListener('popstate', () => trackNavigation('popstate')); window.addEventListener('hashchange', () => trackNavigation('hashchange')); // Handle SPA navigation const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { originalPushState.apply(this, args); trackNavigation('pushState'); }; history.replaceState = function(...args) { originalReplaceState.apply(this, args); trackNavigation('replaceState'); }; // Monitor title changes (for dynamic title updates) let titleObserver; if (document.querySelector('title')) { titleObserver = new MutationObserver(() => trackNavigation('titleChange')); titleObserver.observe(document.querySelector('title'), { childList: true }); } WebView から invoke された Rust の track_navigation では、Vue 側に add-to-recent イベントを発行します。 注入スクリプトから invoke される Tauri コマンド #[tauri::command] async fn track_navigation(app: tauri::AppHandle, window_label: String, url: String, title: String) -> Result<(), String> { println!("Navigation tracked: {} -> {} ({})", window_label, url, title); // Emit event to main window for history tracking app.emit("add-to-recent", NavigationEvent { window_label, url, title, }).map_err(|e| e.to_string())?; Ok(()) } Vue 側では add-to-recent イベントを受けてリストを更新し、重複を排除するなどの処理をしてローカルストレージに書き込みます。 Vue 側の処理 // Listen for navigation events from WebView windows navigationUnlisten = await listen('add-to-recent', (event: any) => { const { window_label, url, title } = event.payload; addToRecent({ id: `${window_label}-${Date.now()}`, title: title || new URL(url).hostname, url, lastAccessed: new Date() }); console.log(`Navigation tracked: ${title} (${url})`); }); // Recent windows functions const addToRecent = (window: RecentWindow) => { recentWindows.value = recentWindows.value.filter(w => w.id !== window.id); recentWindows.value.unshift(window); saveToStorage(); }; // Data persistence const saveToStorage = () => { localStorage.setItem('sbe-recent', JSON.stringify(recentWindows.value.map(w => ({ ...w, lastAccessed: w.lastAccessed.toISOString() })))); localStorage.setItem('sbe-favorites', JSON.stringify(favorites.value)); }; --> Information 今回はフロントエンド側で LocalStorage に保存しましたが、Rust 側で JSON ファイルとしてセーブ・ロードするように実装すれば、マシンが変わっても履歴を持っていけるので便利かもしれません。 Electron がページ内遷移の細やかなイベントを提供してくれていたので、Tauri の方式はかなり面倒に感じる部分でした。Electron が Chrome を内包していることで開発者はきめ細かいイベントの捕捉を簡単にできていましたが、Tauri は OS にインストールされた WebView を使用しているのでそこまで WebView 実装に入り込んだイベントの提供はできないようです。そこで、initialization_script スクリプトを注入するという、ややハッキーなやり方が必要でした。 これは Tauri と WebView が疎結合であるためであり、このおかげで Tauri のアプリは軽量で省メモリになっているとも言えます。 Cosense ページ一覧画面のための API 呼び出しと JSON Parse # Cosense プロジェクトのページ一覧を Vue で作成しタブ内で表示します。このためには Cosense の API で該当するプロジェクトのページリストを取得する必要があります。sbe では、およそ以下のような感じでメインプロセス側で Cosense API を使用してページ一覧を取得しています。 Electron での API 呼び出し - main.mjs async function fetchPageInfo(url) { const sid = await getSid(); const res = await fetch(url, { headers: { cookie: sid } }); const data = await res.json(); return data; } async function getSid() { const cookies = await session.defaultSession.cookies.get({ name: 'connect.sid' }); return cookies[0].value; } プライベートな Cosense プロジェクトからも取得できるよう、Cookie をセッションから取得して、リクエストヘッダーに埋め込んでいます。 Tauri でもデータのフェッチは Rust 側でやるのが推奨です。特に API キーなどはフロントエンドに晒さない方がよいでしょう。 Rust 側で API 呼び出しを実装するので、Electron のメインプロセス(の JavaScript) ではするっと実装できていたレスポンスの処理はやや面倒になります。API のレスポンスを解析して以下のように型情報を定義しました。 Rust でのレスポンス型定義 // API Response #[derive(Serialize, Deserialize)] struct ScrapboxPagesResponse { #[serde(rename = "projectName")] project_name: String, skip: i32, limit: i32, count: i32, pages: Vec<ScrapboxPage>, } // Cosense page #[derive(Serialize, Deserialize, Clone)] struct ScrapboxPage { id: String, title: String, image: Option<String>, descriptions: Vec<String>, #[serde(rename = "lastUpdateUser")] last_update_user: Option<ScrapboxUser>, // 中略 #[serde(rename = "charsCount")] chars_count: Option<i32>, helpfeels: Option<Vec<String>>, } // Cosense user #[derive(Serialize, Deserialize, Clone)] struct ScrapboxUser { id: String, } Cosense API を呼び出す fetch_scrapbox_pages コマンドです。Cosense API のページングのためのパラメータを処理しているため少し長ったらしくなっていますが、 cookies_for_url メソッドでウィンドウから Cookie を取得し、ヘッダーに埋め込むところは Electron と同様の流れです。上記で定義した ScrapboxPagesResponse に API のレスポンスを格納しています。 Rust での Cosense API 呼び出し // Command to fetch Scrapbox pages with authentication (supports both public and private projects) #[tauri::command] async fn fetch_scrapbox_pages( app: tauri::AppHandle, project: String, skip: Option<i32>, limit: Option<i32>, sort: Option<String> ) -> Result<ScrapboxPagesResponse, String> { let skip = skip.unwrap_or(0); let limit = limit.unwrap_or(20); let sort = sort.unwrap_or_else(|| "updated".to_string()); let api_url = format!( "https://scrapbox.io/api/pages/{}?skip={}&limit={}&sort={}", project, skip, limit, sort ); let scrapbox_url = Url::parse("https://scrapbox.io").map_err(|e| format!("Invalid URL: {}", e))?; // Try to get cookies from main window's webview let cookies = if let Some(main_window) = app.get_webview_window("main") { main_window.cookies_for_url(scrapbox_url.clone()) .map_err(|e| format!("Failed to get cookies: {}", e))? }; let client = reqwest::Client::new(); let mut request_builder = client.get(&api_url); // Add cookies if available if !cookies.is_empty() { let cookie_header = build_cookie_header(cookies); println!("Using cookies for authentication: {} cookies", cookie_header.matches(';').count() + 1); request_builder = request_builder.header("Cookie", cookie_header); } let response = request_builder .send() .await .map_err(|e| format!("Failed to fetch pages: {}", e))?; if !response.status().is_success() { return Err(format!("API request failed with status: {} - This might be a private project requiring authentication", response.status())); } let pages_data: ScrapboxPagesResponse = response .json() .await .map_err(|e| format!("Failed to parse JSON: {}", e))?; Ok(pages_data) } Vue 側では Rust の fetch_scrapbox_pages を invoke して取得したリストを表示します。 Vue 側の処理 // Scrapbox pages functions const fetchScrapboxPages = async () => { scrapboxLoading.value = true; scrapboxError.value = ''; try { const result = await invoke('fetch_scrapbox_pages', { project: scrapboxProject.value, skip: scrapboxSkip.value, limit: scrapboxLimit.value, sort: scrapboxSort.value }) as { pages: ScrapboxPage[], count: number, skip: number }; scrapboxPages.value = result.pages; console.log(`Fetched ${result.pages.length} pages from ${scrapboxProject.value}`); } catch (error) { console.error('Failed to fetch Scrapbox pages:', error); scrapboxError.value = `ページの取得に失敗しました: ${error}`; } finally { scrapboxLoading.value = false; } }; Electron はメインプロセスも JavaScript で書けるので JSON の処理は楽でした。Tauri(Rust) では実行時ではなくコンパイル時のエラー検出など型安全性によるメリットもありますし、TypeScript でも同様です。大規模な開発では、この辺はコードジェネレータの仕事なんだと思います。 コンテキストメニューのハンドリング # WebView ウィンドウに表示している Cosense ページをお気に入りに追加するための実装を行います。WebView 上でコンテキストメニューを表示して追加してもらうのが自然でしょう。 以前の記事 では、SPA をアプリ化していたので Tauri の JavaScript API で簡単にコンテキストメニューを実装していました。今回のように WebView に Web サイトを表示する場合、コンテキストメニューの処理はやはりスクリプトを注入する必要があります。Tauri API によるコンテキストメニューのコードを注入してもいいのですが、今回は DOM 操作でコンテキストメニューを追加しました。Tauri API で追加するコンテキストメニューは OS ネイティブなものなので、WebView で表示しているサイトのルックアンドフィールに合わせたい場合は、DOM 操作で近い雰囲気のメニューを作るのも選択肢です。 WebView に注入するコンテキストメニュー用スクリプト function showContextMenu(x, y) { // Remove existing context menu if any const existingMenu = document.getElementById('tauri-context-menu'); if (existingMenu) { existingMenu.remove(); } // Create context menu const menu = document.createElement('div'); menu.id = 'tauri-context-menu'; menu.style.cssText = ` position: fixed; left: ${x}px; top: ${y}px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 10000; min-width: 180px; font-size: 14px; `; // Add menu item const menuItem = document.createElement('div'); menuItem.textContent = '⭐ お気に入りに追加'; menuItem.style.cssText = ` padding: 8px 16px; cursor: pointer; border-radius: 4px; transition: background-color 0.2s; `; menuItem.addEventListener('click', () => { addToFavorites(); menu.remove(); }); menu.appendChild(menuItem); document.body.appendChild(menu); document.addEventListener('click', function removeMenu() { menu.remove(); document.removeEventListener('click', removeMenu); }); } このスクリプトを先ほどの、navigation-tracker.js と同様 WebView に注入します。 コンテキストメニューのクリックで addToFavorites 関数を呼び出しており、この中で、 add_to_favorites_from_webview を invoke しています。Rust 側で add_to_favorites_from_webview コマンドが実行され、Vue 側に add-to-favorites イベントが発行されます。 コンテキストメニューから呼び出される Tauri コマンド - lib.rs // Command to add to favorites from WebView #[tauri::command] async fn add_to_favorites_from_webview(app: tauri::AppHandle, url: String, title: String) -> Result<(), String> { // Emit event to main window to add to favorites app.emit("add-to-favorites", FavoriteEvent { url, title, }).map_err(|e| e.to_string())?; Ok(()) } Vue側では、Rust から送信されたイベントを元にお気に入り追加の処理を行います。 Vue 側の処理 const addFavoriteFromWebView = async (url: string, title: string) => { try { // Check if already exists const existingFavorite = favorites.value.find(f => f.url === url); if (existingFavorite) { errorMessage.value = "すでにお気に入りに登録されています"; setTimeout(() => { errorMessage.value = ""; }, 2000); return; } const favorite: Favorite = { id: `fav-${Date.now()}`, title, url }; favorites.value.unshift(favorite); saveToStorage(); errorMessage.value = `お気に入りに追加しました: ${title}`; setTimeout(() => { errorMessage.value = ""; }, 3000); } catch (error) { console.error('Failed to add favorite from WebView:', error); errorMessage.value = `お気に入りの追加に失敗しました: ${error}`; } }; GitHub Actions ワークフローでプラットフォーム毎のインストーラーを生成 # 一通り動作する Tauri 版の Cosense アプリができたので、macOS や Windows 向けのインストーラを CI で作成するようにしてみます。 Tauri は OS の WebView を使用するため、クロスコンパイルはできません。OS ごとにビルド環境を用意する必要があります。Electron でも OS 毎の Chrome を同梱させるため、OS ごとのビルド環境が必要になるのでそこは変わりません。 GitHub Actions の Strategy Matrix を使って、macOS と Windows のインストーラを作成して成果物として保存するワークフローを定義しました。 .github/workflows/build-installers.yml name: Build Installers on: workflow_dispatch: jobs: build: strategy: matrix: include: - os: macos-latest name: macos-installer path: | src-tauri/target/release/bundle/dmg/*.dmg src-tauri/target/release/bundle/macos/*.app - os: windows-latest name: windows-installer path: | src-tauri/target/release/bundle/msi/*.msi src-tauri/target/release/bundle/nsis/*.exe runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Setup Rust uses: dtolnay/rust-toolchain@stable - name: Install dependencies run: npm install - name: Build Tauri app run: npm run tauri build - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} path: ${{ matrix.path }} retention-days: 30 このワークフローを実行して、生成された Tauri のアプリのインストーラは 5-7MB 程度、インストールされるバイナリは 3-4MB 程度です。 sbe のインストーラは 100MB 前後、macOS のユニバーサルインストーラは200MB近くあります。 Tauri アプリのフットプリントの軽さは魅力的ですね。起動が速くてアプリのレスポンスも軽快です。 ソースコードのリポジトリ # 今回の PoC の結果は以下のリポジトリに置いています。 https://github.com/kondoumh/sbe-tauri-poc Copilot に README を書いてもらったので表現がやや大袈裟になってしまっている点はご了承ください😅。 さいごに # 以上、Electron のアプリを Tauri 2.0 に移植してみる PoC のご紹介でした。今回の題材だと Electron の機能性や利便性が逆に強調される感じでしたが、軽量で高速なバイナリが生成される点や、Rust/Tauri のエコシステム、型安全性による開発体験は魅力ですね。 --> Information Tauri では、.NET の Blazor もサポートされています。 https://v2.tauri.app/ja/start/create-project/#%E6%96%B0%E3%81%97%E3%81%84%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B Blazor については昨年のアドベントカレンダーで紹介されています。 /blogs/2024/12/20/asp-dotnet-core-blazor/ Tauri 2.0 では OS の WebView を利用していますが、Servo ベースのクロスプラットフォームな WebView を開発するプロジェクト Verso があります。 NLnet; Servo improvements for Tauri Verso により 各 OS の WebView 間の差異が吸収され、主要なデスクトップおよびモバイルプラットフォームで一貫性のある体験がもたらされます。今回面倒に感じた Navigation 用の API なども利用しやすくなるかもしれません。 将来 Tauri に Verso プロジェクトの成果が取り込まれれば WebView を腹持ちする構造になるため、バイナリサイズは大きくなるでしょう。そのため、従来の OS の WebView と切り替えるようなオプションが提供されるかもしれませんね。
本記事は、以下イベントで講演した内容の文字起こし版です。 超実践 AI駆動開発 ~生成AIを活用したシステム開発の効率化・品質向上の最前線~ --> Information 2025年11月17日より、Q Developer CLIは正式名称「Kiro CLI」となりました。本記事では執筆時点の名称「Q Developer CLI」で説明していますが、コマンドや機能は同じです。今後のアップデートで q コマンドが kiro コマンドに変更される可能性があります。 目次 # はじめに 背景知識:なぜSDD+TDDなのか 環境構築:最小限の準備で始める 実践①:仕様策定 - specify → clarify の反復 実践②:実装 - TDDサイクルの実践 実践③:デプロイ - 自然言語だけでAWS構築 よくある質問とトラブルシューティング まとめと参考資料 はじめに # 対象読者 # 本記事は以下のような方を対象としています。 生成AIを使った開発に興味がある開発者 AIコード生成で「意図しない実装」に困った経験がある方 品質を保ちながらAIを活用したい方 Amazon Q Developer(Kiro CLI)やSpec Kitを試したい方 前提知識 # 以下の基礎知識があることを前提としています。 Git、GitHubの基本操作 Docker、Docker Composeの基本概念 Java/Spring Boot、React/TypeScriptの基礎(サンプルプロジェクトで使用) AWSの基本知識(デプロイセクション) 本記事で得られること # 本記事を読むことで、以下ができるようになります。 環境構築からデプロイまでの工程を理解できる コピペで動く実践的なプロンプト例を入手できる AI駆動開発における品質確保の方法を学べる 実際に動くサンプルプロジェクトを手に入れられる 背景知識:なぜSDD+TDDなのか # 生成AI駆動開発の現状 # 2022年末のChatGPT登場以降、AIを活用した開発は急速に普及しました。現在、GitHub Copilot、Cursor、Claude Code、Windsurf、Amazon Q Developer [1] 、Kiroなど、多くのコーディングアシスタントが利用可能です。 AIの進化は「モデル中心(GPT時代)」から「エージェントネットワーク中心」へと移行しつつあり、単なるコーディング支援から自律実行型エージェントへと発展しています。 AIコード生成の「影」 # しかし、実際の開発現場では以下のような課題に直面することがあります。 1. 過剰実装 要求していない機能を勝手に追加してしまう。 例 : ログイン機能だけ依頼したのに、パスワードリセット機能まで実装される シンプルなCRUDを依頼したのに、検索機能や並び替えまで追加される 2. 仮定 要求仕様の曖昧な部分を勝手に補完し、意図しない設計になる。 例 : 「ユーザー情報を保存」→ AIが勝手にメールアドレスを必須項目にする 「データを表示」→ AIが勝手にページングを10件単位で実装する 3. 成功宣言 ビルドやテストが失敗しているのに「完了しました」と報告する。 例 : コンパイルエラーがあるのに「実装完了」 テストが落ちているのに「全てのテストが成功」と報告 4. 保守コストの増大 最近の研究 [2] [3] によると、AI支援ツールで開発スピードを上げても、保守やレビュー、品質管理コストは上昇する傾向にあります。また、経験豊富な開発者が本来の新規開発業務から外れ、保守業務に偏るパターンも観察されています。 SDD+TDDによる解決アプローチ # これらの課題を解決するために、以下2つの手法を組み合わせます。 SDD(Specification-Driven Development:仕様駆動開発) コードを書く前に、期待する動作・要件・制約を明確に仕様化し、それをもとに開発を進める手法。 効果 :明確な仕様があると、AIが曖昧さなく正確にコードを生成可能になる [4] 。 TDD(Test-Driven Development:テスト駆動開発) コードを書く前にテストを定義し、失敗を起点に修正・改善を繰り返す手法。 効果 :LLMによるコード生成にTDDの枠組みを導入すると生成成功率が向上 [5] 。テストを「仕様・制約」として明示することで、AIの生成精度が改善 [6] 。 SDDで仕様を固め、TDDで正しい振る舞いを基準化することで、AIの制御可能性を高めます。 環境構築:最小限の準備で始める # 前提条件チェックリスト # 以下がインストール済みであることを確認してください。 Windows + WSL2(Linuxでも可) Git VSCode Docker Desktop(または Docker Engine) AWS Builder ID または AWS IAM Identity Center アカウント(Q Developer用) ステップ1:GitHubリポジトリのクローン # 今回講演用に作成した公開サンプルリポジトリをクローンします。 # 作業ディレクトリに移動 cd ~/workspace # リポジトリをクローン git clone https://github.com/mamezou-tech/aidd-demo.git # ディレクトリに移動 cd aidd-demo https://github.com/mamezou-tech/aidd-demo リポジトリの内容 : backend/ : Spring Boot バックエンド(Java 17, Spring Boot 3.x) frontend/ : React フロントエンド(React 18, TypeScript, Vite) specs/ : 仕様書ドキュメント .devcontainer/ : DevContainer設定 .amazonq/ : Amazon Q Developer設定 docker-compose.yml : 開発環境構成 ステップ2:VSCodeでDevContainerを起動 # # VSCodeで開く code . VSCodeが起動したら、以下の手順を実行します。 左下の緑色のボタン(リモートエクスプローラー)をクリック 「Reopen in Container」を選択 初回は数十分かかります(Dockerイメージのビルド) コンテナ内のターミナルが自動的に開く DevContainerに含まれるもの : Node.js、Java 17、Gradle AWS CLI Q Developer CLI 1.19.7 各種開発ツール ステップ3:Q Developer CLI(Kiro CLI)のログイン # Free プランの場合(AWS Builder ID) # ログインコマンド q login # ブラウザが開くので、AWS Builder IDでログイン # ログイン完了後、ターミナルに戻る Pro プランの場合(IAM Identity Center) [7] # ログインコマンド q login # プロンプトに従ってIAM Identity Centerの情報を入力 # Start URL: https://[your-domain].awsapps.com/start # Region: ap-northeast-1 など # ブラウザが開くので、認証を完了 対話モード開始 : q chat --> Information 2025年11月17日以降、Q Developer CLIは正式名称「Kiro CLI」となりました。本記事では旧名称「Q Developer CLI」で説明していますが、機能は同じです。今後のアップデートで q コマンドが kiro コマンドに変更される可能性があります。 ステップ4:アプリケーションの起動と動作確認 # 1. データベースとバックエンドの起動 Dev Container内のターミナルで以下を実行します。 docker compose up -d このコマンドで以下が起動します。 MySQLデータベース(ポート3306) Spring Bootアプリケーション(ポート8080) コンテナが起動していることを確認します。 docker compose ps # 期待される出力: # mysqldbとappコンテナのSTATUSがUp (healthy) 2. フロントエンドの起動 以下を実行します。 cd frontend npm run dev ※依存パッケージはDev Container起動時に自動インストールされます。 フロントエンドは http://localhost:3000 で起動します。 動作確認 : ブラウザで以下にアクセスします。 フロントエンド: http://localhost:3000 バックエンドAPI: http://localhost:8080/api/health トラブルシューティング(環境構築) # Q1: DevContainerが起動しない 原因 : Dockerが起動していない、またはリソース不足。 解決策 : # Dockerが起動しているか確認 docker ps # リソース設定を確認(Docker Desktopの場合、Settings > Resources) Q2: q コマンドが見つからない 原因 : DevContainerのビルドが不完全。 解決策 : # コンテナを再ビルド # VSCodeで Ctrl+Shift+P → "Dev Containers: Rebuild Container" # または、コマンドラインから docker compose down docker compose up -d --build Q3: ポート3000や8080が既に使用されている 原因 : 他のアプリケーションがポートを使用中。 解決策 : # 使用中のプロセスを確認 lsof -i :3000 lsof -i :8080 # プロセスを終了するか、docker-compose.ymlのポートを変更 # ports: # - "3001:3000" # 3000 → 3001に変更 実践①:仕様策定 - specify → clarify の反復 # Spec Kitコマンド一覧 # Spec Kitは以下のコマンドを提供します。 コマンド 説明 主な用途 @speckit.constitution プロジェクト原則を作成・更新 TDDルールなどの定義 @speckit.specify 機能の要件・ユーザーストーリーを定義 仕様書作成 @speckit.clarify 曖昧な仕様を明確化 仕様の精緻化 @speckit.plan 技術的な実装計画を作成 アーキテクチャ設計 @speckit.analyze 仕様・計画・タスク間の整合性分析 矛盾チェック @speckit.tasks 実装タスク一覧を生成 タスク分解 @speckit.implement 計画とタスクに従って実装 コード生成 @speckit.checklist 要件チェックリスト生成 進捗管理 --> Caution Q Developer CLIでは、スラッシュ( / )ではなくアットマーク( @ )でプロンプトを呼び出します。引数を正しく渡すため、コマンドの先頭に引用符( ' )を付けます [8] 。 対話型セッションの開始 # まず、Q Developer CLIの対話型セッションを開始します。以降のコマンドはすべてこのセッション内で実行します。 q chat セッションが開始されると、プロンプトが表示され、コマンドを入力できるようになります。 ステップ1:プロジェクト原則の確認 # プロジェクトにTDDなどの原則が定義されているか確認します。 追加が必要な場合は、以下のコマンドで追加が可能です。 '@speckit.constitution <プロジェクト原則に追加したい内容>' 記載例 : # Project Constitution ## Test-First Imperative コードの前に必ずテストを書く。 - ユニットテストを先に作成 - テストが失敗すること(Red phase)を確認してから実装 - 非交渉事項として厳格に適用 ## Library-First Principle すべての機能は独立したライブラリとして実装する。 ## Simplicity Gate 過度なエンジニアリングを防ぐ。 - 初期実装は最大3プロジェクトまで 公開リポジトリのconstitution.mdには、Spec Kitで提示されている6つの原則を記述しています。 ステップ2:仕様を策定する(@speckit.specify) # 初回のspecify 社員検索システムMVPの仕様を作成します。 プロンプト例 : '@speckit.specify 社員検索システム(MVP)の仕様を作成してください。 【目的】 社員情報を一元管理し、人事が「誰がどのスキルを持ち、どの組織に所属しているか」をすばやく把握できるようにすること。 【主な利用者】 - 人事:全社の社員・スキル・組織情報を俯瞰し、配置検討や採用計画のインプットに利用する。 【想定ユースケース】 - 人事が、特定スキル(例:Java、AWS)を持つ社員を検索し、部署横断で候補者リストを作成する。 - 顔写真付きの社員一覧・詳細画面で、人物を視覚的に識別できる。 【MVPで提供したい機能範囲】 - ログイン機能(シンプルな認証) - 社員情報の登録・閲覧・検索 - 基本属性(氏名、社員ID、所属組織、役職、雇用区分) - 顔写真の登録・表示 - スキル情報の閲覧 - スキルマスタの管理 - 社員ごとの保有スキルの紐づけ - 組織の階層管理(親子関係のみのシンプルなツリー構造) 【MVP対象外】 - 詳細な権限管理 - 監査ログ - 高度なスキル分析 - 組織改編の履歴管理 - 外部システム連携 【アウトプットの期待】 - ユースケース一覧と簡単なフロー - 画面・APIの概要 - データモデルの概要 - 非機能要件(MVP に必要な最低限) - MVP対象外機能の明示 ' ポイント : 目的を明確に記述する 利用者のペルソナを定義する MVPの範囲を明確にする(特に「対象外」を明示) 曖昧な表現を避ける 出力例 : AIが spec.md ファイルを生成します。内容には以下のような内容が含まれます。 # 社員検索システムMVP 仕様書 ## 1. 概要 ... ## 2. ユースケース ### UC-001: ログイン - アクター: 人事担当者 - 前提条件: ユーザーアカウントが登録されている - 基本フロー: 1. ユーザーがログイン画面でユーザー名とパスワードを入力 2. システムが認証を実行 3. 認証成功時、ホーム画面にリダイレクト ... ## 3. 画面設計 ### 3.1 ログイン画面 - 入力項目: ユーザー名、パスワード - ボタン: ログイン ... ## 4. データモデル ### 4.1 社員(Employee) - employee_id (PK): 社員ID - name: 氏名 - organization_id (FK): 所属組織 - photo_url: 顔写真URL ... ステップ3:曖昧な点を明確化する(@speckit.clarify) # 生成された仕様書には、まだ曖昧な点が残っています。clarifyコマンドで明確化します。 プロンプト例 : '@speckit.clarify 社員検索システムMVP仕様書に対して、以下の点について曖昧な点を洗い出し、質問リストを作成してください: - ログイン/認証方式(MVPでの具体的な前提) - スキル検索と表示方法 - 顔写真の登録方法 - 一覧画面におけるページング/ソートの扱い ' 出力例 : AIが以下のような質問リストを生成します。 # 仕様の曖昧点に関する質問 ## ログイン/認証方式 Q1: パスワードの暗号化方式は何を使用しますか?(bcrypt、PBKDF2など) Q2: セッション管理はどうしますか?(Cookie、JWT、サーバーサイドセッションなど) Q3: ログアウト機能は必要ですか? Q4: パスワードリセット機能はMVPに含まれますか? ## スキル検索と表示方法 Q5: スキル検索は完全一致ですか、部分一致ですか? Q6: 複数スキルのAND検索/OR検索は必要ですか? Q7: スキルレベル(初級/中級/上級など)の扱いはどうしますか? ## 顔写真の登録方法 Q8: 顔写真のファイルサイズ上限は? Q9: 許可する画像形式は?(JPEG、PNG、GIFなど) Q10: 顔写真がない場合、デフォルト画像を表示しますか? ## ページング/ソート Q11: 一覧画面の1ページあたりの表示件数は? Q12: ソート項目は何ですか?(氏名、社員ID、組織など) Q13: デフォルトのソート順は? ステップ4:質問に回答して仕様を修正(@speckit.specify) # AIからの質問に回答し、仕様を修正します。 プロンプト例 : '@speckit.specify 社員検索システムMVP仕様書を、以下の回答に基づいて更新してください。 【ログイン/認証方式】 A1: bcryptを使用 A2: JWTトークンをCookieに保存 A3: ログアウト機能は必要 A4: パスワードリセット機能はMVP対象外 【スキル検索と表示方法】 A5: 部分一致(前方一致) A6: 複数スキルのAND検索のみ A7: スキルレベルは3段階(初級/中級/上級) 【顔写真の登録方法】 A8: 5MB以下 A9: JPEG、PNGのみ A10: デフォルトアバター画像を表示 【ページング/ソート】 A11: 20件/ページ A12: 氏名、社員ID、所属組織でソート可能 A13: 氏名の昇順 ' ステップ5:反復 # specify → clarify → specify のサイクルを繰り返すと、曖昧な点がかなり減少します。 反復のポイント : 仕様が曖昧だと困る点を指定すると効果的 AIに推奨案を提示してもらうことで高速化できる 高速化の例 : '@speckit.clarify 社員情報の登録と更新について、曖昧な点を洗い出し、 あなたの推奨案も合わせて提示してください。 ' 成果物:spec.md # 最終的に以下のような完成度の高い仕様書が得られます。 ユースケースが具体的に記述されている 画面項目・API仕様が明確 データモデルが詳細に定義されている 曖昧な点が解消されている MVP対象外が明確 実践②:実装 - TDDサイクルの実践 # ステップ1:実装計画を作成(@speckit.plan) # @speckit.plan 出力例 : # 実装計画 ## 技術スタック - Backend: Java 17, Spring Boot 3.2.0, Spring Security, Spring Data JPA - Frontend: React 18, TypeScript, Tailwind CSS - Database: MySQL 8.0 - Test: JUnit 5, Testcontainers, Playwright ## アーキテクチャ ### バックエンド - レイヤードアーキテクチャ - Controller層: REST APIエンドポイント - Service層: ビジネスロジック - Repository層: データアクセス ### フロントエンド - コンポーネントベースアーキテクチャ - React Router for SPA - Context API for 状態管理 ## セキュリティ - JWT認証 - bcryptによるパスワードハッシュ化 - CORS設定 この内容が plan.md として保存されます。 ステップ2:タスク一覧を生成(@speckit.tasks) # '@speckit.tasks タレントマネジメントシステムMVPを、2〜3日の粒度の実装タスクに分解してください。 ' 出力例 : # 実装タスク一覧 ## Phase 1: 基盤構築 - Task 1.1: プロジェクト構造のセットアップ - Task 1.2: データベーススキーマの作成 ## Phase 2: 認証機能 - Task 2.1: ユーザーエンティティとRepository - Red: UserRepositoryTestを作成し、失敗を確認 - Green: User.java, UserRepository.javaを実装し、テスト成功 - Refactor: コードの整理 - Task 2.2: JWT認証の実装 - Red: JwtUtilTestを作成し、失敗を確認 - Green: JwtUtil.java, JwtAuthenticationFilter.javaを実装 - Refactor: コードの整理 - Task 2.3: ログインAPIの実装 - Red: AuthControllerTestを作成し、失敗を確認 - Green: AuthController.java, AuthService.javaを実装 - Refactor: コードの整理 ## Phase 3: 社員管理機能 - Task 3.1: 社員エンティティとRepository - Task 3.2: 社員登録API - Task 3.3: 社員検索API - Task 3.4: 社員詳細API ... この内容が tasks.md として保存されます。 ステップ3:実装(@speckit.implement) # いよいよ実装です。Spec Kitのテストファースト原則に基づき、TDDサイクルを使って実装を進めます。 Task 2.1の実装例:ユーザーエンティティとRepository プロンプト例 : '@speckit.implement Task 2.1' AIの動作例 : まずテストを生成 (Red phase) UserRepositoryTest.java : package com.example.demo.repository; import com.example.demo.entity.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @Testcontainers @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class UserRepositoryTest { @Container static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb"); @Autowired private UserRepository userRepository; @Test void ユーザー名でユーザーを検索できる() { // Arrange User user = new User(); user.setUsername("testuser"); user.setPassword("hashedPassword"); userRepository.save(user); // Act Optional<User> found = userRepository.findByUsername("testuser"); // Assert assertThat(found).isPresent(); assertThat(found.get().getUsername()).isEqualTo("testuser"); } @Test void 存在しないユーザー名で検索すると空が返る() { // Act Optional<User> found = userRepository.findByUsername("nonexistent"); // Assert assertThat(found).isEmpty(); } } テストを実行して失敗を確認 # AIが自動的に実行 ./gradlew test --tests UserRepositoryTest # 結果:FAILED(User.javaやUserRepository.javaがまだ存在しないため) 実装を生成 (Green phase) User.java : package com.example.demo.entity; import jakarta.persistence.*; import lombok.Data; @Entity @Table(name = "users") @Data public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false) private String password; @Column(length = 100) private String email; } UserRepository.java : package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); } テストを実行して成功を確認 ./gradlew test --tests UserRepositoryTest # 結果:PASSED リファクタリング (Refactor phase) テストが通った状態で、コードを改善します。AIが以下のようなリファクタリングを実行します。 変数名・メソッド名の改善 重複コードの削除 可読性の向上 テストが通り続けることを確認しながら、安全にコードを改善します。 TDDサイクルの反復ポイント # Red → Green → Refactor のサイクル : Red(失敗するテストを書く) 期待する振る舞いをテストで表現 実装がないため、テストは失敗する Green(テストが通る最小限の実装) テストを通過させるコードを書く この段階では「美しさ」より「動作」を優先 Refactor(リファクタリング) テストが通った状態で、コードを改善 重複を削除、可読性を向上 実践③:デプロイ - 自然言語だけでAWS構築 # 前提条件 # AWS CLIがインストール済み(DevContainerに含まれる) AWSプロファイル(認証情報)が設定済み EC2を起動する権限があるIAMユーザー/ロール 今回のデプロイはテスト用途 のため、以下は簡略化しています。 HTTPSではなくHTTP 単一EC2インスタンス ロードバランサーなし データベースはコンテナ内 ステップ1:EC2インスタンスの作成 # プロンプト例 : Amazon Linux 2023、t3.smallでEC2インスタンスを作成してください。 セキュリティグループは、22番ポート(SSH)と3000番ポート(アプリ)を 自分のIPアドレスからのみ許可してください。 キーペアは "aidd-demo-key" という名前で作成してください。 AIの動作例 : AIが以下を自動実行します。 現在のIPアドレスを取得 セキュリティグループを作成(SSH: 22、App: 3000) キーペアを作成 EC2インスタンスを起動 インスタンスIDとパブリックIPを出力 ステップ2:DockerとDocker Composeのインストール # プロンプト例 : EC2インスタンス((パブリックIP))にSSHで接続し、 Docker と Docker Compose をインストールしてください。 ユーザーをdockerグループに追加し、再ログインせずに使えるようにしてください。 AIの動作例 : AIが以下を自動実行します。 EC2インスタンスにSSH接続 システムパッケージを更新 Dockerをインストール・起動・自動起動設定 ユーザーをdockerグループに追加 Docker Composeをインストール インストール完了を確認 ステップ3:アプリ一式をEC2へ転送 # プロンプト例 : カレントディレクトリのdocker-compose.ymlと、 backend、frontend、dbディレクトリを、 EC2インスタンス((パブリックIP))の /home/ec2-user/aidd-demo へ転送してください。 AIの動作例 : AIが以下を自動実行します。 EC2インスタンス上に転送先ディレクトリを作成 docker-compose.yml、backend、frontend、dbディレクトリをSCPで転送 ステップ4:Docker Composeでアプリ起動 # プロンプト例 : EC2インスタンスで、転送したディレクトリに移動し、 docker compose up -d を実行してください。 全コンテナが Up(healthy) で起動していることを確認してください。 AIの動作例 : AIが以下を自動実行します。 EC2インスタンスにSSH接続 転送したディレクトリに移動 docker compose up -d でコンテナを起動 全コンテナのステータスを確認し、Up (healthy) であることを報告 ステップ5:アプリ動作確認 # ブラウザでアクセス : http://(パブリックIP):3000 API疎通確認 : curl http://(パブリックIP):8080/api/health # 出力例: # {"status":"UP"} ログイン確認 : ブラウザでログイン画面にアクセスし、以下のテストユーザー情報でログインします。 ID: test@example.com PW: aiddTest ログイン後TOP画面が表示され、そのリンクから社員検索システムに遷移します。 ※登録データおよび顔写真はすべてAI生成によるものです。 CORS問題の修正(必要に応じて) # もしCORSエラーが発生した場合は以下を実行します。 プロンプト例 : バックエンドのCORS設定を修正してください。 フロントエンドのオリジン(http://(パブリックIP):3000)からのリクエストを許可してください。 修正後、EC2上でアプリを再起動してください。 まとめと参考資料 # 本記事のポイント # 本記事では、Amazon Q Developer × Spec Kitを使ったAI駆動開発の全工程を解説しました。 環境構築 : GitHubリポジトリのクローンだけで開始可能 DevContainerで依存関係を自動解決 仕様策定 : specify → clarify の反復で仕様を精緻化 曖昧さを排除することがAI活用の鍵 実装 : TDDサイクル(Red → Green → Refactor)の実践 テストファーストの原則でAIの生成精度が向上 デプロイ : 自然言語でAWSリソースを構築 重要な学び # 人間の役割は不可欠 :上流工程と品質確保は現段階では人間が担う 仕様とテストが資産 :高品質な成果物は次のAI駆動開発で再利用可能 Amazon Web Services. Amazon Q Developer . ↩︎ Xu et al. AI-assisted Programming and Maintenance Burden . arXiv, 2025. ↩︎ Amasanti & Jahić. The Impact of Generative AI-Generated Solutions on Software Maintainability . arXiv, 2025. ↩︎ GitHub. Spec-driven development with AI: Get started with a new open source toolkit . 2024. ↩︎ Mathews et al. Test-Driven Development for Code Generation . arXiv, 2024. ↩︎ Chen et al. TENET: Leveraging Tests Beyond Validation for Code Generation . arXiv, 2025. ↩︎ Classmethod. Amazon Q Developer Pro をメンバーアカウントでサブスクライブ利用してみた . 2024. ↩︎ Ahanoff. Amazon Q Developer を使用した Spec Kit:発見事項と癖 . 2024. ↩︎
はじめに # 前回 では、 仕様から実装・テスト生成まで の流れを体験し、AIがどのようにソフトウェア開発を支援できるかを確認しました。 今回(Day 3)は、その延長として 品質保証 に焦点を当てます。 AIによるコード生成が一般化する中で、重要なのは「 どう品質を保証し続けるか 」。 Q Developerの レビュー支援機能とメトリクス可視化 を使いながら、 AIと人間のハイブリッドによる品質保証サイクル を具体的に見ていきましょう。 1. どこを見るか ― 品質観点の棚卸し # まずは「レビューで何を見るのか」を整理します。 これはQ Developerが自動チェックする項目にも関わる部分です。 観点 チェック項目 目的 例外処理 例外の握り潰し・再スロー方針の一貫性 障害解析性を確保 命名規約 クラス名・変数名・ディレクトリ構造の整合性 読解コスト削減 依存関係 循環依存・層違反の有無 構造的健全性の担保 ログ設計 粒度・相関ID・監査性 運用時トレーサビリティの向上 テストの意図 境界条件・オラクルの有無 妥当性と網羅性の保証 2. Q Developerでレビューを回す # コードレビューをAIに依頼する場合、次のようなコマンドで開始します。 サンプルは前回作成したToDoアプリを使用します。 Q Developerを起動します。 # 対話モードを起動 q レビュー観点をレビューのカテゴリにまとめ、カテゴリ単位でQ Developerに指示を出します。 レビュー観点 レビュー・カテゴリ 説明 例外処理 設計/可読性 例外方針の整合性・例外伝播の扱いは設計扱い 命名規約 可読性 命名揺れ・責務不一致は可読性として指摘される 依存関係 設計 層違反・循環依存などアーキテクチャ的問題 ログ設計 可読性/設計 ログ粒度・相関ID は設計・運用設計の観点 テストの意図 設計/品質(テスト品質) AI では「テストの妥当性」=設計・品質問題として扱われる また、上記表のカテゴリにはありませんが、全体横断的にセキュリティは重要な観点のため、Q Developerへの指示に追加します。 起動後のプロンプトで以下のように入力します。 ※ツール実行提案が出た場合、セッション中のみ許可する場合は y 、以後も許可する場合は t を選択します。 main.py と test_api.py のコードをレビューしてください。 セキュリティ、可読性、設計の観点で指摘を分類し、修正提案も示してください。 応答: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/main.py, all lines ✓ Successfully read 1684 bytes from /home/ubuntu/todo-fastapi/main.py ⋮ ● Completed in 0.0s 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/test_api.py, all lines ✓ Successfully read 3155 bytes from /home/ubuntu/todo-fastapi/test_api.py ⋮ ● Completed in 0.0s > ## コードレビュー結果 ### 🔒 セキュリティの問題 main.py: 1. 入力値制限なし python # 問題: title/descriptionの長さ制限なし title: str # DoS攻撃の可能性 2. レート制限なし python # 問題: API呼び出し制限なし @app.post("/todos") # 大量リクエスト可能 3. CORS設定なし python # 問題: クロスオリジン制御なし app = FastAPI() # CORS未設定 test_api.py: 4. ハードコードされたURL python BASE_URL = "http://localhost:8000" # 設定ファイル化推奨 ### 📖 可読性の問題 main.py: 1. グローバル変数 python # 問題: グローバル状態 todos = [] todo_id_counter = 1 2. マジックナンバー python # 問題: ポート番号ハードコード uvicorn.run(app, host="0.0.0.0", port=8000) test_api.py: 3. 重複コード python # 問題: 同じパターンの繰り返し print(f"Create Todo: {response.status_code}") print(json.dumps(response.json(), indent=2)) ### 🏗️ 設計の問題 main.py: 1. 単一責任原則違反 python # 問題: ビジネスロジックとAPI層が混在 def create_todo(todo: TodoCreate): global todo_id_counter # データ操作がAPI層に 2. スレッドセーフティ python # 問題: 競合状態の可能性 todo_id_counter += 1 # 非原子的操作 test_api.py: 3. テスト独立性なし python # 問題: テスト間の依存関係 todo_id = test_create_todo() # 前のテスト結果に依存 ## 🔧 修正提案 ### セキュリティ改善 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/todo-fastapi/main_improved.py + 1: from fastapi import FastAPI, HTTPException + 2: from fastapi.middleware.cors import CORSMiddleware + 3: from pydantic import BaseModel, Field, validator + 4: from typing import List, Optional + 5: from datetime import datetime, timezone ~~~(中略)~~~ + 84: @app.delete("/todos/{todo_id}", status_code=204) + 85: def delete_todo(todo_id: int = Field(..., gt=0)): + 86: if not repo.delete(todo_id): + 87: raise HTTPException(status_code=404, detail="Todo not found") + 88: + 89: if __name__ == "__main__": + 90: import uvicorn + 91: uvicorn.run(app, host="127.0.0.1", port=8000) ⋮ ↳ Purpose: Create improved main.py with security enhancements Creating: /home/ubuntu/todo-fastapi/main_improved.py ⋮ ● Completed in 0.1s > ### テスト改善 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/todo-fastapi/test_improved.py + 1: import requests + 2: import json + 3: from datetime import datetime, timedelta + 4: from typing import Dict, Any + 5: ~~~(中略)~~~ + 121: # クリーンアップ + 122: self.cleanup() + 123: self.test_get_todos() + 124: + 125: if __name__ == "__main__": + 126: tester = TodoAPITester() + 127: tester.run_all_tests() ⋮ ↳ Purpose: Create improved test file with better structure Creating: /home/ubuntu/todo-fastapi/test_improved.py ⋮ ● Completed in 0.3s > ## 📋 改善点まとめ ### セキュリティ改善 • **入力値制限**: Field()でmin_length/max_length設定 • **CORS設定**: 特定オリジンのみ許可 • **スレッドセーフ**: threading.Lockで排他制御 • **バインドアドレス**: 127.0.0.1に変更(外部アクセス制限) ### 可読性改善 • **責任分離**: TodoRepositoryクラスでデータ操作を分離 • **設定外部化**: 環境変数やクラス初期化で設定管理 • **重複排除**: テストクラスで共通処理をメソッド化 ### 設計改善 • **依存性注入**: リポジトリパターン採用 • **テスト独立性**: クリーンアップ機能追加 • **エラーハンドリング**: 適切なHTTPステータスコード使用 • **型安全性**: Field()バリデーション強化 改善版では本格運用に近い品質になっています。 指示したように、AIレビュー結果では以下のように分類されて表示されました。 カテゴリ 例 対応方針 セキュリティ 未処理の例外、ハードコードされた認証情報 優先対応 可読性 命名のばらつき、コメント欠如 対応を検討 設計 クラス肥大化、循環依存 チーム内で合意形成が必要 このようにAIを相手にレビューを実施することが可能です。 ただし、AIの自動修正提案(ツール実行)は “提案”レベル です。 必ず人間が承認する運用にしましょう。 3. メトリクスで「見える化」する # レビューは定性的な活動ですが、Q Developerでは数値指標による定量的管理も可能です。 ここでは、 なぜメトリクスが必要なのか 、そして どのように活用すべきか を、より実務的な観点で詳しく説明します。 3.1 メトリクスの役割とは? # メトリクスとは、コード品質や開発プロセスを「数値」で客観的に評価する道具です。 属人的な「なんとなく良さそう」「たぶん問題ない」を排除し、以下の3つを実現します: 現状の可視化(見える化) :改善すべき箇所を定量的に把握できる 改善効果の測定 :前回との比較で良くなったかどうかがわかる 基準の共有 :チーム全員が同じ尺度で議論できる 特にAIと共存する開発では、生成コードの質にばらつきがあるため、メトリクスは「品質の安定化」に欠かせません。 3.2 主要メトリクスの詳細 # ここでは、コード品質を評価する際に特に重要となるメトリクスを整理します。 これらは単なる“数字”ではなく、 品質改善の優先順位付け や チーム内での合意形成 に欠かせない指標です。 メトリクスの目的や意味を理解しておくことで、レビュー結果をより実務的に活用できます。 指標 目的 目安 詳細説明 静的解析スコア 構文・構造の健全性 90点以上 循環依存、未使用コード、例外処理、セキュリティ問題などを総合評価。点数化することで改善の優先度を決めやすい。 テストカバレッジ 機能網羅率 80%以上 単に数値を見るのではなく、 どの関数が未カバーか まで確認するのが重要。重要ロジックは100%を目指す。 コメント率 可読性・ドキュメント性 10〜20% コメントが「多ければ良い」ではない。コードの意図や設計判断を書き残す“質”が重要。 命名一貫率 品質基準遵守度 95%以上 関数・変数の命名パターンが揃っているか。レビュー時間削減につながる。 変更影響深度 リファクタ容易性 低いほど良い 変更がどれだけ他モジュールへ影響するかを測る。深度が高い=保守コストが高いコード。 3.3 メトリクスを実際に計測する # 前回作成した todo-fastapi/ はモジュール構成が小さいため、まずは テストカバレッジ を例にします。 pytestを使用したカバレッジ計測を実施します。 指示: python main.py pytest --cov=. --cov-report=xml 応答: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: python main.py & ⋮ ↳ Purpose: Start FastAPI server in background ⋮ ● Completed in 0.1s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: sleep 3 && pytest --cov=. --cov-report=xml ⋮ ↳ Purpose: Wait for server to start and run pytest with coverage ============================= test session starts ============================== platform linux -- Python 3.12.3, pytest-9.0.1, pluggy-1.6.0 rootdir: /home/ubuntu/todo-fastapi plugins: anyio-4.11.0, cov-7.0.0 collected 8 items test_api.py ..E..... [100%] ==================================== ERRORS ==================================== ______________________ ERROR at setup of test_delete_todo ______________________ file /home/ubuntu/todo-fastapi/test_api.py, line 24 def test_delete_todo(todo_id): ~~~(中略)~~~ 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: ps aux | grep "python main.py" ⋮ ↳ Purpose: Check if server is running ubuntu 222074 0.2 0.6 231596 49596 pts/7 Sl+ 21:55 0:00 python main.py ubuntu 224348 0.0 0.0 4756 3248 pts/5 S+ 22:02 0:00 bash -c ps aux | grep "python main.py" ubuntu 224350 0.0 0.0 4100 1992 pts/5 S+ 22:02 0:00 grep python main.py ⋮ ● Completed in 0.28s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: curl -s http://localhost:8000/todos || echo "Server not responding" ⋮ ↳ Purpose: Test server connectivity [{"id":1,"title":"Test Todo","description":"Test description","priority":"high","due_date":"2025-11-17T21:55:47.625131","created_at":"2025-11-16T21:55:47.632922"},{"id":2,"title":"Minimal Todo","description":null,"priority":"low","due_date":null,"created_at":"2025-11-16T21:55:47.654090"},{"id":5,"title":"Test Todo","description":"Test description","priority":"high","due_date":"2025-11-17T22:02:54.546964","created_at":"2025-11-16T22:02:54.550647"},{"id":6,"title":"Minimal Todo","description":null,"priority":"low","due_date":null,"created_at":"2025-11-16T22:02:54.575022"}] ⋮ ● Completed in 0.16s > ✅ サーバーが起動し、テストが実行されました 結果: • **サーバー**: 正常起動中(PID 222074) • **XMLレポート**: Coverage XML written to file coverage.xml - 成功 • **テスト**: 7つ成功、1つエラー(fixture問題) • **API**: 正常動作(Todoデータが存在) 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pkill -f "python main.py" ⋮ ↳ Purpose: Stop the server ⋮ ● Completed in 0.15s > サーバーを停止しました。XMLカバレッジレポートが正常に生成されています。 これにより coverage.xml が生成されます。 3.4 coverage.xml を Q Developer に解析させる(対話) # 対話モードで、次のように依頼します: coverage.xml を読み込み、 ・関数ごとのカバレッジ ・未テスト箇所の一覧 ・優先的に追加すべきテストケースの提案 ・複雑度の高い関数ランキング を出してください。 Q Developer は内部ツール(fs_read / analyze_code)を使い、以下を返します: どの関数が未カバーか 条件分岐のテスト不足 異常系・境界テストの欠落 テスト追加の具体例(パラメータ例まで出ることが多い) 応答: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/coverage.xml, all lines ✓ Successfully read 4891 bytes from /home/ubuntu/todo-fastapi/coverage.xml ⋮ ● Completed in 0.0s ~~~(中略)~~~ 問題: サーバーが起動していたにも関わらず、main.pyのカバレッジが0%のままです。これは、テストがHTTPリクエストを送信しているだけで、pytestがmain.pyのコードを直接実行していないためです。TestClientを使用することで解決できます。 どうやら、カバレッジが正しく収集できないようです。 Q Developerは単に「できません」と言うだけでなく、解決案の提示もしてくれます。 Q Developerの進言に従って、TestClientを使用するように依頼します。 指示: TestClientを使用するように変更してください 応答: 🛠️ Using tool: fs_write ⋮ ● Path: test_api.py - 0 : import requests + 0: from fastapi.testclient import TestClient 1, 1: import pytest 2, 2: from datetime import datetime + 3: from main import app 3, 4: - 4 : BASE_URL = "http://localhost:8000" + 5: client = TestClient(app) 5, 6: 6, 7: def test_create_todo(): 7, 8: todo_data = { 8, 9: "title": "Test Todo", 9, 10: "description": "Test description", 10, 11: "priority": "high", 11, 12: "due_date": "2025-11-17T12:00:00" 12, 13: } ~~~(中略)~~~ Coverage XML written to file coverage.xml ========================= 8 passed, 1 warning in 1.17s ========================= ⋮ ● Completed in 1.557s > ✅ TestClientを使用したテストが成功しました! 結果: • **httpx**: インストール完了 • **テスト**: 8つ全て成功 • **XMLレポート**: Coverage XML written to file coverage.xml - 成功 • **実行時間**: 1.17秒 • **警告**: Pydantic V1 validator使用の警告のみ TestClientを使用することで、サーバー起動なしでAPIテストが実行でき、main.pyのコードカバレッジも測定されるようになりました カバレッジが収集されたので、再度以下のように依頼します。 coverage.xml を読み込み、 ・関数ごとのカバレッジ ・未テスト箇所の一覧 ・優先的に追加すべきテストケースの提案 ・複雑度の高い関数ランキング を出してください。 応答: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/coverage.xml, all lines ✓ Successfully read 4181 bytes from /home/ubuntu/todo-fastapi/coverage.xml ⋮ ● Completed in 0.0s > ## カバレッジ分析結果 ### 📊 全体カバレッジ • **総合カバレッジ**: 97.87% (92/94行) - 大幅改善! • **main.py**: 95.83% (46/48行) • **test_api.py**: 100% (46/46行) ### 🔍 関数ごとのカバレッジ #### main.py (95.83%カバレッジ) • create_todo() (38-48行): ✅ 完全テスト済み • get_todos() (49-51行): ✅ 完全テスト済み • delete_todo() (57-63行): ✅ 完全テスト済み • validate_due_date() (24-28行): ✅ 完全テスト済み #### test_api.py (100%カバレッジ) • 全テスト関数が実行済み ### ❌ 未テスト箇所の一覧 #### main.py (わずか2行未テスト) 1. 66行: import uvicorn 2. 67行: uvicorn.run(app, host="0.0.0.0", port=8000) これらはif __name__ == "__main__"ブロック内のサーバー起動コードで、テスト時には実行されません。 ### 🎯 優先的に追加すべきテストケース #### 低優先度(既に高カバレッジのため) 1. エッジケースの追加 • 空文字列タイトル • 極端に長いタイトル/説明 • 境界値テスト 2. エラーハンドリングの詳細テスト • 不正なJSON形式 • 型変換エラー 3. パフォーマンステスト • 大量データでの動作確認 ### 📈 複雑度の高い関数ランキング #### 1位: create_todo() (main.py 38-48行) • **複雑度**: 高 • **カバレッジ**: ✅ 100% • **理由**: グローバル変数操作、オブジェクト生成、リスト操作、カウンター管理 • **行数**: 11行 #### 2位: delete_todo() (main.py 57-63行) • **複雑度**: 中高 • **カバレッジ**: ✅ 100% • **理由**: ループ処理、条件分岐、例外処理、リスト操作 • **行数**: 7行 #### 3位: validate_due_date() (main.py 24-28行) • **複雑度**: 中 • **カバレッジ**: ✅ 100% • **理由**: 条件分岐、日時比較、例外発生 • **行数**: 5行 #### 4位: get_todos() (main.py 49-51行) • **複雑度**: 低 • **カバレッジ**: ✅ 100% • **理由**: 単純なリスト返却 • **行数**: 3行 ### 🎉 成果 TestClient導入により劇的改善: • **カバレッジ**: 35.96% → 97.87% (+61.91%) • **main.py**: 0% → 95.83% (+95.83%) • **全テスト**: 成功(8/8) • **実行時間**: 高速化(サーバー起動不要) ### 💡 推奨アクション 1. 現状維持 - 既に優秀なカバレッジ 2. コード品質向上 • Pydantic V2への移行(警告解消) • 型ヒントの追加 3. 統合テストの追加 • 複数操作の組み合わせテスト 4. ドキュメント整備 • API仕様書の作成 結論: TestClientの導入により、ほぼ完璧なテストカバレッジを達成しました。残りの2行は実行時コードのため、実質的に100%のカバレッジです。 無事にカバレッジを収集することができました。 3.5 メトリクスをどう改善に活かすか # メトリクスは“数値を見ること”が目的ではありません。 むしろ、メトリクスは 「どこから改善すれば最も効果が高いのか」 を示す“改善ナビゲーション”です。 数値が示す意味を正しく理解し、次のアクションへつなげることで、品質改善サイクルが加速します。 以下では、代表的なメトリクスからどのように改善につなげるかを、より具体的に説明します。 改善のために以下のように使います: 低カバレッジ関数 → 追加テストを作成 カバレッジが低いということは、挙動を保証するテストが不足している状態です。特にビジネスロジックやバリデーション処理など、バグが入りやすい部分は優先的にテストを追加します。 例:create_todo() が0% → POST /todos の正常系・異常系テストを追加 複雑度の高い関数 → まずは分割(リファクタ) 複雑度が高い関数は読みづらく、バグが入りやすい上にテストもしづらい傾向があります。 責務を分割したり、共通処理をメソッド化することで可読性が向上し、結果的にテストもしやすくなります。 例:create_todo() に状態管理・バリデーション・登録処理が混在 → 役割ごとに関数分離 命名揺れ → コード規約をAIに与えて揃える 命名規約のばらつきは理解コストを上げ、レビュー時間を増大させます。 Q Developer に命名規約(例:snake_case / camelCase、略語ルール)を伝えておけば、一貫性のない命名を自動で指摘させることも可能です。 また、プロジェクトに合わせて命名変更案をAIが自動生成してくれます。 変更影響深度が高い箇所 → 設計見直しを検討 「この関数を変えると他の10ファイルが壊れる」という状態は、保守性の低さを表します。 依存関係の整理、責務分離、アーキテクチャ層の明確化などを検討し、影響範囲を意図的に小さくしていきます。 また、メトリクスは改善結果を“数字で説明できる”ため、「なぜその改善が必要なのか?」をチームに説明しやすくなります。 定量化により、改善ポイントが明確になり、チーム内での合意形成が容易になります。 このように、どれだけのコードがカバーできているのかを「定量的」に見極めながら、品質を維持しつつ機能を拡張・検証していくことが可能になります。 4. メトリクス駆動のハイブリッドレビュー運用モデル # AI と人間、それぞれの強みを活かしつつ、 定量的メトリクスを軸に品質保証サイクルを回すための運用モデル です。 従来のレビューでは人の経験や勘に依存していましたが、メトリクスを導入することで「観点の統一」「効果測定」「改善の優先順位付け」が可能になり、AI との組み合わせによって品質保証を持続的に行えるようになります。 AI レビューは網羅性と速度に優れ、一方でメトリクスは改善の指針として機能します。 最終判断は人間が行い、 AI × メトリクス × 人間の判断 によって再現性と持続性のある品質保証が成立します。 まとめ # 今回の最大の成果は、 メトリクスによって品質を“定量的に”評価できるようになったこと です。 これにより、これまで感覚的・属人的だったレビュー活動が、数字を用いた客観的な改善サイクルへと進化しました。 特に以下の点が大きな前進です: 静的解析スコア、カバレッジ、複雑度、命名一貫率といった指標により、レビューの“抜け漏れ”が可視化された 数値をもとに「どこを優先して直すべきか」が判断できるようになり、改善活動の効率が向上した AIによるレビュー提案と人間の判断を組み合わせることで、品質改善プロセスを継続的に回せる基盤ができた これらにより、レビュー精度のばらつきが減り、 品質改善のPDCAがデータに基づいて回る状態 へと近づきました。 皆さまの生成AI活用の参考になれば幸いです。 img { border: 1px gray solid; }
はじめに # 前回 は、Q Developerを使って簡単なアプリケーションをインタラクティブモードで作成しました。 今回の記事では、Q Developerをさらに一歩掘り下げ、 仕様 → 実装 → テスト生成 → 修正 の一連の工程を通じて、AIがどのようにソフトウェア開発を支援するのかを確認します。 とくに焦点を当てるのは、 テスト可能な仕様(バリデーション) の考え方です。 AIが生成するテストがどこまで自動化できるのか、そして人間の開発者がどの段階で介入し、品質を保証すべきなのかを、実際のプロセスを通して検証していきます。 この記事を読むことで、次のような理解が得られるでしょう。 Q Developerが仕様からテストまで自動化できる範囲 AIによる自動テスト生成の利点と限界 テスト可能な仕様を設計する際の実務的なポイント 人が介在すべき判断ポイントと品質保証の勘どころ 使用するQ Developerのバージョンは 「 1.13.0 」 を想定しています。 それでは、AIが関わるテスト自動化を具体的に見ていきましょう。 1. シンプルなToDoアプリを作る # 1.1 Q Developer を対話モードで起動 # 作業ディレクトリを作成し、 q を起動します。 mkdir todo-fastapi && cd todo-fastapi q 起動後のプロンプトに、以下のメッセージを貼り付けます。 指示: FastAPIを使ってシンプルなToDoアプリケーション (登録・一覧・削除 API) を作成するためのPythonスクリプトを生成してください。 ファイル名は main.py と test_api.py です。 DBはメモリ、Pydanticでバリデーションを行い、 priorityは low/medium/high、 due_date は未来日チェックを含めてください。 応答: 🛠️ Using tool: fs_write ⋮ ● Path: /home/ubuntu/todo-fastapi/main.py + 1: from fastapi import FastAPI, HTTPException + 2: from pydantic import BaseModel, validator + 3: from typing import List, Optional + 4: from datetime import datetime + 5: from enum import Enum + 6: + 7: app = FastAPI() + 8: + 9: # In-memory database + 10: todos = [] ~~~(中略)~~~ + 57: @app.delete("/todos/{todo_id}") + 58: def delete_todo(todo_id: int): + 59: for i, todo in enumerate(todos): + 60: if todo.id == todo_id: + 61: del todos[i] + 62: return {"message": "Todo deleted successfully"} + 63: raise HTTPException(status_code=404, detail="Todo not found") + 64: + 65: if __name__ == "__main__": + 66: import uvicorn + 67: uvicorn.run(app, host="0.0.0.0", port=8000) ⋮ ↳ Purpose: Create FastAPI ToDo application with registration, listing, and deletion APIs Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]: > t Creating: /home/ubuntu/todo-fastapi/main.py ⋮ ● Completed in 0.0s 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/todo-fastapi/test_api.py + 1: import requests + 2: import json + 3: from datetime import datetime, timedelta + 4: + 5: BASE_URL = "http://localhost:8000" + 6: + 7: def test_create_todo(): + 8: todo_data = { + 9: "title": "Test Todo", + 10: "description": "Test description", ~~~(中略)~~~ + 50: + 51: # Test invalid due date + 52: test_invalid_due_date() + 53: print() + 54: + 55: # Test deleting a todo + 56: test_delete_todo(todo_id) + 57: print() + 58: + 59: # Verify deletion + 60: test_get_todos() ⋮ ↳ Purpose: Create test file for the FastAPI ToDo application Creating: /home/ubuntu/todo-fastapi/test_api.py ⋮ ● Completed in 0.0s > FastAPIを使ったシンプルなToDoアプリケーションを作成しました。 main.py の主な機能: • POST /todos: ToDo登録 (タイトル、説明、優先度、期限日) • GET /todos: ToDo一覧取得 • DELETE /todos/{todo_id}: ToDo削除 • Pydanticでバリデーション (優先度: low/medium/high、期限日: 未来日チェック) • メモリ内データベース test_api.py でAPIテストが可能です。 アプリケーションを起動するには: bash python main.py テストを実行するには (別ターミナルで): bash python test_api.py 指示通りにPythonファイル main.py と test_api.py が作成されました。 1.2 生成コードの確認と適用 # 結果を確認します。 Q を起動しているフォルダ(作業ディレクトリ)上に main.py と test_api.py が保存されています。 ※もし Q Developer が「ツール実行(ファイル作成)」を提案した場合は、 yes で許可します。 依存ライブラリをインストールします。 これも Q Developer に実施してもらいます。 指示: アプリケーションを動かすのに必要なPythonライブラリをインストールしてください 応答: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: python main.py ⋮ ↳ Purpose: Start the FastAPI ToDo application Traceback (most recent call last): File "/home/ubuntu/todo-fastapi/main.py", line 1, in <module> from fastapi import FastAPI, HTTPException ModuleNotFoundError: No module named 'fastapi' ⋮ ● Completed in 0.11s > FastAPIがインストールされていないようです。インストールします: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pip install fastapi uvicorn requests ⋮ ↳ Purpose: Install FastAPI and required dependencies ~~~(中略)~~~ Using cached uvicorn-0.38.0-py3-none-any.whl (68 kB) Downloading typing_extensions-4.15.0-py3-none-any.whl (44 kB) Downloading typing_inspection-0.4.2-py3-none-any.whl (14 kB) Installing collected packages: uvicorn, typing-extensions, typing-inspection, pydantic-core, pydantic, fastapi Attempting uninstall: typing-extensions Found existing installation: typing_extensions 4.13.2 Uninstalling typing_extensions-4.13.2: Successfully uninstalled typing_extensions-4.13.2 Attempting uninstall: typing-inspection Found existing installation: typing-inspection 0.4.0 Uninstalling typing-inspection-0.4.0: Successfully uninstalled typing-inspection-0.4.0 Attempting uninstall: pydantic-core Found existing installation: pydantic_core 2.33.2 Uninstalling pydantic_core-2.33.2: Successfully uninstalled pydantic_core-2.33.2 Successfully installed fastapi-0.121.1 pydantic-2.12.4 pydantic-core-2.41.5 typing-extensions-4.15.0 typing-inspection-0.4.2 uvicorn-0.38.0 [notice] A new release of pip is available: 25.1.1 -> 25.3 [notice] To update, run: pip install --upgrade pip ⋮ ● Completed in 7.388s > ライブラリのインストールが完了しました。アプリケーションを起動できます: bash python main.py 動作確認を行います。 アプリケーションを別コンソールから実行します。 python main.py ブラウザまたは curl で次を確認します。 curl http://127.0.0.1:8000/todos まだデータは空なので結果として「 [] 」(空データ)が出力されるはずです。 1章では「AIに自然言語で仕様を伝え、コードを自動生成させる」までを実施しました。 次に コードを前提に、仕様を改めて明文化し、テストで検証できるように します。 2. バリデーション強化 ― テスト可能な仕様にする # 次の2つの仕様を改めて明文化します。 due_date は「未来日」でなければならない priority は low , medium , high のいずれかである AIに対しては、以下のように日本語で伝えます。 指示: Taskモデルにバリデーションを追加してください。due_dateは未来日でなければならず、priorityはlow・medium・highのいずれかのみ許可してください。 応答: > 既存のコードにはバリデーションが実装されていますが、より明確にするために確認・改善します: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/main.py, from line 10 to 30 ✓ Successfully read 469 bytes from /home/ubuntu/todo-fastapi/main.py ⋮ ● Completed in 0.0s ~~~(中略)~~~ 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pkill -f "python main.py" ⋮ ↳ Purpose: Stop the background FastAPI process ⋮ ● Completed in 0.48s > バリデーションは既に正しく実装されており、動作しています: 1. priority: Priority Enumにより low、medium、high のみ許可 2. due_date: 過去日を指定すると422エラーで「due_date must be in the future」メッセージが返される テスト結果で確認できるように: • 有効なデータ: 正常に作成 (200) • 無効な過去日: バリデーションエラー (422) 現在のコードで要求されたバリデーションは完全に実装されています。 このように、コードを前提に、仕様を改めて明文化し、テストで検証できるようになりました。 これまで1章、2章で実施した内容をまとめると以下のようになります。 章 フェーズ 意図 役割 1章 実装生成 AIに自然言語で仕様を伝え、コードを自動生成させる 「生成の正確さ」を観察 2章 仕様検証 コードを前提に、仕様を改めて明文化・テストで検証 「仕様のテスト化」を学ぶ 3. サーバ実行とテスト # では、実際にアプリケーションを動かして、テストを実施してみましょう。 (テストの内容については、この後の章で説明します) アプリケーションをローカルで実行します python main.py 次にテストを実行します。 python test_api.py 実行結果は以下のようになりました。 Testing FastAPI Todo App ============================== Create Todo: 200 { "id": 1, "title": "Test Todo", "description": "Test description", "priority": "high", "due_date": "2025-11-10T22:59:37.092516", "created_at": "2025-11-09T22:59:37.099295" } Get Todos: 200 [ { "id": 1, "title": "Test Todo", "description": "Test description", "priority": "high", "due_date": "2025-11-10T22:59:37.092516", "created_at": "2025-11-09T22:59:37.099295" } ] Invalid Due Date: 422 { "detail": [ { "type": "value_error", "loc": [ "body", "due_date" ], "msg": "Value error, due_date must be in the future", "input": "2025-11-08T22:59:37.104030", "ctx": { "error": {} } } ] } Delete Todo: 200 { "message": "Todo deleted successfully" } Get Todos: 200 [] テストの戻り値「 200 」「 422 」が確認できていることがわかります。 4. AI生成テストを読み解く # AIは仕様文からテストを推論します。 今回作成されたテストケースは以下の通りです。 def test_create_todo(): todo_data = { "title": "Test Todo", "description": "Test description", "priority": "high", "due_date": (datetime.now() + timedelta(days=1)).isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Create Todo: {response.status_code}") print(json.dumps(response.json(), indent=2)) return response.json()["id"] def test_get_todos(): response = requests.get(f"{BASE_URL}/todos") print(f"Get Todos: {response.status_code}") print(json.dumps(response.json(), indent=2)) def test_delete_todo(todo_id): response = requests.delete(f"{BASE_URL}/todos/{todo_id}") print(f"Delete Todo: {response.status_code}") print(json.dumps(response.json(), indent=2)) def test_invalid_due_date(): todo_data = { "title": "Invalid Todo", "priority": "low", "due_date": (datetime.now() - timedelta(days=1)).isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Invalid Due Date: {response.status_code}") print(json.dumps(response.json(), indent=2)) これらのテストは基本的には正しく動作しますが、 AIが自動生成したものを鵜呑みにせず、人間の観点でレビューすることが極めて重要 です。 AIの推論は仕様文から妥当なロジックを導き出しますが、 暗黙的な前提や境界条件 までは十分に理解していないことが多いためです。 以下の観点でレビューを行うと、テストの品質を一段高めることができます。 観点 チェック内容 対応方針 仕様整合性 未来日境界(今日の日付は有効か?)、入力制約は明確か? 境界テストを追加する 可読性 テスト名が仕様を説明しているか、期待結果が明示されているか 名前やprint内容を改善する 網羅性 low , medium , high の正常系が揃っているか、異常系が網羅されているか 不足分を追加生成させる 独立性 各テストが他テストの結果に依存していないか 前提データ作成・削除処理を分離する 再現性 実行順序や時刻依存で結果が変わらないか 固定日時やID管理の仕組みを導入 AIが生成するテストは、仕様記述をもとに 「最も一般的なケース」 を推論する傾向があります。 そのため、 境界条件(当日や閾値など) エラー系(不正値、欠損値、異常なリクエスト) 並行動作や排他制御の確認 といった “仕様の周縁部” を十分に網羅できていないケースが多いです。 AIに「境界テストも追加して」と明示すれば生成されますが、重要なのは「何を境界とみなすか」を人が定義することです。 AIは仕様書の文章を解析してロジックを作りますが、その仕様書自体が不完全であれば、テストも不完全なままになります。 5. テスト補完(人手で対応) # AIが生成したテストコードは、いわば “骨格” です。 そこに 仕様の解釈・リスクベース思考・品質観点 を肉付けするのが人間の役割です。 とくに、次の3点を意識すると品質が安定します。 仕様理解の再確認 :AIが実装した内容と意図した仕様が一致しているか。 異常系の強化 :予期しない入力や外れ値をどう扱うかを明示する。 テスト設計の再利用性 :後続の機能追加でも使える汎用構造に整理する。 FastAPIのToDoアプリが動作し、基本テストが通ったら、次は 不足しているテストケース(境界・異常系) を補います。 ここでは、AIの提案に頼るのではなく、人が意図をもってテストを追加する手順を示します。 テスト対象: due_date が「今日」→ 無効(未来日限定) priority に不正値 → 無効( low / medium / high のみ許可) 正常系3種(low, medium, high)を網羅 def test_due_date_today_invalid(): """今日の日付を指定した場合の検証(422想定)""" todo_data = { "title": "Today Todo", "priority": "low", "due_date": datetime.now().isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Due Date Today Invalid: {response.status_code}") try: print(json.dumps(response.json(), indent=2)) except Exception: print(response.text) def test_priority_invalid_value(): """priority に不正値を指定した場合の検証(422想定)""" todo_data = { "title": "Invalid Priority Todo", "priority": "urgent", # 許可値: low/medium/high のみ "due_date": (datetime.now() + timedelta(days=1)).isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Invalid Priority: {response.status_code}") try: print(json.dumps(response.json(), indent=2)) except Exception: print(response.text) 6. テスト補完(AIに依頼) # 5章では人手でテストケースを拡充しましたが、AIにテストケースの補完が可能かどうかを確認してみます。 指示: テストケースが十分かどうかを確認してください 応答: > 現在のテストケースを確認し、不足している部分を特定します: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/test_api.py, all lines ✓ Successfully read 1661 bytes from /home/ubuntu/todo-fastapi/test_api.py ⋮ ● Completed in 0.0s > 現在のテストケースは基本的な機能のみをカバーしています。不足しているテストケースを追加します: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: test_api.py 29, 29: def test_invalid_due_date(): 30, 30: todo_data = { 31, 31: "title": "Invalid Todo", 32, 32: "priority": "low", 33, 33: "due_date": (datetime.now() - timedelta(days=1)).isoformat() ~~~(中略)~~~ + 107: # Delete minimal todo if created + 108: if minimal_id: + 109: test_delete_todo(minimal_id) + 110: print() + 111: 91, 112: # Verify deletion 92, 113: test_get_todos() ⋮ ↳ Purpose: Update main test execution to include all test cases Updating: test_api.py ⋮ ● Completed in 0.0s > 追加したテストケース: バリデーションテスト: • test_invalid_priority(): 無効な優先度 ("urgent") をテスト • test_missing_title(): 必須フィールド (title) の欠如をテスト • test_minimal_todo(): 最小限の必須フィールドのみでの作成をテスト エラーハンドリングテスト: • test_delete_nonexistent(): 存在しないToDo削除時の404エラーをテスト テストカバレッジ: • 正常系: 完全なToDo作成、最小限のToDo作成、一覧取得、削除 • 異常系: 過去日、無効な優先度、必須フィールド欠如、存在しないID削除 これで主要なバリデーションとエラーケースがカバーされます。 追加されたテストを実行してみましょう。 指示: テストを実行してください 応答: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: python main.py & ⋮ ↳ Purpose: Start FastAPI application in background ⋮ ● Completed in 0.3s ~~~(中略)~~~ 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pkill -f "python main.py" ⋮ ↳ Purpose: Stop the background FastAPI process ⋮ ● Completed in 0.15s > テスト結果の分析: ✅ 正常系テスト: • 完全なToDo作成: 200 OK • 最小限のToDo作成: 200 OK (description, due_dateはnull) • 一覧取得: 200 OK • ToDo削除: 200 OK ✅ バリデーションテスト: • 過去日指定: 422エラー "due_date must be in the future" • 無効な優先度: 422エラー "Input should be 'low', 'medium' or 'high'" • 必須フィールド欠如: 422エラー "Field required" ✅ エラーハンドリングテスト: • 存在しないToDo削除: 404エラー "Todo not found" すべてのテストケースが期待通りに動作しており、バリデーションとエラーハンドリングが正しく実装されています。テストカバレッ ジは十分です。 このようにAIと対話を進めながら、テストケースを拡充していくこともできます。 AIとペアでプログラミングできる日が来るとは、すごい時代になりました。 まとめ # AIが提案するテストはあくまで「たたき台」です。 人が仕様理解と品質観点でレビューし、採用・修正・削除の判断を行うことが重要です。 観点 採用すべき提案 却下すべき提案 仕様整合性 未来日・優先度の要件を正確に守る 曖昧な条件を含む(例:今日を許可) 可読性 テスト名・変数名が直感的 意味不明な略語や複雑なロジック 保守性 簡潔で再利用可能 重複コードや不要な依存関係を含む 実行容易性 python test_api.py で完結 外部サーバ起動が必要 これがQ Developerによる“品質管理の自動化”を成立させる鍵です。 皆さまの生成AI活用の参考になれば幸いです。 img { border: 1px gray solid; }
はじめに # 最近、Ubuntu Desktop 用にノートPCを購入しました。自宅で Web アプリケーションをホストしてみたいと思ったのがきっかけです。 購入後、早速 Apache Web サーバーをインストールし、 /var/www/html/index.html を直接編集してブラウザからアクセスしてみました。思った通りに表示されるのを見て、「これは楽しい!」と感じたのを覚えています。 しかし、このままでは問題があります。 この状態で外部にページを公開すると、編集内容が即座に本番環境へ反映されてしまう ということです。つまり、開発中の未完成なコードや、テスト用のデータが訪問者に見えてしまう可能性があります。 「開発環境と本番環境を分けたい...!」 そこで、Apache Web サーバーの公開ディレクトリとは別に開発用のディレクトリを作成し、そちらで開発を進めることにしました。同時に Git/GitHub を使ったバージョン管理も導入し、一般的な dev ブランチから main ブランチへのプルリクエスト・マージという開発フローも採用しました。 しかし、新たな問題が発生しました。この開発フローを取り入れたことで、開発が完了するたびに 開発ディレクトリから公開ディレクトリへの手動コピー&ペースト作業 が必要になったのです。GitHub 上でプルリクエストをマージさせつつ、手元のローカルリポジトリの main ブランチを基準にして、開発ディレクトリから公開ディレクトリへの手動コピー&ペーストを行う必要があります。流石にこれは面倒です。 「プルリクエストをマージしたら、更新内容が自動的に Apache Web サーバーの公開ディレクトリに反映されてほしい...!」 この面倒な手作業をなんとか自動化できないかと考えた結果、GitHub Actions による CD パイプラインを構築することにしました。 本記事では、GitHub Actions を用いて自宅 LAN 内の Ubuntu Desktop に対する継続的デプロイメント(CD)パイプラインの構築経験を、具体的な設定手順も併せて解説したいと思います。 構築するパイプライン # 本記事で構築する CD パイプラインは、以下のような仕組みで動作します。 実現される動作フロー # graph TD A[コード編集] --> B[dev ブランチにコミット] B --> C[GitHub で PR 作成] C --> D[PR を main にマージ] D --> E[GitHub Actions 自動起動] E --> F[自宅サーバーに自動デプロイ] F --> G[本番環境に反映完了] style A fill:#e1f5fe style D fill:#fff3e0 style G fill:#c8e6c9 手動で行っていた「開発ディレクトリから公開ディレクトリへのコピー&ペースト」が、 プルリクエストのマージをトリガーに完全自動化 されます。 システム構成 # graph TD A[GitHub Actions Runner] --> B[Cloudflare Edge] B --> C[Cloudflare Tunnel] C --> D[自宅 Ubuntu Desktop] D --> E[Apache Web Server] style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#fff3e0 style D fill:#c8e6c9 style E fill:#c8e6c9 コンポーネント 役割 GitHub Actions マージをトリガーにデプロイワークフローを実行 Cloudflare Tunnel 自宅 IP を公開せずに安全な通信経路を提供 Cloudflare Access Service Token による自動認証を実現 SSH + rsync 変更されたファイルのみを効率的に同期 完成後にできること # ✅ プルリクエストをマージするだけで本番環境に自動反映 ✅ 自宅のパブリック IP アドレスを公開せずに安全に運用 ✅ デプロイ履歴が GitHub Actions のログに自動記録 ✅ 手動デプロイ作業から完全に解放 この記事の前提条件 # 本記事は以下の知識・環境をお持ちの方を対象としています。 必要な知識 # GitHub Actions の基本 : workflow ファイルの書き方や基本的なアクションの使い方 Linux の基本操作 : コマンドライン操作、ファイル権限、SSH 接続の概念 Apache Web サーバーの基本 : DocumentRoot の概念、基本的な設定 Git の基本 : ブランチ、プルリクエスト、マージの概念 必要な環境 # GitHub リポジトリ : プライベート・パブリックは問いません Ubuntu Desktop 環境 : 今回は Ubuntu 24.04.3 LTS を想定 Apache Web サーバー : インストール・設定済み Cloudflare アカウント : Cloudflare Zero Trust の利用を想定 独自ドメイン : ネームサーバーが Cloudflare に設定されているドメインであること 必要な技術概念と知識 # パイプラインを構築する前に、関連する技術概念を整理します。 GitHub Actions Workflows # GitHub Actions は、GitHub リポジトリ内で CI/CD パイプラインを実行するためのプラットフォームです。 https://docs.github.com/ja/actions/get-started/understand-github-actions 重要な概念 ワークフロー : .github/workflows/ 内のYAMLファイルで定義された1つ以上のジョブを実行するプロセス イベント : ワークフローをトリガーする、リポジトリ内の特定のアクティビティ(例えばプルリクエストのマージ) ジョブ : 同じランナーで実行される、ワークフロー内の一連のステップ アクション : ワークフロー内で特定のタスクを実行するコンポーネント (GitHub Actions で提供されるものや、自作のものを使用できる) ランナー : ワークフローを実行する仮想環境。各ランナーは一度に1つのジョブを実行できる。(今回は ubuntu-latest を使用します) SSH 接続とセキュリティ # SSH(Secure Shell)の基本 SSH は暗号化された通信路を使ってリモートサーバーに安全に接続するプロトコルです。 ホスト認証 SSH クライアント側で、接続先が 本物の SSH サーバー であることを確認する。 ホスト認証の仕組み SSH サーバーには ホスト鍵(host key) という固有の秘密鍵/公開鍵ペアが設定されている。 SSH クライアント側でこの公開鍵を保存する先が、 ~/.ssh/known_hosts ファイル。 初回接続時に、SSH サーバーのホスト鍵 (公開鍵) を SSH クライアント側の ~/.ssh/known_hosts ファイルに追加する。 その後の接続時に、このファイルに登録されているホスト鍵 (公開鍵) と SSH サーバーのホスト鍵 (秘密鍵) を比較して、接続先が本物の SSH サーバーであることを確認する。 ユーザー認証 SSH サーバー側で、接続してきた SSH クライアントのユーザーが 本物のユーザー であることを確認する。 ユーザー認証の仕組み 公開鍵認証方式を使用する。 ざっくりなイメージは以下のとおり。 SSH サーバーは、許可された SSH クライアントの公開鍵を ~/.ssh/authorized_keys ファイルに保持している。 SSH クライアントは、自身の秘密鍵を使用して署名を生成する。 SSH サーバーは、SSH クライアントの公開鍵を使用して署名を検証する。 署名が検証された場合、SSH サーバーは SSH クライアントのユーザーとして接続を許可する。 https://qiita.com/whoami_priv/items/9f165f8dfd95edb169b7 https://qiita.com/pyon_kiti_jp/items/f89b8fa9f5b7f8abac23 --> Information 今回の SSH 接続における役割 SSH クライアント : GitHub Actions のランナー環境 (ubuntu-latest) SSH サーバー : 自宅の Ubuntu Desktop なぜこの関係になるのか? GitHub Actions のワークフローは GitHub のクラウド環境で実行されます。 したがって、クラウド環境からみて外部のサーバー(自宅の Ubuntu Desktop)にファイルをデプロイするには、GitHub Actions ランナーから自宅サーバーに向けて SSH 接続を開始する必要があります。 つまり、「クラウド → 自宅」という方向での接続となり、GitHub Actions ランナーが接続を開始する側(クライアント)、自宅 Ubuntu Desktop が接続を受ける側(サーバー)となります。 Cloudflare Tunnel # Cloudflare Tunnel とは Cloudflare Tunnel は、 パブリック IP アドレスを公開することなく、ローカルサーバーを安全にインターネットに公開できるサービス です。 https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/ https://qiita.com/keke21/items/efaa2b2c35dfb646a43e 従来の自宅サーバー公開方法との違い 従来の方法(ポートフォワーディング): インターネット → ルーター(ポート22開放) → 自宅 Ubuntu Desktop ↑ セキュリティリスク ❌️ ルーターで SSH ポート(22番)を開放する必要 ❌ パブリック IP アドレスが直接露出 ❌ DDoS 攻撃やブルートフォース攻撃の標的になりやすい ❌ 自宅の IP アドレスが特定される可能性 Cloudflare Tunnel を使った方法: GitHub Actions → Cloudflare Edge → Cloudflare Tunnel → 自宅 Ubuntu Desktop ↑ 暗号化されたトンネル ✅️ ルーターでポートを開放する必要がない ✅️ パブリック IP アドレスを隠匿 ✅️ Cloudflare のセキュリティ機能を活用 ✅️ トラフィックが Cloudflare エッジを経由してフィルタリング 今回の構成での役割 graph TD A[GitHub Actions Runner] --> B[Cloudflare Edge] B --> C[Cloudflare Tunnel] C --> D[自宅 Ubuntu Desktop] style A fill:#e1f5fe style D fill:#c8e6c9 style C fill:#fff3e0 自宅 Ubuntu Desktop 側 : cloudflared デーモンが常時 Cloudflare に接続 GitHub Actions 側 : cloudflared access ssh コマンドでプロキシ経由接続 Cloudflare :両者の間でセキュアなトンネルを提供 セキュリティ上のメリット IP 隠匿 :自宅のパブリック IP アドレスが外部に露出しない DDoS 保護 :Cloudflare のセキュリティインフラを活用 アクセス制御 :Cloudflare Access による詳細な制御 Cloudflare Access # Cloudflare Access とは Cloudflare Access は、 Zero Trust Network Access(ZTNA)を実現する Cloudflare のセキュリティサービス です。従来の VPN に代わって、アプリケーションレベルでの認証・認可を提供します。 Zero Trust Network Access(ZTNA)の概念 https://www.cloudflare.com/ja-jp/learning/access-management/what-is-ztna/ 従来の境界セキュリティモデル: 外部(危険) | ファイアウォール | 内部(安全) 社内ネットワークとインターネットの間に境界を引き、ファイアウォールで外部からの脅威をブロックする 「内部ネットワークは信頼できる」という前提 一度内部に侵入されると横展開のリスク Zero Trust アプローチ: すべてのアクセスを検証 → 認証 → 認可 → アクセス許可 「信頼しない、常に検証する」(Never Trust, Always Verify) ネットワークの場所に関係なく、すべての接続を認証・認可 今回の構成での Cloudflare Access の役割 sequenceDiagram participant GA as GitHub Actions participant CF as Cloudflare Access participant CT as Cloudflare Tunnel participant UD as Ubuntu Desktop GA->>CF: 1. SSH 接続リクエスト + 認証情報 CF->>CF: 2. Access Policy 確認 CF->>GA: 3. 認証成功 GA->>CT: 4. cloudflared プロキシ経由で SSH 接続 CT->>UD: 5. セキュアトンネル経由で SSH 接続 UD->>GA: 6. SSH 接続確立 セキュリティ上のメリット 細かいアクセス制御 :リソースごと、ユーザーごとの詳細な権限設定 監査ログ :すべてのアクセス試行が記録される MFA 対応 :多要素認証の強制が可能(人間のユーザーの場合) リアルタイム制御 :ポリシー変更が即座に反映 セッション管理 :接続セッションの詳細な管理・監視 従来の VPN との違い 項目 従来の VPN Cloudflare Access 接続範囲 ネットワーク全体 アプリケーション単位 認証 接続時のみ アクセス毎に検証 設定複雑さ 複雑 比較的シンプル スケーラビリティ 制限あり 高い 監査ログ 限定的 詳細 Service Token による自動認証 Service Token とは Service Token は、 自動化システムやマシン間通信のための認証メカニズム です。人間のユーザーによるインタラクティブな認証(OAuth、SAMLなど)とは異なり、完全にプログラマティックなアクセスを可能にします。 従来のユーザー認証との違い: 項目 ユーザー認証 (OAuth/SAML) Service Token 対象 人間のユーザー 自動化システム・アプリケーション 認証フロー ブラウザベース・インタラクティブ API ベース・プログラマティック 認証情報 ユーザー名・パスワード・MFA Client ID・Client Secret 有効期限 セッション単位 (数時間) 長期間 GitHub Actions での役割 1. 自動化された SSH 接続の実現 GitHub Actions のワークフローでは、人間による認証操作ができないため、Service Token が不可欠です。 GitHub Actions Runner → Service Token 認証 → Cloudflare Access → SSH 接続 2. CD パイプラインに最適な理由 無人実行 : 人間の介入なしで24時間実行可能 スケーラビリティ : 複数のワークフローから同時利用可能 信頼性 : セッション切れやタイムアウトの心配がない セキュリティ : GitHub Secrets での安全な管理 3. 自動化ワークフローでのセキュリティ利点 最小権限の原則 : 特定のアプリケーションのみへのアクセス 監査可能性 : すべてのアクセスがログに記録 中央管理 : Cloudflare ダッシュボードでの一元管理 認証フローの概念 Service Token 認証プロセス: sequenceDiagram participant GA as GitHub Actions participant CF as Cloudflare Access participant App as Protected Application GA->>CF: 1. Service Token (Client ID + Secret) CF->>CF: 2. Token 有効性確認 CF->>CF: 3. Access Policy 評価 CF->>GA: 4. 認証成功 (Access Token) GA->>App: 5. アプリケーションアクセス (Access Token) App->>GA: 6. レスポンス Cloudflare Access による検証プロセス: Token 有効性確認 : 提供された Client ID と Secret の検証 Policy 評価 : 該当する Access Policy の条件確認 アクセス許可判定 : すべての条件を満たした場合のみアクセス許可 監査ログ記録 : 認証試行と結果の詳細ログ この Service Token による認証機構により、セキュリティを損なうことなく、完全に自動化された CD パイプラインを実現できます。 完成形のパイプライン全体像 # まずは最終的に完成したCDパイプラインの全体像を紹介します。 deploy.yml ファイルの全容 # name: Deploy to Server on: pull_request: types: [closed] branches: - main jobs: deploy: runs-on: ubuntu-latest if: github.event.pull_request.merged == true env: REMOTE_HOST: ssh.your-domain.com REMOTE_USER: deploy-user REMOTE_DIR: /var/www/html SSH_PROXY_COMMAND: /tmp/cloudflared/cloudflared access ssh --id ${{ secrets.CLOUDFLARED_SSH_ID }} --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} --hostname %h steps: - name: Install cloudflared run: | latest_version=$(curl -s $GITHUB_API_URL/repos/cloudflare/cloudflared/releases/latest | jq -r '.tag_name') mkdir -p /tmp/cloudflared curl -sL -o /tmp/cloudflared/cloudflared $GITHUB_SERVER_URL/cloudflare/cloudflared/releases/download/$latest_version/cloudflared-linux-amd64 chmod +x /tmp/cloudflared/cloudflared /tmp/cloudflared/cloudflared --version - name: Prepare .ssh/known_hosts from secrets run: | mkdir -p $HOME/.ssh chmod 700 $HOME/.ssh echo "${{ secrets.SSH_KNOWN_HOSTS }}" > $HOME/.ssh/known_hosts chmod 644 $HOME/.ssh/known_hosts - name: Set up SSH key run: | SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions echo "${{ secrets.SSH_PRIVATE_KEY }}" > "$SSH_KEY_PATH" chmod 600 "$SSH_KEY_PATH" - name: Checkout repository uses: actions/checkout@v4 - name: Deploy application via rsync run: | SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions rsync -rvz --no-group --no-perms --omit-dir-times \ -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" \ --include='public/***' \ --include='src/***' \ --include='views/***' \ --include='composer.json' \ --include='composer.lock' \ --exclude='*' \ ./ \ $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ - name: Run composer install on remote run: | echo "Installing composer dependencies..." SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions ssh -i "$SSH_KEY_PATH" \ -o StrictHostKeyChecking=yes \ -o ProxyCommand="$SSH_PROXY_COMMAND" \ $REMOTE_USER@$REMOTE_HOST " set -euo pipefail cd \"$REMOTE_DIR\" if command -v composer >/dev/null 2>&1; then COMPOSER_NO_INTERACTION=1 \ composer install \ --no-dev \ --prefer-dist \ --no-interaction \ --no-progress \ --optimize-autoloader if sudo -n true 2>/dev/null; then sudo chown -R www-data:www-data vendor sudo chmod -R 755 vendor else echo '⚠️ sudo権限がないため vendor の所有権/権限変更をスキップしました' fi echo '✅ Composer install completed.' else echo '❌ composer not found on remote host.' exit 1 fi " パイプラインの動作フロー # graph TD A[プルリクエストが main ブランチにマージ] --> B[GitHub Actions 起動] B --> C[リポジトリのコードをチェックアウト] C --> D[cloudflared のインストール] D --> E[SSH 設定とホスト鍵登録] E --> F[rsync によるファイル同期] F --> G[デプロイ完了] F -.-> H[Composer による依存パッケージのインストール<br/>※ PHP 環境のみ] H -.-> G style A fill:#e1f5fe style G fill:#c8e6c9 style H fill:#fff2cc 各ステップの詳細 # 1. トリガー条件 on: pull_request: types: [closed] branches: [main] main ブランチへのプルリクエストがクローズされた際に実行 github.event.pull_request.merged == true でマージされた場合のみ処理 2. cloudflared のインストール latest_version=$(curl -s $GITHUB_API_URL/repos/cloudflare/cloudflared/releases/latest | jq -r '.tag_name') mkdir -p /tmp/cloudflared curl -sL -o /tmp/cloudflared/cloudflared $GITHUB_SERVER_URL/cloudflare/cloudflared/releases/download/$latest_version/cloudflared-linux-amd64 chmod +x /tmp/cloudflared/cloudflared /tmp/cloudflared/cloudflared --version GitHub APIから最新バージョンを動的に取得 3. SSH 接続におけるホスト鍵の事前登録 mkdir -p $HOME/.ssh chmod 700 $HOME/.ssh echo "${{ secrets.SSH_KNOWN_HOSTS }}" > $HOME/.ssh/known_hosts chmod 644 $HOME/.ssh/known_hosts 事前に登録したホスト鍵を使用して安全な SSH 接続を確立 --> Caution セキュリティを重視した設計判断 自動化システムで SSH 接続を行う際、 StrictHostKeyChecking=no オプションを使用することで初回接続時のホスト確認を省略し、設定の簡素化も可能です。 # 簡単だがセキュリティリスクのあるアプローチ ssh -o StrictHostKeyChecking=no user@hostname "command" しかし、このアプローチは以下のセキュリティリスクを抱えています: ❌️ 中間者攻撃(MITM)の危険性 : 悪意のあるサーバーが正規のサーバーになりすますことが可能 ❌️ 接続先の真正性確認不可 : 意図した正しいサーバーに接続しているかの保証がない ❌️ セキュリティポリシーの妥協 : 自動化のために基本的なセキュリティチェックを無効化 事前ホスト鍵登録によるメリット: # セキュアなアプローチ echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts ssh -o StrictHostKeyChecking=yes user@hostname "command" # デフォルト動作 ✅️ 接続先の検証 : 事前に登録した正規のホスト鍵との照合により接続先を確実に検証 ✅️ 中間者攻撃の防止 : 不正なサーバーへの接続を自動的に拒否 ✅️ 自動化とセキュリティの両立 : 人間の介入なしに安全な接続を実現 ✅️ 監査適合性 : セキュリティ基準を満たした自動化システムの構築 このパイプラインでは、 自動化の利便性よりもセキュリティを優先 し、事前にホスト鍵を取得・管理する手間を承知で、より安全なアプローチを採用しました。 https://io.cyberdefense.jp/entry/dangerous_ssh_sftp_usage/ 4. ソースコードのチェックアウト - name: Checkout repository uses: actions/checkout@v4 actions/checkout@v4 : GitHub Actions の公式アクションを使用してリポジトリのソースコードを取得 デプロイ対象ファイルの準備 : rsync での転送前に、最新のマージされたコードをランナー環境に配置 5. ファイル同期 SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions rsync -rvz --no-group --no-perms --omit-dir-times \ -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" \ --include='public/***' \ --include='src/***' \ --include='views/***' \ --include='composer.json' \ --include='composer.lock' \ --exclude='*' \ ./ \ $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ rsync コマンドの詳細解説 SSH_KEY_PATH 変数 : SSH 秘密鍵のパスを変数化して可読性と保守性を向上 ed25519 鍵タイプ : RSA より高速で安全な楕円曲線暗号を使用 GitHub Actions 専用鍵 : デプロイ専用の識別しやすい鍵名 rsync オプションの解説 rsync -rvz --no-group --no-perms --omit-dir-times オプション 説明 理由 -r 再帰的コピー ディレクトリ構造を保持して全ファイルを転送 -v 詳細出力 転送状況をログで確認可能 -z 圧縮転送 ネットワーク帯域を効率的に利用 --no-group グループ変更スキップ 権限エラーを回避 --no-perms 権限変更スキップ 既存のサーバー権限を保持 --omit-dir-times ディレクトリタイムスタンプスキップ タイムスタンプ関連エラーを防止 https://log.dot-co.co.jp/rsync/ SSH 接続設定 -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" -e フラグ : rsync が使用する SSH コマンドを明示的に指定 -i "$SSH_KEY_PATH" : 指定した秘密鍵ファイルを使用してユーザー認証 StrictHostKeyChecking=yes : ホスト鍵検証を強制(セキュリティ確保) ProxyCommand='$SSH_PROXY_COMMAND' : Cloudflare Access 経由で SSH 接続を確立 SSH_PROXY_COMMAND の詳細 SSH_PROXY_COMMAND: /tmp/cloudflared/cloudflared access ssh --id ${{ secrets.CLOUDFLARED_SSH_ID }} --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} --hostname %h パラメータ 説明 役割 /tmp/cloudflared/cloudflared cloudflared バイナリパス 事前にインストールした cloudflared の実行ファイル access ssh SSH アクセスモード Cloudflare Access 経由での SSH 接続を指定 --id ${{ secrets.CLOUDFLARED_SSH_ID }} Service Token ID Cloudflare Access での認証に使用するクライアント ID --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} Service Token Secret Service Token のクライアントシークレット(GitHub Secrets で管理) --hostname %h 動的ホスト名指定 SSH 接続時のホスト名を動的に取得( %h は SSH の置換変数) なぜこのコマンドが必要なのか? GitHub Actions ランナー(SSH クライアント)は、人間のようにブラウザでログインして ID/パスワードや MFA を入力できません。Cloudflare Access のゲートを通過するためには、プログラマティックな認証メカニズムが必要です。 このコマンドの役割: このコマンドは、SSH 接続の前に Cloudflare Access による認証を自動的に行い、必要な認証情報を付与する ProxyCommand として機能します。具体的には、Service Token を使用して Cloudflare Access の認証を通過し、Cloudflare Tunnel 経由で自宅サーバーへの SSH 接続を確立します。 SSH プロキシコマンドの動作フロー: sequenceDiagram participant SSH as SSH Client participant CF as cloudflared participant CA as Cloudflare Access participant CT as Cloudflare Tunnel participant Server as Ubuntu Server SSH->>CF: ProxyCommand 実行 CF->>CA: Service Token で認証 CA->>CF: 認証成功 CF->>CT: Tunnel 経由接続要求 CT->>Server: SSH 接続転送 Server->>SSH: SSH セッション確立 このプロキシコマンドにより、GitHub Actions ランナー(SSH クライアント)は Cloudflare Access の認証とトンネルを経由して安全に自宅サーバー(SSH サーバー)に接続できます。 ファイル選択ロジック 包含パターン(Include): --include='public/***' # Web 公開ファイル --include='src/***' # アプリケーションソースコード --include='views/***' # テンプレートファイル --include='composer.json' # PHP 依存関係定義 --include='composer.lock' # 依存関係ロックファイル 除外パターン(Exclude): --exclude='*' # デフォルトですべて除外 選択的同期の利点: セキュリティ : 機密ファイル( .env , .git 等)の転送を防止 効率性 : 不要なファイル( node_modules , vendor 等)を除外して高速転送 安全性 : 本番環境に影響を与える可能性のあるファイルを制御 転送元・転送先の指定 ./ $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ ./ : カレントディレクトリ(チェックアウトされたリポジトリのルート) $REMOTE_USER : リモートサーバーのユーザー名(例: deploy-user ) $REMOTE_HOST : 接続先ホスト名(例: ssh.your-domain.com ) $REMOTE_DIR : デプロイ先ディレクトリ(例: /var/www/html ) この設定により、 セキュアで効率的、かつ制御されたファイル同期 を実現しています。 この基本的な4ステップで、GitHub Actions から Cloudflare Access 経由で自宅サーバーへの安全なファイル同期が実現できます。 詳細な設定手順 # 実際に同じ環境を構築するための詳細な手順を説明します。 1. Cloudflare の設定 # 参考 Cloudflare login https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/ https://zenn.dev/z4ck_key/articles/github-actions-to-cloudflare-tunnnel https://zenn.dev/greendrop/articles/2024-04-25-aacf4debe469e8 https://dev.classmethod.jp/articles/use-service-tokens-to-authenticate-cloudflare-access-from-my-application/ https://zenn.dev/takajun/articles/fbd783e459c722 1.1 Cloudflare Tunnel の作成 Zero Trust > ネットワーク > Tunnels に移動 トンネルを作成する をクリック Cloudflared をクリック トンネル名を入力 トンネルを保存 をクリック 表示されたインストールコマンドをコピー 下記項目を入力し、 セットアップを完了する をクリック 項目 説明 サブドメイン (任意) ssh ドメイン 取得済みの独自ドメイン タイプ SSH URL localhost:22 1.3 Cloudflare Access の設定 1.3.1 Service Token の作成 Zero Trust > Access > サービス認証 に移動 サービストークンを作成する をクリック 下記項目を入力し、 トークンを作成する をクリック 項目 説明 サービストークン名 サービストークンの名前 サービストークンの有効期間 任意の選択肢 作成されたサービストークンの クライアント ID と クライアントシークレット をコピー (追って GitHub の Secrets に設定します) 保存 をクリック 1.3.2 Policy の作成 Zero Trust > Access > ポリシー に移動 ポリシーを追加する をクリック 下記項目を入力し、 保存 をクリック 項目 説明 ポリシー名 任意の名前 アクション Service Auth セッション時間 任意の選択肢 セレクター Service Auth 値 1.3.1 Service Token の作成 で作成したサービストークン 1.3.3 Application の作成 Zero Trust > Access > アプリケーション に移動 アプリケーションを追加する をクリック セルフホスト をクリック 下記項目を入力 項目 説明 アプリケーション名 任意の名前 セッション時間 任意の選択肢 サブドメイン (任意) ssh ドメイン 取得済みの独自ドメイン パブリックホスト名 の入力欄は パブリックホスト名を追加 をクリックすると表示されます。 既存のポリシーを選択 をクリックし、 1.3.2 Policy の作成 で作成したポリシーを選択 次へ をクリック エクスペリエンス設定 と 詳細設定 はデフォルトのまま、各自の環境に合わせて設定し、 次へ / 保存 をクリック 2. Ubuntu Desktop 側の設定 # 2.1 SSH サーバーのインストールと設定 # OpenSSH Serverのインストール sudo apt update sudo apt install openssh-server # SSH設定の編集 sudo nano /etc/ssh/sshd_config 設定項目: # パスワード認証を無効化 PasswordAuthentication no # 公開鍵認証を有効化 PubkeyAuthentication yes # rootログインを無効化 PermitRootLogin no # 空のパスワードを持つアカウントのログインを禁止 PermitEmptyPasswords no # 最大認証試行回数 MaxAuthTries 6 # ホスト鍵の指定 HostKey /etc/ssh/ssh_host_rsa_key HostKey /etc/ssh/ssh_host_ecdsa_key HostKey /etc/ssh/ssh_host_ed25519_key 設定後の再起動: sudo systemctl restart ssh.socket sudo systemctl enable ssh.socket 2.2 ファイアウォールの設定 # ファイアウォールの有効化 sudo ufw enable # デフォルトポリシーで受信 (Incoming) を拒否 sudo ufw default deny incoming # デフォルトポリシーで送信 (Outgoing) を許可 sudo ufw default allow outgoing # SSHポートの開放 sudo ufw allow ssh # SSH ブルートフォース攻撃の防止 sudo ufw limit ssh # ファイアウォールの状態を確認 sudo ufw status セキュアなSSHサーバの構築と運用ガイド(Ubuntu上) 2.3 デプロイ用ユーザーの作成 # デプロイ専用ユーザーを作成 sudo adduser deploy-user # デプロイ用ユーザー用に公開鍵を配置するディレクトリを作成する sudo mkdir -p /home/deploy-user/.ssh # 公開鍵ファイルを作成する (今は空ファイル) sudo touch /home/deploy-user/.ssh/authorized_keys # ディレクトリの権限を設定する sudo chmod 700 /home/deploy-user/.ssh # 公開鍵ファイルの権限を設定する sudo chmod 600 /home/deploy-user/.ssh/authorized_keys # ディレクトリの所有者を設定する sudo chown -R deploy-user:deploy-user /home/deploy-user/.ssh 2.4 Apache Web サーバーの設定 # Apacheのインストール sudo apt install apache2 # DocumentRootの権限設定 find /var/www/html -type d -exec chmod 750 {} \; find /var/www/html -type f -exec chmod 640 {} \; sudo chown -R www-data:www-data /var/www/html # deploy-userがwww-dataグループで書き込み可能に sudo usermod -aG www-data deploy-user 2.5 パッケージマネージャーのインストール --> Information この手順について パッケージマネージャーのインストールは今回の主題(CD パイプライン構築)とは別の内容のため、詳細は省略します。 今回は PHP 環境のため Composer をインストールしますが、開発言語に応じて適切なパッケージマネージャーを選択してください。 PHP : Composer Node.js : npm / yarn / pnpm Python : pip / poetry / pipenv Ruby : gem / bundler Go : go mod Rust : cargo Java : Maven / Gradle 各言語の公式ドキュメントを参照してインストールを行ってください。 2.6 cloudflared のインストールと設定 #1.1 Cloudflare Tunnel の作成 でコピーしたインストールコマンドを実行します。 # cloudflaredのインストール # Add cloudflare gpg key sudo mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | sudo tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null # Add this repo to your apt repositories echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list # install cloudflared sudo apt-get update && sudo apt-get install cloudflared サービス化: sudo cloudflared service install [TOKEN] sudo systemctl start cloudflared sudo systemctl enable cloudflared 3. GitHub Secrets の設定 # 3.1 SSH鍵ペアの生成 --> Caution この手順について SSH 鍵の生成と公開鍵の配置は、 2.3 デプロイ用ユーザーの作成 で作成したユーザーのホームディレクトリで実施します。 # ユーザー名を確認 $ whoami deploy-user # カレントディレクトリを確認 $ pwd /home/deploy-user # GitHub Actions用のSSH鍵を生成 ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_github_actions -C "GitHub Actions" # 公開鍵をサーバーに配置 cat ~/.ssh/id_ed25519_github_actions.pub >> ~/.ssh/authorized_keys 3.2 ホスト鍵の取得 Ubuntu Desktop の SSH サーバーに接続して、ホスト鍵を取得します。 下記コマンドの出力をコピーしておきます。 ssh-keyscan localhost 2>/dev/null | sed 's/localhost/ssh.your-domain.com/' ssh-keyscan コマンドで取得したホスト鍵に含まれる文字列 localhost を Cloudflare Tunnel のホスト名に置き換えるため、 sed コマンドの your-domain.com を各自の環境に合わせて置き換えてください。 3.3 GitHub Repository Settings での設定 Settings → Secrets and variables → Actions で New repository secret をクリックし、以下を設定する。 Name Value SSH_PRIVATE_KEY ~/.ssh/id_ed25519_github_actions の内容 SSH_KNOWN_HOSTS 3.2 ホスト鍵の取得 でコピーしたホスト鍵の内容 CLOUDFLARED_SSH_ID 1.3.1 Service Token の作成 で作成したサービストークンの クライアント ID (ヘッダー部分を除いたものをそのまま貼り付け) CLOUDFLARED_SSH_SECRET 1.3.1 Service Token の作成 で作成したサービストークンの クライアントシークレット (ヘッダー部分を除いたものをそのまま貼り付け) 4. ワークフローファイルの配置 # リポジトリに .github/workflows/deploy.yml を作成し、先ほど紹介した完成版のYAMLを配置します。 5. 動作確認 # 5.1 プルリクエストでのテスト 機能ブランチを作成 何らかの変更をコミット main ブランチに対するプルリクエストを作成 プルリクエストをマージ GitHub Actions の実行ログを確認 まとめ # 本記事では、GitHub Actions を使って自宅の Ubuntu Desktop への継続的デプロイメント(CD)パイプラインを構築する方法を解説しました。 達成したこと # 手動デプロイ作業の完全自動化 : プルリクエストのマージをトリガーに、開発ディレクトリから公開ディレクトリへのファイル同期を自動化 セキュアな接続 : Cloudflare Tunnel と Cloudflare Access を活用し、自宅のパブリック IP を公開せずに安全な SSH 接続を実現 Zero Trust アーキテクチャの実装 : Service Token による認証とアクセス制御により、セキュリティを損なわない自動化を構築 重要なポイント # Cloudflare Tunnel の活用 : ルーターのポート開放不要で、セキュアに自宅サーバーを外部に公開 Service Token 認証 : 人間の介入なしに、GitHub Actions からの自動アクセスを安全に実現 ホスト鍵の事前登録 : StrictHostKeyChecking=no の安易な使用を避け、セキュリティを優先した設計 このアプローチの利点 # 開発効率の向上 : デプロイ作業が自動化され、開発に集中できる セキュリティの確保 : 複数のセキュリティレイヤーにより、自宅サーバーを安全に運用 スケーラビリティ : 同じ仕組みを複数のプロジェクトやサーバーに適用可能 監査可能性 : すべてのデプロイが GitHub Actions と Cloudflare のログに記録 今後の発展 # このパイプラインを基盤として、以下のような機能拡張も検討できます。 デプロイ前の自動テスト実行 複数環境(ステージング/本番)への対応 デプロイ失敗時の自動ロールバック Slack/Discord への通知連携 自宅サーバーでの Web ホスティングは、学習や実験に最適な環境だなぁと感じました。大変楽しかったです☺️
はじめに # 先月 Immutable releases が GA になりました。 https://github.blog/changelog/2025-10-28-immutable-releases-are-now-generally-available/ これによりリリースが公開後に変更されていないことを確認でき、改ざんや偶発的な変更を回避できるようになります。 変更不可リリースの特徴 # ドキュメントは以下で参照できます。 https://docs.github.com/ja/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases 変更不可リリースは以下のような特徴があります。 Git タグは移動または削除できない :リリースに関連付けられている Git タグは特定のコミットにロックされ、変更または削除することはできなくなります。 リリースアセットを変更または削除することはできない :リリースにアタッチされているすべてのファイルは、変更または削除から保護されます。 リリース構成証明が自動的に生成され、リリース タグ、コミット SHA、アセットなどの検証が可能になります。 変更不可リリースが有効な場合、そのリポジトリを削除し、同じ名前の新しいリポジトリを作成した場合でも、元のリポジトリの変更不可リリースに関連付けられたタグを再利用することはできなくなります。これは強力な保護機能ですね。 使ってみる # 筆者がメンテナンスしている野良 Cosense アプリ sbe のリポジトリで設定してみました。 --> Information Immutable releases は、リポジトリ単位、オーガニゼーション単位で設定可能です。 一度リリースしてしまうとリポジトリのコミッタでも変更はできないため、以下のようにドラフトリリースで作業する手順が推奨されています。 ドラフトリリースを作成 全てのアセットをドラフトリリースにアタッチ ドラフトリリースを正式リリースにする ちょうど sbe の変更が溜まっていたので、ベストプラクティスに従ってリリースドラフトを作成します。 保存されたドラフトリリースはまだ非公開で変更可能です。 --> Information このアプリのリリース用ワークフローではタグ作成を契機にリリースを作るようにしています。 softprops/action-gh-release という Action を使っています。リリース成果物のアタッチもやってくれます。 prerelease を true にすることで、ドラフトリリースを作成してくれます。 - name: Publish uses: softprops/action-gh-release@v2 with: files: | dist/**/*.exe dist/**/*.deb dist/**/*.AppImage dist/**/*.dmg prerelease: true 添付ファイルや差分を指差し確認してリリースを発行します。 Immutable release を有効にしていると確認のダイアログが出ます。 発行されたリリースには Immutable マークが付きました。 アセットには、リリースの attestation (証明) の JSON ファイルも追加されています。 編集画面では、リリースの説明などは編集可能ですが、タグやアセットは編集できない旨のメッセージが表示されます。 リリースを検証する # 利用者は Immutable release で作成された変更不可リリースを GitHub CLI で検証できます。 https://docs.github.com/ja/code-security/supply-chain-security/understanding-your-software-supply-chain/verifying-the-integrity-of-a-release?utm_medium=changelog&utm_campaign=universe25 リリースが存在し、かつ不変であることを検証するには、クローンしたリポジトリのディレクトリ内で release verify コマンドを実行します。 gh release verify RELEASE-TAG sbe の v3.8.0 リリースを検証すると GitHub API を使って証明を読み取り表示してくれます。 $ gh release verify v3.8.0 Resolved tag v3.8.0 to sha1:1f3f380d33f022230046a3200a67950ea027c8a1 Loaded attestation from GitHub API ✓ Release v3.8.0 verified! Assets NAME DIGEST sbe-3.8.0-universal.dmg sha256:ab1c2595601136bf82aa7594d48bc764fe6f226ed1071c52441eb531f34e0252 sbe-3.8.0.AppImage sha256:de1797b12152531df71e78519d660e32e1a79dca203bc3201d85b2facfe4b5a9 sbe-Setup-3.8.0.exe sha256:a4a8d6fe8ddde6e1a2005a29d7e2759511cb537427e4a0ff03440c5e9f48fb94 ローカルにある成果物がリリース成果物と完全に一致していることを検証するには release verify-asset コマンドを使用します。 gh release verify-asset RELEASE-TAG ARTIFACT-PATH リリースのアセットから macOS 用ユニバーサルインストーラのバイナリをダウンロードして検証してみました。 $ gh release verify-asset v3.8.0 ~/Downloads/sbe-3.8.0-universal.dmg Calculated digest for sbe-3.8.0-universal.dmg: sha256:ab1c2595601136bf82aa7594d48bc764fe6f226ed1071c52441eb531f34e0252 Resolved tag v3.8.0 to sha1:1f3f380d33f022230046a3200a67950ea027c8a1 Loaded attestation from GitHub API ✓ Verification succeeded! sbe-3.8.0-universal.dmg is present in release v3.8.0 --> Information アセットの証明には Sigstore の署名技術が利用されています。ソフトウェアの出所情報を検証可能し、ソフトウェアサプライチェーンの安全性を高めるための技術です。かなり前の記事ですが以下で紹介しています。 https://developer.mamezou-tech.com/blogs/2022/08/17/github-actions-workflows-for-software-supply-chain-security/ さいごに # 以上、Immutable releases の紹介でした。一定数ユーザーがいる OSS では変更不可リリースを採用する方がいいでしょう。 GitHub Actions でもサードパーティの Action を利用する際は、不意な変更の影響を受けないためにバージョンだけでなくコミットハッシュまで指定して固定することもあります。変更不可リリースを採用してくれる Action が増えれば使う側も安心ですね。
Q Developerとは # Q Developer は、AWSが提供する 生成AIによる開発支援ツール です。 ChatGPT や Copilot のように「コードを補完するAI」ではなく、 設計・実装・テスト・レビュー・ドキュメント化までを支援する開発プラットフォーム として設計されています。 特徴を一言で言うと、 AWSの開発を、AIと一緒に進められる統合環境 です。 主な機能と特徴 # 項目 概要 自然言語での開発支援 「Lambda関数を作って」「DynamoDBと連携して」と自然言語で指示できる コード生成と補完 Python, TypeScript, Javaなど主要言語をサポート テストコード自動生成 既存コードからテストを推論して自動生成 レビューと改善提案 コード品質・例外処理・命名規約などをAIが自動レビュー AWS統合 Lambda / API Gateway / DynamoDB / CDK などのAWSリソースを直接扱える VS Code統合 拡張機能からインラインでAIに指示・生成・修正が可能 CLIとVS Codeの2つの使い方 # Q Developerには2つの利用スタイルがあります。 スタイル 概要 コマンド例 CLI(コマンドライン) ターミナル上でAIに直接指示を出す $ q generate lambda --name hello-world VS Code拡張 エディタ内で自然言語プロンプトを入力し、コード生成・修正を行う コマンドパレットで「Q Developer: Generate Code」 Pro版を利用していれば、CLIとVS Codeの両方からAWSサービスを直接操作できるようになります。 “Q”の由来と目的 # 「Q」は “Quality(品質)” と “Question(問い)” の両方を意味します。 このツールの思想は、「AIがコードを書く」のではなく、 開発者がAIと会話しながら品質を作り込む という協働開発にあります。 したがって、Q Developerは単なる補助ツールではなく、 設計レビューから実装・改善までを一貫して支援する“AI開発パートナー” です。 今回は主に Q CLI を使って簡単なアプリケーションを作っていきます。 1. Q Developer CLIの概要 # CLI(コマンドラインインターフェース) では高度な操作が可能になります。 CLIでは、ターミナル上でAIと対話(インタラクティブモード)できるだけでなく、以下のようなコマンドで自動処理や統合が行えます。 機能 説明 コード生成 Lambda・API Gateway・CDK構成などをAIが生成 レビュー 既存コードの品質を分析し、改善案を提示 修正と再生成 問題箇所をAIが自動修正 テスト生成 単体テストコードを自動で生成・実行 ドキュメント化 READMEや設計書を自動出力 動作確認例 # 以下が表示されれば、環境は正しく構成されています。 コマンドラインですべて完結させることができますが、初回の試みということもあるので、 今後はすべて対話形式のインタラクティブモードで実行 します。 $ q --version q 1.13.0 $ q help # 利用可能なサブコマンド一覧を表示 # generate / review / test / fix / metrics / deploy など 2. Q CLIの起動とインタラクティブモード # 以下のコマンド「 q 」を実行します。 $ q 以下のような画面が表示されます。 ⢠⣶⣶⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⢀⣤⣶⣿⣿⣿⣶⣦⡀⠀ ⠀⠀⠀⣾⡿⢻⣿⡆⠀⠀⠀⢀⣄⡄⢀⣠⣤⣤⡀⢀⣠⣤⣤⡀⠀⠀⣠⣤⣤⣤⣄⠀⠀⢀⣤⣤⣤⣤⣤⣤⡀⠀⠀⣀⣤⣤⣤⣀⠀⠀ ⠀⢠⣤⡀⣀⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⢠⣿⣿⠋⠀⠀⠀⠙⣿⣿⡆ ⠀⠀⣼⣿⠇⠀⣿⣿⡄⠀⠀⢸⣿⣿⠛⠉⠻⣿⣿⠛⠉⠛⣿⣿⠀⠀⠛⠉⠉⠻⣿⣧⠀⠈⠛⠛⠛⣻⣿⡿⠀⢀⣾⣿⠛⠉⠻⣿⣷⡀⠀ ⢸⣿⡟⠛⠉⢻⣿⣷⠀⠀⠀⠀⠀⣼⣿⡏⠀⠀⠀⠀⠀ ⢸⣿⣿ ⠀⢰⣿⣿⣤⣤⣼⣿⣷⠀⠀⢸⣿⣿⠀⠀⠀⣿⣿⠀⠀⠀⣿⣿⠀⠀⢀⣴⣶⣶⣶⣿⣿⠀⠀⠀⣠⣾⡿⠋⠀⠀⢸⣿⣿⠀⠀⠀ ⣿⣿⡇⠀⢸⣿⡇⠀⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⢹⣿⣇⠀⠀⠀⠀ ⠀⢸⣿⡿ ⢀⣿⣿⠋⠉⠉⠉⢻⣿⣇⠀⢸⣿⣿⠀⠀⠀⣿⣿⠀⠀⠀⣿⣿⠀⠀⣿⣿⡀⠀⣠⣿⣿⠀⢀⣴⣿⣋⣀⣀⣀⡀⠘⣿⣿⣄⣀⣠⣿⣿⠃⠀⢸⣿⡇⠀⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⠈⢿⣿⣦⣀⣀⣀⣴⣿⡿⠃ ⠚⠛⠋⠀⠀⠀⠀ ⠘⠛⠛⠀⠘⠛⠛⠀⠀⠀⠛⠛⠀⠀⠀⠛⠛⠀⠀⠙⠻⠿⠟⠋⠛⠛⠀⠘⠛⠛⠛⠛⠛⠛⠃⠀⠈⠛⠿⠿⠿⠛⠁⠀⠀⠘⠛⠃⠀ ⠘⠛⠛⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠿⢿⣿⣿⣋⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀     ⠈⠛⠿⢿⡧ ╭─────────────────────────────── Did you know? ────────────────────────────────╮ │ │ │ If you want to file an issue to the Q CLI team, just tell me, or run q │ │ issue │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ /help all commands • ctrl + j new lines • ctrl + s fuzzy search ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🤖 You are chatting with claude-4-sonnet この状態が「 インタラクティブモード 」です。 自然言語で直接話しかけて、AIと一緒に設計・実装を進めることができます。 対話例 # 以下の文章を入力します。 > DynamoDB にデータを保存する Lambda 関数を作成して 応答は次のようになりました。 > DynamoDBにデータを保存するLambda関数を作成します。 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/q/lambda_dynamodb_jp.py + 1: import json + 2: import boto3 + 3: + 4: dynamodb = boto3.resource('dynamodb') + 5: table = dynamodb.Table('your-table-name') + 6: + 7: def lambda_handler(event, context): + 8: try: + 9: # 入力データの検証 + 10: if not event or not isinstance(event, dict): + 11: return { + 12: 'statusCode': 400, + 13: 'body': json.dumps('無効な入力: イベントは空でないオブジェクトである必要があります', ensure_ascii=False) + 14: } + 15: + 16: # DynamoDBにデータを保存 + 17: table.put_item(Item=event) + 18: + 19: return { + 20: 'statusCode': 200, + 21: 'body': json.dumps('データが正常に保存されました', ensure_ascii=False) + 22: } + 23: except Exception as e: + 24: return { + 25: 'statusCode': 500, + 26: 'body': json.dumps(f'エラー: {str(e)}', ensure_ascii=False) + 27: } ⋮ ↳ Purpose: DynamoDBにデータを保存するLambda関数を作成 Creating: /home/ubuntu/q/lambda_dynamodb_jp.py ⋮ ● Completed in 0.1s > Lambda関数を作成しました。主な機能: • boto3を使用してDynamoDBと連携 • イベントデータの入力検証 • DynamoDBテーブルへのデータ保存 • 日本語でのエラーメッセージ • 適切なHTTPステータスコードを返却 使用方法: 1. 'your-table-name'を実際のDynamoDBテーブル名に変更 2. Lambda実行ロールにdynamodb:PutItem権限を付与 3. 関数はイベントで渡されたJSONデータをDynamoDBに保存します 3. 対話で進めるプログラム開発体験 # Q Developer CLI( q )を使って、AIと自然言語で会話しながら、シンプルなアプリケーション(ここでは「テトリス」ゲーム)を一緒に作り上げる流れを紹介します。 $ q 🤖 You are chatting with claude-4-sonnet 3.1 シナリオ設定 # 今回作成するテトリスの要件は以下のとおりです。 ①:通常のテトリスが備えている操作は一通り持っている ②:単なるブロックではなく、動物のアイコン(例えば猫)を使う ③:猫アイコンの表情も一通りではなく、ブロックが積まれてくると、猫アイコンの表情を変える ④:格納場所はAWSのS3として、グローバルでアクセスできるように設定する 3.2 Q Developerとの対話開始 # 指示:(概要+詳細(上記の要件の①と②)) テトリスを作ってください。 テトリスが備えているであろう普通の機能(ブロックの回転、位置決め、落下など)を備えてください。 ブロックは通常のブロックではなく、猫のアイコン画像(画像の選定は任せます)を使ってください。 応答: > 猫のアイコンを使ったテトリスゲームを作成します: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/q/cat-tetris.html + 1: <!DOCTYPE html> + 2: <html> + 3: <head> + 4: <meta charset="UTF-8"> + 5: <title>猫テトリス</title> + 6: <style> + 7: body { margin: 0; padding: 20px; background: #222; color: white; font-family: Arial; text-align: center; } + 8: canvas { border: 2px solid #fff; background: #000; } + 9: .score { font-size: 20px; margin: 10px; } + 10: .controls { margin: 10px; font-size: 14px; } + 11: </style> + 12: </head> ~~~(中略)~~~ + 239: // Start game + 240: updateScore(); + 241: requestAnimationFrame(gameLoop); + 242: </script> + 243: </body> + 244: </html> ⋮ ↳ Purpose: Create cat tetris game with standard tetris features Creating: /home/ubuntu/q/cat-tetris.html ⋮ ● Completed in 0.1s > 猫のアイコンを使ったテトリスゲームを作成しました! 主な機能: • **ブロック回転**: ↑キーで回転 • **左右移動**: ←→キーで移動 • **高速落下**: ↓キーで高速落下 • **ハードドロップ**: スペースキーで瞬間落下 • **ライン消去**: 横一列が揃うと消去 • **スコアシステム**: ライン消去でスコア獲得 • **レベルアップ**: スコアに応じて落下速度が上がる • **猫のアイコン**: 7種類の猫の絵文字をランダム使用 操作方法: • ←→: 左右移動 • ↓: 高速落下 • ↑: 回転 • スペース: ハードドロップ ファイルを開いてブラウザでプレイできます! どうやら「 /home/ubuntu/q/cat-tetris.html 」が成果物のようです。 (出力を明確に指定していなかったので、Qが任意の保存場所とファイル名を用意したようです) HTMLファイルを開いてみます。 猫テトリスが動いています。 猫アイコンもそれぞれ表情が違っていて面白いです。 しかし、最初から猫が泣いていたりして、法則性が無いように思います。 どのように猫の表情を変化させているのかを確認してみます。 指示: > 猫アイコンはどういうロジックで変化しているのですか? 応答: > 猫アイコンの変化ロジックは以下の通りです: 1. ピース生成時にランダム決定: javascript this.catType = Math.floor(Math.random() * CAT_EMOJIS.length); 新しいテトリスピースが生成される際に、7種類の猫絵文字(🐱😺😸😹😻🙀😿)からランダムに1つ選択されます。 2. ピース全体で同じ猫: 各テトリスピース(I、O、T、S、Z、J、L)は生成時に決まった1種類の猫アイコンで統一されます。 ~~~(中略)~~~ 4. 表示時の変換: javascript drawBlock(x, y, board[y][x] - 1); 描画時に-1して元の猫タイプインデックスに戻します。 つまり: 各テトリスピースごとに猫の種類がランダムに決まり、そのピース内のすべてのブロックは同じ猫アイコンになります。ピー スが異なれば猫の種類も変わる可能性があります。 質問を投げかけると、このように応答します。 ただ、ランダムだと面白くないので、さらに制約(詳細)を追加していきます。 指示:(要件:③) ゲームの序盤では余裕の猫で、ブロックが高く積みあがってくるにしたがって、余裕のない表情の猫にしてください。 応答: > ゲームの状況に応じて猫の表情が変わるように修正します: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: cat-tetris.html - 29 : // 猫の絵文字を使用 - 30 : const CAT_EMOJIS = ['🐱', '😺', '😸', '😹', '😻', '🙀', '😿']; + 29: // 猫の絵文字を使用(余裕→緊張の順) + 30: const CAT_EMOJIS = ['😻', '😺', '😸', '🐱', '😹', '🙀', '😿']; ~~~(中略)~~~ > 修正完了しました! 新しい猫の表情システム: • **😻 (余裕)**: ブロックが低い位置にある時 • **😺 (まだ余裕)**: 少し積み上がってきた時 • **😸 (少し心配)**: 中程度の高さ • **🐱 (普通)**: 半分くらいの高さ • **😹 (緊張)**: かなり高く積み上がった時 • **🙀 (焦り)**: 危険な高さ • **😿 (絶望)**: ゲームオーバー寸前 ブロックが高く積み上がるほど、猫の表情が余裕から絶望へと変化します。ゲームの緊張感が猫の表情で表現されるようになりました! 実行して制約を確認します。 以下が実行結果です。 今度は余裕が無くなってくる(ブロックが積み上がる)と、猫アイコンの表情が(余裕→絶望)に変わっていくのがわかります。 ただ、ゲームを少しプレイしていると以下のバグが見つかりました。 横4つつながったブロックが上↑キーで回転しない。 2回目以降のゲーム再開で、最初から絶望や焦りの表情をした猫ブロックが登場する。 バグの修正を「日本語」で指示します。 指示:(バグ修正依頼) バグがあります。修正してください。バグは以下です。 1.横4つつながったブロックが上↑キーで回転しない。 2.2回目のゲームで、最初から絶望や焦りの猫の表情のブロックが登場する。 応答: > バグを修正します: 1. I字ピース(横4つ)の回転バグ修正: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: cat-tetris.html 39, 39: const PIECES = [ 40, 40: // I piece - 41 : [[[1,1,1,1]]], + 41: [[[1,1,1,1]], [[1],[1],[1],[1]]], ⋮ ↳ Purpose: Fix I-piece rotation by adding vertical rotation state Updating: cat-tetris.html ⋮ ● Completed in 0.0s > 2. ゲームリセット時のボード初期化バグ修正: 🛠️ Using tool: fs_read (trusted) ⋮ ● Searching: /home/ubuntu/q/cat-tetris.html for pattern: ゲームオーバー ✓ Found 1 matches for pattern 'ゲームオーバー' in /home/ubuntu/q/cat-tetris.html ⋮ ● Completed in 0.0s 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/q/cat-tetris.html, from line 224 to 234 ✓ Successfully read 464 bytes from /home/ubuntu/q/cat-tetris.html ⋮ ~~~(中略)~~~ ⋮ ↳ Purpose: Fix board reset to ensure proper initialization Updating: cat-tetris.html ⋮ ● Completed in 0.0s > 修正完了しました! 修正内容: 1. I字ピースの回転: 横4つの状態と縦4つの状態を追加し、↑キーで回転できるようになりました 2. ゲームリセット: ボードを完全に0で初期化し、新しいピースを生成するようにしたので、2回目のゲームでも正しく余裕のある猫から始まります これでバグが解消され、正常にゲームが動作するはずです! 修正版のゲームを再度実行します。 回転できなかった横一列のブロックも回転できるようになっています。 また、ゲーム再開時の猫アイコンも正しく表示されるようになっていました。 3.3 AWS S3にアップロード # 出来上がったテトリスアプリケーションはHTMLファイルが一つだけの単純なものです。 このアプリケーションをAWS S3にアップロードしてみます。 (AWS S3へのアクセス権を持っていることが条件です) 指示:(要件:④) ゲームをAWS S3にアップロードして。 アップロード先は新しいバケットを作成して。 応答: > 新しいバケットを作成して猫テトリスをアップロードしますね。 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3 mb s3://cat-tetris-game-$(date +%s) ⋮ ↳ Purpose: 新しいS3バケットを作成 make_bucket: cat-tetris-game-<バケットID> ⋮ ● Completed in 1.573s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3 cp /home/ubuntu/cat-tetris.html s3://cat-tetris-game-<バケットID>/cat-tetris.html --content-type text/html ⋮ ↳ Purpose: 猫テトリスファイルを新しいバケットにアップロード upload: ./cat-tetris.html to s3://cat-tetris-game-<バケットID>/cat-tetris.html ⋮ ● Completed in 0.655s > 完了しました!新しいバケット cat-tetris-game-<バケットID> を作成して、猫テトリスファイルをアップロードしました。 アップロードが完了したようですが、アクセス方法がわかりません。 最終的にどのようにアクセスしたいかを伝えていませんでした。 Q Developerに<最終成果物のイメージ>を伝えます。 指示: URLでアクセスしたいので、URLを教えて 応答:(バケットIDは実際に出力されたものから変更しています) > 現在はS3バケットがプライベートなので、Webからアクセスできません。パブリックアクセスを有効にする必要があります。 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3api put-public-access-block --bucket cat-tetris-game-<バケットID> --public-access-block-configuration "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false" ⋮ ↳ Purpose: バケットのパブリックアクセスブロックを無効化 ⋮ ● Completed in 0.989s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3api put-bucket-policy --bucket cat-tetris-game-<バケットID> --policy '{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::cat-tetris-game-<バケットID>/*" } ] }' ⋮ ↳ Purpose: バケットポリシーでパブリック読み取りを許可 ⋮ ● Completed in 1.112s > これで以下のURLでアクセスできます: https://cat-tetris-game-<バケットID>.s3.amazonaws.com/cat-tetris.html ブラウザでこのURLを開けば猫テトリスがプレイできます! 指定されたURLを開きます。 ローカルで実行したものと同じテトリスを、AWS S3上で実行することが出来ました。 4. 指示の出し方の基本パターン # AIとの対話で成果を出すには、 「どのように伝えるか」 が最も重要です。 Q Developerは自然言語を理解しますが、 明確で構造的な指示 ほど精度の高いコードを生成します。 4.1 指示の基本構造 # AIにタスクを伝えるときは、次の3段階を意識します。 段階 内容 例 ① ゴール 何を達成したいか 「DynamoDBにデータを保存するAPIを作りたい」 ② 制約 技術的な条件・使用技術 「Python 3.11で、Lambda関数として実装して」 ③ 期待結果 出力してほしい成果物の形 「ハンドラー関数とテストコードを生成して」 この3要素を順番に伝えるだけで、出力の再現性と品質が大きく向上します。 4.2 指示の粒度を使い分ける # Q Developerへの指示は、大きく「 部分指示 」と「 全体指示 」の2種類があります。 種類 対象範囲 使い方の例 部分指示 関数・モジュールなど局所的な修正 「この関数をリファクタして、例外処理を標準化して」 全体指示 プロジェクト全体や設計方針 「全APIのレスポンス形式を統一して、共通エラーハンドラを追加して」 部分指示では即時的な改善を狙い、全体指示では設計方針の整合性を図ります。 この2つを組み合わせることで、AIに 意図の上下関係 を理解してもらうことができます。 4.3 フィードバックを循環させる # AIが出した結果に対して「そのまま使う」のではなく、 誤解 → 修正 → 再生成 のサイクルを回すことが重要です。 例: AI:Lambda関数を作成しました。DynamoDBに書き込みます。 ↓ 開発者:このコードではテーブル名が固定なので、環境変数から取得するようにして。 ↓ AI:了解しました。修正したコードを以下に示します。 このように、AIとの対話を「レビューの往復」として扱うと、より高品質なコードを一緒に育てることができます。 4.4 良い指示と悪い指示の比較 # 指示の例 AIの反応 評価 「DynamoDB対応のLambda作って」 動くが、スキーマや例外処理が曖昧 ❌ 不明確 「DynamoDBにタスクを登録するLambda関数を作成。titleとstatusを受け取り、statusの初期値はpendingにして」 適切なハンドラーとバリデーションを自動生成 ✅ 明確 「関数を改善して」 どこをどう直すか分からない ❌ 抽象的 「この関数のエラーハンドリングを追加し、ログに例外内容を出力して」 コードの修正意図を正確に反映 ✅ 具体的 「短い指示」よりも「明確な文脈を含む指示」が結果を良くします。 4.5 応用:AIをレビューアとして使う # AIに “設計者” や “品質管理者” の視点を持たせることも可能です。 たとえば次のような指示が有効です。 このコードの保守性とテスト容易性の観点から、改善点を3つ挙げて。 または、 このAPI設計をREST原則に照らしてレビューして。 これにより、Q Developerは単なる生成エンジンから「レビューAI」として機能し、開発者の品質意識を高めるパートナーになります。 まとめ # 指示は「 ゴール → 制約 → 期待結果 」の順に伝える 粒度(部分/全体)を意識して構造的に指示する フィードバックを繰り返して精度を上げる 最小構成で動かしながら、 AIとの会話 を通じて開発プロセスを理解する Q Developerは「AIがコードを書くツール」ではなく、「 AIと共に設計・実装・品質を磨くためのプラットフォーム 」です。 皆さまの生成AI活用の参考になれば幸いです。 img { border: 1px gray solid; }
はじめに # 社内プロジェクトの 営業支援システム(Sales Support System、以下、SSS) 開発で導入した Webhook のイベントキューイングの Terraform での構築手順を紹介します。 背景 # SSS ではワークフローを提供する SaaS と稟議の進捗イベントを Webhook 連携することでデータのステータス管理をしています。 初期の段階では優先度や工数の制約により、直接呼び出しで運用が開始されました。 しかし、以下にあげる事情により、イベントのキューイングを導入することにしました。 想定していた機能開発が完了し、先送りしていた機能改善に着手する工数ができた。 優先度や頻度の多い他のエラーが解消されて優先度が上位になった。 運用リカバリで、ただでさえ少ない工数なのに手動データパッチの手間やワークフローの再申請などの利用者の負担となることも。 [1] 運用保守向けの補足的な機能で、技術的な選択の自由度が高い。 キューイング機能に対する要件 # 実際に SaaS 連携イベントのキューイングを導入するに当たり、以下のような要件を満たすべく、いくつかの AWS サービスを比較検討しました。 メッセージを取りこぼさないでほしい。 順番を保証してほしい。 順序を保証してほしいイベントは状態が遷移しないと次のイベントが出せないので実質的な問題は発生しないが仕組みとして保証できればしたい。 受信失敗したときにメッセージが残っていてほしい。 失敗したメッセージを簡単に再送出来るとなお良し。 ECS の SSS サービスとは独立させたい。 独立していないとリプレースで ECS サービス停止中に同じ問題が発生してしまう。 既存の SSS サービスへの修正ができるだけ少ない方が良い。 追加機能だけが依存するのがベスト。 どうせならサーバレスなサービスを利用したい。 機能比較/検討 # 以上の要件を踏まえて機能比較表を作成して評価しました。 [2] 本当は重みがありそうですが、ポイントは単純に〇(2)、△ と?(1)、×(0)で換算しています。 案 サービス タイプ 順序 exactly-once サーバレス API GW 統合 [3] 送信失敗時 振り分け ポイント 備考 1 SQS 標準 × × 〇 〇 DLQ Lambda 4 2 SQS FIFO 〇 〇 〇 〇 DLQ Lambda 8 3 SNS 標準 × × 〇 ×(Lambda) ? SNS 4(3-5) 4 SNS FIFO 〇 〇 〇 ×(Lambda) ? SNS 6(5-7) 5 Kinesis DataStream 〇 ? 〇 〇 ? Lambda 7(5-9) 6 SNS+SQS FIFO+FIFO 〇 〇 〇 ×(Lambda) DLQ SNS 6 7 SQS+SNS FIFO+FIFO 〇 〇 〇 〇 DLQ Lambda 8 なお、DLQ(Dead Letter Queue)は正常に処理できなかったメッセージを一時的に保存するための特別なメッセージキューのことです。 --> SNS の送信失敗 当時は見つけられなかったのか、比較表では SNS の送信失敗時は「?」となっていますが、SNS も DLQ があるようです。 Amazon SNS デッドレターキュー - Amazon Simple Notification Service 実体は SQS の DLQ に連携するらしいですが。 比較表からポイントで単純に絞り込んで案 2 か案 7 のいずれか。 1 ポイント差の案 5 も惹かれるけど「イベントストリーム」というほどデータは来ないので廃案。 案 7 は振り分けに SNS が使えないかと考えたがキュー自体を分けるか結局 Lambda を利用する必要があったので組み合わせのメリットがなくなったため廃案。 大したデータ量と頻度もないのに複数のキューに分けて管理とかしたくないのも理由。 1 つのキューにすると結局は Lambda で振り分けになる。 これだと SNS 意味がないのでは? 案 2 に無駄に SNS が追加されただけになる。 案 2 はメッセージグループ ID で Lambda が振り分け。 メッセージグループ ID は API Gateway との統合で設定可能(ルート(URL パス)ごとにできる)。 以上の検討の結果、以下の AWS サービス構成と呼び出しフローとすることになりました。 補足事項 # SQS のイベント監視の Lambda のポーリングは実体がそうなっているだけで実装するわけではない。 イベントソースとして SQS を指定するだけ。 直接 CloudMap を呼び出したかったが上手くいかなかった。 サービスディスカバリで CloudMap の登録サービスの取得まではいけたが、呼び出しが戻ってこないでタイムアウトする。 同じ URL で踏み台サーバから curl で呼び出したら出来たのに AWS Lambda からだとうまくいかなかった。 設定とかいろいろやれば行けるのかもしれないが、後日の課題とした。 当たり前だが、Amazon API Gateway 経由では行けたのでこちらの方式で対応することにした。 結局 Amazon API Gateway のパスがさらされたままだから、SSS サービスを直接呼べるように将来はしたいところ。 構築の前提事項 # 外部システムから既存システムの Webhook の呼び出しの間にキューを差し込む形になるため、以下が前提となっています。 IaC で Sales Support System のインフラ構築 で紹介した API Gateway 経由で ECS サービスを呼び出すシステムが既に構築されていること。 ECS サービスは Webhook 用の API が公開されていること。 この記事では 2 つ目の前提の代替として AWS Lambda の統合を利用するものとします。 次章から具体的な実装について説明していきます。 メッセージキュー # まずは 以下の 2 つの AWS SQS の作成をしていきます。 Webhook 用メッセージキュー DLQ Webhook 用メッセージキュー # メインとなる Webhook 用のメッセージキューの作成です。 AWS SQS では 2 種類のキューがありますが、今回は FIFO キューを利用します。 fifo_queue を true にしていますが FIFO の場合はキュー名のサフィックスが .filo でなければなりません。 また、DLQ を利用するため、関連付け( deadLetterTargetArn )が必要となります。 他にはメッセージの重複判定をコンテンツベースにするのと可視性タイムアウト(処理中に他からメッセージが見えなくなる時間)を設定しています。 main.tf resource "aws_sqs_queue" "webhook_queue" { name = "${local.webhook_queue_name}.fifo" fifo_queue = true content_based_deduplication = true visibility_timeout_seconds = local.processing_timeout redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.webhook_dlq.arn maxReceiveCount = var.webhook.max_receive_count }) } DLQ # 次に DLQ の作成になります。 メインのメッセージキューよりシンプルに定義できます。 キュー名についてはメインのメッセージキューと同様に .fifo サフィックスが必要です。 リカバリ処理で失敗したメッセージ内容の確認をするまでの時間を調整するため、保持期間( message_retention_seconds 、デフォルトは 4 日間)を外部変数で指定しています。 main.tf # DLQ resource "aws_sqs_queue" "webhook_dlq" { name = "${local.webhook_queue_name}-dlq.fifo" fifo_queue = true message_retention_seconds = var.webhook.dlq_retention_second } キューイング用 Webhook API # 今回は SSS 同様に API Gateway は既存のものがある前提となるため、新たに SQS が受けるための設定を API Gateway へ追加することになります。 具体的には以下のものになります。 ルート 統合 なお、API Gateway 自体の構築については IaC で Sales Support System のインフラ構築 の記事を参照ください。 キューイング Webhook API に対するルート # 今回は JWT 認証しないため、以前のアプリケーション用のルートよりもシンプルになります。 ルートキーのパスは /sqs-hook としています。 HTTP メソッドは SSS で利用している SaaS の指定( POST )に合わせています。 なお、API Gateway の ID については既存の参照としてデータソースを利用しています。 API Gateway 自体も新規に作成する場合は通常の AWS リソースへの参照となります。 integration.tf resource "aws_apigatewayv2_route" "webhook_event_route" { api_id = data.aws_apigatewayv2_api.this.id route_key = "POST /sqs-hook" target = "integrations/${aws_apigatewayv2_integration.webhook_event_producer.id}" } SQS との統合 # 続いて API Gateway と SQS を関連付けるための統合を作成します。 integration_subtype として SQS-SendMessage を指定しています。 これによって SQS への送信用として統合されます。 更に request_parameters で以下の設定をします。 [4] キューイング Webhook API に対する URL(必須) メッセージグループ ID メッセージボディ(必須) integration.tf resource "aws_apigatewayv2_integration" "webhook_event_producer" { description = "Queue of Webhook Event" api_id = data.aws_apigatewayv2_api.this.id integration_type = "AWS_PROXY" integration_subtype = "SQS-SendMessage" credentials_arn = aws_iam_role.webhook_event_producer_role.arn request_parameters = { "QueueUrl" = aws_sqs_queue.webhook_queue.url "MessageGroupId" = local.message_group_id "MessageBody" = "$request.body" } } SQS 送信のための IAM ロール # API Gateway が SQS にメッセージ送信するための権限を付与するための IAM ロールを作成します。 API Gateway に対するロールなので信頼ポリシー( apigateway_assume_role )の principals に API Gateway を指定します。 付与するポリシーは SQS への送信のみのため actions として sqs:SendMessage のみを指定します。 これらを API Gateway 統合用の IAM ロールに関連付けます。 念の為ですが、 aws_iam_role_policies_exclusive も指定しておきます。 integration.tf data "aws_iam_policy_document" "apigateway_assume_role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["apigateway.amazonaws.com"] } } } data "aws_iam_policy_document" "sqs_send_only_policy" { statement { actions = ["sqs:SendMessage"] resources = ["${aws_sqs_queue.webhook_queue.arn}"] } } resource "aws_iam_role" "webhook_event_producer_role" { name = "${local.prefix}-webhook-event-producer-role" assume_role_policy = data.aws_iam_policy_document.apigateway_assume_role.json } resource "aws_iam_role_policy" "sqs_integration_access_policy" { name = "sqs-integration-access-policy" role = aws_iam_role.webhook_event_producer_role.id policy = data.aws_iam_policy_document.sqs_send_only_policy.json } resource "aws_iam_role_policies_exclusive" "webhook_event_producer_role_policies" { role_name = aws_iam_role.webhook_event_producer_role.name policy_names = [ aws_iam_role_policy.sqs_integration_access_policy.name ] } SQS Labmda トリガー # SQS の準備ができたので、SQS からメッセージを受け取ってアプリケーションの Webhook に送信するための Lambda トリガーを作成します。 SQS Lambda トリガー用 Lambda 関数 # メッセージを受け取ったらアプリケーションの Webhook に送信するための AWS Lambda 関数を作成します。 SQS のトリガーとして AWS Lambda を関連付けるためにはキューの URL の環境変数と aws_lambda_event_source_mapping の定義が必要となります。 キューへの URL 指定は aws_lambda_function の環境変数で設定し、環境変数名は QUEUE_URL になります。 archive_file データソースなどの他の設定は通常の AWS Lambda と同様に行います。 設定の詳細は 本記事のリポジトリ のコードや Terraform のドキュメントを参照してください。 続いて aws_lambda_event_source_mapping を定義します。 イベントソースは当然ながら Webhook 用メッセージキューを指定します。 Lambda 関数も今回定義したトリガ用のものを指定します。 他にバッチサイズ(SSS は 1 つずつなので 1 )と同時処理最大数を設定しています。 main.tf resource "aws_lambda_function" "webhook_event_producer" { description = "Webhook Event Producer" function_name = local.webhook_event_producer_function_name handler = "${local.webhook_event_producer_module_name}.lambda_handler" filename = data.archive_file.webhook_event_producer.output_path source_code_hash = data.archive_file.webhook_event_producer.output_base64sha256 role = aws_iam_role.webhook_event_producer_execution_role.arn runtime = var.webhook.runtime architectures = ["arm64"] timeout = local.processing_timeout environment { variables = { QUEUE_URL = aws_sqs_queue.webhook_queue.url } } depends_on = [ aws_iam_role_policy_attachment.webhook_event_producer_basic_execution_role_attach, aws_cloudwatch_log_group.webhook_event_producer, ] } resource "aws_lambda_event_source_mapping" "webhook_event_producer_mapping" { event_source_arn = aws_sqs_queue.webhook_queue.arn function_name = aws_lambda_function.webhook_event_producer.function_name batch_size = 1 # 1つのメッセージごとに Lambda 関数を呼び出します scaling_config { maximum_concurrency = var.webhook.max_concurrency } } SQS Lambda トリガーのための IAM ロール # SQS Lambda トリガーは SQS からのメッセージを受信する権限のみを付与します。 システム構成図で SQS のイベント監視の AWS Lambda が SQS をポーリングしていましたが、ここにイベントソース処理実装の影響が出ています。 AWS Lambda のロジックには SQS のメッセージ受信処理はないのに、キューの確認やメッセージ受信、受信後のキューからのメッセージ削除などの権限が必要になっています。 main.tf data "aws_iam_policy_document" "sqs_receive_message_policy" { statement { actions = [ "sqs:ReceiveMessage", "sqs:ChangeMessageVisibility", "sqs:DeleteMessage", "sqs:GetQueueAttributes" ] resources = ["${aws_sqs_queue.webhook_queue.arn}"] } } resource "aws_iam_role" "webhook_event_producer_execution_role" { name = local.webhook_event_producer_execution_role_name assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json } resource "aws_iam_role_policy" "sqs_receive_message_policy" { name = "sqs-receive-message-policy" role = aws_iam_role.webhook_event_producer_execution_role.id policy = data.aws_iam_policy_document.sqs_receive_message_policy.json } resource "aws_iam_role_policies_exclusive" "webhook_event_producer_execution_role_policies" { role_name = aws_iam_role.webhook_event_producer_execution_role.name policy_names = [ aws_iam_role_policy.sqs_receive_message_policy.name, ] } resource "aws_iam_role_policy_attachment" "webhook_event_producer_basic_execution_role_attach" { role = aws_iam_role.webhook_event_producer_execution_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } SQS Lambda トリガー関数 # Lambda 関数は Python で実装しています。 Python ファイル内には 3 つの関数が定義されています。 lambda_handler extract_data_from_event send_request lambda_handler 関数は AWS Lambda のエントリポイント関数で主処理になります。 まず、第 1 引数で渡されたイベントから extract_data_from_event で宛先とメッセージを取り出します。 次に、 send_request で取得した宛先とメッセージを API Gateway のアプリケーションの Webhook API に転送します。 extract_data_from_event はデータ構造 [5] をチェックしながら、メッセージグループ ID とメッセージ自体を取り出します。 イベントは Python では dict として扱うことができます。 メッセージグループ ID からアプリケーションの URL に変換して、その URL とメッセージ内容を返します。 send_request は引数で渡された元のアプリケーションの Webhook API の URL に対してメッセージを HTTP の POST メソッド で呼び出します。 API Gateway の呼び出しは普通に HTTP 通信すれば大丈夫です。 Python の Lambda 関数の実装に際して以下の点に注意してください。 データはエンコードする必要がある。 処理に失敗した(DLQ に入れる)場合は例外にする。 お行儀よく 4xx や 5xx のコードを返して正常終了にしていたら、DLQ にメッセージが転送されませんでした。 AWS Lambda 関数のコード詳細は 本記事のリポジトリ を参照ください。 データソース # API Gateway は IaC で Sales Support System のインフラ構築 で構築したものを取得します。 統合の定義などに API Gateway の ID が必要ですが、 aws_apigatewayv2_api データソースを直接使うと ID が必要になってしまうのでひと工夫しています。 data.tf data "aws_apigatewayv2_apis" "this" { protocol_type = "HTTP" name = var.apigw_name } data "aws_apigatewayv2_api" "this" { api_id = one(data.aws_apigatewayv2_apis.this.ids) } 最後に # SSS では定期リリースのおりに、作業前に通知しているにもかかわらず、SaaS からのメッセージを送る操作をしてしまうユーザがいました。 しかし、これで開発者もユーザもリリースのことを気にせずに作業できるようになりました。 キューイング機能をリリースしてから、実際に何度かリリース中に操作が行われてしまうことがありました。 ですが、DLQ にメッセージが保持されていたため、リリース後に再送することで、後続業務が支障なく進められました。 SSS アプリケーションサービスとは独立したキューとして作成することで、SSS と外部の SaaS との結合度を軽減することが出来ました。 更に、既存のシステムへの改修もなかったため、短期間での導入もできました。 また、DLQ からの再送も AWS 管理コンソールや AWS CLI の機能が使えたため、保守ツールの開発コストも抑えることも出来ました。 今回紹介した内容は IaC で Webhook イベントのキューイングを構築のリポジトリ からコードを入手可能です。 また、 IaC で Sales Support System のインフラ構築 のリポジトリコードと合わせることで、実際に動作させて確認することが出来ます。 運用工数削減に関しては他のエラー対応の改善(半自動化やチェック強化など)の一環でもあります。 ↩︎ 参考として AWS のメッセージングサービスの決定木を紹介している Decision Tree: choose the right AWS messaging service | Better Dev も参照。 ↩︎ AWS API Gateway V2 を使う場合(ECS 統合で利用しているため)。利用できない場合は Lambda 経由となるため Lambda の開発が追加になる。 ↩︎ request_paramters の項目は integration_subtype の値によって変わります。詳細は Integration subtype reference - Amazon API Gateway を参照。 ↩︎ イベントの具体的な構造は FIFO キューメッセージイベントの例 を参照。 ↩︎
はじめに # ちょっと時間が経ってしまいましたが、先月末 GitHub Copilot CLI がパブリックプレビューになりました。 https://github.blog/changelog/2025-09-25-github-copilot-cli-is-now-in-public-preview/ VS Code の拡張から始まった GitHub Copilot も Claude Code や Gemini と同様 CLI としても動作するようになりました。CLI として提供されることで、 IDE 縛りがなくなる パイプを使って他の CLI ツールとの連携が可能 という強みが追加されます。ターミナルだけで作業が完結できるという点にも魅力を感じる人は多いでしょう。 さらに GitHub にログイン状態で利用するため自分が関わっているリポジトリの操作もできますし、GitHub の MCP サーバーもすぐ利用ます。 日本語のドキュメントは以下から参照できます。 GitHub Copilot CLI の使用 - GitHub Docs --> Caution 従来 GitHub CLI の拡張として提供されていた gh-cpilot は Copilot CLI のパブリックプレビューに伴って非推奨となり、Copilot CLI に置き換えられます。 https://github.blog/changelog/2025-09-24-deprecate-github-copilot-extensions-github-apps/ gh-copilot については昨年2月紹介してました。 https://developer.mamezou-tech.com/blogs/2024/02/28/github-copilot-in-cli/ --> Information GitHub Copilot CLI は、GitHub Copilot Pro、GitHub Copilot Pro+、GitHub Copilot Business、GitHub Copilot Enterprise プランで使用できます。 インストールと起動 # ドキュメントに従ってインストールします。 https://docs.github.com/ja/copilot/how-tos/set-up/install-copilot-cli npm でグローバルインストール可能です。 npm install -g @github/copilot GitHub CLI を起動します。 copilot GitHub CLI の TUI が起動し GitHub へのログイン状態、GitHub MCP サーバへの接続状況が表示されます。 作業するフォルダの信頼について聞かれています。いつも $HOME/dev で作業してるので2番の Yes, and remember this folder for future sessions を選択しました。 --> Information 筆者の環境では GitHub CLI (Copilot CLI じゃない GitHub 操作用 CLI) で事前に GitHub にログインしているため、その認証トークンでログイン状態になっているのだと思います。 使ってみる # 使い方を理解する上では以下のページが役立ちます。 https://docs.github.com/ja/copilot/concepts/agents/about-copilot-cli 対話型モード # TUI 上でプロンプトを入力して対話的に作業を進めるモードです。 GitHub から clone した筆者が作っている Electron アプリのリポジトリ( sbe )のディレクトリに移動してから Copilot CLI を起動しました。 cd sbe copilot @ でファイルをメンションできます。 @ に続いてパスの一部を入力すると候補が列挙されます。 ソースコードの説明をしてもらいました。 日本語で質問すると日本語で回答してくれます。 対話モードを抜けると利用実績やコードの変更行数などが表示されます。 Total usage est: 2 Premium requests Total duration (API): 15.1s Total duration (wall): 12m 33.3s Total code changes: 0 lines added, 0 lines removed Usage by model: claude-sonnet-4.5 24.9k input, 414 output, 0 cache read, 0 cache write (Est. 2 Premium requests) Shutting down... プログラムモード # Copilot CLI に引数やパイプでプロンプトを与えて直接実行するモードです。 copilot -p "explain src/favs.js" TUI は起動せず直接プロンプトの結果が出力されます。 I'll read the src/favs.js file to explain it to you. ✓ Read src/favs.js (57 lines) This is a Vue.js 3 app that manages favorites in an Electron application. It creates a UI for displaying and deleting favorite items with these key features: **Core functionality**: Loads favorites from the Electron backend via `window.favsApi`, displays them in a list, and provides delete functionality with a confirmation dialog. It listens for window focus events to refresh the favorites list. **Theme support**: Automatically detects and applies light/dark mode based on system preferences using Vuetify's theming system. Total usage est: 1 Premium request Total duration (API): 11.5s Total duration (wall): 15.2s Total code changes: 0 lines added, 0 lines removed Usage by model: claude-sonnet-4.5 23.6k input, 239 output, 0 cache read, 0 cache write (Est. 1 Premium request) 結果は標準出力に出力され、続いて利用実績も表示されます。 ローカルタスク # ローカルにあるコードを変更するように指示できます。 まず、1つのソースコードのファイルについてのレビューを依頼し、改善ポイントを挙げてもらいました。 Review @src/about.js 良い点として、適切なライフサイクルフックを持つクリーンなコンポーネント構造とか、IPC による関心事の適切な分離などを誉めてくれています。 いくつか問題点を挙げてくれていますが、使用している Vue の beforeUnmount でフォーカス関連のリスナーを削除してないので、メモリリークの懸念があるというのが気になりました。 そこで、この問題を修正するように依頼。 Fix No cleanup problem beforeUnmount のフックメソッドと Listener 削除のコードが追加されました。リスナーを off で削除する API は存在しないため受け入れる変更ではありません。ですが、今は Copilot CLI の機能を試しているため、他のファイルにも同様に適用をお願いしてみました。ファイルごとに変更していいか聞かれます。 Apply this fix to other files too 全てのファイルに適用が終わりました。 --> Information この例では使用しませんでしたが、ローカルタスクにおいて sed とか chmod などの外部コマンドを使用する場合は、使用許可を聞いてきます。 --allow-tool で実行時に予め許可を与えることもできます。 GitHub タスク(issue 一覧取得) # GitHub の操作に関するタスクも実行できます。手始めにリポジトリの issue 一覧を表示させてみました。 list my open issues GitHub タスク(PR 作成) # 先ほど試した Electron アプリへのリスナー削除の変更が手元にあるので、そこから PR を作ってもらいます。 create a pull request from this changes ブランチを作って push までやってくれました。これをもとに PR を作るか確認が入りました。 Yes を選択すると PR が作成されました。 PR の作者は筆者自身となっています。 GitHub タスク(Actions ワークフロー実行) # GitHub タスクでは GitHub Actions ワークフローの操作も可能です。まず、このリポジトリのワークフローを列挙させてみました。 列挙されたワークフローのうち OS Matrix は、クロスプラットフォームで Electron アプリのテストを実行するワークフローです。手動実行時に beta というパラメータを true に設定して実行すると Electron の最新ベータ版をインストールしてテストします。ワークフローファイルは こちら から参照してください。 このワークフローの実行を指示してみました。 Run OS Matrix with input value "beta" to true (GitHub Copilot CLI ではなく) GitHub CLI を使って実行するプランを提示、実行についてのオプションを提示してきました。 1を選択すると、1回限りの実行を許可、2を選択するとこのセッションを通しての許可を与えることになります。1を選択すると無事に実行できたようです。 実際にちゃんと Electron のベータ版をインストールしてテストが実行されていました。 Web UI を使わなくても GitHUb CLI によるワークフロー実行方法を知らなくても、自然言語で指示すればいいので助かりますね。 さいごに # GitHub Copilot CLI は予想以上に強力な Copilot 協調環境を提供してくれていました。 プロンプトの複数行入力にも対応するなど、GA に向けて改善が進んでいます。 https://github.blog/changelog/2025-10-17-copilot-cli-multiline-input-new-mcp-enhancements-and-haiku-4-5/ MCP サーバーとの連携なども使いこなせれば、ターミナルだけで多くの複雑なタスクがこなせそうですね。
C#から7年ほど遠ざかり、久々にデベロッパーサイト向けにC#をやり出しました。そこでふと疑問が出てきました。最近のC#ではDIコンテナはどんなのがあるんだろうと。 以前やっていたときは、Castle WindsorやUnity(ゲーム制作ツールのUnityとは別物)、Seasarなどがありました(実は.NET用のSeasarなんてものがかつては存在しました)。 Castle Windsorのページ https://www.castleproject.org/projects/windsor/ .NET CoreになってからMicrosoft製のDIコンテナも登場しているようです。他にはAUTOFACというものやNinjectというものもあるようです。 AUTOFACのページ https://autofac.org/ Ninjectのページ http://www.ninject.org/ 色々ありますが、Microsoft製のものが一番とっつきやすいかなと思って使ってみたところ、本当にとっつきやすかったです。 そのためこの記事ではMicrosoft製のMicrosoft.Extensions.DependencyInjectionの使い方について、サンプルコード付きで解説します。 DIコンテナとは # あらためてDIコンテナとは何かについて確認します。 DI(Dependency Injection)コンテナはオブジェクトの生成、ライフサイクルの管理、依存関係の注入を自動化するライブラリです。コードを疎結合化し、修正やテストをしやすくします。 DIコンテナを使うメリットは主に以下です。 仕様変更などが発生しても、修正の手間を減らせる。 テスト時にはテスト用のクラス(モックと呼ばれる)に差し替えることで、テストをしやすくできる。 どの部品が他のどの部品を必要としているかが分かりやすいため、システムの構造が見通しやすい。 例えば以下のサンプルコードのように、オブジェクトの生成がハードコードされているとします。 public class Sample { private readonly SampleWriter _sampleWriter = new(); protected override SampleResult ExecuteSample() { return _sampleWriter.Write($"Execute sample at: {DateTimeOffset.Now}"); } } シンプルなコードなので気にならないと思いますが、 SampleWriter クラスを別のクラスで置き換えることを考えてみましょう。 すると SampleWriter クラスのオブジェクトを使っている個所を見直さなければいけなくなります(見直す範囲は仕様次第ですが)。 そこで登場するのがDIコンテナです。DIコンテナは例えるなら「必要なオブジェクトをまとめて提供してくれる万能な倉庫」です。 このインターフェイスにはこのクラスを代入してくださいという設定をDIコンテナに教えます。するとDIコンテナはその設定に基づいて必要なオブジェクトを自動的に作成し、渡してくれます。 例えば先ほどのサンプルコードは次のように修正できます。 public class Sample() { public Sample(ISampleWriter sampleWriter) { _sampleWriter = sampleWriter; } private readonly ISampleWriter _sampleWriter; protected override SampleResult ExecuteSample() { return _sampleWriter.Write($"Execute sample at: {DateTimeOffset.Now}"); } } コンストラクタの引数としてDIコンテナからオブジェクトを受け取ります。そして SampleWrite の型をインターフェイスとしています。 SampleWriter クラスを別のクラスで置き換えるにしても、インターフェイスを使って ISampleWriter としているので、DIコンテナの設定だけ変えればよくなります。 Microsoft.Extensions.DependencyInjectionとは # Microsoft.Extensions.DependencyInjectionはMicrosoft製のDIコンテナです。NuGetからインストールするだけですぐ使えます。またMicrosoft公式の記事も充実しています。 必要十分な機能を備えており、軽量でシンプルです。そして何より実際にやってみてコーディングが簡単でした。プロジェクト作成時に自動生成される Program.cs に少し追加するだけなのです。 Microsoft.Extensions.DependencyInjectionはC#で使うには導入のハードルが低いDIコンテナと言ってよいでしょう。 WebAppSample var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddTransient<ISampleProc, SamplePoc>(); var app = builder.Build(); 主要な概念 # サービス # サービスは依存関係として注入するインスタンスのことです。 例えば IsampleProc というインターフェイスに SampleProc というクラスのインスタンスを注入するよう設定した場合、 SampleProc というクラスのインスタンスがサービスに該当します。 サービスには以下の3つのライフサイクルがあります。 種類 概要 Transient 一時的という意味。 サービスが要求されるたびに、新しいインスタンスが生成される。 一時的な操作を行うサービスや状態を持つべきでないサービスに適している。 Scoped 特定のスコープ内でインスタンスが1つだけ生成される。 例えばWebアプリにおけるHTTPリクエスト(リクエストスコープ)や アプリケーション全体(アプリケーションスコープ)。 Singleton アプリケーション全体でインスタンスが1つだけ生成される。 コンテナ # ISampleProc が要求されたら SampleProc のインスタンスを渡すというインターフェイスとオブジェクトの紐付けや、そのスコープを登録しておくものがコンテナです。 サービスプロバイダー # サービスプロバイダーはコンテナに登録された内容に基づいて依存関係の解決を行います。 あるクラスのオブジェクトが生成されるとき、そのクラスに依存関係の注入が必要なインターフェイスがあったら、オブジェクトを生成して注入します。 言葉だと抽象的なので、コードで見てみましょう。 DiSample というクラスのオブジェクトをサービスプロバイダーが生成するケースを考えます。 このクラスには ISampleProc があります。サービスプロバイダーが DiSample を生成したとき、コンストラクタに ISampleProc があるのを見て SampleProc も生成してくれるのです。 DiSample public class DiSample { private ISampleProc _sampleProc; // コンストラクタでインジェクション public DiSample(ISampleProc sampleProc) { _sampleProc = sampleProc; } } さらにサービスプロバイダーは依存関係を連鎖解決してくれます。 SampleProc に IDbConnection がある場合を考えてみましょう。 DiSample public class SampleProc { private IDbConnection _dbConnection; // コンストラクタでインジェクション public SampleProc(IDbConnection dbConnection) { _dbConnection = dbConnection; } } サービスプロバイダーが DiSample を生成すると、先ほど書いた通り ISampleProc があるのを見て SampleProc も生成が必要だと判断します。 すると次は SampleProc に IDbConnection があるのを見て、 DbConnection の生成が必要だと判断します。 こうしてサービスプロバイダーは連鎖解決してオブジェクトを生成してくれます。なんて便利なのでしょう。 サービスの登録方法 # AddSingleton # ライフサイクルをSingletonにしてサービスを登録するには、下記のように記述します。 SingletonSample services.AddSingleton<ISampleProc, SampleProc>(); AddScoped # ライフサイクルをScopedにしてサービスを登録するには、下記のように記述します。 ScopedSample services.AddScoped<ISampleProc, SampleProc>(); AddTransient # ライフサイクルをTransientにしてサービスを登録するには、下記のように記述します。 TransientSample services.AddTransient<ISampleProc, SampleProc>(); 複数のオブジェクトを登録する方法 # Microsoft.Extensions.DependencyInjectionは1つのインターフェイスに対して複数のオブジェクトを登録できます。その場合は後から追加した設定で上書きされ、最後に追加された設定が使われます。 ただし IEnumerable<{SERVICE}> を使って解決すれば、登録したオブジェクトすべてを生成できます。 サンプルコードを見てみましょう。まずは ISampleProc に注入するオブジェクトを2つ登録します。 DiEnumerable var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcess>(); var app = builder.Build(); ResolveSample public class ResolveSample { public ResolveSample(ISampleProc sampleProc) { // この場合はSampleProcessのオブジェクトが渡される } } ResolveSampleEnumerable public class ResolveSample { public ResolveSample(IEnumerable<ISampleProc> sampleProcs) { // この場合はIEnumerableにSampleProcとSampleProcessのオブジェクトが入って渡される // つまり値が2つあるコレクションとして渡される } } サンプルコードで実践しつつ解説 # サービスとして登録するインターフェイスとクラス # まずはサービスとして登録するインターフェイスとクラスのサンプルコードを提示します。 1つのインターフェイスに対して、インジェクションするオブジェクトのクラスを変えることで、Hello WorldとMorning Worldの表示を切り替えます。また IEnumerable を使って1つのインターフェイスに複数のクラスを登録し、利用するサンプルも掲載します。 以下はHello WorldとMorning Worldを表示するためのインターフェイスとクラスのサンプルコードです。 namespace が DIConsoleApp になっていますが、プロジェクト名やディレクトリ名に合わせてください。 IMessageCreator.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { public interface IMessageCreator { string CreateMessage(); } } MessageCreatorHello.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class MessageCreatorHello : IMessageCreator { public string CreateMessage() { return "Hello, World!"; } } } MessageCreatorMorning.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class MessageCreatorMorning : IMessageCreator { public string CreateMessage() { return "Morning, World!"; } } } 続いてこれらのクラスをコンストラクタからインジェクションするサンプルコードを掲載します。 ISampleProc.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal interface ISampleProc { void DisplayMessage(); } } SampleProc.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class SampleProc : ISampleProc { private IMessageCreator _messageCreator; public SampleProc(IMessageCreator messageCreator) { _messageCreator = messageCreator; } public void DisplayMessage() { Console.WriteLine(_messageCreator.CreateMessage()); } } } こちらは1つのインターフェイスに複数のクラスが登録されている場合に、複数のクラスのオブジェクトを取得するサンプルコードです。 SampleProcEnumerable.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class SampleProcEnumerable : ISampleProc { private IMessageCreator _messageCreator; public SampleProcEnumerable(IEnumerable<IMessageCreator> messageCreators) { _messageCreator = messageCreators.ToArray()[0]; } public void DisplayMessage() { Console.WriteLine(_messageCreator.CreateMessage()); } } } コンソールアプリ # まずはコンソールアプリでDIを試してみましょう。理由はシンプルなものから見ていった方が理解しやすいからです。 コンソールアプリプロジェクトを作ってください。そしてインターフェイスやクラスを作成し、先ほど掲載したサンプルコードをコピペしてください。 それができたら Program.cs にDI設定を記述します。 Program.cs using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using DIConsoleApp; // ビルダーの作成 HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); // ホストの構築 IHost host = builder.Build(); // サービスの取得と使用 ISampleProc sampleProc = host.Services.GetRequiredService<ISampleProc>(); sampleProc.DisplayMessage(); サービス登録の個所を以下のように、 IEnumerable を使わない方のクラスにして実行してみましょう。 Program.cs(一部抜粋) // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); //builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); //builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); コンソールにHello Worldが表示されればOKです。 これを以下のようにMorningWorld用のクラスに変えて実行してみましょう。 Program.cs(一部抜粋) // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); //builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); //builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); 今度はMorning Worldが表示されます。 それではHello WorldのクラスもMorning Worldのクラスも両方とも登録してみましょう。 Program.cs(一部抜粋) // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); //builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); この場合は後勝ちとなってMorning Worldが表示されます。 その次は IEnumerable を試してみましょう。コードを次のように変えて実行します。 Program.cs(一部抜粋) // サービスの登録 //builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); SampleProcEnumerable では、以下のようにインデックスが0のオブジェクトを使うようになっています。そのため先に登録された MessageCreatorHello のオブジェクトがインジェクションされます。インデックスを1にすれば2番目に登録されたクラスのオブジェクトがインジェクションされます。 SampleProcEnumerable.cs(一部抜粋) public SampleProcEnumerable(IEnumerable<IMessageCreator> messageCreators) { _messageCreator = messageCreators.ToArray()[0]; } Webアプリ # WebアプリのDI設定 今度はWebアプリで試してみましょう。やっぱり現実的にはWebアプリのプロジェクトが多いでしょうから、Webアプリでの使い方を知っておきたいところです。 この記事ではRazorページを使って解説していきます。Razorとは何かについてはこちらの記事を参照してください。 C#とRazorで始める効率的なWeb開発!サンプルコード付きで徹底解説 Razorページアプリプロジェクトを作ってください。そしてインターフェイスやクラスを作成し、先ほど掲載したサンプルコードをコピペしてください。 そしたら Program.cs を開いてみてください。次のようになっています。 Program.cs var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); なんとRazorページアプリプロジェクトの Program.cs には、最初からMicrosoft.Extensions.DependencyInjectionで使うビルダーが記述されているのです。RazorページがDI設定同様にサービスとして登録されているのです。 ここがMicrosoft.Extensions.DependencyInjectionの導入のしやすさなのでしょう。仕組みがRazorページのようなC#でよく使う技術と共通化されているわけですね。 Razorページの登録前にDI設定を記述します。サンプルコードは以下です。コメントで「サービスの登録」と記述した個所が該当します。 Program.cs using DIConsoleApp; var builder = WebApplication.CreateBuilder(args); // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); Webアプリの動作確認 Webアプリの場合は確認用の画面を作る必要もあります。 Index を修正し、 SampleProc と SampleProcEnumerable というRazorページを作ってください。ページ遷移のイメージはこの画像のようになります。 Webアプリのサンプルのページ遷移 サンプルコードを掲載します。まずは Index.cshtml に以下のようにアンカータグを2ページ分追加します。 Index.cshtml <div class="text-center"> <h1 class="display-4">Welcome</h1> <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> <p><a href="/SampleProc">SampleProc</a></p> <p><a href="/SampleProcEnumerable">SampleProcEnumerable</a></p> </div> そしたら SampleProc (後勝ち用のページ)と SampleProcEnumerable ( IEnumerable 用のページ)を作ります。まずは SampleProc のサンプルコードを掲載します。 SampleProc.cshtml @page @model DIWebApp.Pages.SampleProcModel @{ } <h2>SampleProc</h2> <p>@Model.DisplayMessage()</p> SampleProc.cshtml.cs using DIWebApp; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace DIWebApp.Pages { public class SampleProcModel : PageModel { private IMessageCreator _messageCreator; public SampleProcModel(IMessageCreator messageCreator) { _messageCreator = messageCreator; } public string DisplayMessage() { return _messageCreator.CreateMessage(); } public void OnGet() { } } } 続いて SampleProcEnumerable ( IEnumerable を使うページ)のサンプルコードを掲載します。 SampleProcEnumerable.cshtml @page @model DIWebApp.Pages.SampleProcEnumerableModel @{ } <h2>SampleProcEnumerable</h2> <p>@Model.DisplayMessage()</p> SampleProcEnumerable.cshtml.cs using DIWebApp; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace DIWebApp.Pages { public class SampleProcEnumerableModel : PageModel { private IMessageCreator _messageCreator; public SampleProcEnumerableModel(IEnumerable<IMessageCreator> messageCreatora) { _messageCreator = messageCreatora.ToArray()[0]; } public string DisplayMessage() { return _messageCreator.CreateMessage(); } public void OnGet() { } } } デバッグ起動すると最初にIndex画面が表示されます。 そしたら SampleProc と SampleProcEnumerable にアクセスして、先ほどのコンソールアプリ同様に、後勝ちであることや IEnumerable について実行して確認してみてください。 おわりに # 私はC#にはLINQやRazorなどとても便利な技術があるのに、DIコンテナはいまいちだなぁと感じていました。 しかし今回Microsoft.Extensions.DependencyInjectionを使ってみて、C#にも簡単に扱えるDIコンテナがあるんだと知りました。 かつて私がCastle Windsorを使ったときは、.NET MVCで今の Program.cs に該当するクラスにもっと複雑なコードを書いていました。そしてXMLに冗長なDI設定を書いていました。 それと比べるとMicrosoft.Extensions.DependencyInjectionは書くべき個所が明確ですし、書き方も簡単ですね。これなら導入のハードルは低いです。 もしC#で開発する際のDIコンテナに迷っているようでしたら、Microsoft.Extensions.DependencyInjectionを使ってみてはいかがでしょうか。その際にこの記事が参考になれば幸いです。