調整なしですぐ使えるノーコンフリクトなマイクロサービス開発環境 - XICA Tech blog

XICA Tech blog

株式会社サイカの開発本部が提供する技術ブログです。データサイエンスに関する取り組みや日々の開発のナレッジをお送りします。

調整なしですぐ使えるノーコンフリクトなマイクロサービス開発環境

はじめに

イカでソフトウェアエンジニアとして、サービス開発をしている浅井と申します。

イカではKubernetes上にマイクロサービスを展開しています。 このマイクロサービスの開発を進めていくにあたり、1つしか存在しない開発環境を使用するのに順番待ちが発生するといった課題が発生しました。
今回の記事では、サイカがこの課題をどのように解決していったかをご紹介します。 チームがスケールしてきて、開発環境がボトルネックになってしまった方の参考になればと思います。

*以下の話は2022年7月に行った意思決定であるため、2023年現在においては当てはまらない内容があるかと思いますが、ご容赦ください。

課題

前提として、サイカでは自社サービスをSPAとして提供しています。まずユーザーはフロントエンドサーバーにアクセスし、HTML等を取得します。その後ユーザー操作に応じて適宜APIリクエストをBFFに飛ばし、BFFがバックエンドマイクロサービス群に対し、gRPCでさらに通信を行っています。技術スタックとしてBFFやマイクロサービスではGo言語を使用しています。(詳細はビジョンを実現させるためのマイクロサービスという選択 - XICA Tech blogを参照)

開発環境が1つしかないものの、マイクロサービスを展開しているためある程度の変更であれば同時に検証することは可能でした。しかし、機能開発が複数並行で進むにつれて同じマイクロサービスに対する変更であったり、マイクロサービス間での整合性の観点から、同時に複数の機能を試すのは難しくなっていきました。

開発環境でのコンフリクト
例えばサービス2に対してfeatureAブランチをデプロイした人とサービス1に対してfeatureBブランチをデプロイする人がいる場合、お互いの修正内容が度々コンフリクトすることがありました。後方互換性を常に保つようにするのもある程度の限界があるため、どうしても順番待ちや譲り合いといったコミュニケーションコストが発生していました。また機能開発をしている途中での突発的なバグ修正などの差し込みをどう検証するかといった調整コストも発生していました。
この問題の温度感はチームの人数が増えていく中で急速に上がってきており、改めて理想的な開発環境とは何かを描く必要がありました。

要件

この課題を解消するにあたって満たすべき要件を以下のように定めました。

  • 複数の開発者が同時に調整不要で、自分の開発中ブランチを開発環境にデプロイできる
    • 例え複数デプロイされていたとしても互いに干渉しないようにする
  • 発行されたURLにアクセスするだけで、その実装された機能を試すことができる
    • QA担当、PM、社内ステークホルダなど裏側のアーキテクチャに詳しくない人間でも容易に試すことができる
  • メンテナンスコストや消費リソースを小さくする
    • 対して開発環境のみに適用するため、可用性やスケーリング性能は下がっても良い

次にこの要件を満たすべくどのようなApproachを取ったかをお話しします。

Approach

まず臨時的な対応としてdev1、dev2と開発環境を増やす対応をしました。これにより一時的にペインは解消されましたが、以下のようなデメリットがありました。

  • インフラコストが増加した
  • DBのマイグレートといったメンテナンスコストが増加した
  • 調整コストは減ったが0にはならなかった

そこで、開発環境を増やすのではなく、1つの開発環境内にPRごとにコンテナを立ち上げ、それにブランチ名でアクセスするような環境(通称プレビュー環境)を構築しました。以下に概念図を記載します。

プレビュー環境概略

まず開発者がfeatureAブランチを切ってPRを作成すると、それに対応するコンテナが立ち上がります。そしてPRに対して発行されたURLにアクセスするだけで、よしなにそのコンテナにアクセスできるようになっています。 そして他の開発者がfeatureBブランチを切ってPRを作成しても同様で、お互いに干渉することなく自分の機能を試すことができます。
また開発者が触っていないサービスに関してはmainブランチのコンテナを共有して使うことで、インフラコストが過度に増加することを防いでいます。

Architecture

このプレビュー環境を作る上での一番の問題は「どうやってブランチ名がついたコンテナへのルーティングするのか」だと考えられます。
この問題を解消する上で我々が選んだ技術はOpenRestyでした。全ての通信がOpenRestyを通るようにし、OpenRestyがブランチ名の伝播及びルーティングを管理するようにしています。以下に概要図を示します。

ルーティング概要

この問題をさらに細分化すると、「ブランチ名をどのようにフロントエンド・BFF・マイクロサービス群に伝播するか」と「ブランチ名を使ってどうアクセスするか」に分けられると思います。

