この記事は 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::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モジュールリポジトリを作成することができます。
これによって、単に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記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!
Follow @DeNAxTech
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。