nginx+circus+gojiによるgolang webアプリケーションの動作環境構築

golangを使ったwebアプリケーションの動作環境構築をansible playbookに落としました。Debian 8(jessie)向けです。

- [構築用playbook+Vagrantfile](https://github.com/reiki4040/playbooks)
- [gojiベースのサンプルWebアプリケーションコード](https://github.com/reiki4040/goweb)

## 参考環境(Mac想定)

vagrant上に構築するplaybookをgithubに上げています。**※HTTPS(TLS)の設定は入っていません。**

必要なもの

* virtualbox（公式からdmg）
* vagrant（公式からdmg）
* ansible（homebrewにて`brew install ansible`）

``` bash:構築
# cloneしてきて中に移動
git clone https://github.com/reiki4040/playbooks
cd playbooks

# 仮想マシン立ち上げ。(192.168.20.11で起動するので、かぶるとかの場合はVagrantfileいじってください）
# jessieのイメージを指定していて、大きめサイズが落ちてくるのでモバイル環境では注意
vagrant up

# 仮想マシンへのsshの設定追加
vagrant ssh-config >> ~/.ssh/config

# ansibleで自動構築
ansible-playbook -i local/inventory goweb.yml
  # apt-get/pip/nginx-buildあたりがしばらくかかります。
```

``` bash:動作確認
# 仮想マシンに入る
vagrant ssh

# ローカルからcurlで確認
curl http://localhost/api/hello
  # hello I'm goji! が返ってきます
```

## ポイントをいくつか

- nginx側でhttpsを受け取って、goji側へはhttpで接続（playbook構築分にはhttpsは入っていません）
- nginx - goji間はunix domain socketを使ってオーバーヘッドを低減
- goji+circusを使って、graceful restartを実現
- nginx, circusはsystemdで管理(debian jessie向け)
- これらをansibleを用いて自動構築

## 簡単なリクエストフローとプロセス管理イメージ

<img width="422" alt="リクエストフロー" src="https://qiita-image-store.s3.amazonaws.com/0/38236/fc98f0a3-9cda-18ad-1464-d63d122bec18.png">
<img width="287" alt="プロセス管理" src="https://qiita-image-store.s3.amazonaws.com/0/38236/540df787-7202-413b-8f29-6c9f1b23ad33.png">

## ポイント詳細

### nginxの設定

upstreamにunix domain socketのファイルパスを指定して、それをlocationディレクティブで、proxy_passを使って、gojiに投げています。unix domain socketの作成は、後述のcircusにて。

``` :nginx.conf抜粋
upstream goweb_upst {
    server unix:/var/run/goweb.sock;
}

# 中略

location ~^/api/.*$ {
    proxy_pass http://goweb_upst;
    proxy_set_header X-Forwarded-Host $host;
}
```

#### unix domain socketを使ってオーバーヘッドを低減

nginxとgojiを繋ぐ時に、unix domain socketを使用することで、通信のオーバーヘッドを減らします。ただし、nginxとgojiは同じサーバに同居する必要があります。

場合によっては、ややパフォーマンスを犠牲にし、nginxとgojiのサーバを分け、nginx N台＋goji M台といった別比率の構成にするのもアリです。

#### nginxを挟む理由

goji(golang)を世界と直接話させない理由は、フロントの処理をnginxに任せ、golangでわざわざ作らないためです。httpsを受ける程度であればいいんですが、簡易的なアクセス制御、静的配信、リバースプロキシ等、豊富な機能がnginxにはあり、一旦nginxでできることは、nginxに任せましょう。細かいことやりたくなった時にgolangで実装なり。


### goji + circus

gojiは、graceful shutdownを実装していて、これにcirucsのsocketを組み合わせることでgraceful restartが実現できます。

この辺りは参考に入れているリンクが詳しいのでそちらを参照していただければと思いますが、簡単に説明すると

- golangでsocketを作ると、どうしてもプロセスを再起動する際には閉じてしまう。
- circusがsocketを作り、渡すことで、golangプロセスの再起動時にもsocketが維持できる。
- なおかつcircusが新しいgolangプロセスを起動してから、古いプロセスを殺すので、途切れない（らしい）

#### gojiのserveの仕方に注意

graceful shutdownについての注意点は、gojiの`Serve()`,`ServeTLS()`または`ServeListener()`を使わないといけません。

gojiを使っていても、golang標準の`http.ListenAndServe()`等で起動すると、サーバ自体は起動して使えますが、graceful shutdownにならず、処理途中で切断されます。

#### Circusでgolangアプリケーションを動かす設定(circus.ini)

```
[circus]
statsd = 1

[watcher:goweb]
cmd = /opt/goweb/goweb -fd $(circus.sockets.web)
stop_signal = SIGINT
numprocesses = 1
use_sockets = True

stdout_stream.class = FileStream
stdout_stream.filename = /var/log/goweb/stdout.log
stdout_stream.refresh_time = 0.3
stdout_stream.max_bytes = 1073741824
stdout_stream.backup_count = 2

stderr_stream.class = FileStream
stderr_stream.filename = /var/log/goweb/stderr.log
stderr_stream.refresh_time = 0.3
stderr_stream.max_bytes = 1073741824
stderr_stream.backup_count = 2

[socket:web]
path = /var/run/goweb.sock
family = AF_UNIX
```

- `[watcher:goweb]`のgowebは、circus上で動かすプログラムの名称です。`circusctl reload goweb`などで使います。
- gojiのgraceful shutdownはSIGINTなので、`stop_signal = SIGINT`を指定しています。(circusデフォルトは、SIGTERM)
- 通信用のsocketは`[socket:web]`で宣言して、cmdの`-fd $(circus.sockets.web)`で渡しています。
- stdoutとstderrをファイルに出力しています。今回のサンプルだと、gojiのデフォルトの動作ログがstderrに出力されます。stdoutには私が適当に出したリクエスト時のmiddlewareのログが出ます。

### nginx, circusはsystemdで管理

debianは8(jessie)から、systemdに移行しました。init.d以下にスクリプトをおいても、start/stop/restartなどは動きますが、systemdのservice unitとして定義しています。内容は公式の案内通りなので、他のオプション等調べておきたいところです。

- [circusのservice unit](https://circus.readthedocs.org/en/latest/for-ops/deployment/)
- [nginxのservice unit](https://www.nginx.com/resources/wiki/start/topics/examples/systemd/#) 

これらを、`/etc/systemd/system/`以下に置くと、`systemctl start circus`といった感じで使えます。debian 7(wheezy)だと、`/etc/init.d/`と`service`コマンドの関係みたいなものです。

#### unitファイルを更新した時はsystemctl daemon-reload

systemd用のunitファイルを更新した場合は、`systemctl daemon-reload`を呼ばないと失敗します。最初単に`systemctl reload circus`呼んだらfailしました。

``` bash:エラーメッセージ
Warning: Unit file of circus.service changed on disk, 'systemctl daemon-reload' recommended.
Job for circus.service failed. See 'systemctl status circus.service' and 'journalctl -xn' for details.
```

### ansible

taskにtagをつけて、全部流す時（構築時）と設定ファイルの更新など、実行するタスクをわけることができます。

``` bash:初回構築時(タグなし)
ansible-playbook -i local/inventory goweb.yml
```

必要に応じて、個別にタグをつけて、reload/restartが必要な場合は、notifyをつけてhandlerを呼び出しています。

```bash:nginx.confのみ更新(update-nginx-confを指定)
ansible-playbook -i local/inventory goweb.yml -t update-nginx-conf
```

```yaml:task抜粋
- name: nginx | put nginx.conf
  tags:
    - nginx
    - update-nginx-conf
  template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf owner=root group=root mode=644
  notify:
    - reload nginx
```

```yaml:handler抜粋
- name: reload nginx
  service: name=nginx state=reloaded
```


単独で流す場合のタグ

| タグ | 内容 |
|:-----------|:------------|
|goweb|goweb構築のみ|
|update-goweb-bin|gowebのバイナリをアップデート|
|update-circus-ini|circus.iniをアップデート|
|update-circus-systemd-unit|circusのsystemd unitファイルをアップデート|
|nginx|nginx構築のみ|
|update-nginx-conf|nginx.confのアップデート|
|update-nginx-systemd-unit|nginxのsystemd unitファイルをアップデート|

開発中に何回も流す場合は、aliasやラッパースクリプトで省略することをお勧めします。

## まとめ

nginx + circus + gojiを使って、golangのWebアプリケーションを動作させることができます。基本的な動作環境が出来上がるので、あとはアプリケーションの開発を進めていくことができます。

ちなみに、graceful restart気にしなければ、nginxとgolangを直接systemdで動かすのもシンプルです。

ansibleを使って構築すると、毎回デプロイしたり変更したりする時にも使えるので、効率が良いです。playbook作るコストはありますが、何してたか忘れても大丈夫ですし、他の人に引き継ぐのも比較的容易かなと思います。

## 参考

- [Goで書いたWebサーバを本番(VPS)で動かす](http://kitak.hatenablog.jp/entry/2015/08/10/121223)
- [Goji のアプリケーションサーバーを Circus で管理する](
http://qiita.com/tchssk/items/e9222facefb1eb049596)
- [Go で書いたサーバーを管理するには circus が便利](
http://methane.hatenablog.jp/entry/2013/11/11/Go_%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%92%E7%AE%A1%E7%90%86%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF_circus_%E3%81%8C%E4%BE%BF%E5%88%A9)
- [Golang_ads_deliver // Speaker DeckGolang_ads_deliver // Speaker Deck](https://speakerdeck.com/catatsuy/golang-ads-deliver?slide=9)
- [Circus Deployment](https://circus.readthedocs.org/en/latest/for-ops/deployment/)
- [Systemd入門(4) - serviceタイプUnitの設定ファイル](http://enakai00.hatenablog.com/entry/20130917/1379374797)
- [nginx systemd service file](https://www.nginx.com/resources/wiki/start/topics/examples/systemd/#)