COLOPL Tech Blog
コロプラのエンジニアブログです
2026-03-17T11:00:00+09:00
colopl-tech
Hatena::Blog
hatenablog://blog/13574176438049975355
「位置ゲー」の今を知る! "有利なスマホ" があるってご存知でしたか?
hatenablog://entry/17179246901354136740
2026-03-17T11:00:00+09:00
2026-03-17T11:00:02+09:00 こんにちは、エンジニアの 工藤 です。 突然の前置きですが、今回の記事はエンジニアだけじゃなく、位置情報ゲームをプレイする方々にも有用かもしれません!
<p>こんにちは、エンジニアの <a href="https://github.com/zeriyoshi">工藤</a> です。</p>
<p>突然の前置きですが、今回の記事はエンジニアだけじゃなく、位置情報ゲームをプレイする方々にも有用かもしれません!</p>
<p>現在、コロプラでは <a href="https://colopl.co.jp/news/info/2026012702.php">COLOPL Gaming Maps</a> を開発・リリースするなど、「コロニーな生活」に始まる祖業である「位置ゲー」へ注力しています。</p>
<p>※「位置ゲー」は株式会社コロプラの登録商標です</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/colopl-tech/20260309/20260309204833.jpg" alt="Colopl Gaming Maps Banner" width="800" height="391" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>「位置ゲー」を実現するには様々な技術とノウハウが必要です。これは COLOPL Gaming Maps のような地図データ、 POI (Point of Interest, 公園などその場所に関する情報) の運用、タイリングはもちろん、 "現在自分が地球上のどこに居るのか" を特定する、いわゆる "GPS" に関するノウハウもとても重要になってきます。</p>
<h2 id="GPS-ってそもそも何">「GPS」 って、そもそも何?</h2>
<p>皆さんが利用されているスマートフォンには当たり前のように GPS 機能が搭載されていると思います。では、 <code>GPS</code> とは一体何なのでしょうか?</p>
<p><code>GPS</code> は <strong>Global Positioning System</strong> の略称で、 <strong>宇宙上に打ち上げられた人工衛星を用いて、地球上のどの位置に自分が居るのかを知ることができる</strong> 機能です。</p>
<p>厳密には、 GPS はあくまでもアメリカが打ち上げた衛星に対する呼称であり、国際的には <code>GNSS</code> (Global Navigation Satellite System) と呼ばれます。例えばそれぞれ以下のような形で呼称が異なります。</p>
<ul>
<li>アメリカ: <code>GPS</code> (Global Positioning System)</li>
<li>日本: <code>QZSS</code> (Quasi-Zenith Satellite System, みちびき)</li>
<li>中国: <code>BeiDou</code> (北斗)</li>
<li>EU: <code>Galileo</code></li>
<li>ロシア: <code>GLONASS</code></li>
<li>インド: <code>NavIC</code></li>
</ul>
<p>近年のスマートフォンでは GPS 以外にも様々な衛星に対応されるようになってきており、現在では当然のようにほとんどの GNSS に対応しています。 <a href="https://www.apple.com/jp/iphone-17/specs/"><strong>例えば iPhone 17 はこれらすべての衛星に対応</strong></a> しています。</p>
<p>一方で、古いスマートフォンでは対応していない衛星があったりもします。特に日本の QZSS に対応しているかは重要で、 GNSS を利用した位置情報の特定には複数の衛星からの電波を利用すること、 QZSS は基本的に日本をフルカバーするように運用されていることから、 <strong>QZSS 対応 / 非対応のデバイスでは精度がかなり変わってきます。</strong></p>
<h2 id="更なる高精度を達成できるL5バンド">更なる高精度を達成できる「L5バンド」</h2>
<p>「じゃあ、とりあえずすべての GNSS に対応したスマートフォンを使えば位置ゲーは快適に遊べるんだね!」と思うかもしれませんが、 <strong>実はそれだけではないのです!</strong></p>
<p>昨今の GNSS は従来の互換性の高い L1 バンドと呼ばれる周波数以外に、 <strong>L5 バンド</strong> と呼ばれる新しい周波数にも対応してきています。これは L1 バンドよりも広い帯域幅を持ち、状況に応じますが、一般的に <strong>精度が向上します。</strong> 他にも高層ビルが立ち並ぶ都市部において有利であったり、信号電力が高いことと広帯域によるマルチパス耐性の向上の関係で屋内でも L1 に比べて受信できる可能性があるなど、様々なメリットがあります。</p>
<p>iPhone では <a href="https://support.apple.com/ja-jp/111849"><strong>iPhone 14 Pro</strong></a> <strong>より Pro シリーズで対応が開始</strong> されました。 iPhone <a href="https://support.apple.com/ja-jp/111850">14</a>, <a href="https://support.apple.com/ja-jp/111831">15</a>, <a href="https://support.apple.com/ja-jp/121029">16</a> 無印は非対応でしたが、 <strong><a href="https://www.apple.com/jp/iphone-17/specs/">iPhone 17 ではついに無印でも L5 バンドが受信</a>できるようになりました🎉</strong> (残念ながら <a href="https://support.apple.com/ja-jp/122208">16e</a>, <a href="https://www.apple.com/jp/iphone-17e/specs/">17e</a> は非対応)<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup></p>
<p>Android スマートフォンでも、以下のチップセット (SoC) 搭載機種からは L5 バンドの受信に対応しています。 (<strong>機種によっては無効化されている場合があるのでご注意ください!</strong>)</p>
<ul>
<li><a href="https://www.qualcomm.com/content/dam/qcomm-martech/dm-assets/documents/prod_brief_qcom_sd855.pdf">Qualcomm Snapdragon 855</a> 以降</li>
<li><a href="https://www.xda-developers.com/mediatek-dimensity-1000-7nm-soc-integrated-5g/">MediaTek Dimensity 1000</a> 以降</li>
<li><a href="https://www.gsmarena.com/google_pixel_6_pro-10918.php">Google Tensor G1</a> 以降</li>
</ul>
<h2 id="忘れちゃいけない-A-GPS">忘れちゃいけない A-GPS</h2>
<p>実際に電波を受信して測位する GNSS だけに限らず、 <code>A-GPS</code> という機能もあります。これは携帯キャリアの基地局が自身の設置位置を元に、電波を受信しているデバイスに大まかな位置情報を伝える機能です。 <code>Assisted GPS</code> を略して <code>A-GPS</code> と呼ばれます。 GPS, L1 バンドのみかつまだまだ衛星数が少ない時代には、これがとても重要だった時代もありました。現在では衛星からの情報をネットワーク経由で再送信することで初回測位にかかる時間短縮などにも使われています。</p>
<p>この機能には大手キャリアでは軒並み対応していますが、一部の MVNO キャリアなどでは対応していない場合もあります。とはいえ、 L5 バンド対応デバイスであれば、もはや非対応のキャリアであってもそこまで問題に感じることはないかもしれません。 (筆者個人の感想です)</p>
<h2 id="押さえておきたいポイントまとめ">押さえておきたいポイントまとめ</h2>
<p>「位置ゲー」を国内で快適に遊ぶなら、次のポイントを押さえたデバイス選びをしておきましょう!</p>
<ul>
<li>対応する GNSS の種類が多いこと
<ul>
<li>他国の衛星も受信できる状況は多く、全部ちゃんと活用されます</li>
</ul>
</li>
<li>L1 / L5 両バンドに対応していること</li>
<li>(おまけ) キャリアが A-GPS に対応しているか</li>
</ul>
<p>もちろんこれは「位置ゲー」だけにかかわらず、 <strong>GPS (GNSS) を使うアプリケーションすべての快適性に影響します。</strong> 地図アプリも快適になるので、覚えておいて損は無いです!</p>
<p>ちょっとしたことですが、私自身も iPhone 15 Pro に機種変更した時に明らかな改善を感じられました。</p>
<p>これらを踏まえて、皆様も良き位置情報ゲームライフを送ってみてください!毎日がほんの少しだけでももっと楽しくなるかもしれません!</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
コロプラでは、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<div class="footnotes">
<hr/>
<ol>
<li id="fn:1">
高精度 2 周波GPSというのが L1 + L5 を指しています<a href="#fnref:1" rev="footnote">↩</a></li>
</ol>
</div>
colopl-tech
社内AI LT会を開催しました
hatenablog://entry/17179246901360864896
2026-03-13T11:00:00+09:00
2026-03-13T11:00:02+09:00 こんにちは!エンジニアの尾崎です。 先日、社内でAIに特化したLT会「AI LT会」を開催しました。今回はその様子をご紹介します。 開催の背景 コロプラでは以前からエンジニアのLT会を継続的に開催してきました。 最近は社内でのAI活用が急速に広まり、社員のAI活用率は9割以上になっています。コーディングや調査といった業務での活用事例はもちろん、プライベートでのユニークな使い方まで、知見が社内にどんどん蓄積されていきました。こうした活用事例をより多くの人に知ってほしいという想いから、AIに特化したLT会を開催しました。 また、今回のLT会にはエンジニア以外の方も参加されていました。 開催概要 会…
<p>こんにちは!エンジニアの尾崎です。</p>
<p>先日、社内でAIに特化したLT会「AI LT会」を開催しました。今回はその様子をご紹介します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/colopl-tech/20260306/20260306181349.png" alt="AI LT会の会場の様子" width="800" height="280" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="開催の背景">開催の背景</h2>
<p>コロプラでは以前からエンジニアのLT会を継続的に開催してきました。<br/>
最近は社内でのAI活用が急速に広まり、社員のAI活用率は9割以上になっています。コーディングや調査といった業務での活用事例はもちろん、プライベートでのユニークな使い方まで、知見が社内にどんどん蓄積されていきました。こうした活用事例をより多くの人に知ってほしいという想いから、AIに特化したLT会を開催しました。<br/>
また、今回のLT会にはエンジニア以外の方も参加されていました。</p>
<h2 id="開催概要">開催概要</h2>
<p>会場は社内の開放的な会議室を使い、オフライン・オンライン双方から参加できるハイブリッド形式で開催しました。当日はオフラインに約20名、オンラインに約40名が集まり、大盛況の会になりました。<br/>
発表は1人5分で、計7名が登壇しました。</p>
<h2 id="発表内容">発表内容</h2>
<h3 id="OpenClaw-と過ごした27日間">OpenClaw と過ごした27日間</h3>
<p>登壇者 上席執行役員 CIO 菅井さん</p>
<p>AIエージェントプラットフォーム「<a href="https://openclaw.ai/">OpenClaw</a>」を自宅に導入し、27日間使い続けた体験談を語っていただきました。</p>
<p>Discord経由でエージェントを呼び出し、学校からのお知らせメールやプリントのスキャン内容を解析して予定管理・カレンダー登録を自動化したり、自宅のMac miniを操作させたりと、生活に溶け込んだ活用をしたそうです。
一方でGoogleやAnthropicによるエージェント操作への制限という現実的な課題もあり、「使える体制づくり」をしていかなければならないということでした。</p>
<p>OpenClawという最新ツールの活用事例を知ることができて、面白かったです。特に完全なアクセス権(人が操作できることは何でもできること)が便利ではありますが、注意しないといけないポイントであると感じました。</p>
<h3 id="ストック情報から始める-AI-Agent-無双-いろんな情報を食べさせたら全自動で仕様を理解する頼もしい相棒に育ったので俺はのんびり定時で帰ります">ストック情報から始める AI Agent 無双 <small>~いろんな情報を食べさせたら、全自動で仕様を理解する頼もしい相棒に育ったので、俺はのんびり定時で帰ります~<small></small></small></h3>
<p>登壇者 技術基盤本部 バックエンドエンジニア 岡村さん</p>
<p>長い年月運用しているプロジェクトでは仕様を人間が把握することは難しいため、その仕様をAIに理解させるという取り組みの発表でした。<br/>
具体的には、AIエージェントが理解しやすい「構造化されたデータとMCP」を利用して育てたとのことでした。<br/>
それにより、AIによる情報収集の自動化や企画の壁打ちができるなどのメリットがあったそうです。<br/>
間違えた情報が入っていると出力も間違えたり、コードレベルの仕様の把握は難しいなどの現状の課題も語られました。</p>
<p>AIに情報をストックさせることは、プロジェクトの仕様以外にも活用できそうだと思いました。良い活用事例で、勉強になりました。</p>
<h3 id="枯れた技術だからこそC言語LLMによる柔軟かつ安全なライブラリ開発">枯れた技術だからこそ!C言語+LLMによる柔軟かつ安全なライブラリ開発</h3>
<p>登壇者 AIイネーブルメントグループ エンジニア 工藤さん</p>
<p>C言語はほぼすべての環境で動く移植性の高さから、今もなお多くのソフトウェアの根幹を支えているという前置きから始まり、LLMを使ったC言語開発のコツを語っていただきました。<br/>
前提として、C言語は「必要最小限のハードウェア抽象化」しか行わず、例外機能やGCもないため細心の注意が必要ですが、長い歴史があるためエージェントはC言語の知識が豊富であり、エージェント以外にも各種検査ツール (Valgrind, Sanitizer 等) が充実しており、それらを活用することで一定の安全性は担保できるとおっしゃっていました。<br/>
また、アプリケーション開発というとアプリケーション自体に機能を実装することを考えがちですが、「移植性の高いライブラリ」を作成し、それを利用する形でアプリケーションを実装することで保守性と汎用性が向上するとのことでした。<br/>
具体的な開発方法として、AGENTS.mdにC99準拠やCMake、テストフレームワークの利用を指示し、最終的には、吐き出されたコードを読み直し、綺麗に整えることを行ったそうです。</p>
<p>C言語について熱く語っていただきました。具体的な話がたくさん盛り込まれており、C言語と周辺知識についての理解が深まりました。</p>
<p>また、将来の展望として、 Better C として <a href="https://ziglang.org/">Zig</a> に期待していることなども説明していました。</p>
<h3 id="Deep-ResearchのAgentえーじゃん">Deep ResearchのAgentえーじゃん!</h3>
<p>登壇者 経営企画本部 エンジニア 石塚さん</p>
<p>Googleが提供する<a href="https://ai.google.dev/gemini-api/docs/deep-research">Deep Research API</a>を使った発表です。
<code>deep-research-pro-preview</code> エージェントをバックグラウンドで非同期実行するAPIの組み込み方を中心に、Deep Researchをエージェントとして動かせることを実際のコードを交えて紹介していただきました。</p>
<p>ブラウザ版では、要約などをしてしまいかえってノイズになることがあり、それをInteraction APIを使うことで構造化データで出力し、次のステップで扱いやすくすることが語られました。<br/>
また、API使用時のコストとコスパについても語っていただき、数時間の業務が数百円で自動化できるのであれば安いとおっしゃっており、確かに安いと思いました。</p>
<p>あまり、Deep Researchは使うことがなかったので知らないことを色々知ることができました。また、今の時代はAIが活用しやすいデータを扱うことが重要なのだなと再認識させられました。</p>
<h3 id="AI翻訳のお話">AI翻訳のお話</h3>
<p>登壇者 技術基盤本部 バックエンドエンジニア 尾崎</p>
<p>私の発表になります。AIを活用した翻訳について話し、AI翻訳のワークフローや仕組みを中心に語りました。<br/>
翻訳時には、翻訳するテキストと類似しているテキストをハイブリッド検索で取得して一緒に渡したり、翻訳後に、別のAI(評価用AI)を用いて品質チェックを行うプロセスなどを紹介しました。
実際に運用してみて、翻訳速度の向上といった大きなメリットを感じた一方で、ネイティブスピーカーが読むと細かな違和感があるなど、品質面での課題も見えてきました。</p>
<p>現状ではまだ改善の余地はありますが、AIのさらなる進化によって、翻訳精度は今後ますます高まっていくと思っています。</p>
<h3 id="howto-rulesync">howto-rulesync</h3>
<p>登壇者 テクノロジー推進本部 クライアントエンジニア 山本さん</p>
<p>CursorやClaude Codeなど複数のAIツールで共通して使いたいルールファイルを一元管理・同期するツール「<a href="https://rulesync.dyoshikawa.com/">rulesync</a>」の使い方を紹介した発表です。AIツールが増えるにつれてルールの管理が煩雑になりがちな課題に対して、rulesyncを使うことでどうシンプルに解決できるかを丁寧に説明していただきました。</p>
<p>この発表の後に自分も使ってみたのですが、便利でした。社内でも活用が進んでいるところを見たので、このようなLT会で情報が広がっていくのは嬉しいなと思いました。</p>
<h3 id="DeepWiki-Deep-Dive">DeepWiki Deep Dive</h3>
<p>登壇者 AIイネーブルメントグループ エンジニア 山田さん</p>
<p>GitHubリポジトリを「何でも質問できるWiki」にするサービス「<a href="https://deepwiki.com/">DeepWiki</a>」を深掘りした発表です。</p>
<p>使い方はシンプルで、URLの <code>github</code> を <code>deepwiki</code> に変えるだけでリポジトリから自動生成されドキュメントを確認でき、コードに関する質問もできます。社内リポジトリやドキュメントが少ないマイナーライブラリの把握に便利で、さらにDeepWiki MCPを使えばClaude Codeから直接リポジトリに質問できることも紹介されました。<br/>
また、質問時に3つのモードがあり、その中の一つのCodemap機能では、コード構造について詳しい説明をしてくれるため、かなり嬉しい機能だと思いました。</p>
<p>「Wikiを眺めるよりも、質問できることがDeepWikiの本質的な価値」という言葉が印象的でした。確かに質問できるのはとても便利だと思います。</p>
<h2 id="終わりに">終わりに</h2>
<p>業務の活用事例からプライベートでのユニークな活用事例まで、バラエティ豊かな発表が揃い、とても充実した会になりました。参加者のアンケートでも好意的な声が多く、知見を共有するという目的を十分に達成できたと感じています。</p>
<p>コロプラでは今後も社内LT会や勉強会を定期的に開催していきます。次回の記事もぜひお楽しみにしてください!</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
コロプラでは、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
colopl-tech
【2026年初登壇!】AI Security Conferenceに登壇させていただきました
hatenablog://entry/17179246901355149083
2026-02-27T11:00:00+09:00
2026-03-10T14:24:12+09:00 こんにちは。コロプラでエンジニアの中途採用と技術広報を担当しているA.T.です。 2026年1月27日に開催された「AI Security Conference」に、CIOの菅井が「クリエイターの「魂」を守る、AI時代の組織セキュリティ」というタイトルで登壇させていただきました。記念すべき今年初のカンファレンス登壇でしたが、セッション会場はほぼ満席となり、非常に幸先の良いスタートを切ることができました! AI Security Conferenceについて 本イベントはファインディ株式会社さんが主催する、AIの急速な発展とそれにより増大するリスクへの対応をテーマにしたカンファレンスです。現在、…
<p><span style="font-weight: 400;">こんにちは。コロプラでエンジニアの中途採用と技術広報を担当しているA.T.です。</span></p>
<p><span style="font-weight: 400;"> </span></p>
<p><span style="font-weight: 400;">2026年1月27日に開催された「</span><a href="https://ai-security-con.findy-tools.io/2026#keynote"><span style="font-weight: 400;">AI Security Conference</span></a><span style="font-weight: 400;">」に、CIOの菅井が「クリエイターの「魂」を守る、AI時代の組織セキュリティ」というタイトルで登壇させていただき</span><span style="font-weight: 400;">ました。記念すべき今年初のカンファレンス登壇でしたが、セッション会場はほぼ満席となり、非常に幸先の良いスタートを切ることができました!</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/r-ozaki/20260216/20260216164251.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h3 id="AI-Security-Conferenceについて"><span style="font-weight: 400;">AI Security Conferenceについて</span></h3>
<p><span style="font-weight: 400;">本イベントはファインディ株式会社さんが主催する、AIの急速な発展とそれにより増大するリスクへの対応をテーマにしたカンファレンスです。現在、「AIの加速」と「リスクの拡大」が同時に進む転換点にあります。 本カンファレンスでは、モデルの安全性・透明性の確保といった新たな課題に対し、AIを安全に運用するための枠組みづくりからAIを用いた次世代のセキュリティ高度化まで国内外の最前線の知見が共有されました。</span></p>
<h3 id="発表内容について"><span style="font-weight: 400;">発表内容について</span></h3>
<p><span style="font-weight: 400;">コロプラは生成AIの全社活用を段階的に推進し、現在では社員の約9割まで浸透しています。本セッションでは、コロプラがどのようにして生成AIを全社に浸透させ、プロダクトにまで適用させたのか、運用で止めず定着させる “浸透ステップ” について発表しました。</span></p>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">意思決定の背景</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">ガイドライン後セキュリティ連携</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">教育とゲーム設計による「使う必然性」の作り込み</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">工数短縮の実績</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">2025年5月にリリースした『</span><a href="https://colopl.co.jp/news/pressrelease/2025050701.php"><span style="font-weight: 400;">神魔狩りのツクヨミ</span></a><span style="font-weight: 400;">』への適用事例</span></li>
</ul>
<p> </p>
<p><span style="font-weight: 400;">「AIを安全に使うための枠組み(セキュリティ)があるからこそ、クリエイターが迷わずアクセルを踏める。そんな『攻めと守り』の理想的な関係性についてお話しさせていただきました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/r-ozaki/20260216/20260216164102.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/r-ozaki/20260216/20260216164237.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h3 id="Ask-the-Speakerの様子"><span style="font-weight: 400;">Ask the Speakerの様子</span></h3>
<p><span style="font-weight: 400;">セッション終了後に設けられた「Ask the Speaker」では、登壇者に質問や意見交換をすることができます。ありがたいことに開始直後から多くの方に来ていただき、制限時間いっぱいまでたくさんのご質問をいただくことができました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/r-ozaki/20260216/20260216164139.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/r-ozaki/20260216/20260216164151.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h3 id="おわりに"><span style="font-weight: 400;">おわりに</span></h3>
<p><span style="font-weight: 400;">会場やブースの盛り上がりを目の当たりにし、AI活用とセキュリティに対する関心の高さを改めて肌で感じることができました。今回の登壇を通じて、少しでも業界全体のAI活用を加速させる一助となれば幸いです。</span></p>
<p> </p>
<p><span style="font-weight: 400;">最後になりますが、運営スタッフの皆様、そして会場に足を運んで下さった皆様、本当にありがとうございました!</span></p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>コロプラでは、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br /><a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
r-ozaki
【資料公開】アーキテクチャカンファレンス2025に登壇・出展させていただきました
hatenablog://entry/17179246901353077859
2026-02-20T11:00:00+09:00
2026-03-10T14:25:52+09:00 こんにちは。コロプラでエンジニアの中途採用と技術広報を担当しているA.T.です。 2025年11月20〜21日に開催された「アーキテクチャカンファレンス2025」に、登壇&Goldスポンサーとして協賛させていただきました。今回はサーバーエンジニアの岡村とKevinが「Cloud Runでコロプラが挑む生成AI×ゲーム『神魔狩りのツクヨミ』の裏側」というタイトルで発表しました。 アーキテクチャカンファレンス2025 について 本イベントはファインディ株式会社さんが主催する、アーキテクチャの構想・判断・構築に焦点を当てたカンファレンスです。2025年は2日間に渡る開催となっており、昨年以上の盛り上…
<p>こんにちは。コロプラでエンジニアの中途採用と技術広報を担当しているA.T.です。 2025年11月20〜21日に開催された「<a href="https://architecture-con.findy-tools.io/2025">アーキテクチャカンファレンス2025</a>」に、登壇&Goldスポンサーとして協賛させていただきました。今回はサーバーエンジニアの岡村とKevinが「Cloud Runでコロプラが挑む生成AI×ゲーム『神魔狩りのツクヨミ』の裏側」というタイトルで発表しました。<br/>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/r-ozaki/20260213/20260213150840.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/r-ozaki/20260213/20260213150907.jpg" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<h3 id="アーキテクチャカンファレンス2025-について">アーキテクチャカンファレンス2025 について</h3>
<p>本イベントはファインディ株式会社さんが主催する、アーキテクチャの構想・判断・構築に焦点を当てたカンファレンスです。2025年は2日間に渡る開催となっており、昨年以上の盛り上がりを感じました!</p>
<p>2年連続の参加となり、2024年の参加レポートと前回の登壇資料は<a href="https://blog.colopl.dev/entry/2024/11/29/111000">こちら</a>で公開しています。<br/>
ぜひ、本記事と合わせてご確認いただけると嬉しいです!</p>
<h3 id="発表内容について">発表内容について</h3>
<p>2025年5月にリリースした『<a href="https://colopl.co.jp/news/pressrelease/2025050701.php">神魔狩りのツクヨミ</a>(生成AI が動的に生み出すカードで戦うモバイル/PC同時展開タイトル)』における、Cloud Run 構成および生成AIがどのようにサービス内で使われているか話させていただきました。
生成AI連携サービスに踏み出す際のボトルネックやコスト管理のヒントを『神魔狩りのツクヨミ』の事例もあわせて紹介しています。</p>
<p>資料の冒頭ではゲームバックエンドの仕組みや特徴、過去約10年分のアーキテクチャの変遷も解説しています。ゲームの裏側はどうなっているの...?ゲーム業界じゃないけどわかるの...?という方も多いと思いますが、非ゲーム業界の方もイメージしやすい内容になっているのでぜひ資料をチェックしていただけると嬉しいです!
<iframe id="talk_frame_1502775" class="speakerdeck-iframe" src="https://speakerdeck.com/player/6e9379f9ffb640349d4563bdf3582853" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> </p>
<h3 id="おわりに">おわりに</h3>
<p>改めてにはなりますが、講演を聴いてくださった皆さん、関係各社の皆さん本当にありがとうございました!</p>
<p>コロプラでは今後も積極的に各種イベントに参加していきたいと考えており、更に積極的なコミュニティへの参加・貢献できるよう尽力してまいります。また、情報発信も力を入れていくので応援よろしくお願いします!</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
コロプラでは、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
r-ozaki
PHPStanのカスタムルールを使って、Laravelアップデート時のシリアライズ事故を防ぐ仕組みを作ってみた
hatenablog://entry/17179246901349319194
2026-02-16T11:00:00+09:00
2026-03-03T17:46:18+09:00 こんにちは。バックエンドエンジニアの薮 (@tyabu12) です。 Laravel 11 のセキュリティEOLが3月に迫ってきましたね。 今回は Laravel 12 への更新時に遭遇したシリアライズの問題と、シリアライズ事故を事前検知する仕組みをご紹介できればと思います。 依存パッケージ更新に伴うシリアライズの問題は、コードレビューやデバッグでの発見が困難で未然に検知するのが難しいです。 本記事では、こうしたシリアライズ事故を未然に防ぐために、PHPStan のカスタムルールを活用する方法を紹介します。 2026/03/03追記・修正 Xにて、にゃんだーすわん (@tadsan) さんから…
<p>こんにちは。バックエンドエンジニアの薮 (<a href="https://x.com/tyabu12">@tyabu12</a>) です。</p>
<p>Laravel 11 の<a href="https://laravel.com/docs/11.x/releases">セキュリティEOL</a>が3月に迫ってきましたね。</p>
<p>今回は Laravel 12 への更新時に遭遇したシリアライズの問題と、シリアライズ事故を事前検知する仕組みをご紹介できればと思います。</p>
<p>依存パッケージ更新に伴うシリアライズの問題は、コードレビューやデバッグでの発見が困難で未然に検知するのが難しいです。</p>
<p>本記事では、こうしたシリアライズ事故を未然に防ぐために、PHPStan のカスタムルールを活用する方法を紹介します。</p>
<h3 id="20260303追記修正">2026/03/03追記・修正</h3>
<p>Xにて、<a href="https://x.com/tadsan">にゃんだーすわん (@tadsan)</a> さんから <a href="https://x.com/tadsan/status/2023252591367639293">PHPStan の型判定に <code>instanceof</code> を使うべきではないというご指摘</a>をいただきました。
(※本記事でのご紹介については、ご本人より掲載のご了承をいただいております)</p>
<p><code>instanceof</code> のチェックでは型を正しく判定できるとは限らないそうで<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>、
<a href="https://x.com/tadsan/status/2025518198561599793"><code>instanceof</code> を使わずヘルパーメソッドを使う記法</a>
を教えていただき、本記事も合わせて <code>isAllowedType()</code> の処理を修正いたしました。</p>
<p>ご指摘ありがとうございました!</p>
<h2 id="背景Laravel-11-から-12-への更新作業でシリアライズのエラーに遭遇">背景:Laravel 11 から 12 への更新作業でシリアライズのエラーに遭遇</h2>
<p>私たちのプロジェクトでは、Laravel 11 から 12 へのアップデート作業中に、
開発環境でシリアライズ (<code>serialize()</code> / <code>unserialize()</code>) を使用している箇所で予期しないエラーが発生することを発見しました。</p>
<p>Model に新規で追加されたプロパティが動的プロパティとして解釈され、Laravel 11 で <code>serialize()</code> した Model を <code>unserialize()</code> すると、以下のような <code>ErrorException</code> が発生したのです。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synIdentifier">ErrorException</span>: Creation of dynamic property App\Models\HogeModel::$previous is deprecated
<span class="synIdentifier">ErrorException</span>: Creation of dynamic property App\Models\HogeModel::$relationAutoloadCallback is deprecated
<span class="synIdentifier">ErrorException</span>: Creation of dynamic property App\Models\HogeModel::$relationAutoloadContext is deprecated
</pre>
<p>シリアライズというと馴染みがない方が多いかもしれません。Laravel で内部的にシリアライズが使われている代表的な処理のひとつに Job があります。</p>
<p>Job を dispatch で送信して Queue に保存される際の処理を見てみましょう。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// このジョブは、デフォルト接続のデフォルトキューに送信される</span>
ProcessPodcast<span class="synStatement">::</span>dispatch<span class="synSpecial">()</span>;
</pre>
<p>dispatch された Job は <code>Queue::createPayload()</code> で string に変換されます。
<code>createPayload()</code> の変換処理を追っていくと Job がオブジェクトの場合は <code>Queue::createObjectPayload()</code> で <code>serialize()</code> されることが分かります。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">namespace</span> Illuminate\Queue;
<span class="synType">abstract</span> <span class="synType">class</span> Queue
<span class="synSpecial">{</span>
<span class="synComment">// ...</span>
<span class="synComment">/**</span>
<span class="synComment"> * Create a payload for an object-based queue handler.</span>
<span class="synComment"> *</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">object $job</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">string $queue</span>
<span class="synComment"> * </span><span class="synPreProc">@return </span><span class="synComment">array</span>
<span class="synComment"> */</span>
<span class="synType">protected</span> <span class="synPreProc">function</span> createObjectPayload<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span>, <span class="synStatement">$</span><span class="synIdentifier">queue</span><span class="synSpecial">)</span>
<span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">payload</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>withCreatePayloadHooks<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">queue</span>, <span class="synSpecial">[</span>
<span class="synConstant">'uuid'</span> <span class="synStatement">=></span> <span class="synSpecial">(</span><span class="synType">string</span><span class="synSpecial">)</span> Str<span class="synStatement">::</span>uuid<span class="synSpecial">()</span>,
<span class="synConstant">'displayName'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>getDisplayName<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span>,
<span class="synConstant">'job'</span> <span class="synStatement">=></span> <span class="synConstant">'Illuminate\Queue\CallQueuedHandler@call'</span>,
<span class="synConstant">'maxTries'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>getJobTries<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span>,
<span class="synConstant">'maxExceptions'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synType">-></span>maxExceptions <span class="synStatement">??</span> <span class="synType">null</span>,
<span class="synConstant">'failOnTimeout'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synType">-></span>failOnTimeout <span class="synStatement">??</span> <span class="synConstant">false</span>,
<span class="synConstant">'backoff'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>getJobBackoff<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span>,
<span class="synConstant">'timeout'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synType">-></span>timeout <span class="synStatement">??</span> <span class="synType">null</span>,
<span class="synConstant">'retryUntil'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>getJobExpiration<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span>,
<span class="synConstant">'data'</span> <span class="synStatement">=></span> <span class="synSpecial">[</span>
<span class="synConstant">'commandName'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">job</span>,
<span class="synConstant">'command'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">job</span>,
<span class="synSpecial">]</span>,
<span class="synConstant">'createdAt'</span> <span class="synStatement">=></span> Carbon<span class="synStatement">::</span>now<span class="synSpecial">()</span><span class="synType">-></span>getTimestamp<span class="synSpecial">()</span>,
<span class="synSpecial">])</span>;
<span class="synStatement">try</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">command</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>jobShouldBeEncrypted<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span> <span class="synStatement">&&</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>container<span class="synType">-></span>bound<span class="synSpecial">(</span>Encrypter<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>
<span class="synStatement">?</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>container<span class="synSpecial">[</span>Encrypter<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">]</span><span class="synType">-></span>encrypt<span class="synSpecial">(</span><span class="synIdentifier">serialize</span><span class="synSpecial">(</span><span class="synPreProc">clone</span> <span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">))</span>
<span class="synStatement">:</span> <span class="synIdentifier">serialize</span><span class="synSpecial">(</span><span class="synPreProc">clone</span> <span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span> <span class="synStatement">catch</span> <span class="synSpecial">(</span><span class="synConstant">Throwable</span> <span class="synStatement">$</span><span class="synIdentifier">e</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">throw</span> <span class="synPreProc">new</span> <span class="synIdentifier">RuntimeException</span><span class="synSpecial">(</span>
<span class="synIdentifier">sprintf</span><span class="synSpecial">(</span><span class="synConstant">'Failed to serialize job of type [%s]: %s'</span>, <span class="synIdentifier">get_class</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span>, <span class="synStatement">$</span><span class="synIdentifier">e</span><span class="synType">-></span>getMessage<span class="synSpecial">())</span>,
<span class="synConstant">0</span>,
<span class="synStatement">$</span><span class="synIdentifier">e</span>
<span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synIdentifier">array_merge</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">payload</span>, <span class="synSpecial">[</span>
<span class="synConstant">'data'</span> <span class="synStatement">=></span> <span class="synIdentifier">array_merge</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">payload</span><span class="synSpecial">[</span><span class="synConstant">'data'</span><span class="synSpecial">]</span>, <span class="synSpecial">[</span>
<span class="synConstant">'commandName'</span> <span class="synStatement">=></span> <span class="synIdentifier">get_class</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">job</span><span class="synSpecial">)</span>,
<span class="synConstant">'command'</span> <span class="synStatement">=></span> <span class="synStatement">$</span><span class="synIdentifier">command</span>,
<span class="synSpecial">])</span>,
<span class="synSpecial">])</span>;
<span class="synSpecial">}</span>
</pre>
<p>今度は Job 実行時の処理を見ていきましょう。</p>
<p><code>createObjectPayload()</code> で <code>'job'</code> に指定している<code>Illuminate\Queue\CallQueuedHandler@call</code> を見てみます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">namespace</span> Illuminate\Queue;
<span class="synType">class</span> CallQueuedHandler
<span class="synSpecial">{</span>
<span class="synComment">// ...</span>
<span class="synComment">/**</span>
<span class="synComment"> * Get the command from the given payload.</span>
<span class="synComment"> *</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">array $data</span>
<span class="synComment"> * </span><span class="synPreProc">@return </span><span class="synComment">mixed</span>
<span class="synComment"> *</span>
<span class="synComment"> * </span><span class="synPreProc">@throws </span><span class="synComment">\RuntimeException</span>
<span class="synComment"> */</span>
<span class="synType">protected</span> <span class="synPreProc">function</span> getCommand<span class="synSpecial">(</span><span class="synType">array</span> <span class="synStatement">$</span><span class="synIdentifier">data</span><span class="synSpecial">)</span>
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">str_starts_with</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">data</span><span class="synSpecial">[</span><span class="synConstant">'command'</span><span class="synSpecial">]</span>, <span class="synConstant">'O:'</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">unserialize</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">data</span><span class="synSpecial">[</span><span class="synConstant">'command'</span><span class="synSpecial">])</span>;
<span class="synSpecial">}</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>container<span class="synType">-></span>bound<span class="synSpecial">(</span>Encrypter<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">unserialize</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>container<span class="synSpecial">[</span>Encrypter<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">]</span><span class="synType">-></span>decrypt<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">data</span><span class="synSpecial">[</span><span class="synConstant">'command'</span><span class="synSpecial">]))</span>;
<span class="synSpecial">}</span>
<span class="synStatement">throw</span> <span class="synPreProc">new</span> <span class="synIdentifier">RuntimeException</span><span class="synSpecial">(</span><span class="synConstant">'Unable to extract job payload.'</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p><code>unserialize()</code> によりシリアライズを解除してオブジェクトを復元していることが分かります。
特に意識していないだけで、自然とシリアライズ処理を使っていることが分かりましたね!</p>
<p>例えば Laravel 更新対応をローリングアップデートでデプロイする際、新旧の Laravel アプリケーションでシリアライズ済みのデータを <code>unserialize()</code> でシリアライズ解除する可能性が出てきます。</p>
<p>もしシリアライズ解除ができない場合は <code>ErrorException</code> を吐いてしまい、ジョブの実行が止まってしまいます<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup>。</p>
<p>ローリングアップデートで曲者なのが、更新後の Laravel が更新前の Job をシリアライズ解除することもあれば、更新前の Laravel が更新後の Job をシリアライズ解除する可能性もあります。</p>
<pre class="code mermaid" data-lang="mermaid" data-unlink>sequenceDiagram
box ローリングアップデート中の問題(逆もあり)
participant L12 as Laravel 12
participant Q as Queue
participant L11 as Laravel 11
end
L12->>Q: serialize()
Q->>L11: Job取得
L11->>L11: unserialize()
Note over L11: ErrorException<br/>発生の可能性</pre>
<p>この現象はデプロイ前後という限られたタイミングでのみ発生し、コードレビューやQAでも検知は困難です。</p>
<p>そこで、この問題を PHPStan のカスタムルールを使って静的解析をすることで、事前に自動検知する仕組みを導入してみました。</p>
<h2 id="PHPStan-のカスタムルールによる検出">PHPStan のカスタムルールによる検出</h2>
<p>PHPStan では通常の静的解析に加えて独自でカスタムルールを定義できます。</p>
<p>カスタムルールの詳しい作成方法については、下記の記事をご参照ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2026%2F02%2F12%2F113000" title="PHPStanのカスタムルールの作り方 - ASTを理解してプロジェクト固有の検査を実装する - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2026/02/12/113000">blog.colopl.dev</a></cite></p>
<p>今回実装するカスタムルールは、Job クラス (<code>ShouldQueue</code> を <code>implements</code> したクラス) のプロパティの型が、以下のいずれかの条件を満たすかチェックします。</p>
<ul>
<li>null</li>
<li>スカラー型 (bool, int, float, string)</li>
<li>array (array のネスト含む)</li>
<li>enum</li>
<li>(<code>SerializesModels</code> trait を使用している場合は) Model</li>
</ul>
<p>これにより、Closure や CarbonInterface などをはじめとするオブジェクトなど、シリアライズ時に危険性がある型使った場合に、静的解析で機械的に検知できます。</p>
<h3 id="実装のポイント">実装のポイント</h3>
<p>カスタムルールの実装にあたっていくつかポイントがあったため、そちらをご紹介します。</p>
<p>先に <a href="#%E6%9C%80%E7%B5%82%E7%9A%84%E3%81%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89">最終的なソースコードを見たい場合はこちら</a> 。</p>
<h4 id="1-InClassNode-の活用">1. <code>InClassNode</code> の活用</h4>
<p>PHPではクラスのプロパティの定義方法は2種類あります。
通常のプロパティ宣言と、コンストラクタでのプロパティ昇格です。
それぞれAST上では異なるノードとして表現されます。</p>
<table>
<thead>
<tr>
<th style="text-align:left;"></th>
<th style="text-align:left;">ノード</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;">通常のプロパティ</td>
<td style="text-align:left;"><code>Node\PropertyItem</code></td>
</tr>
<tr>
<td style="text-align:left;">プロパティ昇格</td>
<td style="text-align:left;"><code>Node\Param</code> (<code>isPromoted() = true</code>)</td>
</tr>
</tbody>
</table>
<p>異なるノードそれぞれに対するカスタムルールを書くこともできます。
ただ、今回作成するカスタムルールではプロパティの型さえ分かれば良く、どちらの方法で定義されているかを区別する必要はありません。</p>
<p>こういう時は <code>InClassNode</code> と <code>ClassReflection</code> を組み合わせることで、ソースコード上の記述位置を問わず、クラスが持つプロパティを一貫してチェックするルールを記述できます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">class</span> JobOnlyPrimitiveOrQueueablePropertiesRule <span class="synType">implements</span> Rule
<span class="synSpecial">{</span>
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@inheritDoc</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> InClassNode<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@inheritDoc</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synComment">// 対象のクラスのリフレクションが取得できる!</span>
<span class="synComment">// getNodeType() が Stmt\Class_ だとまだクラスの解析が終わっていないので、ここで $scope->getClassReflection() は取得できずにnullになってしまう</span>
<span class="synStatement">$</span><span class="synIdentifier">classReflection</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getClassReflection<span class="synSpecial">()</span>;
<span class="synComment">// ShouldQueue インターフェースを実装しているかチェック</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">classReflection</span> <span class="synStatement">===</span> <span class="synType">null</span> <span class="synStatement">||</span> <span class="synStatement">!$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>implementsInterface<span class="synSpecial">(</span>ShouldQueue<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 通常のプロパティも、コンストラクタのプロパティ昇格も一括で取得できる</span>
<span class="synStatement">$</span><span class="synIdentifier">properties</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>getNativeReflection<span class="synSpecial">()</span><span class="synType">-></span>getProperties<span class="synSpecial">()</span>;
<span class="synComment">// </span><span class="synTodo">TODO</span><span class="synComment">: $properties のチェック処理</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<h4 id="2-再帰的な型判定">2. 再帰的な型判定</h4>
<p>プロパティの型は、配列のネストや Union 型 (例: <code>int|Carbon</code>) が含まれる場合があります。そのため、以下のように再帰的にチェックします。</p>
<p><a href="#20260303%E8%BF%BD%E8%A8%98%E4%BF%AE%E6%AD%A3">※2026/03/03 ご指摘をいただいたので追記・修正しました</a></p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synPreProc">function</span> isAllowedType<span class="synSpecial">(</span>Type <span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">bool</span>
<span class="synSpecial">{</span>
<span class="synComment">// 1. まず Null を型から取り除く(Nullable をシンプルに扱うため)</span>
<span class="synStatement">$</span><span class="synIdentifier">type</span> <span class="synStatement">=</span> TypeCombinator<span class="synStatement">::</span>removeNull<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">)</span>;
<span class="synComment">// 元が null のみだった場合は NeverType(BottomType) になるので許可</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span> <span class="synStatement">instanceof</span> NeverType<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 2. 配列(またはそのUnion)の判定</span>
<span class="synComment">// removeNull したおかげで、`array|null` も安全にここでキャッチできます</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isArray<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>isAllowedType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>getIterableValueType<span class="synSpecial">())</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 3. スカラー、またはEnumの判定</span>
<span class="synComment">// `int|string` のような同種のUnionであっても、yes() が true になります</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isScalar<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">()</span> <span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isEnum<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 4. 異種の複合型(例: `int|array` や `string|QueueableEntity`)のフォールバック処理</span>
<span class="synComment">// instanceof UnionType を避け、PHPStan公式の安全な分解ユーティリティを使います</span>
<span class="synStatement">$</span><span class="synIdentifier">types</span> <span class="synStatement">=</span> TypeUtils<span class="synStatement">::</span>flattenTypes<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">)</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">count</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">types</span><span class="synSpecial">)</span> <span class="synStatement">></span> <span class="synConstant">1</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">foreach</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">types</span> <span class="synStatement">as</span> <span class="synStatement">$</span><span class="synIdentifier">innerType</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">!$</span><span class="synIdentifier">this</span><span class="synType">-></span>isAllowedType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">innerType</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">false</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synConstant">false</span>;
<span class="synSpecial">}</span>
</pre>
<p>再帰処理により、<code>array<int, string></code> や <code>array<Model></code> のようなネストされた型も正しく検証できます。</p>
<h4 id="3-SerializesModels-trait-の判定">3. <code>SerializesModels</code> trait の判定</h4>
<p>Laravel の <code>SerializesModels</code> trait を使用している場合は、Eloquent Model のシリアライズが安全に行われます。</p>
<p><code>SerializesModels</code> trait は、安全に Model クラスをシリアライズする手法です。
モデルの全プロパティをシリアライズするのではなく、モデルのクラス名やIDなどの識別情報のみをシリアライズし、デシリアライズ時はDBから再度モデルを取得します。
これによりシリアライズに起因する問題や、Job のキューイング中に Model が変更されて古い状態のまま処理されてしまうリスクを軽減できます。ただし、キュー投入から実行までの間に Model 自体が削除されたり、前提としている状態から大きく変化した場合の扱いについては、引き続き注意が必要です。</p>
<p><code>SerializesModels</code> trait を使用しているかは <code>$classReflection</code> から容易に判定できます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">classReflection</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getClassReflection<span class="synSpecial">()</span>;
<span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>hasTraitUse<span class="synSpecial">(</span>SerializesModels<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>;
</pre>
<h4 id="4-外部パッケージの除外">4. 外部パッケージの除外</h4>
<p><code>Illuminate\</code> の以下のプロパティは静的解析の対象から除外します。</p>
<p>これによって <code>Illuminate\Queue\InteractsWithQueue</code> や <code>Illuminate\Bus\Queueable</code> などの trait によって追加されるプロパティは検証対象から除外します<sup id="fnref:3"><a href="#fn:3" rel="footnote">3</a></sup>。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">declaringClassName</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getDeclaringClass<span class="synSpecial">()</span><span class="synType">-></span>getName<span class="synSpecial">()</span>;
<span class="synComment">// 依存パッケージ側はチェックしない</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">str_starts_with</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">declaringClassName</span>, <span class="synConstant">'Illuminate</span><span class="synSpecial">\\</span><span class="synConstant">'</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">continue</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="最終的なソースコード">最終的なソースコード</h3>
<p>以上を踏まえて、最終的なソースコードはこのような形になります。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synType">namespace</span> CustomPHPStan\Rules\Jobs;
<span class="synPreProc">use</span> Illuminate\Contracts\Queue\QueueableCollection;
<span class="synPreProc">use</span> Illuminate\Contracts\Queue\QueueableEntity;
<span class="synPreProc">use</span> Illuminate\Contracts\Queue\ShouldQueue;
<span class="synPreProc">use</span> Illuminate\Queue\SerializesModels;
<span class="synPreProc">use</span> PhpParser\Node;
<span class="synPreProc">use</span> PHPStan\Analyser\Scope;
<span class="synPreProc">use</span> PHPStan\Node\InClassNode;
<span class="synPreProc">use</span> PHPStan\Rules\Rule;
<span class="synPreProc">use</span> PHPStan\Rules\RuleErrorBuilder;
<span class="synPreProc">use</span> PHPStan\Type\ObjectType;
<span class="synPreProc">use</span> PHPStan\Type\Type;
<span class="synPreProc">use</span> PHPStan\Type\UnionType;
<span class="synPreProc">use</span> PHPStan\Type\VerbosityLevel;
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@implements </span><span class="synComment">Rule<InClassNode></span>
<span class="synComment"> */</span>
<span class="synType">class</span> JobOnlyPrimitiveOrQueueablePropertiesRule <span class="synType">implements</span> Rule
<span class="synSpecial">{</span>
<span class="synComment">/** </span><span class="synPreProc">@var </span><span class="synComment">list<ObjectType>|null */</span>
<span class="synType">private</span> <span class="synStatement">?</span><span class="synType">array</span> <span class="synStatement">$</span><span class="synIdentifier">queueableObjectTypes</span> <span class="synStatement">=</span> <span class="synType">null</span>;
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@inheritDoc</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synComment">// Stmt\Class_ だとまだクラスの解析が終わっていないので、$scope->getClassReflection() が取得できずにnullになってしまう</span>
<span class="synStatement">return</span> InClassNode<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@inheritDoc</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">classReflection</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getClassReflection<span class="synSpecial">()</span>;
<span class="synComment">// ShouldQueue インターフェースを実装しているかチェック</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">classReflection</span> <span class="synStatement">===</span> <span class="synType">null</span> <span class="synStatement">||</span> <span class="synStatement">!$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>implementsInterface<span class="synSpecial">(</span>ShouldQueue<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>hasTraitUse<span class="synSpecial">(</span>SerializesModels<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>;
<span class="synStatement">$</span><span class="synIdentifier">errors</span> <span class="synStatement">=</span> <span class="synSpecial">[]</span>;
<span class="synStatement">$</span><span class="synIdentifier">properties</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>getNativeReflection<span class="synSpecial">()</span><span class="synType">-></span>getProperties<span class="synSpecial">()</span>;
<span class="synStatement">foreach</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">properties</span> <span class="synStatement">as</span> <span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synComment">// staticプロパティはシリアライズ対象外なのでスキップ</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>isStatic<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">continue</span>;
<span class="synSpecial">}</span>
<span class="synStatement">$</span><span class="synIdentifier">declaringClassName</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getDeclaringClass<span class="synSpecial">()</span><span class="synType">-></span>getName<span class="synSpecial">()</span>;
<span class="synComment">// 依存パッケージ側はチェックしない</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">str_starts_with</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">declaringClassName</span>, <span class="synConstant">'Illuminate</span><span class="synSpecial">\\</span><span class="synConstant">'</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">continue</span>;
<span class="synSpecial">}</span>
<span class="synStatement">$</span><span class="synIdentifier">type</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getType<span class="synSpecial">()</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span> <span class="synStatement">===</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">errors</span><span class="synSpecial">[]</span> <span class="synStatement">=</span> RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span>
<span class="synConstant">"Property </span><span class="synSpecial">\${</span><span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getName()<span class="synSpecial">}</span><span class="synConstant"> in job class should have a type declaration."</span>,
<span class="synSpecial">)</span><span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'job.propertyMissingType'</span><span class="synSpecial">)</span><span class="synType">-></span>build<span class="synSpecial">()</span>;
<span class="synStatement">continue</span>;
<span class="synSpecial">}</span>
<span class="synComment">// プロパティの型指定チェック</span>
<span class="synStatement">$</span><span class="synIdentifier">phpstanType</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>getNativeProperty<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getName<span class="synSpecial">())</span><span class="synType">-></span>getReadableType<span class="synSpecial">()</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">!$</span><span class="synIdentifier">this</span><span class="synType">-></span>isAllowedType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">phpstanType</span>, <span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">errors</span><span class="synSpecial">[]</span> <span class="synStatement">=</span> RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span>
<span class="synConstant">"Property </span><span class="synSpecial">\${</span><span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getName()<span class="synSpecial">}</span><span class="synConstant"> in job class should be a primitive type, QueueableCollection, or QueueableEntity. Found: </span><span class="synSpecial">{</span><span class="synStatement">$</span><span class="synIdentifier">phpstanType</span><span class="synType">-></span>describe(VerbosityLevel::typeOnly())<span class="synSpecial">}</span><span class="synConstant">"</span>,
<span class="synSpecial">)</span><span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'job.propertyType'</span><span class="synSpecial">)</span><span class="synType">-></span>build<span class="synSpecial">()</span>;
<span class="synSpecial">}</span> <span class="synStatement">elseif</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>isQueueableType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">phpstanType</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">errors</span><span class="synSpecial">[]</span> <span class="synStatement">=</span> RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span>
<span class="synConstant">"Property </span><span class="synSpecial">\${</span><span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getName()<span class="synSpecial">}</span><span class="synConstant"> in job class should be a primitive type or use SerializesModels trait. Found: </span><span class="synSpecial">{</span><span class="synStatement">$</span><span class="synIdentifier">phpstanType</span><span class="synType">-></span>describe(VerbosityLevel::typeOnly())<span class="synSpecial">}</span><span class="synConstant">"</span>,
<span class="synSpecial">)</span><span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'job.propertyType'</span><span class="synSpecial">)</span><span class="synType">-></span>build<span class="synSpecial">()</span>;
<span class="synSpecial">}</span> <span class="synStatement">else</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">errors</span><span class="synSpecial">[]</span> <span class="synStatement">=</span> RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span>
<span class="synConstant">"Property </span><span class="synSpecial">\${</span><span class="synStatement">$</span><span class="synIdentifier">property</span><span class="synType">-></span>getName()<span class="synSpecial">}</span><span class="synConstant"> in job class should be a primitive type. Found: </span><span class="synSpecial">{</span><span class="synStatement">$</span><span class="synIdentifier">phpstanType</span><span class="synType">-></span>describe(VerbosityLevel::typeOnly())<span class="synSpecial">}</span><span class="synConstant">"</span>,
<span class="synSpecial">)</span><span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'job.propertyType'</span><span class="synSpecial">)</span><span class="synType">-></span>build<span class="synSpecial">()</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synStatement">$</span><span class="synIdentifier">errors</span>;
<span class="synSpecial">}</span>
<span class="synComment">/**</span>
<span class="synComment"> * $type が許可された型かどうかを判定</span>
<span class="synComment"> *</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">bool $allowQueueable Queueableな型を許容するか</span>
<span class="synComment"> */</span>
<span class="synType">private</span> <span class="synPreProc">function</span> isAllowedType<span class="synSpecial">(</span>Type <span class="synStatement">$</span><span class="synIdentifier">type</span>, <span class="synType">bool</span> <span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">bool</span>
<span class="synSpecial">{</span>
<span class="synComment">// 1. まず Null を型から取り除く(Nullable をシンプルに扱うため)</span>
<span class="synStatement">$</span><span class="synIdentifier">type</span> <span class="synStatement">=</span> TypeCombinator<span class="synStatement">::</span>removeNull<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">)</span>;
<span class="synComment">// 元が null のみだった場合は NeverType(BottomType) になるので許可</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span> <span class="synStatement">instanceof</span> NeverType<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 2. 配列(またはそのUnion)の判定</span>
<span class="synComment">// removeNull したおかげで、`array|null` も安全にここでキャッチできます</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isArray<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>isAllowedType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>getIterableValueType<span class="synSpecial">()</span>, <span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 3. スカラー、またはEnumの判定</span>
<span class="synComment">// `int|string` のような同種のUnionであっても、yes() が true になります</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isScalar<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">()</span> <span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isEnum<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 4. Queueableなオブジェクトの判定</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span> <span class="synStatement">&&</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>isQueueableType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 5. 異種の複合型(例: `int|array` や `string|QueueableEntity`)のフォールバック処理</span>
<span class="synComment">// instanceof UnionType を避け、PHPStan公式の安全な分解ユーティリティを使います</span>
<span class="synStatement">$</span><span class="synIdentifier">types</span> <span class="synStatement">=</span> TypeUtils<span class="synStatement">::</span>flattenTypes<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">)</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">count</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">types</span><span class="synSpecial">)</span> <span class="synStatement">></span> <span class="synConstant">1</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">foreach</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">types</span> <span class="synStatement">as</span> <span class="synStatement">$</span><span class="synIdentifier">innerType</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">!$</span><span class="synIdentifier">this</span><span class="synType">-></span>isAllowedType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">innerType</span>, <span class="synStatement">$</span><span class="synIdentifier">allowQueueable</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">false</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synConstant">false</span>;
<span class="synSpecial">}</span>
<span class="synComment">/**</span>
<span class="synComment"> * Queueableな型かを判定</span>
<span class="synComment"> */</span>
<span class="synType">private</span> <span class="synPreProc">function</span> isQueueableType<span class="synSpecial">(</span>Type <span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">bool</span>
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isObject<span class="synSpecial">()</span><span class="synType">-></span>no<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">false</span>;
<span class="synSpecial">}</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>queueableObjectTypes <span class="synStatement">===</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>queueableObjectTypes <span class="synStatement">=</span> <span class="synSpecial">[</span>
<span class="synPreProc">new</span> ObjectType<span class="synSpecial">(</span>QueueableCollection<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>,
<span class="synPreProc">new</span> ObjectType<span class="synSpecial">(</span>QueueableEntity<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>,
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">foreach</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>queueableObjectTypes <span class="synStatement">as</span> <span class="synStatement">$</span><span class="synIdentifier">queueableObjectType</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">queueableObjectType</span><span class="synType">-></span>isSuperTypeOf<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synSpecial">)</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synConstant">false</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<h3 id="テスト">テスト</h3>
<p><code>PHPStan\Testing\RuleTestCase</code> クラスを継承して、カスタムルールに対するテストコードを書くことができます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synType">namespace</span> Tests\CustomPHPStan\Rules;
<span class="synPreProc">use</span> CustomPHPStan\Rules\Jobs\JobOnlyPrimitiveOrQueueablePropertiesRule;
<span class="synPreProc">use</span> PHPStan\Rules\Rule;
<span class="synPreProc">use</span> PHPStan\Testing\RuleTestCase;
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@extends </span><span class="synComment">RuleTestCase<JobOnlyPrimitiveOrQueueablePropertiesRule></span>
<span class="synComment"> */</span>
<span class="synType">class</span> JobOnlyPrimitiveOrQueueablePropertiesRuleTest <span class="synType">extends</span> RuleTestCase
<span class="synSpecial">{</span>
<span class="synType">protected</span> <span class="synPreProc">function</span> getRule<span class="synSpecial">()</span><span class="synStatement">:</span> Rule
<span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synPreProc">new</span> JobOnlyPrimitiveOrQueueablePropertiesRule<span class="synSpecial">()</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> testRule<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">void</span>
<span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>analyse<span class="synSpecial">([</span><span class="synConstant">__DIR__</span> <span class="synStatement">.</span> <span class="synConstant">'/data/job-only-primitive-or-queueable-properties-args.php'</span><span class="synSpecial">]</span>, <span class="synSpecial">[</span>
<span class="synSpecial">[</span>
<span class="synConstant">'Property $noTypeHint in job class should have a type declaration.'</span>,
<span class="synConstant">8</span>,
<span class="synSpecial">]</span>,
<span class="synSpecial">[</span>
<span class="synConstant">'Property $carbon in job class should be a primitive type. Found: Illuminate\Support\Carbon|null'</span>,
<span class="synConstant">8</span>,
<span class="synSpecial">]</span>,
<span class="synComment">// ...</span>
<span class="synSpecial">[</span>
<span class="synConstant">'Property $model in job class should be a primitive type or use SerializesModels trait. Found: Illuminate\Database\Eloquent\Model'</span>,
<span class="synConstant">249</span>,
<span class="synSpecial">]</span>,
<span class="synSpecial">])</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<pre class="code lang-php" data-lang="php" data-unlink>// tests/CustomPHPStan/Rules/data/job-only-primitive-or-queueable-properties-args.php
<span class="synSpecial"><?php</span>
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synPreProc">use</span> Carbon\CarbonInterface;
<span class="synPreProc">use</span> Illuminate\Support\Carbon;
<span class="synType">class</span> TestJob <span class="synType">implements</span> \Illuminate\Contracts\Queue\ShouldQueue
<span class="synSpecial">{</span>
<span class="synType">public</span> <span class="synStatement">$</span><span class="synIdentifier">noTypeHint</span>;
<span class="synType">public</span> <span class="synPreProc">function</span> <span class="synStatement">__construct</span><span class="synSpecial">(</span>
<span class="synType">public</span> Carbon <span class="synStatement">$</span><span class="synIdentifier">carbon</span>,
<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synComment">// ShouldQueue を implement していないならオブジェクトをプロパティに持ってもOK</span>
<span class="synType">class</span> NotShouldQueue
<span class="synSpecial">{</span>
<span class="synType">public</span> <span class="synPreProc">function</span> <span class="synStatement">__construct</span><span class="synSpecial">(</span>
<span class="synType">private</span> Carbon <span class="synStatement">$</span><span class="synIdentifier">carbon</span>,
<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synComment">// SerializesModels trait を使っていないモデルを持つジョブはNG</span>
<span class="synType">class</span> NotSerializesModelJob <span class="synType">implements</span> \Illuminate\Contracts\Queue\ShouldQueue
<span class="synSpecial">{</span>
<span class="synType">public</span> <span class="synPreProc">function</span> <span class="synStatement">__construct</span><span class="synSpecial">(</span>
<span class="synType">public</span> \Illuminate\Database\Eloquent\Model <span class="synStatement">$</span><span class="synIdentifier">model</span>,
<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synComment">// SerializesModels trait を使っているモデルを持つジョブはOK</span>
<span class="synType">class</span> SerializesModelJob <span class="synType">implements</span> \Illuminate\Contracts\Queue\ShouldQueue
<span class="synSpecial">{</span>
<span class="synPreProc">use</span> \Illuminate\Queue\SerializesModels;
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@template </span><span class="synComment">TModel of \Illuminate\Database\Eloquent\Model</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">TModel $model</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">\Illuminate\Database\Eloquent\Collection<int, TModel> $models</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> <span class="synStatement">__construct</span><span class="synSpecial">(</span>
<span class="synType">public</span> \Illuminate\Database\Eloquent\Model <span class="synStatement">$</span><span class="synIdentifier">model</span>,
<span class="synType">public</span> \Illuminate\Database\Eloquent\Collection <span class="synStatement">$</span><span class="synIdentifier">models</span>,
<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<h3 id="PHPStan-へのカスタムルール組み込み">PHPStan へのカスタムルール組み込み</h3>
<p>このカスタムルールを組み込むことで、Pull Request の段階でCIから問題を検知できます。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># phpstan.neon</span>
<span class="synIdentifier">rules</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>CustomPHPStan\Rules\JobOnlyPrimitiveOrQueueablePropertiesRule
</pre>
<h3 id="カスタムルール適用後の実行結果">カスタムルール適用後の実行結果</h3>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">namespace</span> App\Jobs;
<span class="synPreProc">use</span> Carbon\CarbonInterface;
<span class="synPreProc">use</span> Illuminate\Bus\Queueable;
<span class="synPreProc">use</span> Illuminate\Contracts\Queue\ShouldQueue;
<span class="synPreProc">use</span> Illuminate\Foundation\Bus\Dispatchable;
<span class="synPreProc">use</span> Illuminate\Queue\InteractsWithQueue;
<span class="synType">class</span> HogeJob <span class="synType">implements</span> ShouldQueue
<span class="synSpecial">{</span>
<span class="synPreProc">use</span> Dispatchable;
<span class="synPreProc">use</span> InteractsWithQueue;
<span class="synPreProc">use</span> Queueable;
<span class="synType">private</span> <span class="synPreProc">function</span> <span class="synStatement">__construct</span><span class="synSpecial">(</span>
<span class="synType">private</span> readonly CarbonInterface <span class="synStatement">$</span><span class="synIdentifier">triggeredAt</span>,
<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> handle<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">void</span>
<span class="synSpecial">{</span>
<span class="synComment">// ...</span>
</pre>
<pre class="code" data-lang="" data-unlink> ------ --------------------------------------------------------------------
Line app/Jobs/HogeJob.php
------ --------------------------------------------------------------------
13 Property $triggeredAt in job class should be a primitive type.
Found: Carbon\CarbonInterface
🪪 job.propertyType
------ --------------------------------------------------------------------</pre>
<p>無事にシリアライズ時に危険性がある Job を検出できるようになりましたね!</p>
<h2 id="まとめ">まとめ</h2>
<p>今回ご紹介した PHPStan カスタムルールを導入することによって、シリアライズで危険性がある実装があった際に、自動検知する仕組みが導入できました。</p>
<p>この仕組みを応用することで、Job 以外でも同様な検知を仕組み化できます。
例えば Redis にシリアライズ化してオブジェクトキャッシュを保存していたりする場合に、オブジェクトがあったら検知するといったことも可能です。</p>
<p>PHPStan のカスタムルールは最初は書き方が分からなくてとっつきにくい印象ですが、
うまく使うとチェックを仕組み化できるため、かなり便利です。</p>
<p>ぜひ皆さんもカスタムルールを活用してみてください!</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
コロプラでは、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<div class="footnotes">
<hr/>
<ol>
<li id="fn:1">
<a href="https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated">Why Is instanceof *Type Wrong and Getting Deprecated?</a><a href="#fnref:1" rev="footnote">↩</a></li>
<li id="fn:2">
Job の場合は、Model に関してはそもそも <code>SerializesModels</code> trait を use して、Model インスタンスはシリアライズしないようにするのが適切です。<a href="#fnref:2" rev="footnote">↩</a></li>
<li id="fn:3">
厳密には既存のジョブに対して新規で trait を use したり外すと、 <code>unserialize()</code> に失敗し <code>ErrorException</code> が発生する可能性があります。今回ご紹介する方法では検知が困難なため、検知対象外としています。<a href="#fnref:3" rev="footnote">↩</a></li>
</ol>
</div>
colopl-tech
PHPStanのカスタムルールの作り方 - ASTを理解してプロジェクト固有の検査を実装する
hatenablog://entry/17179246901348279817
2026-02-12T11:30:00+09:00
2026-02-12T11:30:00+09:00 こんにちは。バックエンドエンジニアの平野です。 前回の記事では、運用中のタイトルに静的解析を導入してコード品質を継続的に改善した話を紹介しました。 今回はPHPStanの機能のひとつである、ユーザー独自の静的解析ルールを定義できる「カスタムルール」の作り方を解説します。
<p>こんにちは。バックエンドエンジニアの平野です。</p>
<p><a href="https://blog.colopl.dev/entry/phpstan-legacy-project">前回の記事</a>では、運用中のタイトルに静的解析を導入してコード品質を継続的に改善した話を紹介しました。</p>
<p>今回はPHPStanの機能のひとつである、ユーザー独自の静的解析ルールを定義できる「カスタムルール」の作り方を解説します。</p>
<h2 id="カスタムルールとは">カスタムルールとは</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>の静的解析ツールであるPHPStanには、標準のルールセットに加えてユーザー独自の解析ルールを定義できる機能があります。
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fphpstan.org%2Fdeveloping-extensions%2Frules" title="Custom Rules" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://phpstan.org/developing-extensions/rules">phpstan.org</a></cite></p>
<p>カスタムルールを使うことで、以下のような要件をコードレビューではなく自動チェックで検出できるようになります。</p>
<ul>
<li>特定の関数の使用を禁止したい</li>
<li>クラス名に一定の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE%B5%AC%C2%A7">命名規則</a>を持たせたい</li>
<li>依存関係に一定の制約を持たせたい</li>
</ul>
<h2 id="カスタムルールを作成するメリット">カスタムルールを作成するメリット</h2>
<p>カスタムルールを導入することで、プロジェクトの開発効率とコード品質を向上できます。</p>
<p>プロジェクトの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D3%A5%B8%A5%CD%A5%B9%A5%ED%A5%B8%A5%C3%A5%AF">ビジネスロジック</a>に特化したルールを定義すれば、標準ルールでは検知できない特有のバグや問題を検出できます。特定の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%B6%A5%A4%A5%F3%A5%D1%A5%BF%A1%BC%A5%F3">デザインパターン</a>の違反や<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>使用規約の遵守、データベースアクセスパターンの制約なども自動チェックの対象にできます。</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE%B5%AC%C2%A7">命名規則</a>や禁止パターンのチェックを自動化すれば、コードレビューの負担を軽減でき、レビュアーは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D3%A5%B8%A5%CD%A5%B9%A5%ED%A5%B8%A5%C3%A5%AF">ビジネスロジック</a>の妥当性や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>設計といった、より重要な論点に集中できます。</p>
<p>プロジェクトやチーム固有のコーディング規約が自動チェックされることで、新規メンバーのオンボーディングもスムーズになり、コードベース全体の一貫した品質維持にもつながります。</p>
<h2 id="PHPStanの動作原理">PHPStanの動作原理</h2>
<p>カスタムルールを作成するために、まずPHPStanがどのようにコードを解析しているかを理解します。</p>
<p>PHPStanは内部で<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BD%CA%B8%B2%F2%C0%CF">構文解析</a>器を使用し、プログラムをAST(抽象<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BD%CA%B8%CC%DA">構文木</a>)という中間表現に変換してコードを解析しています。</p>
<h3 id="ASTとは">ASTとは</h3>
<p>ASTとはプログラムにおける<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>や変数、キーワードを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%DA%B9%BD%C2%A4">木構造</a>で表現したものです。</p>
<p>例えば、<a href="https://github.com/nikic/PHP-Parser">nikic/PHP-Parser</a>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>のプログラムをASTに変換するパーサーで、PHPStanの内部でも使用されています。このパーサーを使って<code>1+2</code>という演算を変換してみます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synPreProc">use</span> PhpParser\ParserFactory;
<span class="synStatement">$</span><span class="synIdentifier">parser</span> <span class="synStatement">=</span> <span class="synSpecial">(</span><span class="synPreProc">new</span> ParserFactory<span class="synSpecial">)</span><span class="synType">-></span>create<span class="synSpecial">(</span>ParserFactory<span class="synStatement">::</span>PREFER_PHP7<span class="synSpecial">)</span>;
<span class="synStatement">$</span><span class="synIdentifier">ast</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">parser</span><span class="synType">-></span>parse<span class="synSpecial">(</span><span class="synConstant">'<?php 1+2 ?>'</span><span class="synSpecial">)</span>;
</pre>
<p>出力は以下のようになります。</p>
<pre class="code lang-php" data-lang="php" data-unlink>[
PhpParser\Node\Stmt\Expression {
+expr: PhpParser\Node\Expr\BinaryOp\Plus {
+left: PhpParser\Node\Scalar\LNumber {
+value: 1,
},
+right: PhpParser\Node\Scalar\LNumber {
+value: 2,
},
},
},
]
</pre>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%FE%A4%EC%BB%D2">入れ子</a>構造の結果が得られました。</p>
<h3 id="ノードの操作">ノードの操作</h3>
<p>各ノードは子ノードへの参照をプロパティとして持っているため、以下のようにアクセスできます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>expr<span class="synType">-></span>left<span class="synType">-></span><span class="synIdentifier">value</span>; <span class="synComment">// 1</span>
</pre>
<p>この構造を視覚的に表現すると以下のようになります。</p>
<pre class="code" data-lang="" data-unlink>$ast[0]
└─ expr (Expr\BinaryOp\Plus)
├─ left (Scalar\LNumber)
│ └─ value: 1
└─ right (Scalar\LNumber)
└─ value: 2</pre>
<p>演算式全体のノードを根として、<code>+</code><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>の子ノード、さらにその下に左右の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AB%A5%E9%A1%BC">スカラー</a>値のノードが連なる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%DA%B9%BD%C2%A4">木構造</a>になっています。</p>
<p>一部のノードは自身の型に関する情報も持っており、型の妥当性チェックに利用できます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>expr<span class="synType">-></span>getType<span class="synSpecial">()</span>; <span class="synComment">// Expr_BinaryOp_Plus</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>expr<span class="synType">-></span>left<span class="synType">-></span>getType<span class="synSpecial">()</span>; <span class="synComment">// Scalar_LNumber</span>
</pre>
<h3 id="より複雑な例">より複雑な例</h3>
<p>次に、プロパティとアクセサを持つ簡単なクラスを変換してみます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">code</span> <span class="synStatement">=</span> <span class="synStatement"><<<</span><span class="synSpecial">'CODE'</span>
<?php
class Foo
{
private int $bar;
public function getBar(): int
{
return $this->bar;
}
public function setBar(int $value): void
{
$this->bar = $value;
}
}
<span class="synSpecial">CODE</span>;
<span class="synStatement">$</span><span class="synIdentifier">ast</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">parser</span><span class="synType">-></span>parse<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">code</span><span class="synSpecial">)</span>;
</pre>
<p>変換結果は少し長いので中間要素を抜き出すと以下のようになります。</p>
<pre class="code lang-php" data-lang="php" data-unlink>[
PhpParser\Node\Stmt\Class_ {
+name: PhpParser\Node\Identifier {
+name: "Foo",
},
+stmts: [
PhpParser\Node\Stmt\Property {},
PhpParser\Node\Stmt\ClassMethod {},
PhpParser\Node\Stmt\ClassMethod {},
],
+attrGroups: [],
+namespacedName: null,
+flags: 0,
+extends: null,
+implements: [],
},
]
</pre>
<p>根にクラス宣言のノード<code>Stmt\Class_</code>があり、<code>name</code>と<code>stmts</code>に以下の子ノードが連なっています。</p>
<ul>
<li>クラスの識別子 <code>Identifier</code></li>
<li>プロパティ <code>Stmt\Property</code></li>
<li>クラスメソッド <code>Stmt\ClassMethod</code>(2つ)</li>
</ul>
<p>さらにそれぞれの要素を見ていくと以下のような構成になっています。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synType">-></span><span class="synIdentifier">name</span>; <span class="synComment">// "Foo"</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>stmts<span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>props<span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synType">-></span><span class="synIdentifier">name</span>; <span class="synComment">// "bar"</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>stmts<span class="synSpecial">[</span><span class="synConstant">1</span><span class="synSpecial">]</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synType">-></span><span class="synIdentifier">name</span>; <span class="synComment">// "getBar"</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>stmts<span class="synSpecial">[</span><span class="synConstant">1</span><span class="synSpecial">]</span><span class="synType">-></span>returnType<span class="synType">-></span><span class="synIdentifier">name</span>; <span class="synComment">// "int"</span>
<span class="synStatement">$</span><span class="synIdentifier">ast</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span>stmts<span class="synSpecial">[</span><span class="synConstant">2</span><span class="synSpecial">]</span><span class="synType">-></span>params<span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span><span class="synType">-></span><span class="synIdentifier">type</span><span class="synType">-></span><span class="synIdentifier">name</span>; <span class="synComment">// "int"</span>
</pre>
<pre class="code" data-lang="" data-unlink>$ast[0] (Stmt\Class_)
├─ name (Identifier)
│ └─ name: "Foo"
└─ stmts (array)
├─ [0] (Stmt\Property)
│ └─ props[0]
│ └─ name: "bar"
├─ [1] (Stmt\ClassMethod)
│ ├─ name: "getBar"
│ └─ returnType
│ └─ name: "int"
└─ [2] (Stmt\ClassMethod)
├─ name: "setBar"
└─ params[0]
└─ type
└─ name: "int"</pre>
<p>クラス定義のノードを根として、クラス名、プロパティ、メソッドが階層的に配置されています。各メソッドはさらに戻り値の型や引数の型情報を持っています。</p>
<p>PHPStanはこのようなASTを探索してコードを解析しており、カスタムルールもこのデータ構造に基づいて実装します。</p>
<h2 id="カスタムルールの作成方法">カスタムルールの作成方法</h2>
<p>実際にカスタムルールを作成する手順を見ていきます。</p>
<h3 id="1-ルールクラスの作成">1. ルールクラスの作成</h3>
<p>PHPStanが提供する<code>PHPStan\Rules\Rule</code>インターフェースを実装したクラスを作成します。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synType">namespace</span> <span class="synType">Namespace</span>\To\RuleFile;
<span class="synPreProc">use</span> PhpParser\Node;
<span class="synPreProc">use</span> PHPStan\Rules\Rule;
<span class="synPreProc">use</span> PHPStan\Analyser\Scope;
<span class="synType">class</span> SampleCustomRule <span class="synType">implements</span> Rule
<span class="synSpecial">{</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span> <span class="synSpecial">{}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span> <span class="synSpecial">{}</span>
<span class="synSpecial">}</span>
</pre>
<h3 id="2-設定ファイルへの登録">2. 設定ファイルへの登録</h3>
<p><code>phpstan.neon</code>の<code>rules</code>ブロックに作成したカスタムルールの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BE%C1%B0%B6%F5%B4%D6">名前空間</a>を追加します。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">rules</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>Namespace\To\RuleFile\SampleCustomRule
</pre>
<p>これで次回の実行時から追加ルールが適用されます。</p>
<h3 id="3-getNodeTypeの実装">3. getNodeType()の実装</h3>
<p><code>getNodeType()</code>は、このルールがどのタイプのノードに対して適用されるかを宣言するメソッドです。</p>
<p>PHPStanはASTを探索する際、すべてのノードをこのメソッドで指定した型と照合し、マッチしたノードのみを後述する<code>processNode()</code>に渡します。</p>
<p>戻り値には<code>PhpParser\Node</code>のサブクラスのクラス名(class-string)を指定します。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// 演算子のノードのみを対象とする場合</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> BinaryOp<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synComment">// メソッド呼び出しのノードのみを対象とする場合</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> MethodCall<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
</pre>
<p>例えば<code>BinaryOp::class</code>を指定すると、コード中の<code>+</code>、<code>-</code>、<code>*</code>、<code>/</code>などの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>ノードだけが<code>processNode()</code>に渡され、変数やメソッド呼び出しなどは無視されます。</p>
<h3 id="4-processNodeの実装">4. processNode()の実装</h3>
<p><code>processNode()</code>は実際の解析処理を定義するメソッドです。<code>getNodeType()</code>でマッチしたノードに対して実行されます。</p>
<h4 id="Nodeパラメータの活用">Nodeパラメータの活用</h4>
<p>第1引数の<code>$node</code>は、<code>getNodeType()</code>で指定した型にマッチしたノードです。ノードタイプに応じた様々なプロパティを持っており、コードの詳細な情報にアクセスできます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// 二項演算子ノード(BinaryOp)の場合</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>left; <span class="synComment">// 左辺のノード</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>right; <span class="synComment">// 右辺のノード</span>
<span class="synComment">// メソッド呼び出しノード(MethodCall)の場合</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>var; <span class="synComment">// メソッドが呼ばれるオブジェクト</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span>; <span class="synComment">// メソッド名</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>args; <span class="synComment">// 引数のリスト</span>
<span class="synComment">// クラスノード(Class_)の場合</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span>; <span class="synComment">// クラス名</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>extends; <span class="synComment">// 継承元クラス</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>implements; <span class="synComment">// 実装するインターフェース</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>stmts; <span class="synComment">// クラス内の実装(メソッドやプロパティ)</span>
</pre>
<h4 id="戻り値の形式">戻り値の形式</h4>
<p><code>processNode()</code>の戻り値は<code>IdentifierRuleError</code>の配列です。ルール違反を検出した場合は<code>RuleErrorBuilder</code>でエラーを構築し、問題がなければ空配列を返します。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synPreProc">use</span> PHPStan\Rules\RuleErrorBuilder;
<span class="synComment">/**</span>
<span class="synComment"> * ゼロ除算の検査</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synComment">// ノードが除算演算子かつ右辺が数値の0ならエラーを返す</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> Node\Expr\BinaryOp\Div
<span class="synStatement">&&</span> <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>right <span class="synStatement">instanceof</span> Node\Scalar\LNumber
<span class="synStatement">&&</span> <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>right<span class="synType">-></span><span class="synIdentifier">value</span> <span class="synStatement">===</span> <span class="synConstant">0</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">'zero division error'</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.zeroDivision'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synComment">// 問題ない場合は空配列を返す</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<p><code>RuleErrorBuilder</code>の<code>message()</code>でエラーメッセージを設定し、<code>identifier()</code>でエラーの識別子を指定します。識別子はエラーの分類や抑制設定に使用されます。</p>
<h3 id="Scopeの活用">Scopeの活用</h3>
<p>第2引数の<code>$scope</code>は、ノード単体では取得できない文脈の情報を提供します。</p>
<h4 id="Scopeの役割">Scopeの役割</h4>
<p><code>Scope</code>オブジェクトは、現在解析中のコードのスコープに関する情報を保持しています。具体的には以下のような情報にアクセスできます。</p>
<ul>
<li>現在のクラスやメソッドの情報</li>
<li>変数や式の型情報</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BE%C1%B0%B6%F5%B4%D6">名前空間</a>の情報</li>
<li>利用可能な関数やクラスの情報</li>
</ul>
<h4 id="Scopeの主な機能">Scopeの主な機能</h4>
<p><strong>1. 型情報の取得</strong></p>
<p>PHPStanの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%BF%BF%E4%CF%C0">型推論</a>エンジンによって計算された、ノードの式が持つ型を取得できます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// 式の型を取得</span>
<span class="synStatement">$</span><span class="synIdentifier">type</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>var<span class="synSpecial">)</span>;
<span class="synComment">// 型をチェック</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>isObject<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">classNames</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">type</span><span class="synType">-></span>getObjectClassNames<span class="synSpecial">()</span>; <span class="synComment">// クラス名を取得</span>
<span class="synSpecial">}</span>
</pre>
<p><strong>2. クラス情報の取得</strong></p>
<p>現在のスコープが属するクラスの情報を取得できます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// 現在のクラスのリフレクション情報を取得</span>
<span class="synStatement">$</span><span class="synIdentifier">classReflection</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getClassReflection<span class="synSpecial">()</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">classReflection</span> <span class="synStatement">!==</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">className</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>getName<span class="synSpecial">()</span>; <span class="synComment">// クラス名</span>
<span class="synStatement">$</span><span class="synIdentifier">parentClass</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>getParentClass<span class="synSpecial">()</span>; <span class="synComment">// 親クラス</span>
<span class="synStatement">$</span><span class="synIdentifier">interfaces</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">classReflection</span><span class="synType">-></span>getInterfaces<span class="synSpecial">()</span>; <span class="synComment">// 実装インターフェース</span>
<span class="synSpecial">}</span>
</pre>
<p><strong>3. メソッド情報の取得</strong></p>
<p>現在のスコープが属するメソッドの情報を取得できます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// 現在のメソッド名を取得</span>
<span class="synStatement">$</span><span class="synIdentifier">methodReflection</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getFunction<span class="synSpecial">()</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">methodReflection</span> <span class="synStatement">!==</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">methodName</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">methodReflection</span><span class="synType">-></span>getName<span class="synSpecial">()</span>;
<span class="synSpecial">}</span>
</pre>
<p><strong>4. 変数の型チェック</strong></p>
<p>スコープ内で定義された変数の型を確認できます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// 変数が定義されているかチェック</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>hasVariableType<span class="synSpecial">(</span><span class="synConstant">'variableName'</span><span class="synSpecial">)</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">variableType</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getVariableType<span class="synSpecial">(</span><span class="synConstant">'variableName'</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
</pre>
<h4 id="Scopeの実践例">Scopeの実践例</h4>
<p>メソッド呼び出し元の実装クラスを取得する例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// メソッド呼び出しノードの場合</span>
<span class="synComment">// $node: Node\Expr\MethodCall</span>
<span class="synStatement">$</span><span class="synIdentifier">calledOnType</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>var<span class="synSpecial">)</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">calledOnType</span><span class="synType">-></span>isObject<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">classNames</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">calledOnType</span><span class="synType">-></span>getObjectClassNames<span class="synSpecial">()</span>;
<span class="synComment">// $classNamesには実際のクラス名が入る</span>
<span class="synSpecial">}</span>
</pre>
<p><code>$node->var</code>だけでは変数名しか分かりませんが、<code>$scope->getType()</code>を使うことで、その変数の実際の型(クラス名)を解決できます。</p>
<h2 id="ノード別の実装例">ノード別の実装例</h2>
<p>ここからは、ノードごとの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>と実装例を紹介します。</p>
<h3 id="変数と代入NodeExprVariable-NodeExprAssign">変数と代入(Node\Expr\Variable, Node\Expr\Assign)</h3>
<p>変数名の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE%B5%AC%C2%A7">命名規則</a>をチェックする例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Expr\Variable<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">is_string</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synSpecial">)</span> <span class="synStatement">&&</span> <span class="synIdentifier">preg_match</span><span class="synSpecial">(</span><span class="synConstant">'/^[a-z][a-zA-Z0-9]*$/'</span>, <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synSpecial">)</span> <span class="synStatement">!==</span> <span class="synConstant">1</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">"変数名はキャメルケースで記述してください: </span><span class="synSpecial">\${</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>name<span class="synSpecial">}</span><span class="synConstant">"</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.variable.camelCase'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="演算子NodeExprBinaryOp"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>(Node\Expr\BinaryOp)</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E2%C6%B0%BE%AE%BF%F4%C5%C0%BF%F4">浮動小数点数</a>を含む演算で誤差の可能性を警告する例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Expr\BinaryOp<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">((</span><span class="synStatement">$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> BinaryOp\Plus
<span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> BinaryOp\Minus
<span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> BinaryOp\Mul
<span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> BinaryOp\Div
<span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> BinaryOp\Mod
<span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> BinaryOp\<span class="synIdentifier">Pow</span>
<span class="synSpecial">)</span> <span class="synStatement">&&</span> <span class="synSpecial">(</span>
<span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>right<span class="synSpecial">)</span><span class="synType">-></span>isFloat<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">()</span>
<span class="synStatement">||</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>left<span class="synSpecial">)</span><span class="synType">-></span>isFloat<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">()</span>
<span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">'浮動小数点数を含む演算です。誤差の可能性があります'</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.float.arithmetic'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="インスタンス化NodeExprNew_"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>化(Node\Expr\New_)</h3>
<p>Singletonパターンのクラスが直接<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>化されていないかチェックする例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Expr\New_<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synComment">// Singletonを名前に含むクラスがnewされていないか</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>class <span class="synStatement">instanceof</span> Name<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synComment">// クラス名のみを取得(名前空間を除く)</span>
<span class="synStatement">$</span><span class="synIdentifier">className</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>class<span class="synType">-></span>getLast<span class="synSpecial">()</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">str_contains</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">className</span>, <span class="synConstant">'Singleton'</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">'このクラスのインスタンス化は禁止されています'</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.singleton.new'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="メソッド呼び出しNodeExprMethodCall-NodeExprStaticCall">メソッド呼び出し(Node\Expr\MethodCall, Node\Expr\StaticCall)</h3>
<p>PHPDocの<code>@deprecated</code>タグが付けられたメソッドの使用を検出する例です。非推奨<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を段階的に廃止する際に有用です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Expr\MethodCall<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">!$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span> <span class="synStatement">instanceof</span> Node\Identifier<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">$</span><span class="synIdentifier">calledOnType</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synType">-></span>getType<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>var<span class="synSpecial">)</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">!$</span><span class="synIdentifier">calledOnType</span><span class="synType">-></span>isObject<span class="synSpecial">()</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">$</span><span class="synIdentifier">methodName</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synType">-></span>toString<span class="synSpecial">()</span>;
<span class="synComment">// メソッドの型情報を取得</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">calledOnType</span><span class="synType">-></span>hasMethod<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">methodName</span><span class="synSpecial">)</span><span class="synType">-></span>yes<span class="synSpecial">())</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">methodReflection</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">calledOnType</span><span class="synType">-></span>getMethod<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">methodName</span>, <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span>;
<span class="synComment">// PHPDocから</span><span class="synPreProc">@deprecated</span><span class="synComment">タグをチェック</span>
<span class="synStatement">$</span><span class="synIdentifier">docComment</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">methodReflection</span><span class="synType">-></span>getDocComment<span class="synSpecial">()</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">docComment</span> <span class="synStatement">!==</span> <span class="synType">null</span> <span class="synStatement">&&</span> <span class="synIdentifier">str_contains</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">docComment</span>, <span class="synConstant">'@deprecated'</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">classNames</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">calledOnType</span><span class="synType">-></span>getObjectClassNames<span class="synSpecial">()</span>;
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synIdentifier">sprintf</span><span class="synSpecial">(</span>
<span class="synConstant">"非推奨メソッド %s::%s() が使用されています"</span>,
<span class="synStatement">$</span><span class="synIdentifier">classNames</span><span class="synSpecial">[</span><span class="synConstant">0</span><span class="synSpecial">]</span>,
<span class="synStatement">$</span><span class="synIdentifier">methodName</span>,
<span class="synSpecial">))</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.method.deprecated'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<p>可変関数(<code>$func()</code>のような形式)の使用を検出する例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Expr\FuncCall<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synComment">// 関数名が変数の場合(可変関数)</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span> <span class="synStatement">instanceof</span> Node\Expr\Variable<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">'可変関数の使用は推奨されません。静的解析が困難になります'</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.function.variable'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="関数呼び出しNodeExprFuncCall">関数呼び出し(Node\Expr\FuncCall)</h3>
<p>危険な関数の使用を検出する例です。セキュリティ上のリスクがある関数を禁止することで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>の混入を防げます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Expr\FuncCall<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span> <span class="synStatement">instanceof</span> Name<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">functionName</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synType">-></span>toString<span class="synSpecial">()</span>;
<span class="synComment">// セキュリティリスクのある関数リスト</span>
<span class="synStatement">$</span><span class="synIdentifier">dangerousFunctions</span> <span class="synStatement">=</span> <span class="synSpecial">[</span>
<span class="synConstant">'exec'</span> <span class="synStatement">=></span> <span class="synConstant">'コマンドインジェクションのリスクがあります'</span>,
<span class="synConstant">'shell_exec'</span> <span class="synStatement">=></span> <span class="synConstant">'コマンドインジェクションのリスクがあります'</span>,
<span class="synConstant">'system'</span> <span class="synStatement">=></span> <span class="synConstant">'コマンドインジェクションのリスクがあります'</span>,
<span class="synConstant">'passthru'</span> <span class="synStatement">=></span> <span class="synConstant">'コマンドインジェクションのリスクがあります'</span>,
<span class="synSpecial">]</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">isset</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">dangerousFunctions</span><span class="synSpecial">[</span><span class="synStatement">$</span><span class="synIdentifier">functionName</span><span class="synSpecial">]))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synIdentifier">sprintf</span><span class="synSpecial">(</span>
<span class="synConstant">"危険な関数 %s() の使用は禁止されています: %s"</span>,
<span class="synStatement">$</span><span class="synIdentifier">functionName</span>,
<span class="synStatement">$</span><span class="synIdentifier">dangerousFunctions</span><span class="synSpecial">[</span><span class="synStatement">$</span><span class="synIdentifier">functionName</span><span class="synSpecial">]</span>
<span class="synSpecial">))</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.function.dangerous'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="クラス定義NodeStmtClass_">クラス定義(Node\Stmt\Class_)</h3>
<p>クラス名の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE%B5%AC%C2%A7">命名規則</a>をチェックする例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Stmt\Class_<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span> <span class="synStatement">!==</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">className</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synType">-></span>toString<span class="synSpecial">()</span>;
<span class="synComment">// クラス名がPascalCaseになっているかチェック</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">preg_match</span><span class="synSpecial">(</span><span class="synConstant">'/^[A-Z][a-zA-Z0-9]*$/'</span>, <span class="synStatement">$</span><span class="synIdentifier">className</span><span class="synSpecial">)</span> <span class="synStatement">!==</span> <span class="synConstant">1</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">"クラス名はPascalCaseで記述してください: </span><span class="synSpecial">{</span><span class="synStatement">$</span><span class="synIdentifier">className</span><span class="synSpecial">}</span><span class="synConstant">"</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.class.pascalCase'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="制御構文NodeStmtIf-NodeStmtSwitch">制御構文(Node\Stmt\If<em>, Node\Stmt\Switch</em>)</h3>
<p><code>switch(true)</code>のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%F3%A5%C1%A5%D1%A5%BF%A1%BC%A5%F3">アンチパターン</a>を検出する例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Stmt\Switch_<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synComment">// switch(true)のようなcaseでブール式を評価させるケースの検出</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>cond <span class="synStatement">instanceof</span> Node\Expr\ConstFetch
<span class="synStatement">&&</span> <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>cond<span class="synType">-></span><span class="synIdentifier">name</span><span class="synType">-></span>toString<span class="synSpecial">()</span> <span class="synStatement">===</span> <span class="synConstant">'true'</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">'switch(true)の使用は推奨されません。if-elseifを使用してください'</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.switch.true'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="例外処理NodeStmtTryCatch">例外処理(Node\Stmt\TryCatch)</h3>
<p>空のcatchブロックを検出する例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeType<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">string</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> Node\Stmt\TryCatch<span class="synStatement">::</span><span class="synType">class</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> processNode<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span>, Scope <span class="synStatement">$</span><span class="synIdentifier">scope</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">foreach</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span>catches <span class="synStatement">as</span> <span class="synStatement">$</span><span class="synIdentifier">catch</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synIdentifier">count</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">catch</span><span class="synType">-></span>stmts<span class="synSpecial">)</span> <span class="synStatement">===</span> <span class="synConstant">0</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>
RuleErrorBuilder<span class="synStatement">::</span>message<span class="synSpecial">(</span><span class="synConstant">'空のcatchブロックは避けてください。最低限ログ出力を行ってください'</span><span class="synSpecial">)</span>
<span class="synType">-></span>identifier<span class="synSpecial">(</span><span class="synConstant">'custom.catch.empty'</span><span class="synSpecial">)</span>
<span class="synType">-></span>build<span class="synSpecial">()</span>
<span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synStatement">return</span> <span class="synSpecial">[]</span>;
<span class="synSpecial">}</span>
</pre>
<h3 id="カスタムルール実装のポイント">カスタムルール実装のポイント</h3>
<p>ここまでの実装例を踏まえて、ポイントをまとめます。</p>
<p><strong>1. ノードタイプの選択</strong></p>
<p>検出したいパターンに最も近いノードタイプを<code>getNodeType()</code>で指定します。メソッド呼び出しなら<code>MethodCall</code>、クラス定義なら<code>Class_</code>というように、対象を絞ることで不要なノードの処理をスキップできます。</p>
<p><strong>2. Scopeを活用した型解決</strong></p>
<p>ノード単体では変数名やメソッド名しか分かりませんが、<code>Scope</code>を使うことで実際の型情報を解決でき、より高度なチェックが可能になります。</p>
<p><strong>3. 段階的な条件チェック</strong></p>
<p><code>instanceof</code>による型チェックと早期リターンを活用することで、コードの可読性と安全性を保てます。</p>
<p><strong>4. 分かりやすいエラーメッセージ</strong></p>
<p>エラーメッセージには、何が問題でどうすれば良いかを明確に記載することで、開発者が素早く修正できます。</p>
<p>これらの実装例はプロジェクトの要件に合わせてカスタマイズできます。</p>
<h2 id="Rectorへの応用">Rectorへの応用</h2>
<p>PHPStanがコードの問題を検出するツールであるのに対し、<a href="https://github.com/rectorphp/rector">Rector</a>はコードを自動的に書き換える<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>ツールです。RectorもPHPStanと同様にASTを使用してコードを解析・変更するため、カスタムルールで学んだASTの知識をそのまま活用できます。</p>
<h3 id="Rectorの既定ルールの紹介">Rectorの既定ルールの紹介</h3>
<p>Rectorには、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>のバージョンアップやコードの近代化に役立つ既定ルールが多数用意されています。いくつか紹介します。</p>
<p><strong>1. <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>バージョンアップ対応</strong></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 7.4から8.0への移行時に、nullセーフ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>を自動で適用する<a href="https://github.com/rectorphp/rector/blob/main/rules/Php80/Rector/If_/NullsafeOperatorRector.php">NullsafeOperatorRector</a>です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// rector.php</span>
<span class="synPreProc">use</span> Rector\Php80\Rector\If_\NullsafeOperatorRector;
<span class="synStatement">$</span><span class="synIdentifier">rectorConfig</span><span class="synType">-></span>rule<span class="synSpecial">(</span>NullsafeOperatorRector<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>;
</pre>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// Before</span>
<span class="synStatement">$</span><span class="synIdentifier">country</span> <span class="synStatement">=</span> <span class="synType">null</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">session</span> <span class="synStatement">!==</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">user</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">session</span><span class="synType">-></span><span class="synIdentifier">user</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">user</span> <span class="synStatement">!==</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">address</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">user</span><span class="synType">-></span>getAddress<span class="synSpecial">()</span>;
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">address</span> <span class="synStatement">!==</span> <span class="synType">null</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">country</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">address</span><span class="synType">-></span>country;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synComment">// After (Rector適用後)</span>
<span class="synStatement">$</span><span class="synIdentifier">country</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">session</span><span class="synType">?-></span><span class="synIdentifier">user</span><span class="synType">?-></span>getAddress<span class="synSpecial">()</span><span class="synType">?-></span>country;
</pre>
<p><strong>2. 条件文の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a></strong></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B0%B9%E0%B1%E9%BB%BB%BB%D2">三項演算子</a>を簡潔なnull合体<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>に置き換える<a href="https://github.com/rectorphp/rector/blob/main/rules/Php70/Rector/Ternary/TernaryToNullCoalescingRector.php">TernaryToNullCoalescingRector</a>です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// rector.php</span>
<span class="synPreProc">use</span> Rector\Php70\Rector\Ternary\TernaryToNullCoalescingRector;
<span class="synStatement">$</span><span class="synIdentifier">rectorConfig</span><span class="synType">-></span>rule<span class="synSpecial">(</span>TernaryToNullCoalescingRector<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>;
</pre>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// Before</span>
<span class="synStatement">$</span><span class="synIdentifier">name</span> <span class="synStatement">=</span> <span class="synStatement">isset</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">user</span><span class="synSpecial">[</span><span class="synConstant">'name'</span><span class="synSpecial">])</span> <span class="synStatement">?</span> <span class="synStatement">$</span><span class="synIdentifier">user</span><span class="synSpecial">[</span><span class="synConstant">'name'</span><span class="synSpecial">]</span> <span class="synStatement">:</span> <span class="synConstant">'Guest'</span>;
<span class="synComment">// After (Rector適用後)</span>
<span class="synStatement">$</span><span class="synIdentifier">name</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">user</span><span class="synSpecial">[</span><span class="synConstant">'name'</span><span class="synSpecial">]</span> <span class="synStatement">??</span> <span class="synConstant">'Guest'</span>;
</pre>
<p><strong>3. declare文の自動追加</strong></p>
<p>全ての<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>ファイルに<code>declare(strict_types=1)</code>を自動で追加する<a href="https://github.com/rectorphp/rector/blob/main/rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php">DeclareStrictTypesRector</a>です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// rector.php</span>
<span class="synPreProc">use</span> Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
<span class="synStatement">$</span><span class="synIdentifier">rectorConfig</span><span class="synType">-></span>rule<span class="synSpecial">(</span>DeclareStrictTypesRector<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">)</span>;
</pre>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// Before</span>
<span class="synType">namespace</span> App;
<span class="synComment">// After (Rector適用後)</span>
<span class="synStatement"><?</span>php
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synType">namespace</span> App;
</pre>
<p><strong>4. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BE%C1%B0%B6%F5%B4%D6">名前空間</a>の整理</strong></p>
<p>完全修飾名(<a class="keyword" href="https://d.hatena.ne.jp/keyword/FQCN">FQCN</a>)を使っている箇所をuse文に集約する機能です。これは個別のルールではなく、Rectorの設定メソッド <a href="https://getrector.com/documentation/automated-import-names"><code>importNames()</code></a> で有効化します。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// rector.php</span>
<span class="synStatement">$</span><span class="synIdentifier">rectorConfig</span><span class="synType">-></span>importNames<span class="synSpecial">()</span>;
</pre>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">// Before</span>
<span class="synType">public</span> <span class="synPreProc">function</span> process<span class="synSpecial">()</span><span class="synStatement">:</span> \App\Service\UserService
<span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synPreProc">new</span> \App\Service\UserService<span class="synSpecial">()</span>;
<span class="synSpecial">}</span>
<span class="synComment">// After (Rector適用後)</span>
<span class="synPreProc">use</span> App\Service\UserService;
<span class="synType">public</span> <span class="synPreProc">function</span> process<span class="synSpecial">()</span><span class="synStatement">:</span> UserService
<span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synPreProc">new</span> UserService<span class="synSpecial">()</span>;
<span class="synSpecial">}</span>
</pre>
<p>これらの既定ルールを使うだけでも、コードの品質向上や保守性の改善に貢献します。</p>
<h3 id="Rectorのカスタムルール例">Rectorのカスタムルール例</h3>
<p>既定ルールに加えて、プロジェクト固有のルールも作成できます。古いメソッドを新しいメソッドに自動置換するルールの例です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synPreProc">use</span> PhpParser\Node;
<span class="synPreProc">use</span> PhpParser\Node\Expr\MethodCall;
<span class="synPreProc">use</span> PhpParser\Node\Identifier;
<span class="synPreProc">use</span> Rector\Rector\AbstractRector;
<span class="synPreProc">use</span> Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
<span class="synType">final</span> <span class="synType">class</span> RenameOldMethodRector <span class="synType">extends</span> AbstractRector
<span class="synSpecial">{</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getRuleDefinition<span class="synSpecial">()</span><span class="synStatement">:</span> RuleDefinition
<span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synPreProc">new</span> RuleDefinition<span class="synSpecial">(</span><span class="synConstant">'Rename oldMethod to newMethod'</span>, <span class="synSpecial">[])</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> getNodeTypes<span class="synSpecial">()</span><span class="synStatement">:</span> <span class="synType">array</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synSpecial">[</span>MethodCall<span class="synStatement">::</span><span class="synType">class</span><span class="synSpecial">]</span>;
<span class="synSpecial">}</span>
<span class="synType">public</span> <span class="synPreProc">function</span> refactor<span class="synSpecial">(</span>Node <span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synSpecial">)</span><span class="synStatement">:</span> <span class="synStatement">?</span>Node
<span class="synSpecial">{</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">!$</span><span class="synIdentifier">node</span> <span class="synStatement">instanceof</span> MethodCall<span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synType">null</span>;
<span class="synSpecial">}</span>
<span class="synStatement">if</span> <span class="synSpecial">(</span><span class="synStatement">!$</span><span class="synIdentifier">this</span><span class="synType">-></span>isName<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span>, <span class="synConstant">'oldMethod'</span><span class="synSpecial">))</span> <span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synType">null</span>;
<span class="synSpecial">}</span>
<span class="synStatement">$</span><span class="synIdentifier">node</span><span class="synType">-></span><span class="synIdentifier">name</span> <span class="synStatement">=</span> <span class="synPreProc">new</span> Identifier<span class="synSpecial">(</span><span class="synConstant">'newMethod'</span><span class="synSpecial">)</span>;
<span class="synStatement">return</span> <span class="synStatement">$</span><span class="synIdentifier">node</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>PHPStanの<code>processNode()</code>が問題を検出してエラーメッセージを返すのに対し、Rectorの<code>refactor()</code>はノードを書き換えて返すことで、実際にコードを変更します。</p>
<h2 id="まとめ">まとめ</h2>
<p>PHPStanのカスタムルールは、標準のルールだけではカバーできない、プロジェクト固有の検査要件に対応できる強力な機能です。</p>
<h3 id="カスタムルールの有用性">カスタムルールの有用性</h3>
<p>本記事では、ASTの基本的な概念からカスタムルールの実践的な作成方法まで解説しました。</p>
<p>ASTの仕組みを理解すれば、変数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE%B5%AC%C2%A7">命名規則</a>チェックから複雑な依存関係の検証まで、プロジェクトの要件に合わせた多様なルールを実装できます。<code>Node</code>と<code>Scope</code>を組み合わせることで、ノードの構造だけでなく型情報やクラス情報を用いた高度な解析も可能です。</p>
<h3 id="実務での活用ポイント">実務での活用ポイント</h3>
<p>カスタムルールを実務で活用する際のポイントは以下の通りです。</p>
<p><strong>1. 段階的な導入</strong></p>
<p>最初からすべての要件を満たす必要はありません。まずは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE%B5%AC%C2%A7">命名規則</a>チェックなどの簡単なルールから始めて、徐々に拡張していくのが効果的です。</p>
<p><strong>2. チームでのルール共有</strong></p>
<p>カスタムルールはチーム全体で合意を取ることが重要です。ルールの意図や背景をドキュメント化し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%C5%AA">機械的</a>にチェックする部分と人間が判断する部分を明確に分けることで、レビュー時間を短縮しつつ品質を保てます。</p>
<p><strong>3. 継続的な改善</strong></p>
<p>プロジェクトの成長に合わせてルールも進化させていく必要があります。誤検知が多いルールは改善し、新たな課題が見つかれば新しいルールを追加することで、コードベースの品質を継続的に向上できます。</p>
<h3 id="発展的な活用">発展的な活用</h3>
<p>カスタムルールで学んだASTの知識は、Rectorなど他の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>ツールにも応用できます。静的解析とコード変換を組み合わせることで、「問題を検出する」だけでなく「問題を自動で修正する」という、一歩進んだコード品質管理が実現できます。</p>
<h3 id="最後に">最後に</h3>
<p>ぜひプロジェクトの要件に合わせたカスタムルールを作成し、コードベースの品質向上に役立ててください。</p>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://phpstan.org/">PHPStan - PHP Static Analysis Tool</a></li>
<li><a href="https://phpstan.org/developing-extensions/rules">PHPStan Custom Rules</a></li>
<li><a href="https://github.com/nikic/PHP-Parser">nikic/PHP-Parser</a> - <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>のASTパーサー</li>
<li><a href="https://github.com/rectorphp/rector">Rector - Instant Upgrades and Automated Refactoring</a></li>
</ul>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
colopl-tech
k6 + Cursor の負荷試験がとても手軽だった件
hatenablog://entry/6802888565300568708
2025-11-04T11:00:00+09:00
2025-11-04T11:00:00+09:00 こんにちは、バックエンドエンジニアの岡村です。 新サービス「FANPARK」をリリースするにあたり、負荷試験を実施しました。今回は負荷試験ツールとしてk6を採用し、その手軽さに驚いたので、この経験を共有させていただきます。特に、AIエージェント(Cursor)がほとんどの作業を代行してくれたことで、負荷試験の実施が非常にスムーズに進みました。
<p>こんにちは、バックエンドエンジニアの岡村です。</p>
<p>新サービス「FANPARK」をリリースするにあたり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>を実施しました。今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツールとして<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>を採用し、その手軽さに驚いたので、この経験を共有させていただきます。特に、AIエージェント(Cursor)がほとんどの作業を代行してくれたことで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>の実施が非常にスムーズに進みました。</p>
<h2 id="背景">背景</h2>
<p>「FANPARK」は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%CC%C3%D6%A5%B2%A1%BC">位置ゲー</a>のノウハウを活用してライブの待ち時間を遊び時間に変える新サービスです。10月に開催されたファッションと音楽が交差する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DA%A5%B7%A5%E3">スペシャ</a>ルイベント「ZOZOFES」に導入しました。会場であるKアリーナは2万人のキャパシティを有しており、来場が一番多くなるのは開場時間周辺ということで、それなりの負荷が予想される状況でした。</p>
<p>今回のサービスは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Web%A5%B5%A1%BC%A5%D3%A5%B9">Webサービス</a>として実装しており、フロントエンド側はNext.js、バックエンドはLaravelで構築されています。ゲームタイトルの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>を行う際は、弊社のゲーム基盤に最適化された内製ツール(Go言語)を使用していますが、今回は比較的シンプルな<a class="keyword" href="https://d.hatena.ne.jp/keyword/Web%A5%B5%A1%BC%A5%D3%A5%B9">Webサービス</a>であるため、より一般的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツールでの実施を検討しました。</p>
<h2 id="k6について簡単な説明"><a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>について簡単な説明</h2>
<p><a href="https://k6.io">k6</a>は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を記述できる軽量な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツールです。データ可視化ツールでお馴染みのGrafana Labsが開発とメンテナンスを行っています。従来の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツールと比較して、以下の特徴があります。</p>
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>で記述できるため、フロントエンド開発者にとって馴染みのある言語でテスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を書ける</li>
<li><a href="https://grafana.com/docs/k6/latest/set-up/install-k6/">単一のバイナリファイルで動作し、インストールが簡単</a></li>
<li><a href="https://grafana.com/docs/k6/latest/using-k6/protocols/">HTTPリクエスト、WebSocket、gRPCなど様々なプロトコルに対応した豊富な機能</a></li>
<li><a href="https://grafana.com/docs/k6/latest/using-k6/scenarios/">時間経過に応じて負荷を段階的に増減させる設定が可能</a></li>
<li><a href="https://grafana.com/docs/k6/latest/using-k6/metrics/">レスポンス時間、スループット、エラー率などの詳細な統計情報を提供</a></li>
</ul>
<h2 id="k6を選んだ理由"><a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>を選んだ理由</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツールの選定において、次の理由から<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>を選択しました。</p>
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>でシナリオが書けるため、フロントエンドのコードをある程度流用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>が可能</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Web%A5%B5%A1%BC%A5%D3%A5%B9">Webサービス</a>に特化した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツールとして適している</li>
<li>「FANPARK」が<a class="keyword" href="https://d.hatena.ne.jp/keyword/Web%A5%B5%A1%BC%A5%D3%A5%B9">Webサービス</a>として比較的シンプルだったことも後押し</li>
</ul>
<p>これらの理由から、<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>が今回の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>に最適なツールであると判断しました。</p>
<h2 id="k6の導入と設定"><a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>の導入と設定</h2>
<h3 id="インストール">インストール</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>のインストールは非常にシンプルでした。<a class="keyword" href="https://d.hatena.ne.jp/keyword/macOS">macOS</a> + Homebrew 環境では以下のコマンドだけでインストールできます</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>brew install k6
</pre>
<h3 id="テストスクリプトの作成">テスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>の作成</h3>
<p>まずはドキュメント通りに雛形を作成し、簡単なシナリオをCursorに書いてもらいました。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># scenario.jsという名前で雛形を作成</span>
k6 new scenario.js
</pre>
<p>実際に作成した<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>の例を以下に示します。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synPreProc">import</span> http <span class="synPreProc">from</span> <span class="synConstant">'k6/http'</span><span class="synStatement">;</span>
<span class="synPreProc">import</span> <span class="synSpecial">{</span> check<span class="synStatement">,</span> sleep <span class="synSpecial">}</span> <span class="synPreProc">from</span> <span class="synConstant">'k6'</span><span class="synStatement">;</span>
<span class="synPreProc">export</span> <span class="synType">let</span> options <span class="synStatement">=</span> <span class="synSpecial">{</span>
<span class="synIdentifier">vus</span><span class="synStatement">:</span> <span class="synConstant">1</span><span class="synStatement">,</span> <span class="synComment">// 1ユーザーで実行</span>
<span class="synIdentifier">duration</span><span class="synStatement">:</span> <span class="synConstant">'30s'</span><span class="synStatement">,</span> <span class="synComment">// 30秒間実行</span>
<span class="synSpecial">}</span><span class="synStatement">;</span>
<span class="synPreProc">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> <span class="synSpecial">()</span> <span class="synSpecial">{</span>
<span class="synComment">// ユーザー登録APIの呼び出し</span>
<span class="synType">let</span> response <span class="synStatement">=</span> http<span class="synStatement">.</span><span class="synIdentifier">post</span><span class="synSpecial">(</span><span class="synConstant">'https://api.example.com/user/register'</span><span class="synStatement">,</span> <span class="synSpecial">{</span>
<span class="synIdentifier">name</span><span class="synStatement">:</span> <span class="synConstant">'Test User'</span><span class="synStatement">,</span>
<span class="synIdentifier">email</span><span class="synStatement">:</span> <span class="synConstant">`test</span><span class="synSpecial">${</span><span class="synType">Math</span><span class="synStatement">.</span><span class="synIdentifier">random</span><span class="synSpecial">()}</span><span class="synConstant">@example.com`</span><span class="synStatement">,</span>
<span class="synSpecial">})</span><span class="synStatement">;</span>
<span class="synIdentifier">check</span><span class="synSpecial">(</span>response<span class="synStatement">,</span> <span class="synSpecial">{</span>
<span class="synConstant">'status is 200'</span><span class="synStatement">:</span> <span class="synSpecial">(</span>r<span class="synSpecial">)</span> <span class="synStatement">=></span> r<span class="synStatement">.</span>status <span class="synStatement">===</span> <span class="synConstant">200</span><span class="synStatement">,</span>
<span class="synConstant">'response time < 500ms'</span><span class="synStatement">:</span> <span class="synSpecial">(</span>r<span class="synSpecial">)</span> <span class="synStatement">=></span> r<span class="synStatement">.</span>timings<span class="synStatement">.</span>duration <span class="synStatement"><</span> <span class="synConstant">500</span><span class="synStatement">,</span>
<span class="synSpecial">})</span><span class="synStatement">;</span>
<span class="synIdentifier">sleep</span><span class="synSpecial">(</span><span class="synConstant">1</span><span class="synSpecial">)</span><span class="synStatement">;</span>
<span class="synSpecial">}</span>
</pre>
<h3 id="シナリオの作成">シナリオの作成</h3>
<p>雛形での動作確認が済んだ後は、下記の流れでシナリオを作成しました。</p>
<ol>
<li>フロントエンド側に実装していた通信基盤周りのコード流用し、Cursorに「<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>をしたいので書き換えて」と指示</li>
<li>バックエンド側にE2Eテストを実装していたため、Cursorに「テスト参考に<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>の形式でシナリオを書いて」と指示</li>
<li>その後、生成されたコードを見渡して細かい調整をする</li>
</ol>
<p>AIがサクサクと実装を進めてくれたおかげで、初めて触る<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>でもつまづくことがなく非常に助かりました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>のモジュール(通信部分などには<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>/httpなど)を使用してシナリオを書く必要がありますが、AIがいい感じに実装してくれました。比較的馴染みのある<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>の実装という点もあり、個人的には生成結果を読みやすかったのも良かったです。</p>
<h2 id="負荷試験の実施"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>の実施</h2>
<h3 id="負荷パターン">負荷パターン</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>は段階的に実施しました。</p>
<ol>
<li>初めは簡単なシナリオ、1ユーザーでのシナリオ、次に50人でのシナリオを試す</li>
<li>その後、50人の負荷を5分間、その後100人の負荷を5分間、最後に200人の負荷を5分間、というように段階的に負荷を上げていくシナリオを作成</li>
<li>段階的に負荷を本番想定にしていく</li>
</ol>
<h3 id="結果と発見">結果と発見</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>の結果、以下のようなことが分かりました。</p>
<ul>
<li>特別な環境を用意することなく、ローカルPCでも十分な内容の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>を実施できた</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>の結果をいい感じにまとめてくれる
<ul>
<li>そのままAIに投げれば要約してくれるので、認知負荷が低い</li>
<li>次にすべきこともAIが提案してくれる</li>
</ul>
</li>
<li>時系列のグラフも出力する設定もあって便利
<ul>
<li>Cloud RunやCloud <a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>のメトリクスを見れば間に合うので、今回は本格的には使用しなかった</li>
</ul>
</li>
</ul>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>では以下のような詳細な指標が自動的に出力されます。
これらの指標から、レスポンス時間の分布、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%EB%A1%BC%A5%D7%A5%C3%A5%C8">スループット</a>、エラー率などが一目で分かります。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># 負荷試験の実行</span>
k6 run scenario.js
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾<span class="synStatement">\</span>
/ \ | <span class="synPreProc">(</span><span class="synSpecial"> </span><span class="synStatement">|</span><span class="synSpecial"> </span><span class="synPreProc">(</span><span class="synSpecial">‾</span><span class="synPreProc">)</span><span class="synSpecial"> </span><span class="synStatement">|</span>
<span class="synSpecial"> / __________ \ </span><span class="synStatement">|</span><span class="synSpecial">_</span><span class="synStatement">|</span><span class="synSpecial">\_\ \_____/</span>
<span class="synSpecial"> execution: local</span>
<span class="synSpecial"> script: scenario.js</span>
<span class="synSpecial"> output: -</span>
<span class="synSpecial"> scenarios: </span><span class="synPreProc">(</span><span class="synConstant">100</span><span class="synSpecial">.</span><span class="synConstant">00</span><span class="synSpecial">%</span><span class="synPreProc">)</span><span class="synSpecial"> </span><span class="synConstant">1</span><span class="synSpecial"> scenario, </span><span class="synConstant">10</span><span class="synSpecial"> max VUs, 1m0s max duration </span><span class="synPreProc">(</span><span class="synSpecial">incl. graceful </span><span class="synStatement">stop</span><span class="synPreProc">)</span><span class="synSpecial">:</span>
<span class="synSpecial"> * default: </span><span class="synConstant">10</span><span class="synSpecial"> looping VUs </span><span class="synStatement">for</span> 30s <span class="synPreProc">(</span>gracefulStop: 30s<span class="synPreProc">)</span>
█ TOTAL RESULTS
HTTP
http_req_duration..............: <span class="synIdentifier">avg</span>=<span class="synConstant">171</span>.56ms <span class="synIdentifier">min</span>=<span class="synConstant">166</span>.75ms <span class="synIdentifier">med</span>=<span class="synConstant">171</span>.36ms <span class="synIdentifier">max</span>=<span class="synConstant">181</span>.49ms p<span class="synPreProc">(</span><span class="synConstant">90</span><span class="synPreProc">)</span><span class="synStatement">=</span><span class="synConstant">174</span>.05ms p<span class="synPreProc">(</span><span class="synConstant">95</span><span class="synPreProc">)</span><span class="synStatement">=</span><span class="synConstant">174</span>.91ms
<span class="synSpecial">{</span> expected_response:true <span class="synSpecial">}</span>...: <span class="synIdentifier">avg</span>=<span class="synConstant">171</span>.56ms <span class="synIdentifier">min</span>=<span class="synConstant">166</span>.75ms <span class="synIdentifier">med</span>=<span class="synConstant">171</span>.36ms <span class="synIdentifier">max</span>=<span class="synConstant">181</span>.49ms p<span class="synPreProc">(</span><span class="synConstant">90</span><span class="synPreProc">)</span><span class="synStatement">=</span><span class="synConstant">174</span>.05ms p<span class="synPreProc">(</span><span class="synConstant">95</span><span class="synPreProc">)</span><span class="synStatement">=</span><span class="synConstant">174</span>.91ms
http_req_failed................: <span class="synConstant">0</span>.<span class="synConstant">00</span>% <span class="synConstant">0</span> out of <span class="synConstant">250</span>
http_reqs......................: <span class="synConstant">250</span> <span class="synConstant">8</span>.<span class="synConstant">243623</span>/s
EXECUTION
iteration_duration.............: <span class="synIdentifier">avg</span>=<span class="synConstant">1</span>.21s <span class="synIdentifier">min</span>=<span class="synConstant">1</span>.16s <span class="synIdentifier">med</span>=<span class="synConstant">1</span>.17s <span class="synIdentifier">max</span>=<span class="synConstant">2</span>.11s p<span class="synPreProc">(</span><span class="synConstant">90</span><span class="synPreProc">)</span><span class="synStatement">=</span><span class="synConstant">1</span>.17s p<span class="synPreProc">(</span><span class="synConstant">95</span><span class="synPreProc">)</span><span class="synStatement">=</span><span class="synConstant">1</span>.18s
iterations.....................: <span class="synConstant">250</span> <span class="synConstant">8</span>.<span class="synConstant">243623</span>/s
vus............................: <span class="synConstant">10</span> <span class="synIdentifier">min</span>=<span class="synConstant">10</span> <span class="synIdentifier">max</span>=<span class="synConstant">10</span>
vus_max........................: <span class="synConstant">10</span> <span class="synIdentifier">min</span>=<span class="synConstant">10</span> <span class="synIdentifier">max</span>=<span class="synConstant">10</span>
NETWORK
data_received..................: <span class="synConstant">850</span> kB <span class="synConstant">28</span> kB/s
data_sent......................: <span class="synConstant">19</span> kB <span class="synConstant">609</span> B/s
running <span class="synPreProc">(</span>0m30.3s<span class="synPreProc">)</span>, <span class="synConstant">00</span>/<span class="synConstant">10</span> VUs, <span class="synConstant">250</span> <span class="synStatement">complete</span> and <span class="synConstant">0</span> interrupted iterations
default ✓ <span class="synStatement">[======================================]</span> <span class="synConstant">10</span> VUs 30s
</pre>
<h3 id="注意点">注意点</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>の実施において、以下の点に注意が必要でした。</p>
<ul>
<li>AIに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%EF%E7%C3%CD">閾値</a>設定を任せると、想定より緩い設定になる場合があるため注意が必要
<ul>
<li>例えば、latency 5s (めっちゃ遅い!) を許容する設定を提案されることがありました</li>
</ul>
</li>
<li>AIが生成したコードや試験結果の要約は、正しさを検証しつつ利用する必要
<ul>
<li>生成されたコードは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%E9%A5%C3%A5%AF%A5%DC%A5%C3%A5%AF%A5%B9">ブラックボックス</a>として扱うのでなく、動作原理をなるべく理解しましょう</li>
<li>試験結果は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>側のメトリクスを見るなどし、多角的に正しい負荷をかけられていることを確認しましょう</li>
</ul>
</li>
</ul>
<h2 id="AIエージェント駆動開発の効果">AIエージェント駆動開発の効果</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>とAIエージェント(Cursor)を組み合わせることで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>の実施が格段に効率化されました。</p>
<p>従来の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツール導入では、新しいツールの仕様や<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を理解する時間、ゼロからテスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を書く時間、エラー修正や動作確認の時間、そして出力された数値の解釈と改善点の特定など、多くの工程が必要でした。</p>
<p>今回のAIエージェント活用では、これらの工程が効率的に短縮されました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>の詳細な仕様を覚える必要がなく、AIが<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>の構文に準拠したコードを生成してくれます。既存のE2Eテストを参考に、数分で<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を生成でき、エラー発生時もAIが原因と解決策を提案してくれます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>結果の要約と改善提案も自動化され、開発効率が格段に向上しました。</p>
<p>ただし、 <strong>AIエージェントのことを100%信じるのは危険です。</strong> あくまで最短距離へ導くためのツールとして使うのにはとても良いツールですが、生成されたコードや提案された設定は必ず検証が必要です。AIエージェントは<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>のことを上手に生成できるようなので、そのような使い方に合っているように感じました。</p>
<h2 id="おわりに">おわりに</h2>
<p>今後の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Web%A5%B5%A1%BC%A5%D3%A5%B9">Webサービス</a>開発でも、要件に応じて<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>での<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>を積極的に検討していきたいと考えています。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>ツールとして非常に手軽で、特に<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>で書ける点とAIとの相性の良さが印象的でした。シュッと試験したい際にはとても有用なツールだと思います。</p>
<p>より複雑なサービスでの活用については今後検証していきたいと考えています。</p>
<p>今後も<a class="keyword" href="https://d.hatena.ne.jp/keyword/k6">k6</a>を活用して、より良いサービスを提供していきたいと思います。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
Tencent Global Digital Ecosystem Summit 2025 参加レポート
hatenablog://entry/6802888565297468689
2025-10-29T11:00:00+09:00
2025-10-29T11:46:48+09:00 こんにちは、エンジニアの岡村です。 2025年9月、Tencentからご招待いただき、Tencent Global Digital Ecosystem Summit 2025に参加してきました。このイベントは、Tencentの最新技術や製品、ソリューションが包括的に紹介される年次戦略イベントで、今年で6回目の開催を迎えました。 コロプラからは、CIOの菅井と私の2名で参加。私にとっては初海外・初出張という貴重な経験となりました。深圳という都市の魅力、華強北での熱烈なセールス体験、そしてAIコンパニオンの実装事例など、3日間で得られた濃密な体験についてご紹介します。 Tencent Global…
<p>こんにちは、エンジニアの岡村です。</p>
<p>2025年9月、Tencentからご招待いただき、<a href="https://www.tencentcloud.com/act/pro/2025gdes">Tencent Global Digital Ecosystem Summit 2025</a>に参加してきました。このイベントは、Tencentの最新技術や製品、ソリューションが包括的に紹介される年次戦略イベントで、今年で6回目の開催を迎えました。
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>からは、CIOの菅井と私の2名で参加。私にとっては初海外・初出張という貴重な経験となりました。深圳という都市の魅力、華強北での熱烈なセールス体験、そしてAIコンパニオンの実装事例など、3日間で得られた濃密な体験についてご紹介します。</p>
<h2 id="Tencent-Global-Digital-Ecosystem-Summit-とは">Tencent Global Digital Ecosystem Summit とは</h2>
<p>Tencent Global Digital Ecosystem Summitは、中国・深圳に本社を置くTencentが主催する年次戦略イベントです。AIや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9%A5%B3%A5%F3%A5%D4%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">クラウドコンピューティング</a>を中心とした最新技術の動向や、金融、ゲーム、製造、医療など様々な業界のデジタル変革について、世界中の企業や専門家が集まって議論する国際的なイベントです。Tencentは「Baidu, Alibaba, Tencent」の頭文字から成る「BAT」の一角を担う世界的なIT・ネットサービス企業で、WeChatやTencent Cloudなどのサービスで知られています。</p>
<p>会場である深圳(Shenzhen)は、中国の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%D0%BA%D1%C6%C3%B6%E8">経済特区</a>として発展し、現在では世界有数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%CE%A5%D9%A1%BC%A5%B7%A5%E7%A5%F3">イノベーション</a>都市として知られています。非常にキャッシュレス化が進んでおり、AI音声アシスタントやスマートシティシステムなど高度なデジタル技術が人々の生活に浸透しています。最新のテク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%CE%A5%ED">ノロ</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B8%A1%BC">ジー</a>が日常的に活用されている様子を目にでき、デジタル変革の最前線を体験できました。</p>
<p><img alt="Tencent Global Digital Ecosystem Summitの入口" width="340" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162414.jpg"/>
<img alt="サミットでの展示、ロボットのリモートコントロール" width="340" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162556.jpg"/></p>
<h2 id="1日目Tencent-Cloud-International-Gala-Dinner">1日目:Tencent Cloud International Gala Dinner</h2>
<p>イベント初日は、Tencent Cloud International Gala Dinnerからスタートしました。Tencent Cloud の導入を検討もしくは活用している国内の企業の方々との交流ができました。</p>
<p>中にはゲーム業界の方々もおり、「日本ゲーム業界で一致団結してグローバルに戦っていきたいよね」という熱い話を聞く機会がありました。
組織や業界全体など大きな視点での議論に参加する機会があり、自分の知識や経験の幅を広げる必要性を感じました。 異業種の方々との会話では、Tencent Cloudの導入事例など、具体的な質問をすることでさらに学びを深められる機会でした。</p>
<p>プログラムの中にはTencent Cloudの活用を特に推進した企業への表彰式がありました。数々の日本企業も表彰されており、日本でこれほどTencent Cloudが活用されているのは意外でした。導入時には丁寧なサポートがあるとお聞きしたので、それが要因の一つかもしれません。</p>
<p><img alt="招待された企業" width="340" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162536.jpg"/>
<img alt="Dinnerの様子" width="340" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162444.jpg"/></p>
<h2 id="2日目AIワークショップとメインイベント">2日目:AIワークショップとメインイベント</h2>
<p>2日目は、AIワークショップから始まりました。特に印象的だったのは、「Game for Peace (PUBGの中国版) 」というゲームにAIコンパニオンを実装した事例についてでした。</p>
<p>AIコンパニオンとは一緒に仲間として遊んでくれたり、アド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>をしてくれる存在のことです。マイクを使ったユーザーがより楽しんでゲームをプレイしている傾向がある中、使ったことのないユーザー向けにAIコンパニオンという選択肢を増やしたとのことでした。実際にリリース後、マイクの継続的な使用率が向上し、ユーザーからのポジティブな評判も多いようです。</p>
<p>AIコンパニオンの技術的な実装についてもお聞きしました。記憶領域をいくつか(期間毎など)に分けて管理することで、より効率的にコンテキストを扱える仕組みになっています。</p>
<p>AIコンパニオンと違和感のないコミュニケーションを実装する際には、ユーザーの入力の解釈、コンパニオンを操作するコマンドの理解、ゲームオブジェクトの理解、発話内容の決定、ゲームのフィールドに合うような音声の合成など、さまざまな工程が存在します。これらは単一の万能モデルではなく、音声処理、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>理解、環境認識、戦術判断といった各機能に特化した複数のモデルが連携することで実現しているとのことでした。</p>
<p>また、絶対に間違えてはいけない情報(イベントの報酬内容など)は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%E8%C4%EA%CF%C0">決定論</a>的なロジックに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%A9%A5%EF">フォワ</a>ードさせているというのも興味深い点でした。RAGの構築方法も重要で、キャラ設定だけで(中国語で)小説がかけるくらいの量があるらしいです!</p>
<h3 id="Tencent-Global-Digital-Ecosystem-Summit-で見てきたもの">Tencent Global Digital Ecosystem Summit で見てきたもの</h3>
<p>メインイベントでは、Tencent Cloudを用いた各種ソリューションの展示と公演が行われました。展示会場を2つ貸切して開催されており、カンファレンスも盛況でした(中国語での進行だったので理解はできませんでしたが、会場の熱気は十分に感じることができました)。</p>
<p>特に印象的だった展示の一つが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%AF%A5%C9">マクド</a>ナルドとの連携事例でした。カーナビにAIAgentを組み込んで音声でモバイルオーダー、決済、目的地設定ができるシステムで、実際にデモンストレーションを見ることができました。</p>
<p>また、Tencent RTCを用いたロボット操作の展示もありました。クレーンゲームを遠隔操作するデモで、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トが処理されて戻ってくるまでで合計300msという高速なレスポンス速度を実感できました。</p>
<p>車とドローンの展示も印象的で、中国の車はネットカフェのような状態になっていました。冷蔵庫、カラオケ、高級な皮が貼ってあるシートなど、もう安さでは売れないからラグジュアリーさで差別化を図っているという話を聞き、中国市場の変化を感じることができました。</p>
<p><img alt="マクドナルドのエージェント" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162634.png" width="400"/>
<img alt="空飛ぶモビリティ" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162525.jpg" width="280"/></p>
<h3 id="華強北Huaqiangbei見学">華強北(Huaqiangbei)見学</h3>
<p>メインイベントの合間には、深圳の華強北地区を訪れました。華強北は「中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%F1%C5%C5">国電</a>子第一街」として知られる世界最大規模の電子部品市場です。</p>
<p>あらゆる種類の電子部品やガジェット、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%D5%A5%A9%A5%F3">スマートフォン</a>部品、ドローン、AIチップなどが並んでいました。迅速かつ低コストな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%D7%A5%E9%A5%A4%A5%C1%A5%A7%A1%BC%A5%F3">サプライチェーン</a>が特徴で、試作品のア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>から完成品までをわずか数日で実現できるとのことです。</p>
<p>有名ブランドの模倣品(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B3%DC%CD">山寨</a>(サンザイ)と呼ぶらしい)もたくさん安く販売されていましたが、見た目や機能は一見ほぼ変わらないクオリティに見えました。また、値切り交渉が活発に行われる雑多な雰囲気で、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BD%A9%CD%D5%B8%B6">秋葉原</a>や中野のカオスな部分を濃縮したような場所でした。「このAIマウスがすごいから買って!」CIOの菅井が熱烈なセールスを受ける様子も見ることができ、現地の活気を肌で感じました。</p>
<p><img alt="華強北の電子部品市場の様子" width="340" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162434.jpg"/>
<img alt="熱烈なセールスを受ける様子" width="340" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162424.jpg"/></p>
<h2 id="3日目Tencent本社見学">3日目:Tencent本社見学</h2>
<p>最終日は、Tencentの本社を見学しました。一つの企業でこれほど巨大なビルを建てていることだけでも驚きでしたが、さらに「ペンギン島」というTencentの本社を中心に据えた人工島を建設予定だという話を聞いたときは、その規模感に畏怖の念を覚えました。</p>
<p>本社内では、教育、交通、通信、医療、決済、データセンター、建設、オートメーション、エンタメなど数々の事業領域への貢献を展示していました。中国国内のデジタル産業のほとんどに関わっているような勢いで、その影響力の大きさを実感しました。</p>
<p>Tencentの企業文化や技術開発の取り組みについて詳しく説明を受け、大規模な技術組織がどのように<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%CE%A5%D9%A1%BC%A5%B7%A5%E7%A5%F3">イノベーション</a>を生み出しているかを学びました。</p>
<p><img alt="展示スペースの様子 初代サーバー" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162625.jpg" width="200"/>
<img alt="展示スペースの様子 歴史" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162605.jpg" width="355"/>
<br/>
<img alt="展示スペースの様子 航空機の制御システム" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162454.jpg" width="340"/>
<img alt="展示スペースの様子 教育現場" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162505.jpg" width="340"/></p>
<h2 id="学びと今後の展望">学びと今後の展望</h2>
<p>今回の参加を通じて、多くの学びと気づきを得ることができました。</p>
<p>今回の参加で得た知見を、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>の各事業に活かしていきたいと考えています。特にAIコンパニオンの実装事例は、今後のゲーム開発において非常に参考になるものでした。単一のLLMのみを用いるのではなく、複数モデルを併用するアプローチや記憶領域の工夫など、実用的かつ合理的で、誰もが楽しめるAI実装のヒントを今後の開発に活かしていきたいと思います。</p>
<p>また、国内の技術動向はもちろん、グローバルな視点での技術戦略についても継続的に学習を深めていきたいと思います。今後もこのような国際的なイベントに積極的に参加し、世界の技術動向を把握することで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>の技術力向上に貢献していきたいと考えています。</p>
<h2 id="おわりに">おわりに</h2>
<p>Tencent Global Digital Ecosystem Summit 2025への参加は、私にとって非常に貴重な経験となりました。初海外・初出張という個人的な体験も含めて、多くの学びと気づきを得ることができました。</p>
<p>華強北での熱烈なセールス体験、AIコンパニオンの実装詳細、そして深圳の活気ある街並みなど、すべてが新鮮で刺激的な体験でした。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、最新のテク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%CE%A5%ED">ノロ</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B8%A1%BC">ジー</a>と独創的なア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>で新しい体験を追求し、人々の日常をより楽しく、より素晴らしくしていくことを目指しています。今回の経験で得た知見を活かし、今後もこの目標に向けて取り組んでいきたいと思います。</p>
<p>最後に、このような貴重な機会を提供していただいた関係者の皆様に心より感謝申し上げます。</p>
<p><img alt="深圳の街並みを眺める人形(景品で獲得)" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162615.jpg" width="200"/>
<img alt="なぜか日本語のガチャが置いてあった" src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20251024/20251024162515.jpg" width="355"/></p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
Claude Codeのプラグイン機能で社内向けプラグインマーケットプレイスを作った
hatenablog://entry/6802888565282323143
2025-10-17T11:00:00+09:00
2025-10-17T11:00:00+09:00 こんにちは、エンジニアの山田(@yamadashy)です。 Claude Codeに プラグイン機能が追加 されたので、実際に触ってみるために社内向けのプラグインマーケットプレイスを作ってみました。この記事では、プラグイン機能の概要と、実際に作ってみてわかったことを共有します。 Claude Codeのプラグイン機能とは 2025年10月10日、Claude Codeにプラグイン機能がリリースされました。 Today we’re introducing Claude Code Plugins in public beta.Plugins allow you to install and sha…
<p>こんにちは、エンジニアの山田(<a href="https://x.com/yamadashy">@yamadashy</a>)です。</p>
<p>Claude Codeに <a href="https://www.anthropic.com/news/claude-code-plugins">プラグイン機能が追加</a> されたので、実際に触ってみるために社内向けの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>を作ってみました。この記事では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機能の概要と、実際に作ってみてわかったことを共有します。</p>
<h2 id="Claude-Codeのプラグイン機能とは">Claude Codeの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機能とは</h2>
<p>2025年10月10日、Claude Codeに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機能がリリースされました。</p>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">Today we’re introducing Claude Code Plugins in public beta.<br><br>Plugins allow you to install and share curated collections of slash commands, agents, <a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a> servers, and hooks directly within Claude Code. <a href="https://t.co/pF50UJ5zOo">pic.twitter.com/pF50UJ5zOo</a></p>— Claude (@claudeai) <a href="https://twitter.com/claudeai/status/1976332881409737124?ref_src=twsrc%5Etfw">2025年10月9日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <cite class="hatena-citation"><a href="https://x.com/claudeai/status/1976332881409737124">x.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.anthropic.com%2Fnews%2Fclaude-code-plugins" title="Customize Claude Code with plugins" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.anthropic.com/news/claude-code-plugins">www.anthropic.com</a></cite></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機能を使うことで、Claude Codeのカスタマイズ設定を共有できるようになります。個々の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>には、以下のような要素を含めることができます。</p>
<ul>
<li>カスタムコマンド</li>
<li>エージェント</li>
<li>フック</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>サーバーの設定</li>
</ul>
<p>これらを「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>」という単位でまとめて配布する仕組みです。1つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>が1つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>になり、その中に複数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を含められます。</p>
<pre class="code" data-lang="" data-unlink>マーケットプレイス (GitHubリポジトリ)
├── プラグイン1 (MCP設定)
├── プラグイン2 (カスタムコマンド)
├── プラグイン3 (エージェント)
└── プラグイン4 (フック + コマンド)</pre>
<p>この仕組みの良いところは、配布者が好きな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を詰め合わせられる一方で、利用者は必要なものだけを選んでインストールできる点です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>の追加は一度だけで済み、その後は含まれる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>から自由に選択できます。</p>
<p>管理側と利用者の両方にとって使いやすい、Anthropicのセンスが光る設計だと感じています。</p>
<h2 id="社内向けマーケットプレイスを作った理由">社内向け<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>を作った理由</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機能がリリースされたので、「まずやってみる」という姿勢で社内向け<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>を作成しました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では社内サービスと連携する<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>サーバーを複数開発してきました。ただ、<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>サーバーを使うには各自が設定ファイルを編集する必要があり、少し手間がかかっていました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機能はこうした設定の配布と相性が良いのではないかと考え、まずは社内の<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>をそれぞれ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>にして詰め込んで社内公開してみました。</p>
<p>社内にはCursor, Claude Code, Copilot Agentなど様々なツールを使っているメンバーがいるため、実際に業務に活用していくのは難しいですが、効率化を図っていく上での選択肢としては検討していきたいなと思っています。</p>
<h2 id="作ったもの">作ったもの</h2>
<p>社内向け<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>として「ccplugins」という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を作成しました。名前はClaude Code関連ツールの <a href="https://github.com/ryoppippi/ccusage">ccusage</a> を参考に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE">命名</a>しています。</p>
<p>この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>では、社内サービスと連携する<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を複数提供しています。</p>
<p>利用者は、以下のように<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>を追加し、</p>
<pre class="code" data-lang="" data-unlink>/plugin marketplace add <組織名>/ccplugins</pre>
<p>必要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>をインストールします。これだけで、その<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>が使えるようになります。</p>
<pre class="code" data-lang="" data-unlink>/plugin install <プラグイン名>@<組織名></pre>
<p>今回は相性の良さそうな<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>を個別で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>にしていますが、フックやサブエージェント、コマンドも追加できるので、今後はより実用的なものを追加していく予定です。</p>
<h2 id="プラグインマーケットプレイスの作り方"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>の作り方</h2>
<p>実際に作成した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>の構成は、以下のようになっています。</p>
<pre class="code" data-lang="" data-unlink>ccplugins/
├── .claude-plugin/
│ └── marketplace.json
└── plugins/
├── mcp-plugin1/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ └── .mcp.json
├── mcp-plugin2/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ └── .mcp.json
└── mcp-plugin3/
├── .claude-plugin/
│ └── plugin.json
└── .mcp.json</pre>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>のルートには <code>marketplace.json</code> を配置し、利用可能な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>のリストを定義します。各<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>には <code>plugin.json</code> で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E1%A5%BF%A5%C7%A1%BC%A5%BF">メタデータ</a>を記述し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>サーバーの設定は <code>.mcp.json</code> で行います。</p>
<p>詳しい作り方は、<a href="https://docs.claude.com/en/docs/claude-code/plugins">公式ドキュメント</a>を参照してください。</p>
<h2 id="実際に使ってみて">実際に使ってみて</h2>
<p>手元で使っている分には、<code>/plugin</code> コマンドで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A5%E9%A5%AF%A5%C6%A5%A3%A5%D6">インタラクティブ</a>に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を操作できるところは体験が良いと感じています。ただ、少しまだ操作部分で不安定な挙動もあります。</p>
<p>社内での展開はこれからですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機能は<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>サーバーの設定だけでなく、カスタムコマンドやエージェント、フックなども追加できます。今後は、より実用的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を追加していきたいですね。</p>
<h2 id="これから">これから</h2>
<p>これまではClaude Codeのカスタマイズ機能を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>単位での共有しかできませんでしたが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>によって、もっと柔軟な組織の単位で共有できるようになりました。</p>
<p>今回作った<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>は社内の共通<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>置き場という位置づけですが、今後は各チームやプロジェクトごとに独自の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を作る流れになっていくと良いなと考えています。あらゆるセクションで便利な活用や共有がされていくことを期待しています。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
CursorなどのAIエージェントのルールをシンボリックリンクで共通化
hatenablog://entry/6802888565277555313
2025-10-15T11:00:00+09:00
2025-10-15T11:00:03+09:00 こんにちは、エンジニアの山田 (@yamadashy) です。 皆さんのプロジェクトでは、AIコーディングツールのルール、コマンドをどのように管理していますか? 現在、私たちの周りには Cursor、Claude Code、Codex CLI など、多種多様なAIツールが存在します。コロプラでも、以前こちらの記事でCursorの導入事例を紹介しましたが、今ではエンジニアがそれぞれの好みに合わせてツールを自由に選択する文化が根付いています。 しかし、この自由なツール選択の裏側で、私たちは新たな課題に直面していました。それは、「プロジェクトのルールを各ツールの設定ファイルへ個別に反映させる負担」で…
<p>こんにちは、エンジニアの山田 (<a href="https://x.com/yamadashy">@yamadashy</a>) です。</p>
<p>皆さんのプロジェクトでは、AIコーディングツールのルール、コマンドをどのように管理していますか?</p>
<p>現在、私たちの周りには Cursor、Claude Code、Codex <a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a> など、多種多様なAIツールが存在します。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>でも、以前<a href="https://blog.colopl.dev/entry/cursor-engineer-adoption-2025">こちらの記事</a>でCursorの導入事例を紹介しましたが、今ではエンジニアがそれぞれの好みに合わせてツールを自由に選択する文化が根付いています。</p>
<p>しかし、この自由なツール選択の裏側で、私たちは新たな課題に直面していました。それは、「プロジェクトのルールを各ツールの設定ファイルへ個別に反映させる負担」です。</p>
<p>AIコーディングツール過渡期の今、社内でのツールの統一は現実的ではなく、かといって設定同期のために特別なツールを導入するのも避けたい。そんな状況で、私たちが一つの解決策として見出したのが、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%DC%A5%EA%A5%C3%A5%AF%A5%EA%A5%F3%A5%AF">シンボリックリンク</a>」を活用したシンプルなルール共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>の手法でした。</p>
<p>本記事では、特別なツールやコマンドを必要とせず、多様なAIツール間でプロジェクトルールを効率的に共有するための具体的な方法をご紹介します。</p>
<h2 id="基本的な考え方">基本的な考え方</h2>
<p>この手法の基本的な考え方は非常にシンプルです。</p>
<p>まず、プロジェクトの共通ルールを記述したファイル(例: <code>base.md</code>)を <code>.agents/</code> のような特定の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リに一元的に配置します。そして、各AIツールが参照する設定ファイルのパスには、この <code>base.md</code> への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%DC%A5%EA%A5%C3%A5%AF%A5%EA%A5%F3%A5%AF">シンボリックリンク</a>を作成します。</p>
<p>表題通り、これだけです。
これで動くのか?と思う方もいらっしゃるかと思いますが、Cursor、Codex <a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a>、Claude Code、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot Agent Mode で動作を確認しています。
<code>.claude/commands</code> などのコマンドも同様の方法で共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>できます。</p>
<p>この方法により、開発者はツールの違いを意識することなく、常に最新のプロジェクトルールをAIに読み込ませることができます。</p>
<p>ちなみに、共通<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ名を <code>.agents</code> としているのは、AIエージェントの標準化を目指すコミュニティの動きである <a href="https://agents.md/">AGENTS.md</a> を参考にしています。</p>
<h2 id="具体的な設定方法">具体的な設定方法</h2>
<p>具体的には、以下の2つのステップで設定します。</p>
<h3 id="1-共通ルールファイルの作成">1. 共通ルールファイルの作成</h3>
<p>はじめに、プロジェクトのルート<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リに <code>.agents/rules/</code> のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを作成し、そこに共通ルールを記述した <code>base.md</code> を配置します。</p>
<p>このファイルに、CLAUDE.md などで書いたベースのルールを記述します。</p>
<p>Cursorルールの場合、ファイルの先頭に <code>alwaysApply: true</code> のようなFront matterの記述があるので、それも一緒に記述します。これが他のツール(Claude Codeなど)の動作に影響を与えないことも確認済みです。このように、ツール間の差異を吸収しつつ、共通の情報を一元管理できるのがこの手法の強みです。</p>
<h3 id="2-シンボリックリンクの作成">2. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%DC%A5%EA%A5%C3%A5%AF%A5%EA%A5%F3%A5%AF">シンボリックリンク</a>の作成</h3>
<p>次に、各ツールが参照する設定ファイルの場所へ、先ほど作成した <code>base.md</code> への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%DC%A5%EA%A5%C3%A5%AF%A5%EA%A5%F3%A5%AF">シンボリックリンク</a>を作成します。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># for Cursor</span>
ln <span class="synSpecial">-s</span> ../../.agents/rules/base.md .cursor/rules/base.mdc
<span class="synComment"># for Claude Code</span>
ln <span class="synSpecial">-s</span> .agents/rules/base.md CLAUDE.md
<span class="synComment"># for Codex CLI</span>
ln <span class="synSpecial">-s</span> .agents/rules/base.md AGENTS.md
<span class="synComment"># for GitHub Copilot Agent Mode</span>
ln <span class="synSpecial">-s</span> ../.agents/rules/base.md .github/copilot-instructions.md
</pre>
<p>これで、各ツールは同じ <code>base.md</code> ファイルをルールとして読み込むようになります。ルールの更新は <code>base.md</code> を編集するだけで、すべてのツールに即座に反映されます。</p>
<p>また、コマンド機能についても、同様の方法で共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>が可能です。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># 共通のコマンドを配置するディレクトリ</span>
<span class="synStatement">mkdir</span> <span class="synSpecial">-p</span> .agents/commands
<span class="synComment"># for Cursor</span>
ln <span class="synSpecial">-s</span> ../.agents/commands .cursor/commands
<span class="synComment"># for Claude Code</span>
ln <span class="synSpecial">-s</span> ../.agents/commands .claude/commands
<span class="synComment"># Codex CLIなどは現状で非対応</span>
</pre>
<p>Codex <a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a> では、現時点でプロジェクト単位のコマンド共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CD%AD%B5%A1">有機</a>能がサポートされていませんが、対応されれば同様の手法で共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>が可能でしょう。</p>
<h2 id="社内への展開">社内への展開</h2>
<p>この仕組みは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>社内の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>プロジェクト雛形である <code>laravel-skeleton</code> に標準で導入されており、誰でもすぐに試せる状態で共有されています。</p>
<p>私自身が関わるプロジェクトでもこの仕組みを運用しており、Claude CodeとCursorを状況に応じて使い分けていたりしますが、ルールのメンテナンスコストが削減され開発者体験の向上にも繋がっていると感じます。</p>
<p>もちろん課題もあり、Cursorのようにルールを複数作成できるようなケースや、ルールからコマンドを呼ぶなど、ツールごとの差異を吸収することは難しいです。</p>
<p>とはいえ、プロジェクトの基本的なルールや構造といった情報は、ツール間で共通している部分がほとんどで、一つの落とし所としてはこの手法が現実的で効果的な解決策だと考えています。</p>
<h2 id="まとめ">まとめ</h2>
<p>ツールの進化が続く現代の開発現場において、プロジェクトの一貫性をいかに保つかは重要な課題です。もし皆さんのチームでも同様の課題に直面しているのであれば、今回紹介した方法が、シンプルながら解決の一助となれば幸いです。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
神魔狩りのツクヨミ勉強会を実施しました
hatenablog://entry/6802888565226715059
2025-09-17T11:00:00+09:00
2025-09-17T14:03:46+09:00 こんにちは!バックエンドエンジニアのR.Oです。 コロプラでは、新作タイトルリリース後などに新しい技術的な挑戦の紹介や知見の共有を勉強会を通じて行っております。 今回は9月上旬に神魔狩りのツクヨミ社内勉強会が実施されましたので、内容の一部をご紹介いたします。 主にゲームを構成するインフラ環境、ゲーム内でのAIの活用方法、少人数ならではの開発など技術的な取り組みを中心に幅広く紹介していただきました。 神魔狩りのツクヨミとは 本作は2025年5月7日(水)にサービス開始されたローグライクカードゲームで、金子一馬の独自の世界観が描かれています。 特徴的な部分はゲームに画像生成AIが使われているところ…
<p>こんにちは!バックエンドエンジニアのR.Oです。<br/>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、新作タイト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EB%A5%EA%A5%EA">ルリリ</a>ース後などに新しい技術的な挑戦の紹介や知見の共有を勉強会を通じて行っております。<br/>
今回は9月上旬に神魔狩りの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C4%A5%AF%A5%E8%A5%DF">ツクヨミ</a>社内勉強会が実施されましたので、内容の一部をご紹介いたします。<br/>
主にゲームを構成するインフラ環境、ゲーム内でのAIの活用方法、少人数ならではの開発など技術的な取り組みを中心に幅広く紹介していただきました。</p>
<h2 id="神魔狩りのツクヨミとは">神魔狩りの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C4%A5%AF%A5%E8%A5%DF">ツクヨミ</a>とは</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250911/20250911132233.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>本作は2025年5月7日(水)にサービス開始された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%B0%A5%E9%A5%A4%A5%AF">ローグライク</a>カードゲームで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B6%E2%BB%D2%B0%EC%C7%CF">金子一馬</a>の独自の世界観が描かれています。<br/>
特徴的な部分はゲームに画像生成AIが使われているところで、プレイヤーの行動ログを元に自分だけのカードが創り出されます。<br/>
プレイしたことのない方はぜひこの機会にやってみてください!<br/>
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fjintsuku.jp%2F" title="神魔狩りのツクヨミ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://jintsuku.jp/">jintsuku.jp</a></cite></p>
<h2 id="発表内容">発表内容</h2>
<h3 id="1-Cloud-Run環境の構築">1. Cloud Run環境の構築</h3>
<ul>
<li>GKEに代わりCloud Runを採用した背景には、インフラの柔軟性とコスト効率の最適化という目的がありました。アクセス状況に応じてリソースを動的に調整できる従量課金モデルが、本プロジェクトの要件に適していると判断しました。</li>
<li>また、Cloud Runの特徴や変更点のメリットなども紹介していただきました。</li>
<li>最後に今後の展望として、タイトルの規模によってGKE/CloudRunを使い分けることに加え、例えば開発環境のみCloud Runを利用するといった、より柔軟なインフラ構成についても検討を進めていることが語られました。</li>
</ul>
<h3 id="2-AIのゲーム内活用">2. AIのゲーム内活用</h3>
<ul>
<li>本ゲーム内では、画像生成AI、テキスト生成AIが使われていると紹介されました。</li>
<li>画像生成AIの仕組みや開発中の苦難が語られました。</li>
<li>テキスト生成AIの使用しているタイミングはカード創成時で、道中の行動結果の要約を生成AIに渡していることが明かされました。</li>
<li>また、テキスト生成AIを利用する処理はキューに逃がして通信時の負担を減らしていることが紹介されました。</li>
</ul>
<h3 id="3-多言語対応">3. 多言語対応</h3>
<ul>
<li>LQAではWOVN.gamesを活用し、テキストの翻訳/調整をしたことが紹介されました。
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/CEDEC">CEDEC</a>の発表もあるので興味があれば見てみてください。</li>
</ul>
</li>
</ul>
<p><iframe width="560" height="315" src="https://www.youtube.com/embed/0tPBJntfSfg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="【CEDEC2025】AIローカライズ:コロプラ『神魔狩りのツクヨミ』の多言語化を支えたWOVN.games"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=0tPBJntfSfg">www.youtube.com</a></cite></p>
<ul>
<li>グローバ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EB%A5%EA%A5%EA">ルリリ</a>ースに向けて、開発者が翻訳を意識せずに開発できる仕組みを構築したことが紹介されました。マスタデータやエラーメッセージなどを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%D7%A5%EC%A5%C3%A5%C9%A5%B7%A1%BC%A5%C8">スプレッドシート</a>で一元管理することで、翻訳プロセスを開発フローから分離し、迅速な多言語対応を実現したとのことです。</li>
</ul>
<h3 id="4-短期間--少人数でのリリース">4. 短期間 ∩ 少人数でのリリース</h3>
<ul>
<li>少人数で開発を行っていたため、圧倒的な速度で開発を進めることできたそうです。その中でも品質を保つために、テストの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%D0%A5%EC%A5%C3%A5%B8">カバレッジ</a>率は高めに設定したという話がありました。</li>
<li>社内で培われた既存の技術資産を有効活用することで、開発効率を最大化したという話もありました。</li>
<li>最後に、クライアントデータや定数を工夫して効率的に管理したという話をしていただきました。</li>
</ul>
<h2 id="告知">告知</h2>
<p>11月21日(金)に開催される<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>ーカンファレンスで「生成AIが紡ぐ新体験『神魔狩りの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C4%A5%AF%A5%E8%A5%DF">ツクヨミ</a>』の裏側」というタイトルで発表があります。詳細はこちらでお話いたしますのでぜひご参加ください。<br/>
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Farchitecture-con.findy-tools.io%2F2025%3Fm%3D2025%2Fsession%2Fmdl%2FDpa1nUoA" title="最適なアーキテクチャをどう描くか。|アーキテクチャConference 2025 " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://architecture-con.findy-tools.io/2025?m=2025/session/mdl/Dpa1nUoA">architecture-con.findy-tools.io</a></cite></p>
<h2 id="終わりに">終わりに</h2>
<p>今回の社内勉強会では、『神魔狩りの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C4%A5%AF%A5%E8%A5%DF">ツクヨミ</a>』における数々の技術的な挑戦が共有されました。<br/>
特に、Cloud Runの採用や生成AIの活用といった新しい取り組みから得られた知見は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>全体の技術力向上に繋がり、今後のタイトル開発にも大いに活かされていくと思います。<br/>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では定期的に社内勉強会を開催していますので、次回の記事もお楽しみにお待ち下さい。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
超簡単!社内ツール向け Homebrew + GitHub 活用術
hatenablog://entry/6802888565214303903
2025-09-12T11:00:00+09:00
2025-09-12T11:00:01+09:00 こんにちは、エンジニアの工藤 @zeriyoshi です。 まえがき: 休職と PHP Conference Japan 2025 について 5月頃からフィジカル面で体調が悪く休職・入院しておりましたが、病気の特定と対処のための服薬治療によって症状が改善し、8月より復職させていただきました。 採択いただいていた PHP Conference Japan 2025 での 登壇 を辞退することになってしまい、大変ご迷惑をおかけいたしました。スピーカーとしてだけではなく、イベント参加者や運営スタッフなどとして引き続きカンファレンスなどのイベントに積極的に参加し協力していきたいと思っておりますので、今…
<p>こんにちは、エンジニアの工藤 <a href="https://x.com/zeriyoshi">@zeriyoshi</a> です。</p>
<h2 id="まえがき-休職と-PHP-Conference-Japan-2025-について">まえがき: 休職と <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan 2025 について</h2>
<p>5月頃からフィジカル面で体調が悪く休職・入院しておりましたが、病気の特定と対処のための服薬治療によって症状が改善し、8月より復職させていただきました。</p>
<p>採択いただいていた <a href="https://phpcon.php.gr.jp/2025/">PHP Conference Japan 2025</a> での <a href="https://fortee.jp/phpcon-2025/proposal/cc892e7a-9a42-4b3d-b1e0-9bab76ed59af">登壇</a> を辞退することになってしまい、大変ご迷惑をおかけいたしました。スピーカーとしてだけではなく、イベント参加者や運営スタッフなどとして引き続きカンファレンスなどのイベントに積極的に参加し協力していきたいと思っておりますので、今後とも何卒よろしくお願いいたします。</p>
<h2 id="Homebrew">Homebrew</h2>
<p>みなさん、 <a href="https://brew.sh">Homebrew</a> は使っていますか?仕事で <a class="keyword" href="https://d.hatena.ne.jp/keyword/macOS">macOS</a> を使っている人であればほとんどが使っているのではないかと思われます。</p>
<p>Homebrew はコマンドなど実行ファイルを配布するのにとても便利な手段で、例えば私は <code>zsh</code> よりも <code>bash</code> 派なのもあり Homebrew で <code>bash</code> をインストールしてデフォルトシェルに指定したり、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> を操作する <code>gh</code> コマンドなどをインストールして使っています。更新も <code>brew upgrade</code> だけで完了するので、とても便利です。</p>
<p>しかし、社内でツールを配布するとなるとどうでしょうか。仕事のオペレーションに最適化された内製ツールを配布したいとなっても Homebrew ほどシンプルに行える方法はなかなか存在せず、ある人にとっては便利であっても別の人は不要なものもあるわけで、自分が必要なものだけを選んでいれることができ、アップデートも利用者が好きなタイミングで行え、かつ自動化できる Homebrew は開発者体験を効果的に向上してくれます。</p>
<h2 id="Homebrew-tap-によるリポジトリ追加">Homebrew tap による<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>追加</h2>
<p>Homebrew には <code>tap</code> と呼ばれる外部 git <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a> (Formula と呼ばれます) を追加する機能があります。例えば <a href="https://developer.hashicorp.com/terraform">Terraform</a> で有名な <a href="https://www.hashicorp.com/">HashiCorp</a> も <a href="https://github.com/hashicorp/homebrew-tap">自前のリポジトリ</a> を公開しており、 <code>brew tap</code> で追加することで最新の Terraform を導入・更新できるようになります。</p>
<h2 id="Internal-Private-リポジトリでの-Homebrew-tap">Internal, Private <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>での Homebrew tap</h2>
<p>Homebrew は internal (組織所属者のみ) や private (権限付与者のみ) の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の追加にも対応しており、適切に <a class="keyword" href="https://d.hatena.ne.jp/keyword/SSH">SSH</a> アクセスができれば利用できるようになっています。</p>
<p>しかし、自前で Homebrew の Formula <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を維持管理するのは思っているよりも大変です。具体的には</p>
<ol>
<li>ツールごとの定義ファイルを <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/DSL">DSL</a> で記述する必要があること</li>
<li>ツールのバージョンアップの度に <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/DSL">DSL</a> の定義も変更する必要があること</li>
<li>Assets にあるバイナリが internal, private な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>だとそのままではダウンロードできず、インストールできないこと</li>
</ol>
<p>などが挙げられます。特にバイナリのダウンロードができないのが致命的です。</p>
<p>考えられる対応策として、以下のようなものが挙げられます。</p>
<h3 id="Formula-リポジトリ自体にツールのバイナリをそのまま配置する">Formula <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>自体にツールのバイナリをそのまま配置する</h3>
<h4 id="Pros">Pros</h4>
<ul>
<li>単一の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に全てが完結して揃った状態にできる</li>
</ul>
<h4 id="Cons">Cons</h4>
<ul>
<li>git <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>が肥大化し、管理が煩雑になる</li>
<li>ツール側から Formula <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を操作しなければならず、権限管理が煩雑
<ul>
<li>最もセキュアな形を取るのなら Formula <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を手動で更新するしかない
<ul>
<li>手間がかかるため使いづらい可能性</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="IP-アドレス等で接続元制限をかけた-Amazon-S3-などにバイナリを配置しておく">IP アドレス等で接続元制限をかけた <a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon%20S3">Amazon S3</a> などにバイナリを配置しておく</h3>
<h4 id="Pros-1">Pros</h4>
<ul>
<li>比較的シンプルな構造でわかりやすい</li>
</ul>
<h4 id="Cons-1">Cons</h4>
<ul>
<li>IP アドレスによる制限はモダンでなく管理が煩雑
<ul>
<li>リモートワーク時には <a class="keyword" href="https://d.hatena.ne.jp/keyword/VPN">VPN</a> への接続が必須になり手間</li>
</ul>
</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions でビルドした結果を毎回 S3 にアップロードする必要が生じる
<ul>
<li>アップロード時の鍵などの共有に課題</li>
<li>アップロードしたあと Formula の url の向き先をアップロード後のものに更新しなければならない
<ul>
<li>手動ですると手間がかかる</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="Homebrew-Download-Strategy">Homebrew Download Strategy</h2>
<p>Homebrew は様々な機能拡張が行えるようになっており、例えばバイナリファイルをダウンロードしたり SHA-256 の検証をしたりするなどの部分をベースクラスを継承したクラスを作成することでカスタマイズできるようになっています。</p>
<p>通常、バイナリのダウンロードは <code>CurlDownloadStrategy</code> を使って行われます。そこで今回はこのクラスを継承し、以下の要件を満たすクラスを独自で実装しました。</p>
<ul>
<li><code>org/repository</code> を指定することで必須のフィールドを自動で設定してくれる</li>
<li>対象の <code>org/repository</code> の Release 一覧を取得し、 <code>gh</code> コマンドを用いて draft ではない最新のリリース (SemVer 準拠) を取得する</li>
<li>Release の Assets にある実行バイナリを <code>gh</code> コマンドを用いてダウンロードする</li>
</ul>
<p>Homebrew は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>がしっかりしており、これらを自身でも読み解きつつ、 Claude Code のアシストも得ながら <code>GitHubGHInternalReleaseDownloadStrategy</code> を作成しました。</p>
<p><details>
<summary> ソース: <code>lib/github_gh_internal_release_download_strategy.rb</code> </summary></p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">json</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">digest</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">tmpdir</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">open3</span><span class="synSpecial">'</span>
<span class="synPreProc">class</span> <span class="synType">GitHubGHInternalReleaseDownloadStrategy</span> < <span class="synType">CurlDownloadStrategy</span>
<span class="synType">URL_PATTERN</span> = <span class="synSpecial">%r{</span><span class="synConstant">https://github</span><span class="synSpecial">.</span><span class="synConstant">com/</span><span class="synSpecial">([^</span><span class="synConstant">/</span><span class="synSpecial">]+)</span><span class="synConstant">/</span><span class="synSpecial">([^</span><span class="synConstant">/</span><span class="synSpecial">]+)</span><span class="synConstant">/releases/download/</span><span class="synSpecial">([^</span><span class="synConstant">/</span><span class="synSpecial">]+)</span><span class="synConstant">/</span><span class="synSpecial">(\S+)}</span>.freeze
<span class="synType">GH_EXECUTABLE_PATHS</span> = [
<span class="synSpecial">"</span><span class="synConstant">/opt/homebrew/bin/gh</span><span class="synSpecial">"</span>,
<span class="synSpecial">"</span><span class="synConstant">/usr/local/bin/gh</span><span class="synSpecial">"</span>,
<span class="synSpecial">"</span><span class="synConstant">/home/linuxbrew/.linuxbrew/bin/gh</span><span class="synSpecial">"</span>
].freeze
<span class="synType">CHECKSUM_FILE_PATTERNS</span> = <span class="synSpecial">%w[</span><span class="synConstant">checksums.txt SHA256SUMS sha256sums.txt checksums.sha256</span><span class="synSpecial">]</span>.freeze
<span class="synType">RELEASE_LIMIT</span> = <span class="synConstant">50</span>
<span class="synType">LATEST_TAG</span> = <span class="synSpecial">"</span><span class="synConstant">latest</span><span class="synSpecial">"</span>
<span class="synType">VERSION_PREFIX</span> = <span class="synSpecial">"</span><span class="synConstant">v</span><span class="synSpecial">"</span>
<span class="synType">SHA256_PATTERN</span> = <span class="synSpecial">/^</span><span class="synConstant">SHA256</span><span class="synSpecial">\s*\((.+?)\)\s*</span><span class="synConstant">=</span><span class="synSpecial">\s*([</span><span class="synConstant">a-f0-9</span><span class="synSpecial">]{64})$/i</span>.freeze
<span class="synType">SHA256_ALTERNATE_PATTERN</span> = <span class="synSpecial">/^([</span><span class="synConstant">a-f0-9</span><span class="synSpecial">]{64})\s+\*?(.+)$/i</span>.freeze
<span class="synPreProc">attr_reader</span> <span class="synConstant">:latest_release_info</span>
<span class="synPreProc">def</span> <span class="synIdentifier">initialize</span>(url, name, version, **meta)
<span class="synIdentifier">@meta</span> = meta
<span class="synIdentifier">@auto_verify_sha256</span> = meta.fetch(<span class="synConstant">:auto_verify_sha256</span>, <span class="synConstant">true</span>)
<span class="synIdentifier">@original_url</span> = url
parse_github_url(url)
setup_github_cli
<span class="synStatement">if</span> <span class="synIdentifier">@tag</span> == <span class="synType">LATEST_TAG</span>
<span class="synIdentifier">@latest_release_info</span> = fetch_latest_stable_release
<span class="synIdentifier">@tag</span> = <span class="synIdentifier">@latest_release_info</span>[<span class="synConstant">:tag</span>]
url = build_download_url(<span class="synIdentifier">@tag</span>)
<span class="synStatement">end</span>
<span class="synStatement">super</span>(url, name, version, **meta)
parse_url_components
<span class="synStatement">if</span> <span class="synIdentifier">@latest_release_info</span> && (version.nil? || version.to_s.empty?)
<span class="synIdentifier">@version</span> = <span class="synType">Version</span>.new(<span class="synIdentifier">@latest_release_info</span>[<span class="synConstant">:version</span>])
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">class</span> << <span class="synConstant">self</span>
<span class="synPreProc">def</span> <span class="synIdentifier">get_latest_release_info</span>(repo_url, asset_pattern = <span class="synConstant">nil</span>)
strategy = new(repo_url, <span class="synConstant">nil</span>, <span class="synConstant">nil</span>, <span class="synConstant">use_latest_release</span>: <span class="synConstant">true</span>, <span class="synConstant">skip_validation</span>: <span class="synConstant">true</span>)
strategy.get_latest_release_info(asset_pattern)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">get_latest_version_for_formula</span>(repo)
gh_command = find_gh_executable
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> gh_command
releases = fetch_releases(gh_command, repo)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> releases
extract_latest_version(releases)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">get_sha256_for_binary_set</span>(repo, binary_names = {})
gh_command = find_gh_executable
<span class="synStatement">return</span> {} <span class="synStatement">unless</span> gh_command
releases = fetch_releases(gh_command, repo)
<span class="synStatement">return</span> {} <span class="synStatement">unless</span> releases
tag = extract_latest_tag(releases)
<span class="synStatement">return</span> {} <span class="synStatement">unless</span> tag
checksums = collect_checksums(repo, tag, binary_names, gh_command)
checksums
<span class="synPreProc">end</span>
<span class="synPreProc">private</span>
<span class="synPreProc">def</span> <span class="synIdentifier">find_gh_executable</span>
<span class="synType">GH_EXECUTABLE_PATHS</span>.each <span class="synStatement">do</span> |path|
<span class="synStatement">return</span> path <span class="synStatement">if</span> executable_exists?(path)
<span class="synStatement">end</span>
system(<span class="synSpecial">"</span><span class="synConstant">gh version > /dev/null 2>&1</span><span class="synSpecial">"</span>) ? <span class="synSpecial">"</span><span class="synConstant">gh</span><span class="synSpecial">"</span> : <span class="synConstant">nil</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">executable_exists?</span>(path)
<span class="synType">File</span>.exist?(path) && <span class="synType">File</span>.executable?(path)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">fetch_releases</span>(gh_command, repo)
command = build_release_list_command(gh_command, repo)
stdout, stderr, status = <span class="synType">Open3</span>.capture3(*command)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> status.success?
<span class="synType">JSON</span>.parse(stdout)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">build_release_list_command</span>(gh_command, repo)
[gh_command, <span class="synSpecial">"</span><span class="synConstant">release</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">list</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">--repo</span><span class="synSpecial">"</span>, repo, <span class="synSpecial">"</span><span class="synConstant">--limit</span><span class="synSpecial">"</span>, <span class="synType">RELEASE_LIMIT</span>.to_s,
<span class="synSpecial">"</span><span class="synConstant">--json</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">tagName,publishedAt,isPrerelease</span><span class="synSpecial">"</span>]
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">extract_latest_version</span>(releases)
release = find_latest_stable_release(releases)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> release
release[<span class="synSpecial">"</span><span class="synConstant">tagName</span><span class="synSpecial">"</span>].gsub(<span class="synSpecial">/^#{</span><span class="synType">VERSION_PREFIX</span><span class="synSpecial">}/</span>, <span class="synSpecial">""</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">extract_latest_tag</span>(releases)
release = find_latest_stable_release(releases)
release&.dig(<span class="synSpecial">"</span><span class="synConstant">tagName</span><span class="synSpecial">"</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">find_latest_stable_release</span>(releases)
stable_releases = releases.reject { |r| r[<span class="synSpecial">"</span><span class="synConstant">isPrerelease</span><span class="synSpecial">"</span>] }
stable_releases.max_by { |r| parse_version(r[<span class="synSpecial">"</span><span class="synConstant">tagName</span><span class="synSpecial">"</span>]) }
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">parse_version</span>(tag)
cleaned_tag = tag.gsub(<span class="synSpecial">/^#{</span><span class="synType">VERSION_PREFIX</span><span class="synSpecial">}/</span>, <span class="synSpecial">""</span>)
<span class="synType">Gem</span>::<span class="synType">Version</span>.new(cleaned_tag)
<span class="synPreProc">rescue</span> <span class="synType">ArgumentError</span>
<span class="synType">Gem</span>::<span class="synType">Version</span>.new(<span class="synSpecial">"</span><span class="synConstant">0.0.0</span><span class="synSpecial">"</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">collect_checksums</span>(repo, tag, binary_names, gh_command)
checksums = fetch_checksums_from_file(repo, tag, binary_names)
<span class="synStatement">if</span> checksums.empty?
checksums = calculate_checksums_directly(repo, tag, binary_names, gh_command)
<span class="synStatement">end</span>
checksums
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">fetch_checksums_from_file</span>(repo, tag, binary_names)
checksums = {}
binary_names.each <span class="synStatement">do</span> |platform, filename|
sha256 = get_sha256_for_asset(repo, tag, filename)
checksums[platform] = sha256 <span class="synStatement">if</span> sha256
<span class="synStatement">end</span>
checksums
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">calculate_checksums_directly</span>(repo, tag, binary_names, gh_command)
checksums = {}
binary_names.each <span class="synStatement">do</span> |platform, filename|
sha256 = calculate_sha256_by_downloading(repo, tag, filename, gh_command)
checksums[platform] = sha256 <span class="synStatement">if</span> sha256
<span class="synStatement">end</span>
checksums
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">calculate_sha256_by_downloading</span>(repo, tag, filename, gh_command)
<span class="synType">Dir</span>.mktmpdir <span class="synStatement">do</span> |temp_dir|
command = build_download_command(gh_command, repo, tag, filename, temp_dir)
stdout, stderr, status = <span class="synType">Open3</span>.capture3(*command)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> status.success?
downloaded_file = <span class="synType">File</span>.join(temp_dir, filename)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> <span class="synType">File</span>.exist?(downloaded_file)
<span class="synType">Digest</span>::<span class="synType">SHA256</span>.file(downloaded_file).hexdigest
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">build_download_command</span>(gh_command, repo, tag, filename, dir)
[gh_command, <span class="synSpecial">"</span><span class="synConstant">release</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">download</span><span class="synSpecial">"</span>, tag,
<span class="synSpecial">"</span><span class="synConstant">--repo</span><span class="synSpecial">"</span>, repo,
<span class="synSpecial">"</span><span class="synConstant">--pattern</span><span class="synSpecial">"</span>, filename,
<span class="synSpecial">"</span><span class="synConstant">--dir</span><span class="synSpecial">"</span>, dir,
<span class="synSpecial">"</span><span class="synConstant">--clobber</span><span class="synSpecial">"</span>]
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">get_sha256_for_asset</span>(repo, tag, filename)
gh_command = find_gh_executable
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> gh_command
<span class="synType">Dir</span>.mktmpdir <span class="synStatement">do</span> |temp_dir|
checksums_content = download_checksums_file(gh_command, repo, tag, temp_dir)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> checksums_content
parse_checksum_for_file(checksums_content, filename)
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">download_checksums_file</span>(gh_command, repo, tag, temp_dir)
<span class="synType">CHECKSUM_FILE_PATTERNS</span>.each <span class="synStatement">do</span> |pattern|
command = build_download_command(gh_command, repo, tag, pattern, temp_dir)
stdout, stderr, status = <span class="synType">Open3</span>.capture3(*command)
<span class="synStatement">if</span> status.success?
checksums_file = <span class="synType">File</span>.join(temp_dir, pattern)
<span class="synStatement">return</span> <span class="synType">File</span>.read(checksums_file) <span class="synStatement">if</span> <span class="synType">File</span>.exist?(checksums_file)
<span class="synStatement">end</span>
<span class="synStatement">end</span>
<span class="synConstant">nil</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">parse_checksum_for_file</span>(content, filename)
content.each_line <span class="synStatement">do</span> |line|
line = line.strip
<span class="synStatement">next</span> <span class="synStatement">if</span> line.empty? || line.start_with?(<span class="synSpecial">"</span><span class="synConstant">#</span><span class="synSpecial">"</span>)
<span class="synStatement">if</span> line =~ <span class="synType">SHA256_PATTERN</span>
<span class="synStatement">return</span> <span class="synIdentifier">$2</span> <span class="synStatement">if</span> <span class="synIdentifier">$1</span> == filename
<span class="synStatement">elsif</span> line =~ <span class="synType">SHA256_ALTERNATE_PATTERN</span>
<span class="synStatement">return</span> <span class="synIdentifier">$1</span> <span class="synStatement">if</span> <span class="synIdentifier">$2</span> == filename
<span class="synStatement">end</span>
<span class="synStatement">end</span>
<span class="synConstant">nil</span>
<span class="synPreProc">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">get_latest_release_info</span>(asset_pattern = <span class="synConstant">nil</span>)
<span class="synStatement">return</span> <span class="synIdentifier">@latest_release_info</span> <span class="synStatement">if</span> <span class="synIdentifier">@latest_release_info</span>
setup_github_cli
<span class="synIdentifier">@latest_release_info</span> = fetch_latest_stable_release
<span class="synStatement">if</span> asset_pattern
<span class="synIdentifier">@latest_release_info</span>[<span class="synConstant">:assets</span>] = fetch_assets_info(<span class="synIdentifier">@latest_release_info</span>[<span class="synConstant">:tag</span>], asset_pattern)
<span class="synStatement">end</span>
<span class="synIdentifier">@latest_release_info</span>
<span class="synPreProc">end</span>
<span class="synPreProc">private</span>
<span class="synPreProc">def</span> <span class="synIdentifier">_fetch</span>(<span class="synConstant">url</span>:, <span class="synConstant">resolved_url</span>:, <span class="synConstant">timeout</span>:)
<span class="synType">Dir</span>.mktmpdir <span class="synStatement">do</span> |temp_dir|
checksums_sha256 = fetch_checksums(temp_dir) <span class="synStatement">if</span> <span class="synIdentifier">@auto_verify_sha256</span>
download_release_asset(temp_dir)
downloaded_file = <span class="synType">File</span>.join(temp_dir, <span class="synIdentifier">@filename</span>)
verify_and_move_file(downloaded_file, checksums_sha256)
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">fetch_checksums</span>(temp_dir)
checksums_content = download_checksums_file(temp_dir)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> checksums_content
parse_checksums(checksums_content)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">download_checksums_file</span>(temp_dir)
<span class="synType">CHECKSUM_FILE_PATTERNS</span>.each <span class="synStatement">do</span> |pattern|
content = attempt_checksum_download(pattern, temp_dir)
<span class="synStatement">if</span> content
ohai <span class="synSpecial">"</span><span class="synConstant">Found checksums file: </span><span class="synSpecial">#{</span>pattern<span class="synSpecial">}"</span>
<span class="synStatement">return</span> content
<span class="synStatement">end</span>
<span class="synStatement">end</span>
<span class="synConstant">nil</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">attempt_checksum_download</span>(pattern, temp_dir)
command = build_release_download_command(pattern, temp_dir)
stdout, stderr, status = <span class="synType">Open3</span>.capture3(*command)
<span class="synStatement">return</span> <span class="synConstant">nil</span> <span class="synStatement">unless</span> status.success?
checksums_file = <span class="synType">File</span>.join(temp_dir, pattern)
<span class="synType">File</span>.exist?(checksums_file) ? <span class="synType">File</span>.read(checksums_file) : <span class="synConstant">nil</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">parse_checksums</span>(content)
checksums = {}
content.each_line <span class="synStatement">do</span> |line|
line = line.strip
<span class="synStatement">next</span> <span class="synStatement">if</span> line.empty? || line.start_with?(<span class="synSpecial">"</span><span class="synConstant">#</span><span class="synSpecial">"</span>)
filename, sha256 = parse_checksum_line(line)
checksums[filename] = sha256 <span class="synStatement">if</span> filename && sha256
<span class="synStatement">end</span>
checksums
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">parse_checksum_line</span>(line)
<span class="synStatement">if</span> line =~ <span class="synType">SHA256_PATTERN</span>
[<span class="synIdentifier">$1</span>, <span class="synIdentifier">$2</span>]
<span class="synStatement">elsif</span> line =~ <span class="synType">SHA256_ALTERNATE_PATTERN</span>
[<span class="synIdentifier">$2</span>, <span class="synIdentifier">$1</span>]
<span class="synStatement">else</span>
[<span class="synConstant">nil</span>, <span class="synConstant">nil</span>]
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">download_release_asset</span>(temp_dir)
command = build_release_download_command(<span class="synIdentifier">@filename</span>, temp_dir)
ohai <span class="synSpecial">"</span><span class="synConstant">Downloading </span><span class="synSpecial">#{</span><span class="synIdentifier">@filename</span><span class="synSpecial">}</span><span class="synConstant"> from </span><span class="synSpecial">#{</span><span class="synIdentifier">@owner</span><span class="synSpecial">}</span><span class="synConstant">/</span><span class="synSpecial">#{</span><span class="synIdentifier">@repo</span><span class="synSpecial">}</span><span class="synConstant"> (tag: </span><span class="synSpecial">#{</span><span class="synIdentifier">@tag</span><span class="synSpecial">}</span><span class="synConstant">)...</span><span class="synSpecial">"</span>
opoo <span class="synSpecial">"</span><span class="synConstant">Using gh at: </span><span class="synSpecial">#{</span><span class="synIdentifier">@gh_command</span><span class="synSpecial">}"</span> <span class="synStatement">if</span> <span class="synType">Homebrew</span>.verbose?
stdout, stderr, status = <span class="synType">Open3</span>.capture3(*command)
<span class="synStatement">unless</span> status.success?
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <span class="synSpecial">"</span><span class="synConstant">Failed to download: </span><span class="synSpecial">#{</span>stderr<span class="synSpecial">}"</span>
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">build_release_download_command</span>(pattern, dir)
[<span class="synIdentifier">@gh_command</span>, <span class="synSpecial">"</span><span class="synConstant">release</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">download</span><span class="synSpecial">"</span>, <span class="synIdentifier">@tag</span>,
<span class="synSpecial">"</span><span class="synConstant">--repo</span><span class="synSpecial">"</span>, <span class="synSpecial">"#{</span><span class="synIdentifier">@owner</span><span class="synSpecial">}</span><span class="synConstant">/</span><span class="synSpecial">#{</span><span class="synIdentifier">@repo</span><span class="synSpecial">}"</span>,
<span class="synSpecial">"</span><span class="synConstant">--pattern</span><span class="synSpecial">"</span>, pattern,
<span class="synSpecial">"</span><span class="synConstant">--dir</span><span class="synSpecial">"</span>, dir,
<span class="synSpecial">"</span><span class="synConstant">--clobber</span><span class="synSpecial">"</span>]
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">verify_and_move_file</span>(downloaded_file, checksums_sha256)
<span class="synStatement">unless</span> <span class="synType">File</span>.exist?(downloaded_file)
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <span class="synSpecial">"</span><span class="synConstant">Downloaded file not found at expected location</span><span class="synSpecial">"</span>
<span class="synStatement">end</span>
calculated_sha256 = <span class="synType">Digest</span>::<span class="synType">SHA256</span>.file(downloaded_file).hexdigest
verify_sha256(calculated_sha256, checksums_sha256) <span class="synStatement">if</span> <span class="synIdentifier">@auto_verify_sha256</span>
<span class="synType">FileUtils</span>.mv(downloaded_file, temporary_path)
<span class="synIdentifier">@latest_release_info</span>[<span class="synConstant">:sha256</span>] = calculated_sha256 <span class="synStatement">if</span> <span class="synIdentifier">@latest_release_info</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">verify_sha256</span>(calculated_sha256, checksums_sha256)
<span class="synStatement">if</span> checksums_sha256
expected_sha256 = checksums_sha256[<span class="synIdentifier">@filename</span>]
<span class="synStatement">if</span> expected_sha256
verify_checksum_match(calculated_sha256, expected_sha256)
<span class="synStatement">else</span>
opoo <span class="synSpecial">"</span><span class="synConstant">No SHA256 found in checksums file for </span><span class="synSpecial">#{</span><span class="synIdentifier">@filename</span><span class="synSpecial">}"</span>
<span class="synStatement">end</span>
<span class="synStatement">else</span>
opoo <span class="synSpecial">"</span><span class="synConstant">SHA256 calculated but not verified (no checksums file): </span><span class="synSpecial">#{</span>calculated_sha256<span class="synSpecial">}"</span>
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">verify_checksum_match</span>(calculated, expected)
<span class="synStatement">if</span> calculated.downcase != expected.downcase
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>,
<span class="synSpecial">"</span><span class="synConstant">SHA256 mismatch for </span><span class="synSpecial">#{</span><span class="synIdentifier">@filename</span><span class="synSpecial">}\n</span><span class="synConstant">Expected: </span><span class="synSpecial">#{</span>expected<span class="synSpecial">}\n</span><span class="synConstant">Actual: </span><span class="synSpecial">#{</span>calculated<span class="synSpecial">}"</span>
<span class="synStatement">end</span>
ohai <span class="synSpecial">"</span><span class="synConstant">SHA256 verified: </span><span class="synSpecial">#{</span>calculated<span class="synSpecial">}"</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">parse_github_url</span>(url)
<span class="synStatement">unless</span> url =~ <span class="synType">URL_PATTERN</span>
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <span class="synSpecial">'</span><span class="synConstant">Invalid url pattern for GitHub Release.</span><span class="synSpecial">'</span>
<span class="synStatement">end</span>
_, <span class="synIdentifier">@owner</span>, <span class="synIdentifier">@repo</span>, <span class="synIdentifier">@tag</span>, <span class="synIdentifier">@filename</span> = *url.match(<span class="synType">URL_PATTERN</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">parse_url_components</span>
parse_github_url(<span class="synIdentifier">@url</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">build_download_url</span>(tag)
<span class="synIdentifier">@original_url</span>.gsub(<span class="synSpecial">/\/</span><span class="synConstant">releases</span><span class="synSpecial">\/</span><span class="synConstant">download</span><span class="synSpecial">\/#{</span><span class="synType">LATEST_TAG</span><span class="synSpecial">}\//</span>, <span class="synSpecial">"</span><span class="synConstant">/releases/download/</span><span class="synSpecial">#{</span>tag<span class="synSpecial">}</span><span class="synConstant">/</span><span class="synSpecial">"</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">setup_github_cli</span>
<span class="synIdentifier">@gh_command</span> = find_gh_command
validate_gh_authentication
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">find_gh_command</span>
candidates = build_gh_candidates
candidates.uniq.each <span class="synStatement">do</span> |path|
<span class="synStatement">if</span> <span class="synType">File</span>.exist?(path) && <span class="synType">File</span>.executable?(path)
opoo <span class="synSpecial">"</span><span class="synConstant">Found gh at: </span><span class="synSpecial">#{</span>path<span class="synSpecial">}"</span> <span class="synStatement">if</span> <span class="synType">Homebrew</span>.verbose?
<span class="synStatement">return</span> path
<span class="synStatement">end</span>
<span class="synStatement">end</span>
<span class="synStatement">return</span> <span class="synSpecial">"</span><span class="synConstant">gh</span><span class="synSpecial">"</span> <span class="synStatement">if</span> system(<span class="synSpecial">"</span><span class="synConstant">gh version > /dev/null 2>&1</span><span class="synSpecial">"</span>)
raise_gh_not_found_error(candidates)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">build_gh_candidates</span>
candidates = []
candidates << <span class="synSpecial">"#{</span><span class="synType">HOMEBREW_PREFIX</span><span class="synSpecial">}</span><span class="synConstant">/bin/gh</span><span class="synSpecial">"</span> <span class="synStatement">if</span> <span class="synStatement">defined?</span>(<span class="synType">HOMEBREW_PREFIX</span>)
candidates.concat(<span class="synType">GH_EXECUTABLE_PATHS</span>)
candidates << <span class="synSpecial">"#{</span><span class="synIdentifier">ENV</span>[<span class="synSpecial">"</span><span class="synConstant">HOMEBREW_PREFIX</span><span class="synSpecial">"</span>]<span class="synSpecial">}</span><span class="synConstant">/bin/gh</span><span class="synSpecial">"</span> <span class="synStatement">if</span> <span class="synIdentifier">ENV</span>[<span class="synSpecial">"</span><span class="synConstant">HOMEBREW_PREFIX</span><span class="synSpecial">"</span>]
which_result = which(<span class="synSpecial">"</span><span class="synConstant">gh</span><span class="synSpecial">"</span>)
candidates << which_result <span class="synStatement">if</span> which_result
<span class="synIdentifier">ENV</span>[<span class="synSpecial">"</span><span class="synConstant">PATH</span><span class="synSpecial">"</span>].split(<span class="synSpecial">"</span><span class="synConstant">:</span><span class="synSpecial">"</span>).each <span class="synStatement">do</span> |path|
gh_path = <span class="synType">File</span>.join(path, <span class="synSpecial">"</span><span class="synConstant">gh</span><span class="synSpecial">"</span>)
candidates << gh_path <span class="synStatement">if</span> <span class="synType">File</span>.exist?(gh_path)
<span class="synStatement">end</span>
candidates
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">raise_gh_not_found_error</span>(candidates)
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <<~<span class="synSpecial">EOS</span>
<span class="synConstant"> GitHub CLI (gh) is not installed or not found in expected locations.</span>
<span class="synConstant"> </span>
<span class="synConstant"> Searched locations:</span>
<span class="synConstant"> </span><span class="synSpecial">#{</span>candidates.join(<span class="synSpecial">"\n</span><span class="synConstant"> </span><span class="synSpecial">"</span>)<span class="synSpecial">}</span>
<span class="synConstant"> </span>
<span class="synConstant"> Please install GitHub CLI with:</span>
<span class="synConstant"> brew install gh</span>
<span class="synConstant"> </span>
<span class="synConstant"> Then authenticate with:</span>
<span class="synConstant"> gh auth login</span>
<span class="synConstant"> </span><span class="synSpecial">EOS</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">validate_gh_authentication</span>
validate_gh_version
validate_gh_auth_status
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">validate_gh_version</span>
stdout, stderr, status = <span class="synType">Open3</span>.capture3(<span class="synIdentifier">@gh_command</span>, <span class="synSpecial">"</span><span class="synConstant">version</span><span class="synSpecial">"</span>)
<span class="synStatement">unless</span> status.success?
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <<~<span class="synSpecial">EOS</span>
<span class="synConstant"> GitHub CLI (gh) command failed to execute.</span>
<span class="synConstant"> Command: </span><span class="synSpecial">#{</span><span class="synIdentifier">@gh_command</span><span class="synSpecial">}</span><span class="synConstant"> version</span>
<span class="synConstant"> Error: </span><span class="synSpecial">#{</span>stderr<span class="synSpecial">}</span>
<span class="synConstant"> </span>
<span class="synConstant"> Please ensure GitHub CLI is properly installed:</span>
<span class="synConstant"> brew install gh</span>
<span class="synConstant"> </span><span class="synSpecial">EOS</span>
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">validate_gh_auth_status</span>
stdout, stderr, status = <span class="synType">Open3</span>.capture3(<span class="synIdentifier">@gh_command</span>, <span class="synSpecial">"</span><span class="synConstant">auth</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">status</span><span class="synSpecial">"</span>)
<span class="synStatement">unless</span> status.success?
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <<~<span class="synSpecial">EOS</span>
<span class="synConstant"> GitHub CLI is not authenticated.</span>
<span class="synConstant"> </span>
<span class="synConstant"> Please authenticate with:</span>
<span class="synConstant"> gh auth login</span>
<span class="synConstant"> </span>
<span class="synConstant"> Error: </span><span class="synSpecial">#{</span>stderr<span class="synSpecial">}</span>
<span class="synConstant"> </span><span class="synSpecial">EOS</span>
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">fetch_latest_stable_release</span>
releases = fetch_all_releases
latest = find_latest_stable(releases)
<span class="synStatement">unless</span> latest
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <span class="synSpecial">"</span><span class="synConstant">No stable releases found</span><span class="synSpecial">"</span>
<span class="synStatement">end</span>
build_release_info(latest)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">fetch_all_releases</span>
command = [<span class="synIdentifier">@gh_command</span>, <span class="synSpecial">"</span><span class="synConstant">release</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">list</span><span class="synSpecial">"</span>,
<span class="synSpecial">"</span><span class="synConstant">--repo</span><span class="synSpecial">"</span>, <span class="synSpecial">"#{</span><span class="synIdentifier">@owner</span><span class="synSpecial">}</span><span class="synConstant">/</span><span class="synSpecial">#{</span><span class="synIdentifier">@repo</span><span class="synSpecial">}"</span>,
<span class="synSpecial">"</span><span class="synConstant">--limit</span><span class="synSpecial">"</span>, <span class="synType">RELEASE_LIMIT</span>.to_s,
<span class="synSpecial">"</span><span class="synConstant">--json</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">tagName,publishedAt,isPrerelease</span><span class="synSpecial">"</span>]
stdout, stderr, status = <span class="synType">Open3</span>.capture3(*command)
<span class="synStatement">unless</span> status.success?
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <span class="synSpecial">"</span><span class="synConstant">Failed to fetch release list: </span><span class="synSpecial">#{</span>stderr<span class="synSpecial">}"</span>
<span class="synStatement">end</span>
<span class="synType">JSON</span>.parse(stdout)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">find_latest_stable</span>(releases)
stable_releases = releases.reject { |r| r[<span class="synSpecial">"</span><span class="synConstant">isPrerelease</span><span class="synSpecial">"</span>] }
stable_releases.max_by <span class="synStatement">do</span> |release|
tag = release[<span class="synSpecial">"</span><span class="synConstant">tagName</span><span class="synSpecial">"</span>].gsub(<span class="synSpecial">/^#{</span><span class="synType">VERSION_PREFIX</span><span class="synSpecial">}/</span>, <span class="synSpecial">""</span>)
parse_version_safe(tag)
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">parse_version_safe</span>(tag)
<span class="synType">Gem</span>::<span class="synType">Version</span>.new(tag)
<span class="synPreProc">rescue</span> <span class="synType">ArgumentError</span>
<span class="synType">Gem</span>::<span class="synType">Version</span>.new(<span class="synSpecial">"</span><span class="synConstant">0.0.0</span><span class="synSpecial">"</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">build_release_info</span>(release)
tag = release[<span class="synSpecial">"</span><span class="synConstant">tagName</span><span class="synSpecial">"</span>]
{
<span class="synConstant">tag</span>: tag,
<span class="synConstant">version</span>: tag.gsub(<span class="synSpecial">/^#{</span><span class="synType">VERSION_PREFIX</span><span class="synSpecial">}/</span>, <span class="synSpecial">""</span>),
<span class="synConstant">published_at</span>: release[<span class="synSpecial">"</span><span class="synConstant">publishedAt</span><span class="synSpecial">"</span>]
}
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">fetch_assets_info</span>(tag, pattern)
assets = fetch_release_assets(tag)
<span class="synStatement">case</span> pattern
<span class="synStatement">when</span> <span class="synType">Hash</span>
match_multiple_assets(assets, pattern)
<span class="synStatement">when</span> <span class="synType">String</span>
match_single_asset(assets, pattern)
<span class="synStatement">else</span>
format_all_assets(assets)
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">fetch_release_assets</span>(tag)
command = [<span class="synIdentifier">@gh_command</span>, <span class="synSpecial">"</span><span class="synConstant">release</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">view</span><span class="synSpecial">"</span>, tag,
<span class="synSpecial">"</span><span class="synConstant">--repo</span><span class="synSpecial">"</span>, <span class="synSpecial">"#{</span><span class="synIdentifier">@owner</span><span class="synSpecial">}</span><span class="synConstant">/</span><span class="synSpecial">#{</span><span class="synIdentifier">@repo</span><span class="synSpecial">}"</span>,
<span class="synSpecial">"</span><span class="synConstant">--json</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">assets</span><span class="synSpecial">"</span>]
stdout, stderr, status = <span class="synType">Open3</span>.capture3(*command)
<span class="synStatement">unless</span> status.success?
<span class="synStatement">raise</span> <span class="synType">CurlDownloadStrategyError</span>, <span class="synSpecial">"</span><span class="synConstant">Failed to fetch release assets: </span><span class="synSpecial">#{</span>stderr<span class="synSpecial">}"</span>
<span class="synStatement">end</span>
<span class="synType">JSON</span>.parse(stdout)[<span class="synSpecial">"</span><span class="synConstant">assets</span><span class="synSpecial">"</span>]
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">match_multiple_assets</span>(assets, patterns)
matching_assets = {}
patterns.each <span class="synStatement">do</span> |key, asset_pattern|
asset = find_matching_asset(assets, asset_pattern)
matching_assets[key] = format_asset_info(asset) <span class="synStatement">if</span> asset
<span class="synStatement">end</span>
matching_assets
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">match_single_asset</span>(assets, pattern)
asset = find_matching_asset(assets, pattern)
asset ? format_asset_info(asset) : {}
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">find_matching_asset</span>(assets, pattern)
assets.find { |asset| <span class="synType">File</span>.fnmatch(pattern, asset[<span class="synSpecial">"</span><span class="synConstant">name</span><span class="synSpecial">"</span>]) }
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">format_all_assets</span>(assets)
assets.map { |asset| format_asset_info(asset) }
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">format_asset_info</span>(asset)
{
<span class="synConstant">name</span>: asset[<span class="synSpecial">"</span><span class="synConstant">name</span><span class="synSpecial">"</span>],
<span class="synConstant">download_url</span>: asset[<span class="synSpecial">"</span><span class="synConstant">browserDownloadUrl</span><span class="synSpecial">"</span>],
<span class="synConstant">size</span>: asset[<span class="synSpecial">"</span><span class="synConstant">size</span><span class="synSpecial">"</span>]
}
<span class="synPreProc">end</span>
<span class="synPreProc">end</span>
</pre>
<p></details></p>
<p>また、より簡単に記述できるようにするために Formula も簡素化した継承クラスを作成しました。</p>
<p><details>
<summary> ソース: <code>lib/github_gh_internal_formula.rb</code> </summary></p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require_relative</span> <span class="synSpecial">'</span><span class="synConstant">./github_gh_internal_release_download_strategy</span><span class="synSpecial">'</span>
<span class="synPreProc">class</span> <span class="synType">GitHubGHInternalFormula</span> < <span class="synType">Formula</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">repo</span>(name)
<span class="synIdentifier">@repo_name</span> = name
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">binary</span>(basename)
<span class="synIdentifier">@binary_basename</span> = basename
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">repo_name</span>
<span class="synIdentifier">@repo_name</span> || <span class="synStatement">raise</span>(<span class="synType">NotImplementedError</span>, <span class="synSpecial">"</span><span class="synConstant">Subclass must define repo_name using 'repo' method</span><span class="synSpecial">"</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">binary_basename</span>
<span class="synIdentifier">@binary_basename</span> || <span class="synStatement">raise</span>(<span class="synType">NotImplementedError</span>, <span class="synSpecial">"</span><span class="synConstant">Subclass must define binary_basename using 'binary' method</span><span class="synSpecial">"</span>)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">test_command_args</span>
[<span class="synSpecial">"</span><span class="synConstant">-v</span><span class="synSpecial">"</span>]
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">binary_names</span>
<span class="synIdentifier">@binary_names</span> ||= <span class="synStatement">if</span> respond_to?(<span class="synConstant">:custom_binary_names</span>)
custom_binary_names
<span class="synStatement">else</span>
{
<span class="synConstant">darwin_amd64</span>: <span class="synSpecial">"#{</span>binary_basename<span class="synSpecial">}</span><span class="synConstant">-darwin-amd64</span><span class="synSpecial">"</span>,
<span class="synConstant">darwin_arm64</span>: <span class="synSpecial">"#{</span>binary_basename<span class="synSpecial">}</span><span class="synConstant">-darwin-arm64</span><span class="synSpecial">"</span>,
<span class="synConstant">linux_amd64</span>: <span class="synSpecial">"#{</span>binary_basename<span class="synSpecial">}</span><span class="synConstant">-linux-amd64</span><span class="synSpecial">"</span>,
<span class="synConstant">linux_arm64</span>: <span class="synSpecial">"#{</span>binary_basename<span class="synSpecial">}</span><span class="synConstant">-linux-arm64</span><span class="synSpecial">"</span>
}
<span class="synStatement">end</span>.freeze
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">checksums</span>
<span class="synIdentifier">@checksums</span> ||= <span class="synType">GitHubGHInternalReleaseDownloadStrategy</span>.get_sha256_for_binary_set(repo_name, binary_names)
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">platform_key</span>
<span class="synStatement">if</span> <span class="synType">OS</span>.mac?
<span class="synType">Hardware</span>::<span class="synType">CPU</span>.intel? ? <span class="synConstant">:darwin_amd64</span> : <span class="synConstant">:darwin_arm64</span>
<span class="synStatement">else</span>
<span class="synType">Hardware</span>::<span class="synType">CPU</span>.intel? ? <span class="synConstant">:linux_amd64</span> : <span class="synConstant">:linux_arm64</span>
<span class="synStatement">end</span>
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">setup_platform_binary</span>
key = platform_key
binary_name = binary_names[key]
url <span class="synSpecial">"</span><span class="synConstant">https://github.com/</span><span class="synSpecial">#{</span>repo_name<span class="synSpecial">}</span><span class="synConstant">/releases/download/latest/</span><span class="synSpecial">#{</span>binary_name<span class="synSpecial">}"</span>,
<span class="synConstant">using</span>: <span class="synType">GitHubGHInternalReleaseDownloadStrategy</span>
sha256 checksums[key] <span class="synStatement">if</span> checksums[key]
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">inherited</span>(subclass)
<span class="synStatement">super</span>
<span class="synType">TracePoint</span>.new(<span class="synConstant">:end</span>) <span class="synStatement">do</span> |tp|
<span class="synStatement">if</span> tp.self == subclass
tp.disable
subclass.class_eval <span class="synStatement">do</span>
homepage <span class="synSpecial">"</span><span class="synConstant">https://github.com/</span><span class="synSpecial">#{</span>subclass.repo_name<span class="synSpecial">}"</span>
latest_version = <span class="synType">GitHubGHInternalReleaseDownloadStrategy</span>.get_latest_version_for_formula(subclass.repo_name)
version latest_version <span class="synStatement">if</span> latest_version
<span class="synStatement">if</span> subclass.platform_key
subclass.setup_platform_binary
<span class="synStatement">end</span>
<span class="synStatement">end</span>
<span class="synStatement">end</span>
<span class="synStatement">end</span>.enable
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">install</span>
bin.install <span class="synConstant">self</span>.class.binary_names[<span class="synConstant">self</span>.class.platform_key] => <span class="synConstant">self</span>.class.binary_basename
<span class="synPreProc">end</span>
<span class="synPreProc">def</span> <span class="synIdentifier">test</span>
system <span class="synSpecial">"#{</span>bin<span class="synSpecial">}</span><span class="synConstant">/</span><span class="synSpecial">#{</span><span class="synConstant">self</span>.class.binary_basename<span class="synSpecial">}"</span>, *<span class="synConstant">self</span>.class.test_command_args
<span class="synPreProc">end</span>
<span class="synPreProc">end</span>
</pre>
<p></details></p>
<p>このクラスを用いて以下のように Formula を作成しました。 <code>toilet</code> は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>オフィス社内のトイレの空き状況をチェックする自作の <del>くだらない</del> ツールです。</p>
<p><code>toilet.rb</code></p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require_relative</span> <span class="synSpecial">'</span><span class="synConstant">./lib/github_gh_internal_formula</span><span class="synSpecial">'</span>
<span class="synPreProc">class</span> <span class="synType">Toilet</span> < <span class="synType">GitHubGHInternalFormula</span>
desc <span class="synSpecial">"</span><span class="synConstant">COLOPL toilet vacancy checker</span><span class="synSpecial">"</span>
repo <span class="synSpecial">"</span><span class="synConstant">colopl-indie/toilet</span><span class="synSpecial">"</span>
binary <span class="synSpecial">"</span><span class="synConstant">toilet</span><span class="synSpecial">"</span>
<span class="synPreProc">def</span> <span class="synConstant">self</span>.<span class="synIdentifier">test_command_args</span>
[<span class="synSpecial">"</span><span class="synConstant">--help</span><span class="synSpecial">"</span>]
<span class="synPreProc">end</span>
<span class="synPreProc">end</span>
</pre>
<p>これらを格納した Homebrew tap 用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>である <code>colopl-indie/homebrew-formulas</code> を作成し、以下のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成としました。</p>
<pre class="code" data-lang="" data-unlink>.
├── lib
│ ├── github_gh_internal_formula.rb
│ └── github_gh_internal_release_download_strategy.rb
├── README.md
└── toilet.rb</pre>
<h2 id="実際の使用方法">実際の使用方法</h2>
<p>この方法では常に <code>gh</code> コマンドを必要とするので、事前に Homebrew で <code>gh</code> コマンドをインストールし、権限のある Organization アカウントでログインしておく必要があります。</p>
<p>セットアップが終わったら後は普通に Homebrew でインストールするだけです。 <code>toilet</code> ならこんな感じです (Homebrew 公式に同名のものがあるとそちらが優先されてしまうので、フルネームでインストールするのを推奨します)</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>$ brew install colopl-indie/formulas/toilet
~~~ 省略 ~~~
$ toilet <span class="synSpecial">--help</span>
_
| |
___| | COLOPL
<span class="synPreProc">(</span><span class="synSpecial"> .</span><span class="synStatement">'</span><span class="synConstant"> Toilet</span>
<span class="synConstant"> ) (</span>
<span class="synConstant">COLOPL Toilet Utility v2.0.3</span>
<span class="synConstant">Usage:</span>
<span class="synConstant"> toilet [command]</span>
<span class="synConstant">Available Commands:</span>
<span class="synConstant"> check Check available restrooms</span>
<span class="synConstant"> completion Generate the autocompletion script for the specified shell</span>
<span class="synConstant"> help Help about any command</span>
<span class="synConstant"> watch Watch availability</span>
<span class="synConstant">Flags:</span>
<span class="synConstant"> -h, --help help for toilet</span>
<span class="synConstant">Use "toilet [command] --help" for more information about a command.</span>
</pre>
<h2 id="更新の自動化">更新の自動化</h2>
<p>ちなみにこの方法では、常に対象となる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の最新のリリースを取得し利用するため Release さえ適切に行えていればツールのバージョンアップの度に <strong>Homebrew <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>側を更新する必要はありません</strong>。 逆に言えば古いバージョンを入れられないということにはなりますが、まあ社内ツールなのでその時はリリースを削除するだけで大丈夫です。</p>
<h2 id="おまけ-GitHub-Actions-を用いた-Go-製ツールの自動リリース">おまけ: <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions を用いた Go 製ツールの自動リリース</h2>
<p>先ほどの <code>toilet</code> では、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions にて <code>vX.Y.Z</code> でタグが切られており、正式に SemVer の要件を満たした場合に自動的にリリースを作成しバイナリをアップロードするようにしています。 SemVer の要件を満たさない (例: <code>vX.Y.Z-pre</code> 等) 場合は draft としてリリースします。</p>
<p>以下は <code>toilet</code> で自動リリースするための <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の定義の例です。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build and Release
<span class="synIdentifier">on</span><span class="synSpecial">:</span>
<span class="synIdentifier">push</span><span class="synSpecial">:</span>
<span class="synIdentifier">tags</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synConstant">'v*'</span>
<span class="synIdentifier">concurrency</span><span class="synSpecial">:</span>
<span class="synIdentifier">group</span><span class="synSpecial">:</span> release-${{ github.ref }}
<span class="synIdentifier">cancel-in-progress</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synIdentifier">permissions</span><span class="synSpecial">:</span>
<span class="synIdentifier">contents</span><span class="synSpecial">:</span> read
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">build</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Build binaries
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-24.04
<span class="synIdentifier">strategy</span><span class="synSpecial">:</span>
<span class="synIdentifier">matrix</span><span class="synSpecial">:</span>
<span class="synIdentifier">goos</span><span class="synSpecial">:</span> <span class="synSpecial">[</span>linux, darwin, windows<span class="synSpecial">]</span>
<span class="synIdentifier">goarch</span><span class="synSpecial">:</span> <span class="synSpecial">[</span>amd64, arm64<span class="synSpecial">]</span>
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout code
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v5
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">fetch-depth</span><span class="synSpecial">:</span> <span class="synConstant">0</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Set up Go
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-go@v6
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">go-version</span><span class="synSpecial">:</span> stable
<span class="synIdentifier">cache</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Verify dependencies
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
go mod verify
go mod download
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Replace placeholders
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
sed -i <span class="synConstant">'s#%COLOPL_TOILET_API%#${{ secrets.COLOPL_TOILET_API }}#g'</span> <span class="synConstant">"toilet.go"</span>
sed -i <span class="synConstant">'s#%COLOPL_TOILET_VERSION%#${{ github.ref_name }}#g'</span> <span class="synConstant">"toilet.go"</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build binary
<span class="synIdentifier">env</span><span class="synSpecial">:</span>
<span class="synIdentifier">GOOS</span><span class="synSpecial">:</span> ${{ matrix.goos }}
<span class="synIdentifier">GOARCH</span><span class="synSpecial">:</span> ${{ matrix.goarch }}
<span class="synIdentifier">CGO_ENABLED</span><span class="synSpecial">:</span> <span class="synConstant">0</span>
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
BINARY_NAME="toilet-${{ matrix.goos }}-${{ matrix.goarch }}<span class="synConstant">"</span>
<span class="synConstant"> if test "</span>${{ matrix.goos }}<span class="synConstant">" = "</span>windows"; then
BINARY_NAME="${BINARY_NAME}.exe"
fi
go build -v -ldflags="-s -w" -o <span class="synConstant">"${BINARY_NAME}"</span> <span class="synConstant">"toilet.go"</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Upload artifacts
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/upload-artifact@v4
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> toilet-${{ matrix.goos }}-${{ matrix.goarch }}
<span class="synIdentifier">path</span><span class="synSpecial">:</span> toilet-*
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> <span class="synConstant">1</span>
<span class="synIdentifier">if-no-files-found</span><span class="synSpecial">:</span> error
<span class="synIdentifier">release</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Create Release and Upload Assets
<span class="synIdentifier">needs</span><span class="synSpecial">:</span> build
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-24.04
<span class="synIdentifier">permissions</span><span class="synSpecial">:</span>
<span class="synIdentifier">contents</span><span class="synSpecial">:</span> write
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout code
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v5
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">fetch-depth</span><span class="synSpecial">:</span> <span class="synConstant">0</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Download all artifacts
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/download-artifact@v5
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">path</span><span class="synSpecial">:</span> artifacts/
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check if tag follows semantic versioning pattern
<span class="synIdentifier">id</span><span class="synSpecial">:</span> check_tag
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
TAG_NAME="${{ github.ref_name }}<span class="synConstant">"</span>
<span class="synConstant"> if echo "</span>${TAG_NAME}" | grep -qE <span class="synConstant">'^v[0-9]+\.[0-9]+\.[0-9]+$'</span>; then
echo <span class="synConstant">"draft=false"</span> >> $GITHUB_OUTPUT
else
echo <span class="synConstant">"draft=true"</span> >> $GITHUB_OUTPUT
fi
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create Release
<span class="synIdentifier">id</span><span class="synSpecial">:</span> create_release
<span class="synIdentifier">env</span><span class="synSpecial">:</span>
<span class="synIdentifier">GITHUB_TOKEN</span><span class="synSpecial">:</span> ${{ secrets.GITHUB_TOKEN }}
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
gh release create <span class="synConstant">"${{ github.ref_name }}"</span> \
--title <span class="synConstant">"Release ${{ github.ref_name }}"</span> \
--generate-notes \
--draft="${{ steps.check_tag.outputs.draft }}<span class="synConstant">"</span>
<span class="synConstant"> sleep 3</span>
<span class="synConstant"> gh release view "</span>${{ github.ref_name }}<span class="synConstant">"</span>
<span class="synConstant"> - name: Upload Release Assets</span>
<span class="synConstant"> env:</span>
<span class="synConstant"> GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}</span>
<span class="synConstant"> run: |</span>
<span class="synConstant"> for FILE in artifacts/*/toilet-*; do</span>
<span class="synConstant"> if test -f "</span>${FILE}"; then
gh release upload <span class="synConstant">"${{ github.ref_name }}"</span> <span class="synConstant">"${FILE}"</span> --clobber
fi
done
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Generate and upload checksums
<span class="synIdentifier">env</span><span class="synSpecial">:</span>
<span class="synIdentifier">GITHUB_TOKEN</span><span class="synSpecial">:</span> ${{ secrets.GITHUB_TOKEN }}
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
cd <span class="synConstant">"artifacts"</span>
find . -type f -name <span class="synConstant">'toilet-*'</span> -exec sh -c <span class="synConstant">'echo "$(sha256sum "$1" | cut -d" " -f1) $(basename "$1")"'</span> _ <span class="synSpecial">{}</span> \; > <span class="synConstant">"checksums.txt"</span>
gh release upload <span class="synConstant">"${{ github.ref_name }}"</span> <span class="synConstant">"checksums.txt"</span> --clobber
</pre>
<h2 id="おわりに">おわりに</h2>
<p>この方法により、以下の課題を解決できました。</p>
<ul>
<li>Internal, Private なツールの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>におけるバイナリリリースフローの統一
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> の Release 機能を用いて自動的に Homebrew にもそれを反映できる環境に</li>
</ul>
</li>
<li>SHA-256 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C1%A5%A7%A5%C3%A5%AF%A5%B5%A5%E0">チェックサム</a>を利用したセキュアなバイナリ配布</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> 公式 <a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a> である <code>gh</code> コマンドのフル活用
<ul>
<li>モダンな認証方式でより安全かつ使いやすい仕組みを実現</li>
</ul>
</li>
<li>シンプルな構成による保守コストの低減</li>
</ul>
<p>最後になりますが、体調不良により各所にご迷惑をおかけし大変申し訳ございませんでした。挽回できるようバリバリ活動していきますので、引き続きよろしくお願いいたします!</p>
<h2 id="おまけのおまけ-toilet-について">おまけのおまけ: <code>toilet</code> について</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>オフィスにはトイレの空き状況チェックができる環境が用意されており、 <code>toilet</code> は <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> 経由でそれを参照してチェックしています。
もともと社内向けポータルに表示機能があったのですが、エンジニアとしては<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>の方が使いやすいなと思って移植しました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>オフィスは現状 5F と 6F のフロアがあるのですが、例えば以下のようにすることで 6F のトイレが空いたら通知してくれます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/macOS">macOS</a> なら通知センターにも飛びます。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>$ toilet watch <span class="synSpecial">--targets</span><span class="synStatement">=</span>6F
_
| |
___| | COLOPL
<span class="synPreProc">(</span><span class="synSpecial"> .</span><span class="synStatement">'</span><span class="synConstant"> Toilet</span>
<span class="synConstant"> ) ( </span>
<span class="synConstant">TOILET AVAILABLE!!!: 6F</span>
</pre>
<p>あとは単純に状況のチェックもできます</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>$ toilet check
_
| |
___| | COLOPL
<span class="synPreProc">(</span><span class="synSpecial"> .</span><span class="synStatement">'</span><span class="synConstant"> Toilet</span>
<span class="synConstant"> ) ( </span>
<span class="synConstant">Area Current Status Empty Max</span>
<span class="synConstant">6F Available 3 6</span>
<span class="synConstant">5F Available 2 8</span>
</pre>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
AIで開発はどう変わる?社内勉強会『AIテックトーク』開催レポート
hatenablog://entry/6802418398535961003
2025-08-18T11:00:00+09:00
2025-08-18T11:00:01+09:00 こんにちは!エンジニアの石塚です。 普段は人事部でHR Tech領域を担当しつつ、社内のAI活用を推進する活動にも取り組んでいます。 さて、今回はコロプラで定期開催している「AIトーク」シリーズの第3弾として、エンジニア向けの勉強会「AIテックトーク」を開催しました。この記事では、イベントの様子や、そこで共有されたAIによる開発の変化をご紹介します。 全社で加速するAI活用!「AIトーク」シリーズとは? コロプラでは月に一度、部署や職種の垣根を越えてAIの活用術をカジュアルに語り合う「AIトーク」というイベントを開催しています。「ここでの発見が、誰かのヒントになれば」という想いから始まったこの…
<p>こんにちは!エンジニアの石塚です。
普段は人事部でHR Tech領域を担当しつつ、社内のAI活用を推進する活動にも取り組んでいます。</p>
<p>さて、今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>で定期開催している「AI<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>」シリーズの第3弾として、エンジニア向けの勉強会「AIテック<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>」を開催しました。この記事では、イベントの様子や、そこで共有されたAIによる開発の変化をご紹介します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250812/20250812193425.jpg" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="全社で加速するAI活用AIトークシリーズとは">全社で加速するAI活用!「AI<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>」シリーズとは?</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では月に一度、部署や職種の垣根を越えてAIの活用術をカジュアルに語り合う「AI<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>」というイベントを開催しています。「ここでの発見が、誰かのヒントになれば」という想いから始まったこの取り組み。</p>
<p>回を重ねるごとに、社内のあちこちで「あのツール、どう?」「こんな使い方を試してみたよ」といった会話が自然に生まれるようになり、AI活用の輪が着実に広がっています。</p>
<h3 id="これまでのAIトークシリーズ">これまでの「AI<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>」シリーズ</h3>
<ul>
<li><p>第1回:AIフラット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a></p>
<p> 職種や役割に関係なく、AIの最新情報をシェアする全社向けイベント。初回は「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Gemini Advanced」をテーマに盛り上がりました。</p></li>
<li><p>第2回:AIカジュ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a></p>
<p> バックオフィス部門での活用事例をカジュアルに共有するイベント。「Gemini Deep Research & NotebookLM」を題材に、人事戦略部のリアルな活用術を紹介しました。</p></li>
<li><p>第3回:AIテック<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a></p>
<p> そして今回ご紹介する、エンジニア向けのイベントです。</p></li>
</ul>
<h2 id="今回のテーマはAIコーディングエージェント">今回のテーマは「AIコーディングエージェント」</h2>
<p>当社のCIO菅井は「あなたの当たり前は誰かの学び」という言葉で情報や学びの共有を促しています。今回の「AIテック<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>」は、まさにその言葉を体現する場となり、登壇したエンジニアたちが、日々の業務で得たリアルな知見や実践的な活用事例を共有してくれました。</p>
<p><strong>イベントテーマ:AIコーディングエージェント</strong></p>
<p>今回のイベントは、日進月歩で進化するAIコーディングツールの最新動向をキャッチアップし、それぞれのツールの特性を理解することで、日々の開発をさらに効率化することを目的として開催されました。</p>
<p>当日は、公募メンバーを含むCIOと4名のエンジニアが登壇。AIコーディングの基本から、少し応用的な技術である「<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>(Model Context Protocol)」まで、それぞれの視点から発表が行われました。</p>
<h3 id="発表構成">発表構成</h3>
<ol>
<li>オープニング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>:AIコーディングエージェントの進化と向き合い方(CIO 菅井)</li>
<li>Cursorのすゝめ:AIネイティブなコードエディタを使いこなす(K.Y / インターナルグループ)</li>
<li>Devinで開発を加速!「Vibe Coding」という新しい開発スタイル(石塚 / 経営企画本部 人事部)</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>とは? AIと社内サービスを繋ぐ接続の仕組み(N.S / インターナルグループ)</li>
<li>AIコーディングエージェントの総括と、これからの歩き方(山田 <a href="https://x.com/yamadashy">@yamadashy</a> / CIO室 AIイ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%CD%A1%BC%A5%D6%A5%EB">ネーブル</a>メントグループ)</li>
</ol>
<h2 id="各発表のハイライト">各発表のハイライト</h2>
<p>当日は5つのセッションが行われ、どれも非常に内容の濃いものばかり。それぞれのハイライトをご紹介します。</p>
<h3 id="1-オープニングトークAIコーディングエージェントの進化と向き合い方">1. オープニング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>:AIコーディングエージェントの進化と向き合い方</h3>
<p>AIコーディングエージェントがいかに凄まじいスピードで進化しているか、そして私たちがどう向き合うべきかについて解説。「みんなで気楽に時代の変化を楽しみましょう」というメッセージで、イベントの幕を開けました。</p>
<p>発表では、AIコーディングエージェントの継続的な進化が強調されました。この4年間で、AIは単なる「コード補完」ツールから、ソフトウェアの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>を人間と同等レベルで解決する「自律型エンジニア」へと発展しました。</p>
<p>特に印象的だったのは、「うまくいかない時は、まっさらな状態からやり直そう」という言葉。AIは複雑なものを修正するより、ゼロから作ることの方が得意であり、この特性を理解することがAIと上手く付き合うコツだと語りました。</p>
<h3 id="2-CursorのすゝめAIネイティブなコードエディタを使いこなす">2. Cursorのすゝめ:AIネイティブなコードエディタを使いこなす</h3>
<p>AIコードエディタ「Cursor」の実践的な活用法が紹介されました。</p>
<p>発表では、Cursorに統合されたAIエージェントが、必要なファイルを自律的に判断して読み書きする仕組みや、タブ補完、<code>Command+K</code>を使った指示による編集など、その強力かつ直感的な操作が解説されました。</p>
<p>続くライブデモでは、ToDoリストアプリの実装を例に、Cursorへの指示出しからエラー発生時の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>フロー(AIへの再指示)までが実演され、参加者は具体的な活用イメージを掴むことができました。</p>
<p>安全性にも話が及び、<code>.cursorignore</code>によるAIの読み取り範囲の制御や、「User Rules」を用いたルール設定など、セキュアに利用するための重要なポイントが解説されました。</p>
<h3 id="3-Devinで開発を加速Vibe-Codingという新しい開発スタイル">3. Devinで開発を加速!「Vibe Coding」という新しい開発スタイル</h3>
<p>自律型AIエージェント「Devin」を活用し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Chrome">Chrome</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%C8%C4%A5%B5%A1%C7%BD">拡張機能</a>の社内ツール「くまぱわーあっぷ」の開発を高速化した事例と、新しい開発スタイル「Vibe Coding」について発表しました。</p>
<p>Devinは、タスクの実行からコーディング、インフラ設定までを一貫して担う自律型AIです。Slack連携やWeb UIを通じたタスク管理機能に加え、「Devin Knowledge」によってプロジェクト固有のルールや設計思想を学習させることが可能だと紹介されました。</p>
<p>実際の社内ツール開発では、プロジェクト特有の「お作法」や<a class="keyword" href="https://d.hatena.ne.jp/keyword/Chrome">Chrome</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%C8%C4%A5%B5%A1%C7%BD">拡張機能</a>ならではの制約といった、AIだけでは対処が難しい課題に直面しました。そこで、既存設計の理解には「Devin Knowledge」を、機能移植には関連コードをまとめる「Repomix」を活用するなど、実践的な工夫を凝らした結果、UIや主要機能の大部分をAIに自動生成させることに成功したと報告されました。</p>
<p>発表では、開発スピードの向上というメリットだけでなく、AIを単なる「道具」としてではなく、「プロジェクトメンバーの一員」として根気強く育てていく視点の重要性が共有されました。実際にDevinを用いた新機能追加のデモも成功に終わり、「Vibe Coding」がいかに開発を効率化するかが示されました。</p>
<h3 id="4-MCPとは-AIと社内サービスを繋ぐ接続の仕組み">4. <a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>とは? AIと社内サービスを繋ぐ接続の仕組み</h3>
<p>AIと各種サービスを連携させるための仕組みである「<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>(Model Context Protocol)」について、デモを交えながら紹介しました。</p>
<p>Playwright <a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>を使い、Cursorからの指示一つでテストを自動実行し、その結果をSlackに通知するデモを披露。さらに、チャットで社員名を尋ねるだけで社内システムから情報を取得して回答する「社員情報検索<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>」も紹介され、AIが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>を介して複数のサービスを自在に操る様子は、会場から大きな反響を呼びました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>を活用することで、従来は手作業だった情報取得や操作を自動化し、業務効率を改善できると説明。一度設定すればユーザー側での更新が不要になる点や、ローカルPC上で独自の<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>コマンドを作成できる高い拡張性も、その魅力として挙げられました。</p>
<p>デモで紹介された「社員情報検索<a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>」は、すでに社内で実際にリリースされています。今後は連携先をさらに拡大し、業務におけるAI活用を一層推進していく方針が共有されました。</p>
<h3 id="5-AIコーディングエージェントの総括とこれからの歩き方">5. AIコーディングエージェントの総括と、これからの歩き方</h3>
<p>これまでのセッション内容を網羅しつつ、AI駆動開発のリアルな知見と今後の展望について総括が行われました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilotのような「補助ツール」から、DevinやClaude Codeといった「バックグラウンドエージェント」、さらには「レビュー自動化」や「<a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a>ツール」に至るまで、AIエージェントを体系的に分類。タスクに応じて最適なツールを使い分けることの重要性が解説されました。</p>
<p>実践から得られた活用のコツとして、ルールファイルをシンプルかつ明確に保つことが不可欠だと強調されました。「昨日入社したばかりのメンバーでも理解できる設計」を基準とし、大規模なタスクはAI自身に計画を立てさせ、ステップごとに実行させることが成功の鍵だと紹介されました。</p>
<p>その一方で、実際の開発プロジェクトにおける失敗談も共有されました。古いコードの残骸がAIの動作に悪影響を及ぼし、無限ループに陥るトラブルも。この経験から、コードの定期的な整理やタスク終了後のクリーンアップ、そして最終的な人間によるレビューがいかに重要であるかという教訓が共有されました。</p>
<p>今後の展望として、各プロジェクトに最適化されたルール設計を進め、Claude CodeやGemini <a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a>の活用を促進していくと述べられました。そして、社内外の情報チャネルを積極的に活用し、エンジニア一人ひとりが主体的にAIと共に成長していく姿勢が求められる、とセッションは締めくくられました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250812/20250812193522.jpg" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="開催を終えて">開催を終えて</h2>
<p>今回の「AIテック<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>」は、AIコーディングの最前線で得られた成功体験やリアルな失敗談、そして明日から使える実践的なノウハウが飛び交う、非常に熱量の高い時間となりました。</p>
<p>参加者からは、「各ツールの特性や使い分けがよく分かった」「早速試してみたい!」といった声が多く寄せられ、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>全体のAI活用をさらに加速させる、大きな一歩になったと感じています。</p>
<h2 id="おわりに">おわりに</h2>
<p>AIコーディングエージェントの世界は、急速に進化しています。この大きな波を楽しみながら乗りこなすために、ツールの特性を深く理解し、プロジェクトや自身の開発スタイルに合わせて使い分けることが、これからのエンジニアには欠かせません。</p>
<p>同時に、AIの能力を最大限に引き出すためには、AIが活躍しやすい環境を整える組織的な取り組みも不可欠です。実際に、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では社員の約8割が業務でAIを活用するまでになっており、AIを「クリエイターの創造性を支える黒子」として位置づけた多面的な支援施策を展開しています。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、今後もこうしたナレッジ共有の場を大切にし、社員一人ひとりがもっと楽しく、もっと創造的に働ける環境を、組織全体でつくっていきます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fpinmark.colopl.co.jp%2Fentries%2F84575648" title="AI活用率100%に向けて!コロプラが実践する“クリエイター×AI"新しい共存のカタチ | ピンマーク by Colopl | コロプラの「現在地」と「目的地」を発信するメディア" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://pinmark.colopl.co.jp/entries/84575648">pinmark.colopl.co.jp</a></cite></p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
【クイズ解答・解説公開】PHP Conference Japan 2025に出展させていただきました
hatenablog://entry/6802418398487767188
2025-07-02T11:00:00+09:00
2025-07-02T11:00:03+09:00 こんにちは、エンジニアの山田 (@yamadashy) です。 6月28日に大田区産業プラザPiOで開催された「PHP Conference Japan 2025」に出展しました。 今年はゴールドスポンサーとして協賛し、ブース出展を通して多くのPHP開発者の皆さんと交流する機会をいただきました。 PHP Conference Japan 2025 について PHP Conference Japan は有志によるプログラミング言語 PHP にまつわる国内最大規模のカンファレンスで、一年に一度の頻度で開催されている技術系カンファレンスイベントです。 コロプラとしても 2022 年からシルバースポン…
<p>こんにちは、エンジニアの山田 (<a href="https://x.com/yamadashy">@yamadashy</a>) です。</p>
<p>6月28日に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C2%E7%C5%C4%B6%E8%BB%BA%B6%C8%A5%D7%A5%E9%A5%B6">大田区産業プラザ</a>PiOで開催された「<a href="https://phpcon.php.gr.jp/2025/">PHP Conference Japan 2025</a>」に出展しました。
今年は<a href="https://colopl.co.jp/news/info/2025042501.php">ゴールドスポンサーとして協賛</a>し、ブース出展を通して多くの<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>開発者の皆さんと交流する機会をいただきました。</p>
<h2 id="PHP-Conference-Japan-2025-について"><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan 2025 について</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan は有志による<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> にまつわる国内最大規模のカンファレンスで、一年に一度の頻度で開催されている技術系カンファレンスイベントです。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>としても 2022 年からシルバースポンサーとして協賛させていただいており、 今年はゴールドスポンサーとして協賛規模を拡大し、より積極的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コミュニティへ貢献しました。</p>
<h2 id="スポンサーブースの様子">スポンサーブースの様子</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ブースに来ていただいた方に、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>水」「ステッカー」「クリアファイル」を配布しました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250701/20250701131412.jpg" alt="" /></p>
<p>今年のブースでは、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>予想外クイズ」というコンテンツを企画。 簡単なものからベテランの<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>開発者でも悩む問題まで5つの問題を用意し、回答いただいた方にはテックブログの二次元バーコード入りのコーヒーをプレゼントしました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250701/20250701131129.png" alt="" /></p>
<p>ブースで使うテーブルクロスやロールアップバナーも新調。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250701/20250701125807.png" alt="" /></p>
<h2 id="PHP予想外クイズの解説"><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>予想外クイズの解説</h2>
<p>ブースで出題したクイズの内容をご紹介します。皆さんも挑戦してみてください!</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>のバージョンは8.4です。</p>
<h3 id="問題1正解率22">問題1(正解率:22%)</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか?</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">arr</span> <span class="synStatement">=</span> <span class="synSpecial">[</span><span class="synType">null</span> <span class="synStatement">=></span> <span class="synConstant">1</span><span class="synSpecial">]</span>;
<span class="synIdentifier">print_r</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">arr</span><span class="synSpecial">)</span>;
</pre>
<p><strong>選択肢</strong></p>
<ol>
<li><code>Array ( [null] => 1 )</code></li>
<li><code>Array ( [0] => 1 )</code></li>
<li><code>Array ( [] => 1 )</code></li>
<li><code>TypeError: Illegal offset type</code></li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 3. <code>Array ( [] => 1 )</code></strong></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>では、配列のキーとして<code>null</code>を使用すると、自動的に空文字列<code>''</code>に変換されます。これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>の型変換の仕様によるものです。</p>
<p>この例では、<code>null</code>キーが空文字列に変換され、結果として<code>Array ( [] => 1 )</code>が表示されます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>の<code>print_r</code>関数では、空文字列のキーは<code>[]</code>として表示されます。</p>
<p>参考: <a href="https://www.php.net/manual/ja/language.types.array.php">PHP: 配列</a></p>
<p></details></p>
<h3 id="問題2正解率-35">問題2(正解率 35%)</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">var</span> <span class="synStatement">=</span> <span class="synConstant">1</span> <span class="synStatement">+</span> <span class="synStatement">+</span> <span class="synConstant">8</span>;
<span class="synIdentifier">var_dump</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">var</span><span class="synSpecial">)</span>;
</pre>
<p><strong>選択肢</strong></p>
<ol>
<li><code>int(1)</code></li>
<li><code>int(9)</code></li>
<li><code>int(18)</code></li>
<li><code>Parse error: syntax error</code></li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 2. <code>int(9)</code></strong></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>では、<code>1 + + 8</code>という式は構文エラーではなく、正常に解析されます。この式は実際には<code>1 + (+8)</code>として解釈されます。</p>
<p><code>+8</code>の最初の<code>+</code>は単項<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>です。この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>は値が文字列や論理値の場合はintまたはfloatに変換しますが、すでに数値の場合は実質的に何も変更しません。そのため、<code>+8</code>は単純に<code>8</code>と同じ値になります。</p>
<p>結果として、<code>1 + + 8</code>は<code>1 + 8</code>と同じ計算になり、答えは<code>9</code>となります。</p>
<p>参考: <a href="https://www.php.net/manual/ja/language.operators.precedence.php">PHP: 演算子の優先順位</a>, <a href="https://www.php.net/manual/ja/language.operators.arithmetic.php">PHP: 算術演算子</a></p>
<p></details></p>
<h3 id="問題3正解率25">問題3(正解率:25%)</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか?</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">str</span> <span class="synStatement">=</span> <span class="synConstant">'z'</span>;
<span class="synStatement">$</span><span class="synIdentifier">str</span><span class="synStatement">++</span>;
<span class="synPreProc">echo</span> <span class="synStatement">$</span><span class="synIdentifier">str</span>;
</pre>
<p><strong>選択肢</strong></p>
<ol>
<li><code>z</code></li>
<li><code>aa</code></li>
<li><code>{</code></li>
<li>エラーが発生する</li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 2. <code>aa</code></strong></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>では文字列をインクリメント<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>(<code>++</code>)で操作できます。文字列の末尾が'z'の場合、次の文字'a'が追加され、元の'z'は'a'に変わります。</p>
<p>参考: <a href="https://www.php.net/manual/ja/language.operators.increment.php">PHP: 加算子/減算子</a></p>
<p></details></p>
<h3 id="問題4正解率33">問題4(正解率:33%)</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか?</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">value</span> <span class="synStatement">=</span> <span class="synConstant">0</span>;
<span class="synStatement">switch</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">value</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">case</span> <span class="synConstant">''</span><span class="synStatement">:</span>
<span class="synPreProc">echo</span> <span class="synConstant">'empty string'</span>;
<span class="synStatement">break</span>;
<span class="synStatement">case</span> <span class="synConstant">false</span><span class="synStatement">:</span>
<span class="synPreProc">echo</span> <span class="synConstant">'false'</span>;
<span class="synStatement">break</span>;
<span class="synStatement">case</span> <span class="synConstant">0</span><span class="synStatement">:</span>
<span class="synPreProc">echo</span> <span class="synConstant">'zero'</span>;
<span class="synStatement">break</span>;
<span class="synSpecial">}</span>
</pre>
<p><strong>選択肢</strong></p>
<ol>
<li><code>empty string</code></li>
<li><code>false</code></li>
<li><code>zero</code></li>
<li>何も出力されない</li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 2. <code>false</code></strong></p>
<p>switch文では緩い比較(<code>==</code>)が使用されます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 8.4では以下の比較結果になります。</p>
<ul>
<li><code>0 == ''</code> → <code>false</code></li>
<li><code>0 == false</code> → <code>true</code></li>
<li><code>0 == 0</code> → <code>true</code></li>
</ul>
<p>switch文では上から順番に比較し、最初にマッチしたcaseが実行されます。</p>
<ol>
<li><code>0 == ''</code> は <code>false</code> なのでマッチしない</li>
<li><code>0 == false</code> は <code>true</code> なのでマッチし、<code>case false:</code> が実行される</li>
</ol>
<p>そのため、<code>'false'</code>が出力されます。</p>
<p><strong><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 8.0以降の重要な変更</strong>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 8.0以降では、数値と文字列の比較時の動作が変更されました。数値文字列ではない文字列と数値の比較では、数値を文字列に変換して比較するようになりました。空文字列<code>''</code>は数値文字列ではないため、<code>0 == ''</code>は<code>false</code>になります。</p>
<p>参考: <a href="https://www.php.net/manual/ja/control-structures.switch.php">PHP: switch</a>, <a href="https://www.php.net/manual/ja/types.comparisons.php">PHP: 型の比較表</a></p>
<p></details></p>
<h3 id="問題5正解率41">問題5(正解率:41%)</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか?</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">a</span> <span class="synStatement">=</span> <span class="synConstant">28</span>;
<span class="synStatement">$</span><span class="synIdentifier">b</span> <span class="synStatement">=</span> <span class="synConstant">6</span>;
<span class="synStatement">$</span><span class="synIdentifier">result</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">a</span> <span class="synStatement">|</span> <span class="synStatement">$</span><span class="synIdentifier">b</span>;
<span class="synPreProc">echo</span> <span class="synConstant">"PHPは今年で</span><span class="synSpecial">{</span><span class="synStatement">$</span><span class="synIdentifier">result</span><span class="synSpecial">}</span><span class="synConstant">周年!"</span>;
</pre>
<p><strong>選択肢</strong></p>
<ol>
<li><code>PHPは今年で22周年!</code></li>
<li><code>PHPは今年で28周年!</code></li>
<li><code>PHPは今年で30周年!</code></li>
<li><code>PHPは今年で34周年!</code></li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 3. <code>PHPは今年で30周年!</code></strong></p>
<p>ビットOR演算(<code>|</code>)を使用しています。</p>
<pre class="code" data-lang="" data-unlink>28 = 11100 (2進数)
6 = 00110 (2進数)
OR = 11110 (2進数) = 30 (10進数)</pre>
<p>ビットOR演算では、どちらかのビットが1の場合、結果も1になります。</p>
<p>結果: <code>11110</code> = 16 + 8 + 4 + 2 + 0 = 30</p>
<p>ということで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>は1995年に誕生し、2025年でちょうど30周年を迎えます!</p>
<p>この問題は一見鬼畜な問題ですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>への愛があれば解ける問題でしょう。</p>
<p>参考: <a href="https://www.php.net/manual/ja/language.operators.bitwise.php">PHP: ビット演算子</a></p>
<p></details></p>
<h2 id="追加問題">追加問題</h2>
<p>カンファレンスでは出題しなかった、解き応えのある問題も特別に用意しました。
ぜひ挑戦してみてください!</p>
<h3 id="追加問題1">追加問題1</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか?</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">arr</span> <span class="synStatement">=</span> <span class="synSpecial">[</span><span class="synConstant">0.0</span> <span class="synStatement">=></span> <span class="synConstant">0</span>, <span class="synConstant">0.9</span> <span class="synStatement">=></span> <span class="synConstant">1</span><span class="synSpecial">]</span>;
<span class="synIdentifier">print_r</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">arr</span><span class="synSpecial">)</span>;
</pre>
<p><strong>選択肢</strong></p>
<ol>
<li><code>Array ( [0] => 0 [0.9] => 1 )</code></li>
<li><code>Array ( [0] => 0 [1] => 1 )</code></li>
<li><code>Array ( [0] => 1 )</code></li>
<li><code>Array ( [0.9] => 1 )</code></li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 3. <code>Array ( [0] => 1 )</code></strong></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>では、配列のキーとして<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E2%C6%B0%BE%AE%BF%F4%C5%C0%BF%F4">浮動小数点数</a>を使用すると、自動的に整数に変換されます。この変換は「切り捨て」によって行われます。つまり、小数点以下は単純に切り捨てられます。</p>
<p>この例では</p>
<ul>
<li><code>0.0</code>は整数<code>0</code>に変換される</li>
<li><code>0.9</code>も整数<code>0</code>に変換される(小数点以下が切り捨てられる)</li>
</ul>
<p>同じキー(この場合は<code>0</code>)に対して複数の値が設定された場合、後から設定された値が優先されます。そのため、最終的な配列は<code>[0 => 1]</code>となります。</p>
<p>参考: <a href="https://www.php.net/manual/ja/language.types.array.php">PHP: 配列</a></p>
<p></details></p>
<h3 id="追加問題2">追加問題2</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか?</p>
<pre class="code" data-lang="" data-unlink><?php
$family_emoji = '👨👩👧👦';
echo mb_substr($family_emoji, 0, 1);</pre>
<p>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%BF%A5%C3%A5%AF%A5%B9">シンタックス</a>ハイライトを有効にすると実際の問題と乖離が発生するので無効にしています)</p>
<p><strong>選択肢</strong></p>
<ol>
<li><code>👨👩👧👦</code>(家族の絵文字全体)</li>
<li><code>👨</code>(男性の絵文字のみ)</li>
<li>空の文字列</li>
<li>エラーが発生する</li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 2. <code>👨</code>(男性の絵文字のみ)</strong></p>
<p>複合絵文字「👨👩👧👦」(家族)は、実際には以下の要素で構成されています</p>
<ul>
<li>👨 (U+1F468) - 男性</li>
<li>U+200D - ゼロ幅接合子</li>
<li>👩 (U+1F469) - 女性</li>
<li>U+200D - ゼロ幅接合子</li>
<li>👧 (U+1F467) - 女の子</li>
<li>U+200D - ゼロ幅接合子</li>
<li>👦 (U+1F466) - 男の子</li>
</ul>
<p><code>mb_substr</code>関数は、これらの文字を個別の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Unicode">Unicode</a>文字として扱うため、最初の文字である「👨」(男性)だけを返します。</p>
<p>絵文字を正しく扱うには<code>grapheme_substr</code>関数を使用してください。</p>
<p>参考: <a href="https://www.php.net/manual/ja/function.mb-substr.php">PHP: mb_substr</a>, <a href="https://www.php.net/manual/ja/function.grapheme-substr.php">PHP: grapheme_substr</a></p>
<p></details></p>
<h3 id="追加問題3">追加問題3</h3>
<p>次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コードを実行した場合、どのような結果になるでしょうか?</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">$</span><span class="synIdentifier">arr</span> <span class="synStatement">=</span> <span class="synSpecial">[</span><span class="synConstant">1</span>, <span class="synConstant">2</span>, <span class="synConstant">3</span><span class="synSpecial">]</span>;
<span class="synStatement">foreach</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">arr</span> <span class="synStatement">as</span> <span class="synStatement">&$</span><span class="synIdentifier">value</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">value</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">value</span> <span class="synStatement">*</span> <span class="synConstant">2</span>;
<span class="synSpecial">}</span>
<span class="synStatement">foreach</span> <span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">arr</span> <span class="synStatement">as</span> <span class="synStatement">$</span><span class="synIdentifier">value</span><span class="synSpecial">)</span> <span class="synSpecial">{</span>
<span class="synComment">// 何もしない</span>
<span class="synSpecial">}</span>
<span class="synIdentifier">print_r</span><span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">arr</span><span class="synSpecial">)</span>;
</pre>
<p><strong>選択肢</strong></p>
<ol>
<li><code>Array ( [0] => 2 [1] => 4 [2] => 6 )</code></li>
<li><code>Array ( [0] => 2 [1] => 4 [2] => 4 )</code></li>
<li><code>Array ( [0] => 1 [1] => 2 [2] => 3 )</code></li>
<li>エラーが発生する</li>
</ol>
<p><details>
<summary>正解・解説を見る</summary></p>
<p><strong>正解は 2. <code>Array ( [0] => 2 [1] => 4 [2] => 4 )</code></strong></p>
<p>この問題が最も予想外な結果に感じられたでしょう。</p>
<p>以下のような流れで <code>$arr</code> は <code>[2, 4, 4]</code> になります。</p>
<ol>
<li><p>最初のforeachループでは、<code>&$value</code>で参照を使用し、各要素を2倍にする</p>
<ul>
<li>この時点で<code>$arr</code>は<code>[2, 4, 6]</code>になる</li>
<li><code>$value</code>はまだ<code>$arr[2]</code>への参照を保持している</li>
</ul>
</li>
<li><p>2番目のforeachループでは、参照を使用せず、各反復で<code>$value</code>に値が代入される</p>
<ul>
<li>1回目: <code>$value = 2</code> → <code>$arr[2] = 2</code>(まだ<code>$value</code>が<code>$arr[2]</code>への参照を保持しているので、<code>$arr[2]</code>が2になる)</li>
<li>2回目: <code>$value = 4</code> → <code>$arr[2] = 4</code> (参照経由での代入)</li>
<li>3回目: <code>$value = 4</code> → <code>$arr[2] = 4</code>(参照経由での代入だが、<code>$arr[2]</code>が4で<code>$value</code>も4なので変化なし)</li>
</ul>
</li>
<li><p><code>$arr[2]</code>が上書きされ、<code>[2, 4, 4]</code>になる</p></li>
</ol>
<p>参考: <a href="https://www.php.net/manual/ja/control-structures.foreach.php">PHP: foreach</a></p>
<p></details></p>
<h2 id="おわりに">おわりに</h2>
<p>今年も<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japanに参加して、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コミュニティの盛り上がりを肌で感じることができました。ブースに立ち寄ってくださった皆さん、ありがとうございました。</p>
<p>クイズを通じて、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>の意外な挙動や面白い仕様について知っていただけたでしょうか。普段書いているコードにも、まだまだ知らない発見があることでしょう。</p>
<p>記事でクイズの全問題と解答を紹介しましたので、ぜひ挑戦してみてください。同僚やチームメンバーとも共有して、技術議論のきっかけにしていただければ幸いです。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>はこれからも<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>コミュニティの一員として、技術の発展と交流に貢献していきたいと考えています。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
AI駆動開発に向けた取り組み - AI推進組織の発足とCursorの導入
hatenablog://entry/6802418398486571178
2025-06-25T11:00:00+09:00
2025-06-25T12:00:32+09:00 こんにちは、CIO の菅井です。 従来から AI 推進を横断的なチーム、プロジェクトごとに取り組んでいましたが、2025年4月1日にAIイネーブルメントグループを立ち上げました。 取り組みの一環として、AI支援型エディタ「Cursor」をエンジニア組織に導入しました。今回は、その背景と導入1週間で見えてきた成果や課題についてご紹介します。 AIイネーブルメントグループの立ち上げ 私たちのミッションは大きく2つあります。 1. AIを使ってできることを増やす 管理職やリードクラスでのAI活用を促進し、組織全体でのAI利用を底上げしています。また、ゲーム開発においては試作品やプロトタイプ作成の効率…
<p>こんにちは、CIO の菅井です。</p>
<p>従来から AI 推進を横断的なチーム、プロジェクトごとに取り組んでいましたが、2025年4月1日にAIイ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%CD%A1%BC%A5%D6%A5%EB">ネーブル</a>メントグループを立ち上げました。</p>
<p>取り組みの一環として、AI支援型エディタ「<a href="https://www.cursor.com/ja">Cursor</a>」をエンジニア組織に導入しました。今回は、その背景と導入1週間で見えてきた成果や課題についてご紹介します。</p>
<h2 id="AIイネーブルメントグループの立ち上げ">AIイ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%CD%A1%BC%A5%D6%A5%EB">ネーブル</a>メントグループの立ち上げ</h2>
<p>私たちのミッションは大きく2つあります。</p>
<h3 id="1-AIを使ってできることを増やす">1. AIを使ってできることを増やす</h3>
<p>管理職やリードクラスでのAI活用を促進し、組織全体でのAI利用を底上げしています。また、ゲーム開発においては試作品やプロトタイプ作成の効率化を通じて生産性向上を図っています。さらに、AI活用による新しい体験やエンタテインメントの創出にも取り組んでおり、「<a href="https://jintsuku.jp/">神魔狩りのツクヨミ</a>」などの事例が生まれています。</p>
<h3 id="2-AIができることを増やす">2. AIができることを増やす</h3>
<p>AIが活用できるデータ領域の拡大に注力しています。これまでアクセスできなかったデータやデータ化されていない情報の可視化を進めており、具体的にはミーティング内容の録音・テキスト化をデフォルト化し、開発過程での作り変えデータの保持も行っています。最終的には、人の介在なしにAIエージェントがデータを取得できる基盤設計を目指しています。</p>
<h2 id="なぜCursorを選んだのか">なぜCursorを選んだのか</h2>
<p>Cursorは、AIエージェント開発に必要な機能がバランスよく盛り込まれているエディタです。導入の主な理由は以下の通りです。</p>
<ol>
<li>AIエージェント開発に最適化された機能群</li>
<li>リーズナブルな価格で広く展開が可能</li>
</ol>
<p>ただし、重要なのは「Cursorを標準化したい」わけではありません。</p>
<h2 id="柔軟な開発環境の選択">柔軟な開発環境の選択</h2>
<p>基本的な方針は「自分たちが使っているエディタをベースに、AIエージェント機能を活用する」というものです。ルール設定などはチームごとに共通認識を持って進めてもらっています。</p>
<ul>
<li>PhpStorm + Windsurf <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a></li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a> + <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot</li>
<li>Cursor</li>
<li>Claude Code</li>
</ul>
<p>Cursor はあくまでも「AI エージェントによる開発に慣れるための1つのツール」という位置づけです。</p>
<h2 id="導入1週間で見えてきたこと">導入1週間で見えてきたこと</h2>
<h3 id="良かった点">良かった点</h3>
<p>まだ全員が使いこなせているとは言えませんが、一部の方はヘビーに活用しており、数日で500リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを使っている方もいます。Cursorを使う上で見えてきた課題や改善点を一つずつ潰しているところです。</p>
<p>導入後、社内Slackの #pj-ai-env-improvement チャンネルへの投稿が活性化しました。複数人開発での考察やPoC、小さな社内ツールでの効率化事例、既存プロジェクトへの適用の試行など、様々な取り組みが共有されるようになりました。</p>
<p>また、Cursorの導入によってAIエージェントへの感度が組織全体で高まっており、企画職で使いこなす方も出てきました。社内の福利厚生で利用できるClaude ProでのClaude Code利用者も増加し、ナレッジ共有が盛んになっています。</p>
<h3 id="課題と対応">課題と対応</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BB%A5%AD%A5%E5%A5%EA%A5%C6%A5%A3%A5%DD%A5%EA%A5%B7%A1%BC">セキュリティポリシー</a>の整備やコードベースの取り扱いルールの明確化については、導入前から準備を進めていたため粛々と運用しています。ベストプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスをチーム間で共有する仕組みづくりも継続して取り組んでおり、今後は全社への展開計画も策定していく予定です。</p>
<p>引き続き、AI を活用した効率化と新しい体験創出に取り組んでいきます。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
ついにきた!Google Cloud Spannerの「事前分割(Pre-splitting)」で大規模トラフィックに備えよう
hatenablog://entry/6802418398440778444
2025-05-26T11:00:00+09:00
2025-05-26T11:00:01+09:00 こんにちは、バックエンドエンジニアのごましおです。 Google Cloudは2025年4月28日、Spannerデータベースの新機能「事前分割(Pre-splitting)」の一般提供を開始しました。 事前分割の概要 | Spanner | Google Cloud この機能は、大規模かつ予測可能なトラフィック増加に備えて、データベースの分割点(split points)を事前に設定することで、パフォーマンスの安定性を確保するためのものです。 今回は、この事前分割機能について検証した結果をご紹介します。 コロプラとSpanner分割の歩み コロプラでは2018年からSpannerを利用してお…
<p>こんにちは、バックエンドエンジニアのごましおです。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudは2025年4月28日、Spannerデータベースの新機能「事前分割(Pre-splitting)」の一般提供を開始しました。</p>
<p><a href="https://cloud.google.com/spanner/docs/pre-splitting-overview?hl=ja">事前分割の概要 | Spanner | Google Cloud</a></p>
<p>この機能は、大規模かつ予測可能な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>増加に備えて、データベースの分割点(split points)を事前に設定することで、パフォーマンスの安定性を確保するためのものです。</p>
<p>今回は、この事前分割機能について検証した結果をご紹介します。</p>
<h2 id="コロプラとSpanner分割の歩み"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>とSpanner分割の歩み</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では2018年からSpannerを利用しており、特に新しいゲームのリリースタイミングに合わせて実施しているウォームアップに関しては、このテックブログや勉強会でも発信しております。</p>
<ul>
<li><a href="https://speakerdeck.com/colopl/colopltech-02-03">Spannerウォームアップについて/ColoplTech-02-03 | Speaker Deck</a></li>
<li><a href="https://blog.colopl.dev/entry/2023/02/21/110937">大規模モバイルゲームのローンチを支える技術 を実施しました! | COLOPL Tech Blog</a></li>
</ul>
<p>そんな我々にとって待望の事前分割機能がついに公式でGAになったということで、早速検証してみました。</p>
<h2 id="Spannerにおける分割splittingとは">Spannerにおける分割(splitting)とは</h2>
<p>Spannerは通常、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>変化に応じて自動的にデータを分割(スプリット)または統合(マージ)することで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>内のリソース全体に負荷を分散します。しかし、製品リリースなどの大規模イベントに備える場合、この自動分割だけでは十分なパフォーマンスを確保できないケースがあります。</p>
<h3 id="事前分割機能の登場以前の手動分割">事前分割機能の登場以前の手動分割</h3>
<p>事前分割機能が登場する以前は、大規模な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>増加に備えるために、手動でSpannerに負荷をかけて分割を促す必要がありました。この手法では、<a href="https://github.com/cloudspannerecosystem/gcsb">cloudspannerecosystem/gcsb</a> などのツールを使用して、人工的な負荷をかけることでデータの分割を促していました。</p>
<p>しかし、この手動での分割方法には以下のような課題がありました。</p>
<ul>
<li>時間とコストの問題: 十分な分割を促すためには長時間の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>が必要で、その間のリソース消費によるコストが高くなる</li>
<li>分割数の制御が困難: 負荷をかけても、狙った数の分割数にすることが難しく、結果の予測性が低い</li>
<li>運用の複雑さ: 負荷テストの設定や監視、調整などの運用負担が大きく、エンジニアリングリソースを多く消費してしまう</li>
</ul>
<p>事前分割機能を適切に活用することで、これらの課題を解決できます。</p>
<h2 id="用語">用語</h2>
<p>まず、公式ドキュメントで使用されている用語を整理しておきましょう。</p>
<table>
<thead>
<tr>
<th> 英語 </th>
<th> 日本語 </th>
<th> 説明 </th>
</tr>
</thead>
<tbody>
<tr>
<td> splits </td>
<td> スプリット </td>
<td> 分割された領域のこと </td>
</tr>
<tr>
<td> split points </td>
<td> 分割点 </td>
<td> データを分割する境界となるポイント </td>
</tr>
<tr>
<td> splitting </td>
<td> 分割 </td>
<td> データを分割する処理 </td>
</tr>
<tr>
<td> Pre-splitting </td>
<td> 事前分割 </td>
<td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>増加の前にデータを分割すること </td>
</tr>
<tr>
<td> merging </td>
<td> 統合 </td>
<td> 分割されたデータを統合する処理 </td>
</tr>
<tr>
<td> rebalancing </td>
<td> 再調整 </td>
<td> データの分布を調整する処理 </td>
</tr>
</tbody>
</table>
<p>例: 分割点(split points)を10個作ると、11個のスプリット(splits)が作成されます。</p>
<h2 id="運用上の注意点">運用上の注意点</h2>
<p>事前分割機能を利用するうえで、いくつかの重要な注意点があります。</p>
<h3 id="タイミング">タイミング</h3>
<blockquote><p><strong>分割ポイントは、予想される<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>増加の 7 日前から 12 時間前までに作成する必要があります。</strong></p></blockquote>
<p>という記載があります。
これに伴い、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>の実施方法やリリース時の準備も従来の手法とは変わってきます。</p>
<h4 id="負荷試験"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a></h4>
<p>分割点を作って同日中に試験するのは難しいため、試験前日までに分割点を作成しておくなどの工夫が必要になります。</p>
<h4 id="リリース準備">リリース準備</h4>
<p>従来の手法では、スプリット作成のためのウォームアップは製品リリースの2日前に実行することとなっていました。これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud公式ブログなどでも言及されています。</p>
<p><a href="https://cloud.google.com/blog/ja/products/databases/cloud-spanner-makes-application-launches-easier-with-warmup-and-benchmarking-tool">Cloud Spanner のウォームアップ ツールとベンチマーク ツールでアプリケーションのリリースを簡単に | Google Cloud Japan ブログ</a></p>
<p>これはSpannerのスプリットは負荷がかかっていない状態が続くと内部的に統合(merging)されてしまうため、日を空けすぎるとせっかく作成したスプリットが意味を失ってしまうという事情があったものと考えられます。従来の手法ではリリース日によっては休日作業が必要になってしまっていましたが、事前分割機能を活用することで余裕を持ったリリーススケジュールを設定できるようになります。</p>
<h3 id="パフォーマンスへの一時的な影響">パフォーマンスへの一時的な影響</h3>
<blockquote><p><strong>分割は即時には行われません。分割と再調整が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>に対応できない場合、使用可能なコンピューティング リソースとメモリリソースがスプリットによって使い果たされる可能性があります。この場合、Spanner の作業スケジューラがキューに入れるリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トがさらに増え、レイテンシが増加し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%A4%A5%E0%A5%A2%A5%A6%A5%C8">タイムアウト</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の中止につながる可能性があります。</strong></p></blockquote>
<p>という記載があります。分割点は作成してすぐに効果のある機能ではなく、作成後にSpanner内部で行われる再調整に時間とリソースが使われる機能です。すでにアクセスが来ている状態で、負荷を軽減するために応急処置的に使える機能ではないということに注意する必要があります。</p>
<p>実際にアクセスが来ているテーブルやインデックスに対してオンラインで分割点を追加すると、スロークエリが発生するなど一時的な性能劣化につながることが確認できました。対象のテーブルやインデックス、およびそのインターリーブ配下のテーブルが影響を受ける可能性があります。</p>
<p>アクセスが来ていないテーブルやインデックスに対する分割点の作成がデータベース全体のパフォーマンスに与える影響については、現時点で結論が出ていないため、本稿では記載を控えさせていただきます。</p>
<h3 id="リソースの管理">リソースの管理</h3>
<blockquote><p><strong>テーブル、インデックス、データベースを削除する前に、対応する追加されたすべてのスプリット ポイントが期限切れになっていることを確認する必要があります。これを行うには、分割の有効期限を現在の時刻に設定します。これは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a> レベルの割り当てを再利用するために必要です。</strong></p></blockquote>
<p>という記載があります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>中はテーブルを再作成することもよくありますが、その際に分割点を忘れずに削除(有効期限を過去に更新)するようなオペレーションにしなければなりません。</p>
<h2 id="実装方法">実装方法</h2>
<p>ここでは公式の手順を参考にしつつ、gcloudコマンドを使用した分割点の作成手順をご紹介します。</p>
<p><a href="https://cloud.google.com/spanner/docs/create-manage-split-points?hl=ja">分割ポイントを作成、管理する | Spanner | Google Cloud</a></p>
<h3 id="分割点の作成手順">分割点の作成手順</h3>
<h4 id="1-負荷試験によるスプリット数の見積もり">1. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>によるスプリット数の見積もり</h4>
<p>まず、事前分割なしで想定負荷の試験を数時間程度を目安に実施します。この時点ではスプリットが存在しないため、スロークエリの発生などパフォーマンスが安定しないですが気にしません。CPU使用率が上がりすぎないようにノード(Processing Units)数は調整しましょう。</p>
<p>以下の User テーブルと INDEX を例とします。<code>userId</code>, <code>token</code> はそれぞれ UUID v4 の値が入るとします。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">CREATE</span> <span class="synSpecial">TABLE</span> <span class="synIdentifier">User</span> (
userId STRING(<span class="synConstant">36</span>) <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> PRIMARY KEY,
token STRING(<span class="synConstant">36</span>),
);
<span class="synStatement">CREATE</span> <span class="synSpecial">UNIQUE</span> <span class="synSpecial">INDEX</span> User_token_unique <span class="synSpecial">ON</span> <span class="synIdentifier">User</span>(token);
</pre>
<p>試験実施後、対象テーブルのフルスキャンクエリを発行して実行計画から実行数(Number of executions)を確認します。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> <span class="synIdentifier">COUNT</span>(*) <span class="synSpecial">FROM</span> <span class="synIdentifier">User</span>@{FORCE_INDEX=_BASE_TABLE};
<span class="synStatement">SELECT</span> <span class="synIdentifier">COUNT</span>(*) <span class="synSpecial">FROM</span> <span class="synIdentifier">User</span>@{FORCE_INDEX=User_token_unique};
</pre>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250522/20250522192721.png" alt="実行計画の例" /></p>
<p>実行計画は <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Console や gcloud コマンドでも確認できますが、より詳細な情報を確認するには <a href="https://github.com/cloudspannerecosystem/spanner-cli">spanner-cli</a> が便利です。これは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud 公式ではなく、コミュニティによって開発されている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>ツールです。2025/5/21 時点では「Do not use this tool for production databases as the tool is still alpha quality.」という記載があるため、本番環境での使用には注意が必要です。</p>
<p>spanner-<a class="keyword" href="https://d.hatena.ne.jp/keyword/cli">cli</a> で確認する場合は <code>EXPLAIN ANALYZE</code> を使用します。なお、spanner-<a class="keyword" href="https://d.hatena.ne.jp/keyword/cli">cli</a>では「Number of executions」ではなく単に「Executions」という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%E9%A5%E0%CC%BE">カラム名</a>で表示されます。</p>
<pre class="code" data-lang="" data-unlink>spanner> EXPLAIN ANALYZE SELECT COUNT(*) FROM User@{FORCE_INDEX=_BASE_TABLE};
+----+---------------------------------------------------------------------------------------------------------+---------------+------------+---------------+
| ID | Query_Execution_Plan | Rows_Returned | Executions | Total_Latency |
+----+---------------------------------------------------------------------------------------------------------+---------------+------------+---------------+
| 0 | Serialize Result (execution_method: Row) | 1 | 1 | 76.39 msecs |
| 1 | +- Global Stream Aggregate (execution_method: Row, scalar_aggregate: true) | 1 | 1 | 76.39 msecs |
| 2 | +- Distributed Union (distribution_table: User, execution_method: Row, split_ranges_aligned: false) | 0 | 1 | 76.38 msecs |
| 3 | +- Local Stream Aggregate (execution_method: Row, scalar_aggregate: true) | 0 | 31 | 0.81 msecs |
| 4 | +- Local Distributed Union (execution_method: Row) | 0 | 31 | 0.76 msecs |
| 5 | +- Table Scan (Full scan: true, Table: User, execution_method: Row, scan_method: Automatic) | 0 | 31 | 0.51 msecs |
+----+---------------------------------------------------------------------------------------------------------+---------------+------------+---------------+</pre>
<p>この実行数がスプリットと深い相関関係にあるそうです。以降この実行数=スプリット数のターゲットとしていきます。</p>
<h4 id="2-対象テーブルの再作成">2. 対象テーブルの再作成</h4>
<p>事前分割を作成する前に、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>で使ったテーブルやINDEXは再作成(<a class="keyword" href="https://d.hatena.ne.jp/keyword/DROP">DROP</a> + CREATE)しておきます。</p>
<h4 id="3-インスタンスのノード数を調整する">3. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>のノード数を調整する</h4>
<p><a href="https://cloud.google.com/spanner/docs/pre-splitting-overview?hl=ja#split-count">1ノードにつき分割点を10作るのが推奨</a>されています。例えば、25スプリットが必要な場合は、最低3ノードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が必要になります。</p>
<h4 id="4-分割点を記載したファイルを用意する">4. 分割点を記載したファイルを用意する</h4>
<p>今回はUserテーブルとtokenインデックスを31分割するため、それぞれ30個の分割点を作成します。分割点を作成するためのファイルは以下のようになります。</p>
<p>user_splits.txt</p>
<pre class="code" data-lang="" data-unlink>TABLE User ('08000000-0000-0000-0000-000000000000')
TABLE User ('11000000-0000-0000-0000-000000000000')
...
TABLE User ('F8000000-0000-0000-0000-000000000000')</pre>
<p>token_splits.txt</p>
<pre class="code" data-lang="" data-unlink>INDEX User_token_unique ('08000000-0000-0000-0000-000000000000')
INDEX User_token_unique ('11000000-0000-0000-0000-000000000000')
...
INDEX User_token_unique ('F8000000-0000-0000-0000-000000000000')</pre>
<p>今回はUUIDのカラムを対象にしているため、UUIDの範囲を31等分するような値を用意します。実際のカラム型とデータ範囲に合わせて用意してください。</p>
<h4 id="5-gcloudコマンドで分割点を作成する">5. gcloudコマンドで分割点を作成する</h4>
<pre class="code lang-sh" data-lang="sh" data-unlink>gcloud spanner databases splits add your-database <span class="synStatement">\</span>
<span class="synSpecial">--instance</span><span class="synStatement">=</span>your-instance <span class="synStatement">\</span>
<span class="synSpecial">--splits-file</span><span class="synStatement">=</span>user_splits.txt <span class="synStatement">\</span>
<span class="synSpecial">--project</span><span class="synStatement">=</span>your-project-id
gcloud spanner databases splits add your-database <span class="synStatement">\</span>
<span class="synSpecial">--instance</span><span class="synStatement">=</span>your-instance <span class="synStatement">\</span>
<span class="synSpecial">--splits-file</span><span class="synStatement">=</span>token_splits.txt <span class="synStatement">\</span>
<span class="synSpecial">--project</span><span class="synStatement">=</span>your-project-id
</pre>
<p>分割点作成に関する上限として、1ノードにつき1分間に10個の分割点までと制限されています。例えば、3ノードに対してUserテーブル用の30個とtokenインデックス用の30個を一度に作成できません。ファイルを2つ用意して個別に実行する必要があります。また、Userテーブル用の30個を作ったら1分間隔を空けてtokenインデックス用の30個を作成する必要があります。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># 1ノードにつき1分間に10個の分割点作成の上限に達した場合</span>
$ gcloud spanner databases splits add your-database <span class="synStatement">\</span>
<span class="synSpecial">--instance</span><span class="synStatement">=</span>your-instance <span class="synStatement">\</span>
<span class="synSpecial">--splits-file</span><span class="synStatement">=</span>user_splits.txt <span class="synStatement">\</span>
<span class="synSpecial">--project</span><span class="synStatement">=</span>your-project-id
ERROR: <span class="synPreProc">(</span><span class="synSpecial">gcloud.spanner.databases.splits.add</span><span class="synPreProc">)</span> RESOURCE_EXHAUSTED: Total user split point count processed per minute limit is <span class="synConstant">30</span>.Total number of splits processed <span class="synError">in</span> the last minute was <span class="synConstant">30</span>, the request contains <span class="synConstant">30</span> excessive split points.
</pre>
<h4 id="6-分割点を確認する">6. 分割点を確認する</h4>
<p>以下のクエリを実行して作成した分割点を確認できます。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> * <span class="synSpecial">FROM</span> SPANNER_SYS.USER_SPLIT_POINTS
</pre>
<h4 id="7-実行計画で分割点が使われることを確認する">7. 実行計画で分割点が使われることを確認する</h4>
<p>分割数を見積もるときと同様のフルスキャンクエリを発行して、実行計画から分割点が使われていることを確認します。実行数(Number of executions)が作成した分割数になっていればOKです。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> <span class="synIdentifier">COUNT</span>(*) <span class="synSpecial">FROM</span> <span class="synIdentifier">User</span>@{FORCE_INDEX=_BASE_TABLE};
<span class="synStatement">SELECT</span> <span class="synIdentifier">COUNT</span>(*) <span class="synSpecial">FROM</span> <span class="synIdentifier">User</span>@{FORCE_INDEX=User_token_unique};
</pre>
<h3 id="分割点の削除">分割点の削除</h3>
<p>分割点を削除するための<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>は提供されていないため、有効期限を過去に設定するという方法で削除します。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>gcloud spanner databases splits add your-database <span class="synStatement">\</span>
<span class="synSpecial">--instance</span><span class="synStatement">=</span>your-instance <span class="synStatement">\</span>
<span class="synSpecial">--splits-file</span><span class="synStatement">=</span>user_splits.txt <span class="synStatement">\</span>
<span class="synSpecial">--split-expiration-date</span><span class="synStatement">=</span>2023-01-01T00:00:00Z <span class="synStatement">\</span>
<span class="synSpecial">--project</span><span class="synStatement">=</span>your-project-id
gcloud spanner databases splits add your-database <span class="synStatement">\</span>
<span class="synSpecial">--instance</span><span class="synStatement">=</span>your-instance <span class="synStatement">\</span>
<span class="synSpecial">--splits-file</span><span class="synStatement">=</span>token_splits.txt <span class="synStatement">\</span>
<span class="synSpecial">--split-expiration-date</span><span class="synStatement">=</span>2023-01-01T00:00:00Z <span class="synStatement">\</span>
<span class="synSpecial">--project</span><span class="synStatement">=</span>your-project-id
</pre>
<p>通常の有効期限設定も同じコマンドで行えます。指定しなかった場合のデフォルトは10日間で、最大で30日後まで指定可能です。</p>
<h2 id="検証結果">検証結果</h2>
<p>ちょうど<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>を行っていたプロジェクトがあったため、従来の手法でウォームアップした状態と事前分割機能を用いてウォームアップした状態それぞれで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>を実施してみました。試行回数は多くないですが、従来の手法と比較して事前分割機能を用いたウォームアップでも同等の安定性が得られました。</p>
<h3 id="その他の確認ポイント">その他の確認ポイント</h3>
<h4 id="1-分割点作成にかかる時間">1. 分割点作成にかかる時間</h4>
<p>分割点の作成自体は数秒で完了し、作成後すぐにスプリットが使われる実行計画になることが確認できました。公式ドキュメントでは作成後に最低12時間空けるよう推奨されており、この間にデータの再配置などが裏側で行われているものと思われます。</p>
<h4 id="2-分割点が期限切れになったときの挙動">2. 分割点が期限切れになったときの挙動</h4>
<p>アクセスがある状態で分割点が期限切れになっても、即座にスプリットなしの状態にはなりませんでした。つまり、期限が切れていきなり性能が劣化するということはありません。ドキュメントによれば、管理がユーザーからSpannerに移るだけとのことで、従来のスプリットと同じく負荷に応じて自動的に分割・統合が行われていくと考えられます。</p>
<h2 id="まとめ">まとめ</h2>
<p>Spannerの事前分割機能は、明示的に削除する<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>がないなどオペレーション面でやや不便な点もあるものの、基本的には我々が求めていた機能が提供されたと言える評価でした。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>やリリース前の準備にかかるコストの大幅削減が期待できます。</p>
<p>今回はgcloudコマンドを使って分割点を作成しましたが、クライアントライブラリーでも提供されているため、機能実装することでより使いやすい形で利用できる可能性もあります。</p>
<p>大規模な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>増加を見込むサービスのリリースなどを計画している場合、Spannerの事前分割機能を活用することで、より安定したパフォーマンスの確保が期待できます。</p>
<h2 id="参考資料">参考資料</h2>
<ul>
<li><a href="https://cloud.google.com/spanner/docs/pre-splitting-overview?hl=ja">事前分割の概要 | Spanner | Google Cloud</a> - 公式ドキュメント</li>
<li><a href="https://cloud.google.com/spanner/docs/create-manage-split-points?hl=ja">分割ポイントを作成、管理する | Spanner | Google Cloud</a> - 公式ドキュメント</li>
<li><a href="https://github.com/cloudspannerecosystem/gcsb">cloudspannerecosystem/gcsb</a> - Spannerの負荷テストツール</li>
<li><a href="https://cloud.google.com/blog/ja/products/databases/cloud-spanner-makes-application-launches-easier-with-warmup-and-benchmarking-tool">Cloud Spanner のウォームアップ ツールとベンチマーク ツールでアプリケーションのリリースを簡単に</a> - 事前分割機能が登場する以前のウォームアップに関する情報</li>
</ul>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
Google Cloud Next '25 に参加しました
hatenablog://entry/6802418398374990171
2025-04-25T11:00:00+09:00
2025-04-25T11:00:04+09:00 こんにちは、バックエンドエンジニアの山田 (@yamadashy) です。 4/9(水)から4/11(金)にかけて開催されたGoogle Cloud Next '25に参加してきました。 Google Cloud Nextは、Google Cloudに関する最新情報や今後の展望を共有する年次イベントで、今年もラスベガスのMandalay Bay Convention Centerで開催されました。 コロプラからは、薮、駒崎、そして私の3名が参加。昨年に続いて2年目の参加となりました。今年のカンファレンスでは、生成AIを中心とした多くの発表があり、その中から特に印象に残った内容について詳しくご紹…
<p>こんにちは、バックエンドエンジニアの山田 (<a href="https://x.com/yamadashy">@yamadashy</a>) です。</p>
<p>4/9(水)から4/11(金)にかけて開催された<a href="https://cloud.withgoogle.com/next/25">Google Cloud Next '25</a>に参加してきました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Nextは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudに関する最新情報や今後の展望を共有する年次イベントで、今年もラスベガスの<a href="https://mandalaybay.mgmresorts.com/en/meetings-groups/meeting-convention-facilities.html">Mandalay Bay Convention Center</a>で開催されました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>からは、薮、駒崎、そして私の3名が参加。昨年に続いて2年目の参加となりました。今年のカンファレンスでは、生成AIを中心とした多くの発表があり、その中から特に印象に残った内容について詳しくご紹介させていただきます。</p>
<p>なお、昨年の参加レポートはこちらで詳しく紹介しています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2024%2F07%2F29%2F110000" title="初めての海外出張に行ってきました!【Google Cloud Next ‘24 in Las Vegas】 - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2024/07/29/110000">blog.colopl.dev</a></cite></p>
<h1 id="準備">準備</h1>
<p>約1ヶ月前には公式サイトの<a href="https://cloud.withgoogle.com/next/25/session-library#all">Session Library</a>が公開されます。
予約が必要なセッションはすぐに埋まるので、興味のあるものをなるはやで予約することをおすすめします。</p>
<p>セッションの種類は以下の通りです。</p>
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Keynote">Keynote</a>(基調講演)</li>
<li>Breakout Session(技術解説や事例紹介の講演)</li>
<li>Hands-on Lab(実際に手を動かすワークショップ)</li>
<li>Developer Meetup(開発者同士の交流会)</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Roundtable">Roundtable</a>(少人数のディスカッション形式)</li>
</ul>
<p>また、現地参加の場合は以下のような準備もしておくと良いと思います。</p>
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Roundtable">Roundtable</a>や日本向けのイベントなどは名刺交換なども行うので、十分な枚数の名刺を用意しておく</li>
<li>公式アプリを活用すると、セッション情報や会場マップ、最新のお知らせを確認できて便利</li>
<li>イベント中はセッション以外にも Expo など他の展示もあることを念頭に置いてスケジュールを決める</li>
</ul>
<h1 id="渡航スケジュール"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C5%CF%B9%D2">渡航</a>・スケジュール</h1>
<p>海外出張の準備は、昨年の経験を活かして比較的スムーズに進めることができました。ラスベガス行きのフライトは乗り継ぎを含む長時間の旅でしたが、機内では映画を観たりしてリラックスして過ごしました。</p>
<p>今回の全体的な出張スケジュールは以下の通りです。</p>
<table>
<thead>
<tr>
<th> 日付 </th>
<th> 時刻 </th>
<th> 場所 </th>
<th> 内容 </th>
</tr>
</thead>
<tbody>
<tr>
<td> 4/8(火) </td>
<td> 14:27 </td>
<td> Las Vegas </td>
<td> ハリー・リード国際空港到着 </td>
</tr>
<tr>
<td> 〃 </td>
<td> 18:30 </td>
<td> Mandalay Bay </td>
<td> Japan Welcome Reception </td>
</tr>
<tr>
<td> 4/9(水) </td>
<td> 終日 </td>
<td> Mandalay Bay </td>
<td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next '25 </td>
</tr>
<tr>
<td> 〃 </td>
<td> 19:00 </td>
<td> <a href="https://maps.app.goo.gl/ViXXa3hy5KBHfary5">Bonito Michoacan Mexican Restaurant</a> </td>
<td> GME Dinner </td>
</tr>
<tr>
<td> 4/10(木) </td>
<td> 終日 </td>
<td> Mandalay Bay </td>
<td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next '25 </td>
</tr>
<tr>
<td> 〃 </td>
<td> 19:00 </td>
<td> <a href="https://maps.app.goo.gl/tQChL2LYDBoMqRvAA">アレジアント・スタジアム</a> </td>
<td> Next at Night </td>
</tr>
<tr>
<td> 4/11(金) </td>
<td> 終日 </td>
<td> Mandalay Bay </td>
<td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next '25 </td>
</tr>
<tr>
<td> 〃 </td>
<td> 19:00 </td>
<td> Mandalay Bay </td>
<td> Japan Session & Reception </td>
</tr>
<tr>
<td> 4/12(土) </td>
<td> 08:00 </td>
<td> Las Vegas </td>
<td> ハリー・リード国際空港発 </td>
</tr>
</tbody>
</table>
<p>イベント前日の4/8(火)にラスベガスに到着。長時間のフライトで疲れは隠せませんでしたが、到着後すぐにJapan Welcome Receptionに参加しました。
このイベントには日本からエンジニアだけでなく、営業や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C6%A5%A3%A5%F3%A5%B0">マーケティング</a>など様々な分野・業種の方々が集まっており、普段は接点の少ない方々との交流ができました。エンジニアとも現在話題のGemini 2.5 Proを初め、Cline, Cursor などのコーディングエージェントの話もでき、とても刺激的な時間となりました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424134459.png" alt="" /></p>
<p>今回の滞在先は <a href="https://www.caesars.com/horseshoe-las-vegas/v2">Horseshoe Las Vegas</a> でした。会場までは車で10~15分ほどのホテルです。ホテルのロビーには定番のカジノが広がり、エレベータホールからは話題の<a href="https://www.thesphere.com/">スフィア</a>も見ることができました。</p>
<h1 id="Google-Cloud-Next-25-in-Las-Vegas"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next '25 in Las Vegas</h1>
<p>今年は世界中から30,000名以上の参加者が集まり、日本からも850名以上が参加しました。300以上のセッション、ハンズオンなどが提供され、展示エリアでは多くのパートナー企業がブースを出展し、最新のソリューションやサービスを紹介していました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133352.png" alt="" /></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next 2025 をまとめた記事(英語)が公式で公開されているので、興味のある方はぜひご覧ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.google.com%2Fblog%2Ftopics%2Fgoogle-cloud-next%2Fgoogle-cloud-next-2025-wrap-up%2F" title="Google Cloud Next 2025 Wrap Up | Google Cloud Blog" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://cloud.google.com/blog/topics/google-cloud-next/google-cloud-next-2025-wrap-up/">cloud.google.com</a></cite></p>
<h1 id="Opening-Keynote">Opening <a class="keyword" href="https://d.hatena.ne.jp/keyword/Keynote">Keynote</a></h1>
<p><a href="https://cloud.withgoogle.com/next/25/session-library?session=GENKEY">Opening Keynote: The new way to cloud</a></p>
<p>カンファレンス初日の朝、Mandalay Bay のメインホールで基調講演が開催されました。<br/>
我々は1時間前には会場に到着していましたが、その時点ですでに多くの席が埋まっておりました。次回参加される方は早めの行動をおすすめします。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133458.png" alt="" /></p>
<p>講演には <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud CEO のトーマス・キュリアン氏と、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> および Alphabet CEO のスンダー・ピチャイ氏が登壇。生でスンダー・ピチャイ氏のスピーチを聞けたのは、オフライン参加ならではの貴重な体験でした。</p>
<p>今回のキーノートでは、やはり生成AI関連の話題が中心でした。
エンジニアとして興味深かったのは、マルチエージェントシステム構築を支援する「Agent Development Kit(<a class="keyword" href="https://d.hatena.ne.jp/keyword/ADK">ADK</a>)」や、エージェント間の通信を可能にする「Agent2Agent Protocol(A2A)」の紹介です。A2A は <a class="keyword" href="https://d.hatena.ne.jp/keyword/SNS">SNS</a> でも話題になっており、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/MCP">MCP</a>(Model Context Protocol)と比較されていましたが、A2A はエージェント間の協調動作に特化しており用途や思想が異なるようですね。</p>
<p>講演中には製品の実際の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>を用いたデモも多数紹介されました。<br/>
たとえば、Vertex AI の 「Vertex Media Studio」 を使ってテキストから高品質な動画を生成する「Veo 2」や、テキストから音楽を生成できる「Lyria」のデモは、AI の実用性と創造性の可能性を強く感じさせるものでした。</p>
<p>キーノートの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AB%A5%A4%A5%D6">アーカイブ</a>は以下でご覧いただけます。</p>
<p><iframe width="560" height="315" src="https://www.youtube.com/embed/Md4Fs-Zc3tg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="Google Cloud Next 25 Opening Keynote: The new way to cloud"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=Md4Fs-Zc3tg">www.youtube.com</a></cite></p>
<h1 id="セッション">セッション</h1>
<p>Cloud Next では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud の製品/サービスの発表の他にも、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> のパートナー企業などが自社のソリューションを紹介するセッションも多数開催されていました。</p>
<p>そのなかでも現地のメンバーが気になったセッションをピックアップしてご紹介します。</p>
<h2 id="Whats-next-for-Google-Cloud-databases-in-the-AI-era"><a href="https://cloud.withgoogle.com/next/25/playlists?session=SPTL200">What's next for Google Cloud databases in the AI era</a></h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudのデータベースがAI時代にどう進化しているかについて紹介がありました。</p>
<p>特に印象的だったのは、データベースに対する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>インターフェースの進化です。BigQueryやSpannerで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>クエリが可能になり、例えば「過去30日間の売上トップ10の商品を教えて」のような質問を直接投げかけることができるようになりました。</p>
<p>AlloyDBでは、AIクエリエンジンが導入され、<a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>内に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>表現を組み込めるようになったほか、ベクトル検索も大幅に最適化されています。標準のPostgresと比較して最大10倍の高速化が実現し、テキストだけでなく画像や動画も含めたマルチモーダル検索が可能になったとのことです。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>に精通していないユーザーでもデータにアクセスしやすくなりそうなのは大きな変化だと感じました。
特に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>でのクエリ機能は、データ分析の敷居を下げ、より多くの人がデータドリブンな意思決定を行える可能性を秘めていますね。一方で、負荷やコストが読めないところは慎重になる必要はありそうです。</p>
<h2 id="Accelerate-your-code-reviews-with-Gemini-Code-Assist-in-GitHub"><a href="https://cloud.withgoogle.com/next/25/playlists?session=BRK2-078">Accelerate your code reviews with Gemini Code Assist in GitHub</a></h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>上でGemini Code Assistによるコードレビューの自動化が紹介されました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>から導入したGeminiは、プルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト作成時に自動的にレビュアーとして加わり、変更内容の要約・改善提案・マージ準備状況評価を短時間で提供します。<code>/gemini</code>コマンドで特定コードの質問も可能で、<code>.gemini/styleguide.md</code>ファイルで企業独自の規約に合わせたカスタマイズができます。</p>
<p>導入事例では、レビューサイクル短縮や早期バグ発見など顕著な効果が報告され、90%の開発者が生産性向上を実感したとのことです。デモでは実際のプルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トでGeminiが行うレビューやアド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>が示されました。</p>
<p>CodeRabbitや<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilotなど他のツールとの比較において、レビューの精度や使い心地が気になるところですね。</p>
<h2 id="Build-AI-agents-on-Cloud-Run"><a href="https://cloud.withgoogle.com/next/25/session-library?session=BRK3-002">Build AI agents on Cloud Run</a></h2>
<p>Cloud Run を利用してAIエージェントを構築・実行する方法が紹介されました。</p>
<p>LangChainの共同創設者であるHarrison氏がLangGraph<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>について説明した後、Cloud Runがエージェント実行に最適な理由として、自動スケーリング、使用時のみの課金体系、高い信頼性、ストリーミングのネイティブサポートなどが紹介されました。</p>
<p>最も印象的だったのは、Code Rabbitの事例です。このサービスは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>などのプラットフォーム上でAIによるコードレビューを自動化しています。特に興味深かったのは、AIが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>をクローンし、影響範囲を把握するためにrip-<a class="keyword" href="https://d.hatena.ne.jp/keyword/grep">grep</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/grep">grep</a>検索を実行するなど、人間のような探索方法でコードを理解していく点です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>図や実際の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>数なども共有されており、コードレビューアシスタントの裏側を垣間見れて良いセッションでした。</p>
<h2 id="Maximize-the-availability-and-performance-of-your-Cloud-SQL-workloads"><a href="https://cloud.withgoogle.com/next/25/session-library?session=BRK2-109">Maximize the availability and performance of your Cloud SQL workloads</a></h2>
<p>Cloud <a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>のEnterprise Editionについて紹介されたセッションでした。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>のカスタムArmベースのプロセッサ(プレビュー、6:14〜)を利用することで、N2に比べて48%ものパフォーマンス向上が見込めるとのことです。</p>
<p>また、「optimize write」(9:57〜)機能についても解説があり、2h 以上にわたるwrite<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%EB%A1%BC%A5%D7%A5%C3%A5%C8">スループット</a>の向上や、98%以下のwrite<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%A4%A5%C6%A5%F3%A5%B7%A1%BC">レイテンシー</a>の実現が強調されていました。</p>
<p>さらに、「Managed Connection pooling」(12:52〜)の紹介もあり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%EB%A1%BC%A5%D7%A5%C3%A5%C8">スループット</a>が5倍、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%A4%A5%C6%A5%F3%A5%B7%A1%BC">レイテンシー</a>も85%改善されるなど、接続管理の効率化による大幅なパフォーマンス向上が示されていました。</p>
<h2 id="Solve-real-time-AI-challenges-Bigtable-and-BigQuery-in-Spotifys-music-recommendation-engine"><a href="https://cloud.withgoogle.com/next/25/session-library?session=BRK2-105#all">Solve real-time AI challenges: Bigtable and BigQuery in Spotify's music recommendation engine</a></h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Spotify">Spotify</a>の音楽レコメンデーションシステム(8:35〜)では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%A4%A5%C6%A5%F3%A5%B7%A1%BC">レイテンシー</a>と新鮮さの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%EC%A1%BC%A5%C9%A5%AA%A5%D5">トレードオフ</a>という課題に直面しています。
このセッションでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Storage, <a class="keyword" href="https://d.hatena.ne.jp/keyword/Bigtable">Bigtable</a>, Dataflow, BigQueryなどの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのデータ分析基盤を活用して、この課題をどのように解決しているかについて、具体的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B1%A1%BC%A5%B9%A5%B9%A5%BF%A5%C7%A5%A3">ケーススタディ</a>として解説されました。</p>
<h2 id="Go-developers-meetup"><a href="https://cloud.withgoogle.com/next/25/session-library?session=MTUP203#all">Go developers meetup</a></h2>
<p>Go言語ユーザーが集まり、どのようなきっかけでGo言語を使い始めたのか(<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>がきっかけという方が多かったです)などをグループでディスカッションしました。技術的な新しい学びは多くありませんでしたが、さまざまなバックグラウンドを持つ参加者と、Go言語歴や経験について気軽に語り合うことができ、とても楽しい時間となりました。</p>
<p>共通のトピックがあることで、初対面の方とも自然にコミュニケーションが生まれるのが meetup の魅力です。他の開発者と交流したい方には、meetup はぜひおすすめしたいセッションです。</p>
<h2 id="Transform-cloud-operations-and-management-with-generative-AI"><a href="https://cloud.withgoogle.com/next/25/session-library?session=BRK2-032#all">Transform cloud operations and management with generative AI</a></h2>
<p>GoogleCloudコンソールの至るところにAIアシスタンス機能が実装されたことを紹介するセッションでした。
ローコードでインフラ構築ができるApplication Design Center、チャットサポートのCloud Assist、コスト最適化をサポートするCloud Hub、アプリケーションのメトリクスやログを横断的に見ることができるApplication Monitoringなどが紹介されました。</p>
<p>GoogleCloudのドキュメントを調べなくても答えてくれるCloud Assistはとても便利にいますぐでも使えそうです。Application Desing Centerは使ってみないと実用できるかはまだわからなさそうでした。Application MonitoringはCloud Traceと統合されて、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud上にシグナルを流しとけばAIパワーでいい感じに問題特定できるようになったりすると、Observability関係でアプリケーション実装者が気にすることが減り、とても便利になりそうな未来が見えました。</p>
<h2 id="Whats-new-with-Memorystore-for-Valkey"><a href="https://cloud.withgoogle.com/next/25/session-library?session=BRK2-110#all">What's new with Memorystore for Valkey</a></h2>
<p>2025/4/2にGAとなったMemoryStore for Valkeyのセッションです。
Valkey Recommended for all new workloadsと言っていたので、もう使うしかありません。
Valkey8.0で7.2に比べて並列処理性能が向上した部分などについても解説していました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/MLB">MLB</a>のリアルタイムstatsデータを配信するシステム周辺のValkeyとMemoryStoreの使い方についても聞くことができました。ほぼリードオンリーの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>とはいえ膨大なデータがどのくらい発生してどのように捌いているのか、生々しいデータが聞けて興味深かったです。</p>
<h1 id="ラウンドテーブル">ラウンドテーブル</h1>
<p>カンファレンス期間中、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DA%A5%B7%A5%E3">スペシャ</a>リストと直接対話できるラウンドテーブルに参加しました。少人数制の対話形式で行われたため、普段の技術セッションでは聞けないような具体的な質問もでき、非常に有意義な時間となりました。</p>
<h1 id="Cloud-Next-展示エリア">Cloud Next 展示エリア</h1>
<p>展示エリアでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>のパートナーやソリューションプロバイダーが多数出展していました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133932.png" alt="" /></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133918.png" alt="" /></p>
<p>特に印象的だったのは以下のブースです。</p>
<h2 id="Anthropic-ブース">Anthropic ブース</h2>
<p>Anthropicのブースでは、Claudeや<a class="keyword" href="https://d.hatena.ne.jp/keyword/SDK">SDK</a>のハンズオンセッションが実施されていました。残念ながら時間の制約で直接の対話は叶いませんでしたが、日頃からClaudeを活用している身として、開発チームの方々を直接拝見できたことは貴重な経験でした。また、Anthropicのブランドロゴ入りペンを記念品としていただき、カンファレンスの思い出の品として大切にしています。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133841.png" alt="" /></p>
<h2 id="Datadog-ブース">Datadog ブース</h2>
<p>Datadogのブースでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>で日常的に活用しているDatadogについて、エンジニアの方々と気軽に情報交換をさせていただきました。技術的な詳細まで踏み込むことは叶いませんでしたが、Datadogのエンジニアチームの雰囲気を直接感じられる貴重な機会となりました。また、Datadogのブランドロゴ入りソックスを記念品としていただきました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133852.png" alt="" /></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424134052.png" alt="" /></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>はDatadogに関する記事をいくつか出しているので、興味のある方はぜひご覧ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2024%2F12%2F16%2F110000" title=" dd-trace-phpとOpenTelemetry Collectorを使ってDatadogにトレースを送る際のハマりポイントと解決方法 - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2024/12/16/110000">blog.colopl.dev</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffindy-tools.io%2Fproducts%2Fdatadog%2F5%2F391" title="Google Cloud RunでのDatadog APM活用事例" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://findy-tools.io/products/datadog/5/391">findy-tools.io</a></cite></p>
<h2 id="Redis-ブース">Redis ブース</h2>
<p>3日目に立ち寄ったRedisブースではルーレットを連続で目押ししてうまく全部止められたら景品がもらえました。
私はRedisカーをもらいました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133911.png" alt="" /></p>
<h2 id="Google-Cloud認定資格ラウンジ"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud認定資格ラウンジ</h2>
<p>以前、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud主催の「E.G.G. Japan」というプログラムで、Professional Cloud Developer認定資格を取得しており、認定資格保持者専用のラウンジに入ることができました。</p>
<p>内部は空港のラウンジのような空間で、ソファが配置されており、専用のお菓子やコーヒーを楽しみながらゆったり休憩できました。また、認定資格保持者限定のバッジもいただけました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133955.png" alt="" /></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424133943.png" alt="" /></p>
<p>「E.G.G. Japan」プログラムの詳細や参加体験については、以下の記事で詳しく紹介しています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2023%2F04%2F19%2F105953" title="Google Cloud 主催の E.G.G. Japan に 参加し Professional Cloud Developer を取得しました! - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2023/04/19/105953">blog.colopl.dev</a></cite></p>
<h1 id="Cloud-Next-その他イベント">Cloud Next その他イベント</h1>
<h2 id="Next-at-Night">Next at Night</h2>
<p>カンファレンスの2日目には、Allegiant Stadium で開催された Next at Night に参加しました。
開演は19:00からで、ラスベガス出身の <a class="keyword" href="https://d.hatena.ne.jp/keyword/The%20Killers">The Killers</a> をヘッドライナーに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B0%A5%E9%A5%DF%A1%BC%BE%DE">グラミー賞</a>受賞者 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Wyclef%20Jean">Wyclef Jean</a> と新進気鋭のシンガーソングライター Tate Renner が特別出演。技術カンファレンスの参加者限定のコンサートで、リフレッシュするのに最適なイベントでした。</p>
<p><a href="https://cloud.withgoogle.com/next/25?session-details=NXN-001">https://cloud.withgoogle.com/next/25?session-details=NXN-001</a></p>
<h2 id="日本語セッションと懇親会">日本語セッションと懇親会</h2>
<p>カンファレンス最終日の夕方には、日本人参加者向けに日本語でのおさらいセッションが開催されました。主要な発表内容を日本語で解説してくれるため、英語のセッションで聞き逃した部分を補完することができました。</p>
<p>このセッションで話された内容はこちらのページから資料を確認できます。</p>
<p><a href="https://cloud.google.com/resources/content/intl/ja-jp/nextlv25-jp-recap?hl=ja">https://cloud.google.com/resources/content/intl/ja-jp/nextlv25-jp-recap?hl=ja</a></p>
<p>セッション後には日本人参加者による懇親会も開催され、普段関わることのない企業の方々とも情報交換できたのは非常に貴重な経験でした。</p>
<h1 id="エンタメ体験">エンタメ体験</h1>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>はエンターテインメントを大切にする会社です。カンファレンス以外の時間もラスベガスのエンターテインメントを存分に楽しみました。</p>
<h2 id="シルクドソレイユカー">シルクドソレイユ「カー」</h2>
<p>世界的に有名な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%EB%A5%AF%A1%A6%A5%C9%A5%A5%A1%A6%A5%BD%A5%EC%A5%A4%A5%E6">シルク・ドゥ・ソレイユ</a>の「<a href="https://www.cirquedusoleil.com/ka">カー</a>」を観賞しました。このショーは、垂直に動く巨大なステージを特徴とし、武術とアクロバティックなパフォーマンスが融合した圧巻のエンターテインメントです。</p>
<p>少し印象的だったのは、開演前のパフォーマンスで、おそらく準備運動の一貫かと思われますが、観客の期待感を高める良いア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>だなと感じました。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424134004.png" alt="" /></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424134016.png" alt="" /></p>
<h2 id="スフィア">スフィア</h2>
<p>ラスベガスの新しいランドマークである<a href="https://www.thesphere.com/">スフィア</a>を訪れました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next 25の前日には特別な発表会が開催され、1939年公開の名作映画『<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A5%BA%A4%CE%CB%E2%CB%A1%BB%C8">オズの魔法使</a>(The Wizard of Oz)』をスフィアの16Kスクリーンで上映するプロジェクトが<a href="https://blog.google/products/google-cloud/sphere-wizard-of-oz/">発表</a>されていました。</p>
<p>今回は上映を見れませんでしたが、Keynotesでも紹介されていたこのプロジェクトは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudと生成AIを活用して、オリジナルの精神を尊重しつつ前例のない没入型体験を実現していると話されており、来年も参加できればぜひ見に行きたいですね。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424134031.png" alt="" /></p>
<h2 id="HyperX-Arena-Las-Vegas">HyperX Arena Las Vegas</h2>
<p>eスポーツ専用アリーナである<a href="https://hyperxarenalasvegas.com/">HyperX Arena Las Vegas</a>も訪問しました。プロゲーマーの試合の実況が本格的に行われており、ゲーム会社の社員として非常に興味深い場所でした。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20250424/20250424134041.png" alt="" /></p>
<h1 id="おわりに">おわりに</h1>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next '25は、最新技術の動向を知るだけでなく、世界中の様々な職種の方々と交流できる、非常に刺激的で学びの多いイベントでした。生成AIや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>インフラの進化を肌で感じ、日々の業務や今後のプロジェクトに活かせるヒントがたくさん得られたのは大きな収穫です。</p>
<p>また、現地のブース展示や他社の方々との会話を通じて、ドキュメントや動画だけでは得られないリアルな気づきや、新たな視点にも多く出会えました。こうした偶発的な出会いや体験こそが、オフライン参加の醍醐味だと改めて感じました。</p>
<p>この記事が、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Nextへの参加を検討されている方や、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>技術に関心をお持ちの方にとって、少しでも参考になれば幸いです。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p>
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />
<a href="https://colopl.connpass.com/">connpass</a> および <a href="https://x.com/colopl_tech">X</a> で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。
</p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
Findyイベント「次世代DB戦略を支えるNewSQL」に登壇しました
hatenablog://entry/6802418398328594801
2025-02-21T15:53:02+09:00
2025-02-21T15:53:02+09:00 こんにちは、バックエンドエンジニアの山田です。 コロプラのPlatform Engineerの尾山が、2024年2月19日(水)に開催された「次世代DB戦略を支えるNewSQL 〜導入企業が語る導入背景と今後の展望〜」にて「Cloud Spanner 導入で実現した快適な開発と運用について」というタイトルで登壇させていただきました。 findy-tools.connpass.com 登壇内容について 登壇では、2017年頃にコロプラが直面していたデータベースの課題から、Cloud Spannerの採用に至った経緯をお話しさせていただきました。 MySQLの複雑化する運用課題に対し、Cloud …
<p>こんにちは、バックエンドエンジニアの山田です。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のPlatform Engineerの尾山が、2024年2月19日(水)に開催された「次世代DB戦略を支えるNewSQL 〜導入企業が語る導入背景と今後の展望〜」にて「Cloud Spanner 導入で実現した快適な開発と運用について」というタイトルで登壇させていただきました。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffindy-tools.connpass.com%2Fevent%2F343774%2F" title="次世代DB戦略を支えるNewSQL 〜導入企業が語る導入背景と今後の展望〜 (2025/02/19 19:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://findy-tools.connpass.com/event/343774/">findy-tools.connpass.com</a></cite></p>
<h2 id="登壇内容について">登壇内容について</h2>
<p>登壇では、2017年頃に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>が直面していたデータベースの課題から、Cloud Spannerの採用に至った経緯をお話しさせていただきました。
<a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a>の複雑化する運用課題に対し、Cloud Spannerという選択がどのような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%D1%A5%AF">インパク</a>トをもたらしたのか。また、大規模アクセス時の知見など、実際の運用経験から得られた学びについても共有させていただきました。</p>
<p>より詳細な内容については、ぜひスライドをご覧ください。</p>
<p><iframe id="talk_frame_1327281" class="speakerdeck-iframe" src="//speakerdeck.com/player/d4061f0bb64544acb0e45727adaf18c2" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/colopl/implementing-cloud-spanner-achieving-comfortable-development-and-operations">speakerdeck.com</a></cite></p>
<p>また、イベントの模様は<a href="https://findy-tools.io/events/04c7d4310295bc6358b8">アーカイブ動画</a>でもご覧いただけます。</p>
<h2 id="おわりに">おわりに</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、以前にも「Cloud Spanner への挑戦と今」という勉強会を実施するなど、積極的に技術共有を行ってきました。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2022%2F02%2F24%2F105350" title="COLOPL Tech 勉強会 「Cloud Spanner への挑戦と今」を実施しました! - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2022/02/24/105350">blog.colopl.dev</a></cite></p>
<p>今後も積極的に各種イベントへの参加や技術共有を続けていきたいと考えております。
引き続きご注目いただければ幸いです。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
コロプラの「オンボーディング事例」をFindyイベントで紹介しました
hatenablog://entry/6802418398326140478
2025-02-06T11:00:00+09:00
2025-02-06T11:00:00+09:00 こんにちは、バックエンドエンジニアの山田です。 2025年1月15日(水)に開催されたFindyのオンラインイベント「エンジニアがチームで活躍するまでの『オンボーディング事例』~新メンバー側と受け入れ側に学ぶ~」にて、弊社の田中諒(たなりょ)が登壇しましたのでご報告させていただきます。 findy.connpass.com イベント概要 本イベントは、エンジニアのオンボーディングにおける課題や工夫について、新メンバー側と受け入れ側の両者の視点から知見を共有することを目的として開催されました。特に、以下のような課題に焦点を当てています。 ジョイン前からの高い期待が重圧となり、パフォーマンスの妨げ…
<p><span style="font-weight: 400;">こんにちは、バックエンドエンジニアの山田です。</span></p>
<p><span style="font-weight: 400;">2025年1月15日(水)に開催されたFindyのオンラインイベント「エンジニアがチームで活躍するまでの『オンボーディング事例』~新メンバー側と受け入れ側に学ぶ~」にて、弊社の田中諒(たなりょ)が登壇しましたのでご報告させていただきます。</span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffindy.connpass.com%2Fevent%2F339398%2F" title="エンジニアがチームで活躍するまでの「オンボーディング事例」~新メンバー側と受け入れ側に学ぶ~ (2025/01/15 12:00〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><a href="https://findy.connpass.com/event/339398/"> </a><a href="https://findy.connpass.com/event/339398/">findy.connpass.com</a></p>
<h1 id="イベント概要"><span style="font-weight: 400;">イベント概要</span></h1>
<p><span style="font-weight: 400;">本イベントは、エンジニアのオンボーディングにおける課題や工夫について、新メンバー側と受け入れ側の両者の視点から知見を共有することを目的として開催されました。特に、以下のような課題に焦点を当てています。</span></p>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">ジョイン前からの高い期待が重圧となり、パフォーマンスの妨げになっていないか</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">受け入れ側の"お手並み拝見感"が出ていないか</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">専門的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知識の効率的な共有方法</span></li>
</ul>
<h1 id="登壇内容について"><span style="font-weight: 400;">登壇内容について</span></h1>
<p><span style="font-weight: 400;">弊社は「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のオンボーディングを採用から語りたい」というテーマで登壇し、バックエンドエンジニア部署におけるオンボーディングの取り組みについて紹介しました。</span></p>
<p><span style="font-weight: 400;">ゲーム業界が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%C3%A5%C9%A5%AA%A1%BC%A5%B7%A5%E3%A5%F3">レッドオーシャン</a>と言われる中で、「なぜ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C3%E6%C5%D3%BA%CE%CD%D1">中途採用</a>を積極的に行うのか?」という問いから話は始まります。ただの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%F3%A5%D1%A5%EF%A1%BC">マンパワー</a>確保ではなく、組織の持続的な成長と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%CE%A5%D9%A1%BC%A5%B7%A5%E7%A5%F3">イノベーション</a>を生み出すために、社内の「知見共有の活性化」が不可欠であるという考えが根底にあります。新たに加わるエンジニアが、これまでの経験を活かしながらスムーズにチームに馴染み、成果を発揮する。そしてそれを組織的な知見の獲得と活用に繋げていく。それが、私たちのオンボーディングの位置付けです。</span></p>
<p><span style="font-weight: 400;">この考えのもと、採用プロセスから配属後までの一貫したストーリーを構築しています。「どのような経歴を持つエンジニアが、どのような期待を受けてチームに加わるのか」を現場としっかり共有することで、受け入れ側の課題を最小化し、新メンバーが早期に活躍できる環境づくりを進めています。</span></p>
<p><span style="font-weight: 400;">具体的な取り組みや施策の詳細については、ぜひこちらの登壇資料をご覧ください。</span></p>
<p> </p>
<p><iframe id="talk_frame_1310617" class="speakerdeck-iframe" src="//speakerdeck.com/player/66f95a0ff8f54beebbdd91d23b1c4ea6" width="710" height="399" style="aspect-ratio: 710/399; border: 0; padding: 0; margin: 0; background: transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/colopl/findy-event-colopl-the-journey-from-recruitment-to-onboarding-ae23e9b6-3ce7-4471-b146-20a17f6eb14b">speakerdeck.com</a></cite></p>
<p> </p>
<p><span style="font-weight: 400;">また、イベントの模様は</span><a href="https://findy-code.io/events/GW0wqkk5_0KEr?fr=event_archive_20250115"><span style="font-weight: 400;">アーカイブ動画</span></a><span style="font-weight: 400;">でもご覧いただけます。</span></p>
<h1 id="おわりに"><span style="font-weight: 400;">おわりに</span></h1>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では今後も、エンジニアの成長と組織の発展に関する知見を積極的に発信していく予定です。引き続きご注目いただければ幸いです。</span></p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
【資料公開】PHP Conference Japan 2024 に登壇・出展させていただきました
hatenablog://entry/6802418398313938809
2024-12-24T11:00:00+09:00
2024-12-24T11:00:00+09:00 こんにちは、 Platform Engineer の工藤です。 12 月 22 日 (日曜) に開催された PHP Conference Japan 2024 にて「怖くない!ゼロから始めるPHPソースコードコンパイル入門」というタイトルで登壇させていただき、また株式会社コロプラもシルバースポンサーとして出展させていただきました。 PHP Conference Japan について PHP Conference Japan は有志によるプログラミング言語 PHP にまつわる国内最大規模のカンファレンスで、一年に一度の頻度で開催されている技術系カンファレンスイベントです。 コロプラとしても 20…
<p>こんにちは、 Platform Engineer の工藤です。</p>
<p>12 月 22 日 (日曜) に開催された <a href="https://phpcon.php.gr.jp/2024/">PHP Conference Japan 2024</a> にて「怖くない!ゼロから始める<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>入門」というタイトルで登壇させていただき、また株式会社<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>もシルバースポンサーとして出展させていただきました。</p>
<h2 id="PHP-Conference-Japan-について"><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan について</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan は有志による<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> にまつわる国内最大規模のカンファレンスで、一年に一度の頻度で開催されている技術系カンファレンスイベントです。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>としても 2022 年から協賛させていただいており、 <a href="https://blog.colopl.dev/entry/2023/10/26/110000">2023 年同様</a>今年もスポンサーブースの出展をさせていただきました。</p>
<h2 id="スポンサーブース出展について">スポンサーブース出展について</h2>
<p>スポンサーブースでは「あなたの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Awesome">Awesome</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>を教えてください!」という企画を実施させていただき、来場いただいた方々に「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Awesome">Awesome</a>」と感じる <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> にまつわるあれこれを投票していただきました!</p>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/phpcon?src=hash&ref_src=twsrc%5Etfw">#phpcon</a> にて「あなたの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Awesome">Awesome</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>を教えてください!」というアンケートを実施させていただきました!<br><br>多くの方にブースへお立ち寄りいただき、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>の開発技術について活発な議論ができました。<br>貴重なご意見をありがとうございました! <a href="https://t.co/o6aejWcSIW">pic.twitter.com/o6aejWcSIW</a></p>— COLOPL_Tech (@colopl_tech) <a href="https://twitter.com/colopl_tech/status/1870746390030369255?ref_src=twsrc%5Etfw">2024年12月22日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<p>ダントツで得票数が多かった Laravel や、地位を確立し普及期に入ったことがわかる PHPStan の得票数、そして何より <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan というイベントへの投票など、国内における <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> の今を知ることができる楽しい企画になったのではないかと思います!</p>
<p>特に出展メンバーの中では <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a> のシールがとても規則的に貼られていて、テスタビリティを重視する人達の特性が現れてそう、と話題になったりもしました。</p>
<h2 id="登壇について">登壇について</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>からは Platform Engineer の工藤が「怖くない!ゼロから始める<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>入門」という内容で登壇させていただきました。登壇の様子やスライド資料などは以下で確認できます!</p>
<p><iframe width="560" height="315" src="https://www.youtube.com/embed/XZLzqHPlLh8?start=10384&feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="PHP Conference Japan 2024 - Track 2"></iframe></p>
<p><iframe id="talk_frame_1310619" class="speakerdeck-iframe" src="//speakerdeck.com/player/bdfae8c8a8404f289793002c667cdef4" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> </p>
<p>緊張から早口になったり等反省すべき点も多々見受けられますが、予想よりも多くの方に見ていただくことができとても有意義なものにしていただけたのではないかと感じております。質疑応答にて <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>することを目標に C <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%E9">コンパイラ</a>を自作している方がいらっしゃったりと、登壇者としてもとても良い刺激を受けることができました!</p>
<p>また、登壇内容については内容や質疑応答への補足を含めた記事を予定しております!</p>
<h3 id="おわりに">おわりに</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、今後も積極的に各種イベントに参加していきたいと考えており、来年は更に積極的なコミュニティへの参加・貢献ができるようもう一歩踏み込んで行きたいと考えております!まだまだ未熟な点も多いですが、今後ともよろしくお願いいたします!</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
dd-trace-phpとOpenTelemetry Collectorを使ってDatadogにトレースを送る際のハマりポイントと解決方法
hatenablog://entry/6802418398311043596
2024-12-16T11:00:00+09:00
2024-12-16T11:00:02+09:00 こんにちは、LCE(Launch Coordination Engineering)チームとして新作ゲーム開発の支援を行っている駒崎(@dkkoma)です。 コロプラではLaravelアプリケーションの計装にDatadog製のトレーシングライブラリであるdd-trace-phpを利用しています。 今回、新作ゲーム開発プロジェクトのCloudRun環境においてDatadogにトレース情報を送る経路を構築する中でいくつかハマりどころがあったため、その内容と解決方法を解説したいと思い、こちらの記事を書くことにしました。 構成 アプリケーションからDatadogまでの経路はLaravel(dd-tra…
<p>こんにちは、LCE(Launch Coordination Engineering)チームとして新作ゲーム開発の支援を行っている駒崎(<a href="https://twitter.com/dkkoma">@dkkoma</a>)です。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではLaravelアプリケーションの計装にDatadog製のトレーシングライブラリである<a href="https://github.com/DataDog/dd-trace-php">dd-trace-php</a>を利用しています。</p>
<p>今回、新作ゲーム開発プロジェクトのCloudRun環境においてDatadogにトレース情報を送る経路を構築する中でいくつかハマりどころがあったため、その内容と解決方法を解説したいと思い、こちらの記事を書くことにしました。</p>
<h2 id="構成">構成</h2>
<p>アプリケーションからDatadogまでの経路はLaravel(dd-trace-<a class="keyword" href="https://d.hatena.ne.jp/keyword/php">php</a>) -> OpenTelemetry Collector(datadogreceiver + otlpexporter) -> Datadog Agent -> Datadogというパターンで構築しています。Datadog AgentがOTLP Receiverを実装しているのとCloudRunのサービス間認証を利用する都合で、OpenTelemetry CollectorからDatadog AgentへはOTLPで送信しています。Datadogに送信する手前でDatadog Agentを使っているのはレア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%F3%A5%D7%A5%E9%A1%BC">サンプラー</a>やエラー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%F3%A5%D7%A5%E9%A1%BC">サンプラー</a>などの機能を使いたいためです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20241212/20241212162146.png" width="1200" height="539" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="ハマりポイント1-Spanの名前が期待通り送られない">ハマりポイント1: Spanの名前が期待通り送られない</h2>
<p>Datadog上で見えるSpan名が以下のようになってしまっていました。間にOpenTelemetry Collectorを挟まなければクラス名や<a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>が表示されるのですが、これではどのSpanが何なのか識別がとても難しいです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20241212/20241212162201.png" width="547" height="193" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>調査の結果、この問題はOpenTelemetry Collectorのtransform <a class="keyword" href="https://d.hatena.ne.jp/keyword/processor">processor</a>を使用することで解決できることが分かりました。具体的には、<code>operation.name</code>と<code>span.name</code>に適切な値を設定することで、Datadogが意図しているであろう形式でSpan情報を保持することが可能になります。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">transform</span><span class="synSpecial">:</span>
<span class="synIdentifier">error_mode</span><span class="synSpecial">:</span> ignore
<span class="synIdentifier">trace_statements</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">context</span><span class="synSpecial">:</span> span
<span class="synIdentifier">statements</span><span class="synSpecial">:</span>
<span class="synComment"> # ↓の2つの設定で span.kind, span.name がdatadog agent経由と同じになるっぽい</span>
<span class="synComment"> # 元々は name に laravel.event.handle</span>
<span class="synComment"> # dd.span.Resource に bootstrapping: Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables などが入ってる</span>
<span class="synStatement">- </span>set(attributes["operation.name"], name)
<span class="synStatement">- </span>set(name, attributes["dd.span.Resource"])
</pre>
<h2 id="ハマりポイント2-エラーメッセージがステータスコードになる">ハマりポイント2: エラーメッセージが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">ステータスコード</a>になる</h2>
<p>以下のように例外のメッセージが勝手に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">ステータスコード</a>だけになってしまいます。本来ならエラーメッセージと例外の発生箇所がわかるようになっているはずです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20241212/20241212162216.png" width="198" height="36" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>この問題もハマりポイント1と同じく、OpenTelemetry Collectorのtransform <a class="keyword" href="https://d.hatena.ne.jp/keyword/processor">processor</a>を使用して対処することができます。具体的には、<code>error.msg</code>フィールドに元の<code>error.message</code>の値を保持するよう設定することで、エラーの詳細情報を維持したままDatadogへトレース情報を送信することが可能になりました。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">context</span><span class="synSpecial">:</span> span
<span class="synIdentifier">statements</span><span class="synSpecial">:</span>
<span class="synComment"> # error.msg にメッセージをいれておかないと変に置換されてしまう</span>
<span class="synComment"> # exception.message にいれてもよさそうなのだが span.name が exception じゃないからか出来なかった</span>
<span class="synComment"> # see: https://github.com/DataDog/datadog-agent/blob/a9eac7672a6cf91bec6da77c18313c6f3db4c22d/pkg/trace/api/otlp.go#L619</span>
<span class="synStatement">- </span>set(attributes["error.msg"], attributes["error.message"])
</pre>
<p>調査中にわかったこととしては、エラーメッセージを<a class="keyword" href="https://d.hatena.ne.jp/keyword/HTTP%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">HTTPステータスコード</a>に変換してしまう処理はdatadog-agent用ライブラリ内にあったのですが、opentelemetry-collector-contribのdatadogexporterでもDatadog/datadog-agentのパッケージが使われているために、このようなことになっているようでした。ただ、<a href="https://github.com/DataDog/datadog-agent/blob/a9eac7672a6cf91bec6da77c18313c6f3db4c22d/pkg/trace/transform/transform.go#L367">transform.go</a>を見る限り、<code>exception.message</code>に入れておけばDatadog用に変換してくれるロジックが実装されているようには見えたのですが、transform processerを使って設定してもダメだったのでdd-trace-<a class="keyword" href="https://d.hatena.ne.jp/keyword/php">php</a>で作ったSpanを変換することは想定していなさそうです。</p>
<h2 id="所感">所感</h2>
<p>そもそも、Datadogの計装ライブラリ(dd-trace-somelang) -> OpenTelemetry Collector -> Datadog Agent -> Datadogというパターンで構成しているケースが探してもあまりなかったので、OpenTelemetry Collectorを使うなら計装ライブラリもOpenTelemetryを使うべしということだったのかもしれません。今回はアプリケーション側をあまり変更したくなかったのでこの構成でやった結果、なかなかハマってしまいました。</p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
【スライド公開】アーキテクチャ Conference 2024 に登壇・出展させていただきました
hatenablog://entry/6802418398307465239
2024-11-29T11:10:00+09:00
2024-11-29T11:10:00+09:00 こんにちは。バックエンドエンジニアの仲山です。 先日11月26日(火)に開催されたアーキテクチャ Conference 2024 に登壇させていただきました。この記事ではその報告と共有を書きたいと思います。 architecture-con.findy-tools.io イベントについて アーキテクチャ Conference は、ファインディ株式会社さんが主催する、ソフトウェアエンジニアリングにおけるアーキテクチャに焦点を当てたカンファレンスです。 今回は現地参加のほか、オンラインでの参加も可能となっており、現地だけで800人以上の参加者と、たくさんの企業のブース出展もあり、会場にいて大いに盛…
<p><span style="font-weight: 400;">こんにちは。バックエンドエンジニアの仲山です。</span></p>
<p><span style="font-weight: 400;"> 先日11月26日(火)に開催された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a> Conference 2024 に登壇させていただきました。この記事ではその報告と共有を書きたいと思います。</span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Farchitecture-con.findy-tools.io%2F" title="「ソフトウェアアーキテクチャ・ハードパーツ」著者来日!11/26東京・浜松町開催|アーキテクチャConference 2024" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://architecture-con.findy-tools.io/">architecture-con.findy-tools.io</a></cite></p>
<h2 id="イベントについて"><span style="font-weight: 400;">イベントについて</span></h2>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a> Conference は、ファインディ株式会社さんが主催する、ソフトウェアエンジニアリングにおける<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>に焦点を当てたカンファレンスです。</span></p>
<p><span style="font-weight: 400;">今回は現地参加のほか、オンラインでの参加も可能となっており、現地だけで800人以上の参加者と、たくさんの企業のブース出展もあり、会場にいて大いに盛り上がりを感じました。 すでに来年度の開催も決定しているなど、今とても勢いのあるイベントとなっております。</span></p>
<h2 id="発表内容について"><span style="font-weight: 400;">発表内容について</span></h2>
<p><span style="font-weight: 400;">今回は「</span><a href="https://architecture-con.findy-tools.io/2024/sessions?m=2024/mdl/special/6Rf9obn5"><span style="font-weight: 400;">大規模トラフィックを支えるゲームバックエンドの課題と構成の変遷 ~安定したゲーム体験を実現するために~</span></a><span style="font-weight: 400;">」というタイトルで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>の約10年間の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の変遷を振り返りました。過去の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を眺めながら、当時どんな課題があり、その後の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>にどのような変化があったのかをお話しさせていただきました。</span></p>
<p><span style="font-weight: 400;">ゲームサービスの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>にあまり馴染みのない方でも、なんとなく<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のゲームの裏側がわかってもらえるように構成したつもりなので、まだご覧になられていない方はぜひ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AB%A5%A4%A5%D6">アーカイブ</a>の方もチェックしていただければと思います。</span></p>
<p><iframe id="talk_frame_1285892" class="speakerdeck-iframe" src="//speakerdeck.com/player/795f4e7d3b7d4e8eb80ea04b60460ae7" width="710" height="399" style="aspect-ratio: 710/399; border: 0; padding: 0; margin: 0; background: transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/colopl/da-gui-mo-torahuitukuwozhi-eru-gemubatukuendonoke-ti-togou-cheng-nobian-qian-an-ding-sitagemuti-yan-woshi-xian-surutameni">speakerdeck.com</a></cite></p>
<h2 id="おわりに"><span style="font-weight: 400;">おわりに</span></h2>
<p><span style="font-weight: 400;">初めて参加させていただいたカンファレンスでしたが、大物ゲストの基調講演から始まり、様々な企業の実際の構成や、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>に対する考え方などが学べる非常に有意義なイベントでした。次回は羽田で世界を巻き込んだイベントになることが計画されているみたいなので、今後の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a> Conference もますます楽しみです。</span></p>
<p><span style="font-weight: 400;">講演を聴いてくださった皆さん、ブースに足を運んでくれた皆さん、関係各社の方々、ありがとうございました!</span></p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
時間変更で開発を支える colopl_timesfhiter
hatenablog://entry/6802418398303403407
2024-11-18T11:00:00+09:00
2024-11-18T11:00:00+09:00 こんにちは、 Platform Engineer の工藤です。 突然ですが、 PHP でプロダクトを開発していて、テストやデバッグのために現在時刻を変更したくなることってありませんか? 昨今のフレームワークを利用している場合は Carbon::setTestNow メソッドなどで解決するような簡単な話ですが、運用が長期に渡るシステム (レジェンドプロダクト) においては歴史的経緯により PHP 組み込みの date() 関数や MySQL クエリ内で NOW() 関数を利用してしまっている場合があり、後からロジック的な時間変更機能を導入することは非常に困難な場合があります。 今回はそうした課題…
<p>こんにちは、 Platform Engineer の工藤です。</p>
<p>突然ですが、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> でプロダクトを開発していて、テストや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>のために現在時刻を変更したくなることってありませんか?</p>
<p>昨今の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>を利用している場合は <code>Carbon::setTestNow</code> メソッドなどで解決するような簡単な話ですが、運用が長期に渡るシステム (レジェンドプロダクト) においては歴史的経緯により <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 組み込みの <code>date()</code> 関数や <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> クエリ内で <code>NOW()</code> 関数を利用してしまっている場合があり、後からロジック的な時間変更機能を導入することは非常に困難な場合があります。</p>
<p>今回はそうした課題に対応するために開発した開発補助用 <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Extension 、 <code>colopl_timeshifter</code> について紹介します。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fcolopl%2Fphp-colopl_timeshifter" title="GitHub - colopl/php-colopl_timeshifter: PHP modify current time extension for testing, DO NOT USE PRODUCTION" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/colopl/php-colopl_timeshifter">github.com</a></cite></p>
<p><strong>※ 開発環境用です、本番環境では絶対に使用しないでください</strong></p>
<h2 id="レジェンドプロダクトと時間変更">レジェンドプロダクトと時間変更</h2>
<p>最近の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>などを使っていればそのような機能が最初から提供されているのでそれを使えば良いですが、 10 年前に作成されたレジェンドプロダクトではロジックの中で直接 <code>date()</code> 関数を使っていたりして、なかなかテストしにくい構造になってしまっている場合も多いのではないかと思います。</p>
<p>もちろん地道に直していくのがベストな解決策ではありますが、往々にしてコード量の多いレジェンドプロダクトではなかなか実現が難しい問題があります。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>が必要ない低レイヤーでの時間変更手段としては <code>libfaketime</code> を用いて <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> そのものに偽りの現在時刻を指定する方法などが考えられます。</p>
<p><code>libfaketime</code> は <code>LD_PRELOAD</code> で libc の現在時刻に関わる関数をフックし、 <code>FAKETIME</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>で指定された時間に偽るというライブラリです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fwolfcw%2Flibfaketime" title="GitHub - wolfcw/libfaketime: libfaketime modifies the system time for a single application" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/wolfcw/libfaketime">github.com</a></cite></p>
<pre class="code lang-sh" data-lang="sh" data-unlink>$ <span class="synIdentifier">TZ</span>=Asia/Tokyo date <span class="synStatement">"</span><span class="synConstant">+%Y-%m-%d %H:%M:%S</span><span class="synStatement">"</span>
2024-11-14 16:18:07
$ <span class="synIdentifier">LD_PRELOAD</span>=./libfaketime.so.<span class="synConstant">1</span> <span class="synIdentifier">FAKETIME</span>=<span class="synStatement">"</span><span class="synConstant">1994-10-26 11:22:33</span><span class="synStatement">"</span> <span class="synIdentifier">TZ</span>=Asia/Tokyo date <span class="synStatement">"</span><span class="synConstant">+%Y-%m-%d %H:%M:%S</span><span class="synStatement">"</span>
1994-10-26 11:22:33
or
$ <span class="synIdentifier">TZ</span>=Asia/Tokyo date <span class="synStatement">"</span><span class="synConstant">+%Y-%m-%d %H:%M:%S</span><span class="synStatement">"</span>
2024-11-14 16:25:33
$ <span class="synIdentifier">TZ</span>=Asia/Tokyo faketime <span class="synStatement">"</span><span class="synConstant">1994-10-26 11:22:33</span><span class="synStatement">"</span> date <span class="synStatement">"</span><span class="synConstant">+%Y-%m-%d %H:%M:%S</span><span class="synStatement">"</span>
1994-10-26 11:22:33
</pre>
<p>しかし <code>libfaketime</code> を用いた時間変更はあくまでも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>が渡されたプロセスにしか影響しないため、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/php">php</a>-fpm などのプロセスをフォークする環境では変更を適用するのが困難です (<code>clear_env</code> などの適切な設定が必要) 。</p>
<p>また systemd などを利用している場合にも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を引き渡す際に同様の問題が発生し、時間変更を実現する環境を整えるために考慮すべきことが非常に多くなってしまいます。 <code>faketime</code> コマンドを利用する例もありますが、何にせよ複雑です。</p>
<p>また、当該プロジェクトでは <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> の <code>NOW()</code> 関数を多用しており (やるべきではない) 、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> だけを時間変更することができたとしても <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> 側の時間と食い違うことになってしまう状況で、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> の <code>SET @@session.timestamp</code> を使おうにも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>の ORM を利用していたりしていなかったりするため漏れなく対応するのが困難な状況に陥っていました。</p>
<p>現在は開発環境では同一の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%BE%C1%DB%A5%DE%A5%B7%A5%F3">仮想マシン</a>やコンテナ上で <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>, HTTP サーバー, <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> を実行し、 OS の時間変更を用いることで対応していましたが、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a> への移行などに伴って今後の運用においてこの方法が通用しなくなる可能性が高く、何らかの対応が必要な状況でした。</p>
<p>そこで、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Extension の仕組みを用いて時間変更を行える<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%C8%C4%A5%B5%A1%C7%BD">拡張機能</a> <code>colopl_timeshifter</code> を作ることになりました。</p>
<h2 id="colopl_timeshifter-について"><code>colopl_timeshifter</code> について</h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fcolopl%2Fphp-colopl_timeshifter" title="GitHub - colopl/php-colopl_timeshifter: PHP modify current time extension for testing, DO NOT USE PRODUCTION" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/colopl/php-colopl_timeshifter">github.com</a></cite></p>
<p><code>colopl_timeshifter</code> は <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Extension (<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%C8%C4%A5%B5%A1%C7%BD">拡張機能</a>) として以下の機能を提供しています。</p>
<ul>
<li>現在時刻を扱う <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 組み込み関数に対する時間変更機能</li>
<li>PDO <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> ドライバでのクエリ実行時、透過的に <code>@@session.timestamp</code> で変更された時刻を設定する機能</li>
</ul>
<p>次のようなコードで時間変更を行うことができます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synPreProc">use</span> <span class="synPreProc">function</span> Colopl\ColoplTimeShifter\<span class="synSpecial">{</span>register_hook, unregister_hook<span class="synSpecial">}</span>;
<span class="synComment">/* 今より 3 日前に設定: https://www.php.net/manual/ja/dateinterval.construct.php */</span>
register_hook<span class="synSpecial">(</span><span class="synPreProc">new</span> \<span class="synIdentifier">DateInterval</span><span class="synSpecial">(</span><span class="synConstant">'P3D'</span><span class="synSpecial">))</span>;
<span class="synPreProc">echo</span> <span class="synIdentifier">date</span><span class="synSpecial">(</span><span class="synConstant">'Y-m-d H:i:s'</span><span class="synSpecial">)</span>, \PHP_EOL;
<span class="synComment">/* 元に戻す */</span>
unregister_hook<span class="synSpecial">()</span>;
<span class="synComment">/* 特定の日時に設定 */</span>
register_hook<span class="synSpecial">((</span><span class="synPreProc">new</span> <span class="synIdentifier">DateTime</span><span class="synSpecial">(</span><span class="synConstant">'1994-10-26 11:22:33'</span><span class="synSpecial">))</span><span class="synType">-></span>diff<span class="synSpecial">(</span><span class="synPreProc">new</span> <span class="synIdentifier">DateTime</span><span class="synSpecial">(</span><span class="synConstant">'now'</span><span class="synSpecial">)))</span>;
<span class="synPreProc">echo</span> <span class="synIdentifier">date</span><span class="synSpecial">(</span><span class="synConstant">'Y-m-d H:i:s'</span><span class="synSpecial">)</span>, \PHP_EOL;
<span class="synComment">/* 元に戻す */</span>
unregister_hook<span class="synSpecial">()</span>;
</pre>
<p>また、接続先のデータベースが <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> (または互換のあるエンジン) であればクエリに対しても現在時刻をモックすることができます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synPreProc">use</span> <span class="synPreProc">function</span> Colopl\ColoplTimeShifter\<span class="synSpecial">{</span>register_hook, unregister_hook<span class="synSpecial">}</span>;
<span class="synComment">/* 特定の日時に設定 */</span>
register_hook<span class="synSpecial">((</span><span class="synPreProc">new</span> <span class="synIdentifier">DateTime</span><span class="synSpecial">(</span><span class="synConstant">'1994-10-26 11:22:33'</span><span class="synSpecial">))</span><span class="synType">-></span>diff<span class="synSpecial">(</span><span class="synPreProc">new</span> <span class="synIdentifier">DateTime</span><span class="synSpecial">(</span><span class="synConstant">'now'</span><span class="synSpecial">)))</span>;
<span class="synPreProc">echo</span> <span class="synSpecial">(</span><span class="synPreProc">new</span> \<span class="synIdentifier">PDO</span><span class="synSpecial">(</span><span class="synConstant">'mysql:dbname=testing;host=mysql'</span>, <span class="synConstant">'testing'</span>, <span class="synConstant">'testing'</span><span class="synSpecial">))</span><span class="synType">-></span>query<span class="synSpecial">(</span><span class="synConstant">'SELECT NOW() AS `now`;'</span><span class="synSpecial">)</span><span class="synType">-></span>fetch<span class="synSpecial">()[</span><span class="synConstant">0</span><span class="synSpecial">]</span>, \PHP_EOL;
<span class="synComment">/* 元に戻す */</span>
unregister_hook<span class="synSpecial">()</span>;
</pre>
<h2 id="動作原理">動作原理</h2>
<p><code>colopl_timeshifter</code> は既存の <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> の関数やメソッドをフックする仕組みで実装されており、実際に <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Extension として読み込まれた時に以下ように行われます。</p>
<ol>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> の関数定義テーブル (HashMap) <code>CG(function_table)</code> から対象となる関数ポインタを検索</li>
<li>既存の関数ポインタを <code>colopl_timeshifter</code> のグローバル領域に保管</li>
<li>関数ポインタを <code>colopl_timeshifter</code> 側の関数に置き換え</li>
</ol>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> における主な日付関連処理 (<code>ext-date</code>) は著名な <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 用デバッガである <a href="https://xdebug.org/">Xdebug</a> の作者でもある Derick Rethans さんの <code>timelib</code> をベースとしています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fderickr%2Ftimelib" title="GitHub - derickr/timelib: Timelib is a timezone and date/time library that can calculate local time, convert between timezones and parse textual descriptions of date/time information." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/derickr/timelib">github.com</a></cite></p>
<p><code>timelib</code> は非常に高機能で、日の出 (sunrise) の時刻や日没 (sunset) の時刻を取得したり各国の夏時間に対応していますが、中でも特徴的なものとして <strong>非常に口語的な日時のパースをサポートしている</strong> というものがあります。</p>
<p>次の日時指定構文は有効なフォーマットです。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">declare</span><span class="synSpecial">(</span>strict_types<span class="synStatement">=</span><span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synComment">/* 今が 2024-11-14 だとすると結果は 2024-11-18 12:30:15 となる */</span>
<span class="synPreProc">echo</span> <span class="synIdentifier">date</span><span class="synSpecial">(</span><span class="synConstant">'Y-m-d H:i:s'</span>, <span class="synSpecial">(</span><span class="synPreProc">new</span> \<span class="synIdentifier">DateTime</span><span class="synSpecial">(</span><span class="synConstant">'next monday 15sec 12pm 30min'</span><span class="synSpecial">))</span><span class="synType">-></span>getTimestamp<span class="synSpecial">())</span>, \PHP_EOL;
</pre>
<p>これは場合によっては直感的に利用できて便利な一方、実際にパースが終わるまで指定されたフォーマットが絶対時刻 (現在時刻に依存しない常に一定の時刻を示す文字列) なのか相対時刻 (現在時刻に対する差分で表される文字列) なのかがわからないという問題を抱えていることになります。</p>
<p>実際に<code>timelib</code> ではフォーマットのパース実装のために <a href="https://re2c.org/"><code>re2c</code></a> レキサジェネレータが利用されており、その複雑性を伺うことができます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fderickr%2Ftimelib%2Fblob%2Fmaster%2Fparse_date.re" title="timelib/parse_date.re at master · derickr/timelib" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/derickr/timelib/blob/master/parse_date.re">github.com</a></cite></p>
<p>しかし、現在時刻をモックする <code>colopl_timeshifter</code> にとってこの仕様は非常に厳しいものです。というのも、 <strong>渡されたフォーマットが絶対時刻なのか相対時刻なのかを判別することができず、時間変更を適用すべきかしないべきかの判断ができない</strong> のです。</p>
<p><code>timelib</code> の内部実装などを調べてみましたが、やはりパース結果が絶対時刻なのか相対時刻なのかを適切に判別する方法は見当たらず、仕方がないので <strong>ごく僅かなウェイトを挟んでパースし、結果が異なっているかどうかで判別する</strong> という力技の実装を行うこととなりました。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fcolopl%2Fphp-colopl_timeshifter%2Fblob%2Fmain%2Fext%2Fhook.c%23L288" title="php-colopl_timeshifter/ext/hook.c at main · colopl/php-colopl_timeshifter" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/colopl/php-colopl_timeshifter/blob/main/ext/hook.c#L288">github.com</a></cite></p>
<p>このウェイトは INI ディレクティブ <code>colopl_timeshifter.usleep_sec</code> で設定することができます。</p>
<h2 id="PDO-のフック">PDO のフック</h2>
<p>では PDO <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> への時間変更はどのように行っているのでしょうか?</p>
<p>PDO は <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> の汎用データベースドライバ構造で、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Java">Java</a> の世界での <a href="https://ja.wikipedia.org/wiki/Java_Database_Connectivity">JDBC</a> のようなものです。簡単に言うと様々なデータベースを抽象化し、ある程度共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>した仕組みで利用することができるようにするものです。</p>
<p>PDO は内部実装もある程度抽象化されており、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> の組み込み関数同様に関数ポインタを用い、構造体にセットする形で行われています。また、この構造体は <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Extension に対して公開されており、やろうと思えば <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> の組み込み関数同様処理を乗っ取ることができます。</p>
<p><code>colopl_timeshifter</code> は次のような順序で PDO の <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> ドライバに対してフックを行います。</p>
<ol>
<li>INI 設定 <code>colopl_timeshifter.is_hook_pdo_mysql</code> が有効であるかチェック</li>
<li><code>PDO</code> クラスのコンスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ (<code>__construct</code>) の関数ポインタを取得し <code>colopl_timeshifter</code> のグローバル領域に保管</li>
<li><code>PDO::__construct</code> メソッドの関数ポインタを <code>colopl_timeshifter</code> の関数ポインタに置き換え</li>
<li>コンスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ内で現在の PDO ドライバを取得し、ドライバ名が <code>mysql</code> であれば <code>doer</code> と <code>preparer</code> 関数ポインタ (クエリ実行関数) を乗っ取り</li>
<li><code>doer</code> と <code>preparer</code> 実行時、クエリの先頭に <code>SET @@session.timestamp = <モックした日時>;</code> を付加</li>
</ol>
<p>幸いにも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のプロジェクトはすべて <a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a> でのクエリ実行に PDO を用いているため、この手法で解決することができました。 <code>ext-mysqli</code> を利用しているものがあった場合にはそちらについても対応する必要があるでしょう。</p>
<h2 id="php-timecop"><code>php-timecop</code></h2>
<p>どうしても必要になったので <code>colopl_timeshifter</code> を作っていたのですが、途中で KLab の hnw さんによるほぼ同様の <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Extension <code>php-timecop</code> があったことに気付きました...とはいえ近年の <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> では動作しなかったのと、 PDO のフックも必要だったので結果オーライだと思いたい...</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhnw%2Fphp-timecop" title="GitHub - hnw/php-timecop: A PHP extension providing "time travel" capabilities inspired by ruby timecop gem" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hnw/php-timecop">github.com</a></cite></p>
<p>自分が思いつくものの大半をすでにやられている hnw さんはやっぱりすごいなと思いました。勝てない...!</p>
<h2 id="colopl_timeshifter-について-1"><code>colopl_timeshifter</code> について</h2>
<p><code>colopl_timeshifter</code> は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>ソフトウェアとして<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>の <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> 上で公開しています。 issue や PR も受け付けています (必ず対応するわけではないですが...) ので、興味があればぜひどうぞ</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fcolopl%2Fphp-colopl_timeshifter" title="GitHub - colopl/php-colopl_timeshifter: PHP modify current time extension for testing, DO NOT USE PRODUCTION" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/colopl/php-colopl_timeshifter">github.com</a></cite></p>
<p><strong>でも絶対にプロダクション環境では使わないでくださいね!</strong></p>
<p><br></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
zeriyoshi_colopl
PHP Conference Japan 2024 スポンサー協賛・登壇のお知らせ
hatenablog://entry/6802418398299557676
2024-10-30T11:00:00+09:00
2024-10-30T11:00:00+09:00 こんにちは。 Platform Engineer の工藤です。 2024 年 12 月 22 日に開催される PHP Conference Japan 2024 に株式会社コロプラとしてシルバースポンサーで協賛させていただきました。また、Platform Engineer の工藤がスピーカーとして登壇させていただきます。 PHP Conference Japan について PHP Conference Japan は、プログラミング言語 PHP を中心とした国内最大級のカンファレンスイベントです。例年開催されており、今年は 2024 年 12 月 22 日 (日) に大田区産業プラザ PiO …
<p>こんにちは。 Platform Engineer の工藤です。</p>
<p>2024 年 12 月 22 日に開催される <a href="https://phpcon.php.gr.jp/2024/">PHP Conference Japan 2024</a> に株式会社<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>としてシルバースポンサーで協賛させていただきました。また、Platform Engineer の工藤がスピーカーとして登壇させていただきます。</p>
<h4 id="PHP-Conference-Japan-について"><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan について</h4>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> を中心とした国内最大級のカンファレンスイベントです。例年開催されており、今年は 2024 年 12 月 22 日 (日) に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C2%E7%C5%C4%B6%E8%BB%BA%B6%C8%A5%D7%A5%E9%A5%B6">大田区産業プラザ</a> PiO にて行われます。</p>
<p>イベントでは多くのプロポーザルの中から採択されたセッションや、スポンサーブースを始めとした様々な出展が行われます。<a href="https://blog.colopl.dev/entry/2023/09/29/110000">去年に引き続き</a>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>はシルバースポンサーとして協賛させていただくこととなりました!</p>
<h4 id="登壇内容について">登壇内容について</h4>
<p>プロポーザルを採択いただき、今年も Platform Engineer の工藤が登壇させていただけることとなりました。今回は「怖くない!ゼロから始める<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>入門」というタイトルで登壇させていただきます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffortee.jp%2Fphpcon-2024%2Fproposal%2F76a8cc7a-c0c7-406d-80fd-a0a978dbe164" title="怖くない!ゼロから始めるPHPソースコードコンパイル入門" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://fortee.jp/phpcon-2024/proposal/76a8cc7a-c0c7-406d-80fd-a0a978dbe164">fortee.jp</a></cite></p>
<p>本登壇では <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>からビルドする具体的な方法の解説のほか、それによって得られるメリットなどについてお話させていただきます。バグ報告や <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>を行う際にも有用な情報となる予定ですので、ぜひ聞きに来ていただけると嬉しいです!</p>
<h4 id="おわりに">おわりに</h4>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> Conference Japan は年々盛り上がりを増しつつありますが、参加のハードルは非常に低く保たれている初心者にも優しい技術イベントの一つです。勉強会やカンファレンスは行ったことないけど、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> は使っている...という人にこそ、是非とも参加いただきたいと思っております。</p>
<p>参加費は無料で、以下の connpass から事前に参加登録が可能です。イベント後には懇親会も予定されているので、ぜひご参加いただければと思います!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fphpcon.connpass.com%2Fevent%2F329839%2F" title="PHPカンファレンス 2024 (2024/12/22 09:50〜)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://phpcon.connpass.com/event/329839/">phpcon.connpass.com</a></cite></p>
<hr>
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
zeriyoshi_colopl
マルチプレイヤーゲームのバックエンドを学べる!|コロプラ3daysインターンを実施しました
hatenablog://entry/6802340630903194924
2024-09-03T11:00:00+09:00
2024-09-03T11:00:02+09:00 こんにちは!バックエンドエンジニアの阿部です。 2024年8月5日〜7日にマルチプレイゲームのリアルタイム技術について知ろう!というタイトルでサーバーサイドエンジニア3daysインターンシップを開催しました。 実施内容 さて、皆様は複数人が参加する形式のゲームで遊んだことはあるでしょうか? 昨今のビデオゲームでは、シューティングゲームやスポーツゲーム、ボードゲームまで様々な形式のゲームがオンラインでリアルタイムに遊ぶことができます。 今回のインターンシップでは、そのようなゲームを遊ぶ上で重要な専用ゲームサーバーについて、その役割の1つである「同期」をテーマにTPS用専用ゲームサーバーの設計から…
<p><span style="font-weight: 400;">こんにちは!バックエンドエンジニアの阿部です。</span></p>
<p><span style="font-weight: 400;">2024年8月5日〜7日に</span><a href="https://colopl.co.jp/recruit/internship2024/serverside_3days.php"><span style="font-weight: 400;">マルチプレイゲームのリアルタイム技術について知ろう!</span></a><span style="font-weight: 400;">というタイトルでサーバーサイドエンジニア3days<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3%A5%B7%A5%C3%A5%D7">インターンシップ</a>を開催しました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240902/20240902110230.jpg" width="660" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p> </p>
<h2 id="実施内容"><span style="font-weight: 400;">実施内容</span></h2>
<p><span style="font-weight: 400;">さて、皆様は複数人が参加する形式のゲームで遊んだことはあるでしょうか?</span></p>
<p><span style="font-weight: 400;">昨今の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D3%A5%C7%A5%AA%A5%B2%A1%BC%A5%E0">ビデオゲーム</a>では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0%A5%B2%A1%BC%A5%E0">シューティングゲーム</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DD%A1%BC%A5%C4%A5%B2%A1%BC%A5%E0">スポーツゲーム</a>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A1%BC%A5%C9%A5%B2%A1%BC%A5%E0">ボードゲーム</a>まで様々な形式のゲームがオンラインでリアルタイムに遊ぶことができます。</span></p>
<p><span style="font-weight: 400;">今回の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3%A5%B7%A5%C3%A5%D7">インターンシップ</a>では、そのようなゲームを遊ぶ上で重要な専用ゲームサーバーについて、その役割の1つである「同期」をテーマにTPS用専用ゲームサーバーの設計から実装までを3日間で行いました。</span></p>
<p><span style="font-weight: 400;">「同期」と一言で表すと簡単そうに思えます。しかし実際に作ってみると思わぬ落とし穴があったり、普段の <a class="keyword" href="https://d.hatena.ne.jp/keyword/REST%20API">REST API</a> 開発とは勝手が違う部分があるために思った以上に苦戦することが多いです。「ステートフルなサーバーならではの双方向の通信」、また、「同期をとるために必要なデータの選定方法」などを課題を通して体験できる形式を採りました。</span></p>
<p><span style="font-weight: 400;">参加者の方には2人1組のチームで課題に取り組んでいただきました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240902/20240902110241.png" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240902/20240902110253.png" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h3 id="講義編マルチプレイヤーゲームの仕組みとコロプラの専用ゲームサーバー"><span style="font-weight: 400;">講義編:<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%EB%A5%C1%A5%D7%A5%EC%A5%A4%A5%E4%A1%BC">マルチプレイヤー</a>ゲームの仕組みと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>の専用ゲームサーバー</span></h3>
<p><span style="font-weight: 400;">初日の午前中は課題に取り組む上で必要になる知識を得てもらうため座学を行いました。</span></p>
<p><span style="font-weight: 400;">オンラインで複数のデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>が接続して遊ぶゲームは1人向けのゲームとは異なり、それぞれのデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>が状態を持つためその同期が重要になります。その同期をとるためになぜRESTではなく専用ゲームサーバーなのか、RESTとの役割の違いについて、また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>がこれまで開発してきた<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%EB%A5%C1%A5%D7%A5%EC%A5%A4%A5%E4%A1%BC">マルチプレイヤー</a>ゲームを例に、実際にどのような技術を用いて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%EB%A5%C1%A5%D7%A5%EC%A5%A4%A5%E4%A1%BC">マルチプレイヤー</a>ゲームが開発されてきたか、そのときどきでなぜその技術を選択してきたかについても紹介しました。</span></p>
<h3 id="設計編リアルタイム通信の設計とクライアントサーバー間のプロトコルの工夫"><span style="font-weight: 400;">設計編:リアルタイム通信の設計とクライアントサーバー間の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>の工夫</span></h3>
<p><span style="font-weight: 400;">オンラインゲームを開発していく上で、そのクライアントと専用ゲームサーバーがどのように情報をやりとりするか、受け取った情報をどのように処理するかを設計しておくことは非常に重要です。これにより、実装の分担や手戻りの防止ができ、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>の削減につながります。</span></p>
<p><span style="font-weight: 400;">ゲームを開始する際に発生するクライアントそれぞれが動作する環境によるズレの待ち合わせや、ゲーム中に発生する様々なイベントについてのアプリケーション<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>を設計する課題に取り組み、発表しました。</span></p>
<p><span style="font-weight: 400;">お題としては正常系を想定して出題しましたが、取り組みの中で途中で切断した場合などの異常系を考慮してくれる方もいて素晴らしいと感じました。</span></p>
<h3 id="実装編専用ゲームサーバーの構築とリアルタイム処理の実装体験"><span style="font-weight: 400;">実装編:専用ゲームサーバーの構築とリアルタイム処理の実装体験</span></h3>
<p><span style="font-weight: 400;">2日目の午後からは実装に移りました。</span></p>
<p><span style="font-weight: 400;">設計編で解答例として用意したアプリケーション<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>を元に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%C2%C1%F5%BA%D1">実装済</a>みのクライアントと連携する専用ゲームサーバーの実装をしました。内製サーバーエンジン”prizm”(</span><a href="https://blog.colopl.dev/entry/2022/03/28/105041"><span style="font-weight: 400;">COLOPL Tech 勉強会 「ゲーム開発におけるリアルタイム通信基盤とKubernetes/Agones」を実施しました!</span></a><span style="font-weight: 400;">)を用いて実装し、実装をテストやクライアントアプリから確認することで、参加、開始、インゲーム処理、アイテム処理、と課題を追う毎にゲームが完成していくのを体験していただきました。</span></p>
<p><span style="font-weight: 400;">専用ゲームサーバーはステートフルなのでRESTとは違った特性に初めは戸惑う場面もありましたが、段々とコツを掴んで取り組まれていました。</span></p>
<h3 id="チャレンジ編運用に向けた課題への取り組み"><span style="font-weight: 400;">チャレンジ編:運用に向けた課題への取り組み</span></h3>
<p><span style="font-weight: 400;">3日目の午後には実装編の延長や、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>上で展開する際の課題、チート対策についての考察など、各チームで好きな課題に挑戦しました。</span></p>
<p><span style="font-weight: 400;">どの課題も限られた時間の中で取り組むのは難しいものでしたが、実際に扱われている仕組みに近い部分まで考察されていたり、UXまで考えて実装されていたり、予想を上回る成果物が上げられていて驚かされました。</span></p>
<p> </p>
<h2 id="参加した学生からの感想紹介"><span style="font-weight: 400;">参加した学生からの感想紹介</span></h2>
<p><span style="font-weight: 400;">「</span><span style="font-weight: 400;">第一にすごく楽しかったです!Go言語の経験が少ないので、最初は非常に不安でしたが、そのような状態からでもかけるように準備されていました。実際に全てのチームがアプリ完成まで終わって、追加の実装まで進んでいました。今回の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3">インターン</a>では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>サーバとゲームサーバの違いや、ゲームサーバを使ったバックエンドの実装を理解することが出来ました。</span><span style="font-weight: 400;">」</span></p>
<p><span style="font-weight: 400;">「</span><span style="font-weight: 400;">リアルタイム通信という自分がこれまでに触れたことの無い技術での開発経験を積むことができ、自身の成長に繋がったと感じました。</span><span style="font-weight: 400;">」</span></p>
<p><span style="font-weight: 400;">「</span><span style="font-weight: 400;">リアルタイムゲームサーバーの設計と実装を経験できました。様々な方法で機能を設計することができると知り、チームによって色が出たと思います。</span><span style="font-weight: 400;">」</span></p>
<p><span style="font-weight: 400;">「</span><span style="font-weight: 400;">サーバーサイドのゲームの開発について実際体験することが出来てよかったです!</span><span style="font-weight: 400;">」</span></p>
<p> </p>
<h2 id="最後に"><span style="font-weight: 400;">最後に</span></h2>
<p><span style="font-weight: 400;">これまでバックエンドの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3">インターン</a>で扱ってきた課題は「RESTやDBを用いて大規模・高負荷なアクセスを如何に捌くか」を体験するようなものでしたが、今回は「ユーザーさまに対してリアルタイムで快適な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%EB%A5%C1%A5%D7%A5%EC%A5%A4">マルチプレイ</a>を届けるために必要な技術」にフォーカスしたものになりました。</span></p>
<p><span style="font-weight: 400;">このように<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では様々な技術を駆使してユーザーさまに新しい体験を届けたいと考えています。同じ思いの方がいらっしゃいましたらぜひ一緒に働けたらと思います。ご応募お待ちしております。</span></p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><br /><br /></p>
kaz-yamada
初めての海外出張に行ってきました!【Google Cloud Next ‘24 in Las Vegas】
hatenablog://entry/6801883189124307892
2024-07-29T11:00:00+09:00
2024-07-29T11:00:02+09:00 はじめに こんにちは。バックエンドエンジニアの Y.N. です。 普段は LCE チームの一員として新作開発のお手伝いなどをしています。(→ LCE については過去の記事があるのでそちらもぜひご覧ください) 最近はブロックチェーンゲーム Brilliantcrypto のリリースへ向けてバタバタしていましたが、無事リリースを迎えることができました。PC の方は、ぜひこの記事を読みながらダウンロードして遊んでみてください! そんな私ですが、先日入社して初めての海外出張へ行く機会があったので、今回はその様子について共有させていただきます。 出張について 今回の出張は、コロプラが主に利用しているイン…
<h2 id="はじめに"><span style="font-weight: 400;">はじめに</span></h2>
<p><span style="font-weight: 400;">こんにちは。バックエンドエンジニアの Y.N. です。</span></p>
<p><span style="font-weight: 400;">普段は LCE チームの一員として新作開発のお手伝いなどをしています。<br /></span><span style="font-weight: 400;">(→ LCE については</span><a href="https://blog.colopl.dev/entry/2023/02/21/110937"><span style="font-weight: 400;">過去の記事</span></a><span style="font-weight: 400;">があるのでそちらもぜひご覧ください)</span></p>
<p><span style="font-weight: 400;">最近は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%ED%A5%C3%A5%AF%A5%C1%A5%A7%A1%BC%A5%F3">ブロックチェーン</a>ゲーム </span><a href="https://brilliantcrypto.net/jp/"><span style="font-weight: 400;">Brilliantcrypto</span></a><span style="font-weight: 400;"> のリリースへ向けてバタバタしていましたが、無事リリースを迎えることができました。PC の方は、ぜひこの記事を読みながらダウンロードして遊んでみてください!</span></p>
<p><span style="font-weight: 400;">そんな私ですが、先日入社して初めての海外出張へ行く機会があったので、今回はその様子について共有させていただきます。</span></p>
<h2 id="出張について"><span style="font-weight: 400;">出張について</span></h2>
<p><span style="font-weight: 400;">今回の出張は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>が主に利用しているインフラ基盤である <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud が主催するカンファレンス「</span><a href="https://cloud.withgoogle.com/next"><span style="font-weight: 400;">Cloud Next ‘24</span></a><span style="font-weight: 400;">」に参加し、最新の動向をキャッチアップすることが目的でした。</span></p>
<p><span style="font-weight: 400;">Cloud Next ‘24 は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%E1%A5%EA">アメリ</a>カのラスベガスで開催され、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>からは私を含めて二人で現地参加する運びとなりました。「海外出張で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%E1%A5%EA">アメリ</a>カに行く」というワードを使う日が来るとは想像しておらず、内心不安もありましたが、日本とは違った空気感が味わえるのかなといった期待もありました。</span></p>
<h2 id="準備編"><span style="font-weight: 400;">準備編</span></h2>
<p><span style="font-weight: 400;">出張に行くことが決まると、それに向けていくつか準備を進める必要があります。</span></p>
<p><span style="font-weight: 400;">初めての海外出張でわからないことがたくさんありましたが、会社の手厚いサポートのおかげで手続きなどに困ることはほとんどありませんでした。バックオフィスの方々に参加するイベントの情報や出発・到着予定時刻などを伝えると、それに基づいて飛行機やホテルの予約、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Wifi">Wifi</a> の手配、社内の稟議申請などたくさんのことを対応してもらえました。サポートしてくださった皆様にとても感謝しています。</span></p>
<p><span style="font-weight: 400;">また、事務的な手続きとは別に、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%E1%A5%EA">アメリ</a>カに着いてからの行動を事前に計画しておく必要もありました。特にカンファレンスでの立ち回り方については、相談の場を設けて二人で検討し、なるべく個人が別々のセッションに参加することで、情報を広く取り入れられるようにスケジュールを組みました。Cloud Next では事前に Web やアプリからセッションの予約ができるようになっていたので、それぞれのセッションの参加予約をして当日に備えました。</span></p>
<h2 id="渡航編"><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C5%CF%B9%D2">渡航</a>編</span></h2>
<p><span style="font-weight: 400;">今回の全体的な出張スケジュールは以下の通りです。</span></p>
<table style="height: 345px;" width="513">
<tbody>
<tr>
<td>
<p><strong>日付</strong></p>
</td>
<td>
<p><strong>時刻</strong></p>
</td>
<td>
<p><strong>場所</strong></p>
</td>
<td>
<p><strong>行動</strong></p>
</td>
</tr>
<tr>
<td>
<p><span style="font-weight: 400;">4/8(月)</span></p>
</td>
<td>
<p><span style="font-weight: 400;">17:00</span></p>
</td>
<td>
<p><span style="font-weight: 400;">Tokyo</span></p>
</td>
<td>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%AE%C5%C4%B9%F1%BA%DD%B6%F5%B9%C1">成田国際空港</a>発</span></p>
</td>
</tr>
<tr>
<td>
<p><span style="font-weight: 400;">4/8(月)</span></p>
</td>
<td>
<p><span style="font-weight: 400;">15:00</span></p>
</td>
<td>
<p><span style="font-weight: 400;">Las Vegas</span></p>
</td>
<td>
<p><span style="font-weight: 400;">ハリー・リード国際空港到着</span></p>
</td>
</tr>
<tr>
<td>
<p><span style="font-weight: 400;">4/9(火)</span></p>
</td>
<td>
<p><span style="font-weight: 400;">終日</span></p>
</td>
<td>
<p><span style="font-weight: 400;">Mandalay Bay</span></p>
</td>
<td>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next ‘24</span></p>
</td>
</tr>
<tr>
<td>
<p><span style="font-weight: 400;">4/10(水)</span></p>
</td>
<td>
<p><span style="font-weight: 400;">終日</span></p>
</td>
<td>
<p><span style="font-weight: 400;">Mandalay Bay</span></p>
</td>
<td>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next ‘24</span></p>
</td>
</tr>
<tr>
<td>
<p><span style="font-weight: 400;">4/11(木)</span></p>
</td>
<td>
<p><span style="font-weight: 400;">終日</span></p>
</td>
<td>
<p><span style="font-weight: 400;">Mandalay Bay</span></p>
</td>
<td>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Next ‘24</span></p>
</td>
</tr>
<tr>
<td>
<p><span style="font-weight: 400;">4/12(金)</span></p>
</td>
<td>
<p><span style="font-weight: 400;">07:00</span></p>
</td>
<td>
<p><span style="font-weight: 400;">Las Vegas</span></p>
</td>
<td>
<p><span style="font-weight: 400;">ハリー・リード国際空港発</span></p>
</td>
</tr>
<tr>
<td>
<p><span style="font-weight: 400;">4/13(土)</span></p>
</td>
<td>
<p><span style="font-weight: 400;">17:00</span></p>
</td>
<td>
<p><span style="font-weight: 400;">Tokyo</span></p>
</td>
<td>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%AE%C5%C4%B9%F1%BA%DD%B6%F5%B9%C1">成田国際空港</a>到着</span></p>
</td>
</tr>
</tbody>
</table>
<p> </p>
<p><span style="font-weight: 400;">イベント前日の夕方にホテルに到着し、三日間のイベントに参加、次の日の早朝の飛行機で帰国しました。東京を出発してラスベガスに到着したら時間が巻き戻っているのですから、不思議な感覚を覚えますね。</span></p>
<p><span style="font-weight: 400;">ラスベガスにはサンフランシスコを経由して向かったのですが、途中の乗り換え待ちの間、すでに周りには <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Certified のワッペンがついたリュックを持っている人々が散見され、「プロがたくさん集うイベントなんだなぁ」と改めて感じました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724114155.jpg" width="181" height="322" loading="lazy" title="" class="hatena-fotolife" itemprop="image" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p><span style="font-weight: 400;">ラスベガス到着後、空港からホテルまではタクシーで移動しました。タクシーの運転手さんが移動中にトランシーバーでずっと友人らしき人と会話していた様子が「日本ではあまり見ない光景だなぁ」と思って日米の違いを感じたことを覚えています。</span></p>
<p><span style="font-weight: 400;">今回宿泊したのは、Excalibur <a class="keyword" href="https://d.hatena.ne.jp/keyword/Hotel">Hotel</a> & Casino というカラフルなお城のような見た目が特徴的なホテルでした。受付カウンターの前にはカジノスペースが広がっており、スロットの光と騒音に囲まれた時にラスベガスに来たことを実感しました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724114222.jpg" width="613" height="466" loading="lazy" title="" class="hatena-fotolife" itemprop="image" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p> </p>
<h2 id="Cloud-Next-24"><span style="font-weight: 400;">Cloud Next ‘24</span></h2>
<p><span style="font-weight: 400;">次の日からいよいよ Cloud Next ‘24 が始まりました。</span></p>
<p><span style="font-weight: 400;">会場は金色の建物が印象的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%F3%A5%C0%A5%EC%A5%A4">マンダレイ</a>・ベイという施設でした。</span></p>
<p><span style="font-weight: 400;">建物内には <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> らしいカラーの装飾が至る所に施されていて、イベントへの期待感が高まっていきました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724114251.jpg" width="237" height="356" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724114304.jpg" width="356" height="468" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><span style="font-weight: 400;">大きなキーノートホールの一角に着席して待つこと数分、ポップな音楽とともに基調講演が始まり、会場が沸き立ちます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud CEO のトーマス・クリアン氏をはじめとして、登壇者の方々が堂々と発表している姿を見ると、内容どうこう以前に「すごい仕事をしているなぁ」とそのスケールに圧倒されてしまいました。普段はモニター越しに見ている光景が目の前で起こっているのを見ると、それだけで刺激になりますね。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724114345.jpg" width="1200" height="684" loading="lazy" title="" class="hatena-fotolife" itemprop="image" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p><span style="font-weight: 400;">今回の Cloud Next ‘24 の主な話題はやはり AI でした。新たに <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> のフラッグシップモデルの Gemini 1.5 Pro というマルチモーダルのモデルが発表され、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud の他の様々なプロダクトでも AI 技術をベースとした機能追加が多数発表されました。</span></p>
<p><span style="font-weight: 400;">個人的には、AI アプリを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>で作成する </span><a href="https://cloud.google.com/products/agent-builder?hl=ja"><span style="font-weight: 400;">Vertex AI Agent Builder</span></a><span style="font-weight: 400;"> という機能がとても印象的でした。AI というとエンジニアが手作業で細かいパラメータチューニングを頑張る姿を連想してしまうので、1つ良いモデルが出来上がるとそのモデルに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%AB%C1%B3%B8%C0%B8%EC">自然言語</a>で依頼するだけで望んだ出力が得られるという時代が来ていることに驚きました。</span></p>
<p><span style="font-weight: 400;">Expo エリアでは多くの企業がブースを出展し、その場で説明や実演を行っていました。特に印象に残ったのは、サッカーの PK を評価し、AI コーチからアド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>を受けられるブースでした。実際に試してみると、私のスコアは80点の評価をいただきました。個人的には悪くないスコアだなと思いましたが、AI コーチからは「蹴る力が強くなるとより良くなる」とアド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>をいただいたので、頑張って脚を鍛えようと思いました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724202228.jpg" width="248" height="370" loading="lazy" title="" class="hatena-fotolife" itemprop="image" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p><span style="font-weight: 400;">さらに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>が利用しているプロダクトのチームとのミーティングの機会もありました。名刺交換などもさせていただき、少し緊張する場面ではありましたが、気さくに話しかけてもらえたおかげで緊張もほぐれました。日本からもリモートでメンバーが参加してくださり、短い時間でしたが有意義なミーティングになりました。</span></p>
<p><span style="font-weight: 400;">そのほか、イベント期間中は連日交流会が開催されていたのでそちらに参加し、「(当時まだ日本未発売だった)<a class="keyword" href="https://d.hatena.ne.jp/keyword/Apple">Apple</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Vision">Vision</a> Pro を試しに来た」や、「英語学習の苦労話」などなどいろいろなお話を聞かせていただきながら楽しい時間を過ごすことができました。交流してくださった皆様どうもありがとうございました。</span></p>
<p><span style="font-weight: 400;">全てのイベントが終了した後は、少しだけラスベガスを観光しました。3,400億円かけて建設された全面 LED のアリーナである <a class="keyword" href="https://d.hatena.ne.jp/keyword/Sphere">Sphere</a> を見に行くと、ここでも <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A5%B4%A5%DE%A1%BC%A5%AF">ロゴマーク</a>が表示され、その圧倒的なスケールに度肝を抜かれました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724114433.jpg" width="311" height="175" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20240724/20240724114442.jpg" width="313" height="176" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h2 id="感想"><span style="font-weight: 400;">感想</span></h2>
<p><span style="font-weight: 400;">イベント全体を通して、話題は常に生成 AI が中心のように感じました。あまり AI 技術に触れていない自分にとって、漠然とした危機感を覚えるような感覚もありましたが、AI をうまく活用する事例も見れたのでよかったです。イベントでは500以上のセッションが開催されていたらしく、到底全て回り切れるものではありませんでしたが、各社の AI の活用事例や様々なチャレンジについての話を聞いていると、とても良い刺激をもらえました。この経験を糧に、自分でも周りに自信を持って話せる仕事をしていきたいと感じました。</span></p>
<p><span style="font-weight: 400;">最後に、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%E1%A5%EA">アメリ</a>カの物価は高いですね。イベント期間中はなんだかんだ食事が提供されたので助かりましたが、そうでなかったらと考えると結構ゾッとします。</span></p>
<h2 id="後日談"><span style="font-weight: 400;">後日談</span></h2>
<p><span style="font-weight: 400;">海外出張を終えて、社内では「Cloud Next '24 参加レポート」と題して勉強会を実施しました。様々なセッションを聴いて気になった話題や注目機能、行ってきた感想などについての発表を行いました。</span></p>
<p><span style="font-weight: 400;">実際に Gemini 1.5 Pro を使ったデモなどを発表に組み込んでみたところ、好評のコメントをいただくことができたので、発表に組み込んでみてよかったなと思います。</span></p>
<h2 id="イベント参加時の注意点"><span style="font-weight: 400;">イベント参加時の注意点</span></h2>
<p><span style="font-weight: 400;">今回 Cloud Next ‘24 に参加してみて、海外出張で大規模カンファレンスに参加する際の失敗ポイントもいくつか発見したので、参考にしてみてください。<br /><br /></span></p>
<p><strong>◆ セッションの予約システムがある場合は、できるだけ早く予約する</strong></p>
<p><span style="font-weight: 400;">人気のセッションはすぐに満席になる可能性が非常に高いです。せっかく現地に行くのであれば、聴きたいセッションを逃すのは勿体無いので、早めに予約を済ませてしまいましょう。<br /></span></p>
<p><span style="font-weight: 400;">◆ </span><strong>基調講演などの巨大ホールで行われるセッションだとしても早めに会場に着くようにする</strong></p>
<p><span style="font-weight: 400;">自分の周りでキーノート会場に入れなかったという声をちらほら聞きました。現地にいるのにモニター越しになってしまうと悲しいので、会場が広い場合も油断せず、早めに会場入りするのが良さそうです。<br /></span></p>
<p><span style="font-weight: 400;">◆ </span><strong>イベント中はセッション以外にも Expo など他の展示もあることを念頭に置いてスケジュールを決める</strong></p>
<p><span style="font-weight: 400;">予約システムがあると予定を詰めてしまいがちですが、企業ブースの展示も充実しているイベントはそちらを見て回る時間も確保しておけると良いと思いました。</span></p>
<h2 id="まとめ"><span style="font-weight: 400;">まとめ</span></h2>
<p><span style="font-weight: 400;">今回は初めての海外出張で Cloud Next ‘24 に参加して感じたことや、注意点などについて紹介させていただきました。</span></p>
<p><span style="font-weight: 400;">今回のイベントのように大規模なカンファレンスは参加するだけでも大きなモチベーションアップに繋がるので、開催場所が海外でもチャンスがあればぜひ参加してみることをおすすめします!この記事がその背中を押す一助になれば幸いです。</span></p>
<p><span style="font-weight: 400;">最後に、そんな海外出張のチャンスが眠っている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、一緒にチャレンジングな仕事をする仲間を募集中です!</span></p>
<p> </p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
Go Conference 2024 スポンサー協賛のお知らせ
hatenablog://entry/6801883189104703635
2024-05-09T11:00:00+09:00
2024-05-31T13:37:02+09:00 こんにちは!バックエンドエンジニアの松永です。サーバー基盤グループのRTPE(Real-Time Platform Engineering)チームに所属し、リアルタイム通信に関わる基盤の開発や各タイトルの開発支援を行っています。 今回、2024年6月8日(土)に開催される Go Conference 2024 にコロプラは Bronze スポンサーとして協賛することが決まりました。そこでこの記事では、 Go Conference 2024 の概要と、コロプラにおける Go の活用について紹介したいと思います。 Go Conference について Go Conference とは、年に一度開催…
<p><span style="font-weight: 400;">こんにちは!バックエンドエンジニアの松永です。サーバー基盤グループのRTPE(Real-Time Platform Engineering)チームに所属し、リアルタイム通信に関わる基盤の開発や各タイトルの開発支援を行っています。</span></p>
<p><span style="font-weight: 400;">今回、2024年6月8日(土)に開催される </span><a href="https://gocon.jp/2024/"><span style="font-weight: 400;">Go Conference 2024</span></a><span style="font-weight: 400;"> に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>は Bronze スポンサーとして協賛することが決まりました。そこでこの記事では、 Go Conference 2024 の概要と、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>における Go の活用について紹介したいと思います。</span></p>
<h3 id="Go-Conference-について"><span style="font-weight: 400;">Go Conference について</span></h3>
<p><span style="font-weight: 400;">Go Conference とは、年に一度開催される Go に関するカンファレンスです。今年は数年ぶりのオフライン開催で、テーマは「一期一会」。オフラインならではのセッションやイベントが企画されているようです。現時点ではまだ企画の詳細は公表されていませんが、順次アナウンスがあるはずなので、最新の情報は</span><a href="https://gocon.jp/2024/"><span style="font-weight: 400;">公式サイト</span></a><span style="font-weight: 400;">や</span><a href="https://twitter.com/goconjp"><span style="font-weight: 400;">Xの公式アカウント</span></a><span style="font-weight: 400;">を確認してみてください。</span></p>
<h3 id="コロプラにおける-Go-の活用"><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>における Go の活用</span></h3>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では Go を活用する場面が増えてきています。その一つが、リアルタイム通信に関わる開発です。</span></p>
<p><span style="font-weight: 400;">例えば、対戦プレイや協力プレイのようなオンライン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%EB%A5%C1%A5%D7%A5%EC%A5%A4">マルチプレイ</a>を実現するためのゲームサーバーでは、多数のプレイヤーがリアルタイムにゲームサーバーを介してデータをやり取りする必要があります。 Go では並行処理を簡潔に記述できるため、同時に大量のデータを処理するようなゲームサーバーの開発に適しています。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>言語ならではの高速な実行速度も、リアルタイム通信を実現する上で大きな利点となります。</span></p>
<p><span style="font-weight: 400;">また、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> のようなインフラや、 Agones や Open Match といった Gaming <a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a> でも Go が採用されており、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>が利用しているこれらのプロダクトとの相性の良さも Go を選ぶ理由の一つになっています。</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のリアルタイム通信基盤や、 Agones や Open Match といった Gaming <a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a> の利用に興味のある方は、以下の記事も読んでみてください!</span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2022%2F03%2F28%2F105041" title="COLOPL Tech 勉強会 「ゲーム開発におけるリアルタイム通信基盤とKubernetes/Agones」を実施しました! - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2022/03/28/105041">blog.colopl.dev</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2023%2F04%2F05%2F104246" title="第4回 Game Engineers Meetup に登壇しました - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2023/04/05/104246">blog.colopl.dev</a></cite></p>
<h3 id="今後の-Go-に関する発信について"><span style="font-weight: 400;">今後の Go に関する発信について</span></h3>
<p><span style="font-weight: 400;">今回の Go Conference 2024 には弊チームのメンバーがプロポーザルを提出したのですが、残念ながら採択までには至りませんでした。プロポーザルで提案した内容は今後勉強会や技術ブログを通じて紹介する予定です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では今後も Go に関する情報を積極的に発信していきたいと考えています。</span></p>
<h3 id="おわりに"><span style="font-weight: 400;">おわりに</span></h3>
<p><span style="font-weight: 400;">Go Conference 2024 は、初心者からベテランまで Go を使う全ての人が楽しめるカンファレンスです。参加費は無料で、</span><a href="https://gocon.connpass.com/event/314876/"><span style="font-weight: 400;">connpassの申込みページ</span></a><span style="font-weight: 400;">から参加登録が行えるのですが、オフライン参加枠は受付開始早々に上限に達してしまいました。キャンセル待ちができるので、少しでも興味のある方は是非参加を検討してみてください!</span></p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびXで情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fx.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / X" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://x.com/colopl_tech">x.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
コロプラにバックエンドエンジニアとして入社しました
hatenablog://entry/6801883189093740121
2024-04-01T11:00:00+09:00
2024-04-01T11:00:02+09:00 こんにちは!コロプラのバックエンドエンジニアの大宮です。現在とあるゲームタイトルのバックエンドエンジニアとして仕事をしています。入社して3ヶ月が経とうとしている今、入社エントリーとして、転職活動をしていた当時や、入社してからのコロプラでの経験を振り返り、ゲーム業界未経験のエンジニア目線で感じたことを今回お伝えできればと思っています。 自己紹介 前職は8年程同じ会社でWebシステム開発とAndroidアプリケーションの開発をしていまして、ゲーム開発の経験は全くありませんでした。Web開発の領域としてはフロントエンド、バックエンド、そしてインフラも触りながら幅広く開発していました。触っていたフレー…
<p><span style="font-weight: 400;">こんにちは!<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のバックエンドエンジニアの大宮です。<br /></span><span style="font-weight: 400;">現在とあるゲームタイトルのバックエンドエンジニアとして仕事をしています。入社して3ヶ月が経とうとしている今、入社エントリーとして、転職活動をしていた当時や、入社してからの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>での経験を振り返り、ゲーム業界未経験のエンジニア目線で感じたことを今回お伝えできればと思っています。</span></p>
<h1 id="自己紹介"><span style="font-weight: 400;">自己紹介</span></h1>
<p><span style="font-weight: 400;">前職は8年程同じ会社でWeb<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%B9%A5%C6%A5%E0%B3%AB%C8%AF">システム開発</a>と<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>アプリケーションの開発をしていまして、ゲーム開発の経験は全くありませんでした。<br /></span><span style="font-weight: 400;">Web開発の領域としてはフロントエンド、バックエンド、そしてインフラも触りながら幅広く開発していました。触っていた<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>やライブラリも様々で、</span></p>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">バックエンド:SpringFrameworkやLaravel</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">フロントエンド:Reactメイン</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">インフラ:<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a></span></li>
</ul>
<p><span style="font-weight: 400;">を使って開発することが多かったです。PJによって開発言語から違うことが多かったので、結果的に幅広く経験を積むことができました。</span></p>
<h1 id="なぜコロプラに転職したか"><span style="font-weight: 400;">なぜ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>に転職したか</span></h1>
<p><span style="font-weight: 400;">転職を決意した一番大きな理由は、エンジニアとしてステップアップしたいなと思ったことです。転職することでより多くの刺激を受けて、技術者として成長できればいいなと思いました。<br /></span><span style="font-weight: 400;">転職活動を始めた頃は転職の軸として「自分のスキルが活かせるか」「成長できそうか」という観点で会社を探していました。</span></p>
<p><span style="font-weight: 400;">特に業界にこだわりなく活動していましたが、最終的に入社を決めたきっかけは、面接や面談でゲームと技術の話ができたことです。元々ゲームは好きで、ゲームがどんな仕組みで作られているのか、どういった考えでゲーム作りをしていくのかを技術的な面も交えて話ができて一気に興味を持ちました。好きなゲームの質問から始まり、そのゲームのどんなところが好きか、それを実現するには技術的にどういう方法が取れるかを、ディスカッションする形でDB設計の話や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">通信プロトコル</a>の話をしたのを覚えています。</span></p>
<h1 id="入社してみて"><span style="font-weight: 400;">入社してみて</span></h1>
<h2 id="入社前のコロプラの印象"><span style="font-weight: 400;">入社前の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>の印象</span></h2>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>という会社について、多くの方がそうだと思うのですが「有名な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>ゲームを開発している会社」という印象が強かったです。特に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C7%F2%C7%AD%A5%D7%A5%ED%A5%B8%A5%A7%A5%AF%A5%C8">白猫プロジェクト</a>は自分もプレイしていて、周りの人も結構知っているタイトルでした。<br /></span><span style="font-weight: 400;">それ以外の印象だと技術ブログなどの記事や動画をみて、技術的な挑戦をたくさんしている会社だなと思ってました。</span></p>
<h2 id="入社後の印象"><span style="font-weight: 400;">入社後の印象</span></h2>
<p><span style="font-weight: 400;">仕事をしやすい環境が整っているなというのが第一印象でした。<br /></span><span style="font-weight: 400;">知見、知識がまとまっていてタイトルを跨いで簡単にアクセスできるのは驚きました。「このタイトルではこの技術をこういう理由で使っています。使い方はこんな感じです。」といった情報がまとまっていてサクッと検索できます。<br /></span><span style="font-weight: 400;">加えて技術的なトピックについてSlack等で積極的に議論されているなという印象がありますし、書籍も社内の</span><a href="https://colopl.co.jp/recruit/company/no7.php#:~:text=%E7%94%9F%E3%81%BE%E3%82%8C%E5%A4%89%E3%82%8F%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82-,%E2%96%B6%E3%82%AF%E3%83%9E%E5%9B%B3%E6%9B%B8%E9%A4%A8,-%E5%9F%B7%E5%8B%99%E3%82%A8%E3%83%AA%E3%82%A2%E5%86%85"><span style="font-weight: 400;">クマ図書館</span></a><span style="font-weight: 400;">に揃っていて借りられたりします。技術力アップにとてもいい環境だなと思います。</span></p>
<h2 id="前職とここが違う"><span style="font-weight: 400;">前職とここが違う</span></h2>
<p><span style="font-weight: 400;">良い意味でサーバーエンジニアの業務の範囲を越えてプロジェクトに貢献できます。サーバーエンジニアとしてのタスクをこなしつつ、UIや仕様に対しても色々と意見を言える雰囲気なので、ゲーム作りのプロセスに強く関わっているなという実感があります。</span></p>
<h2 id="いいと思ったところ"><span style="font-weight: 400;">いいと思ったところ</span></h2>
<p><span style="font-weight: 400;">人間関係の面では周りのメンバーはゲーム開発未経験であることを理解して接してくれるので、とてもやりやすいです。<br /></span><span style="font-weight: 400;">技術的な面では興味のある事柄について情報収集しやすいところがとてもいいなと思います。Slack上にカテゴリ毎に発信するチャンネルが作られていて、面白いサービスが共有されていたり、それを使ってみた結果を載せていたり個人個人がアウトプットしています。自分はよくAIのチャンネルと<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>のチャンネルを目的もなく覗いています。技術書も色々揃っていて借りられる環境がある所も嬉しいです!!</span></p>
<h1 id="これからについて"><span style="font-weight: 400;">これからについて</span></h1>
<p><span style="font-weight: 400;">ひとまずは業務に慣れることが目標ですが、技術が好きなので将来的にはテッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A1%BC%A5%C9">クリード</a>を目指していきたいです。<br /></span><span style="font-weight: 400;">施策の開発担当をこなしつつ、ゲーム業界以外の経験を活かしていけたらなと思っています。</span></p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびX(<a class="keyword" href="https://d.hatena.ne.jp/keyword/Twitter">Twitter</a>)で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftwitter.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / Twitter" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://twitter.com/colopl_tech">twitter.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
バックエンドエンジニアの社内LT会を開催しました
hatenablog://entry/820878482970523621
2024-01-11T11:00:00+09:00
2024-01-11T11:00:00+09:00 こんにちは!コロプラのバックエンドエンジニアの山田です。 コロプラでは、社内・社外問わずコミュニケーションや情報共有の強化の一貫として勉強会やLT会を継続して開催しています。 様々なチームが不定期でLT会を開催しており、バックエンドエンジニアを包括する技術基盤本部という大きな組織単位でもLT会を開催しております。 前回開催した際の記事も公開しているので、是非ご覧ください。 blog.colopl.dev LT会のトーク内容 LTの内容は大きくは2つに分けて人数半々で募集しました。 テック・真面目系 エンタメ・ネタ枠 すべてが「テック・真面目系」だと堅苦しく発表・参加ハードルが上がってしまうため…
<p><span style="font-weight: 400;">こんにちは!<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のバックエンドエンジニアの山田です。</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、社内・社外問わずコミュニケーションや情報共有の強化の一貫として勉強会やLT会を継続して開催しています。</span></p>
<p><span style="font-weight: 400;">様々なチームが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%D4%C4%EA">不定</a>期でLT会を開催しており、バックエンドエンジニアを包括する技術基盤本部という大きな組織単位でもLT会を開催しております。</span></p>
<p><span style="font-weight: 400;">前回開催した際の記事も公開しているので、是非ご覧ください。</span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.colopl.dev%2Fentry%2F2023%2F06%2F21%2F105125" title="サーバーエンジニア社内LTレポート - COLOPL Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.colopl.dev/entry/2023/06/21/105125">blog.colopl.dev</a></cite></p>
<h1 id="LT会のトーク内容"><span style="font-weight: 400;">LT会の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>内容</span></h1>
<p><span style="font-weight: 400;">LTの内容は大きくは2つに分けて人数半々で募集しました。</span></p>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">テック・真面目系</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">エンタメ・ネタ枠</span></li>
</ul>
<p><span style="font-weight: 400;">すべてが「テック・真面目系」だと堅苦しく発表・参加ハードルが上がってしまうため「エンタメ・ネタ枠」も用意しています。</span></p>
<h2 id="1Incremental-game-を布教する"><span style="font-weight: 400;">1.「Incremental game を布教する」</span></h2>
<p><span style="font-weight: 400;">登壇者 T.U. さん</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Cookie">Cookie</a> Clicker などを代表するいわゆる「放置ゲーム」の歴史とおすすめのゲームの紹介です。</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Wikipedia">Wikipedia</a> によると 2002 に出た Progress Quest が始まりとされていたりと、案外知らないことが多くて興味深い内容でした。</span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FIncremental_game" title="Incremental game - Wikipedia" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://en.wikipedia.org/wiki/Incremental_game">en.wikipedia.org</a></cite></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20230925/20230925184420.png" width="1200" height="678" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h2 id="2モンユニローカライズ裏話"><span style="font-weight: 400;">2.「モンユニ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%AB%A5%E9%A5%A4%A5%BA">ローカライズ</a>裏話」</span></h2>
<p><span style="font-weight: 400;">登壇者 Kevin さん</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>がリリースしているゲームの </span><a href="https://colopl.co.jp/news/pressrelease/2023012402.php"><span style="font-weight: 400;">MONSTER UNIVERSE</span></a><span style="font-weight: 400;"> の英語への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%AB%A5%E9%A5%A4%A5%BA">ローカライズ</a>対応の話になります。</span></p>
<p><span style="font-weight: 400;">マスターデータやUIなどあらゆる箇所の翻訳が必要になり、単純に英訳するだけでは不適切な場合など様々な課題がでてきますが、それらの課題に対してどう向き合って進めてきたのかをお話されました。</span></p>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">英訳すると長くなりUIに入り切らない場合は、短くしても伝わる言葉に変える</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">漢字一文字の表現は 無理に翻訳せずアイコンにしてしまう</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">細かいニュアンスの違いなどは ChatGPT に頼る</span></li>
</ul>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20230925/20230925184452.png" width="1200" height="677" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h2 id="3PHP-予想外シリーズ"><span style="font-weight: 400;">3.「<a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a> 予想外シリーズ」</span></h2>
<p><span style="font-weight: 400;">登壇者 尾山 貴康 さん</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/PHP">PHP</a>のコードで予想外な挙動になるコードをいくつか紹介されています。<br /></span><span style="font-weight: 400;">普段は書かないであろうコードもありますが、もし書いてしまうとハマりそうな全く予想外の挙動もあって面白い内容でした。</span></p>
<p><span style="font-weight: 400;">詳しい内容はまた別の記事にて公開したいと思っています。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20230925/20230925184356.png" width="1200" height="679" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h2 id="4無料で始めるAI画像生成"><span style="font-weight: 400;">4.「無料で始めるAI画像生成」</span></h2>
<p><span style="font-weight: 400;">登壇者 山田 和毅(私です)</span></p>
<p><a href="https://colab.research.google.com/?hl=ja"><span style="font-weight: 400;">Google Colab</span></a><span style="font-weight: 400;"> を使って無料で </span><a href="https://github.com/Stability-AI/stablediffusion"><span style="font-weight: 400;">Statble Diffusion</span></a><span style="font-weight: 400;"> を始めるという内容です。自宅で個人的に画像生成を色々と試していて、なかなか面白かったのでLTしてみました。<br /></span><span style="font-weight: 400;">なお、最近はこの用途で使うと警告が出るので今なら課金必須です。</span></p>
<p><span style="font-weight: 400;">簡単な始め方から、</span><a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui"><span style="font-weight: 400;">Statable Diffusion Web UI</span></a><span style="font-weight: 400;"> の使い方、プロンプトのコツ、</span><a href="https://www.bing.com/images/create/"><span style="font-weight: 400;">Bing Image Creator</span></a><span style="font-weight: 400;"> などとの比較について話しました。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20230925/20230925184339.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h2 id="5メンズメイクの始め方"><span style="font-weight: 400;">5.「メンズメイクの始め方」</span></h2>
<p><span style="font-weight: 400;">登壇者 喜田 祐介 さん</span></p>
<p><span style="font-weight: 400;">エンタメ枠としてメンズメイクについて初心者向けのおすすめ商品などを紹介されていました。<br /></span><span style="font-weight: 400;">手軽に始めるならunoや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%A4%A5%BD%A1%BC">ダイソー</a>などで安く揃えられるようで、私はこのあたりに全然疎くて調べもしない範囲なのでLT会としてはとても良い発表でした。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20230925/20230925184406.png" width="1200" height="677" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h2 id="6-業務メモのとり方-ベストプラクティス"><span style="font-weight: 400;">6. 業務メモのとり方 ベストプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス</span></h2>
<p><span style="font-weight: 400;">登壇者 西川 蒼太郎 さん</span></p>
<p><span style="font-weight: 400;">「業務メモは他人と共有するもの」という考えのもと、様々なメモアプリを試した発表者が Slack のメモアプリとしての素晴らしさや活用方法を紹介しています。</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では Confluence などの情報共有ツールをいくつか使用していますが、この発表で話されていた「やらないコンフルよりやるSlack」というフレーズはまさにそうだなと思いました。</span></p>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">メモがそのままドキュメントになる</span></li>
<ul>
<li style="font-weight: 400;" aria-level="2"><span style="font-weight: 400;">引き継ぎする際はリンクを渡すだけ</span></li>
<li style="font-weight: 400;" aria-level="2"><span style="font-weight: 400;">何かで困った方がSlackで検索した際に解決の足がかりになる</span></li>
</ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">メモがそのまま進捗共有になる</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">メモがそのまま質問<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%C7%BC%A8">掲示</a>板になる</span></li>
<ul>
<li style="font-weight: 400;" aria-level="2"><span style="font-weight: 400;">通りすがりの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CD%AD%BC%B1%BC%D4">有識者</a>が見てアド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>をくれる</span></li>
</ul>
</ul>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20230925/20230925184437.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h2 id="7-SMBC-の-Olive-はおトクだから申し込んだらいいと思うよの話"><span style="font-weight: 400;">7. 「<a class="keyword" href="https://d.hatena.ne.jp/keyword/SMBC">SMBC</a> の Olive はおトクだから申し込んだらいいと思うよの話」</span></h2>
<p><span style="font-weight: 400;">登壇者 工藤 剛 さん</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/SMBC">SMBC</a>のOliveを使った際の様々なメリットと、始める場合の注意点、クレカ投資 (積立 NISA 含) 、有効な活用方法について話されていました。</span></p>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">三井住友・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B0%C9%A9UFJ">三菱UFJ</a> の ATM の時間外手数料無料</span></li>
</ul>
<ul>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">同行宛だけじゃなく他行あても振込手数料無料</span></li>
<li style="font-weight: 400;" aria-level="1"><span style="font-weight: 400;">翌月に受けられる特典を 2 つまで (一部重複可) 選べる</span></li>
<ul>
<li style="font-weight: 400;" aria-level="2"><span style="font-weight: 400;">コンビニ ATM 手数料無料 (重複可)</span></li>
<li style="font-weight: 400;" aria-level="2"><span style="font-weight: 400;">V ポイントアッププログラム +1% 還元 (重複可)</span></li>
<li style="font-weight: 400;" aria-level="2"><span style="font-weight: 400;">ご利用特典 100pt (重複可)</span></li>
<li style="font-weight: 400;" aria-level="2"><span style="font-weight: 400;">給与受取特典 200pt</span></li>
</ul>
</ul>
<p><span style="font-weight: 400;">これを機に私も Olive 始めました(笑)</span></p>
<p> </p>
<h1 id="終わりに"><span style="font-weight: 400;">終わりに</span></h1>
<p><span style="font-weight: 400;">真面目系からエンタメ枠まで様々な発表があり、自分が普段調べない範囲をさらっと学べてとても良い機会でした。</span></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では社内LT会を継続的に開催していますので、次回の記事もお楽しみにお待ち下さい!</span></p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびX(<a class="keyword" href="https://d.hatena.ne.jp/keyword/Twitter">Twitter</a>)で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftwitter.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / Twitter" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://twitter.com/colopl_tech">twitter.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada
MIGによるサーバー費用削減:エンジニアによる実践記
hatenablog://entry/6801883189059511392
2023-12-13T07:00:00+09:00
2023-12-13T07:00:05+09:00 都内某日、在宅勤務でとあるゲームの運用を行っていた男はプロジェクトマネージャーから打診を受ける。 M「サーバー費用はこんなにかかるものなのですか?」他のプロジェクトと比較してみても、そこまで目立って多いわけではないある意味では適正である。スペックの見直しをしてどのくらい費用が抑えられるのか ─────その後、男はある疑問を抱えることになる。「夜間のサーバー維持費はもっと抑えることができるのではないか?」男は名を田中という、コロプラのバックエンドエンジニアである。〜プロジェクト MIG〜 お急ぎの方は、記事の最後に対応まとめを記載しておりますので、そちらをご覧ください 目標と課題 時は少し遡りマ…
<p><span style="font-weight: 400;">都内某日、在宅勤務でとあるゲームの運用を行っていた男は<br /></span><span style="font-weight: 400;">プロジェクトマネージャーから打診を受ける。<br /></span><span style="font-weight: 400;"> M「サーバー費用はこんなにかかるものなのですか?」<br /><br /></span><span style="font-weight: 400;">他のプロジェクトと比較してみても、そこまで目立って多いわけではない<br /></span><span style="font-weight: 400;">ある意味では適正である。<br /></span><span style="font-weight: 400;">スペックの見直しをしてどのくらい費用が抑えられるのか ─────<br /><br /></span><span style="font-weight: 400;">その後、男はある疑問を抱えることになる。<br /></span><span style="font-weight: 400;">「夜間のサーバー維持費はもっと抑えることができるのではないか?」<br /><br /></span><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">男は名を田中という、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>のバックエンドエンジニアである。<br /><br /></span><strong><span style="font-size: 150%;">〜プロジェクト MIG〜</span></strong><span style="font-size: 200%;"><strong><br /></strong></span><span style="font-size: 80%;"><strong> お急ぎの方は、記事の最後に対応まとめを記載しておりますので、そちらをご覧ください</strong></span></p>
<p> </p>
<h3 id="目標と課題"><span style="font-weight: 400;">目標と課題</span></h3>
<p><span style="font-weight: 400;">時は少し遡り<br /></span><span style="font-weight: 400;">マネージャーからの打診を受けた日、田中は売り上げとサーバー維持費の比率を眺めていた。<br /></span><span style="font-weight: 400;"> 田中「意外と日々の売り上げに対してサーバーの維持費は結構かかってるなぁ」</span></p>
<p><span style="font-weight: 400;">マネージャーから打診を受けたのち、田中はサーバーのスペックダウンをインフラチームに検討依頼した。<br /></span><span style="font-weight: 400;"> インフラ「1段階スペック下げても運用上問題ないですね」<br /></span><span style="font-weight: 400;"> 田中「それでは下げて運用しましょう」<br /></span><span style="font-weight: 400;">とんとん拍子に事は進み、APサーバーのスペックを下げ、当然費用も多少は削減した。</span></p>
<p><span style="font-weight: 400;">ただ、プロジェクトとしてスペックダウンだけの費用削減効果は<br /></span><span style="font-weight: 400;">マネージャーが期待していたものよりも少なかった────</span></p>
<p><span style="font-weight: 400;">もっと費用を下げれないものか、、、頭を抱える田中は様々なデータを確認する<br /></span><span style="font-weight: 400;">ユーザーリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを眺めていた。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20231129/20231129204223.png" width="1200" height="298" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><span style="font-weight: 400;">16時台がピークになっていて、20時台にも少し上がるいつも通りのグラフだ。<br /></span><span style="font-weight: 400;">ピークを超え0時付近を境にリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トは減っていく。<br /></span><span style="font-weight: 400;">・・・当然だ、ほとんどのユーザーは就寝している。</span></p>
<p><span style="font-weight: 400;">ふとこの時、日々のタスクの中でサーバーの台数調整は<br /></span><span style="font-weight: 400;">1日に1回しか行っていないことが引っかかった。<br /></span><span style="font-weight: 400;">日毎に、16時台のピークを予想して台数を変更しているが<br /></span><span style="font-weight: 400;">それ以外の時間帯に合わせて台数調整はしていない。</span></p>
<p><span style="font-weight: 400;"> 田中「夜間のサーバー維持費はもっと抑えることができるのではないか?」</span></p>
<p><span style="font-weight: 400;">もし、0時過ぎた段階で台数を下げ<br /></span><span style="font-weight: 400;">リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トが増加してくる前の6時台に台数を元に戻せば、その間の維持費が削減できる。</span></p>
<p><span style="font-weight: 400;">希望が、見えた ─────</span></p>
<p><span style="font-weight: 400;">しかし、実際に運用を行おうとすると大きな壁があった。<br /></span><span style="font-weight: 400;"> ー当然のことながら深夜・早朝に手動で台数調整を行うことは考えられない<br /></span><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;"> ーそこで、行うとしたらcronにて自動で台数調整を行うことが思い浮かぶのだが<br /></span><span style="font-weight: 400;"> 自動で行っている作業にエラーが発生した時どうするのか<br /></span><span style="font-weight: 400;"> ー深夜・早朝に見守ることになると結局手動で調整するのとほぼ変わらないことになる<br /></span><span style="font-weight: 400;">どうにもこの方法は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%A8%B6%F5%BB%F6">絵空事</a>のようだ。</span></p>
<p><span style="font-weight: 400;">持て余した田中はインフラチームに相談することにする。</span></p>
<p><span style="font-weight: 400;"> 田中「</span><span style="font-weight: 400;">APサーバーの台数を深夜帯(時間はまだ未確定)で削減できないか検討しているのですが<br /></span><span style="font-weight: 400;"> 自動でAPサーバーの台数を増減することは可能でしょうか?</span><span style="font-weight: 400;">」<br /></span><span style="font-weight: 400;"> インフラ「現在利用しているシステムに、</span><span style="font-weight: 400;">カレンダーに登録した時間で<br /></span><span style="font-weight: 400;"> 台数を調整する自社製<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>[semi-autoscale] がありますので利用できるかもしれません<br /></span><span style="font-weight: 400;"> 調査に1ヶ月ほどかかってしまうのでお待ちいただけますか?」<br /></span></p>
<p><span style="font-weight: 400;">なるほど、そんな便利な機能があるのか!<br /></span><span style="font-weight: 400;">実現性を帯びた返答に、田中は一も二もなく導入することをお願いした。<br /></span></p>
<p><span style="font-weight: 400;">1ヶ月後、インフラから返答が届く─────</span></p>
<p><span style="font-weight: 400;"> インフラ「</span><span style="font-weight: 400;">semi-autoscale では台数を全て決めておかなければならないのに対し、<br /></span><span style="font-weight: 400;"> <a class="keyword" href="https://d.hatena.ne.jp/keyword/gcp">gcp</a> の </span><a href="https://cloud.google.com/compute/docs/autoscaler?hl=ja"><span style="font-weight: 400;">managed instance group(MIG) の autoscaling</span></a><span style="font-weight: 400;"> が利用できるかもしれないので<br /></span><span style="font-weight: 400;"> 調査していました</span><span style="font-weight: 400;">」</span></p>
<p><span style="font-weight: 400;">田中は驚愕する。台数に関しては台数指定をするタイミングを設ける必要があるため<br /></span><span style="font-weight: 400;">ある程度余剰が生じても仕方がないと考えていたからだ。<br /></span><span style="font-weight: 400;">それが、余剰が極力発生しない形で管理できるとは、、、</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20231129/20231129204559.png" width="303" height="75" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /> <span style="font-weight: 400;">→ </span><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20231129/20231129204606.png" width="304" height="75" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p style="text-align: center;"><span style="font-weight: 400;">サーバー台数変動想像図</span></p>
<p><span style="font-weight: 400;">さらに説明は続く<br /></span><span style="font-weight: 400;">インフラ「</span><span style="font-weight: 400;">ただし、デプロイ方法が変更になってしまいます<br /></span><span style="font-weight: 400;"> 新しい方式では、新たにサーバーを作成し本番反映した後に<br /></span><span style="font-weight: 400;"> 旧サーバーを抜いていくことになり、旧サーバーは全て削除されてしまいます」</span></p>
<p><span style="font-weight: 400;">ここで従来のデプロイ方法を説明しておく必要がある。</span></p>
<p><span style="font-weight: 400;">例えば10台サーバーが稼働し、ゲームを運用しているとする。<br /></span><span style="font-weight: 400;">追加で作成したり、修正したプログラムを稼働しているサーバーに反映するときには</span></p>
<p><span style="font-weight: 400;">まず2台のサーバーを「ユーザーからアクセスできる環境」から切り離し、<br /></span><span style="font-weight: 400;">プログラムファイルを更新、再び「ユーザーからアクセスできる環境」に戻す。<br /></span><span style="font-weight: 400;">この工程を繰り返し、全ての台数分ファイルの更新が完了すれば完了となる。<br /></span><span style="font-weight: 400;">この方法だと、使用しているサーバー自体は変わらないことになる。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20231129/20231129204918.png" width="1200" height="557" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><span style="font-weight: 400;">これに対し、新たなデプロイだと<br /></span><span style="font-weight: 400;">先に新たなサーバーを用意しておき、「ユーザーからアクセスできる環境」に追加した後で<br /></span><span style="font-weight: 400;">旧サーバーを削除することになる。</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20231129/20231129204939.png" width="1200" height="488" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">この方法だとサーバー自体が新たなものに置き換わってしまう。<br /></span><span style="font-weight: 400;">新たなデプロイ方法で問題となるのは<br /></span><span style="font-weight: 400;"> ・旧サーバーに保存されているログが削除されてしまうこと<br /></span><span style="font-weight: 400;"> ・それぞれのサーバのキャッシュデータが取り直しになり<br /></span><span style="font-weight: 400;"> 一時的に負荷が上がってしまうこと<br /></span><span style="font-weight: 400;">などが考えられた。<br /></span><span style="font-weight: 400;">それら全て対応しても費用削減の効果は大きそうだ。</span></p>
<p><span style="font-weight: 400;"> 田中「費用削減効果の方が大きそうなので、その方法で運用を目指しましょう」</span></p>
<p><span style="font-weight: 400;">インフラと協力し合い、それぞれの問題に対する対応策を考えた。</span></p>
<p><span style="font-weight: 400;">まず、ログに関して2つの問題への対応である。</span></p>
<p><span style="font-weight: 400;">1つ目は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>・実行ログ・エラーログなど通常確認は行わないが調査などに使用することがあるログの類を避難させること。</span></p>
<p><span style="font-weight: 400;"> これはサーバーを</span><span style="font-weight: 400;">削除</span><span style="font-weight: 400;">する前にインフラ側でファイルを取得し、</span><a href="https://cloud.google.com/storage?hl=ja"><span style="font-weight: 400;">GCS(Google Cloud Storage)</span></a><span style="font-weight: 400;">に保存することで事なきを得た。<br /></span><span style="font-weight: 400;">ただし、調査の際にはアクセス権限を持っているインフラにデータ取得をお願いすることとなった。</span></p>
<p><span style="font-weight: 400;">2つ目は、普段確認するエラーログの監視方法の修正。</span></p>
<p><span style="font-weight: 400;"> 展開時だけでなく、MIGが</span><span style="font-weight: 400;">autoscaling</span><span style="font-weight: 400;">を行うタイミング(自動で行われるためタイミングをはかることはできない)でサーバーが切り替わってしまうため、<br /></span><span style="font-weight: 400;">既存では運用を行っている特定サーバーのエラーログを取得していたロジックを変更し、<br /></span><span style="font-weight: 400;">エラーログを取得するタイミングで毎回稼働サーバーを検出することで対応する事にした。</span></p>
<p><span style="font-weight: 400;">次に、キャッシュデータに対しての対応だが</span></p>
<p><span style="font-weight: 400;"> 元々、MIGは急激なアクセス過多に対しては対応しきれないところがあるため<br /></span><span style="font-weight: 400;">アクセスが集中する時間帯の前に予め台数を増やすスケジュールを組み急激に台数変動が起こらないように予防しておく想定だった。<br /></span><span style="font-weight: 400;">これに伴い、キャッシュデータに関してはピーク時点で既に作成されているため問題ないという結論に至った。</span></p>
<p><span style="font-weight: 400;">ちなみにMIGのスケジュール設定では、サーバーの下限台数とその開始時間、継続時間が設定できるようになっている。<br /></span><span style="font-weight: 400;">例えば普段5,6台で運用しているところ、16時のピークに合わせ台数を36台まで増台しておき、その状態を1時間継続するといった具合だ。</span></p>
<p> </p>
<h3 id="導入と不具合"><span style="font-weight: 400;">導入と不具合</span></h3>
<p><span style="font-weight: 400;">考えつく問題点を取り除きリリースして問題ないと思われる段階まで進んだ。</span></p>
<p><span style="font-weight: 400;">2023年1月某日 いよいよMIGを利用しての運用を開始する日が来た。</span></p>
<p><span style="font-weight: 400;">インフラと連携して作業を行う。<br /></span><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">まず、サービスインしていない環境で問題なくデプロイできていることを確認<br /></span><span style="font-weight: 400;"> そして本番へ─────</span></p>
<p><span style="font-weight: 400;">ここでデプロイ時に30分程時間がかかることが発覚する。<br /></span><span style="font-weight: 400;">普段10分程で完了することを考えると3倍かかっていることになる。<br /></span><span style="font-weight: 400;">不具合が発生した時にすぐに修正ができないのは問題があるため今後の改修課題となった。</span></p>
<p><span style="font-weight: 400;">それ以外は順調に新たなデプロイでサーバーを追加していく、、<br /></span><span style="font-weight: 400;">動作している、大丈夫そうだ そう思った時、、、不具合が発生した。</span></p>
<p><span style="font-weight: 400;">一部のサーバーにアクセスが集中したらしく、集中したアクセスによりサーバーが落ちる。<br /></span><span style="font-weight: 400;">落ちたサーバーに割り振られていたアクセスが他のサーバーに再度振り分けられ<br /></span><span style="font-weight: 400;">他のサーバーにも飛び火していく。</span></p>
<p><span style="font-weight: 400;">すぐさまサーバー台数を増やし、負荷分散を行い事態の収拾をはかる。<br /></span><span style="font-weight: 400;">程なくして、負荷が正しく分散され正常に戻っていく。</span></p>
<p><span style="font-weight: 400;">この時点では原因がわからず、たまたま運悪く調子の悪いサーバーを掴んでしまい<br /></span><span style="font-weight: 400;">問題が発生したと思っていたが、<br /></span><span style="font-weight: 400;">2回目にデプロイを行った時にも同様の問題が発生してしまった。<br /></span><span style="font-weight: 400;">(2回目の際はサーバーダウンが起こる前に台数を増やして事無きを得た)</span></p>
<p><span style="font-weight: 400;">毎回デプロイを行うと不具合が発生するため、一度MIGでの運用を取りやめ<br /></span><span style="font-weight: 400;">既存の方法で運用を行うこととなった。</span></p>
<p><span style="font-weight: 400;">ひとまず直面したサーバー停止の不具合は収束したのだが<br /></span><span style="font-weight: 400;">重要な課題が残されてしまう形となった。</span></p>
<p> </p>
<h3 id="原因の特定と解決"><span style="font-weight: 400;">原因の特定と解決</span></h3>
<p><span style="font-weight: 400;">翌日、調査を行い<br /></span><span style="font-weight: 400;">一部のサーバーで負荷が上がり、アクセスを処理できなくなったサーバーが停止、再起動を行っている間に<br /></span><span style="font-weight: 400;">稼働している別のサーバーへアクセスが集中し、また停止する というように<br /></span><span style="font-weight: 400;">連鎖的にサービスアウトとサービスインを繰り返すようになっていたことが発覚したのだが</span></p>
<p><span style="font-weight: 400;">インフラでもなぜそのような動作になるのか調べても肝心の理由がわからず<br /></span><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>への問い合わせを行うことになった。</span></p>
<p><span style="font-weight: 400;">ただ今回の問題に関してはサーバー1台あたりのリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト上限を上げる事により解消できるとの提案も受け<br /></span><span style="font-weight: 400;">このタイミングで再度MIGの運用に切り替えることも検討したが、安全に運用を行うことを重視し、デプロイ時間の短縮を優先し一時見送る事にした。<br /></span></p>
<p><span style="font-weight: 400;">デプロイ時間の短縮が完了した連絡を受け、2023年3月末頃<br /></span><span style="font-weight: 400;">再度、本番反映が行われる運びとなる。<br /></span><span style="font-weight: 400;">デプロイ時間の短縮も行われており、前回よりスムーズに作業が進む。<br /></span><span style="font-weight: 400;">特に前回のような問題も起こらず、既存のサーバーからMIGサーバーへの移管が行われる。</span></p>
<p><span style="font-weight: 400;">───── 問題、ないのか? ─────</span></p>
<p><span style="font-weight: 400;">今回問題がないのであれば、前回起こった不具合は何が原因だったんだ<br /></span><span style="font-weight: 400;">そんな疑問を抱きながら、作業は完了を迎える。</span></p>
<p><span style="font-weight: 400;">結局、特に問題も起こらず導入が完了する。<br /></span><span style="font-weight: 400;">必要に応じてサーバー台数が増減し、適切な台数に保たれる様をグラフで確認できるようになると、感慨深いものがある。</span></p>
<p><span style="font-weight: 400;">その後1,2日ほどMIGの最大/最小台数の設定や、適切なスケジュール設定などの<br /></span><span style="font-weight: 400;">詳細なカスタマイズを行いながら通常運用を行っていく。</span></p>
<p><span style="font-weight: 400;">このまま何もなく過ごせると思っていた矢先─────</span></p>
<p><span style="font-weight: 400;"> インフラ<br /></span><span style="font-weight: 400;"> 「</span><span style="font-weight: 400;">12:00 から何か施策やっていましたか?」</span></p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kaz-yamada/20231129/20231129205035.png" width="1200" height="325" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><span style="font-weight: 400;">突如寄せられた質問と一緒に、サーバー負荷をグラフ化したスクショが送られてくる。</span><span style="font-weight: 400;">この時、特に大きな施策は行っておらず<br /></span><span style="font-weight: 400;"> 変更があったのはMIGを利用してのサーバー自動増加ぐらいのものだった。</span></p>
<p><span style="font-weight: 400;">サーバー台数がMIGによって自動で増加され、大きな問題に繋がることなく<br /></span><span style="font-weight: 400;"> 負荷は収まっていったのだが、原因を突き止めるべく議論は続く。</span></p>
<p><span style="font-weight: 400;"> ーdbへの負荷が12:00過ぎに上がっている<br /></span><span style="font-weight: 400;"> ーサーバー台数が増えてキャッシュに保存前に<br /></span><span style="font-weight: 400;"> リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トがきて詰まっていったのではないか<br /></span><span style="font-weight: 400;"> ー詰まった状態でMIGが自動で台数増やして<br /></span><span style="font-weight: 400;"> さらにキャッシュ保存前のサーバーが増えているのではないか<br /></span><span style="font-weight: 400;"> ーとすると、増台したサーバーへは<br /></span><span style="font-weight: 400;"> リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トの振り分けを通常よりも少なめにすれば良いのでは</span></p>
<p><span style="font-weight: 400;">インフラ<br /></span><span style="font-weight: 400;"> 「</span><span style="font-weight: 400;">リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トをコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>できるか週明け調べてみます</span><span style="font-weight: 400;">」</span></p>
<p><span style="font-weight: 400;">この日は土曜でアラートの収束と今後の方針が決定したことで解散となった。<br /></span><span style="font-weight: 400;">すぐさま意見交換や合意形成が行え、調査検証に移れることがこの会社の強みだなとひしひしと感じる。</span></p>
<p><span style="font-weight: 400;">しかし、調査を行う前に幾度かアラートが発生することになった。<br /></span><span style="font-weight: 400;">MIGが自動的に台数を上げて負荷を下げてくれるため致命傷とまでは行かないまでも、<br /></span><span style="font-weight: 400;">このまま運用を続けていくのには不安になる。</span></p>
<p><span style="font-weight: 400;">1週間ほどの間に、何度かアラートが届く状態が続いたところで<br /></span><span style="font-weight: 400;">どうやら台数を増やしているタイミングではなく、減らしているタイミングで負荷が増大していることに気づく。<br /></span><span style="font-weight: 400;">原因の特定には未だ至らないが、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トの分散を行っている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%C9%A5%D0%A5%E9%A5%F3%A5%B5%A1%BC">ロードバランサー</a>の部分で何やら問題がありそうなことがわかってきた。<br /></span><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>への問い合わせを行うことで何かわかりそうだ。</span></p>
<p><span style="font-weight: 400;">2日後、インフラから連絡が届く。</span></p>
<p><span style="font-weight: 400;">インフラ「</span><span style="font-weight: 400;">先ほど先方から回答が届いたのですが、<br /></span><span style="font-weight: 400;"> サーバー台数が急に変更される際、均等に配分されるのに2−3分かかるそうで<br /></span><span style="font-weight: 400;"> 増台/減台関わらず再振り分けが行われるみたいです」</span></p>
<p><span style="font-weight: 400;">なんと<br /></span><span style="font-weight: 400;"> リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト分配は 増台/減台 共に時間経過しないと正常に動作しないことがわかった。<br /></span><span style="font-weight: 400;">増台時に割り振りが偏ることは想像できたのだが、減台するときは均等分配されているものを均等に削除していくものだと思っていたため想定外だった。<br /></span></p>
<p><span style="font-weight: 400;">長い間わからなかった、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トが偏ってしまう謎の原因がようやく特定できたのだった。</span></p>
<p> </p>
<h3 id="本当の導入"><span style="font-weight: 400;">本当の導入</span></h3>
<p><span style="font-weight: 400;">原因がわかってしまえば、対処法はすぐに思いついた。<br /></span><span style="font-weight: 400;">増台を行う際は<br /></span><span style="font-weight: 400;"> 5分以上をかけてスケジュールを分割して目標台数まで上げれば良く、<br /></span><span style="font-weight: 400;">減台を行う際は<br /></span><span style="font-weight: 400;"> 1度に削除する台数に上限を設け、こちらも数回に分けて削除しておけば良い。</span></p>
<p><span style="font-weight: 400;">この回答が届いたのが金曜日、即座に対応を行えば週末に間に合う。<br /></span><span style="font-weight: 400;">スケジュールの見直し、減台数の制御など諸々の設定を無事に行い週末を迎える。</span></p>
<p><span style="font-weight: 400;"> 何も起こらないでくれ─────</span></p>
<p><span style="font-weight: 400;">結果、減台/増台を繰り返したにもかかわらず、<br /></span><span style="font-weight: 400;">アラートは発生せず、週末を無事に乗り越えれたのである。</span></p>
<p><span style="font-weight: 400;">稼働しているサーバー費用の削減をしたい<br /></span><span style="font-weight: 400;">そんな思いから長い間、取り組んできたMIGの導入がここに完結したのである。<br /></span><span style="font-weight: 400;">実に構想から10ヶ月程経っての目標達成だった。</span></p>
<p> </p>
<h3 id="あとがき"><span style="font-weight: 400;">あとがき</span></h3>
<p><span style="font-weight: 400;">というわけで、いかがでしたでしょうか?<br /></span><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a> バックエンドエンジニア 田中でございます。</span></p>
<p><span style="font-weight: 400;">実際にMIGを導入するにあたって行ったことをまとめると</span></p>
<p><span style="font-weight: 400;">◆対応まとめ<br /></span><span style="font-weight: 400;"> ・ログが削除されてしまうので、退避方法とエラー確認処理の見直し<br /></span><span style="font-weight: 400;"> ・リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト肥大タイミングに向けてのスケジュール調整<br /></span><span style="font-weight: 400;"> ・減台/増台タイミングの分割対応<br /></span><span style="font-weight: 400;"> ・デプロイ処理の変更</span></p>
<p><span style="font-weight: 400;">といった内容が主になり、<br /></span><span style="font-weight: 400;">作業的にはインフラ側に対応していただいた作業がメインになるため<br /></span><span style="font-weight: 400;">今回は運用に至るまでの体験談のように記事を書かせていただきました。</span></p>
<p><span style="font-weight: 400;">所属プロジェクトでの成果はサーバー費用全体の1割ほどの削減になり、現状でも安定して運用できております。<br /></span><span style="font-weight: 400;">試行錯誤の大切さ、原因究明の当たりをつけた上で検証/調査を進めていく<br /></span><span style="font-weight: 400;">他部署との合意形成など、普段行っている業務の一旦を感じ取ってくれたなら幸いです。</span></p>
<p><span style="font-weight: 400;">MIGを導入以降、日々のサーバー台数調整が無くなった事で他の作業に集中できるようになったり<br /></span><span style="font-weight: 400;">所属プロジェクトで安定運用ができることがわかった段階で、他のプロジェクトにも続々導入され、結果として複数プロジェクトの費用削減に繋り<br /></span><span style="font-weight: 400;">自分が想像していたよりも大きな成果が出たのではないかと満足しています。</span></p>
<p><span style="font-weight: 400;">最後に、エンタメ精神を存分に含んだ文体を公式で掲載していただいた寛大な担当者への感謝と、今回導入させていただいたMIGに関連するリンクをご紹介させていただいて<br /></span><span style="font-weight: 400;">締めくくりとさせていただきます。<br /></span><span style="font-weight: 400;">長文、駄文を最後までお読みいただきありがとうございました。</span></p>
<p> </p>
<p><span style="font-weight: 400;">◆関連リンク</span></p>
<p><span style="font-weight: 400;">マネージド <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a> グループ(MIG)<br /></span><a href="https://cloud.google.com/compute/docs/instance-groups/creating-groups-of-managed-instances?hl=ja"><span style="font-weight: 400;">https://cloud.google.com/compute/docs/instance-groups/creating-groups-of-managed-instances?hl=ja</span></a></p>
<p><span style="font-weight: 400;">自動スケーリング<br /></span><a href="https://cloud.google.com/compute/docs/autoscaler?hl=ja#specifications"><span style="font-weight: 400;">https://cloud.google.com/compute/docs/autoscaler?hl=ja#specifications</span></a></p>
<p><span style="font-weight: 400;"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%C9%A5%D0%A5%E9%A5%F3%A5%B5%A1%BC">ロードバランサー</a><br /></span><a href="https://cloud.google.com/load-balancing/docs/application-load-balancer?hl=ja"><span style="font-weight: 400;">https://cloud.google.com/load-balancing/docs/application-load-balancer?hl=ja</span></a></p>
<p> </p>
<hr />
<h2 id="ColoplTechについて">ColoplTechについて</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>では、勉強会やブログを通じてエンジニアの方々に役立つ技術や取り組みを幅広く発信していきます。<br />connpassおよびX(<a class="keyword" href="https://d.hatena.ne.jp/keyword/Twitter">Twitter</a>)で情報発信していますので、是非メンバー登録とフォローをよろしくお願いいたします。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.connpass.com%2F" title="株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
<p><cite class="hatena-citation"><a href="https://colopl.connpass.com/">colopl.connpass.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftwitter.com%2Fcolopl_tech" title="COLOPL_Tech (@colopl_tech) / Twitter" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://twitter.com/colopl_tech">twitter.com</a></cite></p>
<p> </p>
<p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%ED%A5%D7%A5%E9">コロプラ</a>ではゲームや基盤開発のバックエンド・インフラエンジニアを積極採用中です!<br />興味を持っていただいた方はぜひお気軽にご連絡ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcolopl.co.jp%2Frecruit%2F" title="採用情報|株式会社コロプラ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p>
kaz-yamada