newmo 技術ブログ

技術で地域をカラフルに

renovatebotとpnpm catalogで実現する依存関係の自動アップデートとハンドブックによる安全な更新

はじめに

モノレポ環境での依存関係管理は、プロジェクトの規模が大きくなるほど複雑になります。 newmoでpnpm catalogを導入してOne Version Ruleを実装し、パッケージのバージョンを一元管理する仕組みを構築しました。詳細はmonorepo内でのパッケージのバージョンを1つだけに統一するOne Version Ruleをpnpm catalogで実装する - newmo 技術ブログを参照してください。 しかし、一元管理の実現だけでは依存関係の更新作業そのものは依然として手動であり、セキュリティアップデートの適用遅延やレビュー負荷の増大という課題が残っていました。

本記事では、renovatebotとpnpm catalogを組み合わせた依存関係の自動更新システムについて、実装の詳細から運用方法まで解説します。 特にAuto Merge機能において、パッケージの自動更新とハンドブックを活用した安全な更新を両立する方法に焦点を当てていきます。

自動化の必要性とコスト削減

依存関係の自動更新を導入する背景には、開発効率の向上だけでなく、具体的なコスト削減の必要性がありました。

newmoでは、renovateのパッケージのアップデートPRを、ハンドブックを用いてレビューしてマージする運用をしていました。 一方で、一部のCIでは、Chromaticを使用したVisual Regression Testを実施しています。

このような中で、renovateのライブラリのアップデートPRが手動レビュー待ちで滞留する場合が あります。 これによって、renovatebotが自動的に実行するリベース処理により、CI実行回数が想定以上に増加していました。

具体的な問題の流れは次の通りです。

  1. renovatebotがライブラリのアップデートPRを作成すると、レビュー待ちの状態になる
  2. その間にmainブランチへ他のPRがマージされると、renovatebotはPRのコンフリクトを解消するために自動的にリベースを実行する
  3. リベースのたびにCIが実行されるため、結果として実際のPR数よりも多くのCIが実行される

このようのパッケージのアップデートのPRが滞留すると、Chromaticのような実行回数制限があるサービスでは、無駄なリソース消費につながっていました。

この問題の解決策として、Auto Merge機能の導入を決定しました。 安全にAuto Mergeできるパッケージを選別します。CI通過後は即座にマージすることで、PRの滞留時間を短縮し、リベース回数とCI実行回数を削減します。

Renovateのrebaseを減らす方法として、パッケージのアップデートをDraft PRにすることも検討しました。 しかし、ハンドブックを活用した手動レビューの段階で、多くが自動的にマージして問題ないこともわかっていました。 Draft PRにしてもコストは減らせますが、目的はパッケージのアップデートなので、Auto Mergeを実装する方法を選択しました。

pnpm catalogによるパッケージ管理

renovatebotによる自動化の前提として、pnpm catalogによるバージョン管理があります。 この仕組みについては既存の技術ブログ記事「pnpm catalog で構築する One Version Rule」で詳しく解説していますが、簡単に振り返ります。

pnpm catalogは、pnpm 9.5で追加された機能です。pnpm-workspace.yamlに依存関係のバージョンを集約して管理できます。 各パッケージのpackage.jsonではcatalog:プロトコルを使用してcatalogのバージョンを参照します。これにより、モノレポ全体で同じパッケージが同じバージョンで使用されることを保証できます。

newmoでは、このpnpm catalogに対してアノテーションを追加することで、メタ情報を管理しています。 具体的には、@pkg-groupでパッケージのグループ化、@pkg-automergeでAuto Merge対象の指定、@pkg-ignoreで更新対象外の指定をします。これらのアノテーションは、renovateの設定を自動生成する際の基礎となります。

# `pnpm-workspace.yaml`
catalog:
  # @pkg-group: react
  # @pkg-automerge
  react: 19.0.0
  
  # @pkg-group: react
  # @pkg-automerge
  react-dom: 19.0.0
  
  # @pkg-group: react
  # @pkg-automerge
  "@types/react": 19.0.8

このアノテーション方式を採用した理由は、pnpm-workspace.yamlをSingle Source of Truthとして扱うためです。 renovateの設定ファイルを直接編集するのではなく、pnpm-workspace.yamlから自動生成することで、設定の一貫性を保ち、他のツールへの移行も容易にします。

その結果、pnpm-workspace.yamlには次の情報が含まれます。 すべてのパッケージ、パッケージを導入したDesign DocのURL、パッケージのグループ分け、パッケージをAuto Merge対象として指定するための情報です。 この設計については、次の発表資料で詳しく説明しています。

renovatebotの選定

当初はGitHubのdependabotを使用していました。 しかし、pnpm catalogの動作がrenovatebotの方が安定していたことと、設定の柔軟さから、renovatebotへの移行を決定しました。

renovatebotがpnpm catalogに対応したのは2025年1月28日のv39.138.0リリースです。 dependabotも2025年2月4日に対応を発表し、現在はどちらもpnpm catalogに対応しています。 renovatebotを選択した理由は、対応の速さもあります。 しかし、主な理由は設定のカスタマイズ性が高く、グループ化やスケジューリングなど、モノレポ運用に必要な機能が充実しているためです。

renovateの設定の自動生成

renovateの設定を手動で管理する代わりに、pnpm-workspace.yamlとそのアノテーションから自動生成するスクリプトを作成しました。 このスクリプトは、pnpm-workspace.yamlを解析してrenovate.json5を生成します。 これにより、パッケージの追加や削除があった場合でも、設定の更新を自動化できます。

グループ分けの基準

レビューしやすくするために、renovateのgroup機能を使ってグループ単位でパッケージのアップデートをしています。グループ化は、レビューや動作検証がしやすい単位を意識して設計しています。

パッケージのグループ化は、次のような基準で行っています。

  1. アップデートが同じタイミングのものをグループ化(バージョンが同じタイミングで上がるもの)
  2. 意味的なまとまりでグループ化(ESLintなどの関連パッケージ)
  3. 確認手順が全く同じになるものをグループ化

また、特定のアプリケーションのみで使用しているパッケージは、{app-name}-sdkのようにアプリケーション名のprefixをつけてまとめます。 これにより、レビューや動作検証がしやすい単位でグループ化され、Pull Requestもこの単位で作成されます。

renovateの設定の生成ロジック

生成ロジックは次の通りです。@pkg-groupが同じパッケージは同じグループとしてまとめられ、単一のPRで更新されます。 さらに、@pkg-automergeが付与されたパッケージは、別のグループに分割され、Auto Mergeが有効化されます。

例えば、pnpm-workspace.yamlに次のような設定がある場合を考えます。

# `pnpm-workspace.yaml`
catalog:
  # @pkg-group: react
  # @pkg-automerge
  react: 19.0.0

  # @pkg-group: react
  # @pkg-automerge
  react-dom: 19.0.0

  # @pkg-group: react
  react-some-pkg: 1.0.0

このpnpm-workspace.yamlから、次のような2つのグループが生成されます。

[
  {
    "matchPackageNames": ["react", "react-dom"],
    "groupName": "web/react automerge group",
    "groupSlug": "react-automerge",
    "reviewers": ["team:frontend"],
    "addLabels": ["dependencies/automerge"],
    "automerge": true,
    "platformAutomerge": true
  },
  {
    "matchPackageNames": ["react-some-pkg"],
    "groupName": "web/react group",
    "groupSlug": "react",
    "reviewers": ["team:frontend"],
    "automerge": false
  }
]

このように、pnpm-workspace.yamlからスクリプトを使ってrenovateの設定を生成しています。パッケージの追加や変更があった場合は、スクリプトを実行してrenovate.json5を更新します。

automerge機能の実装

automerge機能は、opt-in方式で実装しました。デフォルトでは全てのパッケージが手動マージ対象であり、@pkg-automergeタグを明示的に付与したパッケージのみがAuto Mergeされます。この方針により、安全性を保ちながら段階的に自動化の範囲を拡大できます。

automerge機能の実現には、Renovate Approve GitHub Appを使用します。このアプリは、renovatebotが作成したPRを自動的に承認します。GitHubのBranch Protection設定でapprovalを必須にしている場合でも、Renovate Approve Appの承認により、Auto Mergeが可能になります。

automergeの設定

renovate.json5では、automergeplatformAutomergeの両方を有効にします。 automerge: trueはrenovatebot自身によるマージを有効にし、platformAutomerge: trueはGitHubのAuto-merge機能を利用します。 両方を有効にすることで、Branch Protectionの設定に応じた方法でマージされます。

ただし、全てのパッケージをAuto Mergeするわけではありません。Auto Mergeの対象は、次の条件を満たすパッケージに限定します。

  1. CIでの自動チェックが可能である
  2. バージョンアップデートによる影響範囲が明確である
  3. パッチバージョンまたはマイナーバージョンの更新である

さらに、Auto Mergeには次の設定を設けています。

制限項目 設定値 説明
バージョン minor/patch only メジャーバージョンは自動除外
minimumReleaseAge 7日間 リリース後7日経過が必要
実行時間 平日9-18時 営業時間内のみ実行
CI必須 すべてグリーン テスト失敗時はマージしない

minimumReleaseAgeは、新しいバージョンに潜在的な問題があった場合でも、コミュニティで発見される期間を設けることができます。

メジャーバージョンの更新は、設定に関わらずrenovatebotの対象外としています。 メジャーバージョンアップデートは破壊的変更を含む可能性が高く、CIの失敗や影響範囲の不明確さから、即座の対応が困難です。 そのため、メジャーバージョンアップデートはLinearでIssueを作成し、手動またはDevinで対応します。

スクリプトで生成されるrenovate.json5には、全てのパッケージのメジャーバージョンアップデートを無視する設定が含まれています。

{
  "packageRules": [
    {
      "matchPackageNames": ["*"],
      "matchUpdateTypes": ["major"],
      "enabled": false
    }
  ]
}

この設定により、メジャーバージョンアップデートのPRは作成されません。メジャーバージョンの更新が必要な場合は、目視でのチェックまたはDevinによる月次通知で検知し、手動で対応します。

CODEOWNERSとの共存

Renovate Approve Appを使う方法は、CODEOWNERSファイルと相性が悪いという課題があります。しかし、pnpm catalogの仕組みを活用することで、この問題をうまく解決できます。

CODEOWNERSファイルでは、次のように設定しています。

# フロントエンドの依存関係の管理
**/package.json                  @newmohq/frontend

# renovatebotでautomergeするためコードオーナーを外す
**/pnpm-lock.yaml
**/`pnpm-workspace.yaml`

ポイントは、pnpm-workspace.yamlpnpm-lock.yamlのアップデートだけはCODEOWNERSを外し、renovate appでもapproveできるようにしていることです。 一方で、package.jsonはfrontendチームをレビュアーに入れています。

