本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーは、はてなでマンガビューアを開発しているhitode909さんで、テーマは「依存モジュールの更新」です。
長期間にわたって開発・運用するアプリケーションでは、依存モジュールを管理し、最新版に更新することが重要です。本稿では、アプリケーションの依存モジュールの管理方針のグッドプラクティスと、ツールを使って継続的にモジュールを更新するための手法を紹介します。
本稿のサンプルコードは、執筆時点(2020年11月)の最新であるPerl 5.32.0で動作確認を行っています。本稿のサンプルコードは、WEB+DB PRESS Vol.120のサポートサイトから入手できます。
Perlのアプリケーションでの依存モジュール管理
まず、PerlアプリケーションにおけるCartonを使った依存モジュールの管理について簡単におさらいします。
Cartonを使ったモジュール管理
Cartonは、CPANモジュールの依存管理ツールです。Node.jsにおけるnpm(Node Package Manager)やyarn、RubyにおけるBundlerと同種のツールで、Perlを使ったアプリケーション開発ではデファクトスタンダードです。
Cartonがモジュール管理のために利用するファイルがcpanfileとcpanfile.snapshotです。
そのうち、開発者が主に触ることになるのはcpanfileです。cpanfileには、アプリケーションが要求するモジュールと、バージョンの範囲を記します。
cpanfileを用意したのちにcarton install
を実行すると、cpanfileでの指定をもとに依存モジュールがインストールされ、cpanfile.snapshotが生成されます。cpanfile.snapshotには、インストールされたモジュールとそのバージョンが書き込まれます。
上記のcpanfile.snapshotではList::MoreUtils
部分のみを抜粋しています。「List-MoreUtils
の0.418以上」という要求をもとに、インストール当時のCPANでの最新版であった、バージョン0.419がインストールされたことが記録されています。
このようにCartonは、cpanfile.snapshotが存在しない場合や、cpanfileとcpanfile.snapshotの状態がずれている場合に、CPANに公開されているバージョンからcpanfileの要求に合うバージョンを探索してインストールします。2ファイル間の状態がそろっている間は、再度carton install
を実行しても結果は変わりません。
本番環境の更新がゴール
アプリケーションの依存モジュールを更新するゴールは、本番環境のアプリケーションを、新しいモジュールとともに期待どおりに動作させることです。依存モジュールの更新時には、更新前後の2つのバージョンが登場するほか、どこまで更新するかを決めるためにCPANで公開されているモジュールのバージョンを確認したり、本番環境で稼働しているバージョンを確認したりと、さまざまな要素が登場します。
冒頭のcpanfileでのList::MoreUtils
について状況を整理すると、次のようになります。
- 本稿執筆時において、CPANで公開されている最新版は0.428である
- cpanfileでは、0.418以上という範囲を要求している
- cpanfile.snapshotには、cpanfileでの要求範囲を満たす0.419が記録されている
- 本番環境のサーバには、cpanfile.snapshotの指定に基づいて0.419がインストールされている
本番環境のサーバで動いているバージョンは0.419ですが、CPANの最新バージョンは0.428ですので、古びている状態です。cpanfileやcpanfile.snapshotを編集し、本番環境のバージョンを0.428まで上げることができれば、List::MoreUtils
については更新成功となります。
その際に、本番サーバを直接操作して新しいモジュールをインストールするのではなく、まずはcpanfileでの指定や、cpanfile.snapshotに記録されたバージョンを書き換え、それらのファイルの指示に従って本番環境に新しいバージョンをインストールするのが、Cartonで管理されたアプリケーションのモジュール更新の流儀となります。
最新のモジュールを使うことの重要性
ここでは、最新のモジュールを使うことの重要性を説明します。
新しいことは良いこと
モジュールが新しくリリースされる理由は、不具合の修正、新機能の追加、パフォーマンスの改善など、モジュールの利用者にとって喜ばしいものばかりです。モジュールの不具合修正やパフォーマンスの改善は、そのままアプリケーションの不具合修正やパフォーマンスの改善につながることが期待されます。
このようにモジュールの更新は、モジュールの利用者である開発者だけでなく、アプリケーションのユーザーにとってもメリットがあります。「新しいモジュールは良いモジュール」ということで、どんどん更新しましょう。
脆弱性はいつ発見されるかわからない
利用中のモジュールに脆弱性が発見され、脆弱性の修正がリリースされた場合には、アプリケーション側ではすみやかな更新が要求されます。このとき、アプリケーションが古すぎるモジュールを使っていると、更新に手間どり、脆弱性の対処に時間がかかる恐れがあります。したがって、ちょっとした更新であっても、モジュールは日ごろからこまめに更新しましょう。
モジュールの管理につきまとう問題
次に、モジュールの管理をどう実現するかを考えます。モジュールの管理にあたっては、cpanfile.snapshotをGitなどのリポジトリに含めてバージョン管理するかどうかが大きな判断ポイントとなります。
cpanfile.snapshotをバージョン管理しない手法は、CPANに公開されるようなモジュールを作っているときに採られる方式です。cpanfileだけをリポジトリにコミットします。インストール時に、cpanfileの要求範囲内で最新のモジュールがインストールされます。
cpanfile.snapshotをバージョン管理する手法は、アプリケーションを作っているときに採られる方式です。cpanfile、cpanfile.snapshotともにリポジトリにコミットします。cpanfile.snapshotに記載された固定のバージョンのモジュールがインストールされます。
それぞれのメリット、デメリットを説明します。
snapshotを管理しないと本番一発勝負となる
cpanfile.snapshotをバージョン管理しない場合、cpanfileでの要求範囲から、CPANで公開されている最新のモジュールがインストールされます。
最新のモジュールがインストールされると聞くと、自動で更新されて便利そうにも思いますが、これには問題があります。
アプリケーションのデプロイ時に最新のモジュールをインストールするので、本番環境のアプリケーションが新しいモジュールとともに期待どおりに動くかどうかは、デプロイするまでわかりません。つまり、本番環境での一発勝負となります。最新のバージョンでの動作に問題があった場合、バージョンを戻そうにも、どのモジュールをどこまで戻せばよいかを調べるのは困難です。
以上の理由で、アプリケーションを開発している場合、この方式はお勧めしません。
ただし、モジュールを開発している場合は、この方式を採ります。モジュールを開発しているときは、後方互換性を重視し、なるべく幅広いバージョンのモジュールとともに動くべきです。そのため、cpanfileでのバージョン指定には、問題なく動作する限り古いバージョンを指定します。詳しくは、本連載の第50回「Minillaを使ったモダンなCPANモジュール開発」を参考にしてください。
snapshotを管理すると更新が必要となる
cpanfile.snapshotをリポジトリに含めておけば、インストールすべきバージョンがリポジトリで管理されます。そのため、本番環境で動かしたいバージョンを使って事前に自動テストを動かしたり、動作確認を行ったりと、リリース前の準備ができます。安全な更新を行える点で、本稿ではこの方式をお勧めします。
ただし、このままではインストールされるモジュールは常に固定となってしまいます。モジュールの更新は開発者の仕事となるので、手作業や、のちに紹介するツールなどを使って、モジュールを最新に追従する方法を考える必要があります。
以降では、cpanfile.snapshotはリポジトリにコミットして管理するものとして話を進めていきます。
アプリケーションの依存モジュールの更新方法
アプリケーションの依存モジュールの更新手順は、cpanfile.snapshotを更新し、動作確認を行い、本番環境にデプロイするという3つのステップに分かれています。
snapshotを更新する
まずは、新しいモジュールがインストールされたcpanfile.snapshotを用意します。これには、cpanfileを手作業で編集する方法と、carton update
コマンドを使う方法があります。
cpanfileを手作業で編集する方法
cpanfileを書き換えて、新しいバージョンを要求することで、cpanfile.snapshotを更新できます。
まず、cpanfileに記録しているモジュールから、更新したいモジュールを選び、CPANに公開されている最新のバージョンを調べます。筆者はmetacpanを利用しています。
次に、cpanfileをテキストエディタで編集し、requires 'List::MoreUtils', '0.428';
のようにバージョンを変更して保存します。これは、「List::MoreUtils
の0.428以上を要求する」という意味です。==0.428
にすると、0.428ぴったりに固定という意味になります。編集によって、cpanfile.snapshotでの記録は、cpanfileでのバージョン要求を満たせない状態となりました。
続いて、carton install
コマンドを実行すると、Cartonコマンドは、cpanfileでの要求を満たす形でCPANから新しいバージョンのモジュールをインストールし、cpanfile.snapshotにインストールされた新しいバージョンを記録します。cpanfileとcpanfile.snapshotの両方が新しいバージョンを指し示し、2ファイル間の整合性が保たれた形になったので、これら2ファイルをリポジトリにコミットします。
carton updateを使う方法
Cartonのサブコマンドであるcarton update
コマンドは、cpanfileに指定しているモジュールを、バージョン要求の範囲内で更新してインストールし、cpanfile.snapshotを書き換えます。
carton update
コマンドはcpanfileを書き換えず、cpanfileでのバージョン指定を満たす範囲での更新が行われます。'JSON::XS', '<= 3.00'
のように上限のある指定や、'JSON::XS', '== 3.00'
のように完全一致でのバージョンを指定している場合には、CPANで公開された最新バージョンではなく、cpanfileでの指定を上限とした更新になります。
すべてのモジュールがまとめて最新化されるので更新手順としては簡単ですが、変更が多岐にわたると、どこに着目して動作確認すればよいかわかりづらい、という扱いづらさがあります。
前項の手作業での更新のようにモジュールを一つずつ更新したい場合は、carton update List::MoreUtils
として特定のモジュールのみを更新することもできます。
更新によって書き換えられたcpanfile.snapshotは、リポジトリにコミットしましょう。
正しく更新できたか動作確認する
新しいバージョンのモジュールをインストールした環境で、自動テストを動かしたり、実際に手もとや開発環境などでアプリケーションを動かしたりして、挙動を確認します。
CI(Continuous Integration、継続的インテグレーション)環境を用意している場合は、新しいモジュールで自動テストが通ることを確認してからマージできるような開発フローを用意しておくと便利です。
本番環境へのデプロイ
こうして更新の用意ができたら、アプリケーションのデプロイ時に新しいモジュールをインストールします。
carton install --deployment
を実行すると、cpanfile.snapshotに記録されたバージョンのみに従ってモジュールをインストールします。モジュールのインストール後、本番環境のアプリケーションを再起動すれば、本番環境でのモジュールの更新の完了です。
<続きの(2)はこちら。>
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT