GeekFactory

int128.hatenablog.com

2025年のお仕事まとめ

2025年のお仕事をまとめてみた。

プラットフォームの機能拡充

KEDA による外部メトリクスや時間帯のスケーリング

通知や集計といった重い処理を非同期ジョブで処理しているが、worker pod のスケーリングが難しいという課題があった。例えば、非同期ジョブがない場合は pod が暇になってしまうが、スパイクした場合は瞬く間に pod が足りなくなってしまう。通知や集計処理などの遅れはユーザー体験に直結するため、なるべく早く完了することが望ましい。非同期ジョブは主に SQS や Redis を経由して処理されている。これまで HPA から Datadog のメトリクスを参照するスケーリングが導入されていたが、どうしても数分の遅延が発生するという問題を抱えていた。

そこで、KEDA で SQS や Redis のメトリクスを直接参照するスケーリングを導入した。KEDA の導入は、非同期ジョブの課題を持っているサービスオーナーと Kubernetes のプラットフォームを提供している SRE で横断的なチームを作り、週2回30分枠のモブプロで進めた。非同期ジョブで要求されるユーザー体験によってスケーリングのパラメーターは異なる。例えば、多少レスポンスが悪くても問題ないジョブの場合は worker pod を0台までスケールインできる。しかし、スパイクに耐えられるようにするには min replicas を上げておくことが望ましい。そのような塩梅を判断するにはサービスオーナーの高い解像度が不可欠である。

また、ピークタイムに Pod を増やす仕組みを KEDA に移行した。これまでは GitHub Actions で HPA の min replicas にパッチを適用するという複雑な仕組みが導入されていたが、KEDA では時間帯と最低台数を宣言的に定義できるようになった。プラットフォームのセルフサービス化という意味でも大きな改善であった。

Self-hosted Renovate への移行とサービスオーナーによる分割統治

現職では大規模な monorepo を運用している。これまで Mend-hosted Renovate が導入されていたが、2025年の春ぐらいからリソース不足による失敗が目立つようになった。Renovate が途中で異常終了した場合、Renovate は PR を rebase するため、日中帯に GitHub Actions が詰まるという問題が常態化していた。この機会に GitHub Actions で Renovate を運用する構成に移行することにした。

移行にあたっては、以下のような課題も解決することを目指した。

  • 長年に渡り、さまざまなサービスオーナーによって設定ファイルの継ぎ足しが繰り返されていた
  • 横断的な設定とマイクロサービス固有の設定が複雑に入り組んでおり、最終的な結果を読み解くのが困難であった
  • 歴史的経緯から Dependabot と Renovate が併用されていたが、一部は重複していた

まずは、一部のマイクロサービスだけ Renovate の設定ファイルを切り出して、Self-hosted Renovate の検証を進めた。Self-hosted Renovate が安定的に稼働するようになってからは、チームごとに設定ファイルの切り出しを進めた。人間が大量の設定を読み書きすると疲れてしまうので、claude-code-action を利用して設定を切り出した。Dependabot の設定はすべて Renovate に移行して、セキュリティに関する機能のみ利用する方針に転換した。

GPU 実行環境の整備

プロダクトでの AI 利用が広がり、ローカル GPU を利用したいユースケースが出てきた。これまで Kubernetes クラスターでは GPU を利用したことがなかったため、クラスターに nvidia-device-plugin を導入したり、GPU アプリケーション向けに NodePool を用意するといった対応を進めた。

claude-code-action を monorepo に導入する

