CLOVER🍀

That was when it all began.

TerraformでNode.js+PostgreSQLなアプリケーションをHerokuにデプロイする

これは、なにをしたくて書いたもの?

Terraformの勉強にと、Herokuを使って、アプリケーションとデータベースをデプロイする超簡単な例を作って試してみようと。

お題

Herokuのフリープランで利用可能なPostgreSQLを使い、以下のサンプルとほぼ同等なアプリケーションをHerokuにデプロイ
してみます。

Connecting in Node.js

information_schema、tables内のtable_schema、table_nameを取得するだけの、簡単なサンプルです。

環境

今回の環境は、こちら。

$ terraform version
Terraform v0.11.13

TerraformのHeroku Providerは、1.9.0を使用します。

また、お題でも書きましたが、Herokuはフリープランで使っています。

Terraformのリソース定義ファイル

それでは、最初にTerraformのリソース定義ファイルを作成します。

今回は、アプリケーションをデプロイするので「heroku_app」と「heroku_build」を使い、

Heroku: heroku_app - Terraform by HashiCorp

Heroku: heroku_build - Terraform by HashiCorp

PostgreSQLのデプロイには「heroku_addon」を使用します。

Heroku: heroku_addon - Terraform by HashiCorp

利用するHeroku Postgresのプランは、「hobby-dev」です。

Heroku Postgres - Add-ons - Heroku Elements

で、作成したメインのリソース定義ファイル。

heroku-app-postgresql.tf

provider "heroku" {
  email = "${var.heroku_email}"
  api_key = "${var.heroku_api_key}"
}

resource "heroku_app" "app_postgresql" {
  name = "app-postgresql"
  region = "us"
}

resource "heroku_build" "app_postgresql_build" {
  app = "${heroku_app.app_postgresql.name}"

  source = {
    url = "https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz"
  }
}

resource "heroku_addon" "postgresql" {
  app = "${heroku_app.app_postgresql.name}"
  plan = "heroku-postgresql:hobby-dev"
}

「heroku_build」でアプリケーションのソースコードアーカイブを参照していますが、これは後でまた。

PostgreSQLのリソース定義は、こちらですね。

resource "heroku_addon" "postgresql" {
  app = "${heroku_app.app_postgresql.name}"
  plan = "heroku-postgresql:hobby-dev"
}

「hobby-dev」プランを利用するようにしています。

TerraformでHerokuを使う時には、メールアドレスとAPIキーが必要になりますが、今回は変数として切り出すことにしました。

変数宣言をした定義ファイル。
variables.tf

variable "heroku_email" { }
variable "heroku_api_key" { }

メインの定義ファイルからは、この内容をvarで参照しています。

provider "heroku" {
  email = "${var.heroku_email}"
  api_key = "${var.heroku_api_key}"
}

実際に使う値は、tfvarsファイルに記載。
secret.tfvars

heroku_email = "....."
heroku_api_key = "....."

このあたりは、こちらを参考に。

Input Variables | Terraform - HashiCorp Learn

Input Variables - Configuration Language - Terraform by HashiCorp

Terraformで利用する際には、以下のように「-var-file」として指定します。

$ terraform apply -var-file=secret.tfvars

とりあえず、ここまでで「terraform init」してHeroku Providerをダウンロードしておきます。

$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "heroku" (1.9.0)...

サンプルアプリケーションの作成

それでは、サンプルアプリケーションを作成します。

Node.js+Expressで、簡単なREST APIとして作ってみましょう。ExpressでPostgreSQLを使う例としては、以下を参照しました。

PostgreSQL

PostgreSQLとの接続には、pg-promiseを使います、と。

GitHub - vitaly-t/pg-promise: Promises/A+ interface for PostgreSQL

pg-promise API

Expressとpg-promiseのインストール。

$ npm i express pg-promise

インストールされたライブラリのバージョン。

  "dependencies": {
    "express": "^4.16.4",
    "pg-promise": "^8.7.2"
  }

作成したソースコード
server.js

const express = require('express');
const pgp = require('pg-promise')({ });
const app = express();

const postgresql = pgp(`${process.env.DATABASE_URL}?ssl=true`);

const port = process.env.PORT || 3000;

app.get('/', async (req, res) => {
    return postgresql
        .any('SELECT table_schema,table_name FROM information_schema.tables')
        .then(data => res.send(JSON.stringify(data)));
});

app.listen(port, () => console.log(`[${new Date()}] Server startup, port = ${port}`));

