ハウテレビジョンブログ

『外資就活ドットコム』『Liiga』『Mond』を開発している株式会社ハウテレビジョンのブログです。

vs. 依存関係アップデート

Renovate - GitHub
Renovate

はじめに

こんにちは、新型質問箱 mond の開発チームに所属している id:chima91 です。 今回は、プロダクトにおける依存関係のアップデートとその自動化について語ろうと思います。 mond では昨年末に依存関係の一括アップデートを行い大変な思いをしましたが、その後アップデート自動化を導入することなく半年経ってしまいました。このままでは昨年末の一括アップデートによる謎のエラー発生を繰り返す恐れが大きいため、自動化を進める決意をした次第です。

依存関係を定期的にアプデしたほうが良い理由

依存関係の自動更新をしてくれる GitHub App である Renovate のドキュメントを引用しながら、定期的にアプデする理由を考えたいと思います。

docs.renovatebot.com

Regular updates tend to be small

  • アップデートの変更履歴は小さく、すぐに読めて理解しやすい。PR をマージするために必要な変更があったとしても数か所だけである可能性が高い。
  • 変更履歴を定期的に読むことで、ライブラリにおける最新の方向性を感じ取ることができる。

Applying major updates is easier

  • 最新のベストプラクティスに従ったり、関数や変数の最新の名前を使用したりすることで、メジャーバージョンへのアップデートが容易となる。

You'll be ready for CVE patches

  • 最新バージョンがクリティカルな CVE (Common Vulnerabilities and Exposures, 共通脆弱性識別子) のパッチをリリースしたときにすぐに対応することができる。

You'll look for ways to automate the updates

  • 頻繁にアップデートをするようになると、アップデートを自動化する方法を探し始める。

どうやらメリットしかなさそうです。 特に、アップデートの変更履歴が小さくてマージしやすく、メジャーバージョンへの移行も容易となる点は個人的に大きな利点だと感じています。 また、CVE のパッチがリリースされた時には可能な限りすぐに適用し、セキュリティを向上させたいものです。 では、どうして依存関係のアップデートを後回しにしてしまうのでしょうか。

依存関係のアプデをサボってしまう理由

この理由も Renovate のドキュメントにまとめられています。

docs.renovatebot.com

Developers get blamed when things break in production

  • 本番で障害が発生した時に開発者が責められる。

There are no tests, so merging updates is scary

  • テストがないので依存関係のアップデートをマージするのは怖い。

The test suite is slow

  • テストスイートの実行に時間がかかる。

Releasing a new version of the project must be done by hand

  • プロジェクトの新しいバージョンのリリースは手動で行わなければならない。

Updating must be done by hand

  • アップデートは手動で行わなければならない。

The company doesn't allow developer time for updates

  • 会社が開発者にアップデートの時間を与えない。

The company has complex rules about updates

  • 会社におけるアップデートのルールが複雑である。

たくさんありますね。 一部はアップデートの自動化で解決できそうです。 ただ、依存関係アップデートの時間が与えられない点やアップデートに関する複雑なルールが会社にある点については、チームにおける開発者以外のメンバーの理解・協力が必要となりそうです。 この度、チームで相談して時間を得ることに成功したので、依存関係のアップデートを進めていきます。

手動でのアップデート

今回は主に npm パッケージのアップデートをしていこうと思います。

npm outdated

まずは、古くなっているパッケージを一覧で確認します。

docs.npmjs.com

This command will check the registry to see if any (or, specific) installed packages are currently outdated.

npm outdated [<package-spec> ...]

ずらりと古くなったパッケージたちが現れました。

npm outdated の実行結果
npm outdated の実行結果

