ngx_dynamic_upstreamでnginxのアップストリームを動的に変更する

# nginxのアップストリームを変更する

通常nginxでアップストリームの変更をするには`nginx.conf`を変更した後、nginxを`reload`ないしは`restart`しなければなりません。例えば以下のようにアップストリームが定義されている場合、

```nginx:upstream_backends.conf
upstream backends {
    server 127.0.0.1:6001;
    server 127.0.0.1:6002;
    server 127.0.0.1:6003;
}
```

このアップストリームから`127.0.0.1:6003`を外すには以下のように該当行に`down`と書いて`reload`します。(あるいはコメントアウトする)

```nginx:upstream_backends.conf
upstream backends {
    server 127.0.0.1:6001;
    server 127.0.0.1:6002;
    server 127.0.0.1:6003 down;
}
```

あるいはアップストリームの各サーバのパラメータ(`weight`、`max_fails`、`fail_timeout`)を変更するのにも`reload`が必要です。(下記の各パラメータはデフォルト値)

```nginx:upstream_backends.conf
upstream backends {
    server 127.0.0.1:6001 weight=1 max_fails=1 fail_timeout=10;
    server 127.0.0.1:6002 weight=1 max_fails=1 fail_timeout=10;
    server 127.0.0.1:6003 weight=1 max_fails=1 fail_timeout=10;
}
```

もっと言うとアップストリームにサーバを動的に追加することはできないので、サーバを追加するにはやっぱり手で`nginx.conf`を修正した後に`reload`が必要です。

```nginx:upstream_backends.conf
upstream backends {
    server 127.0.0.1:6001;
    server 127.0.0.1:6002;
    server 127.0.0.1:6003;
    server 127.0.0.1:6004; # 追加したサーバ
}
```

という具合に通常nginxのアップストリームを変更するにはnginxの`reload`や`restart`が必要です。
これで十分な場合も多いですが、一方でサービス要件によってはELBのようにアップストリームを動的に変更したいというニーズもあることでしょう。

# nginxのアップストリームを動的に変更する

nginxはマルチプロセスアーキテクチャなので`nginx.conf`を変更せずにオンザフライでアップストリームを変更するには何らかの方法でアップストリームの情報を各ワーカプロセス間で共有する必要があります。

アップストリームの情報を各ワーカプロセス間で共有する方法には例えば以下のものが挙げられます。

 1. NGINX Plusを購入する
 1. memcached等の外部KVSでアップストリームを共有する
 1. ngx_luaやngx_mrubyで頑張る
 1. アップストリームを共有メモリ化する

## NGINX Plusを購入する

