Spring Boot Config Client for Server with vault backend

Spring Boot Config Serverでvaultの設定ファイルを取得し、そのserverに対して設定ファイルを取得しにいくclientのサンプルです。

  • イメージ図

f:id:tomoTaka:20171229085155p:plain

  • vaultのtokenをbootstrap.ymlファイルに追加
spring:
  cloud:
    config:
      token: your-vault-token
  • controllerクラスでvaultに保管しているファイルの値をconfig server 経由で取得
package com.example.configclient.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RefreshScope
public class ConfigController {
    @Value("${foo}")
    private String vaultFoo;

    @Value("${baz}")
    private String vaultBaz;
    @RequestMapping("/show/vaultconfig")
    @ResponseBody
    public String getVaultConfig() {
        String config = String.format("vault:foo=[%s], baz=[%s]", vaultFoo, vaultBaz);
        return config;
    }
}
  • serverで取得した時のvaultの値
{"name":"app-config","profiles":["default"],"label":null,"version":null,"state":null,"propertySources":[{"name":"vault:app-config","source":{"foo":"bar"}},{"name":"vault:application","source":{"baz":"bam","foo":"bar"}}]}
  • clientから設定ファイルを取得

f:id:tomoTaka:20171229082643p:plain

コードは、アップ
github.com

Spring Boot Config Server with vault backend

ローカル環境でvaultサーバを起動

昨日、試したので、
tomotaka.hatenablog.com

Spring Boot Cloud Config Server

コードは、gitにアップ
github.com

すごく簡単です。application.ymlの設定にvaultを追加するだけ(gitは、無効に)

server:
  port: 8888
spring:
  cloud:
    config:
      server:
#       git:
#         uri: file:${HOME}/config-sample
        vault:
          host: 127.0.0.1
          port: 8200
          scheme: http
          backend: secret
          defaultKey: application
  profiles:
    active: vault

vaultに必要なデータを書いておきます。
以下のようにコマンドからwriteできます。

vault write secret/application foo=bar baz=bam
vault write secret/app-config foo=bar

vaultにデータを追加するのもhttpを使用してできます。
それもSpring Bootで試してみました。
github.com
これも、とても簡単にできました。(productionで使う場合は、さらに考慮が必要ですが)

@SpringBootApplication
public class SpringVaultSampleApplication {
    private static Logger logger = LoggerFactory.getLogger(SpringVaultSampleApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(SpringVaultSampleApplication.class, args);
    }

    @Bean
    CommandLineRunner commandLineRunner(VaultTemplate vaultTemplate) {
        return x -> {
            vaultTemplate.write("secret/hello", new Hello("world"));
            VaultResponseSupport<Hello> hello = vaultTemplate.read("secret/hello", Hello.class);
            logger.info("vault value is [{}]", hello.getData().getVault());
            VaultResponse response = vaultTemplate.read("secret/hello");
            logger.info("vault json response is [{}]", response.getData());
        };
    }

    public static class Hello {
        String vault;

        public Hello(@JsonProperty("value") String value) {
            this.vault = value;
        }

        public String getVault() {
            return vault;
        }
    }

}

curlコマンドで、vaultの値を取得。ここでのyour-tokenは、サーバを起動した時にコンソールに表示される値を使用

curl -X "GET" "http://localhost:8888/app-config/default" -H "X-Config-Token: your-token"

出力される値に、applicationと、上記curlコマンドで指定したcpp-configの2つが取れるのは、application.ymlのdefaultKey: applicationと指定しているからのようです。
f:id:tomoTaka:20171224085742p:plain

helloのデータを取得する場合

curl -X "GET" "http://localhost:8888/hello/default" -H "X-Config-Token: your-token"

f:id:tomoTaka:20171224091136p:plain

HashiCorp Vault Server of dev

install

以下のページに記載しているようにしてインストール
just follow the below guide
https://www.vaultproject.io/intro/getting-started/install.html

  • make directory to install vault server.

serverをインストールするためにdirectoryを作成

mkdir valut-server

download zip from Download Vault - Vault by HashiCorp and unzip
vaultを上記サイトよりダウンロードして、上記の作成したvault-server directoryにunzip

cd vault-server
unzip vault_0.9.1_darwin_amd64.zip
  • set path for vault and see where vault is

vaultにパスを通して、パスを確認

export PATH=$PATH:/Users/tomo/vault-server
source $PATH
which vault
vault version

f:id:tomoTaka:20171223092403p:plain

  • start vault server for development

vault server を開始 (開発用モード)

vault server -dev
  • vault client on another shell

vault clientをサーバとは、別のシェルで開始

export VAULT_ADDR='http://127.0.0.1:8200'
vault status