Connection StringとSSLの設定については、Herokuのドキュメントを参考にしています。

Connecting in Node.js

const postgresql = pgp(`${process.env.DATABASE_URL}?ssl=true`);

このスクリプトを「npm start」で起動できるようにして

  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

tar.gzで固めて、今回はGitHubに起きました。

https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz

中身は、こんな感じです。

$ tar -tvf nodejs-app-postgresql.tar.gz
-rw-rw-r-- xxxxx/xxxxx 487 2019-05-13 23:09 nodejs-app-postgresql/server.js
-rw-rw-r-- kazuxxxxxhira/xxxxx 19095 2019-05-13 22:33 nodejs-app-postgresql/package-lock.json
-rw-rw-r-- xxxxx/xxxxx   344 2019-05-13 22:34 nodejs-app-postgresql/package.json

このURLが、先ほど「heroku_build」のsourceに指定していたアーカイブです。

resource "heroku_build" "app_postgresql_build" {
  app = "${heroku_app.app_postgresql.name}"

  source = {
    url = "https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz"
  }
}

これで、準備は完了です。

デプロイ

それでは、ここまで定義した内容を、デプロイします。

まずは、「terraform plan」で確認。「terraform plan」時にも、「-var-file」で変数の内容を指定します。

$ terraform plan -var-file=secret.tfvars
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + heroku_addon.postgresql
      id:                      <computed>
      app:                     "app-postgresql"
      config_vars.#:           <computed>
      name:                    <computed>
      plan:                    "heroku-postgresql:hobby-dev"
      provider_id:             <computed>

  + heroku_app.app_postgresql
      id:                      <computed>
      all_config_vars.%:       <computed>
      config_vars.#:           <computed>
      git_url:                 <computed>
      heroku_hostname:         <computed>
      internal_routing:        <computed>
      name:                    "app-postgresql"
      region:                  "us"
      sensitive_config_vars.#: <computed>
      stack:                   <computed>
      uuid:                    <computed>
      web_url:                 <computed>

  + heroku_build.app_postgresql_build
      id:                      <computed>
      app:                     "app-postgresql"
      buildpacks.#:            <computed>
      local_checksum:          <computed>
      output_stream_url:       <computed>
      release_id:              <computed>
      slug_id:                 <computed>
      source.%:                "1"
      source.url:              "https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz"
      stack:                   <computed>
      status:                  <computed>
      user.%:                  <computed>
      uuid:                    <computed>


Plan: 3 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

では、デプロイ。

$ terraform apply -var-file=secret.tfvars

やや時間がかかりますが、デプロイが完了しました。

heroku_addon.postgresql: Creation complete after 1s (ID: d86ace58-a0a6-4bb8-9ba1-3def3d22921a)
heroku_build.app_postgresql_build: Still creating... (10s elapsed)
heroku_build.app_postgresql_build: Still creating... (20s elapsed)
heroku_build.app_postgresql_build: Creation complete after 27s (ID: 8e2731d5-2929-44dd-9eea-a246d9d07cf0)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

確認。

$ curl -s https://app-postgresql.herokuapp.com | jq 
[
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_foreign_table"
  },
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_roles"
  },
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_settings"
  },
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_subscription"
  },

〜省略〜

できました!

最後に、デプロイしたリソースを破棄しておしまい。

$ terraform destroy -var-file=secret.tfvars

どちらかというと、TerraformというよりNode.jsとHerokuにてこずりましたが、とりあえずやりたいことはできましたよ、と。

QuarkusのConfigurationを試す

これは、なにをしたくて書いたもの?

Quarkusを使った場合に、アプリケーションの設定をどう扱うのかをちょっと見てみようと。

結論から言うと、Configuration for MicroProfileを使用します。Quarkus上でConfiguration for MicroProfileを使う場合には、どんな感じに
なるのかをちょっと見てみましょう。

Configuration for MicroProfile

Configuration for MicroProfileとは、文字通りMicroProfileでのアプリケーションの設定に関するAPIを定めたものです。

Project – MicroProfile

QuarkusにおけるConfiguration for MicroProfileの実装は、こちらのライブラリです。

GitHub - smallrye/smallrye-config: SmallRye implementation of Eclipse MicroProfile Config

今回利用するQuarkus 0.14.0でのConfiguration for MicroProfileのバージョンは、1.3のようです。

