SREとして入社し1年たつので振り返り

こんにちは。SREの栗原です。 この記事は10Xアドベントカレンダーの15日目の記事です。

私がSREとして2022年の10月に入社し1年が経ちました。 この1年間でやってきたことについて書いていきます。 現在SREチームは採用募集中です。この記事を見てスキルがマッチしていたり興味が湧いた方は是非カジュアル面談をしましょう!

SRE(Site Reliability Engineer) / 株式会社10X

  • 入社初期の取り組み
  • 自動化と効率化への取り組み
    • Terraform moduleへのresource追加
    • Redashやめる
    • Kubernetes yamlのコピペ運用をやめる
    • サービスアカウントキーの発行方法の見直し
  • 育休の取得
  • その後の取り組み
    • Deny policiesの導入
    • Terraform Planの権限修正
  • これから
続きを読む

GitHubの監査ログを定期的にexportして保存する

こんにちは。セキュリティチームでソフトウェアエンジニアをしてる@sota1235です。

この記事は10Xアドベントカレンダーの13日目の記事です。

www.notion.so

昨日の記事はSuzuki Ryotaさんのお届けチームでオーナーシップを持っていくぞでした!

今回はGitHubの監査ログを定期的にexportし、保存する仕組みを作った話をします。

  • 監査ログとは
  • GitHubの監査ログ
    • GitHubの監査ログは永久には保存されない
    • 監査ログの出力方法
    • この記事の本題
  • 監査ログ出力の仕組み
    • ざっくり要件
    • 技術選定
      • ログの保存場所
      • ログの取得処理
      • ログの取得・保存処理はGitHub Actionsで行う
    • 全体像
      • 1. BigQueryに最新データを取得しに行く
      • 2. 監査ログを取得する
      • 3. 監査ログを保存する
    • 権限管理
      • 監査ログに含まれるデータについて考える
      • 具体的にどこに制限をつけるか
    • 今後の活用方法
  • 最後に
  • 明日は
続きを読む

Relay Proxyを活用してLaunchDarklyを導入する

Relay Proxyを活用してLaunchDarklyを導入する

はじめに

この記事は 10X アドベントカレンダー2023 11日目の記事です。

前日の記事は id:hisaichi5518 さんによる「“10xを創る”開発チーム文化とはなにか 〜お届けチーム編〜」でした。


こんにちは、今年の6月に10Xに入社して今はお届けチームでソフトウェアエンジニアをしているgenkey6です。

この記事では、お届けチームで直近取り組んでいるfeature flag管理サービスのLaunchDarkly導入に関する話をお届けします。

10XではServer Sideの開発言語としてDartを採用していますが、feature flag管理サービスを導入するにあたってServer Side DartのSDKやライブラリを公式で提供しているサービスがなく苦戦しました。

最終的に採用したLaunchDarklyでは、SDKこそ存在しないもののRelay Proxyの活用という選択肢をとることで公式がサポートする方法でServer Sideからサービスを利用することができました。

LaunchDarklyがSDKでサポートしている開発言語は非常に充実しているためこうした対応が必要になるケースは稀だと思いますが、まだ見ぬ未来のLaunchDarklyユーザーに向けて、導入にあたって検討した内容やハマったポイント等をまとめておきます。

ニッチな内容ですが最後までぜひお付き合いください!

導入の背景

大変ありがたいことに、10Xが提供しているStailerのプラットフォームを活用してネットスーパー/ネットドラッグストア事業を運営されるパートナーの数は日々増えており、併せてお届けチームで開発している小売事業者向けのスタッフアプリや管理画面を利用するユーザーも増加しています。

これらのアプリケーションの特徴として、ユーザーである現場スタッフの方々の業務時間中はほぼ常時アプリケーションを使用している状態になるため、高い信頼性が求められるという点があります。

また、開発している機能に占めるCUJ (Critical User Journey) の割合が大きいため、特定の機能に障害が発生すると業務のブロッカーとなる上、最悪の場合はエンドユーザーであるネットスーパー/ネットドラッグストアのお客様のサービス利用体験にも影響が出てしまいます。ゆえに、毎回のリリースに対して慎重な姿勢を取らざるを得ないという状態にありました。

一方で、現場のオペレーション効率化にアプリケーションが寄与できる余地はまだまだ残されているため、なるべく細かくリリースを行なってインクリメンタルに改善を行なっていきたいというニーズも存在します。

こうした背景から、信頼性の担保と開発速度の向上を両立する手段として、feature flagを活用してリリースの影響範囲を抑えつつ障害発生時の復旧時間を短縮することを目指した検討が始まりました。

LaunchDarklyについて

前提として、feature flagの仕組み自体は単純なものなので、これまでの開発でも各所で利用していました。

具体的にはアプリケーションの実装内にflagをハードコーディングする方法や、環境変数 (もしくはそれに準ずる設定ファイル) を用いて機能のon/offを切り替える方法が現在進行形で利用されています。

こうした素朴な実装でもやりたいことは部分的に実現できますが、

  • 影響範囲を抑えるために一部のユーザーに限定したリリースを行うための仕組みが欲しい (単純なユーザーidによる指定に加えて、ユーザーが所属するパートナーや店舗といった単位も想定)
  • 障害発生時の復旧時間を可能な限り圧縮するために、flagの値を更新する際はデプロイを不要としたい
  • これらの仕組みを自前で実装・保守するコストはできるだけ払いたくない

といった理由により、はじめからSaaSを導入できないか検討しました。

ここでは世の中に数多く存在するfeature flag管理サービスの詳細な比較 *1 は行いませんが、LaunchDarklyを採用した理由については簡単に触れておきます。

調査を進めていくと、冒頭で述べたようにServer Side Dartで利用できるSDKを公式にサポートしているサービスが存在しないという壁にぶつかりました。

サービスによってはSDK自体は必ずしも必要ではなく、用意されているエンドポイントにリクエストを送るだけで済むようなものも存在しましたが、flagの値をなるべくリアルタイムに更新したいことを考えると何かしらstreamingな形でサービスとやり取りを行う仕組みは欲しいところです。

そんな中で、LaunchDarklyではRelay Proxyというコンポーネントが間に立ってサービス本体とのstreaming通信を肩代わりしつつスケーラブルなアーキテクチャを用意してくれる、かつこのRelay Proxyは公式がサポートするOSSとして提供されていることから、最有力な選択肢として残りました。

加えて、同時に導入を進める予定のモバイルアプリで利用できるFlutter向けのClient SDKが存在する点や、既に開発の中で利用していた各種ツールとのインテグレーションが充実している点などから活用イメージがつきやすかったことが決め手となり、採用に踏み切りました。

Relay Proxyについて

Relay Proxyとは

LaunchDarklyは公式ドキュメントが非常に充実していますが、Relay Proxyについては例えば以下のページにまとまっています。

docs.launchdarkly.com

また、以下ではSDKが存在しない言語でLaunchDarklyを利用する際に選択できるオプションの1つとしてRelay Proxyが紹介されています。

docs.launchdarkly.com

これらのページに書かれている内容を総合すると、

  • Relay ProxyはGoで実装されたマイクロサービスで、LaunchDarklyのstreaming APIと通信を行ってflagの値を保持する
  • ユーザーはLaunchDarklyと直接通信する代わりにRelay Proxyにリクエストを送るようにすることで、LaunchDarkly側に大量のリクエストが飛んでしまうのを防げる
  • SDKのない言語でLaunchDarklyを利用する方法は他にも存在し、例えば既存のSDKのラッパーを実装する方法や自前でSDKを実装する方法 *2 が紹介されている

