【中級編】drone.io + exrmで今日から始めるElixir on Phoenixホットデプロイ入門

# 何これ？
皆さん、おはこんばんちは。

前回の記事、[CentOS + Erlang + Elixir + PhoenixのDockerイメージで今日から始めるElixir on Phoenix入門](http://qiita.com/xtity/items/969eefb83fcac9023542)に引続き、Elixir/Phoenixについての入門記事です。

入門なのに中級編です。

今回は前回の予告通り、deployとci周りをやっつけます。
これができれば、Elixir/PhoenixでHello Worldは卒業して、
実際の開発をしていけると思います。

ゴールは、

0. プルリクでCI回してテストする
0. プルリクをmasterにマージする
0. リリースビルド作る
0. GitHub Releases使って、tag切って実稼働用のバイナリ上げる
0. ホットコードスワップ（ホットデプロイ）本番環境を無停止で更新する

という一連の流れを全てGitHubのプルリクとマージだけで行うことです。

Jenkinsのように、辛い設定ファイルを保守したり、
ボタンをポチポチ押す必要はもうないです。

# CIとデプロイについて
これも予告通り、[drone.ioのOSS版](https://github.com/drone/drone)を使います。

なぜか？答えは簡単で、本番と同じDockerイメージでテストしたいからです。

本番で動くDockerイメージと同じイメージでCIが回せれば、
開発環境と本番環境での環境差分に由来するバグは無くなります。

Dockerfileが同じであれば出来上がる環境は同じになるので、
chef等で苦心する冪等性とかも気にしなくていいです。

ただし、DockerはDockerで別の難しさもあります。
構築した環境は基本イミュータブルという思想で、
sshd立てて外部からゴニョゴニョするととたんに上記の嬉しさが無くなってしまいます。

そのような環境でどうホットデプロイをするか、それが今回の記事のポイントです。
それでは前置きはこのくらいにして、そろそろ行ってみましょう。

# CIサーバーの構築
例によって、Dockerイメージ作りました。

* https://github.com/xtity/docker-centos7-elixir-drone
* https://registry.hub.docker.com/u/xtity/docker-centos7-elixir-drone/

私はさくらのVPSで立てているんですが、一つ注意点としてはホストOSがCentOSの場合、
できれば7系の上でdockerは動かして下さい。

私はもともと借りてたヤツを７系にするのがめんどかったので６系を最新にして使ってますが、７系推奨です。

dockerのインストールの仕方としては特に難しいことはなくて、

``` bash
sudo yum install docker-io
sudo service docker start
sudo chkconfig docker on
```

でいいじゃないでしょうか。詳しいことはWebにたくさん記事が上がっているので参照して下さい。下記とかを参考にさせて頂きました。

* [CentOS6.5にDockerインストールしてみた](http://blog.pg1x.com/entry/2014/07/16/235001)
* [さくらVPS上のCentOSにDockerをインストールした](https://www.sevenspirals.net/2014/08/14/docker_install/)

で、ホスト側で、 ```docker ps``` 等打って確認出来たら、下記のコマンドでdrone.ioを立てましょう。

``` bash
docker run -d -p 127.0.0.1:10001:80 -v /etc/localtime:/etc/localtime:ro -v /var/run/docker.sock:/var/run/docker.sock --name drone -t xtity/docker-centos7-elixir-drone
```

さて、これで素のdrone.ioは立ったと思いますが、色々とやるべき設定があります。
私の場合は以下のことをやりました。

* ホストサーバーのnginxをリバースプロキシとして使う設定
* GitHubとの連携をするための設定
* droneでCI、デプロイ、GitHub Releaseをするための設定

順番に見て行きましょう。


## ホストサーバーのnginxをリバースプロキシとして使う設定
上記の ``` docker run ``` コマンドの通り、127.0.0.1:10001 => 80のルーティングをしています。なんで、下記のようなnginxの設定をして、サブドメインをよしなにdroneさんへリダイレクトさせましょう。

``` nginx:ssl.conf
#
# HTTPS server configuration
#
server {
    listen       443;
    server_name  ~^(?<subdomain>.*)\.xtity.com;

    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    ssl on;

    ssl_prefer_server_ciphers on;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK;

    ssl_session_cache  builtin:1000  shared:SSL:10m;

    set $redirect_port -1;
    if ($subdomain = drone) {
        set $redirect_port 10001;
    }

    if ($redirect_port = -1) {
        return 404;
    }
    
    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Origin "";
    
        proxy_pass http://127.0.0.1:$redirect_port;
        proxy_redirect off;
    }

    location /api/stream {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Origin "";
    
        proxy_pass http://127.0.0.1:$redirect_port;
        proxy_redirect off;
    
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }

    location /favicon {
      empty_gif;
      access_log    off;
      log_not_found off;
    }

}
```

[これ](http://readme.drone.io/setup/misc/nginx/)を参考にしつつ、わりと適当に設定してるんでイケてない所はよしなに読み替えて下さい。

因みに、GitHubのREADME.mdにステータスバッジ（こんなの-> ![status.png](https://qiita-image-store.s3.amazonaws.com/0/20725/ae15e843-6f81-1719-94c3-eab10c9c5f63.png))を貼りたかったんですが、どうもHTTPSでは上手くｷﾞﾊﾌﾞ先生がキャッシュしてくれなかったんで、そこのAPIだけ普通に80に飛ばしてます。


``` nginx:virtual.conf
server {
    listen 80;
    server_name drone.xtity.com;

    set $redirect_port 10001;
    set $subdomain drone;

    location /api/badge {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Origin "";

        proxy_pass http://127.0.0.1:$redirect_port;
        proxy_redirect off;
        break;
    }

}
```

めんどいんでオレオレ証明書使ってるせいかなと思ったりもしたんですが、よう分からん。


## GitHubとの連携をするための設定
大体設定する順番は下記の通り。

0. /etc/drone/drone.tomlの設定
1. GitHubとのアカウント連携
2. CI回したいリポジトリの設定
3. デプロイしたいサーバーへ公開鍵を追加

### /etc/drone/drone.tomlの設定
これもWebに色々と記事が転がってると思うんで、よしなに参照すると良いです。
[ここ](http://yosssi.hatenablog.com/entry/2014/02/08/161500)とか。

主にいじるのは下記くらいでしょう。

``` bash:/etc/drone/drone.toml
[github]
client=""  ## GitHub>Personal settings>Applicationsで作ったヤツ
secret=""  ## GitHub>Personal settings>Applicationsで作ったヤツ
orgs=[ "hoge" ]  ## organizationsで縛りたい時のみ設定
open=false

[worker] ## nodesの数はホストのCPUコア数分追加でいいじゃないかなあ 
nodes=[　
   "unix:///var/run/docker.sock",
   "unix:///var/run/docker.sock"
]
```

一つだけ注記するとすれば、orgsですかね。

本当はdroneちゃんにBasic認証を掛けたかったんですが、色々上手くいかなかったんで断念しました。
droneを使えるユーザーをｷﾞﾊﾌﾞの特定のorganizationsに所属しているユーザーのみに縛る事によって、その代わりとしてます。

まあ、管理上こっちの方が面倒は無いので使いたい人はどぞ。

### GitHubとのアカウント連携
上記のようにWeb参照。特に難しい事はありません。

"Authorization callback URL"は、https://drone.hoge.com/api/auth/github.com
とかでいいんじゃないかな。
 
### CI回したいリポジトリの設定
手順。

0. ｷﾞﾊﾌﾞのアカウントでログインしたら、まずsyncボタン押す
1. 自分の持ってるリポジトリの中でdroneと連携したいやつを選択
2. オレオレ証明書使ってる人は、ｷﾞﾊﾌﾞのリポジトリごとの設定画面にある、Webhooks & Services>WebhooksでSSL verificationをオフりましょう

### デプロイしたいサーバーへ公開鍵を追加
droneでビルドしたリリースリソースを上げる先のサーバーに、``` settings ``` ページで出てる公開鍵を登録。
こんな画面↓
![scrn_150526_1.png](https://qiita-image-store.s3.amazonaws.com/0/20725/90311683-aae8-17af-ffa6-18217157177d.png)


# ホットデプロイ
さて、これでマイdroneが立ちました。
次はいよいよCIを回して、ホットデプロイしてみます。

取り敢えず、Elixir/Phoenixでホットデプロイするためのプロジェクトを作ってみたので、
こいつを使って説明していきます。

https://github.com/xtity/deploy_phoenix

かなり実験用にコミット乗っかってますが、まあ気にしないでください。
こいつの大元は下記コマンドで生成した素のPhoenixプロジェクトです。

``` bash
mix phoenix.new deploy_phoenix --no-ecto
```

でそこから何が変わったかは下記のdiffで分かると思います。

https://github.com/xtity/deploy_phoenix/compare/e6a1ebcac7c90f9212de61540641ce19e4e80a81...73b96639886ff8a710f7d2392e23a02262021919?w=

いっぱい差分出てますが、関係があるのは下記のファイルだけです。

* Dockerfile
* incron_release.sh
* .drone.yml
* mix.exs
* VERSION

ここにDockerで動くPhoenixプロジェクトに対して、ホットコードスワップするための仕組みがつまっています。

一つ一つ説明するとこの記事の量がもっと増えると思うのでそれは今後に譲るとして、
大まかな戦略だけ説明すると、

0. mix.exsで、exrmが使えるように依存関係追加
0. Phoenixプロジェクトが動かすDockerfileで、incrondを使って特定のファイル（今回は ``` VERSION ``` ）の更新を監視させるように動かす設定もしてやる
1. incron_release.shで、VERSIONが更新された時の処理を記述
2. １のイメージを、``` docker run ``` する時に``` VERSION ```ファイルをホストと共有する
3. .drone.ymlの ``` publish: ``` でmasterブランチの更新時のみ下記をやってます
    0. exrmでリリースリソースを作成
    1. 公開鍵認証でホスト側にSSH接続
    2. ``` VERSION ``` を更新
    3. GitHub Release
4. incron_release.shが走り、ホットコードスワップ

何となくイメージはつくでしょうか。

Dockerコンテナ上で動くサービスのホットデプロイについて色々試行錯誤してみたのですが、
前述のとおりsshdとかは勿論立てられません。

結論としては、

何らかの方法でホストとコンテナ間で「更新したよ」というメッセージのやりとりさえできればいい
↓
ファイルの更新検知でいい
↓
じゃあ、incrondでいいじゃん。

となりました、シンプルです。

因みに上のプロジェクトは勿論DockerHubにも登録されています。

https://registry.hub.docker.com/u/xtity/deploy-phoenix/

下記のコマンドで一応動きますが、よしなにDockerfile弄ってもらってもおｋです。

``` bash
docker run -d -p 127.0.0.1:15001:4000 -v /var/run/docker.sock:/var/run/docker.sock -v /usr/local/app/phoenix/deploy_phoenix/RELEASE_VERSIONS:/usr/local/app/phoenix/deploy_phoenix/RELEASE_VERSIONS --name deploy_phoenix -t xtity/deploy-phoenix
```

因みに、deploy_phoenixを動かすためのnginxの設定はこんな感じです。
15001を80に飛ばしてるだけですが。

``` nginx:virtual.conf
server {
    listen 80;
    server_name phoenix.xtity.com;

    set $redirect_port 15001;
    set $subdomain phoenix;

    location / {
        proxy_set_header        Host $host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;

        set_real_ip_from    153.121.61.37;
        real_ip_header      X-Forwarded-For;
        proxy_pass          http://127.0.0.1:$redirect_port;
        proxy_read_timeout  90;
        proxy_redirect      http://127.0.0.1:$redirect_port https://$subdomain.xtity.com;
        break;
    }

}
```

動いてる姿はこんな感じです。

http://phoenix.xtity.com/

バージョン情報だけ、デフォルトのページに追加してやります。
こいつを変更してmasterにpushすると、ぬるっとバージョン情報だけ更新されます。

因みに、Github Releaseはこんな感じ

https://github.com/xtity/deploy_phoenix/releases

rel以下をtarで固めて放り込んであります。
なんかあったら、こいつ落として解凍すれば元通りです。

まあ、exrm自体がダウングレードもサポートしてるのでそういう状況はほとんどないとは思いますが。

取り敢えず、ホットコードスワップしたいときは、``` VERSION ``` ファイルを更新すればよいです。

因みに、exrm自体もともとそのような事をしたい欲求から作られたプロダクトだそうです。
以下にそれがよくまとまっています。

* [Releases for Elixir](http://bitwalker.org/blog/2014/03/20/releases-for-elixir/)


## 次回予告
ふう、やっと中級編が書けました。長かった...

次回はElixir/Phoenix上級編ということで何やろうかなと今も考えてる最中なんですが、
作る準備が今回で整ったということで、おそらく実際に何かサービス作ってみようと思います。

多分またエイヤと作ってしまうと思うんですが、
どんなサービスになるか本人もドキドキしている次第です。

## 終わりに
気がつけば、桜が散り、超会議が終わり、半袖だけでもいいようなそこそこ暑い日もチラホラ。

その間にもElixir/Phoenixの話題は色々な所で盛り上がってきていると思います。

http://ch.nicovideo.jp/dwango-engineer/blomaga/ar796480

![d_lang.png](https://qiita-image-store.s3.amazonaws.com/0/20725/b9dc0fed-026d-40f0-16f2-3f3d247773d5.png)

こんなのも出ていたり。
Elixir、そこそこ健闘してますねー