ちなみに、Configuration for MicroProfileについては、以前にエントリを書いたことがあります。

Configuration for MicroProfileを試す - CLOVER🍀

この時は、Apache Geronimo Configを使用しました。

今回は、ちょっと簡単にQuarkusで試してみるとしましょう。

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-2ubuntu0.18.04.1-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)


$ mvn -version
Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_191, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-48-generic", arch: "amd64", family: "unix"

GraalVMも用意。

$ $GRAALVM_HOME/bin/java -version
openjdk version "1.8.0_202"
OpenJDK Runtime Environment (build 1.8.0_202-20190206132807.buildslave.jdk8u-src-tar--b08)
OpenJDK GraalVM CE 1.0.0-rc16 (build 25.202-b08-jvmci-0.59, mixed mode)

簡単なサンプルアプリケーションの作成

それでは、最初にQuarkusのプロジェクトを作成します。

$ mvn io.quarkus:quarkus-maven-plugin:0.14.0:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=quarkus-configuration \
    -DprojectVersion=0.0.1-SNAPSHOT \
    -Dextensions=resteasy-jsonb

RESTEasy JSONBもちょっと有効にしておきました。

Quarkusでの設定ファイルは、デフォルトで「application.properties」のようです。プロジェクト作成時に、中身のないファイルが
できているので、こちらに追記しましょう。
src/main/resources/application.properties

# Configuration file
# key = value

sample.greeting.message = Hello Quarkus!!

設定ファイルに追加した項目を利用する、設定用のクラスを作成します。
src/main/java/org/littlewings/quarkus/configuration/MyConfiguration.java

package org.littlewings.quarkus.configuration;

import javax.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public class MyConfiguration {
    @ConfigProperty(name = "sample.greeting.message")
    String greetingMessage;

    @ConfigProperty(name = "sample.int.value", defaultValue = "10")
    int intValue;

    public String getGreetingMessage() {
        return greetingMessage;
    }

    public int getIntValue() {
        return intValue;
    }
}

@ConfigPropertyは、Configuration for MicroProfileのアノテーションです。ちなみに、ひとつ設定ファイルに記載していない項目が
ありますが、こちらにはデフォルト値を設定しています。

では、こちらを使用するJAX-RSリソースクラスを作成してみましょう。
src/main/java/org/littlewings/quarkus/configuration/SampleResource.java

package org.littlewings.quarkus.configuration;

import java.util.Properties;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.config.Config;

@Path("sample")
public class SampleResource {
    @Inject
    MyConfiguration myConfiguration;

    @Inject
    Config config;

    @GET
    @Path("greeting")
    @Produces(MediaType.TEXT_PLAIN)
    public String greeting() {
        return myConfiguration.getGreetingMessage();
    }

    @GET
    @Path("int-value")
    @Produces(MediaType.TEXT_PLAIN)
    public int intValue() {
        return myConfiguration.getIntValue();
    }

    @GET
    @Path("greeting-by-lookup")
    @Produces(MediaType.TEXT_PLAIN)
    public String greetingByLookup() {
        return config.getValue("sample.greeting.message", String.class);
    }

    @GET
    @Path("system-properties")
    @Produces(MediaType.APPLICATION_JSON)
    public Properties systemProperties() {
        return System.getProperties();
    }
}

使い方の例として、作成した設定用のクラスをインジェクションして利用する方法と

    @Inject
    MyConfiguration myConfiguration;

    @GET
    @Path("greeting")
    @Produces(MediaType.TEXT_PLAIN)
    public String greeting() {
        return myConfiguration.getGreetingMessage();
    }

Configuration for MicroProfileが提供するConfigインターフェースをインジェクションして、設定項目をルックアップする方法を書いています。

    @Inject
    Config config;

    @GET
    @Path("greeting-by-lookup")
    @Produces(MediaType.TEXT_PLAIN)
    public String greetingByLookup() {
        return config.getValue("sample.greeting.message", String.class);
    }

確認

では、こちらをビルドして

$ mvn package

確認してみましょう。

$ java -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar

インジェクションしているものと、Configインターフェースから取得しているもの。

$ curl localhost:8080/sample/greeting
Hello Quarkus!!

$ curl localhost:8080/sample/greeting-by-lookup
Hello Quarkus!!

デフォルト値で動作しているもの。

$ curl localhost:8080/sample/int-value
10

OKですね。

Configuration for MicroProfileは、値をシステムプロパティや環境変数で上書きすることができます。

システムプロパティで上書きする場合。

$ java -Dsample.greeting.message='Hello World!' -Dsample.int.value=15 -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar 

確認。

$ curl localhost:8080/sample/greeting
Hello World!

$ curl localhost:8080/sample/greeting-by-lookup
Hello World!

$ curl localhost:8080/sample/int-value
15

全部変更されましたね。

環境変数で設定を上書きする場合。

$ export SAMPLE_GREETING_MESSAGE='Hello World!'
$ export SAMPLE_INT_VALUE=15

$ java -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar

上書きした値は、システムプロパティの時と同じなので省略します。

続いて、ネイティブイメージを作成して確認してみましょう。

$ mvn package -Dnative

起動しようとすると…怒られます。

$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner 
java.util.ServiceConfigurationError: org.eclipse.microprofile.config.spi.ConfigSourceProvider: Provider io.quarkus.runtime.configuration.TemporaryConfigSourceProvider not found
    at java.util.ServiceLoader.fail(ServiceLoader.java:239)
    at java.util.ServiceLoader.access$300(ServiceLoader.java:185)
    at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:372)
    at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
    at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
    at java.lang.Iterable.forEach(Iterable.java:74)
    at io.smallrye.config.SmallRyeConfigBuilder.discoverSources(SmallRyeConfigBuilder.java:80)
    at io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:190)
    at io.quarkus.runtime.generated.RunTimeConfig.getRunTimeConfiguration(Unknown Source)
    at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source)
    at io.quarkus.runtime.Application.start(Application.java:101)
    at io.quarkus.runtime.Application.run(Application.java:213)
    at io.quarkus.runner.GeneratedMain.main(Unknown Source)
Exception in thread "main" java.lang.RuntimeException: Failed to start quarkus
    at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source)
    at io.quarkus.runtime.Application.start(Application.java:101)
    at io.quarkus.runtime.Application.run(Application.java:213)
    at io.quarkus.runner.GeneratedMain.main(Unknown Source)
Caused by: java.util.ServiceConfigurationError: org.eclipse.microprofile.config.spi.ConfigSourceProvider: Provider io.quarkus.runtime.configuration.TemporaryConfigSourceProvider not found
    at java.util.ServiceLoader.fail(ServiceLoader.java:239)
    at java.util.ServiceLoader.access$300(ServiceLoader.java:185)
    at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:372)
    at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
    at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
    at java.lang.Iterable.forEach(Iterable.java:74)
    at io.smallrye.config.SmallRyeConfigBuilder.discoverSources(SmallRyeConfigBuilder.java:80)
    at io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:190)
    at io.quarkus.runtime.generated.RunTimeConfig.getRunTimeConfiguration(Unknown Source)
    ... 4 more

どうやら、QuarkusのService Providerで参照している、Configuration for MicroProfileのConfigSourceProviderの実装クラスが
見つけられないようです。

https://github.com/quarkusio/quarkus/blob/0.14.0/core/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider

もしかして、リフレクションの情報を登録する必要がある?

というわけで、pom.xmlにGraalVM(Substrate VM)への依存関係を追加して

    <dependency>
      <groupId>com.oracle.substratevm</groupId>
      <artifactId>svm</artifactId>
      <version>1.0.0-rc16</version>
      <scope>provided</scope>
    </dependency>

対象のクラスの情報を登録します。 src/main/java/org/littlewings/quarkus/configuration/QuarkusConfigurationSubstitutions.java

package org.littlewings.quarkus.configuration;

import com.oracle.svm.core.annotate.AutomaticFeature;
import io.quarkus.runtime.configuration.TemporaryConfigSourceProvider;
import org.graalvm.nativeimage.Feature;
import org.graalvm.nativeimage.RuntimeReflection;

public class QuarkusConfigurationSubstitutions {
}