といったことが分かります。

Relay Proxyは元々は負荷分散を想定した仕組みですが、言語非依存かつ独立してスケール可能なコンポーネントを実装コストを抑えつつ用意できるため、今回のユースケースにぴったりと当てはまるというわけです。

Relay Proxyの設定を決める

さて、Relay Proxyが目的に適していることが分かったので、続いてはRelay Proxyの設定について考えていきます。

この項の記述は主に公式ドキュメントの以下のページで触れられている内容に基づいているので、実際に導入を検討される場合は一度目を通すことをおすすめします。 (なお、Relay Proxyのバージョンは執筆時点で社内で利用されているv8.2.0を前提とします)

docs.launchdarkly.com docs.launchdarkly.com

最初に決めるべきはRelay Proxyのmodeです。Relay Proxyには proxy modeとdaemon modeという2つのmodeが存在します。両者の大きな違いは、LaunchDarklyと通信して手に入れたflagの値をどこに保存するかです。

proxy modeではflagの値をインメモリなキャッシュとして保持するのに対し、daemon modeではpersistent storeと呼ばれるデータベースに保存します。 (persistent storeの実装にはRedisやDynamoDBが用いられます)

ドキュメントの記述によると、We generally recommend configuring your SDKs for proxy mode. Daemon mode is a workaround for environments where normal operation is not possible. とのことなので、今回はproxy modeを採用することにしました。*3

他に試せていない内容として、proxy modeとpersistent storeを組み合わせて使うケースも存在します。具体的には、LaunchDarklyのBig Segmentsという機能を利用する場合はpersistent storeを利用することが推奨されていますが、今回のユースケースには当てはまらなかったため検討対象から外しました。

もう1つのポイントは、Relay Proxyのスケール数です。(StailerのインフラではKubernetesを採用しているため、ここでのスケール数とはRelay Proxyをデプロイする際のPodのreplica数を指します)

公式ドキュメントのScaling guidelinesには、We recommend you provision the Relay Proxy the same way you would an HTTPS proxy, and plan for at least twice the number of concurrent connections you expect to see. と記載されていたため、Relay ProxyにアクセスするServerコンポーネントに対するHTTP Proxyで設定しているreplica数に揃える形で設定することにしました。

また、オートスケールの設定については最初はリクエスト数が少ない箇所から運用を開始することを想定して、一旦は見送っています。

今回の取り組みと並行して、DatadogのメトリクスによるHPA (Horizontal Pod Autoscaler) の設定を可能にする仕組みをSREチームに用意してもらったので、今後のリクエスト数の実績値を元に導入を検討していく予定です。

Relay Proxyをデプロイする

設定の方針が決まったので、続いてはRelay Proxyを実際にデプロイするステップです。

以下のページで複数のデプロイ方式が紹介されていますが、社内で既にHelmを活用していたためそちらを使うことにしました。

docs.launchdarkly.com

公式のHelm chartが以下のrepositoryで管理されているため、repository内のドキュメントを参考にしつつvalues.yamlの中身を埋めていきます。

github.com

各種configurationの項目の意味合いについては以下にまとまっているため適宜参照しつつ、

以下のファイルで記述されているデフォルト値を更新する形でvalues.yamlを記述すればOKです。

10Xではこうして用意した設定に基づいて、Helmfileなどの仕組みを活用しつつKubernetesのマニフェストを生成してデプロイしています。

Relay Proxyを監視する

Relay Proxyがダウンしてしまうとアプリケーション側でLaunchDarklyのfeature flagの値に依存している箇所は全てデフォルト値にfallbackされてしまうため、運用開始後の監視は厚めに設定しています。

監視内容の設計にあたっては公式ドキュメントの以下のページが参考になりました。

docs.launchdarkly.com

上記を参考にしつつ、10Xでは次に挙げるような項目を監視しています。

  • Podの死活監視
    • (デプロイ時の設定で無効化していなければ) PodのlivenessProbe/readinessProbeがRelay Proxyの /status エンドポイントにリクエストを送るように設定されているため、これをhealth checkとして利用しています
  • LaunchDarklyとの接続が正常かどうかの監視
    • /status エンドポイントのresponseにはLaunchDarklyとのstreaming通信が正常に行われているかどうかを示す情報が含まれます (JSONPathで書くと $.environments.NameOfEnvironment.status の部分)
    • これをDatadogのSynthetics Testを用いて定期的に確認することで、通信が途絶えた場合にアラートを出すようにしています
  • Relay Proxyのパフォーマンス
  • Relay Proxyにリクエストを行うClientがflagの評価に失敗した際に出力するログ
    • 必ずしも全てのケースで緊急の対応が必要なわけではありませんが、flag評価に失敗してfallback値が使われたことは検知したいため、Cloud Loggingのlog-based alertsを用いた通知を行っています

Relay Proxyにリクエストする

ここまででRelay Proxyを利用する準備が整ったため、いよいよServer SideからRelay ProxyにリクエストできるようにDartでClientを実装していきます。

大まかな方針としてはflag評価用のエンドポイントに対して、Contexts *4 の情報を付与してリクエストするだけのシンプルなものになります。

以下で、実装時にハマった点をいくつか取り上げておきます。

  • undocumentedな仕様が多い
    • requestで渡すべきContextの型、特に複数のContextを組み合わせるMulti-contextsの場合の情報が記載されておらず、最終的にRelay Proxy本体とテストの実装を読んで理解しました
    • また、responseの型についても明示されておらず、手元でAPIを呼び出してresponseの内容を直接確認する必要がありました
  • 同じくDartで実装されているFlutter向けのSDKと型を共有できそうで出来ない
    • Contextの型 (上記で確認したもの) が実は違います
    • packageをそのまま追加するとFlutterまで依存に入ってきてしまうため、実際に参照する型定義だけをコピペする必要がありました
    • 最初はClient実装に関わる型定義部分はほとんど流用できるか?と考えてチャレンジしてみましたが、結果的に正しく使い回せたのは LDValue というflagの値に関する型定義のみでした

これより詳細な実装の中身に関してはここでは立ち入りませんが、以下のrepositoryにて社内で利用しているものとほぼ同じ実装を行っているので、気になった方はご参照ください。

github.com

今後に向けて

以上、Relay Proxyを活用してSDKのない言語でLaunchDarklyを導入するまでの道のりについて書いてきました。

導入してからまだ日は浅いですが、チーム内では既に以下のようなユースケースでLaunchDarklyを活用し始めています。

  • 細かな調整が求められるUX改善タスクにおけるExperimental Flag
  • 比較的長期にわたって行われるリアーキテクチャプロジェクトにおけるMigration Flag

また、将来的には次のようなユースケースにも活用の幅を広げていきたいと考えています。

  • 特定の機能を限られたユーザーに提供する際のPermission Flag
  • 外部サービスに依存している箇所のKill Switch
  • Firebase Remote Configを利用してA/Bテストを実施しているケースの代替

加えて、今回の記事では触れられませんでしたが、LaunchDarklyの運用面の整備として以下のような取り組みも並行して進めています。

  • flagの負債化を防ぐ仕組み
  • ソフトウェアエンジニア以外がflagの値を安全に変更できるような仕組み
  • 複数チームで利用する際のガバナンスの仕組み (flagの命名規則やflagに付与するTagsのルールなど)

