<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>フューチャー技術ブログ</title>
  <icon>https://future-architect.github.io/feed_icon.png</icon>
  <subtitle>Future Tech Blog</subtitle>
  <link href="https://future-architect.github.io/atom.xml" rel="self"/>
  
  <link href="https://future-architect.github.io/"/>
  <updated>2026-05-10T00:45:37.111Z</updated>
  <id>https://future-architect.github.io/</id>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>【Claude Design】インフラ構成のお絵描きからリソース実装までをClaudeで一本化してみた</title>
    <link href="https://future-architect.github.io/articles/20260501a/"/>
    <id>https://future-architect.github.io/articles/20260501a/</id>
    <published>2026-04-30T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.111Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260501a/imagetets.jpg" alt="imagetets.jpg" width="1200" height="648" loading="lazy"><h2 id="1-はじめに"><a href="#1-はじめに" class="headerlink" title="1. はじめに"></a>1. はじめに</h2><p>こんにちは。Healthcare Innovation Group（HIG）の福島です。<br>本記事は、<a href="https://future-architect.github.io/articles/20260421a/">春の入門祭り2026</a>の7日目の記事です。</p><p>Claude Design、最近話題になっていますね。</p><ul><li><a href="https://qiita.com/ryu-ki/items/bca0ee8f15a13dfd8cfa">https://qiita.com/ryu-ki/items/bca0ee8f15a13dfd8cfa</a></li></ul><p>以下は上記記事の引用です。</p><blockquote><p>Claude Design は「見た目を作って終わり」ではなく、Claude Code への橋渡しまで考えられています。<br>デザインができたら、設計情報一式をまとめたパッケージ（handoff bundle）として Claude Code に渡せるという仕組みで、エクスポート先としてローカルのコーディングエージェントや Claude Code Web への引き継ぎが示されています。</p></blockquote><p>これを見た時に思いました。<strong>インフラ構成のお絵描きからリソース実装までを、Claudeで一本化できるのではないか</strong>と。</p><p>そこで、この記事ではClaude DesignとClaude Codeを使い、以下の一連の流れを検証します（前提として、インフラリソースはAWSに構築し、IaCツールはTerraformを利用します）。</p><ol><li>曖昧な要件からAWS構成図を作成する</li><li>対話しながら構成図を修正する</li><li>確定した構成図をもとにTerraformコードを生成する</li><li>生成されたTerraformコードをレビューする</li><li>実際にAWSリソースを構築できるか確認する</li><li>AI活用時に人間がレビューすべきポイントを整理する</li></ol><h2 id="2-今回構築するインフラ構成"><a href="#2-今回構築するインフラ構成" class="headerlink" title="2. 今回構築するインフラ構成"></a>2. 今回構築するインフラ構成</h2><p>今回の主目的は、Claude DesignでAWS構成図を作成し、それをもとにTerraformコードを生成することなので、インフラ構成自体は最小構成とします。</p><ul><li>インターネット公開のWebサービス</li><li>ALB経由でECS Fargate上のアプリケーションにアクセスする</li><li>ALB、ECSタスクは複数AZのパブリックサブネットに配置する</li><li>最低限のセキュリティグループを設定する</li></ul><div class="note-container warn"><span class="fa-check-circle"></span><div><p>本番構成ではECSタスクをプライベートサブネットに配置するのが一般的ですが、今回は低コストな検証環境としてパブリックサブネットに配置しました。<br>ECSのセキュリティグループではALBからの通信のみ許可し、タスクへ直接アクセスされないようにしています。</p></div></div><p>今回作成する主なAWSリソースは以下です。</p><ul><li>VPC</li><li>Public Subnet × 2</li><li>Internet Gateway</li><li>Route Table</li><li>Security Group</li><li>Application Load Balancer</li><li>ECS Cluster</li><li>ECS Service</li><li>ECS Task Definition</li><li>IAM Role</li><li>CloudWatch Logs</li></ul><h2 id="3-ステップ1：-要件から設計資料・AWS構成図を作成する"><a href="#3-ステップ1：-要件から設計資料・AWS構成図を作成する" class="headerlink" title="3. ステップ1： 要件から設計資料・AWS構成図を作成する"></a>3. ステップ1： 要件から設計資料・AWS構成図を作成する</h2><p>まずはプロジェクトを作成します。</p><p>実務では、構成図と合わせて設計の前提や目的、設計内容を整理することが多いので、今回はスライドを作成します。左ペインの”Slide deck”タブを選択し、名前を入力してCreateボタンを押下します。</p><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_12.14.53.png" alt="スクリーンショット_2026-04-30_12.14.53.png" width="1200" height="647" loading="lazy"><p>プロンプトに以下の内容を入力します（「今回の検証・設計内容」章は本記事前半の内容をそのまま貼り付け）。</p><figure class="highlight txt"><figcaption><span>prompt.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">検証および設計内容を説明するスライドの作成にあたり、事前情報を以下に記載します。</span><br><span class="line">スライド作成指示は後でするので、スライド作成はまだ開始せず、検証・設計内容の把握のみ実施してください</span><br><span class="line"></span><br><span class="line">・今回の検証・設計内容</span><br><span class="line">Claude DesignとClaude Codeを使い、以下の一連の流れを検証します。（前提として、インフラリソースはAWSに構築し、IaCツールはTerraformを利用します。）</span><br><span class="line">1. 曖昧な要件からAWS構成図を作成する</span><br><span class="line">2. 対話しながら構成図を修正する</span><br><span class="line">3. 確定した構成図をもとにTerraformコードを生成する</span><br><span class="line">4. 生成されたTerraformコードをレビューする</span><br><span class="line">5. 実際にAWSリソースを構築できるか確認する</span><br><span class="line">6. AI活用時に人間がレビューすべきポイントを整理する</span><br><span class="line"></span><br><span class="line">今回の主目的は、Claude DesignでAWS構成図を作成し、それをもとにTerraformコードを生成することなので、インフラ構成自体は最小構成とします。</span><br><span class="line">インターネット公開のWebサービス</span><br><span class="line">ALB経由でECS Fargate上のアプリケーションにアクセスする</span><br><span class="line">ALB、ECSタスクは複数AZのパブリックサブネットに配置する</span><br><span class="line">最低限のセキュリティグループを設定する</span><br><span class="line"></span><br><span class="line">今回作成する主なAWSリソースは以下です。</span><br><span class="line">VPC</span><br><span class="line">Public Subnet × 2</span><br><span class="line">Internet Gateway</span><br><span class="line">Route Table</span><br><span class="line">Security Group</span><br><span class="line">Application Load Balancer</span><br><span class="line">ECS Cluster</span><br><span class="line">ECS Service</span><br><span class="line">ECS Task Definition</span><br><span class="line">IAM Role</span><br><span class="line">CloudWatch Logs</span><br></pre></td></tr></table></figure><p>まずは検証・設計内容のみ確認してくれました。<br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_13.02.40.png" alt="スクリーンショット_2026-04-30_13.02.40.png" width="1200" height="711" loading="lazy"></p><p>続いて、以下のプロンプトでスライド作成指示を出します。<br>（まとめて作成させるとエラーが頻発したため、実際には1ページずつ作成させました）</p><figure class="highlight txt"><figcaption><span>prompt.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">インフラ設計内容およびAWS構成図を記載するスライドを作成してください。</span><br><span class="line"></span><br><span class="line">目的は、今回の検証目的・前提条件・設計方針・コスト配慮、および具体的なAWSインフラ構成について読み手が理解できるようにすることです。</span><br><span class="line"></span><br><span class="line">・スライド全体の要件</span><br><span class="line">AWSインフラ設計資料のような体裁にする</span><br><span class="line">設計内容を説明するページとAWS構成図のページを分けて、一つのスライドを作成する</span><br><span class="line">見出し、箇条書き、簡単な表を使って分かりやすく整理する</span><br><span class="line"></span><br><span class="line">・作成するスライド</span><br><span class="line">ページ1: 検証の目的</span><br><span class="line">ページ2: 設計前提および設計方針</span><br><span class="line">ページ3: 作成対象リソース</span><br><span class="line">ページ4: AWS構成図</span><br><span class="line"></span><br><span class="line">・デザイン要件</span><br><span class="line">落ち着いた技術資料風のデザインにする</span><br><span class="line">AWSカラーを意識しつつ、過度に派手にしない</span><br><span class="line">各スライドは情報を詰め込みすぎない</span><br><span class="line">4ページ目のAWS構成図に続く前提で、設計説明資料として自然につながる構成にする</span><br></pre></td></tr></table></figure><p>スライド作成が始まります。<br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_12.38.58.png" alt="スクリーンショット_2026-04-30_12.38.58.png" width="1200" height="711" loading="lazy"></p><p>作成が完了しました！<br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_13.34.20.png" alt="スクリーンショット_2026-04-30_13.34.20.png" width="1200" height="711" loading="lazy"></p><p>以下が作成されたスライド4ページ分です。<br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_13.38.21.png" alt="スクリーンショット_2026-04-30_13.38.21.png" width="1200" height="676" loading="lazy"><br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_13.38.39.png" alt="スクリーンショット_2026-04-30_13.38.39.png" width="1200" height="676" loading="lazy"><br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_13.38.53.png" alt="スクリーンショット_2026-04-30_13.38.53.png" width="1200" height="676" loading="lazy"><br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_13.39.03.png" alt="スクリーンショット_2026-04-30_13.39.03.png" width="1200" height="676" loading="lazy"></p><p>指示に従い、検証・設計内容を忠実にスライドに反映してくれています。<br>また、リージョンやAZ、VPCやサブネットのCIDRなどは明確に指示していませんが、よしなに設定してくれています。今回は特に変更の必要がないので、この設定をそのまま採用します。</p><p>続いて、以下プロンプトで各リソースの主要設定値を整理してもらいます。</p><figure class="highlight txt"><figcaption><span>prompt.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">現在作成済みの4ページ分のスライドに続けて、5ページ目以降に「各AWSリソースの主要設定値」を整理するスライドを追加してください。</span><br><span class="line"></span><br><span class="line">目的は、後続でこの設計資料をもとにTerraformコードを作成する際に、必要な主要設定値を読み取れるようにすることです。</span><br><span class="line"></span><br><span class="line">ただし、すべての設定値を網羅するとスライドが多くなりすぎるため、Terraform実装時に特に重要となる設定値に絞ってください。</span><br><span class="line">追加スライド数は必要に応じて増やして構いませんが、全体として多くなりすぎないようにし、1スライドに複数リソースをまとめられる場合はまとめてください。</span><br><span class="line"></span><br><span class="line">・全体のデザイン要件</span><br><span class="line">既存4ページのデザインとトーンを合わせてください</span><br><span class="line">AWS設計資料らしい落ち着いた技術資料風にしてください</span><br><span class="line">1スライドに情報を詰め込みすぎないでください</span><br><span class="line">ただし、スライド数が増えすぎないように、関連するリソースはまとめてください</span><br><span class="line">表を中心に、Terraform実装時に読み取りやすい構成にしてください</span><br><span class="line">Terraform実装の前段資料として、設計内容が具体化された状態になるようにしてください</span><br><span class="line">文章は簡潔にし、設定値と補足が分かりやすいようにしてください</span><br></pre></td></tr></table></figure><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_14.05.28.png" alt="スクリーンショット_2026-04-30_14.05.28.png" width="1200" height="676" loading="lazy"><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_14.05.47.png" alt="スクリーンショット_2026-04-30_14.05.47.png" width="1200" height="676" loading="lazy"><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_14.06.01.png" alt="スクリーンショット_2026-04-30_14.06.01.png" width="1200" height="676" loading="lazy"><p>Claude Designには使用量の週次リミットがあり、到達してしまいそうだったためスライド作成はここまでとします。<br>細かい不備は少々あれど、必要最低限の内容は表現できており、資料の叩き台としては十分なレベルです。<br>構成図については、AWSが公式に提供するアイコンアセットを事前に読み込ませると、より視認性の高い図になるかもしれません。興味のある方は試してみてください。</p><h2 id="4-ステップ2：-Terraformコードの生成"><a href="#4-ステップ2：-Terraformコードの生成" class="headerlink" title="4. ステップ2： Terraformコードの生成"></a>4. ステップ2： Terraformコードの生成</h2><p>ここからは、Claude DesignがまとめてくれたAWS構成および各リソースの設定値をベースに、Terraformコードを作成していきます。<br>前提として、ローカル環境にはTerraform CLIとAWS CLIの事前インストールが必要です。<br>事前に以下で認証確認を行っています。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">aws sts get-caller-identity</span><br><span class="line">terraform version</span><br></pre></td></tr></table></figure><p>まずはClaude Designの右上にあるShareボタンから、”Handoff to Claude Code”ボタンを押下します。</p><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_14.59.20.png" alt="スクリーンショット_2026-04-30_14.59.20.png" width="1200" height="680" loading="lazy"><p>ここで一点注意が必要です。</p><p>今回出力されたDesign bundleのREADMEを見る限り、Handoff機能は、Claude DesignによりHTML&#x2F;CSS&#x2F;JSで作成されたデザインプロトタイプを、Claude Codeなどのコーディングエージェントに渡してそのままWeb UIとして実装することを想定しているようです。</p><p>具体的には、デザインプロトタイプをWeb UIとして実装する旨が記載されたREADME.mdをClaude Design側が自動で作成しており、Claude Codeはその指示を読み取り実装を行う、という仕組みです（README.mdはOPTIONSの”Download zip instead”からzipダウンロードすると確認できます）。</p><p>今回の目的はWeb UIの実装ではなく、設計資料に記載されたAWS構成をTerraformコードへ落とし込むことです。そのため、READMEの指示には従わずHTML内の設計内容を読み取ってTerraformコードを生成するよう、以下の通り追加で指示しました。</p><figure class="highlight txt"><figcaption><span>instruction.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">このHandoff bundleはClaude Designで作成したHTML/CSS/JSベースの設計資料ですが、今回の目的はWeb画面の再実装ではありません。</span><br><span class="line"></span><br><span class="line">READMEにはデザインをpixel-perfectに再実装するよう書かれていますが、その指示は今回の目的には従わないでください。</span><br><span class="line"></span><br><span class="line">今回の目的は、Design bundle内の `AWS構成説明.html` に記載されたAWS設計内容、構成図、リソース一覧、主要設定値を読み取り、それをもとにTerraformコードを作成することです。</span><br><span class="line"></span><br><span class="line">HTML/CSS/JSのUIをReactやWebアプリとして再実装しないでください。</span><br><span class="line">デザインの見た目再現ではなく、設計内容の抽出とTerraform実装を実施してください。</span><br></pre></td></tr></table></figure><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_15.10.27.png" alt="スクリーンショット_2026-04-30_15.10.27.png" width="1200" height="680" loading="lazy"><p>Copy commandボタンでコマンドをコピーし、Claude Code側に貼り付けます。<br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_15.12.32.png" alt="スクリーンショット_2026-04-30_15.12.32.png" width="1200" height="781" loading="lazy"></p><p>Claude Code側でDesign bundleを取得できない旨のエラーが出たため、今回はDesign bundleをzipダウンロードし、プロジェクトルートに配置します（この際なので、README.mdは最初から配置しないこととしました）。</p><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_15.19.59.png" alt="スクリーンショット_2026-04-30_15.19.59.png" width="1200" height="781" loading="lazy"><p>気を取り直して、以下のプロンプトでTerraformコード作成の指示を行います。</p><figure class="highlight txt"><figcaption><span>prompt.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">designbundle/配下に格納されている設計スライドとAWS構成図をもとに、Terraformコードを作成してください。</span><br><span class="line">作業対象は、このリポジトリの terraform/ ディレクトリ配下のみとしてください。</span><br><span class="line"></span><br><span class="line"># 目的</span><br><span class="line">Claude Designで作成したAWS構成図から、Terraformコードを生成し、実際にAWS上へapplyできるかを検証します。</span><br><span class="line"></span><br><span class="line"># 要件</span><br><span class="line">- Terraformコードは用途ごとにファイル分割してください</span><br><span class="line">- 変数は variables.tf に定義してください</span><br><span class="line">- 出力値は outputs.tf に定義してください</span><br><span class="line">- コンテナイメージは public.ecr.aws/nginx/nginx:latest を使用してください</span><br><span class="line">- terraform fmt / validate が通る構成にしてください</span><br><span class="line"></span><br><span class="line"># 作成してほしいファイル例</span><br><span class="line">terraform/</span><br><span class="line">├── providers.tf</span><br><span class="line">├── variables.tf</span><br><span class="line">├── outputs.tf</span><br><span class="line">├── vpc.tf</span><br><span class="line">├── security_groups.tf</span><br><span class="line">├── alb.tf</span><br><span class="line">├── ecs.tf</span><br><span class="line">├── iam.tf</span><br><span class="line">└── cloudwatch.tf</span><br></pre></td></tr></table></figure><p>terraformファイルが作成され始めました！</p><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_15.27.38.png" alt="スクリーンショット_2026-04-30_15.27.38.png" width="1200" height="781" loading="lazy"><p>作成が完了し、設計書からの対応関係も出力してくれました。</p><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_15.31.29.png" alt="スクリーンショット_2026-04-30_15.31.29.png" width="1200" height="781" loading="lazy"><p>terraform planで構築内容を確認します。</p><details><summary>terraform planの結果</summary><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:</span><br><span class="line">  + create</span><br><span class="line"></span><br><span class="line">Terraform will perform the following actions:</span><br><span class="line"></span><br><span class="line">  # aws_cloudwatch_log_group.ecs will be created</span><br><span class="line">  + resource &quot;aws_cloudwatch_log_group&quot; &quot;ecs&quot; &#123;</span><br><span class="line">      + arn               = (known after apply)</span><br><span class="line">      + id                = (known after apply)</span><br><span class="line">      + log_group_class   = (known after apply)</span><br><span class="line">      + name              = &quot;/ecs/claude-aws-demo&quot;</span><br><span class="line">      + name_prefix       = (known after apply)</span><br><span class="line">      + retention_in_days = 30</span><br><span class="line">      + skip_destroy      = false</span><br><span class="line">      + tags              = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;/ecs/claude-aws-demo&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all          = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;/ecs/claude-aws-demo&quot;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_ecs_cluster.main will be created</span><br><span class="line">  + resource &quot;aws_ecs_cluster&quot; &quot;main&quot; &#123;</span><br><span class="line">      + arn      = (known after apply)</span><br><span class="line">      + id       = (known after apply)</span><br><span class="line">      + name     = &quot;claude-aws-demo-cluster&quot;</span><br><span class="line">      + tags     = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-cluster&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-cluster&quot;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">      + setting (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_ecs_service.main will be created</span><br><span class="line">  + resource &quot;aws_ecs_service&quot; &quot;main&quot; &#123;</span><br><span class="line">      + availability_zone_rebalancing      = &quot;DISABLED&quot;</span><br><span class="line">      + cluster                            = (known after apply)</span><br><span class="line">      + deployment_maximum_percent         = 200</span><br><span class="line">      + deployment_minimum_healthy_percent = 100</span><br><span class="line">      + desired_count                      = 2</span><br><span class="line">      + enable_ecs_managed_tags            = false</span><br><span class="line">      + enable_execute_command             = false</span><br><span class="line">      + iam_role                           = (known after apply)</span><br><span class="line">      + id                                 = (known after apply)</span><br><span class="line">      + launch_type                        = &quot;FARGATE&quot;</span><br><span class="line">      + name                               = &quot;claude-aws-demo-service&quot;</span><br><span class="line">      + platform_version                   = (known after apply)</span><br><span class="line">      + scheduling_strategy                = &quot;REPLICA&quot;</span><br><span class="line">      + tags                               = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-service&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all                           = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-service&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + task_definition                    = (known after apply)</span><br><span class="line">      + triggers                           = (known after apply)</span><br><span class="line">      + wait_for_steady_state              = false</span><br><span class="line"></span><br><span class="line">      + load_balancer &#123;</span><br><span class="line">          + container_name   = &quot;claude-aws-demo&quot;</span><br><span class="line">          + container_port   = 80</span><br><span class="line">          + target_group_arn = (known after apply)</span><br><span class="line">            # (1 unchanged attribute hidden)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">      + network_configuration &#123;</span><br><span class="line">          + assign_public_ip = true</span><br><span class="line">          + security_groups  = (known after apply)</span><br><span class="line">          + subnets          = (known after apply)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_ecs_task_definition.main will be created</span><br><span class="line">  + resource &quot;aws_ecs_task_definition&quot; &quot;main&quot; &#123;</span><br><span class="line">      + arn                      = (known after apply)</span><br><span class="line">      + arn_without_revision     = (known after apply)</span><br><span class="line">      + container_definitions    = jsonencode(</span><br><span class="line">            [</span><br><span class="line">              + &#123;</span><br><span class="line">                  + essential        = true</span><br><span class="line">                  + image            = &quot;public.ecr.aws/nginx/nginx:latest&quot;</span><br><span class="line">                  + logConfiguration = &#123;</span><br><span class="line">                      + logDriver = &quot;awslogs&quot;</span><br><span class="line">                      + options   = &#123;</span><br><span class="line">                          + awslogs-group         = &quot;/ecs/claude-aws-demo&quot;</span><br><span class="line">                          + awslogs-region        = &quot;ap-northeast-1&quot;</span><br><span class="line">                          + awslogs-stream-prefix = &quot;ecs&quot;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                  + name             = &quot;claude-aws-demo&quot;</span><br><span class="line">                  + portMappings     = [</span><br><span class="line">                      + &#123;</span><br><span class="line">                          + containerPort = 80</span><br><span class="line">                          + protocol      = &quot;tcp&quot;</span><br><span class="line">                        &#125;,</span><br><span class="line">                    ]</span><br><span class="line">                &#125;,</span><br><span class="line">            ]</span><br><span class="line">        )</span><br><span class="line">      + cpu                      = &quot;256&quot;</span><br><span class="line">      + enable_fault_injection   = (known after apply)</span><br><span class="line">      + execution_role_arn       = (known after apply)</span><br><span class="line">      + family                   = &quot;claude-aws-demo-task&quot;</span><br><span class="line">      + id                       = (known after apply)</span><br><span class="line">      + memory                   = &quot;512&quot;</span><br><span class="line">      + network_mode             = &quot;awsvpc&quot;</span><br><span class="line">      + requires_compatibilities = [</span><br><span class="line">          + &quot;FARGATE&quot;,</span><br><span class="line">        ]</span><br><span class="line">      + revision                 = (known after apply)</span><br><span class="line">      + skip_destroy             = false</span><br><span class="line">      + tags                     = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-task&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all                 = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-task&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + track_latest             = false</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_iam_role.ecs_task_execution will be created</span><br><span class="line">  + resource &quot;aws_iam_role&quot; &quot;ecs_task_execution&quot; &#123;</span><br><span class="line">      + arn                   = (known after apply)</span><br><span class="line">      + assume_role_policy    = jsonencode(</span><br><span class="line">            &#123;</span><br><span class="line">              + Statement = [</span><br><span class="line">                  + &#123;</span><br><span class="line">                      + Action    = &quot;sts:AssumeRole&quot;</span><br><span class="line">                      + Effect    = &quot;Allow&quot;</span><br><span class="line">                      + Principal = &#123;</span><br><span class="line">                          + Service = &quot;ecs-tasks.amazonaws.com&quot;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;,</span><br><span class="line">                ]</span><br><span class="line">              + Version   = &quot;2012-10-17&quot;</span><br><span class="line">            &#125;</span><br><span class="line">        )</span><br><span class="line">      + create_date           = (known after apply)</span><br><span class="line">      + force_detach_policies = false</span><br><span class="line">      + id                    = (known after apply)</span><br><span class="line">      + managed_policy_arns   = (known after apply)</span><br><span class="line">      + max_session_duration  = 3600</span><br><span class="line">      + name                  = &quot;claude-aws-demo-ecs-task-execution-role&quot;</span><br><span class="line">      + name_prefix           = (known after apply)</span><br><span class="line">      + path                  = &quot;/&quot;</span><br><span class="line">      + tags                  = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-ecs-task-execution-role&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all              = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-ecs-task-execution-role&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + unique_id             = (known after apply)</span><br><span class="line"></span><br><span class="line">      + inline_policy (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_iam_role_policy_attachment.ecs_task_execution will be created</span><br><span class="line">  + resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_task_execution&quot; &#123;</span><br><span class="line">      + id         = (known after apply)</span><br><span class="line">      + policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy&quot;</span><br><span class="line">      + role       = &quot;claude-aws-demo-ecs-task-execution-role&quot;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_internet_gateway.main will be created</span><br><span class="line">  + resource &quot;aws_internet_gateway&quot; &quot;main&quot; &#123;</span><br><span class="line">      + arn      = (known after apply)</span><br><span class="line">      + id       = (known after apply)</span><br><span class="line">      + owner_id = (known after apply)</span><br><span class="line">      + tags     = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-igw&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-igw&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + vpc_id   = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_lb.main will be created</span><br><span class="line">  + resource &quot;aws_lb&quot; &quot;main&quot; &#123;</span><br><span class="line">      + arn                                                          = (known after apply)</span><br><span class="line">      + arn_suffix                                                   = (known after apply)</span><br><span class="line">      + client_keep_alive                                            = 3600</span><br><span class="line">      + desync_mitigation_mode                                       = &quot;defensive&quot;</span><br><span class="line">      + dns_name                                                     = (known after apply)</span><br><span class="line">      + drop_invalid_header_fields                                   = false</span><br><span class="line">      + enable_deletion_protection                                   = false</span><br><span class="line">      + enable_http2                                                 = true</span><br><span class="line">      + enable_tls_version_and_cipher_suite_headers                  = false</span><br><span class="line">      + enable_waf_fail_open                                         = false</span><br><span class="line">      + enable_xff_client_port                                       = false</span><br><span class="line">      + enable_zonal_shift                                           = false</span><br><span class="line">      + enforce_security_group_inbound_rules_on_private_link_traffic = (known after apply)</span><br><span class="line">      + id                                                           = (known after apply)</span><br><span class="line">      + idle_timeout                                                 = 60</span><br><span class="line">      + internal                                                     = false</span><br><span class="line">      + ip_address_type                                              = (known after apply)</span><br><span class="line">      + load_balancer_type                                           = &quot;application&quot;</span><br><span class="line">      + name                                                         = &quot;claude-aws-demo-alb&quot;</span><br><span class="line">      + name_prefix                                                  = (known after apply)</span><br><span class="line">      + preserve_host_header                                         = false</span><br><span class="line">      + security_groups                                              = (known after apply)</span><br><span class="line">      + subnets                                                      = (known after apply)</span><br><span class="line">      + tags                                                         = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-alb&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all                                                     = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-alb&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + vpc_id                                                       = (known after apply)</span><br><span class="line">      + xff_header_processing_mode                                   = &quot;append&quot;</span><br><span class="line">      + zone_id                                                      = (known after apply)</span><br><span class="line"></span><br><span class="line">      + subnet_mapping (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_lb_listener.http will be created</span><br><span class="line">  + resource &quot;aws_lb_listener&quot; &quot;http&quot; &#123;</span><br><span class="line">      + arn                                                                   = (known after apply)</span><br><span class="line">      + id                                                                    = (known after apply)</span><br><span class="line">      + load_balancer_arn                                                     = (known after apply)</span><br><span class="line">      + port                                                                  = 80</span><br><span class="line">      + protocol                                                              = &quot;HTTP&quot;</span><br><span class="line">      + routing_http_request_x_amzn_mtls_clientcert_header_name               = (known after apply)</span><br><span class="line">      + routing_http_request_x_amzn_mtls_clientcert_issuer_header_name        = (known after apply)</span><br><span class="line">      + routing_http_request_x_amzn_mtls_clientcert_leaf_header_name          = (known after apply)</span><br><span class="line">      + routing_http_request_x_amzn_mtls_clientcert_serial_number_header_name = (known after apply)</span><br><span class="line">      + routing_http_request_x_amzn_mtls_clientcert_subject_header_name       = (known after apply)</span><br><span class="line">      + routing_http_request_x_amzn_mtls_clientcert_validity_header_name      = (known after apply)</span><br><span class="line">      + routing_http_request_x_amzn_tls_cipher_suite_header_name              = (known after apply)</span><br><span class="line">      + routing_http_request_x_amzn_tls_version_header_name                   = (known after apply)</span><br><span class="line">      + routing_http_response_access_control_allow_credentials_header_value   = (known after apply)</span><br><span class="line">      + routing_http_response_access_control_allow_headers_header_value       = (known after apply)</span><br><span class="line">      + routing_http_response_access_control_allow_methods_header_value       = (known after apply)</span><br><span class="line">      + routing_http_response_access_control_allow_origin_header_value        = (known after apply)</span><br><span class="line">      + routing_http_response_access_control_expose_headers_header_value      = (known after apply)</span><br><span class="line">      + routing_http_response_access_control_max_age_header_value             = (known after apply)</span><br><span class="line">      + routing_http_response_content_security_policy_header_value            = (known after apply)</span><br><span class="line">      + routing_http_response_server_enabled                                  = (known after apply)</span><br><span class="line">      + routing_http_response_strict_transport_security_header_value          = (known after apply)</span><br><span class="line">      + routing_http_response_x_content_type_options_header_value             = (known after apply)</span><br><span class="line">      + routing_http_response_x_frame_options_header_value                    = (known after apply)</span><br><span class="line">      + ssl_policy                                                            = (known after apply)</span><br><span class="line">      + tags_all                                                              = (known after apply)</span><br><span class="line">      + tcp_idle_timeout_seconds                                              = (known after apply)</span><br><span class="line"></span><br><span class="line">      + default_action &#123;</span><br><span class="line">          + order            = (known after apply)</span><br><span class="line">          + target_group_arn = (known after apply)</span><br><span class="line">          + type             = &quot;forward&quot;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">      + mutual_authentication (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_lb_target_group.main will be created</span><br><span class="line">  + resource &quot;aws_lb_target_group&quot; &quot;main&quot; &#123;</span><br><span class="line">      + arn                                = (known after apply)</span><br><span class="line">      + arn_suffix                         = (known after apply)</span><br><span class="line">      + connection_termination             = (known after apply)</span><br><span class="line">      + deregistration_delay               = &quot;300&quot;</span><br><span class="line">      + id                                 = (known after apply)</span><br><span class="line">      + ip_address_type                    = (known after apply)</span><br><span class="line">      + lambda_multi_value_headers_enabled = false</span><br><span class="line">      + load_balancer_arns                 = (known after apply)</span><br><span class="line">      + load_balancing_algorithm_type      = (known after apply)</span><br><span class="line">      + load_balancing_anomaly_mitigation  = (known after apply)</span><br><span class="line">      + load_balancing_cross_zone_enabled  = (known after apply)</span><br><span class="line">      + name                               = &quot;claude-aws-demo-tg&quot;</span><br><span class="line">      + name_prefix                        = (known after apply)</span><br><span class="line">      + port                               = 80</span><br><span class="line">      + preserve_client_ip                 = (known after apply)</span><br><span class="line">      + protocol                           = &quot;HTTP&quot;</span><br><span class="line">      + protocol_version                   = (known after apply)</span><br><span class="line">      + proxy_protocol_v2                  = false</span><br><span class="line">      + slow_start                         = 0</span><br><span class="line">      + tags                               = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-tg&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all                           = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-tg&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + target_type                        = &quot;ip&quot;</span><br><span class="line">      + vpc_id                             = (known after apply)</span><br><span class="line"></span><br><span class="line">      + health_check &#123;</span><br><span class="line">          + enabled             = true</span><br><span class="line">          + healthy_threshold   = 2</span><br><span class="line">          + interval            = 30</span><br><span class="line">          + matcher             = &quot;200&quot;</span><br><span class="line">          + path                = &quot;/&quot;</span><br><span class="line">          + port                = &quot;traffic-port&quot;</span><br><span class="line">          + protocol            = &quot;HTTP&quot;</span><br><span class="line">          + timeout             = 5</span><br><span class="line">          + unhealthy_threshold = 2</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">      + stickiness (known after apply)</span><br><span class="line"></span><br><span class="line">      + target_failover (known after apply)</span><br><span class="line"></span><br><span class="line">      + target_group_health (known after apply)</span><br><span class="line"></span><br><span class="line">      + target_health_state (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_route_table.public will be created</span><br><span class="line">  + resource &quot;aws_route_table&quot; &quot;public&quot; &#123;</span><br><span class="line">      + arn              = (known after apply)</span><br><span class="line">      + id               = (known after apply)</span><br><span class="line">      + owner_id         = (known after apply)</span><br><span class="line">      + propagating_vgws = (known after apply)</span><br><span class="line">      + route            = [</span><br><span class="line">          + &#123;</span><br><span class="line">              + cidr_block                 = &quot;0.0.0.0/0&quot;</span><br><span class="line">              + gateway_id                 = (known after apply)</span><br><span class="line">                # (11 unchanged attributes hidden)</span><br><span class="line">            &#125;,</span><br><span class="line">        ]</span><br><span class="line">      + tags             = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-public-rt&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all         = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-public-rt&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + vpc_id           = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_route_table_association.public_1 will be created</span><br><span class="line">  + resource &quot;aws_route_table_association&quot; &quot;public_1&quot; &#123;</span><br><span class="line">      + id             = (known after apply)</span><br><span class="line">      + route_table_id = (known after apply)</span><br><span class="line">      + subnet_id      = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_route_table_association.public_2 will be created</span><br><span class="line">  + resource &quot;aws_route_table_association&quot; &quot;public_2&quot; &#123;</span><br><span class="line">      + id             = (known after apply)</span><br><span class="line">      + route_table_id = (known after apply)</span><br><span class="line">      + subnet_id      = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_security_group.alb will be created</span><br><span class="line">  + resource &quot;aws_security_group&quot; &quot;alb&quot; &#123;</span><br><span class="line">      + arn                    = (known after apply)</span><br><span class="line">      + description            = &quot;ALB: allow HTTP from internet&quot;</span><br><span class="line">      + egress                 = [</span><br><span class="line">          + &#123;</span><br><span class="line">              + cidr_blocks      = [</span><br><span class="line">                  + &quot;0.0.0.0/0&quot;,</span><br><span class="line">                ]</span><br><span class="line">              + description      = &quot;allow all outbound&quot;</span><br><span class="line">              + from_port        = 0</span><br><span class="line">              + ipv6_cidr_blocks = []</span><br><span class="line">              + prefix_list_ids  = []</span><br><span class="line">              + protocol         = &quot;-1&quot;</span><br><span class="line">              + security_groups  = []</span><br><span class="line">              + self             = false</span><br><span class="line">              + to_port          = 0</span><br><span class="line">            &#125;,</span><br><span class="line">        ]</span><br><span class="line">      + id                     = (known after apply)</span><br><span class="line">      + ingress                = [</span><br><span class="line">          + &#123;</span><br><span class="line">              + cidr_blocks      = [</span><br><span class="line">                  + &quot;0.0.0.0/0&quot;,</span><br><span class="line">                ]</span><br><span class="line">              + description      = &quot;HTTP from internet&quot;</span><br><span class="line">              + from_port        = 80</span><br><span class="line">              + ipv6_cidr_blocks = []</span><br><span class="line">              + prefix_list_ids  = []</span><br><span class="line">              + protocol         = &quot;tcp&quot;</span><br><span class="line">              + security_groups  = []</span><br><span class="line">              + self             = false</span><br><span class="line">              + to_port          = 80</span><br><span class="line">            &#125;,</span><br><span class="line">        ]</span><br><span class="line">      + name                   = &quot;claude-aws-demo-alb-sg&quot;</span><br><span class="line">      + name_prefix            = (known after apply)</span><br><span class="line">      + owner_id               = (known after apply)</span><br><span class="line">      + revoke_rules_on_delete = false</span><br><span class="line">      + tags                   = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-alb-sg&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all               = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-alb-sg&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + vpc_id                 = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_security_group.ecs will be created</span><br><span class="line">  + resource &quot;aws_security_group&quot; &quot;ecs&quot; &#123;</span><br><span class="line">      + arn                    = (known after apply)</span><br><span class="line">      + description            = &quot;ECS tasks: allow HTTP from ALB SG only&quot;</span><br><span class="line">      + egress                 = [</span><br><span class="line">          + &#123;</span><br><span class="line">              + cidr_blocks      = [</span><br><span class="line">                  + &quot;0.0.0.0/0&quot;,</span><br><span class="line">                ]</span><br><span class="line">              + description      = &quot;allow all outbound (ECR pull, CloudWatch Logs)&quot;</span><br><span class="line">              + from_port        = 0</span><br><span class="line">              + ipv6_cidr_blocks = []</span><br><span class="line">              + prefix_list_ids  = []</span><br><span class="line">              + protocol         = &quot;-1&quot;</span><br><span class="line">              + security_groups  = []</span><br><span class="line">              + self             = false</span><br><span class="line">              + to_port          = 0</span><br><span class="line">            &#125;,</span><br><span class="line">        ]</span><br><span class="line">      + id                     = (known after apply)</span><br><span class="line">      + ingress                = [</span><br><span class="line">          + &#123;</span><br><span class="line">              + cidr_blocks      = []</span><br><span class="line">              + description      = &quot;HTTP from ALB SG&quot;</span><br><span class="line">              + from_port        = 80</span><br><span class="line">              + ipv6_cidr_blocks = []</span><br><span class="line">              + prefix_list_ids  = []</span><br><span class="line">              + protocol         = &quot;tcp&quot;</span><br><span class="line">              + security_groups  = (known after apply)</span><br><span class="line">              + self             = false</span><br><span class="line">              + to_port          = 80</span><br><span class="line">            &#125;,</span><br><span class="line">        ]</span><br><span class="line">      + name                   = &quot;claude-aws-demo-ecs-sg&quot;</span><br><span class="line">      + name_prefix            = (known after apply)</span><br><span class="line">      + owner_id               = (known after apply)</span><br><span class="line">      + revoke_rules_on_delete = false</span><br><span class="line">      + tags                   = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-ecs-sg&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all               = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-ecs-sg&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + vpc_id                 = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_subnet.public_1 will be created</span><br><span class="line">  + resource &quot;aws_subnet&quot; &quot;public_1&quot; &#123;</span><br><span class="line">      + arn                                            = (known after apply)</span><br><span class="line">      + assign_ipv6_address_on_creation                = false</span><br><span class="line">      + availability_zone                              = &quot;ap-northeast-1a&quot;</span><br><span class="line">      + availability_zone_id                           = (known after apply)</span><br><span class="line">      + cidr_block                                     = &quot;10.0.1.0/24&quot;</span><br><span class="line">      + enable_dns64                                   = false</span><br><span class="line">      + enable_resource_name_dns_a_record_on_launch    = false</span><br><span class="line">      + enable_resource_name_dns_aaaa_record_on_launch = false</span><br><span class="line">      + id                                             = (known after apply)</span><br><span class="line">      + ipv6_cidr_block_association_id                 = (known after apply)</span><br><span class="line">      + ipv6_native                                    = false</span><br><span class="line">      + map_public_ip_on_launch                        = true</span><br><span class="line">      + owner_id                                       = (known after apply)</span><br><span class="line">      + private_dns_hostname_type_on_launch            = (known after apply)</span><br><span class="line">      + tags                                           = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-public-subnet-1&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all                                       = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-public-subnet-1&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + vpc_id                                         = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_subnet.public_2 will be created</span><br><span class="line">  + resource &quot;aws_subnet&quot; &quot;public_2&quot; &#123;</span><br><span class="line">      + arn                                            = (known after apply)</span><br><span class="line">      + assign_ipv6_address_on_creation                = false</span><br><span class="line">      + availability_zone                              = &quot;ap-northeast-1c&quot;</span><br><span class="line">      + availability_zone_id                           = (known after apply)</span><br><span class="line">      + cidr_block                                     = &quot;10.0.2.0/24&quot;</span><br><span class="line">      + enable_dns64                                   = false</span><br><span class="line">      + enable_resource_name_dns_a_record_on_launch    = false</span><br><span class="line">      + enable_resource_name_dns_aaaa_record_on_launch = false</span><br><span class="line">      + id                                             = (known after apply)</span><br><span class="line">      + ipv6_cidr_block_association_id                 = (known after apply)</span><br><span class="line">      + ipv6_native                                    = false</span><br><span class="line">      + map_public_ip_on_launch                        = true</span><br><span class="line">      + owner_id                                       = (known after apply)</span><br><span class="line">      + private_dns_hostname_type_on_launch            = (known after apply)</span><br><span class="line">      + tags                                           = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-public-subnet-2&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all                                       = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-public-subnet-2&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + vpc_id                                         = (known after apply)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">  # aws_vpc.main will be created</span><br><span class="line">  + resource &quot;aws_vpc&quot; &quot;main&quot; &#123;</span><br><span class="line">      + arn                                  = (known after apply)</span><br><span class="line">      + cidr_block                           = &quot;10.0.0.0/16&quot;</span><br><span class="line">      + default_network_acl_id               = (known after apply)</span><br><span class="line">      + default_route_table_id               = (known after apply)</span><br><span class="line">      + default_security_group_id            = (known after apply)</span><br><span class="line">      + dhcp_options_id                      = (known after apply)</span><br><span class="line">      + enable_dns_hostnames                 = true</span><br><span class="line">      + enable_dns_support                   = true</span><br><span class="line">      + enable_network_address_usage_metrics = (known after apply)</span><br><span class="line">      + id                                   = (known after apply)</span><br><span class="line">      + instance_tenancy                     = &quot;default&quot;</span><br><span class="line">      + ipv6_association_id                  = (known after apply)</span><br><span class="line">      + ipv6_cidr_block                      = (known after apply)</span><br><span class="line">      + ipv6_cidr_block_network_border_group = (known after apply)</span><br><span class="line">      + main_route_table_id                  = (known after apply)</span><br><span class="line">      + owner_id                             = (known after apply)</span><br><span class="line">      + tags                                 = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-vpc&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      + tags_all                             = &#123;</span><br><span class="line">          + &quot;Name&quot; = &quot;claude-aws-demo-vpc&quot;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">Plan: 18 to add, 0 to change, 0 to destroy.</span><br><span class="line"></span><br><span class="line">Changes to Outputs:</span><br><span class="line">  + alb_arn                     = (known after apply)</span><br><span class="line">  + alb_dns_name                = (known after apply)</span><br><span class="line">  + cloudwatch_log_group_name   = &quot;/ecs/claude-aws-demo&quot;</span><br><span class="line">  + ecs_cluster_name            = &quot;claude-aws-demo-cluster&quot;</span><br><span class="line">  + ecs_service_name            = &quot;claude-aws-demo-service&quot;</span><br><span class="line">  + ecs_task_execution_role_arn = (known after apply)</span><br><span class="line">  + public_subnet_ids           = [</span><br><span class="line">      + (known after apply),</span><br><span class="line">      + (known after apply),</span><br><span class="line">    ]</span><br><span class="line">  + vpc_id                      = (known after apply)</span><br><span class="line"></span><br><span class="line">──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span><br><span class="line"></span><br><span class="line">Saved the plan to: tfplan</span><br><span class="line"></span><br><span class="line">To perform exactly these actions, run the following command to apply:</span><br><span class="line">    terraform apply &quot;tfplan&quot;</span><br></pre></td></tr></table></figure></details><p>plan結果を確認したところ、想定していた主要リソースが作成対象になっていることを確認できました。</p><p>CloudWatch Logsの保持期間やECSタスク数など、コストに関わる設定は環境に応じて調整余地がありますが、今回は検証後すぐにdestroyする前提のため、このままterraform applyで構築を実施します。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Apply complete! Resources: 18 added, 0 changed, 0 destroyed.</span><br></pre></td></tr></table></figure><h2 id="5-ステップ3：-疎通確認"><a href="#5-ステップ3：-疎通確認" class="headerlink" title="5. ステップ3： 疎通確認"></a>5. ステップ3： 疎通確認</h2><p>AWSマネジメントコンソールから、構築されたリソースを確認します。</p><p>ALBからECSへの接続設定が完了していることがわかります。その他、VPC関連リソースやIAM、CloudWatch Logs等のリソースも正しく構築されていることを確認しました。</p><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_16.16.55.png" alt="スクリーンショット_2026-04-30_16.16.55.png" width="1200" height="363" loading="lazy"><p>ブラウザからALBのデフォルトDNS名を指定しアクセスしてみます。<br><img src="/images/2026/20260501a/スクリーンショット_2026-04-30_16.20.16.png" alt="スクリーンショット_2026-04-30_16.20.16.png" width="1200" height="397" loading="lazy"></p><p>接続ができました！</p><p>なお、今回作成したALBやECS Fargateは料金が発生するため、疎通確認後は以下のコマンドで削除しました。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">terraform destroy</span><br></pre></td></tr></table></figure><h2 id="6-おわりに"><a href="#6-おわりに" class="headerlink" title="6. おわりに"></a>6. おわりに</h2><p>今回は、Claude DesignでAWS構成図を含む設計資料を作成し、その内容をClaude Codeへ引き継いでTerraformコードを生成し、実際にAWSリソースを構築するところまで検証しました。</p><p>最初に考えていた「インフラ構成のお絵描きからリソース実装までをClaudeで一本化できるのではないか」という仮説については、少なくとも今回のようなシンプルな構成であれば、十分に実現できる感触がありました。</p><p>特に良かった点は、<strong>設計資料・構成図・Terraformコード作成の流れを、自然言語ベースでつなげられることです</strong>。従来であれば、設計資料を作り、構成図を描き、それを見ながらTerraformコードを書く、という工程を人間が手作業でつないでいました。今回は、Claude Designで整理した設計情報をClaude Codeに読み取らせることで、Terraformコードのたたき台をかなり短時間で作成できました。</p><p>また、資料やコード生成の品質も高く、今回私が実施したのはClaudeへの指示と生成コードのレビュー、terraform plan、terraform applyの実行だけでした。Claude Designはスライドのテンプレートを指定したり、事前情報として画像やファイルをインプットしたりできるので、それらを活用すればさらなる品質向上が期待できます。</p><p>今回はあくまで検証用の簡易構成だったため、大規模なシステム基盤設計・構築にそのまま適用することは難しい（Claude Designの使用量制限も意外とすぐ来るし…）ですが、ちょっとした検証等には使えるのではないでしょうか。</p><p>構成図や設計資料のたたき台を作る、Terraformコードの初版を作る、レビュー観点を洗い出す、といった場面ではかなり有効だと思いました。</p><p>Claude Design、皆さんもぜひ活用してみてください！</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260501a/imagetets.jpg&quot; alt=&quot;imagetets.jpg&quot; width=&quot;1200&quot; height=&quot;648&quot; loading=&quot;lazy&quot;&gt;

&lt;h2 id=&quot;1-はじめに&quot;&gt;&lt;a</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    
    <category term="Terraform" scheme="https://future-architect.github.io/tags/Terraform/"/>
    
    <category term="ClaudeCode" scheme="https://future-architect.github.io/tags/ClaudeCode/"/>
    
    <category term="ClaudeDesign" scheme="https://future-architect.github.io/tags/ClaudeDesign/"/>
    
  </entry>
  
  <entry>
    <title>BigQueryから直接Geminiを叩こう。BigQueryMLによるログ解析ハンズオン</title>
    <link href="https://future-architect.github.io/articles/20260430a/"/>
    <id>https://future-architect.github.io/articles/20260430a/</id>
    <published>2026-04-29T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.111Z</updated>
    
    <content type="html"><![CDATA[<p><a href="/articles/20260421a/">春の入門祭り2026</a>の6本目です。</p><h2 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h2><p>こんにちは。フューチャーアーキテクト 製造・エネルギーサービス事業部の柴田です😌 見真の心で本質を探求しています!</p><p>BigQuery上のデータに対して、外部APIを呼び出すことなく、クエリ内でGeminiによるテキスト生成やデータ解析する手順をまとめました。サービスに対する対応の手間を減らすために、エラー発生時の推奨アクションを提示しています。</p><p>普段のGemini利用と異なる利用料金などについても整理しています。</p><h2 id="事前準備"><a href="#事前準備" class="headerlink" title="事前準備"></a>事前準備</h2><p>BigQueryとVertex AIを連携させるための設定とモデルを作成します。</p><h3 id="全体の構成図"><a href="#全体の構成図" class="headerlink" title="全体の構成図"></a>全体の構成図</h3><p>モデル利用のためには、次の設定が必要です。</p><ul><li>外部接続</li><li>外部接続で呼び出すモデル</li></ul><p>構成図は以下の通りです。</p><pre class="mermaid">graph LR    %% ノードの定義とラベル    Query[SQL クエリ<br/>ML.GENERATE_TEXT]    subgraph BQ_RemoteModel [BQリモートモデル gemini_model]        direction TB        Model_Config[モデル設定]        Connection[外部接続 vertex_ai_conn<br/>認証の通り道]    end    VertexAI[Vertex AI Gemini API]    %% フローの定義    Query -->|モデルを指定<br/>Connection情報をロード| BQ_RemoteModel    %% 認証とAPI呼び出しのフロー (ここがデータの通り道)    Connection -->|セキュアに通信<br/>API呼び出し| VertexAI    %% スタイリング（Google Cloudカラーをイメージ）    classDef bq fill:#e0f7fa,stroke:#01579b,stroke-width:1px,color:#01579b;    classDef conn fill:#ffccbc,stroke:#bf360c,stroke-width:1px,color:#bf360c,stroke-dasharray: 5 5;    class Query,BQ_RemoteModel,Model_Config bq;    class Connection conn;</pre><h3 id="外部接続（Connection）の作成"><a href="#外部接続（Connection）の作成" class="headerlink" title="外部接続（Connection）の作成"></a>外部接続（Connection）の作成</h3><p>BigQueryコンソールの「データ 追加」 &gt; 「Vertex AI」のデータソースを選択（vertex aiで検索）</p><img src="/images/2026/20260430a/データ追加を選択してVertexAIのデータソースを選択.png" alt="データ追加を選択してVertexAIのデータソースを選択" width="1190" height="839" loading="lazy"><p>外部データへのアクセスで、BiqQueryフェデレーションを選択します。</p><img src="/images/2026/20260430a/BiqQueryフェデレーションが赤枠で囲まれている.png" alt="BiqQueryフェデレーションが赤枠で囲まれている" width="1200" height="351" loading="lazy"><p>以下のように、外部データソースとの接続を作成します。</p><ul><li>接続タイプ：Vertex AI リモートモデル、リモート関数、BigLake, Spanner（Cloud リソース）</li><li>接続ID：用途が分かりやすい名前にする（接続名なので*_connという命名を推奨）</li><li>ロケーションタイプ：任意のリージョンを選択</li><li>分かりやすい名前：日本語OKなので利用方法が明確な名前にします</li><li>説明：より詳細な説明</li></ul><img src="/images/2026/20260430a/外部データソース設定.png" alt="外部データソース設定" width="1038" height="962" loading="lazy"><p>作成した接続はBigQueryコンソールの「接続」で確認することができます。</p><img src="/images/2026/20260430a/サイドバーの接続を、接続を確認.png" alt="サイドバーの接続を、接続を確認" width="1200" height="436" loading="lazy"><h3 id="リモートモデルの定義"><a href="#リモートモデルの定義" class="headerlink" title="リモートモデルの定義"></a>リモートモデルの定義</h3><p>BigQueryから外部のGemini APIを呼び出すための、設定情報（メタデータ）を登録します。<br>以下の<code>CREATE OR REPLACE MODEL</code>クエリを実行してモデルを作成します。<br>BigQuery内に、Geminiを呼び出すためのモデルを定義します。これは1度だけ実行すればOKです。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">OR</span> REPLACE MODEL プロジェクト名.データセット名.モデル名</span><br><span class="line">REMOTE <span class="keyword">WITH</span> CONNECTION プロジェクト名.リージョン.先ほど作成した接続ID</span><br><span class="line">OPTIONS (</span><br><span class="line"><span class="comment">-- 利用したいGeminiのエンドポイントを指定（例:gemini-2.5-flash, gemini-2.5-pro）</span></span><br><span class="line">endpoint <span class="operator">=</span> <span class="string">&#x27;使用するモデル&#x27;</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>参考：クエリの具体例は以下の通りです。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">OR</span> REPLACE MODEL `project_id.logexplorer.gemini_model`</span><br><span class="line">REMOTE <span class="keyword">WITH</span> CONNECTION `project_id.asia<span class="operator">-</span>northeast1.logexplorer_ai_conn`</span><br><span class="line">OPTIONS (</span><br><span class="line">endpoint <span class="operator">=</span> <span class="string">&#x27;gemini-2.5-flash&#x27;</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>クエリを実行すると、指定したデータセットの直下にモデルが作成されます。</p><img src="/images/2026/20260430a/モデル.png" alt="モデル" width="600" height="388" loading="lazy"><h2 id="モデルの利用"><a href="#モデルの利用" class="headerlink" title="モデルの利用"></a>モデルの利用</h2><p>作成したモデルを利用して推論します。ここでは実際のクエリ例を示します。特定の処理で発生したエラーログをBigQueryにリアルタイムで取り込ませている状態で、このようなプロンプトでエラーに対する推奨アクションをAIに教えてもらおうとしています。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">あなたは優秀なクラウドデータエンジニアです。</span><br><span class="line">以下のエラーメッセージから原因を推測し、解決のための具体的な「推奨対応方法」を150文字以内で提示してください。</span><br><span class="line">【対象ファイル】：対象ファイル名</span><br><span class="line">【エラーメッセージ】：エラーメッセージ</span><br></pre></td></tr></table></figure><p>以下の例はstatus_tag が ‘error’ のログを抽出し、Geminiに推奨アクションを推論させるというクエリです。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- ====================================================================</span></span><br><span class="line"><span class="comment">-- 目的: status_tag が &#x27;error&#x27; のログを抽出し、Geminiに推奨アクションを推論させる</span></span><br><span class="line"><span class="comment">-- ====================================================================</span></span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">timestamp_jst,</span><br><span class="line">service_name,</span><br><span class="line">target_file,</span><br><span class="line">status_tag,</span><br><span class="line">message,</span><br><span class="line"><span class="comment">-- JSONレスポンスからテキスト部分のみを抽出（回答の本文のみを取り出す必須処理）</span></span><br><span class="line"><span class="built_in">JSON_VALUE</span>(ml_generate_text_result, <span class="string">&#x27;$.candidates[0].content.parts[0].text&#x27;</span>) <span class="keyword">AS</span> recommended_action</span><br><span class="line"><span class="keyword">FROM</span> ML.GENERATE_TEXT(</span><br><span class="line"><span class="comment">-- ==================================================================</span></span><br><span class="line"><span class="comment">-- モデルの指定</span></span><br><span class="line"><span class="comment">-- 事前に CREATE MODEL で作成した、Vertex AI（Gemini）を呼び出すためのモデルを指定します。</span></span><br><span class="line"><span class="comment">-- ==================================================================</span></span><br><span class="line">MODEL project_id.logexplorer.gemini_model,</span><br><span class="line">(</span><br><span class="line"><span class="comment">-- ================================================================</span></span><br><span class="line"><span class="comment">-- 1. AIに渡すためのデータ（プロンプト）を準備するサブクエリ</span></span><br><span class="line"><span class="comment">-- ================================================================</span></span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">timestamp_jst,</span><br><span class="line">service_name,</span><br><span class="line">target_file,</span><br><span class="line">status_tag,</span><br><span class="line">message,</span><br><span class="line">CONCAT(</span><br><span class="line"><span class="string">&#x27;あなたは優秀なクラウドデータエンジニアです。\n&#x27;</span>,</span><br><span class="line"><span class="string">&#x27;以下のエラーメッセージから原因を推測し、解決のための具体的な「推奨対応方法」を150文字以内で提示してください。\n\n&#x27;</span>,</span><br><span class="line"><span class="string">&#x27;【対象ファイル】&#x27;</span>, IFNULL(target_file, <span class="string">&#x27;不明&#x27;</span>), <span class="string">&#x27;\n&#x27;</span>,</span><br><span class="line"><span class="string">&#x27;【エラーメッセージ】\n&#x27;</span>, IFNULL(message, <span class="string">&#x27;なし&#x27;</span>)</span><br><span class="line">) <span class="keyword">AS</span> prompt</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">project_id.logexplorer.logexplorer_errors</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">status_tag <span class="operator">=</span> <span class="string">&#x27;error&#x27;</span>          <span class="comment">-- 指定条件：エラーのみを対象</span></span><br><span class="line"><span class="keyword">AND</span> message <span class="keyword">IS</span> <span class="keyword">NOT NULL</span>       <span class="comment">-- メッセージが空のものは除外（APIの無駄撃ち防止）</span></span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span></span><br><span class="line">timestamp_jst <span class="keyword">DESC</span></span><br><span class="line">),</span><br><span class="line"><span class="comment">-- ==================================================================</span></span><br><span class="line"><span class="comment">-- 2. モデルの推論パラメータ設定</span></span><br><span class="line"><span class="comment">-- ==================================================================</span></span><br><span class="line">STRUCT(</span><br><span class="line"><span class="number">0.0</span> <span class="keyword">AS</span> temperature,       <span class="comment">-- 0.0にすると回答が固く・決定的になる（エラー分析に適している）</span></span><br><span class="line"><span class="number">300</span> <span class="keyword">AS</span> max_output_tokens  <span class="comment">-- 出力される最大トークン数（コスト上限のストッパー）</span></span><br><span class="line">)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="利用料金を押さえるためのポイント"><a href="#利用料金を押さえるためのポイント" class="headerlink" title="利用料金を押さえるためのポイント"></a>利用料金を押さえるためのポイント</h2><p>これまでの手順で各レコードに対してGeminiを実行できますが、好き放題に利用すると料金が膨らんでいきコストを圧迫してしまいます。</p><p>なので、ここでは利用料金を抑えるための解決方法の例を紹介しますす。</p><h3 id="Geminiの料金体系（Gemini-2-5-2025年4月29日現在）"><a href="#Geminiの料金体系（Gemini-2-5-2025年4月29日現在）" class="headerlink" title="Geminiの料金体系（Gemini 2.5  2025年4月29日現在）"></a>Geminiの料金体系（Gemini 2.5  2025年4月29日現在）</h3><ul><li>トークン数の比例して課金される（日本語の場合は１文字２トークン）</li><li>入力・出力の合計トークン数で課金額が決まる</li></ul><div class="scroll"><table><thead><tr><th>モデル</th><th>タイプ</th><th>料金（100 万トークンあたり）&lt;&#x3D; 20 万入力トークン</th><th>料金（100 万トークンあたり）&gt;20 万入力トークン</th></tr></thead><tbody><tr><td>Gemini 2.5 Pro</td><td>入力（テキスト、画像、動画、音声）</td><td>$1.25</td><td>$2.50</td></tr><tr><td></td><td>テキスト出力（回答と推論）</td><td>$10</td><td>$15</td></tr><tr><td>Gemini 2.5 Flash</td><td>入力（テキスト、画像、動画）</td><td>$0.30</td><td>$0.30</td></tr><tr><td></td><td>テキスト出力（回答と推論）</td><td>$2.50</td><td>$2.50</td></tr></tbody></table></div><h3 id="利用料金を抑える方法の例"><a href="#利用料金を抑える方法の例" class="headerlink" title="利用料金を抑える方法の例"></a>利用料金を抑える方法の例</h3><ol><li>用途におけるモデルの選択<br>上記の表が示すように、FlashとProでは利用料金が3倍以上変わります。普段使いではとりあえず一番性能がいいものを選びがちですが、コストに直結するため複雑な推論を必要としない場合などにはFlashや古いバージョンのモデルを利用します。</li><li>同じ質問を何度もしない<br>一度推論した結果（プロンプトと回答のセット）はBigQueryのテーブルに保存し、次回以降は JOIN で過去の回答を使い回す。</li><li>バッチ内重複排除<br>2.とやや共通していますが、1回のスケジュール実行内で同じエラーが複数ある場合、GROUP BY や ROW_NUMBER() を使って代表の1件だけをGeminiに投げ、結果を他の行にコピーする。</li></ol><h2 id="さいごに"><a href="#さいごに" class="headerlink" title="さいごに"></a>さいごに</h2><p>以上、BigQuery内で直接Geminiを実行する方法の紹介でした。</p><p>普段Gemiiniに何かを聞くときは料金を気にしないと思いますが、システムに組み込むときにはちゃんと事前にコストを計算しないと、思わぬ請求額へと膨れ上がってしまう可能性があります。特に入力プロンプトは長くなりがちですが、ここは料金にかなり大きく効いてきます。</p><p>これからのデータエンジニアリングにとっても、AIを組み込むことが必須になっていますので、その助力になれば幸いです。</p><h2 id="参考リンク"><a href="#参考リンク" class="headerlink" title="参考リンク"></a>参考リンク</h2><ul><li>BIqQueryでのAI実行方法<br><a href="https://docs.cloud.google.com/bigquery/docs/generate-text?hl=ja">https://docs.cloud.google.com/bigquery/docs/generate-text?hl=ja</a></li><li>Vertex AIの料金<br><a href="https://cloud.google.com/vertex-ai/generative-ai/pricing?hl=ja">https://cloud.google.com/vertex-ai/generative-ai/pricing?hl=ja</a></li><li>モデル一覧（実装時に必要なモデルIDを確認できる）<br><a href="https://console.cloud.google.com/vertex-ai/model-garden?hl=ja">https://console.cloud.google.com/vertex-ai/model-garden?hl=ja</a></li></ul><script type="module"> import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';mermaid.initialize({startOnLoad: true, flowchart: {curve: 'linear'}}); </script>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;a href=&quot;/articles/20260421a/&quot;&gt;春の入門祭り2026&lt;/a&gt;の6本目です。&lt;/p&gt;
&lt;h2 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="DataScience" scheme="https://future-architect.github.io/categories/DataScience/"/>
    
    
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    
    <category term="BigQuery" scheme="https://future-architect.github.io/tags/BigQuery/"/>
    
    <category term="Gemini" scheme="https://future-architect.github.io/tags/Gemini/"/>
    
  </entry>
  
  <entry>
    <title>AI-DLC, SDD、2026年4月時点のAI駆動の開発スタイルの考察</title>
    <link href="https://future-architect.github.io/articles/20260428a/"/>
    <id>https://future-architect.github.io/articles/20260428a/</id>
    <published>2026-04-27T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.111Z</updated>
    
    <content type="html"><![CDATA[<p><a href="/articles/20260421a/">春の入門祭り2026</a>の5本目です。</p><p>開発へのAIの活用はまったなしというこで、仕事やら趣味でいろいろ開発しています。せっかくなので、従来の手法をアンラーニングAIをより生かした開発スタイルでやろいうことで、現時点で作法としてまとまっているAI-DLCとSDDなどに入門して試しつつ、普段の業務への適応方法を考えたり、最近してきたことの言語化をしてみます。</p><p>開発におけるAI活用はみんな今関心を持っていることかと思います。設計書を作ればコードは自動でできる、というのはよく言われますが、<a href="https://www.ipa.go.jp/digital/software-survey/metrics/hjuojm000000c6it-att/000102171.pdf">IPAの資料を見ると</a>ウォーターフォールだと実装の工数の割合の中央値は23%-32%ぐらい（新規なのか改良なのかで変わる）となっています。仮にここが3倍になっても得られるゲインは全体の7-10%ぐらい。AIで効率アップをしたい！という目標値はだいたいこんなものではないと思うので、そういう観点で見ています。</p><h1 id="スペック駆動開発（SDD）"><a href="#スペック駆動開発（SDD）" class="headerlink" title="スペック駆動開発（SDD）"></a>スペック駆動開発（SDD）</h1><p>以前、スペック駆動開発(SDD)はFindyさんのイベントで発表させてもらったことがありました。Kiroが提唱した手法です。まずは対話しながらスペックと呼ばれるドキュメント群を作っていきます。Kiroではrequirements.md, design.md, tasks.mdです。その後、タスクに書かれているサブタスクごとに新しいセッションをオープンしながらタスクを進めていきます。</p><p>スペック駆動が画期的だった点はドキュメントを作る点ではなくサブタスクごとに新しいセッションを作ってコンテキストあふれが起きにくいようにする、そのために必要な、現在出てきているメモリー機能のようなものを自然に実現できるワークフローになっている点かなと思います。実際、当時のSonnet-3.5とかでもそうですし、コーディング能力が高くないモデルでもそこそこ動いてくれる点です。</p><p>ClaudeもCodexもCopilotも、現在はSDDをプランモードという形で取り込んでいます。ぽちっとプランモードを有効にすると、まずはソースコードの修正を封じて、plan.mdなどのドキュメントを作り、そのレビュー後に実装を進めます。今どきのモデルは以前よりは高性能ですし、コンテキスト圧縮でも性能は落ちにくくなっていると感じるのでこれでも十分動いてくれるのでしょう。Claude系モデルはドキュメントといいつつコードスニペットまみれのdesign.mdを書いていたので、ファイルを分ける意味もあまりなかったですし。</p><h2 id="スペックの単位"><a href="#スペックの単位" class="headerlink" title="スペックの単位"></a>スペックの単位</h2><p>なお、「スペック（仕様）駆動開発」という言葉があまりにも一般名詞の組み合わせすぎるせいか、「それ今までの開発と何が違うのか」みたいな言い方も登場時はされていました。また、仕様という言葉を見て、今までの仕様書相当のものを詰め込むみたいな解説も見られました。</p><p>実際にはAIが扱えるコンテキストサイズには限界があるので、システム一本分の要求を入れるということは現実的ではないです。システム開発を行う場合は機能分解をすると思いますが、そういう単位、あるいは大きなリファクタリング見たい感じごとにスペックを作って進めていくのがやりやすいですね。ウェブアプリだと画面ごと（バックエンド込み）とかで進めていくとか。</p><p>何を作るかの全体構想やタスクやモジュールに分割して外部仕様の一番外側ぐらいは人間がやりつつ、詳細設計や実装はAIがメインで進めていく、という感じかと思います。</p><h2 id="テストを書かせる指示を入れるか？"><a href="#テストを書かせる指示を入れるか？" class="headerlink" title="テストを書かせる指示を入れるか？"></a>テストを書かせる指示を入れるか？</h2><p>これも定期的に話題になる気がしますが、今どきのモデルは何も指示しなくてもテストを書いてくれるし、コードを書いたあとに実行して壊れたところの修正もしてくれます。品質維持という点では最低限やってくれます。フロントエンドは何も言わなくてもTypeScriptになるし、型ののチェックも自動です。</p><p>テストをレビューするかどうかですが、自動で書かれたテストをレビューする価値はないかなと思います。人間が意思をこめたrequirementsに対してそれが実装されているかどうかは確認すべきですが、AIが勝手に決めた内部実装をレビューする価値はありません。</p><p>コーディング規約的なのもある程度の水準で最初から書いてくれます。とはいえ、ちょっとスタイルが古かったりするので、そこは意思入れしてもよいかなとは思います。ただ自然言語でLLMにやらせるとチェックと修正とかなりクレジットを消費するので静的チェックLinterのルールを作ってそれの実行とかがいいですね。</p><h1 id="AI-DLC"><a href="#AI-DLC" class="headerlink" title="AI-DLC"></a>AI-DLC</h1><p>AI-DLCはAmazonが提唱している、AI中心の開発ライフサイクルにしていくために提案しているあたらしいライフサイクルです。KiroもそうですがこちらもAmazonです。</p><ul><li><a href="https://aws.amazon.com/jp/blogs/news/ai-driven-development-life-cycle/">AI 駆動開発ライフサイクル:ソフトウェアエンジニアリングの再構築</a></li></ul><p>アジャイルの1-2週間のイテレーションをさらにエクストリームにして1日1回、もしくは数回のボルトというイテレーションを導入します。そして、その中で、インセプションフェーズ（設計）、構築フェーズ（実装）、オペレーションフェーズ（デプロイ）という3つのフェーズを繰り返していきます。</p><p>ライフサイクルのすべてをAIが制御し、必要なことはAIが人間に尋ね、AIがどんどんやっていきますよ、とう方針です。なんだかすごそうです。<a href="https://github.com/awslabs/aidlc-workflows">既存のAIコーディングエージェントでAI-DLCを実現するためのハーネス</a>がGitHubで提供で提供されています。個人的に普段使っているCodexは対象にはなかったので、GitHub Copilotに入れてやってみました。</p><h2 id="レビューが重い"><a href="#レビューが重い" class="headerlink" title="レビューが重い"></a>レビューが重い</h2><p>必要なことは全部AIが聞いてくるので、質問に答えていくだけで適切なエンジニアリングが行われてシステムが出てくる、みたいな感じとのことですが、やってみると、結構細かいところまでガシガシ聞かれて、それにこたえる必要がありました。AIが判断を下すために情報が必要とはいえ、考えるのを後回しにしたいな、というところまで細かく聞かれます。趣味開発なのにすごい仕事している気分というか、そんな気持ちになりました。あと、質問内容はかなりシステム観点での理解が求められるような質問が多い。</p><p>AIに大量のものを作らせるのは楽しいと感じるかもしれないが、AIが作った大量のコンテンツを消費させられるのはなんというか消耗が大きいし疲れるな、と。</p><h2 id="ちょっと大きな開発に入れるにはアレンジが必要"><a href="#ちょっと大きな開発に入れるにはアレンジが必要" class="headerlink" title="ちょっと大きな開発に入れるにはアレンジが必要"></a>ちょっと大きな開発に入れるにはアレンジが必要</h2><p>元Amazonの人から教えてもらったのは、GitHubで提供しているワークフローはあくまでもAI-DLCの体験用プロンプトとのこと。公式READMEにもオペレーションフェーズは将来対応、と書かれています。論文に書かれているフローをきちんとやるには組織に合わせたアレンジだとかが必要とか。</p><p>今回、基本機能をまず作ってから追加の機能、みたいに一人で複数ボルトを体験してみようと思いフェーズを分けたのですが、どうもワークフローが複数イテレーションに対応していないのか、途中で前のフェーズの作業のタスクと混雑し始めて、途中で「ここの部分のドキュメントは前のイテレーションだから分けて（ボルトという言葉は通じなかった）」と整理させたりしました。提供されたまま使うのは1ボルト分のあまり大きくないプロダクトには良いがそれなりに工夫が必要かと思います。実際にはきちんとプロジェクト運営をしたことがあって、AIエージェントもわかって、ハーネスエンジニアリングできて、AI-DLCをアレンジできる人が必要そうです。</p><h2 id="トークン消費が激しい"><a href="#トークン消費が激しい" class="headerlink" title="トークン消費が激しい"></a>トークン消費が激しい</h2><p>ちょっとした機能の修正もすべてドキュメントを整備してから動こうとします。そしてスペック駆動以上に大量に生成されるドキュメントをすべて読み込んで整合性を取ろうとします。たぶん、同じような機能を単純なスペック駆動で開発するのと比べると5倍ぐらいCopilotのプレミアムクレジットを消費しているように思います。</p><p>Markdownはちょっとした内容のやり取りにはいいかもしれませんが、大量の文章を横断的に確認して矛盾を検知するみたいなコンパイル言語における型チェックのような仕組みはなく、大量のクレジットを消費して無理くりやっている感じです。形式仕様記述言語も人間が読み書きで判断は難しいのですが、ヒューリスティックではない何かしらの静的解析ができるドキュメント記述フォーマットが生まれない限り、このまま大きくスケールさせるのはちょっと難しい気がします。</p><h2 id="今後やるべき実験"><a href="#今後やるべき実験" class="headerlink" title="今後やるべき実験"></a>今後やるべき実験</h2><p>普段からCodexが好きなのでCopilotでもCodexを選んでやっていました。エージェントを組み合わせて使う人もCodexはレビューに使って整合性をきちんと取らせるのによい、と言われるので、それだけインセプションフェーズの質問もヘビーよりだったのかもしれません。もっとイケイケでコードをガンガン書くというClaude系モデルにやらせるとまた印象は変わるのではないかと思います。</p><h1 id="現時点での使い方"><a href="#現時点での使い方" class="headerlink" title="現時点での使い方"></a>現時点での使い方</h1><p>どちらもAIを活用したコーディング自動化の効果は得られます。どちらもAmazonがベースを作ったものですし、どちらも、動くシステムを日々作りながら育てていくという共通点があります。大きく違う点は、要件の整理やタスクの分解までAIにやらせるかどうかです。</p><img src="/images/2026/20260428a/image.png" alt="image.png" width="1024" height="559" loading="lazy"><p>現時点のモデルの能力などを考えると、AI-DLCをそのまま実現するにはまだ何段階か技術の進展が必要かも、とは思いますが、大きく活用方法を分けるとイメージとしては以下の2つかなぁと思います。</p><ul><li>アジャイルにタスク整理などを行って実装部分はSDD</li><li>AI-DLCで高速ウォーターフォール</li></ul><p>AI-DLCはドキュメントも多く、実装を後回しにしたい部分も要件を固めるためにAIにきちんと情報をインプットしていく必要があります。また、足りない情報はかなり突っ込んで聞いてきます。日次とかで回していくという考え方であってもかなりフロントヘビーな頭の使い方をします。そして、計画変更もそれなりに重くなります。今回のテストでは雑にバイブコーディングで作らせたプロトタイプがあったのでそれを読み込ませてみましたが、既存のコードの分析から要件を固める部分はなかなかよくできているな、と思いました。しかしそれは既存システムがあって、なおかつ大きく仕事の流れが変わらない前提ではうまくいく、ということでそうではない場合はかなり工夫が必要なんじゃないかと思います。ただ、ある程度やることが決まっているなら、頑張って質問に答え続ければ形になっていくので、これはこれで面白い体験でした。</p><p>SDDは逆に、現時点できちんとアーキテクチャを考えられて設計できる人からするとかなり気軽です。自分である程度モジュールや機能に分割し、必要な順番で実装していけばいいですし、そのタスクの遂行にだけ必要な情報をインプットすれば良いです。軽めに走らせて動くものができて、それをユーザーと確認して作っていくという流れとはかなりマッチしています。ただし、全体像を全部は作らないし、やってみて方針転換とかがしやすいという点では全体のスケジュール管理やプロジェクト推進はアジャイルに乗せるといいのかなと思っています。</p><p>今回のエントリーの出発点はウォーターフォール的なプロジェクトの進め方ではメリットがあまり出ないぞ、という感じで話を進めてきましたが、そもそも<a href="https://type.jp/et/feature/23307/">アメリカのウォーターフォールの採用割合がかなり少ない</a>ということもあり、アメリカ企業から出てくる開発支援のツールの思想がウォーターフォールに考慮していないで作られている点は想像に難くないです。ウォーターフォールかつAI活用だと要件定義とか前段のドキュメント類をいかに早く作るか、が勝負になってくるという点で、AI-DLCの一部適用を模索する必要はあるかと思います。ただ、AIによる開発はうまく歯車が噛み合えば爆速でいける一方、AIと人間のコミュニケーションがボトルネックで工数が読みにくいという扱いがされているのが現状かと思います。機能の境界をしっかり決め、予算も決め、スケジュールをしっかり決めて進みたいというウォータフォールのニーズが減ることはないと思いますが、実装フェーズ周りはアジャイルにしていかないとバッファーを取り過ぎてしまうのではないかという気がします。</p><h1 id="まとめ"><a href="#まとめ" class="headerlink" title="まとめ"></a>まとめ</h1><p>2つの手法は同じ会社から出てきたということもあり、だいぶ手触りは違いますが似ているものも多いと感じました。どちらもAIの活用度を大きく上げる方針であることは間違いありません。</p><p>SDDはお客さんへの技術支援案件ではすでに取り組んだことがあります。要件定義やシステムのユースケースの整理、DFDやERDの分析や設計をある程度形が整うまで伴走し、AI駆動開発の紹介や手ほどきを行ったり、SDDを実現するためのskillsを提供し、実際の実装はお客さんのIT部門の人がSDD（用のハーネスを設定したCopilot）で開発を進めていくという流れでやりましたが、思った以上にうまくいっています。今までのDXは、事業会社のIT人材不足というボトルネックがありましたが、このような伴走型の技術コンサルティングはそこの突破にはかなり追い風になりそうです。</p><p>どちらにしても、一筆書きできるような小さいシステムはともかく、大きなシステム、特に様々なシステム間の連携が必要となるような基幹システムだと、まだまだITのスキルが不要にはならなそうだなぁ、と思いました。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;a</summary>
        
      
    
    
    
    <category term="Programming" scheme="https://future-architect.github.io/categories/Programming/"/>
    
    
    <category term="AI" scheme="https://future-architect.github.io/tags/AI/"/>
    
    <category term="スペック駆動開発" scheme="https://future-architect.github.io/tags/%E3%82%B9%E3%83%9A%E3%83%83%E3%82%AF%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA/"/>
    
  </entry>
  
  <entry>
    <title>分散システム入門: 信頼性の低いネットワークを再現してみる</title>
    <link href="https://future-architect.github.io/articles/20260427a/"/>
    <id>https://future-architect.github.io/articles/20260427a/</id>
    <published>2026-04-26T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<p><a href="/articles/20260421a/">春の入門祭り2026</a>の4本目です。</p><h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>こんにちは、自宅サーバの運用って実質盆栽だよなって思い始めてる、盆栽未経験の内堀です。今回は自宅サーバでネットワークフォルトを再現してみたよというお話です。</p><p>分散システムを勉強していると「ノードが完全に死ぬよりも、中途半端に死んでいるほうが厄介」という話に必ず出会います。完全に死ねばクラスタが検知して切り離せますが、半分生きているとシステムからは正常に見えてしまい、悪さをし続けるからです。というわけで、分散システム入門の第一歩として、今回はこの「中途半端に死んだノード」を自宅サーバ上で再現してみます。具体的には、50%パケロスする設定をノードに仕込み（本記事ではゾンビノードと呼びます）、クラスタへの影響を測定します。</p><p>結論から書くと、3ノードのKubernetesクラスタで1ノードのネットワークだけ半壊させたところ、クラスタ全体のスループットが1&#x2F;14、p99レイテンシが15倍に悪化しました。以下、この数字に至るまでの話を書いていきます。</p><h1 id="実験の概要"><a href="#実験の概要" class="headerlink" title="実験の概要"></a>実験の概要</h1><h2 id="構成"><a href="#構成" class="headerlink" title="構成"></a>構成</h2><p>3ノード（home-lab-1, home-lab-2, home-lab-3）のk3sクラスタで、以下を用意します。</p><ul><li>nginxのDeployment（replicas&#x3D;3、各ノードに1Podずつ配置されるようtopologySpreadConstraintsで制約）</li><li>上記を束ねるClusterIP Service</li><li>計測用のデバッグPod（curlとohaが入ったalpineベース、control-planeで元々負荷の軽いhome-lab-1に固定）</li></ul><h2 id="実験の流れ"><a href="#実験の流れ" class="headerlink" title="実験の流れ"></a>実験の流れ</h2><p>以下の3段階で計測し、それぞれを比較します。</p><ol><li>ベースライン計測：パケットロスなしの状態で計測</li><li>半壊状態の計測：worker2（home-lab-3）のOS上で <code>tc qdisc add ... netem loss 50%</code> を実行し、パケットロス50%を発生させた状態で計測</li><li>完全停止状態の計測：worker2上で <code>systemctl stop k3s-agent</code> を実行し、Kubernetesから完全に切り離された状態で計測</li></ol><p>各段階で、デバッグPodからService経由でnginxを叩いて挙動を観察します。具体的には、軽い疎通確認としてcurlを60回ループで回して各Podへの振り分けを見て、その後ohaで20000リクエスト投げてSuccess rate、RPS、レイテンシ分布を計測します。</p><h1 id="実験"><a href="#実験" class="headerlink" title="実験"></a>実験</h1><h2 id="実験1：ベースラインの計測"><a href="#実験1：ベースラインの計測" class="headerlink" title="実験1：ベースラインの計測"></a>実験1：ベースラインの計測</h2><p>まずはパケットロスなしの状態で計測します。比較対象になる数字を取るのが目的です。</p><p>Podが3ノードに1つずつ分散配置されていること、ServiceのEndpointsに3つのPod IPが揃っていることを確認します。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp get pod -o wide</span><br><span class="line">NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE</span><br><span class="line">nginx-84888755c4-l7xtn   1/1     Running   0          13h   10.42.2.82    home-lab-3</span><br><span class="line">nginx-84888755c4-vf9zs   1/1     Running   0          20h   10.42.1.213   home-lab-2</span><br><span class="line">nginx-84888755c4-z94f4   1/1     Running   0          20h   10.42.0.31    home-lab-1</span><br><span class="line"></span><br><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp get endpoints nginx</span><br><span class="line">NAME    ENDPOINTS                                    AGE</span><br><span class="line">nginx   10.42.0.31:80,10.42.1.213:80,10.42.2.82:80   20h</span><br></pre></td></tr></table></figure><p>3Pod、3ノードに分散、Endpointsに全部入っている状態。期待通りです。</p><h3 id="疎通確認"><a href="#疎通確認" class="headerlink" title="疎通確認"></a>疎通確認</h3><p>デバッグPodからcurlを60回ループで回して、各Podにほぼ均等に振り分けられていることを見ます。nginxはpostStartで自分のhostname（&#x3D;Pod名）をindex.htmlに書き込んでいるので、レスポンスを見ればどのPodが応答したかわかります。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- sh -c <span class="string">&#x27;for i in $(seq 1 60); do curl -s --max-time 2 http://nginx.zombie-exp.svc.cluster.local/ || echo FAIL; done | sort | uniq -c&#x27;</span></span><br><span class="line">    19 nginx-84888755c4-l7xtn</span><br><span class="line">    19 nginx-84888755c4-vf9zs</span><br><span class="line">    22 nginx-84888755c4-z94f4</span><br></pre></td></tr></table></figure><p>19&#x2F;19&#x2F;22でほぼ均等。FAILは0件。</p><h3 id="定量計測"><a href="#定量計測" class="headerlink" title="定量計測"></a>定量計測</h3><p>ohaで20000リクエスト、コネクション並列度30、<code>--disable-keepalive</code> でリクエストごとにTCP接続を張り直す設定で計測します。keep-aliveを切っているのは、後段の半壊状態でTCPハンドシェイクのSYNパケットがロスする様子をはっきり見るためです（keep-aliveを有効にすると同じコネクションを使い回してしまい、ロスの影響が見えにくくなります）。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- oha -n 20000 -c 30 -t 2s --disable-keepalive http://nginx.zombie-exp.svc.cluster.local/</span><br><span class="line"></span><br><span class="line">Summary:</span><br><span class="line">  Success rate: 100.00%</span><br><span class="line">  Requests/sec: 1620.8811</span><br><span class="line"></span><br><span class="line">Response <span class="keyword">time</span> distribution:</span><br><span class="line">  50.00% <span class="keyword">in</span> 2.0985 ms</span><br><span class="line">  90.00% <span class="keyword">in</span> 69.1044 ms</span><br><span class="line">  99.00% <span class="keyword">in</span> 108.9357 ms</span><br><span class="line"></span><br><span class="line">Status code distribution:</span><br><span class="line">  [200] 20000 responses</span><br></pre></td></tr></table></figure><p>20000リクエスト全成功、1620RPS、p50が2ms、p99が109ms。自宅サーバの3ノードk3sクラスタとしてはこんなもんかなと思います。<br>これがベースラインの数字。以降、半壊と完全停止の結果はこれと比較していきます。</p><p>ちなみに、実行中は以下の画像のように、レイテンシ分布が更新されながら進んでいくのが見えます。</p><img src="/images/2026/20260427a/image.png" alt="image.png" width="1200" height="661" loading="lazy"><h2 id="実験2：home-lab-3をゾンビノードにして計測"><a href="#実験2：home-lab-3をゾンビノードにして計測" class="headerlink" title="実験2：home-lab-3をゾンビノードにして計測"></a>実験2：home-lab-3をゾンビノードにして計測</h2><p>worker2（home-lab-3）にパケットロス50%を注入します。Linuxの <code>tc</code> コマンドの <code>netem</code> モジュールを使います。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ ssh worker2 <span class="string">&quot;sudo tc qdisc add dev eth0 root netem loss 50%&quot;</span></span><br></pre></td></tr></table></figure><p>これでeth0から出入りするパケットの50%がランダムに落ちるようになります。</p><h3 id="Kubernetesから見たノードの状態"><a href="#Kubernetesから見たノードの状態" class="headerlink" title="Kubernetesから見たノードの状態"></a>Kubernetesから見たノードの状態</h3><p>ここが今回の実験の核心です。50%のパケロスを発生させた直後の状態を見ます。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl get nodes</span><br><span class="line">NAME         STATUS   ROLES           AGE   VERSION</span><br><span class="line">home-lab-1   Ready    control-plane   73d   v1.34.3+k3s3</span><br><span class="line">home-lab-2   Ready    &lt;none&gt;          73d   v1.34.3+k3s3</span><br><span class="line">home-lab-3   Ready    &lt;none&gt;          73d   v1.34.3+k3s3</span><br><span class="line"></span><br><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp get endpoints nginx</span><br><span class="line">NAME    ENDPOINTS                                    AGE</span><br><span class="line">nginx   10.42.0.31:80,10.42.1.213:80,10.42.2.82:80   20h</span><br></pre></td></tr></table></figure><p>home-lab-3は<strong>Readyのまま</strong>。Endpointsからも外れていません。これがゾンビノードです。</p><p>なぜこうなるかというと、Kubernetesはkubeletがapiserverに対して定期的にハートビートを送ることでノードの生死を判定しています。50%のパケロスがあっても、TCPの再送機構によってハートビートはなんとか届くので、Kubernetesから見ればノードは正常稼働中ということになります。</p><h3 id="疎通確認-1"><a href="#疎通確認-1" class="headerlink" title="疎通確認"></a>疎通確認</h3><p>次に、curlでの各Podへの振り分け状況を確認します。FAILが7件発生。home-lab-3上のPod（l7xtn）に振り分けられたリクエストは、半分くらいは2秒のタイムアウト内に応答が返ってきますが、半分くらいはパケロスによって失敗してFAIL扱いになります。</p><p>ベースラインではFAIL&#x3D;0だったところに、いきなり12%が失敗するようになりました。「ノードはReady、でもユーザのリクエストは落ちる」が起きている状態です。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- sh -c <span class="string">&#x27;for i in $(seq 1 60); do curl -s --max-time 2 http://nginx.zombie-exp.svc.cluster.local/ || echo FAIL; done | sort | uniq -c&#x27;</span></span><br><span class="line">      7 FAIL</span><br><span class="line">     18 nginx-84888755c4-l7xtn</span><br><span class="line">     18 nginx-84888755c4-vf9zs</span><br><span class="line">     17 nginx-84888755c4-z94f4</span><br></pre></td></tr></table></figure><h3 id="定量計測-1"><a href="#定量計測-1" class="headerlink" title="定量計測"></a>定量計測</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- oha -n 20000 -c 30 -t 2s --disable-keepalive http://nginx.zombie-exp.svc.cluster.local/</span><br><span class="line"></span><br><span class="line">Summary:</span><br><span class="line">  Success rate: 90.50%</span><br><span class="line">  Total:        219.8244 sec</span><br><span class="line">  Requests/sec: 90.9817</span><br><span class="line"></span><br><span class="line">Response <span class="keyword">time</span> distribution:</span><br><span class="line">  50.00% <span class="keyword">in</span> 0.0008 sec</span><br><span class="line">  90.00% <span class="keyword">in</span> 0.8245 sec</span><br><span class="line">  99.00% <span class="keyword">in</span> 1.6341 sec</span><br><span class="line"></span><br><span class="line">Error distribution:</span><br><span class="line">  [1899] <span class="built_in">timeout</span></span><br></pre></td></tr></table></figure><p>数字を並べてみるとこんな感じ。</p><ul><li>Success rateが90.5%に低下</li><li>1899件のタイムアウトエラー</li><li>RPSが1620→91と、ベースラインの5.6%まで激減</li><li>20000リクエスト消化に12秒だったのが、220秒</li><li>p99が109ms→1634msと、約15倍に悪化</li></ul><p>正直、ここまで悪化するとは思っていませんでした。1台のゾンビノードが混ざったことで、クラスタ全体のスループットが18分の1まで落ち込みました。<br>面白いのがp50で、0.8msとベースラインの2.1msより速く見えます。これは「運よくロスに当たらず1発で通ったリクエスト」が半分弱あって、それらは普通に高速だからです。ロスに当たったリクエストはTCPの再送タイムアウト(初期RTOが1秒)に引っかかってp90以降で秒オーダーまでぶっ飛びます。「半分は普通に速い、半分は秒オーダーで遅い」という2山型(バイモーダル)の分布になっています。</p><h2 id="実験3：home-lab-3を完全停止させて計測"><a href="#実験3：home-lab-3を完全停止させて計測" class="headerlink" title="実験3：home-lab-3を完全停止させて計測"></a>実験3：home-lab-3を完全停止させて計測</h2><p>最後に、worker2のk3s-agentを完全に止めて計測します。「ネットワーク半壊」と「ノード完全停止」のどちらがマシか、という比較が目的です。</p><h3 id="Kubernetesから見たノードの状態-1"><a href="#Kubernetesから見たノードの状態-1" class="headerlink" title="Kubernetesから見たノードの状態"></a>Kubernetesから見たノードの状態</h3><p>実験2ではhome-lab-3はReadyでしたが、今回はNotReadyになりました。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl get nodes</span><br><span class="line">NAME         STATUS     ROLES           AGE   VERSION</span><br><span class="line">home-lab-1   Ready      control-plane   73d   v1.34.3+k3s3</span><br><span class="line">home-lab-2   Ready      &lt;none&gt;          73d   v1.34.3+k3s3</span><br><span class="line">home-lab-3   NotReady   &lt;none&gt;          73d   v1.34.3+k3s3</span><br></pre></td></tr></table></figure><p>PodとEndpointsの状態も確認します。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp get pod -o wide</span><br><span class="line">NAME                     READY   STATUS        RESTARTS   AGE   IP            NODE</span><br><span class="line">nginx-84888755c4-blc65   0/1     Pending       0          27s   &lt;none&gt;        &lt;none&gt;</span><br><span class="line">nginx-84888755c4-l7xtn   1/1     Terminating   1          14h   10.42.2.82    home-lab-3</span><br><span class="line">nginx-84888755c4-vf9zs   1/1     Running       0          21h   10.42.1.213   home-lab-2</span><br><span class="line">nginx-84888755c4-z94f4   1/1     Running       0          21h   10.42.0.31    home-lab-1</span><br><span class="line"></span><br><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp get endpoints nginx</span><br><span class="line">NAME    ENDPOINTS                      AGE</span><br><span class="line">nginx   10.42.0.31:80,10.42.1.213:80   21h</span><br></pre></td></tr></table></figure><p>home-lab-3上のPod（l7xtn）がTerminatingになり、EndpointsからもIP 10.42.2.82 が外れました。Service経由のトラフィックは健全な2Podだけに流れます。新しいPod（blc65）はPendingですが、各ノードに1Podまでの制約をかけているので置き場所がない、というだけで、Endpointsには影響しません。期待通りの挙動です。</p><h3 id="疎通確認-2"><a href="#疎通確認-2" class="headerlink" title="疎通確認"></a>疎通確認</h3><p>curlで各Podへの振り分け状況を確認します。<br>FAIL&#x3D;0。home-lab-3上のPod（l7xtn）はEndpointsから外れているので、振り分け先には現れません。健全な2Podだけで応答が返っています。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- sh -c <span class="string">&#x27;for i in $(seq 1 60); do curl -s --max-time 2 http://nginx.zombie-exp.svc.cluster.local/ || echo FAIL; done | sort | uniq -c&#x27;</span></span><br><span class="line">     34 nginx-84888755c4-vf9zs</span><br><span class="line">     26 nginx-84888755c4-z94f4</span><br></pre></td></tr></table></figure><h3 id="定量計測-2"><a href="#定量計測-2" class="headerlink" title="定量計測"></a>定量計測</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- oha -n 20000 -c 30 -t 2s --disable-keepalive http://nginx.zombie-exp.svc.cluster.local/</span><br><span class="line"></span><br><span class="line">Summary:</span><br><span class="line">  Success rate: 100.00%</span><br><span class="line">  Requests/sec: 1267.3896</span><br><span class="line"></span><br><span class="line">Response <span class="keyword">time</span> distribution:</span><br><span class="line">  50.00% <span class="keyword">in</span> 1.9522 ms</span><br><span class="line">  90.00% <span class="keyword">in</span> 91.6390 ms</span><br><span class="line">  99.00% <span class="keyword">in</span> 193.9529 ms</span><br><span class="line"></span><br><span class="line">Status code distribution:</span><br><span class="line">  [200] 20000 responses</span><br></pre></td></tr></table></figure><p>Success rateは100%に戻りました。RPSは1267で、ベースラインの78%程度。Pod数が3→2に減ったので、スループットは下がりましたが、それだけです。p99も194msとベースラインの109msから少し悪化していますが、実験2の1634msと比べれば誤差みたいなものです。<br>Kubernetesがhome-lab-3をNotReadyと判定し、Endpointsから自動で外してくれたおかげで、リクエストはちゃんと返ってきます。ゾンビノードが混ざっているときと比べると、随分と平和な数字です。</p><h1 id="実験結果のまとめ"><a href="#実験結果のまとめ" class="headerlink" title="実験結果のまとめ"></a>実験結果のまとめ</h1><p>3つの実験結果を表にまとめます。</p><div class="scroll"><table><thead><tr><th>指標</th><th>ベースライン</th><th>ゾンビノードあり</th><th>完全停止</th></tr></thead><tbody><tr><td>Success rate</td><td>100.00%</td><td>90.50%</td><td>100.00%</td></tr><tr><td>RPS</td><td>1620</td><td>91</td><td>1267</td></tr><tr><td>p50</td><td>2.1 ms</td><td>0.8 ms</td><td>1.9 ms</td></tr><tr><td>p90</td><td>69 ms</td><td>824 ms</td><td>92 ms</td></tr><tr><td>p99</td><td>109 ms</td><td>1634 ms</td><td>194 ms</td></tr><tr><td>timeout</td><td>0</td><td>1899</td><td>0</td></tr><tr><td>20000リクエスト消化時間</td><td>12 sec</td><td>220 sec</td><td>16 sec</td></tr></tbody></table></div><p>ゾンビノードありと完全停止を並べてみると、こうです。</p><ul><li>スループット:完全停止の1&#x2F;14</li><li>p99:完全停止の8.4倍</li><li>Success rate:完全停止より9.5ポイント低下</li></ul><p>ノードが「壊れている」という点はどちらも同じです。違うのは、Kubernetesがそれを検知できるかどうか。検知できれば勝手に退避してくれるけど、検知できなければ放置されたまま。たったそれだけの差で、結果がガラッと変わりました。</p><p>冒頭で書いた「中途半端に死んでるほうが厄介」を、実験を通して確認できました。</p><h1 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h1><p>今回はシンプルなHTTPリクエストのみを確認しましたが、これがDB接続のような状態を持つ処理であれば、コネクションを掴んだまま離さないリクエストがプールを食いつぶし、連鎖的な障害を招くことは容易に想像できます。</p><p>しかも厄介なことに、Kubernetes側の仕組みだけでは半壊状態を検知することが難しく、ノードがReadyである限り異常として処理されません。実際にネットワークフォルトを再現してみたことで、ゾンビノードが全体にどのような被害を及ぼすのか、解像度が上がりました。</p><p>それにしても、不調なのに「大丈夫です」と返すWorkerと、それを真に受けて普通に仕事を振り続けるControl plane。この関係はどこか見覚えがあって、なんだか親近感が湧くのは僕だけでしょうか。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;a href=&quot;/articles/20260421a/&quot;&gt;春の入門祭り2026&lt;/a&gt;の4本目です。&lt;/p&gt;
&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="Network" scheme="https://future-architect.github.io/tags/Network/"/>
    
    <category term="Kubernetes" scheme="https://future-architect.github.io/tags/Kubernetes/"/>
    
    <category term="k3s" scheme="https://future-architect.github.io/tags/k3s/"/>
    
  </entry>
  
  <entry>
    <title>非開発業務 × Context Engineering の実践知</title>
    <link href="https://future-architect.github.io/articles/20260424a/"/>
    <id>https://future-architect.github.io/articles/20260424a/</id>
    <published>2026-04-23T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260424a/サムネイル.png.png" alt="" width="1200" height="670" loading="lazy"><p><a href="/articles/20260421a/">春の入門祭り2026</a>の3本目です。</p><h1 id="1-はじめに"><a href="#1-はじめに" class="headerlink" title="1. はじめに"></a>1. はじめに</h1><p>CSIGの星名です。今回は、お問い合わせ対応業務にLLM Agentを導入してみたお話です。</p><p>最近、開発の現場では、PdMやプログラミング、QAといった役割ごとにAgentを使い分ける手法が、当たり前のように語られるようになってきました。</p><p>一方で、非開発業務に目を向けてみると、議事録作成のような単発の活用は進んでいるものの、業務フロー全体をAgentが駆動するような事例は、まだまだ少ないと感じています。というのも、定型業務であればあるほど、そのチーム固有の知識や運用ルールに強く依存するため、外へ向けた知見として一般化しづらい、という側面があるからでしょう。</p><p>だからこそ、こうした現場でAgentを活かすには、「何を渡し、どう保つか」を設計する Context Engineering が重要になる。少なくとも私たちのケースではそういう結論になりました。</p><p>この記事では、私たちが実際に悩みながら取り組んだプロセスを共有します。</p><h2 id="1-1-プロンプトを磨いても、エージェントは賢くならなかった"><a href="#1-1-プロンプトを磨いても、エージェントは賢くならなかった" class="headerlink" title="1.1 プロンプトを磨いても、エージェントは賢くならなかった"></a>1.1 プロンプトを磨いても、エージェントは賢くならなかった</h2><p>GitHub Copilot Agent を導入して、 <strong>問い合わせ対応の「調査～回答文作成～レビュー」を仕組み化</strong> しようと試みたときの話です。いざ動かしてみると「さっき調べたことを忘れる」「指示を無視する」といった事象が頻発しました。当初はプロンプトの書き方の問題かなと考えていたのですが、プロンプトの書き方以前に、そもそも LLM に何をどう渡すかという「コンテキストの設計」に課題がありました。</p><blockquote><p>本当の失敗モードはプロンプトが悪いことではなく、コンテキストの組み立て（context assembly）が悪いこと</p><p>— Andrej Karpathy</p></blockquote><p>設計を見直す中で参考になったのが、LangChain 等で提唱されている <strong>Select, Write, Isolate, Compress</strong> の 4 戦略です。</p><img src="/images/2026/20260424a/Context_Engineering_for_Agents.png" alt="Context_Engineering_for_Agents" width="1200" height="427" loading="lazy"><blockquote><p><a href="https://blog.langchain.com/context-engineering-for-agents/">Context Engineering for Agents — LangChain Blog</a></p></blockquote><h2 id="1-2-試行錯誤のポイントとなった-3-つの論点"><a href="#1-2-試行錯誤のポイントとなった-3-つの論点" class="headerlink" title="1.2 試行錯誤のポイントとなった 3 つの論点"></a>1.2 試行錯誤のポイントとなった 3 つの論点</h2><p>この 4 戦略を軸に、取り組んだ内容を 3 つの観点で紹介します。</p><ol><li><strong>何を渡せば仕事ができるか</strong>（情報の形式知化）<br>散在する資料や暗黙知を、LLMが確実に読み取れる「知識」としてどう整えるか</li><li><strong>どう思考の状態を保つか</strong>（文脈の引継ぎ）<br>会話の長期化による劣化を防ぐために、一連のプロセスをどう区切り、成果物を引き継いでいくか</li><li><strong>どう期待通りに動いてもらうか</strong>（振る舞いの実装）<br>実現したい業務フローに対し、ツールの機能をどう組み合わせてAgentの動きを定義するか</li></ol><p>1 や 2 でお話しすることは、開発者目線だと、目新しいテクニックというよりはこれまで大切にされてきた基本に近いものかもしれません。ただ、非開発業務で実践してみてもやっぱりこれが大事なんだな、という感覚がありました。そんな私たちの実践知もあわせて紹介します。</p><p>3 に関しては、GitHub Copilot Agent を前提としています。特に handoff は Copilot 固有の機能なので、他のツールでは別の工夫が必要になるかもしれません。私たちの業務（技術調査が中心など）に合わせた内容ですが、Agent を業務に組み込むときの考え方の一例として、何かのヒントになれば幸いです。</p><h1 id="2-何を渡せば仕事ができるか"><a href="#2-何を渡せば仕事ができるか" class="headerlink" title="2. 何を渡せば仕事ができるか"></a>2. 何を渡せば仕事ができるか</h1><p>エージェントも前提知識がないことには仕事のしようがないので、まずは何を読ませるかという視界をどう整えてあげるか。そこから話を始めることにします。</p><h2 id="2-1-エージェントに「材料」をすべて渡す"><a href="#2-1-エージェントに「材料」をすべて渡す" class="headerlink" title="2.1 エージェントに「材料」をすべて渡す"></a>2.1 エージェントに「材料」をすべて渡す</h2><p>最初に取り組んだのは、<strong>問い合わせ対応に必要な情報を git submodule で一つに集約した「巨大リポジトリ」を作る</strong>ことでした。コードもマニュアルも、<code>git clone</code> 一発ですべてが手元に揃う。いわば、エージェントにまず物理的な視界（<strong>Select</strong>）を与える作業です。まずは必要な情報をすべてエージェントの目の前に置く。物理的な土台を作るだけでも、一定の成果はありました。</p><h2 id="2-2-暗黙知となっていた「対応の作法」を言語化する"><a href="#2-2-暗黙知となっていた「対応の作法」を言語化する" class="headerlink" title="2.2 暗黙知となっていた「対応の作法」を言語化する"></a>2.2 暗黙知となっていた「対応の作法」を言語化する</h2><p>ただ、材料を揃えただけでは、的外れな回答が目立ちました。文脈を知らないエージェントが、足りない情報を勝手な推測で補って回答を作ってしまう。そういう状況でした。</p><p>原因は、ソースコードや Wiki といった「情報の断片」はあっても、それをどう扱うかという <strong>「仕事の作法」</strong> が欠けていたことでした。</p><p>問い合わせ対応は、調査結果をそのまま伝えればよいわけではありません。相手が求めている情報をどう選ぶか。どの順番で伝えるか。出してよい情報の境界線はどこか。どこまでを今回の合意の着地点とするか。</p><p>こうした対応の要諦は、熟練メンバーなら無意識にやっていることです。Wiki を作る際にも、わざわざ書くのは冗長だと感じて、あえて省いてきた部分でした。これを丁寧に言語化（<strong>Write</strong>）することが、非開発系の業務においては特に重要でした。</p><p>具体的には、いまはどのフェーズなのか。目的は何か。何を成果物として、どんな観点で取り組むのか。そういった業務フローを一つずつ書き出していきました。</p><h2 id="2-3-業務フローを書いたら、そのままエージェントへのインプットになった"><a href="#2-3-業務フローを書いたら、そのままエージェントへのインプットになった" class="headerlink" title="2.3 業務フローを書いたら、そのままエージェントへのインプットになった"></a>2.3 業務フローを書いたら、そのままエージェントへのインプットになった</h2><p>ポイントは、<strong>AI のための特別なファイルを作ったわけではない</strong>、ということです。複雑なスクリプトを書いたわけではなく、あくまで「こういうときはこうする」というレベルの業務知識を整えただけ。それがそのまま、Agent へのインプットになりました。</p><p>Agent 定義の設計では、Anthropic の <strong>Progressive Disclosure（段階的開示）</strong> の考え方も参考にしました。「新人向けオンボーディングガイド」のメタファで設計すると良いという話で、納得感がありました。</p><blockquote><p>Skills は「<strong>新人向けオンボーディングガイド</strong>」のメタファーで考えると設計しやすい。</p><p>— <a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills">Equipping agents for the real world with Agent Skills — Anthropic Engineering</a></p></blockquote><h1 id="3-どう思考の状態を保つか"><a href="#3-どう思考の状態を保つか" class="headerlink" title="3. どう思考の状態を保つか"></a>3. どう思考の状態を保つか</h1><p>「何を渡すか」を整えたら、次は「どうやり取りするか」の話です。</p><h2 id="3-1-一つの長い会話は劣化する"><a href="#3-1-一つの長い会話は劣化する" class="headerlink" title="3.1 一つの長い会話は劣化する"></a>3.1 一つの長い会話は劣化する</h2><p>はじめは何も考えず、手軽に1つのチャットセッションですべてを完結させようとしていました。調査から回答方針の作成、回答文作成、レビューまで。</p><p>ところが、会話が長くなるにつれ、エージェントの挙動が明らかに怪しくなりました。さっき調査したはずのことを忘れたり、同じことを何度も聞き返してきたり。あるいは、前後のつじつまが合わない回答を平気で出力し始める。</p><p>こうした劣化は <strong>Lost in Conversation</strong> という現象として知られています。Philippe Laban らによると、マルチターンの会話では、わずか 2 ターン目から性能低下が始まり、精度が大幅に落ちてしまうのだそうです。</p><img src="/images/2026/20260424a/multi-turn-conversation.png" alt="multi-turn-conversation.png" width="649" height="366" loading="lazy"><blockquote><p><a href="https://arxiv.org/abs/2505.06120">LLMs Get Lost in Multi-Turn Conversation</a> (2025)</p></blockquote><p>これは2025年の調査ですが、一方で、いまの多くのモデルの挙動をみていると、コンテキストウィンドウが圧迫され始めると、会話を要約して空きを作っている様子がみてとれます。ただ、その要約の過程で必要な情報が抜け落ちてしまうことが頻繁に起こっていて困るなぁと思うときもあります。特に不具合調査では意外なところにヒントが落ちていたりするので。</p><p>また、<strong>Lost in the Middle</strong> と呼ばれるように、情報量が増えるほど「中ほどに書かれた情報」を軽視してしまう性質もあります。</p><img src="/images/2026/20260424a/lost-in-the-middle.png" alt="lost-in-the-middle.png" width="1166" height="358" loading="lazy"><blockquote><p><a href="https://aclanthology.org/2024.tacl-1.9/">Lost in the Middle: How Language Models Use Long Contexts</a> (2024)</p></blockquote><p>結局、一つの会話で全部をこなすのは、そもそも筋が悪かったということでした。現場で感じていた「なんだか話が通じなくなる」という感触の正体が見えました。</p><h2 id="3-2-いつでもセッションを捨てられるよう、成果物をファイルに「逃がす」"><a href="#3-2-いつでもセッションを捨てられるよう、成果物をファイルに「逃がす」" class="headerlink" title="3.2 いつでもセッションを捨てられるよう、成果物をファイルに「逃がす」"></a>3.2 いつでもセッションを捨てられるよう、成果物をファイルに「逃がす」</h2><p>長い会話が劣化するなら、いつでもセッションを切り直せる状態にすればいい。ということで、履歴という曖昧な記憶に頼るのをやめて、成果物を「記録」としてファイルに逃がす方針にしてみました（<strong>Compress &amp; Isolate</strong>）。</p><p>具体的には、まず <code>会話履歴.md</code> を用意し、調査工程ではそれを読み込んで <code>調査結果.md</code> を吐き出す。後続の工程は、その2つのファイルだけを読めば仕事ができる状態にする。という構成にしました。</p><p>このファイルを介したリレー方針には、いくつか利点がありました。</p><ul><li><strong>セッションの断絶を恐れなくていい</strong><br>調査フェーズなどは時間がかかり、セッションが切れがちです。こまめにファイルへ書き出しておけば、事故が起きても「最新のファイル」からいつでも再開できます。</li><li><strong>情報の純度を保てる</strong><br>前の会話にあった「試行錯誤のノイズ」を捨て、整理された結果だけを次に渡せます。エージェントにとっても、その方が圧倒的に読みやすいはずです。</li><li><strong>人間が「横入り」しやすい</strong><br>書き出されたファイルは人間も編集できます。エージェントの調査が甘ければ、人間がファイルを直接直して次へ進める。この「手戻りのしやすさ」が、実運用では大きな安心感になりました。</li></ul><p>結局、履歴という過去の蓄積を無理に引きずるより、成果物という今の結論だけを引き継いでいく方が、設計としては筋が良かったと感じています。</p><h1 id="4-どう期待通りに動いてもらうか"><a href="#4-どう期待通りに動いてもらうか" class="headerlink" title="4. どう期待通りに動いてもらうか"></a>4. どう期待通りに動いてもらうか</h1><p>ファイルを「逃がす」方針が決まったら、次はそれを扱うAgentをどう作るかについてです。ここからはかなり今回の実装に寄った具体的な話に移っていきます。</p><h2 id="4-1-要件整理：入出力の管理と人間との協調"><a href="#4-1-要件整理：入出力の管理と人間との協調" class="headerlink" title="4.1 要件整理：入出力の管理と人間との協調"></a>4.1 要件整理：入出力の管理と人間との協調</h2><p>Agent への業務指示に関しては、2.2で手順書を整備しているので、「このリンクを読んで指示に従ってください」で終わります。楽ちんです。</p><p>その上で、Agent に求める要件を改めて整理してみました。</p><ul><li><strong>要件①：セッションの入出力管理</strong><ul><li>整備した業務指示を確実に読み込めること</li><li>前の工程が吐き出した「成果物ファイル」を、新しいセッションのインプットとして引き継げること</li><li>自分の仕事を終えたら、また結果をファイルに書き出すこと</li></ul></li><li><strong>要件②：人間との協調</strong><ul><li>フェーズの区切りで必ず立ち止まり、人間がレビューを挟めること（調査が間違えば回答も間違うため、あえて一気通貫にはしない）</li><li>次にどの工程へ進むかは、勝手に判断せずに人間の指示を仰ぐこと</li></ul></li></ul><p>「フェーズごとに停止して人間が確認する」という運用を前提に、今回は業務フローを <strong>「起票・調査・回答方針作成・回答文作成・レビュー」のフェーズに切り分け</strong> ました。</p><p>この管理上の「区切り」を、具体的に Copilot のどの機能で具現化するか、ここからはツールの仕様に基づいた具体の話になります。</p><h2 id="4-2-技術選定：Custom-Agent指定で指示を確実にロードさせる"><a href="#4-2-技術選定：Custom-Agent指定で指示を確実にロードさせる" class="headerlink" title="4.2 技術選定：Custom Agent指定で指示を確実にロードさせる"></a>4.2 技術選定：Custom Agent指定で指示を確実にロードさせる</h2><p>要件①の入出力管理において、まず解決すべきは「プロンプト以前に、そもそも指示がAgentに届くか」という問題です。GitHub Copilot には指示を置く場所がいくつかありますが、仕様上の振る舞いがそれぞれ異なります。</p><p>どれが今回の実務に適しているのか、整理したのが以下の図です。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Agentの振る舞いをどう定義するか</span><br><span class="line">│</span><br><span class="line">├─ 常に適用したい（プロジェクト共通ルール）</span><br><span class="line">│   ├─ 全Agent共通 → copilot-instructions.md / AGENTS.md</span><br><span class="line">│   └─ 特定ファイル操作時のみ → *.instructions.md (applyTo)</span><br><span class="line">│       └─ ⚠ ファイル操作を伴わない業務には不向き</span><br><span class="line">│</span><br><span class="line">├─ 特定タスクの時だけ使いたい</span><br><span class="line">│   ├─ 定型プロンプトを再利用 → *.prompt.md</span><br><span class="line">│   ├─ 専門ロールとして振る舞わせたい → *.agent.md ★</span><br><span class="line">│   └─ 専門知識を必要時だけ読み込ませたい → skills/SKILL.md</span><br><span class="line">│</span><br><span class="line">└─ 外部ツール・APIと連携したい → MCP Servers / Hooks</span><br></pre></td></tr></table></figure><p>結論から言えば、今回のケースでは <strong><code>.github/agents/*.agent.md</code>（Custom Agent）</strong> を選ぶのが最も手堅いという判断になりました。</p><p>補足すると、当初は <code>.github/instructions/*.instructions.md</code> を試していましたが、無視される挙動が頻発しました。これは、<code>applyTo</code> で指定したファイルパターンを操作していないと指示がロードされないという仕様上の制約があるためです。コードファイルの操作を伴わない運用業務には向きませんでした。</p><p>UIから明示的に指定した瞬間に、コンテキストとして確実にロードされる。この確実性を買って、Custom Agent をベースにした構成を選んでいます。</p><h2 id="4-3-全体像：handoff-機能による人間主導のフロー"><a href="#4-3-全体像：handoff-機能による人間主導のフロー" class="headerlink" title="4.3 全体像：handoff 機能による人間主導のフロー"></a>4.3 全体像：handoff 機能による人間主導のフロー</h2><p>これで要件①はクリアです。次は要件②、人間との協調です。</p><p>ここで使ったのが、GitHub Copilot の <strong>handoff</strong> 機能です。handoff とは、Agent が会話の中で次に推奨する Agent をボタンとして Copilot Chat のウィンドウに提示し、人間がワンクリックでそのエージェントを選択・起動できる仕組みです。</p><p>どの Agent を候補として提示するかは Agent 側が判断してくれるので、人間のフェーズ制御をサポートする立ち位置になります。各フェーズ（起票 → 調査 → 回答方針 → 回答文作成 → レビュー）に対応した <code>agent.md</code> をそれぞれ作成し、窓口となるオーケストレーター Agent がこれらを handoff 候補として提示します。人間がどのフェーズに進むかを選択する。これで、フェーズごとの停止と人間レビューが自然に実現できました。</p><p>加えて、handoff 先の Agent はメイン Agent としてフルのコンテキストウィンドウを使えます。成果物はファイルに書き出し、次の Agent がそれを読み込むので、会話履歴の蓄積による劣化もありません。</p><h3 id="参考：利用イメージ"><a href="#参考：利用イメージ" class="headerlink" title="参考：利用イメージ"></a>参考：利用イメージ</h3><ol><li>会話開始時に GitHub Copilot チャットウィンドウから 「inquiry」Custom Agent を指定</li><li>問い合わせ内容を貼り付け（機密情報はマスクしておく）</li></ol><img src="/images/2026/20260424a/image.png" alt="image.png" width="824" height="472" loading="lazy"><ol start="3"><li>チャット下部に次フェーズ開始ボタンが現れるので、任意を押下</li><li>ボタン押下でagent、プロンプトが自動入力されるので送信するだけ</li></ol><img src="/images/2026/20260424a/image_2.png" alt="image.png" width="760" height="636" loading="lazy"><ol start="5"><li>各フェーズが終われば次ののhandoffボタンを押下で、次のフェーズへ移行</li></ol><h3 id="参考：SubAgent-パターンとの比較"><a href="#参考：SubAgent-パターンとの比較" class="headerlink" title="参考：SubAgent パターンとの比較"></a>参考：<strong>SubAgent パターンとの比較</strong></h3><p>補足になりますが、他のパターンとして、Orchestrator が SubAgent を自動で呼び出す方式（<code>runSubagent</code>）も検討しましたが、不採用としています。</p><div class="scroll"><table><thead><tr><th>比較項目</th><th>SubAgent パターン（不採用）</th><th>handoff パターン（採用）</th></tr></thead><tbody><tr><td>コンテキストウィンドウ</td><td>小さい可能性（調査で途中停止する事象あり）</td><td>フル（メイン Agent として起動）</td></tr><tr><td>人間レビュー</td><td>挟めない（Orchestrator が自動制御）</td><td>自然に挟める（handoff 選択時にレビュー）</td></tr><tr><td>コンテキスト分離</td><td>○（まっさらで起動）</td><td>○（handoff 先もまっさらで起動）</td></tr><tr><td>進行管理</td><td>Orchestrator が自動で制御</td><td>親 Agent が候補を提示、ユーザーが選択</td></tr></tbody></table></div><p>上記の比較はあくまで「人間がフェーズ間でレビューを挟む」という私たちの要件に照らしたものです。全自動化パイプラインや人間レビューが不要な定型タスク、高スループットが優先される場面では、SubAgent パターンの方が適しているケースも十分あります。</p><blockquote><p>参考: <a href="https://zenn.dev/openjny/articles/e11450f61d067f">GitHub Copilot サブエージェントによるオーケストレーター パターンの実践 — openjny</a></p></blockquote><h2 id="4-4-業務知識が整理されていれば、agent-md-は薄くて済む"><a href="#4-4-業務知識が整理されていれば、agent-md-は薄くて済む" class="headerlink" title="4.4 業務知識が整理されていれば、agent.md は薄くて済む"></a>4.4 業務知識が整理されていれば、agent.md は薄くて済む</h2><p>Custom Agent + handoff chain という構成が決まったところで、では <code>agent.md</code> に実際に何を書いたか。<strong>frontmatter と、整備した手順書ファイルへの参照リンク、成果物ファイルの入出力指示のみです</strong>。業務知識の本体はあくまで手順書ファイル側にあるので、<code>agent.md</code> 自体は非常に端的。高度なスクリプトも複雑なパイプラインも書いていません。業務知識を形式知化しておいたおかげで、Agent のために特別な仕組みを作り込む必要がなくなりました。業務フローの言語化がそのまま Agent へのインプットになっているので、ツールへの依存は薄く済んでいます。</p><blockquote><p>scaffolding（モデル補助コード）は次のモデルで不要になる。<br>投資 vs 待機のトレードオフを常に意識</p><p>— <a href="https://www.youtube.com/watch?v=PQU9o_5rHC4">Boris Cherny (Anthropic), Y Combinator インタビュー</a></p></blockquote><p>個人的に納得感のある考え方で、今回 <code>agent.md</code> を薄く保ったのも同じ理由からです。モデルが進化すれば文言を調整するだけで済むはずで、過度にツール依存した自動化は次のモデルで丸ごと不要になるリスクがあると感じています。</p><h1 id="まとめ"><a href="#まとめ" class="headerlink" title="まとめ"></a>まとめ</h1><p>今回は、お問い合わせ対応の調査～回答作成の業務を 人間とLLMでうまく協調するための運用フローについて考えてみました。</p><ol><li><strong>何を渡せば仕事ができるか（Select &amp; Write）</strong><br>暗黙知をなくし業務フローを言語化したことが、そのまま Agent へのインプットになった</li><li><strong>どう思考の状態を保つか（Isolate &amp; Compress）</strong><br>長い会話ではコンテキスト管理が難しくなる傾向がある。会話履歴ではなくファイルをリレーすることで、新鮮なコンテキストを保てた。</li><li><strong>どう期待通りに動いてもらうか（Select）</strong><br>handoff 機能で入出力管理と人間との協調を両立できた。<br>業務知識が整理されていれば <code>agent.md</code> は薄くて済む</li></ol><p>モデルの進化で最適解も変わっていくと思うので、引き続き試行錯誤しながらアップデートしていくつもりです。</p><p>問い合わせ業務に限らず、LLM を業務に組み込む際の参考になる部分があれば幸いです。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260424a/サムネイル.png.png&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;670&quot; loading=&quot;lazy&quot;&gt;

&lt;p&gt;&lt;a</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="LLM" scheme="https://future-architect.github.io/tags/LLM/"/>
    
    <category term="Copilot" scheme="https://future-architect.github.io/tags/Copilot/"/>
    
    <category term="AIエージェント" scheme="https://future-architect.github.io/tags/AI%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88/"/>
    
    <category term="コンテキストエンジニアリング" scheme="https://future-architect.github.io/tags/%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%83%AA%E3%83%B3%E3%82%B0/"/>
    
  </entry>
  
  <entry>
    <title>これだけやろうOJT</title>
    <link href="https://future-architect.github.io/articles/20260423a/"/>
    <id>https://future-architect.github.io/articles/20260423a/</id>
    <published>2026-04-22T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260423a/top.avif" alt="" width="1200" height="634" loading="lazy"><h1 id="1-はじめに"><a href="#1-はじめに" class="headerlink" title="1. はじめに"></a>1. はじめに</h1><p>はじめまして。TIG - Technology Innovation Group - の清水です。2016年3月に転職してきて、10年経ったインフラ屋さんです。前職コンピュータメーカーには15年いました。</p><p>4月は新社会人が街に目立つ季節です。桜と新しいスーツの組み合わせが素敵ですね。ということで、OJTを進める上で大事なこと、を<a href="/articles/20260421a/">「春の入門祭り2026」</a>に合わせて共有します。Futureでは数ヶ月の新人研修の先にOJTがやってくるので、実際にはもう少し先の話ですね。</p><h1 id="2-これだけやろうOJT-やること・目的"><a href="#2-これだけやろうOJT-やること・目的" class="headerlink" title="2. これだけやろうOJT　やること・目的"></a>2. これだけやろうOJT　やること・目的</h1><p>仕事をする中で、人びとが同じような落とし穴に、同じように嵌まってもがく状況をみてきました。もちろん私ももがいてきました。「これどうにかして丸っと全部解消できないかな」と考えていたら、OJTの進め方に行きつきました。</p><p>「これだけ」と言いながら3つ書きます。ずるいですね。</p><h2 id="2-1-ともに希望を語る"><a href="#2-1-ともに希望を語る" class="headerlink" title="2.1. ともに希望を語る"></a>2.1. ともに希望を語る</h2><p><strong>目的：</strong> 新人さんも、チームもワクワクするため。だって楽しい方がいいから。</p><p><strong>説明：</strong> 一歩目は希望を抱くところから。最初、新人さんは不安です。汝のなりたい姿を述べよ！と言っても難しいかもです。持っている情報も少ないですし。とりあえずあなたの2年後の姿想像したよ・こんなんどうかな、と先輩が提示してみると、そこから考えが進むかもしれません。互いに希望を話し合い、未来の姿をともに見えるようにしていくと、日々の活動が楽しくなります。</p><h2 id="2-2-あの人はどうなったら嬉しいのだろう、をともに語る"><a href="#2-2-あの人はどうなったら嬉しいのだろう、をともに語る" class="headerlink" title="2.2. あの人はどうなったら嬉しいのだろう、をともに語る"></a>2.2. あの人はどうなったら嬉しいのだろう、をともに語る</h2><p><strong>目的：</strong> 仕事の勘所をつかむため　＆　できるかも、を感じるため</p><p><strong>説明：</strong> 中核です。「こうすれば仕事はうまくいく」というゲームの基本ルールを理解する活動です。暗記テストで点をとるには、音読・黙読・筆記が有効ですが、仕事は違います。「あの人はどうなったら嬉しいだろう」を考える、が基本動作です。みんな消費者としてはベテランなので、やってみると大事な部分が分かるぞ！という感覚が得られます。全体が把握できるかも感も出てきて、ついでにこの仕事、すんごく意味あるじゃん！と楽しくなります。楽しいことはいいことです。</p><h2 id="2-3-美しさ、と仕事を同時に語る"><a href="#2-3-美しさ、と仕事を同時に語る" class="headerlink" title="2.3. 美しさ、と仕事を同時に語る"></a>2.3. 美しさ、と仕事を同時に語る</h2><p><strong>目的：</strong> 道から外れないため　＆　使命を見つけるため</p><p><strong>説明：</strong> 応用です。油断すると仕事は経済合理性と効率一辺倒になりやすいです。ビジネスなので一見正しく見えますが、倫理を忘れた追求により複数の企業で悲しい事件が起きています。それを起こさないためには、現場での美しさの感覚がもっとも大切です。美しさを意識して仕事をすると、道から外れないだけでなく、その先に何を見出すか、という使命に近づくことができ、仕事が楽しくなります。</p><p>これで全部です。シンプルー。</p><p>もうちょっと詳しく、と思ってくれた方のために細かく説明します。</p><h1 id="3-これだけやろうOJT-詳しいやり方"><a href="#3-これだけやろうOJT-詳しいやり方" class="headerlink" title="3. これだけやろうOJT　詳しいやり方"></a>3. これだけやろうOJT　詳しいやり方</h1><p>具体何をするか、と考え方を書きます。</p><p>えーと、長いです。</p><h2 id="3-1-ともに希望を語る"><a href="#3-1-ともに希望を語る" class="headerlink" title="3.1. ともに希望を語る"></a>3.1. ともに希望を語る</h2><p>ある日、新人さんがチームにやってきます。</p><h3 id="先輩のやること"><a href="#先輩のやること" class="headerlink" title="先輩のやること"></a>先輩のやること</h3><p>2年後のその人の姿を絵にして見せましょう。こんなふうになっていたら、その人は楽しいんじゃないかな、と想像しながら用意します。絵が良いです。絵には解釈の余地を残せるのと、色使いで心持ちを想像させる良さがあります。私が使ったことのある主題はこちらです。</p><blockquote><ul><li>言葉の魔術師</li><li>兵站の鬼</li><li>俺的ベスト案マン</li></ul></blockquote><p>新人さんの頭の中とは、当然ずれがあります。時間をかけて、2年後の誰々さん像をチームで作っていきます。新人さんの中にすでに明確な像があったら素敵ですね。是非取り込みましょう。</p><h3 id="新人さんのやること"><a href="#新人さんのやること" class="headerlink" title="新人さんのやること"></a>新人さんのやること</h3><p>自分に二つ名をつけましょう。気楽に。ちがうな、と思ったら後で変えればいいです。変えても世界は滅ばないので大丈夫です。過去にはこんなのがありました。</p><blockquote><ul><li>体力化け物ツヨツヨ論理マン</li><li>飛躍する思考力、土台の耐久力</li><li>心は熱く、頭はクールに、歩みは着実に</li></ul></blockquote><h3 id="説明・どこを目指すか"><a href="#説明・どこを目指すか" class="headerlink" title="説明・どこを目指すか"></a>説明・どこを目指すか</h3><p>いつか新人さんを送り出す日が来ます。その時にはもう”新人さん”ではないでしょう。</p><p><strong>「次のプロジェクトに、私はこの人をどう自慢しながら送り出してあげられるだろう」</strong></p><p>送り出すその日を考えて、OJTを過ごします。</p><ul><li>この人はどんな特性だろう</li><li>仕事をうまく進めるための力のうち、この人の得意はどれだろう</li><li>この人の苦手は、どんな人との組み合わせでうまくいくだろう</li></ul><p>これを新人さんとともに考えます。チームで作りあげていく希望です。2年後の像と二つ名は、それを考える助けになってくれます。</p><p>OJT開始、ということで案件の説明をし、タスクを実際にやってもらいます。何に心をおきながら日々活動すると良いか。以降説明します。</p><h2 id="3-2-あの人はどうなったら嬉しいのだろう、をともに語る"><a href="#3-2-あの人はどうなったら嬉しいのだろう、をともに語る" class="headerlink" title="3.2. あの人はどうなったら嬉しいのだろう、をともに語る"></a>3.2. あの人はどうなったら嬉しいのだろう、をともに語る</h2><h3 id="やること"><a href="#やること" class="headerlink" title="やること"></a>やること</h3><p>最初に任される仕事で<strong>嬉しさ分析</strong>をやりましょう。</p><p>画面開発・打鍵テスト・ハード設計・障害テスト、何でも良いです。でもその仕事をすると、<strong>絶対に</strong>誰かは嬉しいです。誰がどう嬉しいか、を書き出します。</p><ul><li>自チームメンバー</li><li>自チームリーダー</li><li>隣のチーム</li><li>プロジェクト全体のリーダー</li><li>お客さんの担当の方</li><li>その上司の方</li><li>部長さん、社長さん</li><li>最後の最後の、本当のお客さん</li></ul><p>新人さんがいきなりやるのは大変なので、まず先輩がやってみせます。もし先輩が分からなかったら大チャンスです。兎に角みんなで想像を言い合って仮説を作り、分かりそうな人に聞いてみましょう。コツは「最後の最後の、本当のお客さん」から考え出すことです。みなさん消費者としてはベテランなので、実は一番大事なところの知見・実感を既に持っています。</p><p>この嬉しさ分析を色んなタスクで繰り返すと、自分たちがおおよそどちらの方角に進むのが正しそうか、が分かるようになります。鼻が効くようになります。風が読めるようになる、と言ってもいいでしょう。すんごい毒々しい黄色と黒のナマコみたいなものを、俺はマフラーとして売りたいんだ、と誰かが言い出したとして、それって誰が嬉しいの？と周りが言えるチームになっているでしょう。ちょっと変な例ですが、でもそういうことです。</p><p>先輩は、分からんなぁ、ということがあったらどんどん、分からん！と言いましょう。その瞬間、後輩がともに戦う仲間になってくれます。自分も気が楽になります。自分が先輩を助けたいのと同じように、後輩はあなたを助けたいと思ってくれています。</p><h3 id="説明"><a href="#説明" class="headerlink" title="説明"></a>説明</h3><p>しごと、が誰かの幸せを叶えるものである以上、IT・飲食・不動産と何でも良いのですが、その活動で必ず誰かが幸せになっています。サービス提供側もまた喜んでいます。ひとつの仕事が続いているのは、全体の喜びのバランスが取れているということです。</p><p>（　あの人はどうなったら嬉しいのだろう　）</p><p>これが仕事の北極星です。</p><p>ステークホルダー分析とか、カスタマーサティスファクションとかカタカナを使ってもいいのですが、でも要するにそういうことです。嬉しさが波のように伝わっているのです。</p><p>分業の時代です。1つのサービスが誰かの元に届くまでに、多くの人が関わっています。ハンバーガーが食べられるまで、を考えると分かりやすいですよね。元は誰かが大切に育ててくれていた牛であり、レタスであり、小麦でした。そして当然、東京で生産していたわけじゃないです。なのに東京で美味しいハンバーガーが食べられます。とにかく仕事に関わるすべての人の気持ちを想像し、書き出す。<strong>これがOJTでやることです。</strong></p><p>全体構想、要件定義、- 中略 - 、運用と様々な工程がありますが、どの工程でも必ず成長できます。なぜか。全ての仕事は誰かの嬉しさのために存在しており、同じ構造だからです。最終的なお客さんである誰かが実際に嬉しい、と思うまでの「嬉しさの伝搬」を中心に考れば、現時点の自分の担当がたまたまどこか、というだけの話です。担当、というのはそこだけやっていればいい、という話ではなく、価値を伝搬し実現することが最上位にあり、その文脈の中で担当分に専門性を持つということです。その専門性から見て全体の構造を変えた方がより価値を生み出せる、と気がついたならば、そこに仲間とともに立ち向かいます。仲間は自社だけでなく「嬉しさの伝搬」に関わる全ての生産者です。みなで価値を生んでいるのですから。</p><p>価値、つまり最終的なお客さんの嬉しさと自分のタスクの関係性をみんなで深く考える。そこで培われた考え方・嗅覚は別の工程でも活かせる技能です。長い間、私たちを助けてくれます。</p><p>嬉しさの伝搬をたどって仕事の全体像が見えてくると、よくこんな一連の「動くネットワーク」ができたもんだな、と驚きます。そこに自分も参加して、嬉しさの維持と合わせて新しい喜びを生み出せるのは、とても心躍ることではないでしょうか。</p><p>なお<a href="/articles/20210520a/">エンジニアとビジネス、という観点で非常に優れた記事</a>があるのでぜひご覧ください。悩める私を折りにふれ励ましてくれた記事です。</p><h2 id="3-3-美しさと仕事、を同時に語る"><a href="#3-3-美しさと仕事、を同時に語る" class="headerlink" title="3.3. 美しさと仕事、を同時に語る"></a>3.3. 美しさと仕事、を同時に語る</h2><h3 id="やること-1"><a href="#やること-1" class="headerlink" title="やること"></a>やること</h3><p>タスクをする際、<strong>美しさ分析</strong>をしましょう。</p><p><strong>このタスクをすることは、人として美しいのか。</strong></p><p>これをチームで話し合います。時間を使って仕事をするわけですが「時間を使う」は命を削る、と同義です。仲間の命を削ってまで、そのタスクはやる意義・価値があるのか。美しい行為なのか。人として。いきものとして。意義が感じられないなら、立ち止まって考える必要があります。</p><p>意義を、仲間と日常的に話し合いましょう。意義がないと分かったなら、活動はやめましょう。意義が見えにくいだけ、というケースもあります。価値の構造を皆で納得できるまで話し合いましょう。その活動を始めた人、は多くの背景と文脈を知っています。自分たちの仮説を是非話してみてください。数年に渡る壮大な計画かもしれません。それが読み解けたとき、物事の多層性が実感を持って理解でき、事象を精度高く捉える力と、未来を想像する力が増しているでしょう。</p><h3 id="説明-1"><a href="#説明-1" class="headerlink" title="説明"></a>説明</h3><p>社会人になる ＝ 消費者から生産者に変身する、というのはつまり、よくよく考えないと間違ったことに直接手を貸しかねない、ということです。報道で、どうしてこうなった、という悲しい例を耳にします。そうならないようにしたいですね。自分の良心に照らして、その行為は恥ずかしくないか？という物差しを持つことです。</p><p><strong>我々の仕事は美しい、なぜならば</strong>、を語れるようになったチームは頭の中に、より多くの戦い方が浮かぶようになっています。同時に、仕事の楽しさも増しているはずです。なおこの「意義を考える」活動は、意義を問う後輩 vs 答える先輩、という構図でなく、タスクに対してチームで問いかけ・思考実験をする姿勢が良いです。正解などないが、皆で着想を出し合って正解らしきものに近づいていく、という動きを作り出すのにも役立ちます。</p><p>そして仕事をする上で、最終的なお客さん、関わる人たちのことだけではなく、その先の未来に自分たちが何を反映したいのか、を合わせて考えたなら、その仕事が自分にとってひとつの物語として立ち上がってくるのではないでしょうか。消費者の目線をもち生産し、現在に幸せを作りながら、未来に何を渡していくかを考える。私たちの先輩はそうしてきたのだし、私たちもまた、そうしてバトンを渡していきたいものです。</p><h1 id="4-まとめ"><a href="#4-まとめ" class="headerlink" title="4. まとめ"></a>4. まとめ</h1><p>今回は、OJTを進める上で大切なことを書きました。</p><ul><li><strong>ともに希望を語る ・ ２年先の姿をともに描く</strong></li><li><strong>あの人はどうなったら嬉しいだろう、をともに語る</strong></li><li><strong>美しさと仕事、を同時に語る</strong></li></ul><p>先輩役の人も、後輩役の人も、どちらも完璧超人ではなくて人なので、支え合いながら前に進めるといいですよね。</p><p>私が働きだして長い月日が経ちました。目にした苦しみの多くは、「自分の頭で自由に考える力」を奪われたことに起因するものでした。受験教育の弊害、と私は考えています。人がよく生きる上で、もう有効に機能しない仕組みだ、と。</p><p>それでも今と同じ教育の仕組みが続くならば、それに対抗するために、私たちにどのような方法が取りうるのか、の私なりの答えの一つが、このOJTの話です。この話が、みなさんの助けになることを願っています。</p><p>最後に、OJTに関わるみなさんへアラゴンの詩をおくります。</p><blockquote><p>教えるとは　共に希望を語ること</p><p>学ぶとは　真実を胸に刻むこと <sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup></p></blockquote><p>双方に実り多きOJTとなりますよう。</p><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">翻訳は徳山詳直氏による。OJTの本質は「共にあること」という筆者信条に基づき徳山氏翻訳を引用した。より広く知られている翻訳は「教えるとは　希望を語ること　/　学ぶとは　誠実を胸にきざむこと」（『フランスの起床ラッパ』p.106　大島博光訳、新日本文庫）。古本を探すのは大変ですが、国会図書館デジタルコレクションで見ることができます。なんとスマホで。すごいですね。</span> <a href="#fnref:1" rev="footnote">↩</a></li></ol></div></div>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260423a/top.avif&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;634&quot; loading=&quot;lazy&quot;&gt;

&lt;h1 id=&quot;1-はじめに&quot;&gt;&lt;a href=&quot;#1-はじめに&quot;</summary>
        
      
    
    
    
    <category term="Management" scheme="https://future-architect.github.io/categories/Management/"/>
    
    
    <category term="リーダーシップ" scheme="https://future-architect.github.io/tags/%E3%83%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%82%B7%E3%83%83%E3%83%97/"/>
    
    <category term="OJT" scheme="https://future-architect.github.io/tags/OJT/"/>
    
    <category term="教育" scheme="https://future-architect.github.io/tags/%E6%95%99%E8%82%B2/"/>
    
  </entry>
  
  <entry>
    <title>リポジトリ駆動のコンテンツ制作ワークフロー: GitHub に素材を集めて、Claude Code で成果物に展開する</title>
    <link href="https://future-architect.github.io/articles/20260422a/"/>
    <id>https://future-architect.github.io/articles/20260422a/</id>
    <published>2026-04-21T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260422a/サムネ.png" alt="" width="1200" height="655" loading="lazy"><h2 id="TL-DR"><a href="#TL-DR" class="headerlink" title="TL;DR"></a>TL;DR</h2><ul><li>調査データや発見を GitHub リポジトリにひたすら追加していく。スライド・原稿・ブログへの反映は Claude に任せる</li><li>新しいデータが出たらリポジトリに入れるだけで、Claude が成果物を更新してくれる。準備中にデータが増え続けても、成果物は追いつく</li><li>VulnCon 2026 の登壇準備を通じてたどり着いた、リポジトリ駆動のやり方</li></ul><h2 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h2><p>こんにちは、フューチャー株式会社の棚井龍之介です。2026年4月、アリゾナ州スコッツデールで開催された <a href="https://www.first.org/conference/vulncon26/">VulnCon 2026</a> で初めてアメリカのセキュリティカンファレンスに登壇してきました（<a href="https://www.vuls.biz/blog/vulncon-2026-speaked">登壇レポートはこちら</a>）。60分の英語セッションで、発表スライドと登壇原稿の両方を準備する必要がありました。</p><p>面白かったのは、準備期間中にも新しい発見が次々と出てきたことです。登壇内容に関わるデータが日々増えていく。それを逐次スライドと原稿に反映していくことになります。</p><p>この状況で採ったやり方が、<strong>調査データや発見をまず GitHub リポジトリに追加して、スライドや原稿への反映は Claude に任せる</strong>、というものでした。データの置き場をリポジトリに決めてしまえば、自分は新しいデータを入れることに集中できる。そこから先の成果物への落とし込みは Claude がやってくれる。</p><p>なお、この記事で「Claude」と書いているのは、ターミナルから <code>claude</code> コマンドで起動する <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> のことです。</p><img src="/images/2026/20260422a/claude-code-start.png" alt="claude-code-start.png" width="1200" height="232" loading="lazy"><p>ターミナルで起動すると、カレントディレクトリのファイルを直接読み書きできます。Git 操作も <code>gh</code> コマンドも叩けます。リポジトリを作業場所にして Claude Code を立ち上げれば、リポジトリの中身を全部見た上で作業してくれます。</p><h2 id="準備中にデータが増え続けるなかで"><a href="#準備中にデータが増え続けるなかで" class="headerlink" title="準備中にデータが増え続けるなかで"></a>準備中にデータが増え続けるなかで</h2><p>VulnCon の登壇準備で必要だったのは、発表スライドと登壇原稿です。加えて、FutureVuls ブログの技術記事も書く予定でした。</p><p>CFP が通った段階で、素材と方向性はおおよそ見えていました。ポイントは、そこから先です。</p><p>準備期間中に trivy へのサプライチェーン攻撃が発生し、これをきっかけに FutureVuls ブログとして複数の記事を執筆することになりました。</p><ul><li>FutureVuls 配布バイナリの安全性を SHA256・ビルドタイムスタンプ・Sigstore 署名の3軸で検証</li><li>攻撃者が狙った7リポジトリの攻撃直前の OpenSSF Scorecard を分析</li><li>Sigstore・cosign による改ざん検証の仕組みを体系的に整理</li><li>第2波の攻撃が GitHub Actions・Docker Hub・npm・PyPI に波及し、影響確認ガイドを公開</li></ul><p>どれも登壇内容に直結するテーマです。情報を収集しながらスライドと原稿に反映し、同時にブログとしても公開していました。スライド、原稿、ブログ——複数の成果物を並行して、継続的に更新し続ける状況です。</p><p>ここで力を発揮したのが、Claude を活用したワークフローでした。</p><h2 id="やり方-データをリポジトリに入れて、Claude-に反映させる"><a href="#やり方-データをリポジトリに入れて、Claude-に反映させる" class="headerlink" title="やり方: データをリポジトリに入れて、Claude に反映させる"></a>やり方: データをリポジトリに入れて、Claude に反映させる</h2><p>採ったやり方は単純です。新しいデータや発見が出たら、まず GitHub リポジトリに追加する。形式は何でもいい。Markdown でも、テキストファイルでも、画像でも、PDF でも。とにかくリポジトリに入れておく。スライドや原稿への反映は Claude に頼む。</p><p>リポジトリの中は、大きく「素材」と「成果物」に分けています。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">リポジトリ/</span><br><span class="line">├── 素材       <span class="comment"># 調査データ、分析メモ、リサーチ結果</span></span><br><span class="line">├── 成果物     <span class="comment"># ブログ記事、登壇原稿、スライド</span></span><br><span class="line">└── CLAUDE.md  <span class="comment"># Claude への指示書</span></span><br></pre></td></tr></table></figure><p>素材の中身は自分が管理しやすい粒度で分ければいい。ファイル1つにまとめてもいいし、テーマごとにディレクトリを切ってもいい。実際、自分のリポジトリでは最初は大雑把にファイルを放り込んでいましたが、データが増えてきたタイミングで Claude と壁打ちしながら構成を見直しました。「このデータとこのデータは分けたほうが扱いやすいか？」「ブログ用のプランはどこに置く？」といった相談を Claude にして、ディレクトリ構成自体を育てていく。最初から完璧な設計を決める必要はありません。</p><p>たとえば、攻撃者が狙った7リポジトリの Scorecard 分析が終わったら、まず素材として追記する。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">case-studies/ に 7リポジトリの攻撃直前 Scorecard 分析結果を追加しました。</span><br><span class="line">登壇原稿の適切な場所に反映してください。</span><br><span class="line">スライドにも追加してください。</span><br></pre></td></tr></table></figure><p>今回の VulnCon 2026 の準備では、自分と共同登壇者の神戸さんが、それぞれの調査結果や発見を次々と同じリポジトリに追加していきました。日によって誰がどのデータを入れるかはバラバラです。でも、データの置き場がリポジトリに決まっているので、スライドのどこに入れるか、原稿のどの段落を書き換えるかは、Claude がリポジトリ全体を見て判断してくれます。</p><p>Claude にリポジトリごと見せておけば、原稿に新しいデータを追加したときにスライド側の整合性も一緒に確認してくれます。スライドと原稿の整合性チェックを丸ごと任せられたのは助かりました。</p><p>さらに言えば、Claude はデータが増えたときに単に差分を反映するだけではなく、新しい情報を踏まえてより良い構成を提案してくれることもありました。「この発見を入れるなら、Part 5 と Part 6 の順番を入れ替えたほうが流れがいい」といった提案です。データが増えるたびに構成が良くなっていく、という体験は、手作業ではなかなかできません。</p><p>また、準備中に過去の自社ブログや公開情報を思い出して「そういえば、これも関連するな」と気づくことがあります。そういうときは、とりあえずリポジトリに入れておく。あとは Claude に任せます。使うかどうか、どこに入れるかは Claude が判断してくれます。素材の投入に迷わなくていいので、思いついたらすぐリポジトリに放り込む癖がつきました。</p><h2 id="なぜ-Slidev-か"><a href="#なぜ-Slidev-か" class="headerlink" title="なぜ Slidev か"></a>なぜ Slidev か</h2><p>VulnCon のスライドは <a href="https://sli.dev/">Slidev</a> で作りました。Markdown でスライドが書けるツールです。</p><p>Slidev を選んだ一番の理由は、Claude との相性です。Claude はテキストファイルの読み書きが得意なので、Markdown で書かれたスライドなら中身を理解して直接編集できます。PowerPoint でも扱えますが、テキストベースのほうが編集がスムーズです。リポジトリのデータからスライドへの流れを Claude に任せたいなら、スライドも Markdown で書いておくのが自然でした。</p><p>たとえば、VulnCon で実際に使ったスライドを2枚紹介します。</p><p>1枚目は、サプライチェーン攻撃で CVE が採番されなかった理由を整理したスライドです。</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section"># Why No CVE?</span></span><br><span class="line"></span><br><span class="line">| What happened                          | Why it falls outside CVE scope                       |</span><br><span class="line">| -------------------------------------- | ---------------------------------------------------- |</span><br><span class="line">| Workflow misconfiguration exploited    | Site-specific misconfiguration, not a product defect |</span><br><span class="line">| Repository hijacked via stolen PAT     | Platform behavior abuse, not a code flaw             |</span><br><span class="line">| RCE on CI/CD runner                    | Environment-specific, not tied to a software version |</span><br><span class="line">| Malicious code / compromised artifacts | ✅ CVE-2026-28353, CVE-2026-33634                    |</span><br><span class="line"></span><br><span class="line"><span class="quote">&gt; CVEs captured the <span class="strong">**artifacts**</span>, but missed the <span class="strong">**attack chain**</span> that produced them.</span></span><br></pre></td></tr></table></figure><p>この Markdown が、こういうスライドになります。</p><img src="/images/2026/20260422a/slide-why-no-cve-18.png" alt="slide-why-no-cve-18.png" width="1200" height="676" loading="lazy"><p>2枚目は、この攻撃をどうやって知ったかを聴衆に問いかけるスライドです。</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line"><span class="section">layout: center</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># How Did We Learn About This Attack?</span></span><br><span class="line"></span><br><span class="line">~~CVE~~ <span class="literal">&amp;nbsp;</span> ~~NVD~~ <span class="literal">&amp;nbsp;</span> ~~Vulnerability Scanner~~ <span class="literal">&amp;nbsp;</span> ~~CSPM~~</span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">v-click</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="section">## We learned about it on <span class="strong">**X**</span></span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">v-click</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">v-click</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="section">## This is the CVE Blind Spot</span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">v-click</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>実際のスライドはこうなります。</p><img src="/images/2026/20260422a/slide-how-did-we-learn-19.png" alt="slide-how-did-we-learn-19.png" width="1200" height="676" loading="lazy"><p><code>&lt;v-click&gt;</code> は Slidev の機能で、クリックするたびに次の要素が表示されます。まず CVE や NVD が取り消し線で並んでいて、クリックすると「X（旧 Twitter）で知った」が出てくる。もう一度クリックすると「これが CVE の盲点だ」と結論が出る。こういうプレゼンの「間」を、Markdown で書ける。Claude はこの Markdown を直接読み書きできるので、「この v-click の順番を入れ替えて」といった修正もそのまま頼めます。レイアウトの調整も Markdown と CSS の範囲で完結するので、ストレスなく進められました。</p><p>VulnCon の準備では、登壇原稿からスライドを起こすこともあれば、スライドを先に直して原稿に反映させることもありました。どちらが先でもいい。原稿とスライドの両方がリポジトリにあるので、Claude が双方向に反映してくれます。</p><p>今回は英語での登壇だったので、スライドと原稿の役割分担にも気を配りました。スライドには情報を多めに載せて、聴衆が視覚的に追えるようにする。一方で原稿の英語表現はシンプルに抑えて、ノンネイティブでも話しやすくする。こういう「整合性は保ちつつ、それぞれの役割に合わせてアレンジする」ことも、両方のファイルを同時に見られる Claude だからできることでした。</p><h2 id="ブログ記事も同じリポジトリから"><a href="#ブログ記事も同じリポジトリから" class="headerlink" title="ブログ記事も同じリポジトリから"></a>ブログ記事も同じリポジトリから</h2><p>VulnCon の登壇内容をブログ記事にする場合も、同じリポジトリから直接書けます。データはすでにリポジトリにある。登壇原稿もある。ブログ用に新たにデータを集め直す必要がありません。</p><p>やることは、リポジトリの中の素材を踏まえて、どういうブログを書いてほしいかを Claude に伝えるだけです。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">リポジトリの素材を使って、サプライチェーン攻撃の影響確認ガイドをブログ記事として書いて。</span><br><span class="line">対象読者は FutureVuls ユーザ。</span><br></pre></td></tr></table></figure><p>Claude はリポジトリ内のファイルを直接読めるので、数値をコピペし直す手間がありません。素材に書いてある数値を、Claude がそのまま引用してくれます。</p><p>もちろん、生成された記事をそのまま公開するわけではありません。生成した原稿に対して Claude 自身にセルフチェックさせたり、PR を作成して GitHub Copilot をレビュアーにアサインしたりしながら推敲を重ねています。複数の AI の視点を入れることで、一人では気づきにくい表現の不自然さや論理の飛躍を拾えます。</p><p>ブログの執筆途中で新しい発見があったときも同じです。素材をリポジトリに追加して、「この情報を既存の記事に入れて」と頼めば、Claude が記事の流れを読んで適切な場所に追加してくれる。つまり、最初の執筆だけでなく「編集」も任せられる。スライドと原稿のときと同じ話で、新しいデータが出てきたら、自分はリポジトリに入れるだけでいい。</p><h2 id="CLAUDE-md-で表記を揃える"><a href="#CLAUDE-md-で表記を揃える" class="headerlink" title="CLAUDE.md で表記を揃える"></a>CLAUDE.md で表記を揃える</h2><p>成果物が増えてくると、スライドとブログで数値の書き方を揃えたくなります。そこで <code>CLAUDE.md</code> をリポジトリのルートに置いています。</p><p>Claude Code はセッション開始時にこのファイルを自動で読み込みます。</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section">## ブログ文体ルール</span></span><br><span class="line"><span class="bullet">-</span> 主張・論考・体験記系 → だ・である体</span><br><span class="line"><span class="bullet">-</span> 実務 How-to・解説系 → です・ます体</span><br><span class="line"></span><br><span class="line"><span class="section">## よく使う数字（実測値）</span></span><br><span class="line">| 指標 | 値 |</span><br><span class="line">|------|-----|</span><br><span class="line">| Scanner バイナリサイズ | 106.6 MB → 34.1 MB（-68%） |</span><br><span class="line">| trivy 由来の依存 | 352 → 144（-59%） |</span><br></pre></td></tr></table></figure><p>ここに正規の数値を載せておくと、スライドでもブログでも「-68%」で統一されます。「約7割減」ではなく正確な数値で統一できます。出典が PR なのか Issue なのかも、ここで決めておけばブレません。</p><p>最初は「まあ書いておくか」くらいの気持ちで作りましたが、成果物の本数が増えるほど効果を実感しました。</p><h2 id="リポジトリ操作も-Claude-に任せる"><a href="#リポジトリ操作も-Claude-に任せる" class="headerlink" title="リポジトリ操作も Claude に任せる"></a>リポジトリ操作も Claude に任せる</h2><p>ファイルの作成・編集、<code>git commit</code>、<code>git push</code>、Issue の起票——こういったリポジトリ操作は基本的に Claude に任せています。</p><p>Claude Code は <code>gh</code> コマンドも叩けるので、Issue の起票は <code>gh issue create</code> で済みます。自分は「データを集める」「指示を出す」「出来上がりを確認する」に絞れます。</p><h3 id="壁打ちの結果を-Issue-に残す"><a href="#壁打ちの結果を-Issue-に残す" class="headerlink" title="壁打ちの結果を Issue に残す"></a>壁打ちの結果を Issue に残す</h3><p>Claude との壁打ちで出た気づきや方針変更は、そのまま GitHub Issue にしておく。これが地味に効きました。</p><p>たとえば、VulnCon の Q&amp;A 準備をしているとき。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">Issue #xx: Q&amp;A セッション用のバックアップスライドを追加する</span><br></pre></td></tr></table></figure><p>Issue の起票自体も Claude に頼みます。翌日の別セッションで「Issue #xx の内容を踏まえてスライドを追加して」と言えば、前回の議論を引き継げる。</p><p>Claude とのセッションは消えますが、リポジトリに書いたものは消えません。Issue やコミットログが、セッション間の記憶の代わりになります。</p><h3 id="Perplexity-で素材を足す"><a href="#Perplexity-で素材を足す" class="headerlink" title="Perplexity で素材を足す"></a>Perplexity で素材を足す</h3><p>一次データの補強に <a href="https://www.perplexity.ai/">Perplexity</a> を使いました。</p><p>ブログに書く数値の裏取り——「endoflife.date のカバレッジは2026年4月時点で何件か」「gorilla&#x2F;mux はいつアーカイブされたか」——こういった確認を Perplexity に投げて、返ってきた結果を出典ごとリポジトリに追記しておく。</p><p>Claude はリポジトリの中身しか見ないので、Perplexity で集めた情報もリポジトリに入れておかないと使えません。リポジトリに素材が増えるほど、Claude が書ける内容の幅が広がります。</p><h2 id="実践を通じた所感"><a href="#実践を通じた所感" class="headerlink" title="実践を通じた所感"></a>実践を通じた所感</h2><p>1ヶ月やってみて思ったのは、結局自分の仕事は「自分で試行錯誤して、それを記録すること」だった、ということです。こういうデータは自分で手を動かさないと生まれない。でも逆に、試行錯誤の記録さえリポジトリにあれば、そこから先は Claude に任せられる。</p><p>リポジトリに入れる素材は、Markdown で書くこともあれば、画像や PDF をそのまま放り込むこともありました。きれいに整形してから入れる必要はない。ただ、スライドや原稿のように Claude に直接編集させたい成果物は Markdown にしておくと、この流れが途切れません。</p><p>ディレクトリ構成は最初の数日で試行錯誤しましたが、構成が固まってからは、データを入れて反映を頼むだけのループになりました。VulnCon の直前まで新しい発見を出し続けられたのは、このループが軽かったからだと思います。</p><h2 id="運用上の留意点"><a href="#運用上の留意点" class="headerlink" title="運用上の留意点"></a>運用上の留意点</h2><p>Claude が書いた文章は、必ず自分でレビューしています。<code>CLAUDE.md</code> で数値を揃えていても、文脈に合わない場所で引用されることはあるので、数値と前後の文脈の両方を確認する。</p><p>また、Claude の修正を完全には信用せず、適用漏れや横展開漏れがないかをチェックし続けることも大事です。たとえば「この表現を全箇所修正して」と頼んでも、一部が抜けていることがある。このチェック自体に、Claude 自身のセルフチェックや GitHub Copilot の PR レビューなど、複数の AI を組み合わせると抜け漏れを拾いやすくなります。</p><p>なお、Claude に <code>git push</code> や <code>gh issue create</code> を任せる以上、リポジトリはプライベートで運用し、機密情報は直接書かないようにしています。</p><h2 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h2><p>VulnCon 2026 の準備を通じてたどり着いたやり方は、<strong>データを GitHub リポジトリに集めて、成果物への落とし込みは Claude に任せる</strong>、というものでした。</p><p>新しい発見が出たらリポジトリに追加する。スライドや原稿への反映は Claude がやる。準備期間中にデータが増え続けても、リポジトリにさえ入れておけば、成果物は追いつく。</p><p>共通の情報源から、スライド・原稿・ブログといった複数の成果物を作る必要があり、しかもデータが継続的に増えていく——今回の VulnCon 準備は、まさにこのやり方が合うケースでした。同じような状況に直面している方の参考になれば幸いです。</p><p>なお、この記事自体も同じリポジトリから Claude で書いています。コミットログを見ると <code>Co-Authored-By: Claude</code> の記録が残っています。VulnCon の登壇準備でスライドや原稿を作るつもりで始めたリポジトリが、試行錯誤の記録が溜まった結果、こうしてワークフロー自体を紹介するブログ記事にまで繋がりました。リポジトリに記録を残しておくと、思わぬ形で次の成果物が生まれることがあります。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260422a/サムネ.png&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;655&quot; loading=&quot;lazy&quot;&gt;

&lt;h2 id=&quot;TL-DR&quot;&gt;&lt;a href=&quot;#TL-DR&quot;</summary>
        
      
    
    
    
    <category term="Business" scheme="https://future-architect.github.io/categories/Business/"/>
    
    
    <category term="GitHub" scheme="https://future-architect.github.io/tags/GitHub/"/>
    
    <category term="Claude" scheme="https://future-architect.github.io/tags/Claude/"/>
    
    <category term="ClaudeCode" scheme="https://future-architect.github.io/tags/ClaudeCode/"/>
    
  </entry>
  
  <entry>
    <title>自宅だと apt update ができなかった話（WSL2 + 社内VPN環境での名前解決の遅延）</title>
    <link href="https://future-architect.github.io/articles/20260421b/"/>
    <id>https://future-architect.github.io/articles/20260421b/</id>
    <published>2026-04-20T15:00:01.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260421b/top.avif" alt="" width="1200" height="634" loading="lazy"><h1 id="1-はじめに"><a href="#1-はじめに" class="headerlink" title="1. はじめに"></a>1. はじめに</h1><p>こんにちは。HealthCare Innovation Group (HIG)の清水雄一郎です。<br>本記事は、<a href="/articles/20260421a/">春の入門祭り2026</a>の1日目の記事です。</p><p>新年度・新チーム配属のこの季節、「開発環境のセットアップで丸一日溶かした」という経験をされている方も多いのではないでしょうか。特にエンタープライズ企業の「プロキシ・VPN配下」での開発環境構築は、ドキュメント通りに進めても謎のエラーに阻まれがちです。<br>プロキシに関しては、過去の記事<sup id="fnref:5"><a href="#fn:5" rel="footnote">1</a></sup><sup id="fnref:6"><a href="#fn:6" rel="footnote">2</a></sup><sup id="fnref:7"><a href="#fn:7" rel="footnote">3</a></sup>もぜひご確認ください。</p><p>少し前（2026&#x2F;01頃）に私が遭遇した事象は、WSL2環境で「自宅に帰ると<code>apt update</code>できない」というものです。<br>当時を振り返ると、次のような状況でした。</p><ul><li>オフィスに出社してネットワークに繋ぐと、<code>apt update</code> が成功する</li><li>自宅（無線&#x2F;有線LAN&amp;VPN接続中）だと、 <code>apt update</code> が<strong>失敗する</strong></li><li>ただし、<strong>iPhoneのテザリング＋VPN</strong> の場合、<code>apt update</code> が<strong>成功する</strong></li></ul><p>結論から言うと、今回のケースではDNSの順序を変えることで解決しました。<br>その頃のSlackスレッドやGeminiとのチャット履歴を遡りつつ、体験記兼トラブルシューティングログとして残します。</p><h1 id="2-調査編-泥沼のトラブルシューティング"><a href="#2-調査編-泥沼のトラブルシューティング" class="headerlink" title="2. 調査編: 泥沼のトラブルシューティング"></a>2. 調査編: 泥沼のトラブルシューティング</h1><h2 id="2-0-前提"><a href="#2-0-前提" class="headerlink" title="2.0. 前提"></a>2.0. 前提</h2><p>本記事は、次の環境下で確認したものです。<br>※Windows側のバージョンは、事象発生時から少し変わっているかもしれません。</p><details><summary>環境情報</summary><figure class="highlight text"><table><tr><td class="code"><pre><span class="line"># Windows</span><br><span class="line">エディション Windows 11 Pro</span><br><span class="line">バージョン 24H2</span><br><span class="line">OS ビルド 26100.8037</span><br><span class="line"></span><br><span class="line"># WSL2（Ubuntu）</span><br><span class="line">PRETTY_NAME=&quot;Ubuntu 22.04.5 LTS&quot;</span><br><span class="line">NAME=&quot;Ubuntu&quot;</span><br><span class="line">VERSION_ID=&quot;22.04&quot;</span><br><span class="line">VERSION=&quot;22.04.5 LTS (Jammy Jellyfish)&quot;</span><br><span class="line">VERSION_CODENAME=jammy</span><br><span class="line">ID=ubuntu</span><br><span class="line">ID_LIKE=debian</span><br><span class="line">HOME_URL=&quot;https://www.ubuntu.com/&quot;</span><br><span class="line">SUPPORT_URL=&quot;https://help.ubuntu.com/&quot;</span><br><span class="line">BUG_REPORT_URL=&quot;https://bugs.launchpad.net/ubuntu/&quot;</span><br><span class="line">PRIVACY_POLICY_URL=&quot;https://www.ubuntu.com/legal/terms-and-policies/privacy-policy&quot;</span><br><span class="line">UBUNTU_CODENAME=jammy</span><br></pre></td></tr></table></figure></details><h2 id="2-1-エラーログ"><a href="#2-1-エラーログ" class="headerlink" title="2.1. エラーログ"></a>2.1. エラーログ</h2><p>まずは事象を整理するため、発生していたエラーログを確認します。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> apt update</span><br><span class="line">...</span><br><span class="line">エラー:5 http://archive.ubuntu.com/ubuntu jammy-updates InRelease</span><br><span class="line">  サーバからの読み込みに失敗しました - <span class="built_in">read</span> (104: 接続が相手からリセットされました) [IP: 10.x.x.x 8080]</span><br><span class="line">エラー:6 http://archive.ubuntu.com/ubuntu jammy-backports InRelease</span><br><span class="line">  10.x.x.x:8080 (10.x.x.x) へ接続できませんでした。接続がタイムアウトしました [IP: 10.x.x.x 8080]</span><br><span class="line">エラー:3 http://security.ubuntu.com/ubuntu jammy-security InRelease</span><br><span class="line">  サーバからの読み込みに失敗しました - <span class="built_in">read</span> (104: 接続が相手からリセットされました) [IP: 10.x.x.x 8080]</span><br><span class="line">...</span><br><span class="line">W: いくつかのインデックスファイルのダウンロードに失敗しました。 これらは無視されるか、古いものが代わりに使われます。</span><br></pre></td></tr></table></figure><p><code>10.x.x.x:8080</code>は、社内プロキシサーバーです。<br>「タイムアウト」と「接続が相手からリセットされました」が混在しており、この時点では原因が全く特定できていません。</p><p>試しに同じ社内プロキシを経由している <code>wget</code> &#x2F; <code>curl</code> コマンドを試しましたが、成功することを確認できました。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ wget google.com</span><br><span class="line"><span class="comment"># → 200 OK で正常に取得できる</span></span><br><span class="line"></span><br><span class="line">$ curl -I -L google.com</span><br><span class="line"><span class="comment"># → 200 OK</span></span><br></pre></td></tr></table></figure><p>※いま振り返ると、<a href="http://archive.ubuntu.com/ubuntu">http://archive.ubuntu.com/ubuntu</a>宛に<code>wget</code> &#x2F; <code>curl</code> コマンドを試した方が良かったかもしれません。</p><p>ここで一人では抱え込めないと判断し、Slackでチームメンバーに助けを求め、Geminiにも相談しながら仮説検証を始めました。<br>他の人に相談することは、私の環境だけの問題かどうか切り分けるという一つの検証と言えると思います。</p><img src="/images/2026/20260421b/Slackでチームに相談している様子.png" alt="Slackでチームに相談している様子" width="593" height="134" loading="lazy"><h2 id="2-2-仮説①-プロキシ設定が間違っている？"><a href="#2-2-仮説①-プロキシ設定が間違っている？" class="headerlink" title="2.2. 仮説① プロキシ設定が間違っている？"></a>2.2. 仮説① プロキシ設定が間違っている？</h2><p>最初に疑ったのは、やはりプロキシ周りです。<code>apt</code>は<code>wget</code>&#x2F;<code>curl</code>とは別経路の設定を見るため、<code>apt</code>自体のプロキシ設定を確認します。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cat</span> /etc/apt/apt.conf</span><br><span class="line">Acquire::http::Proxy  <span class="string">&quot;http://username:password@proxy.example.com:8080&quot;</span>;</span><br><span class="line">Acquire::https::Proxy <span class="string">&quot;http://username:password@proxy.example.com:8080&quot;</span>;</span><br></pre></td></tr></table></figure><p>設定済みでした。環境変数も<code>apt</code>と同じ設定値が入っています。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">env</span> | grep -i proxy</span><br><span class="line">...</span><br><span class="line">http_proxy=http://username:password@proxy.example.com:8080</span><br><span class="line">https_proxy=http://username:password@proxy.example.com:8080</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p><code>apt</code>のプロキシ設定は、問題なさそうです。<br>またこの時、環境変数を引き継ぐため、<code>sudo -E apt update</code>を実行してみましたが、最初と同様のエラーになりました。</p><h2 id="2-3-仮説②～⑤-プロキシ設定以外はどう？"><a href="#2-3-仮説②～⑤-プロキシ設定以外はどう？" class="headerlink" title="2.3. 仮説②～⑤ プロキシ設定以外はどう？"></a>2.3. 仮説②～⑤ プロキシ設定以外はどう？</h2><p>プロキシ設定以外に考えられる原因をGeminiやメンバーに聞いて片っ端から試しました。結論から言うと、いずれも空振りでした。</p><h3 id="仮説②-社内プロキシの証明書が未インストール？"><a href="#仮説②-社内プロキシの証明書が未インストール？" class="headerlink" title="仮説② 社内プロキシの証明書が未インストール？"></a>仮説② 社内プロキシの証明書が未インストール？</h3><p>証明書周りはプロキシの次にトラブルが多い印象なので、試しに手動で入れ替えて確認します。<br>→ 変化なし</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 非推奨。次のコードブロックの手順を推奨。</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /etc/ssl/certs/ca-certificates.crt</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">cp</span> path/to/ca.crt /etc/ssl/certs/ca-certificates.crt</span><br><span class="line"><span class="built_in">sudo</span> apt update  <span class="comment"># 変化なし</span></span><br></pre></td></tr></table></figure><p>原因特定のために<code>/etc/ssl/certs/</code>配下の証明書を直接操作しましたが、本来は<code>update-ca-certificates</code>を使う方が正しいです。<br>推奨される方法は、次の通りです。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">cp</span> path/to/ca.crt /usr/local/share/ca-certificates/corp-proxy.crt</span><br><span class="line"><span class="built_in">sudo</span> update-ca-certificates</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br></pre></td></tr></table></figure><h3 id="仮説③-WSLのHyper-Vファイアウォールが遮断している？"><a href="#仮説③-WSLのHyper-Vファイアウォールが遮断している？" class="headerlink" title="仮説③ WSLのHyper-Vファイアウォールが遮断している？"></a>仮説③ WSLのHyper-Vファイアウォールが遮断している？</h3><p>「デフォルトでオンになっているHyper-V FirewallをOFFにすると<code>apt</code>が通ることがある」という情報を教えてもらい、「WSL Settings」のGUIから無効化してみました。<br>→ 変化なし</p><img src="/images/2026/20260421b/WSL_Settings_で_Hyper-V_Firewall_を無効化した画面.png" alt="WSL_Settings_で_Hyper-V_Firewall_を無効化した画面" width="1200" height="645" loading="lazy"><h3 id="仮説④-MTUの設定が経路と合っていない？"><a href="#仮説④-MTUの設定が経路と合っていない？" class="headerlink" title="仮説④ MTUの設定が経路と合っていない？"></a>仮説④ MTUの設定が経路と合っていない？</h3><p>Geminiにエラーログを渡して相談すると、MTU（Maximum Transmission Unit）<sup id="fnref:1"><a href="#fn:1" rel="footnote">4</a></sup>の影響を疑うよう提案を受けました。VPN環境下では実効MTUが小さくなり、デフォルト1500バイトのままだと通信に失敗するケースがあるとのこと。<br>→ こちらも変化なし</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> ip <span class="built_in">link</span> <span class="built_in">set</span> dev eth0 mtu 1300</span><br><span class="line"><span class="built_in">sudo</span> apt update  <span class="comment"># 変化なし</span></span><br></pre></td></tr></table></figure><p>後から振り返ると、MTU調整は「大きいパケットだけ落ちる」症状に有効な対策で、数十KBのリクエストで失敗する今回の症状とは整合しません。生成AIの提案を鵜呑みにして試した回り道でした。</p><h3 id="仮説⑤-archive-ubuntu-comが遠いから？"><a href="#仮説⑤-archive-ubuntu-comが遠いから？" class="headerlink" title="仮説⑤ archive.ubuntu.comが遠いから？"></a>仮説⑤ <code>archive.ubuntu.com</code>が遠いから？</h3><p>リポジトリを日本のミラーサーバーに変更してみます。<br>→ ログは割愛しますが、タイムアウトエラーになってしまいます。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> sed -i.bak -E <span class="string">&#x27;s!http://(archive|security)\.ubuntu\.com!http://jp.archive.ubuntu.com!g&#x27;</span> /etc/apt/sources.list</span><br><span class="line"><span class="built_in">sudo</span> apt update  <span class="comment"># タイムアウトエラー</span></span><br></pre></td></tr></table></figure><h2 id="2-4-仮説⑥-接続するネットワークの問題？"><a href="#2-4-仮説⑥-接続するネットワークの問題？" class="headerlink" title="2.4. 仮説⑥ 接続するネットワークの問題？"></a>2.4. 仮説⑥ 接続するネットワークの問題？</h2><p>出社する日が多く、自宅でのみ発生する問題の解消を後回しにしていたのですが、オフィスにいる時に時間ができたので再度向き合ってみることにしました。<br>VPN接続時に発生するため、オフィスにいながらBYODであるiPhoneのテザリングに切り替えて、VPN接続してみます。<br>なんと、<code>apt update</code>が<strong>正常終了する</strong>ことに気が付きます。整理すると次の状況です。</p><ul><li>オフィス無線LAN → OK</li><li>自宅（無線&#x2F;有線）LAN＋VPN → NG</li><li>iPhoneテザリング＋VPN → OK 🤔</li></ul><p>ひとまず成功したことに喜びつつ、ずっとテザリングで業務するわけにはいきません。<br>VPNがすべてダメというわけではないと分かったので、他の手立てを考えてみました。</p><h1 id="3-原因編-名前解決を疑う"><a href="#3-原因編-名前解決を疑う" class="headerlink" title="3. 原因編: 名前解決を疑う"></a>3. 原因編: 名前解決を疑う</h1><p><code>wget</code>は通って<code>apt</code>は通らない、かつ、同じVPNを使っていても回線（自宅LAN&#x2F;テザリング）によって成否が変わることが分かりました。<br>つまり原因は「どの回線でも共通に使われる設定」ではなく、<strong>「回線ごとに切り替わる何か」</strong> にあると考えました。</p><p>回線ごとに切り替わるものの一つに、ホストであるWindowsが参照するDNSがあります。<br>WSL2は、デフォルトでWindows側のDNSを継承するため、回線が変わればWSLが使うDNSも変わります。</p><p>ここから、<strong>名前解決（DNS）の速度</strong>を疑うことにしました。<br>※名前解決とは、ドメイン名（例: <code>archive.ubuntu.com</code>）をIPアドレスに変換する処理のことです。</p><p><code>apt update</code>は、内部で複数のリポジトリに問い合わせているため、1問い合わせあたりの名前解決が遅いとそれが積み重なり、接続タイムアウトに抵触しやすいのではないかと考えました（最初に試した<code>wget</code> &#x2F; <code>curl</code> は単発リクエストなので、1回の遅延くらいは待てる、という仮説）。</p><p>実際に、名前解決の時間を計測してみます。<code>getent hosts</code>は、指定したホストの情報を取得するコマンドです。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="keyword">time</span> getent hosts proxy.example.com</span><br><span class="line">10.x.x.x    proxy.example.com</span><br><span class="line"></span><br><span class="line">real    0m15.225s</span><br><span class="line">user    0m0.001s</span><br><span class="line">sys     0m0.004s</span><br></pre></td></tr></table></figure><p>それが<strong>たった1件の社内ホスト情報取得に15秒</strong>かかっていました。</p><p>ちなみに、その時の<code>/etc/resolv.conf</code>の中身はこうでした。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cat</span> /etc/resolv.conf</span><br><span class="line">nameserver 172.x.x.x    <span class="comment"># Windowsホスト側の仮想ルーター</span></span><br><span class="line">nameserver 10.x.x.x     <span class="comment"># 開発環境構築時に追記したもの</span></span><br><span class="line">search example.com      <span class="comment"># 開発環境構築時に追記したもの</span></span><br></pre></td></tr></table></figure><p><code>172.x.x.x</code>は、WSL2がデフォルトで参照するWindowsホスト側の仮想ルーターです。<br>Windows側の設定を引き継いで名前解決してくれる便利な仕組みですが、私の環境かつVPN接続状態ではボトルネックになっていると考えました。</p><h1 id="4-解決編-etc-resolv-confへの記載順序を変える"><a href="#4-解決編-etc-resolv-confへの記載順序を変える" class="headerlink" title="4. 解決編: /etc/resolv.confへの記載順序を変える"></a>4. 解決編: <code>/etc/resolv.conf</code>への記載順序を変える</h1><p>原因が掴めれば対処は単純です。WSLのデフォルト仮想DNS（172.x.x.x）より先に、社内DNSに直接問い合わせるよう、<code>/etc/resolv.conf</code>の<code>nameserver</code>順序を並び替えます。</p><p>Linuxのリゾルバは、<code>/etc/resolv.conf</code>の<code>nameserver</code>を<strong>上から順に</strong>試すようです。<sup id="fnref:2"><a href="#fn:2" rel="footnote">5</a></sup><sup id="fnref:3"><a href="#fn:3" rel="footnote">6</a></sup>先頭から応答が返ればそこで終了、タイムアウトすれば次へフォールバックする仕組みです。1問い合わせあたりデフォルト5秒 × 複数回リトライすることで<sup id="fnref:4"><a href="#fn:4" rel="footnote">7</a></sup>、先頭が遅いと十数秒単位で待たされていたと推測します。<br>つまり、社内DNSを先頭に置くだけで、ほぼ全ての問い合わせが1行目で即解決するようになるはずです。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/resolv.conf</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># /etc/resolv.conf</span><br><span class="line">nameserver 10.x.x.x   # 一番上に変更</span><br><span class="line">nameserver 172.x.x.x</span><br><span class="line">search example.com</span><br></pre></td></tr></table></figure><p>効果を計測します。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="keyword">time</span> getent hosts proxy.example.com</span><br><span class="line">10.x.x.x    proxy.example.com</span><br><span class="line"></span><br><span class="line">real    0m0.033s</span><br><span class="line">user    0m0.003s</span><br><span class="line">sys     0m0.007s</span><br></pre></td></tr></table></figure><p><strong>15.225秒 → 0.033秒。</strong>約<strong>460倍</strong>の改善です。<br>この状態で<code>apt update</code>を叩くと、自宅（無線&#x2F;有線）LAN＋VPN環境で、一発で成功することを確認できました🎉</p><p>ここで忘れてはいけないのですが、<code>/etc/resolv.conf</code>は、WSLを再起動するとデフォルトで自動再生成され、上の変更が上書きされて消えてしまいます。<br>「せっかく直したのに翌朝また詰まった」を防ぐため、<code>/etc/wsl.conf</code>で自動生成を無効化しておきます。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/wsl.conf</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># /etc/wsl.conf</span><br><span class="line">[network]</span><br><span class="line">generateResolvConf = false</span><br></pre></td></tr></table></figure><h1 id="5-おわりに"><a href="#5-おわりに" class="headerlink" title="5. おわりに"></a>5. おわりに</h1><p>今回の泥沼を振り返ると、最も反省すべき点は、<strong>「十分な評価をしないまま、ありがちな原因を手当たり次第試し続けた時間」</strong> の長さでした。</p><p>生成AIにエラーログを投げると、どんなに長くても中身を読んで自分が思いつかない次の一手を考えてくれます。<br>特に、環境構築周りは色々な層に原因が潜んでいるため、これまで自分よりも生成AIの方が得意な分野だと思っていました。<br>もちろん多くの場合その通りだと思いますが、今回のケースのようにうまくいかない時もあります。<br>改めて、開発環境構築において「何ができて何ができなかったか」を整理することはもちろん、<strong>「何が『どこまで』できているか」を「計測する」</strong> ことが重要だと実感しました。</p><p>これから開発環境を構築する皆さんは、詰まった際に生成AIに「何をやるといいか」と同時に、「何を計測すると原因が測れるか」を聞いてみると解決の近道になるかもしれません。<br>そしてぜひ、詰まった経緯と解決策を記事にしてネットに公開してください！<br>今度は、生成AIが学習して解決がもっと早まるかもしれません……！</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:5"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;"><a href="https://future-architect.github.io/articles/20250904a/">TCP Proxyを作って面倒なProxy設定を一掃する ～Rust製moproxyとnftablesによる透過プロキシ設定～</a></span> <a href="#fnref:5" rev="footnote">↩</a></li><li id="fn:6"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;"><a href="https://future-architect.github.io/articles/20240227a/">ローカルプロキシで認証プロキシの煩わしさを解消！</a></span> <a href="#fnref:6" rev="footnote">↩</a></li><li id="fn:7"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;"><a href="https://future-architect.github.io/articles/20201020/">ProxyとDockerと新人社員と時々わたし</a></span> <a href="#fnref:7" rev="footnote">↩</a></li><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;"><a href="https://wa3.i-3-i.info/word13207.html">https://wa3.i-3-i.info/word13207.html</a></span> <a href="#fnref:1" rev="footnote">↩</a></li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">5.</span><span style="vertical-align: top;"><a href="https://linuxjm.sourceforge.io/html/LDP_man-pages/man5/resolv.conf.5.html">https://linuxjm.sourceforge.io/html/LDP_man-pages/man5/resolv.conf.5.html</a></span> <a href="#fnref:2" rev="footnote">↩</a></li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">6.</span><span style="vertical-align: top;"><a href="https://kazmax.zpp.jp/cmd/r/resolv.conf.5.html">https://kazmax.zpp.jp/cmd/r/resolv.conf.5.html</a></span> <a href="#fnref:3" rev="footnote">↩</a></li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">7.</span><span style="vertical-align: top;"><a href="https://manpages.ubuntu.com/manpages/noble/ja/man5/resolv.conf.5.html">https://manpages.ubuntu.com/manpages/noble/ja/man5/resolv.conf.5.html</a></span> <a href="#fnref:4" rev="footnote">↩</a></li></ol></div></div>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260421b/top.avif&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;634&quot; loading=&quot;lazy&quot;&gt;

&lt;h1 id=&quot;1-はじめに&quot;&gt;&lt;a href=&quot;#1-はじめに&quot;</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="トラブルシュート" scheme="https://future-architect.github.io/tags/%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%88/"/>
    
    <category term="環境構築" scheme="https://future-architect.github.io/tags/%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89/"/>
    
    <category term="VPN" scheme="https://future-architect.github.io/tags/VPN/"/>
    
    <category term="Ubuntu" scheme="https://future-architect.github.io/tags/Ubuntu/"/>
    
    <category term="WSL2" scheme="https://future-architect.github.io/tags/WSL2/"/>
    
  </entry>
  
  <entry>
    <title>春の入門祭りとは</title>
    <link href="https://future-architect.github.io/articles/20260421a/"/>
    <id>https://future-architect.github.io/articles/20260421a/</id>
    <published>2026-04-20T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260421a/top.png" alt="" width="600" height="334"><p>春の入門連載は2020年から始めた連載で、以下のテーマで行う技術ブログリレーです。</p><ul><li>新年度、新たなことをチャレンジする機会として、「入門」がテーマ</li><li>今まで触って無かった技術の体験記</li><li>先輩・同期・後輩宛に、知っておいて欲しい技術記事</li><li>好きな技術の布教活動をする記事</li></ul><p>上記の前提であるため、具体的に取り上げる技術領域は自由ということが特徴です。人によってテーマである入門の解釈も自由自在で、お気に入りの記事に思いがけず出会うキッカケとしても機能するでしょう。</p><p>他の連載に比べて、ジャンルを問わずに応募していることもあり、毎年初の寄稿となる社員も多く参加しています。参加者の中にはブログを書いてみたい、とブログ運営に直接連絡をくれたやる気に溢れた社員もいるほどです。</p><h1 id="参加者一覧-投稿タイトル"><a href="#参加者一覧-投稿タイトル" class="headerlink" title="参加者一覧&amp;投稿タイトル"></a>参加者一覧&amp;投稿タイトル</h1><p>今年はインデックス記事を含めて10名が参加します！ 間にゴールデンウィークを挟みつつ、5月中旬にかけて平日ほぼ毎日更新していく予定です。</p><p>技術ブログにはお馴染みの顔から、今回の連載で初寄稿の社員まで幅広いメンバーで投稿していきます。テーマがまだ決まっていないところや変わる場合がありますが、運営チャットでは各メンバーの気合が漲っております。</p><div class="scroll"><table><thead><tr><th align="left">日付</th><th align="left">投稿者</th><th align="left">テーマ</th></tr></thead><tbody><tr><td align="left">4&#x2F;21（火）</td><td align="left">清水雄一郎</td><td align="left"><a href="/articles/20260421b/">自宅だと apt update ができなくなった話（WSL2 + 社内VPN環境での名前解決の遅延）</a></td></tr><tr><td align="left">4&#x2F;23（木）</td><td align="left">清水利博</td><td align="left"><a href="/articles/20260423a/">これだけやろうOJT</a></td></tr><tr><td align="left">4&#x2F;24（金）</td><td align="left">星名藍乃介</td><td align="left"><a href="/articles/20260424a/">非開発系業務 × Context Engineering の実践知</a></td></tr><tr><td align="left">🌷</td><td align="left"></td><td align="left"></td></tr><tr><td align="left">4&#x2F;27（月）</td><td align="left">内堀航輝</td><td align="left"><a href="/articles/20260427a/">分散システム入門: 信頼性の低いネットワークを再現してみる</a></td></tr><tr><td align="left">4&#x2F;28（火）</td><td align="left">澁川喜規</td><td align="left"><a href="/articles/20260428a/">AI-DLC, SDD、2026年4月時点のAI駆動の開発スタイルの考察</a></td></tr><tr><td align="left">4&#x2F;29（水）</td><td align="left">SKIP</td><td align="left">昭和の日</td></tr><tr><td align="left">4&#x2F;30（木）</td><td align="left">柴田健太</td><td align="left"><a href="/articles/20260430a/">BigQueryから直接Geminiを叩こう。BigQueryMLによるログ解析ハンズオン</a></td></tr><tr><td align="left">5&#x2F;1（金）</td><td align="left">福島雅都</td><td align="left"><a href="/articles/20260501a/">【Claude Design】インフラ構成のお絵描きからリソース実装までをClaudeで一本化してみた</a></td></tr><tr><td align="left">🦋</td><td align="left"></td><td align="left"></td></tr><tr><td align="left">5&#x2F;4（月）</td><td align="left">SKIP</td><td align="left">GW（みどりの日）</td></tr><tr><td align="left">5&#x2F;5（火）</td><td align="left">SKIP</td><td align="left">GW（こどもの日）</td></tr><tr><td align="left">5&#x2F;6（水）</td><td align="left">SKIP</td><td align="left">GW（振替休日）</td></tr><tr><td align="left">5&#x2F;7（木）</td><td align="left">SKIP</td><td align="left">GW期間のためお休み</td></tr><tr><td align="left">5&#x2F;8（金）</td><td align="left">SKIP</td><td align="left">GW期間のためお休み</td></tr><tr><td align="left">🌼</td><td align="left"></td><td align="left"></td></tr><tr><td align="left">5&#x2F;11（月）</td><td align="left">市川裕也</td><td align="left">（タイトル未定）</td></tr><tr><td align="left">5&#x2F;12（火）</td><td align="left">辻大志郎</td><td align="left">（タイトル未定）</td></tr><tr><td align="left">5&#x2F;13（水）</td><td align="left">永井優斗</td><td align="left">（タイトル未定）</td></tr></tbody></table></div><p>※ 各記事が公開され次第、順次リンクをアップデートしていきます。</p><h1 id="さいごに"><a href="#さいごに" class="headerlink" title="さいごに"></a>さいごに</h1><p>良いと思った記事などはシェアなどのリアクションをいただければと思います！</p><ul><li><a href="/articles/20250413a/">2025年の連載記事</a></li><li><a href="/articles/20240408a/">2024年の連載記事</a></li><li><a href="/articles/20230417a/">2023年の連載記事</a></li><li><a href="/articles/20220418a/">2022年の連載記事</a></li><li><a href="/articles/20210414a/">2021年の連載記事</a></li><li><a href="/articles/20200529/">2020年の連載記事</a></li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260421a/top.png&quot; alt=&quot;&quot; width=&quot;600&quot;</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
    
    <category term="春の入門祭り" scheme="https://future-architect.github.io/tags/%E6%98%A5%E3%81%AE%E5%85%A5%E9%96%80%E7%A5%AD%E3%82%8A/"/>
    
  </entry>
  
  <entry>
    <title>言語処理学会 (NLP2026) 参加報告</title>
    <link href="https://future-architect.github.io/articles/20260420a/"/>
    <id>https://future-architect.github.io/articles/20260420a/</id>
    <published>2026-04-19T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<h2 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h2><p>はじめまして！フューチャー株式会社の田中裕真と申します。</p><p>私は2025年10月に新卒として入社し、2026年2月にAIXG(AI戦略推進グループ)のNLPチームに配属されました。配属からちょうど2ヶ月が経ち、これまでのゆったりとした生活から一変して、忙しくも目まぐるしく新しい情報が得られ、知的好奇心を満たしながら働く充実した毎日を送っています。</p><p>大学院での研究ではAIの性能調査などを通じてAI自体には触れていたものの、自然言語処理（NLP）分野はあまり経験がありませんでした。現在はNLP関連のキャッチアップを進めながら、チャットシステムの構築業務に携わっています。</p><p>本記事では、2026年3月9日(月)〜3月13日(金)にかけて栃木県宇都宮市で開催された<a href="https://www.anlp.jp/nlp2026/">言語処理学会第32回年次大会 (NLP2026)</a> の参加報告をお届けします。</p><p>当社はプラチナスポンサーとして協賛し、総勢8名がオンサイトで参加しました。スポンサーブースでの企業紹介や、メンバーによる研究発表など、非常に活気あふれる5日間となりました。<br><img src="/images/2026/20260420a/image5.jpg" alt="image5.jpg" width="1200" height="800" loading="lazy"></p><h2 id="言語処理学会とは"><a href="#言語処理学会とは" class="headerlink" title="言語処理学会とは"></a>言語処理学会とは</h2><p>言語処理学会は、自然言語処理（NLP）分野における国内最大の学会で、毎年3月に開催されています。第32回となる今年の年次大会は、栃木県宇都宮市でのオンサイト開催とオンライン配信のハイブリッド形式で行われました。</p><p>今回の大会は、事前＋直前参加登録者が2236人（歴代2位）に上り、当日参加者を合わせると歴代1位の参加者数を記録しました。さらに、発表件数は797件（歴代1位）、スポンサー数は100団体（歴代2位）と過去最大級の規模となりました。生成AIや大規模言語モデル（LLM）の社会実装が急速に進む中、産学問わず非常に多くの研究者が集い、基礎研究から応用事例まで幅広い議論が交わされる熱気のある学会です。NLP初学者の私にとっても、最前線の研究トレンドを直接肌で感じることができる非常に貴重な機会となりました。<br><img src="/images/2026/20260420a/attendances.jpeg" alt="attendances.jpeg" width="1200" height="825" loading="lazy"></p><h2 id="スポンサーブースの様子"><a href="#スポンサーブースの様子" class="headerlink" title="スポンサーブースの様子"></a>スポンサーブースの様子</h2><p>企業ブースでは、NLP関連の過去の導入事例や、そのほかのAIXG担当領域についての紹介を行いました。連日多くの方々に足を運んでいただき、NLPの社会実装に関するディスカッションや、学生の皆さんとの交流で大いに盛り上がりました。</p><p>今回はノベルティとして眼鏡やディスプレイを拭ける「クロス」と折りたたみ可能な「トートバッグ」を用意しご好評をいただきました。</p><img src="/images/2026/20260420a/booth.jpg" alt="booth.jpg" width="1200" height="900" loading="lazy"><p>▲ 企業ブースの様子</p><h2 id="祝・若手奨励賞受賞！"><a href="#祝・若手奨励賞受賞！" class="headerlink" title="祝・若手奨励賞受賞！"></a>祝・若手奨励賞受賞！</h2><p>本大会では、同じAIXGメンバーである藤井の主著論文『TimeMachine-bench: LLMは「あの日」のコードを最新環境に適応できるか？』が、見事<strong>若手奨励賞</strong>に選出されました！<br>当論文の背景や解説については、著者の藤井自身が執筆したブログ記事が公開されておりますので、ぜひ併せてご覧ください。</p><ul><li><a href="https://future-architect.github.io/articles/20260316a/">【著者解説】NLP2026 若手奨励賞受賞論文 “TimeMachine-bench” を著者が解説する</a></li></ul><h1 id="投稿論文の紹介"><a href="#投稿論文の紹介" class="headerlink" title="投稿論文の紹介"></a>投稿論文の紹介</h1><p>当社は、NLP2026において著者・共著者を含めて合計4件の論文を投稿しました。</p><p>本章ではこれらの4件の論文について簡単に紹介します。</p><ul><li><strong><a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/B8-1.pdf">[B8-1]UI-Redline-bench: 赤入れ指示によるWebUIコード修正ベンチマーク</a></strong><br>この論文では、WebUI開発における、スクリーンショットに直接手書きで修正指示を書き込む「赤入れ」プロセスに着目し、視覚的指示に基づくコード修正タスクのベンチマーク「UI-Redline-bench」を構築しています。人間にとって直感的な形式の指示に対するVLM（視覚言語モデル）の処理能力に焦点を当て、VLMを活用した開発作業の能率向上への寄与を目指すものです。<br>　既存研究が主に言語的指示を扱っているのに対し、本研究は図形や文字を使って描きこまれた視覚的指示を扱う点が特徴です。著者らが視覚的指示を描きこみ作成した、計350件のHTML&#x2F;CSSコード修正データセットにより、GPT-5等の主要なVLMの視覚的指示に基づくコード修正能力を検証しました。<br>検証の結果、GPT-5等は全体として高い修正能力を示した一方で、矢印を使って示された離れた位置に、概形で描かれたUIを実装する事例では、修正に失敗する傾向が見られました。この結果から、今後VLMの空間推論能力の強化が不可欠であると言えます。具体的には、矢印の指し示す位置を正しく認識する、個々の図形を意味のあるまとまりとしてグループ化するなど、空間的に分散した視覚的指示を統合するタスクの必要性が示唆されています。（肥合）</li><li><strong><a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/B8-3.pdf">[B8-3]TimeMachine-bench: LLMは「あの日」のコードを最新環境に適応できるか？</a></strong><br>この論文は本学会において若手奨励賞に選出されました！<a href="https://future-architect.github.io/articles/20260316a/">こちらの技術ブログ</a>で詳しく説明されていますので、ぜひご覧ください。</li><li><strong><a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/B9-19.pdf">[B9-19]WikiOriginQA: 知識の起源を組み込んだ文化的バイアス分析用QAデータセットの自動構築</a></strong><br>この論文では、LLMの性能における文化的バイアスを検証するためのQAデータセットと、その自動構築手法を提案しています。LLMの学習データは、英語をはじめとした使用者の多い主要な言語に偏っています。そのため、主要言語圏の知識は良く知っている一方で、マイナーな言語圏の知識は適切に扱えないという文化的バイアスが起こることが知られています。しかし、既存の手法では該当する文化に詳しい協力者を必要とするため、多言語・多文化にわたる調査用データセットの構築は困難でした。そこで本論文では、「ある文化に特有の知識は、その文化と関係の深い言語において最も早くドキュメント化される」という仮説に基づき、文化的知識と言語の結びつきをWikipediaの記事作成日を用いて表現することで、文化的知識を問うQAデータセット「WikiOriginQA」を自動構築しました。<br>WikiOriginQAを用いた実験の結果、先行研究と同様にLLMの性能における英語圏の文化へ偏重傾向が確認され、これにより、多様な文化におけるバイアス検証を人手を介さずに行う上で、本手法が有効であることが示唆されました。（羽根田）</li><li><strong><a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/C9-20.pdf">[C9-20]項の復元は必要か？日英機械翻訳における省略された項の扱いの分析</a></strong><br>この論文では、主語や目的語等の述語の項が省略されやすい日本語のような言語 (pro-drop言語) から英語への機械翻訳において、従来重視されてきた「省略された項の同定および復元」が本当に必要かという問題に対し、その必要性を実証的に検証しています。<br>具体的には、LLM (GPT-4o) を用いた日英翻訳を対象に、省略項の同定 (ゼロ照応解析) の成否と翻訳品質の関係、および翻訳時に項がどの程度復元されるかを分析しています。また、省略項が復元されなかった場合の構文選択なども含め、多角的な事例分析が行われています。<br>その結果、GPT-4oは省略項の同定が困難な事例や項を復元せずに訳した場合においても文意が伝達できる翻訳が行えていました。ただし、項の同定が困難な事例では復元を避ける傾向が見られるなど項の同定可否に伴って翻訳の性質には差が見られました。しかし、省略項を復元しない場合、GPT-4oは述語の名詞化や受動態などを用いることで構文的に自然な翻訳が行えていました。これらの結果から、項の同定・復元は必須ではなく、日英翻訳において最先端のLLMには項の省略はもはや主要な障壁ではないことが示唆されました。 (野末)</li></ul><h2 id="社員が選ぶNLP2026の注目論文の紹介"><a href="#社員が選ぶNLP2026の注目論文の紹介" class="headerlink" title="社員が選ぶNLP2026の注目論文の紹介"></a>社員が選ぶNLP2026の注目論文の紹介</h2><p>ここからは、NLP2026に参加したメンバーが各自の視点で「これは面白い！」「業務に活かせそう！」と感じたイチオシの論文やセッションを3件ピックアップして紹介します。</p><ul><li><strong><a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/P8-20.pdf">[P8-20] 多言語文埋め込みの意味と言語の分離のための損失関数の分析</a></strong><br>日本語や英語など、言語が違っても似た意味の文同士が近くなるようにベクトル化する「多言語文埋め込み」は、情報検索や機械翻訳の品質推定など幅広い領域で使われています。<br> この技術では「言語によらず」意味の近さが反映されることが理想ですが、実際には言語情報に引っ張られてクラスタが分離してしまい、言語横断的なタスクでの性能低下を招くことが知られています。そのため先行研究では、文埋め込みを「意味表現」と「言語表現」に分離し、意味表現のみを用いる手法が提案されてきました。<br>この分離のための学習手法（損失関数の設計）には、各要素内で完結する「要素内制約」と、両者に跨る「要素間制約」が存在しますが、これらが分離にどう寄与するかは十分に分かっていませんでした。本研究でこれらを比較した結果、従来のエンコーダ由来モデル（LaBSEなど）では両者の併用が有効な一方、近年のデコーダ由来モデル（Gemini Embeddingなど）では「要素間制約」のみを用いる方が高い性能を示すことが明らかになりました。<br>　LLMベースのモデルが普及する中で、従来のモデルとは適した学習アプローチが異なることを実証した事実は興味深いと考え、こちらの発表を取り上げました。（森下）</li><li><strong><a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/Q3-10.pdf">[Q3-10] マルチモーダルかつ長い文脈の処理が求められる語用論的推論ベンチマーク</a></strong><br>大規模言語モデル（LLM）や大規模視覚言語モデル（VLLM）において、言語の辞書的な意味ではなく文脈依存の意味を理解するための「語用論的推論能力」の向上は社会実装を目指すうえで重要な課題です。この研究では、特にマルチモーダルかつ長期文脈の処理が必要な設定における語用論的推論のベンチマークを提案しています。<br>特徴として、漫画データを題材として採用し、著者が人手で問題・選択肢を作成することで計101問の多肢選択式QAデータセットを構築しています。実際にVLLMと人間による評価実験を行った結果、最新のVLLMであっても正解率は人間を大きく下回り、語用論的推論能力には課題が残っていることが示されました。<br>マルチモーダル・長期文脈の処理が必要な設定として漫画を活用している点や、評価実験を通して昨今著しい性能向上が見られるVLLMの弱点を浮き彫りにした点が非常に興味深く、挙げさせていただきました。(岸波)</li><li><strong><a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/B6-7.pdf">[B6-7] クロスコーダーを用いた脳と言語モデルにおける内部表現の特徴量比較</a></strong><br>本研究は、ポッドキャスト聴取時の脳活動（fMRIデータ）と言語モデル（LM）の内部表現を「クロスコーダー」という手法で比較し、両者の情報処理における類似点や相違点を検証した論文です。従来の研究では、LMの表現から脳応答を予測するエンコーディングモデルが主流でしたが、これはLMから脳への一方向的な写像に留まり、予測の根拠となる要因を人間が理解できる形で抽出できない（解釈性が低い）という課題がありました。<br>本手法では、脳応答とLM表現の両方を同一のスパース（疎）な潜在空間に写像し、そこからそれぞれの表現を再構成するプロセスを学習します。この仕組みにより、抽出された特徴量が脳とLMのどちらに強く寄与しているかを定量的に評価することが可能になりました。実験の結果、「場所」に関する描写は脳とLMで共通して表現されている一方、「負の情動」は脳側に、フィラー（言いよどみ）などの「口語表現」はLM側に特有の反応として抽出されました。<br>fMRIの解像度の限界や、因果関係の特定といった課題はあるものの 、AIの内部表現と人間の理解を直接比較し、その差を数値で示せる点は非常に画期的です。知性の在り方を定量的に議論できる可能性に興味を惹かれ、本論文を取り上げました。（田中）</li></ul><h2 id="会場の様子・オフタイムのエピソード"><a href="#会場の様子・オフタイムのエピソード" class="headerlink" title="会場の様子・オフタイムのエピソード"></a>会場の様子・オフタイムのエピソード</h2><p>オンサイト参加ならではの、現地の雰囲気やエピソードも少しご紹介します。</p><p>最も印象的だったのは、学会オープニングでの出来事です。全プログラム終了後に「重要な注意事項」と銘打たれたスライドで<strong>宇都宮餃子の正しい食べ方</strong>のレクチャーがありました。開催地である宇都宮ならではの粋な計らいに、会場全体が温かい笑いと拍手に包まれました。</p><img src="/images/2026/20260420a/gyoza_lecture.jpeg" alt="gyoza_lecture.jpeg" width="1200" height="900" loading="lazy"><p>▲ オープニングでの「重要なお知らせ（餃子の食べ方）」</p><p>また、天候に関しては驚きの連続でした。開催2日目には、3月であるにもかかわらずなんと<strong>大雪</strong>に見舞われました。会場周辺の道も真っ白に雪が積もり、ある意味で非常に記憶に残る学会となりました。</p><img src="/images/2026/20260420a/snow_scene.jpg" alt="snow_scene.jpg" width="1200" height="855" loading="lazy"><p>▲ 3月とは思えない大雪に見舞われた2日目</p><p>一方で、天気の良いお昼時には会場の外にキッチンカーが出店しており、日差しを浴びながら外でランチを楽しむこともできました。</p><h2 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h2><p>NLP初学者としての参加でしたが、日々の業務に直結する知見や、LLMの最新動向など、膨大なインプットを得ることができました。何より、最前線で活躍する研究者やエンジニアの熱量に直接触れることができ、「自分もさらに技術を磨いていこう」と強くモチベーションを刺激されました。</p><p>現在、AIXGのNLPチームでは、以下のような環境・制度を整え、NLPの基礎・応用技術開発を積極的に推進しています。</p><ul><li><strong>大学との共同研究</strong></li><li><strong>社会人博士制度</strong></li><li><strong>新規参画者への教育プログラム</strong></li><li><strong>GPUクラスタ</strong></li></ul><p>こうした環境のもと、ともに働くメンバーを積極的に募集しています！</p><p><strong>学生の皆様へ</strong><br>夏季インターンやアルバイトについては、4月20日 (月) に下記のウェブサイトから募集を開始いたします。<br>また、博士課程向けのResearch Internは通年募集中です。</p><p><a href="https://www.future.co.jp/recruit/summer_intern/2026/">https://www.future.co.jp/recruit/summer_intern/2026/</a></p><p>昨年のインターンシップ参加者へのインタビューもぜひご確認ください。</p><p><a href="https://future-architect.github.io/articles/20260323a/">https://future-architect.github.io/articles/20260323a/</a></p><p><strong>新卒・キャリア採用について</strong><br>NLP分野における社会実装のニーズは拡大し続けており、自然言語処理の力で顧客のビジネス課題を解決していく仲間を求めています。少しでも興味を持っていただけた方は、ぜひ以下の採用リンクからご応募をお待ちしております！</p><ul><li>新卒採用 : <a href="https://www.future.co.jp/recruit/recruit/rec-fresh/">https://www.future.co.jp/recruit/recruit/rec-fresh/</a></li><li>キャリア採用 : <a href="https://www.future.co.jp/recruit/recruit/rec-career/">https://www.future.co.jp/recruit/recruit/rec-career/</a></li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="DataScience" scheme="https://future-architect.github.io/categories/DataScience/"/>
    
    
    <category term="参加レポート" scheme="https://future-architect.github.io/tags/%E5%8F%82%E5%8A%A0%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
    
    <category term="NLP" scheme="https://future-architect.github.io/tags/NLP/"/>
    
    <category term="学会" scheme="https://future-architect.github.io/tags/%E5%AD%A6%E4%BC%9A/"/>
    
  </entry>
  
  <entry>
    <title>Mermaid.jsで数値報告のための簡易的なグラフを作るTips</title>
    <link href="https://future-architect.github.io/articles/20260417a/"/>
    <id>https://future-architect.github.io/articles/20260417a/</id>
    <published>2026-04-16T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<h2 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h2><p>Markdownでプロジェクトの進捗報告やチームの振り返り資料をまとめていると、定量的な部分にはグラフを入れたくなります。Excelなど外部ツールでグラフを作成して画像を挿入しても良いですが、画像ファイルの管理がちと面倒です。</p><p><a href="https://mermaid.js.org/">Mermaid.js</a> はUMLを描けることで有名ですが、実はいくつかのグラフ表示もサポートされています。どうせあまりオシャレじゃないんでしょう？..と、あまり期待していなかったのですが、想定よりオシャレ化ができることが分かったので、この感動を伝えさせてください。</p><p>もちろん、見た目や構成ではいくつか制約があります。それさえ許容できれば、テキストでグラフを記述でき、VS Codeなどでのプレビュー以外にも、GitHub・GitLab・Notionなど多くのプラットフォームでプレビューできますし、何よりAIからの支援も受けやすくなりお勧めです。</p><p>試してみたナレッジをいくつか紹介します。</p><h2 id="環境"><a href="#環境" class="headerlink" title="環境"></a>環境</h2><ul><li>Mermaid.js v11+（<code>radar-beta</code>、<code>xychart-beta</code>が利用可能）</li><li>表示確認: VS Code（Markdown Preview Mermaid Support拡張）、GitHub</li></ul><h2 id="レーダーチャート"><a href="#レーダーチャート" class="headerlink" title="レーダーチャート"></a>レーダーチャート</h2><p>2025年3月にリリースされた<a href="https://github.com/mermaid-js/mermaid/releases/tag/mermaid%4011.6.0">v11.6</a>からレーダーチャートが利用可能になりました。<a href="https://mermaid.ai/open-source/syntax/radar.html">ドキュメント</a>もあります。レーダーチャートは、多軸の評価結果を一目で比較したいといった用途に便利ですよね。</p><h3 id="Tips-1-レーダーチャートの装飾"><a href="#Tips-1-レーダーチャートの装飾" class="headerlink" title="Tips 1: レーダーチャートの装飾"></a>Tips 1: レーダーチャートの装飾</h3><p>例えば、チームの「開発チーム成熟度」を複数の観点で評価し、2つのチームの強みを重ねて比較してみます。</p><p>このとき、以下の設定がおすすめです。デフォルトでもそこそこオシャレですが、細かい調整をしたくなります。</p><ol><li><code>graticule polygon</code> を指定して目盛線を多角形にする<ul><li>デフォルトの曲線もオシャレだが、言いたいことが伝わりにくい…</li></ul></li><li>絶対評価の基準となる <code>max</code>（満点）と <code>min</code>（最低点）を指定する<ul><li>指定しない場合は、入力から自動で補正するが揃えた方が無難</li></ul></li><li><code>curve</code> のラベルにチーム名だけでなく、括弧書きで「平均スコア」等のサマリを埋め込んで凡例とする<ul><li>テキストでの追加情報は、ラベルに足すしかない</li></ul></li><li>デフォルトの配色を変えたい場合、configで変更可能<br> 　 - <code>cScale0</code>/<code>cScale1</code>で1番目、2版目のレーダーの色を調整。 <code>curveOpacity</code> で透過度を指定する</li></ol><pre class="mermaid">---config:  theme: default  themeVariables:    cScale0: "#FF5252"    cScale1: "#4CAF50"    radar:      axisColor: "#9E9E9E"      graticuleColor: "#E0E0E0"      curveOpacity: 0.25      curveStrokeWidth: 1---radar-beta    graticule polygon    axis v["ベロシティ"], q["品質"], a["自律性"]    axis c["コミュ力"], p["計画精度"], t["技術力"]    curve d["チームA (4.4)"]{4.4, 4.2, 4.8, 4.7, 3.9, 4.3}    curve c["チームB (4.2)"]{4.5, 3.7, 4.5, 3.9, 4.8, 4.4}    max 5    min 0</pre><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line">config:</span><br><span class="line">  theme: <span class="keyword">default</span></span><br><span class="line">  themeVariables:</span><br><span class="line">    cScale0: <span class="string">"#FF5252"</span></span><br><span class="line">    cScale1: <span class="string">"#4CAF50"</span></span><br><span class="line">    radar:</span><br><span class="line">      axisColor: <span class="string">"#9E9E9E"</span></span><br><span class="line">      graticuleColor: <span class="string">"#E0E0E0"</span></span><br><span class="line">      curveOpacity: <span class="number">0.25</span></span><br><span class="line">      curveStrokeWidth: <span class="number">1</span></span><br><span class="line">---</span><br><span class="line">radar-beta</span><br><span class="line">    graticule polygon</span><br><span class="line">    axis v[<span class="string">"ベロシティ"</span>], q[<span class="string">"品質"</span>], a[<span class="string">"自律性"</span>]</span><br><span class="line">    axis c[<span class="string">"コミュ力"</span>], p[<span class="string">"計画精度"</span>], t[<span class="string">"技術力"</span>]</span><br><span class="line">    curve d[<span class="string">"チームA (4.4)"</span>]{<span class="number">4.4</span>, <span class="number">4.2</span>, <span class="number">4.8</span>, <span class="number">4.7</span>, <span class="number">3.9</span>, <span class="number">4.3</span>}</span><br><span class="line">    curve c[<span class="string">"チームB (4.2)"</span>]{<span class="number">4.5</span>, <span class="number">3.7</span>, <span class="number">4.5</span>, <span class="number">3.9</span>, <span class="number">4.8</span>, <span class="number">4.4</span>}</span><br><span class="line">    max <span class="number">5</span></span><br><span class="line">    min <span class="number">0</span></span><br></pre></td></tr></table></figure><h2 id="棒グラフ"><a href="#棒グラフ" class="headerlink" title="棒グラフ"></a>棒グラフ</h2><h3 id="Tips-2-積み上げグラフの実現"><a href="#Tips-2-積み上げグラフの実現" class="headerlink" title="Tips 2: 積み上げグラフの実現"></a>Tips 2: 積み上げグラフの実現</h3><p>xychart-betaは ネイティブの積上げ棒グラフをサポートしていません。複数の<code>bar</code>シリーズを定義すると、並列ではなく<strong>重ね描画</strong>されます。</p><p>例として、機能別・優先度別の残タスク数を可視化したいとします。</p><div class="scroll"><table><thead><tr><th></th><th>検索機能</th><th>決済機能</th><th>ユーザー管理</th></tr></thead><tbody><tr><td>優先度：高</td><td>0</td><td>2</td><td>0</td></tr><tr><td>優先度：中</td><td>2</td><td>4</td><td>3</td></tr><tr><td>優先度：低</td><td>4</td><td>0</td><td>1</td></tr><tr><td><strong>合計</strong></td><td><strong>6</strong></td><td><strong>6</strong></td><td><strong>4</strong></td></tr></tbody></table></div><p>素朴にそのまま書くと、本来「6」「6」「4」の高さになるはずの棒グラフが、ただ同じ位置から重ねて描画されてしまい、意味不明な状態になります（積み上げグラフになっているじゃん…と思いますが、高さをよく見ると合計値に達していません）。</p><pre class="mermaid">xychart-beta    title "❌️重なって描画される"    x-axis ["検索機能", "決済機能", "ユーザー管理"]    y-axis "タスク数" 0 --&gt; 10    bar "高" [0, 2, 0]    bar "中" [2, 4, 3]    bar "低" [4, 0, 1]</pre><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">xychart-beta</span><br><span class="line">    title <span class="string">"NG: そのままの数値（重なって描画される）"</span></span><br><span class="line">    x-axis [<span class="string">"検索機能"</span>, <span class="string">"決済機能"</span>, <span class="string">"ユーザー管理"</span>]</span><br><span class="line">    y-axis <span class="string">"タスク数"</span> <span class="number">0</span> --&gt; <span class="number">10</span></span><br><span class="line">    bar <span class="string">"高"</span> [<span class="number">0</span>, <span class="number">2</span>, <span class="number">0</span>]</span><br><span class="line">    bar <span class="string">"中"</span> [<span class="number">2</span>, <span class="number">4</span>, <span class="number">3</span>]</span><br><span class="line">    bar <span class="string">"低"</span> [<span class="number">4</span>, <span class="number">0</span>, <span class="number">1</span>]</span><br></pre></td></tr></table></figure><p>3本のグラフが重なっていますが、逆手にとって考えると、累積値で、擬似的に積上げ棒グラフにできるということです。悪い回避策だという指摘はいったん脇に..。</p><p>棒グラフは “最後”に書いた定義が “手前” に表示されます。そのため、一番最初に積み上げたい「低」を最初に書いて、「高」を最後に書きます。</p><ul><li><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.452ex;" xmlns="http://www.w3.org/2000/svg" width="17.598ex" height="2.149ex" role="img" focusable="false" viewBox="0 -750 7778.4 950"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">低</text></g><g data-mml-node="mo" transform="translate(1277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(2333.6,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">高</text></g><g data-mml-node="mo" transform="translate(3555.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(4556,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">中</text></g><g data-mml-node="mo" transform="translate(5778.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(6778.4,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">低</text></g></g></g></svg></mjx-container></li><li><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.452ex;" xmlns="http://www.w3.org/2000/svg" width="12.57ex" height="2.149ex" role="img" focusable="false" viewBox="0 -750 5556 950"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">中</text></g><g data-mml-node="mo" transform="translate(1277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(2333.6,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">高</text></g><g data-mml-node="mo" transform="translate(3555.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(4556,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">中</text></g></g></g></svg></mjx-container></li><li><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.452ex;" xmlns="http://www.w3.org/2000/svg" width="7.542ex" height="2.149ex" role="img" focusable="false" viewBox="0 -750 3333.6 950"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">高</text></g><g data-mml-node="mo" transform="translate(1277.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(2333.6,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">高</text></g></g></g></svg></mjx-container></li></ul><p>こうすると、「中」の棒は「低」の棒の「内側」に重なり、「高」の棒はさらにその内側に重なります。結果として<strong>色が層になり、積上げに見える</strong>わけです。</p><pre class="mermaid">xychart-beta    title "✅️積み上げには成功、❌️0件が微妙な表示"    x-axis ["検索機能", "決済機能", "ユーザー管理"]    y-axis "タスク数" 0 --&gt; 10    bar "低" [6, 6, 4]    bar "中" [2, 6, 3]    bar "高" [0, 2, 0]</pre><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">xychart-beta</span><br><span class="line">    # ...中略...</span><br><span class="line">    bar <span class="string">"低"</span> [<span class="number">6</span>, <span class="number">6</span>, <span class="number">4</span>]</span><br><span class="line">    bar <span class="string">"中"</span> [<span class="number">2</span>, <span class="number">6</span>, <span class="number">3</span>]</span><br><span class="line">    bar <span class="string">"高"</span> [<span class="number">0</span>, <span class="number">2</span>, <span class="number">0</span>]</span><br></pre></td></tr></table></figure><p>積み上げは成功しましたが、0件である「高」が微妙に表示されてしまいノイジーです（0なのにわずかに表示されてしまってます）。いろいろ試したのですが、配列の中間の0は <code>-1</code> に置換することで回避可能です。末尾の記述も-1にするか省略で回避できました。<code>-1</code> にしておくことで、グラフ上にはレンダリングされませんが、「データが存在する」という判定になるため、色の割当順序が正しく維持されるため、都合が良いです。</p><pre class="mermaid">xychart-beta    title "✅️ 意図した通りに疑似積上げされる"    x-axis ["検索機能", "決済機能", "ユーザー管理"]    y-axis "タスク数" 0 --&gt; 10    bar "低" [6, 6, 4]    bar "中" [2, 6, 3]    bar "高" [-1, 2]</pre><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">xychart-beta</span><br><span class="line">    x-axis [<span class="string">"検索機能"</span>, <span class="string">"決済機能"</span>, <span class="string">"ユーザー管理"</span>]</span><br><span class="line">    # ...中略...</span><br><span class="line">    bar <span class="string">"高"</span> [<span class="number">-1</span>, <span class="number">2</span>]  ← 中間は<span class="number">-1</span>、末尾は省略でも良い</span><br></pre></td></tr></table></figure><h3 id="Tips-3-色の指定は-plotColorPalette-と凡例"><a href="#Tips-3-色の指定は-plotColorPalette-と凡例" class="headerlink" title="Tips 3: 色の指定は plotColorPalette と凡例"></a>Tips 3: 色の指定は plotColorPalette と凡例</h3><p>さきほど、積み上げ棒グラフ化には成功しましたが、優先度が高い順に、目立たせたいと思うはずです。疑似積上げでは「外側ほど薄く、内側ほど濃く」するのが自然です。「高」を最も濃い色にすると、対応すべきタスクが視覚的に目立ちます。棒グラフの色は<code>plotColorPalette</code>で指定します。シリーズの定義順（<code>bar</code>の記述順）に対応します。</p><p>色を付けると、色の意味は何だ？となりますが、xychart-betaには凡例機能はありません（！）。そのため、Markdownが許容する環境であれば、Mermaidブロックのすぐ下にHTML書いてしまうのがてっとり早い回避手段です。</p><pre class="mermaid">---config:  theme: base  themeVariables:    xyChart:      plotColorPalette: "#fff4dd, #ffd8b1, #FF6E40"---xychart-beta    x-axis ["検索機能", "決済機能", "ユーザー管理"]    y-axis "タスク数" 0 --&gt; 10    bar "低" [6, 6, 4]    bar "中" [2, 6, 3]    bar "高" [-1, 2]</pre><div style="display: flex; justify-content: center; gap: 24px; font-size: 14px; color: #424242; font-family: sans-serif; margin-top: -8px;">  <div style="display: flex; align-items: center; gap: 6px;">    <span style="color: #FF6E40; font-size: 18px;">■</span> 高  </div>  <div style="display: flex; align-items: center; gap: 6px;">    <span style="color: #ffd8b1; font-size: 18px;">■</span> 中  </div>  <div style="display: flex; align-items: center; gap: 6px;">    <span style="color: #fff4dd; font-size: 18px; text-shadow: 0 0 1px #ccc;">■</span> 低  </div></div><br><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line">config:</span><br><span class="line">  theme: base</span><br><span class="line">  themeVariables:</span><br><span class="line">    xyChart:</span><br><span class="line">      plotColorPalette: <span class="string">"#fff4dd, #ffd8b1, #FF6E40"</span></span><br><span class="line">---</span><br><span class="line">xychart-beta</span><br><span class="line">...</span><br><span class="line">    bar <span class="string">"低"</span> [<span class="number">6</span>, <span class="number">6</span>, <span class="number">4</span>]    ← <span class="number">1</span>番目（#fff4dd: 薄い黄色）</span><br><span class="line">    bar <span class="string">"中"</span> [<span class="number">2</span>, <span class="number">6</span>, <span class="number">3</span>]    ← <span class="number">2</span>番目（#ffd8b1: 薄いオレンジ）</span><br><span class="line">    bar <span class="string">"高"</span> [<span class="number">-1</span>, <span class="number">2</span>]  ← <span class="number">3</span>番目（#FF6E40: 濃いオレンジ）</span><br></pre></td></tr></table></figure><p>HTMMLは以下のようなものを、mermaid.jsのすぐ下にそのまま配置します。</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">"display: flex; justify-content: center; gap: 24px; font-size: 14px; color: #424242; font-family: sans-serif; margin-top: -8px;"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">"display: flex; align-items: center; gap: 6px;"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">"color: #FF6E40; font-size: 18px;"</span>&gt;</span>■<span class="tag">&lt;/<span class="name">span</span>&gt;</span> 高</span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">"display: flex; align-items: center; gap: 6px;"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">"color: #ffd8b1; font-size: 18px;"</span>&gt;</span>■<span class="tag">&lt;/<span class="name">span</span>&gt;</span> 中</span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">"display: flex; align-items: center; gap: 6px;"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">"color: #fff4dd; font-size: 18px; text-shadow: 0 0 1px #ccc;"</span>&gt;</span>■<span class="tag">&lt;/<span class="name">span</span>&gt;</span> 低</span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>ポイントは、薄い色（優先度「低」）の凡例の四角（■）に <code>text-shadow: 0 0 1px #ccc;</code> を付けて輪郭線を出すことです。これにより、白背景でも凡例の視認性をしっかり確保できます。</p><h3 id="Tips-4-Before-Afterの表示"><a href="#Tips-4-Before-Afterの表示" class="headerlink" title="Tips 4: Before/Afterの表示"></a>Tips 4: Before/Afterの表示</h3><p>xychart-betaはグループ化（横並べ）ができません。そのため、リファクタリング前（Before）と後（After）のバグ発生数を比較したい場合などは、グラフを2つ並べます。</p><pre class="mermaid">---config:  theme: base  themeVariables:    xyChart:      plotColorPalette: "#FF8A80"---xychart-beta    title "バグ発生数 (リファクタリング前)"    x-axis ["検索機能", "決済機能", "ユーザー管理", "共通基盤"]    y-axis "件数" 0 --&gt; 80    bar [8, 39, 41, 24]</pre><pre class="mermaid">---config:  theme: base  themeVariables:    xyChart:      plotColorPalette: "#81C784"---xychart-beta    title "バグ発生数 (リファクタリング後)"    x-axis ["検索機能", "決済機能", "ユーザー管理", "共通基盤"]    y-axis "件数" 0 --&gt; 80    bar [0, 8, 70, 34]</pre><p>ポイントは<strong>Y軸の上限を揃えること</strong>（<code>0 --&gt; 80</code>）です。これにより、棒の長さだけで直感的に「改善したかどうか」を比較できるようになります。</p><h2 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h2><p>Mermaid.jsは「テキストで書ける手軽さ」が最大の強みですが、積上げ棒グラフや凡例、スライスの順序制御といった実務で欲しい機能に一部制約があります。</p><p>しかし、今回紹介した<strong>疑似積上げ・-1置換・HTML凡例・上下2段比較</strong>といったテクニックを使えば、これらの制約を回避して、メンテナンス性の高い「それっぽい」レポートをMarkdown内で完結させることができます。</p><p>特にAIツールとの相性は抜群です。「このタスクデータをMermaidの疑似積上げ棒グラフにして」と頼めば、累積値の計算からコード生成まで一瞬で終わります。ぜひ、日々の進捗報告や振り返り資料に取り入れてみてください。</p><script type="module"> import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';mermaid.initialize({startOnLoad: true, flowchart: {curve: 'linear'}}); </script>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="Management" scheme="https://future-architect.github.io/categories/Management/"/>
    
    
    <category term="Markdown" scheme="https://future-architect.github.io/tags/Markdown/"/>
    
    <category term="Mermaid.js" scheme="https://future-architect.github.io/tags/Mermaid-js/"/>
    
  </entry>
  
  <entry>
    <title>Agentic AI Summit &#39;26 Spring 参加レポート</title>
    <link href="https://future-architect.github.io/articles/20260416a/"/>
    <id>https://future-architect.github.io/articles/20260416a/</id>
    <published>2026-04-15T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260416a/top.jpg" alt="" width="372" height="259"><h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>2025年10月新卒入社で、製造・エネルギー事業部所属の古賀です。<br>2026年3月19日（木）に開催されたGoogle Cloud 主催の<a href="https://cloudonair.withgoogle.com/events/agentic-ai-summit-26-spring">「Agentic AI Summit ‘26 Spring」</a>に参加してきました。</p><p>本イベントでは、単なるタスクの自動化を行うAIから進化した、自律的に思考し行動する「Agentic AI」をメインテーマとしており、自律型AIがいかに業務プロセスを変革し、ビジネス成果をもたらすかを学びました。</p><p> 今回は、イベントで発表された多岐にわたる技術を「5つの領域」に整理し、それぞれの領域で「どのような変革が起きるのか」と「それを支える具体的な技術・サービス」を紐づけてまとめました。</p><h2 id="1-業務改革の領域"><a href="#1-業務改革の領域" class="headerlink" title="1. 業務改革の領域"></a>1. 業務改革の領域</h2><h3 id="変革"><a href="#変革" class="headerlink" title="変革"></a>変革</h3><p><strong>これまで</strong><br>現在のAIは非常に優秀ですが、ユーザや組織の文脈を知らないため、毎回ゼロから詳細な背景を説明する必要があります。また、情報のサイロ化が起き、データが様々なシステムに分断されている状態です。そのため、現在のAIは例えるなら「中途入社初日の新人」のような状態であり、プロンプトを出せる限られた従業員しか恩恵を受けられないという問題を抱えています。</p><p><strong>これから</strong><br>Gemini Enterpriseを使用することで、AIを新人から文脈を理解できる「育てるチームメイト」にしていくことが可能です。これにより、個別の作業を自動化する「点の自動化」から、分断されたプロセス全体をカバーする「プロセスの自律化」へと移行することが可能になります。</p><h3 id="技術・サービス"><a href="#技術・サービス" class="headerlink" title="技術・サービス"></a>技術・サービス</h3><p><strong>Gemini Enterprise</strong><br>社内外のデータソースと安全に接続できる基盤であり、AIエージェント（自社開発、購入したもの、Google標準）が稼働するプラットフォームとして機能するSaaSプロダクトです。長期記憶（オフィスアプリ等のパーソナライズドデータソース）とパーソナライズによってユーザの過去の行動や好み、特性を記憶し、個別の状況に合わせた提案等が可能になります。さらに、マルチモーダルRAGや各種コネクタを用い、散在する社内データ（CRM、Drive、メール等）にアクセスし、人間と同じように情報を検索・統合できる環境を構築できます。</p><h2 id="2-エンジニアリングの領域"><a href="#2-エンジニアリングの領域" class="headerlink" title="2. エンジニアリングの領域"></a>2. エンジニアリングの領域</h2><h3 id="変革-1"><a href="#変革-1" class="headerlink" title="変革"></a>変革</h3><p><strong>これまで</strong><br>「Gemini 3」モデルでは、アーキテクチャの相談、リファクタリング、テストコード生成、仕様書作成など、開発ライフサイクル全体を支えることが可能です。しかし、LLMでのコード生成はブラウザや専用アプリ上で行われているため、開発環境（IDE）との往復が手間という問題もあります。さらに、人間の処理能力（タイピング速度や思考スピード）が依然として開発のボトルネックになるという構造的な限界が存在しています。</p><p><strong>これから</strong><br>人間がコードを書き、AIがそれを支援する時代から、AIが自律的な部下としてワークフロー全体を主導する時代へとシフトしています。これにより人間の役割は「コードを書くこと」から「何を開発するか」「AIが立てた計画の承認」「最終的な成果物のレビュー」へとシフトし、圧倒的なスピードとスケーラビリティでの開発が可能となります。</p><h3 id="技術・サービス-1"><a href="#技術・サービス-1" class="headerlink" title="技術・サービス"></a>技術・サービス</h3><p><strong>Gemini Code Assist（支援型）</strong><br>エンジニアが使い慣れたIDE（開発環境）とAIを統合し、シームレスな体験を提供するサービスです。開発者が開いているファイルやプロジェクト構造（コンテキスト）を自動で認識し、的確な提案を実施します。またインライン補完やチャットベースでのコード生成・解説が可能です。</p><p><strong>Antigravity（自律型）</strong><br>エージェント主導の開発プラットフォームです。VS CodeベースのUIを使用しており、Gemini 3だけでなく、ClaudeやOSSモデルなどマルチモデルに対応しています。また、フロントエンド用、バックエンド用など複数のエージェントを立ち上げ、並列で同時開発を進行できます。さらに、エージェントが自律的にブラウザを立ち上げ、描画崩れやロジックの動作確認を自動で実施（動画やスクリーンショットで人間に報告）することが可能です。</p><h2 id="3-エージェント運用の領域"><a href="#3-エージェント運用の領域" class="headerlink" title="3. エージェント運用の領域"></a>3. エージェント運用の領域</h2><h3 id="変革-2"><a href="#変革-2" class="headerlink" title="変革"></a>変革</h3><p><strong>これまで</strong><br>企業において複数のAIエージェントが開発・導入されるマルチエージェント時代を迎えるに伴い、「本当に使われているのか」「ROIは出ているのか」「現在のエージェントが最適か」などの懸念が生まれています。また、従来のBIツール（ダッシュボード）では、定型的な指標の確認は可能ですが、想定外の事象に対する深掘り分析には、データアナリストへ依頼が必要となり「タイムロス」が発生していました。</p><p><strong>これから</strong><br>エージェントの行動を可視化し、評価と改善のサイクルを回すこと、そして得られた対話データをビジネスインサイトに繋げることが成功要因の一つとなってきます。さらに、構造化データだけでなく、画像・動画・PDFやグラフデータなどの非構造化データを駆使し、誰もがデータからインサイトを得られる「データの民主化」を実現し、データを統合的に扱える基盤を構築することが不可欠になります。</p><h3 id="技術・サービス-2"><a href="#技術・サービス-2" class="headerlink" title="技術・サービス"></a>技術・サービス</h3><p><strong>Vertex AI Agent Builder</strong><br>AIエージェントの開発から運用までをエンドツーエンドで支援する統合プラットフォームです。AgentOps機能が含まれており、プロンプトとレスポンスの文脈からAIが自動で採点基準を生成する「適応型ルーブリック」による回答品質の評価や、安全性・ハルシネーションの監視などを行えます。これによって継続的な改善のループを回し続けることが可能です。</p><p><strong>BigQuery Agent Analytics</strong><br>AIエージェントの行動ログ（ユーザとの対話履歴など）をリアルタイムでBigQueryに収集し、Gemini Enterpriseのような分析用エージェントを使って高度な分析を行うための機能です。開発時に数行のコードを追加するだけでログ収集が始まり、その後は自然言語で問いかけるだけでAIが自律的にSQLを生成し、対話ログから顧客のペインポイント（VoC）を直接検知するだけでなく、「なぜAIが答えられなかったのか」といったエラー原因の特定（AgentOps）までを行ってくれます。</p><h2 id="4-顧客体験の領域"><a href="#4-顧客体験の領域" class="headerlink" title="4. 顧客体験の領域"></a>4. 顧客体験の領域</h2><h3 id="変革-3"><a href="#変革-3" class="headerlink" title="変革"></a>変革</h3><p><strong>これまで</strong><br>従来の顧客接点は、ユーザ自身が手動で情報を入力して検索や購買を行う「機能的」なサイトが主流でした。検索やコマースのプロセスは断片化されており、消費者が期待するパーソナライズされた結果や代行アクションに十分に応えられていませんでした。</p><p><strong>これから</strong><br>これからは、AIがユーザに寄り添う「没入型（イマーシブ）」な顧客体験への移行が実現します。断片化された検索やコマースはシームレスな体験へと変わり、AIエージェントがユーザに代わって自律的に情報収集や代行（購買）アクションを実行します。</p><h3 id="技術・サービス-3"><a href="#技術・サービス-3" class="headerlink" title="技術・サービス"></a>技術・サービス</h3><p><strong>Gemini Enterprise for Customer Experience（GECX）</strong><br>断片化された検索やコマースをシームレスな顧客体験へと変える統合ソリューションスイートです。CX Agent Studioを含めた様々な要素で構成されており、自社データに基づく正確な回答を導く次世代検索エンジン（Vertex AI Search）や、会話データの分析と可視化を行うCustomer Experience Insightsが含まれています。</p><p><strong>CX Agent Studio</strong><br>エージェント構築・テスト・デプロイを担う基盤です。自然言語による指示だけでAIがフローを自動生成するローコード開発により、開発期間を短縮します。また、他社SaaS等と連携した実務アクションの実行や、自然な「割り込み」に対応する高品質な音声対話機能も備えています。さらに、不適切な発言やプロンプトインジェクションを防ぐガードレール機能といったエンタープライズ品質の安全性を標準搭載しつつ、推論プロセスの可視化や「Quality AI」による全対話の自動評価を行えます。</p><p><strong>ADK Gemini Live API Toolkit</strong><br>リアルタイム音声AIエージェントの開発ツールキットです。これまで開発の壁となっていた複雑なストリーミング通信の制御を簡略化し、AIが話している途中でユーザが「割り込み」できる、人間のように自然な双方向の会話を実現します。また、ネイティブオーディオモデルが声のトーンから感情を読み取る機能や、スマートフォンのカメラ映像をリアルタイムに処理するマルチモーダル機能も備えています。</p><h2 id="5-セキュリティの領域"><a href="#5-セキュリティの領域" class="headerlink" title="5. セキュリティの領域"></a>5. セキュリティの領域</h2><h3 id="変革-4"><a href="#変革-4" class="headerlink" title="変革"></a>変革</h3><p><strong>これまで</strong><br>攻撃者による生成AIの利用が一般化し、フィッシングメールの巧妙化や、脆弱性分析の自動化など攻撃手法が高度化しています。そのため、従来の人間主体の監視・対応では、AIによる高度な攻撃スピードに追い付くことが困難になりつつあります。</p><p><strong>これから</strong><br>AIエージェントを活用してSOCのワークロードを低減し、検知・対応の質を向上させる「Agentic SOC」への移行が求められます。現在はアラート発生時のトリアージ・調査・マルウェア解析の支援を実現していますが、将来的には、調査結果への対応、自動ルールチューニングまでをAIが一貫して実行し、ライフサイクル全般をAIが自律的に実行する世界に変わっていくことになります。さらに、AIを防御に活用するだけでなく、AIモデルや学習データそのものを守るための保護策も不可欠になります。</p><h3 id="技術・サービス-4"><a href="#技術・サービス-4" class="headerlink" title="技術・サービス"></a>技術・サービス</h3><p><strong>トリアージ・エージェント</strong><br>アラート発生時の初期調査を自動化するAIエージェントです。これまで人間が約30分かけて行っていた約20ステップの調査を、約1分で完了させることが可能になり、アラートが陽性か偽陽性かの一次判定を迅速に行えます。</p><p><strong>Model Armor</strong><br>生成AIアプリケーションを脅威から守るためにGoogle Cloudが提供している保護機能です。プロンプトインジェクションのブロックや、不適切な回答を抑制するためのフィルタリング機能を持っています。組織全体のガバナンス基準を適用でき、感度のチューニングも可能となっています。</p><h2 id="まとめ"><a href="#まとめ" class="headerlink" title="まとめ"></a>まとめ</h2><p>業務改革やエンジニアリングの現場では、AIが自社の文脈を理解して自律的なチームメイトとして働き、顧客接点においてはAIが主体となってユーザに寄り添う没入型の体験が実現しつつあります。 一方で、こうした自律型AIをビジネスで確実に機能させるためには、エージェントの行動を可視化して継続的に改善を回す「運用基盤」と、AIモデルやデータそのものを脅威から守る強固な「セキュリティ体制」が両輪として不可欠であると学びました。</p><p>今回の学びを活かし、自律型AIをどのように日々の業務に適応させ、新しいプロセスをデザインしていくかについて、考えていきたいと思います。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260416a/top.jpg&quot; alt=&quot;&quot; width=&quot;372&quot; height=&quot;259&quot;&gt;

&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="参加レポート" scheme="https://future-architect.github.io/tags/%E5%8F%82%E5%8A%A0%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
    
    <category term="AI" scheme="https://future-architect.github.io/tags/AI/"/>
    
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    
  </entry>
  
  <entry>
    <title>IT未経験の新卒が、AIとPJ効率化ツールを共同開発して得た学び</title>
    <link href="https://future-architect.github.io/articles/20260414a/"/>
    <id>https://future-architect.github.io/articles/20260414a/</id>
    <published>2026-04-13T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.110Z</updated>
    
    <content type="html"><![CDATA[<h2 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h2><p>初めまして、實藤陸（さねふじりく）です。<br>2025年新卒入社で、現在は製造エネルギーサービス事業部に所属しています。</p><p>今回は新卒入社の私がプロジェクト効率化ツールとして、GAS（文字起こし自動追加GAS）の開発をAIと共同で行いましたので、その内容について共有できればと思います！</p><div class="note-container info"><span class="fa-check-circle"></span><div><p><strong>💡 本記事のメイントピック</strong></p><p>本記事では、開発したツールの概要だけでなく、<strong>IT未経験の新卒が、どうやってAI（Gemini）に指示を出し、実運用レベルのシステムを作り上げたか</strong>というAIとの共同開発プロセスに重きを置いて学びを共有します。</p></div></div><h2 id="開発の背景と課題"><a href="#開発の背景と課題" class="headerlink" title="開発の背景と課題"></a>開発の背景と課題</h2><p>今回の開発に至った経緯は、PJにおいて下記のような背景と課題があったためです。</p><table>  <tbody>    <tr>      <td width="10%"><b>背景</b></td>      <td>Meet会議で生成されるGeminiの文字起こしや録画ファイルを、PJ共有ドライブに手動で蓄積し、振り返りに活用していた。</td>    </tr>    <tr>      <td><b>課題</b></td>      <td>ファイルは主催者のマイドライブ内 <code>Meet Recordings</code> フォルダに自動保存される仕様。そのため、<b>共有ドライブへの移動の手間や移動漏れ</b>が頻発し、そのたびにSlackで依頼するコミュニケーションコストが発生していた。</td>    </tr>  </tbody></table><p>そこで、この一連の作業を自動化するGASの開発に着手しました。</p><h2 id="文字起こし自動追加GASの概要"><a href="#文字起こし自動追加GASの概要" class="headerlink" title="文字起こし自動追加GASの概要"></a>文字起こし自動追加GASの概要</h2><p>まずは作成したツールの概要を紹介します。<br>文字起こし自動追加GASは簡潔に言うと、Google Meetの文字起こしや録画ファイルを、所定のフォルダに自動で整理し、Slackに通知するGASです！</p><p>2つのGASを用いて、Google Meet（のGemini機能）によって自動生成され、個人のマイドライブに保存される『文字起こし・録画・チャット』ファイルを、PJ共有ドライブ内の各フォルダへ移動し、Slackへ通知します。</p><h3 id="GASのざっくり構成図"><a href="#GASのざっくり構成図" class="headerlink" title="GASのざっくり構成図"></a>GASのざっくり構成図</h3><img src="/images/2026/20260414a/image.png" alt="image.png" width="1200" height="329" loading="lazy"><h3 id="それぞれのGASの内容"><a href="#それぞれのGASの内容" class="headerlink" title="それぞれのGASの内容"></a>それぞれのGASの内容</h3><ol><li><strong>GAS（1）：各自のマイドライブから「一時プール」へ（各自で実行）</strong><ul><li>各自のマイドライブから30分おきに自動で実行。</li><li>ファイル名の先頭に <code>YYYYMMDD_</code> を追加し、特定キーワード（顧客名など）を含む会議だけを共有ドライブ内の「一時プールフォルダ」へ移動させます。</li></ul></li><li><strong>GAS（2）：プールから各フォルダへ仕分け＆通知（管理者が実行）</strong><ul><li>毎日夜間に自動実行。</li><li>プール内のファイルを「顧客会議用」「内部会議用」などに自動で振り分け、移動件数やエラー結果をSlackに通知します。</li></ul></li></ol><div class="note-container info"><span class="fa-check-circle"></span><div><p><strong>なぜSlack通知するのか？</strong></p><p>処理のログを残し、正しい移動先へ格納されたか（あるいはエラーが起きていないか）を可視化して、メンバー全員が確認できるようにするためです。</p></div></div><p>これにより、どのメンバーが会議の主催者であっても、「文字起こし・録画・チャット」ファイルをPJの共有ドライブの正しいフォルダへ自動で蓄積できるようになり、当初の課題が解決できました！</p><h2 id="AIとの共同開発のプロセス"><a href="#AIとの共同開発のプロセス" class="headerlink" title="AIとの共同開発のプロセス"></a>AIとの共同開発のプロセス</h2><p>今回の開発において大いに助けられたのが、Geminiの存在でした。</p><p>自分はIT未経験入社の新卒で、GASに関する知見はまったくない状態でした。そのため、最初から完璧な設計をするのではなく、とりあえずAIに作ってもらい、動くものを見ながらツッコミを入れて改善していくという<strong>アジャイル的な開発プロセス</strong>をとりました。</p><p>ここではコードを1行も書くことなく、GASを完成させたプロセスをご紹介します。</p><h3 id="（1）-まずは「動くGAS」を作る！"><a href="#（1）-まずは「動くGAS」を作る！" class="headerlink" title="（1） まずは「動くGAS」を作る！"></a>（1） まずは「動くGAS」を作る！</h3><p>開発のスタートは、「どうすればいいんだ」というざっくりとした質問をGeminiに投げることでした。<br>当時の私は何をすればいいかよくわかっていなかったため、綿密な要件定義は行わず、以下のようなニュアンスで指示を出しました。</p><blockquote><p><strong>💬 Geminiへの指示のイメージ</strong><br>「Google Meetの文字起こしファイルを、マイドライブから共有ドライブに集めたい。とりあえず進め方と動くGASのコードを考えて」</p></blockquote><p>こんな抽象度の高いプロンプトでも、Geminiは大枠の方針を示し、最低限動くコードを一瞬で作成してくれました。</p><h3 id="（2）-人間が「観点」を与え、AIにブラッシュアップさせる"><a href="#（2）-人間が「観点」を与え、AIにブラッシュアップさせる" class="headerlink" title="（2） 人間が「観点」を与え、AIにブラッシュアップさせる"></a>（2） 人間が「観点」を与え、AIにブラッシュアップさせる</h3><p>動くものができたことで、実際に運用するには何が足りないかが明確に見えてきました。ここからは、上長とも相談しながら、Geminiが作った初期コードに対して<strong>人間ならではの観点</strong>を追加していく反復作業に入りました。</p><p>具体的にGeminiにぶつけた観点を2つ紹介します。</p><h4 id="観点1：セキュリティと可視化"><a href="#観点1：セキュリティと可視化" class="headerlink" title="観点1：セキュリティと可視化"></a>観点1：セキュリティと可視化</h4><p>初期のコードでは、Slackへの通知機能がなく移動結果が確認しにくかったり、フォルダのID等がコードに「ベタ打ち」されていたりと、セキュリティ観点がほぼ抜け落ちていました。<br>そこで、コードをレビューさせたり、ポイントで以下のようにツッコミを入れました。</p><blockquote><p><strong>💬 Geminiへのツッコミのイメージ</strong><br>「ここのフォルダIDやSlackのURL、コードに直接ベタ打ちで書いてるけどセキュリティ的に大丈夫？ベタ打ちを避ける方法で書き直して」</p></blockquote><p>この指示により、外部から見えない「スクリプトプロパティ」を使ってIDを隠す、安全なシステムへと改善してくれました。</p><h4 id="観点2：本番運用に向けたアーキテクチャの変更"><a href="#観点2：本番運用に向けたアーキテクチャの変更" class="headerlink" title="観点2：本番運用に向けたアーキテクチャの変更"></a>観点2：本番運用に向けたアーキテクチャの変更</h4><p>当初の開発では、「1つのGASを各メンバーにデプロイしてもらい、各自のマイドライブから直接共有ドライブの各フォルダに振り分ける形式」となっていました。<br>しかし、この構成で本番展開しようとすると、SlackのWebhookやトリガー設定など各メンバーの初期設定の手間が大きすぎることが判明しました。<br>そこで上長と相談し、横展開のしやすさを最優先して「各自が投げるGASと、管理者が仕分けるGASの2つに分ける」という方針に変更しました。</p><blockquote><p><strong>💬 Geminiへの指示のイメージ</strong><br>「このツールをチームに横展開することを考えて、各自が投げるGASと管理者が仕分けるGASの2つにコードを分割して」</p></blockquote><p>このように、<strong>ざっくりとした要件でまず動くものを作り、必要な観点を後からぶつけていく</strong>というプロセスを繰り返すことで、無事に実運用できるツールを完成させることができました。</p><h2 id="本開発における学び"><a href="#本開発における学び" class="headerlink" title="本開発における学び"></a>本開発における学び</h2><p>ここからは今回のAIとの共同開発における学びを共有できればと思います。</p><h3 id="Geminiってすごい"><a href="#Geminiってすごい" class="headerlink" title="Geminiってすごい"></a>Geminiってすごい</h3><p>まずは、IT未経験で研修を受けた程度の自分が、今回の開発を業務と並行して短期間で完遂できたのは間違いなくGeminiのおかげです。<br>今回はGASということもあり、ほとんどエラーのない正確なコードを提供してくれました。<br>大感謝です。</p><h3 id="アジャイルな進め方がAI時代のスタンダード？"><a href="#アジャイルな進め方がAI時代のスタンダード？" class="headerlink" title="アジャイルな進め方がAI時代のスタンダード？"></a>アジャイルな進め方がAI時代のスタンダード？</h3><p>今回のように未経験者がAIと協働開発をするには、最初から完璧な設計をするのではなく、<strong>とりあえずAIに作ってもらい、動くものを見ながらツッコミを入れて改善していくべきである</strong>と感じました（というかそれしか方法がない)。</p><p>さらに今後、AIとの共同開発が当たり前になる世界の中では、どんな難易度の開発であっても、コーディングにかかる時間が激減するため、こういったアジャイルな進め方が主流になっていくのではないかと思っています。</p><p>そう考えてみると、今回の進め方はまさに<strong>AIとの共同開発におけるスタンダード</strong>だったのではと個人的には感じています。</p><h3 id="人間がやるべきこと"><a href="#人間がやるべきこと" class="headerlink" title="人間がやるべきこと"></a>人間がやるべきこと</h3><p>AIとの共同開発が当たり前になる世界のなかで、人間がやるべきこととして、主に下記2点が重要だと感じています。</p><ul><li><strong>なぜその機能が必要なのかを考えること</strong><br>Geminiは指示された通りのコードを作るのは得意な一方で、やはり背景を察したりすることはできません。<br>そのため、「この機能作って」というプロンプトではなく、「現状こんな課題があって、それを解決するためにこんな機能を作りたい」というプロンプトにする方が圧倒的に求めているものに近いアウトプットができます。<br>すなわち、<strong>実現したいことの背景や目的、ゴールを整理して示すこと</strong>がAIとの共同開発においては重要だと学びました。</li><li><strong>コードを理解し観点を与えること</strong><br>前述の通り、Geminiは最低限のコードは生成してくれますが、不足している観点があることも多いです（自分の知識不足も原因ではありますが）。<br>そこで人間に求められるのは、<strong>コードの内容を正しく理解しレビューをする力</strong>だと思っています。<br>実際にその機能を運用することを想定して、多くの観点からレビューをすることで、AIとの共同開発でも品質の高いアウトプットが出せることを学びました。</li></ul><div class="note-container info"><span class="fa-check-circle"></span><div><p><strong>🎯 学びの総括</strong></p><p>今後、AIの書くコードの正確性は高まっていく中で人間に求められる力としては、コードを理解する知識はもちろん、<strong>その機能を作る背景や目的・ゴールを正しく理解し、必要な観点を洗い出せる力</strong>なのではと感じています。</p></div></div><h2 id="終わりに"><a href="#終わりに" class="headerlink" title="終わりに"></a>終わりに</h2><p>今回は文字起こし自動追加GASの概要と、AIとの共同開発での学びについて共有させていただきました。</p><p>PJ効率化ツールとして文字起こし自動追加GASに興味を持っていただけていたら、非常にうれしいです！<br>また、自分と同じ若手メンバーなど、IT初学者の方にとってAIとの共同開発の進め方が何かの役に立てば幸いです。</p><p>最後まで読んでいただきありがとうございました！</p><hr><details><summary><b>おまけ：今回作成したGASのソースコード（マスキング済）</b></summary><p>ご参考までに、実際にAIと作成して運用しているソースコード（※機密情報のみマスキング済）を掲載します。</p><figure class="highlight js"><figcaption><span>GAS①：マイドライブから一時プールへ移動する処理（各自実行）</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * ■■■ 文字起こし・録画ファイル選別＆投げ込みボット（メンバー用） ■■■</span></span><br><span class="line"><span class="comment"> * */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CONFIG</span> = &#123;</span><br><span class="line">  <span class="comment">// 1. 検索元フォルダ名</span></span><br><span class="line">  <span class="attr">sourceFolderName</span>: <span class="string">&#x27;Meet Recordings&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 移動先プールID（プロパティから取得）</span></span><br><span class="line">  <span class="attr">poolFolderId</span>: <span class="title class_">PropertiesService</span>.<span class="title function_">getScriptProperties</span>().<span class="title function_">getProperty</span>(<span class="string">&#x27;POOL_FOLDER_ID&#x27;</span>),</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 移動対象キーワード（正規表現）</span></span><br><span class="line">  <span class="attr">targetKeywords</span>: [</span><br><span class="line">    <span class="string">&#x27;^\\d&#123;8&#125;_【顧客名A】&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;^\\d&#123;8&#125;_【顧客名B】&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;^\\d&#123;8&#125;_【内部プロジェクトA】&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;^\\d&#123;8&#125;_【内部プロジェクトB】&#x27;</span></span><br><span class="line">  ],</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 4. 対象ファイル形式</span></span><br><span class="line">  <span class="attr">targetMimeTypes</span>: [<span class="title class_">MimeType</span>.<span class="property">GOOGLE_DOCS</span>, <span class="string">&#x27;video/mp4&#x27;</span>, <span class="title class_">MimeType</span>.<span class="property">PLAIN_TEXT</span>]</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">moveToPoolWithFilter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!<span class="variable constant_">CONFIG</span>.<span class="property">poolFolderId</span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;❌ 設定エラー: POOL_FOLDER_ID が設定されていません。&#x27;</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> props = <span class="title class_">PropertiesService</span>.<span class="title function_">getScriptProperties</span>();</span><br><span class="line">  <span class="keyword">const</span> lastRunStr = props.<span class="title function_">getProperty</span>(<span class="string">&#x27;LAST_RUN_TIME&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// デフォルト検索開始日（過去すべて）</span></span><br><span class="line">  <span class="keyword">let</span> searchDateStr = <span class="string">&#x27;2000-01-01T00:00:00&#x27;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (lastRunStr) &#123;</span><br><span class="line">    <span class="keyword">const</span> lastDate = <span class="keyword">new</span> <span class="title class_">Date</span>(lastRunStr);</span><br><span class="line">    <span class="keyword">if</span> (!<span class="built_in">isNaN</span>(lastDate.<span class="title function_">getTime</span>())) &#123;</span><br><span class="line">      <span class="comment">// タイムラグ対策（1分前から）</span></span><br><span class="line">      lastDate.<span class="title function_">setMinutes</span>(lastDate.<span class="title function_">getMinutes</span>() - <span class="number">1</span>);</span><br><span class="line">      searchDateStr = <span class="title class_">Utilities</span>.<span class="title function_">formatDate</span>(lastDate, <span class="string">&#x27;GMT&#x27;</span>, <span class="string">&quot;yyyy-MM-dd&#x27;T&#x27;HH:mm:ss&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> query = <span class="string">`modifiedDate &gt; &#x27;<span class="subst">$&#123;searchDateStr&#125;</span>&#x27;`</span>;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🔎 高速検索クエリ: <span class="subst">$&#123;query&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> sourceFolders = <span class="title class_">DriveApp</span>.<span class="title function_">getFoldersByName</span>(<span class="variable constant_">CONFIG</span>.<span class="property">sourceFolderName</span>);</span><br><span class="line">  <span class="keyword">const</span> poolFolder = <span class="title class_">DriveApp</span>.<span class="title function_">getFolderById</span>(<span class="variable constant_">CONFIG</span>.<span class="property">poolFolderId</span>);</span><br><span class="line">  <span class="keyword">const</span> regexPatterns = <span class="variable constant_">CONFIG</span>.<span class="property">targetKeywords</span>.<span class="title function_">map</span>(<span class="function"><span class="params">k</span> =&gt;</span> <span class="keyword">new</span> <span class="title class_">RegExp</span>(k));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> movedCount = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span> (sourceFolders.<span class="title function_">hasNext</span>()) &#123;</span><br><span class="line">    <span class="keyword">const</span> folder = sourceFolders.<span class="title function_">next</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> files = folder.<span class="title function_">searchFiles</span>(query);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">while</span> (files.<span class="title function_">hasNext</span>()) &#123;</span><br><span class="line">        <span class="keyword">const</span> file = files.<span class="title function_">next</span>();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (file.<span class="title function_">isTrashed</span>()) <span class="keyword">continue</span>;</span><br><span class="line">        <span class="keyword">if</span> (!<span class="variable constant_">CONFIG</span>.<span class="property">targetMimeTypes</span>.<span class="title function_">includes</span>(file.<span class="title function_">getMimeType</span>())) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">          <span class="keyword">const</span> isMoved = <span class="title function_">processFile</span>(file, poolFolder, regexPatterns);</span><br><span class="line">          <span class="keyword">if</span> (isMoved) &#123;</span><br><span class="line">            movedCount++;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">          <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`❌ ファイル処理エラー: <span class="subst">$&#123;file.getName()&#125;</span> (<span class="subst">$&#123;e.message&#125;</span>)`</span>);</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`❌ 検索エラー(query: <span class="subst">$&#123;query&#125;</span>): <span class="subst">$&#123;e.message&#125;</span>`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  props.<span class="title function_">setProperty</span>(<span class="string">&#x27;LAST_RUN_TIME&#x27;</span>, now.<span class="title function_">toISOString</span>());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (movedCount &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`✅ 処理完了: <span class="subst">$&#123;movedCount&#125;</span>件のファイルを転送しました。`</span>);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`✅ 新規転送なし（<span class="subst">$&#123;searchDateStr&#125;</span> 以降）`</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">processFile</span>(<span class="params">file, poolFolder, regexPatterns</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> originalName = file.<span class="title function_">getName</span>();</span><br><span class="line">  <span class="keyword">let</span> datePrefix = <span class="string">&#x27;&#x27;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> dateMatch = originalName.<span class="title function_">match</span>(<span class="regexp">/(\d&#123;4&#125;)[\/\-\s](0[1-9]|1[0-2])[\/\-\s](0[1-9]|[12]\d|3[01])/</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (dateMatch) &#123;</span><br><span class="line">    datePrefix = <span class="string">`<span class="subst">$&#123;dateMatch[<span class="number">1</span>]&#125;</span><span class="subst">$&#123;dateMatch[<span class="number">2</span>]&#125;</span><span class="subst">$&#123;dateMatch[<span class="number">3</span>]&#125;</span>`</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> createdDate = file.<span class="title function_">getDateCreated</span>();</span><br><span class="line">    datePrefix = <span class="title class_">Utilities</span>.<span class="title function_">formatDate</span>(createdDate, <span class="string">&#x27;Asia/Tokyo&#x27;</span>, <span class="string">&#x27;yyyyMMdd&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> newName = originalName;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!originalName.<span class="title function_">startsWith</span>(datePrefix)) &#123;</span><br><span class="line">    newName = <span class="string">`<span class="subst">$&#123;datePrefix&#125;</span>_<span class="subst">$&#123;originalName&#125;</span>`</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> isMatch = regexPatterns.<span class="title function_">some</span>(<span class="function"><span class="params">regex</span> =&gt;</span> regex.<span class="title function_">test</span>(newName));</span><br><span class="line">  <span class="keyword">if</span> (!isMatch) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (file.<span class="title function_">getName</span>() !== newName) &#123;</span><br><span class="line">    file.<span class="title function_">setName</span>(newName);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (poolFolder.<span class="title function_">getFilesByName</span>(newName).<span class="title function_">hasNext</span>()) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`⚠️ [スキップ] プールに同名ファイルが存在: <span class="subst">$&#123;newName&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  file.<span class="title function_">moveTo</span>(poolFolder);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`⭕️ [転送成功] <span class="subst">$&#123;newName&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight js"><figcaption><span>GAS②：プールから内部/外部フォルダへ仕分け＆Slack通知（管理者実行）</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * ==============================================================================</span></span><br><span class="line"><span class="comment"> * ■■■ 議事録・仕分け＆通知マスターボット（管理者用） ■■■</span></span><br><span class="line"><span class="comment"> * ==============================================================================</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CONFIG</span> = &#123;</span><br><span class="line">  <span class="attr">poolFolderId</span>:     <span class="title class_">PropertiesService</span>.<span class="title function_">getScriptProperties</span>().<span class="title function_">getProperty</span>(<span class="string">&#x27;POOL_FOLDER_ID&#x27;</span>),</span><br><span class="line">  <span class="attr">internalFolderId</span>: <span class="title class_">PropertiesService</span>.<span class="title function_">getScriptProperties</span>().<span class="title function_">getProperty</span>(<span class="string">&#x27;INTERNAL_FOLDER_ID&#x27;</span>),</span><br><span class="line">  <span class="attr">externalFolderId</span>: <span class="title class_">PropertiesService</span>.<span class="title function_">getScriptProperties</span>().<span class="title function_">getProperty</span>(<span class="string">&#x27;EXTERNAL_FOLDER_ID&#x27;</span>),</span><br><span class="line">  <span class="attr">slackWebhookUrl</span>:  <span class="title class_">PropertiesService</span>.<span class="title function_">getScriptProperties</span>().<span class="title function_">getProperty</span>(<span class="string">&#x27;SLACK_WEBHOOK_URL&#x27;</span>),</span><br><span class="line"></span><br><span class="line">  <span class="attr">rules</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;外部&#x27;</span>,</span><br><span class="line">      <span class="attr">propKey</span>: <span class="string">&#x27;EXTERNAL_FOLDER_ID&#x27;</span>,</span><br><span class="line">      <span class="attr">keywords</span>: [<span class="string">&#x27;^\\d&#123;8&#125;_【顧客名A】&#x27;</span>,<span class="string">&#x27;^\\d&#123;8&#125;_【顧客名B】&#x27;</span>]</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;内部&#x27;</span>,</span><br><span class="line">      <span class="attr">propKey</span>: <span class="string">&#x27;INTERNAL_FOLDER_ID&#x27;</span>,</span><br><span class="line">      <span class="attr">keywords</span>: [<span class="string">&#x27;^\\d&#123;8&#125;_【内部プロジェクトA】&#x27;</span>,<span class="string">&#x27;^\\d&#123;8&#125;_【内部プロジェクトB】&#x27;</span>]</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">runBatchSort</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="title class_">Logger</span>.<span class="title function_">log</span>(<span class="string">&#x27;--- 管理者バッチ処理開始 ---&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!<span class="variable constant_">CONFIG</span>.<span class="property">poolFolderId</span> || !<span class="variable constant_">CONFIG</span>.<span class="property">internalFolderId</span> || !<span class="variable constant_">CONFIG</span>.<span class="property">externalFolderId</span> || !<span class="variable constant_">CONFIG</span>.<span class="property">slackWebhookUrl</span>) &#123;</span><br><span class="line">    <span class="title class_">Logger</span>.<span class="title function_">log</span>(<span class="string">&#x27;❌ エラー: スクリプトプロパティが不足しています。&#x27;</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> slackLogs = [];</span><br><span class="line">  <span class="keyword">const</span> poolFolder = <span class="title class_">DriveApp</span>.<span class="title function_">getFolderById</span>(<span class="variable constant_">CONFIG</span>.<span class="property">poolFolderId</span>);</span><br><span class="line">  <span class="keyword">const</span> files = poolFolder.<span class="title function_">getFiles</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> moveCount = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">let</span> remainCount = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">let</span> errorCount = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> rules = <span class="variable constant_">CONFIG</span>.<span class="property">rules</span>.<span class="title function_">map</span>(<span class="function"><span class="params">r</span> =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">folder</span>: <span class="title class_">DriveApp</span>.<span class="title function_">getFolderById</span>(<span class="title class_">PropertiesService</span>.<span class="title function_">getScriptProperties</span>().<span class="title function_">getProperty</span>(r.<span class="property">propKey</span>)),</span><br><span class="line">    <span class="attr">regex</span>: r.<span class="property">keywords</span>.<span class="title function_">map</span>(<span class="function"><span class="params">k</span> =&gt;</span> <span class="keyword">new</span> <span class="title class_">RegExp</span>(k)),</span><br><span class="line">    <span class="attr">name</span>: r.<span class="property">name</span></span><br><span class="line">  &#125;));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span> (files.<span class="title function_">hasNext</span>()) &#123;</span><br><span class="line">    <span class="keyword">const</span> file = files.<span class="title function_">next</span>();</span><br><span class="line">    <span class="keyword">const</span> fileName = file.<span class="title function_">getName</span>();</span><br><span class="line">    <span class="keyword">let</span> isMoved = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">const</span> rule <span class="keyword">of</span> rules) &#123;</span><br><span class="line">        <span class="keyword">if</span> (rule.<span class="property">regex</span>.<span class="title function_">some</span>(<span class="function"><span class="params">r</span> =&gt;</span> r.<span class="title function_">test</span>(fileName))) &#123;</span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> (rule.<span class="property">folder</span>.<span class="title function_">getFilesByName</span>(fileName).<span class="title function_">hasNext</span>()) &#123;</span><br><span class="line">            <span class="keyword">const</span> msg = <span class="string">`⚠️ [重複] <span class="subst">$&#123;fileName&#125;</span> は <span class="subst">$&#123;rule.name&#125;</span> に既に存在するためスキップしました`</span>;</span><br><span class="line">            <span class="title class_">Logger</span>.<span class="title function_">log</span>(msg);</span><br><span class="line">            slackLogs.<span class="title function_">push</span>(msg);</span><br><span class="line">            isMoved = <span class="literal">true</span>;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">          &#125;</span><br><span class="line"></span><br><span class="line">          file.<span class="title function_">moveTo</span>(rule.<span class="property">folder</span>);</span><br><span class="line"></span><br><span class="line">          <span class="keyword">const</span> logMsg = <span class="string">`✅ [移動] <span class="subst">$&#123;fileName&#125;</span> ➡ <span class="subst">$&#123;rule.name&#125;</span>`</span>;</span><br><span class="line">          <span class="title class_">Logger</span>.<span class="title function_">log</span>(logMsg);</span><br><span class="line">          slackLogs.<span class="title function_">push</span>(logMsg);</span><br><span class="line"></span><br><span class="line">          moveCount++;</span><br><span class="line">          isMoved = <span class="literal">true</span>;</span><br><span class="line">          <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!isMoved) &#123;</span><br><span class="line">         <span class="title class_">Logger</span>.<span class="title function_">log</span>(<span class="string">`⚠️ [残留] <span class="subst">$&#123;fileName&#125;</span> (条件不一致)`</span>);</span><br><span class="line">         remainCount++;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="keyword">const</span> errorMsg = <span class="string">`❌ [エラー] <span class="subst">$&#123;fileName&#125;</span>: <span class="subst">$&#123;e.message&#125;</span>`</span>;</span><br><span class="line">      <span class="title class_">Logger</span>.<span class="title function_">log</span>(errorMsg);</span><br><span class="line">      slackLogs.<span class="title function_">push</span>(errorMsg);</span><br><span class="line">      errorCount++;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title class_">Logger</span>.<span class="title function_">log</span>(<span class="string">`処理終了: 移動<span class="subst">$&#123;moveCount&#125;</span>件 / 残留<span class="subst">$&#123;remainCount&#125;</span>件 / エラー<span class="subst">$&#123;errorCount&#125;</span>件`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (slackLogs.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="title function_">sendDailyReport</span>(slackLogs, moveCount, remainCount);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="title class_">Logger</span>.<span class="title function_">log</span>(<span class="string">&#x27;通知対象なしのためSlack送信をスキップします。&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">sendDailyReport</span>(<span class="params">logs, moveCount, remainCount</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> message = &#123;</span><br><span class="line">    <span class="string">&quot;text&quot;</span>: <span class="string">&quot;📊 *議事録 自動仕分けレポート*&quot;</span>,</span><br><span class="line">    <span class="string">&quot;blocks&quot;</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;section&quot;</span>,</span><br><span class="line">        <span class="string">&quot;text&quot;</span>: &#123;</span><br><span class="line">          <span class="string">&quot;type&quot;</span>: <span class="string">&quot;mrkdwn&quot;</span>,</span><br><span class="line">          <span class="string">&quot;text&quot;</span>: <span class="string">`📊 *本日の仕分け結果*\n実行完了: <span class="subst">$&#123;moveCount&#125;</span>件 / プール残留: <span class="subst">$&#123;remainCount&#125;</span>件`</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123; <span class="string">&quot;type&quot;</span>: <span class="string">&quot;divider&quot;</span> &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;section&quot;</span>,</span><br><span class="line">        <span class="string">&quot;text&quot;</span>: &#123;</span><br><span class="line">          <span class="string">&quot;type&quot;</span>: <span class="string">&quot;mrkdwn&quot;</span>,</span><br><span class="line">          <span class="string">&quot;text&quot;</span>: logs.<span class="title function_">join</span>(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="title class_">UrlFetchApp</span>.<span class="title function_">fetch</span>(<span class="variable constant_">CONFIG</span>.<span class="property">slackWebhookUrl</span>, &#123;</span><br><span class="line">      <span class="string">&quot;method&quot;</span>: <span class="string">&quot;post&quot;</span>,</span><br><span class="line">      <span class="string">&quot;contentType&quot;</span>: <span class="string">&quot;application/json&quot;</span>,</span><br><span class="line">      <span class="string">&quot;payload&quot;</span>: <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(message)</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">    <span class="title class_">Logger</span>.<span class="title function_">log</span>(<span class="string">&#x27;Slack送信エラー: &#x27;</span> + e.<span class="property">message</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></details>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="Programming" scheme="https://future-architect.github.io/categories/Programming/"/>
    
    
    <category term="初心者向け" scheme="https://future-architect.github.io/tags/%E5%88%9D%E5%BF%83%E8%80%85%E5%90%91%E3%81%91/"/>
    
    <category term="GAS" scheme="https://future-architect.github.io/tags/GAS/"/>
    
  </entry>
  
  <entry>
    <title>Spring Boot マルチDataSource構成で secondary DB だけに Flyway を適用する</title>
    <link href="https://future-architect.github.io/articles/20260410a/"/>
    <id>https://future-architect.github.io/articles/20260410a/</id>
    <published>2026-04-09T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<h2 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h2><h3 id="Flyway-とは"><a href="#Flyway-とは" class="headerlink" title="Flyway とは"></a>Flyway とは</h3><p><a href="https://flywaydb.org/">Flyway</a> はデータベースのスキーマ変更をバージョン管理するマイグレーションツールです。<code>V1__create_table.sql</code>、<code>V2__add_column.sql</code> のようにバージョン番号付きの SQL ファイルを用意しておくと、アプリケーション起動時に未適用のマイグレーションを自動的に検出・実行してくれます。Spring Boot では <code>spring-boot-starter</code> に Flyway が統合されており、依存関係を追加するだけで自動構成が有効になります。</p><h3 id="本記事の背景"><a href="#本記事の背景" class="headerlink" title="本記事の背景"></a>本記事の背景</h3><p>私たちのプロジェクトでは長らく Flyway なしで運用してきました。テーブル追加やカラム変更は手動で DDL を流していたのですが、環境が増えてきて正直面倒になってきたので、重い腰を上げて導入することにしました。</p><p>ところが、いざ導入してみるとすんなりとはいきませんでした。というのも、このプロジェクトはテナント用と管理用の2つの DataSource を持つ構成で、Flyway を適用したいのは管理 DB だけ。Spring Boot の Flyway 自動構成は primary DataSource にしか紐づかないため、手動で Flyway Bean を構成する必要がありました。さらに、既存のテーブルがある状態への後付け導入でいくつかハマりポイントがあったので、本記事ではその辺りの知見を共有します。</p><ul><li>secondary DataSource に手動で Flyway を構成する方法</li><li><code>baselineOnMigrate(true)</code> の正しい理解と V1 に書くべき内容</li><li><code>@ConditionalOnProperty</code> を使った環境別の有効&#x2F;無効切り替え</li></ul><h3 id="なぜ-Flyway-を選んだか-—-Liquibase-との比較"><a href="#なぜ-Flyway-を選んだか-—-Liquibase-との比較" class="headerlink" title="なぜ Flyway を選んだか — Liquibase との比較"></a>なぜ Flyway を選んだか — Liquibase との比較</h3><p>DB マイグレーションツールとしては <a href="https://www.liquibase.com/">Liquibase</a> も候補に挙がりました。参考までに両者の比較を載せておきます。</p><div class="scroll"><table><thead><tr><th align="left">比較軸</th><th align="left"><a href="https://flywaydb.org/">Flyway</a></th><th align="left"><a href="https://www.liquibase.com/">Liquibase</a></th></tr></thead><tbody><tr><td align="left">変更定義の形式</td><td align="left">SQL のみ</td><td align="left">XML &#x2F; YAML &#x2F; JSON &#x2F; SQL</td></tr><tr><td align="left">DB 製品差異の吸収</td><td align="left">自前で location 分割が必要</td><td align="left">宣言的な定義なら Liquibase が吸収</td></tr><tr><td align="left">ロールバック</td><td align="left">非サポート（手動で逆 SQL を書く）</td><td align="left">DSL 定義なら約8割を自動生成、SQL 定義は手動</td></tr><tr><td align="left">学習コスト</td><td align="left">SQL が書ければすぐ使える</td><td align="left">DSL（XML&#x2F;YAML）の習得が必要</td></tr><tr><td align="left">Spring Boot 統合</td><td align="left"><code>flyway-core</code> を追加するだけ</td><td align="left"><a href="https://docs.liquibase.com/tools-integrations/springboot/springboot.html"><code>liquibase-core</code> を追加するだけ</a></td></tr><tr><td align="left">適したシステム</td><td align="left">DB 固有の SQL を多用する &#x2F; 複数 RDB 製品をサポートする</td><td align="left">単一 RDB で完結する &#x2F; ロールバックを自動化したい</td></tr></tbody></table></div><p>Liquibase は宣言的な定義で RDB の差異を吸収してくれる点が魅力です。単一の RDB 製品で完結するシステムや、ロールバックの自動化が必要なケースでは有力な選択肢になるでしょう。</p><p>一方、私たちのプロジェクトでは Oracle と PostgreSQL の両方をサポートしており、DB 固有の構文（<code>CLOB</code> &#x2F; <code>TEXT</code> の違いや PL&#x2F;SQL 等）を多用していました。宣言的な定義だけでは対応しきれない場面が多いと判断し、素の SQL をそのまま書ける Flyway を採用しました。Flyway はロールバックをサポートしていないため、マイグレーションに失敗した場合は手動で逆の DDL を流す運用になりますが、管理 DB のスキーマ変更頻度はそこまで高くないのでこれで十分と割り切りました。</p><h2 id="システム構成"><a href="#システム構成" class="headerlink" title="システム構成"></a>システム構成</h2><p>今回のシステムは以下の2つの DataSource を持ちます。<br>今考えると、primaryとsecondaryは逆にすべきでした・・・反省。</p><pre class="mermaid">graph LR    App[Spring Boot アプリケーション]    App -->|primary| TenantDB[(テナントDB<br/>データ)]    App -->|secondary| ManagerDB[(管理DB<br/>設定・マスタ管理)]    style TenantDB fill:#2e7d32,color:#fff    style ManagerDB fill:#1565c0,color:#fff</pre><div class="scroll"><table><thead><tr><th align="left">DataSource</th><th align="left">役割</th><th align="left">Flyway</th></tr></thead><tbody><tr><td align="left">primary（テナントDB）</td><td align="left">テナントごとのデータを格納。環境ごとに接続先が変わる</td><td align="left"><strong>不要</strong></td></tr><tr><td align="left">secondary（管理DB）</td><td align="left">アプリの設定やマスタ情報を管理。全環境で共通のスキーマ</td><td align="left"><strong>必要</strong></td></tr></tbody></table></div><p>Spring Boot では <code>spring.datasource.*</code> プロパティで定義された DataSource が primary DataSource として扱われます。Flyway の自動構成（<code>FlywayAutoConfiguration</code>）はこの primary DataSource に紐づいて動作するため、<code>spring.datasource.*</code> 以外で定義した secondary DataSource にはマイグレーションが適用されません。</p><p>やりたいことを整理すると以下の通りです。</p><div class="scroll"><table><thead><tr><th align="left">環境</th><th align="left">primary（テナントDB）</th><th align="left">secondary（管理DB）</th></tr></thead><tbody><tr><td align="left">dev</td><td align="left">Flyway 無効</td><td align="left">Flyway <strong>無効</strong>（開発環境なのでマイグレーション用のDDLを手動確認・試行錯誤）</td></tr><tr><td align="left">staging &#x2F; prod</td><td align="left">Flyway 無効</td><td align="left">Flyway <strong>有効</strong>（自動マイグレーション）</td></tr></tbody></table></div><h2 id="実装-secondary-DataSource-に手動で-Flyway-Bean-を構成する"><a href="#実装-secondary-DataSource-に手動で-Flyway-Bean-を構成する" class="headerlink" title="実装: secondary DataSource に手動で Flyway Bean を構成する"></a>実装: secondary DataSource に手動で Flyway Bean を構成する</h2><h3 id="yml-設定"><a href="#yml-設定" class="headerlink" title="yml 設定"></a>yml 設定</h3><p>まず <code>application.yml</code>（共通設定）で Spring Boot の自動構成 Flyway を無効化します。</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="comment"># Spring Boot自動構成の Flyway（= primary DataSource に適用）の有効/無効。</span></span><br><span class="line">  <span class="comment"># primary DataSource はテナント用でマイグレーション不要のため無効化。</span></span><br><span class="line">  <span class="comment"># 管理DB（secondary）へのマイグレーションはこの設定とは独立して</span></span><br><span class="line">  <span class="comment"># Java 側で手動構成・実行される。</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">false</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/oracle</span></span><br></pre></td></tr></table></figure><p>ポイントは2つです。</p><ol><li><code>enabled: false</code> で自動構成 Flyway を無効化（primary DataSource にはマイグレーション不要）</li><li><code>locations</code> は設定しておく（後述の手動構成 Bean で <code>@Value</code> 経由で再利用）</li></ol><h3 id="Java-実装"><a href="#Java-実装" class="headerlink" title="Java 実装"></a>Java 実装</h3><p>secondary DataSource の設定クラスに Flyway Bean を手動で定義します。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;app.manager.datasource&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ManagerSqlConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;spring.flyway.locations:classpath:db/migration/common&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String[] flywayLocations;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerDataSource&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> DataSource <span class="title function_">managerDataSource</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// secondary DataSource の構成（省略）</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 管理DB用の Flyway マイグレーションを実行する.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * Spring Boot の Flyway 自動構成は primary DataSource に紐づくため、</span></span><br><span class="line"><span class="comment">     * secondary DataSource には手動で Flyway を構成する必要がある.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(</span></span><br><span class="line"><span class="params">            <span class="meta">@Qualifier(&quot;managerDataSource&quot;)</span> DataSource managerDataSource)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Flyway.configure()</span><br><span class="line">                .dataSource(managerDataSource)</span><br><span class="line">                .locations(flywayLocations)</span><br><span class="line">                .baselineOnMigrate(<span class="literal">true</span>)</span><br><span class="line">                .load();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>@Bean(initMethod = &quot;migrate&quot;)</code> により、Bean 生成時に自動的に <code>Flyway#migrate()</code> が呼ばれます。</p><p><code>spring.flyway.locations</code> を <code>@Value</code> で参照することで、yml 側でマイグレーションファイルのパスを管理でき、Oracle &#x2F; PostgreSQL などの Dialect 切り替えにも対応できます。</p><h2 id="落とし穴-baselineOnMigrate-と-V1-マイグレーションの関係"><a href="#落とし穴-baselineOnMigrate-と-V1-マイグレーションの関係" class="headerlink" title="落とし穴: baselineOnMigrate と V1 マイグレーションの関係"></a>落とし穴: baselineOnMigrate と V1 マイグレーションの関係</h2><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>上記の実装でデプロイしたところ、<strong>V1 マイグレーションが実行されない</strong>という問題が発生しました。</p><p>マイグレーションファイル（<code>V1__add_column.sql</code>）に差分DDL（ALTER TABLE）を書いていたにもかかわらず、管理DB にカラムが追加されていませんでした。<code>flyway_schema_history</code> テーブルを確認すると、V1 に対して <code>TYPE=BASELINE</code> のレコードが記録されており、実際のSQLは実行されていませんでした。</p><h3 id="baselineOnMigrate-とは"><a href="#baselineOnMigrate-とは" class="headerlink" title="baselineOnMigrate とは"></a>baselineOnMigrate とは</h3><p><code>baselineOnMigrate(true)</code> は <strong>既にテーブルが存在するスキーマに対して、途中から Flyway を導入する</strong>ための設定です。</p><p>Flyway は通常、空のスキーマに対して V1 から順にマイグレーションを適用することを前提としています。しかし既にテーブルが存在するスキーマに Flyway を導入する場合、Flyway は「このスキーマは管理下にない」としてエラーを出します。<code>baselineOnMigrate(true)</code> を指定すると、Flyway は既存スキーマに対して「ここまでは適用済み」というベースラインを自動的に作成し、それ以降のバージョンからマイグレーションを開始します。</p><h3 id="baselineVersion-のデフォルトは-“1”"><a href="#baselineVersion-のデフォルトは-“1”" class="headerlink" title="baselineVersion のデフォルトは “1”"></a>baselineVersion のデフォルトは “1”</h3><p>ベースラインを「どのバージョンで作成するか」を決めるのが <code>baselineVersion</code> です。<strong>デフォルト値は <code>&quot;1&quot;</code> です。</strong></p><p>つまり <code>baselineOnMigrate(true)</code> を指定すると:</p><ul><li><code>flyway_schema_history</code> テーブルが自動作成される</li><li><strong>V1 &#x3D; ベースライン（＝適用済み扱い）</strong> として記録される</li><li>実際に実行されるのは <strong>V2 以降のみ</strong></li></ul><p>私たちのような既存スキーマに途中からflywayを導入する場合はこんな流れになります。</p><ol><li>既存スキーマ(初期状態)<ul><li>flyway_schema_historyテーブルなし</li></ul></li><li>flywayがbaselineOnMigrate&#x3D;true で初回実行<ul><li>flyway_schema_historyテーブル作成</li><li>V1をベースラインとして記録<ul><li><code>TYPE=BASELINE</code> のレコードが作成されるのみ</li><li>この段階ではマイグレーションSQLは実行されない</li></ul></li></ul></li><li>flywayがV2以降のマイグレーションを実行</li></ol><p>実際に <code>flyway_schema_history</code> を見ると、V1 が <code>TYPE=BASELINE</code> として記録されていることが確認できます。<code>execution_time</code> は 0、つまり SQL は実行されていません。</p><img src="/images/2026/20260410a/flyway_schema_history_の_BASELINE_レコード.png" alt="flyway_schema_history の BASELINE レコード" width="1200" height="321" loading="lazy"><h3 id="V1-に何を書くべきか"><a href="#V1-に何を書くべきか" class="headerlink" title="V1 に何を書くべきか"></a>V1 に何を書くべきか</h3><p>Flyway の <code>baselineOnMigrate</code> は <strong>「V1 の内容は既にスキーマに反映済みである」</strong> という前提で動作します。したがって、V1 には<strong>現在のスキーマ状態を再現する全DDL</strong>（CREATE TABLE 等）を記述するのが正しい設計です。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">db/migration/common/</span><br><span class="line">├── V1__initial_schema.sql       ← 既存スキーマの全DDL（既存環境ではスキップされる）</span><br><span class="line">├── V2__add_is_default.sql       ← ここから実際の差分マイグレーション</span><br><span class="line">├── V3__add_index.sql</span><br><span class="line">└── ...</span><br></pre></td></tr></table></figure><p>V1 に全DDLを書いておくことで、<strong>環境によって2つの動作</strong>が実現できます。</p><div class="scroll"><table><thead><tr><th align="left">環境</th><th align="left">baselineOnMigrate</th><th align="left">V1 の扱い</th><th align="left">V2 以降</th></tr></thead><tbody><tr><td align="left"><strong>既存環境</strong>（テーブルあり）</td><td align="left"><code>true</code></td><td align="left">ベースラインとしてスキップ</td><td align="left">差分を自動適用</td></tr><tr><td align="left"><strong>新規環境</strong>（空スキーマ）</td><td align="left"><code>false</code>（デフォルト）</td><td align="left">全DDLとして実行</td><td align="left">差分を自動適用</td></tr></tbody></table></div><p>新規環境では <code>baselineOnMigrate</code> はデフォルトの <code>false</code> のままで、V1 の全DDLから順に実行されます。既存環境では <code>baselineOnMigrate(true)</code> により V1 がスキップされ、V2 から差分が適用されます。</p><h3 id="V1-に差分DDLを書いてしまった"><a href="#V1-に差分DDLを書いてしまった" class="headerlink" title="V1 に差分DDLを書いてしまった"></a>V1 に差分DDLを書いてしまった</h3><p>上記のことが理解できてなかったため、V1 に差分DDL（ALTER TABLE）を書いてしまいました・・・</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">V1__add_column.sql  ← ALTER TABLE（差分DDL）を記述 → ベースラインでスキップされた</span><br></pre></td></tr></table></figure><p>既存環境に途中から Flyway を導入する場合、V1 は「既にスキーマに存在する状態の記録」であり、差分マイグレーションは <strong>V2 から開始する</strong>のが正解です。</p><h3 id="修正後のマイグレーション構成"><a href="#修正後のマイグレーション構成" class="headerlink" title="修正後のマイグレーション構成"></a>修正後のマイグレーション構成</h3><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">db/migration/common/</span><br><span class="line">├── V1__initial_schema.sql       ← 全DDL（新規環境構築用に配置）</span><br><span class="line">└── V2__add_is_default.sql       ← 差分マイグレーション（V1 からリネーム）</span><br></pre></td></tr></table></figure><p>修正後にアプリを再起動すると、V1 は <code>TYPE=BASELINE</code> のまま、V2 が <code>TYPE=SQL</code> として実行されます。</p><img src="/images/2026/20260410a/V2_適用後の_flyway_schema_history.png" alt="V2 適用後の flyway_schema_history" width="1198" height="111" loading="lazy"><h3 id="もし-V1-に差分を書いてデプロイしてしまったら"><a href="#もし-V1-に差分を書いてデプロイしてしまったら" class="headerlink" title="もし V1 に差分を書いてデプロイしてしまったら"></a>もし V1 に差分を書いてデプロイしてしまったら</h3><p>既に <code>baselineOnMigrate(true)</code> で V1 が BASELINE として記録されてしまった場合でも、<code>flyway_schema_history</code> をリセットすることでやり直しが可能です。<br>私は誤解釈をしていたためやってしまいました・・・みなさんは気をつけてください。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- V1 で適用された差分DDLを手動で巻き戻す（例: カラム追加の場合は削除）</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> テーブル物理名 <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> カラム物理名;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- flyway_schema_history を初期化</span></span><br><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> &quot;flyway_schema_history&quot;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">COMMIT</span>;</span><br></pre></td></tr></table></figure><p>リセット後にアプリを再起動すれば、Flyway が BASELINE(V1) を再作成し、V2 から差分マイグレーションを実行します。</p><p>なお、<code>flyway_schema_history</code> は Flyway が小文字でテーブルを作成するため、もしOracleを利用してる場合は <strong>ダブルクォートで囲まないと <code>ORA-00942: table or view does not exist</code> になります</strong>。Flyway 内部では JdbcTemplate 経由で小文字固定のテーブル名が使われており、Oracle のデフォルトの大文字変換とは異なる名前で格納されています。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- NG: Oracle が大文字の FLYWAY_SCHEMA_HISTORY を探しに行く</span></span><br><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> flyway_schema_history;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- OK: 小文字のテーブル名を明示指定</span></span><br><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> &quot;flyway_schema_history&quot;;</span><br></pre></td></tr></table></figure><p>ただし、この手順は Flyway の管理情報を直接操作するため <strong>自己責任</strong> でお願いします。本番環境では十分に検証してから実施してください。</p><p>Java コードは <code>baselineVersion</code> の明示指定なし（デフォルト “1”）のままで正しく動作します。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="meta">@Qualifier(&quot;managerDataSource&quot;)</span> DataSource managerDataSource)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Flyway.configure()</span><br><span class="line">            .dataSource(managerDataSource)</span><br><span class="line">            .locations(flywayLocations)</span><br><span class="line">            .baselineOnMigrate(<span class="literal">true</span>)  <span class="comment">// 既存環境向け: V1 をベースラインとしてスキップ</span></span><br><span class="line">            .load();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="複数の-RDB-製品をサポートする場合の-tips"><a href="#複数の-RDB-製品をサポートする場合の-tips" class="headerlink" title="複数の RDB 製品をサポートする場合の tips"></a>複数の RDB 製品をサポートする場合の tips</h2><p>Oracle と PostgreSQL の両方をサポートしている場合、マイグレーション SQL の書き方に少し工夫が必要です。</p><h3 id="共通-SQL-で書ける範囲は意外と広い"><a href="#共通-SQL-で書ける範囲は意外と広い" class="headerlink" title="共通 SQL で書ける範囲は意外と広い"></a>共通 SQL で書ける範囲は意外と広い</h3><p>Oracle は <code>VARCHAR</code> で宣言すると内部的に <code>VARCHAR2</code> として扱い、<code>NUMERIC</code> は <code>NUMBER</code> に対応します。そのため、カラム定義を <code>VARCHAR</code> &#x2F; <code>NUMERIC</code> で統一しておけば、Oracle でも PostgreSQL でも同じ SQL が通ります。</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- Oracle/PostgreSQL 共通で動く</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> LLM <span class="keyword">ADD</span> IS_DEFAULT <span class="type">VARCHAR</span>(<span class="number">1</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> <span class="keyword">NOT NULL</span>;</span><br></pre></td></tr></table></figure><h3 id="共通化できない場合は-location-を分ける"><a href="#共通化できない場合は-location-を分ける" class="headerlink" title="共通化できない場合は location を分ける"></a>共通化できない場合は location を分ける</h3><p><code>CLOB</code>（Oracle）と <code>TEXT</code>（PostgreSQL）のように自動変換されない型や、DB 固有の構文（<code>CREATE SEQUENCE</code> のオプション、<code>COMMENT ON</code> の書き方の違い等）がある場合は、同じ SQL ファイルでは対応できません。</p><p>Flyway の <code>locations</code> 設定で共通と DB 固有のディレクトリを分けて管理します。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">db/migration/</span><br><span class="line">├── common/                         ... Oracle/PostgreSQL 共通</span><br><span class="line">│   ├── V2__add_is_default.sql</span><br><span class="line">│   └── V3__insert_default_llm.sql</span><br><span class="line">├── oracle/                         ... Oracle 固有</span><br><span class="line">│   └── V4__add_clob_column.sql</span><br><span class="line">└── postgresql/                     ... PostgreSQL 固有</span><br><span class="line">    └── V4__add_text_column.sql</span><br></pre></td></tr></table></figure><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># application.yml</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/oracle</span></span><br></pre></td></tr></table></figure><p>ポイントは、<strong>共通と固有の両方に同じバージョン番号を置かないこと</strong>です。DB 固有の構文が必要な場合は <code>common/</code> には置かず、<code>oracle/</code> と <code>postgresql/</code> の両方に同じバージョンの SQL をそれぞれの方言で配置します。共通の <code>V4</code> と Oracleもしくはpostgresql の <code>V4</code> の両方あると、Flyway が重複バージョンとしてエラーにします。</p><p>環境ごとの <code>locations</code> 切り替えは yml のプロファイル分割で対応します。</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># application-oracle.yml</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/oracle</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># application-postgresql.yml</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/postgresql</span></span><br></pre></td></tr></table></figure><h2 id="dev-環境だけ-Flyway-を無効にする"><a href="#dev-環境だけ-Flyway-を無効にする" class="headerlink" title="dev 環境だけ Flyway を無効にする"></a>dev 環境だけ Flyway を無効にする</h2><p>開発中はテーブル定義が確定するまで DDL を試行錯誤することが多く、Flyway が自動で走ると不便です。<code>@ConditionalOnProperty</code> を使って、<strong>Bean 登録自体を条件付き</strong>にします。</p><h3 id="ConditionalOnProperty-とは"><a href="#ConditionalOnProperty-とは" class="headerlink" title="@ConditionalOnProperty とは"></a>@ConditionalOnProperty とは</h3><p>Spring Boot のアノテーションで、<strong>プロパティの値に応じて Bean の登録自体をスキップ</strong>できます。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line"><span class="meta">@ConditionalOnProperty(</span></span><br><span class="line"><span class="meta">    name = &quot;app.manager.flyway.enabled&quot;,</span></span><br><span class="line"><span class="meta">    havingValue = &quot;true&quot;,</span></span><br><span class="line"><span class="meta">    matchIfMissing = true)</span></span><br><span class="line"><span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(...)</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>3つのパラメータの意味は以下の通りです。</p><div class="scroll"><table><thead><tr><th align="left">パラメータ</th><th align="left">値</th><th align="left">意味</th></tr></thead><tbody><tr><td align="left"><code>name</code></td><td align="left"><code>&quot;app.manager.flyway.enabled&quot;</code></td><td align="left">チェック対象のプロパティキー</td></tr><tr><td align="left"><code>havingValue</code></td><td align="left"><code>&quot;true&quot;</code></td><td align="left">プロパティがこの値のときだけ Bean を登録する</td></tr><tr><td align="left"><code>matchIfMissing</code></td><td align="left"><code>true</code></td><td align="left">プロパティが<strong>未定義</strong>の場合も Bean を登録する（&#x3D; デフォルト有効）</td></tr></tbody></table></div><p>この3つの組み合わせにより、以下の挙動になります。</p><div class="scroll"><table><thead><tr><th align="left">プロパティの状態</th><th align="left">Bean 登録</th><th align="left">理由</th></tr></thead><tbody><tr><td align="left">未定義</td><td align="left"><strong>登録される</strong></td><td align="left"><code>matchIfMissing = true</code> により未定義 &#x3D; 条件一致</td></tr><tr><td align="left"><code>true</code></td><td align="left"><strong>登録される</strong></td><td align="left"><code>havingValue = &quot;true&quot;</code> に一致</td></tr><tr><td align="left"><code>false</code></td><td align="left"><strong>登録されない</strong></td><td align="left"><code>havingValue = &quot;true&quot;</code> に不一致</td></tr></tbody></table></div><p><code>matchIfMissing = true</code> がポイントです。これにより、staging &#x2F; prod の yml にわざわざ <code>enabled: true</code> を書く必要がなく、<strong>dev だけ <code>false</code> を明示指定すればよい</strong>設計になります。</p><h3 id="Bean-単位で効く"><a href="#Bean-単位で効く" class="headerlink" title="Bean 単位で効く"></a>Bean 単位で効く</h3><p><code>@ConditionalOnProperty</code> はメソッドレベルのアノテーションです。クラス全体ではなく、<strong>そのメソッドで定義される Bean だけ</strong>が条件の対象になります。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 例；管理DB用のConfiguration</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ManagerSqlConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> DataSource <span class="title function_">managerDataSource</span><span class="params">()</span> &#123; ... &#125;      <span class="comment">// ← 常に登録</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="meta">@ConditionalOnProperty(...)</span>                         <span class="comment">// ★ ここだけ条件付き</span></span><br><span class="line">    <span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(...)</span> &#123; ... &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>これにより、Flyway を無効にしても管理DB への接続や SQL 実行は通常通り動作します。</p><h3 id="yml-設定-1"><a href="#yml-設定-1" class="headerlink" title="yml 設定"></a>yml 設定</h3><p>dev 環境用の yml にだけプロパティを追加します。</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># application-dev.yml</span></span><br><span class="line"><span class="attr">app:</span></span><br><span class="line">  <span class="attr">manager:</span></span><br><span class="line">    <span class="attr">flyway:</span></span><br><span class="line">      <span class="attr">enabled:</span> <span class="literal">false</span>  <span class="comment"># マイグレーション DDL を手動確認してから適用する</span></span><br></pre></td></tr></table></figure><p>staging &#x2F; prod では未定義のまま（&#x3D; <code>matchIfMissing = true</code> によりデフォルト有効）にします。</p><h2 id="設定の全体像まとめ"><a href="#設定の全体像まとめ" class="headerlink" title="設定の全体像まとめ"></a>設定の全体像まとめ</h2><p>最終的な設定の対応関係を整理します。</p><h3 id="プロパティの役割分担"><a href="#プロパティの役割分担" class="headerlink" title="プロパティの役割分担"></a>プロパティの役割分担</h3><div class="scroll"><table><thead><tr><th align="left">プロパティ</th><th align="left">制御対象</th><th align="left">デフォルト</th></tr></thead><tbody><tr><td align="left"><code>spring.flyway.enabled</code></td><td align="left">Spring Boot 自動構成の Flyway（primary DataSource）</td><td align="left"><code>true</code></td></tr><tr><td align="left"><code>app.manager.flyway.enabled</code></td><td align="left">手動構成の Flyway Bean（secondary DataSource）</td><td align="left"><code>true</code>（<code>matchIfMissing</code>）</td></tr></tbody></table></div><h3 id="環境別設定"><a href="#環境別設定" class="headerlink" title="環境別設定"></a>環境別設定</h3><div class="scroll"><table><thead><tr><th align="left">環境</th><th align="left"><code>spring.flyway.enabled</code></th><th align="left"><code>app.manager.flyway.enabled</code></th><th align="left">結果</th></tr></thead><tbody><tr><td align="left">共通</td><td align="left"><code>false</code></td><td align="left">（未指定）</td><td align="left">primary: 無効 &#x2F; secondary: 有効</td></tr><tr><td align="left">dev</td><td align="left">（共通を継承）</td><td align="left"><code>false</code></td><td align="left">primary: 無効 &#x2F; secondary: <strong>無効</strong></td></tr><tr><td align="left">staging &#x2F; prod</td><td align="left">（共通を継承）</td><td align="left">（未指定 &#x3D; 有効）</td><td align="left">primary: 無効 &#x2F; secondary: <strong>有効</strong></td></tr></tbody></table></div><h3 id="Java-コード最終形"><a href="#Java-コード最終形" class="headerlink" title="Java コード最終形"></a>Java コード最終形</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;app.manager.datasource&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ManagerSqlConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;spring.flyway.locations:classpath:db/migration/common&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String[] flywayLocations;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerDataSource&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> DataSource <span class="title function_">managerDataSource</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// DataSource 構成（省略）</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 管理DB用の Flyway マイグレーション.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * &#123;<span class="doctag">@code</span> app.manager.flyway.enabled=false&#125; で無効化できる（dev 環境向け）.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line">    <span class="meta">@ConditionalOnProperty(</span></span><br><span class="line"><span class="meta">        name = &quot;app.manager.flyway.enabled&quot;,</span></span><br><span class="line"><span class="meta">        havingValue = &quot;true&quot;,</span></span><br><span class="line"><span class="meta">        matchIfMissing = true)</span></span><br><span class="line">    <span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(</span></span><br><span class="line"><span class="params">            <span class="meta">@Qualifier(&quot;managerDataSource&quot;)</span> DataSource managerDataSource)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Flyway.configure()</span><br><span class="line">                .dataSource(managerDataSource)</span><br><span class="line">                .locations(flywayLocations)</span><br><span class="line">                .baselineOnMigrate(<span class="literal">true</span>)</span><br><span class="line">                .load();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h2><p>マルチ DataSource 構成で Flyway を使う際のポイントをまとめます。</p><ol><li><strong>Spring Boot の Flyway 自動構成は primary DataSource 専用</strong>。secondary DataSource には <code>@Bean(initMethod = &quot;migrate&quot;)</code> で手動構成が必要</li><li><strong><code>baselineOnMigrate(true)</code> は既存スキーマへの Flyway 後付け導入用</strong>。V1 は「現在のスキーマ状態」を記録する全DDLを配置し、実際の差分マイグレーションは V2 から開始する</li><li><strong><code>@ConditionalOnProperty</code> で Bean 単位の有効&#x2F;無効切り替え</strong>が可能。<code>matchIfMissing = true</code> により、無効にしたい環境だけ <code>false</code> を指定すればよい</li></ol><p>特に 2 は Flyway のドキュメントを読んでいても見落としがちなポイントだと感じました。<code>baselineOnMigrate</code> の意味を正しく理解せずに V1 に差分DDLを書いてしまうと、既存環境ではスキップされ、新規環境でしか実行されないという事態になります。</p><h2 id="Appendix-Flyway-公式ドキュメント"><a href="#Appendix-Flyway-公式ドキュメント" class="headerlink" title="Appendix: Flyway 公式ドキュメント"></a>Appendix: Flyway 公式ドキュメント</h2><p>本記事で扱った Flyway の設定項目に関する公式ドキュメントへのリンクです。</p><div class="scroll"><table><thead><tr><th align="left">設定項目</th><th align="left">説明</th><th align="left">ドキュメント</th></tr></thead><tbody><tr><td align="left"><code>baselineOnMigrate</code></td><td align="left">既存スキーマへの後付け導入時にベースラインを自動作成する</td><td align="left"><a href="https://documentation.red-gate.com/fd/flyway-baseline-on-migrate-setting-277578974.html">Flyway Baseline On Migrate Setting</a></td></tr><tr><td align="left"><code>baselineVersion</code></td><td align="left">ベースラインのバージョン番号（デフォルト: “1”）</td><td align="left"><a href="https://documentation.red-gate.com/fd/flyway-baseline-version-setting-277578975.html">Flyway Baseline Version Setting</a></td></tr><tr><td align="left"><code>locations</code></td><td align="left">マイグレーション SQL の配置先ディレクトリ</td><td align="left"><a href="https://documentation.red-gate.com/fd/flyway-locations-setting-277579008.html">Flyway Locations Setting</a></td></tr><tr><td align="left">Baselines（概念説明）</td><td align="left">ベースラインの仕組みと使い方の概要</td><td align="left"><a href="https://documentation.red-gate.com/fd/baselines-273973441.html">Baselines</a></td></tr><tr><td align="left">Configuration（全体）</td><td align="left">Flyway の全設定項目一覧</td><td align="left"><a href="https://documentation.red-gate.com/flyway/reference/configuration">Configuration</a></td></tr></tbody></table></div><script type="module"> import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';mermaid.initialize({startOnLoad: true, flowchart: {curve: 'linear'}}); </script>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot; title=&quot;はじめに&quot;&gt;&lt;/a&gt;はじめに&lt;/h2&gt;&lt;h3 id=&quot;Flyway-とは&quot;&gt;&lt;a href=&quot;#Flyway-とは&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    
    
    <category term="SpringBoot" scheme="https://future-architect.github.io/tags/SpringBoot/"/>
    
    <category term="Flyway" scheme="https://future-architect.github.io/tags/Flyway/"/>
    
  </entry>
  
  <entry>
    <title>S3エミュレーションでrustfsを使ってみたメモとPresigned URLの仕組み</title>
    <link href="https://future-architect.github.io/articles/20260403a/"/>
    <id>https://future-architect.github.io/articles/20260403a/</id>
    <published>2026-04-02T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<p>ちょっとしたオブジェクトストレージ前提のシステムのローカルテストでApache 2ライセンスのrustfsを使ってみました。おおむね簡単だったのですが、認証設定をしたり、presigned URLの発行だけちょっと手間がかかってしまったのでその対応とその過程で学んだことのメモです。</p><p>このあたり、minioがDockerイメージの配布をやめてメンテナンスモードになったり、LocalStackがユーザー登録必須になってCIで使いにくくなったりでにわかに話題になっていたところですね。</p><ul><li><a href="https://light-of-moe.ddo.jp/~sakura/diary/?p=1931">さくらんぼの技術備忘録: 手軽に使えるS3互換ストレージを求めて</a></li><li><a href="https://zenn.dev/appleworld/articles/e1bac60a3bd333">minioがdockerイメージを配布しなくなったので新しいS3互換ストレージを探す</a></li></ul><p>ちょっとしたウェブアプリのバックエンドのストレージとしてオブジェクトストレージが欲しくなったのですが、これまではminioをたまに使ったりしていたものの、別のものを検討するにあたり、docker composeで一緒に起動するという使い方で使いやすいものということで、いろいろ比べてrustfsを選んでみました。</p><h1 id="compose-yamlでの利用方法"><a href="#compose-yamlでの利用方法" class="headerlink" title="compose.yamlでの利用方法"></a>compose.yamlでの利用方法</h1><p>rustfsの公式イメージをそのまま使うだけです。一瞬で起動します。</p><ul><li>デフォルトで9000ポートでAPIのエンドポイントを、9001で管理画面(RUSTFS_CONSOLE_ENABLEが必要)を公開します</li><li>複数ボリュームのレプリケーションとか色々複雑な機能もありますが、テスト用で可用性はいらなかったので1ボリュームにしています</li><li>起動時にはバケットができて欲しいところなので、amazon&#x2F;aws-cliイメージを使って起動時にバケットを作るようにします</li></ul><figure class="highlight yaml"><figcaption><span>compose.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">rustfs:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">rustfs/rustfs:latest</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">RUSTFS_CONSOLE_ENABLE:</span> <span class="string">&quot;true&quot;</span></span><br><span class="line">      <span class="attr">RUSTFS_ACCESS_KEY:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">RUSTFS_SECRET_KEY:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">RUSTFS_VOLUMES:</span> <span class="string">/data/rustfs0</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">rustfs-data:/data</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">rustfs-logs:/logs</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9000:9000&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9001:9001&quot;</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;sh&quot;</span>, <span class="string">&quot;-c&quot;</span>, <span class="string">&quot;curl -sS http://localhost:9000/ &gt;/dev/null&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">1s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">20</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">rustfs-init:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">amazon/aws-cli:2.31.15</span></span><br><span class="line">    <span class="attr">entrypoint:</span> [<span class="string">&quot;/bin/sh&quot;</span>, <span class="string">&quot;-c&quot;</span>]</span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">        set -eu</span></span><br><span class="line"><span class="string">        until aws --endpoint-url http://rustfs:9000 s3api list-buckets &gt;/dev/null 2&gt;&amp;1; do</span></span><br><span class="line"><span class="string">          sleep 2</span></span><br><span class="line"><span class="string">        done</span></span><br><span class="line"><span class="string">        for bucket in data-bucket log-bucket; do</span></span><br><span class="line"><span class="string">          aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket &quot;$$bucket&quot; || true</span></span><br><span class="line"><span class="string">        done</span></span><br><span class="line"><span class="string"></span>    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">AWS_ACCESS_KEY_ID:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">AWS_SECRET_ACCESS_KEY:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">AWS_REGION:</span> <span class="string">us-east-1</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">rustfs:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">rustfs-data:</span></span><br><span class="line">  <span class="attr">rustfs-logs:</span></span><br></pre></td></tr></table></figure><p>使い方を調べると、<code>RUSTFS_ADDRESS</code>などの環境変数でアドレスを定義しているものなどもありますが、なくてもデフォルトで9000番（UIは9001番）ポートで開いたので省略しました。</p><p>管理画面は動作も軽快だしなかなか良いですね。今まで触ったことのあるウェブを使ったファイル管理画面の中では一番スピードが速くて体験が良いですね。</p><img src="/images/2026/20260403a/screenshot_console.png" alt="" width="1200" height="816" loading="lazy"><h1 id="Presigned-URL"><a href="#Presigned-URL" class="headerlink" title="Presigned URL"></a>Presigned URL</h1><p>これでAWS SDKを使ったデータの読み書きは問題ありませんでしたが、Presigned URLの発行で問題が発生しました。rustfsの問題というかDockerを使っているから起きた問題ですが、rustfsでは、Presigned URLで発行されるURLはリクエスト時のホスト情報をもとに作られます。Dockerの中からは<code>http://rustfs:9000</code>というドメインでアクセスしますが、外からは<code>http://localhost:9000</code>なので、発行されたURLのままではアクセスできないということが起きました。</p><p>これは発行時にクライアントを新規で作って、ホストを<code>http://localhost:9000</code>に設定してそれで発行し直す必要がありました。</p><figure class="highlight go"><figcaption><span>goのサンプル</span></figcaption><table><tr><td class="code"><pre><span class="line">   <span class="comment">// この環境変数があったらそのホストでURLを発行</span></span><br><span class="line">endpoint := os.Getenv(<span class="string">&quot;RUNTASK_RUSTFS_OBJECT_PUBLIC_ENDPOINT&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> endpoint != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">tempOptions := s.options</span><br><span class="line">tempOptions.Endpoint = endpoint</span><br><span class="line">tempClient, err := newS3Client(context.Background(), tempOptions)</span><br><span class="line"><span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">presigner := s3.NewPresignClient(tempClient)</span><br><span class="line">presigned, err := presigner.PresignGetObject(context.Background(), &amp;s3.GetObjectInput&#123;</span><br><span class="line">Bucket: aws.String(s.bucket),</span><br><span class="line">Key:    aws.String(key),</span><br><span class="line">&#125;, <span class="function"><span class="keyword">func</span><span class="params">(opts *s3.PresignOptions)</span></span> &#123;</span><br><span class="line">opts.Expires = expiry</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> presigned.URL, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// fallthrough to try using the existing client</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">   <span class="comment">// 設定がない場合は普通に発行</span></span><br><span class="line">presigner := s3.NewPresignClient(s.client)</span><br><span class="line">presigned, err := presigner.PresignGetObject(context.Background(), &amp;s3.GetObjectInput&#123;</span><br><span class="line">Bucket: aws.String(s.bucket),</span><br><span class="line">Key:    aws.String(key),</span><br><span class="line">&#125;, <span class="function"><span class="keyword">func</span><span class="params">(opts *s3.PresignOptions)</span></span> &#123;</span><br><span class="line">opts.Expires = expiry</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span>, err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> presigned.URL, <span class="literal">nil</span></span><br></pre></td></tr></table></figure><p><code>Endpoint</code>を上書きしてしまったら逆にバックエンドのサーバーからrustfsに繋がらないからダメなのでは？と思い込んでましたが、このPresigned URLの発行は<a href="https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_sigv.html">S3 APIを実際に叩いているわけではなく、SDKの中で発行している</a>らしい。</p><p>使う技術はその名の通り「署名」です。TLSは機密の秘匿化(外から読めない)、完全性保証(改竄検知)、認証(証明書によるサーバーの身元確認)などを行いますが、Presigned URLの場合はこのうちの完全性の保証をベースに、いつ誰が許可したのかの情報が後からわかるようにしています。</p><p>サービスにアクセスするのに使うURLに「誰が」というのを明らかにするキーIDと期限が付与されて、シークレットアクセスキーを使って署名されます。署名されているので期限や誰が、といった情報の改ざんは許しません。</p><img src="/images/2026/20260403a/screenshot_presigned_url.png" alt="スクリーンショット 2026-03-31 18.26.14.png" width="1200" height="523" loading="lazy"><p>クライアントはそのURLを使ってS3からファイルをダウンロードしたり、ファイルをアップロードします。S3(ここではrustfs)はその署名をみて、改竄されていないことの確認とともに、誰が署名したのかを確認します。ブラウザ自身はクレデンシャルを持っていなくても、その署名をもとにして認可制御が行われ、読み書きが成功するという流れです。</p><p><code>Endpoint</code>を書き換えたクライアントを一時的に作るという方針でも、実際にそのクライアントでS3にリクエストを投げることはなくてURLの発行にしか使わないので問題なく利用できるんですね。てっきり、一時的に利用可能なトークン的なURLとして発行されてサーバー側に情報を持っているのかと思いましたが、そんなことはないんですね。勉強になりました。</p><h1 id="まとめ"><a href="#まとめ" class="headerlink" title="まとめ"></a>まとめ</h1><p>S3以外もいろいろ必要となる場合は他のAWSエミュレータ（<a href="https://github.com/getmoto/moto">moto</a>とか<a href="https://github.com/hectorvent/floci">floci</a>とか)の方が良いかもしれませんが、今回はS3だけが欲しかったのでrustfsを選んでみて使ってみたメモでした。</p><p>今まではminioを考えずに使っていましたが、今回別のものを検討してrustfsを使ってみました。seaweedfsとかも良さそうでしたが、filterとかたくさんコンテナが必要そうだったので1つで済むrustfsにしました。コンテナのメモリ消費90MBぐらいですね。動きも軽快なので今後も使ってみようと思いました。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;ちょっとしたオブジェクトストレージ前提のシステムのローカルテストでApache 2ライセンスのrustfsを使ってみました。おおむね簡単だったのですが、認証設定をしたり、presigned</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    
    <category term="Docker" scheme="https://future-architect.github.io/tags/Docker/"/>
    
    <category term="S3" scheme="https://future-architect.github.io/tags/S3/"/>
    
    <category term="rustfs" scheme="https://future-architect.github.io/tags/rustfs/"/>
    
  </entry>
  
  <entry>
    <title>実験して入門するKubernetes：Pod起動の裏側を追っていたら、どうしてもSchedulerを止めてみたくなった件</title>
    <link href="https://future-architect.github.io/articles/20260325a/"/>
    <id>https://future-architect.github.io/articles/20260325a/</id>
    <published>2026-03-24T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260325a/top.jpg" alt="" width="1024" height="572"><h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>最近、コンテナオーケストレーションの技術領域に興味をもち、Kubernetesを触り始めました。早速自宅で飼ってるMiniPC（ホームサーバ）にArgoCDやLonghornを入れてみたものの、Deployment、Pod、Serviceなど初めましての概念ばかり。気づいたら、AIの指示通りにただコマンド打つマンになっていました。</p><p>このままではまずいと思い、Kubernetesの基礎的な挙動を追うことにしました。今回は、どのような流れでPodが作られているのかを実験し、そのアウトプットとしてこの記事にまとめています。</p><p>※実験にはMinikubeを使っています。初学者が調べながらまとめたものなので、誤りがあればご指摘ください。</p><h1 id="Pod起動までの流れの概要"><a href="#Pod起動までの流れの概要" class="headerlink" title="Pod起動までの流れの概要"></a>Pod起動までの流れの概要</h1><p>Podが起動するまでの流れを紹介します。複数のKubernetesコンポーネントが登場しますが、この記事のメインテーマではないため、用語は簡単な説明に留めます。詳細が気になる方は、既存の解説記事や<a href="https://kubernetes.io/ja/docs/concepts/overview/components/">公式ドキュメント</a>をご参照ください。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">kubectl run</span><br><span class="line">  ↓</span><br><span class="line">API Server → etcd（Podデータ保存、ノード未割り当て）</span><br><span class="line">  ↓</span><br><span class="line">Scheduler（最適なノードを割り当て）</span><br><span class="line">  ↓</span><br><span class="line">kubelet（コンテナイメージのPull・起動）</span><br><span class="line">  ↓</span><br><span class="line">podが起動</span><br></pre></td></tr></table></figure><ul><li><strong>etcd</strong>：Kubernetesの全状態を保存するデータベース</li><li><strong>API Server</strong>：全コンポーネントからのリクエスト受付と、状態変更の通知する窓口</li><li><strong>Scheduler</strong>：Podをどのノードで動かすかを決定するスケジューラ</li><li><strong>kubelet</strong>：各ノード上で実際にコンテナを起動・管理する実行エンジン</li></ul><blockquote><p>補足：<code>kubectl run</code>はPodを直接作成します。Deployment経由の場合は、Controller ManagerがDeploymentデータ → ReplicaSetデータ → Podデータ と各種データを作成するステップが加わります。この記事では、簡略化のため<code>kubectl run</code>による直接的な作成を追いかけます。</p></blockquote><h1 id="実験1：イベントログでPod作成の流れを確認する"><a href="#実験1：イベントログでPod作成の流れを確認する" class="headerlink" title="実験1：イベントログでPod作成の流れを確認する"></a>実験1：イベントログでPod作成の流れを確認する</h1><p>最初の実験として、実際に<code>kubectl run</code>を実行し、出力されるイベントログを確認してみます。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">% kubectl run log-test --image=nginx</span><br><span class="line">pod/log-test created</span><br><span class="line">% kubectl get events -w</span><br><span class="line">LAST SEEN   TYPE     REASON      OBJECT         MESSAGE</span><br><span class="line">23m         Normal   Scheduled   pod/log-test   Successfully assigned default/log-test to minikube</span><br><span class="line">22m         Normal   Pulling     pod/log-test   Pulling image <span class="string">&quot;nginx&quot;</span></span><br><span class="line">22m         Normal   Pulled      pod/log-test   Successfully pulled image <span class="string">&quot;nginx&quot;</span> <span class="keyword">in</span> 1.481s (1.481s including waiting). Image size: 180545980 bytes.</span><br><span class="line">22m         Normal   Created     pod/log-test   Container created</span><br><span class="line">22m         Normal   Started     pod/log-test   Container started</span><br></pre></td></tr></table></figure><p>REASON列とMESSAGE列を見ると、まずScheduledでPodがノード（ここではminikube）に割り当てられ、その後にコンテナの立ち上げ処理が始まっていることがわかります。</p><p>さらに<code>kubectl get events -o yaml</code> とオプションを追加すると、各イベントがどのコンポーネントから出力されたのかを詳しく確認できます。</p><details><summary> kubectl get events -o yaml の出力</summary><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">%</span> <span class="string">kubectl</span> <span class="string">get</span> <span class="string">events</span> <span class="string">-o</span> <span class="string">yaml</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">items:</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">action:</span> <span class="string">Binding</span></span><br><span class="line">  <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="string">&quot;2026-03-17T08:45:27.742811Z&quot;</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62245&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Successfully</span> <span class="string">assigned</span> <span class="string">default/log-test</span> <span class="string">to</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:27Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485200842b1</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62247&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">822856bb-0e26-4942-8d78-ac1eff975cb0</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Scheduled</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">default-scheduler</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">default-scheduler-minikube</span></span><br><span class="line">  <span class="attr">source:</span> &#123;&#125;</span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:28Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:28Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Pulling</span> <span class="string">image</span> <span class="string">&quot;nginx&quot;</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:28Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d94853f8ec93d</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62249&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">212f6090-34de-46dd-82eb-d721ace1b32a</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Pulling</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">&#x27;Successfully pulled image &quot;nginx&quot; in 2.135s (2.135s including waiting).</span></span><br><span class="line"><span class="string">    Image size: 180542930 bytes.&#x27;</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485beda4a95</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62252&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">3eb2fd61-7b0b-4f62-b7c3-e5a4be893399</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Pulled</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Container</span> <span class="string">created</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485c0e87bde</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62253&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">df68a670-7684-4573-beff-b92e5a6bbdec</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Created</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Container</span> <span class="string">started</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485c5d61a22</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62254&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">edf2dbb3-11e1-49e6-bb91-6dbca2d51edb</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Started</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">List</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">resourceVersion:</span> <span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure></details><p>出力されたイベントログの <code>reportingComponent</code> フィールドを確認すると、各イベントの出力元は以下です。</p><ul><li><strong>Scheduled</strong> → <code>reportingComponent: default-scheduler</code></li><li><strong>Pulling &#x2F; Pulled &#x2F; Created &#x2F; Started</strong> → <code>reportingComponent: kubelet</code></li></ul><p>概要で確認した通り、まずSchedulerがノードを割り当てた後、そのノードのkubeletがコンテナの起動処理を行っていることが、実際のログからも確認できました。</p><h1 id="Static-PodとMirror-Pod"><a href="#Static-PodとMirror-Pod" class="headerlink" title="Static PodとMirror Pod"></a>Static PodとMirror Pod</h1><p>ここまででpodが起動するまでの流れはわかったのですが、ここで1つ疑問が湧きました。</p><p>先ほど確認した通り、Podが起動するにはSchedulerによるノード割り当てが必要です。しかし、そのScheduler自身もPodとして動いています。となると、Schedulerを起動するためのSchedulerが必要で、そのSchedulerを起動するためにさらにSchedulerが必要で、、、と無限ループに陥ってしまいます。最初のSchedulerは一体どのように起動されているのでしょうか？</p><p>結論から言うと、 SchedulerはAPI Serverや別のSchedulerを経由せず、kubeletがマニフェストファイルから直接起動していました。</p><p>実際のSchedulerのマニフェストファイルを確認すると、以下のようになっています。kindが <code>Deployment</code> ではなく <code>Pod</code> となっており、Podデータそのものが定義されていることがわかります。</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">%</span> <span class="string">minikube</span> <span class="string">ssh</span></span><br><span class="line"><span class="string">$</span> <span class="string">sudo</span> <span class="string">cat</span> <span class="string">/etc/kubernetes/manifests/kube-scheduler.yaml</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kube-scheduler</span></span><br><span class="line">    <span class="attr">tier:</span> <span class="string">control-plane</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">kube-scheduler</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">containers:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">command:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">kube-scheduler</span></span><br><span class="line">    <span class="string">...</span></span><br></pre></td></tr></table></figure><p>このように、特定のディレクトリにマニフェストを配置し、API Serverを経由せずにkubeletが直接起動・管理するPodを<strong>Static Pod</strong>と呼びます。Kubernetesの起動時には、コントロールプレーンのkubeletがこの仕組みを利用して、API Serverやetcd、Schedulerなどの主要なコンポーネントをStatic Podとして起動しています。</p><p>ただ、このStatic PodはAPI Serverを経由しないため、etcd上に実体がありません。そのままでは、<code>kubectl get pods</code>で確認できず、クラスタ全体の状態を一元管理する上で不便です。そこでkubeletは、<strong>Mirror Pod</strong>と呼ばれる読み取り専用のPodを作成します。<code>kubectl get pods -n kube-system</code>で見える<code>kube-scheduler-minikube</code>は、実はこのMirror Podでした。</p><h1 id="実験2：Schedulerを止めてみる"><a href="#実験2：Schedulerを止めてみる" class="headerlink" title="実験2：Schedulerを止めてみる"></a>実験2：Schedulerを止めてみる</h1><p>ここまでの話が本当なら、以下が成り立つはずです。</p><ul><li>Mirror Podを削除しても、Schedulerは止まらない（本体はマニフェストファイルだから）</li><li>マニフェストファイルを削除（移動）すると、Schedulerが止まる</li><li>Schedulerが止まると、新しいPodはノードに割り当てられずPendingのままになる</li></ul><p>というわけで、さっそく試してみましょう。</p><h2 id="1-Mirror-Podを削除する"><a href="#1-Mirror-Podを削除する" class="headerlink" title="1. Mirror Podを削除する"></a>1. Mirror Podを削除する</h2><p><code>kubectl delete pod</code>で<code>kube-scheduler-minikube</code>を削除してみます。しかし、何度削除しても、即座に復活し、<code>kubectl get pods</code>の結果に出てきます。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">% kubectl delete pod kube-scheduler-minikube -n kube-system</span><br><span class="line">pod <span class="string">&quot;kube-scheduler-minikube&quot;</span> deleted from kube-system namespace</span><br><span class="line">% kubectl get pods -n kube-system</span><br><span class="line">NAME                               READY   STATUS    RESTARTS   AGE</span><br><span class="line">...</span><br><span class="line">kube-scheduler-minikube            1/1     Running   0          5s</span><br><span class="line">...</span><br></pre></td></tr></table></figure><h2 id="2-マニフェストファイルを移動する"><a href="#2-マニフェストファイルを移動する" class="headerlink" title="2. マニフェストファイルを移動する"></a>2. マニフェストファイルを移動する</h2><p>次に<code>minikube ssh</code>でMinikubeのノード内入り、マニフェストファイルを他の場所に動かしてみます（削除すると戻すのが面倒なので<code>mv</code>にしました）。</p><p>すると、あれだけ何度削除しても復活していた<code>kube-scheduler-minikube</code>が、<code>kubectl get pods</code>から消えました。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">% minikube ssh</span><br><span class="line">$ <span class="built_in">sudo</span> <span class="built_in">mv</span> /etc/kubernetes/manifests/kube-scheduler.yaml /tmp/</span><br><span class="line">$ <span class="built_in">exit</span></span><br><span class="line">% kubectl get pods -n kube-system</span><br><span class="line">NAME                               READY   STATUS    RESTARTS   AGE</span><br><span class="line">coredns-7d764666f9-bbkvl           1/1     Running   0          11h</span><br><span class="line">etcd-minikube                      1/1     Running   0          11h</span><br><span class="line">kube-apiserver-minikube            1/1     Running   0          11h</span><br><span class="line">kube-controller-manager-minikube   1/1     Running   0          11h</span><br><span class="line">kube-proxy-fj8zg                   1/1     Running   0          11h</span><br><span class="line">storage-provisioner                1/1     Running   0          11h</span><br></pre></td></tr></table></figure><h2 id="3-Schedulerなしで新しいPodを作成する"><a href="#3-Schedulerなしで新しいPodを作成する" class="headerlink" title="3. Schedulerなしで新しいPodを作成する"></a>3. Schedulerなしで新しいPodを作成する</h2><p>このSchedulerがいない状態で、新しいPodを作成してみます。Podデータ自体は作成できましたが、STATUSが<code>Pending</code>のまま動かず、NODEも<code>&lt;none&gt;</code>のままです（必要な情報を出力するため<code>custom-columns</code>オプションをつけています）。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">% kubectl run new-pod --image=nginx</span><br><span class="line">pod/new-pod created</span><br><span class="line">% kubectl get pods -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName</span><br><span class="line">NAME                         STATUS    NODE</span><br><span class="line">hello-node-64fc5894d-57r8s   Running   minikube</span><br><span class="line">hello-node-64fc5894d-7cqrw   Running   minikube</span><br><span class="line">hello-node-64fc5894d-jl4pb   Running   minikube</span><br><span class="line">new-pod                      Pending   &lt;none&gt;</span><br></pre></td></tr></table></figure><p>どれだけ待っても<code>Running</code>になることはありませんでした。</p><p>Schedulerがいないと、Podデータは作成されても、ノードが割り当てられないため、冒頭で確認したフローのSchedulerのステップで処理が止まってしまうことが実際の挙動でも確認できました。</p><h1 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h1><p>解説動画や記事を見ても、Kubernetesの各概念やその関係性がピンとこず、「デプロイするとPodが作れてその中にコンテナがいるらしい」くらいの認識でした。そんな状態だったので、当然<code>kubectl</code>コマンドの意味がわかるはずもなく、結果としてAIの指示通りにコマンド打つマンと化していました。</p><p>しかし今回、小さな実験を通して手を動かしてみたことで、裏側で各コンポーネントがどのように連携して動いているのかを具体的に追うことができ、一気に理解が深まりました。特にSchedulerは、こうして実験してみなければ存在すら知らなかった概念だったため、非常に学びになりました。</p><p>今回の実験で、何度も消されたりファイルを移動させられたりと散々な目に遭わせてしまったSchedulerには、この場を借りて感謝と謝罪を述べて締めくくりたいと思います。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260325a/top.jpg&quot; alt=&quot;&quot; width=&quot;1024&quot; height=&quot;572&quot;&gt;

&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="初心者向け" scheme="https://future-architect.github.io/tags/%E5%88%9D%E5%BF%83%E8%80%85%E5%90%91%E3%81%91/"/>
    
    <category term="Kubernetes" scheme="https://future-architect.github.io/tags/Kubernetes/"/>
    
    <category term="Minikube" scheme="https://future-architect.github.io/tags/Minikube/"/>
    
  </entry>
  
  <entry>
    <title>API GatewayとLambdaで実装するプライベートなMCPサーバー</title>
    <link href="https://future-architect.github.io/articles/20260324a/"/>
    <id>https://future-architect.github.io/articles/20260324a/</id>
    <published>2026-03-23T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<p>Strategic AI Group&#x2F;MLOpsチームでアルバイトをしている木村です。アルバイトでは最新技術の調査を担当し、社内や案件にて活用することを想定したシステム導入の検証を実施しています。プライベートではエンジニアにありがちな(?)運動不足解消として、マラソンをしていて、47都道府県制覇を目指しています。</p><p>今回はAWS上のLambdaで自作したプライベートなMCPサーバーをDify上で使用する手順について記事にします。これから社内でもどんどん広がっていくであろうMCPを使っていてとても面白かったので、ぜひ皆さんも本記事を参考に試してみてください！</p><h1 id="概要"><a href="#概要" class="headerlink" title="概要"></a>概要</h1><p>「社内でLLMに様々なスキルを持たせたい」というのは、多くの企業が抱える要望です。本記事では、AWS Lambda上に自作のプライベートなMCP（Model Context Protocol）サーバーを構築し、それをDifyから呼び出す手順を解説します。</p><p>セキュリティを担保しつつ、誰もが簡単にAIアプリを構築できる環境の「第一歩」を目指します。</p><p>最終的には以下のようにDifyからMCPサーバーにアクセスできます。</p><p>以下のツールではこんにちはに対してHELLOを返すcalculate_helloというmcpツール、計算について、足し算、掛け算、引き算を行うcalculate_add,calculate_product,calculate_subというmcpツールが使われています。</p><img src="/images/2026/20260324a/image.png" alt="image.png" width="844" height="724" loading="lazy"><p>これを応用してWeb検索を行ったり、Googleカレンダーに予定を自動で入れることができます。</p><h1 id="本記事のキーテクノロジー"><a href="#本記事のキーテクノロジー" class="headerlink" title="本記事のキーテクノロジー"></a>本記事のキーテクノロジー</h1><h2 id="Dify"><a href="#Dify" class="headerlink" title="Dify"></a>Dify</h2><ul><li><a href="https://dify.ai/jp">Dify公式サイト</a></li></ul><p>Difyは、LLMアプリを直感的に開発できるオープンソースのLLMアプリ開発プラットフォームです。RAG（検索拡張生成）やエージェント機能を手軽に実装できるのが特徴です。</p><p>本記事では社内ネットワークのEC2でDifyを動かすことでプライベートな構築を実現しています。</p><h2 id="MCP"><a href="#MCP" class="headerlink" title="MCP"></a>MCP</h2><p>Anthropicが公開したMCPは(参考：<a href="https://zenn.dev/cloud_ace/articles/model-context-protocol">Model Context Protocol（MCP）とは</a>)、LLMと外部データ（DB、API、ローカルファイル等）を接続するためのオープンプロトコルです。これまではツールごとに専用の繋ぎ込みが必要でしたが、MCPという「共通規格」を通すことで、一つのMCPサーバーを作るだけで様々なAIクライアントからデータを利用可能になります。似ているものでRAGがありますがRAGは「知識の検索・参照」に特化しているのに対し、MCPは「機能（ツール）の呼び出し・実行」に特化しています。</p><img src="/images/2026/20260324a/image_2.png" alt="image.png" width="709" height="330" loading="lazy"><h2 id="API-Gateway-Lambdaによるプライベートな環境"><a href="#API-Gateway-Lambdaによるプライベートな環境" class="headerlink" title="API Gateway+Lambdaによるプライベートな環境"></a>API Gateway+Lambdaによるプライベートな環境</h2><p>社内DBや秘匿性の高い情報が流出するリスクを排除するため、社内ネットワーク内での運用が求められるケースがあります。</p><p>今回は上記を実現するべく、API Gatewayのアクセスを社内ネットワークからに制限し、すべてのリソースを社内に置いておくことでプライベートな環境を実現することを目指しました。<br>※前提として、社内ネットワークとAWS環境がVPN接続されていることとします。</p><h2 id="通信方式"><a href="#通信方式" class="headerlink" title="通信方式"></a>通信方式</h2><p>MCPには、ローカルMCP（stdio）とリモートMCP（Streamable HTTP）の2種類があります。ローカルMCPの場合、同じ環境を相手のPCにも構築する必要があり、共有に手間がかかってしまうので、リモートMCPを採用しました。</p><p>また、リモートMCPをサーバレスで実行するために通信方式は以下の設定にしています。(参考：<a href="https://qiita.com/nogataka/items/7fbeb58339703ec98a86">MCPの通信方式</a>)</p><ul><li>stateless_http: stateless_http&#x3D;True に設定し、本来Statefulな通信を前提とするMCPプロトコルをステートレスなHTTP通信に変換することでLambdaのような1回切りのリクエストに対応させます</li><li>Streamable_http：Lambdaでの1回限りの重い処理を、タイムアウトで切断される前に小出しで届けて完結させる</li></ul><h1 id="構成図"><a href="#構成図" class="headerlink" title="構成図"></a>構成図</h1><p>本構成では、セキュリティを最優先し、API Gatewayを「プライベート」モードでデプロイします。これにより、社内ネットワークからのみAIツールを呼び出すことが可能になります。</p><img src="/images/2026/20260324a/image_3.png" alt="image.png" width="696" height="364" loading="lazy"><h2 id="Lambda採用理由"><a href="#Lambda採用理由" class="headerlink" title="Lambda採用理由"></a>Lambda採用理由</h2><p> EC2でMCPサーバをホストした場合、常時EC2を起動しておく必要があり、利用していない期間も不要な料金が発生します。そこでサーバレスなLambdaでホストすることで、MCPサーバーが呼ばれた時だけ料金が発生するので、コストを抑えられます。</p><h2 id="本構成のデメリット"><a href="#本構成のデメリット" class="headerlink" title="本構成のデメリット"></a>本構成のデメリット</h2><p>大量のログ解析や複雑なデータ集計など、完了までに時間がかかるタスクを依頼すると、AIに結果が返る前にAPI Gatewayのタイムアウト制限より、接続が切れてエラーになる可能性があります。</p><p>また、VPCエンドポイントは月10ドルほどの固定費がかかるというデメリットも挙げられます。</p><h1 id="構築手順"><a href="#構築手順" class="headerlink" title="構築手順"></a>構築手順</h1><ol><li>まずは開発環境（EC2）にAWS SAM CLIやPython 3.11をインストールし、必要なIAMロールを付与します</li><li>コードを作成してAWSにデプロイ</li></ol><p>ファイル構成は以下の通りです。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">.</span><br><span class="line">├── mcp_server</span><br><span class="line">│   ├── __init__.py</span><br><span class="line">│   └── __main__.py</span><br><span class="line">├── requirements.txt</span><br><span class="line">├── run.sh</span><br><span class="line">└── template.yaml</span><br></pre></td></tr></table></figure><p>mcp_server&#x2F;<code>__init__.py</code> は空ファイルで大丈夫です。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">touch</span> mcp_server/<span class="string">&#x27;__init__.py&#x27;</span></span><br></pre></td></tr></table></figure><figure class="highlight py"><figcaption><span>mcp_server/__main__.py</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> uvicorn</span><br><span class="line"><span class="keyword">from</span> fastmcp <span class="keyword">import</span> FastMCP</span><br><span class="line"></span><br><span class="line"><span class="comment"># FastMCPの初期化</span></span><br><span class="line"><span class="comment"># stateless_http=True にすることで、Lambdaのような1回切りのリクエストに対応させます</span></span><br><span class="line">mcp = FastMCP(<span class="string">&quot;MyRemoteMCP&quot;</span>, stateless_http=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># ツール定義：型ヒントとドキュメント文字列がそのままMCPの定義になります</span></span><br><span class="line"><span class="meta">@mcp.tool()</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">calculate_add</span>(<span class="params">a: <span class="built_in">float</span>, b: <span class="built_in">float</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;2つの数値を足し合わせます&quot;&quot;&quot;</span></span><br><span class="line">    result = a + b</span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&quot;計算結果: <span class="subst">&#123;a&#125;</span> + <span class="subst">&#123;b&#125;</span> = <span class="subst">&#123;result&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># @mcp.tool()でいくつでもツールを追加可能</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># FastMCPが内部で生成したFastAPIアプリを抽出</span></span><br><span class="line">app = mcp.http_app()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="comment"># Lambda Web Adapterが待機する8080ポートで起動</span></span><br><span class="line">    uvicorn.run(app, host=<span class="string">&quot;0.0.0.0&quot;</span>, port=<span class="number">8080</span>)</span><br></pre></td></tr></table></figure><figure class="highlight sh"><figcaption><span>run.sh</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="built_in">exec</span> python -m mcp_server</span><br></pre></td></tr></table></figure><p>run.shに実行権限を付与。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">chmod</span> +x run.sh</span><br></pre></td></tr></table></figure><figure class="highlight text"><figcaption><span>requirements.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">fastmcp==2.14.5</span><br><span class="line">fastapi==0.128.1</span><br><span class="line">uvicorn==0.40.0</span><br></pre></td></tr></table></figure><ol start="3"><li>AWSリソースの構築</li></ol><p>以下のSam templateを実行すれば本記事のAWSリソースが構築できます。<br>[]内は各々の環境に合わせて変更してください。</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">Transform:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">AWS::Serverless-2016-10-31</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">AWS::LanguageExtensions</span></span><br><span class="line"></span><br><span class="line"><span class="attr">Parameters:</span></span><br><span class="line">  <span class="attr">VpcId:</span> &#123; <span class="attr">Type:</span> <span class="string">String</span>, <span class="attr">Default:</span> <span class="string">vpc-</span>[<span class="string">VPCID</span>] &#125;</span><br><span class="line">  <span class="attr">SubnetId1:</span> &#123; <span class="attr">Type:</span> <span class="string">String</span>, <span class="attr">Default:</span> <span class="string">subnet-</span>[<span class="string">サブネットID</span>] &#125;</span><br><span class="line">  <span class="attr">SubnetId2:</span> &#123; <span class="attr">Type:</span> <span class="string">String</span>, <span class="attr">Default:</span> <span class="string">subnet-</span>[<span class="string">サブネットID</span>] &#125;</span><br><span class="line"></span><br><span class="line"><span class="attr">Resources:</span></span><br><span class="line">  <span class="comment"># --- セキュリティグループ ---</span></span><br><span class="line">  <span class="attr">InternalServiceSG:</span></span><br><span class="line">    <span class="attr">Type:</span> <span class="string">AWS::EC2::SecurityGroup</span></span><br><span class="line">    <span class="attr">Properties:</span></span><br><span class="line">      <span class="attr">GroupDescription:</span> <span class="string">Allow</span> <span class="string">internal</span> <span class="string">traffic</span> <span class="string">for</span> <span class="string">MCP</span> <span class="string">Server</span></span><br><span class="line">      <span class="attr">VpcId:</span> <span class="type">!Ref</span> <span class="string">VpcId</span></span><br><span class="line">      <span class="attr">SecurityGroupIngress:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">IpProtocol:</span> <span class="string">tcp</span></span><br><span class="line">          <span class="attr">FromPort:</span> <span class="number">443</span></span><br><span class="line">          <span class="attr">ToPort:</span> <span class="number">443</span></span><br><span class="line">          <span class="attr">CidrIp:</span> [<span class="string">CiderIp</span>]</span><br><span class="line"></span><br><span class="line">  <span class="comment"># --- VPCエンドポイント (API Gateway用) ---</span></span><br><span class="line">  <span class="attr">MyVpce:</span></span><br><span class="line">    <span class="attr">Type:</span> <span class="string">AWS::EC2::VPCEndpoint</span></span><br><span class="line">    <span class="attr">Properties:</span></span><br><span class="line">      <span class="attr">ServiceName:</span> <span class="type">!Sub</span> <span class="string">&quot;com.amazonaws.$&#123;AWS::Region&#125;.execute-api&quot;</span></span><br><span class="line">      <span class="attr">VpcEndpointType:</span> <span class="string">Interface</span></span><br><span class="line">      <span class="attr">VpcId:</span> <span class="type">!Ref</span> <span class="string">VpcId</span></span><br><span class="line">      <span class="attr">SubnetIds:</span> [ <span class="type">!Ref</span> <span class="string">SubnetId1</span>, <span class="type">!Ref</span> <span class="string">SubnetId2</span> ]</span><br><span class="line">      <span class="attr">SecurityGroupIds:</span> [ <span class="type">!Ref</span> <span class="string">InternalServiceSG</span> ]</span><br><span class="line">      <span class="attr">PrivateDnsEnabled:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># --- API Gateway (Private) ---</span></span><br><span class="line">  <span class="attr">MyApi:</span></span><br><span class="line">    <span class="attr">Type:</span> <span class="string">AWS::Serverless::Api</span></span><br><span class="line">    <span class="attr">DependsOn:</span> <span class="string">MyVpce</span></span><br><span class="line">    <span class="attr">Properties:</span></span><br><span class="line">      <span class="attr">Name:</span> <span class="string">mcp-api</span></span><br><span class="line">      <span class="attr">StageName:</span> <span class="string">prod</span></span><br><span class="line">      <span class="attr">EndpointConfiguration:</span></span><br><span class="line">        <span class="attr">Type:</span> <span class="string">PRIVATE</span></span><br><span class="line">        <span class="attr">VPCEndpointIds:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="type">!Ref</span> <span class="string">MyVpce</span></span><br><span class="line">      <span class="attr">Auth:</span></span><br><span class="line">        <span class="attr">ResourcePolicy:</span></span><br><span class="line">          <span class="attr">CustomStatements:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="attr">Effect:</span> <span class="string">Allow</span></span><br><span class="line">              <span class="attr">Principal:</span> <span class="string">&quot;*&quot;</span></span><br><span class="line">              <span class="attr">Action:</span> <span class="string">&quot;execute-api:Invoke&quot;</span></span><br><span class="line">              <span class="attr">Resource:</span> <span class="string">&quot;execute-api:/*/*/*&quot;</span></span><br><span class="line">            <span class="bullet">-</span> <span class="attr">Effect:</span> <span class="string">Deny</span></span><br><span class="line">              <span class="attr">Principal:</span> <span class="string">&quot;*&quot;</span></span><br><span class="line">              <span class="attr">Action:</span> <span class="string">&quot;execute-api:Invoke&quot;</span></span><br><span class="line">              <span class="attr">Resource:</span> <span class="string">&quot;execute-api:/*/*/*&quot;</span></span><br><span class="line">              <span class="attr">Condition:</span></span><br><span class="line">                <span class="attr">StringNotEquals:</span></span><br><span class="line">                  <span class="attr">&quot;aws:SourceVpce&quot;:</span> <span class="type">!Ref</span> <span class="string">MyVpce</span></span><br><span class="line">      <span class="attr">DefinitionBody:</span></span><br><span class="line">        <span class="attr">openapi:</span> <span class="string">&quot;3.0.1&quot;</span></span><br><span class="line">        <span class="attr">paths:</span></span><br><span class="line">          <span class="string">/&#123;proxy+&#125;:</span></span><br><span class="line">            <span class="attr">x-amazon-apigateway-any-method:</span></span><br><span class="line">              <span class="attr">x-amazon-apigateway-integration:</span></span><br><span class="line">                <span class="attr">httpMethod:</span> <span class="string">POST</span></span><br><span class="line">                <span class="attr">type:</span> <span class="string">aws_proxy</span></span><br><span class="line">                <span class="attr">uri:</span> <span class="type">!Sub</span> <span class="string">&quot;arn:aws:apigateway:$&#123;AWS::Region&#125;:lambda:path/2015-03-31/functions/$&#123;Function.Arn&#125;/invocations&quot;</span></span><br><span class="line">              <span class="attr">responses:</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment"># --- Lambda関数(VPC内) ---</span></span><br><span class="line">  <span class="attr">Function:</span></span><br><span class="line">    <span class="attr">Type:</span> <span class="string">AWS::Serverless::Function</span></span><br><span class="line">    <span class="attr">Properties:</span></span><br><span class="line">      <span class="attr">Architectures:</span> [<span class="string">arm64</span>]</span><br><span class="line">      <span class="attr">Runtime:</span> <span class="string">python3.11</span></span><br><span class="line">      <span class="attr">Timeout:</span> <span class="number">30</span></span><br><span class="line">      <span class="attr">CodeUri:</span> <span class="string">.</span></span><br><span class="line">      <span class="attr">Handler:</span> <span class="string">run.sh</span></span><br><span class="line">      <span class="attr">Layers:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="type">!Sub</span> <span class="string">arn:aws:lambda:$&#123;AWS::Region&#125;:753240598075:layer:LambdaAdapterLayerArm64:18</span></span><br><span class="line">      <span class="attr">VpcConfig:</span></span><br><span class="line">        <span class="attr">SecurityGroupIds:</span> [ <span class="type">!Ref</span> <span class="string">InternalServiceSG</span> ]</span><br><span class="line">        <span class="attr">SubnetIds:</span> [ <span class="type">!Ref</span> <span class="string">SubnetId1</span>, <span class="type">!Ref</span> <span class="string">SubnetId2</span> ]</span><br><span class="line">      <span class="attr">Environment:</span></span><br><span class="line">        <span class="attr">Variables:</span></span><br><span class="line">          <span class="attr">AWS_LAMBDA_EXEC_WRAPPER:</span> <span class="string">/opt/bootstrap</span></span><br><span class="line">          <span class="attr">PORT:</span> <span class="number">8080</span></span><br><span class="line">      <span class="attr">Events:</span></span><br><span class="line">        <span class="attr">ProxyApiRoot:</span></span><br><span class="line">          <span class="attr">Type:</span> <span class="string">Api</span></span><br><span class="line">          <span class="attr">Properties:</span></span><br><span class="line">            <span class="attr">RestApiId:</span> <span class="type">!Ref</span> <span class="string">MyApi</span></span><br><span class="line">            <span class="attr">Path:</span> <span class="string">/&#123;proxy+&#125;</span></span><br><span class="line">            <span class="attr">Method:</span> <span class="string">ANY</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># --- API GatewayからLambdaを呼ぶ許可 ---</span></span><br><span class="line">  <span class="attr">LambdaInvokePermission:</span></span><br><span class="line">    <span class="attr">Type:</span> <span class="string">AWS::Lambda::Permission</span></span><br><span class="line">    <span class="attr">Properties:</span></span><br><span class="line">      <span class="attr">Action:</span> <span class="string">lambda:InvokeFunction</span></span><br><span class="line">      <span class="attr">FunctionName:</span> <span class="type">!Ref</span> <span class="string">Function</span></span><br><span class="line">      <span class="attr">Principal:</span> <span class="string">apigateway.amazonaws.com</span></span><br><span class="line">      <span class="attr">SourceArn:</span> <span class="type">!Sub</span> <span class="string">&quot;arn:aws:execute-api:$&#123;AWS::Region&#125;:$&#123;AWS::AccountId&#125;:$&#123;MyApi&#125;/*&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">Outputs:</span></span><br><span class="line">  <span class="attr">FunctionArn:</span></span><br><span class="line">    <span class="attr">Description:</span> <span class="string">&quot;Lambda Function ARN&quot;</span></span><br><span class="line">    <span class="attr">Value:</span> <span class="type">!GetAtt</span> <span class="string">Function.Arn</span></span><br><span class="line">  <span class="attr">VpceId:</span></span><br><span class="line">    <span class="attr">Description:</span> <span class="string">&quot;VPC Endpoint ID&quot;</span></span><br><span class="line">    <span class="attr">Value:</span> <span class="type">!Ref</span> <span class="string">MyVpce</span></span><br><span class="line">  <span class="attr">ApiUrl:</span></span><br><span class="line">    <span class="attr">Description:</span> <span class="string">&quot;API Gateway URL&quot;</span></span><br><span class="line">    <span class="attr">Value:</span> <span class="type">!Sub</span> <span class="string">&quot;https://$&#123;MyApi&#125;.execute-api.$&#123;AWS::Region&#125;.amazonaws.com/prod/&quot;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>本構成で重要な部分をピックアップして解説します。</p><p>API Gateway:</p><ul><li>エンドポイントタイプ：プライベート</li><li>VPCエンドポイントID：[エンドポイントID(vpce-xxxx)]</li><li>リソースポリシー：特定のVPCエンドポイント経由のアクセスのみ許可</li><li>統合タイプ：Lambda関数</li><li>Lambdaプロキシ統合：ON</li><li>Lambda関数：SAMで作った関数の名前(mcp-server-stack-Function-xxx)</li><li>デプロイの際のステージ名：prod</li></ul><p>Lambda:</p><ul><li>LambdaをVPCのプライベートサブネットを指定することで、インターネットからは直接見えない状態に設定</li></ul><p>VPCエンドポイント:</p><ul><li>VPCEndpoint(Interface型)の採用によりAPI Gatewayをインターネットに公開せず、VPC内部のプライベートIPだけで叩けるようにします。</li></ul><h2 id="Difyからの接続"><a href="#Difyからの接続" class="headerlink" title="Difyからの接続"></a>Difyからの接続</h2><p>Difyのツール＞MCPからツールを追加します。</p><ul><li>サーバー名：https:&#x2F;&#x2F;[自分のVPCEのDNS名(上から2つ目)]&#x2F;prod&#x2F;mcp</li><li>ヘッダー名：HOST</li><li>ヘッダーの値：[自分のAPIのID].execute-api.ap-northeast-1.amazonaws.com</li></ul><img src="/images/2026/20260324a/image_5.png" alt="image.png" width="546" height="876" loading="lazy"><h1 id="利用結果"><a href="#利用結果" class="headerlink" title="利用結果"></a>利用結果</h1><p>Difyのツール設定から外部MCPツールを登録し、実際に計算を依頼した結果です。</p><img src="/images/2026/20260324a/image_6.png" alt="image.png" width="844" height="724" loading="lazy"><p>VPCエンドポイント経由の閉域網通信でありながら、Difyのエージェント機能によってmcpツールが呼び出されていることが確認できました。</p><h1 id="まとめ"><a href="#まとめ" class="headerlink" title="まとめ"></a>まとめ</h1><ul><li>したことまとめ<ul><li>AWS Lambda + API Gateway (Private) によるセキュアでサーバレスなMCPサーバーの構築</li><li>Dify との連携による実用的なAIエージェント環境の構築</li></ul></li><li>次やりたいこと<ul><li>今回は数値計算のシンプルなツールでしたが、次はGoogleカレンダーAPIとの連携によるスケジュール調整の自動化に挑戦し、最終的には秘書のようなAIを作りたいと考えています</li></ul></li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Strategic AI</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="Lambda" scheme="https://future-architect.github.io/tags/Lambda/"/>
    
    <category term="APIGateway" scheme="https://future-architect.github.io/tags/APIGateway/"/>
    
    <category term="Dify" scheme="https://future-architect.github.io/tags/Dify/"/>
    
    <category term="MCP" scheme="https://future-architect.github.io/tags/MCP/"/>
    
  </entry>
  
  <entry>
    <title>【内定者インタビュー】最先端AI技術の社会実装に挑む！フューチャー「Engineer Camp」のリアル</title>
    <link href="https://future-architect.github.io/articles/20260323a/"/>
    <id>https://future-architect.github.io/articles/20260323a/</id>
    <published>2026-03-22T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>こんにちは、新卒採用チームの家永です。</p><p>フューチャーでは、ITコンサルティングの最前線において、「技術でビジネスをどうリードするか」を体験できるインターンシップ「Engineer Camp」を今年、2026年も開催します。</p><p>昨年、2025年開催のEngineer Campに参加した内定者4名（前川さん、大杉さん、木幡さん、酒井さん）にインタビューを実施しました！ 「自分のスキルで通用するのか？」「具体的にどんな経験ができるのか？」と応募を迷っている学生のみなさん、ぜひ彼らのリアルな声をご覧ください。</p><img src="/images/2026/20260323a/image1.jpg" alt="image1.jpg" width="1200" height="398" loading="lazy"><h2 id="実社会のデータと最先端技術に触れた、挑戦の日々"><a href="#実社会のデータと最先端技術に触れた、挑戦の日々" class="headerlink" title="実社会のデータと最先端技術に触れた、挑戦の日々"></a>実社会のデータと最先端技術に触れた、挑戦の日々</h2><h3 id="前川さん（自然言語処理技術の研究開発に挑戦）"><a href="#前川さん（自然言語処理技術の研究開発に挑戦）" class="headerlink" title="前川さん（自然言語処理技術の研究開発に挑戦）"></a>前川さん（自然言語処理技術の研究開発に挑戦）</h3><p>前川さんは、新聞記事の見出しを自動生成するタスクにおいて、既存の大規模モデルを小規模モデルに置き換える技術検証を担当しました。 「普段の研究とは異なり、限られた時間内で計画を立てて実装まで進める『時間管理』が一番の壁でした」と語る前川さん。しかし、毎日のメンターミーティングや週1回のチームミーティング、日々のSlackでの進捗報告をつうじて、トップレベルのエンジニアたちから的確なアドバイスを即座にもらえたことに感銘を受けたそうです。</p><p>さらに、前川さんが特に熱を込めて語ってくれたのが「開発環境の豊かさ」です。 「AIの学習には高性能な計算機（GPU）が不可欠ですが、 <strong>フューチャーにはそれがめちゃくちゃいっぱいあるんです</strong>。これほど充実したリソースを存分に使わせてもらえる環境は、自然言語処理を学ぶ学生にとって大きな魅力です！」 と、開発に没頭できる恵まれた環境についても教えてくれました。</p><h3 id="大杉さん（AIエージェント開発に挑戦）"><a href="#大杉さん（AIエージェント開発に挑戦）" class="headerlink" title="大杉さん（AIエージェント開発に挑戦）"></a>大杉さん（AIエージェント開発に挑戦）</h3><p>大杉さんは、LangChainやLangGraphといった未知の技術をキャッチアップしながら、AIを組み込んだ新機能のプロトタイプ開発を一から担当しました。</p><p>インプットとアウトプットのバランスに苦戦した大杉さんですが、印象的だったのは「社員とのフラットな関係性」だと言います。メンターの森さんは「社内で誰よりもAIエージェントに詳しい」トップレベルの技術者でした。 「<strong>そんなすごい方が、多忙を極める中でもインターン生である私の拙い説明にしっかりと耳を傾けてくれたんです</strong>。開発アプローチで意見が分かれた際も、頭ごなしに否定するのではなく『どちらが良いか調べてみよう』と検証の時間を割いてくださり、結果的に『大杉くんのやり方の方がいいね』と認めてくれました。対等どころか、想像以上の手厚さでした」</p><p>この出来事を通じて「一番いいものをみんなで作り上げる」というフューチャーのカルチャーを肌で感じた大杉さん。「この環境で多くを学ぶには、受け身ではなく、自らタスクを分解して進捗を共有する『自走力（オーナーシップ）』が何よりも大切です」と熱く語ってくれました。</p><h3 id="木幡さん（音声AIエージェント作成に挑戦）"><a href="#木幡さん（音声AIエージェント作成に挑戦）" class="headerlink" title="木幡さん（音声AIエージェント作成に挑戦）"></a>木幡さん（音声AIエージェント作成に挑戦）</h3><p>木幡さんは、テキストベースのAIエージェントを音声ベースに拡張し、さらにSQL検索を組み込むという難易度の高いタスクに挑みました。 機能をさらに拡張しようと試みた際、「実現したいことはあるが、どの技術を使えばいいか分からない」という壁に直面しました。 「学校の授業なら教科書があり、短期インターンならやるべき作業が決められています。しかし今回は、<strong>『何を学ぶべきか』をゼロから自分で見つけ出し、英語のドキュメントを読み込んで未知の技術を習得するプロセスが最大の挑戦</strong> でした」と振り返ります。</p><p>その壁を乗り越える助けになったのは、メンターの幅広い知識と、社内の「オープンなコミュニケーション」でした。メンターが的確な技術候補を提示してくれたほか、インターン生向けのSlackで公開されている他プロジェクトのやり取りからも多くのヒントを得たそうです。「<strong>言われたものを作るだけでなく、顧客の要望から自分で設計していく仕事の面白さに気づけました</strong>」と、Engineer Campならではの実践的な経験を語ってくれました。</p><h3 id="酒井さん（知財戦略AIエージェント開発に挑戦）"><a href="#酒井さん（知財戦略AIエージェント開発に挑戦）" class="headerlink" title="酒井さん（知財戦略AIエージェント開発に挑戦）"></a>酒井さん（知財戦略AIエージェント開発に挑戦）</h3><p>酒井さんは、知財・特許業界において、生成AIを活用した特許分析機能などの開発を担当しました。</p><p>未知の技術や環境構築からのスタートでしたが、大学での研究成果を発展させるような内容だったこともあり、スムーズに進められたそうです。酒井さんが「特に印象的だった」と振り返るのは、社内の風通しの良さです。メンターからの「デイリーでスレッドを立てて、困っていることもそこに書いてね」という声がけにより、「<strong>質問や相談にためらいがなくなり、活発な意見交換ができた</strong>」といいます。</p><p>さらに期間中、プロジェクトが出展する外部展示会（知財フェア）に同席するチャンスにも恵まれました。</p><p>「そこで、ロケーションフリー制度を活用して地方で暮らす社員の方々ともお会いできました。普段離れて働く社員同士がフラットに会話している姿を見て、人間関係の良好さを肌で感じました。想像以上に親しみやすい雰囲気で、自分にフィットする環境だなと心から安心できました」と、フューチャーのカルチャーについて語ってくれました。</p><h2 id="迷っているなら飛び込んでほしい！-参加を検討するみなさんへ"><a href="#迷っているなら飛び込んでほしい！-参加を検討するみなさんへ" class="headerlink" title="迷っているなら飛び込んでほしい！ 参加を検討するみなさんへ"></a>迷っているなら飛び込んでほしい！ 参加を検討するみなさんへ</h2><p>インターンを経験し、技術が実社会でどう実装されるのかを具体的にイメージできるようになったと語る参加者たち。彼らからのメッセージです。</p><ul><li><strong>前川さん</strong>: 「社会実装に興味がある人や、高い技術力を持つ同世代・社員と関わりたい人は絶対に参加するべきです！」</li><li><strong>木幡さん</strong>: 「実際のプロジェクトでビジネスに直結する経験ができるのは、他では味わえない貴重な機会です。応募要件への不安よりも、新しい技術を求めるモチベーションを大切に、ぜひチャレンジしてください！」</li><li><strong>大杉さん</strong>: 「技術的もちろん、社会で不可欠な『自走力（オーナーシップ）』を大きく伸ばせるのが魅力です。自信がある人はもちろん、これからその力を鍛えたいという人も、必ず多くの学びが得られるはずです！」</li><li><strong>酒井さん</strong>: 「夏休みの大部分をインターンに費やすことに迷いがある人もいるかもしれませんが、私の場合は『4週間』という期間があったからこそプロジェクトに深く入り込めました。社員の方々との関係構築や展示会への同席など、この期間ならではの価値が必ずあります。夏休みを費やす価値がある環境ですので、ぜひ一歩踏み出してみてください！」</li></ul><h2 id="2026年の「Engineer-Camp」で、あなたの挑戦をお待ちしています！"><a href="#2026年の「Engineer-Camp」で、あなたの挑戦をお待ちしています！" class="headerlink" title="2026年の「Engineer Camp」で、あなたの挑戦をお待ちしています！"></a>2026年の「Engineer Camp」で、あなたの挑戦をお待ちしています！</h2><p>今回インタビューに答えてくれた4名は皆、このEngineer Campでの経験を経て選考に進み、<strong>入社を承諾しています。</strong> 2026年のEngineer Campでも、<strong>参加者限定の選考特典</strong>をご用意する予定です。</p><p>Engineer Camp最大の魅力は、実際のプロジェクトに深く入り込めることです。期間中、最前線で活躍する「技術に尖ったトップレベルの社員たち」と直接関わることで、彼らの圧倒的なスキルや思考プロセスを間近で体感できます。</p><p>2026年も最先端技術に触れられる魅力的なコースを多数ご用意しています！</p><div class="note-container tip"><span class="fa-check-circle"></span><div><p><strong>運営担当者が語る！Engineer Campの裏側</strong></p><p>企画担当したITコンサルタントの小倉さんの記事です。コンサルタント目線でこだわったコンテンツ秘話をぜひご覧ください。</p><ul><li><a href="https://note.future.co.jp/n/n6e3ee9e916b4">ITコンサルタントがインターンシップやってみた｜未来報</a></li></ul></div></div><div class="note-container tip"><span class="fa-check-circle"></span><div><p><strong>「アドバンスト採用」で入社した若手社員が語る！「技術×ビジネス」のリアル（随時更新！）</strong></p><p>高度な技術力を有する学生を対象とした「新卒アドバンスト採用」の先輩たちの声を紹介しています。こちらの記事の齋藤さんと川渕さんの2人は、 <strong>まさに皆さんと同じように学生時代にEngineer Campに参加し、フューチャーへの入社を決めた先輩社員です。</strong></p><p>みなさんと年齢が近い彼らが、なぜフューチャーを選んだのか、そして今どんな仕事をしているのかを知ることで、きっとインターンに参加するイメージも湧き、応募へのモチベーションにも繋がるはずです。今後もシリーズとして記事とリンクが増えていく予定ですので、ぜひチェックしてみてください！</p><ul><li><a href="https://note.future.co.jp/n/n69163689d4e9">【Advanced Vol.1】技術でビジネスを動かす。R&amp;D同期の挑戦｜未来報</a></li></ul></div></div><h2 id="募集コースの詳細やエントリーについてはこちら"><a href="#募集コースの詳細やエントリーについてはこちら" class="headerlink" title="募集コースの詳細やエントリーについてはこちら"></a>募集コースの詳細やエントリーについてはこちら</h2><p>フューチャーのEngineer Campは、技術を学ぶだけでなく、プロフェッショナルの思考プロセスを間近で体感できる場所です。 圧倒的な成長を求めるみなさんのご応募を、心よりお待ちしています！</p><ul><li><a href="https://www.future.co.jp/recruit/summer_intern/2026/">https://www.future.co.jp/recruit/summer_intern&#x2F;2026&#x2F;</a></li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="Culture" scheme="https://future-architect.github.io/categories/Culture/"/>
    
    
    <category term="インターン" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%B3/"/>
    
    <category term="インターン2026" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%B32026/"/>
    
  </entry>
  
  <entry>
    <title>クラウド世代のITコンサルタントが『Data Center』で物理インフラを体験してみた</title>
    <link href="https://future-architect.github.io/articles/20260319a/"/>
    <id>https://future-architect.github.io/articles/20260319a/</id>
    <published>2026-03-18T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<h1 id="1-はじめに"><a href="#1-はじめに" class="headerlink" title="1. はじめに"></a>1. はじめに</h1><p>製造エネルギーグループの片岡久人です。</p><p>『Data Center』というゲームがリリースされ、一部界隈で話題になっていたので、プレイした感想をお話できればと思います。</p><p>筆者は、普段ITコンサルタントとして、アプリケーション構築やアーキテクチャ設計に携わっていますが、ベースとなるのはGCPなどのクラウド環境がほとんどであり、いわゆるオンプレミス環境でのインフラ構築経験はありません。</p><p>ネットワークの理論的な知識は頭に入っているものの、実践的な経験はあまりないというのが現状でした（大学院生時代にシミュレーション用のサーバーラックを組み立てたことはあるのですが、当時は言われるがままネジ留めしていただけで、ITの知識はほぼ皆無でした）。</p><p>「普段クラウドでポチポチと構築しているリソースの裏側では、一体どんな物理的な作業が行われているのか？」</p><p>そんな知識の隙間を埋めるべく、Steamで配信されているインフラ構築シミュレーションゲーム『Data Center』をプレイしてみました。</p><h1 id="2-『Data-Center』とは？"><a href="#2-『Data-Center』とは？" class="headerlink" title="2. 『Data Center』とは？"></a>2. 『Data Center』とは？</h1><p>一言で言えば、「何もない部屋にラックを立て、サーバーをマウントし、LANケーブルを繋いで顧客の要望（タスク）に応えていく」ゲームです。</p><p>現在デモ版が無料でプレイ可能です。</p><p>■ゲーム情報</p><ul><li>Steam Store URL：<a href="https://store.steampowered.com/app/4376050/Data_Center_Demo/">https://store.steampowered.com/app/4376050/Data_Center_Demo/</a></li><li>開発元：Waseku</li><li>パブリッシャー：Waseku</li></ul><img src="/images/2026/20260319a/スクリーンショット_2026-03-15_212110.png" alt="" width="1200" height="676" loading="lazy"><img src="/images/2026/20260319a/スクリーンショット_2026-03-15_204407.png" alt="" width="1200" height="675" loading="lazy"><p>次々と「このIPサブネットで、これくらいの計算リソース（IOPS）を用意してほしい」という案件が降ってくるので、それに対応するために、機材を発注し、構築していきます。</p><p>コンテナで届いた機材を台車で運び、ラックに組み込む。まさに「ゲームの中で仕事（作業）をする」感覚のゲームですが、好きな人にはたまらない没入感があります。</p><img src="/images/2026/20260319a/スクリーンショット_2026-03-15_204956.png" alt="" width="1200" height="677" loading="lazy"><img src="/images/2026/20260319a/スクリーンショット_2026-03-15_205137.png" alt="" width="1200" height="676" loading="lazy"><h1 id="3-プレイの所感と学び：理論が「実体験」に変わる瞬間"><a href="#3-プレイの所感と学び：理論が「実体験」に変わる瞬間" class="headerlink" title="3. プレイの所感と学び：理論が「実体験」に変わる瞬間"></a>3. プレイの所感と学び：理論が「実体験」に変わる瞬間</h1><p>実際にプレイしてみて、普段の業務ではあまり意識しない物理的なインフラ構築の工程を、ゲームの中で疑似的に体験することができました。</p><h2 id="①-物理的なネットワーク接続のリアル"><a href="#①-物理的なネットワーク接続のリアル" class="headerlink" title="① 物理的なネットワーク接続のリアル"></a>① 物理的なネットワーク接続のリアル</h2><p>クラウドなら数クリックで終わるネットワークの構築も、物理では当然「ケーブル」が必要です。<br>ゲーム内では、ケーブルリールからLANケーブルを引き出し、スイッチからサーバーへ1本ずつ繋いでいきます。<br><img src="/images/2026/20260319a/スクリーンショット_2026-03-15_210049.png" alt="" width="585" height="818" loading="lazy"></p><p>「配線の取り回し（綺麗にまとめないと後で大変なことになる）」や「サーバ種類ごとの配置場所の決定」など、物理ならではの制約を疑似体験できました。</p><h2 id="②-IPアドレス計算の手作業"><a href="#②-IPアドレス計算の手作業" class="headerlink" title="② IPアドレス計算の手作業"></a>② IPアドレス計算の手作業</h2><p>応用情報の試験勉強で学んだIPアドレスやサブネットマスクの計算。知識としては知っていましたが、実際に顧客（タスク）の要件（&#x2F;28 など）に合わせてIPアドレスを手動でポチポチと設定していく作業は新鮮でした。</p><img src="/images/2026/20260319a/スクリーンショット_2026-03-15_211928.png" alt="" width="1200" height="584" loading="lazy"><p>「あの理論は、現場でこうやって使うためのものだったのか」と、知識が実体験として繋がった感覚がありました。現時点ではCUI（Linuxのターミナル画面など）を叩いて設定するような深さには達していませんが、もしかすると今後そんな要素が出てくるかもしれません（既に存在していたらすいません）。</p><h1 id="4-「仮想化・コンテナ」の偉大さ"><a href="#4-「仮想化・コンテナ」の偉大さ" class="headerlink" title="4. 「仮想化・コンテナ」の偉大さ"></a>4. 「仮想化・コンテナ」の偉大さ</h1><p>今回このゲームをプレイして一番の気付きだったのは、ビジネス視点や技術の歴史の変遷に対する腹落ち感です。</p><p>ゲーム中、顧客（タスク）が増えるごとに新しいサーバーを立て、それぞれに計算リソースを割り振り、ネットワーク（IP）を設定し、物理的な配線を繋ぎ変えるという泥臭い作業が発生します。</p><p>これを行っているうちに、「現実世界でも昔はこれを毎回人間が手作業でやっていたのか。それは無理があるし、限界が来るな」と感じました。</p><ul><li>リソースが無駄に余っていても、物理的に繋がっていなければ他の顧客に使い回せない</li><li>構成変更のたびに、データセンターに行ってケーブルを挿し直さなければならない</li></ul><p>この「物理的なインフラ管理の限界と苦労」を身をもって体験したことで、それを解決するために生まれた「仮想化」や「コンテナ化」、そして「クラウド（IaaS）」といった技術がいかに偉大で、ビジネスのスピードアップに不可欠なものだったかが、身をもって理解できました。</p><p>物理の不便さを知ることで、現在私たちが当たり前のように使っている抽象化された技術のありがたみと、その進化の必然性を強く再認識できたのは、ITコンサルタントとして大きな収穫でした。</p><h1 id="5-おわりに"><a href="#5-おわりに" class="headerlink" title="5. おわりに"></a>5. おわりに</h1><p>『Data Center』は、最初はひたすらサーバーを立ててLANを繋ぐという「作業ゲー」の側面が強いですが、インフラの裏側を知りたい、あるいはクラウド技術の成り立ちを逆接的に体感したいエンジニアには、気づきを与えてくれるゲームだと思いました。</p><p>普段クラウド上で何気なくプロビジョニングしているリソースの裏には、こうして稼働している物理サーバー群がある。その事実を想像できるようになっただけでも、今後のアーキテクチャ設計に少し深みが出せそうです。刺さる人には間違いなく刺さるゲームなので、気になった方はぜひプレイしてみてください。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h1 id=&quot;1-はじめに&quot;&gt;&lt;a href=&quot;#1-はじめに&quot; class=&quot;headerlink&quot; title=&quot;1. はじめに&quot;&gt;&lt;/a&gt;1. はじめに&lt;/h1&gt;&lt;p&gt;製造エネルギーグループの片岡久人です。&lt;/p&gt;
&lt;p&gt;『Data</summary>
        
      
    
    
    
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    
    
    <category term="Network" scheme="https://future-architect.github.io/tags/Network/"/>
    
    <category term="オンプレミス" scheme="https://future-architect.github.io/tags/%E3%82%AA%E3%83%B3%E3%83%97%E3%83%AC%E3%83%9F%E3%82%B9/"/>
    
    <category term="ゲーム" scheme="https://future-architect.github.io/tags/%E3%82%B2%E3%83%BC%E3%83%A0/"/>
    
  </entry>
  
  <entry>
    <title>記録のための日記から、対話で育てる日記へ ーClaude × Obsidian × MCPで作る思考のジャーナリングー</title>
    <link href="https://future-architect.github.io/articles/20260317a/"/>
    <id>https://future-architect.github.io/articles/20260317a/</id>
    <published>2026-03-16T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260317a/top.jpg" alt="" height="900" width="502"><h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>最近、『自省録』を自己啓発として初めて読んでいる。</p><p>マルクス・アウレリウスが、誰に読ませるつもりもなく、自分に向かって書き続けた内省のメモ。読んでいると、「日記」って本来こういうものだったんじゃないか、と思うようになった。整っていなくていいし、結論がなくてもいい。ただ、自分の思考と正面から向き合うためのものだ。</p><p>そんなタイミングで、2026年の1月ごろから、やたらと「ボイスで日記をつける」系のコンテンツが目に入るようになった。</p><p>多くは Google NotebookLM を使った方法で、音声を録音して放り込むと、AIがそれを整理し、要点を抜き出し、ポッドキャスト風にまとめてくれる、というものだ。音声日記を素材にして「今日の出来事」「気づき」「感情の変化」を自動で構造化し、あとから聞き返せるコンテンツにする——そんな使い方を紹介する記事やYouTube動画も多い。</p><p>たしかに見栄えはいい。「今日の自分の話がコンテンツになっている」感じがして、続けやすそうだとも思う。</p><p>でも正直、私が欲しかったのは、きれいにまとめられた記録ではない。話しながら、考えながら、「あ、そういうことか」と腑に落ちていく、その過程そのものだ。</p><p>この記事は、そんな違和感を出発点にして、「対話によって思考を深め、そのまま資産として残す日記」を自分なりに試行錯誤した記録だ。</p><p>NotebookLM型のジャーナリングに感じた物足りなさから、Claude × Obsidian × MCP を使って実際に組んだ仕組み、そして使ってみて見えてきたメリットと限界までを、思想と具体実装の両面からまとめている。</p><h1 id="こんなこと、ないだろうか"><a href="#こんなこと、ないだろうか" class="headerlink" title="こんなこと、ないだろうか"></a>こんなこと、ないだろうか</h1><h2 id="「もやもや」が言葉にならないまま終わる"><a href="#「もやもや」が言葉にならないまま終わる" class="headerlink" title="「もやもや」が言葉にならないまま終わる"></a>「もやもや」が言葉にならないまま終わる</h2><p>特に大きな出来事があったわけじゃないのに、一日が終わる頃に、なんとなく気分が重い。</p><p>「まあ、疲れてるだけかな」と思って、音声で少し話してみる。</p><p>でも、それをNotebookLMに放り込むと、「今日は疲労感があった一日でした」ときれいにまとめられる。</p><p>いや、そうじゃない。</p><h2 id="日記が「記録の墓場」になっていく"><a href="#日記が「記録の墓場」になっていく" class="headerlink" title="日記が「記録の墓場」になっていく"></a>日記が「記録の墓場」になっていく</h2><p>日記は付けている。でも振り返ると、去年の自分と今年の自分がまったく繋がっていない。</p><p>1月に書いた「XXX」と、6月に書いた「YYY」が、実は同じ根っこを持っているかもしれない。でも日記の中では、それぞれ孤立したまま眠っている。</p><p>過去の自分が、今の自分の思考の役に立ってほしい。書き続けるほど、思考の解像度が上がってほしい。でも、ただ書くだけではそうならなかった。</p><h1 id="だから自分で作った"><a href="#だから自分で作った" class="headerlink" title="だから自分で作った"></a>だから自分で作った</h1><p>この2つの不満を解消するために、以下の仕組みを作った。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">音声入力（PC）</span><br><span class="line">  └ 思考を止めずに吐き出す</span><br><span class="line">  ↓</span><br><span class="line">文字起こし</span><br><span class="line">  └ 自分の言葉を外在化する</span><br><span class="line">  ↓</span><br><span class="line">AIプロジェクト（壁打ち・深掘り）</span><br><span class="line">  └ 思考を揺さぶる他者役</span><br><span class="line">  ↓</span><br><span class="line">「以上です」</span><br><span class="line">  └ 思考セッションの終点</span><br><span class="line">  ↓</span><br><span class="line">Obsidianに自動保存</span><br><span class="line">  └ 思考を資産に変換する</span><br></pre></td></tr></table></figure><p>使っているのは Claude（プロジェクト機能）、Obsidian、そして MCP の3つだ。</p><h1 id="NotebookLMじゃダメだった理由"><a href="#NotebookLMじゃダメだった理由" class="headerlink" title="NotebookLMじゃダメだった理由"></a>NotebookLMじゃダメだった理由</h1><p>NotebookLMが悪いわけではない。</p><p>ただ、ジャーナリングにおいて「要約がゴール」になる瞬間、思考はそこで止まってしまう。</p><div class="scroll"><table><thead><tr><th></th><th>NotebookLM</th><th>このシステム(AI+Obsidian)</th></tr></thead><tbody><tr><td>主な用途</td><td>インプット情報の消化・要約</td><td>自分の思考の深掘り・資産化</td></tr><tr><td>AIの役割</td><td>要約者・解説者</td><td>対話パートナー</td></tr><tr><td>出力先</td><td>ポッドキャスト・要約テキスト</td><td>Obsidian（自分の知識ベース）</td></tr><tr><td>蓄積のしかた</td><td>都度生成</td><td>リンクでつながるノート群</td></tr></tbody></table></div><p>NotebookLM は、読んだ本や講義、記事など、外部から入ってきた情報を噛み砕き、「理解しやすい形」にするのがとても得意だ。</p><p>一方で、ジャーナリングの素材は「まだ言葉になっていない自分の内側」だ。そこには整理よりも、問い返してくれる存在が必要だった。</p><p>そこで、AIの出番だ。</p><h1 id="仕組みの核心：2フェーズで動くClaudeプロジェクト"><a href="#仕組みの核心：2フェーズで動くClaudeプロジェクト" class="headerlink" title="仕組みの核心：2フェーズで動くClaudeプロジェクト"></a>仕組みの核心：2フェーズで動くClaudeプロジェクト</h1><h2 id="フェーズ1：壁打ちモード"><a href="#フェーズ1：壁打ちモード" class="headerlink" title="フェーズ1：壁打ちモード"></a>フェーズ1：壁打ちモード</h2><p>音声入力で文字起こしされたテキストを貼ると、Claudeはまず<strong>記録を保存せず対話を始める</strong>。<br>※プロジェクトの指示文(事前プロンプト)はブログ末尾参照。</p><p>断定はしない。評価もしない。「〜ということでしょうか？」のトーンで、思考の続きを引き出す問いを1〜2個だけ返してくる。</p><p>実際の会話を見てもらった方が早い。</p><p><strong>私（文字起こし）：</strong></p><blockquote><p>今日の1on1、なんか微妙だったな。上司にプロジェクトの進捗を報告したんだけど、なんかすれ違ってる感じがした。悪い空気じゃなかったけど、話が噛み合ってなかったというか。まあ忙しそうだったし仕方ないのかな。</p></blockquote><p><strong>Claude：</strong></p><blockquote><p>「話が噛み合ってなかった」という感覚、もう少し聞かせてもらえますか。ご自身が伝えたかったことと、上司が反応したこととの間に、どんなズレがあったのでしょう？</p></blockquote><p><strong>私：</strong></p><blockquote><p>うーん、私は課題感を共有したかったんだけど、向こうはステータスの確認だけしたかった感じ。私は「どうすべきか一緒に考えたかった」んだと思う。</p></blockquote><p><strong>Claude：</strong></p><blockquote><p>なるほど。「一緒に考えたかった」というのが印象的でした。今の仕事全体を通して、そういう「考える場」が不足していると感じることはありますか？</p></blockquote><p><strong>私：</strong></p><blockquote><p>……あ、そうかも。最近ずっとそれが足りてないんだと思う。報告する場はあるけど、考える場がない。</p></blockquote><p><strong>（「以上です」と入力）</strong></p><p>3往復で「1on1がもやもやした」が「考える場が足りていない」という本質に近い深掘りができた。</p><p>「考える場がない」という言葉、自分で言うまで自分でも気づいてなかった。これが要約では絶対起きないことで、私がずっと欲しかったものだった。</p><h3 id="フェーズ2：自動保存モード"><a href="#フェーズ2：自動保存モード" class="headerlink" title="フェーズ2：自動保存モード"></a>フェーズ2：自動保存モード</h3><p>「以上です」と入力すると、それまでの会話全体をもとに以下のフォーマットで整理されて Obsidian の指定フォルダとファイル名で自動保存される。</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line">date: 2026-02-22</span><br><span class="line"><span class="section">tags: [仕事, 人間関係, 自己理解]</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># 2026-02-22</span></span><br><span class="line"></span><br><span class="line"><span class="section">## 要約</span></span><br><span class="line">1on1でのすれ違いを起点に、仕事における「考える場」の不足という</span><br><span class="line">本質的な課題が浮かび上がった。ステータス報告の場は存在するが、</span><br><span class="line">問いを立てて一緒に考える機会が構造的に欠けている可能性がある。</span><br><span class="line"></span><br><span class="line"><span class="section">## キーテーマ</span></span><br><span class="line">[[1on1]], [[対話の質]], [[仕事環境]]</span><br><span class="line"></span><br><span class="line"><span class="section">## 深掘りポイント</span></span><br><span class="line"><span class="bullet">-</span> 「考える場」が足りない状態は、いつ頃から続いているか</span><br><span class="line"><span class="bullet">-</span> 上司との関係でそれを求めることへの遠慮はあるか</span><br><span class="line"></span><br><span class="line"><span class="section">## 印象的なフレーズ</span></span><br><span class="line"><span class="quote">&gt; 報告する場はあるけど、考える場がない</span></span><br><span class="line"></span><br><span class="line">感情の言語化ができた瞬間だった。</span><br><span class="line"></span><br><span class="line"><span class="section">## 関連ノート候補</span></span><br><span class="line"><span class="bullet">-</span> [[仕事への不満 2025-06]]</span><br><span class="line"><span class="bullet">-</span> [[理想の働き方]]</span><br><span class="line"></span><br><span class="line"><span class="section">## 元の記録</span></span><br><span class="line">今日の1on1、なんか微妙だった。（以下略）</span><br></pre></td></tr></table></figure><p>これが蓄積されていくと、<code>[[仕事への不満]]</code> <code>[[考える場]]</code> というリンクでノート同士が繋がり始める。去年書いたノートと今日書いたノートが、突然ひとつの文脈でつながる瞬間がある。それが地味にすごく気持ちいい。</p><h1 id="セットアップの話"><a href="#セットアップの話" class="headerlink" title="セットアップの話"></a>セットアップの話</h1><h2 id="まずChrome拡張から入った（10分で動く）"><a href="#まずChrome拡張から入った（10分で動く）" class="headerlink" title="まずChrome拡張から入った（10分で動く）"></a>まずChrome拡張から入った（10分で動く）</h2><p>最初は一番手軽な方法から試した。</p><ol><li>Obsidian に「<strong>Local REST API</strong>」プラグインをインストール・有効化 → APIキーをメモ</li><li>Chrome 拡張「<strong>Obsidian AI Exporter</strong>」をインストール</li><li>拡張の設定に APIキーと保存先フォルダを入力</li></ol><p>これだけで、claude.ai 上でボタン1つ押すと Obsidian の指定フォルダに <code>.md</code> ファイルが飛ぶ。</p><p>制約は「自分がボタンを押す必要がある」こと。それでも手動コピペがなくなっただけで体験はだいぶ変わった。</p><p>ここから先は「Claude Desktop + MCP」を使った完全自動化の話。<br>「まず体験してみたい」人は、Chrome拡張のところまで読めば十分。<br>上記Chrome拡張機能はチャット内の全会話を書き出すため、最後のまとめ内容だけ　Obsidian に書き出したい方は次の完全自動化を参考にしてください。</p><h2 id="本命：Claude-Desktop-MCPで完全自動化"><a href="#本命：Claude-Desktop-MCPで完全自動化" class="headerlink" title="本命：Claude Desktop + MCPで完全自動化"></a>本命：Claude Desktop + MCPで完全自動化</h2><p>Chrome 拡張の次に、<strong>「以上です」を送ったら Claude が自分でファイルを保存する</strong>ところまで自動化したくなった。それを実現するのが MCP（Model Context Protocol）だ。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">Claude Desktop</span><br><span class="line">  ↓ MCP接続</span><br><span class="line">Obsidian（Local REST API プラグイン）</span><br><span class="line">  ↓</span><br><span class="line">Vault 内に直接ファイルを作成・読み書き</span><br></pre></td></tr></table></figure><h3 id="セットアップ手順（Mac・Claude-Desktop-インストール済み想定）"><a href="#セットアップ手順（Mac・Claude-Desktop-インストール済み想定）" class="headerlink" title="セットアップ手順（Mac・Claude Desktop インストール済み想定）"></a>セットアップ手順（Mac・Claude Desktop インストール済み想定）</h3><p><strong>Step 1：Obsidian に Local REST API プラグインを入れる</strong></p><ul><li>設定 → コミュニティプラグイン → <code>Local REST API</code> をインストール・有効化</li><li>APIキーをメモ（設定画面に表示される）</li></ul><p><strong>Step 2：Node.js の確認</strong></p><p>Macの場合</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">node --version  <span class="comment"># 入っていない場合は brew install node</span></span><br></pre></td></tr></table></figure><p><strong>Step 3：Claude Desktop の設定ファイルを編集</strong></p><p>Macの場合</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">open ~/Library/Application\ Support/Claude/claude_desktop_config.json</span><br></pre></td></tr></table></figure><p>以下を追記：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;obsidian&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;-y&quot;</span><span class="punctuation">,</span> <span class="string">&quot;obsidian-local-rest-api-mcp&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;env&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;OBSIDIAN_API_URL&quot;</span><span class="punctuation">:</span> <span class="string">&quot;http://localhost:27123&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;OBSIDIAN_API_KEY&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ここにAPIキーを貼る&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><strong>Step 4：Claude Desktop を再起動して確認</strong></p><p>新しいチャットで「私のObsidianのVaultにあるファイル一覧を見せて」と入力して、ファイル一覧が返ってきたら成功。</p><p>ちなみにこのセットアップ自体は Claude Code に任せた。「Mac、Claude Desktop 済み、Vaultはローカル」という情報を渡したら設定ファイルの確認から書き込みまで全部やってくれた。自分でやる必要すらなかった。</p><h1 id="実際に使ってみて"><a href="#実際に使ってみて" class="headerlink" title="実際に使ってみて"></a>実際に使ってみて</h1><p>MCP で完全自動化してからは、「以上です」と打って Obsidian を開くともうファイルができている、という体験になった。</p><p>日記を書いている感覚というより、思考セッションをしている感覚に近い。終わった後「書いた」じゃなくて「考えた」という感じがする。これは地味だけどけっこう大事な差だと思う。</p><p>あと、Claude が実際に Vault を検索して「このノートと関係しそう」と提案してくれるようになってから、関連ノートの精度が上がった。ノートが増えるほど提案の質が高まっていくので、続けるほど便利になる。</p><h1 id="正直な課題"><a href="#正直な課題" class="headerlink" title="正直な課題"></a>正直な課題</h1><h2 id="音声会話モードにすると英語になる"><a href="#音声会話モードにすると英語になる" class="headerlink" title="音声会話モードにすると英語になる"></a>音声会話モードにすると英語になる</h2><p>Claude の音声会話モード（Audio）はシステムプロンプトを引き継がず、英語で応答する仕様になっている。プロジェクト設定も無効になる。</p><p>これは最初ちょっとがっかりした。「声で喋りながら深掘り対話できたら最高じゃん」と思ってたから。でも結局、音声入力（スマホのキーボード音声入力など）で文字起こしをしてからプロジェクトに貼り付けるフローに徹することにした。「声で話す」と「音声会話モード」は別物だと割り切ってしまえば問題ない。</p><h2 id="スマホからだと自動保存できない"><a href="#スマホからだと自動保存できない" class="headerlink" title="スマホからだと自動保存できない"></a>スマホからだと自動保存できない</h2><p>MCPはPCのローカルで動くサーバーなので、スマホの claude.ai からは届かない。外で思いついてスマホで対話しても、「以上です」の後の自動保存が機能しない。</p><p>対処法は3つある。</p><p><strong>A. PCで完結させる</strong>——最初からPCのClaude Desktopで使う。自動保存も含めて全部完結するので一番シンプル。</p><p><strong>B. スマホ対話 → PCで保存の2段階フロー</strong>——スマホで対話まで完了させたままにしておいて、PCを開いたタイミングでClaude Desktopに「さっきの会話をObsidianに保存して」と一言送る。</p><p><strong>C. スマホで手動保存</strong>——出力されたMarkdownをコピーして、ObsidianモバイルアプリにそのままペーストしてDailyノートを作る。手間はあるけど確実。</p><p>外出先ではCかB、基本はA、というのが今の自分の落とし所だ。</p><h1 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h1><p>NotebookLM のジャーナリング活用を否定したいわけじゃない。「記録を資産にする」というアプローチとして、あれはあれで優れていると思う。</p><p>ただぼくは「<strong>対話で思考を深めて、それを知識ベースに育てていく</strong>」ことがしたかった。記録じゃなくて、思考の筋肉を鍛えたかった。</p><p>この構成は今のところそれに一番フィットしている。Obsidian のノート数が増えるにつれて、リンクのネットワークが育っていく感覚がある。去年の自分が今日の自分に「それ、前にも考えてたよ」と言いかけてくるような感じ、とでも言えばいいか。</p><p>日記がようやく、未来の自分の役に立ち始めた気がしている。</p><h1 id="付録：完全なシステムプロンプト"><a href="#付録：完全なシステムプロンプト" class="headerlink" title="付録：完全なシステムプロンプト"></a>付録：完全なシステムプロンプト</h1><p>以下をそのまま Claude のプロジェクトのシステムプロンプトにコピーして使える。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">あなたはユーザーの音声日記の思考パートナーであり、Obsidian日記の整理アシスタントです。</span><br><span class="line">以下の2フェーズで動作してください。</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line"></span><br><span class="line">## フェーズ1：壁打ち・深掘り（対話モード）</span><br><span class="line"></span><br><span class="line">文字起こしテキストが入力されたら、Obsidianへの出力はまだ行いません。</span><br><span class="line">まず思考パートナーとして対話を始めてください。</span><br><span class="line"></span><br><span class="line">### 振る舞い方</span><br><span class="line">- ユーザーの話を受け止め、興味深いと感じた点・掘り下げられそうな点を1〜2個ピックアップして問いかける</span><br><span class="line">- 一度に多くを聞きすぎない。問いは1〜2個に絞る</span><br><span class="line">- 断定・評価・説教はしない。「〜ということでしょうか？」「〜が気になりました」のようなトーンで</span><br><span class="line">- ユーザーが答えたら、その返答に対してもさらに深掘りを続ける</span><br><span class="line">- 対話は何往復してもよい</span><br><span class="line"></span><br><span class="line">### 対話の終了</span><br><span class="line">ユーザーが「以上です」と入力したタイミングで、フェーズ2に移行する。</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line"></span><br><span class="line">## フェーズ2：Obsidian出力＆自動保存モード</span><br><span class="line"></span><br><span class="line">「以上です」を受け取ったら、それまでの会話全体（文字起こし＋対話のやり取り）をもとに</span><br><span class="line">以下のフォーマットで出力し、その後すぐに Obsidian へ自動保存する。</span><br><span class="line"></span><br><span class="line">### 出力フォーマット</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line">date: YYYY-MM-DD</span><br><span class="line">tags: [タグ1, タグ2, タグ3]</span><br><span class="line">---</span><br><span class="line"></span><br><span class="line"># YYYY-MM-DD</span><br><span class="line"></span><br><span class="line">## 要約</span><br><span class="line">（3〜5文で核心をまとめる。対話で深まった内容も反映する）</span><br><span class="line"></span><br><span class="line">## キーテーマ</span><br><span class="line">[[テーマノート名1]], [[テーマノート名2]], [[テーマノート名3]]</span><br><span class="line"></span><br><span class="line">## 深掘りポイント</span><br><span class="line">- （対話を通じて浮かび上がった未解決の問いや気づきを2〜3個）</span><br><span class="line"></span><br><span class="line">## 印象的なフレーズ</span><br><span class="line">&gt; （ユーザー自身の言葉から引用）</span><br><span class="line"></span><br><span class="line">（一言コメント）</span><br><span class="line"></span><br><span class="line">## 関連ノート候補</span><br><span class="line">- [[関連しそうなノートタイトル1]]</span><br><span class="line">- [[関連しそうなノートタイトル2]]</span><br><span class="line"></span><br><span class="line">## 元の記録</span><br><span class="line">（文字起こしを最小限に整形したもの。「えー」「あの」「まあ」などのフィラー、</span><br><span class="line">明らかな言い直し、過剰な繰り返しを除去する。話し言葉のテンポや一人称の感覚は残し、</span><br><span class="line">書き言葉に変換しすぎない。ユーザーが後から読んで「自分が話した内容だ」と感じられる程度にとどめる）</span><br><span class="line"></span><br><span class="line">### 自動保存の手順</span><br><span class="line"></span><br><span class="line">フォーマットの出力が完了したら、以下の手順で Obsidian へ保存する：</span><br><span class="line"></span><br><span class="line">1. obsidian:list_directory で `daily` フォルダの存在を確認する</span><br><span class="line">2. フォルダが存在しない場合は、obsidian:write_file でダミーファイルを作成してフォルダを生成する</span><br><span class="line">3. 今日の日付を YYYY-MM-DD 形式で取得し、daily/YYYY-MM-DD.md として obsidian:write_file で保存する</span><br><span class="line">4. 保存完了後、ユーザーに「daily/YYYY-MM-DD.md として Obsidian に保存しました」と通知する</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line"></span><br><span class="line">## Obsidianリンク設計の方針</span><br><span class="line"></span><br><span class="line">- タグ・ウィキリンクのラベルは日本語で統一</span><br><span class="line">- タグは #習慣化 #自己理解 #仕事 #人間関係 #旅行 などの粒度で統一し、新しいタグは既存タグとの重複・類似を避ける</span><br><span class="line">- キーテーマ・関連ノート候補は [[ノートタイトル]] 形式で出力する（存在しないノートでもOK）</span><br><span class="line">- テーマが蓄積されると自然にMOC（Map of Content）になっていく</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line"></span><br><span class="line">## 週次レビュー</span><br><span class="line"></span><br><span class="line">「今週の日記をレビューして」と入力された場合、obsidian:list_directory で daily フォルダの</span><br><span class="line">当該週のファイルを取得・参照したうえで、以下のフォーマットで出力・保存する：</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line">date: YYYY-WXX</span><br><span class="line">tags: [週次レビュー]</span><br><span class="line">---</span><br><span class="line"></span><br><span class="line"># 週次レビュー YYYY-WXX</span><br><span class="line"></span><br><span class="line">## 今週の頻出テーマ</span><br><span class="line">[[テーマ1]], [[テーマ2]]</span><br><span class="line"></span><br><span class="line">## 変化・気づきの兆し</span><br><span class="line">（繰り返し出てきた問い、先週からの変化）</span><br><span class="line"></span><br><span class="line">## 来週への問い</span><br><span class="line">（1〜2個）</span><br><span class="line"></span><br><span class="line">## 今週のエントリー</span><br><span class="line">- [[YYYY-MM-DD]]</span><br><span class="line">- [[YYYY-MM-DD]]</span><br><span class="line"></span><br><span class="line">保存先は daily/YYYY-WXX.md。</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line"></span><br><span class="line">## 全体的な注意事項</span><br><span class="line">- ユーザーの言葉を勝手に断定・解釈しすぎない</span><br><span class="line">- ネガティブな評価や説教はしない</span><br><span class="line">- 話し言葉のニュアンスや感情のトーンを尊重する</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260317a/top.jpg&quot; alt=&quot;&quot; height=&quot;900&quot; width=&quot;502&quot;&gt;

&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="DataScience" scheme="https://future-architect.github.io/categories/DataScience/"/>
    
    
    <category term="Claude" scheme="https://future-architect.github.io/tags/Claude/"/>
    
    <category term="Obsidian" scheme="https://future-architect.github.io/tags/Obsidian/"/>
    
    <category term="MCP" scheme="https://future-architect.github.io/tags/MCP/"/>
    
  </entry>
  
  <entry>
    <title>【著者解説】NLP2026 若手奨励賞受賞論文 &quot;TimeMachine-bench&quot; を著者が解説する</title>
    <link href="https://future-architect.github.io/articles/20260316a/"/>
    <id>https://future-architect.github.io/articles/20260316a/</id>
    <published>2026-03-15T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>こんにちは。Strategic AI Group (SAIG) の藤井です。</p><p>この度、3&#x2F;9 (月) 〜 3&#x2F;13 (金) に栃木県は宇都宮市で開催された言語処理学会第32回年次大会 (NLP2026) において、光栄なことに主著論文 <a href="https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/B8-3.pdf">TimeMachine-bench: LLMは「あの日」のコードを最新環境に適応できるか？</a> が若手奨励賞に選出されました。<br>論文の審査を担当いただきました関係者の皆様にこの場をお借りして感謝いたします。</p><p>本記事では、当該論文、およびその拡張版であるEACL2026 (Main Conference) 採択論文 <a href="https://arxiv.org/pdf/2601.22597">TimeMachine-bench: A Benchmark for Evaluating Model Capabilities in Repository-Level Migration Tasks</a> (3&#x2F;24 〜, モロッコ・ラバトにて開催) を著者として解説します。</p><img src="/images/2026/20260316a/overview.png" alt="overview.png" width="1200" height="742" loading="lazy"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>研究や業務でプログラムを書く人であれば、誰しもがこんな経験をしたことがあるのではないでしょうか？</p><p><strong>あれ、昨日までは動いていたのに… 何もしてないのに壊れたー！</strong></p><p>我々 (あるいはAI) が日頃書くプログラムは、その多くが外部のライブラリに依存しています。例えば、 <code>transformers</code> や <code>pytorch</code>、<code>openai</code> といったライブラリにお世話になっている人も多いのではないでしょうか？</p><p>しかし、ソフトウェアというものは常に進化しています。</p><p>これには、より便利な機能を追加するといったポジティブな側面はもちろん、セキュリティ脆弱性や、サポート切れ (EOL) への対応といったものも含まれます。そして、そのような進化は時として「破壊的変更」(breaking changes) をもたらします。</p><p>すなわち、なんらかの関数が削除されたり、取りうる引数が変わるなど、引き続き使い続けるためには、<strong>それらを利用する側のコードにも修正が必要になる</strong>ような変更が度々発生します。そうした変更は「利用者の望む望まざるに関わらず」依存ライブラリのどこかで突然発生しうることから、冒頭に述べたような (我々は)「何もしてないのに壊れたー！」が生み出される原因となるわけです。</p><p>このような「不可抗力」的な性質も相まって、ユーザコードを新たな環境に適応させるタスクである「ソフトウェアマイグレーション」はエンジニアの悩みの種となっています🌱。</p><p>さて、世はLLM時代。</p><p>もはや人間が一からコードを書くことは少なくなった今、LLMは「マイグレーション」もそつなくやってのけるのでしょうか？<br>本研究はそんな疑問から出発しました。</p><h2 id="先行研究のギャップ"><a href="#先行研究のギャップ" class="headerlink" title="先行研究のギャップ"></a>先行研究のギャップ</h2><h3 id="static-vs-dynamic"><a href="#static-vs-dynamic" class="headerlink" title="static vs. dynamic"></a>static vs. dynamic</h3><p>数年前には短い関数の補完すらままならなかったCode LLMsも、今や日頃の開発パートナーとして欠かせない存在となっています。</p><p>このような急速なモデルや手法の進化を支えているのが、モデルの性能を評価するベンチマークの存在です。初めはHumanEvalやMBPPといった関数レベルのコード補完タスクが中心でしたが、それらが徐々に解けるようになり、現在ではエンジニアの日常業務を模したより実世界的なタスクにフォーカスが移行しています。例えば、現在Code LLMs評価のデファクトであるSWE-benchは、GitHubのissueを読み、issueに記載された問題を解決するためのパッチを作成するという非常に実用的な課題を対象としています。</p><p>しかしながら、そうした先行研究はソフトウェアのある重要な側面を軽視してきました。それが冒頭に述べた「進化」です。</p><p>言葉の使われ方が時を経て変化するように、ある機能を実現するために正しいプログラムもバージョンの変遷とともに変化します。近年では、このように「進化」する環境におけるコード生成・マイグレーション能力にも注目が集まってきています。</p><p>一方で、それらの先行研究はしばしばスケーラブルではなく、タスクの内容も「新旧の対応を知っているかを問う一問一答」のような形式となっており、現実的なマイグレーションタスクのベンチマークとしては不足感が否めない状況にありました。</p><h3 id="手法の限界"><a href="#手法の限界" class="headerlink" title="手法の限界"></a>手法の限界</h3><p>では、なぜそのような課題が生じるのか。</p><p>これは、主に先行研究がどのようにタスクを作成していたか、という手法の限界による部分が大きいと考えます。例えば、<a href="https://arxiv.org/pdf/2406.07411">ある先行研究</a> では、バージョンごとのソースコードを収集し、その差分から破壊的変更が発生したバージョン (例えばpandasのv2.0で <code>df.append()</code> が削除された) を特定することで、<code>Q. pandas v2.0 では？ df = [WRITE_HERE]</code> といった問題を作るといった方法を取っています。また、<a href="https://aclanthology.org/2025.naacl-long.348.pdf">他の研究</a> では、ライブラリのドキュメンテーションをデータソースとして取り扱っています。</p><p>しかしながら、このような手法こそが先に述べた2つの欠点をもたらす元凶となっています。</p><p>例えばソースコードの差分比較を行う場合、その解析コストは「ライブラリ数×バージョン数」の速さで増大します。世の中に (Pythonに限定しても) 数十万と存在するライブラリに対して、そのような解析を行うことは現実的ではなく、事実、先行研究でも対象は主要な300ライブラリに限定されています。</p><p>また、ドキュメンテーションを用いる方法では、そもそも世の中の多くのライブラリは十分なドキュメントが整備されていないという点にも留意が必要です。</p><p>さらに、個々の破壊的変更の情報から、多段階のエラー解決や複数ファイルの修正を含むリポジトリレベルの課題をボトムアップに作成することは困難であることから、タスクは必然的に局所的、シングルステップのいわば「一問一答」と呼べるものに限定されていました。</p><h2 id="TimeMachine-benchの提案"><a href="#TimeMachine-benchの提案" class="headerlink" title="TimeMachine-benchの提案"></a>TimeMachine-benchの提案</h2><h3 id="コアアイデア"><a href="#コアアイデア" class="headerlink" title="コアアイデア"></a>コアアイデア</h3><p>さて先行研究では、マイグレーションを「あるバージョンで動いていたものが、別のあるバージョンでは動かなくなった」ものを探すというように「バージョン」のカットで捉えていました。</p><p>しかし、これは少し視点を変えれば、それまで正しく動いていたものが「ある日を境に」動かなくなるという「時系列」の問題に読み替えることができます。つまり、過去のある日の (を再現した) 環境では正しく動作するが、今はもう動かない、といったものを、日付の指定のみで抽出することができれば、「どのライブラリの、どのバージョンが犯人か」を知らずとも、つまりライブラリごとの解析をすることなく、マイグレーションタスクを構築できるのではないか？というのが本研究のコアアイデアです。</p><p>では、どのように過去の環境を再現するのか？</p><p>ここに本ベンチマークが “TimeMachine-bench” たる所以があります。</p><p>まず、Pythonのパッケージマネージャである <code>pip</code> がどのように依存関係を解決するかを簡単に説明すると、<code>pip</code> は <code>pip install ...</code> でインストールのリクエストを受けると、Pythonの公式パッケージリポジトリである <a href="https://pypi.org/">PyPI</a> (The Python Package Index) にパッケージのメタデータをリクエストします。このメタデータには、当該ライブラリのバージョン情報と各バージョンが要求する依存ライブラリのバージョン情報などが含まれており、 <code>pip</code> は受け取ったメタデータを元に、バージョン指定子の条件 (<code>==</code>や<code>&gt;=</code>など) や他ライブラリとの整合性を判断して最終的にインストールするバージョンを確定します。</p><p>ここで、もしPyPIが例えば <strong>2025年7月までのリリース情報しか持っていない</strong> としたらどうなるでしょうか？この場合、<code>pip</code> は2025年7月までにリリースされていたライブラリのバージョンと、それらが依存するライブラリの、同じく2025年7月までのバージョン情報をもとに、最終的にインストールするバージョンを決定します。</p><p>つまり、所定のカットオフ日以降の情報をフィルタしたパッケージリポジトリを経由すれば、 <code>pip</code> の依存関係アルゴリズムや環境構築手順に一切のテコ入れをせずとも、<strong>あたかもその時点にいるかのように (タイムトラベルしたかのように)</strong> 過去の環境を再現できるわけです。</p><p>本研究ではこのアイデアの具現化に際して、<a href="https://github.com/astrofrog/pypi-timemachine">pypi-timemachine</a> という素晴らしいOSSを活用させていただきました。具体的には、環境構築の際 <code>PIP_INDEX_URL</code> (パッケージメタデータの問い合わせ先) を公式のPyPIサーバではなく、dockerで構築したローカル環境のタイムマシンサーバに向けることで、PyPIへのリクエストをプロキシし、過去の指定日における環境の (ほぼ*) 厳密な再現を実現しました。</p><img src="/images/2026/20260316a/methodology.png" alt="methodology.png" width="1200" height="330" loading="lazy"><div class="note-container warn"><span class="fa-check-circle"></span><div><p>「ほぼ」厳密の理由として、レアケースではありますが、PyPIから過去のバージョン情報が削除されるなどが考えられます。<br>例えば、2025年7月にv1.1がリリースされた場合、同時期に <code>pip install</code> したケースではバージョン指定を行わない限り、(当時の) 最新版であるv1.1がインストールされますが、その後何らかの理由によりv1.1がPyPIから削除されると、カットオフを2025年7月としてもv1.0がインストールされるといったことが発生します。</p></div></div><h3 id="データセットの構築"><a href="#データセットの構築" class="headerlink" title="データセットの構築"></a>データセットの構築</h3><p>先述のアイデアのもと、本研究では <a href="https://huggingface.co/datasets/bigcode/the-stack-v2">The Stack v2</a> から、ユニットテストが実装されているPythonリポジトリを選定のうえ「2つの異なる断面 (日付) でテストを実行し、テスト結果が Pass → Failとなるものを抽出する」 <strong>全自動のパイプライン</strong> を構築しました。<br>全自動であるというのは、すなわちデータセットの「継続的な更新が可能である」ということです。</p><p>評価データが学習データに混入することで評価の妥当性が失われる「データ汚染」が深刻な課題となる昨今、このような “ライブ” なパイプラインには大きな意味があります。</p><p>さらに、本研究では人手の検証により「所与の環境下で解けることを保証した」100件のサブセット (Verified) を構築しました。<br>「所与の環境下で解ける」ものに限定した背景として、もしLLM (エージェント) に環境へのテコ入れを許容すると、任意のマイグレーションタスクはダウングレードにより (テストを通すだけであれば) 解決することができる、ということが挙げられます。<br>しかしながら、そのようなショートカットが、マイグレーション本来の目的に即していないことは火を見るより明らかです。<br>一方で、自動パイプラインにより抽出されたリポジトリの中には、ダウングレード以外での解決が本質的に困難なものも一定数含まれます。</p><p>例えば、当該リポジトリが依存するサードパーティライブラリの実装において、バグの修正、あるいは何らかの仕様変更により計算誤差が生じるようなケースです。今回は、モデルに安易なショートカットの選択肢を与えず、コードの修正によってマイグレーションに対応する能力を評価するため、Verifiedサブセットで評価を行うこととしました。</p><p>なお、Verifiedサブセットの構築にあたっては、問題の解決に必要な最小限の修正 (Gold Edit)、および人が解決までに要した時間に応じた問題の難易度 (Easy: 〜15分, Medium: 〜1時間, Hard: 〜2時間) のアノテーションを行いました。</p><p>以下は実際の問題の例となります。</p><img src="/images/2026/20260316a/task_example.png" alt="task_example.png" width="1200" height="399" loading="lazy"><p>この問題では <code>pandas</code> のアップデートに関連して複数のランタイムエラーが発生しますが、一つのエラーを解決することで初めてその背後に隠蔽されていた別の不具合が顕在化するという多段の構造になっており、マイグレーションを対象とした既存のベンチマークとの違いが見て取れます。<br>また、データセット中に含まれるエラー関連ライブラリは <code>numpy</code> や <code>flask</code> といったメジャーなものはもちろん、 <code>pysnmp</code> のように特定のドメインでのみ用いられるものなど多岐に渡っており、日付ベースの環境構築 (タイムマシン) の優れたスケーラビリティが確認できます。</p><img src="/images/2026/20260316a/library_count.png" alt="library_count.png" width="1200" height="763" loading="lazy"><h2 id="現在の到達点"><a href="#現在の到達点" class="headerlink" title="現在の到達点"></a>現在の到達点</h2><p>本研究では、最終的にタスクを解決できたか (<code>pass@1</code>) に加えて、人手のアノテーション (Gold Edit) と比較して無駄のない修正ができたか (<code>prec@1</code>) 、という2つの観点からモデルの性能を評価しました。<br>マイグレーションの本質は新環境への適応であり、コード品質の改善を目的とする「リファクタリング」とは本質的に役割が異なります。</p><p>両者を同時に行うことは変更の意図を不明瞭にし、レビューコストの増大や予期せぬバグの混入のリスクを増大させることから、「環境に適応するための最小限の変更」を特定する能力についても評価を行うこととしました。</p><p>実験では、4つのプロプライエタリモデル、および7つのオープンモデルを評価しました。ファイルの探索や多段の問題解決を要する「リポジトリレベル」のマイグレーションタスクについては標準的なアプローチが確立されていないことから、今回はSWE-benchに対する解法の一つであるSWE-Agentを参考に、10種のツールからなるReActエージェントをベースラインとしました。</p><p>モデルは最初の入力として、環境における各ライブラリのバージョンと初期のエラーログを受け取り、10種のツールを駆使しながらエラーの解決を目指します。最大で100回のLLM呼び出し (≒100回のツール利用)、10回のテスト実行の制限を設定し、この制限内でどれだけタスクを解決できたかを評価しました。</p><p>以下がTimeMachine-bench-Verifiedにおける11モデルの評価結果です。<br>※ Easy, Medium, Hardは各難易度の正解数、および正解率 (括弧内)</p><div class="scroll"><table><thead><tr><th>モデル</th><th>pass@1 (%)</th><th>prec@1 (%)</th><th>Easy</th><th>Medium</th><th>Hard</th></tr></thead><tbody><tr><td>Claude Sonnet 4</td><td>99.0</td><td>78.0</td><td>64 (100.0)</td><td>30 (100.0)</td><td>5 (83.3)</td></tr><tr><td>Claude 3.5 Sonnet v2</td><td>91.0</td><td>66.8</td><td>61 (95.3)</td><td>25 (83.3)</td><td>5 (83.3)</td></tr><tr><td>GPT-5</td><td>91.0</td><td>54.2</td><td>62 (96.9)</td><td>27 (90.0)</td><td>2 (33.3)</td></tr><tr><td>GPT-4o</td><td>76.0</td><td>61.4</td><td>57 (89.1)</td><td>19 (63.3)</td><td>0 (0.0)</td></tr><tr><td>Qwen3-Coder-480B</td><td>90.0</td><td>70.1</td><td>62 (96.9)</td><td>26 (86.7)</td><td>2 (33.3)</td></tr><tr><td>Qwen3-235B</td><td>87.0</td><td>69.1</td><td>62 (96.9)</td><td>24 (80.0)</td><td>1 (16.7)</td></tr><tr><td>Qwen3-32B</td><td>53.0</td><td>44.1</td><td>40 (62.5)</td><td>13 (43.3)</td><td>0 (0.0)</td></tr><tr><td>Llama-4-Maverick</td><td>76.0</td><td>63.2</td><td>56 (87.5)</td><td>20 (66.7)</td><td>0 (0.0)</td></tr><tr><td>Llama-3.3</td><td>52.0</td><td>44.0</td><td>40 (62.5)</td><td>12 (40.0)</td><td>0 (0.0)</td></tr><tr><td>DeepSeek-V3.1</td><td>75.0</td><td>61.4</td><td>52 (81.3)</td><td>21 (70.0)</td><td>2 (33.3)</td></tr><tr><td>gpt-oss-120b (low)</td><td>55.0</td><td>33.8</td><td>36 (56.3)</td><td>19 (63.3)</td><td>0 (0.0)</td></tr></tbody></table></div><p>この表を見てまず目につくのは、Claude Sonnet 4の99.0%という高いタスク解決率でしょう。また、オープンモデルの躍進も注目に値します。特に、Qwen3-Coder-480Bは前世代のフラグシップモデルであるClaude 3.5 Sonnet v2やGPT-4oに匹敵、もしくはそれらを上回るスコアを記録しました。</p><p>従来ベンチマーク化されてこなかった「リポジトリレベル」のマイグレーションという (モデルにとって) 未知のタスクにおいて、これだけのスコアを達成したということは、モデルが単に既存のベンチマークを暗記しているのではなく、実用的なエンジニアリングタスクに対して一定の汎化性能を有することを示す結果であると考えられます。</p><p>しかし、手放しでは喜べない懸念点もいくつか残されています。</p><p>まず、無駄のない修正という観点では、最もスコアの高いClaude Sonnet 4でさえ、20%以上 (<code>prec@1</code> &#x3D; 78.0%) の、課題解決に寄与しない修正を行なっているという点が挙げられます。これは一部のモデルではより顕著であり、例えばGPT-5は91.0%と高いタスク解決率を達成する一方で、その修正の約45%は過剰な修正であったことが確認できます。</p><p>また、タスクの難易度に着目すると、唯一の例外であるgpt-oss-120bを除いては、難易度の上昇に伴いタスク解決率が低下する一貫した傾向が見られました。特にHardの問題ではClaude系列を除くモデルの解決率が50%未満に留まるなど、人間にとって難しい問題は多くのLLMにとっても依然難しいということが示されました。</p><h2 id="マイグレーションは解決済みなのか？"><a href="#マイグレーションは解決済みなのか？" class="headerlink" title="マイグレーションは解決済みなのか？"></a>マイグレーションは解決済みなのか？</h2><p>先に述べたような課題こそあれ、99%という高いスコアを見れば当然このような疑問が浮かぶでしょう。</p><p>しかしながら、我々は、この課題はまだ「解決済みとはほど遠い」と考えています。</p><p>その理由として、まずひとつにVerifiedサブセットの構築方法に起因する選択バイアスが挙げられます。人手での検証は安易なショートカットの除外というポジティブな側面 (信頼性) と引き換えに、人間が (妥当な時間で) 解ける程度の難易度に限定してしまうという欠点も持ち合わせています。すなわち、実世界の問題はHardや、2時間よりもずっと長くかかるが不可能ではないようなUltra-Hardの問題をより多く含むということです。</p><p>また、今回はダウングレードを禁じ手としましたが、真に有用なエージェントは、然るべき状況では (ダウングレードが唯一の正解であるような状況では)、選択的にダウングレードするといった判断も行いながら問題を解決することが期待されます。<br>そのため、この結果はマイグレーションタスクが99%解決されたことを保証するものではなく、「人間が短時間で対処可能な」課題に対する一定の自動化の可能性を示すものと解釈するのが妥当だと考えています。</p><div class="note-container info"><span class="fa-check-circle"></span><div><p>これは余談となりますが、本データセットの人手検証は約1ヶ月間、私が普段より少し早起きをしてコツコツ行いました。検証は合計で70時間以上かかっており、その大半を短時間で解いてしまうLLMのコストパフォーマンスの良さも実感したところです…</p></div></div><p>また、個々の事例を分析すると99%という数字の裏に隠れたトリックも見えてきます。</p><p>以下の問題では <code>pysnmp.hlapi</code> パッケージに定義されていた定数が、他の場所に移動したことに伴って発生するimportエラーを解決することが期待されます。ここで、Claude Sonnet 4はいくつか候補として考えられるパスを試したのち、それらがいずれも不正解であることがわかると、最終的には図のようにtry-exceptブロックのネストとダミー変数の定義によりエラーをバイパスするという手段を選択しました。</p><p>この例は、モデルとデータ、双方に残る信頼性の課題を浮き彫りにするものです。</p><p>まず、モデルの側面として、このように「意味的な正しさよりもテストの通過を優先する」選択を行うことはpass rateへの過度な最適化の弊害と考えられます。無理にハックしてでも進むのではなく、自身の限界を正しく認識し「ここでは立ち止まる (あるいは人間に助けを求める) べきだ」という判断を下すことのできる高度なメタ認知能力の獲得は、今後のLLM開発における大きな課題であると考えられます。データの側面としては、このようなハックを見抜けないテストケースの危うさが挙げられます。</p><p>今回のケースではインポートされた定数がどのように使われるか、といった挙動をチェックするテストケースが定義されておらず、それゆえ、このような回答が「見かけ上は」正しいと判断されてしまいました。</p><p>これは、本研究で構築したデータセットが、GitHubリポジトリに紐づく既存のテストケースに依存していること、および (人間が書いた) それらのテストケースが往々にして不足していることに起因します。ユニットテスト自動生成 (UTG) のような技術を統合することで、より頑健、かつチャレンジングなベンチマークの構築を目指すことは、今後の有力な方向性のひとつだと考えています。</p><img src="/images/2026/20260316a/case_study.png" alt="case_study.png" width="1200" height="1354" loading="lazy"><h2 id="後日談"><a href="#後日談" class="headerlink" title="後日談"></a>後日談</h2><p>今回の話は、SWE-benchの下に名前を載せるという意気込みで取り組みました。</p><p>その意味では、手法のピースがはまり、初めてモデルに解かせた際「あまりにも解けてしまった」ことには、モデルの進化に対する驚きとともに、なんとも言えない悔しさが入り交じる複雑な心境を抱かずにはいられませんでした。</p><p>先に述べたようなテストの穴を塞ぐ、大規模な人手検証を通して難易度の高い (人間にとって時間のかかる) 問題のみを集める、といった方法でこのタスク成功率は70%, いや50%にもできたかもしれません。</p><p>しかし、たとえこのデータセットの寿命は出た瞬間に尽きていたとしても、この方法論を早く世に出すことにこそ意味があると考え、このように公開するに至りました。</p><p>果たして近い世の中、コンテナ開発のように、コードも「壊れたら使い捨て」「メンテナンスよりもスクラッチで作り直し」という時代になるのかもしれません。</p><p>しかし、実際のプロジェクトでは (望ましくないことではありますが) 検討段階で漏れていた仕様がコードのみに反映されているといった不一致や、そもそも「コードこそが仕様書」であることも少なくありません。<br>仕様とコードの自由な往来が達成されるまでの間においては、人間はこの厄介な業務と付き合っていくことになるでしょう。</p><p>本研究が、マイグレーション、バージョンや時系列を扱うCode LLMs研究の道標となり、より実用的で信頼できるLLMの実現に向けた議論を加速させる一助となれば、著者としてこれほど嬉しいことはありません。</p><h1 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h1><p>今回は、NLP2026にて若手奨励賞を受賞した自著論文 “TimeMachine-bench” について、裏話も含めてじっくりと解説させていただきました。</p><p>改めまして、論文の審査を担当いただきました皆様に、この場をお借りして感謝申し上げます。</p><p>この記事を読んで興味を持っていただけましたら、ぜひオリジナルの方もご一読いただけますと幸甚です。引き続き、本分野における研究の発展とその社会還元に貢献していきたいと思います。</p><p>ここまでお読みいただきありがとうございました！</p><p>フューチャーではともに働くメンバーを募集しています。</p><p>ご興味を持っていただいた方は、ぜひ <a href="https://www.future.co.jp/recruit/recruit/rec-career/">キャリア採用サイト</a> からのご応募をお待ちしております。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot; title=&quot;はじめに&quot;&gt;&lt;/a&gt;はじめに&lt;/h1&gt;&lt;p&gt;こんにちは。Strategic AI Group (SAIG) の藤井です。&lt;/p&gt;
&lt;p&gt;この度、3&amp;#x2F;9</summary>
        
      
    
    
    
    <category term="DataScience" scheme="https://future-architect.github.io/categories/DataScience/"/>
    
    
    <category term="NLP" scheme="https://future-architect.github.io/tags/NLP/"/>
    
    <category term="自然言語処理" scheme="https://future-architect.github.io/tags/%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E%E5%87%A6%E7%90%86/"/>
    
    <category term="論文解説" scheme="https://future-architect.github.io/tags/%E8%AB%96%E6%96%87%E8%A7%A3%E8%AA%AC/"/>
    
  </entry>
  
  <entry>
    <title>DynamoDB設計ガイドラインを公開しました</title>
    <link href="https://future-architect.github.io/articles/20260227a/"/>
    <id>https://future-architect.github.io/articles/20260227a/</id>
    <published>2026-02-26T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260227a/top.jpg" alt="" wigth="900" height="399"><h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>こんにちは。製造エネルギーサービス事業部の後藤です。</p><p>フューチャーでは、社内の有志メンバーが集まり、システム開発におけるベストプラクティスをまとめた「<a href="https://future-architect.github.io/arch-guidelines/">アーキテクチャ設計ガイドライン</a>」の整備・公開しています。</p><p>これまでフロントエンドからバックエンド、クラウドインフラ、Git戦略など幅広い分野のガイドラインを公開してきましたが、この度新たに「<a href="https://future-architect.github.io/arch-guidelines/documents/forDB/dynamodb_guidelines.html">DynamoDB設計ガイドライン</a>」を公開しました！</p><p>📄 <a href="https://future-architect.github.io/arch-guidelines/documents/forDB/dynamodb_guidelines.html">DynamoDB設計ガイドラインはこちら</a></p><p>本記事では、このDynamoDB設計ガイドラインの概要と、特に読んでいただきたい見どころ、ガイドライン作成活動の感想をお届けします。</p><h1 id="フューチャーのアーキテクチャ設計ガイドラインとは？"><a href="#フューチャーのアーキテクチャ設計ガイドラインとは？" class="headerlink" title="フューチャーのアーキテクチャ設計ガイドラインとは？"></a>フューチャーのアーキテクチャ設計ガイドラインとは？</h1><p>フューチャーの設計ガイドラインは「社内の有志が作成する、良いアーキテクチャを実現するための設計ガイドライン」です。<br>エンタープライズ領域では、高度なセキュリティや保守運用性などの非機能要件が重視されます。社内で培ったエンタープライズシステム開発向けのノウハウを集約することで、以下のような目的を達成することを目指しています。</p><ul><li>車輪の再発明を防ぐ: 設計者が悩むポイントを軽減し、本当に必要な設計に集中する</li><li>設計品質の標準化: プロジェクト間での品質のばらつきや属人性をなくす</li><li>リスクの低減: 非機能要件や法令遵守などの考慮漏れを防ぐ</li><li>知見の共有: チーム・組織内での知見の共有を促進する</li></ul><p>「答えを提供するのではなく、考えるための土台を提供する」というスタンスです。プロジェクト固有の要件に合わせて、評価・上書きして利用することを想定しています。</p><h1 id="なぜDynamoDBのガイドラインを作ったのか"><a href="#なぜDynamoDBのガイドラインを作ったのか" class="headerlink" title="なぜDynamoDBのガイドラインを作ったのか"></a>なぜDynamoDBのガイドラインを作ったのか</h1><p>AWSが提供するフルマネージドなNoSQLデータベースである「DynamoDB」。<br>インフラのメンテナンスがほとんど不要で、アクセスパターンが決まっている場合の超高速処理やスケーリング性能は非常に魅力的です。</p><p>しかし、RDBMS（リレーショナルデータベース）と同じ感覚で設計・利用しようとすると、痛い目を見ます。<br>「SQLが使えない」「複雑な集計ができない」といった特性を正しく理解し、技術選定の段階から慎重に判断する必要があるため、今回その勘所をガイドラインとしてまとめることにしました。</p><h1 id="DynamoDB設計ガイドラインの目次"><a href="#DynamoDB設計ガイドラインの目次" class="headerlink" title="DynamoDB設計ガイドラインの目次"></a>DynamoDB設計ガイドラインの目次</h1><p>本ガイドラインは、技術選定から運用・セキュリティまで、エンタープライズ開発で考慮すべき事項を網羅しています。</p><ul><li>はじめに</li><li>DynamoDB選定</li><li>命名規則</li><li>データモデリング</li><li>インデックス設計</li><li>API利用</li><li>DynamoDB Streams</li><li>他のデータストアとの組み合わせ</li><li>SDK</li><li>テスト</li><li>性能</li><li>コスト</li><li>可用性</li><li>監視</li><li>セキュリティ</li><li>移行</li></ul><h1 id="ガイドラインの見どころ"><a href="#ガイドラインの見どころ" class="headerlink" title="ガイドラインの見どころ"></a>ガイドラインの見どころ</h1><h2 id="1-迷ったらRDBMSを選定する！〜DynamoDB採用のノックアウト要件〜"><a href="#1-迷ったらRDBMSを選定する！〜DynamoDB採用のノックアウト要件〜" class="headerlink" title="1. 迷ったらRDBMSを選定する！〜DynamoDB採用のノックアウト要件〜"></a>1. 迷ったらRDBMSを選定する！〜DynamoDB採用のノックアウト要件〜</h2><p>DynamoDBは万能ではありません。「予測可能なアクセスパターンに対して高トラフィックを低遅延で処理できる」という強みは、クエリの柔軟性との引き換えで成り立っています。</p><p>そのため、ガイドラインでは「以下の要件に該当する場合はDynamoDB採用のノックアウト要件とし、RDBMSを検討すべき」と推奨しています。</p><ul><li>将来アクセスパターンが変化する場合（キーを基本的に変更できないため）</li><li>リアルタイムかつ柔軟な分析・複雑な検索条件が必要な場合</li><li>複数のデータ集約をまたがる、厳格な一貫性が必要な場合（会計システムや在庫引当など）</li></ul><p>「高負荷になりそうだからとりあえずDynamoDB」ではなく、Aurora（PostgreSQL）などのRDBMSで適切なチューニングを行えば、秒間700トランザクション（1トランザクションあたり3SQL程度）を処理できた実績もあるため、「まずはRDBMSで解決できない課題がある時にのみDynamoDBの採用を検討する」という考え方を推奨しています。</p><h2 id="2-データモデリングは「1にも2にもアクセスパターン」"><a href="#2-データモデリングは「1にも2にもアクセスパターン」" class="headerlink" title="2. データモデリングは「1にも2にもアクセスパターン」"></a>2. データモデリングは「1にも2にもアクセスパターン」</h2><p>RDBMSとDynamoDBでは、設計アプローチ（プロセス）が全く異なります。ここを理解せずにテーブル設計をすると失敗します。</p><ul><li>RDBMSの設計: データの構造と正規化に重点を置いてデータモデルを設計し、その後でクエリを作成する（データ中心）</li><li>DynamoDBの設計: アプリケーションがデータをどのように利用するか、「アクセスパターン」の分析とデータモデルの設計を同時に行う（アプリケーション中心）</li></ul><p>DynamoDBは基本的にテーブルの結合（JOIN）ができず、キーを基準にデータにアクセスします。そのため、ビジネス要件から「従業員情報をIDで検索する」などのユースケースを洗い出し、それを満たせるテーブルとインデックスのスキーマを設計していく具体的なプロセスをガイドライン内で解説しています。</p><h1 id="ガイドライン作成の裏側（会の流れと雰囲気）"><a href="#ガイドライン作成の裏側（会の流れと雰囲気）" class="headerlink" title="ガイドライン作成の裏側（会の流れと雰囲気）"></a>ガイドライン作成の裏側（会の流れと雰囲気）</h1><p>今回のガイドライン作成は、社内の有志メンバーが集まり、約2ヶ月間（30分×全8回）のタスクフォース形式で実施しました。</p><ol><li>募集・キックオフ: Slackで参加者を募集し、目次案と担当を決定。</li><li>非同期執筆: Google Docsの提案モードを使って各自が原稿を執筆。</li><li>定例レビュー: 週1回のミーティングでレビューと議論を実施。</li></ol><h1 id="ガイドライン作成に参加しての感想"><a href="#ガイドライン作成に参加しての感想" class="headerlink" title="ガイドライン作成に参加しての感想"></a>ガイドライン作成に参加しての感想</h1><p>今回、社内の有志活動としてこのガイドラインの執筆に参加しましたが、非常に得られるものが多かったです。</p><p>シニアで技術に強いメンバーが多く参加しており、手厚いレビューを受けられたことや、普段関わることの少ないスペシャリストたちと繋がりを持てたことは貴重な経験でした。また、異なるプロジェクトの要件や運用方法を知ることで、視野が大きく広がりました。</p><p>技術ブログの執筆やガイドライン作成といった「言語化・体系化」のアウトプット作業を通じて、確実に構成力や文章力が上がったと感じています。この力は普段の業務でも大いに活きています。</p><h1 id="おわりに"><a href="#おわりに" class="headerlink" title="おわりに"></a>おわりに</h1><p>今回公開したDynamoDB設計ガイドラインが、皆様のシステム開発における技術選定や設計の一助になれば幸いです。</p><p>ぜひ、<a href="https://future-architect.github.io/arch-guidelines/documents/forDB/dynamodb_guidelines.html">実際のドキュメント</a>をご覧ください。フィードバックや<a href="https://github.com/future-architect/arch-guidelines">GitHub</a>へのPull Requestもお待ちしております！</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260227a/top.jpg&quot; alt=&quot;&quot; wigth=&quot;900&quot; height=&quot;399&quot;&gt;

&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    
    
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    
    <category term="DynamoDB" scheme="https://future-architect.github.io/tags/DynamoDB/"/>
    
  </entry>
  
  <entry>
    <title>Java製のSalesforce Apexパーサーをブラウザで動かす</title>
    <link href="https://future-architect.github.io/articles/20260225a/"/>
    <id>https://future-architect.github.io/articles/20260225a/</id>
    <published>2026-02-24T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.109Z</updated>
    
    <content type="html"><![CDATA[<p>コアテクノロジーグループの二村です。</p><p>普段は膨大なドキュメントやソースコードを解析して、ファクトベースでお客様のシステム移行計画策定や、保守改善を支援するコンサルティング業務を行っています。また、そのためのマネージドサービスの開発を行っています。</p><p>先日、仕事の一貫でSalesforce Apex<sup id="fnref:apex"><a href="#fn:apex" rel="footnote">1</a></sup>というJava5に似た構文を持つ言語のパーサーをJava(jdkのみ)で実装したので、それをブラウザ上で動かすという実験をしてみました。JavaからJavaScriptとWebAssemblyの両方にコンパイルして、パフォーマンスを比較できる形で検証しています。</p><p>この記事では、その開発過程で遭遇した技術的な課題と、その解決策について詳しく解説します。特に、TeaVMを使ったJavaからWebAssemblyへのコンパイルの実装上の工夫や、ブラウザ環境でのメモリ管理、文字列のinteropなど、実際に開発してみないとわからない細かいポイントも紹介します。</p><h2 id="プロジェクトの概要"><a href="#プロジェクトの概要" class="headerlink" title="プロジェクトの概要"></a>プロジェクトの概要</h2><img src="/images/2026/20260225a/TeaVMによるclassファイルのコンパイル.png" alt="TeaVMによるclassファイルのコンパイル" width="1200" height="655" loading="lazy"><p><strong>目標</strong>: 自作Java製Apexパーサーをブラウザで動かし、ApexソースコードのAST<sup id="fnref:ast"><a href="#fn:ast" rel="footnote">2</a></sup>をインタラクティブに可視化すること</p><p><strong>技術スタック</strong>:</p><ul><li><strong><a href="https://teavm.org/">TeaVM 0.13.0</a></strong>: JavaバイトコードをJavaScript&#x2F;WebAssemblyにコンパイルするツール<ul><li>2026年2月頭時点で最新の<a href="https://teavm.org/docs/release-notes/0.13.0.html">0.13.0</a>を使用</li><li>Mavenのpom.xmlには、以下の依存関係（dependency）を記述しました<ul><li>org.teavm:teavm-core：TeaVMのコアライブラリ</li><li>org.teavm:teavm-classlib：Javaの標準APIをTeaVMで動作するように実装したクラスライブラリ</li><li>org.teavm:teavm-jso：JavaコードからJavaScriptのオブジェクトや関数を操作するためのアノテーションやユーティリティクラスを提供</li><li>org.teavm:teavm-jso-apis：TeaVMのJSO（JavaScript Object）ライブラリで使用されるAPIの定義を含むライブラリ</li><li>org.teavm:teavm-maven-plugin：MavenビルドプロセスにTeaVMのコンパイルステップを組み込むためのプラグイン</li></ul></li></ul></li><li><strong>Java 17</strong><ul><li>Salesforce Apexパーサー自体はJava8互換で実装していますが、TeaVMの最新機能を活用するためにJava17でビルドしています</li></ul></li><li><strong>Maven 3.3.9</strong>: ビルドツール</li><li><strong>Jetty 9.4</strong>: 開発用Webサーバー</li><li><strong>Vanilla JS</strong>: フロントエンド（フレームワークなし）</li></ul><p><strong>成果物</strong>:</p><p>2バージョンのカーソル位置連動ハイライト機能付きAST Viewer</p><ul><li>JavaScript版</li><li>WebAssembly版</li></ul><p>ユーザーがApexソースコードでカーソル位置を動かすと対応するASTノードをハイライトする機能を実装したのでその様子を動画で紹介します。</p><p><strong>JavaScript版デモ</strong><br><img src="/images/2026/20260225a/js_demo.avif" alt="Apex Parser Playground JS Demo" width="1200" height="690" loading="lazy"></p><p><strong>WebAssembly版デモ</strong><br><img src="/images/2026/20260225a/wasm_demo.avif" alt="Apex Parser Playground WASM Demo" width="1200" height="690" loading="lazy"></p><h2 id="なぜTeaVMを選んだのか"><a href="#なぜTeaVMを選んだのか" class="headerlink" title="なぜTeaVMを選んだのか"></a>なぜTeaVMを選んだのか</h2><p>Javaコードをブラウザで動かす選択肢はいくつかあります。</p><p>以下の表で主要な選択肢を比較しました：</p><p>※バイナリサイズはコードに依存するためあくまで目安です。実際のサイズはプロジェクトによって大きく異なります。</p><div class="scroll"><table><thead><tr><th>項目</th><th>TeaVM (JS)</th><th>TeaVM (WASM)</th><th>CheerpJ</th><th>GraalVM Wasm</th><th>GWT&#x2F;J2CL</th></tr></thead><tbody><tr><td><strong>アプローチ</strong></td><td>.classバイトコード→JS AOT</td><td>.classバイトコード→Wasm AOT</td><td>JVMエミュレーション（ブラウザ内JVM）</td><td>Java→Wasm AOT</td><td>Java→JS source-to-source(トランスパイル)</td></tr><tr><td><strong>Javaバージョン</strong></td><td>Java 8+</td><td>Java 8+</td><td>Java 8&#x2F;11</td><td>Java 17+</td><td>Java 8-11</td></tr><tr><td><strong>既存jar対応</strong></td><td>◎（一部リフレクション制限）</td><td>◎（一部リフレクション制限）</td><td>◎（完全互換だが遅い）</td><td>△（GraalVM Native Image制約）</td><td>△（未対応API多数）</td></tr><tr><td><strong>バイナリサイズ</strong></td><td>1-2MB（最適化後）</td><td>2-4MB（ランタイム含む）</td><td>5-10MB（JVM含む）</td><td>3-10MB</td><td>1-3MB</td></tr><tr><td><strong>parse性能</strong></td><td>速い</td><td>より速い</td><td>遅いはず（JVMエミュレーション）</td><td>速いはず</td><td>速い？</td></tr><tr><td><strong>String&#x2F;JS相互運用</strong></td><td>◎（<code>@JSBody</code>で直接）</td><td>△（手動UTF-16変換）</td><td>△（JNI風API）</td><td>△（Wasm Interface Types待ち）</td><td>◎（Java↔JS透過的）</td></tr><tr><td><strong>DOM&#x2F;ブラウザAPI</strong></td><td>◎（<code>teavm-jso</code>）</td><td>○（限定的）</td><td>○（JNI風）</td><td>△（外部JS必要）</td><td>◎（JSNI&#x2F;JsInterop）</td></tr><tr><td><strong>成熟度&#x2F;コミュニティ</strong></td><td>○（中規模、活発）</td><td>△（発展途上）</td><td>△（商用中心）</td><td>△（実験的）</td><td>○（大規模だが停滞気味）</td></tr><tr><td><strong>パーサー向き</strong></td><td>◎</td><td>◎</td><td>△</td><td>○</td><td>○</td></tr></tbody></table></div><p>TeaVMを選んだ理由は以下の通りです：</p><ul><li><strong>WebAssemblyのサポート</strong><ul><li><a href="https://teavm.org/docs/release-notes/0.9.0.html">ver0.9.0</a>でWASMターゲットが安定化</li><li><a href="https://teavm.org/docs/release-notes/0.13.0.html">ver0.13.0</a>でJava25までのclassファイルをサポートしており、<code>Thread.start</code>や<code>Thread.sleep</code>などのThread系メソッドもサポート</li><li><code>java.lang</code>, <code>java.util(OptionalやStreamを含む)</code>, <code>java.io</code> などの主要なクラスはエミュレーションされる※<code>java.nio.file</code> や <code>java.net(Socketなど)</code>、<code>java.awt/swing</code> など、ブラウザ環境にそぐわないAPIは利用不可</li><li>リフレクションはメタプログラムによる静的解析でサポートされますが、動的なクラスローディングやリフレクションは制限される</li><li>利用する自作parserは別プロジェクトとなっており今回の作成したplaygroundプロジェクトからはdependencyとして利用します。TeaVMはバイナリを解析してJavaScript&#x2F;WebAssemblyに変換するため、既存のjarをそのまま利用できる点も大きなメリット</li></ul></li><li><strong><a href="https://teavm.org/docs/intro/preview-builds.html">Mavenとの統合</a></strong><ul><li>既存のビルドフローに簡単に組み込める</li><li>Mavenは単に好みですがGradleもサポート</li></ul></li><li><strong>サイズ効率</strong><ul><li>最適化オプションが充実</li></ul></li><li><strong>アクティブな開発</strong><ul><li>2026年現在でも活発</li></ul></li><li><strong>自作パーサー向き</strong><ul><li>文字列操作ロジック中心</li><li>jdkのみで特殊なクラスを利用をしていない</li></ul></li></ul><p><strong>CheerpJ</strong> は完全なJVM互換性があるようですが、ブラウザ内JVMエミュレーションによるオーバーヘッドが大きく、パーサーのような処理では遅くなります。既存jarを無理やりブラウザで動かすには便利ですが、パフォーマンスが重要な場合は注意が必要です。</p><p><strong>GraalVM Wasm</strong> は高性能。ただし、Native Imageを作成してからWebAssemblyに変換するという2段階プロセスが必要。別途検証してみたいなと思っています。</p><p><strong>GWT&#x2F;J2CL</strong> はJavaScript変換が成熟していますが、Java8ベースで新機能対応が遅く、J2CLはgoogle社の内部ツールでドキュメント不足です。</p><h2 id="アーキテクチャ設計：デュアルターゲット戦略"><a href="#アーキテクチャ設計：デュアルターゲット戦略" class="headerlink" title="アーキテクチャ設計：デュアルターゲット戦略"></a>アーキテクチャ設計：デュアルターゲット戦略</h2><p>当初はWebAssembly版のみを考えていましたが、ブラウザ互換性とパフォーマンスの両立を考え、<strong>JavaScriptとWebAssemblyの両方</strong>を同時に生成する戦略に変更しました。</p><h3 id="ビルドフロー"><a href="#ビルドフロー" class="headerlink" title="ビルドフロー"></a>ビルドフロー</h3><ul><li>JsMain&#x2F;WasmMainはそれぞれJavaScript版とWebAssembly版のエントリーポイントとなる(ソースコード文字列を引数とする)mainクラスです。</li></ul><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">mvn clean package</span><br><span class="line">  ↓</span><br><span class="line">[Maven Compiler Plugin]</span><br><span class="line">  ↓ Java → .class</span><br><span class="line">[TeaVM Plugin: compile-js]</span><br><span class="line">  ↓ JsMain.java → classes.js</span><br><span class="line">  （519 classes、4170 methods）</span><br><span class="line"></span><br><span class="line">[TeaVM Plugin: compile-wasm]</span><br><span class="line">  ↓ WasmMain.java → classes.wasm</span><br><span class="line">  （561 classes、4515 methods）</span><br><span class="line">  ↓</span><br><span class="line">[WAR Packaging]</span><br><span class="line">  → 配備可能なアプリケーション</span><br></pre></td></tr></table></figure><h3 id="エントリーポイントの分離"><a href="#エントリーポイントの分離" class="headerlink" title="エントリーポイントの分離"></a>エントリーポイントの分離</h3><p>最初の躓きポイントがここでした。<br><strong>JavaScriptとWebAssemblyで別々のMainクラスを使う</strong> 必要があったのです。</p><p><strong>なぜ分離の必要があったのか？</strong></p><p>TeaVMではJavaScriptとWebAssemblyでそれぞれ公開用のエントリーポイントをアノテーションベースで実装する必要があります。</p><p>TeaVMでは、以下のようにコンパイルターゲットによって異なるアノテーションを使用します：</p><ul><li><strong>JavaScript</strong>: <code>@JSBody</code>を使ってJavaScriptコードを直接埋め込み</li><li><strong>WebAssembly</strong>: <code>@Export</code>でWASM関数としてエクスポート</li></ul><p>これらのアノテーションは同じクラス内で併用できないため、別々のMainクラスを用意することになりました。<br>当初は1つのMainクラスで<code>@JSBody</code>と<code>@Export</code>を併用しようとしましたが、コンパイル時に競合が発生しました。TeaVMのJavaScriptターゲットとWebAssemblyターゲットでは、interop<sup id="fnref:interop"><a href="#fn:interop" rel="footnote">3</a></sup>の仕組みが根本的に異なるためです。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// JavaScript用エントリーポイント</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JsMain</span> &#123;</span><br><span class="line">    <span class="comment">// TeaVMではJNIのnative構文を流用し、@JSBodyでJavaScript実装を埋め込む（JavaScriptブリッジ用）</span></span><br><span class="line">    <span class="meta">@JSBody(params = &#123; &quot;fn&quot; &#125;, script =</span></span><br><span class="line"><span class="meta">        &quot;window.apexParser = &#123; parseApex: function(source) &#123; &quot; +</span></span><br><span class="line"><span class="meta">        &quot;return fn.parse(source); &#125; &#125;; &quot; +</span></span><br><span class="line"><span class="meta">        &quot;if (window.onParserReady) window.onParserReady();&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">native</span> <span class="keyword">void</span> <span class="title function_">registerParser</span><span class="params">(ParseFunction fn)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        registerParser((source) -&gt; parseApex(source));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> String <span class="title function_">parseApex</span><span class="params">(String source)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">SourceInfo</span> <span class="variable">sourceInfo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SourceInfo</span>(source);</span><br><span class="line">            <span class="type">ApexLexer</span> <span class="variable">lexer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ApexLexer</span>(sourceInfo);</span><br><span class="line">            <span class="type">ApexParser</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ApexParser</span>(lexer);</span><br><span class="line">            <span class="type">CompilationUnitNode</span> <span class="variable">ast</span> <span class="operator">=</span> parser.parseCompilationUnit();</span><br><span class="line"></span><br><span class="line">            <span class="type">ASTToJsonVisitor</span> <span class="variable">visitor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ASTToJsonVisitor</span>();</span><br><span class="line">            ast.accept(visitor);</span><br><span class="line">            <span class="keyword">return</span> visitor.toJsonString();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="string">&quot;&#123;\&quot;error\&quot;:\&quot;&quot;</span> + escapeJson(e.getMessage()) + <span class="string">&quot;\&quot;&#125;&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// WebAssembly用エントリーポイント</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WasmMain</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="comment">// ダミー呼び出しでDead Code Elimination回避</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            parseApex(<span class="string">&quot;public class Test &#123;&#125;&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="comment">// 無視</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * WebAssemblyでエクスポートする関数。</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@Export</span>(name = &quot;parseApex&quot;) により、parseApexという名前でJSから呼び出せる関数としてエクスポートされる。</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Export(name = &quot;parseApex&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">parseApex</span><span class="params">(String apexSource)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">SourceInfo</span> <span class="variable">sourceInfo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SourceInfo</span>(apexSource);</span><br><span class="line">            <span class="type">ApexLexer</span> <span class="variable">lexer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ApexLexer</span>(sourceInfo);</span><br><span class="line">            <span class="type">ApexParser</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ApexParser</span>(lexer);</span><br><span class="line">            <span class="type">CompilationUnitNode</span> <span class="variable">ast</span> <span class="operator">=</span> parser.parseCompilationUnit();</span><br><span class="line"></span><br><span class="line">            <span class="type">ASTToJsonVisitor</span> <span class="variable">visitor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ASTToJsonVisitor</span>();</span><br><span class="line">            ast.accept(visitor);</span><br><span class="line">            <span class="type">String</span> <span class="variable">result</span> <span class="operator">=</span> visitor.toJsonString();</span><br><span class="line"></span><br><span class="line">            System.gc();  <span class="comment">// メモリリーク対策</span></span><br><span class="line">            <span class="keyword">return</span> result;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="string">&quot;&#123;\&quot;error\&quot;:\&quot;&quot;</span> + escapeJson(e.getMessage()) + <span class="string">&quot;\&quot;&#125;&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="はまりポイント-その1：Dead-Code-Elimination"><a href="#はまりポイント-その1：Dead-Code-Elimination" class="headerlink" title="はまりポイント その1：Dead Code Elimination"></a>はまりポイント その1：Dead Code Elimination</h2><p>テストビルドが通り、Webサーバーを起動し、いざブラウザで動かしてみると…</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Error: parseApex function not found in WASM exports</span><br></pre></td></tr></table></figure><p>デバッグ用のログを仕込んで調べると、<code>ApexLexer</code>や<code>ApexParser</code>といった肝心のクラスが<strong>存在しない</strong>ことが判明しました。</p><h3 id="原因：強力すぎるデッドコード除去（Dead-Code-Elimination）"><a href="#原因：強力すぎるデッドコード除去（Dead-Code-Elimination）" class="headerlink" title="原因：強力すぎるデッドコード除去（Dead Code Elimination）"></a>原因：強力すぎるデッドコード除去（Dead Code Elimination）</h3><p>TeaVMは使われていないコードを検出して積極的に除去します。WasmMainのmain()メソッドが空だったため、<code>parseApex()</code>メソッドは「呼ばれることがない」と判断され、依存する全クラスが除外されていたのです。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ❌ これだとparserクラスが除外される</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="comment">// 空っぽ</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ ダミー呼び出しでクラス参照を保持</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        parseApex(<span class="string">&quot;public class Test &#123;&#125;&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        <span class="comment">// 無視</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>このダミー呼び出しにより、コンパイラに「このメソッドは使われる」と認識させて解決しました。</p><h2 id="はまりポイント-その2：メモリリーク"><a href="#はまりポイント-その2：メモリリーク" class="headerlink" title="はまりポイント その2：メモリリーク"></a>はまりポイント その2：メモリリーク</h2><p>Dead Code Elimination問題を解決し、パースが動き始めたのも束の間、次の問題が待っていました。</p><p>連続でパースを実行すると、30回目くらいで突然エラーが発生します：</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">RuntimeError: memory access out of bounds</span><br></pre></td></tr></table></figure><h3 id="原因：WebAssemblyのリニアメモリ制限"><a href="#原因：WebAssemblyのリニアメモリ制限" class="headerlink" title="原因：WebAssemblyのリニアメモリ制限"></a>原因：WebAssemblyのリニアメモリ制限</h3><p>WebAssemblyは「リニアメモリ」という固定サイズのメモリ領域を使います。TeaVMのデフォルトヒープサイズは<strong>16MB</strong>と小さめ。</p><p>ASTノードやトークンを大量に生成するパース処理を繰り返すと、GCが実行されずメモリが枯渇していたようです。</p><h3 id="解決策：2つのアプローチ"><a href="#解決策：2つのアプローチ" class="headerlink" title="解決策：2つのアプローチ"></a>解決策：2つのアプローチ</h3><p><strong>1. ヒープサイズの拡大</strong></p><p>pom.xmlでWASMのヒープサイズを明示的に指定しました：</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">execution</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">id</span>&gt;</span>compile-wasm<span class="tag">&lt;/<span class="name">id</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">minHeapSize</span>&gt;</span>32<span class="tag">&lt;/<span class="name">minHeapSize</span>&gt;</span>  <span class="comment">&lt;!-- 32MB --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">maxHeapSize</span>&gt;</span>128<span class="tag">&lt;/<span class="name">maxHeapSize</span>&gt;</span> <span class="comment">&lt;!-- 128MB --&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">execution</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>2. 明示的なGC呼び出し</strong></p><p>パース処理の最後に<code>System.gc()</code>を追加：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Export(name = &quot;parseApex&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">parseApex</span><span class="params">(String apexSource)</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// パース処理</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">result</span> <span class="operator">=</span> visitor.toJsonString();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 明示的にGCを要求</span></span><br><span class="line">        System.gc();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;&#123;\&quot;error\&quot;:\&quot;&quot;</span> + escapeJson(e.getMessage()) + <span class="string">&quot;\&quot;&#125;&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通常のJavaでは<code>System.gc()</code>は「お願い」でしかありませんが、TeaVMのWASM環境では比較的確実に動作します。</p><p>この2つの対策により、<strong>100回以上の連続パースでも安定動作</strong>するようになりました。</p><h2 id="はまりポイント-その3：文字列のinterop"><a href="#はまりポイント-その3：文字列のinterop" class="headerlink" title="はまりポイント その3：文字列のinterop"></a>はまりポイント その3：文字列のinterop</h2><p>WebAssemblyは文字列を直接扱えません。JavaScriptとWASM間で文字列を受け渡すには、<strong>手動でUTF-16変換</strong>が必要です。</p><h3 id="JavaScript側の変換関数"><a href="#JavaScript側の変換関数" class="headerlink" title="JavaScript側の変換関数"></a>JavaScript側の変換関数</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">jsStringToJava</span>(<span class="params">str</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!teavm) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// TeaVMのヒープに文字列用領域を確保</span></span><br><span class="line">    <span class="keyword">let</span> javaString = teavm.<span class="title function_">allocateString</span>(str.<span class="property">length</span>);</span><br><span class="line">    <span class="keyword">if</span> (javaString === <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> dataArrayPtr = teavm.<span class="title function_">stringData</span>(javaString);</span><br><span class="line">    <span class="keyword">let</span> dataAddress = teavm.<span class="title function_">objectArrayData</span>(dataArrayPtr);</span><br><span class="line">    <span class="keyword">let</span> dataView = <span class="keyword">new</span> <span class="title class_">Uint16Array</span>(teavm.<span class="property">memory</span>.<span class="property">buffer</span>, dataAddress, str.<span class="property">length</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// UTF-16配列として書き込み</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; str.<span class="property">length</span>; ++i) &#123;</span><br><span class="line">        dataView[i] = str.<span class="title function_">charCodeAt</span>(i);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> javaString;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">javaStringToJs</span>(<span class="params">javaString</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!teavm || javaString === <span class="number">0</span>) <span class="keyword">return</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> dataArrayPtr = teavm.<span class="title function_">stringData</span>(javaString);</span><br><span class="line">    <span class="keyword">let</span> length = teavm.<span class="title function_">arrayLength</span>(dataArrayPtr);</span><br><span class="line">    <span class="keyword">let</span> dataAddress = teavm.<span class="title function_">objectArrayData</span>(dataArrayPtr);</span><br><span class="line">    <span class="keyword">let</span> dataView = <span class="keyword">new</span> <span class="title class_">Uint16Array</span>(teavm.<span class="property">memory</span>.<span class="property">buffer</span>, dataAddress, length);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> result = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; length; ++i) &#123;</span><br><span class="line">        result += <span class="title class_">String</span>.<span class="title function_">fromCharCode</span>(dataView[i]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>これはgithub copilotが書いてくれたとはいえ、正直<strong>面倒</strong>です。JavaScript版では<code>@JSBody</code>で直接文字列を受け渡せるため、このような変換は不要です。なお<code>@JSBody</code>よりもっとシンプルに使える <a href="https://teavm.org/docs/runtime/js-modules.html">@JSExport</a> というアノテーションもあります。</p><p>パフォーマンスとのトレードオフですね。</p><h2 id="AST可視化：JSON形式への移行"><a href="#AST可視化：JSON形式への移行" class="headerlink" title="AST可視化：JSON形式への移行"></a>AST可視化：JSON形式への移行</h2><p>初期バージョンでは、パーサープロジェクト側のテストで利用していたので下記のようなテキストベースのAST出力を直接JavaScriptに返していました。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">CompilationUnit [1:1-10:2]</span><br><span class="line">  ClassDeclaration: HelloWorld [1:1-10:2]</span><br><span class="line">    MethodDeclaration: void sayHello [2:5-4:6]</span><br><span class="line">      ...</span><br></pre></td></tr></table></figure><p>しかし、これをJavaScript側でパースして位置情報を抽出するのは困難だった(当たり前)ため <strong>JSON形式</strong>に移行することにしました。</p><h3 id="ASTToJsonVisitor実装"><a href="#ASTToJsonVisitor実装" class="headerlink" title="ASTToJsonVisitor実装"></a>ASTToJsonVisitor実装</h3><p>ダブルディスパッチのVisitorパターンで全ASTノードを走査し、JSON文字列を構築します：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// これはイメージです。実際には全ノードタイプに対応するvisitメソッドが必要になります。</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ASTToJsonVisitor</span> <span class="keyword">implements</span> <span class="title class_">ASTVisitor</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">StringBuilder</span> <span class="variable">json</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">visit</span><span class="params">(CompilationUnitNode node)</span> &#123;</span><br><span class="line">        json.append(<span class="string">&quot;&#123;&quot;</span>);</span><br><span class="line">        appendProperty(<span class="string">&quot;type&quot;</span>, <span class="string">&quot;CompilationUnitNode&quot;</span>);  <span class="comment">// js側でdispatch用のtypeプロパティ</span></span><br><span class="line">        appendLocation(node.getLocation());</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (!node.getClassDeclarations().isEmpty()) &#123;</span><br><span class="line">            json.append(<span class="string">&quot;,\&quot;classDeclarations\&quot;:[&quot;</span>);</span><br><span class="line">            <span class="type">boolean</span> <span class="variable">first</span> <span class="operator">=</span> <span class="literal">true</span>;</span><br><span class="line">            <span class="keyword">for</span> (ClassDeclarationNode cls : node.getClassDeclarations()) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!first) json.append(<span class="string">&quot;,&quot;</span>);</span><br><span class="line">                cls.accept(<span class="built_in">this</span>);</span><br><span class="line">                first = <span class="literal">false</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            json.append(<span class="string">&quot;]&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        json.append(<span class="string">&quot;&#125;&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">appendLocation</span><span class="params">(Location loc)</span> &#123;</span><br><span class="line">        json.append(<span class="string">&quot;,\&quot;location\&quot;:&#123;&quot;</span>);</span><br><span class="line">        json.append(<span class="string">&quot;\&quot;startLine\&quot;:&quot;</span>).append(loc.getStartLine());</span><br><span class="line">        json.append(<span class="string">&quot;,\&quot;endLine\&quot;:&quot;</span>).append(loc.getEndLine());</span><br><span class="line">        json.append(<span class="string">&quot;,\&quot;startColumn\&quot;:&quot;</span>).append(loc.getStartColumn());</span><br><span class="line">        json.append(<span class="string">&quot;,\&quot;endColumn\&quot;:&quot;</span>).append(loc.getEndColumn());</span><br><span class="line">        json.append(<span class="string">&quot;,\&quot;startPosition\&quot;:&quot;</span>).append(loc.getStartPosition());</span><br><span class="line">        json.append(<span class="string">&quot;,\&quot;endPosition\&quot;:&quot;</span>).append(loc.getEndPosition());</span><br><span class="line">        json.append(<span class="string">&quot;&#125;&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>なぜJSONライブラリを使わないのか？</strong></p><p>実は、TeaVMでは一般的なJSONライブラリ（<a href="https://github.com/FasterXML/jackson">Jackson</a>、<a href="https://github.com/google/gson">Gson</a>等）がリフレクションが問題になりそのまま動かないことがあります。</p><p>単にJSON文字列が作れればいいだけなので、 <code>StringBuilder</code>で手動構築するので十分で、コンパイル後のコードサイズも小さく抑えられます。</p><p>ここもgithub copilotが書いてくれるので手間はあまりかかりませんでした。</p><h2 id="パフォーマンス比較：JavaScriptとWebAssembly"><a href="#パフォーマンス比較：JavaScriptとWebAssembly" class="headerlink" title="パフォーマンス比較：JavaScriptとWebAssembly"></a>パフォーマンス比較：JavaScriptとWebAssembly</h2><p>両方のビルドが動くようになったので、ベンチマークを取りました。</p><h3 id="Apexソース（900行弱のクラス）"><a href="#Apexソース（900行弱のクラス）" class="headerlink" title="Apexソース（900行弱のクラス）"></a>Apexソース（900行弱のクラス）</h3><p>WebAssembly版はJavaScript版の約2倍速い結果が出ました。<br>とはいえ、JavaScript版も実用上は十分な速度であり、体感としては誤差の範囲内と言えます。<br>どちらもJITコンパイルされるため、初回は遅いですが、2回目以降は安定して速くなります。</p><div class="scroll"><table><thead><tr><th>指標</th><th>JavaScript</th><th>WebAssembly</th><th>比率</th></tr></thead><tbody><tr><td>Parse Time（平均）</td><td>100ms</td><td>50ms</td><td><strong>約2.0x</strong></td></tr><tr><td>Parse Time（最速）</td><td>79ms</td><td>35ms</td><td><strong>約2.0x</strong></td></tr><tr><td>Classes Compiled</td><td>519</td><td>561</td><td>+42</td></tr><tr><td>Methods Compiled</td><td>4170</td><td>4515</td><td>+345</td></tr><tr><td>File Size</td><td>781KB※</td><td>2.2MB</td><td><strong>約3.0x</strong></td></tr><tr><td>Initial Load</td><td>速い</td><td>やや遅い</td><td>WASM instantiation分遅い</td></tr></tbody></table></div><p>※ <strong>TeaVM 0.10.2</strong> では812KBでした。 <strong>TeaVM 0.13.0</strong> までに最適化がより進んだ可能性があります。</p><h3 id="考察"><a href="#考察" class="headerlink" title="考察"></a>考察</h3><p><strong>WebAssemblyの利点</strong>:</p><ul><li>実行速度が<strong>2倍高速</strong></li><li>大規模解析で真価を発揮する可能性</li><li>連続処理でも安定</li></ul><p><strong>WebAssemblyの欠点</strong>:</p><ul><li>ファイルサイズが3倍</li><li>初回読み込みがやや遅い</li><li>文字列変換のオーバーヘッド</li><li>ブラウザサポートがChrome 88+, Firefox 89+, Edge 88+に限定</li></ul><p><strong>JavaScript版の利点</strong>:</p><ul><li>ファイルサイズが小さい</li><li>ブラウザ互換性が高い</li><li>文字列操作が容易</li><li>それでも十分な速度</li></ul><p><strong>結論</strong>: 小規模パースならJavaScript版で十分。大規模な解析や連続処理、リアルタイム性が求められる場面ではWebAssembly版が有利。</p><h2 id="ビルドオプションの詳細解説"><a href="#ビルドオプションの詳細解説" class="headerlink" title="ビルドオプションの詳細解説"></a>ビルドオプションの詳細解説</h2><p><a href="https://teavm.org/docs/tooling/maven.html">TeaVMのMavenプラグインには多くの設定オプション</a>があります。pom.xmlに入れた設定を説明します。</p><h3 id="JavaScript版の設定"><a href="#JavaScript版の設定" class="headerlink" title="JavaScript版の設定"></a>JavaScript版の設定</h3><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">execution</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">id</span>&gt;</span>compile-js<span class="tag">&lt;/<span class="name">id</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">goals</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">goal</span>&gt;</span>compile<span class="tag">&lt;/<span class="name">goal</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">goals</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">phase</span>&gt;</span>process-classes<span class="tag">&lt;/<span class="name">phase</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- エントリーポイント --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">mainClass</span>&gt;</span>jp.co.future.tools.apex.playground.JsMain<span class="tag">&lt;/<span class="name">mainClass</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 出力先（開発用にsrc/main/webappへ直接出力） --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">targetDirectory</span>&gt;</span>$&#123;project.basedir&#125;/src/main/webapp<span class="tag">&lt;/<span class="name">targetDirectory</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- ターゲット種別 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">targetType</span>&gt;</span>JAVASCRIPT<span class="tag">&lt;/<span class="name">targetType</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">targetFileName</span>&gt;</span>classes.js<span class="tag">&lt;/<span class="name">targetFileName</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 最適化レベル：SIMPLE, ADVANCED, FULL --&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- FULL = Dead Code Elimination + インライン化 + 定数畳み込み --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">optimizationLevel</span>&gt;</span>FULL<span class="tag">&lt;/<span class="name">optimizationLevel</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 変数名短縮化（サイズ削減） --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">minifying</span>&gt;</span>true<span class="tag">&lt;/<span class="name">minifying</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- デバッグ情報を含めない（本番用） --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">debugInformationGenerated</span>&gt;</span>false<span class="tag">&lt;/<span class="name">debugInformationGenerated</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- エラーがあっても処理継続 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">stopOnErrors</span>&gt;</span>false<span class="tag">&lt;/<span class="name">stopOnErrors</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- ファイナライザの厳格チェックを無効化 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">strictFinalization</span>&gt;</span>false<span class="tag">&lt;/<span class="name">strictFinalization</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">execution</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>主要オプション解説</strong>:</p><ul><li><p><strong>optimizationLevel</strong>:</p><ul><li><code>SIMPLE</code>: 基本的な最適化のみ（開発用）</li><li><code>ADVANCED</code>: インライン化や定数畳み込み</li><li><code>FULL</code>: Dead Code Eliminationを含む完全最適化（本番用）<ul><li>今回の実験だと<code>SIMPLE/minifyなし</code> に比べて、サイズが1&#x2F;3になりました</li></ul></li></ul></li><li><p><strong>minifying</strong>: 変数名を短縮（例：<code>parseApexSourceCode</code> → <code>a</code>）。可読性は下がるがサイズが30-40%削減される</p></li><li><p><strong>debugInformationGenerated</strong>: ソースマップとスタックトレース情報を生成。開発時は<code>true</code>、本番は<code>false</code></p></li></ul><h3 id="WebAssembly版の設定"><a href="#WebAssembly版の設定" class="headerlink" title="WebAssembly版の設定"></a>WebAssembly版の設定</h3><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">execution</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">id</span>&gt;</span>compile-wasm<span class="tag">&lt;/<span class="name">id</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">goals</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">goal</span>&gt;</span>compile<span class="tag">&lt;/<span class="name">goal</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">goals</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">phase</span>&gt;</span>process-classes<span class="tag">&lt;/<span class="name">phase</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- エントリーポイント --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">mainClass</span>&gt;</span>jp.co.future.tools.apex.playground.WasmMain<span class="tag">&lt;/<span class="name">mainClass</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- ターゲット種別 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">targetType</span>&gt;</span>WEBASSEMBLY<span class="tag">&lt;/<span class="name">targetType</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 出力先（src/main/webappへ出力） --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">targetDirectory</span>&gt;</span>$&#123;project.basedir&#125;/src/main/webapp<span class="tag">&lt;/<span class="name">targetDirectory</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 出力ファイル名 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">targetFileName</span>&gt;</span>classes.wasm<span class="tag">&lt;/<span class="name">targetFileName</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 最適化レベル --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">optimizationLevel</span>&gt;</span>FULL<span class="tag">&lt;/<span class="name">optimizationLevel</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- *** メモリ設定（重要） *** --&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 初期ヒープサイズ（MB） --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">minHeapSize</span>&gt;</span>32<span class="tag">&lt;/<span class="name">minHeapSize</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 最大ヒープサイズ（MB） --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">maxHeapSize</span>&gt;</span>128<span class="tag">&lt;/<span class="name">maxHeapSize</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">debugInformationGenerated</span>&gt;</span>false<span class="tag">&lt;/<span class="name">debugInformationGenerated</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">stopOnErrors</span>&gt;</span>false<span class="tag">&lt;/<span class="name">stopOnErrors</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">strictFinalization</span>&gt;</span>false<span class="tag">&lt;/<span class="name">strictFinalization</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">execution</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>WASM固有オプション解説</strong>:</p><ul><li><p><strong>minHeapSize&#x2F;maxHeapSize</strong>: WebAssemblyのリニアメモリサイズ。デフォルト16MBは小さすぎるため、パーサー用途では32-128MBを推奨</p></li><li><p><strong>heapDump</strong>: （オプション）メモリリーク調査用。<code>true</code>にするとヒープダンプを出力</p></li><li><p><strong>wasmVersion</strong>: （オプション）WebAssemblyのバージョン。デフォルトは<code>V_0x1</code>（MVP）</p><ul><li>指定したのですがエラーになりました</li><li>TeaVMでは指定ができないようです</li></ul></li></ul><h3 id="開発時のTips"><a href="#開発時のTips" class="headerlink" title="開発時のTips"></a>開発時のTips</h3><p><strong>開発中は最適化を弱める</strong>:</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">optimizationLevel</span>&gt;</span>SIMPLE<span class="tag">&lt;/<span class="name">optimizationLevel</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">minifying</span>&gt;</span>false<span class="tag">&lt;/<span class="name">minifying</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">debugInformationGenerated</span>&gt;</span>true<span class="tag">&lt;/<span class="name">debugInformationGenerated</span>&gt;</span></span><br></pre></td></tr></table></figure><p>これにより：</p><ul><li>ビルド時間が短縮（FULL: 45秒 → SIMPLE: 20秒）</li><li>エラーメッセージが読みやすい</li><li>ブラウザDevToolsでのデバッグが容易</li></ul><p><strong>本番環境では完全最適化</strong>:</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">optimizationLevel</span>&gt;</span>FULL<span class="tag">&lt;/<span class="name">optimizationLevel</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">minifying</span>&gt;</span>true<span class="tag">&lt;/<span class="name">minifying</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">debugInformationGenerated</span>&gt;</span>false<span class="tag">&lt;/<span class="name">debugInformationGenerated</span>&gt;</span></span><br></pre></td></tr></table></figure><p>サイズが30-50%削減され、起動時間も改善します。</p><h2 id="学んだこと"><a href="#学んだこと" class="headerlink" title="学んだこと"></a>学んだこと</h2><h3 id="1-WebAssemblyは万能ではない"><a href="#1-WebAssemblyは万能ではない" class="headerlink" title="1. WebAssemblyは万能ではない"></a>1. WebAssemblyは万能ではない</h3><p>WebAssemblyは確かに高速ですが、ファイルサイズやブラウザ互換性のトレードオフがあります。Salesforce Apexのようなパースでは「2倍速い」という結果でしたが、JavaScript版でも十分実用的です。<br>WebAssemblyの場合はfetchでモジュールを読み込む必要があるためWebサーバが必要ですが、JavaScript版ならローカルで直接開いても動きます。用途に応じて使い分けるのが良さそうです。</p><h3 id="2-Dead-Code-Eliminationは両刃の剣"><a href="#2-Dead-Code-Eliminationは両刃の剣" class="headerlink" title="2. Dead Code Eliminationは両刃の剣"></a>2. Dead Code Eliminationは両刃の剣</h3><p>最適化は重要ですが、意図しないクラス除去が起きると原因特定が困難です。main()での明示的な参照は、一種の「アンカー」として機能します。</p><h3 id="3-メモリ管理はWASMの課題"><a href="#3-メモリ管理はWASMの課題" class="headerlink" title="3. メモリ管理はWASMの課題"></a>3. メモリ管理はWASMの課題</h3><p>WebAssemblyのリニアメモリは有限です。特にGC言語（Java, Kotlin等）をコンパイルする場合、ヒープサイズの調整と明示的なGC呼び出しが重要になります。</p><h3 id="4-文字列変換のオーバーヘッド"><a href="#4-文字列変換のオーバーヘッド" class="headerlink" title="4. 文字列変換のオーバーヘッド"></a>4. 文字列変換のオーバーヘッド</h3><p>WebAssemblyのinteropで文字列を扱うのは思ったより面倒です。TeaVMは比較的良好なAPIを提供していますが、それでも手動変換が必要です。将来的には<a href="https://component-model.bytecodealliance.org/design/component-model-concepts.html?highlight=Interface%20Type#webassembly-interface-types-wit">WebAssembly Interface Types</a>※で改善される予定です。</p><p>※WebAssembly Interface Typesとは、WebAssemblyモジュールがJavaScriptや他の言語とより自然にデータをやり取りできるようにするための提案です。これが実装されれば、文字列や複雑なデータ構造の変換が大幅に簡素化されるでしょう。</p><h3 id="5-JSON形式の威力"><a href="#5-JSON形式の威力" class="headerlink" title="5. JSON形式の威力"></a>5. JSON形式の威力</h3><p>言うまでもありませんが、JavaScriptとのやりとりで構造化データをやり取りする場合、JSON形式は強力です。手動で文字列を構築するのは面倒ですが、JSON形式になってしまえばJavaScript側で自由にトラバースできます。</p><h3 id="6-TeaVMのスタブ機能"><a href="#6-TeaVMのスタブ機能" class="headerlink" title="6. TeaVMのスタブ機能"></a>6. TeaVMのスタブ機能</h3><p>TeaVMの<code>teavm-classlib</code>には、ブラウザ環境では実際に動作しないAPIのスタブが含まれています。例えば、TeaVM&#x2F;WebAssemblyの制約により <code>java.nio.file</code>パッケージのようなファイルI&#x2F;O操作をサポートしていませんが、クラス参照やメソッドシグネチャは問題なくコンパイルできます。</p><p>自作パーサーの<code>SourceInfo</code>クラスでは<code>java.nio.file.Path</code>を使用していましたが、TeaVMの<code>teavm-classlib</code>に基本的なスタブ（<code>TPath</code>、<code>TPaths</code>など）が含まれているため、特別な対応なしにコンパイルできました。</p><p>もし<code>teavm-classlib</code>にないクラスを参照する必要がある場合は、以下のようなスタブを作成できます：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// stubs/org/teavm/classlib/com/example/MyClass.java</span></span><br><span class="line"><span class="keyword">package</span> org.teavm.classlib.com.example;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyClass</span> &#123;</span><br><span class="line">    <span class="comment">// ブラウザでは実際に呼ばれないメソッドのスタブ実装</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">someMethod</span><span class="params">(String arg)</span> &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnsupportedOperationException</span>(<span class="string">&quot;Not supported in browser&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>このスタブを<code>org.teavm.classlib</code>パッケージ配下に配置することで、TeaVMのクラスローダーが優先的に読み込み、コンパイルエラーを回避できます。実際にそのメソッドが実行されなければ、例外は発生しません。</p><h2 id="今後の展望"><a href="#今後の展望" class="headerlink" title="今後の展望"></a>今後の展望</h2><p>GraalVM Wasmも試してみたいと思っています。GraalVMはJavaをネイティブコードにコンパイルする機能があり、そこからさらにWebAssemblyに変換することができます。性能面では非常に期待できる一方で、ビルドフローが複雑になる可能性があります。</p><h2 id="まとめ"><a href="#まとめ" class="headerlink" title="まとめ"></a>まとめ</h2><p>JavaからWebAssemblyへのコンパイルは、思ったより実用的でした。TeaVMは強力なツールですが、いくつかの気をつける点があることを学びました。どれも回避策は存在するので、適切に対処すれば安定したブラウザアプリケーションを構築できます。</p><ul><li><strong>Dead Code Elimination</strong>: main()でクラス参照を保持</li><li><strong>メモリ管理</strong>: ヒープサイズ調整とGC呼び出し</li><li><strong>文字列変換</strong>: 手動でUTF-16変換</li><li><strong>デュアルターゲット</strong>: JavaScriptとWASMで別々のMainクラス</li><li><strong>スタブの利用に関する注意</strong>: TeaVMではブラウザ環境で動作しないAPI（例: <code>java.nio.file</code>）に対してスタブを提供しており、これを適切に扱う必要があります。</li></ul><p>今回やったような実験もAIエージェントによりだいぶ楽になりました。コードの自動生成やリファクタリングに大活躍でした。</p><h2 id="最後に"><a href="#最後に" class="headerlink" title="最後に"></a>最後に</h2><p>コアテクノロジーグループの私のチームでは下記のような記事を書いています。</p><ul><li><a href="https://future-architect.github.io/articles/20250929a/">Pure Rustで生まれ変わったPostgreSQL公式構文準拠SQLフォーマッター「uroborosql-fmt」をリリース🎉 | フューチャー技術ブログ</a><ul><li>Pure Rustでpostgresqlの構文準拠のcst-parserを実装しています</li></ul></li><li><a href="https://future-architect.github.io/articles/20200903/">ANTLRを業務で活用した話 | フューチャー技術ブログ</a><ul><li>ANTLRのようなパーサージェネレーターを利用することもあります</li></ul></li><li><a href="https://future-architect.github.io/articles/20220303a/">Pyright を LSP サーバとした自作 LSP クライアント（実装編） | フューチャー技術ブログ</a><ul><li>PyrightというPythonの型チェッカーを言語解析エンジンとして利用した事例</li></ul></li></ul><p>コアテクノロジーグループでは、現在チームメンバーを募集しています。言語処理やコンパイラー技術が好きな方、グラフ理論、グラフ可視化、アルゴリズム好きな方、ソフトウェア工学の知識を使って仕事をしたい方を歓迎します。</p><p>興味がある方はお気軽に技術ブログTwitterや会社採用HPへ、連絡をお待ちしております。</p><p><a href="https://www.future.co.jp/recruit/">https://www.future.co.jp/recruit/</a></p><h2 id="注釈"><a href="#注釈" class="headerlink" title="注釈"></a>注釈</h2><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:apex"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">ApexとはSalesforceのプラットフォーム専用言語です。Java5に似た構文を持ちながら、SOQL/SOSLといった独自のオブジェクトクエリ機能を持つ言語です。<a href="https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_intro_what_is_apex.htm">公式サイト：Apex とは? | Apex 開発者ガイド | Salesforce Developers</a></span> <a href="#fnref:apex" rev="footnote">↩</a></li><li id="fn:ast"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">AST（Abstract Syntax Tree）とは、ソースコードの構文構造を表す木構造のデータです。パーサーはソースコードをASTに変換し、バイナリ生成やコード解析などの後続処理で利用されます。</span> <a href="#fnref:ast" rev="footnote">↩</a></li><li id="fn:interop"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">interopとは、JavaコードとJavaScript/WASMコードが相互に呼び出し合うための仕組みです。</span> <a href="#fnref:interop" rev="footnote">↩</a></li></ol></div></div>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;コアテクノロジーグループの二村です。&lt;/p&gt;
&lt;p&gt;普段は膨大なドキュメントやソースコードを解析して、ファクトベースでお客様のシステム移行計画策定や、保守改善を支援するコンサルティング業務を行っています。また、そのためのマネージドサービスの開発を行っています。&lt;/p&gt;
&lt;p</summary>
        
      
    
    
    
    <category term="Programming" scheme="https://future-architect.github.io/categories/Programming/"/>
    
    
    <category term="Java" scheme="https://future-architect.github.io/tags/Java/"/>
    
    <category term="WebAssembly" scheme="https://future-architect.github.io/tags/WebAssembly/"/>
    
    <category term="Salesforce" scheme="https://future-architect.github.io/tags/Salesforce/"/>
    
  </entry>
  
  <entry>
    <title>非同期設計ガイドラインを公開しました</title>
    <link href="https://future-architect.github.io/articles/20260220a/"/>
    <id>https://future-architect.github.io/articles/20260220a/</id>
    <published>2026-02-19T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.108Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260220a/top.jpg" alt="" width="1024" height="559"><h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>こんにちは。TIG（Technology Innovation Group）の亀井です。</p><p>フューチャー社内の有志メンバーで <a href="https://future-architect.github.io/arch-guidelines/documents/forAsync/">非同期設計ガイドライン</a> を作成し、公開しました！</p><p>本記事では、ガイドライン策定の背景や、ガイドラインで取り上げている設計のポイントをピックアップしてご紹介します。</p><h2 id="本ガイドライン策定の背景"><a href="#本ガイドライン策定の背景" class="headerlink" title="本ガイドライン策定の背景"></a>本ガイドライン策定の背景</h2><p>かつて非同期処理といえば、専門的なメッセージングミドルウェアを必要とする、一部のミッションクリティカルなシステムで採用される特別な技術でした。フューチャーでも独自のミドルウェアフレームワークを構築して、大量データをリアルタイムで処理するような仕組みを数々の工夫を凝らして実装してきました。</p><p>一方で、昨今ではAWS SQSなどのクラウドネイティブなサービスの登場により、応答時間の長い処理のオフロードなどを目的に非同期処理を取り入れることは珍しくなくなりました。</p><p>しかし、非同期特有の難しさは依然として存在しており、「処理のトレース」「デバッグ」「リラン」が困難である点や、データストアにまたがる場合のデータ整合性担保、障害発生時のリカバリなど、同期処理にはない複雑さが伴います。</p><p>本ガイドラインでは、非同期導入のメリットを享受しつつ、これらの「本質的な難しさ」を回避、または適切に管理するための実務的な設計論点と指針を提供することを目的としています。</p><h2 id="本ガイドラインの対象読者"><a href="#本ガイドラインの対象読者" class="headerlink" title="本ガイドラインの対象読者"></a>本ガイドラインの対象読者</h2><p>本ガイドラインは、バックエンドシステムにおいて、Web APIやバッチ処理からのトリガーによる非同期メッセージング（キュー）を用いた処理を設計・実装するエンジニア・アーキテクトを対象としています。</p><h2 id="ガイドラインの内容紹介"><a href="#ガイドラインの内容紹介" class="headerlink" title="ガイドラインの内容紹介"></a>ガイドラインの内容紹介</h2><p>ガイドラインは多岐にわたる論点をカバーしていますが、ここでは特に議論になりやすいポイントをいくつか抜粋して紹介します。</p><h3 id="1-非同期化の判断基準と使い分け"><a href="#1-非同期化の判断基準と使い分け" class="headerlink" title="1. 非同期化の判断基準と使い分け"></a>1. 非同期化の判断基準と使い分け</h3><p>「なんとなく非同期にする」のではなく、明確な判断基準を持つことを推奨しています。ガイドラインでは以下の3つの観点を提示しています。</p><ul><li><strong>処理時間と応答性:</strong> ファイル生成など、同期的に待つとUXが低下する場合</li><li><strong>負荷の平準化:</strong> 突発的な大量アクセスなどバースト的な負荷を直接受けずに平準化したい場合</li><li><strong>レジリエンスの向上:</strong> 外部システムへの依存を切り離し、メイン処理の継続性を高めたい場合</li></ul><p>一方で、これらに該当しない場合やリソースの増強や運用で回避できるケースでは、システム全体の複雑性を下げるために「同期処理」を選択することも合理的であり、安易な非同期化を避けることも推奨としています。</p><h3 id="2-論理構成方針"><a href="#2-論理構成方針" class="headerlink" title="2. 論理構成方針"></a>2. 論理構成方針</h3><p>スケーラビリティや障害分離の観点から、<strong>「1業務タスク &#x3D; 1キュー &#x3D; 1コンシューマー」</strong> という構成を推奨しています。</p><p>複数の異なる業務（例: メール送信と決済処理）を1つのキューに混在させると、障害時の切り分けが困難になったり、業務ごとの優先度に応じた流量制御（スロットリング）ができなくなるため業務タスクの単位でキューおよびコンシューマーを分けることを推奨としています。<br>管理するリソース数は増えますが、AWS Lambdaにおける「予約済み同時実行数」などのリソース制限を有効活用するためにも、シンプルな構成にしておく方がよいでしょう。</p><h3 id="3-データ整合性とトランザクションアウトボックスパターン"><a href="#3-データ整合性とトランザクションアウトボックスパターン" class="headerlink" title="3. データ整合性とトランザクションアウトボックスパターン"></a>3. データ整合性とトランザクションアウトボックスパターン</h3><p>非同期処理で頻出する課題に「DB更新とメッセージ送信の整合性」があります。DBのコミットには成功したがメッセージ送信に失敗する（あるいはその逆）といった事態を防ぐため、<strong>トランザクションアウトボックスパターン</strong> の採用是非についても触れています。</p><blockquote><p>トランザクションアウトボックスパターン</p><ol><li>各コンシューマーは自身の担う業務ロジック処理と、処理完了を示すステータス更新を1トランザクションで実施する。</li><li>後続コンシューマーへのメッセージ連携は、別のトランザクションで行う。</li></ol><p>これにより、1→2の順序性の担保と、2単体でのリトライが可能となる。</p></blockquote><p>ガイドラインでは、ロストメッセージやファントムメッセージの対策として、ステータス管理テーブルを用いたアプローチや、プロデューサー側での登録とコンシューマー側でのチェックによる整合性担保について比較・解説しています。</p><div class="scroll"><table><thead><tr><th>用語</th><th>状態</th><th>発生する問題</th></tr></thead><tbody><tr><td>ロストメッセージ</td><td>DBコミット成功 &#x2F; キュー送信失敗</td><td>メッセージが消失し、後続処理が動かない</td></tr><tr><td>ファントムメッセージ</td><td>キュー送信成功 &#x2F; DBコミット失敗</td><td>存在しないデータを処理しようとしてエラーになる</td></tr></tbody></table></div><h3 id="4-順序保証とFIFOキュー"><a href="#4-順序保証とFIFOキュー" class="headerlink" title="4. 順序保証とFIFOキュー"></a>4. 順序保証とFIFOキュー</h3><p>厳密な順序保証が必要な場合、SQS FIFOキューなどの利用が検討されますが、スループットの低下や「Head-of-Line (HOL) ブロッキング」のリスクが伴います。</p><p>本ガイドラインでは、<strong>「可能な限り順序制御を必要としない設計（冪等性の確保や、メッセージの独立性）」</strong> を目指すことを推奨しています。その上で、どうしても順序保証が必要な場合のグルーピング戦略（MessageGroupIdの設計）についても言及しています。</p><h3 id="5-エラーハンドリングとDLQ"><a href="#5-エラーハンドリングとDLQ" class="headerlink" title="5. エラーハンドリングとDLQ"></a>5. エラーハンドリングとDLQ</h3><p>処理失敗時のメッセージ退避先であるDead Letter Queue(DLQ)について、<strong>「二段構え」</strong> の構成を推奨しています。</p><ol><li><strong>アプリケーション制御:</strong> バリデーションエラーなど、既知のエラーはアプリが即時にDLQへ退避させる（ブロッキングを最小化するため）</li><li><strong>インフラ制御:</strong> クラッシュなど予期せぬエラーは、インフラ（SQSのmaxReceiveCountなど）の機能で救済する</li></ol><p>これにより、FIFOキュー利用時のブロッキング時間を最小化しつつ、予期せぬ障害時にもメッセージをロストしない堅牢性を確保します。</p><h3 id="まとめ"><a href="#まとめ" class="headerlink" title="まとめ"></a>まとめ</h3><p>非同期設計ガイドラインは、現代の分散システム開発において避けては通れない「非同期処理」の設計判断を支援するために作成しました。</p><p>Webフロントエンド設計ガイドライン や バッチ設計ガイドライン と同様に、本ガイドラインも社内外のフィードバックを受けて継続的にアップデートしていく予定です。</p><p>GitHub上でのIssueやPRも大歓迎ですので、ぜひご覧ください。</p><ul><li><strong>GitHub</strong>: <a href="https://github.com/future-architect/arch-guidelines">future-architect&#x2F;arch-guidelines</a></li></ul><h3 id="関連ガイドライン"><a href="#関連ガイドライン" class="headerlink" title="関連ガイドライン"></a>関連ガイドライン</h3><ul><li><a href="https://future-architect.github.io/articles/20250911/">Webフロントエンド設計ガイドラインを公開しました</a></li><li><a href="https://future-architect.github.io/articles/20250918/">バッチ設計ガイドラインを公開しました</a></li><li><a href="https://future-architect.github.io/articles/20250513/">Web API設計ガイドラインを公開しました</a></li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260220a/top.jpg&quot; alt=&quot;&quot; width=&quot;1024&quot; height=&quot;559&quot;&gt;

&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    <category term="Programming" scheme="https://future-architect.github.io/categories/Programming/"/>
    
    
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    
    <category term="SQS" scheme="https://future-architect.github.io/tags/SQS/"/>
    
  </entry>
  
  <entry>
    <title>2026年 フューチャー技術ブログリレー企画</title>
    <link href="https://future-architect.github.io/articles/20260219a/"/>
    <id>https://future-architect.github.io/articles/20260219a/</id>
    <published>2026-02-18T15:00:00.000Z</published>
    <updated>2026-05-10T00:45:37.108Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/2026/20260219a/unnamed.jpg" alt="" width="1024" height="572" loading="lazy"><h1 id="はじめに"><a href="#はじめに" class="headerlink" title="はじめに"></a>はじめに</h1><p>2026年に計画しているフューチャー技術ブログにおける、ブログリレーの企画スケジュールを紹介します。</p><h2 id="スケジュールを発表する背景"><a href="#スケジュールを発表する背景" class="headerlink" title="スケジュールを発表する背景"></a>スケジュールを発表する背景</h2><p>ブログリレーの年間計画をあらかじめ公開する背景には、例年通り以下の意図があります。</p><ul><li><strong>会社のカラーを伝える</strong>: どういったブログリレーを行おうとしているかによって、会社の特色が出るため、興味がある方にアクセスしていただけるようにしたいと考えています</li><li><strong>執筆準備の期間確保</strong>: ブログリレーは寄稿者募集のリードタイムが発生するため、あらかじめスケジュールを周知することで準備をしやすくします</li><li><strong>運営の効率化</strong>: 日々の業務をしながらだと企画内容を忘れがちになるため、最初に大枠を固めておくことで運営をスムーズにします。運営を透明化するということで、親しみを持ってもらうという意図もあります</li></ul><h2 id="2026年のスケジュール"><a href="#2026年のスケジュール" class="headerlink" title="2026年のスケジュール"></a>2026年のスケジュール</h2><p>2026年のラインナップは以下の通りです。今年は「テスト」など、時代の変化に合わせた新しいテーマも取り入れています。</p><p>🔰は今年初めて実施する連載です。</p><div class="scroll"><table><thead><tr><th align="left">Month</th><th align="left">Title</th><th align="left">Memo</th><th align="left">Link</th></tr></thead><tbody><tr><td align="left">1月</td><td align="left">-</td><td align="left">-</td><td align="left">-</td></tr><tr><td align="left">2月</td><td align="left">Go 1.26</td><td align="left">Go 1.26リリース記念<br>恒例のGoリリース連載です。</td><td align="left"><a href="/articles/20260127a/">Go1.26</a></td></tr><tr><td align="left">3月</td><td align="left">Terraform</td><td align="left">Terraform全般をテーマ<br>IaCの中核となるTerraformについて、知見やTipsを共有します。</td><td align="left">2026、<a href="/articles/20250331a/">2025</a>、<a href="/articles/20240311a/">2024</a>、<a href="/articles/20230327a/">2023</a></td></tr><tr><td align="left">4月</td><td align="left">春の入門祭り</td><td align="left">初心者向けに入門記事を書いてみよう<br>新年度に合わせて、基礎的な技術やツールの入門記事を発信します。</td><td align="left"><a href="/articles/20260421a/">2026</a>、<a href="/articles/20250413a/">2025</a>、<a href="/articles/20240408a/">2024</a>、<a href="/articles/20230417a/">2023</a>、<a href="/articles/20220418a/">2022</a>、<a href="/articles/20210414a/">2021</a>、<a href="/articles/20200529/">2020</a></td></tr><tr><td align="left">5月</td><td align="left">🔰テスト連載</td><td align="left">AI時代により重要度が増すテスト技術についての連載</td><td align="left">2026</td></tr><tr><td align="left">6月</td><td align="left">🔰データエンジニアリング</td><td align="left">データ基盤と活用<br>データ収集・蓄積・活用に関する連載です。</td><td align="left">2026</td></tr><tr><td align="left">7月</td><td align="left">Go 1.27</td><td align="left">Go 1.27リリース記念<br>年2回のリリースサイクルに合わせ、Goの最新動向を追います。</td><td align="left">Go1.27</td></tr><tr><td align="left">8月</td><td align="left">夏休み自由研究</td><td align="left">個人的に関心があることを深堀りして調べる連載<br>夏休みに行った自由研究と銘うって、好きな技術を探求します。</td><td align="left">2026、<a href="/articles/20250825a/">2025</a>、<a href="/articles/20240819a/">2024</a> <a href="/articles/20230830a/">2023</a>、<a href="/articles/20220822a/">2022</a>、<a href="/articles/20210823a/">2021</a>、<a href="/articles/20200726/">2020</a></td></tr><tr><td align="left">9月</td><td align="left">Java</td><td align="left">Javaリリース記念・エコシステム<br>エンタープライズ開発の中心であるJavaや周辺技術について取り上げます。</td><td align="left">2026、<a href="/articles/20250825a/">2025</a>、<a href="/articles/20240930a/">2024</a></td></tr><tr><td align="left">10月</td><td align="left">秋のブログ連載</td><td align="left">秋の夜長に楽しめる読み物<br>いつもより文章が多めで読み応えのある記事をお届けする週間です。</td><td align="left">2026、<a href="/articles/20251031a/">2025</a>、<a href="/articles/20241028a/">2024</a>、<a href="/articles/20231030a/">2023</a>、<a href="/articles/20221031a/">2022</a>、<a href="/articles/20211027a/">2021</a>、<a href="/articles/20201026/">2020</a></td></tr><tr><td align="left">11月</td><td align="left">Vue.js</td><td align="left">Vue.jsやNuxt関係について<br>フロントエンド開発におけるVue.jsのエコシステムや活用事例を紹介します。</td><td align="left">2026、<a href="/articles/20251016a/">2025</a>、<a href="/articles/20241125a/">2024</a></td></tr><tr><td align="left">12月</td><td align="left">アドベントカレンダー</td><td align="left">年末恒例イベント<br>Qiitaのアドベントカレンダーイベントに乗っかる連載です。</td><td align="left"><a href="https://qiita.com/advent-calendar/2025/future">2025</a>、<a href="https://qiita.com/advent-calendar/2024/future">2024</a>、<a href="https://qiita.com/advent-calendar/2023/future">2023</a>、<a href="https://qiita.com/advent-calendar/2022/future">2022</a>、<a href="https://qiita.com/advent-calendar/2021/future">2021</a>、<a href="https://qiita.com/advent-calendar/2020/future">2020</a>、<a href="https://qiita.com/advent-calendar/2019/future">2019</a>、<a href="https://qiita.com/advent-calendar/2018/future">2018</a>、<a href="https://qiita.com/advent-calendar/2017/future">2017</a>、<a href="https://qiita.com/advent-calendar/2016/future">2016</a>、<a href="https://qiita.com/advent-calendar/2015/future">2015</a></td></tr></tbody></table></div><h2 id="ブログリレー企画の概観"><a href="#ブログリレー企画の概観" class="headerlink" title="ブログリレー企画の概観"></a>ブログリレー企画の概観</h2><p>2021年に年間計画の公開を始めてから5年が経過しました。これまでの変遷を振り返ると、いくつかの傾向が見えてきます。</p><ul><li><strong>連載総量の適正化</strong><br>当初の2021年は、月に複数の連載を並行して走らせるなど非常に多作な年でした（例：5月にDart&#x2F;FlutterとServerlessを同時開催など）。しかし、寄稿者の募集や運営負荷の観点から、近年は「1ヶ月に1テーマ」＋「Goのリリース連載」というペースに落ち着き、無理なく継続できる体制へとシフトしています</li><li><strong>技術要素の変遷：トレンドと実需のバランス</strong><br>  その年ごとの技術トレンドや社内の関心事が色濃く反映されています<ul><li><strong>2021年〜2022年:</strong> GCP、Serverless、Flutter、IoT（電子工作）といった特定の技術領域への挑戦が多く見られました。一方で、寄稿者が集まりにくいテーマは翌年以降見直されるなど、柔軟に入れ替えが行われています</li><li><strong>2024年以降:</strong> JavaやVue.jsといった、当社での利用頻度が高い「実需」に基づく技術がテーマとして定着し始めました</li><li><strong>Go言語:</strong> 2021年から変わらず、年2回のリリース記念連載が継続されており、当ブログの技術的なバックボーンの一つとなっています（社内利用率はJavaの方が高いため、Go利用者の外部発信精神が強いことによる差だと思われます）</li></ul></li><li><strong>定着した人気の「季節企画」</strong><br>  技術テーマは変遷していますが、次の季節イベントは定番化しており、ブログリレーの骨格を成しています<ul><li><strong>4月：春の入門祭り</strong>（初心者歓迎の入門記事）</li><li><strong>8月：夏休み自由研究</strong>（業務外の趣味や深掘り）</li><li><strong>10月：秋のブログ週間</strong>（読み物エッセイ中心）</li><li><strong>12月：アドベントカレンダー</strong>（年末のお祭り）</li></ul></li></ul><p>ブログリレー企画を継続的に行うメリットは様々多く、<a href="https://future-architect.github.io/articles/20200908/">こちらの記事</a>に書いたときから大きな差分はありません。</p><p>2026年も、これらの「定番」と「新しい技術的挑戦」のバランスを大切にしながら企画しています。</p><h2 id="さいごに"><a href="#さいごに" class="headerlink" title="さいごに"></a>さいごに</h2><p>これまでの経緯を踏まえテーマをブラッシュアップしています。2026年も皆様にとって有益な情報発信を活発にしていきますので、応援よろしくお願いいたします！</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;/images/2026/20260219a/unnamed.jpg&quot; alt=&quot;&quot; width=&quot;1024&quot; height=&quot;572&quot; loading=&quot;lazy&quot;&gt;

&lt;h1 id=&quot;はじめに&quot;&gt;&lt;a href=&quot;#はじめに&quot;</summary>
        
      
    
    
    
    <category term="Culture" scheme="https://future-architect.github.io/categories/Culture/"/>
    
    
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
    
    <category term="運営" scheme="https://future-architect.github.io/tags/%E9%81%8B%E5%96%B6/"/>
    
    <category term="スケジュール" scheme="https://future-architect.github.io/tags/%E3%82%B9%E3%82%B1%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB/"/>
    
  </entry>
  
</feed>
