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 に下記の内容を書いても OK.

: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 記事で同じようなはまり方をしているのは見つからなかったので…

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

2 comments — post a comment

mkouhei

同じ問題はまってました。私も同じ方法で回避していたのですが、特にブログにまとめませんでした。いい記事ですね。ありがとうございます。

Pingback: jenkins+chroot+antでchef-soloのテストをやってみた | まよぽん

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA * Time limit is exhausted. Please reload CAPTCHA.