pnpm catalogでは、パッケージのバージョンアップデート時にpackage.jsonを変更する必要がありません。 バージョンはpnpm-workspace.yamlで一元管理されているため、アップデートはpnpm-workspace.yamlpnpm-lock.yamlのみの変更で完結します。

この仕組みにより、次のような運用が可能になります。

  • 新規パッケージの追加: package.jsonを変更するため、frontendチームのレビューが必須
  • パッケージのアップデート: package.jsonを変更しないため、automergeが可能

ハンドブックによる運用

全てのパッケージがautomergeできるわけではありません。 そのため、newmoでは全てのパッケージについて、レビュー時の確認手順をハンドブックとしてドキュメント化しています。 このハンドブックは、Notionのデータベースで管理され、パッケージグループごとに確認手順が記載されています。

Notionのハンドブック

ハンドブックの重要な原則は、初めてそのパッケージや該当アプリケーションを触る人でもわかるように書くことです。 既存の知識を前提とした記述は避け、具体的な手順を明確に示します。

良い例として、次のような記述があります。

1. https://localhost:8090/test にアクセス
2. "送信する"ボタンが表示されることを確認
3. ボタンをクリックして、送信が成功することを確認

悪い例は、次のような記述です。

既存のものとは動作が変わらないことを確認

この記述では、既存の動作を知らない人は確認できません。 初めての人でも対応できるように、比較対象を明確に示すか、期待される動作を具体的に記述します。

ハンドブックには、次の情報を含めます。

  1. 何に使っているライブラリか
  2. 何をすると動作が確認できるか
  3. リリースノートを確認するポイント
  4. 過去の事例へのリンク

実際の例として、GIS系のパッケージ(deck.gl、duckdb-wasm等)のハンドブックでは、次のような具体的な確認手順を記載しています。

### エリア編集ページでの確認手順

1. エリア管理ページへアクセス
2. 任意のエリアの編集ページを開く
3. ポリゴンを編集(例:頂点を移動、エリアを削除/追加)
4. 編集内容が次のように表示されることを確認
   - 削除された部分:赤色で表示
   - 追加された部分:緑色で表示
5. 「GeoJSONで確認」ボタンをクリック
6. 出力されたGeoJSONが次の構造を持つことを確認
   - `type: "FeatureCollection"`が含まれている
   - `features`配列に編集内容が反映されている
   - ジオメトリの座標が正しい形式である

この例のように、具体的な操作手順と期待される結果を明示することで、初めてそのパッケージに触れる人でも確認ができます。 さらに、スクリーンショットを交えることで、視覚的にわかりやすい手順にしています。視覚的な期待値(赤色/緑色)や技術的な検証項目(GeoJSONの構造)を明記することで、確認の客観性を高めています。

また、確認手順を明文化することで、それが自動化できるかをチェックできます。実際に、7割のパッケージの確認手順は自動テストでカバーされているため、automerge対象となっています。

レビュープロセスは次の流れになります。

  1. renovatebotが自動的にPRを作成し、reviewerにfrontendチームをアサイン
  2. レビュアーはリリースノートをチェック
  3. めぼしい変更があればコメント
  4. ハンドブックを元に動作確認
  5. LGTMしてマージ

automerge対象のパッケージは、CIが通過すれば自動的にマージされるため、レビュアーの介入は不要です。ただし、CIが失敗した場合は、reviewerに設定されている人が対応します。

Devinによる支援

レビューの効率化のため、Devinとの連携も活用しています。

PRレビューの支援

@devin !depsというマクロコマンドでDevinに依存のアップデートを手伝ってもらえるようにしています。 実際のプロンプトは次の通りです。

## macro: deps

現在の {repo_name} のOpen状態のPRを確認して、`dependencies``frontend`のラベルがついているPRについて以下のタスクを実行してください。

## タスク

ライブラリバージョン更新に伴う以下の作業を行ってください:

1. 各PRの変更内容を確認し、更新されるライブラリのバージョンを特定する
2. 更新されるライブラリのリリースノートを確認し、Breaking Changesがないかを確認する
3. Breaking Changesがある場合は、それがコードにどのような影響を与えるかを分析する
4. Breaking Changesに対応する修正が必要な場合は、元のPRのブランチから新しいブランチを切って、必要な修正を加えたPRを作成する
   - 修正PRを作成する場合は、元のPRをこの修正PRにリンクする
   - renovateがついているPRは、Renovatebotが自動的に作成するので、PRに対する直接の変更は避ける
5. PRのレビューコメントに、以下の情報を日本語で投稿する:
   - 更新されるライブラリとそのバージョンの変更内容のサマリ
   - Breaking Changesの有無とその影響
   - 修正が必要な場合は、修正PRへのリンク
   - 動作への影響の評価

{repo_name} のソースコードへのアクセス方法:
- GitHub CLI: 利用可能
- Web UI: 利用可能

## 出力

各PRについて、レビューコメントとして投稿してください。
複数のPRがある場合は、それぞれのPRに対して個別にレビューを実施してください。

ただし、Devinはドキュメントデータベースを読めないため、ハンドブックの確認は人間が担当します。Devinはあくまで補助であり、最終的な判断はレビュアーが行います。

メジャーバージョンアップデートの通知

renovateではメジャーバージョンアップデートを自動化していないため、手動でアップデートします。しかし、アップデートを忘れてしまうリスクがあります。

この問題に対して、Devinに毎月1回、メジャーバージョンアップデートをまとめて報告してもらう仕組みを導入しています。実際のプロンプトは次の通りです。

## Task: Monthly Major Version Update Report

{repo_name} のパッケージについて、メジャーバージョンアップデートをSlackで報告してください。

## 手順

1. リポジトリをクローン
2. `pnpm outdated -r` を実行し、メジャーバージョンアップデートのあるパッケージを検出
3. 検出された各パッケージについて、次の情報をSlackチャンネル {slack_channel} に投稿:
   - パッケージ名
   - 現在のバージョン
   - 最新のバージョン
   - リリースノートのURL
   - そのパッケージが使われているプロジェクト(workspaceの名前)
   - リリースノートからの簡単なサマリ(日本語、主な変更点を3〜5個程度)

4. 全てのパッケージの報告が終わったら、最後に @dev-frontend をメンションして完了を通知

## 注意事項

- パッケージごとに別々のメッセージとして投稿する(後でLinearでIssueを作成しやすくするため)
- リリースノートが見つからない場合は、GitHubのリリースページやCHANGELOGを確認する
- サマリは技術的な内容を含め、開発者が判断できる情報を提供する

{repo_name} のソースコードへのアクセス方法:
- GitHub CLI: 利用可能
- Slack API: 利用可能

実行の自動化には、Slack Workflowを使用しています。Slack Workflowで毎月1回Devinにメンションすることで、cronjobのようにDevinの実行を自動化できます。

実装の効果

automergeの適用状況

newmoの現在の状況は次の通りです。

  • パッケージ総数: 96個
  • automerge可能なパッケージ: 71個(約74%)
  • 手動マージが必要なパッケージ: 22個
  • 無視されたパッケージ: 3個
  • グループ数: 21グループ

約7割のパッケージがautomerge対象となっており、これらのパッケージについてはレビュー作業が不要になりました。 残りの約3割は、まだ自動テスト化できていない確認項目があるため人間によるレビューをしていますが、今後も段階的に自動化を進めていく予定です。

得られた効果

  1. パッケージ更新作業の削減:約7割のパッケージでレビュー作業が不要
  2. CI実行回数の削減:PR滞留時間の短縮により、リベース回数が減少
  3. セキュリティアップデートの適用速度向上:セキュリティパッチを即座に適用
  4. レビュー品質の向上:ハンドブックによる基準の明確化

Chromaticのビルド回数が削減され、コストの増加を抑制できています。 マージコンフリクトの発生頻度も減少して、Auto Mergeによりレビュー待ちで滞留する時間が短縮されました。

今後の展望

現在約7割のパッケージがautomerge対象ですが、残りの約3割についても、自動テストの拡充により段階的にautomerge対象を増やしていく予定です。

また、pnpm-workspace.yamlをSingle Source of Truthとしているため、将来的に他のツールへの移行が必要になった場合も、比較的容易に対応できます。実際に、dependabotからrenovatebotへ移行した際には、スクリプトを書き換えるだけで移行できています。

まとめ

renovatebotとpnpm catalogの組み合わせにより、依存関係の自動更新システムを実現しました。 重要なのは、全パッケージの確認手順を明文化したハンドブックです。この確認手順のうち自動テストでカバーできるもの(約7割)はautomerge対象とし、それ以外は人間がハンドブックを見て確認します。 ハンドブックによる基準の明確化があって初めて、どこまで自動化できるかの判断が可能になります。 automergeだけでは安全性が保てず、ハンドブックだけでは効率が悪くなります。この2つを組み合わせることで、効率性と安全性を両立した依存関係管理が可能になりました。

pnpm catalogによる一元管理が基盤となり、アノテーションによるメタ情報管理、renovateの設定自動生成、automerge機能、ハンドブックによる運用という一連の仕組みが連携しています。 この仕組みにより、monorepoでの依存関係管理の複雑さを軽減し、開発者がコア機能の開発に集中できる環境を整備できました。

feature flag 入門と newmo の feature flag 基盤について

こんにちは。Platform Team の tobi (@iam__tobi) です。

本記事では feature flag の基礎的事項の説明と、Platform Team で開発してきた newmo 独自の feature flag 基盤の設計思想と全貌について二段構成でご紹介します。

これから feature flag を導入しようと考えている方にとって参考になれば幸いです。

背景・概要

スタートアップである newmo では、変化に俊敏に追従し、開発サイクルを高速に回すことが事業成長の鍵となります。そのためには「いつでもリリースできる状態」を保ち、機能追加を素早く本番環境に届ける開発フローが欠かせません。 トランクベース開発 *1 は、単一のメインブランチへ小さな変更を高頻度にマージすることで (CI/CD が適切に構築されている場合)、高頻度なデプロイを実現できます。実際 newmo ではトランクベース開発を採用しており、最近では 1 日あたり平均 100 回ほど main へブランチがマージされ、そのたびに開発環境へコードがデプロイされています。

しかしこの状態では、デプロイと機能リリースが同時に行われるため、実験的な機能や不具合が即座に全ユーザーへ露出し、修正やロールバックのたびに再デプロイが必要となるため大きな負担となります。 また、高速な開発サイクルのなかで、開発環境と本番環境の機能差異を単なる環境変数やコードベースの差分だけで実現するのは現実的ではありません。ユーザー属性ごとに機能を出し分けることも困難です。

ここで feature flag が重要な役割を果たします。newmo の Platform Team では、トランクベース開発のスピードと安全性を両立させるため、高信頼・低コスト・開発者フレンドリーな feature flag 基盤をゼロから設計・実装しました。

feature flag 入門