f:id:tomoTaka:20171223092426p:plain

  • write secret
vault write secret/hello value=world
  • read secret
vault read secret/hello
  • read secret for json
vault read -format=json secret/hello

f:id:tomoTaka:20171223092711p:plain

Spring Boot Validation and to customize Error Message

Spring Boot で画面の入力チェックとエラーメッセージの表示を実装
Spring Boot Version 2.0.0.M7で実装してみました。

エラー時の画面サンプル

f:id:tomoTaka:20171222212933p:plain

@ NumberFormatで、フォーマットしてくれて便利
String型以外の項目は、RequestをFormクラスに変換する時にエラーを検知して、BindingResultにエラーが追加
なので、以下のmessages.propertiesで、typeMismatchが先頭についているエラーとなっているよう

public class PersonForm {
    @NotEmpty
    private String firstName;

    @NotEmpty
    private String lastName;

    @Max(100)
    private Integer age;

    @Past
    @DateTimeFormat(pattern = "uuuu-MM-dd")
    private LocalDate birthDay;

    @Digits(integer = 10, fraction = 0)
    @NumberFormat(pattern = "#,###")
    private BigDecimal salary;
... setter and getter
@Controller
@RequestMapping("/person")
public class PersonController {
    private static final Logger logger = LoggerFactory.getLogger(PersonController.class);

    @ModelAttribute
    public PersonForm setUp() {
        PersonForm personForm = new PersonForm();
        // set init value
        personForm.setFirstName("first Name1");
        personForm.setLastName("last name1");
        return personForm;
    }

    @RequestMapping("/")
    public String home() {
        logger.info("home");
        return "/person";
    }

    @PostMapping("/valid")
    public String valid(@Validated PersonForm personForm, BindingResult bindingResult) {
        logger.info("person valid");
        bindingResult.getAllErrors().forEach(e -> logger.error("error=[{}]", e.getDefaultMessage()));

        return "person";
    }
}
  • エラーメッセージは、resources directory配下に、messages.propertiesで以下の内容で作成

以下のキーに例えば「validation」とかの接頭語を付けたいのですが、方法が?です。

# for form column of error message
firstName=名
lastName=姓
age=年齢
birthDay=誕生日
salary=給与
# for validation error message
NotEmpty={0}は、必須です。
Max={0}は、{1}文字までで入力してください。
Max.personForm.age={0}は、{1}歳までで入力してください。
Digits={0}は、整数部{2}文字で入力してください。
Past={0}は、過去の日を入力してください。
typeMismatch.personForm.java.lang.Integer={0}は、数値で入力してくださいね。
typeMismatch.personForm.age={0}は、数値で入力してください。
typeMismatch.java.math.BigDecimal={0}は、金額で入力してください
typeMismatch.java.time.LocalDate={0}は、日付けを入力してください

コードは、アップ
https://github.com/tomoTaka01/spring-mvc-sample
カスタムバリデーションも作成したいです!

修正したコードを再起動なしで反映する

Spring Boot ではbuild.gradleにdevtoolsを追加するだけ

dependencies {
	runtime('org.springframework.boot:spring-boot-devtools')
}

[ctrl]+[command]+a
f:id:tomoTaka:20171222224427p:plain
f:id:tomoTaka:20171222224444p:plain

Spring Boot Config Server Sample

Cloud Config Server Sample

As of Spring Boot Version 2.0.0.M7(2017-12-22)
Spring Boot Version 2.0.0.M7時点でのサンプル

設定ファイル(ymlファイル)は、ローカルの以下の場所より取得
application.yml

server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: file:${HOME}/config-sample

ローカルマシーンの上記で指定下場所の以下のファイルを置いておきます
この時にこのdirectoryがgitとして管理されています(githubからも取得できます)
f:id:tomoTaka:20171222074012p:plain

アプリを起動して以下のurlで上記ファイルより設定が取得できていることが確認できます

http://localhost:8888/app-api/local

f:id:tomoTaka:20171222074500p:plain

http://localhost:8888/app-config/local

f:id:tomoTaka:20171222074619p:plain

コードは、gitにアップしています
GitHub - tomoTaka01/config-server: Spring Boot Config Server Sample

Spring Boot Multiple yml with profiles

I just wanted to use multiple config(app-key.yml, app-api.yml) for each environment.
複数の設定ファイル(例:app-key.yml, app-api.yml)を各環境ごとで使用できるようにしています。

f:id:tomoTaka:20171217074605p:plain

each file has the below value
各ファイルの値は以下のようにしています

app-key.yml

environment default local prod
key val-default
key1 val1-default val1-local
key2 val2-default val2-prod

app-api.yml

environment default local prod
timeout 3
retry-count 1 5 3

local環境を設定して起動した時の値を表示
The below shows the value for local environment.

--spring.profiles.active=local

f:id:tomoTaka:20171216220528p:plain

prod環境を設定して起動した時の値を表示
The below shows the value for prod environment.

--spring.profiles.active=prod

f:id:tomoTaka:20171216220747p:plain

default設定を基本的には使用し、環境ごとの設定を上書きするために以下のように@PropertySourceで、デフォルトファイル、上書きするために環境(profile)に依存したファイルを設定
You can use PropertySource annotation which has two files, to override default file using the second one.

AppKeyConfig.java

@Configuration
@PropertySource({"classpath:/config/app-key.yml","classpath:/config/app-key-${spring.profiles.active}.yml"})
@ConfigurationProperties
public class AppKeyConfig {
    private String key;
    private String key1;
    private String key2;
...

AppApiConfig.java

@Configuration
@PropertySource({"classpath:/config/app-api.yml","classpath:/config/app-api-${spring.profiles.active}.yml"})
@ConfigurationProperties
public class AppApiConfig {
    private int timeout;
    private int retryCount;
...

テスト時には、@SpringBootTestアノテーションで環境を指定できます。
When testing, @SpringBootTest(properties = "spring.profiles.active=local") works fine.

@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.profiles.active=local")
public class AppKeyConfigLocalTest {
    @Autowired
    private AppKeyConfig appKeyConfig;

