All Articles

Ruby gem の local mirror を作る

仕事で開発をするときに、外部ネットワークとは隔離された環境でいろいろ作るというシチュエーションがけっこうあって、だいぶ前から欲しいと思っていた 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 記事で同じようなはまり方をしているのは見つからなかったので…

自分だけがはまるとドキドキしますねぇ…^^;