feature flag について、もう一度おさらいしておきましょう。

feature flag を解説している入門記事はたくさんありますが、今回は Pete Hogdson 氏の記事を特に参考にさせていただきました。

本記事ではこの記事を前提に話を進めていきます。

feature flag の概要

記事では feature flag を次のように説明しています。

Feature Toggles (often also referred to as Feature Flags) are a powerful technique, allowing teams to modify system behavior without changing code.

(中略)

“Feature Toggling” is a set of patterns which can help a team to deliver new functionality to users rapidly but safely.

feature flag (feature toggle) は、コードを変更することなくシステムの振る舞いを変えられる仕組みです。これにより開発者は、新機能を迅速かつ安全に提供できます。

feature flag を用いたコードのイメージを示します。

func (s *Service) SomeFunc(ctx context.Context) error {
  if s.featureflag.Evaluate(ctx, "do-experiments-flag") {
    // do something experimental
  } else {
    // do something stable
  }
}

do-experiments-flag は feature flag の名前であり、実験的機能のオン・オフを制御します。これにより、実験的機能のロールアウトを環境ごとに切り替えたり、ユーザーごとに評価結果を変化させたりできます。たとえば、あるセグメントにはフラグ値として true を返して実験機能をリリースし、別のセグメントには false を返してリリースしない、といった制御が可能です。

このようにユーザーごとに値の評価を制御する手法は Dynamic Evaluation *2 と呼ばれ、本記事でも以降頻出します。

feature flag の構成要素

「feature flag を導入する」と一口に言っても、実際には複数の細かな要素が精緻に組み合わさって機能しています。ここでは、feature flag 関連のブログ記事でよく取り上げられる構成要素を紹介します *3

まず、以下のようなシンプルなコードを想定します。

if featureflag.IsActive("holiday-greeting") {
  fmt.Println("Happy holidays!")
}

(引用: Feature Toggles (aka Feature Flags))

Toggle Point

Toggle Point とは、フラグを判定するコード上の“分岐点”です。上記の例ではIsActive("holiday-greeting") が該当し、ここでフラグの値に応じて実行パスが切り替わります。

Toggle Router

Toggle Router は、多数の Toggle Point に対して一貫した評価結果を提供する単一の情報源 (single source of truth) です。例では featureflag.IsActive がフラグ名を受け取り、Toggle Configuration と Toggle Context を用いて内部ロジックに基づいて真偽値を計算し、結果を返しています。したがって、この関数が Toggle Router の役割を果たします。

Toggle Context

Toggle Context は、Toggle Router がフラグを評価する際に考慮する文脈情報です。例では「現在の日付」が文脈に当たる可能性がありますが、ほかにもユーザー ID・ユーザー属性・参照元 URL など多岐にわたり、ビジネスロジックと深く結びつきます。OpenFeature では evaluation context がこの役割を担います。

Toggle Configuration

Toggle Configuration は、Toggle Router がフラグを評価するために参照する 外部設定 です。たとえば flagd では、以下のような JSON 定義を利用します。flagd の詳細は後述しますが、ここでは「フラグ評価に必要なすべての情報が格納された設定ファイル」と認識していただければ良いです。

{
  "$schema": "https://flagd.dev/schema/v0/flags.json",
  "flags": {
    "new-welcome-banner": {
      "state": "ENABLED",
      "variants": {
        "on": true,
        "off": false
      },
      "defaultVariant": "off",
      "targeting": {
        "if": [
          { "ends_with": [{ "var": "email" }, "@example.com"] },
          "on",
          "off"
        ]
      }
    }
  }
}

feature flag の種類

記事では feature flag を dynamismlongevity の 2  軸で 4  種類に分類しています。

(引用: Feature Toggles (aka Feature Flags))

  • dynamism
    フラグ評価がどれだけ動的か (=実行時にどの程度変化するか) を示します。軸の左側に行くほど値の変化は デプロイ単位、右側に行くほどより細かい粒度の リクエスト単位 で変化すると例示されています。
  • longevity
    フラグがコードベースにどれだけ長く存続するかを示します。

feature flag はコードベースの複雑性を高めるため、カテゴリごとにオーナーシップや運用ポリシーを明確にすることが重要です。フラグを定義する際は dynamismlongevity の特性を考慮し、適切なカテゴリへ分類しましょう。

Release Toggles

ユースケース 開発中の不完全・未テストの機能を、本番にデプロイしつつユーザーには非公開にする。
トランクベース開発を支援し、「機能のリリースとデプロイを分離する」という継続的デリバリーの原則を実現。
dynamism 非常に静的。特定のデプロイでは常に同じ評価結果を返すよう定義される。
longevity ごく短命 (通常 1〜2 週間程度)。

Experiment Toggles

ユースケース カナリアリリース、A/B テスト、多変量テストの実施。
エンドユーザーを cohort (セグメント) に分け、セグメントごとに評価結果を変える。
dynamism 非常に動的。リクエストごとに評価結果が変化。
longevity 安定性の検証や統計的に有意な結果が得られるまで存続。

Ops Toggles

ユースケース システム運用中に動作を制御する。
高負荷時に重要度の低い機能を無効化する Kill Switch など。
dynamism 静的寄り。デプロイ単位ほど静的ではないが、リクエスト単位ほど動的でもない。
longevity 新機能の安定性やパフォーマンスへの影響が把握できるまで存続。場合によっては無期限に残ることもある。

Permission Toggles

ユースケース ユーザー属性に応じて機能/体験を差別化。
例:有料ユーザーのみ「プレミアム機能」を有効化、内部ユーザー向けに「アルファ機能」を公開。 (Champagne Brunch)
dynamism 非常に動的。リクエストごとに評価結果が変化。
longevity プレミアム機能などを管理する場合、数年単位で維持されることもある。

補足
“Champagne Brunch” は内部またはベータユーザー向けに新機能を先行公開する手法の俗称です。大規模サービスで広く採用されています。

OpenFeature

feature flag サービスは、OSS から SaaS (いわゆる  FFaaS) まで多種多様に存在しています。Unleash、Flagsmith、LaunchDarkly、Split.io など、選択肢に事欠きません。しかし、各プロダクトが提供する 独自 SDK をアプリケーションへ組み込むたびに、次のような課題が生じます。

  • ベンダーロックイン
    乗り換え時にコードを全面改修するコストが発生する
  • オブザーバビリティのばらつき
    SDK ごとにテレメトリツールとの統合方法が異なる

こうした断片化を解消するべく誕生したのが OpenFeature です。

(引用: Welcome to OpenFeature)

“OpenFeature is an open specification that provides a vendor‑agnostic, community‑driven API for feature flagging that works with your favorite feature flag management tool or in‑house solution.”

OpenFeature は、ベンダーニュートラルな共通 API/SDK を 10  以上の言語で提供しています。アプリケーション側は client.BooleanValue("some-flag") のように統一的な呼び出しを書くだけでよく、実際のフラグ評価ロジックは Provider プラグイン (flagd・LaunchDarkly・Flipt など) を差し替えるだけで変更できます。主要プロダクトは多言語で Provider を提供されています。  *4

これにより、次のような利点が得られます。

  • バックエンドを切り替えてもアプリ改修は最小限
  • Hooks 機構で OpenTelemetry など横断的処理を一括注入
  • 自前実装からフルマネージド SaaS まで 同じ SDK で運用可能

feature flag を単なるツールではなく インターフェース として捉えることで、将来の移行・統合・多言語開発にも強い基盤を築ける点が最大の魅力です。

2025  年  7  月  20  日現在、OpenFeature は CNCF の Incubating Project です  *5

flagd

newmo では feature flag の評価エンジンとして flagd を採用しています。

flagd is a feature flag evaluation engine. Think of it as a ready-made, open source, OpenFeature-compliant feature flag backend system.

flagd は OpenFeature 準拠のインターフェースを実装しており、公式に Provider も提供されているため、最初に利用する OSS として非常に有力な選択肢です。

flagd の評価モードには RPC EvaluationIn-Process Evaluation の 2 種類があります。
RPC Evaluation は、外部プロセスが gRPC を介して flagd プロセスにフラグ評価のリクエストを行う方式です。一方、In-Process Evaluation では flagd の評価エンジンを同一プロセス内の goroutine として動かすため、RPC Evaluation に比べて低レイテンシを実現できます。newmo では Modular Monolith を採用していることから、In-Process Evaluation Mode を選択するのが合理的だと判断しました。

また、「SaaS を使わず自社環境で feature flag を運用したい」 というニーズに対しても、flagd は優れた OSS の候補となります。理由は以下のとおりです。

  • flagd は独自スキーマの JSON を Toggle Configuration として採用しており、専用 UI を持たないため 完全な SSoT (Single Source of Truth) を構築しやすい。
  • 評価エンジンをゼロから作るのは大きな労力を要しますが、flagd を使えば既存の仕組みを流用しつつ、エンジン部分の実装知識は不要で運用を開始できため、「feature flag を自社用にカスタマイズしたいが、評価エンジンをフルスクラッチで開発するのは避けたい」 というケースで特に適している。

flagd の Toggle Configuration については 公式ドキュメント に詳細がありますが、以下に例を示します。

{
  "$schema": "https://flagd.dev/schema/v0/flags.json",
  "flags": {
    "new-welcome-banner": {
      "state": "ENABLED",
      "variants": {
        "on": true,
        "off": false
      },
      "defaultVariant": "off",
      "targeting": {
        "if": [
          { "ends_with": [{ "var": "email" }, "@example.com"] },
          "on",
          "off"
        ]
      }
    }
  }
}

flagd の Toggle Configuration は大きく以下の要素で構成されます。

  • state
    • フラグが有効かどうかを示します (true / false の値そのものではなく、利用可能か否かの状態)。
  • variants
    • 取りうる値 (バリアント) とキーのマッピング。
  • defaultVariant
    • エラー発生時にフォールバックするデフォルト値。
    • 通常は false や "stable" など、安全な値を設定しておくのが望ましい (誤って機能がリリースされるのを防ぐため)。
  • targeting
    • evaluation context の情報と組み合わせて、誰にどの値を返すかを決定するルール。JsonLogic で記述する。
    • 上記例では、evaluation context の email が @example.com で終わる場合は true、そうでなければ false を評価するルール。

このように flagd のシンプルな構成はカスタマイズ性と運用の容易さを両立させており、newmo の既存の Platform とも非常に相性が良いです。

フラグ評価の流れ

ここまで抽象的な説明が続いたので、実際に feature flag がどのように 設定 → 呼び出し → 評価 されるのかを、OpenFeature + flagd の構成を例におさらいします。

フラグ評価の流れ

まず Toggle Point で、OpenFeature Client のインターフェースでフラグの値の評価を試みます。またこの時、beta_user=true という evaluation context (Toggle Context) も同時に与えています。

