KINTOテクノロジーズのブログ - TECH PLAY

TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

991

はじめに こんにちは、2026年4月入社の山本です! 本記事では、2026年4月入社メンバーに入社直後の感想をお伺いし、まとめてみました。 前の方からの質問に次の方が答えるリレー形式でお届けします。 KINTO テクノロジーズ(以下、KTC)に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! もっさん ![もっさんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/mossan.png =300x) 自己紹介 KINTO開発部 開発推進Gに所属しています。 案件全体の管理や、AIを活用したサイト構築のプロジェクトを担当しています。 スポーツ観戦が趣味で、最近は地元のBリーグクラブを応援しています。せっかくなのでKINTOと縁のあるアルバルク東京もこれから推していきたいです! 所属チームの体制は? 開発推進Gは8名体制で、KINTOサービスに関わる開発案件のプロジェクトマネジメントを主に行っています。 職種はPMが中心で、事業部側のメンバーや開発メンバーと連携しながら、ものづくりを進めています。 KTCへ入社したときの第一印象は?ギャップはあった? 前職の同僚が在籍していたこともあり、入社前から気軽に質問でき、カジュアル面談でも疑問をその場で解消できたので、不安なく入社できました。 入社後はフットサルや筋トレなど、いろいろな活動を楽しんでいるアクティブな方が多く、いい意味でのギャップを感じました。 エンジニアの勉強会など学びや発信の場が多いのも印象的で、自分の経験がない分野でも興味があれば気軽に参加させてもらっています。 現場の雰囲気はどんな感じ? 休憩室では豆を挽いてコーヒーを淹れている方がいて、香りにつられてふらふらと混ざりに行きました。とてもウェルカムな雰囲気で、業務でまだ関わりのない方ともゆるく繋がれるのがありがたいです。 全員が中途採用ということもあり各分野のプロフェッショナルが揃っていて、日々の会話から多くの刺激をもらっています。 オフィスで気に入っているところ 室町オフィス勤務です。 オフィスから日本橋をわたって八重洲・銀座あたりを歩くのがお気に入りで、街のにぎわいを感じながらの散歩がいい気分転換になっています。 寿司と紅茶さん ⇒ もっさんへの質問 生まれ変わったらなりたい職業とその理由を教えてください 料理人やパティシエですね。今はチームでものづくりをしていますが、生まれ変わったら一人で黙々と何かを突き詰めてみたいです! つじたく ![つじたくさんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/tsujitaku.jpeg =300x) 自己紹介 新サービス開発部 FACTORY EC開発G所属です。 趣味は筋トレです。体を大きくしたいです。 所属チームの体制は? 新サービス開発部 FACTORY EC開発Gで、TOYOTA/LEXUS UPGRADE FACTORYのEC基盤を開発・運用しています。 フロントエンド、バックエンド、PdM、SRE、QA、ディレクター、マネージャーなど合わせて15名ほどの体制です。 KTCへ入社したときの第一印象は?ギャップはあった? 入社前の面接の時も色々話をしていたので大きなギャップはなかったです! 現場の雰囲気はどんな感じ? フロントエンド、バックエンド、PdMなど各方面のメンバーが揃っているので提案→実行までが素早くできて働きやすいです。 オフィスで気に入っているところ 駅から近く、改札出てから5分もたたずに自席に座れるところ。 オフィスが綺麗なところ。 もっさん ⇒ つじたくさんへの質問 TOYOTA UPGRADE FACTORY 担当されていますが、こんなアップグレードあったらいいなと思うのはどんなものですか? 自分の運転スタイルを学習して、燃費・快適性のバランスを自動最適化するAIモードみたいなアップグレードがあったらいいなと思います。 山本雄太 ![山本さんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/yuta_yamamoto.png =300x) 自己紹介 新サービス開発部 プロジェクト推進グループ所属の山本雄太です。ロールはPjMです。 仕事は寄り添い伴走するようなスタイルが性に合っており、誰かの為にだとより力が出るタイプ。 趣味はゴルフ・釣り・サウナ・キャンプなどアクティブ系広め、お酒も大好きです。最近はゴルフにハマってます。やっと100を切りはじめました! 所属チームの体制は? プロジェクト推進グループは10名にも満たない小規模体制ですが、トヨタグループ各社に対してKTCの技術力を活かして支援を主導するグループでもあり、やりがいは大きいです。 最先端な取り組みにも関与できるチャンスがあるので、KTCの中でも一番面白いグループなのではとも思っています。 KTCへ入社したときの第一印象は?ギャップはあった? 第一印象は…オフィスがどの拠点も立派!笑 ギャップは想像以上にスキルフルなメンバーが多い点です。 現場の雰囲気はどんな感じ? 少数チームという性質もあり、各自別のプロジェクトで作業しており実質的な関わりは少ないです。 ただ、定例MTGは情報交換が活発に行われており、相互に助言できる程よい雰囲気に感じます。 オフィスで気に入っているところ 私はFukuoka Tech Lab所属で、大名のリッツ・カールトンの真下にオフィスがあり景色が良い!博多湾を一望できて、天気が良い日には志賀島や能古島も綺麗に見える窓際がお気に入り、良いリフレッシュゾーンになっています。 つじたくさん ⇒ 山本さんへの質問 おすすめの日本酒教えてください! 福岡は酒蔵が意外と多くて迷いますが、寒北斗をおすすめします! 北斗七星をモチーフにしたパッケージやお猪口もあったり、見た目から愛せます。 寿司と紅茶 ![寿司と紅茶さんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/sushitokocha.jpeg =300x) 自己紹介 デジタル戦略部データグロースGでマネージャーをしています。 Webエンジニア → EM・PdM → 事業責任者 → VPoE といったキャリアを歩んでいます。 所属チームの体制は? 1チームに機能横断している方々が揃っているのが特徴です。 PdM、エンジニア、デザイナーといったプロダクトトリオが揃っているため、多角的な視点から機能改善や案件進行ができるのが強みです。 KTCへ入社したときの第一印象は?ギャップはあった? リファラルなので事前に質問できていた点とカジュアル面談も複数回実施させていただいたため、ギャップはありませんでした。 現場の雰囲気はどんな感じ? 比較的スタートアップのようにワイワイやっている雰囲気が強いですね。 会社規模で考えると総合的に珍しいかもしれません。 オフィスで気に入っているところ フリードリンクが用意されているところ、宅配弁当が頼めるところですね。 山本さん ⇒ 寿司と紅茶さんへの質問 これまでで一番歯ごたえのあった仕事はどんな仕事でしたか? ゼロから開発組織を作ることになり、未経験から採用要件、スカウト業務、AG対応、カジュアル面談、面接、オファー対応などすべてやってました。 さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
こんにちは! Principal Generative AI Engineerの森田です。私の所属するAIファーストGでは、社内の生成AI活用にとどまらず、販売店やトヨタグループにおけるAI活用支援を行っております。 社内でAIエージェントの活用が進む中、ある部署から「AIエージェント開発に取り組みたいが、どこから始めていいかわからない」と相談をもらいました。座学だけでは手触り感が得られないので、実際に手を動かすワークショップ形式で開催することにしました。 弊社はAWSを主なクラウド基盤としているため、AWSが提供するAIエージェントの構築・運用基盤である Amazon Bedrock AgentCore と、エージェントのツール連携プロトコルである MCP(Model Context Protocol) を中心テーマに据えました。 教材として使用した書籍 ゼロから資料を作るよりも、体系的にまとまった書籍のハンズオンをベースにした方が、参加者が後から復習しやすいと考えました。今回は以下の2冊を使用しています。 『 Amazon Bedrock AgentCore 実践入門 ── Strands Agentsで構築するAIエージェント 』 (SBクリエイティブ) ※以下、AgentCore本 『 AWSではじめるMCP実践ガイド ── 基礎からAIエージェント構築まで徹底解説 』 (技術評論社) ※以下、MCP本 AgentCore本でエージェントフレームワークであるStrands AgentsやAgentCoreの各機能を学び、MCP本でMCPサーバーの自作とAgentCore Gatewayによるツール統合を学ぶ、という組み合わせです。 実は、どちらも私が共著者として執筆に携わった書籍です。手前味噌で恐縮なのですが、中身を隅々まで把握できている分、参加者がハンズオンで詰まったときにも補足しやすく、教材として選びました。 開発環境 今回は環境を揃えるため、 GitHub Codespaces 上に開発環境を用意しました。書籍の手順をベースに、以下の工夫で環境構築にかかる時間を短縮しています。 ツール管理は mise に統一し、 mise use aws-cli node uv claude でAWS CLI・Node.js・uv・Claude Codeを一括導入 AWSへは aws configure sso でSSOログイン コーディングのお供として Claude Code をセットアップ サンプルコードは書籍著者が公開しているGitHubリポジトリを git clone して参照できるようにし、すべてのコードを手打ちしなくていいようにしました。 ワークショップの構成 1日のワークショップとして、午前・午後に分けて以下の流れで進めました。 午前の部(1.5時間)・・・Strands Agentsに触れる # タイトル 書籍・該当箇所 概要 1 Strands Agents入門 AgentCore本 3.4節 ツール定義、エージェント作成、会話ループの実装など基本を体験 2 リサーチエージェントの構築 AgentCore本 4章 Webから情報収集・要約するエージェントを構築し、ツール連携の開発フローを掴む 任意 AgentCoreハーネス AgentCore本 5.2節 時間に余裕がある人向けに、AgentCoreハーネスを体験 午後の部(3時間)・・・AgentCoreとMCPを実践する # タイトル 書籍・該当箇所 概要 3 AgentCoreランタイム入門 AgentCore本 5.3節 agentcore create → agentcore deploy でエージェントをクラウドにデプロイ 4 アンビエントエージェントの構築 AgentCore本 15章 S3への領収書アップロードをトリガーに、解析→Confluence記録→メール通知するイベント駆動型エージェントをCDKで構築 5 MCPサーバーの構築 MCP本 4.2節 自前のMCPサーバーとホストを実装 6 AgentCore Gateway MCP本 6.2節 MCPサーバーをAgentCore Gatewayに登録し、認証・認可を統合 社内実施にあたって変更が必要だったポイント 書籍のハンズオンをそのまま社内で実施するにあたり、いくつか環境差異への対応が必要でした。 :::message メールアドレスのエイリアス 15章のアンビエントエージェントでは、サンプルデータ内の複数ユーザーのメールアドレスにそれぞれ通知を送る設計になっています。しかし社内のメール環境ではエイリアスが使えなかったため、 data/users.json のメールアドレスをすべて自分のアドレスに統一しました。その結果、同じアドレスでSNSサブスクリプションが重複してしまうため、 chapter15/cdk/lib/expense-agent-stack.ts で重複を排除するよう修正しました。 + const uniqueEmails = [...new Set(emailAddresses)]; - emailAddresses.forEach((email, i) => { + uniqueEmails.forEach((email, i) => { // 各アドレスごとに SNS サブスクリプションを作成 }); ::: :::message Googleカレンダー連携が使えない環境 13章のフルスタックエージェント構築ハンズオンはGoogleカレンダー連携(Google認証)を前提としているため、社内環境では実施が難しく割愛しました。興味がある方には自宅での実施を案内しています。 ::: :::message AWSアカウントの共有 複数人で1つのAWS環境を使ったため、リソース名が他の人と重複・混同しないよう、各自の名前を付与するルールにしました。特にS3バケット名はアカウント内で一意である必要があり、そのままだと衝突してしまいます。CDKスタックにハードコードされたバケット名( expense-agent-${account} )に自分の名前を付けて回避しました。AgentCoreランタイムも、 agentcore create で付ける名前を handsonmorita のように各自で区別できるものにしています。 ::: :::message alert リージョンの差異 AgentCore本とMCP本で、使用リージョンが異なっているため、単一リージョンで実施するには読み替えが必要でした。GitHubで公開されているサンプルソースを利用して進めていたのですが、どこを修正すればよいかが見落としやすいポイントでした。 ::: 参加者からもらった質問 ワークショップ中、参加者からいくつか印象的な質問がありました。 Q. ツールの定義はPythonでもできるのか? Strands Agentsのツール定義はPythonのデコレータ @tool で行えます。関数のdocstringがそのままツールの説明文(description)としてLLMに渡されるため、Pythonで完結します。MCPサーバーとして公開する場合もPython SDKが利用可能です。 Q. Swarmエージェントはどうやってタスクを振り分けているのか? マルチエージェント構成(Swarm)では、オーケストレーターとなるエージェントがLLMの判断に基づいて、各サブエージェントにタスクをルーティングします。各エージェントのnameやdescriptionに基づき、他のエージェントの役割を把握したうえで、作業を委譲するエージェントを選択します。 Q. マルチエージェントにするか、シングルエージェントにするか、どのように決めるのが良いか? 個人的な感覚としては、最初から厳密に切り分けようとしない方がうまくいきます。シングルエージェントとして作り始めて、複雑なタスクをうまく処理できなくなったり、コンテキストを分けた方が動きが安定すると感じた時点で、サブエージェントへの分割を考える、というのが実際の進め方に近いです。 Q. エージェントで使用するモデルはどのような基準で選んでいるのか? コストを最初から意識しすぎないことが大事だと思っています。Sonnetをベースにまず作ってみて、判断の精度が足りない部分はOpusに切り替え、単純作業の部分だけHaikuでコストを下げる、という順番です。先にHaikuでコストを抑えようとすると、実現できる機能のレベルが下がってしまうので、まずは「エージェントとして実現できるか」を確認してから、コスト最適化に移る方がスムーズに進みます。 参加者の声 ワークショップの締めに参加者で「振り返り会」を行い、そこで出た声の中から印象的だったものをいくつか紹介します。 ある参加者は、「エージェントの動きが、これまでふんわりとマジックのように感じていたのに、こういう仕組みで動いていたのかと腹落ちした」と話してくれました。難しさの本質はプロンプトの作り込みにある、という気づきを得られたようです。 別の参加者は、ハーネスの手軽さに驚いたそうです。テンプレートに「あなたはプロ野球に詳しい人です」のような一行を書くだけでエージェントが動いてしまうのを見て、拍子抜けするほど単純だったと話していました。 MCPサーバーの自作に取り組んだ参加者は、午後の中でも特に難しかったと振り返っていました。それでも自分の手で実装したことで、普段使っているMCPの裏側の動きまで想像できるようになった、という言葉が印象的でした。 これまで固定パイプライン的な実装(情報を取得し、LLMに渡し、整形して出力する)に慣れていたという参加者は、エージェントが入力に応じて処理のルートを動的に決めていく発想自体が新鮮だったと語っていました。同じ入力でも出力のパスが事前に決まっていない、というところに面白さを感じたそうです。 ワークショップを終えて 1日にかなりの内容を詰め込んだこともあり、進み具合には個人差が出ました。午前の任意項目だったAgentCoreハーネス(5.2節)まで到達できた方もいれば、そこまで手が回らなかった方もいます。午後はそれぞれの興味に合わせて、アンビエントエージェントに取り組むグループとMCPサーバー構築に取り組むグループに分かれて進めてもらいました。 今回のワークショップを通して、書籍のハンズオンをベースにしたことで準備の負担を抑えつつ、実践的な内容を届けられたのは良かったです。本記事が、これからAIエージェント開発に取り組む方や、同様のワークショップを企画する方の参考になれば幸いです。最後までお読みいただき、ありがとうございました。
こんにちは! KINTOテクノロジーズ(以下、KTC)のAIファーストグループで、生成AIの活用推進を担当している和田です。 先日、JDLA(日本ディープラーニング協会)の資格合格者コミュニティ「CDLE」の業種別勉強会にお招きいただき、「個人の発見を、組織の知恵に」というテーマで登壇してきました。本記事は、その内容を再構成したものです。 https://jdla.connpass.com/event/393970/ 1. はじめに KTCはトヨタ自動車のグループ会社で、クルマのサブスク「KINTO」をテックの力で支える内製開発の会社です。 2023年の春、GPT-4とAPI版が出てきたタイミングで内製の生成AIチャットを立ち上げて以来、3年以上にわたって生成AIの活用推進を続けてきました。最近ではKTC/KINTOで培った技術力を、トヨタグループの各社へとアドバイジングや開発支援を通じた提供もしています。 その立場から国内の状況を眺めると、対照的な数字があります。生成AIを「導入済み」と回答した国内企業は57.7%^[ NRI「IT活用実態調査(2025年)」 ]。導入の壁は、もうほとんど越えられています。一方で、AIが利益(EBIT)に5%以上効いていて、かつ大きな価値を生んでいると答えられる企業は6%^[ McKinsey "The State of AI in 2025" ]。導入はしたが、成果を出していると言い切れる会社はまだ一握りです。 おそらくこの記事を読んでいるような、新しい技術への感度が高い方は、すでに仕事が大きく変わっているはずです。メールの下書き、議事録の要約、コーディング支援。少なくとも、これらを全てカタカタ手で打っている人は、かなり減っているのではないでしょうか。しかし問題はその先です。あなたの「周りの人」はどうでしょうか。個人としては価値が出ている。でも、組織としてはどうでしょう。今回のテーマは、個人の成果と組織の成果の間にある、この溝についてです。 2. キャズムのどこに手を打つか ― 今日は「B」の話 本題に入る前に、組織へ新しいものを広げるときのイメージを共有させてください。何度も見たであろう、キャズム理論^[ジェフリー・ムーアが提唱した、新技術の普及プロセスを説明する理論。利用者をイノベーター/アーリーアダプター/アーリーマジョリティなどの層に分け、層の間にある溝(キャズム)を越えることの難しさを論じたものです。]の図です。 新しい技術や文化を組織へ広げるときの手法。A・B・Cのどこに手を打つか これは生成AIの話だから持ち出した図ではありません。DXのときも、RPAのときも、新しい技術や文化を大きな組織に入れる場面では、いつもこの概念を使ってきました。 図にはA・B・Cという3つの矢印を描いています。それぞれが別の打ち手です。みなさんの組織は、いまどの矢印に手を打っているでしょうか。そしてご自身はどの層にいて、どの矢印ならコミットできそうでしょうか。そんなことを考えながら見てもらえればと思います。 A:イノベーターに自由を渡す。 新しいものは、放っておいても勝手に調べ、勝手に始め、勝手に実験してしまう人たちがいます。彼らにできる限り自由な環境を渡す。ただ自由なだけでなく、ガードレールを敷いて「ここでなら安心して遊んでいい」という空間にするのがAです。 B:キャズムに橋をかける。 イノベーターやアーリーアダプターが見つけた価値に、アーリーマジョリティ以降の人たちが追従できるよう、崖になっているキャズムへ橋をかける取り組みです。 C:後ろ向きな層を動かす。 配ってもなかなか触ってくれない、興味を持ってもらいにくい層に、どう使ってもらうか。ここに悩んでいる会社さんは、きっと多いはずです。 今日お話しするのは、このうちBが中心です。Bをやるには、その手前でAも回っている必要があるのですが、本記事では「見つかった価値を、キャズムの向こう側へどう渡すか」に軸足を置きます。 3. 価値創出の両輪 ― 探索と実装 組織で価値を出すには、2つの循環が要る、と私は考えています。 1つは探索。イノベーターやアーリーアダプターにあたる人たちが自由に価値を探せる環境を用意し、「この使い方は効く」という発見を生んでもらうフェーズです。もう1つは実装。見つかった0→1の発見を、組織に固定して10にも100にもするフェーズです。藪まみれの中をしらみつぶしに歩いてゴールを見つけるのが探索だとすれば、見つかった道を舗装して誰でも歩けるようにするのが実装です。 探索だけでは、個人の発見で止まります。IRレポートに載るようなインパクトは、個人技からは出ません。逆に、発見のない組織でいきなり実装(仕組み化)から入ると、舗装すべき道がどこにあるのか分からないまま工事が始まります。これらは両輪であってどちらが欠けても前進することはできません。 ここからは、KTCがこの両輪をどう回しているか、探索→実装の順でお話しします。 4. 探索:トークンマキシング ― 自転車の乗り方は、本では学べない 探索の打ち手の一つとして「トークンマキシング(Tokenmaxxing)」^[あるものを極限まで盛るというネットスラング "-maxxing" を、トークン消費にくっつけた言葉です。]を紹介します。社内のAIのトークン消費を、とにかく最大化する。価値創出はいったん脇に置いて、まず使う量を増やす施策群のことです。 なぜ価値創出にこだわると言っておきながら、消費量に注目するのでしょう。生成AIの正しい使い方は、机上で学べないからです。私はよく自転車の乗り方に例えるのですが、自転車の乗り方を本で学んだ人は、おそらくいません。補助輪をつけて、サポーターをつけて、河原で何度も転んで、身体で覚えたはずです。「AIにこう頼むとうまくいく」「これはAIには苦手だ」という感覚も同じで、試行錯誤からしか生まれません。だから、まずたくさん漕いで、たくさん転ぶことで、AIと効率よく協業する感覚が磨かれます。 トークンマキシングを構成する施策は、「消費を増やす施策」と「消費量を観測する施策」で構成されます。 消費を増やす施策の例としては「手作業コーディング禁止」があります。一定期間、人手でのコーディングを禁止して実装はAIエージェントに任せ、人間は指示と検証に集中する、というものです。コーディング界隈で広まった施策ですが、「手作業での資料作成禁止」のように事務系へのアレンジも利きます。 私自身は資料作成へのこだわりが強く、今でもつい手作業で熱中してしまう時があるのですが、最初の1割は人間が作り、そこから7〜8割まではAIに持っていかせて、最後にまた、人間のこだわりを入れるようにしています。何度も失敗しながら最近ようやくちょうど良いAIとの協業感覚を掴めてきています。 KINTOテクノロジーズで実施した手作業コーディングを禁止する施策「Vibe Coding Week」については、Findyさんによるインタビューブログでも取り上げていただきました。 https://jp.findy-team.io/blog/ai-casestudy/kintotechnologies_vibecodingweek/ もう1つの要素が観測です。増やしっぱなしではコストが爆発するので、誰が・どれだけ使っているかを可視化する。この文脈で有名になったのがMetaの「Claudeonomics」で、8.5万人超の従業員がトークン消費量でランク付けされ、上位250名にはRPG風の称号が与えられていたそうです^[ Fortune「A Meta employee created a dashboard so coworkers can compete to be the company's No. 1 AI token user」(2026/04/09) ]。KTCでも、Claude Codeのメトリクスを各ユーザーから収集し、個人と組織それぞれの使い方を分析する仕組みを動かしています。ツールは配ったけれどその後を見ていない、という組織は、まずここから始めるのを勧めます。 KTCで運用しているClaude Codeメトリクスのダッシュボード(数値はダミーデータ) 5. ただし、トークン消費はハック可能 ・・・ただしトークン消費量は、あくまで間接指標です。 たくさん使った≠価値が出た。実際、Metaの番付では、順位のためにAIを空回しして消費量を水増しする従業員が現れたという情報もあります。そりゃそうですよね。指標は必ずハックされます。入力量で成果を測るのは、印刷したページ数で文章の質を測るようなものなので、報酬や人事評価に直接ひもづけるのは慎重であるべきです。トークンマキシングは、短期的に組織のモメンタムを作る旗印としては効きますが、ずっと続けるものではありません。習熟が進んで消費が落ち着いてくるところまでがセットです。 消費量はあくまで間接指標。指標は必ずハックされる そしてもう1つ、この打ち手には賞味期限があります。これまでのコーディングエージェントの多くは月額定額、いわば携帯のパケ放題のような契約でした。「とにかく使え」が安心して言えたのは、この建て付けがあったからです。ところが課金体系は従量制へ動いています。 GitHub Copilotは2026年6月1日から使用量ベースの課金へ移行します し、他のエージェントも続々と後を追っています。 Uberが2026年のAI予算をわずか4か月で使い果たし、コーディングエージェントの利用に従業員一人当たりの月額上限を設けた という報道も出始めました。 従量課金の世界で大事になるのは、トークンマネジメントや最適化、つまり妥当なコストで成果を増やす考え方です。難しいのは、マキシングを経験しないまま従量課金に入ってしまった組織で、転んだことのないまま管理から始めることになります。もしいま手元に使い放題のプランがあるなら、それは最後のモラトリアムかもしれません。プランが生きているうちに、探索をやり切ることをお勧めします。 定額制(パケ放題)から従量課金へ。「とにかく使え」が言えた時代は終わりつつある ここまでが探索の話。次は、見つけた発見をどう組織に固定するかです。 6. 実装:発見をAgent Skillに固める ― 発見した本人に、文書化まで背負わせない どの組織にも、キャズムでいうイノベーターやアーリーアダプターにあたる人たちがいます。新しいものを勝手に調べ、勝手に試し、「この頼み方ならうまくいく」という良い使い方を見つけてくる人たちです。問題は、その発見が本人の中にしかないことです。 そこでKTCで今増えているのが、 Agent Skill です。Agent Skillとは、AIエージェントに特定タスクの「やり方」を教える再利用可能な手順書のことで、いつ何をするかを書いた指示書(SKILL.md)、手順とOK/NGの線引き、テンプレートやスクリプトといった参照ファイルを1つのパッケージにまとめたものです。属人的だったカンコツを取り出して、誰でも再現できる形に固める。暗黙知やワザを、Skillという形で全員に配ることができます。 Agent Skillは指示書・手順/判断基準・参照ファイルの3点セット KTCの例でいうと、トヨタグループには「物と情報の流れ図(物情)」という業務の可視化手法があるのですが、このドラフトをAIに作らせるSkillを固めて、社内に配布しています。先行する人たちの発見を、お湯を注げば誰でも食べられるインスタント食品に加工して配る、というイメージです。 一方でこうした探索の担い手は、新しい使い方を探すこと自体は好きでも、それを手順書に書き起こすことには関心が薄かったりします。であれば、横串の推進組織が本人のところへ出向いて、「文書化はうちらが代わりにやります」と引き受けてしまうのはどうでしょうか。発見した本人に、文書化の手間まで負わせる必要はないかもしれません。 7. 運用と文化 ― 「手でプロンプトを打たない」という逆説 Skillは作っておしまいではなく、運用が必要です。誰でもSkillを探して使える場所(Plugin Market)を作る。命名ルールを決める・・・検索できないSkillは、存在しないのと同じだからです。ブランチ名や関数名に払っている気遣いを思い出してください。あれと同じ気遣いがSkillにも必要です。ただ、Skillの命名規則のベストプラクティスはまだ世の中に整備されていないので、会社ごとに独自で決めてしまうのが有効だと思います。それから、定期的なメンテナンス。数か月で前提が変わる領域なので、古いSkillは放置すれば負債になります。 Skill運用を支える4つの仕組み(Plugin Market・命名ルール・定期メンテ・対象発掘) そして、そのメンテナンスをどう回すか。ここが地味に大変なところなのですが、KTCではSkillの保守そのものをSkillにしてしまうことを試しています。いわば、Skillを点検・整備するためのSkill群です^[公開されているmizchiさんの「 waxa 」を参考にしています。]。役割を分けた、4つのモードがあります。 検証する:書いたばかりのSkillを、まっさらな別セッションに白紙で読ませ、「ここが伝わらない」という曖昧さや暗黙知を炙り出す。 棚卸しする:Skill群ぜんぶを複数の観点で健康診断し、どれから手を入れるべきかのリストを作る。迷ったら、まずここから。 命名を整える:名前とdescriptionだけを命名規則に合わせて直す。何をするSkillか一目で理解でき、検索で見つかる状態を保つための整備になる。 本文を直す:古いSkill名やモデル名、URLといった陳腐化した参照を、機械的に一括置換する。 コツは、各修正のポリシーきっちり分けて、1つのセッションに何もかもやらせないこと。さきほど「検索できないSkillは存在しないのと同じ」と書きましたが、その状態を保つ作業自体を、人間の根性ではなくSkillに肩代わりさせるわけです。運用とは、こういう地味な仕組みの積み重ねなのだと思います。 Skillの保守そのものをSkillにする。役割を分けた4モードで運用を回す(参考: mizchiさんのwaxa) 文化の面では、事例共有会や勉強会といった地道な取り組みを続けてください。「こんな効くSkill作ったぜ」を見せ合う場は、評価制度ではなく、つい誰かに見せたくなる気持ちで回り始めます。地味ですが、文化醸成はこういう積み重ねでしか進まないと思っています。 最後に1つ、逆説的な話を。個人の仕事を組織の仕事にする上で、プロンプトエンジニアリングのスキルが邪魔をすることがあります。個人が勘とコツでプロンプトを丁寧に調整し、エージェントをいい感じに動かすのは、良いようでいて横展開が非常にしにくい。プロンプト頼みの業務は、それ自体が属人化です。なのでKTCでは最近、組織の仕事にする場合は手でプロンプトを打つのをできるだけやめて、スラッシュコマンドやSkillの呼び出しだけで完結させることを推奨しています。上手に打てる人ほど、打たない。妙な話ですが、組織化とはそういうことだと考えています。 なお、ここまでの打ち手は、KTCがクラウド・AI領域で新しいことに踏み込みやすい立場にある、という前提と切り離せません。新しいことを試し、うまくいったものを少しずつ周囲へ広げていく——そんな意識で取り組んでいるので、キャズムでいう上位層に意識的に時間を寄せています。どの層にどれだけ時間をかけるかのポートフォリオは、自社の立ち位置や方針に従って決め、経営層と握っておくのが筋だと思います。 8. まとめ ― 個人の発見を、組織の知恵に 「個人の発見を、組織の知恵に」今回お伝えしたかったのは、結局この一行です。探索のフェーズでは、トークンマキシングでたくさん試して、転ぶことを恐れない。正しい使い方は、試行錯誤からしか生まれないからです。実装のフェーズでは、発見をAgent Skillのような形に固め、誰でも再現できるようにして配る。そして、仕組みと文化で回し続ける。 探索→実装→仕組み・文化。個人の発見を、組織の知恵に G検定やE資格を持っているような方は、すでに一本のスペシャリティがある状態です。AIは自力にレバレッジをかける道具なので、自力が10の人と100の人では、掛けた後の差がまるで違います。ご自身のドメイン知識と、資格を通じて学んだ知識と、生成AIやAIエージェントを掛け算して、まずはたくさん転ぶところから。そして、転んで見つけた発見を、ぜひ組織に配ってください。 ここまで読んでいただき、ありがとうございました! あなたや周囲の人の発見が、組織の知恵になっていくことを願っています。
こんにちは。Engineering Officeのアクセシビリティアドボケート、辻勝利です。 6月のある朝、私は名古屋のあるビルの一室で、植木鉢を両手で抱え、聞こえてくる水の音を頼りに歩いていました。隣では竹中さんが猫の鳴き声を追いかけ、浜谷さんは焼き網の上で肉が焼ける音に、息を詰めて耳をすませています。三人が手にしていたのは、どれも映像のない、「音」だけで遊ぶゲームでした。 傍から見たら、なかなか不思議な大人三人組だったと思います。でもその場にいた私たちは、ただただ夢中でした。目で何かを確かめるのではなく、耳をすませて、音だけを頼りに遊ぶ。その新鮮さに、すっかり夢中になっていたのです。 私たちが訪れていたのは、「オーディオゲームセンター」という展示でした。映像ではなく「音」からゲームをつくり、音だけで遊ぶ——そんなユニークな作品が集まった場所です。名古屋で開かれていることを知り、出張のついでに同僚を誘って足を運んでみました。 制限時間内に、音だけを頼りに花へ水をあげる「はなちゃんを救え」。会場で最初に夢中になった作品です。 この記事では、その朝のことを書いてみたいと思います。 なぜ、この三人で行こうと思ったのか 少しだけ、前日の話をさせてください。私と竹中さんは名古屋で仕事があり、前日から出張していました。 せっかく名古屋まで足を運ぶのだから、この出張をアクセシビリティの活動にもつなげられないか——そう考えていたときに、ちょうどオーディオゲームセンターの展示が名古屋で開かれていることを知りました。アクセシビリティのアドボケートとして、これはぜひこの耳で確かめておきたい展示です。そう考えた私は、竹中さんに加えて、名古屋オフィスで働く浜谷さんにも声をかけました。浜谷さんは、ドライバーに向けたサウンド設計を仕事にされている方です。「音」を扱うプロと一緒に、音のゲームを体験できる。これ以上ない組み合わせだと思いました。 正直に打ち明けると、この「お誘い」には、私なりの小さな狙いもありました。 アクセシビリティのアドボケートとして、私が会社のなかで担いたい役割は、製品やサービスを使いやすくすることだけではありません。その手前にある、「アクセシビリティの文化」そのものを社内に少しずつ根づかせていくことだと考えています。 そのためには、ガイドラインやチェックリストと向き合う時間だけでなく、まだ見たこと・触れたことのない領域のアクセシビリティに、同僚たちが自然と出会えるきっかけをつくりたい。そんな思いが、以前からずっとありました。今回の展示は、そのきっかけにぴったりだと感じたのです。 「音」を共通言語にして遊んだ、あの日の記憶 この場所に同僚を連れて行きたかった理由は、もうひとつあります。それは、私自身の忘れられない原体験です。 ずいぶん前のことになりますが、私はかつて東京で、「オーディオゲームをつくるハッカソン」に参加したことがありました。視覚障害のある人も、目の見える人も、一緒になって音だけのゲームをつくり、その場で遊ぶ。そんなイベントでした。 そこで私が感じたのは、なんとも言えない心地よさでした。その場には、難しい「アクセシビリティ」の話は、ほとんど出てこなかったのです。「視覚障害者のために」とか「配慮しなければ」といった肩に力の入った言葉ではなく、ただ純粋に、「音を中心にしたゲームって面白いよね」という一点で人が集まっていました。 「音」という共通言語の前では、目が見えるかどうかは、その場の主役ではありませんでした。みんなが同じように耳をすませ、同じように戸惑い、同じように笑う。あの対等でフラットな空気が、私にはとても新鮮で、心地よかったのです。 この感覚を、ぜひ同僚にも味わってほしい。難しい理屈ではなく、まず「面白い」から入ってもらえる体験として——そう思ったことが、今回のお誘いの根っこにありました。 当日、私たちを夢中にさせた作品たち さて、当日の会場には、いくつもの作品が展示されていました。どれも、思わず「お、これは!」と声が出てしまうような、ユニークなものばかりです。 1つめは、 植木鉢を抱えて、音を頼りに水を探すゲーム 。鉢を持って歩き回り、聞こえてくる音を手がかりに、水のありかを探し当てます。制限時間内に花へ十分なお水をあげられるかは、私たちの耳と方向感覚にかかっています。冒頭で私が抱えていたのが、この鉢でした。 2つめは、 音だけで進行する人狼ゲーム 。誰が人狼なのか、表情ではなく声と音だけで推理していく緊張感がありました。それぞれのプレーヤーには、小さなスピーカーを通して別々の振動が伝えられ、どんな振動が届いたのかを話し合いながら、誰が人狼なのかを推理していきます。自分に届いた振動のリズムを、そのまま声に出してしまわないよう気をつけながら、慎重に人狼を探しました。 声と音だけで人狼を推理する「サウンドウルフ」と、鳴き声から動物を当てるクイズ。プレーヤーには小さなスピーカーから別々の振動が届きます。 3つめは、 猫の鳴き声を頼りに、迷路の中で猫を探して捕まえるゲーム 。「ニャー」という声を追いかけて夢中になっている竹中さんの様子が、その声の弾み方から伝わってきて、なんとも微笑ましく感じました。見つけたと思った猫が、ふっと別の方向へ鳴きながら逃げていく。その手応えのなさに、私は昔飼っていたやんちゃな犬のことを思い出しました。 「Echolocation Maze ― 迷路でねこ探し」。アルミのフレームの中に入り、耳をすませて猫を探しているところ。目ではなく、音に集中する時間です。 4つめは、 音を頼りに、ちょうどよい焼き加減を狙って焼肉を焼くゲーム 。お肉が焼ける音の変化だけで、食べごろを見極める。サウンド設計を仕事にしている浜谷さんが、ここでいちばん真剣になっているのが、その張りつめた集中ぶりから伝わってきました。三人で同時にお肉を取り出すとボーナス点がもらえることもあって、それぞれが真剣にタイミングを見計らいながらゲームを進めました。 お肉が焼ける音の変化だけで、食べごろを見極める焼肉ゲーム。サウンド設計が本職の浜谷さんが、いちばん真剣に聞き入っていました。 そしてもうひとつ、ゲームというより、 動かすと振動に合わせて笑い出す提灯のおもちゃ もありました。手のなかで震えながら笑う提灯に、思わずこちらまで笑ってしまいました。子どもの頃に遊んだ「笑い袋」を思い出すような、ちょっと懐かしい作品です。 どの作品も、目で見て楽しむものではありません。耳をすませ、手で感じ、音の変化に身をゆだねて遊ぶ。気がつけば三人とも、すっかり童心に返って盛り上がっていました。 「面白い」が、いちばん最初にあっていい 会場を出たあと、私はふと、あの東京のハッカソンで感じた心地よさが、そのままここにもあったことに気づきました。 この朝、私たちは一度も、肩肘張った「アクセシビリティ」の話をしませんでした。ただ、音のゲームが面白くて、三人で笑い合っていただけです。目が見える二人と、見えない私とが、まったく同じスタートラインで戸惑い、同じように夢中になれる。そういう体験を、言葉ではなく、身体で分かち合えたことが、私にはとても嬉しかったのです。 アクセシビリティというテーマは、ともすると「正しさ」や「やらなければいけないこと」として語られがちです。それももちろん大切なことです。でも、その入り口に、こんなふうに「ただ純粋に面白い」という体験があってもいいはずだと、あらためて思いました。難しい話の前に、まず一緒に楽しんでしまう。そこから自然と、「音だけでこんなに遊べるんだ」「目に頼らない世界にも、こんな豊かさがあるんだ」という気づきが生まれていく。 これからも私は、社内のいろんな人と、こういう「触れるきっかけ」を少しずつ増やしていきたいと思っています。難しい顔で身構える前に、まず一緒に耳をすませて、笑ってしまう。アクセシビリティの文化は、案外そういう楽しい時間の積み重ねから根づいていくのかもしれない——名古屋出張の締めくくりに、そんなことを思ったのでした。 もうひとつの出会い ― 鼓動を伝えるモビリティ「QUENELLE」 会場には、オーディオゲームとは別に、もうひとつ強く印象に残った展示がありました。「QUENELLE(くねる)」という、小型EVのコンセプト作品です。 これは、乗る人の鼓動を読み取り、それを音や光、振動として映し出す乗り物でした。乗り物を「操作する道具」としてではなく、感覚でつながる相手のように感じさせてくれる——そんな試みです。私も実際にまたがらせてもらいました。 「QUENELLE」にまたがらせてもらいました。なお、同席されたスタッフの方など、ご本人の同意を確認できていない方のお顔は画像処理をしています。 鼓動を音・光・振動で映し出す、小型EVのコンセプト作品。乗り物を「操作する道具」から「ともにある存在」へと近づけようとする試みです。 ドライバーに向けたサウンド設計を仕事にする浜谷さんが、この作品の前で何を感じていたのか。それは、次の感想に譲りたいと思います。 一緒に行った二人から 最後に、同行してくれた二人に、当日の感想を一言ずつ書いてもらいました。 現地に行くまでは「音のゲーム」というものがまったく想像できず、どちらかといえば、見た目には地味で質素な作品をイメージしていました。でも、実際にプレーしてみると、自分自身が夢中になって遊んでしまうほど面白くて驚きました。子どもから大人まで、幅広い世代の人たちが一緒に楽しめる——オーディオゲームには、そんな可能性があると感じました。(竹中) 車を運転中のドライバーは、とても不自由です。ずっと前を見ていないといけないし、好きに動くこともできない。ほとんど唯一の愉しみは「音」ですが、音楽を流すか、ラジオを聴くか、同乗者とのお喋りか。それって数十年前からほとんど何も変わっていない。「音を愉しむ」って、もっと自由で、色んな可能性があるんじゃないか?と、ずっと考えていました。一緒に体験したオーディオゲームセンターの作品は、自分の中にあった漠然とした仮説を、確信へと一歩近づけてくれました。(浜谷) 目を使わずに「音」だけで遊ぶ時間は、私にとって、アクセシビリティの新しい入り口を確かめ直す時間でもありました。もし機会があれば、ぜひ一度、耳だけを頼りに遊んでみてください。きっと、思っているよりずっと豊かな世界が広がっています。 参考リンク オーディオゲームセンター: https://artscape.jp/exhibitions/64694/ オーディオゲームをつくるハッカソン(CCBT): https://ccbt.rekibun.or.jp/events/audiogamecenter_ccbt_hackathon
はじめに QEグループでモバイルチームに所属しているmです。 前職までは約7年ほどモバイルアプリの開発をメインに従事していました。 今はQAとしてプロダクト開発に関わっています。 この記事では、QAが設計や実装といった開発側との共通言語を持っておくと、テスト設計でも不具合対応でも有効である、という話を書きます。 共通言語があると問題の切り分けがしやすくなり、開発とのコミュニケーションコストも下がるため、本来集中すべき業務に工数を割きやすくなると考えています。 なお現在も対応中の取り組みなので、効果については「こう感じている」という話も含みます。 課題:内部の変更は、影響範囲が外から見えづらい まず、この記事の出発点となる課題から整理します。 内部実装だけが変わる対応、特にユーザーから見える動作は変えないリファクタリングなどは、影響範囲が外から見えづらいという特徴があります。 例えば、Swift6対応やKMP対応、UIライブラリの移行などが該当するかと思います。 挙動レベルの影響は開発から共有されるものの、外から触っているだけではどこがどう変わったのかを把握しにくいです。 そのため、テスト設計では「影響範囲が分からないので、念のため全項目をテストしよう」となりやすく、 不具合対応では「ゼロから原因を探そう」という方向に傾きやすくなります。 ただ、実際には期日などの制約もあるため、すべてを見るわけにもいかない場合があります。 この「影響範囲が外から見えづらい」という点が、このあと述べる2つの工程(テスト設計と不具合対応)に共通する課題になります。 前提:アプリ側の設計・実装を把握して見る 課題への向き合い方として、まず普段行っている見方を記載します。 アプリは一枚岩ではなく、機能や役割ごとに分かれて作られています。 たとえば、画面の見た目を作る部分、データを加工する部分、サーバーと通信する部分、データを保存する部分、といった具合です。 何かが起きたとき、「画面上どう見えるか」だけで捉えるのではなく、 「それはどの責務の話なのか」「どことつながっているのか」を、アプリの作りの上で考えるようにしています。 これが、本記事で言う「構造から見る」という見方です。 たとえば「ログイン後の表示がたまにおかしい」という事象であれば、見た目を作る部分ではなく、 データを取得する通信や処理の部分が怪しいのではないか、と当たりをつける、といった具合です。 もちろん開発経験があるため、コードや設計資料を見れば、どの部分がどのように動き、どこにつながっているかは比較的把握しやすいほうだと思います。 ただ、この見方だけに頼るのではなく、開発側にも影響範囲の資料を展開していただいています。 そして、この見方がまず効くのが、テスト設計です。 テスト設計:見るべき範囲を、根拠を持って絞る 先ほどの「つい全項目をテストしたくなる」場面を、前項の考え方で設計します。 ここで行うのは、実装を隅々まで読むことではありません。「この変更は何をしようとしているのか」「だとすれば、何が壊れそうか」を、設計レベルで把握することです。 ここが分かると、何を見るか・どこまで見るかの手がかりになります。 具体的には、次の3つを問いとして使っています。 ①動作は変わるのか、変えないはずなのか 変えないはずの変更であれば、見るべき軸は「変更前と同じか(同等性)」になります。見た目や操作感が、変更前と変わっていないかを確認する、ということです。 ②技術的にどこへ影響のある変更なのか 並行処理なのか、UIの作りなのか、通信まわりなのか。 影響する領域が分かると、その領域で起きやすい問題が、見るべき観点になります。 たとえば並行処理が変わるのであれば、断続的に固まる・クラッシュする・反映が遅延する、といった点です。 ③どの単位で入る変更なのか 画面単位なのか、モジュール単位なのか。これが分かると、見る範囲の単位が定まります。 たとえば、今回改修した画面のみを対象にする、といった具合です。 この3つの問いで根拠を持って絞れると、見る範囲が現実的なところに収まります。 「全部やる」から「変更によって壊れそうな箇所を確かめる」へ変わる、という感覚です。 あわせて、開発との「どこまで実施するか」のすり合わせも速くなると考えます。 後工程でも効く:不具合の起票と切り分け ここまではテスト設計の話でしたが、メリットはその先の工程にも続くと考えています。 テスト設計の段階で把握した「この変更はどの実装箇所に影響があるか」という理解は、不具合の起票や切り分けの場面でも、そのまま活用できます。 一度読み込んだ作りが、ここでも改めて効いてくる、という形です。 そのため、案件にもよりますが、起票を見た時点で「このあたりが怪しいのではないか」と当たりをつけ、仮説つきで起票できます。 たとえば、表示崩れ・断続的に固まる、iOS・Android共通の不具合、といった切り口で、どの部分の話なのかを見立てられます。 重要なのは、これは「QAが原因を完璧に特定できる」という話ではない、ということです。 あくまで当たりをつけ、仮説を立てるところまでです。ただ、その仮説があるかないかで、その後の動きはかなり変わると考えています。 「画面が固まりました」とだけ書かれた起票と、 「ここは非同期処理の変更が入った画面なので、そのあたりが怪しいかもしれません」という仮説つきの起票とでは、 開発側が状況を把握する速さも、誰に依頼すべきかの的確さも変わってきます。 切り分けが、「ゼロから探す」から「仮説を検証する」へ変化し、開発とQA双方にとってメリットが生まれるかと思います。 実装に寄りすぎない ひとつ、意識していることがあります。 現在の実装に寄りすぎると、「すでに実装されているもの」をなぞるだけになり、本来あるべきなのに、実装として抜けている観点を見落とすおそれがあります。 そのため、開発から共有された「本来こう動くべき」という挙動レベルの情報と、 コードや資料から読み取った「現状こうなっている」という状態を、突き合わせるようにしています。 構造理解で当たりをつけつつ、「ユーザーから見てどうあるべきか」という挙動ベースの視点も手放さない。 このバランスが大事だと考えます。 まとめ 今回の内容を要約すると、こうなります。 構造を理解しておくと、テスト設計でも後工程でも、QAの動きが「探す」から「検証する」へ変わる。一度の歩み寄りが、複数の工程で効いてくる。 もちろん、自身の背景などから知見があったという側面はありますが、それがないと無理な話ではないと考えています。 まずはプロジェクトのアーキテクチャ図や設計資料を読んでみる、詳細設計の境界を意識してみる、変更がどの設計箇所に影響するのかを開発に聞いてみる。 そうした小さなところからでも、見立ては変わってくるかと思います。 内部実装の変更で「全部見るしかない」となりがちな場面でも、構造から攻めることで、影響範囲の見立ての精度を上げられます。 同じような場面に立っている方の参考になれば嬉しいです。
0. はじめに 弊社では Claude Code を全社的に導入しており、Claude Code のメトリクスとログを OpenTelemetry で収集し、Grafana で可視化しています。ところがある日、こんな声が飛んできました。 「ダッシュボード、また固まってるんだけど」 社内に Claude Code を展開して、いざ「みんなどれくらい使ってる?」を可視化しようとしたら、開いたダッシュボードがくるくる回り続けて何も出てこない。タイムアウト。リロードしてもまた回る。 原因は Claude Code が吐く 1 つのメトリクス claude_code_token_usage_tokens_total でした。これがカーディナリティ爆発(カーディナリティについては後述)を起こして、Thanos Query のメモリで使い倒していました。 この記事は、その障害を Grafana Managed Recording Rule × Prometheus × Thanos の構成でどう捌いたか、の記録です。打ち手を 3 案で比較した話と、採用案で実際にハマった「書き込み先(write 先)問題」を 2 段階でほどいた話がメインです。同じように AI ツールの利用状況を可視化したい人、そして高カーディナリティに殴られた経験のある Observability 担当者の参考になればと思います。 :::message この記事の構成は「検証で動作確認し、現在は短縮稼働で運用している」段階のものです。本番フル稼働の数値ではなく、構成判断のプロセスと再現できる設計を中心にまとめています。 ::: 1. 何が起きていたか:claude_code_token_usage_tokens_total のカーディナリティ爆発 Claude Code は OpenTelemetry 経由でトークン使用量などのメトリクスを出力できます。その中心が claude_code_token_usage_tokens_total です。 問題はラベルでした。このメトリクスには session_id が付きます。セッションごとにユニークな ID が振られるので、 利用が増えれば増えるほど系列(time series)が線形に増え続ける 。さらに user_email 、 model 、 type (input/output/cacheRead など)も掛け算で効いてくる。 claude_code_token_usage_tokens_total{ session_id="...", # ← セッションごとにユニーク。爆発の主犯 user_email="...", model="claude-...", type="output" } :::details カーディナリティとは メトリクスにおけるカーディナリティとは、あるラベルが取りうる値の種類数のことです。たとえば type ラベルの値が input ・ output ・ cacheRead の 3 種類しか振られなければカーディナリティは低い。一方 session_id のようにセッションごとに異なる値が振られるラベルは、セッションが増えるほど無限に種類が増えていく「高カーディナリティ」なラベルです。ラベルの組み合わせが時系列(time series)の数になるため、高カーディナリティなラベルが 1 つあるだけで系列数が爆発的に膨れ上がります。 ::: session_id のような無限に増える値(unbounded label)が 1 つ混じるだけで、系列数は天井知らずになります。 そして Thanos Query は、クエリ時に対象系列を メモリ上に展開してから 集計します。系列が数十万を超えてくると、 sum by (user_email) のような一見シンプルな集計でもメモリを食い尽くし、タイムアウトか OOM で落ちる。ダッシュボードが固まっていた正体はこれでした。 2. KTC のモニタリング基盤の構成 本題に入る前に、今回の話に登場するコンポーネントを整理します。 2-1. Alloy(OpenTelemetry Collector) Grafana Alloy は OpenTelemetry ベースのコレクターです。KTC では各システムの AWS サービスのログやClaude Code からメトリクス・ログを収集する入口として機能しています。 2-2. Prometheus Kubernetes 上のメトリクスを scrape する OSS の監視ツールです。KTC では scrape したメトリクスを Thanos Receiver への remote write で転送するリレー役を担っています。メトリクスの長期保存は Thanos に委ねる構成です。 2-3. Thanos Prometheus をスケールアウト・長期保存対応させるための OSS です。KTC では以下のコンポーネントで構成しています。 コンポーネント 役割 Routing Receiver remote write の受口。hashring に従って Ingesting Receiver へルーティング Ingesting Receiver 実際の TSDB への書き込み。S3 へ flush Store Gateway S3 のブロックをクエリ可能にする読み取りゲートウェイ Query 実際にクエリーを実行するコンポーネント。 Query Frontend クエリのキャッシュ・スプリットを担当。Grafana からの入口 Compactor S3 のブロックを定期的に圧縮・ダウンサンプリング マルチテナント構成を採用しており、書き込み時は通常 THANOS-TENANT HTTP ヘッダーでテナントを指定します。 2-4. Grafana Grafana は Thanos をデータソースとして接続し、ダッシュボードの描画と Recording Rule の管理を担います。KTC ではシステム単位でマルチテナント化した Grafana インスタンスを各チームに払い出しており、今回 Claude Code コスト可視化用に専用インスタンスを立ち上げました。 3. 課題とゴール 「じゃあ session_id を消せば終わりでは?」と思いますよね。でもそれが簡単に言えない理由がありました。 社内で AI 活用を進めるには、まず 測れること が前提になります。誰がどれくらい使っているのか、どのモデルにトークンが寄っているのか、1 セッションあたりの規模感はどうか。こういう粒度がないと「活用が進んでいるのか」を語れない。 特に session_id 単位の分析は、後から「効果的な使い方をしているチームの傾向を見たい」みたいな分析に効いてきます。つまり 生データとしての session_id は手元に残したい 。でも ダッシュボードのクエリでそのまま頑張るのは無理 。この二つを両立させるのが今回のお題でした。 ゴールはこう整理できます。 生データ( session_id 付き)は Thanos に貯め続ける ダッシュボードは「集約済みの軽いメトリクス」を見る 利用者がセルフサービスで見たい情報を見れるように調整ができること 4. 案比較 カーディナリティへの対象策はいくつかあります。今回は 3 案を並べて比較しました。 案 やること メリット 採用/不採用の理由 案1: Alloy でラベルドロップ 収集段階で session_id を捨てる 一番手前で止められて確実 生データから session_id が消え、後の分析が永久にできなくなるため、今回は不採用 案2: Thanos Ruler サーバ側の Recording Rule で集約 集約結果が Thanos に永続化される ルールの Apply が Platform 側でのオペレーションのみになる運用を実施しているため、利用者とPlatformでのやりとりが増えて運用負荷が高いため、今回は不採用 案3: Grafana Managed Recording Rule Grafana 側で集約ルールを定義し書き戻す 利用者が UI で自分でルールを作れる 生データを残しつつ、ルールの主権を利用者に渡せるため、 今回は 採用 判断の軸は 2 つでした。 生データを失わないか ( session_id を残せるか) ルールを誰が育てられるか (利用者が回せるか) 案1 は軸1で不採用。収集段階で捨てるとデータ分析に利用できません。案2 は軸2で不採用。Thanos Ruler はサーバ側のルールファイルを Platform が管理する世界なので、「ちょっとこの集約を試したい」が毎回 Platform への依頼になってしまう。これは Platform チームのボトルネック化も招きます。 残った案3 が、両方の軸をクリアしました。 5. Remote Write 先問題を 2 段階で解いた 案3 を進める上でも課題がありました。Grafana Managed Recording Rule は「集約した結果を どこかに書き戻す 」必要があります。メトリクスの管理に使っているThanosの場合はここがうまく設定できませんでした。 5-1. 詰まり1: Datasource に Thanos Receiver を直接指定できない Thanosにおいて、メトリクスの書き込み先は Thanos Receiver であるので、 Grafana の Datasource で Thanos Receiver を指定することを考えました。 ところが Datasource として登録できませんでした。理由はこうです。 Grafana の Datasource は基本的に Query(読み取り)系 に対して作るもの Thanos Receiver は Write 専用 で、Query を受け付けられない だから Receiver は Datasource として成立しない ここで再検討した結果、既に別用途で使用していた Prometheus でした。Prometheus は Query も Write(remote write 受信)も両方できる 。そこで Prometheus を Grafana の Datasource として指定することにしました。 Grafana の Datasource → Prometheus(Query を受け付けらるので Datasource になれる) Recording Rule の書き戻し → Prometheus の /api/v1/write で受ける(※) Prometheus が remote write で Thanos Receiver へ流す という形にしました。Prometheus が「Query 窓口」と「Write の中継」を兼ねることで、Datasource 問題が解けました。 ※ Prometheusはデフォルト設定では、Queryを受け付けられないので --web.enable-remote-write-receiver フラグを有効にし、外部から /api/v1/write でメトリクスを受け付けられる** にしています。 5-2. 詰まり2: マルチテナントへの書き込み(HTTP ヘッダーを操作できない) 次の壁はマルチテナントでした。Thanos Receiver はテナントを分けて受け取れますが、その振り分けは通常 HTTP ヘッダー( THANOS-TENANT ) で指定します。 ところが Grafana Managed Recording Rule の設定項目には、 書き戻し時の HTTP ヘッダーを差し込む設定がありません 。ルールの式と書き戻し先は指定できても、ヘッダーは触れない。AI 活用用のテナントへ正しく振り分けたいのに、その手段が塞がれている状態でした。 解決には Thanos Receiver 側の機能を使いました。 thanos receive \ --receive.split-tenant-label-name="tenant_id" --receive.split-tenant-label-name を指定すると、Receiver は HTTP ヘッダーではなくメトリクスのラベル値 を見てテナントを振り分けてくれます。つまり、 Recording Rule の集約結果に THANOS-TENANT="ai-usage" のようなラベルを付けておく Receiver がそのラベルを読んで AI 活用用のテナントへ流し込む ヘッダーが操作できないなら、振り分けの判断材料をラベルに寄せる。発想を「ヘッダーで指定する」から「ラベルで宣言する」に切り替えたのがポイントでした。 5-3. 全体アーキテクチャ 2 つの詰まりを解いた結果、データの流れはこうなりました。まず全体像を俯瞰します。 ポイントをまとめると: Prometheus が二役 :Grafana からの write を受け取る --web.enable-remote-write-receiver と、Thanos への転送( remote_write )を同時に担う テナント振り分けはラベルベース :Routing Receiver の --receive.split-tenant-label-name=THANOS-TENANT により、Recording Rule で付けた THANOS-TENANT ラベルの値でテナントが決まる。HTTP ヘッダーを操作できない制約をラベルで回避した 6. Before / After:タイムアウトが消えた 検証段階での効果はシンプルです。 観点 Before After ダッシュボード表示 タイムアウト / 描画されない 集約済みメトリクスを描画できる Thanos Query への負荷 高カーデ系列をメモリ展開して OOM 級 集約済みの軽い系列を読むだけ session_id 単位の生データ 残る 残る(Thanos に蓄積継続) 集約ルールの主権 (案2なら Platform) 利用者が UI で保持・編集 ダッシュボードが普通に開く、という当たり前を取り戻しつつ、生データも分析の主権も手放さずに済んだ、というのが今回のゴールでした。 7. まとめ 今回やったことを、次に同じ状況に出会った人が調査・検討できる粒度で残しておきます。 無限増殖するラベル( session_id 等)はカーディナリティ爆発の主犯 。 生データを収集段階で捨てる前に「後の分析で使うか」を問う 。Alloy でのドロップは確実だが取り返しがつかない Thanos Receiver は Write 専用なので Datasource にできない 。Query も Write もできる Prometheus を中継に挟む Grafana Managed Recording Rule は書き戻し時に HTTP ヘッダーを操作できない 。テナント振り分けは --receive.split-tenant-label-name でラベルベースに寄せる ここまで読んでいただきありがとうございました!
はじめに こんにちは、KINTOテクノロジーズ(以下、KTC)のQuality Engineering グループでQAエンジニアをしているろきです。 本記事では、マインドマップを使った観点作成の導入経緯、実際の使い方、そして運用を通じて見えてきたことをお伝えします。 マインドマップを試すことになった経緯 現在、QAチームでは観点をConfluenceのページで作成しており、画面単位ではなくテストの目的別(機能確認・表示確認・バリデーションなど)にページを作成し、箇条書きやテーブルで管理しています。 テストの目的別で観点を作成すると同一画面の観点が複数ページに分散するため、全体を一覧で見渡したいと感じる場面がありました。 そこで、1つのボードでQA対象範囲全体の観点を俯瞰できるマインドマップの活用を提案し、試験的に導入することになりました。 マインドマップの作成ルールと構造 ツールを導入するにあたり、作成ルールを統一しました。 使用ツール マインドマップはMiroを使用しています。 基本構造 マインドマップは以下の階層構造で作成しています。 案件名_verX.X.X > 画面/機能 > 観点 > 因子 > 水準 > 期待値 何をマインドマップにするか 現在、KTCのQAチームでは、テスト観点を主に以下の5つに分類して作成しています。 シナリオ 機能確認 表示確認 バリデーション アドホック このうち、マインドマップ化の対象としているのは主に以下の3つです。 機能確認 表示確認 バリデーション 表示確認やバリデーションはConfluenceでリスト化して管理しているため、テスト対象ごとに「表示確認を参照」「バリデーションを参照」のように参照先を記載し、黄色のマーカーで色分けするルールにしています。こうすることで機能確認の観点との区分けがはっきりし、どの対象をどの観点で確認するかを過不足なく整理できます。 一方、 シナリオテストやアドホックテストの観点はマインドマップにしません 。 これらは、機能確認などと比べるとマインドマップで構造化して整理する必要性が低いため、従来通りConfluenceで文章形式のものを使用する運用としています。 使ってみてわかったメリット テスト対象の観点を1か所で把握できる マインドマップ導入における最大のメリットは、1つの画面/機能に関するすべてのテスト観点を1か所で把握できる点でした。 従来のドキュメント形式では観点の種類ごとにページが分かれているため、「この操作はどの観点で確認するのか」を複数ページにまたがって探す必要がありました。マインドマップでは画面/機能を起点に観点が集約されるため、作成時に「各テスト対象にどの観点が紐づいているか」をリアルタイムで確認しながら進めることができます。結果として、テスト対象物と観点の対応関係における過不足を発見し、修正しやすくなりました。 マインドマップがそのままテスト設計の「下書き」に マインドマップは観点から因子・水準へと枝を広げていく構造上、「通知ON/通知OFF」「入力あり/なし」といった条件分岐を自然に書き出せます。具体的な条件分岐まで落とし込めるため、観点とテスト設計を同時に整理できるようになりました。 条件分岐を書くことで、観点と仕様の精度が上がる 条件分岐を書き出す過程で「この条件の場合の仕様が書かれていない」と気づくケースも増えました。観点作成の段階で仕様の抜け漏れを発見できるため、開発チームやPMへの仕様に関する質問や、仕様書レビューをスムーズに進めやすくなりました。 さらに、「ログイン済み/未ログイン」のように対になる条件が枝として横並びになるため、片方だけ記載されていないケースが視覚的にわかりやすくなります。ある観点の枝だけが極端に少ない・多いといったアンバランスも視覚的に気づきやすく、観点の粒度を揃えやすくなりました。 議論が活発になり、認識合わせに有用 これは導入前には想定していなかったことですが、マインドマップを開発チームとの認識合わせの場で使うと、議論がより活発になりました。視覚的に構造が見えることで「この条件でもテストしてほしい」「この枝はカバレッジが高いので減らしてもいいのでは」といった具体的なフィードバックが出やすくなり、認識合わせに有効でした。 見えてきた課題 Confluenceとの連携 MiroボードをConfluenceに埋め込む際、現状では画面/機能単位で特定範囲だけを埋め込むことができず、ボード全体が表示されてしまいます。画面/機能ごとにピンポイントで埋め込めるようになると、Confluenceとの連携がより使いやすくなると感じています。 コスト 現在はトライアルとして数人のみ契約していますが、チーム全体での利用が本格化する場合はコストが増加します。ボードの作成/編集には契約アカウントが必要ですが、ゲストアカウントであれば閲覧やコメント追加ができるため、用途に応じて使い分けていければと思っています。 まとめ Confluenceと併用したマインドマップの導入は、テスト観点の具体化・可視化という点で大きな効果がありました。特に「観点が自然と具体的になる」「議論が活発になる」という副次効果は、設計品質向上に貢献していると感じています。 一方で、Confluence連携やコストといった課題も見えてきました。 まずは無料プランで気軽に試せるツールなので、「テスト観点の整理がうまくいかない」と感じているQAチームにはぜひ一度試していただきたいと思います。
SeleniumConf & AppiumConfとは ブラウザ自動化・モバイル自動化のコミュニティを世界中から集める国際カンファレンスです。 Software Freedom Conservancyが運営しており、SeleniumおよびAppiumのコアコントリビューターも登壇します。 Selenium 5に関する今後の展望、WebDriver BiDi、Appium、Playwright、Cypress、AIテスト、セキュリティテスト、アクセシビリティテストなど、幅広いテーマを扱っています。 基調講演・ハンズオンワークショップ・ネットワーキングの3つの形式で構成されています。 全セッションに英語字幕とスペイン語通訳が提供されるなど、グローバルな参加者を意識した運営が特徴です。 今年は 2026年5月6日〜5月8日 にスペインのバレンシア・Veles e Ventsにて開催され、20カ国以上から約350名が参加しました。 1日目はハンズオンワークショップ、2〜3日目がカンファレンス本番という構成でした。 https://seleniumconf.com/ 今回、KINTOテクノロジーズから 呂文佳 と パンヌウェイ の2名が登壇しました。世界の舞台でKINTOテクノロジーズの取り組みを発信できた、非常に貴重な機会となりました。 登壇者 発表タイトル(英語) 発表タイトル(日本語) 呂文佳 From 50% Cost Reduction to 90% Coverage: Playwright × AI for Non-Technical QA Teams コスト50%削減からカバレッジ90%へ〜Playwright × AI:コーディング経験が浅いQAチームの実践 パンヌウェイ Scaling Mobile Test Automation with Appium and AI: Real Lessons from KINTO Technologies モバイルテスト自動化のスケーリング Appium と AI の活用 バレンシアまでの道のり CfPの告知から登壇当日まで、約8ヶ月の期間がありました。最初のきっかけは2025年9月、会社の同僚が社内SlackチャンネルでCfP(Call for Proposals)開始を告知してくれたことです。「ぜひ挑戦してみてください!」というその一言が、すべての始まりでした。 時期 マイルストーン 内容 2025年9月 CfP告知 会社の同僚がSlackチャンネルでCfP開始を告知。「ぜひ挑戦してみてください!」の一言がきっかけ 2025年10〜11月 CfP作成・社内レビュー チーム内でレビューを依頼し、発表内容と構成を確認し、ブラッシュアップ 2025年12月〜2026年2月 CfP提出・当選通知 最終タイトルを確定。SeleniumConfより提出確認メールを受信後、CfP当選の通知を受ける 2026年2〜4月 採択・スライド作成・発表練習 社内のAIファースト勉強会で日本語版の発表練習を実施。KTC室町オフィスのJCT(会議スペース)で英語版の発表練習と発音練習を2回実施 2026年5月 本番登壇 🎉 バレンシア Veles e Vents にて45分登壇 :::details 承認・ビザ手続きについて CfP当選後は社内手続きも必要でした。社長に登壇内容を説明して承認をもらい、その後カンファレンスチームとメールでやり取りしながらビザ申請の手続きを並行して進めました。国際カンファレンスへの参加には、こうした社内外の調整も大切な準備の一部です。 ::: 参加セッション一覧 日時 セッション名 登壇者 05/06 09:00〜 Making Sense of Mobile Automation with Appium and WebdriverIO to turn frustration into understanding Wim Selles, Christian Bromann 05/07 11:20〜 Quantum Automation: Rethinking Selenium & Appium in the Age of AI Baris Sarialioglu 05/07 11:20〜 From 50% Cost Reduction to 90% Coverage: Playwright × AI for Non-Technical QA Teams 呂文佳 05/07 13:20〜 Test Automation Workflows with Cursor Filip Hric 05/08 Scaling Mobile Test Automation with Appium and AI パンヌウェイ 2026/05/06(1日目):ワークショップ 1日目はカンファレンス本番前のワークショップデーです。終日1つのセッションに集中して参加しました。 Making Sense of Mobile Automation with Appium and WebdriverIO 登壇者 Wim Selles — 2025 Tokyo Test Festにも参加した方 Christian Bromann 内容と学び このワークショップでは、Appiumを ゼロからインストールして2分以内にセットアップが完了する ことを実際に確認しました。セットアップの簡単さを体感できたことで、導入ハードルへの認識が変わりました。 ワークショップ後、登壇者のWim Sellesさんと直接Appiumについて相談する機会も得ました。特に「 要素特定にIDを使うかXPathを使うか 」という実務的なテーマについて深く議論し、それぞれのメリット・デメリットを理解することができました。 ID: 高速・安定だが、開発側でIDが付与されていない場合は使えない XPath: 柔軟性が高いが、UI変更に弱くFlaky Testの原因になりやすい :::details ディナーでの交流(1日目夜) 1日目の夜はカンファレンス関係者とのディナーがあり、非常に充実した交流の場となりました。 Kazuaki Matsuo さんと同席し、Appiumの導入経験や現場の課題について情報交換をしました。 Oscar Barrios さん(昨年も登壇された方)とは今年のイベントの印象やコミュニティの動向についてお話ししました。 Ivan del Viso さん(昨年も登壇された方)は、ご自身が開発したアプリを使った自動化テストのデモを見せてくれました。英語で1行のテストシナリオを書くだけで、実行・分析・ダッシュボードレポートの生成まですべてが完結するシステムで、非常に印象的でした。 ::: 2026/05/07(2日目):カンファレンス本番 2日目からいよいよカンファレンス本番です。複数のトラックが並行して開催され、関心のあるセッションを選びながら参加しました。 セッション①:Quantum Automation — AI時代のSelenium & Appium Quantum Automation: Rethinking Selenium & Appium in the Age of AI (登壇者:Baris Sarialioglu) AI時代における自動化テストの在り方を問い直す内容でした。セッション中に聴衆から質問が上がった場面では、登壇者が次のように答えたのが印象に残っています。 セッション②:呂さんの発表(11:20〜40分) From 50% Cost Reduction to 90% Coverage: Playwright × AI for Non-Technical QA Teams KINTOテクノロジーズの同僚・呂文佳さんによる発表です。コーディング経験が浅いQAメンバーでもPlaywright × AIを活用することでテストカバレッジを大幅に向上させた実践事例を紹介しました。同じチームのメンバーが国際カンファレンスで発表する姿は、大きな刺激になりました。 セッション③:Test Automation Workflows with Cursor(13:20〜90分) Test Automation Workflows with Cursor 登壇者: Filip Hric Cursor(AI統合コードエディタ)を活用したテスト自動化ワークフローについて90分間フルで講演されました。ClaudeとGitHub Copilotの基本的な設定・活用方法がメインテーマで、Mobile QAで一緒に作業している岡さんに教えていただいた内容とほぼ同じでした。世界のカンファレンスでも同様のアプローチが注目されていると確認できたことは収穫でした。 この日のセッション終了後、翌日に控えた自分の発表準備のためホテルへ戻り、最終調整を行いました。 2026/05/08(3日目):自分の発表 いよいよ自分の登壇日です。朝から会場でスライドの確認と発音練習を行いました。 発表概要 項目 内容 タイトル(英語) Scaling Mobile Test Automation with Appium and AI タイトル(日本語) モバイルテスト自動化のスケーリング Appium と AI の活用 発表時間 40分 + 質疑応答 会場 Veles e Vents(バレンシア) 参加状況 満席 なぜCfPが採択されたのか :::message 国際カンファレンスで登壇できることは非常に光栄なこと。世界中のテストエンジニアが集まる場でKINTOテクノロジーズの取り組みを発信できる貴重な機会です。 ::: 今回、採択につながったポイントは、単なる成功事例の紹介ではなく 現場で直面した課題と改善の過程を正直に共有した 点にあると考えています。 実際に直面した課題と、改善によって得られた成果を正直に共有 したこと(理想論ではなく現場の実態) 具体的な数値 で課題を提示:128件のテスト実行に12時間かかっていたという課題を可視化 Claude・Copilot・DevinAI の実践的な活用方法と3ツールの比較 聴衆が 持ち帰ってすぐに実践できるチェックリスト を提供したこと 発表構成(45分) # セクション名 内容 1 The Breaking Point 128テスト・実行12時間という限界点と、その背景にある課題 2 Framework Evolution 課題解決のためのフレームワーク再設計と進化の過程 3 AI Integration Claude・Copilot・DevinAIの統合で得られた成果と課題 4 Tools to Culture ツール導入にとどまらない「チーム文化」への変革 5 Visual Regression Test AIを活用したビジュアルリグレッションテストの実践 6 Real Impact & Takeaways 実際の改善数値と、明日から使える実践チェックリスト 当日の会場の様子と反響 20カ国以上から参加者が集まる満席の会場での登壇でした。発表後の質疑応答では予想以上に多くの質問が集まりました。 ドイツ在住のパキスタン出身のエンジニア から、Appiumの社内導入に関する具体的な質問を多数いただきました。自分たちのチームでも同様の課題を抱えており、ぜひ参考にしたいとのことでした。 複数の参加者から「 自分たちの導入方法の参考になった 」と直接声をかけていただきました。 発表がただの情報共有にとどまらず、世界中のエンジニアの実務に役立ったと感じることができ、大変嬉しかったです。 スポンサー企業との交流と自動化テストツールの調査 カンファレンスにはテスト自動化ツールのスポンサー企業がブースを設けており、担当者から直接、各ツールの詳細を聞く貴重な機会がありました。ここでは、カンファレンスの場で実際に収集した情報をもとに、4つのツールを比較・整理します。 各ツールの概要 ツール 特徴 CloudBeat テスト自動化、実行、分析、モニタリングを統合したクラウド型の品質管理プラットフォーム Sauce Labs エンタープライズ向けクラウドテストの先駆的存在。Salesforce、Twitter、Bank of America などの大手企業で採用実績がある BrowserStack 3,500以上のブラウザ/OS組み合わせ・30,000台以上の実機デバイスを持つ業界でも有数の大手 LambdaTest 2026年1月に「TestMu AI」へリブランドしAIネイティブ化。KaneAIによる自然言語からのテスト自動生成が特徴 機能比較マトリクス 評価項目 CloudBeat Sauce Labs BrowserStack LambdaTest Webテスト ◎ ◎ ◎ ◎ モバイルアプリテスト △ ◎ ◎ ◎ コードレステスト ◎ △ △ ○ 並列実行 ◎ ◎ ◎ ◎ CI/CD連携 ◎ ○ ◎ ◎ AI機能 ○ ○ ○ ◎ 実機デバイス数 少 多 最多 多 価格 中 高 高〜中 低〜中 日本語サポート △ △ ○ △ 初心者にとっての導入しやすさ ○ △ △ ○ 凡例:◎ 優秀 ○ 良好 △ 要改善 各ツールの詳細印象 :::details CloudBeat 強み コードレステストが充実しており、プログラミング経験がなくてもテスト作成・実行が可能 Selenium・Appium・Cypress・Playwright等の主要フレームワークと幅広く統合 AIドリブンなテストレポートで根本原因分析(Root Cause Analysis)が容易 テスト実行・管理・モニタリングをすべて1プラットフォームで完結できる 弱み モバイルアプリテスト(ネイティブアプリ)の対応デバイス数がBrowserStackなどに比べて少ない 英語のみの対応で、日本語UIや日本語サポートが提供されていない 他ツールと比べると国内での導入事例や公開情報が少なく、長期利用を前提とする場合は追加調査が必要 ::: :::details Sauce Labs 強み 長年の実績を持つエンタープライズ向けプラットフォーム。信頼性・安定性が高い SOC2 Type II・GDPR・ISO 27001等のセキュリティ・コンプライアンス認証を取得 Webテストもモバイルアプリテストもどちらもカバーできるオールラウンダー 弱み 今回確認した条件では4ツールの中でも価格面の負担が大きく、中小チームや予算が限られた組織には慎重な検討が必要 コードレステスト機能が弱く、プログラミングスキルがないメンバーには難易度が高い 一部CI/CDツール(AWS CodePipeline・GitLab CI等)に非対応 ::: :::details BrowserStack 強み 実機デバイス数・ブラウザ組み合わせ数が 業界最多水準 (30,000台以上)で、網羅的なテストが可能 Accessibility Testing・Percy Visual Testingなど高度な付加機能が充実 カスタマーサポートの評判が良く、ドキュメントが整備されている 弱み 料金が高額で、コスト面での負担が大きい 基本的にSelenium/Appium等の自動化スクリプト記述が必要で、非エンジニアには敷居が高い ネットワーク遅延や実機テストでの偽陽性(誤検知)が報告されることがある ::: :::details LambdaTest(現 TestMu AI) 強み KaneAI により、自然言語でテストケースを記述するだけでスクリプトが自動生成される 今回比較した条件では、4ツールの中でもコストパフォーマンスが高いと感じた Jenkins・GitLab CI・Azure Pipelines・AWS CodePipelineを含む幅広いCI/CDツールに対応 HyperExecuteによる超高速な並列テスト実行が可能 弱み 実機デバイスの実際の可用性がBrowserStackに比べると劣る場合がある テスト分析レポートの詳細度が競合より低く、根本原因分析に限界がある UIのナビゲーションが複雑で、習熟に学習コストがかかる ::: 総合評価と推奨 現状のチーム状況(非エンジニアメンバーでも扱いやすいこと、Web・モバイルアプリの両方に対応できること)を踏まえた評価です。 ツール 評価 推奨優先度 コメント LambdaTest ★★★★☆ 第1候補 AI機能、コスト、幅広いCI/CD連携の観点から、現状のチームに最も適している CloudBeat ★★★★☆ 第2候補 コードレス機能が充実。ただし、モバイル対応やサポート面は追加確認が必要 BrowserStack ★★★☆☆ 将来候補 エンジニア体制が拡充した場合の有力な候補 Sauce Labs ★★☆☆☆ 保留 現状のチーム構成では導入ハードルが高く、コスト面でも慎重な検討が必要 感想・学び 海外カンファレンスならではの気づき :::message 現地ではコミュニケーション手段としてLinkedInが主流で、名刺交換の機会はあまり多くありませんでした。 現地で知り合った方とは、LinkedInで連絡先を交換しました。海外エンジニアとのつながりを作る際は、事前にLinkedInのプロフィールを整えておくことをおすすめします。 ::: 現地での交流から得た、世界のQA事情についての気づきも多くありました。 開発とQAを兼務しているエンジニアが多い — 日本のように専任QAチームが分離している体制は珍しく、開発者自身がテストも担う形が世界的には一般的なようです ノーコードの自動化ツールを利用している人は少数派 — コードを書いてテストを自動化するスタイルが主流で、ノーコードツール利用者は少数派という印象でした 自動化テストの現状と課題 :::message alert 世界のAppiumユーザーの声:「自動化テストをやめるべきか考えている」という方もいました。 理由は Flaky Test (不安定なテスト)の問題です。今回成功しても次回失敗する、という繰り返しによって、テスト自動化そのものへの信頼が揺らぐケースが世界的にも多いようです。 ::: Flaky Testは自動化テストにおけるグローバルな課題であり、その解消こそが現代のテストエンジニアに求められていることを、改めて実感しました。AIツールを活用した根本原因分析や、要素特定用のIDを活用した安定したテスト設計が、この問題への有効なアプローチとなるでしょう。 まとめ 約8ヶ月の準備を経てバレンシアの国際舞台に立ち、世界中のエンジニアとKINTOテクノロジーズの取り組みを共有できたことは、自分にとって大きな経験となりました。セッションで得た知識・現地での人脈・ツール各社との情報交換、そして自分の発表への反響——すべてが今後の業務に活きる財産です。来年のSeleniumConfにも引き続き注目していきたいと思います。
はじめに こんにちは、2026年3月入社の大園です! 本記事では、2026年3月入社のみなさまに入社直後の感想をお伺いし、まとめてみました。 前の方からの質問に次の方が答えるリレー形式でお届けします。 KINTO テクノロジーズ(以下、KTC)に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! 大園博昭 ![大園のプロフィール画像](/assets/blog/authors/ozono/202603-newcomer/ozono.jpg =300x) 自己紹介 Engineering Officeの大園です! Xは こちら 会社横断での経営・組織課題を解決するという幅広いミッションを受け持つチームの中で、主にソフトウェアテスト周りや生成AI周りの技術部分を担当しています 所属チームの体制は? 上長が名古屋、他チームメンバー3人が東京、私が福岡所属と全国に散らばっています やっていることは全員バラバラの個人商店なチームですが、毎日ZoomやSlackでワイワイやっております KTCへ入社したときの第一印象?ギャップはあった? 入社前からいろんな話を聞かせていただいていたのと、自分がどんなことで貢献できそうかというイメージをしっかり持っていたため、特に大きなギャップはなかったです。 現場の雰囲気はどんな感じ? 福岡オフィスは去年できたばかりというのもあり、人数もまだ10人未満。自分たちで一から作っている感が楽しいです! ブログを書くことになってどう思った? 今後どんどん書いていきたいと思います! ブログではないですが、6/4に福岡オフィスにおいてE2Eテスト x AIというテーマで 勉強会を開催しました 。今後もKINTOテクノロジーズの魅力発信や福岡エンジニアを盛り上げる活動を積極的にしていきたいと思います! 高さん ⇒ 大園さんへの質問 福岡の豚骨らーめんの中で、一番美味しい店について教えてください これは戦争が起きかねない非常にセンシティブな質問ですね…私は昔ながらのとんこつラーメンが好きなので 元祖長浜屋 です! 森重香一 ![森重さんのプロフィール画像](/assets/blog/authors/ozono/202603-newcomer/morishige.jpg =300x) 自己紹介 業務システム開発部業務システムGの森重です。 KINTO事業のシステムのうち、債権領域のビジネスアナリスト(BA)を担当しています。 BAは開発チームと業務部門の橋渡し役で、要件整理や関係者調整が主な仕事です。 大阪オフィス勤務で、前職は業務システム開発に携わっていました。 所属チームの体制は? 直属チームは4名(東京3名・大阪1名)で、同期入社の田平さんも同じチームです。 田平さんの紹介と合わせて読んでいただけると体制がより伝わるかと思います。 離れていても Slack やオンラインミーティングで日々連携できています。 KTCへ入社したときの第一印象?ギャップはあった? 「テックベンチャー」という言葉から、技術中心で少し距離感のある組織をイメージしていました。 実際に入社してみると、勉強会や交流イベントが多く、所属チーム外の方とも自然に話せる機会がたくさんありました。 良い意味でイメージとのギャップを感じています。 現場の雰囲気はどんな感じ? 落ち着いた雰囲気の中で、それぞれが主体的に動いている印象です。 困ったときは気軽に相談できますし、Confluence や Slack を活用した情報共有が活発で部門を超えて動きやすいです。 個人で進める部分とチームで議論する部分のバランスが良いと感じています。 ブログを書くことになってどう思った? これまで読む側だったので、書くのは少し緊張しました。 ただ振り返ってみると、入社後の気づきを言語化する良い機会になりました。 入社を検討されている方に少しでも雰囲気が伝わればうれしいです! 大園さん ⇒ 森重さんへの質問 最近行った旅行でオススメの観光地などあれば教えてください! 昨年末に友人と犬山城へ行きました。 今年の大河ドラマ「豊臣兄弟!」が始まることもあり、放送前に戦国ゆかりの地を訪れてみようと思ったのがきっかけです。 実際に訪れてみると、テレビや写真で見るよりも天守はコンパクトな印象でしたが、その分、現存天守ならではの歴史をぐっと身近に感じることができました。 黒い外観も印象的で、少し離れて見ると存在感がありました。 城下町は大規模な観光地というより歩きながらゆっくり楽しめる雰囲気で、食べ歩きや散策をしながら半日ほど過ごせます。名古屋からのアクセスもよく、週末の小旅行としておすすめです。 佐藤誠 ![佐藤さんのプロフィール画像](/assets/blog/authors/ozono/202603-newcomer/sato.jpg =300x) 自己紹介 プラットフォーム開発部xREグループDBREチーム 所属の佐藤誠です。 KTCのDBRE業務全般を担当してます。AWS Aurora MySQLをたくさん使っていますが、PostgreSQLも社内で安全に使えるように整備を進めています。 所属チームの体制は? DBREチームは5名体制で、全員神保町所属です。 KTCへ入社したときの第一印象?ギャップはあった? 入社前にオフィス見学などもさせてもらっており、印象通りでした。 現場の雰囲気はどんな感じ? 神保町オフィスはいわゆる古書街から少し南に下ったところにあり、オフィス周りを歩くだけで発見がたくさんありますね。ラーメン、カレー、中華、洋食も名店がたくさんあり、ランチは毎日の楽しみです。オフィス内はまだ知り合いが少ないのでこれからですが、皆さんシゴデキな雰囲気があります。 ブログを書くことになってどう思った? いつ来るかと思ってたら入社して3か月経ってました。 森重さん ⇒ 佐藤さんへの質問 散歩が趣味とのことですが、歩いていて起きた忘れられない出来事やハプニングがあれば教えてください! 道を聞かれたり、駅近だと寸借詐欺っぽいことはよくあるくらいですかね。。。散歩の醍醐味としては駅間の移動が点の移動に対して、徒歩は線・面の移動になるので、いろいろな気づきがありますね。雰囲気の良い公園とか地元の小さな雑貨屋が見つかったり。河川敷とか線路沿いをよく歩きますが、人の営みを感じられて良いです。 田平亨斗 ![田平さんのプロフィール画像](/assets/blog/authors/ozono/202603-newcomer/tahira.jpg =300x) 自己紹介 業務システム開発部業務システムGの田平です。 KINTO事業のシステムのうち、与信領域の開発・運用を担当します。 東京の室町オフィス勤務ですが、最近は神保町にいることも多いです。 所属チームの体制は? 直属のチームは4名体制で、東京3名大阪1名です。 加えて、他の部や協力会社の方に入っていただいている形です。 KTCへ入社したときの第一印象?ギャップはあった? 入る前はもっとトヨタ色が強いと思っていました。実際は逆で、そういった物に縛られない集団を目指していると認識しています。 案外車に乗ってない方も多い印象。サーキットを走られているような方もいて多様性があると思います。 現場の雰囲気はどんな感じ? 中途採用で経験豊富な方ばかりで、落ち着いた印象です。 オフィスごとに結構雰囲気も違いそうで、色々行ってみたくはあります。 ブログを書くことになってどう思った? 選考時から存在は知っていたのですが、入社後無事こうしてブログを書けているのがまずは良かったという感じです。 佐藤さん ⇒ 田平さんへの質問 明治大正の建物のおすすめがあれば教えてください(迎賓館赤坂離宮は自分も大好きです) なんといっても上野・湯島の旧岩崎邸庭園がオススメです。都会の喧騒を離れた落ち着いた環境で、装飾やタイル、金唐革紙など見どころが多い所です。 昭和初期にはなりますが、白金台の庭園美術館(旧朝香宮邸)も赤坂離宮と同様に豪奢な印象、アールデコの装飾が非常に多く見応えがあるかと思います。 ついでに大阪あたりだと芦屋のヨドコウ迎賓館が非常にオススメ。福岡はめちゃくちゃ現代ですが、天神地下街が素晴らしい。名古屋には何と言っても明治村があります! 高斯 ![高さんのプロフィール画像](/assets/blog/authors/ozono/202603-newcomer/gao.png =300x) 自己紹介 デジタル戦略部 データサイエンスGの高(si gao)です。 データ分析を担当しています。 東京オフィス勤務です。 趣味:サッカー観戦 / プレー / 漫画アニメ鑑賞 / ゲーム 所属チームの体制は? デジタル戦略部のデータサイエンスGに所属し、データ分析を担当しています。 チームは3名体制です。 分析に使っている言語・ツール例:Python / SQL / BIツール など。 KTCへ入社したときの第一印象?ギャップはあった? 入社前はもっとお堅い雰囲気を想像していました。実際はフラットで風通しがよく、データやテクノロジーで事業を前に進めようという空気の強い組織だと感じました。 エンジニアやデータ分野のスペシャリストが多く、それぞれの専門性を持ち寄って課題に取り組んでいる点が良い意味でのギャップでした。 現場の雰囲気はどんな感じ? 落ち着いていて相談しやすい雰囲気で、分からないことを聞きやすい環境です。 東京オフィスは設備も整っていて働きやすく、チームを越えた交流の機会もあります。 ブログを書くことになってどう思った? 入社して間もないタイミングで、自分の言葉で会社や仕事を振り返る良い機会になりました。 リレー形式で前の方からの質問に答えるのが新鮮で、楽しく書けました。 田平さん ⇒ 高さんへの質問 サッカーかなりお好きなのではないかと思いますが、どういった所に魅力を感じていますか? 大きく3つあります。 どんな体型・体格の人でも活躍できるところ 。背が高くなくても、足が特別速くなくても、技術や判断力、ポジショニングで輝ける選手がたくさんいます。多様な個性が同じピッチで成立するのがサッカーの面白さだと思います。 想像力が問われるところ 。次のプレーを読み、スペースを作り、味方の動きを予測する——決まった正解のない中で発揮される創造性に毎回ワクワクします。 チームワーク 。一人のスター選手だけでは勝てず、全員が役割を全うして初めて勝利に近づく。個と組織のバランスが取れた瞬間の美しさが一番の魅力です。 さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
1. はじめに Quality Engineering Gのとみよしです。 私が所属するQAチームではオープンソースのテスト管理ツールであるTestLinkを採用しています。 :::message TestLinkとは テストケースの作成・管理からテスト計画の立案、実行結果の記録まで一元管理できるオープンソースのテスト管理ツールです。 ::: TestLinkへの結果入力は工数がかかるため、Excelマクロで結果を一括入力できるXMLファイルを生成する仕組みを運用していました。 しかしこのExcelマクロには2つの課題がありました。 チーム内にMac・Windowsユーザーが混在しており、OS差分を考慮した 2本のコードを管理 しなければならなかった ローカル環境でのみ動作するため、 更新のたびにファイル共有が必要 だった これらを解決するために、Microsoft 365のCopilotを活用してGoogle Apps ScriptでWebアプリを作ってみました。 コードは一行も自分で書いていません。 その過程を紹介します。 2. 解決策としてGAS Webアプリを選んだ理由 課題の本質は「 環境に依存している 」ことでした。ブラウザで動くWebアプリにすれば、OSの違いもローカル管理の問題も一度に解決できます。 その中でGAS(Google Apps Script)を選んだ理由は主に3つです。 ① OS差分がなくなる ブラウザで動くため、MacでもWindowsでも同じように使えます。 ② Google スプレッドシート(以下、GSSと表記)と連携できる プロジェクトコードや担当者などのマスタデータをGSSで管理し、Webアプリからそのまま参照できます。更新もGSS上で行うだけなので、誰でもマスタ更新が可能になります。 ③ 無料で使える Google アカウントがあれば追加コストなしで開発・運用できます。 3. 完成したWebアプリの紹介 主な機能は3つです。 ① 一括生成 テストケースが連番になっている場合に使います。開始番号と終了番号を入力するだけで、その範囲をまとめたXMLファイルを出力できます。 例)No.1〜100をまとめて一括出力 ② 個別生成 テストケースを個別に指定して出力します。飛び番号のケースや、特定のケースだけ再テストしたい場合に便利です。 例)No.1, 3, 5, 7, 10を個別に出力 ③ マスタ管理 プロジェクトコードや担当者名をGSSで管理しています。画面右上の「マスタを編集」ボタンからGSSに直接遷移して編集できます。 4. 開発の進め方 〜Copilotと約15往復した話〜 「WebアプリはGASで作ろう」と決めたものの、私は簡単なGASのコードしか書けず、Webアプリに関する知識はほとんどありませんでした。そこで活用したのが Microsoft 365のCopilot です。 Copilotへの最初の一手 まず既存のExcelマクロのコードをそのままCopilotに貼り付け、以下のように依頼しました。 「このExcelマクロと同じ機能をGAS(Google Apps Script)で書いてください」 たったこれだけです。Copilotは既存コードの意図を読み取り、GAS向けのコードを出力してくれました。 Excelマクロという既存の資産がそのまま設計書代わりになった わけです。 機能を1つずつ育てていった 最初から全機能を一度に作ろうとせず、以下の順番で1つずつ機能を追加していきました。 一括生成機能の作成 個別生成機能の追加 マスタ機能の追加 細かい仕様の追加 Webアプリ化 1ステップずつCopilotに依頼し、動作を確認してから次に進む流れです。結果的に 10〜20往復 のやり取りになりました。 エラーが出たらそのまま貼り付ける 開発中にエラーが発生することも何度かありましたが、対処法はシンプルです。 エラーメッセージをそのままCopilotに貼り付ける それだけで原因の説明と修正コードを返してくれました。エラー対応は 1〜2往復で解消 できました。 5. やってみて気づいたこと Copilotへの伝え方のコツ 一度に複数のことを依頼するより、 1つの依頼につき1つの機能追加 に絞ったほうがスムーズでした。欲張って「あれもこれも」と依頼すると、意図が伝わりにくくなることがありました。 QAエンジニアでもWebアプリが作れた 着手前は「自分にWebアプリなんて作れるのか」という不安がありました。しかし Copilotに既存のコードを渡してやり取りを繰り返すだけで、気づけば動くWebアプリができあがっていました。 プログラミングの専門知識がなくても、「何をしたいか」を言葉で伝える力があれば十分です。 Before / After Excelマクロ GAS Webアプリ OS対応 Mac/Windows別管理 ブラウザで統一 コード管理 実質2本 1本 マスタ管理 Excelファイル Google スプレッドシート 共有のしやすさ ファイル共有が必要 URLを共有するだけ 「はじめに」で挙げた2つの課題が、どちらもきれいに解消されました。 6. おわりに 「QAエンジニアにはコードが書けない」なんてことはありません。 AIを活用すれば、 既存の資産を渡すだけで新しい環境向けのコードを生成してもらえます。 私自身、コードを一行も書かずにWebアプリを完成させることができました。 同じようにExcelマクロの管理に悩んでいるQAエンジニアの方がいれば、ぜひ一度試してみてください。まず手元にあるマクロのコードをCopilotに貼り付けるところから始めれば大丈夫です。 この記事が、AIを活用した業務改善の第一歩を踏み出すきっかけになれば嬉しいです。
はじめに QE(Quality Engineering)グループのMobileチームのokapiです。 Claude Codeがあれば、もうAIはこれ一本でいいよね? そう思ってました。 仕様整理、テスト設計、業務効率化アプリ(GASでWebアプリ)の開発も、Claude Codeがあると進めやすくなりましたし、 社内で話していても「Claude Code便利」という話をいろんな方から聞いてました。 そんなある日、社内で「Gemini CLIを検証できる」タイミングがありました。 最初は、一緒に作業しているメンバーが使いたいと言っているので、ついでに試そうくらいな感じでした。 :::message 「Claude Codeで困ってないし、Geminiは必要?」 ::: ふと思ったんですが、 :::message 「Claude Codeを毎日使っているからこそ、できなかった所をGeminiで解消できたり……?」 ::: そんな好奇心から、QEグループとして「Claude Code × Gemini CLI」の併用効果を検証してきました。 結果、当初の予想を超える発見がありました。本記事では、その結果をお伝えします。 Gemini CLIとは? :::message alert 【公開時点での重要なお知らせ】 Gemini CLIは「2026年6月18日」でリクエスト受付が終了し、後継の Antigravity CLI に移行されました。 ※法人向けは引き続き利用可能です。 ::: Gemini CLIは、Googleが提供する生成AI「Gemini」をコマンドライン(CLI)から使えるツールです。 普段Webブラウザで触るチャットUIとは違い、ターミナル上で対話できるのが特徴です。 Claude Codeとの主な違いを整理するとこんな感じです。 ツール 強み 弱み Claude Code コード生成・リファクタ・レビューが得意。仕様理解や論理的な推論に強い 動画解析ができない。20ページ超のPDFは苦手。Web検索の鮮度がやや弱い Gemini CLI 動画・画像・大容量PDFを一括解析できる( analyzeFile )。Google検索で最新情報を取得( googleSearch ) コード生成はClaude Codeで十分なケースが多い こちらの「Claude Code」の弱みを「Gemini CLI」で解消して、 「Claude Code」をさらに使いやすくするのが、今回の検証の狙いです。 graph LR A[Claude Code] B[Gemini CLI] A -.-> X[動画解析] A -.-> Y[50ページ超PDF] A -.-> Z[最新Web情報] B ==> X B ==> Y B ==> Z X --> R[Claude Code × Gemini CLI<br/>QA業務の幅が広がる] Y --> R Z --> R style A fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px style B fill:#ffe6cc,stroke:#d79b00,stroke-width:2px style R fill:#d5e8d4,stroke:#82b366,stroke-width:2px linkStyle 0 stroke:#6c8ebf,stroke-width:1.5px linkStyle 1 stroke:#6c8ebf,stroke-width:1.5px linkStyle 2 stroke:#6c8ebf,stroke-width:1.5px linkStyle 3 stroke:#d79b00,stroke-width:3px linkStyle 4 stroke:#d79b00,stroke-width:3px linkStyle 5 stroke:#d79b00,stroke-width:3px linkStyle 6 stroke:#aaa,stroke-width:1px linkStyle 7 stroke:#aaa,stroke-width:1px linkStyle 8 stroke:#aaa,stroke-width:1px 🔵 点線:Claude Codeの弱み / 🟠 太線:Gemini CLIの強み 検証の進め方 社内のAI検証プロジェクトの一環として、以下の流れで進めました。 項目 内容 検証期間 3週間(2026年4/20〜5/8) 検証ユースケース Claude Codeとの併用(Claude Code × Gemini CLI) 検証方法 同じQA作業を「Claude Code単独」と「Claude Code × Gemini」の両方で実施し、結果を比較 記録方法 Confluenceに検証レポートとして記録 :::message Claude Code内にGemini CLIのMCP(外部ツール連携)を構築することで、 ツールを切り替えずにClaude Code内でGeminiが使えるようになります。 ::: 検証結果まとめ :::message Claude Code単体で使うよりも、Geminiを併用した方がQA作業を効率化できました。 ::: 検証した7つのユースケースを、効果が大きかったものから紹介していきます。 No. ユースケース Claude Codeのみ Claude Code × Gemini併用 コメント 1 動画解析 ❌ ✅ Claude Codeでは不可。Geminiを入れることで可能に 2 ダブルチェック ❌ ✅ Claude CodeとGeminiの視点でレビューできる 3 QA設計(大容量PDF) ⚠️ ✅ 50ページ超の仕様書も一括解析可能に 4 画像解析 ⚠️ ✅ 大容量・複数画像の一括処理が可能 5 Web検索 ⚠️ ✅ 最新情報を効率よく収集 6 並列処理 ⚠️ ✅ 重い処理をバックグラウンドに逃せる 7 コード生成 ✅ ✅ ここはClaude Codeのみで十分 それぞれを少し詳しく紹介します。 効果が大きかった併用パターン 1. 動画解析:Claude Codeのみでは不可。Geminiを入れることで可能に これが一番大きいポイントです。 Claude Code単独では、動画ファイルを直接解析することはできませんが、 Gemini CLIの analyzeFile を使うと、「音声・映像をまとめて解析」できます。 QA業務では「動画」を扱う場面が意外と多いです。 たとえば、 登壇練習動画を見て、話し方や資料の流れをレビュー JIRAの不具合チケットのエビデンスから内容を確認してもらい手順をテキスト化 新しいツールの使い方などの手順を聞くときに動画から詳細手順を確認 が新たにできるようになりました。 :::message 補足:Claude Code単独では動画解析はできませんが、Skills(拡張機能)を使えばClaude側でも対応自体は可能です。 Claude Code:Claude Skills( claude-video-vision など)を使い、「動画をフレーム画像に分解してから解析する」という形で対応 Gemini CLI:動画ファイルをそのまま読み込んで、映像と音声を同時に理解。操作の因果関係や正確なタイムスタンプの特定も可能 そのため、動画解析はGeminiで行うのがおすすめです! ::: 2. ダブルチェック:Claude CodeとGeminiの視点でレビューできる Claude Codeに「Confluence資料のレビューして」とお願いしても、同じセッション内では同じ視点でしかチェックできません。 つまり、Claude Codeが見落とした観点はそのまま見落とされてしまうんです。 そこで、Geminiの chat を併用すると、独立した第三者の視点でレビューしてもらえるようになりますので、 テスト設計やドキュメント修正をする時に、役立ちました。 Claude Codeで作成 Claude CodeとGemini CLIでレビュー 指摘内容をClaude Codeで反映 という流れにすると、Claude CodeのハルシネーションをGeminiが指摘してくれるので、より正確になります。 「人にレビュー依頼する前に」Claude Code × Geminiが代わりにダブルチェックしてくれるので精度が上がりました。 3. QA設計(大容量PDF):50ページ超でも一気に解析 QA設計では、操作手順書やイベント仕様書など、ページ数が多いPDFを読むことが頻繁にあります。 Claude Codeのみだと、20ページのPDFが上限で、それ以上だと読んでいなかったりすることがあるので、 分割して渡したり、md形式に変換したりと、ひと手間必要でした。 Gemini CLIの analyzeFile を使うと、50ページ超のPDFも一括で解析できます。 案件の仕様整理でも、必要な観点を抽出できました。 4. 画像解析:大容量・複数枚の一括処理 Figmaのデザイン画像やスクリーンショットを、AIに見せて確認してもらうケースは多いです。 Claude Codeのみでも画像は読めるのですが、大容量や複数枚をまとめて処理するのは少し苦手でした。 Geminiの analyzeFile を使うと、複数の画像を一気に渡してまとめて分析できます。 QAの表示確認テスト設計では、複数画面のFigmaデザインをまとめて確認した上、設計できるようになりました。 5. Web検索:最新情報を効率よく収集 Claude Codeにも組み込みのWeb検索機能はありますが、確認範囲が狭めで、たまに古い情報を返してくることがありました。 Geminiの googleSearch は、Google検索を直接使うので、最新情報を効率よく収集できます。 6. 並列処理:重い処理をバックグラウンドに逃せる 動画解析や大容量PDF解析は、どうしても時間がかかります。 Claude Codeのみで重い処理を回している間、メイン作業が止まってしまうのが地味にストレスでした。 Claude Codeにもサブエージェント機能はありますが、Geminiをサブエージェントとして併用すると、Claudeが苦手な動画や大容量PDFを任せられるだけでなく、Claudeのコンテキストを圧迫せずに重い処理を進められるメリットがあります。 また、別のAIモデルが裏で動いてくれるので、Geminiが動画解析している間もClaude側でテスト設計を進められるなど、メイン作業を止めずに並行作業できる(スピード早い)点も便利でした。 ここでは「Claude Code → Geminiサブエージェントに重い処理を委譲」という役割分担で活用してます。 効果が変わらなかったパターン コード生成:Claude Codeのみで十分 コード生成についてはClaude Codeのみで困りませんでした。 Webアプリ作成や自動化コードのレビューは、Claude Codeだけでも問題なく進められており、Geminiを足しても大きな差は感じませんでした。 「全部Geminiに頼った方がいい」というわけではなく、 得意分野を見極めて使い分けるのが大事だと感じます。 検証してわかったこと 「単独 → 併用」で何が変わる? 3週間使ってみた感想を、表に整理しました。 観点 Claude Codeのみ Claude Code × Gemini併用 扱える資料の範囲 コード・テキスト・画像・短いPDF 動画・大容量PDF・複数画像 レビューの質 自己レビュー(同一視点) 独立した第三者視点 情報の鮮度 やや古い情報を含むことあり 最新情報を効率よく収集 作業の並列性 サブエージェント並列は可能 重い処理をバックグラウンド処理 初心者が始めるなら、どう使い分ければいい? 「全部のユースケースで併用」となると、最初はハードルが高いので、 まずはClaude Codeを日常使いの主軸にして、以下のシーンでだけGeminiを呼び出すスタイルがおすすめです。 シーン 使うツール 普段のコード生成・テスト設計 Claude Code 動画を扱う時 Gemini CLI( analyzeFile ) 50ページ超のPDFを扱う時 Gemini CLI( analyzeFile ) 重要な変更のダブルチェック Gemini CLI( chat ) 最新情報の調査 Gemini CLI( googleSearch ) 上記で使ってみて、慣れてきたら使う場面を広げていきましょう。 おわりに 「Gemini CLIを社内検証で使ってみた!」というテーマで、 Claude Codeとの併用効果を紹介しました。 今後もAIを活用して、QA業務の効率化を進めていきたいと思います。 これからGemini CLIを試してみる方の参考になれば嬉しいです!
はじめに こんにちは。KINTOテクノロジーズ(以下、KTC)の AIファーストG に所属している、野村宏樹です。 このたび、2026年6月1日付で Microsoft MVP(Most Valuable Professional)を Microsoft Foundry カテゴリ で受賞しました。Azure 上での生成AI/AIエージェント開発を中心に発信してきた活動を評価いただいたもので、とても光栄に思っています。本記事では、受賞のご報告と、その背景にあったKTCでの活動についてご紹介します。 ![Microsoft MVP として届いたトロフィー](/assets/blog/authors/nomura/2026-06-11-microsoft-mvp-foundry/MVP-trophy.jpg =400x) Microsoft MVP として届いたトロフィー 受賞の証として、MVPプロフィールも公開されています。 https://mvp.microsoft.com/ja-JP/mvp/profile/93ebd9f1-a8c0-492c-8cc2-adc7f5f980e9 Microsoft MVP プロフィールページ Microsoft MVP とは Microsoft MVP は、Microsoft 製品・技術に関する深い知見と、技術コミュニティへの継続的な貢献を称えて Microsoft 社が「個人」に授与するアワードです。直近およそ1年間の活動が審査対象で、毎年の更新審査があります。技術領域ごとにカテゴリが分かれており、世界で約 3,000 名が受賞しています。受賞すると、製品の早期アクセスや製品チームと直接つながれるチャネル、年次の Global MVP Summit への招待などの機会があります。 私が受賞した Microsoft Foundry カテゴリ は、比較的新しいカテゴリ名です。 :::message Microsoft Foundry のカテゴリーは、Azure 上で生成AIアプリやAIエージェントを開発するための基盤に関する領域です。Azure AI Foundry がリブランディングされたもので、モデルやエージェントの開発・運用をまとめて扱うエコシステムを指します。 ::: 受賞につながった活動:KTCのカルチャーに支えられて 今回の受賞は、KTC の Output文化・登壇文化 、そして全社的に生成AIの業務活用を進めている環境に、大きく後押ししていただいたものでした。2025年8月にKTCへ入社して以来、「やってみたことを外に出す」「登壇して共有する」が当たり前にある雰囲気のなかで、自然と活動を続けることができました。 発信は、大きく2つの軸で行ってきました。 ① KTC事例の共有 KTCでは2023年から社内向けの生成AIチャットツールを導入し、全社で生成AIの活用を進めています。その現場で得た学びを、たとえば「社内で使うチャットアプリを自分たちで内製する意義」といったテーマで共有してきました。このテーマは、NoMaps 2025(札幌)で当時のチームリーダーである和田颯馬さんと共同で登壇しています。 https://no-maps.jp/program/tech/121500/ 社内で使うものを自分たちの手で作るからこそ、現場のフィードバックを素早く反映でき、業務に本当に必要な形へ磨き込んでいけます。そうした内製ならではの価値を、実体験ベースでお話ししました。 ② ユースケースを軸とした技術活用の共有 個人的に「面白い」「使えそう」と感じた技術を PoC で試し、ブログにまとめ、少し抽象化したものを登壇してOutputする、というサイクルで発信してきました。テーマは MCP(Model Context Protocol)、マルチエージェント(AutoGen / Microsoft Agent Framework)、Azure AI Foundry エコシステム、ローカルLLM/SLM(Foundry Local・phi-4)など。単に「何ができるか」だけでなく、 どんなユースケースで、どう使うか まで踏み込むことを意識しています。 コミュニティ活動としては、KTCが主催する 名古屋LLM MeetUp をはじめ、なごあず(JAZUG 名古屋支部)、JAZUG、すきやねん Azure など各地のコミュニティで登壇させていただきました。2025年度は個人として、ブログ31本・登壇11回ほどの活動になりました。 2025年度の登壇は以下のとおりです。 日付 イベント タイトル 2025/6/21 なごあず(JAZUG 名古屋支部) AzureでMCPサーバ!!どう活用する? 2025/7/23 名古屋LLM MeetUp(KTC主催) チャットアプリ失敗談!製造業業務への生成AI導入 2025/8/16 JAZUG×なんでもCopilot #jaznancopa Azure AI Foundry Portal デモ 2025/9/15 NoMaps 2025(札幌) 生成AI最前線:最新トレンドと活用事例(和田颯馬さんと共同) 2025/11/21 名古屋LLM MeetUp(KTC主催) AzureでのAIエージェントはここから!Azure Functions × AI 2025/11/27 YonaAz AzureでAIエージェント、さて何から始める? 2025/11/29 JAZUG Shizuoka リアルタイム音声モデル gpt-realtime を使った音声対話ツール 2025/12/6 なごあず(JAZUG 名古屋支部) ローカルとクラウドLLMのハイブリッドAI活用 2025/12/26 すきやねんAzure ハイブリッド構成 Queue Polling 2026/2/28 AgentCon Tokyo エージェント開発とライフサイクル管理 ~構築から AgentStore 基盤まで~ 2026/3/14 なごあず(JAZUG 名古屋支部) gpt-realtime-1.5 モデルでスタックチャン これから これからも、 「どう使うか(ユースケース)」を大事にしながら、技術のOutputを続けていきたい と思っています。AIにより技術のキャッチアップや開発は非常にしやすくなっています。大事なのはその手段をどこにどう使うと価値がでるのか?だと思っています。引き続きAzure・生成AI・AIエージェント領域の技術を突き詰めながら、ユースケースを軸にした発信を続けていきます。 改めて、日々の活動を支えてくれているKTCの環境と、関わってくださったみなさまに感謝します。 発信内容は、個人のZennにもまとめています。よろしければこちらもご覧ください。 https://zenn.dev/nomhiro 最後に KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください! 最後までお読みいただき、ありがとうございました。
Flutter SDK 3.29 → 3.38 へのアップグレード中に遭遇した retrofit / analyzer / custom_lint の依存衝突を解いた記録です。 「なぜ pub solver が答えを見つけられないのか」から順を追って説明します。 はじめに はじめまして、KINTOテクノロジーズ(KTC)でモバイルアプリ(Flutter)の開発を担当しているHand-Tomiです。 Flutter SDK のメジャーアップグレードを進めていたある日、 dart pub get が突然失敗するようになりました。エラーメッセージを読み解くと、 retrofit_generator と custom_lint がそれぞれ別々の analyzer バージョンを要求していて、両者が要求する analyzer のバージョン差はわずか 1 パッチ。けれど pub solver ではどうやっても解けない デッドロック でした。 本記事では、その原因と解決方法、そしてなぜ dependency_overrides が罠になるのかを順を追って解説します。同じ Flutter プロジェクトで似た衝突に遭遇した方の参考になれば幸いです。 :::message pub solver は dart pub get の内部で動く依存解決エンジンです。すべての制約を同時に満たすバージョンの組み合わせを探すのが役割で、本記事ではこの後も繰り返し登場します。 ::: :::message バージョン管理でよく耳にする SemVer (Semantic Versioning) は、バージョン番号を MAJOR.MINOR.PATCH の 3 桁で表す規約です。本記事では以降、それぞれ メジャー / マイナー / パッチ と表記します。 MAJOR (メジャー):互換性のない変更(壊れる) MINOR (マイナー):後方互換のある機能追加 PATCH (パッチ):後方互換のあるバグ修正 たとえば analyzer 8.4.0 → 8.4.1 は パッチ リリースなので、本来なら「コードを変えずに上げても安全」なはずです。本記事の 3 節で、この「はず」が崩れる仕組みを掘り下げます。 ::: TL;DR Flutter のメジャーアップグレード中に dart pub get が失敗。原因は retrofit_generator と custom_lint_visitor が同じ analyzer に対して別々のバージョンを要求していたこと。 最終的な解: retrofit: ^4.9.2 + retrofit_generator: ^10.2.1 。 pubspec のピン 2 行ですっきり解決します。 dependency_overrides には罠があり、推奨しません。 pub get は通っても dart_style が知らぬ間に昇格してビルドが壊れます。 この衝突は 構造的な問題 です。 analyzer のメジャーが上がるたびに再発します。 1. 始まり — 止まってしまったビルド Flutter SDK 3.29.2 から 3.38.10 へのメジャーアップグレードを進めていました。 flutter_riverpod 2 → 3、 freezed 2 → 3、 analyzer 6 → 8 といった大きな変更が立て続けに来ていて、いつもなら flutter upgrade のあと dart pub get で済む作業のはずでした。 ところがビルドが止まりました。要点だけ抜き出すと、こういうメッセージです。 And because retrofit_generator >=10.2.4 depends on analyzer >=8.4.1 <13.0.0 and custom_lint_core >=0.7.0 depends on custom_lint_visitor ^1.0.0, if retrofit_generator >=10.2.4 and custom_lint_core >=0.7.0 then analyzer 9.0.0. And because custom_lint >=0.8.1 depends on both analyzer ^8.0.0 and custom_lint_core 0.8.1, custom_lint >=0.8.1 is incompatible with retrofit_generator >=10.2.4. So, because app depends on both retrofit_generator ^10.2.5 and custom_lint ^0.8.1, version solving failed. pub solver が答えを見つけられなかったのです。片方を上げればもう片方が壊れ、下げればまた別のところが壊れる。普通のバージョン衝突ではなく、 デッドロック でした。 2. 誰と誰が戦っているのか 主な登場人物は次のとおりです。 analyzer — Dart コードの静的解析エンジン(共有資源) retrofit_generator — .g.dart を生成するコードジェネレータ custom_lint / custom_lint_core / custom_lint_builder — lint プラグインのランナーと、その builder custom_lint_visitor — analyzer の AST を訪問する visitor 実装 問題の核心は、 analyzer という共有資源 です。両陣営が同じ analyzer に対して別々のバージョンを要求しています。 retrofit_generator 10.2.3+ → 「 analyzer 8.4.1 以上が必要」 custom_lint_visitor 1.0.0+8.4.0 → 「 analyzer 8.4.0 ちょうど」 差は 8.4.0 と 8.4.1、 わずか 1 パッチ 。これだけでビルドが止まるのです。 なお、1 節のエラーメッセージ末尾には analyzer 9.0.0 も登場しますが、これは custom_lint_visitor に 1.0.0+8.4.0 のほかに 1.0.0+9.0.0 ビルドも存在し、 custom_lint_visitor: ^1.0.0 を介した solver が両方を順に試した結果です。どちらも analyzer をバージョン固定で要求する点は同じなので、本質的な対立点は変わりません。 3. なぜ 1 パッチ差で壊れるのか ここで 2 つの事実が噛み合います。 事実 1. analyzer の 内部 API はパッチリリースでも変わる SemVer の約束は「パッチリリースでは 公開 API は後方互換 を保つ」です。ところが custom_lint_visitor が使っているのは analyzer の公開 API ではなく、 内部 API (AST ノードの型など、パッケージの内部実装に属するもの)です。SemVer の保護範囲外なので、メジャー・パッチを問わず、型が消えたり、シグネチャが変わったりするのは珍しくありません。 後ほど引用するメンテナ自身の言葉を借りれば "some more unique APIs" — SemVer の通常のセーフティネットの外側で扱う必要のある API です。本記事の 5 節で扱う dart_style 3.1.9 の LabelReference / NamedArgument 欠落も、「内部 API は SemVer 保護外」という同じ構造から生じる事例の 1 つです(こちらは analyzer のメジャー間で起きたケースで、3 節でいうパッチ単位の例ではありません)。 事実 2. custom_lint_visitor はそれゆえ バージョンを完全に固定 する これを知っているからこそ、 custom_lint_visitor のメンテナは意図的に analyzer を完全に固定しています。パッケージ名そのものがその証拠です。 custom_lint_visitor 1.0.0+8.4.0 ^^^^^ analyzer のバージョン pubspec.yaml の中でも analyzer: 8.4.0 ( ^ (caret) なしのバージョン固定)になっています。 これがミスならば PR 一本で解決する話ですが、これは 関連する GitHub issue でメンテナ自身が明言した 意図的な方針 です。 "Custom_lint depends on some more unique APIs. I'll probably stick to requiring 8.0 for it." — invertase/dart_custom_lint#345 発言の直接の意図は「メジャー( 8.0 )単位で範囲を狭めて require する」ですが、その方針が実際のリリースにも反映されており、リリースされる custom_lint_visitor の各バージョンでは analyzer: 8.4.0 のように バージョンが完全に固定 されています( 1.0.0+8.4.0 → analyzer: 8.4.0 、 ^ (caret) なし)。つまり 意図された決定 の結果としてバージョン固定が生まれており、両者が同じ analyzer バージョンを要求するビルドが揃うまでは pub solver だけでは解けません。 4. 効果のなかった試みリスト 問題が難しく見えると、人は迂回路を探したくなります。しかし直感的に思いつく次の試みはどれも徒労でした。 試み なぜ失敗するのか retrofit_generator を最新(10.2.5)に上げる analyzer 8.4.1 を要求するため custom_lint_visitor と衝突 retrofit の上限を狭めてみる( <4.9.1 ) generator 10.2.1 を選びたい動機は 6 節で詳述しますが、retrofit を 4.9.0 系に下げると今度は generator 10.2.1 のソースが retrofit 4.9.2 の新 enum 値( Parser.DartMappable )を参照しているため、generator 自体の AOT コンパイルが Member not found で失敗します custom_lint_builder のダウングレード analyzer のメジャーが 7.x まで引きずり下ろされ、今度は retrofit_generator (analyzer 8.x 依存)と別の衝突を起こす — freezed / riverpod など analyzer 8 に依存するパッケージがある環境でも同様 analysis_options.yaml の lint を切る ( dependency_overrides で solver を通した後でも) lint を切って exclude: '**/*.g.dart' を両方適用しても、generator AOT 段階で発生する Member not found 系のコンパイルエラーはそのまま発生する dependency_overrides で強制固定 pub get は通るがビルド段階で dart_style が壊れる(5 節を参照) 特に最後の項目、 dependency_overrides は罠が深いので、別途取り上げる価値があります。 上の表の各行は、本記事と同じリポジトリの検証成果物( reports/01-reproduction.md 、 reports/03-overrides-fallback.md )で実際のコマンド出力として再現されています。 5. dependency_overrides という罠 最初の発想は単純です。「2 つのパッケージが争うなら、こちらで強制的に片方のバージョンを打ち込もう」。 dependency_overrides: retrofit: ^4.9.2 retrofit_generator: ^10.2.5 analyzer: ^8.4.1 驚くことに dart pub get は通ります。なぜなら dependency_overrides は オーバーライドした依存に対する他パッケージからの制約を黙らせ 、solver の選択肢を広げるからです。 ところが dart run build_runner build の段階で、突然ビルドが壊れます。 Failed to build build_runner:build_runner: .../dart_style-3.1.9/lib/src/front_end/ast_node_visitor.dart:1279:28: Error: Type 'LabelReference' not found. dart_style です。私たちが明示的に依存もしていないパッケージです。 理由を辿ってみると、次のようになっています。 dependency_overrides がオーバーライドした依存( retrofit 、 retrofit_generator 、 analyzer )に対する他パッケージからの制約を黙らせ、solver の選択肢が広がる その結果、solver は transitive で dart_style の最新版( 3.1.9 )を自動的に選ぶ dart_style 3.1.9 は analyzer の最新メジャーで導入された AST 型( LabelReference 、 NamedArgument 、 BlockEnumBody など)を参照している しかし私たちは override で analyzer ^8.4.1 (解決範囲は >=8.4.1 <9.0.0 )を強制している → その範囲には存在しない型を参照しようとしてコンパイル失敗 要するに dependency_overrides は制約を黙らせるだけで、互換性を保証しません。 一箇所を押さえるとまた別の場所から噴き出します。これを抑え込もうとすると dart_style もピン、 custom_lint_visitor も確認…… と際限なく増えていきます。 6. 結局解けた方法 — シンプルなピン調整 2 行 問題を逆から見ると答えが見えます。 私たちが変えられないもの: custom_lint_visitor 1.0.0+8.4.0 → analyzer 8.4.0 (正確には custom_lint_visitor 自体は 1.0.0+9.0.0 ビルドも存在しますが、それを選ぶと custom_lint 本体が要求する analyzer ^8.0.0 と衝突するため、 custom_lint を使う限り 8.4.0 ピン側に寄せるしかありません。1 節のエラーメッセージにも custom_lint >=0.8.1 depends on ... analyzer ^8.0.0 として現れています) 私たちが変えられるもの: retrofit_generator のバージョン であれば「 analyzer 8.4.0 でも動く最新の retrofit_generator 」を探せばよいわけです。 retrofit_generator のバージョン別要求を表にまとめると: retrofit_generator analyzer 要求 logError の呼び出し形式 10.2.0 >=7.7.1 <10.0.0 positional 4 個 10.2.1 >=8.0.0 <10.0.0 named ( response: _result ) 10.2.3 >=8.4.1 <11.0.0 named 10.2.4 / 10.2.5 >=8.4.1 <13.0.0 named 補足: retrofit.dart は monorepo で、 retrofit_generator (タグ v10.x.x )と retrofit (タグ retrofit-vX.Y.Z )を別系統で管理しています。本記事のリンクで prefix が混在するのはそのためです。なお 10.2.2 はリリースが存在しますが、本記事の議論には影響しないため上の表では省略しています。 答えが見えます。 10.2.1 です。 analyzer 8.4.0 と互換 ✓( >=8.0.0 なので) retrofit 4.9.2 の {Response? response} named optional シグネチャと互換 ✓ dependency_overrides 不要 ✓ dependencies: retrofit: ^4.9.2 dev_dependencies: retrofit_generator: ^10.2.1 これだけです。 ^10.2.1 というキャレット範囲を書いても、10.2.3+ は analyzer 8.4.1 を要求してくるので自動的に候補から外れ、実効的に 10.2.1 が選ばれます。 ちなみに retrofit_generator 10.2.1 と 10.2.5 の logError の呼び出しシグネチャは同一 です。generator のソース( lib/src/generator.dart )を直接比較しても、両バージョンとも '$_errorLoggerVar?.logError(e, s, $_optionsVar, response: $_resultVar);' という同一の出力テンプレートを使っています( v10.2.1#L3777 / v10.2.5#L3849 )。10.2.5 には Stream<Uint8List> / Stream<String> 処理の検証など別の機能が追加されていますが、本記事が扱う retrofit ↔ analyzer インターフェイスそのものは変更されていません。つまり 10.2.1 に留まることは、コア機能面で損ではありません。 7. それで私たちが学んだこと この件が片付いたとき、最初に浮かんだ考えは 「次のメジャーアップグレードでまた出くわすだろうな」 でした。 理由は 2 つです。 analyzer の内部 API は今後もパッチで変わり続ける。 それが AST を扱う解析器パッケージの本質です。 custom_lint_visitor は今後もバージョンを完全に固定し続ける。 メンテナが意図的に取っている方針だからです。 つまりこの衝突は 構造的 です。本記事を書いている 2026 年春の時点で、 analyzer はすでに 13.0.0 までリリースされており、 custom_lint_visitor のピンラインは 1.0.0+9.0.0 までしか追いついていません。 custom_lint_visitor がメジャーごとに 1 〜 2 個のビルドだけ追いつくこのまばらなパターンが続く限り、 analyzer がさらに一段上がるたびに同じ形で再発します。実際、 retrofit.dart の issue tracker を見ると analyzer 10.0 の段階でも同じシグネチャミスマッチが報告されています。 であれば、私たちにできることは: 自然な解決を先に試す。 ピン 1 つの調整で解けるかをまず確認する。シンプルな答えがあるのに dependency_overrides を最初に持ち出さない。 dependency_overrides は最後の手段。 黙らせるだけでは解決にならない。一箇所を押さえると別の場所から噴き出す。 プレイブックを残す。 次の人(あるいは 6 か月後の自分)が同じ罠にはまらないように。メカニズムと意思決定ツリーを一緒に書き残しておく(本記事自体がそのプレイブックの 1 つです)。 最後に ここまで読んでいただき、ありがとうございます。 analyzer のような共有依存をめぐる衝突は、一見すると「2 つのパッケージのバグ」に見えますが、実際にはエコシステム側の構造的な制約が背景にあります。同じ罠に出会ったときに「最初に何を疑い、何を試し、どこで止まるか」を整理できれば、次は数時間で解けるはずです。 皆さんの参考になれば幸いです。 参考 dart_custom_lint #345 — Support analyzer 8 retrofit.dart #911 — analyzer 10.0.0+ compatibility pub.dev — retrofit_generator バージョン別依存関係 retrofit 4.9.2 — Parser.DartMappable enum 追加箇所
※本記事は Claude Code との協働で執筆し、人間がレビューの上投稿しています。 1. はじめに こんにちは、共通サービス開発グループの鳥居( @yu_torii )です。 前回の記事では、Slack 上で LLM を活用する社内チャットボットの実装事例を紹介しました。 @ card 今回は、このテックブログの「関連する記事」と「関連する求人」機能をゼロから再構築した話をします。 「関連する記事」「関連する求人」とは 各記事ページの下部に、2つのレコメンドセクションがあります。 関連する記事: 現在読んでいる記事と内容が近い記事を最大12件表示 関連する求人: 記事の技術領域に関連する KINTO Technologies の求人情報を最大8件表示 読者が興味のある技術領域を深掘りする導線であり、過去の記事の発見にもつながります。採用への接点でもあります。 仕組みの基本:Embedding とコサイン類似度 この機能の核は Embedding (埋め込みベクトル)です。Embedding モデルにテキストを入力すると、その意味を表す数百〜数千次元の数値ベクトルが返ってきます。意味的に近いテキスト同士は、ベクトル空間上で近い位置に配置されます。 2 つのベクトルの「近さ」を測る指標が コサイン類似度 です。値が 1 に近いほど意味が近く、0 に近いほど無関係(直交)です。すべての記事を Embedding し、ペアごとにコサイン類似度を計算してスコアの高い順に並べれば、「関連する記事」のランキングが得られます。 旧システムの課題 この機能は以前、Python + Azure OpenAI の Embedding API で実装されていました。運用を続ける中で 3 つの問題が出てきました。 差分更新が無い。毎回全記事を再 Embed CI が走るたびに全記事(当時 900 件超)を Azure OpenAI に送って Embedding していました。1 記事の追加でも全件再処理が走り、ビルド時間の大半を占めていました。 Azure OpenAI の 429 (Rate Limit) エラーが頻発 900 件超の記事を一気に送ると、Azure OpenAI のレート制限に頻繁にヒットしていました。リトライロジックを入れてもタイミング次第で CI が失敗し、再実行が必要になることも珍しくありませんでした。 外部 API 依存 = コスト増加 Embedding API の呼び出し回数がビルドのたびに積み上がり、コストが増え続けていました。記事数が増えるほど状況は悪化する構造です。 今回やったこと これらの問題を解決するため、Go + Ollama(ローカル Embedding)でシステムを一から再構築しました。 SHA-256 ハッシュで変更記事だけ再 Embed する差分更新と、Ollama による CI ランナー上でのローカル実行(外部 API 呼び出しゼロ)で、旧システムの 3 つの課題を解消しました。 PoC でのモデル選定からパフォーマンス最適化、CI/CD パイプラインの構築まで、実装の全体像を書きます。開発には Claude Code を使いました(おまけで触れます)。 この記事で得られること Go + Ollama + Qwen3-Embedding でローカル Embedding による類似度計算を組む方法 Ollama num_ctx のサイレントトランケーション(無警告の文字切り詰め)問題 事前正規化と min-heap Top-K によるコサイン類似度ランキングの効率化 SHA-256 差分キャッシュで変更記事だけ再 Embed する仕組み :::message この記事の内容は執筆時点(2026年4月)の実装に基づいています。Ollama や Qwen3-Embedding のバージョンアップにより、API の仕様やパフォーマンス特性が変わる可能性があります。また、記事中のベンチマーク値は GitHub Actions ランナーでの計測結果であり、環境によって異なります。 ::: 2. PoC 検証とモデル選定 旧システムの課題(セクション 1 で述べた 429 エラー・全量実行・コスト増加)を解決するため、ローカル Embedding への移行を決めました。Go で使える Embedding ライブラリを 3 つの方式で PoC 検証しました。 3 つの PoC アプローチ 方式 1: hugot(Pure Go ONNX ランタイム) knights-analytics/hugot は Go ネイティブの ONNX ランタイムで、bge-m3 や Qwen3 の ONNX モデルを直接実行できます。cgo 不要ですが、ONNX モデルファイルのサイズが巨大(bge-m3 で約 2.2GB)で、CI 環境でのダウンロードとメモリ管理に課題がありました。 方式 2: kelindar/search(llama.cpp via purego) kelindar/search は一見 Pure Go に見えますが、内部では purego 経由で llama.cpp のバイナリを呼び出しています。cgo は使っていませんが、実質的に llama.cpp バイナリへの外部依存がありました。「cgo 不要」の表面的な特徴に惑わされかけた案件です。 方式 3: Ollama API(HTTP クライアント) 選んだのは Ollama の HTTP API を Go クライアントから呼ぶ方式です。 client, err := api.ClientFromEnvironment() if err != nil { slog.Error("Ollama クライアント作成失敗", "error", err) os.Exit(1) } resp, err := client.Embed(ctx, &api.EmbedRequest{ Model: model, Input: testTexts, }) 比較表 方式 cgo モデル管理 バッチ対応 コンテキスト制御 判定 hugot (ONNX) 不要 手動 ○ × △ モデルサイズ問題 kelindar (llama.cpp) purego 経由で不要に見えるが llama.cpp バイナリ依存 手動 × × × 実質外部依存 Ollama API 不要 自動 ○ ○ ( num_ctx ) ◎ 選定の決め手 cgo 不要で GOOS=linux GOARCH=arm64 go build 一発のクロスコンパイルが壊れない。Ollama がモデルのダウンロードからライフサイクル管理まで担う。バッチ Embed API で複数テキストを一度に送信できる。 num_ctx でコンテキストウィンドウを明示制御できる。 なぜ Qwen3-Embedding-0.6B か Qwen3-Embedding-0.6B を選んだ理由は、2025 年リリースの最新モデルで、量子化後 639MB と CI ランナーのメモリに収まるサイズだったこと。1024 次元ベクトルで表現力と計算量のバランスが良い。日本語・英語のバイリンガルサポートは、当ブログの運用上の必須要件でした。RAG の検索精度が求められるタスクではなく関連記事の推薦用途なので、最高精度モデルは不要です。 :::details 量子化とは 量子化(Quantization)は、モデルの重み(パラメータ)を元の精度(通常 float16 = 16bit)からより少ないビット数(8bit、4bit など)に変換する手法です。精度はわずかに低下しますが、モデルサイズとメモリ使用量を大幅に削減できます。 Qwen3-Embedding-0.6B は Ollama で Q8_0(8bit 量子化) として配布されており、595M パラメータで 639MB。一方、bge-m3 は F16(16bit)配布のため、パラメータ数はほぼ同じ(568M)でもサイズが 1.2GB と約 2 倍になります。 ::: :::message PoC の段階では bge-m3 も候補でしたが、モデルサイズだけでなくベンチマークでも Qwen3 が優位でした。 MTEB ベンチマーク の英語検索(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 を選択しました。 ::: 3. アーキテクチャの全体像 パイプライン flowchart LR A["_posts/*.md"] --> B["Markdown<br>クリーニング"] B --> C["Ollama Embed API<br>(Qwen3-Embedding)"] C --> D["SHA-256<br>キャッシュ"] D --> E["コサイン類似度<br>ランキング"] E --> F["related_posts.json"] Markdown をクリーニングして Ollama で Embedding を取得し、コサイン類似度でランキングして JSON を出力します。 パッケージ構成 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 internal パッケージに分離することで、各パッケージが単一責任を持ち、独立してテスト可能になっています。 run() 関数のパイプライン メイン処理は run() 関数に集約されています。 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) } Next.js フロントエンドとの連携 出力される JSON は Next.js の getStaticProps でビルド時に読み込まれます。 static/related_posts/related_posts.json → lib/related_posts.ts が読み込み フロントエンド側では、JSON に関連記事データがあればそれを使い、無ければカテゴリベースのフォールバックに切り替わります。Go CLI とフロントエンドの間の契約は、この JSON スキーマだけです。 4. Markdown のクリーニングと前処理 当ブログの記事は Zenn Markdown ( :::message 、 :::details 、 @[card]() など)で書かれています。各記事ファイルの先頭には YAML frontmatter(タイトル、著者、公開日、カテゴリなどのメタ情報)があり、これらをそのまま Embed するとノイズになります。 クリーニングパイプライン frontmatter の分離: --- で囲まれた YAML ヘッダーからタイトルだけ抽出し、残りのメタ情報(author, date, category 等)は除去 URL の除去: http:// / https:// で始まるすべての URL を除去 アセットリンクの除去: /assets/ を含むリンク(画像パスなど)を除去 クリーニングのエントリポイントは CleanMarkdown 関数で、frontmatter からタイトルを抽出しつつ、本文のノイズを除去します。frontmatter パースには strings.Cut を使い、 --- デリミタ間の YAML を gopkg.in/yaml.v3 で解析しています。 :::details コードの詳細(cleaner.go / parser.go) var ( reURL = regexp.MustCompile(`https?://[^\s)\]>]+`) reAsset = regexp.MustCompile(`!?\[[^\]]*\]\(/assets/[^)]+\)|/assets/[^\s)]+`) ) func CleanMarkdown(raw []byte) (title, content string) { s := string(raw) if len(s) == 0 { return "", "" } title, body := splitFrontmatter(s) body = removeURLs(body) body = removeAssetLinks(body) return title, body } func splitFrontmatter(s string) (title, body string) { const delimiter = "---" _, after, ok := strings.Cut(s, delimiter) if !ok { return "", s } before, after, ok := strings.Cut(after, delimiter) if !ok { return "", s } var fm frontmatter if err := yaml.Unmarshal([]byte(before), &fm); err == nil { title = fm.Title } return title, after } 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 } ::: ポイントは、Embedding 時にタイトルをテキストの先頭に結合すること( title + "\n" + content )。セクション 5.1 で述べますが、Embedding モデルはテキストの先頭部分を重視する傾向があるため、タイトルの情報がベクトルに強く反映されます。 5. Embedding の最適化 Embedding 処理の高速化で 2 つの工夫をしました。 5.1 : テキストを先頭 4,000 文字に切り詰めて処理時間を約 1/8 に短縮 5.2 : 実装中に踏んだ Ollama num_ctx の無警告切り詰め問題 5.1 テキスト切り詰めの最適化 最初は記事の全文をそのまま Ollama に送っていました。CI で実行すると、全記事の Embedding に数十時間かかる計算です。全文が本当に必要なのか、検証しました。 まず、全記事のクリーニング済みテキスト長の分布を調べました。 平均: 約 8,000 文字 中央値: 約 6,300 文字 上位 10%: 14,600 文字以上 最大: 53,000 文字超 大半の記事は 10,000 文字以内に収まりますが、一部の長文記事は 40,000 文字を超えます。長い記事の後半には参考文献リストや補足情報が多く、記事のテーマを表す情報は先頭に集中する傾向がありました。 そこで「先頭 N 文字に切り詰めても品質を維持できるか?」を検証するため、長文の上位 5 記事で 全文 Embedding( num_ctx=8192 明示指定)をベースライン として、切り詰め文字数を変えて類似度と速度を比較しました。 切り詰め ベースラインとの類似度 平均速度 高速化 全文 1.000 229 秒 1.0x 2,000 文字 0.868 13 秒 17.6x 4,000 文字 0.887 29 秒 7.9x 6,000 文字 0.902 42 秒 5.5x 8,000 文字 0.909 53 秒 4.3x 4,000 → 8,000 文字に増やしても類似度の改善は +2.2 ポイント (0.887 → 0.909)に留まりますが、速度は 1.8 倍遅くなります。関連記事のランキング品質に影響が出ないことを本番データで確認した上で、 先頭 4,000 文字 + num_ctx=8192 を採用しました。 :::message なぜ先頭の切り詰めが有効か? 2 つの要因が相乗しています。 モデルの位置バイアス : Transformer ベースの Embedding モデルでは、テキスト先頭への撹乱がベクトルに与える影響が末尾より約 15% 大きいことが報告されています( arXiv:2412.15241 )。Qwen3-Embedding も RoPE を採用した Transformer モデルであり、同様の傾向があると考えられます。 コンテンツの構造バイアス : 技術ブログは「タイトル→導入→概要→詳細」の逆ピラミッド構造を持ち、テーマ情報が冒頭に集中します(いわゆる Lead Bias )。 ::: 5.2 Ollama の num_ctx に潜む落とし穴 5.1 の検証に入る前に、 num_ctx 周りで罠を踏みました。Ollama で Embedding を扱う人は全員引っかかりうる問題です。 何が起きたか 切り詰めを検証する前に、まず num_ctx の効果を確認しようと次の 2 パターンで全文 Embedding を比較しました。 A: 全文 + num_ctx=4096 B: 全文 + num_ctx=8192 A と B のコサイン類似度が全記事で 1.000 でした。完全に同一のベクトルです。処理時間も平均約 70 秒で差がない。35,000 文字超の記事でコンテキスト長を倍にしたのに、結果が変わっていません。 記事 文字数 平均処理時間(秒) A-B 類似度 torii-ai_tool_slack 35,417 68 1.000 Android-Compose-OO-Nav 37,803 76 1.000 aurora-mysql-stats 32,648 71 1.000 Jetpack-Compose-Anim 34,621 65 1.000 SecureDBPassword 38,978 69 1.000 平均 約 70 秒 1.000 原因: Options に入れないと num_ctx は効かない num_ctx を EmbedRequest.Options で 明示的に渡さない限り 、Ollama は VRAM に応じたデフォルト値 (24GiB 未満で 4k、24-48GiB で 32k、48GiB 以上で 256k。 OLLAMA_CONTEXT_LENGTH 環境変数で変更可能)を使い、超過分を無警告で切り詰めます。 パターン B で num_ctx=8192 を設定したつもりが、API の Options に渡されておらず、A と同じ 4096 トークンで処理されていました。類似度 1.000 は、両方とも同じ入力を処理していた証拠です。 :::message alert 注意: Ollama は入力テキストがコンテキスト長を超えてもエラーを返しません。API レスポンスにも切り詰めの有無を示すフィールドがありません。意図せず不完全な Embedding が生成される可能性があります。これは Ollama の Issue #14259 でも報告されています。 ::: 修正と効果の確認 num_ctx を EmbedRequest.Options で明示的に渡すよう修正したのが、次の実装です。 func (c *Client) Embed(ctx context.Context, texts []string) ([][]float32, error) { req := &api.EmbedRequest{ Model: c.model, Input: texts, } if c.numCtx > 0 { req.Options = map[string]any{"num_ctx": c.numCtx} } resp, err := c.api.Embed(ctx, req) if err != nil { return nil, fmt.Errorf("Ollama Embed API エラー: %w", err) } // レスポンスのバリデーション(件数・空ベクトルチェック) if len(resp.Embeddings) != len(texts) { return nil, fmt.Errorf("レスポンス数が不一致: %d embeddings / %d texts", len(resp.Embeddings), len(texts)) } return resp.Embeddings, nil } 修正後は A-B 類似度が 0.947 に下がり、B の処理時間は A の約 3 倍(229 秒 vs 78 秒)になりました。8192 トークン分を処理していることが時間からも裏付けられます。 記事 文字数 A(秒) B(秒) A-B 類似度 torii-ai_tool_slack 35,417 77 224 0.969 Android-Compose-OO-Nav 37,803 81 218 0.920 aurora-mysql-stats 32,648 84 224 0.919 Jetpack-Compose-Anim 34,621 74 243 0.947 SecureDBPassword 38,978 73 238 0.977 平均 78 229 0.947 CLI のデフォルト値は --num-ctx=8192 に設定し、4000 文字切り詰めと組み合わせることで無警告の文字切り詰めが発生しないことを保証しています。 Ollama 利用者への教訓 Ollama で Embedding や LLM を扱うなら: num_ctx は Modelfile の PARAMETER か、API の Options.num_ctx で 明示的に設定する 入力のトークン数を事前に把握し、コンテキスト長に収まるか確認する 類似度や品質が「なぜか変わらない」ときは、無警告切り詰めを疑う 5.3 コードブロックは残すべきか? 先頭 4000 文字のうち、コードブロックが大量に含まれる記事があります。Android Compose のナビゲーション記事では 2,213 文字(55%超)がコードでした。コードを除去して本文を増やす方が良さそうに思えます。 日英翻訳ペア(同じ postId で locale が異なる記事)のコサイン類似度で検証しました。 コードブロックあり: 0.893 コードブロック除去: 0.868 コードブロックを除去すると類似度が下がりました。 クラス名、関数名、ライブラリ名( NavHost 、 Composable 、 goroutine など)は言語に依存しません。日本語の記事でも英語の記事でも、同じ技術ならコード中に同じキーワードが出現します。コードブロックはクリーニング対象から除外(残す)としました。 切り詰めの実装 const maxEmbedRunes = 4000 for _, p := range dirty { text := p.Title + "\n" + p.Content if p.Content == "" { text = p.Title } if runes := []rune(text); len(runes) > maxEmbedRunes { text = string(runes[:maxEmbedRunes]) } vectors, err := client.Embed(ctx, []string{text}) // ... } []rune に変換してからスライスすることで、マルチバイト文字(日本語)の途中で切れることを防いでいます。 6. SHA-256 差分キャッシュによる効率化 セクション 1 で述べた「毎回全量実行」の問題を解決するため、差分キャッシュを導入しました。「前回から何が変わったか」を高速に判定する必要がありますが、ファイルの更新日時(mtime)は Git のチェックアウトでリセットされるため CI 環境では使えません。そこで、コンテンツ自体の SHA-256 ハッシュで変更を検知する方式を採用しました。 キャッシュの設計 type Cache struct { Version int `json:"version"` ModelName string `json:"model_name"` Entries map[string]CacheEntry `json:"entries"` } type CacheEntry struct { ContentHash string `json:"content_hash"` Vector []float32 `json:"vector"` } SHA-256 による変更検知 記事のタイトルと本文を結合して SHA-256 ハッシュを計算し、前回のキャッシュと比較します。 func ContentHash(title, content string) string { h := sha256.New() h.Write([]byte(title + "\n" + 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 } モデル名が変わると全記事が dirty になります。Embedding モデルが変われば次元数やベクトル空間が異なるため、古いキャッシュは無効です。 キャッシュフロー flowchart TB A["記事読み込み<br>(956件)"] --> B["キャッシュ読み込み"] B --> C{"モデル変更?"} C -->|Yes| D["全記事をEmbed"] C -->|No| E["SHA-256比較"] E --> F{"変更あり?"} F -->|Yes| G["変更分のみEmbed"] F -->|No| H["スキップ"] D --> I["1件ずつ保存<br>(中断耐性)"] G --> I Embed のたびにキャッシュファイルを保存します。CI のタイムアウトや中断が起きても、それまで処理した分はキャッシュに残ります。次回実行時は中断箇所から再開できるため、初回の全量 Embedding を複数回に分けて進められます。 初回構築で効いた「中断耐性」 この「1 記事ごとに cache ファイルへ保存」という設計が、初回構築で実際に役に立ちました。 当時 956 件あった全記事の初回全量ビルドでは、Ollama での Embedding 処理が GitHub Actions の job timeout( timeout-minutes: 60 )に収まらず、5 回連続で 60 分 timeout に到達しました。それでも 6 回目の run で完走できたのは、各 cancelled run で完了していた分の Embedding が次の run に引き継がれたからです。 run 結果 Generate related content 1 〜 5 回目 timeout 各 60 分 6 回目 success 55 分 累計 約 6 時間 これを成立させたのは 2 つの噛み合わせです。 アプリ側 : 1 記事 Embed するごとに output/embeddings_cache.json へ保存 CI 側 : actions/cache/save@v5 を if: always() で走らせる - 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('_posts/**') }}-${{ github.run_id }} if: always() を付けておくと、job が timeout/cancel で終わるときにも cache save ステップが走ります。結果、途中まで処理した Embedding は cache に残り、次 run は restore-keys のフォールバックで前 run の cache を拾って残り分から続行できる。 この仕組みがなければ、60 分 timeout で毎回 Embedding が巻き戻り、6 時間で完走することはなかったはずです。 7. コサイン類似度ランキングの最適化 Embedding ベクトルが得られたら、記事間の類似度を計算してランキングを生成します。956 記事の各記事が他の 955 件と比較するため、約 91 万回の内積計算が走ります。この規模なら FAISS 等の ANN(近似最近傍探索)ライブラリを導入するよりも、brute-force の方がシンプルで依存も増えません。 最初の実装(毎回ノルム計算 + 全件ソート)ではテストで約 1.6 秒かかっていました。事前正規化 + min-heap への変更と、ループアンローリングの 2 段階で 730ms まで改善しました。 :::::details 最適化の詳細 1. 事前正規化 (Pre-normalization) コサイン類似度の式は以下です。 $$ \cos(a, b) = \frac{a \cdot b}{|a| \times |b|} $$ 毎回 2 つのベクトルの長さ(ノルム $|a|$)を計算するのは無駄なので、全ベクトルの長さを事前に 1 に揃えておきます(正規化)。すると分母が $1 \times 1 = 1$ になり、コサイン類似度は内積 $a \cdot b$(各要素を掛けて足すだけ)と等しくなります。正規化は記事数分(956回)だけ。その後の 91 万回のペア比較では掛け算と足し算だけで済みます。 :::details コサイン類似度の補足 内積 $a \cdot b$ は 2 つのベクトルの各要素を掛けて足した値です。意味が近い記事同士は内積が大きくなりますが、長い記事のベクトルは値が大きくなりがちで、内積だけだと「ベクトルの長さ」に引っ張られます。ノルム $|a|$ で割ることで長さの影響を消し、純粋に「向き」(意味の近さ)だけを比較するのがコサイン類似度です。結果は $-1$ 〜 $1$ の範囲で、1 に近いほど意味が近い。 正規化とは、各要素をノルムで割ってベクトルの長さを 1 にする処理です。向きはそのまま、長さだけ揃えます。 元: a = [3, 4] → 長さ = √(9+16) = 5 正規化: a' = [0.6, 0.8] → 長さ = √(0.36+0.64) = 1 ::: 2. min-heap Top-K 全 955 件のスコアを sort.Slice でソートしていましたが、実際に必要なのは上位 12 件だけ。サイズ 12 の min-heap(Go 標準ライブラリの container/heap )を使い、スコアが最小値より大きければ入れ替える方式に変更。計算量は $O(N \log N)$ から $O(N \log K)$ に改善します。 3. ループアンローリング 内積計算のホットパス(約 91 万回 × 1024 次元)に 4-way ループアンローリングを適用。4 つの独立したアキュムレータ変数を使うことで、前のループ結果への依存を断ち切り、CPU が乗算と加算を並列実行できるようになります。 :::details ループアンローリングの補足 通常のループでは 1 つの変数 sum に順番に足していきます。 sum += a[0]*b[0] の結果が出るまで次の sum += a[1]*b[1] が始められません(データ依存)。 4-way では 4 つの変数 s0, s1, s2, s3 に分けて、それぞれ独立に計算します。CPU は依存関係のない命令を同時に実行できるため(命令レベル並列性)、4 つの乗算・加算が並列に走ります。最後に s0 + s1 + s2 + s3 で合計するだけです。 通常: 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 ::: // 事前正規化: 全ベクトルのノルムを 1 にする normalized := normalizeAll(slugs, vectors) // min-heap Top-K: 上位 maxResults 件だけを効率的に抽出 h := &minHeap{} for j, other := range slugs { if i == j { continue } score := dotProduct(vi, normalized[j]) if h.Len() < maxResults { heap.Push(h, ScoredItem{Key: other, Score: score}) } else if score > (*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 <= 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 < n; i++ { s0 += a[i] * b[i] } return s0 + s1 + s2 + s3 } ::::: パフォーマンス推移 段階 手法 ランキング処理時間(956記事) 初期 毎回ノルム計算 + sort.Slice ~1.58s 1 事前正規化 + min-heap Top-K ~1.18s 2 + ループアンローリング(4-way) 730ms 最終的なスペック: 指標 値 記事数 956 件 ベクトル次元数 1024 類似度計算回数 約 912,980 回(956 × 955) ランキング処理時間 730ms なお、Go の map はイテレーション順序が非決定的です。同じ入力に対して常に同じ JSON 出力を得るため、 slices.Sort でスラッグをソートしてから処理しています。これを忘れると CI のたびに diff が発生し、不要なコミットが生まれてしまいます。 8. GitHub Actions での CI/CD ワークフロー全体像 flowchart LR A["create-branch"] --> B["generate-related-content<br>(ARM runner + Ollama)"] A --> C["generate-metadata"] A --> D["generate-search-index"] B --> E["create-pull-request"] C --> E D --> E E --> F["auto-merge"] create-branch でブランチを作成した後、3 つのジョブが並列実行されます。 ARM ランナーの選択 Embedding 処理には arm-ubuntu-latest-4 ランナーを使用しています。GitHub の ARM ランナーは x86 の約半額(1分あたり $0.004 vs $0.008)で、初回の全量 Embedding のように数時間かかるジョブではコスト差が大きくなります。 Ollama モデルキャッシュ 639MB のモデルファイルを毎回ダウンロードしないため、 actions/cache でキャッシュします。 cache/restore + cache/save パターン :::message actions/cache@v5 の save-always オプションは非推奨になりました。代わりに cache/restore と cache/save を分離し、 cache/save に if: always() を付けるパターンを使います。 ::: - name: Restore embeddings cache uses: actions/cache/restore@v5 with: path: output/embeddings_cache.json key: embeddings-cache-${{ hashFiles('_posts/**') }}-${{ 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('_posts/**') }}-${{ github.run_id }} if: always() により、タイムアウト時でもキャッシュを保存します。セクション 6 の「1 件ずつ保存」と組み合わせて、中断と再実行を繰り返してもキャッシュが蓄積されます。 キャッシュキーに run_id を付ける理由 GitHub Actions のキャッシュは同じキーで上書きできません(イミュータブル)。これはタイムアウト→再実行のパターンで問題になります。 run_id なしの場合: key: embeddings-cache-abc123 1回目: save "abc123" → ✅ 200記事分保存 2回目: restore "abc123" → 200記事復元 → 追加200記事 → save "abc123" → ❌ キーが既に存在 3回目: restore "abc123" → 1回目の200記事分しかない(2回目の成果が消えた) run_id ありの場合: save key: embeddings-cache-abc123-{run_id} ← 毎回ユニーク restore-keys: embeddings-cache-abc123- ← プレフィックス一致で最新を取得 1回目: save "abc123-100" → ✅ 200記事分 2回目: restore "abc123-" → run100から200記事復元 → 追加200記事 → save "abc123-200" → ✅ 400記事分 3回目: restore "abc123-" → run200から400記事復元 → 続きから push のリトライロジック 3 つのジョブが並列でブランチに push するため、競合が発生します。指数バックオフ付きのリトライで対処します。 pushed=false for i in 1 2 3 4 5; do git pull --rebase origin "$BRANCH_NAME" && git push origin "$BRANCH_NAME" && pushed=true && break echo "Push failed (attempt $i), retrying..." sleep $((i * 2)) done [ "$pushed" = "true" ] || { echo "ERROR: All push attempts failed"; exit 1; } 古いブランチの問題 自動生成用ブランチが前回の実行から残っている場合、古いコードがベースになります。 git reset --hard ${{ github.sha }} で毎回トリガー元の最新コミットにリセットします。 workflow_dispatch でのテスト実行 main にマージ前の動作確認では workflow_dispatch トリガーを一時的に追加しました。ただし、GUI の Actions タブにはデフォルトブランチのワークフローしか表示されないため、feature ブランチの workflow_dispatch は GUI から実行できません。 CLI 経由であれば --ref でブランチを指定して実行可能です。 gh workflow run "Auto Create Related Data" --ref feat/related-content-gen-go-rewrite 9. 実運用で見えた効果 旧システム(Python + Azure OpenAI)から新システム(Go + Ollama)への移行で、セクション 1 で挙げた 3 つの課題はそれぞれ次のように変わりました。 課題 旧(Python + Azure OpenAI) 新(Go + Ollama) 実行戦略 毎回全量 Embed(900+ 件) 差分のみ Embed(SHA-256 ハッシュ比較) Rate Limit (429) 頻発・リトライで不安定 構造的に発生しない(外部 API なし) 推論コスト 従量課金(Azure OpenAI) ゼロ(CI ランナー内完結) 比較すべきは単発の処理秒数ではなく、「記事追加のたびに全量再計算が必要か」「外部 API 制約に運用が振り回されるか」という運用特性です。旧は Azure のマネージド並列推論、新は self-hosted CI ランナー 1 台のシーケンシャル処理で、そもそも尺度が違います。 差分更新時の実測例 959 記事中 49 件(5%)が dirty だった run では、 19 分 21 秒で完走 しました(self-hosted runner 1 台・逐次処理で 1 記事あたり約 22〜24 秒)。差分ゼロなら Embed はスキップされ、ランキング計算と出力だけで 1〜2 分で完了します。 残課題 dirty が 150 件を超える状況(cache eviction 直後や cron が長期間失敗していたあとなど)では timeout-minutes: 60 に収まらないことがあります。現状は複数 run に分けて進捗を積み上げる設計でカバーしていますが、次の打ち手として timeout 延長と output/embeddings_cache.json の git 管理化が候補です GitHub Actions cache は 7 日アクセスなしで自動 eviction されるため、週次 cron(月曜 9 時)で Restore を触って keep-warm しています。より確実にするなら git 管理化か、S3 などの外部 storage に寄せる手もあります 10. まとめ 本記事では、関連記事のレコメンドシステムを Go + Ollama(ローカル Embedding)で再構築した過程を紹介しました。なお、関連求人についても同様の Embedding + コサイン類似度の仕組みで生成しています。 項目 結果 対象記事数 960 件前後(執筆時点) ランキング計算 730ms(Embedding 生成は含まず、測定時点 956 件) テキスト切り詰め 先頭 4000 文字で全文比 88.7% の類似度を維持 差分キャッシュ 差分ゼロなら 1〜2 分、少数差分なら数分〜十数分 外部依存 Ollama + Qwen3-Embedding(API キー不要) SHA-256 差分キャッシュで変更記事だけを再 Embed し、ランキングは事前正規化と min-heap Top-K で 730ms(956記事のペアワイズ計算)。外部 API 依存を排除して、429 エラーとコストの問題を解消しました。 初回の全量 Embedding は CPU ランナーで数時間かかり、モデル変更や初期導入時にも同じコストを払うことになります。扱い方はセクション 6 と 9 に書いた通りで、GPU ランナーが使えれば改善しますが、現時点では CI の制約です。 もう 1 つ、推薦品質の定量評価がまだありません。「Embedding の類似度が 88.7% 保たれている」ことと「関連記事の推薦が妥当である」ことは別の問題です。旧システムとの Top-K 一致率や、クリックスルー率の計測が残っています。 テキスト切り詰めも改善の余地があります。現在は先頭 4000 文字をルーン単位でカットしていますが、文の途中で切れる可能性があります。句点( 。 )や改行の位置で切る方が、Embedding の入力としてはクリーンです。今回のユースケースでは影響は軽微ですが、精度を追求する場合は検討に値します。 11. この仕組みの応用可能性 「ローカル Embedding + コサイン類似度 + 差分キャッシュ」の仕組みは、ブログの関連記事に限りません。Confluence や Notion の社内ドキュメントを同じパイプラインで Embedding すれば、「この仕様書に関連するドキュメント」を自動提示できます。Ollama はローカル実行なので、社外に送信できない社内文書でも扱えます。 SHA-256 差分キャッシュと 1 件ずつ保存の中断耐性パターンはそのまま流用できます。Ollama + 軽量モデルなら API キー不要で CI でもローカルでも動きます。 おまけ: Claude Code との開発プロセス 今回の開発は Claude Code とのペアプログラミングで進めました。 kairo による開発ワークフロー 開発ワークフローにはクラスメソッド社の tsumiki の kairo を使いました。kairo は Claude Code 向けのスキルで、4 つのコマンドでソフトウェア開発を進めます。 kairo-requirements : EARS 記法で機能・非機能要件を定義。今回は 3 方式の PoC 比較(ONNX / llama.cpp / Ollama)もこのフェーズで実行しました kairo-design : 要件からアーキテクチャ図、データフロー、型定義を生成 kairo-tasks : 設計を実装タスクに分割。依存関係とテストケースも定義。今回は 10 タスク・3 フェーズに分解 kairo-loop : タスクを 1 つずつ Red → Green → Refactor の TDD サイクルで実装。7 タスクをこのコマンドで回しました PR レビュー 実装後の PR レビューでは、Claude Code に以下のように指示しました。 /pr-review-toolkit:review-pr all pr-review-toolkit は Anthropic 公式の Claude Code プラグインで、6 種のレビューエージェント(コード品質、エラーハンドリング、テストカバレッジ、コメント整合性、型設計、コード簡素化)が並列にレビューします。セクション 5.2 のレスポンスバリデーション(件数・空ベクトルチェック)は、このレビューで指摘された問題への対応です。 Go 1.26 での最適化 Claude Code に「Go 1.26 で最適化して」と指示しました。 go fix による自動変換( strings.Index → strings.Cut 、 sort.Strings → slices.Sort 、 context.Background() → t.Context() など)に加え、新しい言語機能やライブラリ API を活用したリファクタリングも実施されました。 記事の執筆・校正 この記事自体も Claude Code で執筆しています。校正には 3 つのツールを使いました。 textlint + ja-technical-writing : 冗長表現や接続詞の重複など、日本語の技術文書向け校正 skill-deslop : AI 生成文章に特有の冗長パターン(回りくどい前置き、受動態の多用など)の検出・除去 Codex plugin for Claude Code : OpenAI 公式の Claude Code プラグインで、Codex CLI をサブエージェントとして呼び出します。記事全体の論理破綻や数値矛盾のチェックに使いました。実験データ更新に伴う数値の不整合やコードスニペットの変数名不一致など、人間のレビューでは見落としやすい問題を検出できました ここまで読んでいただきありがとうございました。何かの参考になれば幸いです。なお、この記事の下部に表示されている「関連する記事」と「関連する求人」が、本記事で紹介した仕組みで生成された実物です。
はじめに こんにちは、 Cloud Infrastructure G の山中です! 「拠点のグローバル IP が変わったので、AWS WAF の allowlist を更新してください」というよくある作業を引き受けたところ、半日以上溶かしました。 原因は 同じシステムの中に WAF が 2 系統 存在しており、片方を更新しても、もう片方が古い IP リストを握っていたためです。さらに Amplify Console の Firewall UI が「Firewall: 無効」と表示しているのに、API で確認すると WebACL がしっかり attach されている という UI と実態の乖離まで重なり、見えている情報をそのまま信じてよいか判断がつかない状況でした。 この記事では、その 2 系統 WAF の全体像と、UI を信用できないときに API で実態を確認する手順を共有します。同じような構成(Amplify Hosting + CDK 管理の WAF)を運用している方の参考になれば幸いです。 TL;DR 拠点 IP 変更で全環境 403 が発生 Backend API 側の WAF(CDK 管理)は Amplify の環境変数 + rebuild で解消できた しかしフロントエンド側は Amplify Hosting が自動管理する別 WAF ( AmplifyIPSet-* )が原因で 403 が残った さらに Amplify Console の Firewall UI は「Firewall: 無効」表示なのに、API では WebACL が attach 済み・ default action が Block だった aws wafv2 list-resources-for-web-acl の --resource-type を AMPLIFY にしないと、attach 状態が見えない罠も踏んだ 最終的に aws wafv2 update-ip-set で直接 IPSet を書き換えて解決 教訓: Amplify Hosting の WAF は UI を信用せず、API で実態を確認すべし 背景 サービス構成 ユーザー向けに提供予定のフロントエンド + バックエンド API のシステムで、構成は以下の通りです。 バックエンド: AWS Amplify Gen 2(CDK で WAF や CloudFront を含むインフラを定義) フロントエンド: AWS Amplify Hosting Gen 1(platform=WEB) CDN: CloudFront WAF: AWS WAFv2 環境: dev / stage / prod(それぞれ別 AWS アカウント) アクセス制御の方針 まだリリース前のシステムのため、複数拠点からのオフィス IP のみを許可しています。 拠点が増減したり、回線変更で IP が変わったりすると、各環境の WAF の IPSet を更新する必要があります。 出来事 ある日「拠点 A が新しい IP に切り替わるので、X 日までに各環境の allowlist を入れ替えてほしい」という依頼が来ました。 作業手順は社内に整備されていたので、淡々と進めていたつもりだったのですが、ここから泥沼に入っていきます。 WAF が 2 系統に分かれている全体像 最初に結論を絵にしておきます。後の章で何度もこの絵に戻ってきます。 ポイントは、 同じ「拠点 IP 許可」という概念を、別々のリソースとして 2 箇所で独立に管理している ことです。 Backend API 側: CDK / CloudFormation で IPSet を定義 → Amplify の環境変数 WAF_ALLOWED_IP_LIST を更新して rebuild すれば IPSet が書き換わる、という仕組みを自前で組んでいる Frontend 側: Amplify Hosting の「Firewall(AWS WAF 統合)」機能を有効にすると、Amplify サービス側で勝手に IPSet と WebACL を作成し、Amplify app にくっつける 片方しか更新しないと、当然、もう片方の経路で 403 が出ます。今回まさにそこにハマりました。 タイムライン 実際に起きた流れを表にすると、こうなります。 タイミング 出来事 Day 0 社内ドキュメントで「新しい拠点 IP 一覧」が共有される Day 1 dev の Backend API WAF を Amplify の環境変数経由で更新 → IPSet 反映確認 Day 2 朝 stage の Backend API WAF を更新 → IPSet 反映確認 Day 2 昼 prod の Backend API WAF を更新 → IPSet 反映確認 Day 2 昼 動作確認のためフロントエンド URL にアクセス → まだ 403 Day 2 昼 「Backend は更新したのに何で?」と調査開始 Day 2 午後 フロントエンドは別 WAF ( AmplifyIPSet-* ) で守られていることに気付く Day 2 午後 Amplify Console の Firewall UI を見る → 「Firewall: 無効」と表示 Day 2 午後 API で確認 → WebACL は attach 済み、IPSet は旧 IP のままで Block Day 2 夕方 aws wafv2 update-ip-set で 3 環境の AmplifyIPSet-* を直接更新 → 全環境 200 OK 「Backend は更新したのにフロントだけ 403」となった時点で、Amplify Hosting 側に別 WAF があると気付くまでが一番遠回りでした。 最初の対応 — Backend API WAF はあっさり直る Backend API 側の更新手順は、すでに社内で整備されていました。 Amplify Console で対象 app の環境変数 WAF_ALLOWED_IP_LIST を新しい IP の CSV に書き換える Amplify の build を回す CDK で定義された CfnIPSet のリソースが新しい IP で更新される この仕組みのおかげで、dev / stage / prod の Backend API は Day 1 から Day 2 にかけて順次切り替え、いずれも更新自体は数分で完了しました。 WAFv2 のコンソールで IPSet を覗いて、新しい IP が並んでいるのを確認 → よし、終わったな、と思ったのが甘かったです。 念のため、フロントエンドの URL にも curl を投げて確認しました。 $ curl -i https://<env>.example.internal/ HTTP/1.1 403 Forbidden content-type: text/html ... <HTML><HEAD><TITLE>Request blocked.</TITLE></HEAD> <BODY>Request blocked.</BODY></HTML> Request blocked. というレスポンスボディは、AWS WAF の Block ルールが返す典型的な文字列です。S3 オリジンの「Access Denied」とは別物なので、これが見えた時点でほぼ WAF を疑って間違いありません。 :::message ちょっとした見分け方 CloudFront 経由の 403 で、ボディに Request blocked. が入っていれば、ほぼ WAF が原因です。 オリジン(S3 など)の Access Denied であれば、ボディは Access Denied という別の文字列になります。 ::: つまり Backend は直ったが、フロントエンドの経路では別の何かが Block している、という状況です。 真犯人を探す — フロントエンドは別 WAF で守られていた 「フロントエンドも CloudFront → S3 のはず。WAF はどこに付いているんだろう?」と探したところ、Amplify Hosting には AWS WAF 統合(Firewall) という機能があり、これを有効化していたことを思い出しました。 :::message 用語の整理 Amplify Hosting には紛らわしい 2 つの保護機能があります。 Access control : ベーシック認証(ユーザー名 + パスワード)でブランチを保護する機能。IP 制限ではありません。 Firewall(AWS WAF 統合) : AWS WAF v2 と統合し、IP allowlist / レートリミット / マネージドルールなどを適用する機能。 今回 IP allowlist として利用していたのは後者の Firewall 機能のほうです。 ::: Amplify Hosting の Firewall を有効化すると、Amplify サービス側で以下を自動的に作成・関連付けします。 AmplifyIPSet-<guid> という名前の IPSet(us-east-1, scope=CLOUDFRONT) CreatedByAmplify-<appId>-<guid> という名前の WebACL 上記 WebACL を Amplify app のリソース ARN に AssociateWebACL で紐付け そして これらのリソースは CloudFormation / CDK の管理外 です。Amplify サービスが直接 WAF API を叩いて作成しています。 そのため、CDK 側でいくら WAF を更新しても、フロントエンドの WAF は変わりません。 WAFv2 のコンソールから IPSet の一覧を眺めると、確かに AmplifyIPSet-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX のような IPSet が、CDK 由来の my-app-api-allowed-ips-<env> とは別に存在していました。中身を見ると、まさに 旧拠点 IP のみが入っている 状態。これが原因です。 :::message Tips: CloudFront scope の WAF は us-east-1 にしかない CloudFront は Global サービスですが、CloudFront に付けるための WAFv2 リソース(scope=CLOUDFRONT)は us-east-1 のエンドポイントからしか触れません 。 東京リージョン ( ap-northeast-1 ) でいくら aws wafv2 list-ip-sets を叩いても、CloudFront scope の IPSet は出てきません。「IPSet が見当たらない!」と焦ったときの 9 割はこれが原因です。 ::: # 正しい(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 UI と API が一致しない問題 「じゃあ Amplify Console の Firewall 画面で IP を入れ替えればいいか」と思って画面を開いたところ、目を疑う表示が出ていました。 Firewall: 無効(このアプリは Web Application Firewall で保護されていません) つまり「WAF はかかっていません」という意味の表示です。 しかし curl を打つと、明らかに 403 ( Request blocked. ) が返ってくる。どちらを信じればよいのか分からない状況です。 ここで AWS CLI を使って、API レベルで実態を確認します。 1. Amplify app に WebACL が attach されているかを確認 list-resources-for-web-acl を使うと、ある WebACL がどのリソースに attach されているかが分かります。 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 これだけだと 空の配列が返ってきます 。 「あれ、attach されていない?じゃあ何が Block しているの?」と一瞬混乱しました。 ですがこれは罠で、 --resource-type を指定していないとデフォルト値 APPLICATION_LOAD_BALANCER で検索されます。Amplify Hosting の場合、resource-type は AMPLIFY なので、デフォルトでは見えません。 さらに list-resources-for-web-acl は そもそも CloudFront Distribution には使えません (CloudFront の関連付けを調べたいときは aws cloudfront list-distributions-by-web-acl-id を使うのが正解です)。 # 正しい呼び方 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 これでようやく、Amplify app の ARN が返ってきました。 UI は「Firewall: 無効」と言っていたが、実態としては WebACL がしっかり attach されていた わけです。 :::message ハマりポイント: --resource-type のデフォルト aws wafv2 list-resources-for-web-acl は --resource-type を省略すると APPLICATION_LOAD_BALANCER で検索されます (CloudFront Distribution はこの API では扱えず、 aws cloudfront list-distributions-by-web-acl-id を使う必要があります)。 Amplify Hosting を疑うときは、必ず --resource-type AMPLIFY を明示しましょう。これに気付かないと、「attach されていない」と誤認して別の方向の調査に走ってしまいます。 ::: 2. WebACL のデフォルトアクションを確認 get-web-acl でデフォルトアクションを覗くと、default は Block 、その上で IPSet ベースの allow ルールが乗っかっている構成でした。 aws wafv2 get-web-acl \ --scope CLOUDFRONT --region us-east-1 \ --name CreatedByAmplify-XXXXXXXXXX-XXXXXXXX --id XXXXXXXX \ --query 'WebACL.{DefaultAction:DefaultAction, Rules:Rules[].Name}' つまり、 IPSet に載っていない IP からのアクセスは全部 Block 。 UI 上は「Firewall: 無効」と表示していても、API レベルでは「Block ベース + 古い IPSet で allow」になっており、見事に食い違っていました。 3. なぜ乖離したのか — CloudTrail で犯人探し UI と API がここまで一致しないのは流石におかしいので、CloudTrail で履歴を漁ってみました。 aws cloudtrail lookup-events \ --max-items 50 \ --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateIPSet \ --output json AssociateWebACL や DisassociateWebACL も同じように引いて、時系列で並べると、過去にある担当者が 何度も Associate / Disassociate を繰り返しており、最終的に Associate で終わっていた ことが分かりました。 時系列を追っても、UI が「Firewall: 無効」を表示し続けるに至った直接的な原因までは特定できませんでした。 ただし「Amplify Console の Firewall UI と、WAFv2 API が示す実態とが食い違うケースが起こりうる」という事実だけは、今回の調査で確認できた ことになります。 ともあれ、API の実態を信じるしかないことが確定したので、次は IPSet を直接書き換えに行きます。 解決手順 — IPSet を直接更新 すでに AmplifyIPSet-* がどこにあり、どの WebACL に紐付いているかは分かっているので、あとは WAFv2 の IPSet を直接更新 すれば終わりです。 ただし WAFv2 には楽観的排他制御の仕組みがあって、 LockToken を毎回取り直す必要があります。 :::message WAFv2 の LockToken(楽観的排他制御) Get* 系 API のレスポンスに LockToken が含まれており、 Update* 系の API ではこの LockToken を渡す必要があります。 他のプロセスが先に更新していると WAFOptimisticLockException で失敗します。 毎回 get → update をセットで実行する のが安全です。 ::: 実際に流したコマンドの雛形がこれです。 # 環境変数で接続先を切り替え(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 "$IPSET_NAME" --id "$IPSET_ID" \ --query "LockToken" --output text) # 2) IPSet の中身を新しい IP リストで上書き aws wafv2 update-ip-set \ --scope CLOUDFRONT --region us-east-1 \ --name "$IPSET_NAME" --id "$IPSET_ID" \ --lock-token "$LOCK" \ --addresses 192.0.2.10/32 192.0.2.11/32 198.51.100.0/24 203.0.113.0/24 これを dev / stage / prod の 3 環境で順に実行し、それぞれの環境で curl を打って 200 OK を確認しました。 :::message ポイント update-ip-set の --addresses は 上書き です(既存に追加ではなく差し替え)。誤って一部の IP を漏らすと、その IP からのアクセスが全部止まるので、現在の中身を一度ファイルに保存してから差分を取り、新しいリストとして渡すのが安全です。 ::: 学んだこと 1. 「同じ目的のリソースが複数経路で管理されている」状態を疑う 今回は、 my-app-api-allowed-ips-<env> と AmplifyIPSet-* という、 同じ「拠点 IP 許可リスト」を別々の場所で独立に管理している 構造が根本原因でした。 インフラを段階的に作っていくと、こうした「二重管理状態」がいつのまにか出来上がっていることがあります。今回のような全社的な IP 変更のタイミングは、その整理の絶好の機会でもあるな、と感じました。 2. UI を信用せず、API で実態を確認する癖を付ける Amplify Console のような上位のマネジメントコンソールは、内部で何かをキャッシュしていたり、過去の状態を表示し続けていたりすることがあります。 今回のように UI 表示 (「Firewall: 無効」) と API が示す実態 (Block ルール有り) が一致しないパターンも、十分起こり得ます。 3. list-resources-for-web-acl の --resource-type を忘れない WAFv2 の list-resources-for-web-acl は、 --resource-type を省略すると デフォルト値 APPLICATION_LOAD_BALANCER で検索されます 。Amplify Hosting の場合は AMPLIFY を明示する必要があります。 また、この API は CloudFront Distribution を対象に取れません 。CloudFront の関連付けは aws cloudfront list-distributions-by-web-acl-id という別の API を使うことになっており、これを知らないと「該当 WebACL がどこにも attach されていない」と誤認してしまいます。 今回も、ここに気付くまでが一番大きく時間を溶かしたポイントでした。 4. CloudFront scope の WAF は us-east-1 にしかない これは基礎中の基礎なのですが、改めて。 WAFv2 で CloudFront に付けるリソースは 必ず us-east-1 にあります。CLI なら --region us-east-1 必須、コンソールなら左上のリージョンを Global (CloudFront) に切り替える必要があります。 5. 「Request blocked.」というレスポンスボディは WAF Block の典型 CloudFront 経由の 403 で、ボディに Request blocked. の文字列があれば、ほぼ WAF の block ルールが原因です。S3 の Access Denied とは見た目で区別できるので、最初の切り分けに便利です。 使ったコマンドまとめ トラブルシュート中に何度も叩いたコマンドを、ここに集めておきます。 IPSet 周り # 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 <name> --id <id> \ --query "IPSet.Addresses" --output json # IPSet の更新(LockToken 必須) LOCK=$(aws wafv2 get-ip-set \ --scope CLOUDFRONT --region us-east-1 \ --name <name> --id <id> \ --query "LockToken" --output text) aws wafv2 update-ip-set \ --scope CLOUDFRONT --region us-east-1 \ --name <name> --id <id> \ --lock-token "$LOCK" \ --addresses 192.0.2.0/24 198.51.100.0/24 WebACL の attach 先確認 # Amplify Hosting に付いているかを確認(--resource-type を忘れない!) aws wafv2 list-resources-for-web-acl \ --region us-east-1 \ --web-acl-arn <arn> \ --resource-type AMPLIFY CloudTrail で履歴を追う # 特定の 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=<arn> まとめ Request blocked. だけが手がかりの 403 から、Amplify Hosting 裏の WAF の存在に気付き、UI と API の乖離まで辿り着いた、というやや遠回りなトラブルシュート記でした。 整理すると、今回の学びは次の通りです。 同じ目的のリソースが 複数経路 で管理されていないかを疑う マネジメントコンソールの UI は実態と一致しないことがある。 最後の真実は API レスポンス WAFv2 の list-resources-for-web-acl は --resource-type を忘れずに CloudFront scope の WAF は 必ず us-east-1 WAFv2 の更新は LockToken をセットで 扱う Amplify Hosting の Firewall (AWS WAF 統合) は便利ですが、「いつのまにか UI と実態が乖離する」リスクがあることは覚えておいて損はないと思います。 同じような構成の運用に関わる方の参考になれば嬉しいです。最後まで読んでいただきありがとうございました! 参考リンク AWS WAF Developer Guide — How AWS WAF works AWS Amplify Hosting User Guide — Firewall support for Amplify hosted sites AWS Blog — Firewall support for AWS Amplify hosted sites list-resources-for-web-acl API リファレンス(WAFv2) list-distributions-by-web-acl-id API リファレンス(CloudFront) lookup-events API リファレンス(CloudTrail) aws/aws-cli #5417 — wafv2 list-resources-for-web-acl only fetches load balancers by default (本記事で触れた --resource-type デフォルト挙動の罠について、CLI リポジトリで「ドキュメントに記載がない」と指摘された Issue)
こんにちは。 KINTO テクノロジーズの DBRE チーム所属の @hoshino です。 はじめに Aurora MySQL 2系(MySQL 5.7互換)から3系(MySQL 8.0互換)へのメジャーバージョンアップを、19クラスタ・46スキーマ規模のメインシステムで実施しました。 このバージョンアップで最も苦労したのが COLLATION の問題です。 Aurora MySQL 3系ではデフォルト COLLATION が utf8mb4_0900_ai_ci に変わりますが、既存システムでは、検索条件、ORDER BY、ユニーク制約、JOIN、帳票、バッチ処理などが utf8mb4_general_ci の比較・ソート挙動を前提に動いています。 utf8mb4_0900_ai_ci への変更は単なる DB 設定変更ではなく、アプリケーション仕様の変更に近いため、今回は互換性維持を優先し、 utf8mb4_general_ci を維持したまま移行する方針を取りました。 しかし、Aurora MySQL 3系では default_collation_for_utf8mb4 が utf8mb4_0900_ai_ci 固定で、サーバー側で変更する手段が用意されておらず、明示的に指定しないとセッションのデフォルトが utf8mb4_0900_ai_ci になってしまいます。そのため、 utf8mb4_general_ci を維持するために以下の対策を実施しました。 今回実施した対策 SCHEMA / TABLE / COLUMN / VIEW / ROUTINE / TRIGGER / EVENT の COLLATION を統一 接続設定・SQL クエリで COLLATION を明示指定することで COLLATION を制御 意図しない COLLATION が設定されないように information_schema を使った Slack 自動通知によるチェック体制の整備 本記事では、これらの対策の詳細について説明します。 背景 KINTO テクノロジーズの DBRE チームでは、Aurora MySQL 2系(MySQL 5.7互換)から3系(MySQL 8.0互換)へのメジャーバージョンアップを進めてきました。 弊社では多数のクラスタを運用していますが、今回対象となったのは複数プロダクトが共有するメインシステムの DB です。 このメインシステムは少し特殊な構成になっています。 1つの環境に対して 2つの Aurora クラスタが存在しており、複数プロダクトがこの2クラスタを共有して利用しています。 両クラスタは密接に連携しているため、片方だけバージョンアップするわけにはいかず、同時に移行する必要がありました。 対象規模は dev・stg・prod などの全環境を合計して 19クラスタ・46スキーマ・56ユーザー にのぼります。 構成を図にすると以下のようになります。 この移行で最も苦労したのが COLLATION の問題でした。 Aurora MySQL 3系(MySQL 8.0)のデフォルト COLLATION は utf8mb4_0900_ai_ci です。 一方、既存のデータベースは utf8mb4_general_ci で運用されていました。 システム全体を utf8mb4_0900_ai_ci に切り替えるという選択肢もゼロではありませんでしたが、COLLATION の変更はアプリケーションの挙動に直接影響します。 utf8mb4_general_ci と utf8mb4_0900_ai_ci は、どちらも大文字・小文字を区別しない COLLATION ですが、内部のソートアルゴリズムが異なります。 utf8mb4_0900_ai_ci は Unicode Collation Algorithm(UCA 9.0.0)に準拠しており、 = 演算子による比較結果や ORDER BY のソート順が utf8mb4_general_ci とは異なるケースがあります。 既存のアプリケーションが utf8mb4_general_ci の挙動を前提としている場合、COLLATION を切り替えただけで検索結果やソート順が変わり、意図しない不具合につながる可能性があります。 そうなると各プロダクト側でも影響調査や改修が必要になります。 複数プロダクトが共有しているデータベースであるため、その改修範囲は広く、プロダクト側の開発コストも大きくなります。 プロダクト側の負担を最小限にするためにも、 utf8mb4_general_ci を維持したままバージョンアップするという方針を選択しました。 Illegal mix of collations に対する対応 utf8mb4_general_ci を維持する方針で進めるにあたって直面したのが、 Illegal mix of collations というエラーです。 このエラーは、テーブル側の COLLATION とセッション側の COLLATION が混在した状態でクエリを実行したときに発生します。Aurora MySQL 3系では、サーバー側でデフォルト COLLATION を変更する手段がないため、何も対策しないとこのエラーが発生しやすい構造になっています。 MySQL 8.0 には default_collation_for_utf8mb4 というシステム変数があります( MySQL 公式: Server System Variables )。 これは CHARACTER SET utf8mb4 を指定して COLLATE を省略したとき、どの COLLATION がデフォルトで使われるかを決める変数で、デフォルト値は utf8mb4_0900_ai_ci です。 通常の MySQL であれば、 SET PERSIST default_collation_for_utf8mb4='utf8mb4_general_ci'; を実行することでこの値を変更できますが、Aurora MySQL ではこの変数を変更する手段がありません。 理由としては SET PERSIST は Aurora では使えず、パラメータグループにもこの設定項目が存在しないためです。 この制約により、 collation_connection を指定せずに接続した場合、セッションのデフォルトが utf8mb4_0900_ai_ci になってしまいます。 影響は実行するクエリだけではありませんでした。 VIEW や ROUTINE(ストアドプロシージャ・ファンクション)は、作成時のセッションの character_set_client や collation_connection が定義に依存するため、 utf8mb4_0900_ai_ci のセッションで VIEW を作成すると、その VIEW 自体が utf8mb4_0900_ai_ci を持ってしまいます。 後からセッションの COLLATION を変えても、すでに作成された VIEW の定義は変わりません。 さらに、クエリの中で COLLATION が動的に決まる箇所にも影響します。 たとえば UNION や CAST 関数を含むクエリでは、TABLE 側の COLLATION( utf8mb4_general_ci )とセッション側の COLLATION( utf8mb4_0900_ai_ci )が混在してエラーが発生します。 例1:CAST 関数を使った JOIN 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) CAST(t1.id AS CHAR) はセッションの collation_connection に従うため、Aurora のデフォルトである utf8mb4_0900_ai_ci になります。一方、 t2.code はテーブル定義の utf8mb4_general_ci のままです。この2つを = で比較するため、COLLATION の不一致が発生します。 例2:UNION で異なる COLLATION が混在 SELECT name FROM table_a -- ^^^^ -- utf8mb4_general_ci(テーブルの COLLATION) UNION SELECT CAST(id AS CHAR) FROM table_b; -- ^^^^^^^^^^^^^^^^ -- utf8mb4_0900_ai_ci(セッションのデフォルト) UNION は各 SELECT の COLLATION を統一する必要がありますが、上記のように一方が utf8mb4_general_ci 、もう一方が utf8mb4_0900_ai_ci になると統一できず、エラーになります。 どちらのクエリも、最終的には以下のエラーになります。 ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_0900_ai_ci,IMPLICIT) for operation '=' これを防ぐには、接続時に COLLATION を明示的に指定するか、SQL 文の中で COLLATE 句を明示する方法があります。 方法1:接続時に COLLATION を指定する -- MySQL クライアントから接続する場合 SET NAMES utf8mb4 COLLATE utf8mb4_general_ci; # JDBC URL での指定例 jdbc:mysql://host:3306/mydb?connectionCollation=utf8mb4_general_ci 方法2:SQL 文の中で COLLATE 句を明示する UNION や CAST など動的に COLLATION が決まる箇所に、直接 COLLATE 句を付与する方法です。 -- 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; しかし、接続時の COLLATION 指定やクエリへの COLLATE 句付与は、あくまで移行後の運用で問題を防ぐための対策です。 移行するにあたり、移行前に Aurora 2系側の COLLATION を統一しておく必要がありました。 注意事項 SET NAMES utf8mb4 (COLLATE 句を省略)を実行すると、それまでに設定していた collation_connection が破棄され、Aurora MySQL 3系のデフォルトである utf8mb4_0900_ai_ci に戻ってしまいます。 SET SESSION collation_connection = 'utf8mb4_general_ci'; -- ↑ ここで utf8mb4_general_ci になる SET NAMES utf8mb4; -- ↑ COLLATE 句がないため utf8mb4_0900_ai_ci に戻ってしまう ORM やアプリケーションフレームワークが内部で SET NAMES utf8mb4 を発行する実装も存在するため、実際に発行されるクエリのログを確認し、暗黙の SET NAMES が含まれていないかを把握しておく必要があります。 SET NAMES を使う場合は、必ず COLLATE 句までセットで指定するのが確実です。 移行手順と事前準備 移行方法 今回の移行は mysqldump などで論理ダンプを取得し、それをインポートする方式を採用しました。 Aurora MySQL 2系から3系への移行方式としては、Blue/Green デプロイやインプレースアップグレードといった選択肢もありますが、今回は以下の理由からダンプ・インポートを採用しました。 COLLATION の事前調整で DDL 変更が必要だった VIEW や ROUTINE の定義を書き換えて再作成する必要がありましたが、Blue/Green デプロイでは DDL 変更が Green 環境へのレプリケーション中断を引き起こすリスクがあり、レプリケーションとの互換性検証コストが高いと判断しました ダンプ・インポート方式の社内実績が豊富だった 弊社では全環境で数百の DB クラスタが存在しており、そのほとんどをダンプ・インポート方式で移行しました そのため、今回のような複数プロダクトが共有する大規模システムの移行において、Blue/Green デプロイなどの実績のない手法を採用するリスクは取れませんでした 安全を最優先に考えた結果、確実にコントロールできるダンプ・インポート方式を選択しました。 ダンプ・インポート時の COLLATION エラー ダンプ・インポート方式で移行を進めたところ、COLLATION の不整合によるエラーが発生しました。 Aurora 2系側の COLLATION が utf8mb4_general_ci に統一されていない状態でダンプを取ってインポートすると、VIEW の作成時に Illegal mix of collations エラーとなり、移行そのものが失敗します。 そのため、以下の手順で移行を実施しました。 Aurora 2系側で COLLATION を utf8mb4_general_ci に統一する その状態でダンプを取得する Aurora 3系にインポートする 事前作業の内容 事前作業では SCHEMA / TABLE / COLUMN / VIEW / ROUTINE のすべてに手を入れる必要がありました。 対象となるのは2クラスタ × 全環境(dev・stg・prod 等)にまたがる数十スキーマです。複数プロダクトが共有しているため、各スキーマの VIEW や ROUTINE がどのプロダクトに属するかを把握し、プロダクトチームと調整しながら進める必要がありました。 調整箇所は全環境合計で数千にのぼり、環境ごとにリストを作成し、プロダクトチームにレビューを依頼し、反映前に最終チェックを行うというサイクルを、すべての環境に対して繰り返し実施しました。 以下、具体的な調整方法をオブジェクトの種類ごとに説明します。 SCHEMA / TABLE / COLUMN の調整 SCHEMA・TABLE・COLUMN は ALTER 文で COLLATION を utf8mb4_general_ci に変更しました。 -- SCHEMA ALTER DATABASE ${schema} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- TABLE ALTER TABLE ${table} CHARACTER SET utf8mb4 COLLATE 'utf8mb4_general_ci'; -- COLUMN ALTER TABLE ${table} CONVERT TO CHARACTER SET utf8mb4 COLLATE 'utf8mb4_general_ci'; VIEW / ROUTINE / TRIGGER / EVENT の調整 一方、VIEW・ROUTINE・TRIGGER・EVENT は ALTER では対応できないため、定義を書き換えて再作成する必要がありました。 定義内の文字コード・COLLATION を一括で置換してから CREATE OR REPLACE VIEW で再作成するアプローチを取りました。主な置換パターンは以下の通りです。 "utf8 " → "utf8mb4 " "utf8_general_ci" → "utf8mb4_general_ci" "utf8mb4_0900_ai_ci" → "utf8mb4_general_ci" "utf8mb4_unicode_ci" → "utf8mb4_general_ci" "charset utf8mb4) AS" → "charset utf8mb4) COLLATE utf8mb4_general_ci AS" 最後のパターンは CAST 関数の末尾に該当します。 CAST(column AS CHAR) のような式では COLLATION が動的に決まるため、明示的に COLLATE を付与する必要がありました。 ただし、文字列置換だけでは対応しきれないケースも存在しました。 CAST 関数の使い方が複雑であったり、置換パターンに収まらない定義を持つ VIEW がいくつかありました。 こうした箇所は information_schema で COLLATION の状態を一つひとつ確認しながら、手動で定義を修正して再作成しました。 -- 置換漏れがないか確認するクエリ SELECT table_schema, table_name, character_set_client, collation_connection FROM information_schema.views WHERE collation_connection != 'utf8mb4_general_ci' AND table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys'); この確認を怠ると、一見すると置換が完了しているように見えても utf8mb4_0900_ai_ci が残ったままの定義が存在する場合インポート時にエラーが発生するか、移行後に Illegal mix of collations となってしまいます。 移行後に発生したインシデント 事前作業で Aurora 2系の COLLATION を統一し、Aurora 3系への移行を完了しました。 しかし移行後、プロダクトから Illegal mix of collations のエラーが発生したとの報告がありました。 発生したエラー エラーの内容は以下の通りです。 1267 (HY000): Illegal mix of collations (utf8mb4_0900_ai_ci,IMPLICIT) and (utf8mb4_general_ci,IMPLICIT) for operation '=' Aurora 2系では問題なく動作していた機能が、Aurora 3系への移行後にエラーとなっていました。 原因の調査 まず、エラーが発生しているクエリの調査を行いました。 問題のクエリには CAST 関数を使った JOIN が含まれていました。 以下は同様の構造を持つ例です。 -- 例:CAST 関数を使った JOIN で COLLATION の不一致が発生するケース SELECT * FROM table_a AS t1 LEFT JOIN table_b AS t2 ON CAST(t1.id AS CHAR) = t2.code; CAST(... AS CHAR) の結果にはセッションの collation_connection が適用されます。 該当のアプリケーションでは collation_connection が指定されておらず、Aurora のデフォルトである utf8mb4_0900_ai_ci が適用されていました。 その結果、CAST 関数の結果は utf8mb4_0900_ai_ci となり、テーブル側の utf8mb4_general_ci と混在して Illegal mix of collations が発生していました。 対処方法 対処として、アプリケーションの DB 接続設定に collation_connection=utf8mb4_general_ci を追加しました。 # 接続文字列に COLLATION 設定を追加 mysql+mysqlconnector://user:password@host/dbname ?init_command=SET SESSION collation_connection=utf8mb4_general_ci init_command は接続確立直後に実行されるため、以降のクエリでは collation_connection が utf8mb4_general_ci の状態で処理されます。 collation_connection が utf8mb4_general_ci になることで、 CAST や UNION のようにセッションの COLLATION 値で動的に COLLATION が決まる箇所も utf8mb4_general_ci に揃えられ、テーブル側との不一致を防げます。 この変更をリリースした後、エラーは解消し、現在は安定稼働しています。 COLLATION の定期チェックと自動通知 一度問題を修正しても、新しい VIEW が作成されたりアプリケーションが更新されたりすると、同様の問題が再発する可能性があります。 本番環境でエラーが発生してから気づくのではなく、開発段階で COLLATION の不一致を早期に検知するために、全環境の COLLATION の状態を定期的にチェックし、意図しない COLLATION が設定された場合に自動で通知する仕組みと、手動で現状のCOLLATIONの状態を確認できる仕組みを構築しました。 自動化の仕組み 仕組みの全体像は以下の通りです。 1. 日次で COLLATION 情報を自動取得 COLLATION をチェックするクエリを CLI コマンドとして実装しました。 このコマンドを全クラスタに対して日次で自動実行し、取得結果を JSON 形式で S3 に保存しています。 2. 期待する COLLATION との照合と Slack 通知 EventBridge で決まった時間に、S3 上の JSON データを精査します。 DynamoDB にあらかじめ登録してある「期待する COLLATION」と照合し、意図しない COLLATION が検出された場合は Slack の専用チャンネルに自動通知します。 3. CLI による手動チェック CLI コマンドは手動でも実行できます。 新規 TABLE 作成後やトラブルシューティング時など、任意のタイミングで特定のクラスタの状態を確認したい場合に使用しています。 COLLATION チェックで実行しているクエリ 自動化の仕組みの中で各クラスタに対して実行しているクエリは、 information_schema を使って utf8mb4_general_ci 以外の COLLATION が混入していないかを検出するものです。対象が SCHEMA・TABLE・COLUMN・VIEW・ROUTINE・TRIGGER の6種類です。 -- SCHEMA の COLLATION 確認 SELECT schema_name, default_character_set_name, default_collation_name FROM information_schema.schemata WHERE schema_name NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys'); -- TABLE の COLLATION 確認 SELECT table_schema, table_name, table_collation FROM information_schema.tables WHERE table_collation != 'utf8mb4_general_ci' AND table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys'); -- COLUMN の COLLATION 確認 SELECT table_schema, table_name, column_name, collation_name FROM information_schema.columns WHERE collation_name IS NOT NULL AND collation_name != 'utf8mb4_general_ci' AND table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys'); -- VIEW の collation_connection 確認 SELECT table_schema, table_name, character_set_client, collation_connection FROM information_schema.views WHERE collation_connection != 'utf8mb4_general_ci'; -- ROUTINE の COLLATION 確認 SELECT routine_schema, routine_name, routine_type, collation_connection, database_collation FROM information_schema.routines WHERE collation_connection != 'utf8mb4_general_ci'; -- TRIGGER の COLLATION 確認 SELECT trigger_schema, trigger_name, collation_connection, database_collation FROM information_schema.triggers WHERE collation_connection != 'utf8mb4_general_ci'; 今後のAuroraバージョンアップ(Aurora MySQL 4)に向けて MySQL 8.4 で default_collation_for_utf8mb4 を SET PERSIST で変更すると、以下の deprecated 警告が表示されます。 mysql> SET PERSIST default_collation_for_utf8mb4='utf8mb4_general_ci'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> SHOW WARNINGS; +---------+------+--------------------------------------------------------------------------------------------------------+ | Level | Code | Message | +---------+------+--------------------------------------------------------------------------------------------------------+ | Warning | 1681 | Updating 'default_collation_for_utf8mb4' is deprecated. It will be made read-only in a future release. | +---------+------+--------------------------------------------------------------------------------------------------------+ 「将来のリリースで read-only にする」と警告されていることから、今後この変数による COLLATION の制御はさらに難しくなる可能性があります。COLLATION を確実に制御するためには、SCHEMA・TABLE・COLUMN・VIEW・ROUTINE のすべてで明示指定し、 information_schema で定期的にチェックするアプローチが引き続き有効です。 Aurora MySQL のリリースカレンダー によると、Aurora MySQL 3 のメジャーバージョン標準サポートは 2028年4月30日 までとなっています。その後は次のメジャーバージョンへの移行が必要になるため、今回整備した定期チェックの仕組みや CLI コマンドを次のバージョンアップでもそのまま活用できるようにしておくことが重要だと考えています。 まとめ Aurora MySQL 3系では MySQL 8.0 互換となり、デフォルト COLLATION が utf8mb4_0900_ai_ci に変わったことで、既存 DB の utf8mb4_general_ci と混在しやすくなった。これが今回の苦労の根本原因 Aurora MySQL では SET PERSIST で default_collation_for_utf8mb4 を変更できないため、サーバー側で utf8mb4 の デフォルト COLLATION を制御できない 接続時に collation_connection を明示指定しないと、セッションのデフォルトが utf8mb4_0900_ai_ci となり Illegal mix of collations が発生する可能性がある ダンプ・インポートで移行する場合、移行前に Aurora 2系側の SCHEMA / TABLE / COLUMN / VIEW / ROUTINE の COLLATION を統一しておく必要がある SET NAMES utf8mb4; (COLLATE 省略)は直前の COLLATE 指定を破棄するため、接続文字列の init_command で指定するのが確実 移行後も information_schema を使った COLLATION の定期チェックと自動通知の仕組みが有効 今回整備した COLLATION チェックの仕組みや CLI コマンドは、次のバージョンアップでもそのまま活用できる 本記事の内容が、同じ課題に取り組んでいる方々の参考になれば幸いです。 参考文献 Changes in MySQL 8.0 collation_server のデフォルトが utf8mb4_0900_ai_ci に変更 Server System Variables default_collation_for_utf8mb4 パラメータの補足 接続に関するパラメータの理解 character_set_* / collation_* 各パラメータの関係 SET NAMES の補足 セッションの COLLATION 指定方法
はじめに こんにちは、2026年2月入社の岩月です! 本記事では、2026年2月入社のみなさまに入社直後の感想をお伺いし、まとめてみました。 KINTO テクノロジーズ(以下、KTC)に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! 森田和明 ![森田和明さんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/morita.jpg =300x) 自己紹介 コーポレートIT部AIファーストGの森田です。 社内の生成AI活用の推進やトヨタグループにおけるAI活用支援を担当しています。 奈良に住んでます。 最近書籍を執筆しました! AWSではじめるMCP実践ガイド 所属チームの体制は? AIファーストGは「AI Transformation 」「AI Engineering」「AI Development」の3チーム体制で、私はAI Engineeringに所属です。 「アイデア生成」→「実現可能性の検証」→「実施とデリバリー」→「ケース展開」→「アイデア生成」とループを回し、AI活用の活性化に取り組んでいます KTCへ入社したときの第一印象?ギャップはあった? 入社前のカジュアル面談などを通じて思っていた通りでした。 エンジニアが多い会社ではありますが、技術スタックが様々で、各自がそれぞれの分野でスペシャリストという印象です。 AIファーストGも全員バックグラウンドが違うので、それぞれの得意分野とAIを掛け合わせて専門性を発揮しています。 現場の雰囲気はどんな感じ? 私はOsaka Tech Labで勤務していまして、まず、オフィスが綺麗です。 所属は様々ですが「大阪を盛り上げていこう!」という雰囲気があり、技術交流イベントなど一致団結できる取り組みがあります。 ブログを書くことになってどう思った? 趣味として技術ブログをやっているので、すんなり書けました! 岩月 ⇒ 森田さんへの質問 森田さんは技術系の書籍をいくつか執筆されていますが、執筆のきっかけや苦労話があったら教えてください! 私が執筆した書籍は、執筆メンバーが共著者を探している中で、声をかけてもらって参加したというのが経緯です。 苦労はたくさんありますが(笑)、扱うテーマがAWSや生成AIなので執筆している最中にアップデートがあり、その度に原稿の更新や画面キャプチャの取り直しを行っています 成島大介 ![成島大介さんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/narushima.jpg =300x) 自己紹介 新サービス開発部 プロジェクト推進Gに所属しています。 名古屋オフィス勤務です。 トヨタグループ向け案件のプロジェクトマネジメントを担当しています。 所属チームの体制は? プロジェクト推進Gは兼務除くと5名体制で名古屋3名、東京1名、福岡1名です。 プロジェクトマネージャだけの組織なので、開発メンバーは状況に応じて他部署から参画してもらい開発体制を作ります。 KTCへ入社したときの第一印象?ギャップはあった? 名古屋オフィスに開発者が少ないことが入社前の印象とのギャップです。 KINTOとKTCが同じフロアなので、入社前のオフィス見学では気づきませんでした。 現場の雰囲気はどんな感じ? 仕事については、問題課題がない限り任されていると感じます。 一緒にランチに行く機会が多くいろいろ情報収集できて助かってます。 ブログを書くことになってどう思った? 個人(匿名)で技術ブログは書いてましたが、最近はさっぱりです。 一番読んでもらえた記事が専門外のC++ネタで何を書くと良いのか分かってません。 森田さん ⇒ 成島さんへの質問 最近の猫ちゃんの面白エピソードを教えてください! 猫がドアノブに飛びついてドアを開けることをマスターしました。 娘(中3)の部屋にも問答無用で侵入します(ドア全開)。 娘も親には怒るが、猫には怒りません。 きゅーじ ![きゅーじさんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/kyuji.jpg =300x) 自己紹介 my route開発部ビジネス開発支援グループに所属しています。 勤務場所は福岡のFukuoka Tech Labです。 所属チームの体制は? 主にトヨタファイナンシャルサービス株式会社のmyroute業務支援を行っています。主に営業、マーケティング業務をサポートしており、2~5名のチームで動いています。 KTCへ入社したときの第一印象?ギャップはあった? Fukuoka Tech Labからの景色が最高に良い!※入社後初めて入りました。 オンボーディングがしっかりあり、安心して業務が開始できました。 現場の雰囲気はどんな感じ? 良いプレッシャーの中で、和やかな雰囲気かなと思います。 それぞれの個性と強みを生かしながら、どんどん仕事を作っていく感じが良いなと思っています。 ブログを書くことになってどう思った? テックブログは、書いたことなかったかつ、非エンジニアの私が書けるのか不安でした。 成島さん ⇒ きゅーじさんへの質問 ミシンで最近作った作品教えてください! 2月にミシンを買っていろいろ作ろうと息巻いておりましたが、現状、カーテンの裾上げ、布団カバーの修理等々が私の作品ですかね。クッションカバーを今度作ろうと布屋さんに行こうと思います。 かわちゃん ![かわちゃんさんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/kawachan.jpg =300x) 自己紹介 my route開発部のプロダクト推進グループに所属しています。 神保町オフィス勤務です。 所属チームの体制は? プロダクト開発チームにいますが、私を含め4名です。うち3名はプロデューサーとして、うち1名は他部署から分析部分のみお手伝いいただいています。 KTCへ入社したときの第一印象?ギャップはあった? オンボーディングの研修が手厚くて驚きました。 フリーアドレスかなと思ったのですが、固定だったのが新鮮でした。 現場の雰囲気はどんな感じ? 大人数ですが、思った以上に静かな部署です。 外国籍の方が多いので最初ドキドキしましたが今は慣れました。 ブログを書くことになってどう思った? テックブログは書いたことがないのでちょっと焦りました。 きゅーじさん ⇒ かわちゃんさんへの質問 国内旅行でおすすめの場所はありますか? あまり国内も海外も旅行に行っておらずおすすめできる場所がありませんが、高知は2回ほど行っていて居心地が良かったです。桂浜がとても素敵でした。 SHN ![SHNさんのプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/shn.jpg =300x) 自己紹介 KTC 業務システム開発部に所属しつつ、現在は KINTO 業務部に出向(兼務)しています。 名古屋市在住で、桜通オフィスに勤務しています。 バックオフィス業務の改善や効率化を担当しています。 所属チームの体制は? 出向先の KINTO 業務部では、IT 推進チームに所属しており、4 人体制で業務にあたっています。 バックオフィス業務を IT 目線で改善するチームとして、KTC の開発編成部や業務委託先などの関係者と連携しながら仕事を進めています。 KTCへ入社したときの第一印象?ギャップはあった? 良い意味で「トヨタっぽくない」ところがギャップでした。 トヨタ系列の会社ということで、縦割りな組織・慎重な意思決定・多重な申請フローなどがある程度あるだろうと想像していましたが、実際にそんなことはなく、オープンでフランク、かつスピード感のある職場だと感じています。 現場の雰囲気はどんな感じ? KINTO 業務部はサービスを円滑に運営するため、販売店様との架電対応を担うメンバーも多く、適度な緊張感があります。 IT スキルだけでなく、リースや保険を含む KINTO サービスへの深い理解がなければ対応しきれない場面も多く、日々多くのことを学んでいます。 ブログを書くことになってどう思った? KTC への応募・入社を検討する前からこのブログを読んでいたので、「とうとう自分の番が来たか」という感慨がありました。 応募・入社を検討されている方にとって有益な情報を発信できる、良い機会だと思っています。 かわちゃんさん ⇒ SHNさんへの質問 ランニングするときのこだわりや、自分だけのルールはありますか? ランニングは習慣的に続けているのですが、「今日は走りたくないな」と感じる日も正直よくあります。 そんな日は、走り終わった後にコンビニへ直行してアイスやスイーツを買うことを自分へのご褒美にして、モチベーションを保つようにしています。 岩月 ![岩月のプロフィール画像](/assets/blog/authors/iwatsuki/2026-05-15-newcomer-202602/iwatsuki.jpg =300x) 自己紹介 コーポレートIT部コーポレートIT Gの岩月です。 社内IT業務の改善や効率化のために座席表システムをはじめとするいくつかのツールを開発しています。 所属チームの体制は? 東京と名古屋合わせて11名が在籍しています。 人数もそれなりに多く業務も多種多様なチームなので、これから業務範囲を広げていく中で、少しずつ全体像を理解していきたいと思っています。 KTCへ入社したときの第一印象?ギャップはあった? 生成AIを活用した、スピード感のある社内IT改善に取り組めると感じて入社しました。 前々職での上司や前職の同僚が在籍していて、入社前から社内の様子を伺えていたこともあり、大きなギャップは感じていません。 現場の雰囲気はどんな感じ? 打ち合わせで積極的に発言が飛び交い、現場からボトムアップに課題を挙げて改善していく雰囲気があると感じています。 ブログを書くことになってどう思った? ここしばらく書く機会がなかったのですが、こうした機会をいただけるのであれば、今後は積極的に情報発信していきたいと思います。 SHNさん ⇒ 岩月への質問 デスクワークで手放せない or 仕事が捗るガジェットはありますか? 業務端末にはセキュリティを考慮して個人所有のデバイスは接続していませんが、その分ソフトウェアで工夫しています。 業務効率化のために自作しているmacOS用のアプリで、メニューバーに次の予定の時刻を常時表示しつつ、当日のスケジュール確認や、ワンクリックでZoom・Teams・Slackハドルへの参加、会議資料へのアクセスができるようにしています。 これまでも似たアプリを個人で作って使い続けていたこともあり、今やこれがないと会議の時間を忘れてしまう体になってしまいました。 さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
はじめに KINTOテクノロジーズ Osaka Tech Lab所属のひがしです。 2026年3月末に、年度末イベント【O-KINI FY2026】を開催しました! Osaka Tech Labでは、メンバーそれぞれが異なるプロジェクトを担当することも多く、普段はなかなか横のつながりが生まれにくいこともあります。そのため、拠点全体で交流を深める文化を大切にしており、今回のイベントもその一環です。 2025年度の活躍をメンバー同士で労いながら、一体感をさらに高めることを目的に、有志メンバーが企画したOsaka Tech Lab初の取り組みとなりました。 企画チーム7名のうち5名は入社1年以内のメンバー。先輩社員2名からOsaka Tech Labの雰囲気や文化を吸収しながら、一緒にイベントを作り上げました。そんな新入り5名で、当日の様子をブログにまとめます! ノベルティ 執筆者:m イベントを行うにあたり今回様々なノベルティを用意しました! 参加してくれたみなさん全員に、Osaka Tech Labらしさをぎゅっと詰め込んだネックストラップとステッカーを用意しました。 また、受賞者の皆さんにはデスクに飾るとふと目に入るたびに「今年もがんばろう」と前向きな気持ちになれる、そんな"日常の中でふりかえれる記念品"になることを目指しました。 全員向け ネックストラップ ステッカー 表彰者 アクリルスタンド トロフィー これらのノベルティのデザインは、すべてOsaka Tech Lab所属のデザイナーが担当しました。 Osaka Tech Labらしさを大切にしながら、日常使いもしやすい世界観に仕上げています。 ![](/assets/blog/authors/higashiji/20260501/image2_trophy.JPG =600x) FY2026 振り返り 執筆者:S.N イベントのトップバッターを飾ったのは、FY2026(2025年度)の拠点振り返りコンテンツです! 出来事の報告にとどまらず、メンバー個人の「色」を引き出すために事前アンケートを実施。「今年がんばった仕事」や「将来の野望」など、共に働くメンバーの意外な一面や熱い想いを共有する時間となりました。 もちろん拠点としてのトピックスも盛りだくさんで、オフィスの引っ越しや新たな仲間の採用、数々のイベント開催など、濃い1年を振り返りました。 笑いあり、涙あり、そして愛のある「メンバーいじり」あり。小気味良くOsaka Tech Labの1年を共有するコンテンツになりました。 参加者からも「プレゼンがYouTubeみたい!」「データの見せ方がおもしろい」「入社直後だが理解が深まった」などといった声をいただきました。 スライドの一部抜粋「今年の印象に残った出来事は?」 ![](/assets/blog/authors/higashiji/20260501/image3_furikaeri1.jpeg =600x) スライドの一部抜粋「来年大阪でなにがしたい?」 ![](/assets/blog/authors/higashiji/20260501/image4_furikaeri2.png =600x) 表彰 執筆者:ひがし 続いて、表彰イベントに移りました!賞は全部で4つ設けました。 HONMA ARIGATO賞 Osaka Tech Labに多大な貢献をされた方へ感謝を伝える賞 MECCHYA TECH賞 技術面で印象的な活躍や貢献をされた方へ贈る賞 BARI NINKIMON賞 部署問わず、多くの方と積極的にコミュニケーションを取った方へ贈る賞 O-KINI AWARD FY2026賞 "めっちゃブレイクスルーするラボ"・"集GO!発SHIN!Co-LAB"というOsaka Tech Labの共通指針・あいことばを最も体現した年間MVPへ贈る賞 各受賞者は、事前に実施したアンケートでの投票数をもとに選定しました。 また、受賞者には景品として、お名前と賞名を記載したアクリルスタンドを贈呈し、そして【O-KINI AWARD FY2026賞】の受賞者にはあわせてトロフィーも贈呈しました! ![](/assets/blog/authors/higashiji/20260501/image5_award.jpg =600x) さらに、贈呈する側のメンバーにもひと工夫を加え、"その受賞者に投票したメンバーの中から1名"が景品を渡す形式にすることで、「1年間の活躍をメンバー同士で労い合う」という本イベントの目的を達成することができました! ST大会 執筆者:さやま 続いてはST大会を開催しました。 STは「ソニックトーク」の略で、LT(ライトニングトーク)よりもさらに短く、気軽に話してもらうことを目的とした発表形式です! STには決まった運用がないため、今回は以下のルールで実施しました。 発表時間は1人3分まで スライド枚数は自由 テーマはOsaka Tech Labに関する内容なら何でもOK 発表者は応募形式とし、11名の方にご応募いただきました。 最新技術の話や採用の話、個人開発の話、Osaka Tech Labにまつわる話まで、かなり幅広いテーマが集まりました。 3分という短い持ち時間での発表は今回が初めてでしたが、そのぶん一人ひとりの個性がしっかり伝わる、濃い内容になりました。 またイベント終了後のアンケートでも、「今まで知らなかった一面を知ることができてよかった」「面白かった」といった声を多数いただきました。 ![](/assets/blog/authors/higashiji/20260501/image6_st.jpg =600x) 懇親会 執筆者:M.K イベントの締めくくりは、Osaka Tech Labらしいカジュアルな懇親会でした。 会場は立食形式とし、「お花見」をコンセプトに飾り付けを実施。ケータリングのオードブルやアルコールを囲みながら、部署や職種を越えて交流できる時間になりました。 ![](/assets/blog/authors/higashiji/20260501/image7_hanami.jpg =600x) 乾杯の挨拶は、Osaka Tech Labメンバー全員の中からルーレットでランダムに選出する方式に。結果として、最年長者が当選し、会場が笑いに包まれるスタートとなりました。 表彰パートとも連動し、社長の小寺が持参してくださったワインが、受賞者への特別な一杯として振る舞われました。また、ちょうど小寺のお誕生月だったこともあり、ギター演奏に合わせた「ハッピーバースデー」の合唱と、名物の豚まんをバースデーケーキに見立てたサプライズでお祝いしました。 ![](/assets/blog/authors/higashiji/20260501/image8_cake.jpg =600x) 懇親会の中盤では、最近Osaka Tech Labに加入された方や、今後異動予定の方にもマイクをお渡しし、イベントや拠点に対する率直な感想を共有していただきました。新しいメンバーを自然と巻き込み、拠点全体で歓迎するOsaka Tech Labの文化が表れた時間になったと感じています。 最後は、小寺から本日の総括と、来年のOsaka Tech Labに期待することについて一言をいただき、一本締めならぬ「おおきに!」の掛け声でクロージングしました。 最後に 【O-KINI FY2026】は、年度の締めくくりとして、Osaka Tech Labのメンバーが互いの頑張りを称え合い、気持ちを1つにする大切な時間となりました。 また、Osaka Tech Labには「自分たちの手で楽しみを共創しよう」という文化があります。今回、企画メンバーとしてその文化を実際に経験することができました。 今後も、Osaka Tech Labの雰囲気や文化を大切にしながら、拠点が大きくなっても、人が増えても、その良さを保ちつつ成長していきたいと思います。 📢 KINTOテクノロジーズ Osaka Tech Lab 積極採用中! 最後までお読みいただき、ありがとうございました!KINTOテクノロジーズでは、Osaka Tech Labを共に創り上げ、一緒に楽しんでくれる仲間を絶賛募集しています。 拠点が拡大していくこのワクワクするフェーズで、あなたの力を発揮してみませんか? 「ちょっと面白そうかも」「まずはオフィスの雰囲気を知りたい」という方は、ぜひ一度ざっくばらんにお話ししましょう! ご応募お待ちしております! ![](/assets/blog/authors/higashiji/20260501/image9_all.jpg =600x) 👇 詳細はこちらをチェック! https://www.kinto-technologies.com/recruit/#job-list https://hrmos.co/pages/kinto-technologies/jobs/1859151978603163665
はじめに こんにちは、2026年1月入社のI.Kobayashiです! 本記事では、2026年1月入社のみなさまに入社直後の感想をお伺いし、まとめてみました。 KINTOテクノロジーズ(以下、KTC)に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! YY ![YYさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/yy.webp =300x) 自己紹介 デジタル戦略部 データグロースグループでプロデューサーをしています。 各サービスの成長に向けて、データドリブンな意思決定を支援する施策を企画・推進しています。 また、社内ツールのプロダクトマネージャー(PdM)も兼任しており、社内業務効率化のためのツール開発や改善にも取り組んでいます。 所属チームの体制は? ビジネス支援を行うチームに所属しており、デジタルマーケティングに強みを持つプロデューサー、定性調査に強みを持つプロデューサー、データサイエンスに強みを持つエンジニア達などに囲まれて仕事をしています。 それぞれの専門性を活かしながら、チーム一丸となってサービスの成長を支えています。 KTCへ入社したときの第一印象?ギャップはあった? 制作・開発に比重の強い会社だという印象を持っていましたが、実際にはビジネス側との距離も近く、連携が密である点にギャップを感じました。 現場の雰囲気はどんな感じ? 私が所属するデータ活用チームは、複数のサービスチームと横断的に関わるため、日常的にコミュニケーションが活発です。データで支援する立場から、サービスの理想の姿やデータから見える実像について、普段の会話の中で自然に議論が交わされています。 オフィスで気に入っているところ スカイツリーを眺めながらランチできる休憩スペースがお気に入りです。 眺望の良さはもちろん、リフレッシュしやすい雰囲気があり、午後の仕事への切り替えにも役立っています。 天野さん ⇒ 吉川さんへの質問 普段の業務でAIってどうやって使われていますか? データ分析をAIに任せて、プロジェクトの進む方向性や現在地を一緒に考えることを行っています。 壁打ち相手としても、分析担当としても利用しており、自分の役割を忘れてしまいそうになるぐらいに多用しています。 会議に向けたアジェンダ作成、それに伴うデータ分析、示唆出しまで、一言のプロンプトで完了してしまうのは革命的だと感じています。 Mizoguchi Hiroki ![Mizoguchi_Hirokiさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/mizoguchi.webp =300x) 自己紹介 KINTOを開発するグループで新車サブスクのWebフロントエンド開発チームに所属しています フロントエンドチームにいますがバックエンドもインフラも全般好きです 自転車に乗って走り回るのが趣味です!走り回りすぎて最近骨折しましたが、治ったら懲りずに走り回ろうと思っています 所属チームの体制は? 東京6名・大阪3名のフロントエンドエンジニア9名で構成されています バックエンド・フロントエンド・PdM・QAなど職種によってチームが分かれていて、開発する機能ごとに各チームから数名集って開発を進めるような体制になっています KTCへ入社したときの第一印象?ギャップはあった? 行動力がある大人が集まっているという印象でした。経験値からくる冷静さと、周りを巻き込んでやりたいこと・やるべきことを進めるアクティブさを持った人が多い印象を受けています 現場の雰囲気はどんな感じ? チームメンバーそれぞれで異なったタスクを進めることが多いので、主にモクモクと作業しています。協力が必要なことや相談したいことをslackや対面で声を掛けると皆さん積極的に会話に参加してくれるのでコミュニケーションは円滑です オフィスで気に入っているところ とにかく開放感があって、外の景色を見渡せるところが気に入っています オフィス全体が車をテーマにデザインされていて、遊び心があるところが気に入っています。イベントも頻繁に開催しているので、ぜひ覗きにきてください 吉川さん ⇒ 溝口さんへの質問 フロントエンド開発において、AIと人とどのように作業を分担されていますか? 私は大枠の設計は完全に人間、業務ロジック設計やコードのレイヤー分割などはAIの提案をもとに対話して決定、具体的な実装は殆どAIに任せるなど具象度に応じてAIへの依存が高まっていくような分担になっています。 地味にUIの見た目チェックや操作時の挙動確認は具象な作業なものの、人間がポチポチ画面操作して担当しています。(なんとかAIを使って自動化できないか模索中) Kosuke Kihara ![Kosuke_Kiharaさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/Kosuke.webp =300x) 自己紹介 新サービス開発部 FACTORY EC開発G所属です。 趣味は自作キーボード・ヴィオラ・園芸、ヴィオラは市民オーケストラで演奏していました。 所属チームの体制は? 新サービス開発部 FACTORY EC開発Gで、TOYOTA/LEXUS UPGRADE FACTORYのEC基盤を開発・運用しています。 フロントエンド、バックエンド、PdM、SRE、QA、ディレクター、マネージャーなど合わせて15名ほどの体制です。 KTCへ入社したときの第一印象?ギャップはあった? 入社前に受けていた説明と大きく印象が異なることもなく、戸惑うことはなかったです。 あえて言えば、自分が勤務しているOsaka Tech Labでは特に遊び心を大事にしているところが良い意味でギャップに感じました。 現場の雰囲気はどんな感じ? チームではバーチャルオフィスのGatherを利用しており、リモートでも気軽に相談できる空気感があります。 オフィスで気に入っているところ Osaka Tech Labのパークです。 ヨギボーを持っていってリラックスしながら仕事すると、頭が柔らかくなっていろんな発想ができる(気がする)。 溝口さん ⇒ 木原さんへの質問 最近AIを使ってうまくいった仕事や作業あれば教えてください! JiraチケットやPRのURLから紐づくConfluence・Jira・Slack・コードを自動で追わせてまとめるスキルを作成しました。 案件の周辺コンテキストの理解にかかる時間を大幅に削減できています。 やまそと ![やまそとさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/yamasoto.webp =300x) 自己紹介 プラットフォーム開発部Cloud Infrastructure Gのやまそとです。 トヨタグループへのクラウド領域の技術支援を担当しています。 前職まではSES/SIerでバックエンドの開発エンジニアとして働いていましたが、気づいたらインフラエンジニアになっていました。 プライベートではビールとバイクにハマってます! 所属チームの体制は? 大阪4名東京2名の体制です KTCへ入社したときの第一印象?ギャップはあった? コミュニケーションが活発でアクティブな人が多いなーという印象でした トップダウンではなくフラットに意見を言えますし、自律的に行動する人が多いのは良いギャップでした 現場の雰囲気はどんな感じ? 普段はみんなそれぞれの案件に携わっていますが、社内のチームミーティングはワイワイやってます オフィスで気に入っているところ OsakaTechLab勤務ですが、全体的にキレイでテンションが上がります 駅と繋がっていて雨に濡れずに済むので助かります 木原さん ⇒ 山外さんへの質問 バイクが趣味とお聞きしましたが、最近バイクで行ったおすすめの場所などあれば教えてください! 去年の秋頃に兵庫の須磨に行きました!海沿いを走るのは気持ちよかったです。 あったかくなってきたので淡路島か琵琶湖にいきたいですね。 やまと ![やまとさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/yamato.jpeg =300x) 自己紹介 my route開発部でAWSインフラアーキテクトとして働いています。 国内旅行が趣味で、アーケードゲームの全国行脚機能で43都道府県、スターバックスのアプリでは14都県巡っています。(2026年4月現在) インドア趣味のほうでは某オンラインゲームの/playtimeが執筆時点で10,047時間でした。 所属チームの体制は? 所属しているバックエンド開発チームでインフラを主に担当するのは私一人で、サーバサイドアプリケーションを開発する他のメンバーと密にコミュニケーションを取って仕事を進めています。 KTCへ入社したときの第一印象?ギャップはあった? 入社前の想像よりも、チームメンバーの一人ひとりが開発しているアプリケーションのことをもっとこうしたい!と考えていると感じました。 現場の雰囲気はどんな感じ? メンバーが2人以上出社すれば一緒にランチに行って雑談をしているので、仕事の依頼や質問もしやすく過ごしやすい雰囲気だと感じています。 オフィスで気に入っているところ 神保町オフィスは集中しやすくもあり孤独を感じるほど少なくもない、ほど良い出社率です。レストエリアがお洒落でアップルティーを取りに行くのがリフレッシュになります。 山外さん ⇒ 大和さんへの質問 おすすめの旅行先を教えてください! 美味しい酒・魚を求めるなら四国地方 or 日本海側、綺麗な景色を求めるなら海沿い、が良かったです! その土地の名産であれば、味はもとよりお値段も都市圏より安くてたくさん食べられます。 ただし、食を堪能する旅には登山やハイキングも取り入れた方が、よいです(戒め)。 I.Kobayashi ![I.Kobayashiさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/Kobayashi.webp =300x) 自己紹介 クラウドセキュリティG所属のI.Kobayashiです。 KTCで利用しているクラウドのセキュリティ改善や改善活動の効率よくするためのツール開発などを行っています。 所属チームの体制は? クラウドセキュリティGは現在、大阪2名、東京3名が在籍しています。 KTCへ入社したときの第一印象?ギャップはあった? 入社前にチーム状況・求められていることなど共有いただいていたのでギャップ全然ありませんでした。 現場の雰囲気はどんな感じ? 皆さん優しいので仕事がしやすいです。 利用したことないサービスや技術であっても一緒に調査してくれます! オフィスで気に入っているところ 1階コンビニ、2階レストランがあるので雨で外出たくない時によく利用しています! 大和さん ⇒ 小林さんへの質問 ご趣味は!(アウトドアでもインドアでも構いませんので!) 音楽・ポッドキャスト聴きながら目的もなく歩くのが好きです! HOKAMA ![HOKAMAさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/HOKAMA.webp =300x) 自己紹介 開発支援部企画管理Gの外間です。 主に会社の予算管理や業務フローの調整などを担っている部署となります。 休みの日は小学3年生の息子の町クラブ(サッカー)でコーチをやっています。 趣味はフットサルとゴルフで夏になると日焼け止めを塗っても真っ黒になります。 所属チームの体制は? 企画管理Gは室町2名、大阪1名、名古屋1名です。 KTCへ入社したときの第一印象?ギャップはあった? ある程度のミッション内容を入社前に伺っていたので、あまりギャップは感じませんでした。 現場の雰囲気はどんな感じ? 全員中途採用なので落ち着いた雰囲気です。 オフィスで気に入っているところ 室町7階の休憩室が気に入っています。マッサージ機もあるので体を労わりながら仕事が出来るので! 小林さん ⇒ 外間さんへの質問 室町周辺でおすすめのお店教えてください!(行ってみたいお店でも大丈夫です!) 室町オフィスから少しあるきますが、「新日本橋中華 龍龍龍龍 TETSU」の炒飯が美味しいです。 週一回は通ってます。 きーた ![きーたさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/kiita.webp =300x) 自己紹介 2026年1月入社のきーたです。 セキュリティ・プライバシー部に所属し、福岡オフィス(Fukuoka Tech Lab)で勤務しています。 アビスパ福岡が好きな方、お待ちしてます! 所属チームの体制は? 所属チームは3名体制です。 TFSグループが定める基準をベースとしたセキュリティのアセスメントを主に担当しています。 少人数なのでコミュニケーションも取りやすく、日々連携しながら進めています。 KTCへ入社したときの第一印象?ギャップはあった? 会社のカルチャーや雰囲気など、良い意味で入社前に抱いた印象とのギャップはありませんでした。 入社後のフォロー面談でも「ギャップはありましたか?」と聞かれますが、いつも「何もないですね」と答えていますw 現場の雰囲気はどんな感じ? 「個々がプロフェッショナルでありつつ、しっかりチームで連携して動ける」といった印象です。 困ったときはすぐに相談に乗ってもらえるので助かっています。 オフィスで気に入っているところ 立地がいいところ。あとは地下街と繋がっていたら最高でした。 外間さん ⇒ 紀伊さんへの質問 これまで仕事で一番やらかしたことはどんなことですか?(言える範囲でお願いします) 言えることだと…、某大手メーカーさんの重要拠点のインフラを数時間止めてしまったこと、でしょうか。 あの経験があったおかげて、作業は人一倍慎重になりました!! sasanoshouta ![sasanoshoutaさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/sasanoshouta.webp =300x) 自己紹介 AIファーストグループでAIエンジニアをしています、sasanoshoutaです。 社内外に対して生成AI活用の推進の為に折衝からPoC、実装までを幅広く行う業務に取り組んでいます。 所属チームの体制は? AIファーストグループは東京9名、名古屋3名、大阪1名の体制を敷いています。 KTCへ入社したときの第一印象?ギャップはあった? 入社間もなく、チーム内にはいい意味で上下の関係がなく、相互に取り組んでいることについて共有しながら技術的な共有や議論について交わすことができる印象を持ちました。 また、事前に自分への期待値や会社・チームの状況を聞いた上で役割を想像しながら入社しているので、ギャップはありませんでした。 現場の雰囲気はどんな感じ? チーム全員が同じ取り組みをしている訳ではないですが、共通言語として「誰の為のものか」を全員が常に意識しながら目の前の事に集中して取り組んでいる雰囲気が常にあります。 オフィスで気に入っているところ 日本橋の室町という歴史あるエリアにあるオフィスで、オフィスの内装もモダンで働きやすいですが、周辺のロケーションも気に入っています。 紀伊さん ⇒ 笹野さんへの質問 今年のサッカーW杯で日本以外に注目している国はありますか? たくさんあります。 優勝候補スペイン・フランスや、逸材を輩出し続けているアフリカ勢の国々、初参加国の中でもノルウェー・ウズベキスタン・エジプトがどこまでいくのか、数大会振り出場のチェコあたりに注目したいと思ってます! satoshi ![satoshiさんのプロフィール画像](/assets/blog/authors/i.kobayashi/2026-04-30-newcomer-202601/satoshi.webp =300x) 自己紹介 AIファーストグループの天野です!生成AIの活用推進を社内外に向けて活動しています。 非ソフトウェアエンジニアリング領域を中心に活動しています。 動画生成や記事執筆、顧客理解に対してのAI活用検証を行なっています。 所属チームの体制は? AIファーストグループは東京9名、名古屋3名、大阪1名の体制を敷いています。 KTCへ入社したときの第一印象?ギャップはあった? 皆さん主体的に新しい事に取り組む方が多いなと感じました! 私もアイデアを出して試してみるのが好きなので、カルチャーに馴染みやすかったです。 現場の雰囲気はどんな感じ? 皆さん和気あいあいとした感じがありながらも、しっかりと目的感を持っている印象でした。 オフィスで気に入っているところ オフィス周辺が綺麗なので帰宅時に優雅な感じに帰れる所です! 笹野さん ⇒ 天野さんへの質問 入社の決め手を教えてください! AIの非ソフトウェア領域での活用や推進ができるポジションがあり、自分のやりたい事と重なった為です。 元々はソフトウェア領域でAIを活用していましたが、開発経験が浅く方向転換をしたかったので、私と同じような考えの方がいればAIファーストGオススメです! さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
こんにちは。KINTOテクノロジーズ(KTC)でKINTOの中古車ECサイトのディレクターをしている かーびー です。KINTO Technologiesでは 「ユーザーファースト」 を会社の重点方針のひとつに掲げ、全社でさまざまな取り組みが進んでいます。私も自分のチームで、ユーザーインタビューの録画をみんなで見る 「ユーザーインタビューわいわい会」 を試すなど、お客様の一次情報に触れる場づくりに取り組んできました。 こうした取り組みをきっかけに、現在はユーザーファーストを社内に広めるための活動にも運営メンバーのひとりとして関わっています。そのひとつが、今回ご紹介する社内勉強会「ユーザーに寄りそわNight! Vol.02」です。 自分たちのサービスを、ユーザーが使っているところを見たことはありますか? 勉強会の中で参加者にこの質問をしたところ、約7割が「ない」と回答しました。 関心がないのではなく、日常の開発フローの中にその機会がない。要件をヒアリングして、仕様に落とし込んで、品質の高いものを作って届ける。エンドユーザーがどんなふうにサービスを使っているかに触れる機会は、意外と少ないのが現実です。 しかもKINTOテクノロジーズの場合、関わるサービスはトヨタ自動車、株式会社KINTO、開発を担う私たちなど、複数の組織で成り立っています。本来なら1社の中で完結する「作って、使ってもらって、フィードバックをもとに改良する」という流れを、組織をまたいで回していく。ここが私たちの組織ならではの難しさだなと感じています。 関わる人が増えるほど、それぞれの立場や見えている景色は違ってきます。だからこそ、作っている一人ひとりがユーザーの姿を知っていることが大事になる。「あのお客様、こう言っていたよね」という共通の記憶がチームにあると、議論もかみ合いやすくなります。 言われたものを作るだけじゃなく、自分たちから価値を届けていく。「ユーザーに寄りそわNight!」は、ユーザーを知るために踏み出した社内チームの取り組みを紹介する勉強会です。 方法論の講義ではなく、隣のチームの体験を共有する場 この勉強会で大事にしているのは、 「私にもできそう!」 と思えることです。 ユーザーリサーチの手法を網羅的に学ぶ場ではなく、他のチームの取り組みを聞いて「これなら自分のチームでもできそう」と感じてもらう。そんな場でありたいと考えています。 toCでもtoBでも、自分たちの仕事の先には必ず使う人がいます。その誰かに寄りそっていくことが、ユーザーファーストの根っこにある考え方だと捉えています。 こうした考えから、勉強会では実際にユーザーと向き合う取り組みをしたチームに登壇してもらい、何をやって、何に気づいたかを共有してもらう形式にしています。専門的な方法論の紹介ではなく、隣のチームの体験を聞くこと。そこから自分のチームでも試してみたいと思える、小さなきっかけが生まれる場になればと思っています。 ユーザーに寄りそわNight! Vol.02:ユーザーと同じ環境で、プロダクトを使ってみる 2026年3月に開催された第2回の勉強会では、実際にユーザーが使っているのと同じような環境で、自分たちもプロダクトをテストしてみるーーそんな取り組みをしているチームに登壇してもらいました。ユーザーファーストの取り組みとして、社内の各所で生まれている実践をキャッチして勉強会に繋げていく中で、この取り組みのことを知り、声をかけたのが始まりでした。 トヨタグループには「現地現物」——実際の現場に足を運び、自分の目で見て判断する——という考え方があります。登壇してくれたチームはこの考え方をユーザー理解にも活かしたいと、開発メンバー自身がユーザーと同じ状況に身を置いてプロダクトを使ってみる、という取り組みに挑戦していました。 机の前の3秒、現場の3秒 登壇でとくに印象に残ったのは、開発環境ではわからなかったことが、ユーザーと同じ状況で使ってみると次々に見えてきたという話でした。 たとえばアプリの表示にかかる時間。開発環境で3秒かかっても「ちょっと遅いな」と感じる程度だけれど、ユーザーが実際に使う状況で体験する3秒はまるで別物。急いでいるとき、周りに人がいるとき、落ち着いて待てないとき。クーラーの効いたオフィスで感じる3秒と、現場で感じる3秒は、同じ時間とは思えないくらい違って感じられた、と。 「仕様通りに動く」はずのものが、ユーザーと同じ状況に置かれるとまったく違う顔を見せる。データでは見えない課題が、身体で感じられる瞬間でした。 「忖度を捨てる」という第一歩 では、現場で気づいたことをどう日常の開発に持ち帰っていくか。パネルディスカッションで印象に残ったのは、「忖度を捨てる」という言葉でした。 「アプリを使っていて『ここ遅いな』と思っても、『APIをたくさん呼んでるからしょうがないか』と開発者としての忖度をしてしまう。その忖度をあえて捨てて、純粋にユーザーとしてアプリを使ってみることが、まずできる第一歩」 開発者として「これはしょうがないか」と自分で飲み込んでしまう場面は、きっと多くの人に心当たりがあると思います。その忖度を一度横に置いて、純粋にユーザーとしてアプリを触ってみる。大がかりな準備をしなくても、今日から始められる小さな一歩として、とても印象に残った言葉でした。 これからも、小さな一歩を重ねていく Vol.02の懇親会では、「うちのチームでもこういうことをやってみたい、でもどう始めればいいんだろう?」という声や、登壇者を囲んで「どうやって社内を巻き込んでいったんですか?」と具体的な進め方を聞く姿が、あちこちで見られました。 アンケートのフリーコメント欄には、約半数の方が「これから自分のチームでやってみたいこと」を書き込んでくれました。印象的だったのは、toCのサービスを作っているチームだけでなく、業務システムやプラットフォームを担当する方々からも、具体的な一歩の言葉が並んだことです。 「業務システムなのでユーザーがKINTO社員であり距離が近い。実際に業務をやらせてもらったり、フィードバックを貯める場を作ったりして、ユーザーファーストを実践する場を作りたい」 「忖度せずに改善アイデアを出し、検討する。アイデアを歓迎する空気を作っていきたい」 自分たちの仕事の先にいる「使う人」は、toCのお客様だけではありません。社内の誰か、パートナー企業の誰か、ときには自分自身かもしれない。それぞれの現場で、それぞれの「寄り添い方」がある。そのことを、登壇してくれたチームの話と、参加者の声から改めて感じた回でした。 Vol.01の開催から半年、社内Slackチャンネルのメンバーは60人から99人に増え、「うちでもこういうことやってるよ!」と声をかけてくれる人も出てきています。これまで各チームの中に閉じていた取り組みが、少しずつ表に出てくるようになりました。 「ユーザーファースト」は2025年の注力テーマとして始まりましたが、ユーザーのことを考えるのはプロダクト開発の基礎の基礎。一年限りのテーマで終わらせず、Vol.03に向けた準備も進行中です。 大がかりな取り組みでなくても、まずは自分のプロダクトをユーザーとして使ってみることから。気づいたことを隣の人に話してみることから。一つひとつのチームで生まれる小さな一歩を、勉強会という場で共有し、また次の一歩へつなげていく。この取り組みの火を絶やさないよう、これからも続けていきます。