複数のDarkPANに依存してもCartonを使いたいッ!

by karupanerura | December 12, 2020
perl | #perl

この記事はDeNA Advent Calendar 2020の12日目の記事です。

こんにちは、@karupaneruraです。 今回は複数のDarkPANに依存するプロジェクトにおいてCartonを導入する上で障害になった課題とその解決策について書きます。

なお、この記事全体的にですが、これらは公式の説明ではなく基本的に著者である自分自身の理解をもとに説明を書いています。 出典の参照が可能な部分にはリンクとして参照を書きますが、紹介している各モジュール作者とは見解が異なる場合がありますのでご了承ください。 もし、間違いや出典の不足等にお気づきの場合は@karupaneruraまでご連絡を頂けますと幸いです。

DarkPAN

DarkPANとはCPANと同様のインターフェースを備えたCPANのように振る舞うPerlモジュールパッケージの中央集権的なリポジトリの総称です。 名前のとおり(?)一般的には社内用など非公開にして用いられます。これはたとえばrubyでいうところのプライベートなrubygemsに相当します。 一般的にはDarkPANはCPANのコンテンツを含まず、そこにアップロードされたプライベートなPerlモジュールパッケージのみをホストします。

Perlのライブラリを社内で共有したい場合、gitでバージョン管理しつつOrePAN2などのDarkPAN Managerを利用して社内用のDarkPANを建て、そこにリリースして利用するのが一般的です。

複数のDarkPAN

しかし、様々な理由により複数のDarkPANの運用が必要となる場面が生じることがあります。 DeNAにおいても例に漏れず複数のDarkPANに依存したPerlプロジェクトが一部存在しています。 そもそもDarkPANを1つにまとめられるならそれが一番良いのですが、様々な事情からそれが難しいケースも残念ながら存在しており、このような状態ができてしまいました。

DarkPANが複数存在すると困ることがいくつかありますが、そのうちの1つがCartonの導入が難しいことです。 今回の話のゴールはこのCartonを複数のDarkPANに依存したPerlプロジェクトで利用できるようにすることです。

Carton

CartonはRubyでいうところのbundler相当のもので、今日では依存CPANモジュールのバージョン固定のためのデファクトスタンダードになっているツールです。

2011年にCartonが発表されるまでは、CPANモジュールをrpmパッケージなどにパッケージ化してOSにインストールして利用したり、perlbrewのperlをgit管理したりなど、力技でバージョン固定がなされていましたが、 Cartonの登場によりPerl Mongerはこれらの苦行から開放されました。

Cartonから複数のDarkPANを利用するときの課題

ところで、Cartonから複数のDarkPANを利用するときに何が課題となるのでしょうか。 1つはPERL_CARTON_MIRRORの扱い、もう1つはcpanfile.snapshotの扱いです。

PERL_CARTON_MIRROR環境変数

PERL_CARTON_MIRRORはCartonが参照する環境変数です。 Cartonのドキュメント上に説明はありませんが、Cartonに任意のCPAN Mirrorを参照させるために利用することができます。 非標準の機能ではありますが、DarkPANを指定するためにも一般的に利用されているかと思います。

しかし、PERL_CARTON_MIRRORには1つのCPAN Mirrorしか指定できません。 そのため、複数のDarkPANを指定するためにはこれをそのまま利用することはできません。

また、これはその名前の通りCPAN Mirrorを指定するためのものであり、DarkPANのようなCPANのコンテンツを含まないものに利用されることが想定された機能ではありません。 PERL_CARTON_MIRRORに単一のDarkPANを指定した場合、そこに無いモジュール・バージョンの取得はBackPAN(過去CPANにアップロードされたすべてのパッケージを持つアーカイブ)にフォールバックされます。 そのため、DarkPANを指定すると一見正常に動くようには見えますが、適切な挙動ではありません。(自分も長らく勘違いをしていました。)

cpanfile.snapshot

また、cpanfile.snapshotはCartonがモジュールのバージョンをロックするために利用するファイルです。 つまり、これはbundlerでいうところのGemfile.lockに相当します。ただし、CPANの場合はCPANモジュールに対してそれを含むモジュールパッケージがあるので、実際にはそのパッケージをダウンロードする必要があることに加え、 そのパッケージのダウンロードパスにはそのバージョンをリリースした人のPAUSE ID(CPAN AuthorのID)が含まれるため、実際にはcpanfile.snapshotにはモジュールパッケージとそのダウンロードパスとその内容物となるパッケージの一覧、依存関係情報が保存されます。

