世の中にはたくさんの OSS が公開されていて、それを Linux 上で動かす選択肢も多様になってきました。 今まで通り自前でビルドするのはもちろん, Go のようにシングルバイナリになってるならバイナリ落としてくるだけのものもあります。 DockerHub で公開されているものなら Docker でコンテナイメージをダウンロードするという手もあります。 Homebrew on Linux なんてものも登場しましたね。
選択肢が増えて動かすだけなら楽になったんですが、 事前の環境構築が最小限で済んで、バージョン管理もできて、依存もいい感じに解決してくれて、 といろいろ考えると結局は Red Hat 系なら標準のパッケージマネージャーである yum が楽なんですよね。
そういうわけで JFrog Bintray にバイナリをあげて、yum レポジトリを公開していました。 ところが今月になって 突然の Bintray 終了のお知らせ!!!
前置きが長くなりましたね。 要するに Bintray からのお引越しを考えないといけなくなったので、 yum レポジトリを AWS S3 上に移行した、というお話です。
標準的な yum レポジトリの作り方
yum レポジトリを作るには、まず公開したい rpm パッケージが必要です。 Bintray だろうが S3 だろうが、rpm 作成の手順は一緒なので省略します。
rpm さえできてしまえば、レポジトリの作成は非常に簡単です。
createrepo
コマンドをインストールして実行するだけ。
yum install createrepo
createrepo /PATH/TO/REPOSITORY
/PATH/TO/REPOSITORY
の中を自動的に検索して、
メタデータを作成してくれます。
これをこのまま HTTP で公開すれば yum レポジトリの完成です。
yum レポジトリをサーバーレス化する
しかしこの方法では HTTP サーバーの運用が必要です。 yum レポジトリが落ちると、利用しているサービスにクリティカルに影響するので、 割と真面目に運用しないといけません。面倒ですね。
S3 にアップロードすれば HTTP サーバーの運用問題は解決なんですが、
createrepo
をどこで実行するかという問題が残ります。
先行事例
S3 の更新イベントを AWS Lambda で受け取って、 EC2 インスタンスを起動するという事例がいくつか見つかります。
EC2 インスタンス上では大まかに以下のようなスクリプトを実行します。
yum install -y createrepo
# S3上のパッケージをすべてダウンロード
aws s3 sync s3://my-yum-repository/ /repo
# メタデータ更新
createrepo /repo
# 更新したメタデータをアップロード
aws s3 sync /repo s3://my-yum-repository/
createrepo
を実行するにはレポジトリ上のすべての rpm ファイルが必要なので、
「S3 上のパッケージをすべてダウンロード」する必要があります。
yum レポジトリの規模にもよりますが、全ファイル転送は時間がかかりそうです。
EC2 インスタンスを毎回起動するのもなんとかしたい部分です。 起動だけで数分必要になってしまいます。
そこで AWS Lambda 内で完結させる方法を考えました。
AWS Lambda に createrepo コマンドをインストールする
AWS Lambda 内で完結するには、 AWS Lambda の環境に createrepo
コマンドをインストールする必要があります。
これは先日の AWS Lambda アップデートで非常に簡単になりました。
Lambda 用のベースイメージには yum コマンドが含まれているので、普通に yum install
するだけです。
FROM amazon/aws-lambda-provided:al2
RUN yum update -y && yum install -y createrepo_c && rm -rf /var/cache/yum/* && yum clean all
これまでは /var/task
以下にインストールする必要があったので、
yum のようなパッケージマネージャーが使えず、自前でビルドする等の工夫が必要でした。
↑ MeCab で頑張ったときの例。
レポジトリのメタデータだけダウンロードする
先行事例と同様に S3 の更新イベントを AWS Lambda で受け取って、メタデータの更新処理を行います。
先行事例では rpm パッケージを含んだレポジトリの全データーをダウンロードしてきていました。
AWS Lambda で自由に使えるディスク容量は /tmp
の 512MB のみで、全データーダウンロードするには心もとないです。
そこでここではメタデータのみをダウンロードします。
aws s3 sync s3://my-yum-repository/repodata /tmp/repo/repodata
メタデータは repodata
ディレクトリに入っているので、このディレクトリだけダウンロードしてきます。
新しい rpm だけの yum レポジトリを構築する
新しくアップロードされた rpm パッケージのみダウンロードして、そのパッケージのみを含んだ yum レポジトリを構築します。
ここでは例として new-package.rpm
がアップロードされたとしましょう。
この場合、以下のような処理を実行します。
mkdir -p /tmp/new-repo
aws s3 cp s3://my-yum-repository/new-package.rpm /tmp/new-repo
createrepo /tmp/new-repo
レポジトリのマージを行う
さて、ここまでで2つのレポジトリのメタデータができました。 これをなんとかひとつにまとめたいです。 何か良い方法は無いものか・・・と、createrepo_c のレポジトリを眺めていたところ、 ピッタリなコマンドがありました。
このコマンドを使うと 2 つ以上のレポジトリをマージすることができます。
その際、rpm パッケージのチェックは行われないので、メタデータしかない不完全なレポジトリ (/tmp/repo
) も扱うことができます。
現在のレポジトリ /tmp/repo
に、 新しい rpm を含んだレポジトリ /tmp/new-repo
をマージして、/tmp/merged-repo
に出力する場合の例は
以下のようになります。
mergerepo --database --omit-baseurl --all --repo /tmp/new-repo --repo /tmp/repo --outputdir /tmp/merged-repo
mergerepo
はマージの際に、パッケージがどのレポジトリ由来のものかという情報をメタデータに追加します。
ここではマージ元にローカルレポジトリを指定しているため、そのまま公開しても外部からはアクセスできません。
そのため --omit-baseurl
でこの挙動を抑制しています。
これで /tmp/merged-repo
に必要なパッケージの情報が全て入ったレポジトリができました。
S3 上のメタデータを更新する
/tmp/merged-repo
のメタデータには必要なパッケージの情報がすべて入っているので、これを S3 にアップロードします。
aws s3 sync /tmp/merged-repo/repodata s3://my-yum-repository/repodata
これで new-package.rpm
のレポジトリへの追加完了です。
実装
これを実装したものを以下で公開しています。
ここで説明したものをそっくりそのまま実装したのではなく、以下のような工夫が入ってます。
- aws cli は重たいので、Go を使って実装
- rpm への署名
repodata/repomd.xml
を解析して本当に必要なメタデータのみダウンロードするrepodata
以下には過去の分のゴミも含まれているので、全部は必要ない
- メタデータのアップロード順序の工夫
- メタデータは複数のファイルから構成されているので、順序を間違うと一時的に壊れた状態になる
- 処理が正常に完了すれば問題ないんだけど、万が一クラッシュしたら・・・?
- 具体的には
repodata/repomd.xml
を最後にアップロードする
- 排他制御
- メタデータ更新が並列して走るとロストアップデートの可能性がある
- DynamoDB を使った排他的ロックを実装
まとめ
S3 上に yum レポジトリを構築しました。 S3 の更新イベントをトリガーにして AWS Lambda でメタデータの更新を行っているので、 S3 へアップロードするだけで rpm の追加が行えます。
さて・・・yum レポジトリの移行は終わったけど、Bintray JCenter の移行が残っている・・・。 特に Android 界隈の影響が大きそうですが、どうなるんですかね?