
SRE
イベント

マガジン
技術ブログ
背景 こんにちは。プログラマティック広告配信プロダクトを提供している AJA で DSP チームの ...
はじめに こんにちは。技術戦略部CTOブロックの ikkou です。ZOZOでは毎年、独自の新卒研修を実施しています。さらに昨年からは、日本CTO協会の新卒エンジニア合同研修にも参加しています。参加は任意として、興味を持つ研修を自身で選択できるようにしました。本年度は2名の新卒エンジニアが参加しました。本記事では参加者によるレポートをお伝えします。 目次 はじめに 目次 日本CTO協会の新卒エンジニア合同研修とは 第1回:関係の質を上げるファシリテーション 第2回:Google Cloudで実現するクラウドネイティブ・アーキテクチャ 第3回:エンジニアの働き方とキャリア論 第4回:Codexを使った開発 第5回:GMOペパボによるサーバー解体研修 第6回:Alibaba Cloudから学ぶクラウドの基礎 第7回:合同ISUCON研修 印象に残った学び 業務・キャリアに活かしたいこと まとめ 日本CTO協会の新卒エンジニア合同研修とは 日本CTO協会は、企業の技術責任者が集まり、日本のDX(デジタルトランスフォーメーション)とCX(コーポレートトランスフォーメーション)の推進を目指す団体です。新卒エンジニア合同研修は、その会員企業の新卒エンジニアを対象とした取り組みで、2024年度に始まり、今年で3年目を迎えました。1社では用意しきれない学びの機会を企業の枠を越えて持ち寄り、業界全体で新卒を育てるという趣旨の研修です。 2026年度は全7回で、回ごとに異なる会員企業が講義を担当し、会場もその企業のオフィスへと変わります。扱われた領域は、ファシリテーションからクラウド、キャリア、コーディングエージェント、物理サーバー、パフォーマンスチューニングまで多岐にわたりました。参加者は各社の新卒エンジニア約100名で、講義だけでなくグループワークや懇親会を通じて他社の同期と交流できるのも、この研修の特徴です。 第1回:関係の質を上げるファシリテーション 第1回は北島が担当いたします! 今回は、株式会社メンバーズさんの神尾武志さんによる「関係の質を上げる」ためのファシリテーションを学ぶ講義で、最後にグループワークを実践するという研修でした。 ファシリテーションとは「人々の活動が容易にできるよう支援し、うまくことが運ぶよう舵取りすること」と定義されています。そのために必要なのが参加者同士の関係の質、つまり心理的安全性や相互理解です。この土台を作るのがファシリテーターの役割だという話でした。 具体的な立ち回りとして「伝える・場を見る・働きかける」の3つが紹介されました。 まず「伝える」では、参加者はファシリテーターより情報が少ない状態でその場に来ます。はじめにアジェンダ・趣旨・ゴールを伝えて心の準備をしてもらうことで、参加者が安心して場へ入れるようになります。途中でまとめを挟んで「今どこにいるか」を共有するのも、透明性を保つための大切な作法です。 次に「場を見る」では、参加者同士が議題を介して自走できているかを観察します。うまくいっていないなら介入し、回っているなら手を引く。どちらの判断も「働きかけ」であり、ファシリテーターが場全体を俯瞰して舵取りする姿勢が関係の質を守ります。 最後に「働きかける」では、問いの設計と仕組みの設計が肝でした。「質問ありませんか?」は答えにくく、「どちらかといえば?」「一言だけ感想を」のようにクローズな問いや小さな問いから入るほうが参加しやすくなります。発言しない人に対しても「やる気がない」と捉えるのではなく、事前に書いてもらう、その場で書く時間を取るなど、場の設計で引き出せるという視点は新鮮でした。 グループワークは「懇親会でもっとお互いを知りたくなる自己紹介を作る」がテーマでした。 私のグループは「話したいエピソードを選んで話す」というお題にし、私はファシリテーターを担当しました。 エンジニアならではのエピソードを中心に、お題を自由に選べる形式にしました。その結果、心理的安全性が保たれ、相互理解も自然に進みました。共通点が見えた瞬間に場の雰囲気はぐっと変わり、ファシリテーションの効果を実感しました。 第2回:Google Cloudで実現するクラウドネイティブ・アーキテクチャ 第2回も北島が担当いたします! 今回は、Google Cloudさんによる「次世代のクラウドネイティブ・アーキテクチャ」をテーマにした講義でした。 コンピュートは仮想マシン → コンテナ → サーバーレスという流れで進化しており、今の新規開発ならCloud Runが主流だという話でした。コンテナを渡すだけでデプロイでき、アクセスがなければ料金もかかりません。このシンプルさが強みです。 データベースもAI目線で見ると選び方が変わってきていて、ベクトル検索に対応しているAlloyDB(PostgreSQL互換の高性能版)が今注目されているとのことでした。 その流れで出てきたデータ基盤の話に、最も関心を引かれました。「統合・品質・可読性」の3条件が揃っていないと、AIエージェントは正しく動かないという話でした。散在データを集めて正確にし、人間とエージェントの双方が理解できる形にします。SREが「信頼性」と向き合うときの考え方と同じだと感じました。 こうしたデータ基盤の上に乗るのがGeminiです。テキスト・画像・動画をまとめて処理できるマルチモーダル性が他社との違いで、さらにGemini Roboticsとして映像・音声・物理動作を統合した身体性AIへの展開も紹介されていました。AIが「考える」から「動く」へ変わっていく様子には、少しゾクっとしました。 全体を通して伝わってきたのは、Google Cloudの強みがデータ基盤とGeminiを同じプラットフォームでつなげられる点にあり、AI活用の本質は技術より先にデータを整えることにあるということです。 懇親会ではGoogleのサービスに関するクイズがありました。結果は100人中9位でした。 第3回:エンジニアの働き方とキャリア論 第3回は、佐藤が担当いたします! 今回は、株式会社LayerX代表取締役CTOの松本勇気さんによる発表でした。テーマはエンジニアの働き方とキャリアです。ご自身の経験を交えながら、「キャリアを投資として考える」「フォロワーシップとマネジメント」「LLM時代のエンジニアのキャリア論」という三部構成でお話を伺いました。 第一部のテーマは、キャリアを投資家の視点で捉えるという考え方です。自分が持っている時間、体力、知識、経験、信用、これらはすべて資産であり、キャリアとはその資産を運用し続けるプロセスだと説明されました。そうであれば、投資と同じようにバランスシートやポートフォリオを意識できるはずです。手元の資産をどこへ配分し、どんなリターンを期待するのかを常に考える、という発想です。 興味深かったのは、リスクの取り方についての話です。大きな挑戦にはレバレッジが効く一方で、当然ながら失敗する可能性もあります。しかし、その経験自体も知識として蓄積され、次の判断の精度を上げてくれます。何もしないという選択にもリスクとリターンがある、という指摘には考えさせられました。もう1つ強調されていたのが、周囲のコミュニティです。友人、一緒に働く上司や同僚、社外のエンジニアコミュニティなど、どのような人たちと時間を共にするかが、そのまま自分の成長につながります。コミュニティを充実させること自体が、資産を増やすための投資の1つだといえます。 第二部では、チーム開発とマネジメントについての話がありました。そもそも、なぜチームで開発するのでしょうか。1人で開発すれば意思決定は速く、他人への配慮も要らないため、効率のよい面もあります。それでもチームを組むのは、大規模なものを作るには人数が必要だからです。1人では視点や考慮も不足します。そのうえでチームの効率を落とさないために欠かせないのが、マネジメントだと説明されました。 ここで出てきたのがフォロワーシップという考え方です。マネージャーも完璧ではありません。だからこそメンバー側から自己開示をしたり、自分の考えを言語化して伝えたりすることで、マネージャーが動きやすい状況を作れます。マネジメントされるのを待つのではなく、こちらからも働きかけていくという姿勢です。この話は個人的にとても面白く聞きました。マネジメントは上司から一方通行で受け取るものだと思っていたのですが、実際にはメンバー側からフォロワーシップという形で応えられます。働くうえで自分たちが楽しいと感じられる状態を作るために、こちらから動く余地があるという視点は、これまで持っていなかったものです。 第三部のテーマは、LLMの登場によってエンジニアのキャリアがどう変わるのかです。結論として示されたのは、デザイナーやエンジニアといった職種の垣根が今後どんどん低くなるという見立てでした。LLMを使って設計からデザイン、実装までを一人で担います。そうした働き方が当たり前になっていくという話です。ここで出てきたキーワードがAI builderです。LayerXでは、この言葉を実際に使っているそうです。 では、新卒は何をすればよいのでしょうか。挙げられていたのは、まず師匠を探すことでした。ロールモデルになる人や、優秀だと思える上司の近くで学ぶ価値は大きいという話です。もう1つが、軸をずらして戦うという戦略です。経理とエンジニアリング、営業とエンジニアリングのように、本来は別々だった職種を掛け合わせて自分の立ち位置を作るという考え方です。掛け合わせが差別化になるという指摘は、今後のキャリアを考えるうえで役に立ちそうだと感じました。 第4回:Codexを使った開発 第4回も佐藤が担当いたします! 今回はオンラインでの開催で、OpenAIでCodexの開発に携わる方による講義でした。前半はCodexの使い方の解説、後半は実際にCodexでアプリを作るハンズオンという構成です。 はじめに扱われたのが、コンテキストとは何かという話でした。コンテキストはエージェントのワーキングメモリであり、そこに何をどれだけ載せるかがエージェントの振る舞いを決めます。コンテキストウィンドウには限りがあるため、必要な情報を必要なだけ渡す設計が重要です。あわせて、SkillsとMCPが何を担うものなのか、両者の違いについても解説がありました。 講義でとくに時間が割かれていたのが、Skillsの作成です。面白かったのは、スキルを作るためのスキルが用意されている点でした。作りたい処理の要件をエージェントと対話しながら整理し、改善を重ねて1つのスキルに落とし込んでいきます。手続きを頭の中に置いたまま毎回プロンプトで説明するのではなく、繰り返す作業をスキルとして切り出していく進め方です。さらに、他の人が使っているスキルを自分のCodexでもそのまま動かせるように環境を整える、という話もありました。個人の工夫で終わらせず、チームで共有できる資産にしていく発想です。 複数のエージェントを並行して動かす場合は、それぞれに必要な処理をどう組み込むかが課題になります。ここで紹介されたのが、ツールを実行するタイミングの設定でした。ある処理の前に動かすもの、後に動かすもの、セッションの開始時に動かすものといった設定を組み合わせることで、エージェントが動く土台そのものを設計できます。いわゆるハーネスエンジニアリングにあたる考え方だと理解しました。 登壇者が強調されていたのが、Auto-reviewの機能です。Codexでは、エージェントが操作できる範囲を定めるサンドボックスと、範囲外の操作をする際の承認方法を組み合わせて設定できます。通常の設定では、ワークスペース内の一般的な操作はそのまま実行でき、設定された境界を越える場合に人間へ承認を求めます。Auto-reviewを有効にすると、こうした承認要求の一部を別のレビューエージェントが審査し、その判定に応じて実行を制御します。人間が逐一確認する手間を減らしつつ、無制限に権限を渡すリスクも避けられる仕組みだといえます。このほかにも、デフォルトの設定の考え方、プランモードの使い方、画像を渡した開発の進め方など、実践的なTipsを数多く紹介いただきました。 講義の後半は、実際にCodexを使ったアプリ開発です。私は、自社のキャラクター(箱猫マックスくん)を題材にしたポモドーロタイマーの拡張機能を作りました。作業と休憩の時間を管理するシンプルなツールです。 驚いたのは、そのスピードでした。作りたいものの要件を伝えてから、動くところまでこぎつけるのにかかった時間は10分ほどです。完成したものが次の画面で、集中する時間をカウントダウンし、残り時間に応じてキャラクターがバーの上を進んでいきます。 この規模のアプリケーションであれば、思いついたその場で形にして試せます。わざわざ作るまでもないと諦めていたアイデアも、まず動かしてから判断できるようになります。手元の道具をこうして気軽に作れるという感覚は、講義を聞くだけでは得られないものでした。 普段からコーディングエージェントは使っていましたが、開発している当人から設計の意図を聞けたのは貴重な機会でした。エージェントにどこまで任せ、どこを人間が握るのかの線引きを、権限の設定という具体的な形で考えられるようになったのが一番の収穫です。 第5回:GMOペパボによるサーバー解体研修 第5回は北島が担当いたします! 今回は、GMOペパボさんによる物理サーバーの話でした。 クラウド全盛の今でも物理サーバーには独自の強みがあるという話は、新鮮に感じられました。コストはクラウドより安く抑えられ、メガクラウドや為替レートにも左右されません。クラウドやAIであっても「末端は物理ハードで動いている」という言葉には、強く納得しました。デジタルガジェットとして楽しい、という言い方も好きでした。 ただ管理の大変さも正直に話していただきました。ラッキングやケーブリング、部品交換、温湿度・入退室管理、深夜のトラブル対応など、クラウドなら画面の操作で済むところが、すべて自前になります。スペック選定もシビアで、買ったら変更できない分、過剰でも不足でも損をします。寿命も3〜5年で、老朽化したら計画的なリプレースとデータ移行が必要になるという話をしていただきました。 その話を聞いた後で、実際にサーバーを解体する体験がありました。CPU、ECCメモリ、ストレージ、マザーボード、冗長電源、NIC、冷却ファン、そしてOSが止まっていても遠隔操作できるリモート管理カード(iLO・iDRAC)まで、構成要素を一通り手で触りました。話で聞いていた「管理が大変」の意味が、実物を目の前にすると一気に腑に落ちました。 普段はKubernetesやクラウドの抽象レイヤーの上で仕事をしているので、こうして物理を触る機会はなかなかありません。重さがあり、熱を持ち、騒音も出します。そうした実体のあるコンピューターと向き合うと、インフラへの解像度は一段上がった気がしました。 第6回:Alibaba Cloudから学ぶクラウドの基礎 第6回は佐藤が担当いたします! 今回のテーマはAlibaba Cloudです。中国の巨大ECを運営するアリババグループ傘下のクラウド事業会社が提供するパブリッククラウドサービスです。欧米含めグローバルに30リージョン・104のアベイラビリティゾーンを展開しており、アジア太平洋地域では1位、世界では4位という立ち位置にあると紹介されました。中でも特にアジア市場に集中的に投資をしており、中国をはじめとするアジア市場との結びつきが強いのも特徴といえます。講義はクラウドの基礎から始まり、後半はAI時代に向けたクラウドの話へと進んでいきました。 まず扱われたのが、クラウドとオンプレミスの違いです。自社でサーバーや機器を購入して設置、管理するのがオンプレミス、プロバイダーが管理するインフラをネットワーク越しに借りるのがクラウドという整理でした。クラウドのメリットとしては、負荷に応じてリソースを増減できるスケーラビリティと、使った分だけ支払えばよいコスト効率が挙げられます。 面白かったのは、この違いを会計の視点から捉え直す話です。オンプレミスはサーバーという資産を購入するため、初期に大きな投資が発生します。これがCapEx、資本的支出です。対してクラウドは月々の利用料として支払うため、発生した期に費用として計上されます。こちらがOpEx、運用費用にあたります。CapExでは、ピーク時の負荷を事前に予測して設備を購入しなければなりません。予測が外れれば過剰投資になり、逆に足りなければ機会損失につながります。OpExであれば需要に応じてコストが変動するため、事業の変化に追従しやすくなります。クラウドが選ばれる理由を技術面だけでなく経営面から説明されると、腹落ちの度合いが違いました。 続いて、どこまでをプロバイダーに任せるかによる分類、クラウドサービスモデルの話です。まずは、仮想マシンやネットワークなどインフラ環境を提供するIaaSがあります。そのうえに、アプリケーションの実行基盤まで用意するPaaS、アプリケーションそのものを提供するSaaSと続きます。プロバイダーに任せる範囲が広がるほど、利用者が管理する範囲は狭くなります。さらに近年は、AIの大規模モデルをサービスとして利用できるMaaS(Model as a Service)も加わってきているという話がありました。クラウドの分類そのものが、AIの登場によって更新されつつあるということだと理解しました。 構成の話として紹介されたのが、ハイブリッドクラウドとマルチクラウドです。ハイブリッドクラウドは、パブリッククラウドとプライベートクラウドやオンプレミスを組み合わせる構成を指します。一方のマルチクラウドは、AWSとGoogle Cloudのように複数のパブリッククラウドを組み合わせる構成です。ベンダーロックインを避けられる、それぞれの強みを活かせるといった利点がある反面、運用や監視が各クラウドで異なるため複雑になるという課題もあります。この話は、自分の業務と結びつけて聞けました。ZOZOでも複数のクラウドを併用しており、普段はその上で開発をしています。当たり前のものとして使っていた構成に名前と理由がついた感覚です。なぜこうなっているのかを考える良いきっかけになりました。 講義の終盤で取り上げられたAIとセキュリティの話には、特に興味を引かれました。AIの普及によって、必要な電力量やリクエストの量が急増し、AIのためのデータ基盤も求められるようになっています。そうした変化のなかで、AI時代のクラウドのセキュリティをどう担保するかに力を入れているという話でした。インフラや資源に強みを持つからこそ描ける展望があるのだと感じました。 第7回:合同ISUCON研修 第7回は北島が担当いたします! 今回は株式会社PR TIMESさんによるISUCON研修 *1 です。Webアプリケーションのパフォーマンスチューニングに取り組みました。チーム戦ではなく個人戦で、AIをどう活用できるかが問われる内容でした。 GETのインデックス最適化、DBのJOIN最適化、POSTの非同期処理と順番にチューニングし、結果は90人中7位でした。スコアが上がるたびに手応えはありましたが、40万点あたりで完全に詰まってしまい、1位の60万点には遠く及びませんでした。 壁にぶつかって気づいたのが、「計測が先、最適化は後」とするべき順番を逆にやっていたということです。アクセスログを集計してどのエンドポイントが遅いかを可視化し、スロークエリを洗い出してからボトルネックを潰していくという順番が正解です。自分は「これが遅そう」という勘で先に手を動かしていました。やったこと自体は間違いではありませんが、どこに時間を使うべきかが見えていないままでした。 AIの使い方も、同じ話でした。後半で引き出しが尽きてきたとき「なんか遅い気がするんだけどどうすればいい?」のような質問をしてもスコアはまったく伸びませんでした。AIは事実ベースで動くため、感覚を渡しても答えは出てきません。「このエンドポイントのレイテンシが〇ms出ている、原因として何が考えられる?」という具体的な数字があってはじめて、有効な回答が返ってきます。 裏を返せば、わからないことやぼんやりした概念はそのままにせず、AIに聞いて解消してしまうのが一番いい使い方です。AIには納得がいくまで質問でき、聞けばすぐ答えてくれます。ぼんやりしたまま放置する理由はありません。事実をつかみ、わからないことをなくしていくのがAI時代の学び方だと思いました。 印象に残った学び 全7回を振り返ると、扱われた領域はキャリアからインフラ、AIまで幅広いものでした。それでも通して聞くと、回をまたいでつながる話がいくつもあります。2人に共通して印象に残ったのは、次の3つです。 1つ目は、AIとの向き合い方が具体的な設計の問題として語られていたことです。第4回では、コマンドの実行権限をどう設定するか、どのタイミングでどのツールを動かすかという形で、エージェントが動く土台そのものを設計する話がありました。第7回では、感覚を投げても答えは返ってこず、具体的な数字を渡してはじめて有効な回答が得られるという経験をしました。第3回で出てきたAI builderという言葉も、AIを前提に自分の仕事の範囲を引き直す姿勢を指しています。AIを使いこなすというと、うまい指示を出す技術の話に聞こえがちです。しかし研修を通して見えてきたのは、どこまでを任せ、何を渡し、どこで人間が判断するのかを決める設計の話でした。 2つ目は、抽象化されたレイヤーの下には実体があるということです。第5回のサーバー解体は、普段の開発では意識しない層に触れる機会でした。第2回と第6回でクラウドの話を聞いた後だったこともあり、抽象化されたサービスの下には物理的な実体があるという当たり前の事実を、実感を伴って理解できました。第6回で出てきたCapExとOpExの話も、同じ方向を向いています。クラウドを使うというのは、本来自分たちが抱えるはずだったコストとリスクを、誰かに預けるという判断です。何が抽象化されているのかを知っていれば、預けている範囲も、自分たちが引き受けるべき範囲も見えてきます。 3つ目は、場や関係性も設計できるという視点です。第1回のファシリテーションと第3回のフォロワーシップは、扱っている題材こそ違うものの、根っこは同じでした。参加者が発言しないのは、やる気がないからではなく場の設計に理由があるのかもしれません。マネジメントがうまく回らないのは、マネージャーだけの問題ではなくメンバー側から働きかける余地があるのかもしれません。どちらも、目の前の状況を所与のものとして受け取らず、自分の側から設計できると捉える見方です。チームで働くとき、技術と同じくらい実践的な考え方だといえます。 業務・キャリアに活かしたいこと 私たちは配属されたチームが異なり、日々向き合っている技術も違います。それでも研修を終えて、共通して持ち帰りたいと考えたことが3つありました。 1つ目は、計測を先に置くことです。第7回で痛感したとおり、勘を頼りに手を動かしても、どこに時間を使うべきかは見えてきません。改善の施策を考える前に、何が起きているのかを数字で押さえることが重要です。当たり前でありながら、忙しくなるほど飛ばしてしまいがちな手順だと反省しました。 2つ目は、エージェントに任せる範囲の設計です。日々の開発でコーディングエージェントを使う場面は増えていますが、その多くは個人の工夫にとどまっています。繰り返す作業をスキルとして切り出し、チームで共有できる形にしていくことは、今すぐ始められる改善だと感じました。 3つ目は、チームへの働きかけです。ミーティングの進行や他チームとの相談の場面で、自分から場を作れるようになりたいと考えています。関係の質が土台にあるという第1回の話は、そのまま日々の仕事に持ち込めるものでした。 キャリアという長い時間軸で見ると、第3回の軸をずらして戦うという話が心に残っています。新卒の今は、複数の領域を掛け合わせられる場所に自分を置いていくための資産を積む時期なのだと考えています。 まとめ 全7回を終えて振り返ると、扱う領域は毎回まったく違っていたにもかかわらず、繰り返し語られていた話題があります。AIによって仕事がどう変わるのか、という問いです。 各回で語られていたのは、たとえば次のような話です。 職種の垣根は低くなり、1人が担える範囲は広がっていく エージェントに任せる範囲を決め、動く土台を整えることが仕事になる データが整っていなければAIは力を発揮できない 感覚ではなく事実を渡してはじめて、有効な答えが返ってくる それぞれ別の会社の、別のテーマの講義でしたが、いずれも同じ変化を違う角度から説明しているように聞こえました。 共通していたのは、AIが人の仕事を奪うという語り口ではなかったことです。むしろ、手を動かす部分をAIが担えるようになったからこそ、何を任せて何を自分で判断するのかを決める仕事の比重が増していくという見立てでした。新卒としては不安を覚えてもおかしくないテーマですが、研修を終えた今は、自分から動かせる余地の大きさのほうを強く感じています。 そしてもう1つ、この研修ならではの価値が、他社の同期と過ごす時間でした。同じ年に社会人となった人たちが、それぞれ違う環境で何を考えているのかを知ることができました。グループワークや懇親会で交わした会話は、講義とは別の刺激になりました。来年以降に参加する方にも、ぜひこの時間を楽しんでもらえたらと思います。 ZOZOでは、新卒・中途に限らず、一緒にサービスを作り上げてくれる方を募集中です。ご興味ある方は以下のリンクからぜひご応募ください。 corp.zozo.com *1 : 「ISUCON」は、さくらインターネット株式会社の商標または登録商標です。
はじめに こんにちは、SRE部カート決済SREブロックの伊藤( @_itito_ )です。普段はZOZOTOWNのカート決済機能のリプレイス・運用・保守に携わっています。また、データベース(以下DB)領域でのテックリードを務めており、DBREとしてDB周りの運用・保守・構築に関わっています。 ZOZOTOWNでは以前からリプレイスを進めており、SQL Serverを中心としていたDB構成も、マイクロサービス化に伴ってAurora MySQLなどへ分割されてきました。 かつてはビジネスロジックの多くを、SQL Serverのストアドプロシージャ(以下、ストアド)が担っていました。データの近くで処理するため高速であり、ロジックをDBに集約できる利点があったためです。 一方で、「ストアドをスケールさせづらい」「ストアドのテストが書きづらい」といったような課題も抱えていました(参考: ZOZOTOWNリプレイス2020 )。 そのためリプレイスを進める中で多くのストアドが剥がされ、ロジックはアプリケーション側へ移ってきました。ただし、すべてを移し終えたわけではありません。残っているストアドもあり、改修は時折発生します。 そして、わずかな変更でも実行計画は変わります。結果として性能が大きく劣化する場合もあります。 本記事では、ストアドの変更をPull Requestの時点で検知する実行計画チェックCIを構築した取り組みを紹介します。GitHub ActionsとArgo Workflowsで本番DBへ一切接続せず推定の実行計画を取得し、Claude Code Actionによるレビューで性能懸念の有無を判定する構成としました。 目次 はじめに 目次 背景と課題 ストアド改修による性能劣化 従来のレビュー運用の限界 仕組みに求めた要件 CIの全体像 処理の流れ 各コンポーネントの実装 実行計画の取得 推定の実行計画を取得する CI用の一時名を付与する 実行計画のチェック ルールベース解析 変更前後の比較 LLMレビュー 判定をCIの成否へつなぐ マージをブロックする仕組み ワークフローの構成 dbre-review-passed ラベルによる救済 ラベルの抜け道をふさぐ 分析結果例 まとめと展望 背景と課題 ストアド改修による性能劣化 きっかけは、ストアドの改修リリース後に発生したDB負荷高騰でした。改修によってWHERE句で参照するカラムが変わり、それまで効いていたインデックスが使われなくなっていました。 問題が表面化したのは、リリースから時間が経ってからでした。通常のアクセス量であれば処理しきれていたためです。負荷の大きいイベントを迎えたところで、大量の読み込みによってDBが耐えられなくなりました。 ただし、より本質的な問題は別にありました。改修内容が数行の軽微なものだったため、リリース前後の性能検証が省略されていた点です。 対策として、リリース後の性能劣化をDBのパフォーマンスチェックやアラートで検知する仕組みも別途用意しました。ただし、リリース後の検知では影響が出てしまう可能性があります。理想はリリース前にも検知できることでした。 従来のレビュー運用の限界 ストアドの性能検証には、実行計画の確認が必要です。従来は、次のような観点での確認を開発者に委ねていました。 推定行数が極端に多い処理がないか テーブルのScanが発生していないか Index Seekでも絞り込める条件になっているか 実際の行数と予測の行数に大きな乖離がないか これらは実行計画を読める人でなければ判断できません。しかも、確認の実施やDBREへの相談も、開発者の判断次第でした。 つまり「改修が軽微かどうか」を、性能への影響を評価する前に人が判断していたのです。軽微な変更ほど検証は省略されやすくなります。この構造そのものが課題でした。 仕組みに求めた要件 再発防止策を検討する中で、仕組みに求める要件を4つ整理しました。 強制力があること … 人の判断で検証を省略できないよう、Pull Requestの単位で自動実行する 本番DBに影響を与えないこと … 検証のために本番DBへ接続したり負荷をかけたりしない 本番相当のデータが入った環境で確認できること … データがほとんどない開発用のDBでは、実行計画がその規模に合わせて最適化されてしまい、正しい情報が取れない 開発者の手間を増やさないこと … 既存のPull Requestフローに組み込み、追加の操作を求めない 要件2と要件3は、そのままでは両立しません。本番のデータ量に近い環境が必要ですが、本番DBは使えないためです。 そこでSTG環境を利用することとしました。ZOZOTOWNでは本番環境へのリリース前にSTG環境での動作確認を必須としており、STG環境へのリリースは stg ブランチへのマージで行う運用です。つまり、ストアドの改修は必ず stg ブランチ向けのPull Requestを通ります。 さらにSTG環境は、負荷試験の実行環境としても使っています。そのため本番規模のデータ量を保つようにしており、実行計画の確認先としても適していました。 stg ブランチ向けのPull Requestでストアドが変更されたときに、STG環境で推定の実行計画を自動取得して解析するCIを構築しました。開発者が必ず通る経路にチェックを置けるうえ、4つの要件をすべて満たせます。 CIの全体像 処理の流れ 構築したCIは、GitHub ActionsとArgo Workflows(Amazon EKS上で稼働)の2つで役割を分担しています。両者のデータの受け渡しはすべてAmazon S3を経由します。全体像は次の図のとおりです。 処理の流れは次のとおりです。 stg ブランチ向けのPull Requestが作成され、GitHub Actionsが起動する 追加・変更されたストアドの定義ファイルをすべて検出する ストアド名をCI用の一時名にリネームしたSQLファイルを作成する SQLと実行先のDB情報をまとめたJSONファイルをS3へアップロードする argo submit --wait でArgo Workflowsを起動する Argo WorkflowsがJSONを読み込み、ストアドごとに CREATE PROCEDURE → SHOWPLAN_XML で実行計画の取得 → DROP PROCEDURE を繰り返す 実行計画XMLと実行ステータスをS3へアップロードする GitHub Actionsが結果を収集し、ルールベース解析と変更前後の比較を実施する 解析結果をPull Requestへコメントする Claude Code Actionがレビュー結果と判定( block / pass )を追記する 手順4でDB情報を渡しているのは、対象となるDBが1つではないためです。ZOZOTOWNには複数のDBが存在し、ストアドの定義ファイルの配置場所に応じて実行先が変わります。そのため、どのストアドをどのDBで確認するかをJSONに含めてArgo Workflowsへ渡しています。 GitHub ActionsとArgo Workflowsで処理を分けたのは、ネットワーク構成が理由です。対象のDBはオンプレミス環境のプライベートなネットワーク内にあり、GitHub Actionsのランナーからは直接届きません。一方、既存のAmazon EKSクラスタからはDBへ到達できます。そこでDBへの接続はArgo Workflowsに任せ、GitHub Actionsは変更検出・解析・コメントだけを担う構成としました。 AWSへの接続には、CI/CDで一般的なOIDCによるIAM Roleの引き受けを利用しています。長期のアクセスキーを保持せずに接続でき、信頼ポリシーでは対象リポジトリのPull Requestイベントのみに絞っています。 両者の受け渡しにS3を選んだのは、実行計画XMLがサイズの大きなファイルになるためです。Argo WorkflowsのパラメータやGitHub Actionsのoutputで渡すには不向きでした。 各コンポーネントの実装 新たに用意したインフラリソースは、次の3種類です。 連携基盤 … GitHub Actions用のIAM Role(OIDC)、受け渡し用のS3バケット、Argo Workflows用のIRSAロール 実行計画の取得 … STG DBで実行計画を取得するArgo WorkflowTemplate LLMレビュー … モデル呼び出し用のApplication Inference Profile モデルの呼び出しにApplication Inference Profileを使っているのは、コスト配分タグを付与するためです。デフォルトの推論プロファイルでは、どのアプリケーションがコストを発生させたのかを追跡できません。 インフラのほかに、GitHub Actions側で動く処理をPythonのツールとして実装しました。責務ごとにモジュールを分割しています。 モジュール 役割 変更検出 git diff でPull Request内の変更されたストアド定義を抽出 定義パーサ ストアド定義の文字コードのデコード、CREATE名の置換 CI名の生成 CI用の一時名を生成(識別子128文字の上限に対応) ワークフロー実行 Argo Workflowsの呼び出しとS3経由の入出力 実行計画の解析 SHOWPLAN_XMLのルールベース解析 実行計画の比較 変更前後の実行計画の比較 コメント生成 Pull Requestコメント(Markdown)の生成 実行計画の取得 ここからは、処理の流れの手順6にあたる部分を説明します。STG環境のDBに一時的にストアドを作成し、推定の実行計画を取得する処理です。 実際にDBへストアドを作成するため、既存のストアドを壊さないための安全策も必要でした。取得の方法と、そのために重ねた安全策の順に説明します。 推定の実行計画を取得する 本CIが取得するのは、 SHOWPLAN_XML による 推定の実行計画 です。 SET SHOWPLAN_XML ON を有効にすると、以降のステートメントはコンパイルだけが行われ、実行計画が返ります。ストアドを EXEC しても、中のクエリは実行されません。 learn.microsoft.com 実行されないという性質には、もう1つ利点があります。 ストアドのパラメータを渡さなくても実行計画を取得できる 点です。 実際にストアドを実行する場合、必須のパラメータを省略すると「プロシージャまたは関数 'X' にはパラメータ '@p' が必要ですが、指定されていません」というエラーになります。しかし SET SHOWPLAN_XML ON の状態ではステートメントが実行されないため、この検査が働きません。そのため引数なしの EXEC だけで実行計画が返ります。 本CIはこの挙動を利用し、パラメータを一切渡していません。ストアドごとに引数を用意する必要がなく、CIとして自動化しやすくなります。 ただし精度は落ちます。パラメータの値が不明なため、オプティマイザは統計情報のヒストグラムではなく、密度ベクターによる平均値から行数を見積もります。データの偏りが大きい列では、本番の実行時と計画の形状が変わる場合もあります。 一方で、どのインデックスが使えるかという構造的な判断は、パラメータの値に左右されません。冒頭の事例のように参照するカラムが変わってインデックスが効かなくなるケースは、変更前後の実行計画を比べればスキャンの出現として現れます。精度が問題になるのは、値の偏りによって選ばれる計画が変わるような場合です。 取得の処理そのものは単純です。Argo WorkflowsからはsqlcmdをインストールしたPodを起動し、次のようなSQLを実行しています。 SET NOCOUNT ON ; GO SET SHOWPLAN_XML ON ; GO EXEC ${CI_PROCEDURE_NAME}; -- CI用にリネームして作成したストアド GO SET SHOWPLAN_XML OFF; GO CI用の一時名を付与する 作成するストアドには _CI_PR<Pull Request番号>_add|before|after_<ストアド名> という一時名を付与します。新規追加は add 、既存の変更は変更前が before 、変更後が after です。 _CI_PR で始まる名前は通常のストアドでは使わない形式のため、既存のストアドと衝突しません。 SQL Serverの識別子は最大128文字です。プレフィックスの付与で上限を超える場合は、元の名前を切り詰めてハッシュを付与し、一意性と可読性を両立させました。 def build_ci_name (proc_name: str , pr_number: int , variant: str ) -> str : suffix = SUFFIX_BY_VARIANT[variant] prefix = f "_CI_PR{pr_number}_{suffix}_" candidate = f "{prefix}{proc_name}" if len (candidate) <= MAX_IDENTIFIER_LEN: return candidate # 超過時: 元名を切り詰め + 8桁ハッシュで一意化 digest = hashlib.sha1(proc_name.encode( "utf-8" )).hexdigest()[: 8 ] budget = MAX_IDENTIFIER_LEN - len (prefix) - 1 - len (digest) if budget < 1 : return f "{prefix}{digest}" [:MAX_IDENTIFIER_LEN] return f "{prefix}{proc_name[:budget]}_{digest}" この関数が返すのはスキーマを含まない名前です。Argo Workflowsへ渡す際に [<スキーマ>].[<CI用の名前>] の形へ修飾しています。 さらにArgo Workflows側でも、渡されたSQLにCI用の名前が含まれているかを検証しています。万が一既存のストアド名でCREATEしようとした場合に、チェックを失敗させるためです。 # The caller supplies the fully-qualified CI procedure name and a # definition that already CREATEs under that name. Validate the form. if ! printf ' %s ' " $CI_PROCEDURE_NAME " | grep -qE ' ^\[[^]]+\]\.\[[^]]+\]$ '; then printf ' ERROR: ci_procedure_name must be in form [schema].[name], got: %s\n ' " $CI_PROCEDURE_NAME " | write_fail exit 0 fi # Sanity check: the definition must reference the CI name, so we never # accidentally CREATE under the original (production) procedure name. if ! grep -qF " $CI_PROCEDURE_NAME " /tmp/ci_def.sql ; then { printf ' ERROR: supplied definition does not reference %s; caller must rename to the CI name before submitting.\n ' " ${CI_PROCEDURE_NAME} " printf ' ---- supplied definition (head) ----\n ' head -c 2000 /tmp/ci_def.sql } | write_fail exit 0 fi GitHub Actions側で一時名に置換する処理が正しく動くことを前提にせず、DBに触れる直前でもう一度検証しています。 一時ストアドがSTG環境に残り続けないよう、 trap でエラー時にも必ず DROP PROCEDURE が実行されるようにしています。処理の冒頭でも同じ削除処理を呼び、前回の実行が異常終了して残っていた場合に備えました。 実行計画のチェック ここまでで実行計画XMLが手に入りました。続いて、処理の流れの手順8から手順10にあたる部分を説明します。 チェックは次の順に実行します。 ルールベース解析 … 決められた観点を機械的に抽出し、Pull Requestへ一覧をコメントする 変更前後の比較 … 変更によって悪化した点を抽出する LLMレビュー … 1と2の結果および実行計画XMLを読み、性能懸念の有無を判定する 最終的な判定を担うのはLLMレビューです。前段の2つは、LLMが実行計画を読み解くためのヒントを用意する簡易チェックという位置づけであり、検出結果そのものでマージをブロックしません。実行計画の解釈はLLMに委ねる方針としたため、ルールベース解析は簡易なものに留めています。 ヒントの用意をLLMに任せず、Python側で先に実行しているのには2つの理由があります。1つは、コストの増加率のような数値の算出や比較を機械的に処理することで、LLMへ渡す値が実行のたびに揺れるのを避けられるためです。もう1つは、LLMレビューより前にPull Requestへコメントまで済ませておくことで、LLMレビューが失敗した場合でも最低限のフィードバックが残るためです。 ルールベース解析 取得したSHOWPLAN_XMLを解析し、性能上の懸念を検出します。検出する観点は次の4つです。 観点 検出内容 Missing Index 推奨インデックスの未作成。Impactが50%以上なら high Scan / Lookup系 Table Scan / Clustered Index Scan / Index Scan / Key Lookup / RID Lookup 高コスト演算子 推定サブツリーコストが10以上の Sort / Hash Match / Nested Loops / Table Spool Warnings 暗黙の型変換、結合述語なし、tempdbへのスピル、統計情報のない列 たとえば暗黙の型変換(PlanAffectingConvert)は high として扱っています。冒頭の事例と同様に、インデックスが効かずスキャンへ変わる結果を招くためです。 変更前後の比較 既存ストアドの変更の場合は、変更前と変更後の両方の実行計画を取得して比較します。Pull Requestのベースブランチ( stg )上の定義と、Pull Requestの定義をそれぞれSTG環境に作成する形です。 悪化の判定基準は次の2つです。 総推定コストの増加率が20%以上 変更後にのみ出現した懸念(Missing Index / Warning / 高コスト演算子 / スキャン) 懸念の同一性は、ステートメント全文ではなくカテゴリとタイトルの組で判定しています。変更によってSQLの文言がわずかに変わっても、同じ懸念を「新規」と誤検知しないためです。 LLMレビュー ルールベース解析は決められた観点しか見られません。そこで、観点の外まで踏み込んだ判定をAmazon Bedrock経由のClaudeに任せています。実装には claude-code-action を使用しています。 github.com LLMには解析結果( findings.json )と実行計画XMLの両方を渡し、 XMLを一次ソースとして優先的に読ませる プロンプトとしました。ルールベースの抽出結果は照合用の参考情報として扱わせています。 - name : LLM review id : claude_review uses : anthropics/claude-code-action@a92e7c70a4da9793dc164451d829089dc057a464 # v1.0.159 with : github_token : ${{ secrets.GITHUB_TOKEN }} use_bedrock : "true" use_sticky_comment : "true" settings : | { "env" : { "ANTHROPIC_DEFAULT_SONNET_MODEL" : "${{ secrets.BEDROCK_CLAUDE_SONNET_INFERENCE_PROFILE_ARN }}" } } claude_args : | --max-turns 20 --model sonnet --json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["block","pass"]},"review":{"type":"string"}},"required":["verdict","review"]}' prompt : | このPRで変更されたストアドプロシージャの推定実行計画チェック結果を、性能観点でレビューしてください。 参照ファイル(${{ env.OUTPUT_DIR }}/ 配下): - findings.json … ストアドごとのルールベース解析結果(Missing Index / Scan / 高コスト演算子 / 警告 / before-after比較) - plans/<proc>.after.xml , plans/<proc>.before.xml … 取得した SHOWPLAN_XML(存在する場合) やること : 1. まず findings.json を読み、各ストアドの status / change_type / proc_name を把握する。 2. `review` フィールドの冒頭に、対象ストアドの一覧を以下の形式で出力する(findings.json の内容をもとに作成): - 各ストアドの proc_name / change_type(追加 or 変更)/ status(実行計画取得済み / スキップ / 取得失敗) 3. status=analyzed のストアドについて、plans/ ディレクトリを確認し、<proc>.after.xml(および modified の場合は <proc>.before.xml)が存在すれば、 **XML を一次ソースとして優先的に読み込み**、以下の観点で直接分析する: - オペレータ構成(Seek vs Scan、Hash/Merge/Nested Loops の選択、Sort の有無) - EstimatedTotalSubtreeCost の配分と高コストノード - MissingIndex 要素の Impact 値とカバー列 - Warnings(PlanAffectingConvert / NoJoinPredicate / SpillToTempDb) - before→after で計画形状・コストが悪化していないか(コスト値も明記する) findings.json の findings はルールベースの抽出結果として参照し、XML から読み取れる追加の懸念と照合する。 4. status=plan_failed(Argo ワークフローでの実行計画取得に失敗したもの)は findings.json の notes にエラー詳細が含まれる。 エラー内容(構文エラー・テーブル不在・権限不足 等)を読み取り、原因と対処法を開発者向けに日本語で説明する。 5. status=skipped(実行計画を取得できなかったもの)は XML が存在しないため、変更されたストアド定義の CREATE 文を読み、 SELECT * / NOLOCK の濫用 / 暗黙の型変換を招く比較 / インデックス非対応のWHERE / カーソル使用 等の静的な懸念があれば指摘する。 6. 改善提案(インデックス追加、述語の見直し等)を簡潔にまとめ、日本語でレビュー内容を Markdown 形式で作成し `review` フィールドに設定する。 7. 以下の基準で判定し、結果("block" または "pass" )を `verdict` フィールドに設定する。 - block : 変更後に severity=high の新規発生・悪化がある、または重大な静的懸念がある - pass : それ以外(status=plan_failed のみの場合も pass とする) 注意 : - これは推定実行計画に基づく参考情報。パラメータを渡さずに取得しているため、本番と計画形状が異なりうる旨を `review` の冒頭(対象一覧の直後)に記載する。 - 懸念が無ければ「重大な性能懸念は検出されませんでした」と簡潔に述べる。 判定をCIの成否へつなぐ 先ほどのステップ定義では、 claude_args に --json-schema を指定していました。このオプションを渡すと、最終的な出力が指定したJSON Schemaに沿う形へ強制されます。今回は verdict ( block / pass )と review (レビュー本文)の2つを必須項目として定義しました。 結果は steps.<id>.outputs.structured_output から取得できます。次のように verdict を読み取り、 block であればジョブを失敗させています。 - name : Check LLM verdict if : vars.SP_PLAN_CHECK_LLM_VERDICT_ENABLED == 'true' env : STRUCTURED_OUTPUT : ${{ steps.claude_review.outputs.structured_output }} run : | VERDICT=$(echo "$STRUCTURED_OUTPUT" | jq -r '.verdict // "pass"' ) echo "LLM verdict: ${VERDICT}" if [ "$VERDICT" = "block" ] ; then echo "::error::LLMレビューにより重大な性能懸念が検出されました。レビューコメントを確認してください。" exit 1 fi 構造化出力を強制しているため、自然言語の応答をパースする必要がありません。LLMの判定をそのままCIの成否へ接続できます。 なお、判定によるブロックはリポジトリ変数 SP_PLAN_CHECK_LLM_VERDICT_ENABLED で切り替えられるようにしました。テスト運用の段階では無効にしておき、誤検知の傾向を確認してから有効化する想定です。 マージをブロックする仕組み ここからは、チェック結果をマージの可否へ接続する仕組みを説明します。ただし 現時点では、この章で説明する設定をまだ有効にしていません 。テスト運用として、結果をPull Requestへコメントするだけの状態で誤検知の傾向を見ています。 ワークフローの構成 ここまで説明したチェックは、1本のGitHub Actionsワークフローで実行しています。 pre-check / plan-check / llm-review の3ジョブで構成しています。 stg ブランチのブランチ保護ルールで plan-check と llm-review を必須ステータスチェックに指定すると、チェックNG時にマージがブロックされます。 加えて、このワークフローを補助する目的で簡単なワークフローを2本追加しています。どちらも後述する dbre-review-passed ラベルの扱いを制御するものです。 dbre-review-passed ラベルによる救済 推定の実行計画によるチェックは万能ではありません。NGと判定されたが実際には問題ないケースや、CI自体が失敗するケースもあります。 そこで、DBREが結果を確認して問題ないと判断した場合に dbre-review-passed ラベルを付与すると、マージできる仕組みにしました。ラベルが付与されている場合は pre-check ジョブが後続をスキップします。 - name : Check dbre-review-passed label id : check_label run : | LABELS=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/labels" --jq '[.[].name] | @json' ) if echo "$LABELS" | jq -e 'contains(["dbre-review-passed"])' > /dev/ null ; then echo "dbre-review-passed ラベルが付与されているため、後続チェックをスキップして成功終了します。" echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi GitHubの仕様上、ジョブレベルのスキップはSuccess扱いになります。そのため必須ステータスチェックを設定したままでも、ラベルによる救済フローが機能します。 ラベルの抜け道をふさぐ ラベルによる救済は、そのままでは抜け道になります。開発者が自分でラベルを付与すればチェックを回避できてしまいます。 ここでGitHubの仕様が制約になりました。 特定のラベルについて、付与できるユーザーを制限する仕組みが存在しません。 ラベルの付与はTriage以上のロールに許可された操作であり、ラベルの種類によって権限を分けることはできません。 docs.github.com そこで、付与を防ぐのではなく、付与された後に取り消す方針としました。ラベルの付与をトリガーとしてワークフローが起動し、条件を満たさない場合は自動で削除します。 ラベル付与後に新しいコミットがpushされた場合、ラベルを自動削除して再チェックを強制する DBREではないユーザー(特定の権限がないユーザー)がラベルを付与した場合、ラベルを自動削除する 分析結果例 以下が実装完了後の分析結果例です。Pull Requestのコメントとして出力されます。 各ストアドの分析結果が羅列された後、最後にマージしても問題ないかの判定が表示されます。 上図は問題なかった時の例です。NGの場合は次のようにCIがエラーになります。 前述のブランチ保護ルールを設定していれば、マージがブロックされます。 まとめと展望 本記事では、ストアドの改修による性能劣化をPull Requestの時点で検知するCIを構築した取り組みを紹介しました。 GitHub ActionsとArgo Workflowsで役割を分担し、S3を介して連携することで、本番DBへ接続せず推定の実行計画を自動取得する構成としています。取得した実行計画はルールベース解析で整理し、その結果と実行計画XMLをLLMレビューが読んで判定する形にしました。 現在はテスト運用の段階であり、マージをブロックする設定はまだ有効化していません。誤検知の傾向を見ながら、ルールベース解析の閾値とLLMレビューのプロンプトの判定条件を調整し、順次適用を進めていく予定です。 一方で、この仕組みでカバーできない観点もあります。取得しているのは推定の実行計画であり、クエリを実際に実行していないため、従来のレビューで確認していた「実際の行数と推定行数の乖離」は判断できません。統計情報の陳腐化に起因するような劣化は、引き続きリリース後のパフォーマンスチェックで捉える必要があります。PR時点のチェックとリリース後の監視は、どちらかで置き換えられるものではなく、互いを補完するものだと捉えています。 今回のターゲットはストアドのみで、アプリケーション側で組み立てるクエリは対象外です。ストアドに絞ったのは、負荷高騰の原因がストアドの改修だったことに加え、定義ファイルから変更内容を特定しやすいためです。ただし、アプリケーションから直接実行されるクエリも、変更差分から抽出して実行計画を取得することは理論上可能です。同じ仕組みを広げていければ、DBへの変更全体がチェックの対象になります。そうした形を目指して、対象範囲を広げていきたいと考えています。 「人が検証の必要性を判断する」運用は、判断が正しくなければ機能しません。判断そのものを仕組みに委ねることで、はじめて再発防止と呼べる状態になると考えています。DBの信頼性を高めるための取り組みを、引き続き進めていきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com


