なお、そのダウンロードパスにはホスト名などCPAN Mirrorの情報は含まれません。なぜなら、cpanfile.snapshotの作成された際に参照したCPAN Mirrorとそれを利用する際に参照するCPAN Mirrorが同一とは限らない(CPANから削除されてBackPANを参照する必要があるケースが起こり得る)ほか、 そもそも仕組み上それらをセットで保存することに意味がないためです。

そのため、原理的にcpanfile.snapshotへ複数のDarkPANのモジュールのバージョン情報を保存することは本質的に難しい課題です。

PERL_CARTON_MIRRORの問題と含めて考えると、CartonでDarkPANを扱うこと、特に複数のDarkPANを扱うことが難しいことがわかります。

解決のためのアイディア

先程の問題は単一のCPAN Mirrorからすべてのモジュールが取得出来れば解決します。 たとえば、オープンなCPANモジュールだけに依存していればなんの問題もありません。

ひとつの解決策はZenPANを利用することです。 DarkPANは一般的にはCPANのコンテンツを持ちませんが、CPANモジュールをDarkPANにも含めることで、CPAN Mirrorのようにも振る舞うことができます。 ZenPANはそれをサポートする機能を持っており、利用するCPANモジュールとプライベートなPerlモジュールをZenPANに追加して利用すれば、DarkPANを運用しながらCartonも利用することができます。

一方で、ZenPANにも課題があり、当然ですがZenPANに無いモジュールはインストールできません。 そのため、新しいバージョンに上げる際にはZenPANにも新しいモジュールをインストールして、Cartonで固定しているバージョンも上げるという2つのステップが必要になります。 また、ZenPAN特有のコードをcpanfileに記載する必要があります。

いずれもZenPANの存在を意識して利用する必要があります。 つまり、開発者はCartonだけでなくZenPANについての理解も要求されることになってしまいます。

ほか、DarkPANやCPAN Mirror毎に別々のcpanfile.snapshotを生成し、cpm--resolverオプションを利用してそれぞれのsnapshotからそれぞれのDarkPANやCPAN Mirrorを参照してインストールすることもできますが、 このアプローチを取る場合も利用者はそれぞれのDarkPANやCPAN Mirrorを別々に扱う必要があり使い方が複雑化してしまいます。

ほかには、cpan-zeroというツールがあります。これもZenPANと同様ですがワンショットでローカルに一時的なCPAN Mirrorを作成しつつインストールするという点が異なります。 自分の知る限りでは最も手軽に使えるものですが、都度CPAN Mirrorを作成することで依存するモジュール数に比例して時間が掛かるため、依存の多い大きなアプリケーションの開発で利用するのには向きません。

これを踏まえて考えると、ZenPANやcpan-zeroのような存在を透過的に扱うためには、それ自体をメンテナンスフリーなものにする必要があることが分かります。

そのための新しいアプローチとして、DarkPAN自身にそのような機能を持たせるのではなく、CPANとDarkPANを透過的に扱えるような存在があると良いのではないと考えました。

そのために作ったのがAnyPANです。

AnyPAN

AnyPANは任意のCPAN MirrorやDarkPANをまとめて、それらに透過的にアクセスするためのツール群です。

このツールは主に以下の2つのモジュールから成ります。

  • AnyPAN::Merger
  • AnyPAN::ReverseProxy

AnyPAN::Merger

CPANやDarkPANは02packages.txt.gzというファイルにモジュール名・バージョンとそれを含むPerlモジュールパッケージのダウンロードパスとの対応表を持っており、 cpanmなどのCPANモジュールのインストーラーはこれをもとにダウンロードするべきPerlモジュールパッケージのダウンロードパスを得て、指定されたCPAN MirrorあるいはデフォルトのCPAN Mirror(www.cpan.org)からそれをダウンロードする仕組みになっています。 ちなみに、Cartonはcpanfile.snapshotからこの02packages.txt.gzを生成し、これをインデックスとして利用する指定と共にMenloを呼び出すことによって、固定した最新ではないバージョンのPerlモジュールパッケージのインストールを実現しています。