これの見方としてはざっくりこのような感じです。赤文字のパッケージが多いですね^^;

  • Wantedpackage.json で指定されている semver*1 の制約を満たす最大のバージョン
  • Latest: レジストリにて最新とタグ付けされているバージョン
  • Location: パッケージが物理ツリーのどこにあるか
  • Depended by: どのパッケージに依存されているか
  • 赤文字: semver を満たす新しいバージョンがあり、すぐアップデートするべきパッケージ
  • 黄文字: semver 以上の新しいバージョン があり、注意して対応するべきパッケージ

npm update

次に、パッケージのアップデートを行います。

docs.npmjs.com

This command will update all the packages listed to the latest version (specified by the tag config), respecting the semver constraints of both your package and its dependencies (if they also require the same package).

npm update [<pkg>...]

使いそうなオプションとしては下記のようなものがあります。

  • dry-run: その名の通りドライラン。実際には実行しないが、実行した場合の結果を表示する。
  • save: インストールしたパッケージを package.json に依存関係として保存する。

かなりたくさんの更新が package.json に書き込まれました。

> npm update --save
added 334 packages, removed 875 packages, and changed 661 packages in 3m

npm audit

最後に、パッケージの脆弱性監査と修正を行います。

docs.npmjs.com

The audit command submits a description of the dependencies configured in your project to your default registry and asks for a report of known vulnerabilities. If any vulnerabilities are found, then the impact and appropriate remediation will be calculated.

npm audit [fix|signatures]

クリティカルな脆弱性もあるみたいですね…

> npm audit
# npm audit report
...
34 vulnerabilities (2 low, 13 moderate, 10 high, 9 critical)

監査後に自動で修正できる分については下記コマンドで修正可能です。package-lock.json に差分が出ました。

> npm audit fix
...
added 168 packages, and audited 7227 packages in 13s
...
31 vulnerabilities (2 low, 14 moderate, 10 high, 5 critical)

Renovate の導入 (自動化)

ある程度、npm パッケージのアップデートができたところで、いよいよ依存関係アップデートの自動化を進めます。 自動化に際して導入するのは、先ほどから引用させていただいている Renovate です。以下のような流れで依存関係の更新 PR を自動で作成してくれます。

  1. The repository has a package.json and package-lock.json with version 1.0.0 of a dependency (バージョンが 1.0.0 の依存関係があるとする)
  2. Renovate sees that version 1.1.0 is available (Renovate がバージョン 1.1.0 が利用可能であることを確認する)
  3. Renovate patches the package.json to change the dependency's version from 1.0.0 to 1.1.0 (Renovate が package.json に手を加え、依存関係のバージョンを 1.1.0 に変更する)
  4. Renovate runs npm install to let npm update the package-lock.json (Renovate は npm install を実行し、package-lock.json も更新する)
  5. Renovate commits the package.json and package-lock.json (Renovate が package.jsonpackage-lock.json の差分をコミットする)
  6. Renovate creates the PR (Renovate が 更新 PR を作成する)

Renovate はリポジトリにある CI/CD コンフィグや IaC ファイルのような DevOps 関連のファイル (Docker、Kubernetes、Terraform) も "パッケージマネージャ" として扱い、それらを見つけて更新することも可能です。

インストールと初期設定

公式ドキュメント を参考にインストールします。 インストールが完了すると、自動で初期設定用の PR を作成してくれます。

Renovate 初期設定用 PR
Renovate 初期設定用 PR

Welcome to Renovate! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin. 🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged.

Renovate を有効化するには、この PR をマージするだけで OK です。

デフォルトの設定 (renovate.json) では、以下のように依存関係を更新する PR が作成されます。

  • 依存関係ごとに別々の PR となる
  • メジャーアップデートとそれ以外のアップデートは別々に管理される

先に述べた通り、npm update 等を実行していくつかアップデートしたもののそれだけでは足りないらしく、初期設定 PR をマージした後、たくさんの更新 PR を作ってくれるようです。

Renovate will create 67 Pull Requests
Renovate will create 67 Pull Requests

Dependency Dashboard