    @Test
    public void keyShouldBeDefaultValue(){
        String key = appKeyConfig.getKey();
        assertThat(key).isEqualTo("val-default");
    }
    @Test
    public void key1ShouleBeLocalValue(){
        String key1 = appKeyConfig.getKey1();
        assertThat(key1).isEqualTo("val1-local");
    }
...

code is here
github.com

Spring Boot Multiple Application Runner

This is very simple example using Spring Boot with 2 stand alone application in 1 project.

The point is you need Java config class and a class which implements ApplicationRunner Interface(or CommandLineRunner) for each job.
And use @ConditionalOnProperty as parameter when execute this.

Moreover if you want it to exit with execution's status like 0 or 1, just throw RuntimeException implements ExitCodeGenerator Interface throws exit code.

  1. TaskSuccessConfig.java
  2. TaskFailureConfig.java
  • ApplicationRunner
  1. TaskSuccess.java
  2. TaskFailure.java
  • package tree

f:id:tomoTaka:20170429205247p:plain

  • TaskSuccess execution log
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --task=success

You can see the arguments [--task=success] in the log.
f:id:tomoTaka:20170429210536p:plain

  • TaskFailure execution log
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --task=failure
You can see the exit code is 1:Failure in the log.

f:id:tomoTaka:20170429210546p:plain

  • DemoApplication.java
@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

TaskSuccess

  • TaskSuccessConfig.java
@Configuration
@ConditionalOnProperty(name={"task"}, havingValue="success")
public class TaskSuccessConfig {
    @Bean
    public ApplicationRunner getRunner() {
        return new TaskSuccess();
    }
}
public class TaskSuccess implements ApplicationRunner {
    private static final Logger logger = LoggerFactory.getLogger(TaskSuccess.class);
    @Autowired
    private DemoService demoService;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        logger.info("args=[{}]", args.getSourceArgs());
        try {
            demoService.convertInt("123");
        } catch (Exception e) {
            throw new FailureException(e.getMessage());
        }
    }
}

TaskFailure

  • TaskFailureConfig.java
@Configuration
@ConditionalOnProperty(name = { "task" }, havingValue = "failure")
public class TaskFailureConfig {
    @Bean
    public ApplicationRunner getRunner() {
        return new TaskFailure();
    }
}
public class TaskFailure implements ApplicationRunner {
    private static final Logger logger = LoggerFactory.getLogger(TaskFailure.class);
    @Autowired
    private DemoService demoService;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        logger.info("args=[{}]", args.getSourceArgs());
        try {
            demoService.convertInt("abc");
        } catch (Exception e) {
            throw new FailureException(e.getMessage());
        }
    }
}

common class

  • FailureException.java
public class FailureException extends RuntimeException implements ExitCodeGenerator {
    private static final long serialVersionUID = 1L;
    public FailureException(String message) {
        super(message);
    }
    @Override
    public int getExitCode() {
        return 1; // you can set the exit code here
    }
}
  • DemoServiceImpl.java
@Service
public class DemoServiceImpl implements DemoService {
    private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);
    @Override
    public void convertInt(String val) {
        logger.info("val=[{}]", val);
        Integer intVal = Integer.valueOf(val);
        logger.info("int val=[{}]", intVal);
    }
}

whole code is here.
github.com

I really enjoy coding with Spring Boot!!!

keep coding ;-)