File:         02packages.details.txt
URL:          http://www.perl.com/CPAN/modules/02packages.details.txt
Description:  Package names found in directory $CPAN/authors/id/
Columns:      package name, version, path
Intended-For: Automated fetch routines, namespace documentation.
Written-By:   PAUSE version 1.005
Line-Count:   246281
Last-Updated: Sun, 11 Oct 2020 23:41:02 GMT

A1z::Html                          0.04  C/CE/CEEJAY/A1z-Html-0.04.tar.gz
A1z::HTML5::Template               0.22  C/CE/CEEJAY/A1z-HTML5-Template-0.22.tar.gz
A_Third_Package                   undef  C/CL/CLEMBURG/Test-Unit-0.13.tar.gz
AAA::Demo                         undef  J/JW/JWACH/Apache-FastForward-1.1.tar.gz
AAA::eBay                         undef  J/JW/JWACH/Apache-FastForward-1.1.tar.gz
AAAA::Crypt::DH                    0.06  B/BI/BINGOS/AAAA-Crypt-DH-0.06.tar.gz
AAAA::Mail::SpamAssassin          0.002  S/SC/SCHWIGON/AAAA-Mail-SpamAssassin-0.002.tar.gz
AAAAAAAAA                          1.01  M/MS/MSCHWERN/AAAAAAAAA-1.01.tar.gz
AAC::Pvoice                        0.91  J/JO/JOUKE/AAC-Pvoice-0.91.tar.gz
AAC::Pvoice::Bitmap                1.12  J/JO/JOUKE/AAC-Pvoice-0.91.tar.gz
AAC::Pvoice::Dialog                1.01  J/JO/JOUKE/AAC-Pvoice-0.91.tar.gz

AnyPAN::Mergerは任意のCPAN MirrorやDarkPANのインデックスをマージした新しいインデックスを作成します。 さらに、そのインデックスに含まれるCPANモジュールパッケージを、作成したインデックスと共に指定したストレージアダプタクラスを通じて任意のストレージに保存することができます。 以下の例ではhttps://cpan.metacpan.org/http://my-darkpan.local/をマージしてそのインデックスとそれに含まれるパッケージを/var/lib/www/anypan.localに保存しています。

use AnyPAN::Merger;
use AnyPAN::Storage::Directory;

my $merger = AnyPAN::Merger->new();

$merger->add_source('http://my-darkpan1.local/');
$merger->add_source('http://my-darkpan2.local/');
$merger->add_source('https://cpan.metacpan.org/');

$merger->merge()->save_with_included_packages(
    AnyPAN::Storage::Directory->new(path => '/var/lib/www/anypan.local'),
);

これによって、複数のCPAN MirrorないしDarkPANの内容を含むインデックスとそのコンテンツを作ることができます。

AnyPAN::Mergerの概観

ちなみに、マージする際にモジュール名が衝突した場合どのように処理するかもマージアルゴリズムアダプタクラスによって指定できます。 デフォルトではより新しいバージョンのものを優先するマージアルゴリズムアダプタクラスが指定されます。

AnyPAN::ReverseProxy

また、Cartonで利用することも踏まえると、固定されたバーションをもとにインストールする場面で、AnyPAN::Mergerでインデックスを生成するより以前に作られたバージョンのモジュールをインストールしたい場面が考えられます。 その場合、AnyPAN::Mergerだけでは本来DarkPANに存在する過去バージョンのモジュールが取得できません。なぜなら、CPANのインデックスには最新バージョンのモジュールしか載らないため、インデックスからは過去バージョンのパスが分からないためです。

このようなことが起きる場面ではAnyPAN::ReverseProxyを利用することで問題が解決できます。

use AnyPAN::ReverseProxy;
use AnyPAN::Storage::Directory;

my $rp = AnyPAN::ReverseProxy->new(
    storage => AnyPAN::Storage::Directory->new(path => '/var/lib/www/anypan.local'),
);

$rp->add_source('http://my-darkpan1.local/');
$rp->add_source('http://my-darkpan2.local/');
$rp->add_source('https://cpan.metacpan.org/');

$rp->to_app(); # PSGI app

AnyPAN::ReverseProxyはPSGIアプリケーションとして実装されたHTTP Reverse Proxyサーバーで、指定したストレージアダプタクラスを通じて任意のストレージからコンテンツを取得します。 もしこの際、そこに存在しないものがリクエストされた場合は、ソースとして指定したCPAN MirrorまたはDarkPANからの取得を試みます。