まず「ブランチ名をどのようにフロントエンド・BFF・マイクロサービス群に伝播するか」に関してお話しします。
フロントエンドサーバーに対してはユーザーがブランチ名をサブドメインにつけたURLを通してアクセスするため、その情報をもとにルーティングします。その際OpenRestyによってそのサブドメインをパースし、cookieとしてブランチ名をセットするようにレスポンスにヘッダーをつけます。これはBFFに対するエンドポイントがフロントエンドと異なりサブドメインの情報が伝わらないことから、cookieを通してブランチ名を伝える必要があるためです。
上記のためBFFへのアクセス過程で、OpenRestyに対してはcookieを通してブランチ名が伝わってきます。そこでバックエンドでは統一的にヘッダーとして扱いたいため、cookieからヘッダーに変換してBFFにリクエストを渡しています。そしてBFFではcontextを使ってブランチ名を引き回しつつ、gRPC metadataにブランチ名を埋め込めるようなgRPC Interceptorを実装し利用しています。

「ブランチ名を使ってどうアクセスするか」に関しては、EKS上でService名にブランチ名を含めてリソースを作っているため、サブドメインcookie・gRPC metadataからブランチ名を取得できればアクセスできます。OpenRestyではLuaを使ってさまざまな処理を書くことができるため、例えば以下のようにブランチ名をsuffixとしてService名につけ、もし存在すればそれを、なければdefaultの接続先を返す関数などを書くことができます。

local function _select(self, suffix, default)
    local resolver = require "resty.dns.resolver"
    local r = resolver:new{
        nameservers = { "kube-dns.kube-system.svc.cluster.local" }
    }
    if not r then
        return default
    end
    local service_name = string.match(default, "(.-)%.")
    local others = string.match(default, ".-(%..*)")
    local target_host = service_name..suffix..others
    local answers = r:query(target_host)
    if not answers or answers.errcode then
        return default
    end
    return target_host
end

またマイクロサービス間の通信でもOpenRestyを通過するように、CoreDNSを使って.proxy.xica.local に対するアクセスが全てOpenRestyを通るようにしています。これによりKubernetesのServiceに対するDNS Aレコード my-svc.my-namespace.svc.cluster.localmy-svc.my-namespace.proxy.xica.localに書き換えるだけで、全ての通信がOpenRestyを通るようになります。

template IN ANY proxy.xica.local {
  answer "{{ .Name }} 60 IN CNAME openresty.dev-proxy.svc.cluster.local."
  fallthrough
}

その後OpenRestyがmy-svc.my-namespaceの部分を見て、元々のアクセス先にリクエストを転送しています。

Why OpenResty

ある識別子をもとにした特定のServiceへのルーティングをKubernetes上で実現するだけであれば、他にもいくつか選択肢があると思います。 事実サイカでもIstioを使って上記の環境を実現できることを確認していました。 これを確認した上で、我々はIstioと比較し以下の理由でOpenRestyを採用しました。

  • Istioの運用コストの重さ
    • 3ヶ月ごとにマイナーリリースがあり、End of Lifeもリリースから半年程度と短い
  • 要件に対するIstioのミスマッチ
    • あくまでも識別子をもとにルーティングしたいだけであり、Istioの主眼であるサービスメッシュの導入というモチベーションがなかった

もちろんデメリットとしては全ての通信がOpenRestyを通ることによるスケーリング性能の低下などが考えられるのですが、開発環境のみへの導入のため許容できるという判断に至りました。
設計当初はKubernetesエコシステムの動向などからIstioが有望な候補になっていたのですが、改めて要件を整理した結果、今回の我々に最も合うのはOpenRestyであるという結論になりました。

こうした意思決定のプロセスはXEPと呼ばれるDesign Doc(詳細は サイカの即戦力を生む2つの設計書 - XEP と Architecture Guide - XICA Tech blogを参照)が大きく関わっており、他に考えられる技術や設計を記載し、さらにレビューのプロセスを挟むことでその状況にあったより良い意思決定ができるようになっています。

最高の体験

裏側ではサブドメイン -> cookie -> ヘッダーとブランチ名をさまざまな形に変えて伝搬しつつ、ルーティングを行っていますが、これを利用する側としては非常にシンプルになっています。
まず開発者はラベルを付与したPRをGithub上に作成します。するとGithub Actionsが起動し、該当PRのブランチをもとに自動でServiceとDevelopmentを作成します。そして最後にブランチ名をサブドメインに含めたURLをPRにコメントします。

PRを通したプレビュー環境管理

これにより開発者としてはただPRにラベルを付与するだけで、勝手にそれを試せる環境が立ち上がることになります。また利用者としてもそのURLにただアクセスするだけで実装された機能を試すことができます。
またPRをマージすると該当のServiceやDevelopmentを削除するようなGithub Actionsも作成しているため、無駄なPodが残り続けないようになっています。

最後に

この記事ではOpenRestyを使うことで、スケーラブルかつ低コストな開発環境を実現したお話をさせていただきました。この環境はリリース当初から2023年11月現在に至るまで問題を起こすことなく順調に稼働しており、日々様々なエンジニアが利用しています。
またエンジニアだけでなくQA担当やPMも発行されたURLをもとにアクセスするだけで実装内容が試せることから、非常に便利に活用しています。
開発環境がボトルネックになって困っている方の参考になればと思います。

CopyRight © XICA CO.,LTD.