Renovateは依存関係の更新を自動化してくれるツールです。本記事ではRenovateの設計思想を知ることで、Renovateをより上手に運用できるようになることを目指します。
Renovateとは
Renovate はMend社 (旧WhiteSource社) が開発するフリーの依存関係更新ツールです。package.jsonなどに書かれたライブラリバージョンが古い場合に、それらのバージョンを新しくするような変更をpull requestとして自動的に作成してくれます。
Dependabot などが競合として知られています。
ソフトウェアとサービスの概要
ソフトウェアとしてのRenovateはAGPLでライセンスされており、renovatebot/renovateから入手できます。 (AGPLというと身構えてしまいそうですが、これは多くの利用シーンでは問題にはなりません。 AGPLを理解する: もっとも誤解されたライセンス などを参考にしてください。)
ユーザーはRenovateを自身で実行してもよいですし、Mend社自身がホストしている GitHub Renovate App を使うこともできます。このホステッドサービスも無償です。具体的なセットアップ方法については Running Renovate - Renovate Docs を参照してください。
Renovateが気に入ったら、Mend社の有償サービスを検討してみてもよいかもしれません。
アーキテクチャ上の特徴
ここからが本題です。Renovateのアーキテクチャ上の特徴には以下のようなものがあります。
- 多様な更新シナリオに対応するために、モジュラーなアーキテクチャになっている。
- 設定ファイルの表現力が高く、設定項目も非常に多い。
- 羃等なジョブとして実現されている。
- 専用のデータストアを持たない。
- 更新対象のリポジトリ (GitHub等) に問い合わせることで状態管理を行う。
- ファイルの書き換えはRenovateと外部ツールチェインの併用によって行われる。
- package.json, Gemfile などのソースファイルはRenovate自身によって書き換えられる。
- yarn.lock, Gemfile.lock などのロックファイルは各言語に固有のツールチェインを呼び出すことで更新される。
これらを意識することで、Renovateを今までよりもうまく運用できるようになるでしょう。これから各特徴を説明しながら、様々な活用法を紹介していきます。
モジュラーなアーキテクチャ
Renovateは以下の4つの役割を持ったモジュールが連携することで更新を行います。
- Platform ... 更新対象リポジトリ (GitHub等) に情報を問い合わせ、最終的な結果 (pull request) を書き込む。
- Manager ... 依存バージョンが書かれたファイル (package.json等) の読み書きを行う。
- Datasource ... 利用可能なバージョンをレジストリ (npm等) に問い合わせる。
- Versioning ... バージョンにあわせてバージョン制約を更新する。
これらはある程度組替えが効くように設計されています。とりわけ、ManagerとDatasourceは多対一の関係になりやすいので、意識しておくとより正確な設定をすることができるでしょう。
たとえば dockerというDatasource を考えます。これはDocker HubをはじめとしたDocker registryからタグの一覧を問い合わせる役割を担っています。
このdocker datasourceの代表的な使い道は Dockerfile manager との組み合わせです。Dockerfile managerの仕事は Dockerfile
というファイルを探し、FROM命令のイメージ名を読み出したり書き戻したりすることです。
しかし、docker datasourceにはそれ以外の使い道もあります。たとえば circleci manager はCircle CIの設定ファイルである .config/circleci.yml
の読み書きを担当しますが、この中でdocker imageが参照されている場合はdocker datasourceに問い合わせることになります。
このようにManagerとDatasourceは必ずしも一対一対応しないということを意識しておくと、よりよい設定を書くことができるでしょう。
regex managerを有効活用する
このモジュラー性を最大限に活かしているのが regex manager です。regex managerは非常に強力な機能で、Renovateがサポートしていないファイルでもうまく設定すれば依存の更新ができるというものです。
たとえば、Wantedlyでは多くのリポジトリのDockerfileで以下のようにbundlerをインストールしていました。
ENV BUNDLER_VERSION 2.4.8
RUN gem install bundler -v "$BUNDLER_VERSION"
またBundlerのバージョンはGemfile.lockにも書かれます。
BUNDLED WITH
2.4.8
これらはどちらもregex managerを適切に設定することで検出できます。また、bundlerはrubygemsにpublishされているため、rubygems datasourceを使えばバージョン情報の取得も可能です。一見すると自動更新が難しそうなケースですが、regex managerで解決できてしまいます。
他にも、unpkgなどのCDNでホストされているJavaScriptライブラリのバージョンを更新対象に含めるといった設定も可能です。
設定ファイルの表現力
Renovateの設定ファイルは非常に高い表現力を持っています。それは以下の2つの点に分けられます:
- 設定項目数自体が非常に多い。
- 設定の共通化・抽象化のための仕組みが備わっている。
設定項目数が多い分目当ての設定を探すのは大変ですが、その分欲しい設定項目が存在する確率は高くなると思って頑張って設定していくしかないでしょう。
重要なのは設定の共通化・抽象化のほうです。
Renovateの設定ファイルはそれなりに表現力が高く、条件を書いたり他の設定ファイルを参照したりできます。ループこそありませんが、どちらかといえばプログラミング言語に近いものを扱っているつもりで考えたほうがいいかもしれません。
この観点から見ると、Renovateの設定ファイルは関数のように何度も繰り返し評価されるものだとみることができます。
- はじめは特に条件の指定もなく、デフォルト設定がそのまま出力されます。たとえば「ダッシュボード用のissueを作るべきか否か」「どのブランチに対して更新を行うべきか」などはリポジトリグローバルな設定です。
- 次に、各パッケージごと (バージョンごと) に設定ファイルを再評価します。このときはpackageRulesの判定によって設定ファイルの出力が変わります。
実のところ、ほとんどの設定項目はパッケージごとに設定できます。特定の名前をもつパッケージだけレビュアーを変えたり、プロダクション依存だけPRの作成スケジュールを変えたりなど、自由自在です。
また、共通設定ファイルを作れることも重要です。Wantedlyでは各マイクロサービスを異なるリポジトリからデプロイしていることもあり、非常に多くのリポジトリを管理する必要があります。そこで、 wantedly/renovate-config という共通設定ファイル用のリポジトリを作り、Renovateの共通設定は中央管理しています。さらに共通設定ファイルも、各リポジトリのニーズにあわせていくつか用意しています。以下はその例です。
- base.json ... 全てのリポジトリで入れてよい共通設定。
- 社内ライブラリにアクセスするためのクレデンシャルもここに置いている。
- js-lib.json ... JavaScriptのライブラリパッケージ用の設定。
- ライブラリでは依存の制約を狭めるよりも広げるような設定のほうが望ましい場合があるため。
- js-automerge.json ... JavaScriptのパッケージをautomergeするための設定。
- 依存をautomergeするかどうかはサービスの性質によって異なるため。
羃等でステートレス
Renovate自身は状態を持ちません。そのかわり、GitHubであればGitHubのリポジトリ上の情報を用いて、自身が次にするべきことを決定しています。
どのように状態管理をしているか
たとえば、RenovateはPRを作る前に同等のPRが過去に存在していないかを確認します。もし同等のPRがある場合、以下の2つのパターンがありえます。
- openだった場合 …… PRを新規作成せず、そのPRを更新する。
- closed (merged / unmerged) だった場合 …… PRは作らず、更新を諦める。
この条件分岐はGitHub上にすでにある情報だけで実現可能です。そして、これにより以下のようなケースでユーザーの期待に沿った挙動を実現できることになります。
- もしユーザーがRenovateのPRを手動でクローズした場合、Renovateが同じ変更を再度作り直すことはない。
- ユーザーがPRをクローズしたということは、ユーザーは当該変更をすぐにはマージできない事情があると考えられる。
- もしユーザーがRenovateのPRをマージしたあと、何らかの事情でそれをrevertする必要があった場合、Renovateが同じ変更を再度作り直すことはない。
- ユーザーがPRをrevertしたということは、ユーザーは当該変更をすぐにはマージできない事情があると考えられる。
一方、ユーザーの意思によらずにRenovateのPRを閉じる必要がある場合もあります。たとえば、別の変更によって当該依存関係が更新されたり、依存が削除されるなどして更新が不要になった場合がそれにあたります。このような場合、RenovateはPRのタイトルを変更してからクローズします。これによりRenovateは手動でクローズされたPRと自動でクローズされたPRを識別し、適切な挙動を選択できるようにしています。
ステートレスであることで注意が必要な例
この仕組みにより、ユーザーはRenovateの状態を完全に制御下に置くことができます。全ての情報はGitHub上にあるため、Renovateがどうしてそのような挙動になっているかはGitHub上の情報だけから推測できますし、必要であればissueやPRに手を加えることでRenovateの挙動を変えることもできます。
ただし、GitHubから情報を取得する都合上、取得漏れによって期待しない挙動になる余地は残されています。たとえば、あるPRが手動でクローズされたという記録は、開発が進むにすれて検索から漏れやすくなり、やがてRenovateはその情報を検出できなくなることがあります。
そのため、Renovateに対して行ったアクションは全てが永久的に記録されるというよりは、検索から漏れることで少しずつ忘却されるという前提でいるほうがよいでしょう。たとえば、automergeが有効なパッケージの更新で意図せず障害が起きたとき、その変更をrevertすれば一時的にRenovateの更新は止まります。しかし、その状態を長く放置するのは危険なので、根本対応をするなり設定でautomergeを止めるなりの黄道を取ったほうがよいでしょう。
羃等なジョブ
Renovateは1時間に1回などの適当な頻度で実行されるスケジュールジョブとして実現されています。設定を書くときは、Renovateがどのタイミングで実行されるかに依存しない書き方をすることになります。
このことが影響するわかりやすい例はPRの作成スケジュールの設定項目です。競合のdependabotでは以下のように設定を書きます。
# .github/dependabot.yml
updates:
- schedule:
# ある一瞬を指定する
time: "09:00"
一方、Renovateでは以下のように設定を書きます。
// renovate.json
{
// 時刻の範囲を指定する
// 言い換えると、時刻に対する述語を指定する
"schedule": ["after 10pm and before 5:00am"]
}
この設定はRenovateの起動時刻を指定しているのではなく、Renovateが起動したときにたまたまこの時刻の範囲内だったらPRの作成を許可するというものです。
副次的な効果として、scheduleもパッケージごとに指定できることになります (※なるはずですが実際に確認まではできていません)
ファイルの書き換えはハイブリッド方式
Renovateはパッケージファイル (package.json など) とロックファイル (yarn.lock など) に対して異なるアプローチを取っています。
- package.json などのファイルは元々人が書くことを想定しているファイルであるため、Renovate自身のロジックで書き換えを行います。
- 一方 yarn.lock などのファイルは機械出力であり完全性が重要になります。下手に本来のツールチェインと異なる出力を与えることは問題にもなるし、そもそも出力がパッケージマネージャーの挙動と深く結びついていて解決が難しい場合もあるため、ロックファイルは実際に使われるツールチェイン (yarn.lockであればyarn、Gemfile.lockであればbundlerなど) を呼び出すことで更新します。
これに関しては、競合であるdependabotでも同様の設計になっています。
Renovateがpackage.jsonの書き換えに成功しても、呼び出されたツールチェインがlockfileの更新に成功するとは限りません。たとえば以下のようなケースが考えられます。
- Bundlerなどパッケージ管理システムによっては、制約同士の矛盾によって解決が不可能な場合があります。この場合はGemfile側では更新に成功しているように見えても、Gemfile.lockがうまく生成できないことになります。
- クレデンシャルの設定に由来する問題。社内ライブラリの取得などのためにクレデンシャルを設定する場合、Renovate自身がパッケージ情報を正しく問い合わせできることと外部ツールチェインがパッケージ情報を正しく問い合わせできることの両方を担保する必要があります。通常、Renovate側がうまく設定を解釈・伝播できていれば問題は起きませんが、設定によっては外部ツールチェイン向けの設定がうまく行われず、lockfileの生成に失敗することが考えられます。
このような場合はlockfileが不整合な状態でPRが作られることになります。lockfileが不整合な状態で変更をマージすると再現性に悪影響があるため、通常はCIで bundle install --locked や yarn install --immutable などのコマンドを使うことでlockfileが最新であることを確認しているはずです。そのため、lockfileが不整合な状態で作られたPRはそのままではマージできず、自動化の意義の大部分を削いでしまうことになります。特にクレデンシャルが由来の場合は、根本原因を対応するか、対応できるまではRenovateの設定をオフにすることが必要になるかもしれません。
まとめ
Renovateの特徴的な設計について紹介しました。また、これらを理解することで実際にどのように運用が改善できるかの例も示しました。
もちろんこれだけでRenovateを効果的に運用できるようになるわけではなく、設定一覧を読み込んだり、既存のpresetなどからアイデアを得るなどの積み重ねでよりよい運用ができるようになるはずです。また対象領域によってはRenovate自身の実装が不十分な面もあり、Renovate本体に貢献することでできる改善も沢山あります。
本記事で紹介したことをヒントにしながら、効果的に依存関係を更新できる仕組みをぜひ作ってみてください。
おまけ: dependabotとの比較
ここまでの議論も踏まえつつ、Renovateとdependabotの比較も行います。
- セキュリティー
- dependabotはサプライチェーンセキュリティーを意識してか、一部機能が意図的に制限されている。
- 具体的にはGitHub Actionsに設定された秘密情報が入らないようになっていたり、automerge機能が削除されていたりする。
- (機械がやっても人間がやってもどうせ碌にチェックされずにマージされるんだからたいして変わらないんじゃないかと思わなくもない)
- 言語サポート
- dependabotはRubyで書かれている。開発者の関心もRubyに向いている傾向が強く、Rubyのサポートは厚め。
- RenovateはJavaScript (TypeScript) で書かれている。開発者の関心もJavaScriptに向いている傾向が強く、JavaScriptのサポートは厚め。逆にRubyは実装が色々足りていないので貢献の余地がある。
- 機能
- dependabotは簡潔で少ない機能をうまくやる。また、設定項目も最低限に抑えられている。
- Renovateは多くの機能をサポートし、カスタマイズ性も非常に高い。
- ただし、これらの全体傾向には例外もある。
- たとえば、Renovateは現在のところ間接依存の更新には弱い。npmの脆弱性更新に対してのみ実験的にサポートがある。
- dependabotは脆弱性アラートの機能が統合されていて、脆弱性情報のあるパッケージは別プロダクト (dependabot security updates) の機能として優先的に更新される。ただし、Renovateは自身で脆弱性アラートの機能は持たないものの、dependabotの脆弱性情報を参照することはできるようになっている。
- 構成
- dependabotはステートフルなサービスとして設計されている。
- Renovateはステートレスな同期バッチとして設計されている。