現職では言語やフレームワークの多様性があるため、AI のコーディングエージェントが高い生産性を発揮するためには開発環境が重要となる。AI にまともな開発環境を与えないと、まるでメモ帳でコードを書いている現場のようになってしまう。claude-code-action の導入にあたっては以下のような工夫を施した。

  • Issue や Pull Request のラベルから開発対象のサービスを判断し、必要な開発環境をセットアップする。例えば、Ruby のサービスが選択されている場合は setup-ruby を実行し、TypeScript のサービスが選択されている場合は setup-node を実行するといった感じである。これにより、AI が必要に応じて linter やテストなどを実行できるようになる。
  • テストが通ることなどの受け入れ条件はサービスによって異なるため、サービスごとのディレクトリで AGENTS.md を整備している。
  • Terraform のリポジトリでは、AI が terraform plan などを実行できるように実行環境を整備した。AI がクラウドを操作できるようになるため、実行可能なコマンドや読み取り可能なリソースを絞るといったガードレールが重要になる。
  • Kubernetesマニフェストを修正する場合は推奨ルールや受け入れ条件を明確にする。これはまだ改善の余地があると考えている。

指標による継続的な改善

Coverband による Ruby アプリケーションのカバレッジ可視化

現職では Coverband が導入されており、実行時のカバレッジ情報をもとにデッドコードを安全に削除できるようになっている。以下の課題があった。

  • マイクロサービスごとに Coverband を設定していたため、誤って外部に公開してしまうとセキュリティ事故になるリスクがあった
  • 複数のマイクロサービスで利用している内部 Gem のカバレッジを参照できない

このような課題の解決をモブプロで進めた。Coverband に詳しい同僚がリードを務めて、モブチームで交代でコードを書きながら探索を進めた。

まずは、カバレッジの記録と参照を分離した。具体的には、各マイクロサービスの Coverband でカバレッジを記録し、社内向けの Coverband でカバレッジを参照する構成に見直した。

また、Coverband で内部 Gem のカバレッジも取得できるように改善した。Coverband は Gem のカバレッジを除外する前提で設計されているため、やむを得ずモンキーパッチで内部 Gem も記録できるように対応した。

SLO による OOM killed イベントの継続的なモニタリング

Kubernetes クラスターを構成する重要なコンポーネントがメモリ不足で動かなくなり、本番サービスに影響するインシデントが発生した。再発防止として、重要なコンポーネントについては OOM killed のイベントを Datadog の time-slice SLO でモニタリングするようにした。これにより、例えば uptime が 99.9% を下回ったら memory request を増やすといったアクションを取りやすくなった。

一定のエラーを許容する場合、エラーをそのままアラート通知すると割れ窓になってしまうことが多い。そのような場合は SLO が向いているのではないかと考えている。

その他

プラットフォームの信頼性の改善やトイルの削減

Argo CD Application の削減による安定性改善

現職では、Pull Request ごとにマイクロサービス一式のプレビュー環境がデプロイされる仕組みを整備している。Pull Request で変更されていないマイクロサービスについては Service リソースを経由して別環境にリクエストがプロキシされるようになっている。これまでマイクロサービスごとに Argo CD Application を作成していたが、マイクロサービスの増加とともに Argo CD の負荷が問題となった。そこで、Service リソースを集約させることで Application を大きく削減した。

具体的には、App of Apps パターンの構成を以下のようにしている。

  • Pull Request のプレビュー環境の Application
    • Pull Request で変更されていないマイクロサービスの Service リソース
    • Pull Request で変更されているマイクロサービスの Application
      • マイクロサービスのリソース群 (Deployment など)

厳密には、Pull Request で変更されたマイクロサービスにリクエストを送信するマイクロサービスも Deployment などを配置している。例えば、サービス間通信が A→B→C の場合、Pull Request で C が変更された場合に前段からの通信を受け取るには A と B の Pod が必要になるためである。これについては今後改善の余地があるかもしれない。

その他

  • Argo CD ApplicationSet Pull Request Generator への移行
  • SRE チームへの問い合わせを AI で要約する
  • AWS インフラコストを AI で要約する
  • GitHub Actions などのサプライチェーン攻撃対策
  • Renovate による EKS control-plane や EKS addon の更新
  • External Secrets Operator への移行
  • Ingress Nginx から Envoy Gateway への移行

2024年のお仕事まとめ

2024年の仕事をまとめてみた。年末に風邪を引いてそのままお蔵入りになっていたが、だいぶ遅れて公開した。

