バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

「常に最新」を保つための、少人数でも継続できるライブラリ管理戦略

「常に最新」を保つための、少人数でも継続できるライブラリ管理戦略

はじめに

こんにちは、テクノロジー戦略本部 開発2部の松下です。 私は今年の4月に新卒としてバイセルに入社し、内定者インターンの頃から現在までオークションシステムの開発・運用に携わっています。

今回は、私たちのチームが少人数でも「常にライブラリが最新の状態」を維持するために取り組んでいる運用戦略についてご紹介します。

ライブラリのアップデートは重要でありながら、後回しにされがちな作業です。 しかし、その放置は大きな技術的負債となり、将来的な開発効率やセキュリティに深刻な影響を及ぼします。 この記事が、アップデート戦略の策定に悩んでいる方や過去に運用がうまくいかなかった方にとって、有益な情報となれば幸いです。

前提

私たちが開発している タイムレスオークション は、フロントエンドにNext.js、バックエンドにRuby on Rails(以下Rails) を採用したWebアプリケーションです。 リアルタイム性が求められるオークションシステムであるため、高い信頼性とパフォーマンスが必要です。

チームでは、GitHubの Dependabot を活用し、フロントエンドとバックエンドそれぞれでライブラリのバージョン管理や脆弱性のチェックを行っています。

Dependabotとは、依存関係のアップデートを自動化したり、セキュリティの問題を通知してくれたりする便利なツールです。 しかし、その活用方法や運用戦略次第では、逆に管理が煩雑になったり、重要なアラートを見逃したりするリスクもあります。

そこで私たちのチームでは、Dependabotを効果的に活用し、少人数でもライブラリを最新に保ち続けるための戦略を策定し、実践しています。

課題とその背景

今年の4月頃のことです。 私たちのチームはサービス開始から約3年間、機能追加や改修のタスクを最優先に進めており、ライブラリのアップデートを後回しにしてきました。 その結果、何年もアップデートされていないライブラリが大量に存在し、技術的負債が積み重なっていました。

ライブラリが古いまま放置されると、以下のような問題が発生します。

  1. 脆弱性の問題

    • セキュリティリスクの増大:ライブラリによる最新の脆弱性対応が適用できておらず、攻撃者に狙われる可能性があります。

    • アラートの見落とし:本来優先的に対処すべき重要なアラートが他の多数のアラートに埋もれてしまい、見落とすリスクが高まります。

  2. 新機能が使えない

    • 開発効率の低下:最新のライブラリが提供する便利な機能やパフォーマンス改善の恩恵を得られず、コードの保守性やサービスの品質が損なわれます。

    • 車輪の再発明:既にライブラリで提供されている機能を自前で実装することで、時間とリソースを無駄に消費してしまうかもしれません。

  3. EOL対応が困難になる

    • 大量の作業負荷:Railsなど多くのライブラリが依存しているものの場合、それらを一斉にアップデートするためには多大な作業時間が必要となります。

    • 障害発生のリスク:大規模なアップデートはシステム全体への影響が大きく、障害発生の可能性が高まります。

    • インフラサポートの終了:サポートが終了したライブラリを放置すると、インフラ側のサポートも終わってしまい、突然デプロイできなくなる可能性があります。デプロイできないと、障害発生時に迅速な対応や切り戻しが困難になります。

さらに、長期間バージョンが更新されていないライブラリの中には、既にサポートが終了していたり、代替への移行を推奨しているものがあります。 これらは最新バージョンが公開されないため、Dependabotでの自動検知ができません。 開発者が定期的にライブラリの情報を追わないと、気づかないまま使い続けてしまうというリスクがあります。

チームでは当時、開発者の人数が減ることが確定しており、限られた人員でこれらの課題に対処し続けるのは困難な状況でした。 利用しているライブラリはフロントエンドとバックエンド合わせて約170個ありましたが、エンジニアの人数は2〜3人になる予定でした。

そこで、少ない人員でもライブラリを最新に保ち続けられるアップデート戦略を立て、運用することに決めました。 しかし、無理のある運用方法では再び放置されてしまう恐れがあります。 そのため、できる限り仕組み化された無理のない運用を目指し、以下のようなアプローチを取りました。

具体的なアプローチ

全ライブラリの一括アップデート

