etcdとconfdでらくらく大規模設定項目デプロイライフ

我ながら意味不明なタイトルだ（笑）

# 解決したい問題
- 多数のサーバやサービスがある。
    - 環境ごとの設定（db名や接続ユーザ、パスワードなど）が各サービス（リポジトリ）ごとに保存されている。
        - 設定内容がバラバラに保存されてしまう。
        - 時に設定値が二重管理どころか多重管理になってしまう。
- 設定値をリポジトリに保存するときに暗号化しないといけない。
    - 差分管理ができず、デプロイプロセスが複雑化してしまう。

特にマイクロサービスアーキテクチャを採用している場合は、設定値が分散しがちなのでこれを何とかします。

# 解決方法
- 設定情報を一元管理する。
- 外部からアクセスできないローカルに設定専用gitサーバを立てて、そこのリポジトリに設定情報をplainで保存。
    - これで機密性と差分更新を両立。
- 環境ごとにetcdを立てる。
    - 設定値をこのetcdに投入する。
    - 本番環境のetcdはクラスタリングで冗長化。
- 各サーバにはconfdを導入。
    - 設定値をetcdからfetchして、各サービスごとの都合に合わせた設定ファイルを生成する。

![スクリーンショット 2017-04-22 17.41.00.png](https://qiita-image-store.s3.amazonaws.com/0/145575/ded89512-b917-c2cb-386e-a2449a61833e.png)

# 導入方法
例として、laravelの`.env`を生成するケースを考えます。
ローカルの設定専用gitサーバを立てるのは省略しますが、このgitは外部から閉じている場所に設置し、アクセスできる人は極力限定しましょう。

## etcd
Ubuntuならapt一発で導入可能です。

```bash
sudo apt-get install etcd
```

etcdは`2379/tcp`, `2380/tcp`の2つのポートを使用しますが、前者はconfdから接続する時に使用するポート、後者はetcd同士間のクラスタリングで使用するポートです。
デフォルトでは`127.0.0.1`のみをlistenしてますので、必要があれば、`ETCD_LISTEN_CLIENT_URLS`でlistenするIPを設定してください。

```ini:/etc/default/etcd
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
```
（この記事ではetcdのクラスタリング方法は省略します。）

データの投入は標準の`etcdctl`でもできますが、いちいちキーごとにデータを投入するのは面倒なので、yamlファイルから一括投入できるruby gemを導入します。

```
gem install etcd-tools --no-rdoc --no-ri
```

このgemでインストールされる`/usr/local/bin/yaml2etcd`を使ってデータを投入します。
まずは設定ファイルをyaml形式で書きましょう。

```yaml:/var/tmp/conf/v1/production.yml
env: production

app:
    name: laravel
    key: base64:y/Xg+4eS2+qBoVpl9izzCSDsvfEB7CgwkQu4FBbBl5I=
    debug: true
    log_level: debug
    url: http://example.comlocalhost

db:
    connection: mysql
    host: db.production.example.com
    port: 3306
    name: laravel
    user: user
    password: passowrd

redis:
    host: redis.production.example.com
    port: 6379
    password: nulll

mail:
    driver: smtp
    host: smtp.production.example.com
    port: 25
```

develop, stagingなどの環境についても、それぞれ設定ファイルも作成します。
これをリポジトリに入れて管理しましょう。

あとは、`yaml2etcd`でyamlデータを一括投入するだけ。
今回は環境ごとにetcdを立てることを前提としているので、各サーバの`/v1`ディレクトリにそれぞれインポートします。

```bash
yaml2etcd -u http://etcd.production.example.com:2379/ -r /v1 < /var/tmp/conf/v1/production.yml
yaml2etcd -u http://etcd.staging.example.com:2379/    -r /v1 < /var/tmp/conf/v1/staging.yml
yaml2etcd -u http://etcd.develop.example.com:2379/    -r /v1 < /var/tmp/conf/v1/development.yml
```

デプロイサーバ（Jenkinsなど）からインポートをするタスクを登録するとwebインターフェイスから実行できます。

投入されたデータの確認についても、標準の`etcdctl`ではなく`etc2yaml`を使うとyaml形式で一括ダンプができるのでこちらの方が便利だと思います。

```bash
etcd2yaml -u http://etcd.production.example.com:2379/ -r /v1
```

## confd
さて、このままではetcdに設定データが投入されているだけです。
各サーバで設定ファイルを生成するために、`confd`を導入しましょう。
confdについては[confd を試す](http://qiita.com/summerwind/items/aa63d2e0cd096fda418d)に詳しい解説があるので、ポイントだけ。

残念ながらUbuntuのパッケージは無いようなので、[開発元](https://github.com/kelseyhightower/confd)から[バイナリ](https://github.com/kelseyhightower/confd/releases)をダウンロードします。
これを`/usr/loca/bin`あたりにコピーして、ファイル名変更と実行属性をつけておきます。

```bash
cd /usr/local/bin
get https://github.com/kelseyhightower/confd/releases/download/v0.11.0/confd-0.11.0-linux-amd64

mv confd-0.11.0-linux-amd64 confd
chmod 755 confd
```

confdの設定ファイルを置くディレクトリを生成します。

```bash
mkdir -p /etc/confd/{conf.d,templates}
```

ここにエントリファイルと、設定ファイルのテンプレートファイルを配置します。

```ini:/etc/confd/conf.d/laravel.toml
[template]
prefix = "/v1"
src = "laravel.tmpl"
keys = [
    "/",
]
dest = "/path/to/.env"
owner = "www"
mode = "0600"
```

そして、etcdからとってきたデータを使って`.env`を生成する元となるテンプレートファイルを作成します。

```ini:/etc/confd/templates/laravel.tmpl
APP_NAME={{getv "/app/name"}}
APP_ENV={{getv "/env"}}
APP_KEY={{getv "/app/key"}}
APP_DEBUG={{getv "/app/debug"}}
APP_LOG_LEVEL={{getv "/app/log_level"}}
APP_URL={{getv "/app/url"}}

(以下略)
```

設定ファイルに`prefix="/v1"`を指定しているので、テンプレートファイル側に"/v1"を指定する必要はありません。
詳しい記述方法は[本家のドキュメント](https://github.com/kelseyhightower/confd/blob/master/docs/templates.md)を参照してください。

ここまで準備が済めば、あとはconfdを実行して設定ファイルを生成するだけです。
実行方法は2つあります。

### 1. confdをデーモンとして起動して、設定値が変化したら自動的に再生成する
confdに`-watch`オプションを指定すると、デーモンとして起動してenvdの値が変更されたら自動的に設定ファイルを再生成してくれます。

```
confd -backend etcd -node http://envd.production.example.com:2379 -watch
```

developmentやstaging環境などではこちらが便利でしょう。（デーモンの管理はsupervisord下に置くと楽です）
新しい設定ファイルが生成された時に実行されるコマンドをエントリファイルの`reload_cmd`に指定しておくと、新しい設定ファイルの反映も自動化させることも可能です。

### 2. confdをデーモンとして起動せず、手動で設定ファイルを生成する
confdに`-onetime`オプションを指定すると、ワンショットで設定ファイルを生成することができます。

```
confd -backend etcd -node http://envd.production.example.com:2379 -ontime
```

production環境の場合、最初はこちらを利用すると安全だと思います。

# Tips
## 設定パスはバージョニングしよう
設定のパスにAPIのURLと同じような`/v1`というバージョニングを入れています。これは何故でしょうか。

システムの拡大に伴い設定値のキーを追加していくと、キー構成がグシャグシャになってしいまい、キー構成を再構成したくなる日が必ずやってきます。
設定パスにバージョニングを入れておけば、新しく`/v2`というバージョンを追加することで、既存の`/v1`を使っているconfdに影響を与えることなく新しいキー構成を作成することができるようになります。

![スクリーンショット 2017-04-22 18.06.45.png](https://qiita-image-store.s3.amazonaws.com/0/145575/333ebff9-1ab8-c1ac-cca2-6c24e9b7a99e.png)

その後、`/v1`を参照しているconfdを`/v2`を参照するように修正すれば、サービスを動作させながら新しいキー構成に移行させることが可能になります。

![スクリーンショット 2017-04-22 18.06.48.png](https://qiita-image-store.s3.amazonaws.com/0/145575/c887f358-4495-cdaa-3045-9a4788de1a10.png)


## 設定ファイルの分割
設定ファイルが巨大になってくると修正が大変になってきます。
その場合は、`yaml2etcd`の`-r`オプションでインポートするパスを調整すると、階層ごとにyamlファイルを分割できます。

```bash
yaml2etcd -u http://etcd.production.example.com:2379/ -r /v1          < /var/tmp/conf/v1/production/main.yml
yaml2etcd -u http://etcd.production.example.com:2379/ -r /v1/database < /var/tmp/conf/v1/production/database.yml
```

それでは良い設定項目デプロイライフを。