Ktorでローカルデバッグ用に設定を切り替える

初めに

この記事はKtorの利用者が見に来ることを想定しているので、Ktorに関する説明等は省きます。
ここでの前提は以下の通り

  • IntelliJ IDEA 2019.1
  • Gradle 5.3.1
  • Ktor 1.2.0
  • Kotlin 1.3.31
  • JetBrains Exposed

まぁ多分、多少バージョンが違っても問題なく動くと思います。
あと、ExposedはRDBMSをExposed使って操作する時用のコードを例示するためなので、メイン部分には関係ありません。

KtorでHttpsへの自動リダイレクトやHSTSを設定していたり、データベース周りの設定の関係上、ローカルでのデバッグができなくて悩んだ事はありませんか?俺はあります。なので、その解決法を考えたので、Ktorがより普及するように願いつつその方法をこの記事で共有します。
また、ここではMariaDBを使用する想定ですが、他のRDBMSでも基本的には一緒だと思います。

方法

概要を説明すると、

  1. デバッグ時用の設定ファイルを用意する
  2. 起動時にコマンドラインからロードする設定ファイルを切り替える
    以上です。

1.設定ファイルの準備

まず、本番環境用のapplication.confの例です。

ktor {  
  deployment {  
    port = 8080  
    port = ${?PORT}  
  }  
  application {  
    modules = [app.example.server.ApplicationKt.module]  
  }  
}  

example {  
  db {  
    url = "jdbc:mariadb://192.168.11.100:3306"  
    user = "root"  
    pass = "root"  
  }  
}  

あくまで例なので、rootログインやパスワードへの突っ込みは無しでお願いします。
ちなみに、example句の名前は何でも良いです。というより、他の設定とかぶらないように開発しているアプリケーション名にしておくのがいいでしょう。

次に、テスト用のtest.confの例です。

ktor {  
  deployment {  
    port = 8080  
    port = ${?PORT}  
    watch = [server]  
  }  
  application {  
    modules = [app.example.server.ApplicationKt.module]  
  }  
}  

example {  
  test = true  
  db {  
    url = "jdbc:mariadb://localhost:3306/exampletest"  
    user = "test"  
    pass = "ExampleApp"  
  }  
}  

重要なのはtest=trueを追記していることです。
あと、ktor.watchのプロパティは使用するならテスト用環境だけにしておきましょう。
オートリロードはパフォーマンスの低下を招くらしいので。
それ以外はデバッグ環境に合うように設定を適当に変えましょう。

ちなみにこの設定ファイルですが、お察しの通り好きな内容を書き込めます。
他に何か設定事項があるなら書き込めば良いと思います。

2.コードの変更

Application.module()等のmodules=[]で指定したモジュールの先頭行に、以下の記述を追加します。

val testing = environment.config.propertyOrNull("example.test")?.getString()?.toBoolean() ?: false  

コンフィグファイルのプロパティはこのようにして取得することができます。
階層構造になっているものは、.でつなぐことでアクセスすることができます。
そして、

if(testing) {  
    // テスト時のみの処理。  
    // 例  
    routing {  
        if (testing) {  
            get("/hello") {  
                call.respond("Hello, World!")  
            }  
        }  
    }  
}  
if(!testing) {  
    // 本番環境のみの処理  
    install(HttpsRedirect) {  
            // The port to redirect to. By default 443, the default HTTPS port.  
            sslPort = 443  
            // 301 Moved Permanently, or 302 Found redirect.  
            permanentRedirect = true  
        }  
    }  
}  

このように書けばテスト時/実行時で処理を分けれます。
(ここでif~elseを使っていないのはわざとで、例に出したroutingとFeatureのinstallは離れた場所でやることが多いからです。)

あと、データベース関係はこのようにすればいいんじゃないでしょうか。

fun initDatabase(dbUrl: String, dbUser: String, dbPassword: String, isTest: Boolean) {  
    Database.connect(  
            url = dbUrl,  
            driver = "org.mariadb.jdbc.Driver",  
            user = dbUser,  
            password = dbPassword  
    )  
    transaction {  
        SchemaUtils.create(  
                UserTable  
        )  
        if (isTest) {  
            addLogger(StdOutSqlLogger)  
        }  
    }  
}  

fun cleanupDatabase() {  
    transaction {  
        SchemaUtils.drop(  
                UserTable  
        )  
    }  
}  

Application.setupDB {  
    // DBの初期化  
    environment.config.let {  conf ->  
        initDatabase(  
                conf.property("reiwa.db.url").getString(),  
                conf.property("reiwa.db.user").getString(),  
                conf.property("reiwa.db.pass").getString(),  
                testing  
        )  
    }  
    // デバッグ時は終了時にDBの内相の消去  
    if (testing) {  
        environment.monitor.subscribe(ApplicationStopping) {  
            cleanupDatabase()  
        }  
    }  
}  

テスト時は、全てのSQLのクエリを標準出力に出力し、Applicationの終了時にDBの内容を消去するようにしています。
テスト時かそうじゃないかのプロパティがあればその辺は自由にできますね。

3.実行

下図のように、実行時の引数に-config=テスト用のconfigファイルのbuild.gradleからの相対パスの形で指定します。

最後に

結局configファイルを引数で指定してるだけなので、やろうと思えば、本番用・ローカルデバッグ用・自動テスト用みたいな感じで3つ以上に分けることもできます。本番用をapplication.confじゃなくてdeploy.confみたいにすることも可能です。
そこら辺は自由にやればいいと思います。