リリースデプロイツールの成長戦略

こんばんは。@umisora です。インフラエンジニアやってます。
Money Forward Advent Calendar 2016の遅刻投稿です。

# 何を書くか
リリースツールを0から作る時に、どんな要求を受けてどう育っていくかを
自分の経験を元にまとめていきたいと思います。
これによってリリースツールを初めて扱う人や、もう一段階成長させたい時に参考になれば良いと思っています。

# リリースツールとは
言語によらず、プログラミングしたソースコードを本番・開発環境で動かすまでのプロセスを自動化するツールの事を本ブログでは指します。会社によってはリリースをデプロイと呼ぶ事もあるかも知れないので、適宜読み替えて下さい。

# リリースプロセス

プロセスは大抵の場合以下のステップで表現する事が出来ると思います。
(大元はMavenのGoalが参考になっています。)

## 1.Build/Compile
  ・各言語毎に存在するCompile等のフェーズ。
  ・各環境で動く様にする為の処理を行います。
## 2. Package
  ・一般的には幾つかのサーバーで動く筈なので、配布に適した形にします。
  ・また、配布に適した場所に置く事もあります。
## 3. Distribute
  ・大抵の場合、リリースツールを要する場合は複数のサーバーが存在すると思います。そのサーバーへ配布し、アプリケーションが起動可能な状態まで準備します。
## 4. Switch
  ・実際に動くフォルダに3.で配布したアプリケーションを配置します。
## 5. Restart/Reload
  ・アプリケーションを最新バージョンで起動します。

5.については1.の手前と5.の後ろでStop/Startで表現する場合があるかもしれないです。

これらは次に書いた内容で進化させることができます。

# シンプルリリース
まず最初は、アプリケーションを停止しても何をしてもいいから
自動化されたプロセスでリリースをしたいという要望から始めましょう。

最もシンプルなのは
1. アプリケーションを停止し
2. Build/Compileを行い
3. Distributeし
4. Switchし
5. アプリケーションを起動する

このステップを一つのShellスクリプト等で実装して安定させましょう。
このリリース方式のいい所は、どのステップにおいてイレギュラーな事が起きても
サービスが止まっているのでユーザー影響が出ない事です。
また、シンプルなので初めての言語や初めてのツールを使った場合の基礎ケースとして実装するには最も扱いやすいです。

# ダウンタイムを減らすリリース

`シンプルリリース` では、ダウンタイムが長い。Webサービスであろうが、社内サービスであろうが、ダウンタイムが減る方がいいと考えるのは自然だと思います。
あまり手をかけずに実現するには、順序を並び替えて実装するのが良いと思います。

1. Build/Compileを行い
2. Distributeし
3. アプリケーションを停止し
4. Switchし
5. アプリケーションを起動する

アプリケーションの停止が切り替えの直前になるだけです。
このタイミングでは`Switch`の実装が少し変更になります。

`シンプルリリース`では同じフォルダを使いまわしておけば良かったのですが、
`ダウンタイムを減らすリリース` においては、起動中のプロセスがある状態で、次のバージョンのアプリケーションをサーバーに配布しなければいけないので、このタイミングで複数バージョンがフォルダ上に存在可能な実装に変える必要があります。また、フォルダが変わる場合は起動スクリプト等も合わせて対応を進めましょう。

実際にバージョン毎のフォルダにパスを向けるのは手間が多いので、シンボリックリンクを使うのが適していると思います。

稼働するアプリケーションは常に `/opt/app/my_app` として、起動スクリプトやらなんやらはここを見させる。アプリケーションのバージョン違いは `/opt/app/version/my_app/version1_app`
`/opt/app/version/my_app/version2_app`
の様に別フォルダに名前を変えて配置するのが適切です。
シンボリックリンクの切り替えは`Switch`のステップで行います。

# 素早いリリース
`ダウンタイムを減らすリリース` と `素早いリリース` はどちらが先に要望されるかはわからないですが、大抵の場合はほぼ同時に求められると思います。

`シンプルリリース`には特に書いていませんでしが、処理を直列で実行している場合、
サーバーが増えるにつれて処理時間が長くなっていくので高速化を期待される事になると思います。

その場合は可能な限り並列処理化を進めていくのが良いでしょう。
具体的に高速化できる部分は `Build/Compile`の部分以外は全て可能です。
(場合によってはBuild/Compileも高速化の余地はあるかもしれない)

配布を`rsync` や `scp` でやってる場合は `prsync`や`pscp`や `pssh` を使う事で対応が可能だし `nohup command &` で多重化する事も考えられます。
(ちなみにWindowsでもPowerShellを使えば対応可能(面倒だが)であるので是非チャレンジしてみて下さい。)

