---
title: Spring Boot 2.4で導入されたConfig Data APIをSpring Cloud Vaultで試す
tags: ["Spring Boot", "Config Data API", "Vault", "Spring Cloud", "Spring Cloud Config"]
categories: ["Programming", "Java", "org", "springframework", "cloud", "vault"]
date: 2020-11-17T11:34:47Z
updated: 2020-11-17T11:36:58Z
---

[前記事](https://blog.ik.am/entries/626)でConfig Treeを使ったConfig Data APIを紹介しましたが、今回は
[Spring Cloud Vault](https://spring.io/projects/spring-cloud-vault)を使ったConfig Data APIの使い方を紹介します。

Config Data APIは抽象化されているので、実質的にはSpring Cloud Vault自体の使い方の説明です。

**目次**
<!-- toc -->

### Vaultのセットアップ

VaultはDockerで起動します。

Docker Composeで使用するファイルを準備してあるので、gitで取得してください。

```
git clone https://github.com/making/demo-configdata-vault
cd demo-configdata-vault
```

Docker Composeで起動します。

```
docker-compose -f vault/docker-compose.yml up 
```

次のコマンドで初期化します。`vault.local.maki.lol`は`127.0.0.1`に解決されます。

```
export VAULT_ADDR=https://vault.local.maki.lol:8200
export VAULT_CACERT=$PWD/vault/certs/ca.pem
vault operator init
```

次のようなログが出力されます。

```
Unseal Key 1: R6vUEJXNrYviuOrRWLbXzQ25hKlNJ5jCzmDcb3Qdosmh
Unseal Key 2: ET9qNWZvo7tTFrQbKDBUcQU0a29TrMFukIem60O9GaSA
Unseal Key 3: pz8RkNphIe6444c5B6KB6RSMIy4LJlR7h42uuMEs19B7
Unseal Key 4: ZqOBy3gqc0e+xvTaO8xNeqxrmP7hk+QOShTmzSmAx+OG
Unseal Key 5: tSiqej576SFAwwCTlODe8aYs4C/qzq1YL3UYLY2+yjGK

Initial Root Token: s.6WAxZ2zq8eZnDqi1RZfBgOct

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated master key. Without at least 3 key to
reconstruct the master key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.
```

ログ中のUnseal Keyを3つ選んで、次のコマンドを実行し、Vaultをunsealします。

```
vault operator unseal R6vUEJXNrYviuOrRWLbXzQ25hKlNJ5jCzmDcb3Qdosmh
vault operator unseal ET9qNWZvo7tTFrQbKDBUcQU0a29TrMFukIem60O9GaSA
vault operator unseal pz8RkNphIe6444c5B6KB6RSMIy4LJlR7h42uuMEs19B7
```

上記ログ中のInitial Root Tokenを使って、次のコマンドを実行し、Vaultにログインします。

```
vault login s.6WAxZ2zq8eZnDqi1RZfBgOct
```

### Secret Backendの作成

Spring Cloud Vaultではデフォルトで次の名前のSecret Backendを使用します([ドキュメント](https://docs.spring.io/spring-cloud-vault/docs/3.0.0-SNAPSHOT/reference/html/#vault.config.backends.kv.versioned))。


* `/secret/{application}/{profile}`
* `/secret/{application}`
* `/secret/{default-context}/{profile}`
* `/secret/{default-context}`

> `{application}`の値は`spring.cloud.vault.application-name`で指定します。<br>
> `{default-context}`の値は`spring.cloud.vault.kv.default-context`で指定でき、デフォルトは`application`です。<br>
> `{profile}`は`spring.cloud.vault.kv.profiles`または`spring.profiles.active`で指定できます。

まずは`secret`という名前のKey-Value Backendを作成します。

```
$ vault secrets enable -path=secret -version=2 kv

Success! Enabled the kv secrets engine at: secret/

$ vault secrets list 
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_2e13c260    per-token private secret storage
identity/     identity     identity_2f4a734c     identity store
secret/       kv           kv_cb7e2889           n/a
sys/          system       system_cd942d8d       system endpoints used for control, policy and debugging
```

後ほど、アプリケーション側で`{application}`の値を`demo`に設定するとして、次の値を`secret/demo`に登録します。

```
vault kv put secret/demo \
  info.vault.name=demo \
  info.vault.message='Hello World!'
```

### PolicyとAppRoleの作成

`secret`以下の読み取り権限だけを持つ`spring` policyを作成します。

```
$ vault policy write spring vault/config/spring.hcl
Success! Uploaded policy: spring
```

アプリからVaultへの認証は[AppRole](https://www.vaultproject.io/docs/auth/approle)で行います。

次のように`spring` roleを作成し、`spring` policyを適用します。

```
$ vault auth enable approle
Success! Enabled approle auth method at: approle/

$ vault write -f auth/approle/role/spring policies=spring period=1h
Success! Data written to: auth/approle/role/spring

$ vault read auth/approle/role/spring
Key                        Value
---                        -----
bind_secret_id             true
local_secret_ids           false
period                     1h
policies                   [spring]
secret_id_bound_cidrs      <nil>
secret_id_num_uses         0
secret_id_ttl              0s
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               1h
token_policies             [spring]
token_ttl                  0s
token_type                 default
```

次のコマンドでrole-idとsecret-idを取得します。

```
$ vault read auth/approle/role/spring/role-id
Key        Value
---        -----
role_id    70814761-f442-1203-9d25-13ed54e2d440

$ vault write -f auth/approle/role/spring/secret-id
Key                   Value
---                   -----
secret_id             e3c3e858-ab01-c374-f2b4-5f954ed5e69e
secret_id_accessor    4ee83f23-2534-356b-2193-a53884f8fbc5
```

試しに

```
$ vault write auth/approle/login \
  role_id="70814761-f442-1203-9d25-13ed54e2d440" \
  secret_id="e3c3e858-ab01-c374-f2b4-5f954ed5e69e"

Key                     Value
---                     -----
token                   s.Oqio0P3eUEz8hIkrZ2nx6FM8
token_accessor          SWJkW5DC6j7jdAg0lXUkBiM9
token_duration          1h
token_renewable         true
token_policies          ["default" "spring"]
identity_policies       []
policies                ["default" "spring"]
token_meta_role_name    spring

$ VAULT_TOKEN=s.Oqio0P3eUEz8hIkrZ2nx6FM8 vault kv get secret/demo
====== Metadata ======
Key              Value
---              -----
created_time     2020-11-17T04:44:57.5322663Z
deletion_time    n/a
destroyed        false
version          1

=========== Data ===========
Key                   Value
---                   -----
info.vault.message    Hello World!
info.vault.name       demo

$ VAULT_TOKEN=s.Oqio0P3eUEz8hIkrZ2nx6FM8 vault kv put secret/demo a=100
Error writing data to secret/data/demo: Error making API request.

URL: PUT https://vault.local.maki.lol:8200/v1/secret/data/demo
Code: 403. Errors:

* 1 error occurred:
	* permission denied
```

### アプリケーションの作成

Spring Initializrで雛形プロジェクトを作成します。Spring Boot 2.4及びSpring Cloud 2020.0が必要です。

執筆時点ではSpring Boot 2.4.0とSpring Cloud 2020.0.0-SNAPSHOT (Spring Cloud Vault 3.0.0-SNAPSHOT)を使用します。

次のコマンドでプロジェクトを生成します。

```
curl https://start.spring.io/starter.tgz \
  -s \
  -d javaVersion=11 \
  -d artifactId=demo-configdata-vault \
  -d baseDir=demo-configdata-vault \
  -d dependencies=cloud-starter-vault-config,actuator,web,configuration-processor \
  -d packageName=com.example \
  -d applicationName=DemoConfigdataVaultApplication | tar -xzvf -
```

執筆時点(Spring Cloud 2020.0.0-M4)では、古いSpring Vault 2.2.0が使用されてしまうという[問題](https://github.com/spring-cloud/spring-cloud-config/issues/1751)があるため、
`<dependencyManagement>`に次のWorkaroundを適用します。

```
	<dependencyManagement>
		<dependencies>
            <!-- Workaround for https://github.com/spring-cloud/spring-cloud-config/issues/1751 -->
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-vault-dependencies</artifactId>
				<version>3.0.0-SNAPSHOT</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>

			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
```

`application.properties`に次の内容を設定します。VaultにTLSで接続するために、Trust Storeの設定をしています。
Vaultに自己署名証明書を使用している場合に必要です。(HTTPは使用しないで！)

```properties
management.endpoints.web.exposure.include=info,health,env,refresh
spring.cloud.vault.application-name=demo
spring.cloud.vault.uri=https://vault.local.maki.lol:8200
spring.cloud.vault.authentication=approle
spring.cloud.vault.app-role.role-id=<the role id retrieved above>
spring.cloud.vault.app-role.secret-id=<the secret id retrieved above>
spring.cloud.vault.ssl.trust-store=file:<path to demo-configdata-vault>/vault/certs/ca.pem
spring.cloud.vault.ssl.trust-store-type=PEM
spring.cloud.vault.fail-fast=true
spring.config.import=vault://
```

`spring.config.import=vault://`が今回のポイントです。`spring.config.import=vault://<path>`のようにVaultから値を読み込むパスを変更することもできます。

これで`DemoConfigdataVaultApplication`クラスを実行します。

`/actuator/env`エンドポイントにアクセスして、Vaultの`secret/demo`からプロパティを取得できていることを確認してください。

```
$ curl -s localhost:8080/actuator/env | jq .

{
  "activeProfiles": [],
  "propertySources": [
    // (omited)
    {
      "name": "secret/application",
      "properties": {}
    },
    {
      "name": "secret/demo",
      "properties": {
        "info.vault.name": {
          "value": "demo"
        },
        "info.vault.message": {
          "value": "Hello World!"
        }
      }
    },
    // (omited)
  ]
}
```

`info.`から始まるプロパティは`/actuator/info`エンドポイントで取得できます。

```
$ curl -s localhost:8080/actuator/info | jq .        
{
  "vault": {
    "name": "demo",
    "message": "Hello World!"
  }
}
```

### Refreshの確認

Spring Cloud Vaultは[Refresh](https://github.com/spring-cloud/spring-cloud-config#spring-cloud-config-client)に対応しています。

アプリケーションを起動したまま、Vault上の値を変更します。

```
vault kv put secret/demo \
  info.vault.name=demo \
  info.vault.message='Hello Vault!'
```

直後に`/actuator/info`を確認しても`message`の値は変わらないです。

```
$ curl -s localhost:8080/actuator/info | jq .        
{
  "vault": {
    "name": "demo",
    "message": "Hello World!"
  }
}
```

`/actuator/refresh`エンドポイントにPOSTのリクエストを送ると、変更されたプロパティがリフレッシュされます。

```
$ curl -XPOST http://localhost:8080/actuator/refresh
["info.vault.message"]

$ curl -s localhost:8080/actuator/info | jq .        
{
  "vault": {
    "name": "demo",
    "message": "Hello Vault!"
  }
}
```

明示的にRefreshしなくても、デフォルトで10秒インターバルでRefreshされます。
このインターバルは`spring.cloud.vault.config.lifecycle.min-renewal`で変更できます。



### Config Treeとの併用

Vaultにアクセスするためのrole-idとsecret-idをアプリケーションに渡すのは最も気をつけないといけないポイントです。
ここでは[前記事](https://blog.ik.am/entries/626)で説明したように、Config Treeを使用して、ファイルマウントによってプロパティを設定するように変更します。
こちらの方法が環境変数で設定するよりもセキュアです。

`application.properties`を次のように変更します。

```
# Remove role-id and secret-id from application.properties
#spring.cloud.vault.app-role.role-id=
#spring.cloud.vault.app-role.secret-id=

spring.config.import=configtree:/tmp/config/,vault://
```

role-idとsecret-idをファイルに書き込みます。

```
mkdir -p /tmp/config/spring/cloud/vault/app-role
echo 70814761-f442-1203-9d25-13ed54e2d440 > /tmp/config/spring/cloud/vault/app-role/role-id
echo e3c3e858-ab01-c374-f2b4-5f954ed5e69e > /tmp/config/spring/cloud/vault/app-role/secret-id
```

Treeは次のようになります。

```
$ tree /tmp/config/spring                            
/tmp/config/spring
`-- cloud
    `-- vault
        `-- app-role
            |-- role-id
            `-- secret-id

3 directories, 2 files
```

アプリケーションを再起動すれば、Vaultからプロパティが取得できます。

---

Spring Cloud Vaultを使ったConfig Data APIの使い方、というよりSpring Cloud Vault自体の使い方の説明しました。

Spring Cloud 2020.0正式リリース後に利用できます。
Config Treeを使えばVaultにアクセスするための情報(金庫の鍵)をよりセキュアに扱えるので良いです。

日本語のVaultの資料は↓がわかりやすいです。
https://github.com/hashicorp-japan/vault-workshop-jp