活用が進んでいった暁にはこれらのテーマでもまた記事を書いてみたいです。

終わりに

この記事では信頼性の担保と開発速度の向上を両立する手段としてのfeature flagについて紹介しましたが、直近では同様のゴールに対して次のような施策も検証されています。

  • リリースの安全性を高めるProgressive Deliveryの仕組み
  • モバイルアプリにおけるhotfixリリースを容易にするためのOTAの仕組み

10Xでは、こうした取り組みを一緒に進めてくれるソフトウェアエンジニア、SREの仲間を募集しています!

open.talentio.com open.talentio.com


最後に、筆者が所属しているお届けチームの取り組みについて、他のメンバーが書いた以下の記事も併せてご覧ください。

続く明日の記事もお届けチーム所属のsuzukiさんによる「お届けチームでオーナーシップを持っていくぞ」です。お楽しみに!

*1:先日Findyさんが主催で開催されたイベントの次の登壇資料に網羅的にまとまっているため、サービス選定に悩まれている方は参考にされると良いと思います: https://speakerdeck.com/gunta/uxno-ding-dian-devcycle-ni-chan-rizhao-kumadenodao-nori

*2:ただし、いずれも公式としてはサポート外ですよという注意書きがされています

*3:じゃあいつdaemon modeを利用すべきなの?かというと、同じドキュメントで Daemon mode requires that you configure server-side LaunchDarkly SDKs to communicate directly with they Relay Proxy's persistent data store. We recommend this configuration when you're using LaunchDarkly with PHP or in a serverless environment. と書かれています

*4:「ユーザー」を拡張した概念で、リクエストを行った主体が、事前に設定した条件に合致するかどうかを判断するためのkey-value形式の属性

“10xを創る”開発チームの文化とはなにか 〜お届けチーム編〜

この記事は 10X アドベントカレンダー2023 の10日目(12/10)の記事です。9日目(12/9)の昨日は、 id:takanamito さんによる「grpc-dartのInterceptorを使う」でした。

10Xのお届けチーム エンジニアリングマネージャー(以下, EM)の id:hisaichi5518です。10Xではソフトウェアエンジニアとしてサーバサイド、Android、iOS、Flutterと色々やってきて、今は「人を動かして、ことを成す」を目指してEMとして活動しています。

今回は、自分が担当しているお届けチームについて「どんな雰囲気なのか」「どういうチームを目指しているのか」「具体的な活動」について書いていこうかなと思います。書き終わって気付いたんですが、今年のテーマは"開発・プロダクト"らしく、ちょっとテーマに沿ってない感があるけどまあいいかってなりました。人生ってそんなもんですよね

お届けチームって?

10Xは、”10xを創る”というミッションを持っていますが、お届けチームは、Stailer*1において"パートナーが、かんたんに届けられる仕組みを提供する"をミッションに持つチームです。このミッションを達成することが10xを創ることとも言えると思います。

イメージしやすいようにどういう機能の開発をしているのか具体の話をすると、店舗スタッフが商品を取ったことを記録するといった機能や配達スタッフが配達時にどの順番で届けるかといった機能などを提供するアプリの開発をしているチームです。

また、お届けチームは執筆時点で、EM1名、SWE4名、QA3名、Designer1名、PdM1名の計10名で構成されています。

お届けチームの雰囲気「楽しく働く」

カジュアル面談をしていると10Xはあんまり雑談がない雰囲気と思われていたりしますが、お届けチームのSlackチャンネルでは、こんな改善した!やこうしたほうがいいかも?や気になった技術ブログの話みたいな仕事に関連する話はもちろん、アニメの話や今日のお昼ごはんの話といった仕事に関係ない雑談もよくしています。他にもSlackのHuddleで作業している人同士で繋げたり雑談をすることもあります。

CIが爆速に!!

スクラムイベントの改善の提案

やいていき!

これは、チームメンバーの根幹に"楽しく働こう!"があると思っていて、それがチームの雰囲気にも出ていると自分は思っています。

お届けチームはどういうチームを目指すのか

もちろん楽しく働くのは大事ですが、それだけではお届けチームの"パートナーが、かんたんに届けられる仕組みを提供する"というミッションは達成出来ません。23年4月からお届けチームは組成*2されたのですが、その時からお届けチームはどういうチームを目指すのかを定義しています。

10Xでは、10X Valuesという価値観を定義していて、それを元に作成されています。

1. 中長期的な観点を持った活動が継続的に行われている

なぜ目指すのか

ある機能を期日に間に合わせるように作るというのは短期的には非常に重要ですが、期日に合わせようと無理をした実装になりバグの温床になったり、作った本人しかわからない機能になったり、後から色々とツラミが出てきます。そうならないためには、中長期的な観点を持って実装面や機能面はどうあるべきかを考え、それを言語化し、一歩ずつ実現する必要があると考えています。

2. チームが自分たちで判断し実行している

なぜ目指すのか

お届けチームは、経営や他事業部から言われたことをただやるチームではなく自分たちで判断して実行するチームでありたいと思っています。そのほうがメンバーがやることに対して自分ごと化出来て成果が出せると考えています。

3. チームで成果を出し、チームで評価される

なぜ目指すのか

“10xを創る”という大きな成果を出すには、1人1人が頑張って成果を出してその総和で勝負するのではなく、チームで成果を出すことで個々の総和をチームで超える必要があります。そのためには、メンバー同士が助けを求め、助け合うことで大きな成果が出せると考えています。

具体的なお届けチームの活動の例

お届けチームがどういうチームを目指すのかはわかったと思うので、それを踏まえて具体的にどういう活動をしているのかの例をこのセクションでは記載します。これは、EMの自分だけがやっているのではなく、メンバーが行っていることです。

理想の姿とギャップをドキュメントにして、チームで取り組む

中長期的な観点を持った活動は、まず理想の姿を定義して現状とのギャップを埋めるために着実に進めることが重要です。そして、一歩ずつ進めるためには1人で実施するのではなく周りを巻き込み、チームとして取り組む必要があります。そのためにはドキュメントを作成して、他の人も理解できるようにする必要があります。

最近作成されたドキュメントは以下のようなものがあります。(一部伏せ字にしています)

  • feature flagの削除を忘れないようにするための運用
  • お届けチームがオーナーシップを持つコンポーネントの監視・オブザーバビリティのあるべきを考える
  • 滞留しているプルリクエストを消化するためにやること
  • 満足のいく引き継ぎってなに?
  • ******* Syncerの現在地と目指しているところと未来
  • 総量ピックへのpick-packモジュール導入

現場リサーチ

お届けチームでは、ネットスーパーを運営する実店舗にお伺いしオペレーションを観察することを現場リサーチと呼んでいます。この現場リサーチを行うことで、”パートナーが、かんたんに届けられる仕組みを提供する"を実現するための課題やあるべき姿を考え、チームで何をやるべきかを判断し実行できるようにしています。

現場リサーチについては、お届けチームのPdMであるkoichi-mさんが12/9に公開した より良いオペレーションのためのプロダクトとは?お届けチームが探索する「現場リサーチ」〜プロローグ〜|こういち / koichi-m にも記載されているので、読んでみてください。

目標共有会

10Xでは個人の目標設定を半期ごとに行っています。目標自体は一覧として共有されるのですが、それだけだと目標設定時に何を考えたのか、どういう思いがあるのかなどがわかりません。