取得に成功した場合はストレージアダプタを通じてそれを保存しつつ、そのコンテンツを返します。コンテンツをストレージに保存することによって、以後はストレージから取得することができるようになるほか、 その後に古いバージョンの参照が不要になった場合にはAnyPAN::ReverseProxyを撤廃できるようになります。なお、取得に失敗した場合は404 Not Foundを返します。

つまり、AnyPAN::Mergerで任意個数の任意のDarkPANと任意のCPAN Mirrorをマージし、AnyPAN::ReverseProxyを使ってホストしたサーバーをCPAN Mirrorとして指定することによって、 CPAN Mirrorとして振る舞いながらDarkPANのモジュールも適切に扱うことができるPerlモジュールリポジトリを作成することができます。

AnyPAN::ReverseProxyの概観

これによって、単にCartonのCPAN Mirrorとしてこれを指定することで、通常のCarton使い勝手と変わらないDX(Developer Experience)を複数のDarkPANを使った開発においても手に入れることができました。

実際の運用

AnyPANのストレージクラスとしてAnyPAN::Storage::S3を使うことで、S3にコンテンツを保存することができます。 このストレージクラスを使い、Amazon EventBridgeから定期的にAmazon ECS on FargateでホストしたコンテナでAnyPAN::Mergerを実行し、同様にECSでAnyPAN::ReverseProxyをホストすることでサーバーレスな環境でホストする構成を実現しました。 このようにしてほぼメンテナンスフリーなものが完成しました。

ゆくゆくは、古いDarkPAN上のモジュールもすべてS3に乗ることでAnyPAN::ReverseProxyも撤廃できるようになり、S3だけでホストできるようになるはずです。 そうなれば、ますますメンテナンスフリーに近づいていくことができます。

まとめ

これまで、DarkPANとCPAN、そしてそれを取り巻く複雑な背景から、複数のDarkPANが存在する環境ではCartonを適切な形で使うのはかなり難しい状況でした。 AnyPANという新しいアプローチの仕組みによって、あらゆるDarkPANに依存した環境でCartonを使いやすくする仕組みが構築できました。 そして、Cartonが普通のPerlプロジェクトと同様に使えるようになったことでDX(Developer Experience)の向上がほぼメンテナンスフリーな仕組みの上で実現できました。

複数のDarkPANが存在する環境でCartonを使うにはどうすればよいかというところに一定の解が見出だせたのではないかなと思います。 実際に自分たちのチームではこれによって抱えいた課題を解消して無事にCartonを導入することができました。

Perlコミュニティへ向けて

Perlコミュニティのこの問題の解決方法としてAnyPANが適切かというと、必ずしもそうではないかもしれません。 たとえば、gitリポジトリの特定のタグなどからモジュールを直接インストールする方法が整備されれはこのツールは不要となるかもしれませんし、それでもやはりDarkPANが必要な場面はあるかもしれません。 CPANのパッケージ管理のエコシステムをまるっと変えるべきだという意見もあるかもしれません。

Perlコミュニティとしてあるべき姿が何か、自分たち自身がそれぞれ考えて意見を発信していき、できれば公式のディスカッションに混ざったりコントリビューションを行っていくことが重要だと思います。 自分も全然十分には出来てはいませんが…。

あくまでもAnyPANは自分の中で出した答えのうちの一つでしかなく、単に実際の問題に対して素直に対処するために作ったものに過ぎません。 ただ、解決策のひとつとして実際に効果的なものでもあるとは思っています。現状はTRIAL RELEASEを出しているので似た課題に直面している方は試して頂けると嬉しいです。 ドキュメントは未整備ですが、より良いアプローチが見つからない限りは整えていくことになります。よかったら、よろしくおねがいします。

最後に

時代の流れからPerlの出番は段々と減りつつはありますが、Perlが活躍している場面はまだまだあります。 DeNAにもPerlで作られ、いまも運用されているシステムがいくつか(たくさん?)存在します。 もしよかったら一緒に、Perlを使っている現場でより良い開発を目指しながら、仕事を通じて世界に価値を届けていきませんか? DeNAではエンジニアを募集中です。

この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!
また、DeNA公式Twitterアカウント@DeNAxTechでは、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!