仕事で開発をするときに、外部ネットワークとは隔離された環境でいろいろ作るというシチュエーションがけっこうあって、だいぶ前から欲しいと思っていた gem ファイルの local mirror を作ることにした。
いくつかのサイトを見たけど、新しめに見えた下記の記述を参考に作業。
準備
Ruby をインストールして、gem コマンドを使えるようにする。
次に rubygems-mirror をインストール。 gem でも入れられるが、少しバージョンが古いようだった。 今回は後述する変なはまり方をしたので、GitHub から並列度 (parallelism) 指定ができる版をダウンロードした。
gem mirror コマンドの設定ファイル、~/.gem/.mirrorrc
を記述。
これも後述するはまりのため、最終的には下記の記述に落ち着いた。
- from: http://production.s3.rubygems.org
to: /path/to/gem/mirror
parallelism: 10
ミラー開始
gem mirror コマンドを実行。
$ gem mirror
Fetching: http://production.s3.rubygems.org/specs.4.8.gz with 10 threads
Total gems: 263717
Fetching 263717 gems
.....(略)
いくつか存在しないファイルがあったが、一日ぐらいでミラー完了していた。
下記のファイルを wget などで、ミラーしたディレクトリに直接取得。
- latest_specs.4.8.gz
- Marshal.4.8.Z
- specs.4.8.gz
- yaml
- quick/latest_index.rz
quick/Marshal.4.8
ディレクトリを作成し、そこにすべての gemspec ファイルをダウンロードする。
なにぶん量が多いので、参考にしたページが紹介していたスクリプトを少し変更して、ローカルの gem ファイルの方が新しい場合に gemspec ファイルを取得し直すようにした。 このままだと gemspec ファイルのみ差し替えがあった場合に対応できないが、上書き取得するオプションでもつけて、ときどき全部取得しなおすことにしよう。
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'net/http'
VERBOSE=false
GEMSRC="http://production.s3.rubygems.org:80"
BASEDIR="/var/service/gem/mirror"
GEMDIR="#{BASEDIR}/gems"
MARSHALDIR="#{BASEDIR}/quick/Marshal.4.8"
def fetch(url)
response = Net::HTTP.get_response(URI.parse(url))
case response
when Net::HTTPSuccess
response
when Net::HTTPRedirection
fetch(response['Location'])
else
return nil
end
end
puts "mirror src #{GEMSRC}" if VERBOSE
Dir.foreach(GEMDIR) do |gemfile|
next if File.ftype("#{GEMDIR}/#{gemfile}") != 'file'
gemname = File.basename gemfile, '.gem'
gemspecfile = gemname + '.gemspec.rz'
marshalfile = "#{MARSHALDIR}/#{gemspecfile}"
if File.exist?(marshalfile) and
File.mtime("#{GEMDIR}/#{gemfile}") < File.mtime(marshalfile)
puts "skip #{gemspecfile}" if VERBOSE
next
end
url = "#{GEMSRC}/quick/Marshal.4.8/#{gemspecfile}"
puts "fetch #{gemspecfile}"
res = fetch(url)
if res.nil?
puts " ERROR: #{url}"
next
end
File.open(marshalfile, 'wb') do |io|
io.write(res.body)
end
end
__END__
ミラーサイト設定
Apache を設定し、上記でミラーを作ったディレクトリを公開する。
<VirtualHost *:80>
ServerName rubygems.your.domain
DocumentRoot /path/to/gem/mirror
ErrorLog logs/rubygems-error_log
CustomLog logs/rubygems-access_log common
</VirtualHost>
gem コマンドの引数で上記のサーバを参照する。
gem install -r --source http://rubygems.your.domain rails
~/.gemrc
に下記の内容を書いてもよい。
:sources:
- http://rubygems.your.domain
access_log に上記のアクセスが記録されれば OK。
はまった点
gem mirror コマンドがちゃんと動かず、しばらくはまりました。 具体的にはこんな感じで、ミラーが途中で止まってしまう。
$ gem mirror
Fetching: http://rubygems.org/specs.4.8.gz with 10 threads
Total gems:
Fetching 263717 gems
...........ERROR: While executing gem ... (Gem::Mirror::Fetcher::Error)
unexpected response #<Net::HTTPServiceUnavailable 503 Service Temporarily Unavailable readbody=false>
$ gem mirror
Fetching: http://rubygems.org/specs.4.8.gz with 4 threads
Total gems: 263717
Fetching 261349 gems
.ERROR: While executing gem ... (Gem::Mirror::Fetcher::Error)
unexpected response #<Net::HTTPServiceUnavailable 503 Service Temporarily Unavailable readbody=false>
翌日、翌々日など試してみても同様の症状。 調べてみても、rubygems.org がトラブっているような記事はみつからず。 (乗っ取られたので、今日は見つかりそうですが…)
そもそも、
- 実行した直後は動いて、十数個で止まる
- 連続して実行すると、すぐに弾かれる
- 少し経ってからもう一度叩くと、いくつかは取得できる。
…という状況から見て、なんらかの rate limit が効いているような気配。
rubygems-mirror を調べてみると、gem install rubygems-mirror で入るバージョンにはない “parallelism” というオプションのあるバージョンが見つかった。 並列度が高すぎると弾かれる問題なのかと考えて、そちらのバージョンをとってきて並列度を下げて実行しても症状変わらず。
エラーメッセージを手がかりに探してみても、「サイトが落ちてたんだろ」的な過去のやりとりが引っかかる程度… と、そんな中でもいろいろ調べていたら手がかりになりそうな blog が見つかった。 2012年9月〜11月ぐらいに tokyo-m.rubygems.org という、rubygems.org の日本向けサーバが落ちていたとのこと。 それぞれに問題の回避方法は違うものの、海外の Proxy を通すとか、リダイレクトされる s3 の URL を直接指定するとか、要するに tokyo-m.rubygems.org へのアクセスを避ける手段だった。
blog の記述でも tokyo-m.rubygems.org は復旧済みのようだし、実際、今回 mirror を考えるまではまったく困っていなかった。 しかしこれは試してみる価値あり… ということで、.gem/.mirrorrc に from: http://production.s3.rubygems.org を指定すると… 動いたー!
上記の挙動から考えて、
- tokyo-m.rubygems.org が落ちた (2012–09〜11 頃?)
- サーバを復旧させたが、そのときに rate limit をかけた
- 普段使う分には問題ない設定だが、mirror のように連続して取得するとひっかかる
ということだったんだろうと想像。 日本語の gem mirror 記事はその時期以前のものばかりだったし、英語の gem mirror 記事で同じようなはまり方をしているのは見つからなかったので…
自分だけがはまるとドキドキしますねぇ…^^;