まず、数ヶ月にわたるスケジュールを策定し、全てのライブラリを最新バージョンにアップデートし切りました。 進める際には以下のポイントに注意しました。

段階的にアップデートすること

ライブラリを以下の3つの属性でグループに分け、段階的にアップデートを行いました。

  1. 開発環境やテスト環境にのみ影響するもの

    例:テストツール、リンターなど

  2. サービス全体に影響するもの

    例:フレームワーク本体、主要なライブラリなど

  3. 特定の機能にのみ影響するもの

    例:外部APIとの連携ライブラリ、特定の機能を提供するライブラリなど

アップデートの順序としてはまず、本番環境の挙動に影響しない「1」のグループを、CIテストだけを実施してサクッとアップデートしました。 続いて、依存関係の不整合を防ぎつつ円滑に作業を進めるため、依存関係の中核となる「2」のグループを先に対応し、最後に「3」のグループをアップデートしました。

段階的なアップデートにより、複数のライブラリをまとめてQAする際の確認範囲が明確になり、効率的に進められました。 また、もし障害が発生した場合も、原因を迅速に特定できます。

コンフリクトを抑制すること

複数の開発者が同時に作業するとコンフリクトが大量に発生する恐れがあったため、アップデート作業は基本的に1人の開発者が担当しました。

また、作業ブランチから別の作業ブランチを切るように進め、それらを順にマージすることでブランチ間のコンフリクトも抑制しました。 他のメンバーは優先的にレビューしたことで、この方針によるPull Request(以下PR) のマージ待ちもほぼ発生しませんでした。

これらの工夫をしつつ、すべてのライブラリを最新の状態にできました。 また、進める中で以下のような副次的な効果も得られました。

  • 利用ライブラリを精査する機会となった
    • 古くなったライブラリやサポートが終了したライブラリを、より優れた代替へ置き換えられた。
    • 使われていないライブラリは削除し、コードを綺麗にできた。
  • TODO系コメントの解消
    • 「アップデートしたら〇〇の機能を使う」といったコメントを解消できた。

ライブラリ管理方針のドキュメント化

次に、ライブラリ管理の方針をドキュメント化し、チーム全体で共有しました。 ここでは主に「マイナー・パッチ対応」「メジャー対応」「ライブラリの棚卸し対応」の3つを策定しています。

マイナー・パッチ対応

Dependabotはgroups設定を活用することで、通常はライブラリごとに1つずつ作成されるPRを、任意の粒度でまとめて作成させることができます。 私たちのチームでは「マイナー・パッチバージョンのアップデート」を1つにまとめており、基本的にはこのPRに対応します。

参考:docs.github.com

docs.github.com

差分の確認には、「破壊的変更が少ない & もし見落としてもQAでカバーできる」という前提のもと、 LLM(大規模言語モデル)を積極的に活用しています。 私はGPT-4oとClaude 3.5 Sonnetを使い、それぞれに対象ライブラリのリリースノートやChangelogを読んでもらいながらアップデートを進めています。 これまで問題は起こっておらず、安全性を高めつつ時間を短縮できているため、個人的にはおすすめしたい方法です。 ただし、見落としや問題が発生した場合は自己責任となりますので、ご注意ください。

作業の様子。2つのモデルへ同じ質問をすることで見落としを防いでいる(ライブラリは一例)

PRの内容に問題がなければQA環境へデプロイします。 QA環境では、毎月末に行っている「プレオークション」で全てをまとめてQAし、動作に問題がないことを確認してから本番環境へリリースしています。

プレオークションとは、以前からチームで行っていた運用で、主要な機能を開発者が手動でテストするものです。 オークションは月に2回しか行われず、障害が発生すると1回の損失が大きいため、事前に丁寧な確認をしています。 既存のプレオークションのフローにアップデート対応を組み込むことで、毎月必ず最新のマイナー・パッチバージョンを適用できるようになりました。

メジャー対応

DependabotによってメジャーバージョンのPRが作成された場合は、一旦チケットを作成し、後から個別に対応するようにします。 これは、メジャーバージョンのアップデートには破壊的変更やリリース後のIssueも多く、本番環境に影響するものは特に、慎重にアップデートの判断をする必要があるためです。 スプリントのプランニング時に、アップデートの緊急度・重要度をチームで話し合い、他のタスクとの兼ね合いを見て優先順位を決めて対応します。