指標に基づく開発体験の継続的な改善

大規模な monorepo で GitHub Actions (self-hosted runners) を運用している。プロダクトチームへのサーベイでは CI が遅いというフィードバックが多数あったが、私は継続的にプロダクト開発に関わる機会がないため、開発体験の改善が後手になっていた。

CI が遅い、flaky といったフィードバックは主観的で漠然としているため、そのままでは解決が難しい。そこで、フィードバックで発見した問題を分解し、指標による継続的な改善を目指すことにした。

テストケースやビルドプロセスの問題

コードベースの肥大化や歴史的経緯によって、スローテストや flaky テストで困っている開発組織は多いと思う。膨大なテストケースに手をつけるためのメトリクスを計測することにした。具体的には、Datadog のダッシュボードで以下のメトリクスを確認できるようにしている。

  • テストワークフローの所要時間
  • 遅いテストケース
  • 最近失敗したテストケース

このようなメトリクスをテスト改善で利用してもらうには工夫が必要そうである。例えば、直近1週間でよく失敗しているテストケースを通知したり、所要時間の長いテストケースのランキングを通知したり、といった広報活動を考えていきたい。

GitHub Actions ワークフローの問題

GitHub Actions のワークフローは

  • ワークフローの branches が適切に設定されておらず、不要なブランチで CI が実行されていた
  • ワークフローの paths が適切に設定されておらず、テストが必要ない場合にも実行されていた
  • ジョブの実行条件が適切に設定されておらず、Dependabot などの bot で不要なジョブも実行されていた
  • ジョブが必要以上に細分化されており、ワークフロー全体の待ち時間のうちジョブの起動待ちが支配的になっていることがあった
  • 並列テストのジョブ数が不適切に設定されており、無駄に時間がかかったり、失敗の確率が高くなってしまっていた

Self-hosted runners の問題

大規模な monorepo を運用しているため、同時に多数のジョブが実行されることが多い。その結果、self-hosted runners が頻繁に詰まるという問題が頻発していた。しかし、self-hosted runners が詰まるという事象をモニタリングできておらず、何らかの改善策を打っても効果を計測できていなかった。

また、リポジトリによっては Runner のメモリ割り当てが適切に設定されておらず、OOM によるジョブのタイムアウトエラーが多発していた。しかし、何も計測していないので長らく放置されていた。

そこで、以下のメトリクスを取得して、Datadog の SLO で定期的に確認するようにした。

  • リポジトリで queued になっているジョブ数(ジョブの event, actor, branch などで集計できるようにしておく)
  • actions-runner-controller の startup duration
  • コンテナの OOM 発生数

開発フローにおけるトイルの削減と継続的改善

リリースプロセスの省力化

EKS クラスタの運用改善

Blue-green update から In-place update への移行

これまで EKS クラスタのバージョンを更新する時は新しいクラスタを作り直していた。もし新しいバージョンで問題が起きた場合に素早く切り戻せるように考慮していた。しかし、新旧バージョンのクラスタを並行運用することで以下の課題が発生していた。

  • バージョンアップの間隔が長くなってしまう
  • 新旧クラスタの並行運用期間にマニフェストの変更漏れが起きていた
  • 新旧クラスタでアプリケーションが同時に実行されるため、非同期処理などで不整合が起きることがある
  • ユーザーに kubeconfig を切り替えてもらう負担が大きい

そこで、同じ EKS クラスタでバージョンを更新していくプロセスに変えることにした。バージョンアップ作業が Terraform で完結するようになり、kubeconfig を切り替える必要もなくなったので、運用のトイルが大きく改善された。

アクセス制御

以前はユーザーのアクセス制御を aws-auth ConfigMap で設定していた。誤った ConfigMap を apply すると本番障害が起きるリスクがあるため、変更の心理的抵抗が高いという課題があった。また、新しいクラスタを構築する手順が複雑になっているという課題もあった。EKS access entries への移行で以下のメリットがあった。

  • アクセス制御の設定を Terraform (aws provider) のみで管理できるので認知負荷が下がった
  • ノードが利用する権限などを誤って書き換えてしまうリスクがなくなった
  • 緊急時でも確実に特権を付与できるようになった