チームで成果を出すには、各個人がどういうことを考え目標を設定したのかや助けてほしいことなどをチームで共有し、職種関係なく助け合える状態になることが必要があると考え、お届けチームでは目標共有会を行っています。

目標共有会では、以下のようなことをチームで話しました。

  • この半期で達成したい目標
  • 実は持っている裏テーマ
  • メンバーに知っておいてほしいこと
  • 職種限らず、達成するために助けてほしいこと
  • メンバーからの質問/感想タイム

まとめ

お届けチームの雰囲気や目指したい形、具体的に何をやっているのかを紹介しました。

ここでは書ききれなかった「ミッションの”かんたん”って何?どういう挑戦がある?」「お届けチームは具体どういう開発をしているの?」「これらを実現するためにEMはどう振る舞っているの?」などもっと詳しく知りたい人は、XのDM/リプライまたはカジュアル面談フォームからどうぞ。

明日は、お届けチームメンバーの id:genkey66 さんによる「Relay Proxyを活用してLaunchDarklyを導入する」です。楽しみですね!

関連

*1:ネットスーパー・ネットドラッグストアの立ち上げと成長を支援するサービスです

*2:ちなみに組成された経緯は、ドメインベースの開発体制への移行 - 10X Product Blogで読めます。

Kubernetes のインスタンスコストを 0.6x した話

10X の Kubernetes おじさん兼娘ちゃん好き好きおじさんこと SRE の @tapih です。

この記事は 10X プロダクトアドベントカレンダー2023 の 8 日目の記事です。昨日は PdM の @enaminnn さんの記事でした。

note.com

本記事では、 2023 年 1 月頃に行っていたインフラコスト削減の施策についての話をご紹介します。

施策を行った背景

弊社が提供している Stailer では、サービスローンチ当初から Kubernetes を採用しています。

自分が入社した 2021 年 5 月時点では数パートナー企業にのみサービス展開をしておりました。ありがたいことにそこから多くの企業様にご導入いただき、短期間で十数パートナーまでサービスを展開することができました (導入の状況は Culture Deck をチェック!)。

一方で、ありがたくないことにインフラコストは増える一方です。サービスが拡大しているのでコストが増えるのは当然ではあるのですが、絶対値で見たときに「ここまでかからないだろう…」という水準に差し掛かっている感覚がチーム内に漂っていました。

では、問題を解消しようとすると、アプリからインフラまで様々なレイヤに手を入れる必要があり、結構大変な印象でした。しかし、一連の施策によってコストカット以外の様々なメリットが得られる期待もあります。このような施策を “10x 案件” と弊社内で呼んだりするのですが、育休復帰後の私は「10x するぞ!(コストは下げるけどね)」と意を決して施策をスタートすることとしました。

Goals / Non Goals

Goal はまず当たり前ですが、 Kubernetes のインスタンスコストを削減することです。その How として Node pool の作り直しが Must であったため、その部分の設計を行うことも当初のゴールと設定しました。また、低リスクで拾える他の改善ポイントがあればついでに拾うこととしました。

一方で、 Cloud Run / GKE Autopilot への移行といった Kubernetes クラスタに閉じない施策は工数や影響範囲が大きく、問題が顕在化した状態で不確実性の高い施策をするのは避けたい、という観点から一旦 Non Goal としました。また、 Kubernetes 以外のコストで大きいものの削減に関しては、他チームに移譲していたり、弊社でコントロールしきれない部分があったため、これも Non Goal としています。

Node pool の命名規則

Node pool を作り直そうとしたところ、 Pod の Node Selector で cloud.google.com/gke-nodepool ラベルを参照していました。このラベルは GKE 側で自動で設定されるものであり、仮に新しい Node pool を作ると別の値が自動で設定されます。そして、この新しい Node pool に Pod をスケジュールさせるには Node Selector の設定を変える必要があります。Node Selector の変更はエディタの一括置換で行えないこともないですが、 Kubernetes マニフェストは現状様々なレポジトリに散在しており、作業は割りと面倒です。 Node pool を作り直すオペレーション自体は今後も発生しうるため、 SRE / SWE の責務を分離できるような状態にしておくのがよさそうでした。

クラスタ上で動いているすべてのワークロードを整理した結果、以下の 2 つの Label / Taint を設定することとしました。

  • 10x.co.jp/owned-by:
    • system (For GCP)
    • platform (For SRE)
    • services (For SWE)
  • 10x.co.jp/instance-type:
    • highmem
    • highcpu
    • standard

また、このタイミングで Spot VM の稼働を開始し、非同期ワーカーの一部の Pod を Spot VM 上で動かすこととしました。以上から Node pool の命名は <10x.co.jp/owned-by>-<10x.co.jp/instance-type>-<cloud.google.com/gke-spot>-YYYYMMDD のようにしました (上限は 39 文字)。なお、 Node Auto-Provisioning のような自動化の機能もありますが、自動化により別の問題が発生する不確実性を排除したかったことから採用を見送っています。 Node pool を分割しすぎると待機リソースが増えてしまうため、このルールに従いつつも必要最小限の Node pool のみを作成しました。

Pod の移行

その後、マニフェストを変更して移行を進めます。この作業は、理想的には各チームに移行を依頼したかったのですが、自分でゴリッと設定していくことにしました。

現在は、ドメインに対する理解をより深めるためにドメインベースの開発体制に移行が進んでおり、各ワークロードに対する Ownership がある程度定まっています。しかし、施策を行った当時は、パートナー企業グループ単位でのチーム分割が行われており、各チーム内で理解が十分でないワークロードがありました。その状況の中では、①それでも各チームに頼む、②チーム移行を待つ、③自分でやってしまう、といったアクションが考えられ、 SRE チームとしては別の施策のブロッカーを解消したかったため、各チームの OnCall-er に軽く共有だけしつつ、③の自分でやってしまうを選択しました (そして後思いの外大変でプチ後悔します)。

Node pool に付与したラベルを指定して移行を行います。

...
spec:
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: 10x.co.jp/owned-by
              operator: In
              values:
              - services
            - key: 10x.co.jp/instance-type
              operator: In
              values:
              - highcpu
    tolerations:
      - key: 10x.co.jp/owned-by
        operator: Equal
        value: services
        effect: NoSchedule
...

AOT コンパイル

以上はよりインフラに近い施策でしたが、よりアプリケーションに近い施策として AOT コンパイル化を行いました。

弊社ではサーバサイドでも Dart を採用しています。 Dart (Native) は実行モードとして JIT / AOT コンパイルモードの 2 つをサポートしており、ざっくり前者は開発用途、後者は本番用途での利用が推奨されております。

しかし、実態は JIT モードで動いている Pod が多く存在しました。 gRPC サーバのような比較的長い時間動き続ける Pod に関しては JIT コンパイラの最適化が働きやすく CPU 効率はむしろ AOT モードよりも良かったのですが、その他に以下のような問題が発生していました。

  • メモリ使用量がそもそも多い
  • メモリ使用量が不安定
  • 最適化の効きづらい CronJob の実行時間が増大
  • Pod の起動時間が遅い

AOT モードへの移行は簡単で、 dart compile exe を実行するだけです。このタイミングで Multi-stage build 対応していない Dockerfile の変更も行いました。