Renovate には Dependency Dashboard という機能が備わっています。 これを有効にしておくと、 すべての依存関係のアップデートに関するステータス (保留中, 対応中, 以前にクローズしたもの) をダッシュボードとして閲覧できる issue をリポジトリに作ってくれます。 また、選択した依存関係の新しいアップデートを承認するワークフローも可能となります。 設定ファイルに下記の文言を追加するだけで有効化できます。

{
  "extends": ["config:recommended", ":dependencyDashboard"]
}

ちなみに、設定ファイルに問題があるかどうかは下記のコマンドで確認できます。

設定ファイルに問題がある場合
> npx -y -p renovate -- renovate-config-validator
 INFO: Validating renovate.json
ERROR: Found errors in configuration
       "file": "renovate.json",
       "errors": [
         {
           "topic": "Configuration Error",
           "message": "Invalid configuration option: hogehoge"
         }
       ]
設定ファイルに問題がない場合
> npx -y -p renovate -- renovate-config-validator
 INFO: Validating renovate.json
 INFO: Config validated successfully

そして、破壊的変更を含みがちなメジャーアップデートについては、ユーザーが承認するまで更新 PR を作らないように設定することもできます。

{
  "major": {
    "dependencyDashboardApproval": true
  }
}

最終的な設定ファイル

最終的に、renovate.json は以下のようになりました。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended", ":dependencyDashboard"],
  "major": {
    "dependencyDashboardApproval": true
  },
  "timezone": "Asia/Tokyo",
  "schedule": ["after 10am and before 3pm every weekday", "every weekend"],
  "packageRules": [
    {
      "description": "Grouping eslint-related dependencies",
      "matchPackagePatterns": ["eslint"],
      "groupName": "eslint"
    },
    {
      "description": "Automerge patch updates (exclude minor and major updates)",
      "matchUpdateTypes": ["patch"],
      "automerge": true
    }
  ]
}

初期設定から以下の変更を行いました。

  • Dependency Dashboard の有効化
  • 更新 PR が作られる時間帯の指定
  • 依存関係の名前に eslint が含まれるものはまとめて1つの PR として作成するグループ化の設定
  • patch バージョンの更新は PR 作成後に自動でマージする設定

これらの設定により、いきなり67個もの更新 PR が作られることはなく、まずは2つの PR が作成されました。

おわりに

時間はかかりましたが、無事に Renovate を導入することができ、依存関係の更新 PR が自動で作成されるようになりました。

最初に自動で作成された更新 PR
最初に自動で作成された更新 PR

良かったこと

  • 今までサボっていた依存関係のアップデートを自動化できた
  • 依存関係における脆弱性を低減させることができた

今後取り組みたいこと

Renovate のドキュメントは分量が多いですが、依存関係の自動アップデートに対する習熟度に応じてユーザーを3つのレベル (Beginners, Intermediate, Advanced) に分け、それぞれが読むべき箇所を分類してくれています。ざっと Intermediate までは読めたので、Advanced の部分も読みつつ以下を進めていこうと思います。

  • patch バージョンに加え、minor バージョンの自動マージ
  • リポジトリ・プロダクト間での設定ファイルの共有
{
  "extends": ["config:recommended", "config:howtelevision"]
}

社内専用のプリセットを1つ作成し各プロダクトで利用することで、依存関係アップデート自動化に関する運用の知見を共有できたり、プロダクト間における異動の負担を減らしたりできるのではないかと思います。

以上、長くなりましたが、依存関係アップデートとの闘いでした。

P.S. 弊社では mond だけでなく、外資就活や Liiga でも エンジニア職やビジネス職を絶賛募集中 です!まずはカジュアル面談からでも大丈夫ですので、ぜひご検討のほどよろしくお願いいたします。

*1:Semantic Versioning の略。APIの変更に互換性のない場合はメジャーバージョンを、後方互換性があり機能性を追加した場合はマイナーバージョンを、後方互換性を伴うバグ修正をした場合はパッチバージョンを上げるルールのこと。