いくつかのコンポーネントについては IRSA から EKS Pod Identity への移行を進めている。IRSA では IAM Role と Service Account で相互に参照関係を設定する必要があるが、EKS Pod Identity では IAM Role から Service Account への参照関係を設定するだけでよいため、認知負荷が大きく下がる。現状では EKS Pod Identity がワイルドカードの namespace に対応していないため、pull request のプレビュー環境では利用できないという課題が残っている。

2023年のお仕事まとめ

2023年のお仕事をまとめてみた。

複雑化するデータパイプラインへの対応

クロスアカウントのリストア

現職では、開発、テスト、カスタマーサポート、データ分析などに使うデータベースを日次もしくは不定期で本番環境から同期する仕組みを整備している。今年は検証環境を独立した AWS アカウントに移行する作業を進めていた。データパイプラインもクロスアカウントに対応した。

本番環境のデータベースには個人情報が含まれるものがあるため、本番環境の AWS アカウント内で個人情報のマスク処理を完結させる必要がある。そのため、本番環境の AWS アカウントでマスク済みの EBS スナップショットや RDS スナップショットを作成し、検証環境の AWS アカウントに共有する構成にしている。以下のはまりどころがあった。

  • スナップショットの内容は共有されるが、スナップショットのタグは共有されない
  • AWS Managed Key で暗号化されているスナップショットは共有できない。代わりに CMK (Customer Managed Key) で暗号化し、別のアカウントから CMK へのアクセスを許可する必要がある
  • EC2 API や RDS API でスナップショットを検索する場合は共有されたものを含めるオプションが必要になる

本番環境の AWS アカウントでスナップショットを作成したら、検証環境の AWS アカウントでスナップショットをリストアする。スナップショットの作成やリストアはもともと Step Functions と ECS Fargate Task で実装しているため、AWS アカウントごとに Step Functions と ECS Fargate Task を配置し、EventBridge を経由して Step Functions が連携する構成にした。以下のはまりどころがあった。

  • EventBridge を経由してデータを送ることも可能だが、キーの間違いや再実行時の考慮不足などで Step Functions の実行時エラーが多発するおそれがある。Step Functions のエラー詳細は Sentry や Datadog などで捕捉できず、AWS コンソールから調査する必要がある。運用負荷を下げるため、基本的にタスクを冪等にして、入出力の受け渡しのないシンプルな設計にしている。
  • クロスアカウントでイベントをやり取りする場合は Event Rule や IAM の設定が難しい。あらかじめ練習用の AWS アカウントで試行錯誤してから Terraform を書いた

2023年の春ごろにクロスアカウントのパイプラインが完成した。当初はマイクロサービスのデータベースごとに大量の Step Functions 定義をコピペしてしまったので、後から Terraform module でリファクタリングした。マイクロサービスのオーナーチームは Terraform module の設定値を埋めるだけでよくなったので、認知負荷もだいぶ下がったと思う。

同時に実行できない制約の解消

現職では基本的に Aurora を利用しているが、歴史的経緯で EC2 self-hosted MongoDB も利用している。MongoDB では EBS スナップショットを経由してデータベースをリストアしている。開発やカスタマーサポートなどの環境によってマスク処理が異なるため、複数のスナップショットが必要になる。

これまで1台の EC2 で EBS スナップショットを作成していたため、同時に複数のスナップショットを作成できない制約があった。Step Functions を直列実行することで対応していたが、問題が起きた場合の手動運用が複雑になる課題があった。また、バージョンアップなどで異なるバージョンの MongoDB サーバが混在する状況に対応できない課題もあった。