次に Toggle Router が呼び出されます。Toggle Router の中では flagd が常に動いており、flagd は

  • 外部ソースを定期的に確認して常に最新の Toggle Configuration をメモリにロードしている
  • リクエストを受けたらメモリの Toggle Configuration とリクエスト内容 (フラグ名、Toggle Context) から値を評価し、結果を返す

というサイクルを続けています。

Toggle Configuration は、flagd のものを例に出しています。targeting の意味は、「渡された evaluation context に beta_user のキーがあり、値が true であるならばフラグ結果として true、 そうでないならば false」です。

Toggle Configuration の保存場所は、S3、GCS、コンテナ内のローカルファイルシステム、gRPC 経由などさまざま用意されています。FFaaS だと 専用 UI から Toggle Configuration を設定して実際にはクラウドに保存されているでしょう。

flagd を前提におさらいしましたが、他の OSS や FFaas でも

  • Toggle Router のなかで動いている評価エンジンや OpenFeature Provider が OSS/FFaaS に依存
  • Toggle Configuration のスキーマや設定の仕方、保存場所

こそ異なりますがおおまかなフラグ評価の流れは同じです。

さてここまでを踏まえると、今回のリクエストでは Toggle Context として beta_user=true を渡しているので true が評価されて返ってくることが理解できます。

feature flag 導入の技術的負債

feature flag は決して銀の弾丸ではありません。むしろ 技術的負債を生み出しやすい ことが、欠点として頻繁に指摘されます。代表的な問題は次のとおりです。

  • Zombie Flags
    役目を終えたフラグがコードに残り、条件分岐が肥大化する問題です。誰が・いつ実装したか履歴が曖昧だと、そもそも削除してよいフラグか判断できません。

  • Flag Proliferation (フラグの増殖)
    フラグが増えすぎると、テストケースが爆発的に増加し、テストカバレッジが低下します。複数フラグの組み合わせでバグが潜在化しやすくなる点も厄介です。

  • フラグ名不一致
    フラグ名を magic string として直接コードに書くと、OSS・SaaS 側で設定したフラグ名との不一致が起こりやすくなります。フラグが存在しない/タイプミスといった単純ミスが、意図しない分岐を招くリスクもあります。

これらの負債は feature flag 自体の価値を大きく損ないかねません。feature flag による負債を抑えつつメリットを最大化するためには、ガバナンスとオートメーション が欠かせません。

newmo の feature flag 基盤

newmo feature flag の利点を最大限に活かしつつ欠点を仕組みで抑え込み、何よりも 開発者の生産性向上 を優先した基盤を構築しました。本章では、その設計思想と前提条件を概観します。

前提

newmo では、サーバーサイド実装には Modular Monolith アーキテクチャを採用しています。機能ごとに分割された各サービスを component と呼び、相互通信には gRPC を用います。この記事で component という語が登場した場合は、「Modular Monolith を構成する 1  モジュール」の意味だと捉えてください。*6

設計思想

ここでは、次の 4  つの観点から newmo の feature flag 基盤の設計思想を説明します。

1. 認知負荷の軽減

Platform Engineering の文脈で頻出する「開発者の認知負荷をいかに下げるか」という課題に対し、newmo ではとくに 宣言的定義・自動生成・fire and forget の 3  点に注目して設計しました。

A. 宣言的定義
feature flag の独自スキーマを Protocol Buffers で定義し、そのスキーマに従って YAML でフラグを記述します。開発者は簡潔で明瞭なインターフェースで Toggle Configuration を宣言でき、GitHub の  main  ブランチが「全環境で有効なフラグ状態」の SSoT (Single Source of Truth) となります。過去の設定も Configuration as Data として完全にバージョン管理されています。

B. 自動生成
A によって定義された Toggle Configuration を入力に、各 OSS/FFaaS 向け Toggle Configuration と Toggle Router を自動生成します。Toggle Context を扱う内部ロジックは画一的に実装して Router から切り離し、各 component 用には薄いラッパーのみを自動生成するため、呼び出すだけで透過的に Dynamic Evaluation が機能します。さらにテスト専用のオンメモリストアとヘルパーも自動生成し、テストケースごとに任意の値を override することも可能です。これによりフラグの取り得る値は enum として型安全に扱え、フラグ名の不一致といったヒューマンエラーを原理的に排除できます。

A でスキーマを丁寧に設計していれば、Toggle Configuration と Toggle Router は原理的に自動生成できるはずです。なぜなら各 OSS, SaaS 向けの Toggle Configuration は独自スキーマの Toggle Configuration 表現の部分集合になっており、Toggle Router は複雑な内部ロジックを切り離すと単に Toggle Context とフラグ名で問い合わせて、フラグ評価結果を返す責務しか負っていないからです。

C. fire and forget
Zombie Flag 対策としては一般に「expiration date の付与」「削除チケットの発行」「Slack 通知」などが提案されますが、開発者の認知負荷が最も低いのは「フラグを定義したら、不要になったとき自動で消える」状態だと考えました。newmo では Toggle Point こそがフラグの存在証明であると定義し、定期ジョブでフラグごとの Toggle Point 数を計測して参照されなくなったフラグを自動削除しています。

こうして 定義 → 生成 → 削除 までのライフサイクルをすべて独自基盤で一元管理しています。各項目の詳細は後述します。

2. 低コスト

「コスト」は component の管理コスト、金銭的コスト、新たなフラグを定義する心理的コストの 3  側面で評価しています。

まず管理コストについて、newmo ではワークロードに Cloud Run を採用していますが、サービスを 1  つ増やすごとに管理対象が増えるのは得策ではありません。そこで flagdIn‑Process Evaluation mode *7 を利用し、Modular Monolith 内で評価エンジンを動かしています。これにより外部サービスを増やさずに済み、運用負荷を極小化できました。

金銭的コストも、flagd をベースにした自前実装であるためランニングコストを基本的に少額に抑えられます。前述のとおり追加の Cloud Run は不要なのでインフラ費用も増えません。

心理的コストについては、認知負荷削減の施策により YAML ファイルを作成・更新し PR を出すだけでフラグを定義できるため、UI 操作や手動設定がなく、開発者は迷わずフラグを追加できます。

3. 既存の仕組みとの統合

newmo のアプリケーションにおける認証・認可は Security Token Service Pattern を用いています。API Gateway レイヤでユーザーの Access Token を Security Token Service に渡し、内部トークンへ変換したうえで gRPC を介して各 component に伝播させています。この内部トークンには sub (ユーザー ID) や各種属性が含まれるため、feature flag の Toggle Context としてそのまま利用可能です。既存認証基盤に自然に組み込めるため開発者は違和感なく Toggle Configuration を記述できます。

(引用: Microservices Security Cheat Sheet)

4. 高信頼性

feature flag 基盤は高い信頼性が必須です。可用性がボトルネックになるケースは現在ありませんが、Toggle Configuration の自動生成や Toggle Context のロジックにバグがあれば即インシデントにつながります。そのため自動生成コードは Golden Files Test *8 で差分を検出し、評価ロジックは fake-gcs-server を立てて実際にエンジンを動かし、多様な条件でフラグ評価を行うことで実環境に近い形でテストカバレッジを確保しています。さらに評価エンジンの Metrics・Trace・Log は OpenTelemetry Collector を経由して Datadog へ送信し、運用面の可観測性も担保しています。*9

その他に、ドキュメント整備や独自 OpenFeature Provider の開発、多言語・多プロセスへの展開など、将来拡張も視野に入れて基盤を進化させており、今後も高い信頼性を保ちながら機能拡充を図る予定です。

newmo における feature flag の分類

newmo では Pete Hodgson 氏の 4 分類を参考にしつつ、プロダクト特性に合わせて定義を再考しました。

nemwo における feature flag の分類

大きな相違点は三つあります。第一に Release Toggles の位置づけが右 (=より動的) へ寄っていること、第二に Static Toggles という新しいカテゴリを設けたこと、第三に Ops TogglesPermission Toggles を現時点では定義していないことです。

Static Toggles はサーバー初期化時の DI や外部サービスのバージョン切り替えなど、環境変数に近い用途で使うフラグです。ただし再デプロイを伴わずに振る舞いを変えられる点が大きな違いで、新規サービスの導入検証や環境ごとのサービス切り替えなどに適しています。

Release Toggles は機能をリリースするかどうかを true / false で判定するフラグで、Hodgson 氏の定義と異なり ユーザー情報を Toggle Context として持つ ことを前提にしています。そのため環境別のロールアウト制御に加え、内部ユーザー限定の QA リリースのようなユースケースにも活用できます。

Experiment Toggles はユーザーを複数のセグメントに分割し、各セグメントに対して任意のパーセンテージで任意の文字列を返すフラグです。A/B/n テストやカナリアリリースなど、より細かな実験・検証で効果を発揮します。

再定義の背景として、newmo ではドライバー向けアプリなどモビリティ関連プロダクトを提供していますが、リリース由来のバグが重大インシデントにつながります。そのため「特定ユーザーだけを安全に対象化する」という要件が多く、Release Toggle の dynamisim を上げてユーザー属性を考慮できるようにしています。

また Experiment Toggle が許容する dynamism は Release Toggle の superset であるため、2 カテゴリのレンジが一部重なるよう図示しています。開発者は A/B/n テストやカナリアリリースをする場合や存続時間が長いと予想される場合には Experiment Toggle を、そうでない場合は Release Toggle を利用します。図は Pete Hodgson 氏のオリジナル図の下位領域を拡張したイメージで、現状ユースケースのない Ops Toggle と Permission Toggle は定義していませんが、将来的に必要になれば上方向へ領域を拡張する想定です。

アーキテクチャ

feature flag 基盤のアーキテクチャ

上図が newmo における feature flag 基盤の全体像です。

まず Toggle Configuration は flagd の JSON Schema に従う JSON 形式です。ここには「宣言的定義」のセクションで述べる newmo 独自スキーマを flagd スキーマへ変換した結果が格納されます。

次に OpenFeature Provider Layer です。flagd の In-Process Resolver (In-Process Evaluation Mode) には、Toggle Configuration をポーリングしてメモリに保持する sync provider という内部コンポーネントが存在します。公式にもいくつか実装が提供されていますが、newmo の Modular Monolith では 1  つの sync provider が複数の GCS  バケットを定期的に監視する必要があるため、自前で sync provider を実装しました。flagd 用の OpenFeature Provider 自体は既に OSS として公開されているため、In-Process Evaluation Mode で起動しつつ自作 sync provider を DI するだけで簡単に Provider を構成できます。

OpenFeature Client Layer では、feature flag のカテゴリ判定に応じた処理、内部トークンから Evaluation Context (Toggle Context) を生成する処理、OpenFeature Client のラップによる BooleanValue/StringValue で値取得、さらに Trace/Metrics/Log を Datadog へ送信する処理を担います。