@AutomaticFeature
class RuntimeReflectionRegistrationFeature implements Feature {
    @Override
    public void beforeAnalysis(Feature.BeforeAnalysisAccess access) {
        try {
            RuntimeReflection.register(TemporaryConfigSourceProvider.class);
            RuntimeReflection.register(TemporaryConfigSourceProvider.class.getDeclaredConstructor());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

再度パッケージングして、起動。

$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner

今度は、起動します。確認結果は、JARファイルでの実行の時と同じなので、省略します。

ネイティブイメージでもJARファイルでの実行の時と同様、システムプロパティや

$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner -Dsample.greeting.message='Hello World!' -Dsample.int.value=15 -Dcustom.message='Hello!?'

環境変数で値を上書きすることができます。

$ export SAMPLE_GREETING_MESSAGE='Hello World!'
$ export SAMPLE_INT_VALUE=15

$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner

環境変数はまあいいとして、システムプロパティはこの場合、ただのコマンドライン引数になっているのでは?と思うのですが、
どうなっているのでしょう。ちょっとわかりませんでした…。

独自の設定ファイルを追加する(ネイティブイメージの時は未確認)

Quarkusでは、デフォルトの設定ファイルをapplication.propertiesとしています。

https://github.com/quarkusio/quarkus/blob/0.14.0/core/runtime/src/main/java/io/quarkus/runtime/configuration/ApplicationPropertiesConfigSource.java#L27

ここで、Configuration for MicroProfileの仕様に沿って、独自の設定項目を追加してみましょう。

Custom configuration sources

こんな設定ファイルを作成。
src/main/resources/my-custom-configuration.properties

custom.message = My Custom Configuration!!

この設定ファイルを読み込む、ConfigSourceProviderの実装を作成します。
src/main/java/org/littlewings/quarkus/configuration/MyCustomConfigSourceProvider.java

package org.littlewings.quarkus.configuration;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;

import io.smallrye.config.PropertiesConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;

public class MyCustomConfigSourceProvider implements ConfigSourceProvider {
    @Override
    public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) {
        return Arrays
                .asList("my-custom-configuration.properties")
                .stream()
                .map(filePath -> forClassLoader.getResource(filePath))
                .filter(Objects::nonNull)
                .map(filePath -> {
                    try {
                        return new PropertiesConfigSource(filePath);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
                })
                .collect(Collectors.toList());
    }
}

プロパティファイルからのConfigSourceの作成は、SmallRye Configのものを利用しました。

このクラスを、Service Providerの仕組みでロードできるように設定します。
src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider

org.littlewings.quarkus.configuration.MyCustomConfigSourceProvider

先ほど作成した設定クラスに、追加した設定ファイルから取得した項目を追加し

@ApplicationScoped
public class MyConfiguration {
    @ConfigProperty(name = "sample.greeting.message")
    String greetingMessage;

    @ConfigProperty(name = "sample.int.value", defaultValue = "10")
    int intValue;

    @ConfigProperty(name = "custom.message")
    String customMessage;

    public String getGreetingMessage() {
        return greetingMessage;
    }

    public int getIntValue() {
        return intValue;
    }

    public String getCustomMessage() {
        return customMessage;
    }
}

JAX-RSのエンドポイントも追加します。

    @GET
    @Path("custom-config-message")
    @Produces(MediaType.TEXT_PLAIN)
    public String customConfigMessage() {
        return myConfiguration.getCustomMessage();
    }

これで、パッケージングして起動。

$ mvn package
$ java -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar

確認。

$ curl localhost:8080/sample/custom-config-message
My Custom Configuration!!

追加した設定項目を認識していることを、確認できました。

追加した設定ファイルに関する項目についても、システムプロパティや環境変数で上書きすることができます。

$ java -Dcustom.message=FooBar -jar target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar

ネイティブイメージでも確認してみましょう。

$ mvn package -Dnative

すると、起動する時に設定のバリデーションに失敗したと怒られます。

$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner 
javax.enterprise.inject.spi.DeploymentException: No config value of type [class java.lang.String] exists for: custom.message
    at io.quarkus.arc.runtime.ConfigDeploymentTemplate.validateConfigProperties(ConfigDeploymentTemplate.java:48)
    at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties7.deploy(Unknown Source)
    at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source)
    at io.quarkus.runtime.Application.start(Application.java:101)
    at io.quarkus.runtime.Application.run(Application.java:213)
    at io.quarkus.runner.GeneratedMain.main(Unknown Source)
Exception in thread "main" java.lang.RuntimeException: Failed to start quarkus
    at io.quarkus.runner.ApplicationImpl1.doStart(Unknown Source)
    at io.quarkus.runtime.Application.start(Application.java:101)
    at io.quarkus.runtime.Application.run(Application.java:213)
    at io.quarkus.runner.GeneratedMain.main(Unknown Source)
Caused by: javax.enterprise.inject.spi.DeploymentException: No config value of type [class java.lang.String] exists for: custom.message
    at io.quarkus.arc.runtime.ConfigDeploymentTemplate.validateConfigProperties(ConfigDeploymentTemplate.java:48)
    at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties7.deploy(Unknown Source)
    ... 4 more

ちなみに、システムプロパティなどで明示的に与えると起動できるようになります。

$ ./target/quarkus-configuration-0.0.1-SNAPSHOT-runner -Dcustom.message=test
2019-05-11 01:06:33,495 INFO  [io.quarkus] (main) Quarkus 0.14.0 started in 0.012s. Listening on: http://[::]:8080
2019-05-11 01:06:33,496 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb]

どうやら、追加した設定ファイルがわかっていないようです。

ちなみに、だいぶ意図的だったのですが、ConfigSourceProviderで設定ファイルが必ずある前提のコードにしてあると、
ネイティブイメージにした時にはファイルが見つけられずにコケることになります…。

public class MyCustomConfigSourceProvider implements ConfigSourceProvider {
    @Override
    public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) {
        return Arrays
                .asList("my-custom-configuration.properties")
                .stream()
                .map(filePath -> forClassLoader.getResource(filePath))
                .filter(Objects::nonNull)
                .map(filePath -> {
                    try {
                        return new PropertiesConfigSource(filePath);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
                })
                .collect(Collectors.toList());
    }
}

なので、今回のConfigSourceProviderは、「設定ファイルがあったら読み込む」という動作にしています。

ちょっと確認のために、JAX-RSリソースクラスのエンドポイントに、こんなメソッドを足してみます。