そのため、一時的な使い捨ての EC2 を起動してスナップショットを作成する実装に改善した。

  • スナップショット作成処理はもともと ECS Fargate Task で実行しているが、Fargate では EBS スナップショットを扱えない。そこで、ECS Fargate Task から一時的な EC2 を起動し、Systems Manager を利用してコマンドを実行する構成にしている
  • 独自の AMI を用意するとメンテナンスの負荷が発生する。そこで、AWS が提供している AMI に Docker をインストールし、mongo コンテナを実行することで、メンテナンスフリーにしている
  • Systems Manager や Docker で複雑なシェルスクリプトを流すと実行時エラーが起きやすい。なるべく ECS Fargate Task 側の Go で複雑な処理を表現するようにしている

これにより、スナップショット作成の Step Functions を同時に実行できるようになり、実行を直列化したり、実行時刻をずらすといった変なハックが不要になった。しかしながら、CloudWatch Logs のログが ECS Fargate Task、 Systems Manager、EC2 上のコンテナで分散してしまうため、問題が起きた場合の調査がつらい課題を抱えている。Datadog APM で一連のトレースを可視化して調査しやすいように工夫しているが、今後の改善としたい。

オーナーシップの明確化による運用負荷の改善

現職ではデータ基盤に Airflow (Cloud Composer) が採用されており、Data Engineer がオーナーシップを持っている。データパイプラインで問題が起きるとデータ分析に影響が出てしまうため、Data Engineer だけで速やかに復旧できることが望ましい。しかし、AWS 上のデータベースについては私が所属する SRE がオーナーシップを持っているので、問題が起きた場合のやり取りが複雑になる傾向があった。

そこで、以下の構成に移行することで運用負荷の改善を図った。

  • データ基盤用のデータベースをリストア/エクスポートする Step Functions を用意し、Airflow から Step Functions を呼び出す構成にする
  • Airflow の構成は Data Engineer がオーナーシップを持つ。問題が起きた場合の判断や再実行は Data Engineer で完結できるようにしている
  • データベースをリストアする実装は SRE がオーナーシップを持つ。アラートの調査や継続的なバージョンアップなどの運用を担う

このようにオーナーシップを明確にすることで、問題が起きた場合に Slack 上で速やかに復旧対応を進めやすくなっている。まだ移行できていないデータベースもあるので、引き続き改善を進めたい。

CI/CD パイプラインの改善

actions-runner-controller の移行

2023年5月ごろから新しい actions-runner-controller が Public Beta になった。新しい actions-runner-controller に移行することで、以下のメリットがあった。

  • オートスケーリングが Pull 方式に変わるため、GitHub Actions のジョブが急激に増えた場合でも安定してスケールアウトできるようになった。現職では大規模な monorepo を運用しているので、開発体験が大きく改善された
  • オートスケーリングのために GitHub から Webhook を受け取る必要がなくなった

しかしながら、公式で用意されている actions/runner イメージには以下の課題があった。

  • インストールされているパッケージが最小限になっている。git や build-essentials すら入っていない
  • Runner と Docker daemon が別のコンテナに分かれている。例えば、テストジョブでは Runner コンテナのリソースが大量に消費されるが、ビルドジョブで Docker daemon コンテナのリソースが大量に消費される、といった不均衡が生じてしまう。Pod のリソース割り当てを最適化するには両者を同じコンテナで実行することが望ましい
  • 当初は arm64 のイメージが提供されていなかった(後に amd64, arm64 の両対応になった)
  • 当初はベースイメージが Debian であった。GitHub-hosted runners は Ubuntu なので環境差異が発生する(後にベースイメージが Ubuntu になった)

現在は独自の runner イメージを用意している。詳細は https://github.com/quipper/actions-runner を参照されたい。

Reusable workflows によるリファクタリング

今年になって Reusable workflows の制限が大きく緩和されたため、抽象化されたワークフローを再利用する構成を採用しやすくなった。現職では、以下のようなユースケースリファクタリングを進めている。

  • コンテナイメージのビルド
  • コンテナイメージの multi-architectures ビルド
  • 言語ごとの共通的な検査(golangci-lint, eslint, standardrb など)
  • マイクロサービスのデプロイ
  • Kubernetes クラスタでのジョブの実行