ここで、Dependabotのデフォルト設定では、アップデートに関するPRは最大5個までしか作成されません。 アップデート対応のタイミングによっては上限数に達してしまうことがあり得るため、open-pull-requests-limit 設定を変更して、上限数を増やしています。

参考:docs.github.com

ライブラリの棚卸し対応

年に1度、年末に棚卸しのためのチケットが自動作成されます。 この作業では、「全ライブラリの最終更新日を古い順に出力するスクリプト」を実行し、更新日が古いライブラリを順に確認します。 古すぎるライブラリは、安定しているものがある一方で、既にサポートが終了していたり、移行を推奨している可能性も高いです。 READMEやIssueを確認し、もし問題があれば削除や移行を検討します。

以下にRailsプロジェクトで使用しているスクリプトと出力結果を示します(実際のコードとは少し異なります)。

【スクリプト】

# frozen_string_literal: true

require 'gems'
require 'bundler'

# Gemfile.lockのパース
def fetch_gem_names
  parser = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
  parser.dependencies.keys
end

# 各Gemの最新情報を取得
def fetch_gem_info(gem_names)
  puts '各Gemの情報を取得中...'
  gems_info = gem_names.map { |name| Gems.info(name) }
  puts '各Gemの情報を取得完了'
  puts
  gems_info
end

# 現在使用中のGemのバージョン情報
def fetch_current_installed_gem_info(parser)
  parser.specs.each_with_object({}) { |spec, res| res[spec.name] = spec.version }
end

# 最新バージョンのリリース日が古い順にソート
def fetch_gem_latest_version_info(gems_info)
  gems_info.map do |g|
    [g['name'], g['version'], g['version_created_at']]
  end.sort_by { |g| g[2] }
end

# 結果の表示
def display_gem_info(gem_latest_version_info, current_installed_gem_info)
  puts '====各Gemの最新versionの作成日(最終更新が古い順)===='
  puts(gem_latest_version_info.map do |name, latest_version, date_str|
    date = Time.parse(date_str).to_date
    days_since_published = (Date.today - date).to_i
    current_installed_version = current_installed_gem_info[name]
    format(
      '%-30<name>s: 最新version=%-10<latest_version>s(使用version:%10<current_installed_version>s) リリース日=%-10<date>s (%4<days_since_published>d日前)',
      name: name,
      latest_version: latest_version,
      current_installed_version: current_installed_version,
      date: date,
      days_since_published: days_since_published
    )
  end)
end

gem_names = fetch_gem_names
gems_info = fetch_gem_info(gem_names)
parser = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
current_installed_gem_info = fetch_current_installed_gem_info(parser)
gem_latest_version_info = fetch_gem_latest_version_info(gems_info)
display_gem_info(gem_latest_version_info, current_installed_gem_info)

【出力結果】 最終更新日の古い順でライブラリが出力されている様子

このようにライブラリ管理の方針を定めたことで、開発者が減少した現在でも、無理なく「多くのライブラリが最新の状態」を維持できています。 また、未対応のメジャーアップデートについても、各ライブラリの状態を把握し、計画的に対処する準備が整っています。 アラートも基本的に数件程度しか溜まらないため、見通しが良くなり、重要なものを見落とすリスクが減少しています。

今後の展望

今後は以下の点を進め、さらに効率的で安定した開発環境を築いていきたいと考えています。

  1. グルーピング設定の最適化

    例えばReactとNext.jsなど、同時にアップデートしたいライブラリのPRをグループ化することで、さらに効率を高めたいと考えています。

  2. 自動マージの導入

    本番環境に影響しないライブラリについては、GitHub Actionsを活用して自動でマージされる仕組みを作りたいと考えています。これにより手動でのアップデート対象が減り、負担の軽減が見込めます。

    設定に関しては、以下のページが参考になりそうです。

    docs.github.com

まとめ

ライブラリの最新化と適切な管理は、ソフトウェア開発における重要な課題です。 特に少人数のチームでは、効率的で仕組み化された運用が不可欠です。 私たちのチームが実践している戦略は、まだ改善の余地がありますが、一定の成果を上げています。

この記事が、同様の課題に直面している開発者の皆様にとって、参考となれば幸いです。

最後に、バイセルではエンジニアを随時募集しています。興味のある方はぜひ以下の採用サイトをご覧ください。

herp.careers