また、このタイミングでShellを卒業し、`Capistrano` 等の並列実行をサポートしているツールに乗り換えるのもいいタイミングかもしれないです。

先日高速ファイル配信の(makuosan)[https://github.com/yasui0906/makuosan] を教えてもらったので是非使ってみたい。

# 閑話休題
私は、金融向けSIで働いた後にWeb系の会社へ転向しています。
SIにいた頃は、ミスなく安定してリリースされる事が最も重要であり、
それが守られるのであれば効率性や多様性を取る必要がありませんでした。
リリースツールにおいては、大体このステップくらいまでを実装しておけば安定運用が開始され、
また自分のできる責務を全うしたとみなされていたし、それ以上は過剰コストだと思われていました。

しかしまぁWeb系に来てみると視点が一つ変わりましたね・・・。
この後に提案するリリース手法についてはSIだろうがWebだろうが是非取り組んで欲しいと思います。
この取り組みによってより柔軟に、よりハイスピードでサービスを提供できると思います。

# ノーダウンタイムリリース

`ダウンタイムを減らすリリース` によってダウンタイムは1~2分まで減ったと思います。
しかし、ノーダウンタイムリリースというもう一つ上のステップが存在します。

これは実現が手間かもしれないが、実現する事で `リリースのタイミング(実施時刻や曜日)を意識しなくて良くなる。` というメリットを得る事ができます。

つまり不具合修正や新機能はいつでも本番に出せる様になります。
SIにいた頃は週末を待たねばならず、ユーザーは1週間不具合と付き合うか
または週の途中にサービス断を受け入れてリリースをするしかありませんでした。いずれにしろユーザーが不利益を受ける状態だったのです。
実際にどう実現するか、幾つかの例をあげていこうと思います。

## Webアプリケーション

ノーダウンタイムリリースはWebアプリケーションが圧倒的に相性が良いと思います。
ノーダウンタイムにおいても、旧プロセスの停止と新プロセスの起動、ユーザーからのコネクションを新プロセスに向ける。この作業は不可避であるが、Webアプリケーションの場合、ミドルウェアでこの辺りを吸収する事ができます。アプリケーション側に実装があまりいらないのが良いところですね。

### Ruby on Rails with Nginx
RoR with Nginxを例にして記載します。

RoRの場合はUnicornやPassengerをアプリケーションサーバーとして起動し、
その手前にNginxをWebサーバーとして配置する事が多いとでしょう。

幸いな事にUnicornやPassengerはGracefull restartを提供しています。
Unicornの場合、以下の感じでRestartが可能です。

```
## Switch
ln -fns /opt/app/version/my_app/version1_app /opt/app/my_app

## リスタートを投げる
kill SIGUSR2 pid
sleep 30

## 新しいMasterが起動してきたか確認する
kill 0 pid

## 古いMasterを殺す 
kill TERM pid
```

新しいWebRequestは新Masterに向き、新Masterは切り替わった後の`/opt/app/my_app` をメモリ上にロードして、リクエストを捌きます。

またPassenger with Nginxの場合は
`touch RAILS_ROOT/tmp/restart.txt` で済んだりもするので楽ちんです。

### Java with Jetty with Nginx
RoRの様な元からWebアプリケーションに特化した言語はいいよなぁ。と思う方も
こんな方法でも実現できます。

今回のサンプルはJavaWebアプリケーションをJettyで起動し、その手前にNginxがいる構成を例にします。Jetty単体ではGracefull restartをサポートしていないです。
* Javaの実装内部でシグナルを受けたらプロセスをリロードする。などを実装すればRoRみたいなことは対応可能ですが、簡単なWebアプリケーションにしては too muchな実装だと思うので他の方法を提案してみます。

2台のサーバー又は2並列でアプリケーションを起動しておいて、1つづつ最新かする事で対応できます。
Nginxにロードバランシング機能があるので、Nginxで1,2のプロセスに常時振り分けを行います。
リリースの際は1つ目のJettyを停止してSwitchしてJettyを起動します。これにより1台は最新になります。同じ要領で2つ目のJettyを対応すれば良いです。Nginxのロードバランシングの設定では、リクエストを送って落ちていたらもう一台に振りなおしをしてくれる機能があるので、ロストする事なく1つづつ切り替えていく事ができます。

Nginxのconfのサンプルは以下の感じです。

```
upstream myweb-lb {
    # 空いてる所優先ラウンドロビン
    least_conn;
    # 30秒間に3回つながらないと30秒間切り離される。
    # つながらなかった場合はNginxが別のサーバへ飛ばしなおしてくれるので1台でも生きていればエラーになる事はない。
    server myweb01:443  max_fails=3 fail_timeout=10s;
    server myweb02:443  max_fails=3 fail_timeout=10s;
}
server {
    listen       443 ;

    location / {
        proxy_pass https://myweb-lb;
        # HTTP Statusが500,502,503,504だったら別のサーバへ振り直す
        proxy_next_upstream error http_500 http_502 http_503 http_504 non_idempotent;
    }

    access_log      /var/log/nginx/myweb.access.log custom ;
    error_log       /var/log/nginx/myweb.error.log  warn ;
}

```
** 2台のサーバを想定したconfだが、1台で2プロセスあげる場合は同じホストでポート番号を分けるといいと思います。

## クラサバアプリケーション
クライアント＆サーバー型のアプリケーションの場合はどうすると良いのでしょうか？
先に断っておくと実際に取り組んだことはないの憶測で書いていきますが、
以下の方法で対応できるのではないでしょうか？

`Java with Jetty with Nginx` の構成と似た様に、TCP ロードバランサーを間において、
ロードバランサーの先に2台のマシンを配置しておくことで実現できそうです。
念の為、アプリケーション側ではリトライ処理を入れておくといいと思います。

またはアプリケーション側に複数の接続先情報を書いておくでもいいと思います。
1つ目がダメになったら2つ目にアクセスする様な実装を入れれば実現できそうです。

# 超高速リリース
`素早いリリース` に取り組んでいるとそこそこ早いリリースが組めたかと思われますが
それでもやはり、サーバー台数が100台を超えてきたり、配布物が重くなると遅さを感じる様になると思います。

この `感じる様になる` をなんとか `感じなくて済む様にする` のを本項目に書きます。
`素早いリリース`で現実的に可能な範囲でリリース処理は高速化されているとします。
可能な限り並列化を行い、サーバースペックも高くなっていても、台数が100台を超えると遅くなってきたと感じるのではないでしょうか。

その場合は、 `リリースにかかってる時間` の体感時間を減らす事で対応ができます。

この考え方の元はsorahさんの[Scalable Deployments](https://speakerdeck.com/sorah/scalable-deployments-how-we-deploy-rails-app-to-150-plus-hosts-in-a-minute)の考えそのままです。

通常、リリースというのはGithub等のソースコードリポジトリにコミット/プッシュした後から作業が始まります。担当者はコミット/プッシュが終わった事を確認して、ビルドボタンを押して、リリースが終わったらテストをして、終わったら、本番のビルドボタンを押す。という運用をしている事が多いです。
大抵、リリース時間中は `待ち` なので、その `待ち` の体感時間を減らすために
・テスト環境用のコミットが来たら、それで本番向けも `Build & Distribute`までしてしまう。
・テストがOKだったら、すでにその頃には `Switch`の手前まで作業が進んでいる。

という状況を作る事で、作業者はテストを終えた後、Switchだけを投げればいいのでリリースが一瞬で終わる様に体感させる事ができます。
GithubにPushされてMergeされたら人の作業に関わらず`Build & Distribute`を行う様にWebhookで連携するだけでも、非常にスピード感が出ます。
人を介すとそれほど、時間がかかったり、手間がかかったり、忘れられたりするのでそれがなくなるだけでも運用は手離れしていきます。

# 部分リリース
だいぶ長くなってきました。残り3項目はまだ実装が甘いので記述も甘くなる事が想定されるので、さらっと書いていきます。

今までやってきた様な仕組みを普通に捉える組織は非常にリリースが活発な組織だと想像されます。
そんな組織ではきっと、`難しい機能を作ったから一部のサーバーだけ最新に切り替えたい` という要望が出てくるだろうし、あれば便利です。

なのでWebサーバー1台だけ配布するオプションを用意しておくと良いと思います。

# 自動フックリリース

私が扱っている環境では、開発環境はMergeが動いた瞬間に `Build & Distribute & Switch` まで自動で動く様にしています。開発者からは常にテスト環境がリポジトリと同じになるというのは「今、環境ってどんな状態だっけ？」というのが忘れられて楽だそうです。

本番はMigrateが必要な時の考慮などもあり、簡単に出す様にするのは憚られるかもしれないが、
仕込むことが出来れば運用は非常に楽になりました。
また、`部分リリース` と組み合わせて、 `Merge` が来たら1台だけ出して様子を見て、
問題なければ手動で `Swtich` を残りの全台に行う。等も組む事が出来れば柔軟な運用につながると思います。

# あとがき
だらだらと書いてしまいましたが、リリースツールも突き詰めていくとまだまだ出来ることがあるし、新しく取り組むと、もっとこうしたいと課題がでるもんだなと思いました。(小並感)

リリースツールが優秀な組織は
変化に強く、変化のスピードが早い組織に慣れると信じているので
より一層がんばっていきたいと思います。

※　本当はBlue/Greenデプロイメントも書きたかったが、今回はアプリケーションレイヤーだけの話に止めたかったので書くのをやめた。