Gem in a Boxサーバーのデータをrsyncで同期する


# TL;DR
* Gem in a BoxのGemを別のGem in a Boxサーバーに同期する
	* 新しくpushされたとき
	* 削除されたとき
* Rack Middlewareでrsyncを使う

# はじめに
Gem in a BoxのデータをRackのMiddlewareを使用して同期する方法を紹介します。
プライベートなGemサーバーをCIに組み込む時にGemデータを同期して冗長化する必要があったのでこの方法をとりました。

あくまでもデータ同期だけです。Active-Standbyを想定してます。

# 方針
## やりたいこと
* Gem in a BoxにpushしたGemをpushされた時に同期する
* 削除された時も同期削除する

## どうするか
* RackでPOSTリクエストまたはDELTEリクエストが来た時にrsyncでデータディレクトリを同期する
* 同期してからレスポンスを返す
* RackのMiddlewareで実現する
* Proxyしてるrubygems.orgのGemなんかはどうでもいい

## そのために
* rsyncするRack Middlewareを使う
* rsyncするため Primary -> Secondary へSSH接続できるようにする
	* Gem in a Box用のユーザを作る
	* 秘密鍵・公開鍵のペアを適切に設定する

## その他
* Unicornを使う
* systemdでUnicornを起動する
* systemdの環境ファイルにお互いのIPを持つ
* Rackは環境ファイルからの環境変数で自分がPrimaryかSecondaryか判断する

## 注意点
* rsyncなのでGem数が増えたとき時間かかるかも
	* rubygems.orgをProxyしなければいい
* GETに比べてPOST/DELETEが少ない前提
* Unicornが落ちたらデータが同期できない

# やってみる
Vagrantでやってみます。一式をGithubに用意しました。
https://github.com/nownabe/example-geminabox-replication

まずこのレポジトリをクローンしてください。

```bash
git clone https://github.com/nownabe/example-geminabox-replication
cd example-geminabox-replication
```

VagrantでGem ServerのPrimary/Secondaryとなる2台のVMを起動してプロビジョンします。
Rubyをビルドするので時間かかります。

```bash
vagrant up
```

このVagrantfileを使うと、次のIPアドレスで起動されます。

* Primary: 192.168.33.29:8080
* Secondary: 192.168.33.30:8080

まずブラウザで確認してみます。どちらのサーバーもGem in a Boxの画面が見れると思います。

![スクリーンショット_2015-08-31_0_05_53.png](https://qiita-image-store.s3.amazonaws.com/0/43208/eb68dabc-1bcb-7d13-7a1b-c6e806bc9d7e.png "スクリーンショット_2015-08-31_0_05_53.png")


適当なGemをPushしてみましょう。

```bash
$ gem push pkg/ringc-0.1.2.gem --host http://192.168.33.29:8080
Pushing gem to http://192.168.33.29:8080...
Gem ringc-0.1.2.gem received and indexed.
```

ブラウザを更新するとGemがpushされたことが確認できます。

![スクリーンショット_2015-08-31_0_23_53.png](https://qiita-image-store.s3.amazonaws.com/0/43208/6568d8e8-a333-7a79-9e1f-bde9dfd7e5fd.png "スクリーンショット_2015-08-31_0_23_53.png")


Secondaryもrsyncによって同期されています。

![スクリーンショット_2015-08-31_0_23_59.png](https://qiita-image-store.s3.amazonaws.com/0/43208/2fea7562-ec13-96f8-4a45-781b4bfdd381.png "スクリーンショット_2015-08-31_0_23_59.png")


削除も同期されます。

![スクリーンショット_2015-08-31_0_24_33.png](https://qiita-image-store.s3.amazonaws.com/0/43208/be9fcd8c-249a-8362-d5d3-1c2bc1e14987.png "スクリーンショット_2015-08-31_0_24_33.png")

SecondaryからGemをインストールできることを確認してみます。

```bash
$ gem install ringc -s http://192.168.33.30:8080
Fetching: ringc-0.1.2.gem (100%)
Successfully installed ringc-0.1.2
Parsing documentation for ringc-0.1.2
Installing ri documentation for ringc-0.1.2
Done installing documentation for ringc after 0 seconds
1 gem installed
```

ばっちりですね！！:satisfied: 

# 解説
具体的な方法はGithubのコードを読んでいただくのが一番だと思います。
重要な一部だけ説明します。

## SSH
* `gem_server`ユーザを作成している
* `gem_server`ユーザのホームディレクトリは`/opt/gem_server`
* `/opt/gem_server/.ssh`以下をごにょごにょやって Primary -> Secondary にSSHできるようにしている

## config.ru
Gem in a Boxを起動する`config.ru`は次のようになってます。

```ruby:config.ru
require "geminabox"
require "rack/rsync"

if ENV["GEMINABOX_PRIMARY"] == ENV["GEMINABOX_MYADDRESS"]
  src = "/opt/gem_server/data/"
  dst = "#{ENV['GEMINABOX_SECONDARY']}:/opt/gem_server/data/"

  use Rack::Rsync, src, dst, "-a", "--delete" do |env|
    env["REQUEST_METHOD"] == "POST" || env["REQUEST_METHOD"] == "DELETE"
  end
end

Geminabox.data = "/opt/gem_server/data"
Geminabox.rubygems_proxy = true
run Geminabox
```

* systemdが読む環境ファイルから自分のIPがPrimaryのIPと一致するかチェックする
* 一致するときだけ`Rack::Rsync`というミドルウェアを使うようにする
* `Rack::Rsync`にリクエストメソッドがPOSTかDELETEのときだけrsyncを実行するように条件を与えている

## systemd
systemdのunitファイルは次のようになってます。

```
[Unit]
Description=Gem Server

[Service]
WorkingDirectory=/opt/gem_server/geminabox
ExecStart=/usr/local/bin/unicorn -p 8080
EnvironmentFile=/opt/gem_server/geminabox/environments
User=gem_server
Group=gem_server

[Install]
WantedBy=multi-user.target
```

なんの変哲もないですね。起動ユーザを`gem_server`ユーザにして、環境ファイルを読むようにしてます。

Unicornが落ちたとき自動で復旧するように`Restart=always`とかしといてもいいかもしれません。

# おわりに
タイミングシビアに同期したい場合にいかがでしょうか。
これだと不安だって場合はもっと下のレイヤーでやったほうがよさそうです。