Toggle Router Layer は「自動生成」のセクションで詳述するように、独自 Toggle Configuration から自動生成された薄いラッパーです。内部的には前述の OpenFeature Client Layer のメソッドを呼び出すだけのシンプルな構造になっています。

Toggle Point Layer は各 component の初期化やビジネスロジックが記述される場所で、新機能のリリースやカナリアリリースを行いたい箇所に Toggle Point を挿入します。

mobile / web などクライアントサイド、あるいは別プロセスから feature flag を参照したい場合も、同じ基盤にアクセスします。OpenFeature Client Layer を薄くラップした featureflag component がその責務を担い、GraphQL や gRPC 経由でフラグ評価を提供します。各言語ごとに独自の OpenFeature Provider を用意し、同様に自動生成された Toggle Router を組み込んでいるため、ここで説明した構造がそれぞれのランタイムに内包されます。

宣言的定義

newmo ではモノレポを採用しており、mobile/web/server/IaC など、すべてのソースコードを Git で一元管理しています。feature flag についても同じ思想を適用し、「Git を見れば現在すべての環境の Toggle Configuration が把握できる」状態を目指しました。そのため Toggle Configuration は YAML で宣言し、ソースコードとともにバージョン管理しています。

featureflags:
  - name: "some-feature"
    release:
      targeting:
        preset: FEATURE_FLAG_TARGET_PRESET_ALL_END_USER
      variant:
        local: true
        dev: true
        stg: true
        prod: false

上記は some-feature という Release Toggles を定義する例です。targeting.preset は評価対象を示すプリセットで、FEATURE_FLAG_TARGET_PRESET_ALL_END_USER は「すべてのエンドユーザー」を表します。このほかにも「内部ユーザーのみ」「環境ごとに指定 ID のユーザー」「特定属性を持つユーザー」など、さまざまなプリセットを用意しており、必要に応じて自由に拡張できます。

variant セクションでは環境ごとの評価結果を宣言できます。例では ローカル・開発・ステージング環境 では常に true、本番環境 では false を返すよう設定しています。コードを変更せず YAML を更新するだけでロールアウト戦略を切り替えられるため、レビューやロールバックの運用コストを最小化できます。

なお Static Toggles と Experiment Toggles も同様に Protocol Buffers で型安全なスキーマを定義しており、開発者は YAML で直感的にフラグを追加・編集できます。

自動生成

上で定義した Toggle Configuration を元に、

  • flagd の Toggle Configuration
  • 各言語向けの Toggle Router
  • 各言語向けのテストヘルパー

を自動生成しています。

some-feature を例にすると、flagd 側の Toggle Configuration は次のようになります。

{
  "$schema": "https://flagd.dev/schema/v0/flags.json",
  "flags": {
    "<component name>.some-feature": {
      "state": "ENABLED",
      "variants": {
        "off": false,
        "on": true
      },
      "defaultVariant": "off",
      "targeting": {
        "if": [
          {
            "and": [true]
          },
          "on",
          "off"
        ]
      },
      "metadata": {
        "category": "release"
      }
    }
  }
}

YAML で宣言した内容がそのまま JSON に変換されています。targeting を見ると「常に "on" (=true)」と評価されることがわかります。targeting.preset に複雑なプリセットを指定した場合でも、JsonLogic へ自動的に変換されるため柔軟なルール生成が可能です。

Go 向けの Toggle Router は次のように自動生成されます。

// Code generated by xxx; DO NOT EDIT.

package featureflag

import (
    "context"

    "github.com/newmo/.../featureflag"
)

const component = "<component name>"

type ReleaseToggle interface {
    SomeFeature(ctx context.Context) bool
}

type releaseToggle struct {
    client featureflag.Client
}

func NewReleaseToggle(client featureflag.Client) ReleaseToggle {
    return &releaseToggle{client: client}
}

func (r *releaseToggle) SomeFeature(ctx context.Context) bool {
    flagName := "<component name>.some-feature"

    v, err := r.client.BooleanValue(ctx, flagName, false)
    if err != nil {
        return false
    }
    return v
}

フラグ名を UpperCamelCase にしたメソッドを自動生成しています。フラグが増えてもメソッドも増えるため、フラグ名の typo が原理的に発生しません。静的解析もしやすく、後述するフラグ使用回数の集計にも役立ちます。また Static Toggles や Experiment Toggles のように評価値が string 型になる場合も、定義された variant 一覧 から enum を自動生成して型安全に扱えます。

テストヘルパーは以下のように生成されます。

// Code generated by xxx; DO NOT EDIT.

package featureflagtest

import (
    "context"

    "github.com/newmo/.../fakefeatureflag"
)

const component = "<component name>"

type FlagSetter struct {
    client *fakefeatureflag.Client
}

func NewFlagSetter(client *fakefeatureflag.Client) *FlagSetter {
    return &FlagSetter{client: client}
}

func (f *FlagSetter) SetSomeFeature(ctx context.Context, value any) error {
    flagName := "<component name>.some-feature"

    err := f.client.SetFlag(ctx, flagName, value)
    if err != nil {
        return err
    }
    return nil
}

fakefeatureflag はメモリ上にフラグ値を保持するフェイククライアントです。テスト前に任意の値をセットできるため、分岐ごとのカバレッジを高めやすくなります。

これらのコードは Go だけでなく、feature flag を利用する他言語向けにも同様に自動生成されます。

fire and forget

“fire and forget” とは、本来は呼び出し側が非同期リクエストを送ったあと、レスポンスを待たずに後続処理を進める設計を指します。newmo の feature flag 基盤でも同じ発想を採り、「一度定義して利用したら、役目を終えたフラグは自動で消えてくれる」状態を目標にしています。

開発者が YAML で宣言した Toggle Configuration は、main へのマージをトリガーに GitHub Actions が自動で flagd 用 JSON に変換し、GCS へアップロードします。逆に Toggle Point (フラグ呼び出し箇所) が 0 になった feature flag は CI によって検出され、同じく CI 上でクリーンアップされます。

定期クリーンアップの Slack 通知

ここで注意すべきは、Toggle Configuration の同期タイミングと Toggle Router のデプロイタイミングが必ずしも一致しない点です。Configuration はすべての環境で main へマージされた瞬間に反映されますが、Router (=アプリケーションのデプロイ) は環境ごとにタイムラグが生じる可能性があります (例えば、リリースするブランチが各環境で異なる運用など)。したがって 真に削除してよいフラグ は、「各環境で稼働しているすべてのバージョンのソースコードで Toggle Point が 0 になった場合」に限られます。

このあたりの詳細は、弊社 Iwamin (@B_Sardine) さんによる発表資料が詳しいのでぜひ参照してください  *10

Toggle Point の検出は次の方法で実装しています。Go コードは AST を用いた静的解析で確実に呼び出しを捉え、ほかの言語は暫定的に正規表現で候補を grep しています。Toggle Router をコード生成しているため、メソッド名を検索すればよいという点でも自動生成の恩恵を受けています。

補足
正規表現での検索は偽陽性が発生しやすいため、本来は tree‑sitter などのパーサを使うのが望ましいです。tree‑sitter には WASM  バインディングがあり、Go であれば wazero を用いて実行することも可能かもしれませんね。

今後の展望

現状では Toggle Point の削除作業を Product Team に全面的に委ねているため、削除漏れが発生すると Zombie Flag が溜まっていきます。このままでは Zombie Flag に対する本質的な解決には至りません。今後は、よりプロアクティブなアプローチを検討していきたいと考えています。

さらに、feature flag 基盤は高い信頼性を担保できなければなりません。評価ロジックにバグや障害があると、Toggle Router が誤った値を返して一般ユーザーに実験機能を公開してしまうなど、重大なインシデントへ直結します。現在はリクエスト数が多くないため In-Process Evaluation mode で運用できていますが、事業がスケールした際には可用性一貫性を両立させる設計が不可欠です。将来的にはリモート評価クラスタの導入やキャッシュ戦略の最適化、マルチゾーン冗長化などを視野に入れ、基盤全体の耐障害性とパフォーマンスを強化していくことも考えられるかもしれません。

さいごに

長文にお付き合いいただき、ありがとうございました。feature flag は Gartner の Hype Cycle に 2023,2024 年で “slope of enlightenment” に位置付けられ *11、CNCF では OpenFeature が Incubating Project に選定されるなど、世界的にさらに注目されている技術領域となっています。一方で国内の具体的な活用事例はまだまだ少なく、導入を躊躇っている開発チームも多いのが実情だと思います。本記事が、トランクベース開発と feature flag を取り入れて開発者体験を向上させたい皆さまの一助になれば幸いです。

最後に、Flagsmith の Ben Rometsch 氏が OpenFeature 公式ブログに寄稿した記事 *12 から印象的な一節を引用します。

“Introducing feature flags will fundamentally change the way you build software, so pulling in the right stakeholders from the beginning is important. Start by creating a working group with representatives from your engineering, QA, product, and DevOps/infrastructure teams. Additionally, you’ll need full executive buy-in to bring flags on at scale. This is true from a budget perspective but, just as importantly, from a time perspective, too. Part of the reason the eBay team was so successful with their OpenFeature adoption is because they were given full license to use engineering hours to get it done.”

feature flag の導入はソフトウェア開発の在り方を根本的に変えるため、初期段階から関係者をしっかり巻き込むことが重要です。まずはエンジニア、QA、プロダクト、DevOps/インフラの代表者でワーキンググループを組織しましょう。さらに、大規模運用には予算面だけでなく工数確保の観点でも経営層の全面的な理解と協力が欠かせません。eBay チームが OpenFeature を成功させた要因の一つは、エンジニアリング時間を自由に投入できた環境があったからです。

newmo では、CI/CD の自動化基盤、認証認可とコンポーネント間伝播の仕組み、OpenTelemetry/Datadog によるオブザーバビリティ、自動生成ツールなどの 既存の Platform があったからこそ、その上に feature flag 基盤を構築することができています。また重要なのは、技術を導入して終わりにするのではなく、継続的な開発への投資と改善を惜しまない姿勢 です。Platform は一朝一夕で完成するものではなく、育て、使い込み、磨きをかけ続けることで、はじめて組織とプロダクトの進化を支える強力なドライバーとなります。

これからも各チームが挑戦の旗を掲げ、高い生産性を維持しながら活躍し続けられるよう、Platform Team は Platform Engineering を推進していきます。

書いた人: tobi

TSKaigi 2025に登壇しました

2025年5月23日と24日に TSKaigi 2025 が開催され、newmo でソフトウェアエンジニアをしている @yui_tang@nozomuikuta が一般応募枠で登壇しました。それぞれの発表の資料と概要は以下のとおりです。

TypeScript エンジニアが Android 開発の世界に飛び込んだ話

speakerdeck.com

