継続的デプロイにおける設定管理の方針
継続的にコードをリリースする開発環境では、同一のアーティファクトが、CIサーバによって、機能テスト、パフォーマンステスト、脆弱性テスト、そして本番環境へと連続して移行していきます。アプリケーションが新しい設定項目を追加した際に、各々の環境に対応する値を手動で設定するのは手間がかかりますし、環境間の一貫性を崩す原因にもなります。PuppetやChefで設定管理をスクリプト化するのは一つの方法ですが、アプリケーションのデベロッパーとDevOpsが分かれている場合、コミュニケーションミスによるエラーが生じることもありますし、リポジトリがアプリケーションのソースコードと分かれていると、ブランチから生成されたアーティファクトを実行する際に、設定値を変えなければいけないシチュエーションもあるでしょう。私の所属している開発チームには、チーム専属のDevOpsがいて、一緒に作業をしますが、それでも設定ミスが時々起こります。
事前にわかっている設定値は、ソースコードの中に埋め込み、同一のソースコード管理に入れると、アプリケーションの開発者はフローの中で対応できます。パスワードやトークンなど、秘匿しなければいけない情報は、DevOpsが暗号化してから開発者に渡し、それぞれの環境にデプロイされているキーを使って復号するライブラリを使用することで、ほとんど全ての静的な設定情報をソースコードにバンドルすることができます。
一方で、アプリケーションのライフサイクルとは別に設定情報をアップデートしなければいけない場合や、Herokuのバックエンドサービスを動的に定義したい場合などは、環境変数を用いて設定を得たい場合もあるでしょう。
更に、動的に設定情報をアップデートしたい場合で、それを複数のノードに反映したい場合は、Zoo KeeperやConsulから動的に取得することになるでしょう。
nomadとは
nomadは環境間で異なる設定情報を持たせることのできる、設定管理のclojureライブラリです。Ruby on RailsのRAILS_ENVのように、キーワードで設定値のグループを切り替えたり、The Twelve-Factor Appで提唱されている、環境変数を用いて環境間で異なる値をフィードするスタイルに対応することも可能にしています。
設定ファイルはURL経由で取得するので、ここではEDNをファイルとしてクラスパスに含めています。外部からEDNを供給して、複数ノードに対して集中管理ができるかどうか、今後試してみたいと思っています。
基本の使い方
- Leiningenを使っていれば、下記のようにnomadの依存関係を追加します。
(defproject com.github.k2n/my-app "0.1.0-SNAPSHOT"
...
:dependencies [[org.clojure/clojure "1.6.0"]
...
[jarohen/nomad "0.7.0"]]
...
-
EDNで設定ファイルを作成します。
resources/my.edn
などと名前をつけて保存しましょう。
{:service-url-context "/v1/catalog/service/"
:kv-url-context "/v1/kv/"
:http-client-options {:timeout 500}
:consul-url-base "http://localhost:8500"
:riemann-hostname "localhost"}
- コードでは、nomadライブラリを
require
して、
(ns my-app
(:require [org.httpkit.client :as http]
...
[nomad :refer [defconfig]]
...
- 設定ファイルをロードします。
(defconfig config (io/resource "my.edn"))
-
config
からキー値の値を取り出します。
(:consul-url-base (config))
応用編
キャッシュ
nomadは可能な場合は設定をキャッシュし、設定ファイルの値が変わった場合には自動的にリロードします。
ホスト固有の値を設定する
:nomad/hosts
の下にホスト名と、キーのマップを定義することで、ホストごとに異なる値を設定することができます。
{:nomad/hosts {"my-laptop" {:key1 "dev-value"}
"my-web-server" {:key1 "prod-value"}}}
同一ホスト内の複数インスタンス
NOMAD_INSTANCE=DEV2 lein ring server
のように、環境変数経由でインスタンス名を指定することで、同一ホスト内で複数の設定値を切り替えることができます。
{:nomad/hosts
{"my-laptop"
{:nomad/instances
{"DEV1"
{:data-directory "/home/me/.dev1"}
"DEV2"
{:data-directory "/home/me/.dev2"}}}}}
複数ホストのグループ化
RailsのRAILS_ENV
のように、環境名を環境変数経由で指定することで、異なるホスト間で同一の設定を共有することができます。
{:nomad/environments
{"dev"
{:send-emails? false}
"prod"
{:send-emails? true}}}
例えば、prod
設定を使用させるためには、下記のように指定します。
NOMAD_ENV=prod lein ring server
Nomad リーダーマクロ
設定ファイル中でさらに別の設定ファイルを読み込ませることができます。
{:nomad/hosts
{"my-host"
{:data-directory #nomad/file "/home/james/.my-app"}}}
スニペット
複数の環境で、同一の設定を共有したい場合、あるいは複数の環境で定義される、同種の定義を一箇所にまとめたい場合、スニペットを用います。
{:nomad/snippets
{:databases
{:dev {:host "dev-host"
:user "dev-user"}}
:prod {:host "prod-host"
:user "prod-user"}}}
このデータベース設定を"my-host"と"prod-host"で共有します。
{:nomad/snippets { ... as before ... }
:nomad/hosts
{"my-host"
{:database #nomad/snippet [:databases :dev]}
"prod-host"
{:database #nomad/snippet [:databases :prod]}}}
Nomadのテスト
my-config
に下記の設定がロードされているものとして、
{:nomad/environments
{"dev"
{:send-emails? false}
"prod"
{:send-emails? true}}}
nomad/with-location-override
で、"dev", "prod"の環境を選んでテストすることができます。
(defconfig my-config (...))
(:send-emails? (my-config))
;; => false
(nomad/with-location-override {:environment "prod"}
(:send-emails? (my-config)))
;; => true
nomad/with-location-override
は、:hostname
, :environment
, :instance
のキーを取ることができます。
プライベート設定
リーダーマクロでクラスパスの外にある、各環境のファイルシステムから設定ファイルを読み込み、その値で既定値を上書きすることが可能です。
config/my-config.edn
{:nomad/hosts
{"my-host"
;; Using the '#nomad/file' reader macro
{:nomad/private-file #nomad/file "/home/me/.my-app/secret-config.edn"
{:database {:username "my-user"
:password :will-be-overridden}}}}}
/home/me/.my-app/secret-config.edn
{:database {:password "password123"}}
コード
(ns my-ns
(:require [nomad :refer [defconfig]
[clojure.java.io :as io]]))
(defconfig my-config (io/resource "config/my-config.edn"))
(get-in (my-config) [:database])
;; -> {:username "my-user", :password "password123"}
環境変数経由で設定
設定ファイル
{:db-password #nomad/env-var "DB_PASSWORD"}
コマンドライン
DB_PASSWORD="password-123" lein repl
さらに、値をString以外で読み込みたいときは、
{:port #nomad/edn-env-var "PORT"}
PORT=3000 lein repl
ポート番号がLongで扱われていることに注目。
(defconfig config (...))
(:port (config))
;; -> 3000
(type (:port (config)))
;; -> java.lang.Long
printf的にフォーマットして環境変数を埋め込む
#nomad/envf
は、java.util.Formatter準拠のテンプレートと環境変数名のベクターを引数にとって、下記のように記述することができる。
{:url #nomad/envf ["http://%s:%s/api/1.0/" API_URL API_PORT]}
デフォルト値
0.8.0ブランチから、環境変数が設定されていない場合のデフォルト値を指定することができる。Github Issueでは、0.7.1-SNAPSHOT
に含まれているとのコメントがあるが、実際は0.8ブランチにしか入っていない。 0.8系はまだベータ版しかリリースされていないもよう。
{:hostname #nomad/env-var ["HOST" "localhost"]}
{:port #nomad/edn-env-var ["PORT" 3000]}
設定の優先順位
- プライベートインスタンス値
- パブリックインスタンス値
- プライベート環境変数値
- パブリック環境変数値
- プライベートホスト値
- パブリックホスト値
- :nomad/hosts外のプライベート値
- :nomad/hosts外のパブリック値