Blog
Typesafe Configを使って環境設定を切り替えしてみる
こんにちは。DynalystのHanです。Dynalystではアプリケーションの設定にTypesafe Configを使っています。
そこでProductionやテスト環境毎にアプリケーション設定をどう切り替えてるのかについて簡単に紹介したいと思います。
Typesafe Configを選んだ理由
Scalaをやる前まではJavaエンジニアだったのでアプリケーションの設定ファイルなら何も考えず.properties
に定義するのは定番だけど、せっかくScalaをやり始めてるのでScalaの開発ではどのライブラリがよく使われているのかを調べていました。
そこで発見したのがTypesafe Configです。
.properties
も勿論対応してますし、ResourceBundleより便利な機能がたくさんある。- そしてAkka, Play, Slick, SprayなどScalaでは有名なOpenSource Projectにもよく使われている。
- HOCONが見やすい
- あとはTypesafeという安定なネームバリュー!
Typesafe ConfigのソースコードはJavaで書かれています。
Typesafe Configの使い方
Typesafe Configの基本的な使い方やRuntime時の動き方は公式レポジトリのREADME.mdに詳しく書いてあります。
たとえば、以下のような設定ファイルがあった場合にConfigオブジエクトから値を取得することができます。
1 2 3 4 5 6 |
# file: application.conf app { message = "hello world" numbers = [10, 20, 30] } |
1 2 3 4 5 6 7 8 |
import com.typesafe.config._ val config = ConfigFactory.load() val message = config.getString("app.message") // message: String = hello world val numbers = config.getIntList("app.numbers") // numbers: java.util.List[Integer] = [10, 20, 30] |
値はシステム環境変数から取得することもできます。たとえばSCALA_HOME
といったシステム環境変数があるとしたら
1 2 3 4 5 6 7 8 9 |
# file: application.conf # export SCALA_HOME=/usr/local/Cellar/scala/2.11.4 app { scala.home = ${SCALA_HOME} message = "hello world" numbers = [10, 20, 30] } |
1 2 |
val home = config.getString("app.scala.home") // home: String = /usr/local/Cellar/scala/2.11.4 |
また、別ファイルに定義されている設定をinclude
して使うこともできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# file: global.conf global { message = "hello global.conf" numbers = [100, 200, 300] } # file: application.conf include classpath("global.conf") app { # scala.home = ${SCALA_HOME} message = "hello world" numbers = [10, 20, 30] global { message = ${global.message} numbers = ${global.numbers} } } |
1 2 3 4 5 |
val message = config.getString("app.global.message") // message: String = hello global.conf val numbers = config.getIntList("app.global.numbers") // numbers: java.util.List[Integer] = [100, 200, 300] |
ここでincludeするときにclasspathというメッソドにinclude先のファイル名を渡していたのでこれを拡張もしくはあるinterfaceを実装すれば環境毎設定ファイルを読ませることができてそれぞれの値で置換できるんじゃないかと思い、ソースコードをみたところConfigIncluder
というinterfaceと実装クラス発見!
- ConfigIncluderClasspath.java – include classpath
- ConfigIncluderFile.java – include file
- ConfigIncluderURL.java – include url
Environment Protocolを定義して環境設定切り替え
urlからも設定ファイルがincludeできるのが分かったのでRuntimeの時にどの環境で実行されてるかを
認知できるcustom protocolを定義して以下のように書けることができます。
1 2 3 4 5 |
# env protocolはsystem propertyの`-Denv=${SYSTEM_ENV}`から{environment}を取得する # {environment}は[production|staging|development]のどれかになる # {environment}/env.confをincludeする include url("environment:///<ENVIRONMENT>/env.conf") |
Environment Protocol定義
custom protocolを定義するには以下のinterfaceを実装してJVMがそのprotocolを認識できるように設定する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 |
import java.net.{URLStreamHandler, URLStreamHandlerFactory} class EnvironmentURLStreamHandlerFactory extends URLStreamHandlerFactory { def createURLStreamHandler(protocol: String): URLStreamHandler = { protocol match { case p if p.startsWith("environment") => new EnvironmentURLStreamHandler case _ => null } } } |
1 2 3 4 5 6 7 8 9 |
import java.net.{URL, URLConnection, URLStreamHandler} class EnvironmentURLStreamHandler extends URLStreamHandler { def openConnection(url: URL): URLConnection = { new EnvironmentConnection(url) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import java.io.InputStream import java.net.{URL, URLConnection} import com.typesafe.scalalogging.StrictLogging class EnvironmentConnection(url: URL) extends URLConnection(url) with StrictLogging { def connect(): Unit = {} override def getInputStream: InputStream = mkInputStream(url) private def mkInputStream(url: URL): InputStream = { val path = url.getPath val env = resolveEnvironment logger.info(s"env(path: $path) - $env") getClass.getResourceAsStream(path.replace("<ENVIRONMENT>", env)) } private def resolveEnvironment: String = { val property = Option(System.getProperty("environment")) val system = Option(System.getenv("SYSTEM_ENV")) (property, system) match { case (Some(env), _) => env // -Denvironment=[production|staging|development] case (_, Some(env)) => env // export SYSTEM_ENV=[production|staging|development] case _ => "development" // default environment } } } |
動作確認
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# file: development/env.conf env { message = "hello development environment!" } # file: staging/env.conf env { message = "hello staging environment!" } # file: production/env.conf env { message = "hello production environment!" } # file: application.conf # export SCALA_HOME=/usr/local/Cellar/scala/2.11.4 include classpath("global.conf") include url("environment:///<ENVIRONMENT>/env.conf") app { # scala.home = ${SCALA_HOME} message = "hello world" numbers = [10, 20, 30] global { message = ${global.message} numbers = ${global.numbers} } env { message = ${env.message} } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import java.net.URL import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.StrictLogging object EnvironmentConfigMain extends StrictLogging { /** * {{ * sbt run * sbt 'run development' * sbt 'run staging' * sbt 'run production' * }} * @param args environment value[development|staging|production] */ def main(args: Array[String]): Unit = { URL.setURLStreamHandlerFactory(new EnvironmentURLStreamHandlerFactory) System.setProperty("environment", args.headOption.getOrElse("development")) val config = ConfigFactory.load() logger.info(config.getString("app.env.message")) } } |
これでそれぞれ環境を引数でsbt run
すると
1 2 3 4 5 6 |
[typesafe-config-example (master=)]$ sbt run ... [info] Running EnvironmentConfigMain 14/12/18 13:42:35.848 INFO EnvironmentConnection - env(path: /<ENVIRONMENT>/env.conf) - development 14/12/18 13:42:35.858 INFO EnvironmentConfigMain$ - hello development environment! [success] Total time: 1 s, completed Dec 18, 2014 1:42:35 PM |
1 2 3 4 5 6 |
[typesafe-config-example (master=)]$ sbt 'run development' ... [info] Running EnvironmentConfigMain development 14/12/18 13:47:24.934 INFO EnvironmentConnection - env(path: /<ENVIRONMENT>/env.conf) - development 14/12/18 13:47:24.943 INFO EnvironmentConfigMain$ - hello development environment! [success] Total time: 3 s, completed Dec 18, 2014 1:47:24 PM |
1 2 3 4 5 6 |
[typesafe-config-example (master=)]$ sbt 'run staging' ... [info] Running EnvironmentConfigMain staging 14/12/18 13:47:33.243 INFO EnvironmentConnection - env(path: /<ENVIRONMENT>/env.conf) - staging 14/12/18 13:47:33.253 INFO EnvironmentConfigMain$ - hello staging environment! [success] Total time: 1 s, completed Dec 18, 2014 1:47:33 PM |
1 2 3 4 5 6 |
[typesafe-config-example (master=)]$ sbt 'run production' ... [info] Running EnvironmentConfigMain production 14/12/18 13:47:41.401 INFO EnvironmentConnection - env(path: /<ENVIRONMENT>/env.conf) - production 14/12/18 13:47:41.414 INFO EnvironmentConfigMain$ - hello production environment! [success] Total time: 1 s, completed Dec 18, 2014 1:47:41 PM |
環境毎に値を取得することができました!
まとめ
アプリケーション設定をしていた時にapplication.confの中身が${database.url}
なっていてあまり直感的なじゃないというのとテストのためにもCustom Protocolを設定しないといけないのはデメリットかもしれません。
Dynalystではテストにspecs2を使っていてテスト環境用の値が取得されるようにtrait EnvResolvedSpecification
をよいしています。
またサーバーやバッチにはtrait EnvironmentProtocolResolver
をよいしてmainクラスにmix-inして使ってます。
本文で使っていた例はGithubに公開されているので違うやりかたで環境設定を解決している方は是非教えてください!
Author