<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>KINTO Tech Blog | キントテックブログ</title>
        <link>https://blog.kinto-technologies.com/</link>
        <description>年齢・性別・国籍問わず多様なメンバーが、トヨタグループのモビリティサービスの世界展開を実現する技術集団として様々な情報を発信します !</description>
        <lastBuildDate>Thu, 11 Jun 2026 03:23:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>KINTO Tech Blog | キントテックブログ</title>
            <url>https://blog.kinto-technologies.com/assets/common/thumbnail_default.png</url>
            <link>https://blog.kinto-technologies.com/</link>
        </image>
        <copyright>©KINTO Technologies Corporation. All rights reserved.</copyright>
        <item>
            <title><![CDATA[Microsoft MVP（Microsoft Foundry カテゴリ）を受賞しました]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-06-11-microsoft-mvp-foundry/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-06-11-microsoft-mvp-foundry/</guid>
            <pubDate>Thu, 11 Jun 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[KTCのAIファーストGに所属する野村が、2026年6月にMicrosoft MVP（Microsoft Foundryカテゴリ）を受賞しました。受賞のご報告と、その背景にあった活動についてご紹介します。]]></description>
            <content:encoded><![CDATA[<h2>はじめに</h2>
<p>こんにちは。KINTOテクノロジーズ（以下、KTC）の AIファーストG に所属している、野村宏樹です。</p>
<p>このたび、2026年6月1日付で <strong>Microsoft MVP（Most Valuable Professional）を Microsoft Foundry カテゴリ</strong> で受賞しました。Azure 上での生成AI／AIエージェント開発を中心に発信してきた活動を評価いただいたもので、とても光栄に思っています。本記事では、受賞のご報告と、その背景にあったKTCでの活動についてご紹介します。</p>
<p>![Microsoft MVP として届いたトロフィー](/assets/blog/authors/nomura/2026-06-11-microsoft-mvp-foundry/MVP-trophy.jpg =400x)
<em>Microsoft MVP として届いたトロフィー</em></p>
<p>受賞の証として、MVPプロフィールも公開されています。</p>
<p><a href="https://mvp.microsoft.com/ja-JP/mvp/profile/93ebd9f1-a8c0-492c-8cc2-adc7f5f980e9">https://mvp.microsoft.com/ja-JP/mvp/profile/93ebd9f1-a8c0-492c-8cc2-adc7f5f980e9</a></p>
<p><img src="/assets/blog/authors/nomura/2026-06-11-microsoft-mvp-foundry/mvp-profile.png" alt="Microsoft MVP プロフィールページのスクリーンショット">
<em>Microsoft MVP プロフィールページ</em></p>
<h2>Microsoft MVP とは</h2>
<p>Microsoft MVP は、Microsoft 製品・技術に関する深い知見と、技術コミュニティへの継続的な貢献を称えて Microsoft 社が「個人」に授与するアワードです。直近およそ1年間の活動が審査対象で、毎年の更新審査があります。技術領域ごとにカテゴリが分かれており、世界で約 3,000 名が受賞しています。受賞すると、製品の早期アクセスや製品チームと直接つながれるチャネル、年次の Global MVP Summit への招待などの機会があります。</p>
<p>私が受賞した <strong>Microsoft Foundry カテゴリ</strong> は、比較的新しいカテゴリ名です。</p>
<p>:::message
Microsoft Foundry のカテゴリーは、Azure 上で生成AIアプリやAIエージェントを開発するための基盤に関する領域です。Azure AI Foundry がリブランディングされたもので、モデルやエージェントの開発・運用をまとめて扱うエコシステムを指します。
:::</p>
<h2>受賞につながった活動：KTCのカルチャーに支えられて</h2>
<p>今回の受賞は、KTC の <strong>Output文化・登壇文化</strong>、そして全社的に生成AIの業務活用を進めている環境に、大きく後押ししていただいたものでした。2025年8月にKTCへ入社して以来、「やってみたことを外に出す」「登壇して共有する」が当たり前にある雰囲気のなかで、自然と活動を続けることができました。</p>
<p>発信は、大きく2つの軸で行ってきました。</p>
<h3>① KTC事例の共有</h3>
<p>KTCでは2023年から社内向けの生成AIチャットツールを導入し、全社で生成AIの活用を進めています。その現場で得た学びを、たとえば「社内で使うチャットアプリを自分たちで内製する意義」といったテーマで共有してきました。このテーマは、NoMaps 2025（札幌）で当時のチームリーダーである和田颯馬さんと共同で登壇しています。
<a href="https://no-maps.jp/program/tech/121500/">https://no-maps.jp/program/tech/121500/</a></p>
<p>社内で使うものを自分たちの手で作るからこそ、現場のフィードバックを素早く反映でき、業務に本当に必要な形へ磨き込んでいけます。そうした内製ならではの価値を、実体験ベースでお話ししました。</p>
<h3>② ユースケースを軸とした技術活用の共有</h3>
<p>個人的に「面白い」「使えそう」と感じた技術を PoC で試し、ブログにまとめ、少し抽象化したものを登壇してOutputする、というサイクルで発信してきました。テーマは MCP（Model Context Protocol）、マルチエージェント（AutoGen / Microsoft Agent Framework）、Azure AI Foundry エコシステム、ローカルLLM／SLM（Foundry Local・phi-4）など。単に「何ができるか」だけでなく、<strong>どんなユースケースで、どう使うか</strong>まで踏み込むことを意識しています。</p>
<p>コミュニティ活動としては、KTCが主催する <strong>名古屋LLM MeetUp</strong> をはじめ、なごあず（JAZUG 名古屋支部）、JAZUG、すきやねん Azure など各地のコミュニティで登壇させていただきました。2025年度は個人として、ブログ31本・登壇11回ほどの活動になりました。</p>
<p>2025年度の登壇は以下のとおりです。</p>
<table>
<thead>
<tr>
<th>日付</th>
<th>イベント</th>
<th>タイトル</th>
</tr>
</thead>
<tbody><tr>
<td>2025/6/21</td>
<td><a href="https://75az.connpass.com/">なごあず（JAZUG 名古屋支部）</a></td>
<td>AzureでMCPサーバ！！どう活用する？</td>
</tr>
<tr>
<td>2025/7/23</td>
<td><a href="https://kinto-technologies.connpass.com/event/354960/">名古屋LLM MeetUp（KTC主催）</a></td>
<td>チャットアプリ失敗談！製造業業務への生成AI導入</td>
</tr>
<tr>
<td>2025/8/16</td>
<td><a href="https://jazug.connpass.com/event/361970/">JAZUG×なんでもCopilot #jaznancopa</a></td>
<td>Azure AI Foundry Portal デモ</td>
</tr>
<tr>
<td>2025/9/15</td>
<td><a href="https://no-maps.jp/program/tech/121500/">NoMaps 2025（札幌）</a></td>
<td>生成AI最前線：最新トレンドと活用事例（和田颯馬さんと共同）</td>
</tr>
<tr>
<td>2025/11/21</td>
<td><a href="https://kinto-technologies.connpass.com/event/370357/">名古屋LLM MeetUp（KTC主催）</a></td>
<td>AzureでのAIエージェントはここから！Azure Functions × AI</td>
</tr>
<tr>
<td>2025/11/27</td>
<td><a href="https://yonayona.connpass.com/event/374591/">YonaAz</a></td>
<td>AzureでAIエージェント、さて何から始める？</td>
</tr>
<tr>
<td>2025/11/29</td>
<td><a href="https://jazug.connpass.com/event/368296/">JAZUG Shizuoka</a></td>
<td>リアルタイム音声モデル gpt-realtime を使った音声対話ツール</td>
</tr>
<tr>
<td>2025/12/6</td>
<td><a href="https://75az.connpass.com/event/373754/">なごあず（JAZUG 名古屋支部）</a></td>
<td>ローカルとクラウドLLMのハイブリッドAI活用</td>
</tr>
<tr>
<td>2025/12/26</td>
<td><a href="https://sukiyanenazure.connpass.com/event/375556/">すきやねんAzure</a></td>
<td>ハイブリッド構成 Queue Polling</td>
</tr>
<tr>
<td>2026/2/28</td>
<td><a href="https://globalai.community/chapters/tokyo/events/agentcon-tokyo/">AgentCon Tokyo</a></td>
<td>エージェント開発とライフサイクル管理 ～構築から AgentStore 基盤まで～</td>
</tr>
<tr>
<td>2026/3/14</td>
<td><a href="https://75az.connpass.com/event/383389/">なごあず（JAZUG 名古屋支部）</a></td>
<td>gpt-realtime-1.5 モデルでスタックチャン</td>
</tr>
</tbody></table>
<h2>これから</h2>
<p>これからも、<strong>「どう使うか（ユースケース）」を大事にしながら、技術のOutputを続けていきたい</strong>と思っています。AIにより技術のキャッチアップや開発は非常にしやすくなっています。大事なのはその手段をどこにどう使うと価値がでるのか？だと思っています。引き続きAzure・生成AI・AIエージェント領域の技術を突き詰めながら、ユースケースを軸にした発信を続けていきます。</p>
<p>改めて、日々の活動を支えてくれているKTCの環境と、関わってくださったみなさまに感謝します。</p>
<p>発信内容は、個人のZennにもまとめています。よろしければこちらもご覧ください。</p>
<p><a href="https://zenn.dev/nomhiro">https://zenn.dev/nomhiro</a></p>
<h2>最後に</h2>
<p>KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています！
詳しくは<a href="https://www.kinto-technologies.com/recruit/">こちら</a>からご確認ください！</p>
<p>最後までお読みいただき、ありがとうございました。</p>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/blog/authors/nomura/2026-06-11-microsoft-mvp-foundry/cover.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[analyzer のパッチ番号 1 違いで止まった Flutter ビルド：retrofit と custom_lint の依存デッドロック解決記]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-06-02-flutter-build-deadlock-retrofit-custom-lint/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-06-02-flutter-build-deadlock-retrofit-custom-lint/</guid>
            <pubDate>Tue, 02 Jun 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[Flutter SDK のメジャーアップグレード中に遭遇した retrofit と custom_lint の analyzer 依存デッドロックを、pubspec のピン 2 行で解いた記録です。]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Flutter SDK 3.29 → 3.38 へのアップグレード中に遭遇した <code>retrofit</code> / <code>analyzer</code> / <code>custom_lint</code> の依存衝突を解いた記録です。
「なぜ <code>pub solver</code> が答えを見つけられないのか」から順を追って説明します。</p>
</blockquote>
<hr>
<h2>はじめに</h2>
<p>はじめまして、KINTOテクノロジーズ（KTC）でモバイルアプリ（Flutter）の開発を担当しているHand-Tomiです。</p>
<p>Flutter SDK のメジャーアップグレードを進めていたある日、<code>dart pub get</code> が突然失敗するようになりました。エラーメッセージを読み解くと、<code>retrofit_generator</code> と <code>custom_lint</code> がそれぞれ別々の <code>analyzer</code> バージョンを要求していて、両者が要求する <code>analyzer</code> のバージョン差はわずか 1 パッチ。けれど <code>pub solver</code> ではどうやっても解けない <strong>デッドロック</strong> でした。</p>
<p>本記事では、その原因と解決方法、そしてなぜ <code>dependency_overrides</code> が罠になるのかを順を追って解説します。同じ Flutter プロジェクトで似た衝突に遭遇した方の参考になれば幸いです。</p>
<p>:::message
<code>pub solver</code> は <code>dart pub get</code> の内部で動く依存解決エンジンです。すべての制約を同時に満たすバージョンの組み合わせを探すのが役割で、本記事ではこの後も繰り返し登場します。
:::</p>
<p>:::message
バージョン管理でよく耳にする <strong>SemVer (Semantic Versioning)</strong> は、バージョン番号を <code>MAJOR.MINOR.PATCH</code> の 3 桁で表す規約です。本記事では以降、それぞれ <strong>メジャー</strong>／<strong>マイナー</strong>／<strong>パッチ</strong> と表記します。</p>
<ul>
<li><code>MAJOR</code>（メジャー）：互換性のない変更（壊れる）</li>
<li><code>MINOR</code>（マイナー）：後方互換のある機能追加</li>
<li><code>PATCH</code>（パッチ）：後方互換のあるバグ修正</li>
</ul>
<p>たとえば <code>analyzer 8.4.0</code> → <code>8.4.1</code> は <strong>パッチ</strong> リリースなので、本来なら「コードを変えずに上げても安全」なはずです。本記事の 3 節で、この「はず」が崩れる仕組みを掘り下げます。
:::</p>
<hr>
<h2>TL;DR</h2>
<ul>
<li>Flutter のメジャーアップグレード中に <code>dart pub get</code> が失敗。原因は <code>retrofit_generator</code> と <code>custom_lint_visitor</code> が同じ <code>analyzer</code> に対して別々のバージョンを要求していたこと。</li>
<li>最終的な解：<strong><code>retrofit: ^4.9.2</code> + <code>retrofit_generator: ^10.2.1</code></strong>。<code>pubspec</code> のピン 2 行ですっきり解決します。</li>
<li><code>dependency_overrides</code> には罠があり、推奨しません。<code>pub get</code> は通っても <code>dart_style</code> が知らぬ間に昇格してビルドが壊れます。</li>
<li>この衝突は <strong>構造的な問題</strong> です。<code>analyzer</code> のメジャーが上がるたびに再発します。</li>
</ul>
<hr>
<h2>1. 始まり — 止まってしまったビルド</h2>
<p>Flutter SDK 3.29.2 から 3.38.10 へのメジャーアップグレードを進めていました。<code>flutter_riverpod</code> 2 → 3、<code>freezed</code> 2 → 3、<code>analyzer</code> 6 → 8 といった大きな変更が立て続けに来ていて、いつもなら <code>flutter upgrade</code> のあと <code>dart pub get</code> で済む作業のはずでした。</p>
<p>ところがビルドが止まりました。要点だけ抜き出すと、こういうメッセージです。</p>
<pre><code class="language-text">And because retrofit_generator &gt;=10.2.4 depends on analyzer &gt;=8.4.1 &lt;13.0.0
and custom_lint_core &gt;=0.7.0 depends on custom_lint_visitor ^1.0.0,
if retrofit_generator &gt;=10.2.4 and custom_lint_core &gt;=0.7.0 then analyzer 9.0.0.

And because custom_lint &gt;=0.8.1 depends on both analyzer ^8.0.0 and custom_lint_core 0.8.1,
custom_lint &gt;=0.8.1 is incompatible with retrofit_generator &gt;=10.2.4.

So, because app depends on both retrofit_generator ^10.2.5 and custom_lint ^0.8.1,
version solving failed.
</code></pre>
<p><code>pub solver</code> が答えを見つけられなかったのです。片方を上げればもう片方が壊れ、下げればまた別のところが壊れる。普通のバージョン衝突ではなく、<strong>デッドロック</strong> でした。</p>
<hr>
<h2>2. 誰と誰が戦っているのか</h2>
<p>主な登場人物は次のとおりです。</p>
<ul>
<li><code>analyzer</code> — Dart コードの静的解析エンジン（共有資源）</li>
<li><code>retrofit_generator</code> — <code>.g.dart</code> を生成するコードジェネレータ</li>
<li><code>custom_lint</code> / <code>custom_lint_core</code> / <code>custom_lint_builder</code> — lint プラグインのランナーと、その builder</li>
<li><code>custom_lint_visitor</code> — analyzer の AST を訪問する visitor 実装</li>
</ul>
<p><img src="/assets/blog/authors/semyeong/2026-06-02-flutter-build-deadlock-retrofit-custom-lint/02-dependency-diagram-ja.png" alt="依存関係図 — analyzer を真ん中に置き、retrofit_generator 陣営と custom_lint_visitor 陣営が両側から引っ張り合う構造"></p>
<p>問題の核心は、<strong><code>analyzer</code> という共有資源</strong> です。両陣営が同じ <code>analyzer</code> に対して別々のバージョンを要求しています。</p>
<ul>
<li><code>retrofit_generator 10.2.3+</code> → 「<code>analyzer</code> 8.4.1 以上が必要」</li>
<li><code>custom_lint_visitor 1.0.0+8.4.0</code> → 「<code>analyzer</code> 8.4.0 ちょうど」</li>
</ul>
<p>差は 8.4.0 と 8.4.1、<strong>わずか 1 パッチ</strong>。これだけでビルドが止まるのです。</p>
<p>なお、1 節のエラーメッセージ末尾には <code>analyzer 9.0.0</code> も登場しますが、これは <code>custom_lint_visitor</code> に <code>1.0.0+8.4.0</code> のほかに <code>1.0.0+9.0.0</code> ビルドも存在し、<code>custom_lint_visitor: ^1.0.0</code> を介した solver が両方を順に試した結果です。どちらも <code>analyzer</code> をバージョン固定で要求する点は同じなので、本質的な対立点は変わりません。</p>
<hr>
<h2>3. なぜ 1 パッチ差で壊れるのか</h2>
<p>ここで 2 つの事実が噛み合います。</p>
<h3>事実 1. <code>analyzer</code> の <strong>内部 API</strong> はパッチリリースでも変わる</h3>
<p>SemVer の約束は「パッチリリースでは <strong>公開 API は後方互換</strong> を保つ」です。ところが <code>custom_lint_visitor</code> が使っているのは <code>analyzer</code> の公開 API ではなく、<strong>内部 API</strong>（AST ノードの型など、パッケージの内部実装に属するもの）です。SemVer の保護範囲外なので、メジャー・パッチを問わず、型が消えたり、シグネチャが変わったりするのは珍しくありません。</p>
<p>後ほど引用するメンテナ自身の言葉を借りれば <em>&quot;some more unique APIs&quot;</em> — SemVer の通常のセーフティネットの外側で扱う必要のある API です。本記事の 5 節で扱う <code>dart_style 3.1.9</code> の <code>LabelReference</code> / <code>NamedArgument</code> 欠落も、「内部 API は SemVer 保護外」という同じ構造から生じる事例の 1 つです（こちらは analyzer のメジャー間で起きたケースで、3 節でいうパッチ単位の例ではありません）。</p>
<h3>事実 2. <code>custom_lint_visitor</code> はそれゆえ <strong>バージョンを完全に固定</strong> する</h3>
<p>これを知っているからこそ、<code>custom_lint_visitor</code> のメンテナは意図的に <code>analyzer</code> を完全に固定しています。パッケージ名そのものがその証拠です。</p>
<pre><code class="language-text">custom_lint_visitor 1.0.0+8.4.0
                          ^^^^^
                          analyzer のバージョン
</code></pre>
<p><code>pubspec.yaml</code> の中でも <code>analyzer: 8.4.0</code>（<code>^</code>(caret) なしのバージョン固定）になっています。</p>
<p>これがミスならば PR 一本で解決する話ですが、これは<a href="https://github.com/invertase/dart_custom_lint/issues/345">関連する GitHub issue</a> でメンテナ自身が明言した <strong>意図的な方針</strong> です。</p>
<blockquote>
<p>&quot;Custom_lint depends on some more unique APIs. I&#39;ll probably stick to requiring 8.0 for it.&quot;
— <a href="https://github.com/invertase/dart_custom_lint/issues/345">invertase/dart_custom_lint#345</a></p>
</blockquote>
<p>発言の直接の意図は「メジャー（<code>8.0</code>）単位で範囲を狭めて require する」ですが、その方針が実際のリリースにも反映されており、リリースされる <code>custom_lint_visitor</code> の各バージョンでは <code>analyzer: 8.4.0</code> のように <strong>バージョンが完全に固定</strong> されています（<code>1.0.0+8.4.0</code> → <code>analyzer: 8.4.0</code>、<code>^</code>(caret) なし）。つまり <strong>意図された決定</strong> の結果としてバージョン固定が生まれており、両者が同じ <code>analyzer</code> バージョンを要求するビルドが揃うまでは <code>pub solver</code> だけでは解けません。</p>
<hr>
<h2>4. 効果のなかった試みリスト</h2>
<p>問題が難しく見えると、人は迂回路を探したくなります。しかし直感的に思いつく次の試みはどれも徒労でした。</p>
<table>
<thead>
<tr>
<th>試み</th>
<th>なぜ失敗するのか</th>
</tr>
</thead>
<tbody><tr>
<td><code>retrofit_generator</code> を最新（10.2.5）に上げる</td>
<td><code>analyzer</code> 8.4.1 を要求するため <code>custom_lint_visitor</code> と衝突</td>
</tr>
<tr>
<td><code>retrofit</code> の上限を狭めてみる（<code>&lt;4.9.1</code>）</td>
<td>generator 10.2.1 を選びたい動機は 6 節で詳述しますが、retrofit を 4.9.0 系に下げると今度は generator 10.2.1 のソースが <code>retrofit</code> 4.9.2 の新 enum 値（<a href="https://github.com/trevorwang/retrofit.dart/blob/retrofit-v4.9.2/retrofit/lib/http.dart#L31"><code>Parser.DartMappable</code></a>）を参照しているため、generator 自体の AOT コンパイルが <code>Member not found</code> で失敗します</td>
</tr>
<tr>
<td><code>custom_lint_builder</code> のダウングレード</td>
<td>analyzer のメジャーが 7.x まで引きずり下ろされ、今度は <code>retrofit_generator</code>（analyzer 8.x 依存）と別の衝突を起こす — freezed / riverpod など analyzer 8 に依存するパッケージがある環境でも同様</td>
</tr>
<tr>
<td><code>analysis_options.yaml</code> の lint を切る</td>
<td>（<code>dependency_overrides</code> で solver を通した後でも）<code>lint</code> を切って <code>exclude: &#39;**/*.g.dart&#39;</code> を両方適用しても、generator AOT 段階で発生する <code>Member not found</code> 系のコンパイルエラーはそのまま発生する</td>
</tr>
<tr>
<td><code>dependency_overrides</code> で強制固定</td>
<td><code>pub get</code> は通るがビルド段階で <code>dart_style</code> が壊れる（5 節を参照）</td>
</tr>
</tbody></table>
<p>特に最後の項目、<code>dependency_overrides</code> は罠が深いので、別途取り上げる価値があります。</p>
<blockquote>
<p>上の表の各行は、本記事と同じリポジトリの検証成果物（<code>reports/01-reproduction.md</code>、<code>reports/03-overrides-fallback.md</code>）で実際のコマンド出力として再現されています。</p>
</blockquote>
<hr>
<h2>5. <code>dependency_overrides</code> という罠</h2>
<p>最初の発想は単純です。「2 つのパッケージが争うなら、こちらで強制的に片方のバージョンを打ち込もう」。</p>
<pre><code class="language-yaml">dependency_overrides:
  retrofit: ^4.9.2
  retrofit_generator: ^10.2.5
  analyzer: ^8.4.1
</code></pre>
<p>驚くことに <code>dart pub get</code> は通ります。なぜなら <code>dependency_overrides</code> は <strong>オーバーライドした依存に対する他パッケージからの制約を黙らせ</strong>、solver の選択肢を広げるからです。</p>
<p>ところが <code>dart run build_runner build</code> の段階で、突然ビルドが壊れます。</p>
<pre><code class="language-text">Failed to build build_runner:build_runner:
  .../dart_style-3.1.9/lib/src/front_end/ast_node_visitor.dart:1279:28:
    Error: Type &#39;LabelReference&#39; not found.
</code></pre>
<p><code>dart_style</code> です。私たちが明示的に依存もしていないパッケージです。</p>
<p>理由を辿ってみると、次のようになっています。</p>
<p><img src="/assets/blog/authors/semyeong/2026-06-02-flutter-build-deadlock-retrofit-custom-lint/03-silent-promotion-chain-ja.png" alt="サイレント昇格の因果連鎖 — override が制約を黙らせる → solver が自由 → 最新 dart_style 3.1.9 を自動選択 → analyzer 13 の AST 型を参照 → 強制された `8.x` 系にないのでコンパイル失敗"></p>
<ol>
<li><code>dependency_overrides</code> がオーバーライドした依存（<code>retrofit</code>、<code>retrofit_generator</code>、<code>analyzer</code>）に対する他パッケージからの制約を黙らせ、solver の選択肢が広がる</li>
<li>その結果、solver は transitive で <code>dart_style</code> の最新版（<code>3.1.9</code>）を自動的に選ぶ</li>
<li><code>dart_style 3.1.9</code> は <code>analyzer</code> の最新メジャーで導入された AST 型（<code>LabelReference</code>、<code>NamedArgument</code>、<code>BlockEnumBody</code> など）を参照している</li>
<li>しかし私たちは override で <code>analyzer ^8.4.1</code>（解決範囲は <code>&gt;=8.4.1 &lt;9.0.0</code>）を強制している</li>
<li>→ その範囲には存在しない型を参照しようとしてコンパイル失敗</li>
</ol>
<p>要するに <strong><code>dependency_overrides</code> は制約を黙らせるだけで、互換性を保証しません。</strong> 一箇所を押さえるとまた別の場所から噴き出します。これを抑え込もうとすると <code>dart_style</code> もピン、<code>custom_lint_visitor</code> も確認…… と際限なく増えていきます。</p>
<hr>
<h2>6. 結局解けた方法 — シンプルなピン調整 2 行</h2>
<p>問題を逆から見ると答えが見えます。</p>
<ul>
<li>私たちが変えられないもの：<code>custom_lint_visitor 1.0.0+8.4.0</code> → <code>analyzer 8.4.0</code>（正確には <code>custom_lint_visitor</code> 自体は <code>1.0.0+9.0.0</code> ビルドも存在しますが、それを選ぶと <code>custom_lint</code> 本体が要求する <code>analyzer ^8.0.0</code> と衝突するため、<code>custom_lint</code> を使う限り 8.4.0 ピン側に寄せるしかありません。1 節のエラーメッセージにも <code>custom_lint &gt;=0.8.1 depends on ... analyzer ^8.0.0</code> として現れています）</li>
<li>私たちが変えられるもの：<code>retrofit_generator</code> のバージョン</li>
</ul>
<p>であれば「<code>analyzer 8.4.0</code> でも動く最新の <code>retrofit_generator</code>」を探せばよいわけです。</p>
<p><code>retrofit_generator</code> のバージョン別要求を表にまとめると：</p>
<p><img src="/assets/blog/authors/semyeong/2026-06-02-flutter-build-deadlock-retrofit-custom-lint/04-version-matrix-ja.png" alt="retrofit_generator バージョン × analyzer 要求範囲のマトリクス — retrofit 4.9.2 互換 ∩ analyzer 8.4.0 互換の交点は 10.2.1 のみ"></p>
<table>
<thead>
<tr>
<th><code>retrofit_generator</code></th>
<th><code>analyzer</code> 要求</th>
<th><code>logError</code> の呼び出し形式</th>
</tr>
</thead>
<tbody><tr>
<td>10.2.0</td>
<td><code>&gt;=7.7.1 &lt;10.0.0</code></td>
<td>positional 4 個</td>
</tr>
<tr>
<td><strong>10.2.1</strong></td>
<td><strong><code>&gt;=8.0.0 &lt;10.0.0</code></strong></td>
<td><strong>named (<code>response: _result</code>)</strong></td>
</tr>
<tr>
<td>10.2.3</td>
<td><code>&gt;=8.4.1 &lt;11.0.0</code></td>
<td>named</td>
</tr>
<tr>
<td>10.2.4 / 10.2.5</td>
<td><code>&gt;=8.4.1 &lt;13.0.0</code></td>
<td>named</td>
</tr>
</tbody></table>
<blockquote>
<p>補足: <code>retrofit.dart</code> は monorepo で、<code>retrofit_generator</code>（タグ <code>v10.x.x</code>）と <code>retrofit</code>（タグ <code>retrofit-vX.Y.Z</code>）を別系統で管理しています。本記事のリンクで prefix が混在するのはそのためです。なお <code>10.2.2</code> はリリースが存在しますが、本記事の議論には影響しないため上の表では省略しています。</p>
</blockquote>
<p>答えが見えます。<strong>10.2.1</strong> です。</p>
<ul>
<li><code>analyzer 8.4.0</code> と互換 ✓（<code>&gt;=8.0.0</code> なので）</li>
<li><code>retrofit 4.9.2</code> の <code>{Response? response}</code> named optional シグネチャと互換 ✓</li>
<li><code>dependency_overrides</code> 不要 ✓</li>
</ul>
<pre><code class="language-yaml:pubspec.yaml">dependencies:
  retrofit: ^4.9.2

dev_dependencies:
  retrofit_generator: ^10.2.1
</code></pre>
<p>これだけです。<code>^10.2.1</code> というキャレット範囲を書いても、10.2.3+ は <code>analyzer 8.4.1</code> を要求してくるので自動的に候補から外れ、実効的に 10.2.1 が選ばれます。</p>
<blockquote>
<p>ちなみに <code>retrofit_generator 10.2.1</code> と <code>10.2.5</code> の <strong><code>logError</code> の呼び出しシグネチャは同一</strong> です。generator のソース（<code>lib/src/generator.dart</code>）を直接比較しても、両バージョンとも <code>&#39;$_errorLoggerVar?.logError(e, s, $_optionsVar, response: $_resultVar);&#39;</code> という同一の出力テンプレートを使っています（<a href="https://github.com/trevorwang/retrofit.dart/blob/v10.2.1/generator/lib/src/generator.dart#L3777">v10.2.1#L3777</a> / <a href="https://github.com/trevorwang/retrofit.dart/blob/v10.2.5/generator/lib/src/generator.dart#L3849">v10.2.5#L3849</a>）。10.2.5 には <code>Stream&lt;Uint8List&gt;</code> / <code>Stream&lt;String&gt;</code> 処理の検証など別の機能が追加されていますが、本記事が扱う retrofit ↔ analyzer インターフェイスそのものは変更されていません。つまり 10.2.1 に留まることは、コア機能面で損ではありません。</p>
</blockquote>
<hr>
<h2>7. それで私たちが学んだこと</h2>
<p>この件が片付いたとき、最初に浮かんだ考えは <strong>「次のメジャーアップグレードでまた出くわすだろうな」</strong> でした。</p>
<p>理由は 2 つです。</p>
<ol>
<li><strong><code>analyzer</code> の内部 API は今後もパッチで変わり続ける。</strong> それが AST を扱う解析器パッケージの本質です。</li>
<li><strong><code>custom_lint_visitor</code> は今後もバージョンを完全に固定し続ける。</strong> メンテナが意図的に取っている方針だからです。</li>
</ol>
<p>つまりこの衝突は <strong>構造的</strong> です。本記事を書いている 2026 年春の時点で、<code>analyzer</code> はすでに 13.0.0 までリリースされており、<code>custom_lint_visitor</code> のピンラインは <code>1.0.0+9.0.0</code> までしか追いついていません。</p>
<p><img src="/assets/blog/authors/semyeong/2026-06-02-flutter-build-deadlock-retrofit-custom-lint/05-analyzer-release-timeline-ja.png" alt="analyzer のメジャーリリースのタイムライン（8 → 9 → 10 → 11 → 12 → 13）の上に custom_lint_visitor のピン（1.0.0+8.4.0、1.0.0+9.0.0）を重ねた図 — custom_lint_visitor がまばらに追従するパターン"></p>
<p><code>custom_lint_visitor</code> がメジャーごとに 1 〜 2 個のビルドだけ追いつくこのまばらなパターンが続く限り、<code>analyzer</code> がさらに一段上がるたびに同じ形で再発します。実際、<a href="https://github.com/trevorwang/retrofit.dart/issues/911"><code>retrofit.dart</code> の issue tracker</a> を見ると <code>analyzer 10.0</code> の段階でも同じシグネチャミスマッチが報告されています。</p>
<p>であれば、私たちにできることは：</p>
<ul>
<li><strong>自然な解決を先に試す。</strong> ピン 1 つの調整で解けるかをまず確認する。シンプルな答えがあるのに <code>dependency_overrides</code> を最初に持ち出さない。</li>
<li><strong><code>dependency_overrides</code> は最後の手段。</strong> 黙らせるだけでは解決にならない。一箇所を押さえると別の場所から噴き出す。</li>
<li><strong>プレイブックを残す。</strong> 次の人（あるいは 6 か月後の自分）が同じ罠にはまらないように。メカニズムと意思決定ツリーを一緒に書き残しておく（本記事自体がそのプレイブックの 1 つです）。</li>
</ul>
<hr>
<h2>最後に</h2>
<p>ここまで読んでいただき、ありがとうございます。</p>
<p><code>analyzer</code> のような共有依存をめぐる衝突は、一見すると「2 つのパッケージのバグ」に見えますが、実際にはエコシステム側の構造的な制約が背景にあります。同じ罠に出会ったときに「最初に何を疑い、何を試し、どこで止まるか」を整理できれば、次は数時間で解けるはずです。</p>
<p>皆さんの参考になれば幸いです。</p>
<hr>
<h2>参考</h2>
<ul>
<li><a href="https://github.com/invertase/dart_custom_lint/issues/345"><code>dart_custom_lint</code> #345 — Support analyzer 8</a></li>
<li><a href="https://github.com/trevorwang/retrofit.dart/issues/911"><code>retrofit.dart</code> #911 — analyzer 10.0.0+ compatibility</a></li>
<li><a href="https://pub.dev/packages/retrofit_generator/versions">pub.dev — <code>retrofit_generator</code> バージョン別依存関係</a></li>
<li><a href="https://github.com/trevorwang/retrofit.dart/blob/retrofit-v4.9.2/retrofit/lib/http.dart#L17-L34"><code>retrofit</code> 4.9.2 — <code>Parser.DartMappable</code> enum 追加箇所</a></li>
</ul>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/blog/authors/semyeong/2026-06-02-flutter-build-deadlock-retrofit-custom-lint/cover.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[テックブログの「関連記事レコメンド」をローカル Embedding で再構築した話]]></title>
            <link>https://blog.kinto-technologies.com/posts/torii-techblog-related-content-gen/</link>
            <guid>https://blog.kinto-technologies.com/posts/torii-techblog-related-content-gen/</guid>
            <pubDate>Mon, 01 Jun 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[テックブログの「関連する記事」を Go + Ollama + Qwen3-Embedding-0.6B で再構築しました。Python + Azure OpenAI から、ローカルで動く Embedding ベースの類似度計算に移行した過程を書いています。PoC でのモデル選定、テキスト切り詰めの品質検証、num_ctx のサイレントトランケーション問題、SHA-256 差分キャッシュの設計など。]]></description>
            <content:encoded><![CDATA[<p>※本記事は Claude Code との協働で執筆し、人間がレビューの上投稿しています。</p>
<h2>1. はじめに</h2>
<p>こんにちは、共通サービス開発グループの鳥居(<a href="https://x.com/yu_torii">@yu_torii</a>)です。</p>
<p>前回の記事では、Slack 上で LLM を活用する社内チャットボットの実装事例を紹介しました。</p>
<p>@<a href="https://blog.kinto-technologies.com/posts/torii-ai_tool_slack/">card</a></p>
<p>今回は、このテックブログの「関連する記事」と「関連する求人」機能をゼロから再構築した話をします。</p>
<h3>「関連する記事」「関連する求人」とは</h3>
<p>各記事ページの下部に、2つのレコメンドセクションがあります。</p>
<ul>
<li>関連する記事: 現在読んでいる記事と内容が近い記事を最大12件表示</li>
<li>関連する求人: 記事の技術領域に関連する KINTO Technologies の求人情報を最大8件表示</li>
</ul>
<p>読者が興味のある技術領域を深掘りする導線であり、過去の記事の発見にもつながります。採用への接点でもあります。</p>
<h3>仕組みの基本：Embedding とコサイン類似度</h3>
<p>この機能の核は <strong>Embedding</strong>（埋め込みベクトル）です。Embedding モデルにテキストを入力すると、その意味を表す数百〜数千次元の数値ベクトルが返ってきます。意味的に近いテキスト同士は、ベクトル空間上で近い位置に配置されます。</p>
<p>2 つのベクトルの「近さ」を測る指標が<strong>コサイン類似度</strong>です。値が 1 に近いほど意味が近く、0 に近いほど無関係（直交）です。すべての記事を Embedding し、ペアごとにコサイン類似度を計算してスコアの高い順に並べれば、「関連する記事」のランキングが得られます。</p>
<h3>旧システムの課題</h3>
<p>この機能は以前、Python + Azure OpenAI の Embedding API で実装されていました。運用を続ける中で 3 つの問題が出てきました。</p>
<ol>
<li>差分更新が無い。毎回全記事を再 Embed</li>
</ol>
<p>CI が走るたびに全記事（当時 900 件超）を Azure OpenAI に送って Embedding していました。1 記事の追加でも全件再処理が走り、ビルド時間の大半を占めていました。</p>
<ol start="2">
<li>Azure OpenAI の 429 (Rate Limit) エラーが頻発</li>
</ol>
<p>900 件超の記事を一気に送ると、Azure OpenAI のレート制限に頻繁にヒットしていました。リトライロジックを入れてもタイミング次第で CI が失敗し、再実行が必要になることも珍しくありませんでした。</p>
<ol start="3">
<li>外部 API 依存 = コスト増加</li>
</ol>
<p>Embedding API の呼び出し回数がビルドのたびに積み上がり、コストが増え続けていました。記事数が増えるほど状況は悪化する構造です。</p>
<h3>今回やったこと</h3>
<p>これらの問題を解決するため、Go + Ollama（ローカル Embedding）でシステムを一から再構築しました。</p>
<p>SHA-256 ハッシュで変更記事だけ再 Embed する差分更新と、Ollama による CI ランナー上でのローカル実行（外部 API 呼び出しゼロ）で、旧システムの 3 つの課題を解消しました。</p>
<p>PoC でのモデル選定からパフォーマンス最適化、CI/CD パイプラインの構築まで、実装の全体像を書きます。開発には Claude Code を使いました（おまけで触れます）。</p>
<hr>
<h2>この記事で得られること</h2>
<ul>
<li>Go + Ollama + Qwen3-Embedding でローカル Embedding による類似度計算を組む方法</li>
<li>Ollama <code>num_ctx</code> のサイレントトランケーション（無警告の文字切り詰め）問題</li>
<li>事前正規化と min-heap Top-K によるコサイン類似度ランキングの効率化</li>
<li>SHA-256 差分キャッシュで変更記事だけ再 Embed する仕組み</li>
</ul>
<p>:::message
この記事の内容は執筆時点（2026年4月）の実装に基づいています。Ollama や Qwen3-Embedding のバージョンアップにより、API の仕様やパフォーマンス特性が変わる可能性があります。また、記事中のベンチマーク値は GitHub Actions ランナーでの計測結果であり、環境によって異なります。
:::</p>
<hr>
<h2>2. PoC 検証とモデル選定</h2>
<p>旧システムの課題（セクション 1 で述べた 429 エラー・全量実行・コスト増加）を解決するため、ローカル Embedding への移行を決めました。Go で使える Embedding ライブラリを 3 つの方式で PoC 検証しました。</p>
<h3>3 つの PoC アプローチ</h3>
<p>方式 1: hugot（Pure Go ONNX ランタイム）</p>
<p><a href="https://github.com/knights-analytics/hugot">knights-analytics/hugot</a> は Go ネイティブの ONNX ランタイムで、bge-m3 や Qwen3 の ONNX モデルを直接実行できます。cgo 不要ですが、ONNX モデルファイルのサイズが巨大（bge-m3 で約 2.2GB）で、CI 環境でのダウンロードとメモリ管理に課題がありました。</p>
<p>方式 2: kelindar/search（llama.cpp via purego）</p>
<p><a href="https://github.com/kelindar/search">kelindar/search</a> は一見 Pure Go に見えますが、内部では <code>purego</code> 経由で llama.cpp のバイナリを呼び出しています。cgo は使っていませんが、実質的に llama.cpp バイナリへの外部依存がありました。「cgo 不要」の表面的な特徴に惑わされかけた案件です。</p>
<p>方式 3: Ollama API（HTTP クライアント）</p>
<p>選んだのは Ollama の HTTP API を Go クライアントから呼ぶ方式です。</p>
<pre><code class="language-go:cmd/related-content-gen-poc-ollama/main.go">client, err := api.ClientFromEnvironment()
if err != nil {
    slog.Error(&quot;Ollama クライアント作成失敗&quot;, &quot;error&quot;, err)
    os.Exit(1)
}

resp, err := client.Embed(ctx, &amp;api.EmbedRequest{
    Model: model,
    Input: testTexts,
})
</code></pre>
<h3>比較表</h3>
<table>
<thead>
<tr>
<th>方式</th>
<th>cgo</th>
<th>モデル管理</th>
<th>バッチ対応</th>
<th>コンテキスト制御</th>
<th>判定</th>
</tr>
</thead>
<tbody><tr>
<td>hugot (ONNX)</td>
<td>不要</td>
<td>手動</td>
<td>○</td>
<td>×</td>
<td>△ モデルサイズ問題</td>
</tr>
<tr>
<td>kelindar (llama.cpp)</td>
<td>purego 経由で不要に見えるが llama.cpp バイナリ依存</td>
<td>手動</td>
<td>×</td>
<td>×</td>
<td>× 実質外部依存</td>
</tr>
<tr>
<td>Ollama API</td>
<td>不要</td>
<td>自動</td>
<td>○</td>
<td>○ (<code>num_ctx</code>)</td>
<td>◎</td>
</tr>
</tbody></table>
<h3>選定の決め手</h3>
<p>cgo 不要で <code>GOOS=linux GOARCH=arm64 go build</code> 一発のクロスコンパイルが壊れない。Ollama がモデルのダウンロードからライフサイクル管理まで担う。バッチ Embed API で複数テキストを一度に送信できる。<code>num_ctx</code> でコンテキストウィンドウを明示制御できる。</p>
<h3>なぜ Qwen3-Embedding-0.6B か</h3>
<p>Qwen3-Embedding-0.6B を選んだ理由は、2025 年リリースの最新モデルで、量子化後 639MB と CI ランナーのメモリに収まるサイズだったこと。1024 次元ベクトルで表現力と計算量のバランスが良い。日本語・英語のバイリンガルサポートは、当ブログの運用上の必須要件でした。RAG の検索精度が求められるタスクではなく関連記事の推薦用途なので、最高精度モデルは不要です。</p>
<p>:::details 量子化とは
量子化（Quantization）は、モデルの重み（パラメータ）を元の精度（通常 float16 = 16bit）からより少ないビット数（8bit、4bit など）に変換する手法です。精度はわずかに低下しますが、モデルサイズとメモリ使用量を大幅に削減できます。</p>
<p>Qwen3-Embedding-0.6B は Ollama で <a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF">Q8_0（8bit 量子化）</a>として配布されており、595M パラメータで 639MB。一方、bge-m3 は F16（16bit）配布のため、パラメータ数はほぼ同じ（568M）でもサイズが 1.2GB と約 2 倍になります。
:::</p>
<p>:::message
PoC の段階では bge-m3 も候補でしたが、モデルサイズだけでなくベンチマークでも Qwen3 が優位でした。<a href="https://qwenlm.github.io/blog/qwen3-embedding/">MTEB ベンチマーク</a>の英語検索（61.82 vs 57.03）、多言語検索（64.64 vs 58.36）、コード検索（75.41 vs 41.38）で Qwen3-Embedding-0.6B が上回っています。bge-m3 が優位なのは長文検索（MLDR: 59.51 vs 50.26）ですが、先頭 4000 文字に切り詰める本システムでは該当しません。Ollama でのモデルサイズも約半分（639MB vs 1.2GB）で、CI キャッシュの効率も含めて総合的に Qwen3 を選択しました。
:::</p>
<hr>
<h2>3. アーキテクチャの全体像</h2>
<h3>パイプライン</h3>
<pre><code class="language-mermaid">flowchart LR
    A[&quot;_posts/*.md&quot;] --&gt; B[&quot;Markdown&lt;br&gt;クリーニング&quot;]
    B --&gt; C[&quot;Ollama Embed API&lt;br&gt;(Qwen3-Embedding)&quot;]
    C --&gt; D[&quot;SHA-256&lt;br&gt;キャッシュ&quot;]
    D --&gt; E[&quot;コサイン類似度&lt;br&gt;ランキング&quot;]
    E --&gt; F[&quot;related_posts.json&quot;]
</code></pre>
<p>Markdown をクリーニングして Ollama で Embedding を取得し、コサイン類似度でランキングして JSON を出力します。</p>
<h3>パッケージ構成</h3>
<pre><code class="language-text">cmd/related-content-gen/
├── main.go                          # CLI エントリポイント
├── internal/
│   ├── markdown/                    # Markdown パース・クリーニング
│   │   ├── cleaner.go              #   frontmatter 除去、URL/assets 除去
│   │   └── parser.go               #   _posts/*.md の読み込み
│   ├── embedding/                   # Ollama クライアント・キャッシュ
│   │   ├── client.go               #   Embed API ラッパー（num_ctx 制御）
│   │   └── cache.go                #   SHA-256 ハッシュベースの差分更新
│   ├── similarity/                  # 類似度計算・ランキング
│   │   ├── cosine.go               #   コサイン類似度（テスト用）
│   │   └── ranking.go              #   L2正規化 + dotProduct、min-heap Top-K
│   └── output/                      # JSON 出力
│       └── json.go                 #   UTF-8、4スペースインデント、HTMLエスケープなし
└── go.mod
</code></pre>
<p><code>internal</code> パッケージに分離することで、各パッケージが単一責任を持ち、独立してテスト可能になっています。</p>
<h3><code>run()</code> 関数のパイプライン</h3>
<p>メイン処理は <code>run()</code> 関数に集約されています。</p>
<pre><code class="language-go:main.go">func run(...) error {
    // 1. 記事の読み込みとクリーニング
    posts, err := markdown.ParsePosts(postsDir)

    // 2. Ollama クライアント作成
    client, err := embedding.NewClient(ollamaURL, model, numCtx)

    // 3. キャッシュ読み込み → 不要エントリ削除 → 変更記事検出
    cache, err := embedding.LoadCache(cacheFile)
    cache.Prune(posts)
    dirty := cache.FindDirty(posts, model)

    // 4. 変更分のみ Embed（1件ずつ処理して都度キャッシュ保存）
    for _, p := range dirty {
        vectors, err := client.Embed(ctx, []string{text})
        cache.Entries[p.Slug] = embedding.CacheEntry{...}
        cache.Save(cacheFile) // 中断耐性のため毎回保存
    }

    // 5. コサイン類似度でランキング
    rankings := similarity.RankRelatedPosts(postVectors, 12)

    // 6. JSON 出力
    output.WriteJSON(outPath, postsOutput)
}
</code></pre>
<h3>Next.js フロントエンドとの連携</h3>
<p>出力される JSON は Next.js の <code>getStaticProps</code> でビルド時に読み込まれます。</p>
<ul>
<li><code>static/related_posts/related_posts.json</code> → <code>lib/related_posts.ts</code> が読み込み</li>
</ul>
<p>フロントエンド側では、JSON に関連記事データがあればそれを使い、無ければカテゴリベースのフォールバックに切り替わります。Go CLI とフロントエンドの間の契約は、この JSON スキーマだけです。</p>
<hr>
<h2>4. Markdown のクリーニングと前処理</h2>
<p>当ブログの記事は <a href="https://zenn.dev/zenn/articles/markdown-guide">Zenn Markdown</a>（<code>:::message</code>、<code>:::details</code>、<code>@[card]()</code> など）で書かれています。各記事ファイルの先頭には YAML frontmatter（タイトル、著者、公開日、カテゴリなどのメタ情報）があり、これらをそのまま Embed するとノイズになります。</p>
<h3>クリーニングパイプライン</h3>
<ol>
<li>frontmatter の分離: <code>---</code> で囲まれた YAML ヘッダーからタイトルだけ抽出し、残りのメタ情報（author, date, category 等）は除去</li>
<li>URL の除去: <code>http://</code> / <code>https://</code> で始まるすべての URL を除去</li>
<li>アセットリンクの除去: <code>/assets/</code> を含むリンク（画像パスなど）を除去</li>
</ol>
<p>クリーニングのエントリポイントは <code>CleanMarkdown</code> 関数で、frontmatter からタイトルを抽出しつつ、本文のノイズを除去します。frontmatter パースには <code>strings.Cut</code> を使い、<code>---</code> デリミタ間の YAML を <code>gopkg.in/yaml.v3</code> で解析しています。</p>
<p>:::details コードの詳細（cleaner.go / parser.go）</p>
<pre><code class="language-go:internal/markdown/cleaner.go">var (
    reURL   = regexp.MustCompile(`https?://[^\s)\]&gt;]+`)
    reAsset = regexp.MustCompile(`!?\[[^\]]*\]\(/assets/[^)]+\)|/assets/[^\s)]+`)
)

func CleanMarkdown(raw []byte) (title, content string) {
    s := string(raw)
    if len(s) == 0 {
        return &quot;&quot;, &quot;&quot;
    }
    title, body := splitFrontmatter(s)
    body = removeURLs(body)
    body = removeAssetLinks(body)
    return title, body
}

func splitFrontmatter(s string) (title, body string) {
    const delimiter = &quot;---&quot;
    _, after, ok := strings.Cut(s, delimiter)
    if !ok { return &quot;&quot;, s }
    before, after, ok := strings.Cut(after, delimiter)
    if !ok { return &quot;&quot;, s }
    var fm frontmatter
    if err := yaml.Unmarshal([]byte(before), &amp;fm); err == nil {
        title = fm.Title
    }
    return title, after
}
</code></pre>
<pre><code class="language-go:internal/markdown/parser.go">type Post struct {
    Slug    string // ファイル名から .md を除去
    Title   string // frontmatter の title フィールド
    Content string // クリーニング済み本文
}

func ParsePosts(dir string) ([]Post, error) {
    entries, err := os.ReadDir(dir)
    // ... *.md ファイルを読み込み、CleanMarkdown で処理
    return posts, nil
}
</code></pre>
<p>:::</p>
<p>ポイントは、Embedding 時にタイトルをテキストの先頭に結合すること（<code>title + &quot;\n&quot; + content</code>）。セクション 5.1 で述べますが、Embedding モデルはテキストの先頭部分を重視する傾向があるため、タイトルの情報がベクトルに強く反映されます。</p>
<hr>
<h2>5. Embedding の最適化</h2>
<p>Embedding 処理の高速化で 2 つの工夫をしました。</p>
<ul>
<li><strong>5.1</strong>: テキストを先頭 4,000 文字に切り詰めて処理時間を約 1/8 に短縮</li>
<li><strong>5.2</strong>: 実装中に踏んだ Ollama <code>num_ctx</code> の無警告切り詰め問題</li>
</ul>
<h3>5.1 テキスト切り詰めの最適化</h3>
<p>最初は記事の全文をそのまま Ollama に送っていました。CI で実行すると、全記事の Embedding に数十時間かかる計算です。全文が本当に必要なのか、検証しました。</p>
<p>まず、全記事のクリーニング済みテキスト長の分布を調べました。</p>
<ul>
<li>平均: 約 8,000 文字</li>
<li>中央値: 約 6,300 文字</li>
<li>上位 10%: 14,600 文字以上</li>
<li>最大: 53,000 文字超</li>
</ul>
<p>大半の記事は 10,000 文字以内に収まりますが、一部の長文記事は 40,000 文字を超えます。長い記事の後半には参考文献リストや補足情報が多く、記事のテーマを表す情報は先頭に集中する傾向がありました。</p>
<p>そこで「先頭 N 文字に切り詰めても品質を維持できるか？」を検証するため、長文の上位 5 記事で<strong>全文 Embedding（<code>num_ctx=8192</code> 明示指定）をベースライン</strong>として、切り詰め文字数を変えて類似度と速度を比較しました。</p>
<table>
<thead>
<tr>
<th>切り詰め</th>
<th>ベースラインとの類似度</th>
<th>平均速度</th>
<th>高速化</th>
</tr>
</thead>
<tbody><tr>
<td>全文</td>
<td>1.000</td>
<td>229 秒</td>
<td>1.0x</td>
</tr>
<tr>
<td>2,000 文字</td>
<td>0.868</td>
<td>13 秒</td>
<td>17.6x</td>
</tr>
<tr>
<td><strong>4,000 文字</strong></td>
<td><strong>0.887</strong></td>
<td><strong>29 秒</strong></td>
<td><strong>7.9x</strong></td>
</tr>
<tr>
<td>6,000 文字</td>
<td>0.902</td>
<td>42 秒</td>
<td>5.5x</td>
</tr>
<tr>
<td>8,000 文字</td>
<td>0.909</td>
<td>53 秒</td>
<td>4.3x</td>
</tr>
</tbody></table>
<p>4,000 → 8,000 文字に増やしても類似度の改善は <strong>+2.2 ポイント</strong>（0.887 → 0.909）に留まりますが、速度は 1.8 倍遅くなります。関連記事のランキング品質に影響が出ないことを本番データで確認した上で、<strong>先頭 4,000 文字 + <code>num_ctx=8192</code></strong> を採用しました。</p>
<p>:::message
なぜ先頭の切り詰めが有効か？</p>
<p>2 つの要因が相乗しています。</p>
<ol>
<li><strong>モデルの位置バイアス</strong>: Transformer ベースの Embedding モデルでは、テキスト先頭への撹乱がベクトルに与える影響が末尾より約 15% 大きいことが報告されています（<a href="https://arxiv.org/html/2412.15241v3">arXiv:2412.15241</a>）。Qwen3-Embedding も <a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B/blob/main/config.json">RoPE</a> を採用した Transformer モデルであり、同様の傾向があると考えられます。</li>
<li><strong>コンテンツの構造バイアス</strong>: 技術ブログは「タイトル→導入→概要→詳細」の逆ピラミッド構造を持ち、テーマ情報が冒頭に集中します（いわゆる <a href="https://arxiv.org/abs/1912.11602">Lead Bias</a>）。
:::</li>
</ol>
<h3>5.2 Ollama の <code>num_ctx</code> に潜む落とし穴</h3>
<p>5.1 の検証に入る前に、<code>num_ctx</code> 周りで罠を踏みました。Ollama で Embedding を扱う人は全員引っかかりうる問題です。</p>
<h4>何が起きたか</h4>
<p>切り詰めを検証する前に、まず <code>num_ctx</code> の効果を確認しようと次の 2 パターンで全文 Embedding を比較しました。</p>
<ul>
<li>A: 全文 + <code>num_ctx=4096</code></li>
<li>B: 全文 + <code>num_ctx=8192</code></li>
</ul>
<p>A と B のコサイン類似度が全記事で <strong>1.000</strong> でした。完全に同一のベクトルです。処理時間も平均約 70 秒で差がない。35,000 文字超の記事でコンテキスト長を倍にしたのに、結果が変わっていません。</p>
<table>
<thead>
<tr>
<th>記事</th>
<th>文字数</th>
<th>平均処理時間(秒)</th>
<th>A-B 類似度</th>
</tr>
</thead>
<tbody><tr>
<td>torii-ai_tool_slack</td>
<td>35,417</td>
<td>68</td>
<td>1.000</td>
</tr>
<tr>
<td>Android-Compose-OO-Nav</td>
<td>37,803</td>
<td>76</td>
<td>1.000</td>
</tr>
<tr>
<td>aurora-mysql-stats</td>
<td>32,648</td>
<td>71</td>
<td>1.000</td>
</tr>
<tr>
<td>Jetpack-Compose-Anim</td>
<td>34,621</td>
<td>65</td>
<td>1.000</td>
</tr>
<tr>
<td>SecureDBPassword</td>
<td>38,978</td>
<td>69</td>
<td>1.000</td>
</tr>
<tr>
<td><strong>平均</strong></td>
<td></td>
<td><strong>約 70 秒</strong></td>
<td><strong>1.000</strong></td>
</tr>
</tbody></table>
<h4>原因: Options に入れないと num_ctx は効かない</h4>
<p><code>num_ctx</code> を <code>EmbedRequest.Options</code> で<strong>明示的に渡さない限り</strong>、Ollama は <a href="https://github.com/ollama/ollama/blob/main/docs/context-length.mdx">VRAM に応じたデフォルト値</a>（24GiB 未満で 4k、24-48GiB で 32k、48GiB 以上で 256k。<code>OLLAMA_CONTEXT_LENGTH</code> 環境変数で変更可能）を使い、超過分を無警告で切り詰めます。</p>
<p>パターン B で <code>num_ctx=8192</code> を設定したつもりが、API の Options に渡されておらず、A と同じ 4096 トークンで処理されていました。類似度 1.000 は、両方とも同じ入力を処理していた証拠です。</p>
<p>:::message alert
注意: Ollama は入力テキストがコンテキスト長を超えてもエラーを返しません。API レスポンスにも切り詰めの有無を示すフィールドがありません。意図せず不完全な Embedding が生成される可能性があります。これは Ollama の <a href="https://github.com/ollama/ollama/issues/14259">Issue #14259</a> でも報告されています。
:::</p>
<h4>修正と効果の確認</h4>
<p><code>num_ctx</code> を <code>EmbedRequest.Options</code> で明示的に渡すよう修正したのが、次の実装です。</p>
<pre><code class="language-go:internal/embedding/client.go">func (c *Client) Embed(ctx context.Context, texts []string) ([][]float32, error) {
    req := &amp;api.EmbedRequest{
        Model: c.model,
        Input: texts,
    }
    if c.numCtx &gt; 0 {
        req.Options = map[string]any{&quot;num_ctx&quot;: c.numCtx}
    }

    resp, err := c.api.Embed(ctx, req)
    if err != nil {
        return nil, fmt.Errorf(&quot;Ollama Embed API エラー: %w&quot;, err)
    }
    // レスポンスのバリデーション（件数・空ベクトルチェック）
    if len(resp.Embeddings) != len(texts) {
        return nil, fmt.Errorf(&quot;レスポンス数が不一致: %d embeddings / %d texts&quot;, len(resp.Embeddings), len(texts))
    }
    return resp.Embeddings, nil
}
</code></pre>
<p>修正後は A-B 類似度が <strong>0.947</strong> に下がり、B の処理時間は A の約 3 倍（229 秒 vs 78 秒）になりました。8192 トークン分を処理していることが時間からも裏付けられます。</p>
<table>
<thead>
<tr>
<th>記事</th>
<th>文字数</th>
<th>A(秒)</th>
<th>B(秒)</th>
<th>A-B 類似度</th>
</tr>
</thead>
<tbody><tr>
<td>torii-ai_tool_slack</td>
<td>35,417</td>
<td>77</td>
<td>224</td>
<td>0.969</td>
</tr>
<tr>
<td>Android-Compose-OO-Nav</td>
<td>37,803</td>
<td>81</td>
<td>218</td>
<td>0.920</td>
</tr>
<tr>
<td>aurora-mysql-stats</td>
<td>32,648</td>
<td>84</td>
<td>224</td>
<td>0.919</td>
</tr>
<tr>
<td>Jetpack-Compose-Anim</td>
<td>34,621</td>
<td>74</td>
<td>243</td>
<td>0.947</td>
</tr>
<tr>
<td>SecureDBPassword</td>
<td>38,978</td>
<td>73</td>
<td>238</td>
<td>0.977</td>
</tr>
<tr>
<td><strong>平均</strong></td>
<td></td>
<td><strong>78</strong></td>
<td><strong>229</strong></td>
<td><strong>0.947</strong></td>
</tr>
</tbody></table>
<p>CLI のデフォルト値は <code>--num-ctx=8192</code> に設定し、4000 文字切り詰めと組み合わせることで無警告の文字切り詰めが発生しないことを保証しています。</p>
<h4>Ollama 利用者への教訓</h4>
<p>Ollama で Embedding や LLM を扱うなら：</p>
<ul>
<li><code>num_ctx</code> は Modelfile の <code>PARAMETER</code> か、API の <code>Options.num_ctx</code> で<strong>明示的に設定する</strong></li>
<li>入力のトークン数を事前に把握し、コンテキスト長に収まるか確認する</li>
<li>類似度や品質が「なぜか変わらない」ときは、無警告切り詰めを疑う</li>
</ul>
<h3>5.3 コードブロックは残すべきか？</h3>
<p>先頭 4000 文字のうち、コードブロックが大量に含まれる記事があります。Android Compose のナビゲーション記事では 2,213 文字（55%超）がコードでした。コードを除去して本文を増やす方が良さそうに思えます。</p>
<p>日英翻訳ペア（同じ <code>postId</code> で <code>locale</code> が異なる記事）のコサイン類似度で検証しました。</p>
<ul>
<li>コードブロックあり: 0.893</li>
<li>コードブロック除去: 0.868</li>
</ul>
<p>コードブロックを除去すると類似度が下がりました。</p>
<p>クラス名、関数名、ライブラリ名（<code>NavHost</code>、<code>Composable</code>、<code>goroutine</code> など）は言語に依存しません。日本語の記事でも英語の記事でも、同じ技術ならコード中に同じキーワードが出現します。コードブロックはクリーニング対象から除外（残す）としました。</p>
<h3>切り詰めの実装</h3>
<pre><code class="language-go:main.go">const maxEmbedRunes = 4000

for _, p := range dirty {
    text := p.Title + &quot;\n&quot; + p.Content
    if p.Content == &quot;&quot; {
        text = p.Title
    }
    if runes := []rune(text); len(runes) &gt; maxEmbedRunes {
        text = string(runes[:maxEmbedRunes])
    }
    vectors, err := client.Embed(ctx, []string{text})
    // ...
}
</code></pre>
<p><code>[]rune</code> に変換してからスライスすることで、マルチバイト文字（日本語）の途中で切れることを防いでいます。</p>
<hr>
<h2>6. SHA-256 差分キャッシュによる効率化</h2>
<p>セクション 1 で述べた「毎回全量実行」の問題を解決するため、差分キャッシュを導入しました。「前回から何が変わったか」を高速に判定する必要がありますが、ファイルの更新日時（mtime）は Git のチェックアウトでリセットされるため CI 環境では使えません。そこで、コンテンツ自体の SHA-256 ハッシュで変更を検知する方式を採用しました。</p>
<h3>キャッシュの設計</h3>
<pre><code class="language-go:internal/embedding/cache.go">type Cache struct {
    Version   int                   `json:&quot;version&quot;`
    ModelName string                `json:&quot;model_name&quot;`
    Entries   map[string]CacheEntry `json:&quot;entries&quot;`
}

type CacheEntry struct {
    ContentHash string    `json:&quot;content_hash&quot;`
    Vector      []float32 `json:&quot;vector&quot;`
}
</code></pre>
<h3>SHA-256 による変更検知</h3>
<p>記事のタイトルと本文を結合して SHA-256 ハッシュを計算し、前回のキャッシュと比較します。</p>
<pre><code class="language-go:internal/embedding/cache.go">func ContentHash(title, content string) string {
    h := sha256.New()
    h.Write([]byte(title + &quot;\n&quot; + content))
    return hex.EncodeToString(h.Sum(nil))
}

func (c *Cache) FindDirty(posts []markdown.Post, modelName string) []markdown.Post {
    if c.ModelName != modelName {
        return posts // モデル変更 → 全記事を再Embed
    }
    var dirty []markdown.Post
    for _, p := range posts {
        entry, ok := c.Entries[p.Slug]
        if !ok || entry.ContentHash != ContentHash(p.Title, p.Content) {
            dirty = append(dirty, p)
        }
    }
    return dirty
}
</code></pre>
<p>モデル名が変わると全記事が dirty になります。Embedding モデルが変われば次元数やベクトル空間が異なるため、古いキャッシュは無効です。</p>
<h3>キャッシュフロー</h3>
<pre><code class="language-mermaid">flowchart TB
    A[&quot;記事読み込み&lt;br&gt;(956件)&quot;] --&gt; B[&quot;キャッシュ読み込み&quot;]
    B --&gt; C{&quot;モデル変更?&quot;}
    C --&gt;|Yes| D[&quot;全記事をEmbed&quot;]
    C --&gt;|No| E[&quot;SHA-256比較&quot;]
    E --&gt; F{&quot;変更あり?&quot;}
    F --&gt;|Yes| G[&quot;変更分のみEmbed&quot;]
    F --&gt;|No| H[&quot;スキップ&quot;]
    D --&gt; I[&quot;1件ずつ保存&lt;br&gt;(中断耐性)&quot;]
    G --&gt; I
</code></pre>
<p>Embed のたびにキャッシュファイルを保存します。CI のタイムアウトや中断が起きても、それまで処理した分はキャッシュに残ります。次回実行時は中断箇所から再開できるため、初回の全量 Embedding を複数回に分けて進められます。</p>
<h3>初回構築で効いた「中断耐性」</h3>
<p>この「1 記事ごとに cache ファイルへ保存」という設計が、初回構築で実際に役に立ちました。</p>
<p>当時 956 件あった全記事の初回全量ビルドでは、Ollama での Embedding 処理が GitHub Actions の job timeout（<code>timeout-minutes: 60</code>）に収まらず、5 回連続で 60 分 timeout に到達しました。それでも 6 回目の run で完走できたのは、各 cancelled run で完了していた分の Embedding が次の run に引き継がれたからです。</p>
<table>
<thead>
<tr>
<th>run</th>
<th>結果</th>
<th>Generate related content</th>
</tr>
</thead>
<tbody><tr>
<td>1 〜 5 回目</td>
<td>timeout</td>
<td>各 60 分</td>
</tr>
<tr>
<td><strong>6 回目</strong></td>
<td><strong>success</strong></td>
<td><strong>55 分</strong></td>
</tr>
<tr>
<td><strong>累計</strong></td>
<td></td>
<td><strong>約 6 時間</strong></td>
</tr>
</tbody></table>
<p>これを成立させたのは 2 つの噛み合わせです。</p>
<ol>
<li><strong>アプリ側</strong>: 1 記事 Embed するごとに <code>output/embeddings_cache.json</code> へ保存</li>
<li><strong>CI 側</strong>: <code>actions/cache/save@v5</code> を <strong><code>if: always()</code></strong> で走らせる</li>
</ol>
<pre><code class="language-yaml:.github/workflows/auto-create-related-data-on-pushd-to-main.yml">- name: Save embeddings cache
  if: always()   # timeout/cancel 時も cache save を走らせる
  uses: actions/cache/save@v5
  with:
    path: output/embeddings_cache.json
    key: embeddings-cache-${{ hashFiles(&#39;_posts/**&#39;) }}-${{ github.run_id }}
</code></pre>
<p><code>if: always()</code> を付けておくと、job が timeout/cancel で終わるときにも cache save ステップが走ります。結果、途中まで処理した Embedding は cache に残り、次 run は <code>restore-keys</code> のフォールバックで前 run の cache を拾って残り分から続行できる。</p>
<p>この仕組みがなければ、60 分 timeout で毎回 Embedding が巻き戻り、6 時間で完走することはなかったはずです。</p>
<hr>
<h2>7. コサイン類似度ランキングの最適化</h2>
<p>Embedding ベクトルが得られたら、記事間の類似度を計算してランキングを生成します。956 記事の各記事が他の 955 件と比較するため、約 91 万回の内積計算が走ります。この規模なら FAISS 等の ANN（近似最近傍探索）ライブラリを導入するよりも、brute-force の方がシンプルで依存も増えません。</p>
<p>最初の実装（毎回ノルム計算 + 全件ソート）ではテストで約 1.6 秒かかっていました。事前正規化 + min-heap への変更と、ループアンローリングの 2 段階で 730ms まで改善しました。</p>
<p>:::::details 最適化の詳細</p>
<p><strong>1. 事前正規化 (Pre-normalization)</strong></p>
<p>コサイン類似度の式は以下です。</p>
<p>$$
\cos(a, b) = \frac{a \cdot b}{|a| \times |b|}
$$</p>
<p>毎回 2 つのベクトルの長さ（ノルム $|a|$）を計算するのは無駄なので、全ベクトルの長さを事前に 1 に揃えておきます（正規化）。すると分母が $1 \times 1 = 1$ になり、コサイン類似度は内積 $a \cdot b$（各要素を掛けて足すだけ）と等しくなります。正規化は記事数分（956回）だけ。その後の 91 万回のペア比較では掛け算と足し算だけで済みます。</p>
<p>:::details コサイン類似度の補足</p>
<p>内積 $a \cdot b$ は 2 つのベクトルの各要素を掛けて足した値です。意味が近い記事同士は内積が大きくなりますが、長い記事のベクトルは値が大きくなりがちで、内積だけだと「ベクトルの長さ」に引っ張られます。ノルム $|a|$ で割ることで長さの影響を消し、純粋に「向き」（意味の近さ）だけを比較するのがコサイン類似度です。結果は $-1$ 〜 $1$ の範囲で、1 に近いほど意味が近い。</p>
<p>正規化とは、各要素をノルムで割ってベクトルの長さを 1 にする処理です。向きはそのまま、長さだけ揃えます。</p>
<pre><code>元: a = [3, 4]       → 長さ = √(9+16) = 5
正規化: a&#39; = [0.6, 0.8] → 長さ = √(0.36+0.64) = 1
</code></pre>
<p>:::</p>
<p><strong>2. min-heap Top-K</strong></p>
<p>全 955 件のスコアを <code>sort.Slice</code> でソートしていましたが、実際に必要なのは上位 12 件だけ。サイズ 12 の min-heap（Go 標準ライブラリの <code>container/heap</code>）を使い、スコアが最小値より大きければ入れ替える方式に変更。計算量は $O(N \log N)$ から $O(N \log K)$ に改善します。</p>
<p><strong>3. ループアンローリング</strong></p>
<p>内積計算のホットパス（約 91 万回 × 1024 次元）に 4-way ループアンローリングを適用。4 つの独立したアキュムレータ変数を使うことで、前のループ結果への依存を断ち切り、CPU が乗算と加算を並列実行できるようになります。</p>
<p>:::details ループアンローリングの補足</p>
<p>通常のループでは 1 つの変数 <code>sum</code> に順番に足していきます。<code>sum += a[0]*b[0]</code> の結果が出るまで次の <code>sum += a[1]*b[1]</code> が始められません（データ依存）。</p>
<p>4-way では 4 つの変数 <code>s0, s1, s2, s3</code> に分けて、それぞれ独立に計算します。CPU は依存関係のない命令を同時に実行できるため（命令レベル並列性）、4 つの乗算・加算が並列に走ります。最後に <code>s0 + s1 + s2 + s3</code> で合計するだけです。</p>
<pre><code>通常:     sum += a[0]*b[0] → sum += a[1]*b[1] → sum += a[2]*b[2] → sum += a[3]*b[3]
          （前の結果を待ってから次へ）

4-way:    s0 += a[0]*b[0]    s1 += a[1]*b[1]    s2 += a[2]*b[2]    s3 += a[3]*b[3]
          （4つ同時に実行）
          → s0 + s1 + s2 + s3
</code></pre>
<p>:::</p>
<pre><code class="language-go:internal/similarity/ranking.go">// 事前正規化: 全ベクトルのノルムを 1 にする
normalized := normalizeAll(slugs, vectors)

// min-heap Top-K: 上位 maxResults 件だけを効率的に抽出
h := &amp;minHeap{}
for j, other := range slugs {
    if i == j { continue }
    score := dotProduct(vi, normalized[j])
    if h.Len() &lt; maxResults {
        heap.Push(h, ScoredItem{Key: other, Score: score})
    } else if score &gt; (*h)[0].Score {
        (*h)[0] = ScoredItem{Key: other, Score: score}
        heap.Fix(h, 0)
    }
}

// 4-way ループアンローリング
func dotProduct(a, b []float32) float32 {
    var s0, s1, s2, s3 float32
    n := len(a)
    i := 0
    for ; i &lt;= n-4; i += 4 {
        s0 += a[i]*b[i]; s1 += a[i+1]*b[i+1]
        s2 += a[i+2]*b[i+2]; s3 += a[i+3]*b[i+3]
    }
    for ; i &lt; n; i++ { s0 += a[i] * b[i] }
    return s0 + s1 + s2 + s3
}
</code></pre>
<p>:::::</p>
<h3>パフォーマンス推移</h3>
<table>
<thead>
<tr>
<th>段階</th>
<th>手法</th>
<th>ランキング処理時間（956記事）</th>
</tr>
</thead>
<tbody><tr>
<td>初期</td>
<td>毎回ノルム計算 + sort.Slice</td>
<td>~1.58s</td>
</tr>
<tr>
<td>1</td>
<td>事前正規化 + min-heap Top-K</td>
<td>~1.18s</td>
</tr>
<tr>
<td>2</td>
<td>+ ループアンローリング（4-way）</td>
<td><strong>730ms</strong></td>
</tr>
</tbody></table>
<p>最終的なスペック：</p>
<table>
<thead>
<tr>
<th>指標</th>
<th>値</th>
</tr>
</thead>
<tbody><tr>
<td>記事数</td>
<td>956 件</td>
</tr>
<tr>
<td>ベクトル次元数</td>
<td>1024</td>
</tr>
<tr>
<td>類似度計算回数</td>
<td>約 912,980 回（956 × 955）</td>
</tr>
<tr>
<td>ランキング処理時間</td>
<td><strong>730ms</strong></td>
</tr>
</tbody></table>
<p>なお、Go の map はイテレーション順序が非決定的です。同じ入力に対して常に同じ JSON 出力を得るため、<code>slices.Sort</code> でスラッグをソートしてから処理しています。これを忘れると CI のたびに diff が発生し、不要なコミットが生まれてしまいます。</p>
<hr>
<h2>8. GitHub Actions での CI/CD</h2>
<h3>ワークフロー全体像</h3>
<pre><code class="language-mermaid">flowchart LR
    A[&quot;create-branch&quot;] --&gt; B[&quot;generate-related-content&lt;br&gt;(ARM runner + Ollama)&quot;]
    A --&gt; C[&quot;generate-metadata&quot;]
    A --&gt; D[&quot;generate-search-index&quot;]
    B --&gt; E[&quot;create-pull-request&quot;]
    C --&gt; E
    D --&gt; E
    E --&gt; F[&quot;auto-merge&quot;]
</code></pre>
<p><code>create-branch</code> でブランチを作成した後、3 つのジョブが並列実行されます。</p>
<h3>ARM ランナーの選択</h3>
<p>Embedding 処理には <code>arm-ubuntu-latest-4</code> ランナーを使用しています。GitHub の ARM ランナーは x86 の約半額（1分あたり $0.004 vs $0.008）で、初回の全量 Embedding のように数時間かかるジョブではコスト差が大きくなります。</p>
<h3>Ollama モデルキャッシュ</h3>
<p>639MB のモデルファイルを毎回ダウンロードしないため、<code>actions/cache</code> でキャッシュします。</p>
<h3>cache/restore + cache/save パターン</h3>
<p>:::message
<code>actions/cache@v5</code> の <code>save-always</code> オプションは非推奨になりました。代わりに <code>cache/restore</code> と <code>cache/save</code> を分離し、<code>cache/save</code> に <code>if: always()</code> を付けるパターンを使います。
:::</p>
<pre><code class="language-yaml:auto-create-related-data-on-push-to-main.yml">- name: Restore embeddings cache
  uses: actions/cache/restore@v5
  with:
    path: output/embeddings_cache.json
    key: embeddings-cache-${{ hashFiles(&#39;_posts/**&#39;) }}-${{ github.run_id }}
    restore-keys: embeddings-cache-

# ... Embedding 実行 ...

- name: Save embeddings cache
  if: always()
  uses: actions/cache/save@v5
  with:
    path: output/embeddings_cache.json
    key: embeddings-cache-${{ hashFiles(&#39;_posts/**&#39;) }}-${{ github.run_id }}
</code></pre>
<p><code>if: always()</code> により、タイムアウト時でもキャッシュを保存します。セクション 6 の「1 件ずつ保存」と組み合わせて、中断と再実行を繰り返してもキャッシュが蓄積されます。</p>
<h3>キャッシュキーに <code>run_id</code> を付ける理由</h3>
<p>GitHub Actions のキャッシュは同じキーで上書きできません（イミュータブル）。これはタイムアウト→再実行のパターンで問題になります。</p>
<p><code>run_id</code> なしの場合:</p>
<pre><code class="language-text">key: embeddings-cache-abc123

1回目: save &quot;abc123&quot; → ✅ 200記事分保存
2回目: restore &quot;abc123&quot; → 200記事復元 → 追加200記事 → save &quot;abc123&quot; → ❌ キーが既に存在
3回目: restore &quot;abc123&quot; → 1回目の200記事分しかない（2回目の成果が消えた）
</code></pre>
<p><code>run_id</code> ありの場合:</p>
<pre><code class="language-text">save key: embeddings-cache-abc123-{run_id}     ← 毎回ユニーク
restore-keys: embeddings-cache-abc123-          ← プレフィックス一致で最新を取得

1回目: save &quot;abc123-100&quot; → ✅ 200記事分
2回目: restore &quot;abc123-&quot; → run100から200記事復元 → 追加200記事 → save &quot;abc123-200&quot; → ✅ 400記事分
3回目: restore &quot;abc123-&quot; → run200から400記事復元 → 続きから
</code></pre>
<h3>push のリトライロジック</h3>
<p>3 つのジョブが並列でブランチに push するため、競合が発生します。指数バックオフ付きのリトライで対処します。</p>
<pre><code class="language-yaml">pushed=false
for i in 1 2 3 4 5; do
  git pull --rebase origin &quot;$BRANCH_NAME&quot; &amp;&amp; git push origin &quot;$BRANCH_NAME&quot; &amp;&amp; pushed=true &amp;&amp; break
  echo &quot;Push failed (attempt $i), retrying...&quot;
  sleep $((i * 2))
done
[ &quot;$pushed&quot; = &quot;true&quot; ] || { echo &quot;ERROR: All push attempts failed&quot;; exit 1; }
</code></pre>
<h3>古いブランチの問題</h3>
<p>自動生成用ブランチが前回の実行から残っている場合、古いコードがベースになります。<code>git reset --hard ${{ github.sha }}</code> で毎回トリガー元の最新コミットにリセットします。</p>
<h3>workflow_dispatch でのテスト実行</h3>
<p><code>main</code> にマージ前の動作確認では <code>workflow_dispatch</code> トリガーを一時的に追加しました。ただし、GUI の Actions タブにはデフォルトブランチのワークフローしか表示されないため、feature ブランチの <code>workflow_dispatch</code> は GUI から実行できません。</p>
<p>CLI 経由であれば <code>--ref</code> でブランチを指定して実行可能です。</p>
<pre><code class="language-bash">gh workflow run &quot;Auto Create Related Data&quot; --ref feat/related-content-gen-go-rewrite
</code></pre>
<hr>
<h2>9. 実運用で見えた効果</h2>
<p>旧システム（Python + Azure OpenAI）から新システム（Go + Ollama）への移行で、セクション 1 で挙げた 3 つの課題はそれぞれ次のように変わりました。</p>
<table>
<thead>
<tr>
<th>課題</th>
<th>旧（Python + Azure OpenAI）</th>
<th>新（Go + Ollama）</th>
</tr>
</thead>
<tbody><tr>
<td>実行戦略</td>
<td>毎回全量 Embed（900+ 件）</td>
<td>差分のみ Embed（SHA-256 ハッシュ比較）</td>
</tr>
<tr>
<td>Rate Limit (429)</td>
<td>頻発・リトライで不安定</td>
<td>構造的に発生しない（外部 API なし）</td>
</tr>
<tr>
<td>推論コスト</td>
<td>従量課金（Azure OpenAI）</td>
<td>ゼロ（CI ランナー内完結）</td>
</tr>
</tbody></table>
<p>比較すべきは単発の処理秒数ではなく、「記事追加のたびに全量再計算が必要か」「外部 API 制約に運用が振り回されるか」という運用特性です。旧は Azure のマネージド並列推論、新は self-hosted CI ランナー 1 台のシーケンシャル処理で、そもそも尺度が違います。</p>
<h3>差分更新時の実測例</h3>
<p>959 記事中 49 件（5%）が dirty だった run では、<strong>19 分 21 秒で完走</strong>しました（self-hosted runner 1 台・逐次処理で 1 記事あたり約 22〜24 秒）。差分ゼロなら Embed はスキップされ、ランキング計算と出力だけで 1〜2 分で完了します。</p>
<h3>残課題</h3>
<ul>
<li>dirty が 150 件を超える状況（cache eviction 直後や cron が長期間失敗していたあとなど）では <code>timeout-minutes: 60</code> に収まらないことがあります。現状は複数 run に分けて進捗を積み上げる設計でカバーしていますが、次の打ち手として timeout 延長と <code>output/embeddings_cache.json</code> の git 管理化が候補です</li>
<li>GitHub Actions cache は 7 日アクセスなしで自動 eviction されるため、週次 cron（月曜 9 時）で Restore を触って keep-warm しています。より確実にするなら git 管理化か、S3 などの外部 storage に寄せる手もあります</li>
</ul>
<hr>
<h2>10. まとめ</h2>
<p>本記事では、関連記事のレコメンドシステムを Go + Ollama（ローカル Embedding）で再構築した過程を紹介しました。なお、関連求人についても同様の Embedding + コサイン類似度の仕組みで生成しています。</p>
<table>
<thead>
<tr>
<th>項目</th>
<th>結果</th>
</tr>
</thead>
<tbody><tr>
<td>対象記事数</td>
<td>960 件前後（執筆時点）</td>
</tr>
<tr>
<td>ランキング計算</td>
<td>730ms（Embedding 生成は含まず、測定時点 956 件）</td>
</tr>
<tr>
<td>テキスト切り詰め</td>
<td>先頭 4000 文字で全文比 88.7% の類似度を維持</td>
</tr>
<tr>
<td>差分キャッシュ</td>
<td>差分ゼロなら 1〜2 分、少数差分なら数分〜十数分</td>
</tr>
<tr>
<td>外部依存</td>
<td>Ollama + Qwen3-Embedding（API キー不要）</td>
</tr>
</tbody></table>
<p>SHA-256 差分キャッシュで変更記事だけを再 Embed し、ランキングは事前正規化と min-heap Top-K で 730ms（956記事のペアワイズ計算）。外部 API 依存を排除して、429 エラーとコストの問題を解消しました。</p>
<p>初回の全量 Embedding は CPU ランナーで数時間かかり、モデル変更や初期導入時にも同じコストを払うことになります。扱い方はセクション 6 と 9 に書いた通りで、GPU ランナーが使えれば改善しますが、現時点では CI の制約です。</p>
<p>もう 1 つ、推薦品質の定量評価がまだありません。「Embedding の類似度が 88.7% 保たれている」ことと「関連記事の推薦が妥当である」ことは別の問題です。旧システムとの Top-K 一致率や、クリックスルー率の計測が残っています。</p>
<p>テキスト切り詰めも改善の余地があります。現在は先頭 4000 文字をルーン単位でカットしていますが、文の途中で切れる可能性があります。句点（<code>。</code>）や改行の位置で切る方が、Embedding の入力としてはクリーンです。今回のユースケースでは影響は軽微ですが、精度を追求する場合は検討に値します。</p>
<hr>
<h2>11. この仕組みの応用可能性</h2>
<p>「ローカル Embedding + コサイン類似度 + 差分キャッシュ」の仕組みは、ブログの関連記事に限りません。Confluence や Notion の社内ドキュメントを同じパイプラインで Embedding すれば、「この仕様書に関連するドキュメント」を自動提示できます。Ollama はローカル実行なので、社外に送信できない社内文書でも扱えます。</p>
<p>SHA-256 差分キャッシュと 1 件ずつ保存の中断耐性パターンはそのまま流用できます。Ollama + 軽量モデルなら API キー不要で CI でもローカルでも動きます。</p>
<hr>
<h2>おまけ: Claude Code との開発プロセス</h2>
<p>今回の開発は Claude Code とのペアプログラミングで進めました。</p>
<h3>kairo による開発ワークフロー</h3>
<p>開発ワークフローにはクラスメソッド社の <a href="https://github.com/classmethod/tsumiki">tsumiki</a> の kairo を使いました。kairo は Claude Code 向けのスキルで、4 つのコマンドでソフトウェア開発を進めます。</p>
<ol>
<li><code>kairo-requirements</code>: EARS 記法で機能・非機能要件を定義。今回は 3 方式の PoC 比較（ONNX / llama.cpp / Ollama）もこのフェーズで実行しました</li>
<li><code>kairo-design</code>: 要件からアーキテクチャ図、データフロー、型定義を生成</li>
<li><code>kairo-tasks</code>: 設計を実装タスクに分割。依存関係とテストケースも定義。今回は 10 タスク・3 フェーズに分解</li>
<li><code>kairo-loop</code>: タスクを 1 つずつ Red → Green → Refactor の TDD サイクルで実装。7 タスクをこのコマンドで回しました</li>
</ol>
<h3>PR レビュー</h3>
<p>実装後の PR レビューでは、Claude Code に以下のように指示しました。</p>
<pre><code>/pr-review-toolkit:review-pr all
</code></pre>
<p><a href="https://github.com/anthropics/claude-plugins-public/tree/main/plugins/pr-review-toolkit">pr-review-toolkit</a> は Anthropic 公式の Claude Code プラグインで、6 種のレビューエージェント（コード品質、エラーハンドリング、テストカバレッジ、コメント整合性、型設計、コード簡素化）が並列にレビューします。セクション 5.2 のレスポンスバリデーション（件数・空ベクトルチェック）は、このレビューで指摘された問題への対応です。</p>
<h3>Go 1.26 での最適化</h3>
<p>Claude Code に「Go 1.26 で最適化して」と指示しました。<code>go fix</code> による自動変換（<code>strings.Index</code> → <code>strings.Cut</code>、<code>sort.Strings</code> → <code>slices.Sort</code>、<code>context.Background()</code> → <code>t.Context()</code> など）に加え、新しい言語機能やライブラリ API を活用したリファクタリングも実施されました。</p>
<h3>記事の執筆・校正</h3>
<p>この記事自体も Claude Code で執筆しています。校正には 3 つのツールを使いました。</p>
<ul>
<li><a href="https://github.com/textlint/textlint">textlint</a> + <a href="https://github.com/textlint-ja/textlint-rule-preset-ja-technical-writing">ja-technical-writing</a>: 冗長表現や接続詞の重複など、日本語の技術文書向け校正</li>
<li><a href="https://github.com/stephenturner/skill-deslop">skill-deslop</a>: AI 生成文章に特有の冗長パターン（回りくどい前置き、受動態の多用など）の検出・除去</li>
<li><a href="https://github.com/openai/codex-plugin-cc">Codex plugin for Claude Code</a>: OpenAI 公式の Claude Code プラグインで、Codex CLI をサブエージェントとして呼び出します。記事全体の論理破綻や数値矛盾のチェックに使いました。実験データ更新に伴う数値の不整合やコードスニペットの変数名不一致など、人間のレビューでは見落としやすい問題を検出できました</li>
</ul>
<hr>
<p>ここまで読んでいただきありがとうございました。何かの参考になれば幸いです。なお、この記事の下部に表示されている「関連する記事」と「関連する求人」が、本記事で紹介した仕組みで生成された実物です。</p>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/common/thumbnail_default_×2.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Amplify Hosting + CDK 環境で WAF が 2 系統に分かれていた話]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-05-29-aws-waf-ip-allowlist-troubleshoot/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-05-29-aws-waf-ip-allowlist-troubleshoot/</guid>
            <pubDate>Fri, 29 May 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[AWS Amplify Hosting + CDK 構成で WAF が 2 系統に分かれており、拠点 IP 変更時に片方しか更新できておらず 403 が残った調査記録です。さらに Amplify Console の UI と API の実態が乖離しているという罠にも直面しました。]]></description>
            <content:encoded><![CDATA[<h2>はじめに</h2>
<p>こんにちは、 Cloud Infrastructure G の山中です！</p>
<p>「拠点のグローバル IP が変わったので、AWS WAF の allowlist を更新してください」というよくある作業を引き受けたところ、半日以上溶かしました。</p>
<p>原因は <strong>同じシステムの中に WAF が 2 系統</strong> 存在しており、片方を更新しても、もう片方が古い IP リストを握っていたためです。さらに <strong>Amplify Console の Firewall UI が「Firewall: 無効」と表示しているのに、API で確認すると WebACL がしっかり attach されている</strong> という UI と実態の乖離まで重なり、見えている情報をそのまま信じてよいか判断がつかない状況でした。</p>
<p>この記事では、その 2 系統 WAF の全体像と、UI を信用できないときに API で実態を確認する手順を共有します。同じような構成（Amplify Hosting + CDK 管理の WAF）を運用している方の参考になれば幸いです。</p>
<h2>TL;DR</h2>
<ul>
<li>拠点 IP 変更で全環境 403 が発生</li>
<li>Backend API 側の WAF（CDK 管理）は Amplify の環境変数 + rebuild で解消できた</li>
<li>しかしフロントエンド側は <strong>Amplify Hosting が自動管理する別 WAF</strong>（<code>AmplifyIPSet-*</code>）が原因で 403 が残った</li>
<li>さらに Amplify Console の Firewall UI は「Firewall: 無効」表示なのに、API では <strong>WebACL が attach 済み・ default action が Block</strong> だった</li>
<li><code>aws wafv2 list-resources-for-web-acl</code> の <code>--resource-type</code> を <code>AMPLIFY</code> にしないと、attach 状態が見えない罠も踏んだ</li>
<li>最終的に <code>aws wafv2 update-ip-set</code> で直接 IPSet を書き換えて解決</li>
<li>教訓：<strong>Amplify Hosting の WAF は UI を信用せず、API で実態を確認すべし</strong></li>
</ul>
<h2>背景</h2>
<h3>サービス構成</h3>
<p>ユーザー向けに提供予定のフロントエンド + バックエンド API のシステムで、構成は以下の通りです。</p>
<ul>
<li>バックエンド: AWS Amplify Gen 2（CDK で WAF や CloudFront を含むインフラを定義）</li>
<li>フロントエンド: AWS Amplify Hosting Gen 1（platform=WEB）</li>
<li>CDN: CloudFront</li>
<li>WAF: AWS WAFv2</li>
<li>環境: dev / stage / prod（それぞれ別 AWS アカウント）</li>
</ul>
<h3>アクセス制御の方針</h3>
<p>まだリリース前のシステムのため、複数拠点からのオフィス IP のみを許可しています。
拠点が増減したり、回線変更で IP が変わったりすると、各環境の WAF の IPSet を更新する必要があります。</p>
<h3>出来事</h3>
<p>ある日「拠点 A が新しい IP に切り替わるので、X 日までに各環境の allowlist を入れ替えてほしい」という依頼が来ました。
作業手順は社内に整備されていたので、淡々と進めていたつもりだったのですが、ここから泥沼に入っていきます。</p>
<h2>WAF が 2 系統に分かれている全体像</h2>
<p>最初に結論を絵にしておきます。後の章で何度もこの絵に戻ってきます。</p>
<p><img src="/assets/blog/authors/d.yamanaka/20260529/waf-two-systems-overview.svg" alt="Backend API 系（CDK 管理の WebACL/IPSet）と Frontend 系（Amplify Hosting が自動管理する WebACL/IPSet）が並列に存在する WAF 2 系統構成図"></p>
<p>ポイントは、<strong>同じ「拠点 IP 許可」という概念を、別々のリソースとして 2 箇所で独立に管理している</strong> ことです。</p>
<ul>
<li>Backend API 側: CDK / CloudFormation で IPSet を定義 → Amplify の環境変数 <code>WAF_ALLOWED_IP_LIST</code> を更新して rebuild すれば IPSet が書き換わる、という仕組みを自前で組んでいる</li>
<li>Frontend 側: Amplify Hosting の「Firewall（AWS WAF 統合）」機能を有効にすると、Amplify サービス側で勝手に IPSet と WebACL を作成し、Amplify app にくっつける</li>
</ul>
<p>片方しか更新しないと、当然、もう片方の経路で 403 が出ます。今回まさにそこにハマりました。</p>
<h2>タイムライン</h2>
<p>実際に起きた流れを表にすると、こうなります。</p>
<table>
<thead>
<tr>
<th>タイミング</th>
<th>出来事</th>
</tr>
</thead>
<tbody><tr>
<td>Day 0</td>
<td>社内ドキュメントで「新しい拠点 IP 一覧」が共有される</td>
</tr>
<tr>
<td>Day 1</td>
<td>dev の Backend API WAF を Amplify の環境変数経由で更新 → IPSet 反映確認</td>
</tr>
<tr>
<td>Day 2 朝</td>
<td>stage の Backend API WAF を更新 → IPSet 反映確認</td>
</tr>
<tr>
<td>Day 2 昼</td>
<td>prod の Backend API WAF を更新 → IPSet 反映確認</td>
</tr>
<tr>
<td>Day 2 昼</td>
<td>動作確認のためフロントエンド URL にアクセス → <strong>まだ 403</strong></td>
</tr>
<tr>
<td>Day 2 昼</td>
<td>「Backend は更新したのに何で？」と調査開始</td>
</tr>
<tr>
<td>Day 2 午後</td>
<td>フロントエンドは別 WAF （<code>AmplifyIPSet-*</code>） で守られていることに気付く</td>
</tr>
<tr>
<td>Day 2 午後</td>
<td>Amplify Console の Firewall UI を見る → <strong>「Firewall: 無効」と表示</strong></td>
</tr>
<tr>
<td>Day 2 午後</td>
<td>API で確認 → <strong>WebACL は attach 済み、IPSet は旧 IP のままで Block</strong></td>
</tr>
<tr>
<td>Day 2 夕方</td>
<td><code>aws wafv2 update-ip-set</code> で 3 環境の <code>AmplifyIPSet-*</code> を直接更新 → 全環境 200 OK</td>
</tr>
</tbody></table>
<p>「Backend は更新したのにフロントだけ 403」となった時点で、Amplify Hosting 側に別 WAF があると気付くまでが一番遠回りでした。</p>
<h2>最初の対応 — Backend API WAF はあっさり直る</h2>
<p>Backend API 側の更新手順は、すでに社内で整備されていました。</p>
<ol>
<li>Amplify Console で対象 app の環境変数 <code>WAF_ALLOWED_IP_LIST</code> を新しい IP の CSV に書き換える</li>
<li>Amplify の build を回す</li>
<li>CDK で定義された <code>CfnIPSet</code> のリソースが新しい IP で更新される</li>
</ol>
<p>この仕組みのおかげで、dev / stage / prod の Backend API は Day 1 から Day 2 にかけて順次切り替え、いずれも更新自体は数分で完了しました。
WAFv2 のコンソールで IPSet を覗いて、新しい IP が並んでいるのを確認 → よし、終わったな、と思ったのが甘かったです。</p>
<p>念のため、フロントエンドの URL にも <code>curl</code> を投げて確認しました。</p>
<pre><code class="language-bash">$ curl -i https://&lt;env&gt;.example.internal/
HTTP/1.1 403 Forbidden
content-type: text/html
...
&lt;HTML&gt;&lt;HEAD&gt;&lt;TITLE&gt;Request blocked.&lt;/TITLE&gt;&lt;/HEAD&gt;
&lt;BODY&gt;Request blocked.&lt;/BODY&gt;&lt;/HTML&gt;
</code></pre>
<p><code>Request blocked.</code> というレスポンスボディは、AWS WAF の Block ルールが返す典型的な文字列です。S3 オリジンの「Access Denied」とは別物なので、これが見えた時点でほぼ WAF を疑って間違いありません。</p>
<p>:::message
<strong>ちょっとした見分け方</strong>
CloudFront 経由の 403 で、ボディに <code>Request blocked.</code> が入っていれば、ほぼ WAF が原因です。
オリジン（S3 など）の Access Denied であれば、ボディは <code>Access Denied</code> という別の文字列になります。
:::</p>
<p>つまり Backend は直ったが、フロントエンドの経路では別の何かが Block している、という状況です。</p>
<h2>真犯人を探す — フロントエンドは別 WAF で守られていた</h2>
<p>「フロントエンドも CloudFront → S3 のはず。WAF はどこに付いているんだろう？」と探したところ、Amplify Hosting には <strong>AWS WAF 統合（Firewall）</strong> という機能があり、これを有効化していたことを思い出しました。</p>
<p>:::message
<strong>用語の整理</strong>
Amplify Hosting には紛らわしい 2 つの保護機能があります。</p>
<ul>
<li><strong>Access control</strong>: ベーシック認証（ユーザー名 + パスワード）でブランチを保護する機能。IP 制限ではありません。</li>
<li><strong>Firewall（AWS WAF 統合）</strong>: AWS WAF v2 と統合し、IP allowlist / レートリミット / マネージドルールなどを適用する機能。</li>
</ul>
<p>今回 IP allowlist として利用していたのは後者の <strong>Firewall</strong> 機能のほうです。
:::</p>
<p>Amplify Hosting の Firewall を有効化すると、Amplify サービス側で以下を自動的に作成・関連付けします。</p>
<ul>
<li><code>AmplifyIPSet-&lt;guid&gt;</code> という名前の IPSet（us-east-1, scope=CLOUDFRONT）</li>
<li><code>CreatedByAmplify-&lt;appId&gt;-&lt;guid&gt;</code> という名前の WebACL</li>
<li>上記 WebACL を Amplify app のリソース ARN に <code>AssociateWebACL</code> で紐付け</li>
</ul>
<p>そして <strong>これらのリソースは CloudFormation / CDK の管理外</strong> です。Amplify サービスが直接 WAF API を叩いて作成しています。
そのため、CDK 側でいくら WAF を更新しても、フロントエンドの WAF は変わりません。</p>
<p>WAFv2 のコンソールから IPSet の一覧を眺めると、確かに <code>AmplifyIPSet-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</code> のような IPSet が、CDK 由来の <code>my-app-api-allowed-ips-&lt;env&gt;</code> とは別に存在していました。中身を見ると、まさに <strong>旧拠点 IP のみが入っている</strong> 状態。これが原因です。</p>
<p>:::message
<strong>Tips: CloudFront scope の WAF は us-east-1 にしかない</strong>
CloudFront は Global サービスですが、CloudFront に付けるための WAFv2 リソース（scope=CLOUDFRONT）は <strong>us-east-1 のエンドポイントからしか触れません</strong>。
東京リージョン （<code>ap-northeast-1</code>） でいくら <code>aws wafv2 list-ip-sets</code> を叩いても、CloudFront scope の IPSet は出てきません。「IPSet が見当たらない！」と焦ったときの 9 割はこれが原因です。
:::</p>
<pre><code class="language-bash"># 正しい（CloudFront scope は us-east-1 で見る）
aws wafv2 list-ip-sets --scope CLOUDFRONT --region us-east-1

# 間違い（REGIONAL scope のものしか返ってこない）
aws wafv2 list-ip-sets --scope REGIONAL --region ap-northeast-1
</code></pre>
<h2>UI と API が一致しない問題</h2>
<p>「じゃあ Amplify Console の Firewall 画面で IP を入れ替えればいいか」と思って画面を開いたところ、目を疑う表示が出ていました。</p>
<blockquote>
<p><strong>Firewall: 無効（このアプリは Web Application Firewall で保護されていません）</strong></p>
</blockquote>
<p>つまり「WAF はかかっていません」という意味の表示です。
しかし <code>curl</code> を打つと、明らかに 403 （<code>Request blocked.</code>） が返ってくる。どちらを信じればよいのか分からない状況です。</p>
<p>ここで AWS CLI を使って、API レベルで実態を確認します。</p>
<h3>1. Amplify app に WebACL が attach されているかを確認</h3>
<p><code>list-resources-for-web-acl</code> を使うと、ある WebACL がどのリソースに attach されているかが分かります。</p>
<pre><code class="language-bash">aws wafv2 list-resources-for-web-acl \
  --region us-east-1 \
  --web-acl-arn arn:aws:wafv2:us-east-1:XXXXXXXXXXXX:global/webacl/CreatedByAmplify-XXXXXXXXXX-XXXXXXXX/XXXXXXXX
</code></pre>
<p>これだけだと <strong>空の配列が返ってきます</strong>。</p>
<p>「あれ、attach されていない？じゃあ何が Block しているの？」と一瞬混乱しました。
ですがこれは罠で、<code>--resource-type</code> を指定していないとデフォルト値 <code>APPLICATION_LOAD_BALANCER</code> で検索されます。Amplify Hosting の場合、resource-type は <code>AMPLIFY</code> なので、デフォルトでは見えません。
さらに <code>list-resources-for-web-acl</code> は <strong>そもそも CloudFront Distribution には使えません</strong>（CloudFront の関連付けを調べたいときは <code>aws cloudfront list-distributions-by-web-acl-id</code> を使うのが正解です）。</p>
<pre><code class="language-bash"># 正しい呼び方
aws wafv2 list-resources-for-web-acl \
  --region us-east-1 \
  --web-acl-arn arn:aws:wafv2:us-east-1:XXXXXXXXXXXX:global/webacl/CreatedByAmplify-XXXXXXXXXX-XXXXXXXX/XXXXXXXX \
  --resource-type AMPLIFY
</code></pre>
<p>これでようやく、Amplify app の ARN が返ってきました。<strong>UI は「Firewall: 無効」と言っていたが、実態としては WebACL がしっかり attach されていた</strong> わけです。</p>
<p>:::message
<strong>ハマりポイント: <code>--resource-type</code> のデフォルト</strong>
<code>aws wafv2 list-resources-for-web-acl</code> は <code>--resource-type</code> を省略すると <strong><code>APPLICATION_LOAD_BALANCER</code> で検索されます</strong>（CloudFront Distribution はこの API では扱えず、<code>aws cloudfront list-distributions-by-web-acl-id</code> を使う必要があります）。
Amplify Hosting を疑うときは、必ず <code>--resource-type AMPLIFY</code> を明示しましょう。これに気付かないと、「attach されていない」と誤認して別の方向の調査に走ってしまいます。
:::</p>
<h3>2. WebACL のデフォルトアクションを確認</h3>
<p><code>get-web-acl</code> でデフォルトアクションを覗くと、default は <code>Block</code>、その上で IPSet ベースの allow ルールが乗っかっている構成でした。</p>
<pre><code class="language-bash">aws wafv2 get-web-acl \
  --scope CLOUDFRONT --region us-east-1 \
  --name CreatedByAmplify-XXXXXXXXXX-XXXXXXXX --id XXXXXXXX \
  --query &#39;WebACL.{DefaultAction:DefaultAction, Rules:Rules[].Name}&#39;
</code></pre>
<p>つまり、<strong>IPSet に載っていない IP からのアクセスは全部 Block</strong>。
UI 上は「Firewall: 無効」と表示していても、API レベルでは「Block ベース + 古い IPSet で allow」になっており、見事に食い違っていました。</p>
<h3>3. なぜ乖離したのか — CloudTrail で犯人探し</h3>
<p>UI と API がここまで一致しないのは流石におかしいので、CloudTrail で履歴を漁ってみました。</p>
<pre><code class="language-bash">aws cloudtrail lookup-events \
  --max-items 50 \
  --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateIPSet \
  --output json
</code></pre>
<p><code>AssociateWebACL</code> や <code>DisassociateWebACL</code> も同じように引いて、時系列で並べると、過去にある担当者が <strong>何度も Associate / Disassociate を繰り返しており、最終的に Associate で終わっていた</strong> ことが分かりました。
時系列を追っても、UI が「Firewall: 無効」を表示し続けるに至った直接的な原因までは特定できませんでした。<strong>ただし「Amplify Console の Firewall UI と、WAFv2 API が示す実態とが食い違うケースが起こりうる」という事実だけは、今回の調査で確認できた</strong>ことになります。</p>
<p>ともあれ、API の実態を信じるしかないことが確定したので、次は IPSet を直接書き換えに行きます。</p>
<h2>解決手順 — IPSet を直接更新</h2>
<p>すでに <code>AmplifyIPSet-*</code> がどこにあり、どの WebACL に紐付いているかは分かっているので、あとは <strong>WAFv2 の IPSet を直接更新</strong> すれば終わりです。
ただし WAFv2 には楽観的排他制御の仕組みがあって、<code>LockToken</code> を毎回取り直す必要があります。</p>
<p>:::message
<strong>WAFv2 の LockToken（楽観的排他制御）</strong>
<code>Get*</code> 系 API のレスポンスに <code>LockToken</code> が含まれており、<code>Update*</code> 系の API ではこの <code>LockToken</code> を渡す必要があります。
他のプロセスが先に更新していると <code>WAFOptimisticLockException</code> で失敗します。
<strong>毎回 get → update をセットで実行する</strong>のが安全です。
:::</p>
<p>実際に流したコマンドの雛形がこれです。</p>
<pre><code class="language-bash"># 環境変数で接続先を切り替え（dev/stage/prod ごとに AWS_PROFILE を変える）
export AWS_PROFILE=my-app-prod
IPSET_NAME=AmplifyIPSet-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
IPSET_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

# 1) 現在の LockToken を取得
LOCK=$(aws wafv2 get-ip-set \
  --scope CLOUDFRONT --region us-east-1 \
  --name &quot;$IPSET_NAME&quot; --id &quot;$IPSET_ID&quot; \
  --query &quot;LockToken&quot; --output text)

# 2) IPSet の中身を新しい IP リストで上書き
aws wafv2 update-ip-set \
  --scope CLOUDFRONT --region us-east-1 \
  --name &quot;$IPSET_NAME&quot; --id &quot;$IPSET_ID&quot; \
  --lock-token &quot;$LOCK&quot; \
  --addresses 192.0.2.10/32 192.0.2.11/32 198.51.100.0/24 203.0.113.0/24
</code></pre>
<p>これを dev / stage / prod の 3 環境で順に実行し、それぞれの環境で <code>curl</code> を打って 200 OK を確認しました。</p>
<p>:::message
<strong>ポイント</strong>
<code>update-ip-set</code> の <code>--addresses</code> は <strong>上書き</strong> です（既存に追加ではなく差し替え）。誤って一部の IP を漏らすと、その IP からのアクセスが全部止まるので、現在の中身を一度ファイルに保存してから差分を取り、新しいリストとして渡すのが安全です。
:::</p>
<h2>学んだこと</h2>
<h3>1. 「同じ目的のリソースが複数経路で管理されている」状態を疑う</h3>
<p>今回は、<code>my-app-api-allowed-ips-&lt;env&gt;</code> と <code>AmplifyIPSet-*</code> という、<strong>同じ「拠点 IP 許可リスト」を別々の場所で独立に管理している</strong> 構造が根本原因でした。
インフラを段階的に作っていくと、こうした「二重管理状態」がいつのまにか出来上がっていることがあります。今回のような全社的な IP 変更のタイミングは、その整理の絶好の機会でもあるな、と感じました。</p>
<h3>2. UI を信用せず、API で実態を確認する癖を付ける</h3>
<p>Amplify Console のような上位のマネジメントコンソールは、内部で何かをキャッシュしていたり、過去の状態を表示し続けていたりすることがあります。
今回のように UI 表示 （「Firewall: 無効」） と API が示す実態 （Block ルール有り） が一致しないパターンも、十分起こり得ます。</p>
<h3>3. <code>list-resources-for-web-acl</code> の <code>--resource-type</code> を忘れない</h3>
<p>WAFv2 の <code>list-resources-for-web-acl</code> は、<code>--resource-type</code> を省略すると <strong>デフォルト値 <code>APPLICATION_LOAD_BALANCER</code> で検索されます</strong>。Amplify Hosting の場合は <code>AMPLIFY</code> を明示する必要があります。
また、この API は <strong>CloudFront Distribution を対象に取れません</strong>。CloudFront の関連付けは <code>aws cloudfront list-distributions-by-web-acl-id</code> という別の API を使うことになっており、これを知らないと「該当 WebACL がどこにも attach されていない」と誤認してしまいます。
今回も、ここに気付くまでが一番大きく時間を溶かしたポイントでした。</p>
<h3>4. CloudFront scope の WAF は us-east-1 にしかない</h3>
<p>これは基礎中の基礎なのですが、改めて。
WAFv2 で CloudFront に付けるリソースは <strong>必ず us-east-1</strong> にあります。CLI なら <code>--region us-east-1</code> 必須、コンソールなら左上のリージョンを Global （CloudFront） に切り替える必要があります。</p>
<h3>5. 「Request blocked.」というレスポンスボディは WAF Block の典型</h3>
<p>CloudFront 経由の 403 で、ボディに <code>Request blocked.</code> の文字列があれば、ほぼ WAF の block ルールが原因です。S3 の Access Denied とは見た目で区別できるので、最初の切り分けに便利です。</p>
<h2>使ったコマンドまとめ</h2>
<p>トラブルシュート中に何度も叩いたコマンドを、ここに集めておきます。</p>
<h3>IPSet 周り</h3>
<pre><code class="language-bash"># IPSet 一覧（CloudFront scope）
aws wafv2 list-ip-sets --scope CLOUDFRONT --region us-east-1

# IPSet の中身を確認
aws wafv2 get-ip-set \
  --scope CLOUDFRONT --region us-east-1 \
  --name &lt;name&gt; --id &lt;id&gt; \
  --query &quot;IPSet.Addresses&quot; --output json

# IPSet の更新（LockToken 必須）
LOCK=$(aws wafv2 get-ip-set \
  --scope CLOUDFRONT --region us-east-1 \
  --name &lt;name&gt; --id &lt;id&gt; \
  --query &quot;LockToken&quot; --output text)

aws wafv2 update-ip-set \
  --scope CLOUDFRONT --region us-east-1 \
  --name &lt;name&gt; --id &lt;id&gt; \
  --lock-token &quot;$LOCK&quot; \
  --addresses 192.0.2.0/24 198.51.100.0/24
</code></pre>
<h3>WebACL の attach 先確認</h3>
<pre><code class="language-bash"># Amplify Hosting に付いているかを確認（--resource-type を忘れない！）
aws wafv2 list-resources-for-web-acl \
  --region us-east-1 \
  --web-acl-arn &lt;arn&gt; \
  --resource-type AMPLIFY
</code></pre>
<h3>CloudTrail で履歴を追う</h3>
<pre><code class="language-bash"># 特定の API イベントを引く
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateIPSet \
  --max-items 20 --output json

# 特定リソースに対する操作履歴
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=&lt;arn&gt;
</code></pre>
<h2>まとめ</h2>
<p><code>Request blocked.</code> だけが手がかりの 403 から、Amplify Hosting 裏の WAF の存在に気付き、UI と API の乖離まで辿り着いた、というやや遠回りなトラブルシュート記でした。</p>
<p>整理すると、今回の学びは次の通りです。</p>
<ul>
<li>同じ目的のリソースが <strong>複数経路</strong> で管理されていないかを疑う</li>
<li>マネジメントコンソールの UI は実態と一致しないことがある。<strong>最後の真実は API レスポンス</strong></li>
<li>WAFv2 の <code>list-resources-for-web-acl</code> は <code>--resource-type</code> を忘れずに</li>
<li>CloudFront scope の WAF は <strong>必ず us-east-1</strong></li>
<li>WAFv2 の更新は <strong>LockToken をセットで</strong> 扱う</li>
</ul>
<p>Amplify Hosting の Firewall （AWS WAF 統合） は便利ですが、「いつのまにか UI と実態が乖離する」リスクがあることは覚えておいて損はないと思います。
同じような構成の運用に関わる方の参考になれば嬉しいです。最後まで読んでいただきありがとうございました！</p>
<h2>参考リンク</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/waf/latest/developerguide/how-aws-waf-works.html">AWS WAF Developer Guide — How AWS WAF works</a></li>
<li><a href="https://docs.aws.amazon.com/amplify/latest/userguide/WAF-integration.html">AWS Amplify Hosting User Guide — Firewall support for Amplify hosted sites</a></li>
<li><a href="https://aws.amazon.com/blogs/aws/firewall-support-for-aws-amplify-hosted-sites/">AWS Blog — Firewall support for AWS Amplify hosted sites</a></li>
<li><a href="https://docs.aws.amazon.com/cli/latest/reference/wafv2/list-resources-for-web-acl.html"><code>list-resources-for-web-acl</code> API リファレンス（WAFv2）</a></li>
<li><a href="https://docs.aws.amazon.com/cli/latest/reference/cloudfront/list-distributions-by-web-acl-id.html"><code>list-distributions-by-web-acl-id</code> API リファレンス（CloudFront）</a></li>
<li><a href="https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"><code>lookup-events</code> API リファレンス（CloudTrail）</a></li>
<li><a href="https://github.com/aws/aws-cli/issues/5417">aws/aws-cli #5417 — <code>wafv2 list-resources-for-web-acl</code> only fetches load balancers by default</a>（本記事で触れた <code>--resource-type</code> デフォルト挙動の罠について、CLI リポジトリで「ドキュメントに記載がない」と指摘された Issue）</li>
</ul>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/blog/authors/d.yamanaka/20260529/coverimage.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Aurora MySQL メジャーバージョンアップで utf8mb4_general_ci を使い続けるために必要だったすべてのこと]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-05-26-aurora-mysql-collation-migration/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-05-26-aurora-mysql-collation-migration/</guid>
            <pubDate>Tue, 26 May 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[Aurora MySQL 2系から3系へのバージョンアップにおいて、既存の utf8mb4_general_ci を維持したまま移行する際に発生した問題と、Illegal mix of collations への対応・移行手順・移行後インシデント・COLLATIONチェックの仕組みについて共有します。]]></description>
            <content:encoded><![CDATA[<p>こんにちは。 KINTO テクノロジーズの DBRE チーム所属の <a href="https://x.com/RmcHoshi">@hoshino</a> です。</p>
<h1>はじめに</h1>
<p>Aurora MySQL 2系（MySQL 5.7互換）から3系（MySQL 8.0互換）へのメジャーバージョンアップを、19クラスタ・46スキーマ規模のメインシステムで実施しました。</p>
<p>このバージョンアップで最も苦労したのが COLLATION の問題です。</p>
<p>Aurora MySQL 3系ではデフォルト COLLATION が <code>utf8mb4_0900_ai_ci</code> に変わりますが、既存システムでは、検索条件、ORDER BY、ユニーク制約、JOIN、帳票、バッチ処理などが<code>utf8mb4_general_ci</code> の比較・ソート挙動を前提に動いています。</p>
<p><code>utf8mb4_0900_ai_ci</code> への変更は単なる DB 設定変更ではなく、アプリケーション仕様の変更に近いため、今回は互換性維持を優先し、<code>utf8mb4_general_ci</code> を維持したまま移行する方針を取りました。</p>
<p>しかし、Aurora MySQL 3系では <code>default_collation_for_utf8mb4</code> が <code>utf8mb4_0900_ai_ci</code> 固定で、サーバー側で変更する手段が用意されておらず、明示的に指定しないとセッションのデフォルトが <code>utf8mb4_0900_ai_ci</code> になってしまいます。そのため、<code>utf8mb4_general_ci</code> を維持するために以下の対策を実施しました。</p>
<p><strong>今回実施した対策</strong></p>
<ul>
<li><code>SCHEMA / TABLE / COLUMN / VIEW / ROUTINE / TRIGGER / EVENT</code> の COLLATION を統一</li>
<li>接続設定・SQL クエリで COLLATION を明示指定することで COLLATION を制御</li>
<li>意図しない COLLATION が設定されないように <code>information_schema</code> を使った Slack 自動通知によるチェック体制の整備</li>
</ul>
<p>本記事では、これらの対策の詳細について説明します。</p>
<h1>背景</h1>
<p>KINTO テクノロジーズの DBRE チームでは、Aurora MySQL 2系（MySQL 5.7互換）から3系（MySQL 8.0互換）へのメジャーバージョンアップを進めてきました。</p>
<p>弊社では多数のクラスタを運用していますが、今回対象となったのは複数プロダクトが共有するメインシステムの DB です。</p>
<p>このメインシステムは少し特殊な構成になっています。</p>
<p>1つの環境に対して 2つの Aurora クラスタが存在しており、複数プロダクトがこの2クラスタを共有して利用しています。</p>
<p>両クラスタは密接に連携しているため、片方だけバージョンアップするわけにはいかず、同時に移行する必要がありました。</p>
<p>対象規模は dev・stg・prod などの全環境を合計して 19クラスタ・46スキーマ・56ユーザー にのぼります。</p>
<p>構成を図にすると以下のようになります。</p>
<p><img src="/assets/blog/authors/hoshino/aurora_collation_system_overview.webp" alt="対象システムの構成図"></p>
<p>この移行で最も苦労したのが COLLATION の問題でした。</p>
<p>Aurora MySQL 3系（MySQL 8.0）のデフォルト COLLATION は <code>utf8mb4_0900_ai_ci</code> です。</p>
<p>一方、既存のデータベースは <code>utf8mb4_general_ci</code> で運用されていました。</p>
<p>システム全体を <code>utf8mb4_0900_ai_ci</code> に切り替えるという選択肢もゼロではありませんでしたが、COLLATION の変更はアプリケーションの挙動に直接影響します。</p>
<p><code>utf8mb4_general_ci</code> と <code>utf8mb4_0900_ai_ci</code> は、どちらも大文字・小文字を区別しない COLLATION ですが、内部のソートアルゴリズムが異なります。</p>
<p><code>utf8mb4_0900_ai_ci</code> は Unicode Collation Algorithm（UCA 9.0.0）に準拠しており、<code>=</code> 演算子による比較結果や <code>ORDER BY</code> のソート順が <code>utf8mb4_general_ci</code> とは異なるケースがあります。</p>
<p>既存のアプリケーションが <code>utf8mb4_general_ci</code> の挙動を前提としている場合、COLLATION を切り替えただけで検索結果やソート順が変わり、意図しない不具合につながる可能性があります。</p>
<p>そうなると各プロダクト側でも影響調査や改修が必要になります。</p>
<p>複数プロダクトが共有しているデータベースであるため、その改修範囲は広く、プロダクト側の開発コストも大きくなります。</p>
<p>プロダクト側の負担を最小限にするためにも、<code>utf8mb4_general_ci</code> を維持したままバージョンアップするという方針を選択しました。</p>
<h1>Illegal mix of collations に対する対応</h1>
<p><code>utf8mb4_general_ci</code> を維持する方針で進めるにあたって直面したのが、<code>Illegal mix of collations</code> というエラーです。</p>
<p>このエラーは、テーブル側の COLLATION とセッション側の COLLATION が混在した状態でクエリを実行したときに発生します。Aurora MySQL 3系では、サーバー側でデフォルト COLLATION を変更する手段がないため、何も対策しないとこのエラーが発生しやすい構造になっています。</p>
<p>MySQL 8.0 には <code>default_collation_for_utf8mb4</code> というシステム変数があります（<a href="https://dev.mysql.com/doc/refman/8.0/ja/server-system-variables.html">MySQL 公式: Server System Variables</a>）。</p>
<p>これは <code>CHARACTER SET utf8mb4</code> を指定して COLLATE を省略したとき、どの COLLATION がデフォルトで使われるかを決める変数で、デフォルト値は <code>utf8mb4_0900_ai_ci</code> です。</p>
<p>通常の MySQL であれば、<code>SET PERSIST default_collation_for_utf8mb4=&#39;utf8mb4_general_ci&#39;;</code> を実行することでこの値を変更できますが、Aurora MySQL ではこの変数を変更する手段がありません。</p>
<p>理由としては <code>SET PERSIST</code> は Aurora では使えず、パラメータグループにもこの設定項目が存在しないためです。</p>
<p>この制約により、<code>collation_connection</code> を指定せずに接続した場合、セッションのデフォルトが <code>utf8mb4_0900_ai_ci</code> になってしまいます。</p>
<p>影響は実行するクエリだけではありませんでした。</p>
<p>VIEW や ROUTINE（ストアドプロシージャ・ファンクション）は、作成時のセッションの <code>character_set_client</code> や <code>collation_connection</code> が定義に依存するため、<code>utf8mb4_0900_ai_ci</code> のセッションで VIEW を作成すると、その VIEW 自体が <code>utf8mb4_0900_ai_ci</code> を持ってしまいます。</p>
<p>後からセッションの COLLATION を変えても、すでに作成された VIEW の定義は変わりません。</p>
<p>さらに、クエリの中で COLLATION が動的に決まる箇所にも影響します。</p>
<p>たとえば UNION や CAST 関数を含むクエリでは、TABLE 側の COLLATION（<code>utf8mb4_general_ci</code>）とセッション側の COLLATION（<code>utf8mb4_0900_ai_ci</code>）が混在してエラーが発生します。</p>
<p><strong>例1：CAST 関数を使った JOIN</strong></p>
<pre><code class="language-sql">SELECT *
FROM table_a AS t1
JOIN table_b AS t2
  ON CAST(t1.id AS CHAR) = t2.code;
--    ^^^^^^^^^^^^^^^^^^   ^^^^^^^
--    utf8mb4_0900_ai_ci   utf8mb4_general_ci
--   （セッションのデフォルト）（テーブルの COLLATION）
</code></pre>
<p><code>CAST(t1.id AS CHAR)</code> はセッションの <code>collation_connection</code> に従うため、Aurora のデフォルトである <code>utf8mb4_0900_ai_ci</code> になります。一方、<code>t2.code</code> はテーブル定義の <code>utf8mb4_general_ci</code> のままです。この2つを <code>=</code> で比較するため、COLLATION の不一致が発生します。</p>
<p><strong>例2：UNION で異なる COLLATION が混在</strong></p>
<pre><code class="language-sql">SELECT name FROM table_a
--     ^^^^
--     utf8mb4_general_ci（テーブルの COLLATION）
UNION
SELECT CAST(id AS CHAR) FROM table_b;
--     ^^^^^^^^^^^^^^^^
--     utf8mb4_0900_ai_ci（セッションのデフォルト）
</code></pre>
<p>UNION は各 SELECT の COLLATION を統一する必要がありますが、上記のように一方が <code>utf8mb4_general_ci</code>、もう一方が <code>utf8mb4_0900_ai_ci</code> になると統一できず、エラーになります。</p>
<p>どちらのクエリも、最終的には以下のエラーになります。</p>
<pre><code class="language-text">ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and
(utf8mb4_0900_ai_ci,IMPLICIT) for operation &#39;=&#39;
</code></pre>
<p>これを防ぐには、接続時に COLLATION を明示的に指定するか、SQL 文の中で COLLATE 句を明示する方法があります。</p>
<p><strong>方法1：接続時に COLLATION を指定する</strong></p>
<pre><code class="language-sql">-- MySQL クライアントから接続する場合
SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;
</code></pre>
<pre><code class="language-text"># JDBC URL での指定例
jdbc:mysql://host:3306/mydb?connectionCollation=utf8mb4_general_ci
</code></pre>
<p><strong>方法2：SQL 文の中で COLLATE 句を明示する</strong></p>
<p>UNION や CAST など動的に COLLATION が決まる箇所に、直接 COLLATE 句を付与する方法です。</p>
<pre><code class="language-sql">-- UNION での指定例
SELECT name COLLATE utf8mb4_general_ci FROM table_a
UNION
SELECT name COLLATE utf8mb4_general_ci FROM table_b;

-- CAST での指定例
SELECT CAST(column AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci
FROM table_a;
</code></pre>
<p>しかし、接続時の COLLATION 指定やクエリへの COLLATE 句付与は、あくまで移行後の運用で問題を防ぐための対策です。</p>
<p>移行するにあたり、移行前に Aurora 2系側の COLLATION を統一しておく必要がありました。</p>
<p><strong>注意事項</strong></p>
<p><code>SET NAMES utf8mb4</code>（COLLATE 句を省略）を実行すると、それまでに設定していた <code>collation_connection</code> が破棄され、Aurora MySQL 3系のデフォルトである <code>utf8mb4_0900_ai_ci</code> に戻ってしまいます。</p>
<pre><code class="language-sql">SET SESSION collation_connection = &#39;utf8mb4_general_ci&#39;;
-- ↑ ここで utf8mb4_general_ci になる

SET NAMES utf8mb4;
-- ↑ COLLATE 句がないため utf8mb4_0900_ai_ci に戻ってしまう
</code></pre>
<p>ORM やアプリケーションフレームワークが内部で <code>SET NAMES utf8mb4</code> を発行する実装も存在するため、実際に発行されるクエリのログを確認し、暗黙の <code>SET NAMES</code> が含まれていないかを把握しておく必要があります。</p>
<p><code>SET NAMES</code> を使う場合は、必ず COLLATE 句までセットで指定するのが確実です。</p>
<h1>移行手順と事前準備</h1>
<h2>移行方法</h2>
<p>今回の移行は mysqldump などで論理ダンプを取得し、それをインポートする方式を採用しました。</p>
<p>Aurora MySQL 2系から3系への移行方式としては、Blue/Green デプロイやインプレースアップグレードといった選択肢もありますが、今回は以下の理由からダンプ・インポートを採用しました。</p>
<ul>
<li>COLLATION の事前調整で DDL 変更が必要だった<ul>
<li>VIEW や ROUTINE の定義を書き換えて再作成する必要がありましたが、Blue/Green デプロイでは DDL 変更が Green 環境へのレプリケーション中断を引き起こすリスクがあり、レプリケーションとの互換性検証コストが高いと判断しました</li>
</ul>
</li>
<li>ダンプ・インポート方式の社内実績が豊富だった<ul>
<li>弊社では全環境で数百の DB クラスタが存在しており、そのほとんどをダンプ・インポート方式で移行しました</li>
<li>そのため、今回のような複数プロダクトが共有する大規模システムの移行において、Blue/Green デプロイなどの実績のない手法を採用するリスクは取れませんでした</li>
</ul>
</li>
</ul>
<p>安全を最優先に考えた結果、確実にコントロールできるダンプ・インポート方式を選択しました。</p>
<h2>ダンプ・インポート時の COLLATION エラー</h2>
<p>ダンプ・インポート方式で移行を進めたところ、COLLATION の不整合によるエラーが発生しました。</p>
<p>Aurora 2系側の COLLATION が <code>utf8mb4_general_ci</code> に統一されていない状態でダンプを取ってインポートすると、VIEW の作成時に <code>Illegal mix of collations</code> エラーとなり、移行そのものが失敗します。</p>
<p>そのため、以下の手順で移行を実施しました。</p>
<ol>
<li>Aurora 2系側で COLLATION を <code>utf8mb4_general_ci</code> に統一する</li>
<li>その状態でダンプを取得する</li>
<li>Aurora 3系にインポートする</li>
</ol>
<h2>事前作業の内容</h2>
<p>事前作業では SCHEMA / TABLE / COLUMN / VIEW / ROUTINE のすべてに手を入れる必要がありました。</p>
<p>対象となるのは2クラスタ × 全環境（dev・stg・prod 等）にまたがる数十スキーマです。複数プロダクトが共有しているため、各スキーマの VIEW や ROUTINE がどのプロダクトに属するかを把握し、プロダクトチームと調整しながら進める必要がありました。</p>
<p>調整箇所は全環境合計で数千にのぼり、環境ごとにリストを作成し、プロダクトチームにレビューを依頼し、反映前に最終チェックを行うというサイクルを、すべての環境に対して繰り返し実施しました。</p>
<p>以下、具体的な調整方法をオブジェクトの種類ごとに説明します。</p>
<h3>SCHEMA / TABLE / COLUMN の調整</h3>
<p>SCHEMA・TABLE・COLUMN は ALTER 文で COLLATION を <code>utf8mb4_general_ci</code> に変更しました。</p>
<pre><code class="language-sql">-- SCHEMA
ALTER DATABASE ${schema} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

-- TABLE
ALTER TABLE ${table} CHARACTER SET utf8mb4 COLLATE &#39;utf8mb4_general_ci&#39;;

-- COLUMN
ALTER TABLE ${table} CONVERT TO CHARACTER SET utf8mb4 COLLATE &#39;utf8mb4_general_ci&#39;;
</code></pre>
<h3>VIEW / ROUTINE / TRIGGER / EVENT の調整</h3>
<p>一方、VIEW・ROUTINE・TRIGGER・EVENT は ALTER では対応できないため、定義を書き換えて再作成する必要がありました。</p>
<p>定義内の文字コード・COLLATION を一括で置換してから <code>CREATE OR REPLACE VIEW</code> で再作成するアプローチを取りました。主な置換パターンは以下の通りです。</p>
<pre><code class="language-text">&quot;utf8 &quot;              → &quot;utf8mb4 &quot;
&quot;utf8_general_ci&quot;    → &quot;utf8mb4_general_ci&quot;
&quot;utf8mb4_0900_ai_ci&quot; → &quot;utf8mb4_general_ci&quot;
&quot;utf8mb4_unicode_ci&quot; → &quot;utf8mb4_general_ci&quot;
&quot;charset utf8mb4) AS&quot; → &quot;charset utf8mb4) COLLATE utf8mb4_general_ci AS&quot;
</code></pre>
<p>最後のパターンは CAST 関数の末尾に該当します。<code>CAST(column AS CHAR)</code> のような式では COLLATION が動的に決まるため、明示的に COLLATE を付与する必要がありました。</p>
<p>ただし、文字列置換だけでは対応しきれないケースも存在しました。</p>
<p>CAST 関数の使い方が複雑であったり、置換パターンに収まらない定義を持つ VIEW がいくつかありました。</p>
<p>こうした箇所は <code>information_schema</code> で COLLATION の状態を一つひとつ確認しながら、手動で定義を修正して再作成しました。</p>
<pre><code class="language-sql">-- 置換漏れがないか確認するクエリ
SELECT table_schema, table_name,
       character_set_client, collation_connection
FROM information_schema.views
WHERE collation_connection != &#39;utf8mb4_general_ci&#39;
  AND table_schema NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;);
</code></pre>
<p>この確認を怠ると、一見すると置換が完了しているように見えても <code>utf8mb4_0900_ai_ci</code> が残ったままの定義が存在する場合インポート時にエラーが発生するか、移行後に <code>Illegal mix of collations</code> となってしまいます。</p>
<h1>移行後に発生したインシデント</h1>
<p>事前作業で Aurora 2系の COLLATION を統一し、Aurora 3系への移行を完了しました。</p>
<p>しかし移行後、プロダクトから <code>Illegal mix of collations</code> のエラーが発生したとの報告がありました。</p>
<h2>発生したエラー</h2>
<p>エラーの内容は以下の通りです。</p>
<pre><code class="language-text">1267 (HY000): Illegal mix of collations (utf8mb4_0900_ai_ci,IMPLICIT) and
(utf8mb4_general_ci,IMPLICIT) for operation &#39;=&#39;
</code></pre>
<p>Aurora 2系では問題なく動作していた機能が、Aurora 3系への移行後にエラーとなっていました。</p>
<h2>原因の調査</h2>
<p>まず、エラーが発生しているクエリの調査を行いました。</p>
<p>問題のクエリには CAST 関数を使った JOIN が含まれていました。</p>
<p>以下は同様の構造を持つ例です。</p>
<pre><code class="language-sql">-- 例：CAST 関数を使った JOIN で COLLATION の不一致が発生するケース
SELECT *
FROM table_a AS t1
LEFT JOIN table_b AS t2
  ON CAST(t1.id AS CHAR) = t2.code;
</code></pre>
<p><code>CAST(... AS CHAR)</code> の結果にはセッションの <code>collation_connection</code> が適用されます。</p>
<p>該当のアプリケーションでは <code>collation_connection</code> が指定されておらず、Aurora のデフォルトである <code>utf8mb4_0900_ai_ci</code> が適用されていました。</p>
<p>その結果、CAST 関数の結果は <code>utf8mb4_0900_ai_ci</code> となり、テーブル側の <code>utf8mb4_general_ci</code> と混在して <code>Illegal mix of collations</code> が発生していました。</p>
<h2>対処方法</h2>
<p>対処として、アプリケーションの DB 接続設定に <code>collation_connection=utf8mb4_general_ci</code> を追加しました。</p>
<pre><code class="language-text"># 接続文字列に COLLATION 設定を追加
mysql+mysqlconnector://user:password@host/dbname
  ?init_command=SET SESSION collation_connection=utf8mb4_general_ci
</code></pre>
<p><code>init_command</code> は接続確立直後に実行されるため、以降のクエリでは <code>collation_connection</code> が <code>utf8mb4_general_ci</code> の状態で処理されます。</p>
<p><code>collation_connection</code> が <code>utf8mb4_general_ci</code> になることで、<code>CAST</code> や <code>UNION</code> のようにセッションの COLLATION 値で動的に COLLATION が決まる箇所も <code>utf8mb4_general_ci</code> に揃えられ、テーブル側との不一致を防げます。</p>
<p>この変更をリリースした後、エラーは解消し、現在は安定稼働しています。</p>
<h1>COLLATION の定期チェックと自動通知</h1>
<p>一度問題を修正しても、新しい VIEW が作成されたりアプリケーションが更新されたりすると、同様の問題が再発する可能性があります。</p>
<p>本番環境でエラーが発生してから気づくのではなく、開発段階で COLLATION の不一致を早期に検知するために、全環境の COLLATION の状態を定期的にチェックし、意図しない COLLATION が設定された場合に自動で通知する仕組みと、手動で現状のCOLLATIONの状態を確認できる仕組みを構築しました。</p>
<h2>自動化の仕組み</h2>
<p>仕組みの全体像は以下の通りです。</p>
<p><img src="/assets/blog/authors/hoshino/aurora_collation_monitoring_architecture.webp" alt="COLLATION チェック自動化の構成図"></p>
<h3>1. 日次で COLLATION 情報を自動取得</h3>
<p>COLLATION をチェックするクエリを CLI コマンドとして実装しました。</p>
<p>このコマンドを全クラスタに対して日次で自動実行し、取得結果を JSON 形式で S3 に保存しています。</p>
<h3>2. 期待する COLLATION との照合と Slack 通知</h3>
<p>EventBridge で決まった時間に、S3 上の JSON データを精査します。</p>
<p>DynamoDB にあらかじめ登録してある「期待する COLLATION」と照合し、意図しない COLLATION が検出された場合は Slack の専用チャンネルに自動通知します。</p>
<h3>3. CLI による手動チェック</h3>
<p>CLI コマンドは手動でも実行できます。</p>
<p>新規 TABLE 作成後やトラブルシューティング時など、任意のタイミングで特定のクラスタの状態を確認したい場合に使用しています。</p>
<h2>COLLATION チェックで実行しているクエリ</h2>
<p>自動化の仕組みの中で各クラスタに対して実行しているクエリは、<code>information_schema</code> を使って <code>utf8mb4_general_ci</code> 以外の COLLATION が混入していないかを検出するものです。対象が SCHEMA・TABLE・COLUMN・VIEW・ROUTINE・TRIGGER の6種類です。</p>
<pre><code class="language-sql">-- SCHEMA の COLLATION 確認
SELECT schema_name, default_character_set_name, default_collation_name
FROM information_schema.schemata
WHERE schema_name NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;);

-- TABLE の COLLATION 確認
SELECT table_schema, table_name, table_collation
FROM information_schema.tables
WHERE table_collation != &#39;utf8mb4_general_ci&#39;
  AND table_schema NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;);

-- COLUMN の COLLATION 確認
SELECT table_schema, table_name, column_name, collation_name
FROM information_schema.columns
WHERE collation_name IS NOT NULL
  AND collation_name != &#39;utf8mb4_general_ci&#39;
  AND table_schema NOT IN (&#39;mysql&#39;, &#39;information_schema&#39;, &#39;performance_schema&#39;, &#39;sys&#39;);

-- VIEW の collation_connection 確認
SELECT table_schema, table_name,
       character_set_client, collation_connection
FROM information_schema.views
WHERE collation_connection != &#39;utf8mb4_general_ci&#39;;

-- ROUTINE の COLLATION 確認
SELECT routine_schema, routine_name, routine_type,
       collation_connection, database_collation
FROM information_schema.routines
WHERE collation_connection != &#39;utf8mb4_general_ci&#39;;

-- TRIGGER の COLLATION 確認
SELECT trigger_schema, trigger_name,
       collation_connection, database_collation
FROM information_schema.triggers
WHERE collation_connection != &#39;utf8mb4_general_ci&#39;;
</code></pre>
<h1>今後のAuroraバージョンアップ（Aurora MySQL 4）に向けて</h1>
<p>MySQL 8.4 で <code>default_collation_for_utf8mb4</code> を <code>SET PERSIST</code> で変更すると、以下の deprecated 警告が表示されます。</p>
<pre><code class="language-text">mysql&gt; SET PERSIST default_collation_for_utf8mb4=&#39;utf8mb4_general_ci&#39;;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql&gt; SHOW WARNINGS;
+---------+------+--------------------------------------------------------------------------------------------------------+
| Level   | Code | Message                                                                                                |
+---------+------+--------------------------------------------------------------------------------------------------------+
| Warning | 1681 | Updating &#39;default_collation_for_utf8mb4&#39; is deprecated. It will be made read-only in a future release. |
+---------+------+--------------------------------------------------------------------------------------------------------+
</code></pre>
<p>「将来のリリースで read-only にする」と警告されていることから、今後この変数による COLLATION の制御はさらに難しくなる可能性があります。COLLATION を確実に制御するためには、SCHEMA・TABLE・COLUMN・VIEW・ROUTINE のすべてで明示指定し、<code>information_schema</code> で定期的にチェックするアプローチが引き続き有効です。</p>
<p><a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.release-calendars.html">Aurora MySQL のリリースカレンダー</a>によると、Aurora MySQL 3 のメジャーバージョン標準サポートは 2028年4月30日 までとなっています。その後は次のメジャーバージョンへの移行が必要になるため、今回整備した定期チェックの仕組みや CLI コマンドを次のバージョンアップでもそのまま活用できるようにしておくことが重要だと考えています。</p>
<h1>まとめ</h1>
<ul>
<li>Aurora MySQL 3系では MySQL 8.0 互換となり、デフォルト COLLATION が utf8mb4_0900_ai_ci に変わったことで、既存 DB の utf8mb4_general_ci と混在しやすくなった。これが今回の苦労の根本原因</li>
<li>Aurora MySQL では <code>SET PERSIST</code> で <code>default_collation_for_utf8mb4</code> を変更できないため、サーバー側で utf8mb4 の デフォルト COLLATION を制御できない</li>
<li>接続時に <code>collation_connection</code> を明示指定しないと、セッションのデフォルトが <code>utf8mb4_0900_ai_ci</code> となり <code>Illegal mix of collations</code> が発生する可能性がある</li>
<li>ダンプ・インポートで移行する場合、移行前に Aurora 2系側の SCHEMA / TABLE / COLUMN / VIEW / ROUTINE の COLLATION を統一しておく必要がある</li>
<li><code>SET NAMES utf8mb4;</code>（COLLATE 省略）は直前の COLLATE 指定を破棄するため、接続文字列の <code>init_command</code> で指定するのが確実</li>
<li>移行後も <code>information_schema</code> を使った COLLATION の定期チェックと自動通知の仕組みが有効</li>
<li>今回整備した COLLATION チェックの仕組みや CLI コマンドは、次のバージョンアップでもそのまま活用できる</li>
</ul>
<p>本記事の内容が、同じ課題に取り組んでいる方々の参考になれば幸いです。</p>
<h1>参考文献</h1>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/upgrading-from-previous-series.html">Changes in MySQL 8.0</a><ul>
<li><code>collation_server</code> のデフォルトが <code>utf8mb4_0900_ai_ci</code> に変更</li>
</ul>
</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/server-system-variables.html">Server System Variables</a><ul>
<li><code>default_collation_for_utf8mb4</code> パラメータの補足</li>
</ul>
</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/charset-connection.html">接続に関するパラメータの理解</a><ul>
<li><code>character_set_*</code> / <code>collation_*</code> 各パラメータの関係</li>
</ul>
</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/set-names.html">SET NAMES の補足</a><ul>
<li>セッションの COLLATION 指定方法</li>
</ul>
</li>
</ul>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/common/thumbnail_default_×2.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[2026年2月入社メンバー紹介]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-05-15-newcomer-202602/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-05-15-newcomer-202602/</guid>
            <pubDate>Fri, 15 May 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[2026年2月に入社した6人の皆様に入社後の感想を伺い、まとめました。]]></description>
            <content:encoded><![CDATA[<h1>はじめに</h1>
<p>こんにちは、2026年2月入社の岩月です！</p>
<p>本記事では、2026年2月入社のみなさまに入社直後の感想をお伺いし、まとめてみました。
KINTO テクノロジーズ（以下、KTC）に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います！</p>
<h1>森田和明</h1>
<p>![森田和明さんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/morita.jpg =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>コーポレートIT部AIファーストGの森田です。</li>
<li>社内の生成AI活用の推進やトヨタグループにおけるAI活用支援を担当しています。</li>
<li>奈良に住んでます。</li>
<li>最近書籍を執筆しました！<a href="https://gihyo.jp/book/2026/978-4-297-15458-5">AWSではじめるMCP実践ガイド</a></li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>AIファーストGは「AI Transformation 」「AI Engineering」「AI Development」の3チーム体制で、私はAI Engineeringに所属です。</li>
<li>「アイデア生成」→「実現可能性の検証」→「実施とデリバリー」→「ケース展開」→「アイデア生成」とループを回し、AI活用の活性化に取り組んでいます</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>入社前のカジュアル面談などを通じて思っていた通りでした。</li>
<li>エンジニアが多い会社ではありますが、技術スタックが様々で、各自がそれぞれの分野でスペシャリストという印象です。</li>
<li>AIファーストGも全員バックグラウンドが違うので、それぞれの得意分野とAIを掛け合わせて専門性を発揮しています。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>私はOsaka Tech Labで勤務していまして、まず、オフィスが綺麗です。</li>
<li>所属は様々ですが「大阪を盛り上げていこう！」という雰囲気があり、技術交流イベントなど一致団結できる取り組みがあります。</li>
</ul>
</li>
<li><strong>ブログを書くことになってどう思った？</strong><ul>
<li>趣味として技術ブログをやっているので、すんなり書けました！</li>
</ul>
</li>
<li><strong>岩月 ⇒ 森田さんへの質問</strong><blockquote>
<p>森田さんは技術系の書籍をいくつか執筆されていますが、執筆のきっかけや苦労話があったら教えてください！</p>
</blockquote>
<ul>
<li>私が執筆した書籍は、執筆メンバーが共著者を探している中で、声をかけてもらって参加したというのが経緯です。</li>
<li>苦労はたくさんありますが（笑）、扱うテーマがAWSや生成AIなので執筆している最中にアップデートがあり、その度に原稿の更新や画面キャプチャの取り直しを行っています</li>
</ul>
</li>
</ul>
<h1>成島大介</h1>
<p>![成島大介さんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/narushima.jpg =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>新サービス開発部 プロジェクト推進Ｇに所属しています。</li>
<li>名古屋オフィス勤務です。</li>
<li>トヨタグループ向け案件のプロジェクトマネジメントを担当しています。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>プロジェクト推進Ｇは兼務除くと5名体制で名古屋3名、東京1名、福岡1名です。</li>
<li>プロジェクトマネージャだけの組織なので、開発メンバーは状況に応じて他部署から参画してもらい開発体制を作ります。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>名古屋オフィスに開発者が少ないことが入社前の印象とのギャップです。</li>
<li>KINTOとKTCが同じフロアなので、入社前のオフィス見学では気づきませんでした。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>仕事については、問題課題がない限り任されていると感じます。</li>
<li>一緒にランチに行く機会が多くいろいろ情報収集できて助かってます。</li>
</ul>
</li>
<li><strong>ブログを書くことになってどう思った？</strong><ul>
<li>個人（匿名）で技術ブログは書いてましたが、最近はさっぱりです。</li>
<li>一番読んでもらえた記事が専門外のC++ネタで何を書くと良いのか分かってません。</li>
</ul>
</li>
<li><strong>森田さん ⇒ 成島さんへの質問</strong><blockquote>
<p>最近の猫ちゃんの面白エピソードを教えてください！</p>
</blockquote>
<ul>
<li>猫がドアノブに飛びついてドアを開けることをマスターしました。
娘（中３）の部屋にも問答無用で侵入します（ドア全開）。
娘も親には怒るが、猫には怒りません。</li>
</ul>
</li>
</ul>
<h1>きゅーじ</h1>
<p>![きゅーじさんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/kyuji.jpg =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>my route開発部ビジネス開発支援グループに所属しています。</li>
<li>勤務場所は福岡のFukuoka Tech Labです。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>主にトヨタファイナンシャルサービス株式会社のmyroute業務支援を行っています。主に営業、マーケティング業務をサポートしており、2～5名のチームで動いています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>Fukuoka Tech Labからの景色が最高に良い！※入社後初めて入りました。</li>
<li>オンボーディングがしっかりあり、安心して業務が開始できました。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>良いプレッシャーの中で、和やかな雰囲気かなと思います。</li>
<li>それぞれの個性と強みを生かしながら、どんどん仕事を作っていく感じが良いなと思っています。</li>
</ul>
</li>
<li><strong>ブログを書くことになってどう思った？</strong><ul>
<li>テックブログは、書いたことなかったかつ、非エンジニアの私が書けるのか不安でした。</li>
</ul>
</li>
<li><strong>成島さん ⇒ きゅーじさんへの質問</strong><blockquote>
<p>ミシンで最近作った作品教えてください！</p>
</blockquote>
<ul>
<li>2月にミシンを買っていろいろ作ろうと息巻いておりましたが、現状、カーテンの裾上げ、布団カバーの修理等々が私の作品ですかね。クッションカバーを今度作ろうと布屋さんに行こうと思います。</li>
</ul>
</li>
</ul>
<h1>かわちゃん</h1>
<p>![かわちゃんさんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/kawachan.jpg =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>my route開発部のプロダクト推進グループに所属しています。</li>
<li>神保町オフィス勤務です。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>プロダクト開発チームにいますが、私を含め4名です。うち3名はプロデューサーとして、うち1名は他部署から分析部分のみお手伝いいただいています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>オンボーディングの研修が手厚くて驚きました。</li>
<li>フリーアドレスかなと思ったのですが、固定だったのが新鮮でした。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>大人数ですが、思った以上に静かな部署です。</li>
<li>外国籍の方が多いので最初ドキドキしましたが今は慣れました。</li>
</ul>
</li>
<li><strong>ブログを書くことになってどう思った？</strong><ul>
<li>テックブログは書いたことがないのでちょっと焦りました。</li>
</ul>
</li>
<li><strong>きゅーじさん ⇒ かわちゃんさんへの質問</strong><blockquote>
<p> 国内旅行でおすすめの場所はありますか？</p>
</blockquote>
<ul>
<li>あまり国内も海外も旅行に行っておらずおすすめできる場所がありませんが、高知は2回ほど行っていて居心地が良かったです。桂浜がとても素敵でした。</li>
</ul>
</li>
</ul>
<h1>SHN</h1>
<p>![SHNさんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/shn.jpg =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>KTC 業務システム開発部に所属しつつ、現在は KINTO 業務部に出向（兼務）しています。</li>
<li>名古屋市在住で、桜通オフィスに勤務しています。</li>
<li>バックオフィス業務の改善や効率化を担当しています。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>出向先の KINTO 業務部では、IT 推進チームに所属しており、4 人体制で業務にあたっています。</li>
<li>バックオフィス業務を IT 目線で改善するチームとして、KTC の開発編成部や業務委託先などの関係者と連携しながら仕事を進めています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>良い意味で「トヨタっぽくない」ところがギャップでした。</li>
<li>トヨタ系列の会社ということで、縦割りな組織・慎重な意思決定・多重な申請フローなどがある程度あるだろうと想像していましたが、実際にそんなことはなく、オープンでフランク、かつスピード感のある職場だと感じています。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>KINTO 業務部はサービスを円滑に運営するため、販売店様との架電対応を担うメンバーも多く、適度な緊張感があります。</li>
<li>IT スキルだけでなく、リースや保険を含む KINTO サービスへの深い理解がなければ対応しきれない場面も多く、日々多くのことを学んでいます。</li>
</ul>
</li>
<li><strong>ブログを書くことになってどう思った？</strong><ul>
<li>KTC への応募・入社を検討する前からこのブログを読んでいたので、「とうとう自分の番が来たか」という感慨がありました。</li>
<li>応募・入社を検討されている方にとって有益な情報を発信できる、良い機会だと思っています。</li>
</ul>
</li>
<li><strong>かわちゃんさん ⇒ SHNさんへの質問</strong><blockquote>
<p>ランニングするときのこだわりや、自分だけのルールはありますか？</p>
</blockquote>
<ul>
<li>ランニングは習慣的に続けているのですが、「今日は走りたくないな」と感じる日も正直よくあります。 </li>
<li>そんな日は、走り終わった後にコンビニへ直行してアイスやスイーツを買うことを自分へのご褒美にして、モチベーションを保つようにしています。</li>
</ul>
</li>
</ul>
<h1>岩月</h1>
<p>![岩月のプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/iwatsuki.jpg =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>コーポレートIT部コーポレートIT Gの岩月です。</li>
<li>社内IT業務の改善や効率化のために座席表システムをはじめとするいくつかのツールを開発しています。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>東京と名古屋合わせて11名が在籍しています。</li>
<li>人数もそれなりに多く業務も多種多様なチームなので、これから業務範囲を広げていく中で、少しずつ全体像を理解していきたいと思っています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>生成AIを活用した、スピード感のある社内IT改善に取り組めると感じて入社しました。</li>
<li>前々職での上司や前職の同僚が在籍していて、入社前から社内の様子を伺えていたこともあり、大きなギャップは感じていません。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>打ち合わせで積極的に発言が飛び交い、現場からボトムアップに課題を挙げて改善していく雰囲気があると感じています。</li>
</ul>
</li>
<li><strong>ブログを書くことになってどう思った？</strong><ul>
<li>ここしばらく書く機会がなかったのですが、こうした機会をいただけるのであれば、今後は積極的に情報発信していきたいと思います。</li>
</ul>
</li>
<li><strong>SHNさん ⇒ 岩月への質問</strong><blockquote>
<p>デスクワークで手放せない or 仕事が捗るガジェットはありますか？</p>
</blockquote>
<ul>
<li>業務端末にはセキュリティを考慮して個人所有のデバイスは接続していませんが、その分ソフトウェアで工夫しています。</li>
<li>業務効率化のために自作しているmacOS用のアプリで、メニューバーに次の予定の時刻を常時表示しつつ、当日のスケジュール確認や、ワンクリックでZoom・Teams・Slackハドルへの参加、会議資料へのアクセスができるようにしています。</li>
<li>これまでも似たアプリを個人で作って使い続けていたこともあり、今やこれがないと会議の時間を忘れてしまう体になってしまいました。</li>
</ul>
</li>
</ul>
<h1>さいごに</h1>
<p>みなさま、入社後の感想を教えてくださり、ありがとうございました！</p>
<p>KINTOテクノロジーズでは日々、新たなメンバーが増えています！
今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。</p>
<p>そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています！
詳しくは<a href="https://www.kinto-technologies.com/recruit/">こちら</a>からご確認ください！</p>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/common/thumbnail_default_×2.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[【Osaka Tech Lab】年度末イベント「O-KINI FY2026」開催！]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-05-01-o-kini-fy2026/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-05-01-o-kini-fy2026/</guid>
            <pubDate>Fri, 01 May 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[2026年3月末に開催した年度末イベントについて本記事で紹介します]]></description>
            <content:encoded><![CDATA[<h2>はじめに</h2>
<p>KINTOテクノロジーズ Osaka Tech Lab所属のひがしです。</p>
<p>2026年3月末に、年度末イベント【O-KINI FY2026】を開催しました！</p>
<p>Osaka Tech Labでは、メンバーそれぞれが異なるプロジェクトを担当することも多く、普段はなかなか横のつながりが生まれにくいこともあります。そのため、拠点全体で交流を深める文化を大切にしており、今回のイベントもその一環です。</p>
<p>2025年度の活躍をメンバー同士で労いながら、一体感をさらに高めることを目的に、有志メンバーが企画したOsaka Tech Lab初の取り組みとなりました。</p>
<p>企画チーム7名のうち5名は入社1年以内のメンバー。先輩社員2名からOsaka Tech Labの雰囲気や文化を吸収しながら、一緒にイベントを作り上げました。そんな新入り5名で、当日の様子をブログにまとめます！</p>
<h2>ノベルティ</h2>
<p><strong>執筆者：m</strong></p>
<p>イベントを行うにあたり今回様々なノベルティを用意しました！
参加してくれたみなさん全員に、Osaka Tech Labらしさをぎゅっと詰め込んだネックストラップとステッカーを用意しました。
また、受賞者の皆さんにはデスクに飾るとふと目に入るたびに「今年もがんばろう」と前向きな気持ちになれる、そんな&quot;日常の中でふりかえれる記念品&quot;になることを目指しました。</p>
<ul>
<li>全員向け<ul>
<li>ネックストラップ</li>
<li>ステッカー</li>
</ul>
</li>
<li>表彰者<ul>
<li>アクリルスタンド</li>
<li>トロフィー</li>
</ul>
</li>
</ul>
<p>これらのノベルティのデザインは、すべてOsaka Tech Lab所属のデザイナーが担当しました。
Osaka Tech Labらしさを大切にしながら、日常使いもしやすい世界観に仕上げています。</p>
<p>![](/assets/blog/authors/higashiji/20260501/image2_trophy.JPG =600x)</p>
<h2>FY2026 振り返り</h2>
<p><strong>執筆者：S.N</strong></p>
<p>イベントのトップバッターを飾ったのは、FY2026（2025年度）の拠点振り返りコンテンツです！
出来事の報告にとどまらず、メンバー個人の「色」を引き出すために事前アンケートを実施。「今年がんばった仕事」や「将来の野望」など、共に働くメンバーの意外な一面や熱い想いを共有する時間となりました。</p>
<p>もちろん拠点としてのトピックスも盛りだくさんで、オフィスの引っ越しや新たな仲間の採用、数々のイベント開催など、濃い1年を振り返りました。
笑いあり、涙あり、そして愛のある「メンバーいじり」あり。小気味良くOsaka Tech Labの1年を共有するコンテンツになりました。</p>
<p>参加者からも「プレゼンがYouTubeみたい！」「データの見せ方がおもしろい」「入社直後だが理解が深まった」などといった声をいただきました。</p>
<blockquote>
<p>スライドの一部抜粋「今年の印象に残った出来事は？」</p>
</blockquote>
<p>![](/assets/blog/authors/higashiji/20260501/image3_furikaeri1.jpeg =600x)</p>
<blockquote>
<p>スライドの一部抜粋「来年大阪でなにがしたい？」</p>
</blockquote>
<p>![](/assets/blog/authors/higashiji/20260501/image4_furikaeri2.png =600x)</p>
<h2>表彰</h2>
<p><strong>執筆者：ひがし</strong></p>
<p>続いて、表彰イベントに移りました！賞は全部で4つ設けました。</p>
<ol>
<li><strong>HONMA ARIGATO賞</strong>
Osaka Tech Labに多大な貢献をされた方へ感謝を伝える賞</li>
<li><strong>MECCHYA TECH賞</strong>
技術面で印象的な活躍や貢献をされた方へ贈る賞</li>
<li><strong>BARI NINKIMON賞</strong>
部署問わず、多くの方と積極的にコミュニケーションを取った方へ贈る賞</li>
<li><strong>O-KINI AWARD FY2026賞</strong>
&quot;めっちゃブレイクスルーするラボ&quot;・&quot;集GO!発SHIN!Co-LAB&quot;というOsaka Tech Labの共通指針・あいことばを最も体現した年間MVPへ贈る賞</li>
</ol>
<p>各受賞者は、事前に実施したアンケートでの投票数をもとに選定しました。
また、受賞者には景品として、お名前と賞名を記載したアクリルスタンドを贈呈し、そして【O-KINI AWARD FY2026賞】の受賞者にはあわせてトロフィーも贈呈しました！</p>
<p>![](/assets/blog/authors/higashiji/20260501/image5_award.jpg =600x)</p>
<p>さらに、贈呈する側のメンバーにもひと工夫を加え、&quot;その受賞者に投票したメンバーの中から1名&quot;が景品を渡す形式にすることで、「1年間の活躍をメンバー同士で労い合う」という本イベントの目的を達成することができました！</p>
<h2>ST大会</h2>
<p><strong>執筆者：さやま</strong></p>
<p>続いてはST大会を開催しました。
STは「ソニックトーク」の略で、LT（ライトニングトーク）よりもさらに短く、気軽に話してもらうことを目的とした発表形式です！</p>
<p>STには決まった運用がないため、今回は以下のルールで実施しました。</p>
<ul>
<li>発表時間は1人3分まで</li>
<li>スライド枚数は自由</li>
<li>テーマはOsaka Tech Labに関する内容なら何でもOK</li>
</ul>
<p>発表者は応募形式とし、11名の方にご応募いただきました。
最新技術の話や採用の話、個人開発の話、Osaka Tech Labにまつわる話まで、かなり幅広いテーマが集まりました。
3分という短い持ち時間での発表は今回が初めてでしたが、そのぶん一人ひとりの個性がしっかり伝わる、濃い内容になりました。
またイベント終了後のアンケートでも、「今まで知らなかった一面を知ることができてよかった」「面白かった」といった声を多数いただきました。</p>
<p>![](/assets/blog/authors/higashiji/20260501/image6_st.jpg =600x)</p>
<h2>懇親会</h2>
<p><strong>執筆者：M.K</strong></p>
<p>イベントの締めくくりは、Osaka Tech Labらしいカジュアルな懇親会でした。</p>
<p>会場は立食形式とし、「お花見」をコンセプトに飾り付けを実施。ケータリングのオードブルやアルコールを囲みながら、部署や職種を越えて交流できる時間になりました。</p>
<p>![](/assets/blog/authors/higashiji/20260501/image7_hanami.jpg =600x)</p>
<p>乾杯の挨拶は、Osaka Tech Labメンバー全員の中からルーレットでランダムに選出する方式に。結果として、最年長者が当選し、会場が笑いに包まれるスタートとなりました。</p>
<p>表彰パートとも連動し、社長の小寺が持参してくださったワインが、受賞者への特別な一杯として振る舞われました。また、ちょうど小寺のお誕生月だったこともあり、ギター演奏に合わせた「ハッピーバースデー」の合唱と、名物の豚まんをバースデーケーキに見立てたサプライズでお祝いしました。</p>
<p>![](/assets/blog/authors/higashiji/20260501/image8_cake.jpg =600x)</p>
<p>懇親会の中盤では、最近Osaka Tech Labに加入された方や、今後異動予定の方にもマイクをお渡しし、イベントや拠点に対する率直な感想を共有していただきました。新しいメンバーを自然と巻き込み、拠点全体で歓迎するOsaka Tech Labの文化が表れた時間になったと感じています。</p>
<p>最後は、小寺から本日の総括と、来年のOsaka Tech Labに期待することについて一言をいただき、一本締めならぬ「おおきに！」の掛け声でクロージングしました。</p>
<h2>最後に</h2>
<p>【O-KINI FY2026】は、年度の締めくくりとして、Osaka Tech Labのメンバーが互いの頑張りを称え合い、気持ちを1つにする大切な時間となりました。</p>
<p>また、Osaka Tech Labには「自分たちの手で楽しみを共創しよう」という文化があります。今回、企画メンバーとしてその文化を実際に経験することができました。</p>
<p>今後も、Osaka Tech Labの雰囲気や文化を大切にしながら、拠点が大きくなっても、人が増えても、その良さを保ちつつ成長していきたいと思います。</p>
<h2>📢 KINTOテクノロジーズ Osaka Tech Lab 積極採用中！</h2>
<p>最後までお読みいただき、ありがとうございました！KINTOテクノロジーズでは、Osaka Tech Labを共に創り上げ、一緒に楽しんでくれる仲間を絶賛募集しています。</p>
<p>拠点が拡大していくこのワクワクするフェーズで、あなたの力を発揮してみませんか？</p>
<p>「ちょっと面白そうかも」「まずはオフィスの雰囲気を知りたい」という方は、ぜひ一度ざっくばらんにお話ししましょう！</p>
<p>ご応募お待ちしております！</p>
<p>![](/assets/blog/authors/higashiji/20260501/image9_all.jpg =600x)</p>
<p>👇 <strong>詳細はこちらをチェック！</strong></p>
<p><a href="https://www.kinto-technologies.com/recruit/#job-list">https://www.kinto-technologies.com/recruit/#job-list</a></p>
<p><a href="https://hrmos.co/pages/kinto-technologies/jobs/1859151978603163665">https://hrmos.co/pages/kinto-technologies/jobs/1859151978603163665</a></p>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/blog/authors/higashiji/20260501/image1_cover.jpeg" length="0" type="image/jpeg"/>
        </item>
        <item>
            <title><![CDATA[2026年1月入社メンバー紹介]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-04-21-newcomer-202601/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-04-21-newcomer-202601/</guid>
            <pubDate>Thu, 30 Apr 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[2026年1月に入社した皆様に入社後の感想を伺い、まとめました。]]></description>
            <content:encoded><![CDATA[<h1>はじめに</h1>
<p>こんにちは、2026年1月入社のI.Kobayashiです！</p>
<p>本記事では、2026年1月入社のみなさまに入社直後の感想をお伺いし、まとめてみました。
KINTOテクノロジーズ（以下、KTC）に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います！</p>
<h1>YY</h1>
<p>![YYさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/yy.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>デジタル戦略部 データグロースグループでプロデューサーをしています。</li>
<li>各サービスの成長に向けて、データドリブンな意思決定を支援する施策を企画・推進しています。</li>
<li>また、社内ツールのプロダクトマネージャー（PdM）も兼任しており、社内業務効率化のためのツール開発や改善にも取り組んでいます。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>ビジネス支援を行うチームに所属しており、デジタルマーケティングに強みを持つプロデューサー、定性調査に強みを持つプロデューサー、データサイエンスに強みを持つエンジニア達などに囲まれて仕事をしています。</li>
<li>それぞれの専門性を活かしながら、チーム一丸となってサービスの成長を支えています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>制作・開発に比重の強い会社だという印象を持っていましたが、実際にはビジネス側との距離も近く、連携が密である点にギャップを感じました。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>私が所属するデータ活用チームは、複数のサービスチームと横断的に関わるため、日常的にコミュニケーションが活発です。データで支援する立場から、サービスの理想の姿やデータから見える実像について、普段の会話の中で自然に議論が交わされています。</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>スカイツリーを眺めながらランチできる休憩スペースがお気に入りです。</li>
<li>眺望の良さはもちろん、リフレッシュしやすい雰囲気があり、午後の仕事への切り替えにも役立っています。</li>
</ul>
</li>
<li><strong>satoshiさん　⇒　YYさんへの質問</strong><blockquote>
<p>普段の業務でAIってどうやって使われていますか？</p>
</blockquote>
<ul>
<li>データ分析をAIに任せて、プロジェクトの進む方向性や現在地を一緒に考えることを行っています。
壁打ち相手としても、分析担当としても利用しており、自分の役割を忘れてしまいそうになるぐらいに多用しています。
会議に向けたアジェンダ作成、それに伴うデータ分析、示唆出しまで、一言のプロンプトで完了してしまうのは革命的だと感じています。</li>
</ul>
</li>
</ul>
<h1>Mizoguchi Hiroki</h1>
<p>![Mizoguchi_Hirokiさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/mizoguchi.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>KINTOを開発するグループで新車サブスクのWebフロントエンド開発チームに所属しています</li>
<li>フロントエンドチームにいますがバックエンドもインフラも全般好きです</li>
<li>自転車に乗って走り回るのが趣味です！走り回りすぎて最近骨折しましたが、治ったら懲りずに走り回ろうと思っています</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>東京6名・大阪3名のフロントエンドエンジニア9名で構成されています</li>
<li>バックエンド・フロントエンド・PdM・QAなど職種によってチームが分かれていて、開発する機能ごとに各チームから数名集って開発を進めるような体制になっています</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>行動力がある大人が集まっているという印象でした。経験値からくる冷静さと、周りを巻き込んでやりたいこと・やるべきことを進めるアクティブさを持った人が多い印象を受けています</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>チームメンバーそれぞれで異なったタスクを進めることが多いので、主にモクモクと作業しています。協力が必要なことや相談したいことをslackや対面で声を掛けると皆さん積極的に会話に参加してくれるのでコミュニケーションは円滑です</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>とにかく開放感があって、外の景色を見渡せるところが気に入っています</li>
<li>オフィス全体が車をテーマにデザインされていて、遊び心があるところが気に入っています。イベントも頻繁に開催しているので、ぜひ覗きにきてください</li>
</ul>
</li>
<li><strong>YYさん ⇒　Mizoguchi Hirokiさんへの質問</strong><blockquote>
<p>フロントエンド開発において、AIと人とどのように作業を分担されていますか？</p>
</blockquote>
<ul>
<li>私は大枠の設計は完全に人間、業務ロジック設計やコードのレイヤー分割などはAIの提案をもとに対話して決定、具体的な実装は殆どAIに任せるなど具象度に応じてAIへの依存が高まっていくような分担になっています。
地味にUIの見た目チェックや操作時の挙動確認は具象な作業なものの、人間がポチポチ画面操作して担当しています。（なんとかAIを使って自動化できないか模索中）</li>
</ul>
</li>
</ul>
<h1>Kosuke Kihara</h1>
<p>![Kosuke_Kiharaさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/Kosuke.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>新サービス開発部 FACTORY EC開発G所属です。</li>
<li>趣味は自作キーボード・ヴィオラ・園芸、ヴィオラは市民オーケストラで演奏していました。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>新サービス開発部 FACTORY EC開発Gで、TOYOTA/LEXUS UPGRADE FACTORYのEC基盤を開発・運用しています。</li>
<li>フロントエンド、バックエンド、PdM、SRE、QA、ディレクター、マネージャーなど合わせて15名ほどの体制です。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>入社前に受けていた説明と大きく印象が異なることもなく、戸惑うことはなかったです。</li>
<li>あえて言えば、自分が勤務しているOsaka Tech Labでは特に遊び心を大事にしているところが良い意味でギャップに感じました。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>チームではバーチャルオフィスのGatherを利用しており、リモートでも気軽に相談できる空気感があります。</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>Osaka Tech Labのパークです。</li>
<li>ヨギボーを持っていってリラックスしながら仕事すると、頭が柔らかくなっていろんな発想ができる（気がする）。</li>
</ul>
</li>
<li><strong>Mizoguchi Hirokiさん ⇒　Kosuke Kiharaさんへの質問</strong><blockquote>
<p>最近AIを使ってうまくいった仕事や作業あれば教えてください！</p>
</blockquote>
<ul>
<li>JiraチケットやPRのURLから紐づくConfluence・Jira・Slack・コードを自動で追わせてまとめるスキルを作成しました。
案件の周辺コンテキストの理解にかかる時間を大幅に削減できています。</li>
</ul>
</li>
</ul>
<h1>やまそと</h1>
<p>![やまそとさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/yamasoto.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>プラットフォーム開発部Cloud Infrastructure Gのやまそとです。</li>
<li>トヨタグループへのクラウド領域の技術支援を担当しています。</li>
<li>前職まではSES/SIerでバックエンドの開発エンジニアとして働いていましたが、気づいたらインフラエンジニアになっていました。</li>
<li>プライベートではビールとバイクにハマってます！</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>大阪4名東京2名の体制です</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>コミュニケーションが活発でアクティブな人が多いなーという印象でした</li>
<li>トップダウンではなくフラットに意見を言えますし、自律的に行動する人が多いのは良いギャップでした</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>普段はみんなそれぞれの案件に携わっていますが、社内のチームミーティングはワイワイやってます</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>OsakaTechLab勤務ですが、全体的にキレイでテンションが上がります</li>
<li>駅と繋がっていて雨に濡れずに済むので助かります</li>
</ul>
</li>
<li><strong>Kosuke Kiharaさん ⇒　やまそとさんへの質問</strong><blockquote>
<p>バイクが趣味とお聞きしましたが、最近バイクで行ったおすすめの場所などあれば教えてください！</p>
</blockquote>
<ul>
<li>去年の秋頃に兵庫の須磨に行きました！海沿いを走るのは気持ちよかったです。
あったかくなってきたので淡路島か琵琶湖にいきたいですね。</li>
</ul>
</li>
</ul>
<h1>やまと</h1>
<p>![やまとさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/yamato.jpeg =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>my route開発部でAWSインフラアーキテクトとして働いています。</li>
<li>国内旅行が趣味で、アーケードゲームの全国行脚機能で43都道府県、スターバックスのアプリでは14都県巡っています。(2026年4月現在)</li>
<li>インドア趣味のほうでは某オンラインゲームの/playtimeが執筆時点で10,047時間でした。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>所属しているバックエンド開発チームでインフラを主に担当するのは私一人で、サーバサイドアプリケーションを開発する他のメンバーと密にコミュニケーションを取って仕事を進めています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>入社前の想像よりも、チームメンバーの一人ひとりが開発しているアプリケーションのことをもっとこうしたい！と考えていると感じました。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>メンバーが2人以上出社すれば一緒にランチに行って雑談をしているので、仕事の依頼や質問もしやすく過ごしやすい雰囲気だと感じています。</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>神保町オフィスは集中しやすくもあり孤独を感じるほど少なくもない、ほど良い出社率です。レストエリアがお洒落でアップルティーを取りに行くのがリフレッシュになります。</li>
</ul>
</li>
<li><strong>やまそとさん ⇒　やまとさんへの質問</strong><blockquote>
<p>おすすめの旅行先を教えてください！</p>
</blockquote>
<ul>
<li>美味しい酒・魚を求めるなら四国地方 or 日本海側、綺麗な景色を求めるなら海沿い、が良かったです！
その土地の名産であれば、味はもとよりお値段も都市圏より安くてたくさん食べられます。
ただし、食を堪能する旅には登山やハイキングも取り入れた方が、よいです（戒め）。</li>
</ul>
</li>
</ul>
<h1>I.Kobayashi</h1>
<p>![I.Kobayashiさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/Kobayashi.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>クラウドセキュリティG所属のI.Kobayashiです。</li>
<li>KTCで利用しているクラウドのセキュリティ改善や改善活動の効率よくするためのツール開発などを行っています。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>クラウドセキュリティGは現在、大阪2名、東京3名が在籍しています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>入社前にチーム状況・求められていることなど共有いただいていたのでギャップ全然ありませんでした。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>皆さん優しいので仕事がしやすいです。</li>
<li>利用したことないサービスや技術であっても一緒に調査してくれます！</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>１階コンビニ、２階レストランがあるので雨で外出たくない時によく利用しています！</li>
</ul>
</li>
<li><strong>やまとさん　⇒　I.Kobayashiさんへの質問</strong><blockquote>
<p>ご趣味は！（アウトドアでもインドアでも構いませんので！）</p>
</blockquote>
<ul>
<li>音楽・ポッドキャスト聴きながら目的もなく歩くのが好きです！</li>
</ul>
</li>
</ul>
<h1>HOKAMA</h1>
<p>![HOKAMAさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/HOKAMA.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>開発支援部企画管理Gの外間です。</li>
<li>主に会社の予算管理や業務フローの調整などを担っている部署となります。</li>
<li>休みの日は小学3年生の息子の町クラブ（サッカー）でコーチをやっています。</li>
<li>趣味はフットサルとゴルフで夏になると日焼け止めを塗っても真っ黒になります。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>企画管理Gは室町2名、大阪1名、名古屋1名です。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>ある程度のミッション内容を入社前に伺っていたので、あまりギャップは感じませんでした。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>全員中途採用なので落ち着いた雰囲気です。</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>室町7階の休憩室が気に入っています。マッサージ機もあるので体を労わりながら仕事が出来るので！</li>
</ul>
</li>
<li><strong>I.Kobayashiさん　⇒　HOKAMAさんへの質問</strong><blockquote>
<p>室町周辺でおすすめのお店教えてください！(行ってみたいお店でも大丈夫です！)</p>
</blockquote>
<ul>
<li>室町オフィスから少しあるきますが、「新日本橋中華 龍龍龍龍 TETSU」の炒飯が美味しいです。
週一回は通ってます。</li>
</ul>
</li>
</ul>
<h1>きーた</h1>
<p>![きーたさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/kiita.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>2026年1月入社のきーたです。</li>
<li>セキュリティ・プライバシー部に所属し、福岡オフィス（Fukuoka Tech Lab）で勤務しています。</li>
<li>アビスパ福岡が好きな方、お待ちしてます！</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>所属チームは3名体制です。</li>
<li>TFSグループが定める基準をベースとしたセキュリティのアセスメントを主に担当しています。</li>
<li>少人数なのでコミュニケーションも取りやすく、日々連携しながら進めています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>会社のカルチャーや雰囲気など、良い意味で入社前に抱いた印象とのギャップはありませんでした。</li>
<li>入社後のフォロー面談でも「ギャップはありましたか？」と聞かれますが、いつも「何もないですね」と答えていますｗ</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>「個々がプロフェッショナルでありつつ、しっかりチームで連携して動ける」といった印象です。</li>
<li>困ったときはすぐに相談に乗ってもらえるので助かっています。</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>立地がいいところ。あとは地下街と繋がっていたら最高でした。</li>
</ul>
</li>
<li><strong>HOKAMAさん　⇒　きーたさんへの質問</strong><blockquote>
<p>これまで仕事で一番やらかしたことはどんなことですか？（言える範囲でお願いします）</p>
</blockquote>
<ul>
<li>言えることだと…、某大手メーカーさんの重要拠点のインフラを数時間止めてしまったこと、でしょうか。
あの経験があったおかげて、作業は人一倍慎重になりました！！</li>
</ul>
</li>
</ul>
<h1>sasanoshouta</h1>
<p>![sasanoshoutaさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/sasanoshouta.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>AIファーストグループでAIエンジニアをしています、sasanoshoutaです。</li>
<li>社内外に対して生成AI活用の推進の為に折衝からPoC、実装までを幅広く行う業務に取り組んでいます。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>AIファーストグループは東京9名、名古屋3名、大阪1名の体制を敷いています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>入社間もなく、チーム内にはいい意味で上下の関係がなく、相互に取り組んでいることについて共有しながら技術的な共有や議論について交わすことができる印象を持ちました。</li>
<li>また、事前に自分への期待値や会社・チームの状況を聞いた上で役割を想像しながら入社しているので、ギャップはありませんでした。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>チーム全員が同じ取り組みをしている訳ではないですが、共通言語として「誰の為のものか」を全員が常に意識しながら目の前の事に集中して取り組んでいる雰囲気が常にあります。</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>日本橋の室町という歴史あるエリアにあるオフィスで、オフィスの内装もモダンで働きやすいですが、周辺のロケーションも気に入っています。</li>
</ul>
</li>
<li><strong>きーたさん　⇒　sasanoshoutaさんへの質問</strong><blockquote>
<p>今年のサッカーW杯で日本以外に注目している国はありますか？</p>
</blockquote>
<ul>
<li>たくさんあります。
優勝候補スペイン・フランスや、逸材を輩出し続けているアフリカ勢の国々、初参加国の中でもノルウェー・ウズベキスタン・エジプトがどこまでいくのか、数大会振り出場のチェコあたりに注目したいと思ってます！</li>
</ul>
</li>
</ul>
<h1>satoshi</h1>
<p>![satoshiさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/satoshi.webp =300x)</p>
<ul>
<li><strong>自己紹介</strong><ul>
<li>AIファーストグループの天野です！生成AIの活用推進を社内外に向けて活動しています。</li>
<li>非ソフトウェアエンジニアリング領域を中心に活動しています。</li>
<li>動画生成や記事執筆、顧客理解に対してのAI活用検証を行なっています。</li>
</ul>
</li>
<li><strong>所属チームの体制は？</strong><ul>
<li>AIファーストグループは東京9名、名古屋3名、大阪1名の体制を敷いています。</li>
</ul>
</li>
<li><strong>KTCへ入社したときの第一印象？ギャップはあった？</strong><ul>
<li>皆さん主体的に新しい事に取り組む方が多いなと感じました！</li>
<li>私もアイデアを出して試してみるのが好きなので、カルチャーに馴染みやすかったです。</li>
</ul>
</li>
<li><strong>現場の雰囲気はどんな感じ？</strong><ul>
<li>皆さん和気あいあいとした感じがありながらも、しっかりと目的感を持っている印象でした。</li>
</ul>
</li>
<li><strong>オフィスで気に入っているところ</strong><ul>
<li>オフィス周辺が綺麗なので帰宅時に優雅な感じに帰れる所です！</li>
</ul>
</li>
<li><strong>sasanoshoutaさん　⇒　satoshiさんへの質問</strong><blockquote>
<p>入社の決め手を教えてください！</p>
</blockquote>
<ul>
<li>AIの非ソフトウェア領域での活用や推進ができるポジションがあり、自分のやりたい事と重なった為です。
元々はソフトウェア領域でAIを活用していましたが、開発経験が浅く方向転換をしたかったので、私と同じような考えの方がいればAIファーストGオススメです！</li>
</ul>
</li>
</ul>
<h1>さいごに</h1>
<p>みなさま、入社後の感想を教えてくださり、ありがとうございました！</p>
<p>KINTOテクノロジーズでは日々、新たなメンバーが増えています！
今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。</p>
<p>そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています！
詳しくは<a href="https://www.kinto-technologies.com/recruit/">こちら</a>からご確認ください！</p>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/common/thumbnail_default_×2.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[『ユーザーに寄りそわNight!』── エンジニアがユーザーを知るための社内勉強会 Vol.02レポート]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-04-27-yorisowa-night-vol02/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-04-27-yorisowa-night-vol02/</guid>
            <pubDate>Mon, 27 Apr 2026 01:00:00 GMT</pubDate>
            <description><![CDATA[社内勉強会「ユーザーに寄りそわNight! Vol.02」のレポート。開発者自身がユーザーと同じ環境でプロダクトを使ってみる取り組みから見えた、日常の開発に持ち帰れる気づきをまとめました。]]></description>
            <content:encoded><![CDATA[<p>こんにちは。KINTOテクノロジーズ（KTC）でKINTOの中古車ECサイトのディレクターをしている<a href="https://x.com/momentofudayo">かーびー</a>です。KINTO Technologiesでは<a href="https://blog.kinto-technologies.com/posts/2025-12-11-userfirst2025/">「ユーザーファースト」</a>を会社の重点方針のひとつに掲げ、全社でさまざまな取り組みが進んでいます。私も自分のチームで、ユーザーインタビューの録画をみんなで見る<a href="https://blog.kinto-technologies.com/posts/2026-01-14-user-interview-waiwai-session/">「ユーザーインタビューわいわい会」</a>を試すなど、お客様の一次情報に触れる場づくりに取り組んできました。</p>
<p>こうした取り組みをきっかけに、現在はユーザーファーストを社内に広めるための活動にも運営メンバーのひとりとして関わっています。そのひとつが、今回ご紹介する社内勉強会「ユーザーに寄りそわNight! Vol.02」です。</p>
<h2>自分たちのサービスを、ユーザーが使っているところを見たことはありますか？</h2>
<p>勉強会の中で参加者にこの質問をしたところ、約7割が「ない」と回答しました。</p>
<p><img src="/assets/blog/authors/y.nakamura/user02/survey-no.jpg" alt="アンケート結果：約7割が「ない」と回答"></p>
<p>関心がないのではなく、日常の開発フローの中にその機会がない。要件をヒアリングして、仕様に落とし込んで、品質の高いものを作って届ける。エンドユーザーがどんなふうにサービスを使っているかに触れる機会は、意外と少ないのが現実です。</p>
<p>しかもKINTOテクノロジーズの場合、関わるサービスはトヨタ自動車、株式会社KINTO、開発を担う私たちなど、複数の組織で成り立っています。本来なら1社の中で完結する「作って、使ってもらって、フィードバックをもとに改良する」という流れを、組織をまたいで回していく。ここが私たちの組織ならではの難しさだなと感じています。</p>
<p>関わる人が増えるほど、それぞれの立場や見えている景色は違ってきます。だからこそ、作っている一人ひとりがユーザーの姿を知っていることが大事になる。「あのお客様、こう言っていたよね」という共通の記憶がチームにあると、議論もかみ合いやすくなります。</p>
<p>言われたものを作るだけじゃなく、自分たちから価値を届けていく。「ユーザーに寄りそわNight!」は、ユーザーを知るために踏み出した社内チームの取り組みを紹介する勉強会です。</p>
<h2>方法論の講義ではなく、隣のチームの体験を共有する場</h2>
<p>この勉強会で大事にしているのは、 <strong>「私にもできそう！」</strong> と思えることです。</p>
<p>ユーザーリサーチの手法を網羅的に学ぶ場ではなく、他のチームの取り組みを聞いて「これなら自分のチームでもできそう」と感じてもらう。そんな場でありたいと考えています。</p>
<p>toCでもtoBでも、自分たちの仕事の先には必ず使う人がいます。その誰かに寄りそっていくことが、ユーザーファーストの根っこにある考え方だと捉えています。</p>
<p>こうした考えから、勉強会では実際にユーザーと向き合う取り組みをしたチームに登壇してもらい、何をやって、何に気づいたかを共有してもらう形式にしています。専門的な方法論の紹介ではなく、隣のチームの体験を聞くこと。そこから自分のチームでも試してみたいと思える、小さなきっかけが生まれる場になればと思っています。</p>
<h2>ユーザーに寄りそわNight! Vol.02：ユーザーと同じ環境で、プロダクトを使ってみる</h2>
<p>2026年3月に開催された第2回の勉強会では、実際にユーザーが使っているのと同じような環境で、自分たちもプロダクトをテストしてみるーーそんな取り組みをしているチームに登壇してもらいました。ユーザーファーストの取り組みとして、社内の各所で生まれている実践をキャッチして勉強会に繋げていく中で、この取り組みのことを知り、声をかけたのが始まりでした。</p>
<p>トヨタグループには「現地現物」——実際の現場に足を運び、自分の目で見て判断する——という考え方があります。登壇してくれたチームはこの考え方をユーザー理解にも活かしたいと、開発メンバー自身がユーザーと同じ状況に身を置いてプロダクトを使ってみる、という取り組みに挑戦していました。</p>
<h3>机の前の3秒、現場の3秒</h3>
<p>登壇でとくに印象に残ったのは、開発環境ではわからなかったことが、ユーザーと同じ状況で使ってみると次々に見えてきたという話でした。</p>
<p>たとえばアプリの表示にかかる時間。開発環境で3秒かかっても「ちょっと遅いな」と感じる程度だけれど、ユーザーが実際に使う状況で体験する3秒はまるで別物。急いでいるとき、周りに人がいるとき、落ち着いて待てないとき。クーラーの効いたオフィスで感じる3秒と、現場で感じる3秒は、同じ時間とは思えないくらい違って感じられた、と。</p>
<p>「仕様通りに動く」はずのものが、ユーザーと同じ状況に置かれるとまったく違う顔を見せる。データでは見えない課題が、身体で感じられる瞬間でした。</p>
<h3>「忖度を捨てる」という第一歩</h3>
<p>では、現場で気づいたことをどう日常の開発に持ち帰っていくか。パネルディスカッションで印象に残ったのは、「忖度を捨てる」という言葉でした。</p>
<blockquote>
<p>「アプリを使っていて『ここ遅いな』と思っても、『APIをたくさん呼んでるからしょうがないか』と開発者としての忖度をしてしまう。その忖度をあえて捨てて、純粋にユーザーとしてアプリを使ってみることが、まずできる第一歩」</p>
</blockquote>
<p>開発者として「これはしょうがないか」と自分で飲み込んでしまう場面は、きっと多くの人に心当たりがあると思います。その忖度を一度横に置いて、純粋にユーザーとしてアプリを触ってみる。大がかりな準備をしなくても、今日から始められる小さな一歩として、とても印象に残った言葉でした。</p>
<h2>これからも、小さな一歩を重ねていく</h2>
<p>Vol.02の懇親会では、「うちのチームでもこういうことをやってみたい、でもどう始めればいいんだろう？」という声や、登壇者を囲んで「どうやって社内を巻き込んでいったんですか？」と具体的な進め方を聞く姿が、あちこちで見られました。</p>
<p>アンケートのフリーコメント欄には、約半数の方が「これから自分のチームでやってみたいこと」を書き込んでくれました。印象的だったのは、toCのサービスを作っているチームだけでなく、業務システムやプラットフォームを担当する方々からも、具体的な一歩の言葉が並んだことです。</p>
<blockquote>
<p>「業務システムなのでユーザーがKINTO社員であり距離が近い。実際に業務をやらせてもらったり、フィードバックを貯める場を作ったりして、ユーザーファーストを実践する場を作りたい」</p>
</blockquote>
<blockquote>
<p>「忖度せずに改善アイデアを出し、検討する。アイデアを歓迎する空気を作っていきたい」</p>
</blockquote>
<p>自分たちの仕事の先にいる「使う人」は、toCのお客様だけではありません。社内の誰か、パートナー企業の誰か、ときには自分自身かもしれない。それぞれの現場で、それぞれの「寄り添い方」がある。そのことを、登壇してくれたチームの話と、参加者の声から改めて感じた回でした。</p>
<p>Vol.01の開催から半年、社内Slackチャンネルのメンバーは60人から99人に増え、「うちでもこういうことやってるよ！」と声をかけてくれる人も出てきています。これまで各チームの中に閉じていた取り組みが、少しずつ表に出てくるようになりました。</p>
<p>「ユーザーファースト」は2025年の注力テーマとして始まりましたが、ユーザーのことを考えるのはプロダクト開発の基礎の基礎。一年限りのテーマで終わらせず、Vol.03に向けた準備も進行中です。</p>
<p>大がかりな取り組みでなくても、まずは自分のプロダクトをユーザーとして使ってみることから。気づいたことを隣の人に話してみることから。一つひとつのチームで生まれる小さな一歩を、勉強会という場で共有し、また次の一歩へつなげていく。この取り組みの火を絶やさないよう、これからも続けていきます。</p>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/blog/authors/y.nakamura/user02/cover.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[「AI-Native Dev」始めました ─ 全社横断で挑むAIネイティブな文化づくり]]></title>
            <link>https://blog.kinto-technologies.com/posts/2026-04-21-introducing-ai-native-dev/</link>
            <guid>https://blog.kinto-technologies.com/posts/2026-04-21-introducing-ai-native-dev/</guid>
            <pubDate>Tue, 21 Apr 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[2026年2月に発足した全社横断プロジェクト「AI-Native Dev」の活動内容を紹介します。文化醸成と開発環境整備の2本柱で、AIネイティブな開発文化の定着を目指しています。]]></description>
            <content:encoded><![CDATA[<p>こんにちは、サイバーセキュリティと生成AI活用推進を担当しているたなちゅーです。この記事では、2026年2月に活動を開始したAI-Native Devプロジェクトについて紹介します。</p>
<h2><strong>活動の背景</strong></h2>
<h3><strong>2025年までの取り組み</strong></h3>
<p>KTCでは2024年の<a href="https://blog.kinto-technologies.com/posts/2024-01-26-GenerativeAIDevelopProject/">生成AI活用プロジェクト</a>を皮切りに、2025年は「AIファースト」「リリースファースト」を掲げ、AI活用は着実に進みました。</p>
<ul>
<li><strong>AIファースト</strong>：すべてのプロダクトへのAI統合、AIプロダクトの開発推進、グループ内でのAI活用ドライバー</li>
<li><strong>リリースファースト</strong>：「いかに速く届けるか」を文化として組織に定着させる</li>
</ul>
<h3><strong>「AIを使う」から「AIネイティブな開発・業務プロセス」へ</strong></h3>
<p>こうした取り組みを経て、昨年末に副社長の景山がテックブログ「<a href="https://blog.kinto-technologies.com/posts/2025-12-25-LookBack2025/">2025年の振り返りと2026年の展望：Agenticな未来へ</a>」で、2026年のキーワードとして「Agentファースト」と「AIエンジニアリングファースト（AI-Native Dev）」を掲げました。</p>
<ul>
<li><strong>Agentファースト</strong>：「対話するAI」から「行動するAI」へ。AIが自律的にタスクを遂行する世界を全社で実現する</li>
<li><strong>AIエンジニアリングファースト（AI-Native Dev）</strong>：AIネイティブな視点で開発・業務プロセスを再構築し、職種の壁を超える</li>
</ul>
<p>目指すのは、<strong>一人ひとりがAIネイティブな視点で開発や業務のプロセスを変えていくこと</strong>。その推進役として、2026年2月にAI-Native Devプロジェクトが発足。プロダクト開発からクラウドインフラ、コーポレート部門まで、10名超が合流した横断チームで活動を開始しています。</p>
<h2><strong>活動の2つの柱</strong></h2>
<p>個人の知見を組織全体で活かす仕組みと、それを支える開発環境の整備。この2つが揃って初めて組織として加速できると考え、活動を <strong>文化醸成</strong> と <strong>開発環境整備</strong> の2本立てで構成しています。</p>
<ul>
<li><strong>文化醸成</strong>：ナレッジの体系化・共有、AIツール利用状況の可視化、社内事例の発信など</li>
<li><strong>開発環境整備</strong>：AI時代のコードレビュー最適化、AI Agent / MCP基盤の整備、エンバイロメント（環境）エンジニアリングなど</li>
</ul>
<p><img src="/assets/blog/authors/tanachu/2026-04-21-introducing-ai-native-dev/01_ai-native-dev.png" alt="AI-Native Devの2本柱：文化醸成と開発環境整備"></p>
<h2><strong>Phase 1：まず土台をつくる</strong></h2>
<p>Phase 1として取り組んだのは、活動の土台となる2つの基盤です。</p>
<h3><strong>AI Native Hub</strong></h3>
<p>1つ目は、社内Wiki上に開設した生成AI活用の社内ポータル「AI Native Hub」です。</p>
<p><img src="/assets/blog/authors/tanachu/2026-04-21-introducing-ai-native-dev/02_ai-native-hub.png" alt="AI Native Hub"></p>
<p>職種別のAIツール活用ガイド、MCP・Skillsの使い方、社内事例などの情報を集約しています。また、コンテンツの運用にはGitHubを採用しています。Markdownで記述し、PRでレビューを回し、mainブランチにマージされると社内Wikiへ自動同期される仕組みです。運営チームだけが管理するのではなく誰でもナレッジを共有できる、社内全体で育てていくナレッジ集約場所を目指しています。</p>
<h3><strong>Claude Code Dashboard</strong></h3>
<p>2つ目は、Claude Codeの利用状況を可視化するダッシュボードです。Claude CodeのOpenTelemetryを活用しています。</p>
<p><img src="/assets/blog/authors/tanachu/2026-04-21-introducing-ai-native-dev/03_claude-code-dashboard.png" alt="Claude Code Dashboard"></p>
<p>ダッシュボードでは、MCPやSkillsの使用回数、利用者のトークン使用量、トークン使用量上位者のトレンドが見えます。自分の活用状況の振り返りやトークン使用量上位者との交流など、AIツール利用促進のきっかけになればと考えています。</p>
<h2><strong>Phase 2：実践と拡張</strong></h2>
<p>Phase 1は立ち上げと基盤整備。4月からのPhase 2は、その基盤の上で実践を加速するフェーズです。</p>
<h3><strong>文化醸成</strong></h3>
<p>文化醸成が目指すのは、AIネイティブな開発・業務のスタイルが組織に根づくことです。</p>
<ul>
<li><strong>もくもく会・ハンズオン会</strong>：気軽に情報交換できるオンラインの場を定期開催し、実践知を共有する</li>
<li><strong>AIネイティブな個人・部署へのインタビューとナレッジの横展開</strong>：先行事例を掘り起こし、他チームへ広げる</li>
<li><strong>AIネイティブな活動の可視化</strong>：AIネイティブ度合いを可視化し、活動の推進に活かす</li>
</ul>
<p>まず動き出したのが「もくもく会」です。週2回オンラインで開催して、ちょっとした困りごとやTipsなどを話しています。また、テーマを決めたハンズオン会も実施しており、初回の「Claude Codeを使い倒す設定を一緒にしよう会」には合計80名以上が参加しました。学びは集約して、後から参照できる形にしています。</p>
<h3><strong>開発環境整備</strong></h3>
<p>開発環境整備が目指すのは、AIエージェントを前提とした開発基盤を整えることです。</p>
<ul>
<li><strong>AI Agent / MCP基盤の整備</strong>：AI AgentやMCPの共有基盤の整備を進め、誰でも見つけて使える状態を目指します。</li>
<li><strong>AI時代に合わせたコードレビューの最適化</strong>：AIが生成したコードに対するレビュー観点や静的解析との連携など、AI前提のレビューフローを検討しています。</li>
<li><strong>エンバイロメント（環境）エンジニアリング</strong>：AIエージェントが安全に活動できる範囲の境界線設計やガードレールなどの整備に取り組んでいきます。</li>
</ul>
<p>既に社内ではエージェント開発・共有基盤「KTC Agent Store」を運用しており、現在は実行基盤をBedrock AgentCoreへの移行を進めています。AIエージェントとしてはAIインタビューという深堀りインタビューエージェントなどの開発が進行中です。</p>
<p><img src="/assets/blog/authors/tanachu/2026-04-21-introducing-ai-native-dev/04_ai_interview.png" alt="AI Interview"></p>
<h2><strong>ここまでの活動で感じたこと</strong></h2>
<p>一番の発見は、AIネイティブな働き方に既に踏み出しているメンバーの多さです。初回ハンズオン会には80名以上が参加し、チャットではおすすめ設定や活用Tipsが飛び交いました。この熱量をつなげれば、もっと大きな力になる。その点と点をつなぐことがAI-Native Devの役割だと改めて感じています。</p>
<p>また、AIネイティブな開発・業務スタイルが根づけば、日々の業務から生まれた余力が新たな価値創出へ向かう流れをつくれるはずです。「攻めのAI活用」と「守りの安全基盤」の両面をつなぎながら、その流れを組織全体で加速させていきます。</p>
<h2><strong>おわりに</strong></h2>
<p>AI-Native Devは始まったばかりです。土台を作るフェーズから、土台の上で走るフェーズへ。活動の進捗やナレッジは引き続きテックブログで発信していきます。</p>
<p>最後まで読んでいただきありがとうございました！</p>
]]></content:encoded>
            <enclosure url="https://blog.kinto-technologies.com/assets/blog/authors/tanachu/2026-04-21-introducing-ai-native-dev/coverimage.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>