移行しても動くだろうと思いつつ、若干自信がなかったので、検証をはさみながら移行を進めたところ、 AOT モードに移行すると動かない CronJob があったことがハマりどころでした。コード内にファイルパスを自動で解決する処理があり、この値が AOT / JIT で違う値を返していたことが原因でした。対策としてはシンプルで、パスを外から渡すようにコードを書き換えました。

final scriptPath =
        io.Platform.script.path.replaceFirst(RegExp(r'pkg/stailer.*'), '');

リソース設定

AOT コンパイルモードに移行した後、 Node pool の移行に合わせてリソース設定によるコストの最適化を行いました。弊社の Kubernetes 上で動いている主なワークロードは以下の通りです。

  • Deployment: gRPC
  • Deployment: 非同期ワーカー
  • CronJob

お客様からのリクエストを直接受ける数種類の gRPC サーバに関しては低負荷時の CPU throttling を避けるために limit を設定していません (詳細はこちらの Issue や様々なブログで言及されてるで割愛)。その他に関しては現状 QoS が Guaranteed になるように設定しています。

特に CronJob に関しては以下の要素に応じて、設定値にどのくらい余裕をもたせるかの判断を行いました。

  • 実行頻度
  • 並列実行数
  • 失敗し続けたときのクリティカル度合い
  • 実装 (特に DB 周り)

振り返り

コスト削減を入り口にスタートしましたが、振り返ってみるとひたすら基本的な設定をし続けている期間でした。

欲を言えば、ドメイン体制での Ownership 移譲が先にできているともう少しよい進め方ができていたと感じています。その他にもデプロイの仕組みが十分整備されていなかったり、マニフェストが散在していたりと、あれが先にできてたらもう少し楽なのに…と思うことが多々ありました。そのような状況でも SRE チームのインセンティブと照らし合わせて、地道な火消しにまず振り切ったことは良かったポイントでした。また、ドメイン体制移行後は、各チームメンバーが自分のした設定を土台にリソース設定を都度ブラッシュアップしてくれており、体制移行をスムーズにすることに役立ったと実感しております。

インスタンスコスト削減

インスタンスコストは 4 割減 (0.6x) となりました。また、インスタンスコストが安定したことで確定利用割引を利用しやすくなりました。最近、他のチームメンバーが割引を有効にするタスクを推進してくれたことで、さらなるコスト削減効果が得られました。

cost
インスタンスコスト削減

CronJob 実行時間の短縮

CronJob 実行時間が短縮されました。

execution time
CronJob 実行時間の短縮

リソース使用の安定化

リソース設定以前は CPU request が設定されていなかったり、メモリ設定が不適切な Pod が多く存在しており、 Noisy Neighbor の影響と思われる現象が発生しておりました。その数をちゃんと計測しているわけではないので主観が入りますが、施策実施後はクラスタ全般がより安定したことを感じています。

今後について

マニフェストレポジトリの統一と自動化

以上により、既存の Pod のマニフェストは適切に設定がなされるようになりました。一方で、新規に追加されるマニフェストについては適切な設定がなされる保証はありません。

マニフェストが追加される頻度はそこまで高くないため、現状は SRE による CODEOWNER レビューによってこれを担保しています。今後は、マニフェストレポジトリの統一と Open Policy Agent (OPA) の導入によって自動化を進められれば、と考えています。

Re-architecture の機運

以上により、 Kubernetes 周りに顕在化していた問題が概ね解消され、運用コストが削減されました。また、その後ドメイン体制移行によって運用面の各チームへの移譲が進んだこと、様々な施策によって Toil が削減されたこと、ローンチが一段落したことも相まって、SRE チームとしてより将来に目を向けられる体制になってきたことを感じています。

現在は Re-architecture に対する機運が高まっていることを感じます。その中でいよいよ Kubernetes クラスタをリプレースしたり、はたまた Cloud Run に移行する話などが上がってくるかもしれませんが、今回の施策を行ったことでそのような議論にもじっくり腰を据えて向き合えようになりました。

Wrap up

弊社では、今後のさらなる 10x 案件に向けて採用を行っています。弊チームは SLO の策定等、スタートアップの事業フェーズながらも SRE プラクティスに向き合える環境を作ってきました。弊社に興味がある方はカジュアル面談へのご応募をお待ちしております!

明日は SWE お買い物チームの @takanamito さんが記事を公開する予定です。お楽しみに!

モノリス解体に向けたパッケージ構成

CTOのishkawaです。

この記事は10X アドベントカレンダー2023の3日目の記事です。

先日、サーバーサイドのメンバーを中心として、コードをどのように分割管理していくか話すオフサイトを実施しました。オフラインで1日中話していたこともあり、話題は色々な方向に進んだのですが、その中でもモノリス解体にトピックを絞ってシェアしたいと思います(他の話は他のメンバーが書いてくれるはず!)。

前提

10Xには4つの開発チームがあります(お買い物チーム、お会計チーム、お届けチーム、マスターデータチーム)。今年の4月にチーム分割が始まり、コードやデータのオーナーシップも各チームにアサインしてきました。

この組織移行によってオーナーシップ分割は進んでいるものの、依然としてモノリスのパッケージ(Dartのパッケージ)は大きいままで、ほとんどのコードがそこにある状態でした。領域ごとのディレクトリ分割は進んでいるものの、パッケージまで分かれる筋道が見えていませんでした。

課題

正直、ディレクトリ分割/オーナーシップ分割が進んでいけば、モノリスパッケージでもまだやっていけなくはないかなあという状況ではありました。しかし、以下のような課題があるのは事実なので、先手を打ってモノリスを解体する段取りを進めることにしました。

  • 業務領域を跨いた処理に対する制限が弱く、意図しない結合が生まれやすい。
  • 開発中のエディタによる解析、テスト実行、コード生成に掛かる時間が伸びている。
  • 名前空間が大きいため、クラスや関数に素朴な名前が使いづらい。
  • 依存パッケージやユーティリティが肥大化しやすい。

1つ目の課題が特に重要なので、ここではそれについて説明します。

業務領域を跨いだ処理に対する制限

Stailerにはネットスーパーの幅広い業務を取り扱うシステムが含まれています。注文管理、決済、店舗でのピッキング、配達といったものです。

各種業務はある程度の独立性がありますが、互いに関連しているものもあります。例えば、お客様からの注文が入っても、店舗でのピッキングすることができなければ欠品となるため、お客様の注文管理と店舗のピッキング管理の2つの業務は関連していると言えます。

この関連を素朴に実装する場合、注文管理でもピッキング管理でも注文データを更新することになりますが、これでは注文データは共有データとなり、管理が難しくなります。

一方、注文を取り扱うモジュールを定義し、データの変更をモジュール内に限定し、外からは指示を出すコマンドを定義すると、注文データの変更は注文モジュールに閉じることになります。

モノリスのパッケージではすべてが同じパッケージに含まれるため、前者の方式で簡単に実装できてしまいます。これは極端な例なので「そうはならんやろ」と思えるものかもしれませんが、いずれにせよ「どの機能がどの機能を利用して良いのか」の統制はコードベースが大きくなるほど難しくなります。ArchUnitやLintなどの方法で統制をかけることもできますが、業務領域レベルの大元の分割ではパッケージの分離の方が有効と考えました。

パッケージが分かれている場合、そもそも注文データへのアクセスのインターフェースも公開しない限りは使用できないため、自然と後者の方式を取ることになります。

我々のアプローチ