本発表では、主に TypeScript で UI 開発を行っているエンジニアの方々に向けて、TypeScript と Kotlin、Web フロントエンドと Android アプリ開発に共通する多くの構造や思想、そしてその背景について紹介しました。これにより、ネイティブアプリ開発に対する心理的ハードルを下げ、ソフトウェアエンジニアとしての関心領域を拡張するきっかけを提供できればと考えています。

スマートフォンアプリ開発においては、Flutter や React Native などクロスプラットフォームな技術も選択肢として一般的ですが、実際にはスマートフォン以外の領域――たとえば Android OS 上で動作する IoT 機器や専用ハードウェア端末――も広く存在しており、Kotlin による Android ネイティブ開発の需要は今後も根強く続くと考えられます。

また、異なる言語・異なるプラットフォームでの開発経験は、主戦場としている技術領域にも新たな気づきやフィードバックをもたらす貴重な機会です。特に Kotlin Multiplatform 等進化により、Web とモバイルの設計戦略が再接近しつつある今、相互理解はますます重要となっています。

今回の発表が、「学び、繋がり、“型”を破ろう」 という本カンファレンスのテーマに沿った形で、皆さんが新たな技術領域へ一歩踏み出すきっかけとなったならば幸いです。

Standard Schema: スキーマライブラリの統一規格とは何か

speakerdeck.com

本発表では、近年流行しているスキーマライブラリの乱立による課題と、それに対するスキーマライブラリ作者たちのソリューションである「Standard Schema」の紹介をおこないました。

スキーマライブラリによって、宣言的にフォームデータを扱えたり、宣言したスキーマのランタイムバリデーションの仕組みや型を利用できる一方、各スキーマライブラリの互換性のなさ(ロックイン)や、他のライブラリとのコネクタが必要になったりする課題がありました。

この課題に対して、著名なスキーマライブラリであるZod、Valibot、Arktypeの作者たちが統一規格である「Standard Schema」を制定し、それぞれのスキーマライブラリはこの規格を実装するかたちに実装を変更しました。これにより、スキーマライブラリを利用するレイヤーはそれぞれのスキーマライブラリに対するコネクタ不要で自由に取り扱いができるようになります(場合により「Standard Schma」のコネクタは必要な場合があります)。スキーマライブラリそのものへのロックイン度合いがさがった分、アプリケーション開発者が複数のスキーマライブラリを併用できるようになったり、必要に応じてどちらかに統一するなどのコストが下がりました。

今後は、スキーマライブラリどうしについては、提供する機能の他にバンドルサイズや開発者体験などの部分でより差別化がされていくのではないかと思います。

TSKaigi 2025 について

TSKaigi は、日本最大級の TypeScript に関する技術カンファレンスで、2024年より開催され、関西での開催を含めて今年で通算3回目となります。TSKaigi 2025 は昨年に引き続きオフライン&オンラインの同時開催および初の2 days開催となり、前年にもまして盛り上がっていました。

当日は 700 人を超える人々が会場に集まったとのことですが、このような熱量と規模を持つイベントは国内でもなかなかありません。TSKaigi を実現するために尽力してくださったボランティアの運営スタッフのみなさま、そしてスポンサー企業のみなさまに、あらためて感謝いたします。

発表中のサカモトユイ
発表中の @yui_tang

発表中のイクタノゾム
発表中の @nozomuikuta

newmo と TypeScript

現在、newmo ではタクシー業界の DX を推進するため、少人数のチームで、外部向けアプリケーション、各種管理画面、タブレット向けのアプリケーションをWebフロントエンドで並行開発しています。そのような状況では、開発をスケールさせる仕組みづくりが重要であり、TypeScript による型安全性の恩恵を受ける度合いが日に日に増しています。newmo では今後も TypeScript とそのコミュニティに貢献しながら、日本のモビリティ業界を盛り上げていきます。

引き続きソフトウェアエンジニアを募集しています

newmo では TypeScript でアプリを開発していきたいエンジニアを積極的に採用中です!

Cloud Service Mesh for Cloud Run で実現する PR 環境

この記事では、Cloud Service Mesh for Cloud Run を利用して PR 環境を構築する方法について紹介します。

背景・概要

newmo ではトランクベース開発を行なっているため、開発環境での動作確認は main branch (trunk) に merge されていることが前提になっています。

そのため現状では、手軽に開発環境で API の動作確認ができなかったり、動作検証が十分でないコードが main branch に merge されてしまう課題があります。CI での test 実行などにより一定品質は担保していますが 、PR 環境 (GitHub の Pull Request ごとに用意される一時的な環境) で QA ができれば問題発見のタイミングを前にずらすことができます。

PR 環境の要件は以下の通りです。

  • 機能追加を行なった PR が実際に Cloud Run Service としてデプロイされ、クライアントアプリからのトラフィックを受け付ける状態になっていること。
  • クライアントアプリはもちろん、その他関連するコンポーネントのコードに手を入れずに、接続する PR 環境を変更できること。
  • PR 環境へのアクセスは HTTP ヘッダ (e.g. pr-number: 2000) のみによりコントロールすること。

本記事の内容は静的コンテンツを配信する Web サーバーなどにも流用できますが、今回は API サーバーに限定してお話しします。

Cloud Service Mesh for Cloud Run とは

Cloud Service Mesh は、Google Cloud が提供する フルマネージドなサービスメッシュ機能です。主に Google Kubernetes Engine で利用されてきましたが、Cloud Run でも利用できるようになりました。(2025/4/8 時点では Preview 機能)

サービスメッシュを導入することで一般的には以下のことが可能になります。

  • サービス間通信の可視化
  • トラフィックの高度なルーティング
    • e.g. Canary リリース、Blue/Green デプロイメントを支える L7 でのルーティング
  • mTLS による通信のセキュリティ強化
  • リトライ・タイムアウト・サーキットブレイカーなどの仕組みによる通信の信頼性向上

Cloud Service Mesh for Cloud Run は現時点でこの全てに対応しているわけではありませんが、今回必要な「高度なルーティング機能」には対応しています。

そのため Cloud Service Mesh for Cloud Run を利用して PR 環境を構築することにしました。

アーキテクチャ

Cloud Service Mesh for Cloud Run は、以下の主要なコンポーネントで構成されています。(公式ドキュメントにプロビジョニングの手順が記載されているのでここでは詳細は割愛します。)

  • Cloud Service Mesh
    • Google Cloud が提供するフルマネージドなトラフィック管理サービス。 (=サービスメッシュのコントロールプレーン)
    • Mesh Client に sidecar として Envoy proxy を注入する。
  • Mesh Client
    • 本記事では、サービスメッシュにクライアントとして参加している Cloud Run のことを Mesh Client と定義します。
    • 下図で言うところの、GraphQL Federation にあたります。
      • GraphQL Federation から PR 環境は単一のホストに見えており、ただヘッダを伝播してリクエストしているだけ。
      • (余談ですが、newmo では GraphQL Federation を採用しています。@mrno110 さんの記事も是非ご覧ください。)
  • Mesh Server
    • 本記事では、サービスメッシュにサーバーとして参加している Cloud Run のことを Mesh Server と定義します。
    • 本記事のメイントピックである PR 環境がこの Mesh Server にあたります。
    • Mesh Server には、さらに以下のリソースが必要です。
      • Backend Service
      • Serverless Network Endpoint Group
  • HTTP Route
    • 特定のホスト名に対して、トラフィックを Mesh Server にルーティングするためのリソースです。
      • Istio の VirtualService に相当。
    • 様々なルーティングのルールを定義することができますが、本記事では HTTP Header によるルーティングを行います。
  • Cloud DNS
    • HTTP Route で使用するホスト名を解決するための Private な DNS です。
    • A レコードの値は RFC 1918 の Private IP アドレスであれば何でも良いです。

PR 環境の構築

PR 環境を新たに構築するには以下の手順が必要です。

  1. PR 環境用の Cloud Run Service を新たに作成する。
  2. Serverless Network Endpoint Group を作成する。
  3. Backend Service を作成し、Serverless Network Endpoint Group をアタッチする。
  4. HTTP Route に新たにルールを追加する。

PR 環境を削除するには逆に以下の手順が必要です。

  1. HTTP Route からルールを削除する。
  2. Backend Service を削除する。
  3. Serverless Network Endpoint Group を削除する。
  4. PR 環境用の Cloud Run Service を削除する。

エラーハンドリングなどを考えると、この手順は GitHub Actions 内で gcloud コマンドで行うには煩雑であるため、Go の SDK を利用した CLI を謹製しました。

また、CI の workflow をシンプルにするため、CLI は冪等性を持つような設計にしました。(冪等性がないと CI 側で gcloud コマンドを実行して存在確認する必要がある。テストもしにくい。)

以下は CLI のインターフェースのイメージです。

# 以下で作成
./newmo-mesh create --pr <PR number> --app <cloud run service name>

# 以下で削除
./newmo-mesh delete --pr <PR number> --app <cloud run service name>

CI ベース (Push 型)

CI ベースでは、GitHub Actions の workflow を利用して PR 環境を構築します。

PR 環境のライフサイクルは以下のように設計しました。

  • PR に特定のラベルが付与された場合に PR 環境を構築する。
  • 特定のラベルがついた PR に更新の commit があった場合に Cloud Run を更新する。
    • Mesh 関連のリソースは更新しない。
  • 特定のラベルがついた PR が merge/close された場合に PR 環境を削除する。

このライフサイクルを実現するために、GitHub Actions の workflow を以下のように記述します。

.github/workflows/create-pr-environment.yaml (一部抜粋)

name: Create PR Environment
on:
  pull_request:
    types:
      - labeled
      - synchronize
...
jobs:
  create-pr-env:
    steps:
      - ...
      - name: Check whether a PR environment needs to be created
        id: set-result
        env:
          EVENT_TYPE: ${{ github.event.action }}
          LABEL_NAME: ${{ github.event.label.name }}
          LABELS_JSON: ${{ toJSON(github.event.pull_request.labels) }}
        run: |
          deploy_pr_env="false"

          # label が付与された場合、そのラベルが特定のラベル名に合致するか
          if [[ "${EVENT_TYPE}" == "labeled" ]]; then
            if [[ "${LABEL_NAME}" == "<特定のラベル名>" ]]; then
              echo "This PR is labeled for PR Environment deployment."
              deploy_pr_env="true"
            fi

          # 更新の commit があった場合、特定のラベルが既に付与されているか
          elif [[ "${EVENT_TYPE}" == "synchronize" ]]; then
            echo "PR labels: ${LABELS_JSON}"
            if echo "${LABELS_JSON}" | grep -E -q '"name": ?"<特定のラベル名>"'; then
              echo "This PR is labeled for PR Environment deployment."
              deploy_pr_env="true"
            fi
          else
            echo "Unsupported event type: ${EVENT_TYPE}"
          fi

          echo "deploy_pr_env=${deploy_pr_env}" >> "$GITHUB_OUTPUT"
      
      # docker build と Artifact Registry への push
      - ...

      # gojq を用いて、以下の変更を行なった YAML を生成
      # (1) .metadata.name を <cloud run service name> + "-" + <PR 番号> に変更
      # (2) .spec.template.spec.containers[0].image を Artifact Registry のイメージに変更
      - ...

      # Cloud Run にデプロイ
      - name: Deploy to Cloud Run
        run: |
          gcloud run services replace ".../tmp-service.yaml"

      # Cloud Service Mesh に必要なリソースの作成
      - name: Create PR Environment
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          APP: <cloud run service name>
        run: |
          ./newmo-mesh create --pr "${PR_NUMBER}" --app "${APP}"