有償版のnginxであるNGINX Plusには[`ngx_http_upstream_conf_module`](http://nginx.org/en/docs/http/ngx_http_upstream_conf_module.html)というアップストリームを動的に操作するためのHTTPインタフェースモジュールが付属しており、CLIで簡単にアップストリームを操作できます。

NGINX Plusについては2年近く前の記事ですが、@harukasanが軽くまとめていて参考になります。

[NGINX Plusでできることまとめ](http://qiita.com/harukasan/items/5123f797a876696b343e)

## memcached等の外部KVSでアップストリームを共有する

そもそもnginxのプロセス内にアップストリームのデータを持つのではなくmemcached等の別サーバプロセスでアップストリームの情報を共有するという方法はどうでしょうか。しかし、この方法だとアップストリームの管理を自前で行わなければならないのでnginxが元々サポートしている以下の機能を利用するのが非常に難しくなります。(というか無理？)

 * アップストリーム内サーバへのロードバランスとそのアルゴリズム(e.g. least_conn、consistent-hashing)の利用
 * アップストリーム内サーバの重み付けや不調サーバのアップストリームからの取り外し

あと、そもそも外部KVSサーバといい感じに通信してロードバランスするモジュール的なものを書く必要があってなかなか大変です。

## ngx_luaやngx_mrubyで頑張る

ngx_luaやngx_mrubyを利用すると`nginx.conf`に特殊なロジックを埋め込んだり、Cではなくスクリプト形式でnginxのモジュールを書くことができます。外部KVSサーバと通信してアップストリームの情報をやりとりするのも幾分簡単になるでしょう。しかし、やはりアップストリームの管理を自前で行わなければならないので煩雑なことにかわりはありません。

## アップストリームを共有メモリ化する

先月リリースされたnginx-1.9.0の[CHANGES](http://nginx.org/en/CHANGES)を見ると以下の一文があります。

```
 Feature: the "zone" directive inside the "upstream" block.
```

TCP Load Balancing機能が目立ってたのとあまりにもシレッと書かれていたので1.9.0がリリースされてから1週間ぐらい気付いてなかったのですが、NGINX PlusだけでなくOSS版でもアップストリームを共有メモリ化できるようになりました。(NGINX Plusのソースコードは公開されていないのであくまで推測になります)

こんな感じに`upstream`コンテキスト内で`zone`ディレクティブを定義します。

```nginx:upstream_backends.conf
upstream backends {
    zone zone_for_backends 128k;
    server 127.0.0.1:6001;
    server 127.0.0.1:6002;
    server 127.0.0.1:6003 down;
}
```

これでnginxのアップストリームを各ワーカプロセス間で共有できるようになります。

# ngx_dynamic_upstreamでnginxのアップストリームを動的に変更する

前置きが長くなりました。さっきも言ったようにnginx-1.9.0から`upstream`コンテキストで`zone`ディレクティブが利用可能になったのでnginxのアップストリームを共有メモリに格納してワーカプロセス間でアップストリームの情報を共有できるようになりました。しかし、OSS版ではそもそもnginxのアップストリームを動的に変更するためのモジュールがありません。


なので、HTTPインタフェースでアップストリームを動的に変更できるモジュールを書いてみました↓

https://github.com/cubicdaiya/ngx_dynamic_upstream

## Quick Start

[ngx_dynamic_upstream](https://github.com/cubicdaiya/ngx_dynamic_upstream)は`dynamic_upstream`というただひとつのディレクティブを持ちます。このディレクティブは`location`コンテキストのみで利用可能です。

```nginx:dynamic_upstream.conf
upstream backends {
    zone zone_for_backends 128k;
    server 127.0.0.1:6001;
    server 127.0.0.1:6002;
    server 127.0.0.1:6003;
}

server {
    listen 6000;

    location /dynamic {
        allow 127.0.0.1;
        deny all;
        dynamic_upstream;
    }

    location / {
        proxy_pass http://backends;
    }
}
```

[ngx_dynamic_upstream](https://github.com/cubicdaiya/ngx_dynamic_upstream)自体はアクセス制御機能を持たないので`allow`や`deny`でIPアドレス制限等をするのを忘れずに。

## アップストリームの一覧を取得する

`upstream`パラメータに`zone`名を指定するとアップストリームの一覧を返します。

```console
$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends"
server 127.0.0.1:6001;
server 127.0.0.1:6002;
server 127.0.0.1:6003;
$
```

## アップストリームからのサーバの一時的な取り外し/復帰

`down`と`server`パラメータを組み合わせることでアップストリームから特定のサーバを一時的に取り外し可能です。

```console
$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&server=127.0.0.1:6003&down="
server 127.0.0.1:6001 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6002 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6003 weight=1 max_fails=1 fail_timeout=10 down;
$
```

取り外したサーバを復帰させるには`up`パラメータを指定します。

```console
$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&server=127.0.0.1:6003&up="
server 127.0.0.1:6001 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6002 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6003 weight=1 max_fails=1 fail_timeout=10;
$
```

# まとめ

nginxのアップストリームを動的に変更する方法と戦略について解説しました。[ngx_dynamic_upstream](https://github.com/cubicdaiya/ngx_dynamic_upstream)はサーバの動的な追加や削除の機能もありますが、`up`と`down`だけでもかなり便利なのではないかと思います。


なお、GWの暇な時に作ったばかりなので、

```
This module is significantly experimental and still under early develpment phase.
```

という感じでまだ絶賛開発中です。**追記：2015/06/09に「production ready」になりました。**