Reusable workflows には認知負荷やメンテナンスを減らせるだけでなくガバナンスを効かせる効果もある。GitHub Actions から AWS にアクセスしている場合、特定の Reusable workflow を経由している場合だけ IAM で許可することも可能だ。デプロイパイプラインのセキュリティ改善を今後進めていきたい。

Buildx への移行

現職ではコンテナイメージのビルドに Kaniko を採用していた。Buildx は特殊なキャッシュマニフェストを採用しているため ECR にキャッシュを保存できないが、Kaniko は通常のイメージなので ECR にキャッシュを保存できる。multi-stage build で効率的にキャッシュを活用できるため、ビルド時間の短縮に大きく貢献していた。

github.com

今年の夏ごろに ECR が Buildx のキャッシュに対応したため、Kaniko から Buildx への移行を検討することにした。Buildx への移行で、キャッシュが効いている場合の所要時間が大きく短縮された。しかし、ECR リポジトリの構成を以下のように変えた影響の可能性もあるので、要因はよく分かっていない。

  • 移行前:イメージと Kaniko キャッシュを別の ECR リポジトリで管理する
  • 移行後:イメージと Buildx キャッシュを同じ ECR リポジトリで管理する

開発体験の改善

Google Cloud Pub/Sub の Kubernetes operator

現職では、一部のマイクロサービスで非同期のイベント連携に Google Cloud Pub/Sub を採用している。Pub/Sub の Topic や Subscription は基本的に Terraform で管理している。Pull Request ごとにデプロイされるプレビュー環境では同じ Topic や Subscription を共有するため、イベント連携がうまく動かない課題がある。ある Pull Request 環境でイベントを流しても他の Pull Request 環境に吸い取られてしまうためだ。

そこで、Pull Request 環境ごとに独立した Topic や Subscription を用意できるように Kubernetes operator を提供している。私は Kubebuilder の開発経験があったが、プロダクトチームで仕事をしていないので開発体験の課題には詳しくない。そこで、プロダクトチームの経験が豊富でかつ Go のエキスパートである同僚と協力して、Kubernetes operator の開発に取り組むことにした。実際にプロダクト開発で Operator を利用してみると以下の課題が見つかった。

  • マイクロサービスのデプロイと Topic や Subscription の作成は並行で実行される。マイクロサービスの起動時に Topic が存在しない場合はエラーが出てしまう。Crash loop backoff で最終的には起動するが、エラーの通知が荒れてしまう
  • Topic と Subscription は独立した Kubernetes リソースで定義しているので、それぞれ並行で作成される。Pub/Sub の仕様では Subscription の作成時に Topic がないとエラーになる。Operator の reconciliation loop で最終的には作成に成功するが、Kubernetes events が荒れてしまう。一定時間が経過してもエラーになった場合だけ Kubernetes events に通知することで、アラートハンドリングを改善した

また、Kubernetes や Kubebuilder のバージョンアップで今後も定常的にメンテナンスは必要になる。引き続き mob programming を続けていきたい。

指標やフィードバックに基づく改善

デプロイパイプラインの運用では以下のメトリクスを継続的に見ている。

  • GitHub Actions のジョブキュー数
  • actions-runner-controller のジョブ起動待ち時間(ただし、v0.8.1 から取れなくなっている)
  • Self-hosted runners ノードの OOM 発生率
  • Self-hosted runners ノードの EC2 月間予測コスト
  • ワークフローやジョブごとの所要時間リスト
  • ワークフローの実行契機(特に Renovate, Dependabot など)

また、本番環境へのデプロイ時に Google Forms でフィードバックを集める取り組みも実験的に進めている。来年はフィードバックを広く集めて、デプロイパイプラインの開発に反映するプロセスを考えていきたい。

昨年の記事はこちら。

int128.hatenablog.com