    @GET
    @Path("dump-properties")
    @Produces(MediaType.TEXT_PLAIN)
    public String dumpProperties() {
        config.getConfigSources().forEach(cs -> {
            System.out.println("====== " + cs.getName());
            cs.getProperties().forEach((k, v) -> System.out.println("  " + k + " = " + v));
        });

        return "OK!!";
    }

現在認識しているConfigSourceを、全部出力してみようと。

この状態で、ひとまずJARファイルで起動してアクセスしてみます。

$ curl localhost:8080/sample/dump-properties

すると、コンソールにはこんな感じで表示されます。

====== SysPropConfigSource
  ...
====== EnvConfigSource
  ...
====== PropertiesConfigSource[source=application.properties]
====== PropertiesConfigSource[source=application.properties]
====== PropertiesConfigSource[source=application.properties]
  sample.greeting.message = Hello Quarkus!!
====== PropertiesConfigSource[source=application.properties]
  sample.greeting.message = Hello Quarkus!!
====== PropertiesConfigSource[source=jar:file:/path/to/quarkus-configuration/target/quarkus-configuration-0.0.1-SNAPSHOT-runner.jar!/my-custom-configuration.properties]
  custom.message = My Custom Configuration!!
====== null:null:ServletConfigSource
====== null:javax.ws.rs.core.Application:FilterConfigSource
====== null:ServletContextConfigSource
  resteasy.injector.factory = io.quarkus.resteasy.runtime.QuarkusInjectorFactory
  resteasy.providers = org.jboss.resteasy.plugins.providers.sse.SseEventOutputProvider,org.jboss.resteasy.plugins.providers.DefaultTextPlain,org.jboss.resteasy.plugins.providers.jsonp.JsonObjectProvider,org.jboss.resteasy.plugins.providers.ReactiveStreamProvider,org.jboss.resteasy.plugins.interceptors.CacheControlFeature,org.jboss.resteasy.plugins.interceptors.ServerContentEncodingAnnotationFeature,org.jboss.resteasy.plugins.providers.CompletionStageProvider,org.jboss.resteasy.plugins.interceptors.ClientContentEncodingAnnotationFeature,org.jboss.resteasy.plugins.providers.DefaultNumberWriter,org.jboss.resteasy.plugins.providers.jsonb.JsonBindingProvider,org.jboss.resteasy.plugins.providers.FileProvider,org.jboss.resteasy.plugins.interceptors.MessageSanitizerContainerResponseFilter,org.jboss.resteasy.plugins.providers.MultiValuedParamConverterProvider,org.jboss.resteasy.plugins.providers.jsonp.JsonArrayProvider,org.jboss.resteasy.plugins.providers.DefaultBooleanWriter,org.jboss.resteasy.plugins.providers.ByteArrayProvider,org.jboss.resteasy.plugins.providers.StringTextStar,org.jboss.resteasy.plugins.providers.StreamingOutputProvider,org.jboss.resteasy.plugins.providers.jsonp.JsonStructureProvider,io.quarkus.resteasy.runtime.RolesFilterRegistrar,org.jboss.resteasy.plugins.providers.ReaderProvider,org.jboss.resteasy.plugins.providers.sse.SseEventSinkInterceptor,org.jboss.resteasy.plugins.providers.DataSourceProvider,org.jboss.resteasy.plugins.providers.FileRangeWriter,org.jboss.resteasy.plugins.providers.InputStreamProvider,org.jboss.resteasy.plugins.providers.jsonp.JsonValueProvider
  resteasy.scanned.resources = org.littlewings.quarkus.configuration.SampleResource
  resteasy.use.builtin.providers = false
  resteasy.servlet.mapping.prefix = /
====== PropertiesConfigSource[source=Default configuration values]
  quarkus.log.filter."org.xnio.nio".if-starts-with = XNIO NIO Implementation Version
  quarkus.log.console.darken = 0
  sample.greeting.message = Hello Quarkus!!
  quarkus.log.filter."org.jboss.resteasy.resteasy_jaxrs.i18n".if-starts-with = RESTEASY002225
  quarkus.log.filter."org.jboss.threads".if-starts-with = JBoss Threads version
  quarkus.log.filter."org.xnio".if-starts-with = XNIO version
====== default values

application.propertiesの内容などが、確認できますね。ところで、application.propertiesと同じ内容が入った以下のようなものもあります。

====== PropertiesConfigSource[source=Default configuration values]

ここで、ネイティブイメージで確認すると、ガラッと結果が変わります。

====== SysPropConfigSource
  ...
====== EnvConfigSource
  ...
====== PropertiesConfigSource[source=application.properties]
====== PropertiesConfigSource[source=application.properties]
====== PropertiesConfigSource[source=application.properties]
====== PropertiesConfigSource[source=application.properties]
====== PropertiesConfigSource[source=Default configuration values]
  quarkus.log.filter."org.xnio.nio".if-starts-with = XNIO NIO Implementation Version
  quarkus.log.console.darken = 0
  sample.greeting.message = Hello Quarkus!!
  quarkus.log.filter."org.jboss.resteasy.resteasy_jaxrs.i18n".if-starts-with = RESTEASY002225
  quarkus.log.filter."org.jboss.threads".if-starts-with = JBoss Threads version
  quarkus.log.filter."org.xnio".if-starts-with = XNIO version
====== default values

こちらの方しか認識していませんね。

====== PropertiesConfigSource[source=Default configuration values]

なので、ネイティブイメージの時には実際には異なるファイルから読み込んでいるようなように見えますね。

その元ネタは、ビルド時に生成されるこちらのファイルのようです。 target/wiring-classes/META-INF/quarkus-default-config.properties

#This is the generated set of default configuration values
#Sat May 11 01:08:30 JST 2019
quarkus.log.filter."org.jboss.resteasy.resteasy_jaxrs.i18n".if-starts-with=RESTEASY002225
quarkus.log.console.darken=0
sample.greeting.message=Hello Quarkus\!\!
quarkus.log.filter."org.xnio".if-starts-with=XNIO version
quarkus.log.filter."org.jboss.threads".if-starts-with=JBoss Threads version
quarkus.log.filter."org.xnio.nio".if-starts-with=XNIO NIO Implementation Version

ソースコードとしては、このあたりに。

https://github.com/quarkusio/quarkus/blob/0.14.0/core/runtime/src/main/java/io/quarkus/runtime/configuration/DefaultConfigSource.java#L20

で、この生成されたquarkus-default-config.properties、Build Time Extensionで細工しているような感じに見えます。

https://github.com/quarkusio/quarkus/blob/0.14.0/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigurationSetup.java#L393-L402

CDI REFERENCE / Build Time Extension Points

https://quarkus.io/guides/extension-authors-guide.html

なんかちょっと遠そうなので、今回はこのあたりで。

どこかでBuild Time Extensionを覚えるなりしてから、また戻ってきましょう。

ネイティブイメージにする時には、いろいろあるんですねぇ…。

まとめ

QuarkusでのConfiguration for MicroProfileを試してみました。

JARファイルでの実行でも、ネイティブイメージの実行でも使えますが、追加の設定ファイルを書いたりすると、
ネイティブイメージにした時に困ったことになるような…。

とりあえず、今はapplication.propertiesだけを使っておくのが無難ですかねぇ。