以下のようなパッケージ群を構成することに決めました。

  • stailer_app: mainのパッケージ。アプリ向けgRPCサーバーやcronジョブなど。
  • stailer_module: 業務を扱うモジュールのパッケージ。注文管理やピッキング管理など。
  • stailer_common: Stailerで汎用的に使うパッケージ。税額計算など。
  • general: Stailerに依存しない一般的なパッケージ。Firestoreクライアントなど。

元々 stailer_appstailer_module のすべてが1つのパッケージとなっていった所から、各種業務ロジックを stailer_module に切り出していくことと、エントリポイントであるstailer_app が別々のパッケージとして切り出していくことが主な決定です。

stailer_moduleは他のモジュールに依存してもOKで、公開インターフェースを通じて領域跨ぎの業務を完遂する想定です。なお、モジュールの循環依存は許容しないものとしています。

パッケージの公開インターフェース

Dartのパッケージの公開インターフェースは、以下のように決まります。

  • lib/src以下の実装ファイルはデフォルトでは公開されない。
  • lib/*.dartで実装ファイルをexportすると、当該ファイルのインターフェースが公開される。

ファイルの構造を図に起こすと、以下のようになります。

package_root
├── pubspec.yaml
└── lib
    ├── some_export.dart <-- lib/src以下のファイルを参照してexportする。
    └── src
        └── ...          <-- lib/src以下に実装ファイルを入れる。

前述の通り、ある業務を扱うモジュールの内部実装は他の業務を扱うモジュールからは不可視としようとしています。そのため、パッケージがexportするものは以下の3種類と決めました。

  • コマンド: モジュールの変更を加えるインターフェース。stailer_moduleから使用される。
  • クエリ: モジュールのデータを取得するインターフェース。stailer_moduleから使用される。
  • gRPCサービス: gRPCサービス。stailer_appから使用される。

exportするものを絞ることにより、モジュール間のコミュニケーションは意図したもののみとなり、不適切な結合を生みにくい構造となりました。

実現への筋道

先日まさに「この形を目指すぞ!」ということになったので、実現はまだこれからです(上手くいかないこともあるかも?)。実現に向けては、以下のようなステップを踏むことを想定しています。

  1. モノリス内にモジュールディレクトリをつくり、関連業務のコードをまとめる。
  2. モジュールディレクトリを跨ぐ処理の呼び出しに制限をかけ、疎にしていく。
  3. モノリス内のモジュールディレクトリへの依存がないモジュールディレクトリをパッケージ化する。

図にすると以下のようなイメージです。まずはモノリスパッケージからスタートします。

次に、モノリスパッケージ内にモジュールディレクトリを業務ごとに作って、分類していきます。

モジュールCはモノリス内へのモジュールへの依存がないため、パッケージにします。

モジュールAとモジュールBもモノリス内への依存がなくなったので、パッケージにします。

結果として、モノリスにひとまとまりになっていたものがパッケージ分割されました。

終わりに

今回のパッケージ分割ではパッケージという境界はできるものの、実体としてDartのメソッド呼び出しに過ぎません。裏を返せば、この方式は各モジュールが同一言語(Dart)で実装されていることや、モジュールがアプリケーションにバンドル可能という前提に依存しています。

将来的にはこの前提を取り払い、各モジュールの直接呼び出しからメッセージベースの連携に変えたり、はたまた各モジュールをサービス化したりするのかもしれません。いずれにせよ、今進めているような分離は必要なので、まずはこれをやり切ろうというのが我々の現在地です。

こうした取り組みは続けていくので、いつかまたこのブログで状況を報告できればと思います。


10X アドベントカレンダー2023はクリスマスの12月25日まで続きます。

明日はtakimoさんによる記事です!お楽しみに!

CIを高速化する技術⚡️

この記事は 10X アドベントカレンダー2023 という企画の1日目(12/1)の記事です。

こんにちは、10Xでソフトウェアエンジニアをしている 岡野(@operandoOS)です。

今回 10Xで3回目となるアドベントカレンダー企画の1日目をありがたく担当させていただきます💪

目次

10X アドベントカレンダー2023ってなに?

株式会社10Xのメンバーが送る、2023年アドベントカレンダー企画です🎄 🎅 🎍

10xall.notion.site

12/1から12/25まで毎日10Xのメンバーが様々なテーマでブログを書いてくれるので、ぜひお楽しみに✨12/25 最終日は10X CEOのyamottyさんがバシッと締めてくれるのも見所です!

さてさて、本題へ

記事のタイトルどおり【CIを高速化する技術】の紹介をしていきます。

今回紹介する高速化のテクニックは多くのCI/CDサービスで汎用的に活用でき、比較的に簡単な実装で高速化が見込めるテクニックを中心に紹介します。紹介するテクニックを身につけることで、多くのCI/CDサービス環境下で高速化が実現できるようになります。

テクニックの実装イメージを掴んでもらうために、GitHub ActionsとCircleCIを参考に取り上げます。今回詳細な実装には触れませんが実装の参考になるドキュメントを記載しています。

CIは絶対に速い方がいい

なぜ速い方がいいのかを説明することが本記事の目的ではないため詳細は割愛しますが、簡単に言うと「速いと待ち時間が短くなりフィードバックサイクルを早く回せる = Developer Experience(開発体験)がいい!!」ので、CIは絶対に速い方がいいんです。

またCIの速度がCD(Continuous Delivery)にも影響するので、CIは絶対に速い方がいいんです

大事なことなのでもう一度言います。CIは絶対に速い方がいいんです

CIを高速化するテクニックの紹介

ここからは多くのCI/CDサービスで汎用的に活用でき、比較的に簡単な実装で高速化が見込めるテクニックをいくつか紹介します。最後に紹介する【テストコードの実行速度を上げる】だけ例外なのですが、こちらはCIを高速化するテクニックとして非常に大事なことなので記載してます。

キャッシュの利用


CI上でビルドやテストを行う前準備としてパッケージ管理ツール(npm、gemなど)でライブラリのダウンロードを行うことはよくあると思います。

CIを実行するたびにそれらを毎回ダウンロードするのは無駄です。一度ダウンロードしたものをキャッシュして以降はダウンロードするものに変化がなければキャッシュを復元して利用することで高速化が見込めます。

このテクニックはどのCI/CDサービスでも簡単に実装できるので、CIを高速化する上で必ず利用すべきテクニックです。

当たり前のように利用してると思いきや改めて見てみると「キャッシュしてないじゃん!」とか「あれ?これもキャッシュできそう!」となることもありますので、今一度キャッシュ利用がどうなってるか見直してみるのもおすすめです。

参考までにGitHub ActionsとCircleCIでのキャッシュ利用に関するドキュメントをのせておきます。

docs.github.com

circleci.com

マシン性能の調整


CIはCI/CDサービスが用意したマシン上で実行されますが、利用するマシンの性能(CPU、メモリなど)次第で大幅な高速化が見込めます。各CI/CDサービスで用意されているマシン性能は異なりますが、マシン性能の変更はどのサービスでも簡単に行なえます。

GitHub Actionsであればruns-on 、CircleCIであれば resource_classで各ジョブを実行する際に利用するマシン性能を指定できます。

例えば各サービスでM1 macOSを利用したい場合は以下のように書くだけです。

# GitHub Actions
runs-on: macos-13-xlarge

# CircleCI
resource_class: macos.m1.large.gen1

参考までにGitHub ActionsとCircleCIが用意しているマシン性能に関するドキュメントをのせておきます。

docs.github.com

docs.github.com

circleci.com

マシン性能を調整するだけで高速化されるわかりやすい例がCI上でiOSアプリのビルドにM1 macOSを利用するケースです。こちらはGitHubから公式アナウンスがあるとおりIntel3コア macOSと比較してビルド時間が最大80%短縮される素晴らしい高速化の事例です。しかもこれがruns-onの指定1行変えるだけで実現できます。

github.blog

マシン性能を上げることでビルド時間やテスト実行時間が短縮される環境では簡単な変更で高速化が見込める一方、マシン性能を上げるに比例して利用時間に対してかかる課金量は増えます。私のこれまでの経験では課金量が増えた分CIの実行時間が正比例して短縮されることは珍しく、性能を調整する際は費用対効果の測定をおすすめします。私が効果測定を行う場合は、数回CIを実行してみて増えた課金量に対してCIの実行時間がどのくらい短縮されるのかざっくり計算してみて判断するだけで難しいことはしてません。ちょっとお金かけてでも実行時間が短くなるならいいよね!という前提がチームや組織内で共有されていると高速化の改善が行いやすいと思います。

ちなみに本記事では詳細には紹介しませんが、CI/CDサービスが提供しているマシン性能では満足しない方はセルフホストランナーという自前で用意したマシン上でCIを動かす仕組みを利用して高速化を実現するケースもあります。

docs.github.com

circleci.com

ジョブの並列実行とテスト分割


CI上でテスト実行を高速化したい場合にジョブの並列実行とテスト分割を利用することで大幅な高速化が見込めます。

言語やテスティングフレームワーク次第ですが多くの場合テストコードそのものをいじらなくても、CI設定ファイルの簡単な変更のみでテスト実行を高速化できます。

ジョブの並列実行とテスト分割による高速化は図にすると以下のような感じです。

テストを各コンテナに分配し並列実行することでジョブの実行時間が短縮され、次のジョブに進むまでの時間が早くなります。

多少注意が必要なのは、並列実行にしたことでジョブ全体でかかる実行時間は並列実行しない時よりも長くなることが多いです。下記図のように並列実行時のステップをまとめて横に並べるとわかるとおりテスト実行準備を2回行うことになるからです。結果的にジョブの実行時間は短縮されますが、ジョブ全体の実行時間は増えるため課金量が増えます。並列実行にしたら絶対増えるわけではなく、並列実行にしたことでCPUやメモリへの負荷が減り、各テスト実行が速くなり全体の実行時間が短くなることもあります。その場合は課金量が減るので良いこと尽くしです。

CircleCIではジョブの並列実行とテスト分割を行うための仕組みが用意されています。並列実行の指定をparallelismで行い、テスト分割をcircleci testsコマンドを利用して行います。

詳細は以下のドキュメントを参照してください。

circleci.com

GitHub ActionsではCircleCIほどわかりやすい機能が提供されていないのですが、matrixとシェルコマンドを利用することで並列実行とテスト分割が行なえます。

以下の記事が参考になります。

kojirooooocks.hatenablog.com

最適なテスト分割


先ほどジョブの並列実行とテスト分割を説明するための図を見て勘のいい方は気づいたかもしれませんが、あの図のあまりよくないテスト分割になっています。

以下の図のようになるのが最適なテスト分割です。

このようにテストの実行時間に基づいて最適なテスト分割を行うことでさらなる高速化が見込めます。

全テストの実行時間がわかるものがあればどんなCI/CDサービスでも最適なテスト分割は実現可能です。一般的にテスティングフレームワークにはJUnit XML形式で全テストの実行時間を書き出す機能があります。その機能を利用してテスト実行時間をファイルに書き出し、書き出したファイルを何かしらのツールに通すことで最適なテスト分割を行うのが一般的です。

CircleCIではcircleci tests splitコマンドを利用することで最適なテスト分割が簡単に行えます。

詳細は以下のドキュメントを参照してください。

circleci.com

GitHub Actionsでは以下のようなカスタムアクションを利用することで最適なテスト分割が行えます。

github.com

ジョブの実行順序・依存関係の最適化


CIのワークフローはいくつかのジョブで構成され、それぞれのジョブは実行順序に依存関係があることが一般的です。実行順序の依存関係のつながりが長ければ長いほどCIの実行時間も長くなります。

ジョブの実行順序・依存関係の最適化とは、ジョブ同士の依存関係を見直し、並列実行できるジョブを増やすことでCIの実行時間を短縮するテクニックです。

わかりやすい例として下記図のようにすべてのジョブを逐次実行しているワークフローあったとします。ジョブ同士の依存関係を見直したところ、ジョブ1の実行結果に基づきジョブ2,3,4は並列実行でき、ジョブ2,3,4の実行結果に基づきジョブ5を実行するワークフローに見直せたとします。その結果、並列実行できるジョブが増えたことでCIの実行時間は短縮されます。

既にジョブ同士の依存関係が整理されているワークフローでも改めて依存関係を見直すことでCIの実行時間を短縮できる可能性がありますので、定期的に見直してみることをおすすめします。

ジョブ同士の依存関係はCircleCIであればrequires、GitHub Actionsであればneedsで定義することができます。それぞれ参考になりそうなドキュメントをのせておきます。

circleci.com

docs.github.com

不要なジョブ・ステップを削除する


CIのワークフローをよくよく眺めてみると不要なジョブやステップを見つけることがあります。それらを削除することでCIの実行時間が短縮されるかもしれません。

長期間色んな人がメンテナンスしてきたワークフローは削除チャンスに出会うことが多いです。不要なものを消すことでCIの実行時間短縮だけでなく、ワークフローのメンテンス性も向上するため定期的に見直してみることをおすすめします。

テストコードの実行速度を上げる


これまで紹介してきたテクニックは主にCI/CDサービスの仕組みを活用してCI設定ファイルを変更することで高速化を実現するものでした。しかしこれらのテクニックを駆使しても思ったよりCIが早くならない場合もあります。

例えばテストコードの実行がなんかすごく遅いなどがそれにあたると思います。テストコードそのものの改善は利用している言語やテスティングフレームワークによって改善方法は様々です。

様々な中でもテスティングフレームワークによくある機能やテスト周辺にある技術、CI/CDサービスに用意された機能などを使いテスト実行速度を上げることで、CIの高速化が見込めます。

テストコードそのものを改善するヒントを得るために、CI/CDサービスのテストインサイトを活用する方法があります。

CircleCIにはテストインサイトというテストのパフォーマンスを表示してくれる素晴らしい機能があります。これを見るだけでテストがどんな状態なのかざっくり理解でき、改善すべきポイントを絞り込むことができます。

circleci.com

利用しているCI/CDサービスのテストインサイト的な機能がない場合は、利用しているテスティングフレームワークの機能を使いテストの状態を可視化できないか調べてみましょう。

例えばrspecには–profile オプションがあり、これを利用することで遅いテストを抽出してくれます。

https://rspec.info/features/3-12/rspec-core/configuration/profile/

他にもテスティングフレームワークには何らかの形式で全テストの実行時間を書き出す機能がついてることが多く、その結果を利用することで遅いテストを特定することができます。

またテスティングフレームワークではなくテストのプロファイリングに特化したライブラリなども言語によっては存在します。

github.com

こういったものを利用し、遅いテストやテスト実行を遅くしている原因を解消することで、さらなるCI高速化を実現できます。

テストコードそのものを変更せずテスト実行時間を短縮したい場合には、CPUコア数分テストを並列実行するような仕組みを検討してみるものいいでしょう。

私達がStailerの開発で利用しているDart testにはconcurrencyオプションがあり、これを使うとより多くのテストを同時に実行できるようになります。

pub.dev

もしお使いのテスティングフレームワークにそういった機能がない場合は、Rubyで代表的なparallel_testのようにテスティングフレームワークとは別のライブラリを使い、並列実行ができないかを調べてみるといいでしょう。

github.com

紹介したテクニックを活用した10XでのCI高速化事例

ここまで紹介してきたテクニックを活用して私が10XでCIの高速化を行った事例を2つ紹介します。

アプリのビルド時間の大幅短縮に成功!!


私達が開発しているStailerではお客様や店舗スタッフさんが利用するアプリをFlutterで開発し、iOS / Androidで提供しています。これらの開発ではCIにGitHub Actionsを利用しています。

Stailerのアプリはパートナー企業様ごとに作成されています。そのため全パートナー企業様のアプリをビルドするためには、2023/12現在OSごとに26ビルド並列実行する必要があります(開発版アプリのビルドも含めると2倍になるので52ビルド…🙄)。

アプリが増え続ける中で特に困っていたのがiOSのビルド時間が長いことでした。GitHub Actionsでは長い間 Intelコア版のmacOSランナーしか提供されておらず、ビルド時間がなかなか縮まりませんでした。セルフホストランナーを利用する選択肢もありましたが、メンテンスコストが懸念であり採用を見送りました。

そんな中今年の10月 GitHub ActionsでM1 macOSランナーの提供が始まりました。

github.blog

Intelランナーで平均ビルド時間が16分だったのが、M1ランナーに変更したことで平均ビルド時間が8分になりビルド時間が半分になりました!やったね!

またAndroidアプリのビルドでも同じようにCPU2コアランナーをCPU4コアランナーに変えたことでビルド時間が半分になりました。

github.blog

このために行った変更はたったの1行のみです。これは高速化テクニックで紹介した【マシン性能の調整】がうまく行った事例です。

その他にも Flutter / Flutter package / Gradleのキャッシュ、不要なステップの削除などを行い、CIの実行時間をかなり短縮できました。

紹介したテクニック【マシン性能の調整】、【キャッシュの利用】、【不要なジョブ・ステップを削除する】を実践した事例紹介でした。

APIのテスト実行時間の大幅短縮に成功!!


私達が開発しているStailerのバックエンド(API)はDartで開発しています。これらの開発ではCIにCircleCIを利用しています(一部GitHub Actionsも利用)。

機能がどんどん増え続ける中で特に困っていたのがテスト実行の時間が長いことでした。またFlakyなテストもあり「困ったな〜😣」って感じでした。

改善を進めるにあたりまずはテストの状態を知るためにCircleCIのテストインサイトを活用し、遅いテストの特定を行ってから遅い原因を調査しました。

調査した結果、原因はテストデータの大量作成による処理のつまりでした。つまりテストコードの実装起因で実行が遅くなっていました。

テストコードを修正しこの問題を解消することで、ハチャメチャに遅いテストが速くなりテスト実行時間が半分になりました!やったね!(この時調査を手伝ってくれた沢田さんありがとう🥹)

その他にも最適なテスト分割、並列実行数を上げる、ジョブの依存関係最適化、Lintの実行時間短縮などを行い、CIの実行時間をかなり短縮できました。

またマシン性能の調整においてはCPUとメモリの使用率がそこまで高くなかったことからresource_class: largemedium+にダウングレードすることで課金量の節約も行いました。

紹介したテクニック【テストコードの実行速度を上げる】、【ジョブの並列実行とテスト分割】、【最適なテスト分割】、【ジョブの実行順序・依存関係の最適化】、【マシン性能の調整】を実践した事例紹介でした。

CIを高速化するために日々取り組んでいること

CIを高速化するために私が日々取り組んでいることをいくつか紹介します。

CI/CDサービスの集計情報を定期的に確認する


CIは生き物なのでいつどんな変化が起きるかわかりませんので、日々体調チェックしてあげることが大事です。

CircleCIであればInsightsダッシュボードがとても役に立ちます。時系列でCIの実行結果を知ることができるため、何か異常が起きた際にいつから起きたのかなども簡単に調べることができます。その他にも実行時間が遅い順にテストを表示してくれるので便利です。

circleci.com

GitHub ActionsにはまだCircleCI相当のInsightsダッシュボードが存在しないため、気になるワークフローは定期的に実行時間を見て異常が起きてないかチェックしています。チェックして「あれ?なんか遅いかも?」と思ったら、実行結果やワークフローを確認して問題がないか確認しています。弊社ではDatadogを利用しているので、今後GitHub Actionsの実行結果可視化はDatadog CI Visibilityを活用する予定があるみたいです。早く使いたい☺️

www.datadoghq.com

CI/CDサービスの最新情報を追う


最新情報が流れてくるブログのRSSを購読したり、Xのポストを追ったりしています。

GitHub Actionsの情報は以下あたりを見てます。

github.blog

https://twitter.com/ghchangelog

CircleCIの情報は以下あたりを見てます。Developer Advocateの方が隔週でWhat's Newまとめ記事をQiitaに書いてくださっていてありがたいです😊

circleci.com

https://twitter.com/circleci

qiita.com

その他にも今度どういった機能が追加される予定なのかを知るために各サービスのロードマップもたまに見てます。

github.com

circleci.com

CI高速化事例のブログなどをいっぱい読む


私と同じように日々CIの高速化と向き合っている方が世界中にたくさんいます。その方々の成果から高速化の知見を得る機会は多いです。自身が知っている高速化テクニックだったとしても細部まで見てみるとそれぞれの工夫がされており、学んでいて飽きない領域だなーと思います。

とにかく色々試す


高速化できそうなアイデアが思いついたり、CI/CDサービスで新機能がリリースされたらとにかく試してみることを心がけています。

やってみてうまくいかなければ適用しなければいいだけですし、試す過程でワークフローの見直しや高速化できそうな勘所が身につくのでガンガン試していきましょう!

ほどほどに高速化する


高速化のためにメンテンス性を犠牲にした結果、自分しかメンテンスできないものができてしまってはいい高速化とは言えません。

「目先の1分を短縮できるけどこの実装は分かりづらいし、このCIが実行される頻度を考えると…やめたほうがいいな!」みたいに自問自答することが大切です。

できる限りシンプルな実装を保つことで、CI/CDサービスが提供する新機能を試しやすくなるメリットもあります。

「CI遅い…待てない…」と日々思い続ける


高速化しても時間が経つにつれてまた遅くなったり、遅くならなくても感覚的な慣れによって「CI遅い…待てない…」となることが多々あります。この気持はとても大事です。

「遅い!」と思った時が見直し時であり、「遅い!」という思いから改善は始まります。

おわりに

「なぜそんなにCIの高速化が好きなんですか?」と聞かれたら、「CIが早くなるとみんな嬉しいじゃないですか」と答えます。

単純にCI/CDが好きというのもありますが、自分が好きな技術を駆使してみんなが喜んでくれるのは幸せなことです。

今回紹介したテクニックなどを参考にした読者の方々のCI/CDが高速化され、共に働くメンバーに喜んでもらえることを祈っています。

ちょっと早いですが、メリークリスマス🎅🎄🎁