CIをJenkinsからCircleCIに載せ替えた話

kintoneチームの川向です。

kintoneチームではCIツールとしてJenkinsを長い間使用してきましたが、 2021年の夏でCircleCIへの移行を完了しました。

今回はその時の様子を共有したいと思います。

2021年初めの状況

2021年初め頃のkintoneチームでは、メインのCIとしてJenkinsを使用し、CircleCIをサブとして併用していました。

リリース候補のブランチのCI(メインのCI)では、Jenkinsを使用して複数のジョブを用意し、 それをJoinプラグインで繋げていました。 このプラグインを使うことで、別々のジョブを同時実行することができます。 これをDelivery Pipelineプラグインで可視化していました。

また、メインのCIではE2Eのテストも自動で実行しています。 テストでは特別にセットアップした開発機材を使う必要があり、その排他制御のため、個々のジョブは同時実行を行わないようにしていました。

GitHubの変更を検知してCIが起動する。CIの中のテストで専用の開発機材を使用する。
メインのCI

トピックブランチでのCIは、社内で運用しているCircleCI Serverを使っていました。 Jenkinsの負荷を下げるために数年前に載せ替えを行いました。 ここでは、lintやユニットテストのみを実行していました。 時間のかかるE2Eテストは、Jenkinsの手動起動のジョブで実行していました。

GitHubの変更を検知してlintやユニットテストがCircleCI上実行される。E2Eのテストは手動で実行する
トピックブランチのCI

他にも手動で起動するジョブがいくつかありました。

なぜ移行を行なったか

Jenkinsの運用ではいくつか問題がありました。

同時実行やスキップの処理が実装しにくい

これまではJoinプラグインで複数のE2Eテストを同時実行していました。

E2Eテストはブラウザ動作やネットワークなどテストケースで十分に制御しきれない要素がある性質上、 不安定な挙動でジョブが失敗することがあり、ジョブを再実行することがあります。 このプラグインの場合、同時実行しているもののうち一つ失敗しているときでも、 その上流のジョブから再実行しないといけないという問題がありました。

以下の図の場合、api-testで失敗しているとき、api-testを再実行して成功してもstore-artifactは実行されません。 この場合は、packageから再実行する必要があります。

CIのサンプル構成。この構成では、packageジョブが終了後、api-testとselenium-testが実行され、それら2つが完了するとstore-artifactが実行される。この図ではapi-testが失敗している

このため、成功しているジョブまで再実行する必要があり、時間もリソースも無駄になっていました。

ジョブの変更が全てのブランチをまたいで影響する

kintoneではJenkinsfileによるファイルベースでの設定方法が登場する前からJenkinsを使用しており、 ほぼすべてのジョブはフリースタイルプロジェクトで作成していました。 この場合、設定変更はWebの画面から行うことになり、それがすべてのブランチでのジョブの実行に影響を与える点も問題でした。 最新のブランチでのみ影響させたい変更があっても、 ジョブを変更すると他のブランチでも影響が出てしまいます。 このため、後方互換を考慮しながら修正する必要がありました。 kintoneでは毎月リリースを行なっていて、同時に3バージョンのブランチが開発されている状況になることがあり、 それぞれのブランチで正しく動くようにするのは手間でした。

プラグインのメンテナンスが困難

プラグインを複数使っていましたが、 メンテナンスが十分にされないものもありました。 例えば、最近ではJenkinsのジョブの設定画面がtableタグからdivタグに変更されたのですが、 それに追従できていないプラグインがまだあります。 それを使っているジョブではwebの画面から設定を変更することができなくなってしまいました。

載せ替え先としてCircleCI Serverを選んだ理由

CircleCIを選んだ理由は以下のものです:

  • 既にkintoneチームでも利用実績がある
  • ジョブ失敗時のリトライが行いやすい。失敗したジョブからリトライが行える
  • CIの設定をyamlでほとんど記述できる

他の比較候補

移行の候補としては、GitHub Actionsも挙がりました。 特に、手動でジョブを起動できる点がCircleCIよりも優位性がありました。 しかし、以下の理由で候補から外れました:

  • kintoneはGitHub Enterpriseでホストされていて、社内のGitHub Enterpriseではまだ利用ができなかった
  • GitHub EnterpriseのGitHub Actionsではキャッシュが利用できない
    • 仮にクラウド版と同条件のキャッシュが使えるようになったとしても、kintoneはビルドに使う依存ライブラリなどが多くキャッシュが多くなるため上限に達しそうだった
  • CI内のジョブが失敗した時、失敗した箇所からのリトライが行えない。メインのCIでは最初から再実行すると70-80分ほどかかる

Jenkins pipelineも代替としては考えられますが、 プラグインのメンテナンスの問題など、 前述の問題を解消するための手間が多くなるため、候補から外れました。

移行方法

今回はCIの大きな流れ自体は変えず、 移行完了を第一目標に段階的に進めていきました。

トピックブランチのCIの移行

まずはトピックブランチの移行を進めました。 トピックブランチではすでにCircleCIを使っており、移行のとっかかりとしては最適でした。 作業としては、E2EのテストだけJenkinsで行われていたので、これをCircleCIに移行するというものでした。

こちらでの作業内容は他の開発者に影響がないため、初期の作業でも安全に進めることができました。