.github/workflows/delete-pr-environment.yaml (一部抜粋)

name: Delete PR Environment
on:
  pull_request:
    types:
      - closed
...
jobs:
  delete-pr-env:
    steps:
      - ...
      # Cloud Service Mesh に必要なリソースの削除
      - name: Delete PR Environment
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          APP: <cloud run service name>
        run: |
          ./newmo-mesh delete --pr "${PR_NUMBER}" --app "${APP}"

      # Cloud Run の削除
      - name: Delete Cloud Run Service
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          APP: <cloud run service name>
        run: |
          service_name="${APP}-${PR_NUMBER}"
          
          if gcloud run services describe "${service_name}" > /dev/null 2>&1; then
            echo "Service ${service_name} exists. Deleting..."
            gcloud run services delete "${service_name}" --quiet
          else
            echo "Service ${service_name} does not exist."
            exit 0
          fi

CI により、単一の HTTP Route リソースを参照しルールの書き換えが行われます。そのため、これらの workflow の同時実行数は1にしておくのが良いです。(Control the concurrency of workflows and jobs)

Reconcile ベース (Pull 型)

こちらは GitOps に着想を得たアプローチで、定期的に実行されるジョブが Observe → Analyze → Act し、actual state (実際の状態、つまり Cloud Run としてデプロイされている PR 環境一覧) を desired state (あるべき状態、つまり GitHub 上で label がつけられた open な PR 一覧) に近づけるアプローチです。

この Reconciler のシーケンス図を書いた時点で、非常に複雑で要件に対して too much になることが判明したため newmo では CI ベースのアプローチを採用しました。

ハマりポイント

構築の中でいくつかハマったポイントがあったので、備忘録として残しておきます。

※ 以下の内容は、2025/4/8 時点での情報です。今後変更される可能性があります。

※ 公式ドキュメントに記載されていない内容もあるので、あくまで参考程度にしてください。newmo では開発環境にのみ適用していますが、この仕組みを本番環境に適用する際は十分に検証された上で適用してください。

YAML で Cloud Run を定義している場合

Cloud Run は serving.knative.dev/v1 の Service として YAML で定義することができます。(参考)

newmo では YAML で Cloud Run を定義していますが、YAML で Mesh Client として参加させるための方法が公式ドキュメントには未だ記載されていません。

そこでリバースエンジニアリング的に、実際に Console 画面から Mesh Client として参加させ、YAML の変更を見てみると以下の追加がありました。

spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/mesh: "projects/<project-id>/locations/global/meshes/<mesh-name>"
        run.googleapis.com/mesh-dataplane: sidecar

逆にこの annotation を付与した状態で gcloud run services replace を実行すると、無事 Mesh Client として参加させることができました。

Shared VPC を利用している

Cloud Run をデプロイするプロジェクトで Shared VPC を利用している場合、以下のようにプロビジョニングするとうまくいきます。

項目 Host / Service
Cloud DNS Host
Cloud Service Mesh Service
HTTP Route Service
Backend Service Service
Serverless NEG Service
Cloud Run Service

今後の展望

PR 環境をさらに良いものにするためには、以下の機能が必要です。

  • Database のブランチ対応
    • PR が DB スキーマの変更を含む場合、現状の PR 環境の構成ではワークしない可能性があります。
    • そのため、メインの Database はそのままで PR 環境用の別の Database を一時的に作って新しいスキーマを migration して利用するような仕組みが必要です。(Neon など)
  • Pub/Sub や外部サービスとの連携
    • Pub/Sub を利用している PR 環境では、発行したメッセージを同じ PR 環境で受ける必要があります。
    • 同様に PR 環境から外部サービスへリクエストした際の callback のレスポンスを PR 環境で受ける必要があります。
  • Load Balancer と直接接続した PR 環境
  • Observability
    • PR 環境ごとのトラフィックの数やエラーレート、レイテンシなどを可視化。

Cloud Service Mesh for Cloud Run の今後のアップデートにも非常に期待しています。

+α: Envoy の xDS configuration と Cloud Service Mesh の debug

Cloud Service Mesh は詳細が隠蔽されており、複雑な仕組みになっているので通信できなかった時の debug が難しいです。

csds-client というツールを利用することで、

  • Envoy proxy が注入されているか
  • Envoy proxy が Cloud Service Mesh から xDS configuration を取得できているか
  • xDS configuration に正しい xDS が定義されているか

などを確認することができます。(参考)

例えば、これまで構築してきた Mesh Server に関する xDS configuration は以下のように確認できます。

  1. Route の確認

typeUrl = type.googleapis.com/envoy.config.route.v3.RouteConfiguration で、Private DNS に定義したドメイン名の route が定義されています。

  1. Cluster の確認

1で確認した RDS の routes[].route.wightedClusters.clusters[].name に、その route に対応する cluster が定義されています。

もちろん対応する Cluster の定義も存在し、かつ transportSocket も自動的に設定されているはずです。

これにより、Mesh Client のアプリケーションで http://<private-dns-name> でリクエストを投げたとしても、Envoy proxy が TLS origination を行い、HTTPS で Mesh Server にリクエストを投げていることがわかります。

書いた人: tobi

Devinが作るPull Requestのセルフマージを禁止する

AI開発ツールDevinが作成したPull Requestに対して、セキュリティと品質を確保するために2人の承認を必要とする実装方法について解説します。

2025/05/21 追記

  • レビューコメントが30件以上あったときに正しく動かない問題を修正しました
  • 開発者が書いたPull RequestをDevinにApproveしてもらってマージするパターンも防ぎたい場合は、ifの条件を少し変える必要があります。

背景

newmoでも少し前からDevinを利用して開発を行っています。 Devinを利用するフローは、以下のような感じになります。

  • エンジニアがSlackやDevinのWeb UIからタスクを依頼
  • DevinがGitHubにPull Requestを作成
  • エンジニアは必要に応じてSlackやPull Request上でコメント
  • Devinがコードを修正
  • 問題なければ承認してマージ

devin-task-flow

このフローには、エンジニア一人が指示して書かれたコードをそのエンジニア自身が承認してマージできてしまうという課題があります。 自分のPCでコーディングのためのAIエージェントを使う場合には、Pull Requestはエンジニアのアカウントで作られるため自分で承認してマージすることはできませんが、Devinを利用する場合はDevinのGitHub AppがPull Requestを作るため、指示したエンジニアが承認してマージすることができてしまいます。

newmoではmainブランチへのマージには1人以上の承認が必要なルールを設定しています。 これはコード品質やセキュリティの確保のためということももちろんありますが、チーム内での知識の共有やチームとしてコードに責任を持つためにも重要なことです。そのため、Devinなどのエージェントを利用してエンジニア一人がPull Requestを作成した場合にも、同じ人が承認してマージしてしまうような状況は防ぎたいです。

解決方法

Devinが作成したPull Requestだけには、2人以上の承認がないとマージできないようにします。 GitHubの設定でも必要レビューワー数を設定できるのですが、すべてのPull Requestで2人の承認が必要になると不便なので、特定のAppが作ったPull Requestだけ2人の承認を必要にしたいと思います。

GitHub Actionsでは、Pull RequestにReviewがついたときにトリガーするpull_request_review というイベントを利用することができます。以下のGitHub Actionsワークフローは、AI(Devin)が作成したPull Requestに対して、pull_request_review イベントを利用して2人以上の人間によるレビュー承認を必要とする機能を実装します。ワークフローの動作は次のとおりです:

  1. Pull Requestが開かれた時点で、「ai-generated」ラベルを自動的に付与し、2人の承認が必要であることを知らせるコメントを投稿します。
  2. レビューが提出されるたびに、有効な承認数をカウントします。このとき、AI自身からのレビューや単なるコメントは承認としてカウントしません。
  3. 承認数に基づいてコミットステータスを更新します。2人未満の承認の場合は「failure」状態となり、Branch Protectionのルールと組み合わせることでマージがブロックされます。2人以上の承認があれば「success」状態となり、マージが可能になります。
name: AI PR Protection

on:
  pull_request:
    types: [opened, reopened, synchronize, ready_for_review]
     branches: [main]
  pull_request_review:
    types: [submitted]

jobs:
  verify-ai-pr-approvals:
    # AuthorがDevinで、main branch向けのPRだけを対象にする
    # GitHub ActionsのVariablesで AI_USER_DEVIN に 'devin-ai-integration[bot]' を入れておく
    if: >
      github.event.pull_request.user.login == vars.AI_USER_DEVIN

    permissions:
      pull-requests: write
      statuses: write

    steps:
      # 初回はPull Requestにラベルとコメントをつける
      - name: Label and Comment
        if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            # PRに、承認が2つ以上必要だよとコメントする
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: '⚠️ **Security Notice**: This PR was created by an AI tool and requires at least 2 human approvals before it can be merged. Please review the changes carefully.'
            });

            # PRにラベルをつける
            await github.rest.issues.addLabels({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              labels: ['ai-generated']
            });

      # 承認数のチェックとステータス更新
      - name: Check Approval Count and Update Status
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            # PullRequestの情報を取得
            const prNumber = context.payload.pull_request 
              ? context.payload.pull_request.number
              : context.payload.pull_request_review.pull_request.number;

            const sha = context.payload.pull_request
              ? context.payload.pull_request.head.sha
              : context.payload.pull_request_review.pull_request.head.sha;

            # レビューの取得
            const reviews = await github.paginate(
              github.rest.pulls.listReviews,
              {
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: prNumber,
                per_page: 100,
              }
            );

            # AI userのリスト。ここではDevinだけ追加する
            const aiUsers = [
              '${{ vars.AI_USER_DEVIN }}',
              // Add more AI users when needed
            ];

            # AIが作成したPRは承認数が2つ必要
            const requiredApprovals = aiUsers.includes(author) ? 2 : 1;

            const latestReviews = new Map();
            for (const review of reviews) {
              const submittedAt = new Date(review.submitted_at);
              const reviewer = review.user.login;

              # AIからのreviewは無視する
              if (aiUsers.includes(reviewer)) {
                continue;
              }

              # approve後にcommentすることもあるので、COMMENTEDのstatusは無視する
              if (review.state === 'COMMENTED') {
                continue;
              }
              
              # reviewerの最新のreview statusを残す
              if (
                !latestReviews.get(reviewer) ||
                new Date(latestReviews.get(reviewer).submitted_at) < submittedAt
              ) {
                latestReviews.set(reviewer, review);
              }
            }

            # Approveした人の数をカウント
            let approvalCount = 0;
            for (const r of latestReviews.values()) {
              console.log(`${r.user.login}: ${r.state} at ${r.submitted_at}`);
              if (r.state === 'APPROVED') {
                approvalCount++;
              }
            }

            # Pull Requestイベントと、Pull Request Reviewイベントで同じcontext名を使用してステータスを更新
            const statusContext = 'AI PR Approval Check';
            # 承認が2以上ならsuccess、それ以外はfailureにする
            await github.rest.repos.createCommitStatus({
              owner: context.repo.owner,
              repo: context.repo.repo,
              sha: sha,
              state: approvalCount >= 2 ? 'success' : 'pending',
              context: statusContext,
              description: approvalCount >= 2 
                ? 'Required approvals received'
                : `Needs ${2 - approvalCount} more approval(s)`,
              target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`
            });

            console.log(`PR: ${prNumber}, Approval Count: ${approvalCount}`);

この仕組みにより、通常のPull Requestには1人の承認でOKという既存のルールを維持しながら、AIが作成したPull Requestに対してのみ2つ以上の承認を必須にすることができます。 もっと簡単にできる方法があったら知りたいので、教えてください。

書いた人: tjun

SRE Kaigi 2025に登壇しました & Marpでスライドを作った話

2025/1/26に開催されたSRE Kaigi 2025に、「SREとしてスタッフエンジニアを目指す」というタイトルで登壇しました。 発表を聴きにきていただいた方、またAsk the Speakerや懇親会で話しかけてくれた方ありがとうございました。 SRE Kaigiは今回が初めての開催なのにコミュニケーションのための工夫がいろいろされていて、多くの人が参加してコミュニケーションしていてとても盛り上がったイベントでした。自分自身も休憩スペースでたこ焼き食べながら知り合いと久しぶりにコミュニケーションしたり、懇親会では技術的な話をしたりと楽しむことができました。

発表内容はnewmoとは関係ないのですが、これまでの経験を元にしたSREのキャリアの話をしました。 以下が発表資料になります。

SREとしてスタッフエンジニアを目指す / SRE Kaigi 2025 - Speaker Deck

登壇資料について、自分はこれまでGoogle Slidesを使って作成することが多かったのですが、今回はMarpをつかってスライドを作成しました。以下ではMarpを使ったスライド作成について簡単に紹介します。

Marpとは

MarpはMarkdownを使ってプレゼンテーションスライドを作成できるツールです。 主な特徴としては、Markdownでスライドを記述できて、HTMLやCSSでデザインを変更できて、HTMLやPDFなどにエクスポートができるという感じです。

Marpを使ったスライドの作成

爆速できれいな LT スライド作りを支える技術 を参考に、テーマの設定などを行いました。 そのあとはMarkdownでスライドを書いていきます。今回の発表資料の元となったMarkdownは以下になります。 marp-slides/src/2025-srekaigi/index.md at main · tjun/marp-slides · GitHub

図の作成はMarpではできないので、別のところで作成して画像として貼り付ける必要があります。 今回は、Napkin AIRecraftを使って作成してみました。 Napkin AIは説明のための図を作るときに利用しました。デザインはあらかじめ用意されたパターンから選ぶ形のためAIで作った感は出ちゃいますが、図のテイストを各スライドで揃えることができたのでその点よかったです。

Napkin AIで作った画像の例

Recraftは、字ばかりのスライドが続くときにそれっぽい雰囲気の図を入れたいときに使いました。

Recraftで作った画像の例

自分は図を作るのはそんなに上手ではなく、時間がかかる割にクオリティも低かったので、AIで作る方が良さそうです。

スライドのスタイルをいじる場合はCSSを書く必要があります。テーマのCSSを変更する以外にもページ固有のスタイルをMarkdown内にCSSで書くことが可能です。 CSSの知識があまりなくてもAIに聞きながら書けばある程度なんとかなりました。

Marpを使った感想

自分には合っていてスライド作りが以前より速くできたので今後も利用しようと思っています。

良かった点

gitで変更を管理できる

これはかなり良いポイントでした。Google Slideでも変更履歴は残ると思いますが、gitで管理できるのは安心感があります。

エディタで書ける

ページを行ったり来たりせずに一つのMarkdownで全部のページを書くことになるので、全体の流れを一気に書いていけて体験がよかったです

AIでのレビューがしやすい

Markdownのスライド全文を入力して構成などをレビューしてもらえるのも良かったです。スライドのドラフト作成もAIでやることもできますが、AIで発表内容をドラフトしてみたところ、間違ってはいないけれども自分の言葉で話せないような一般的すぎる内容という感じだったので、今回は作成時は利用せずレビューに利用しました。

ページのメインメッセージ(H1)が柔軟にレイアウトされる

ページの見出し + 1−2行のスライドが上部に寄らずに中央にレイアウトされるのは見やすくて好きでした。

課題点

課題点はあまり多くないのですが以下のような部分が挙げられます。

  • 作図ができない。図を作って入れたいときに、別の所で作る必要があります。
  • ちょっとしたスタイル変更はCSSを書く必要がある
  • 柔軟なレイアウト、たとえばこの余白のところに画像を入れたい、みたいなことは難しい
  • パワーポイントを持っていない場合PDFやHTML等への出力になるため、遷移のアニメーション等のプレゼンテーションツールの機能は使えない

レイアウトや遷移などにこだわりたい場合には、Marpではやりたいことができないかもしれません。自分にはこれらの課題もちょうどいい制約となり、スライド作成がこれまでより効率良く進めることができました。

まとめ

SRE Kaigi 2025登壇の感想と、Marpを使った資料作りについて書きました。 今後も機会があれば社内の取り組みを発信していきたいと思います。

【年末】DatadogのGoogle Cloud Integration設定を見直そう【大掃除】

こんにちは。 newmoでは、Datadogを利用してGoogle Cloudをはじめとした各種サービスの監視を行っています。今回はDatadogのGoogle Cloud Integration設定の改善をしたことで、コストを削減できた話を共有します。(たぶん)2024年最後の記事ということで、年末の設定見直しの参考にしていただけたら幸いです。

DatadogでのGoogle Cloud Integration

DatadogでGoogle Cloud Integrationを設定してメトリクスを収集することは昔からできました。以前はGoogle Cloud側でService Account Keyを発行してDatadogに登録する必要があったのですが、いまはService Accountのimpersonateの仕組みを使って安全で簡単にIntegrationの設定ができるようになりました。 Terraformで設定するときは datadog_integration_gcp_sts リソースを使って設定できます。

Google Cloud Integrationの設定によってGoogle Cloudの各ProjectのさまざまなメトリクスがDatadog上で見られるようになるので、DashboardやMonitorを作成して利用することができます。しかし、そこには一つ課題がありました。

Cloud MonitoringのAPIの利用コスト

DatadogがCloud MonitoringのAPIへリクエストを送ってMetricsを取得するため、Cloud Monitoringのコストがかかります。 Google Cloudの各Projectで毎日200-400円程度かかっており、Datadogと連携しているGoogle CloudのProjectがすでに20程度あるのでこれだけで毎月10万円以上のコストとなっていました。newmoはまだGoogle Cloudの利用規模がそこまで大きくない中でこの金額は結構な割合を占めており、削減したいと考えていました。

ある日Datadogの設定を眺めていたところ、Google Cloud Integrationの設定の中に「Metrics Collection」という項目があることに気づきました。

DatadogのGoogle Cloud Integration設定画面

以前はこの設定はなかったと思うのですが、メトリクスのnamespace単位で取得するかどうかを設定できるようになっていました。 デフォルトではすべてのメトリクス namespaceを取得する設定になります。しかし、実際には利用していない・利用する予定がないメトリクスnamespaceも多いため、多くをdisableすることができそうです。不要なメトリクスnamespaceをdisableすることで、APIコールの回数が減ってコストの削減につながりそうなので、試してみることにしました。

ちなみにメトリクスのnamespaceは https://cloud.google.com/monitoring/api/metrics_gcp のドキュメントを見ることで、それぞれが何のメトリクスなのか必要なのかどうか判断ができると思います。

Metrics Collectionを設定してみる

TerraformのDatadog Providerでは v3.47.0からこの機能が利用可能になっていました。

Terraformで以下のような設定をしてみます。 disableするものをリストする必要があるので、一覧を取得してリストにしています。 基本はdisableの設定にして、全Projectでよく使う一部のものだけコメントアウトする + 各Projectで個別で必要になるものはvariableで渡せるようにして管理します。 (Terraformのsetsubtractという関数を初めて使いました)

locals {
  # Datadogでメトリクスを取得しないnamespaceのデフォルトのリスト。共通で必要なものはコメントアウトしている。
  datadog_default_disable_metric_namespaces = [
    "actions",
    "aiplatform",
    "alloydb",
    "apigateway",
    "apigee",
    "appengine",
   -- 中略 -- 
    "run",
    # "serviceruntime", Quotaの監視に使うので外しておく
    "spanner",
    # "storage", よく使うので外しておく
    "storagetransfer",
    "telcoautomation",
    "tpu",
    "trafficdirector",
    "transferappliance",
    "translationhub",
    "videostitcher",
    "visionai",
    "vpcaccess",
    "vpn",
    "workflows",
    "workload",
  ]
}

# 有効にするメトリクス namespace
variable "datadog_enable_metric_namespaces" {
  description = "enable metric namespaces for datadog"
  type        = list(string)
  default     = []
}

resource "datadog_integration_gcp_sts" "project_integration" {
  client_email                = google_service_account.datadog.email
  resource_collection_enabled = false
  is_cspm_enabled             = false

  metric_namespace_configs = [
   # defaultでdisableするリストから、variableで指定したenableするリストを取り除く
    for namespace in setsubtract(local.datadog_default_disable_metric_namespaces, var.datadog_enable_metric_namespaces) : {
      id       = namespace
      disabled = true
    }
  ]
}

メトリクスnamespacesの一覧は、DatadogのUIからMetrics Collectionをdisable allしたあとにDatadogのAPIドキュメントのGCP intergrationにあるAPIを叩くことで確認が可能です。今後変化があるかもしれないので、ときどき確認すると良さそうです。

この設定を適用してみた結果、Cloud Monitoringのコストが半分以下になっていることが確認できました。

Cloud Monitoringコストの遷移

みなさまも年末の大掃除として確認してみてはいかがでしょうか。

参考

DatadogのGoogle Cloud Integrationについては以下の記事にまとまっていました。 qiita.com

書いた人: tjun