移行完了後、1ヶ月ほど様子見をし、問題が起きないか確認していました。

手動ジョブの移行

次に手動ジョブの移行を行いました。 CircleCIのCIに問題が起きることに備え、 Jenkinsの古いCIを残したまま作業を行いました。

メインのCIの移行

最後にメインのCIの移行を行いました。 稼働直後は検討漏れなどで動かない箇所がありましたが、1日で対応が完了し、移行が完了となりました。

移行時に発生した課題

移行時にはいくつか問題があり、ひとつずつ改善を進めていきました。

E2Eテスト用開発機材の管理

前述のように、 E2Eテストでは特別にセットアップした開発機材を使う必要があります。 Jenkinsのときは、その排他制御のためジョブの同時実行を行わないようにしていました。

しかし、これらの開発機材の設定はほぼ同じでした。 このため、テストで使用する開発機材を予め幾つかプールしておき、ジョブを開始した時に空いている開発機材をロックする方式に変更しました (この仕組みにはkintoneのアプリを使用しています)。 これにより、ジョブごとに開発機材が占有されることがなくなり、開発機材がある限り同時実行が可能になりました。

アーカイブの切り戻り

kintoneのCIでは、E2Eテストの通過後に社内のドッグフーディング環境にもデプロイしています。

よくあることですが、GitHubに近い時間にPRがマージされると、CIが同時に開始することがあります。 Jenkinsの時はジョブが同時実行できないようにしていたので、それぞれのCIはコミット順に実行されていました。 しかし、CircleCIではそのように制御することはできず、 後のマージで起動したCIが先に終わり、 前のマージで起動したCIがそのあとで終了するケースがあります。

このとき、アーカイブが新しいものから古いものになってしまい、それがドッグフーディング環境にデプロイされるとダウングレードとなってしまいます。

これを解決するため、アーカイブをアップロードする前に現在の最新のアーカイブと比較して、切り戻りがないかをチェックするようにしました。

手動起動のジョブ

Jenkinsではいくつかのジョブを手動で実行していましたが、CircleCIには手動起動の方法がありません。 これについては2種類の対策を行いました。

トピックブランチのE2Eテスト

前述のようにメインのCIではE2Eのテストも自動で実行していますが、 Jenkinsの時はトピックブランチでは手動で起動していました。

これを再実装するため、トピックブランチのCIの中で、承認を行うと実行するジョブを追加しました。 これを承認した時だけE2Eのテストが実行されます。この仕組みにより、実行タイミングをコントロールできるようになりました。 デメリットとしては、これを承認しない時にGitHub上のコミットのステータスが黄色(=CIがまだ実行中)になってしまう点です。 これについては、良い改善策はなく、少し不便な状態です。

他の手動実行ジョブ

移行作業の途中で、社内でもGitHub Actionsが利用可能になっていました。 このため、GitHub ActionsからCircleCIのAPIを実行する形としました。

これにより、GitHubの画面上からジョブを開始できるようになりました。

なお、CircleCIのAPIは3つありますが、当時は実質ジョブトリガーしか利用できない状態でした。

  • API v1.1のジョブトリガーAPI
    • 使えるが、ジョブ単位でしかトリガーできない(ワークフロー全体は起動できない)
  • API v.1.1のビルドトリガーAPI
    • ワークフローも起動できるが、ジョブ/ワークフローの限定はできない(指定したブランチで実行されうるすべてのジョブ/ワークフローが起動してしまう)
    • 社内のCircleCI Serverでは実行するとエラーになってしまった
  • API v2のパイプライントリガーAPI
    • ワークフローを起動できるが、社内のCircleCI Server(バージョン2)では利用できなかった
    • https://circleci.com/docs/ja/2.0/api-intro/

      オンプレミス版をご利用のお客様 API v2 is not supported for self-hosted installations of CircleCI Server 2.x. API v2 is supported for self-hosted installations of CircleCI Server 3.x.

このため、単体のジョブで処理を全て行う必要があり、少し注意が必要でした。

ネットワークが切れる

最後の問題はネットワークでした。

CircleCI ServerはAWS上でホストされており、Transit Gatewayを通じて社内ネットワークと接続しています。 E2Eのテストを実行している時、CircleCIサーバーから社内のテスト用の開発機材にリクエストを飛ばすのですが、このときネットワークが切れる現象が発生していました。 調査したところ、Transit Gatewayのネットワーク帯域が逼迫していることがわかりました。 さらに調べていると、社内のdocker registryとの通信が多いことがわかりました。 特に多いのが、kintoneチームがCircleCIで実行しているdocker imageでした。 原因が分かったため、イメージを社内のレジストリからAWSのECRに移行しました。 これによりネットワーク負荷も減り、ネットワークが切れる現象も減りました。

Transit Gatewayに関連したネットワーク図

成果

今回のCIの移行によって、CIは大きく改善しました。 ジョブの組み替えが容易になったことで、Jenkinsでは70-80分かかっていたCIが50分ほどに短縮されました。

今回はJenkinsからCircleCIへの移行について順を追って説明しましたが、 最初は何人かの人が個別に改善を行なっていました。 最終的にはそれが組み合わさって良い形の改善へとまとめることができ、よかったと思います。 今後も改善を続けていきたいと思います。