---
title: Cloud Foundry上にVaultをデプロイ
tags: ["Cloud Foundry", "Vault"]
categories: ["Dev", "SecretManagement", "Vault"]
date: 2017-06-09T03:32:23Z
updated: 2020-02-23T11:04:20Z
---

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

### やりたいこと

機密情報管理に便利な[Hashicorp Vault](https://www.vaultproject.io/)。
[Spring Vault](http://projects.spring.io/spring-vault/)や[Spring Cloud ConfigのVault Backend](https://cloud.spring.io/spring-cloud-config/spring-cloud-config.html#_vault_backend)、[Cloud Foundry Vault Service Broker](https://www.hashicorp.com/blog/cloud-foundry-vault-service-broker/)など、Vault関連のプロジェクトが増えていき、色々試したいが、実際にアプリケーションをデプロイした時にも接続できる場所にVaultを置いておきたい。
Cloud Foundryにできれば運用が楽になるので、`cf push`でVaultをデプロイしてみた。

Vault自体はGoで書かれたHTTPサーバーなので基本的には`cf push`できる。ただし、そのままpushすると次の課題が出る。

* StorageがIn-Memoryなので再起動するとデータが消える
* Storageを永続化したとしても、再起動すると自動でSealされるので、Unsealしないといけない。


Storageは[いくつか選択肢](https://www.vaultproject.io/docs/configuration/storage/index.html)があるが、Cloud FoundryのサービスとしてもっともポピュラーなMySQLを使う。
[MySQL Storage](https://www.vaultproject.io/docs/configuration/storage/mysql.html)はHA非対応だし、Communityによる開発なので、Production用途ではないけれども、開発用途でCloud上に置いておくバックエンドとしては十分。


### MySQLサービスインスタンスの作成

まずは`vault-db`というMySQLのサービスインスタンスを作成する。`cf login`している前提で説明する。

#### Pivotal Web Servicesの場合

[Pivotal Web Services](https://run.pivotal.io)を使う場合は

```
cf create-service cleardb spark vault-db
```

#### Pivotal Cloud Foundryの場合

[Pivotal Cloud Foundry](https://docs.pivotal.io/pivotalcf/)を使う場合は

```
cf create-service p-mysql 100mb-dev vault-db
```

他のCloud Foundryサービスの場合でもOKだと思う。

### Vaultのデプロイ

ではVaultをCloud Foundryにデプロイする。

#### Vaultのダウンロード

Vaultは実行可能バイナリとして配布されている。[Download Vault](https://www.vaultproject.io/downloads.html)のLinux 64-bitをダウンロードして展開すると実行可能な`vault`ファイルが手に入る。Cloud Foundryのコンテナ上で動かすので、Linux用のバイナリにすること。

以降、空の`/tmp/cf-vault`ディレクトリを作業ディレクトリとして進める。

```
mkdir -p /tmp/cf-vault
cd /tmp/cf-vault
wget https://releases.hashicorp.com/vault/1.3.2/vault_1.3.2_linux_amd64.zip
unzip vault_1.3.2_linux_amd64.zip
rm -f vault_1.3.2_linux_amd64.zip
```

#### 起動スクリプトの作成

次にVaultの起動スクリプト`run.sh`を`/tmp/cf-vault`に作成。ここで先ほど作成したMySQLサービスの接続情報を取得する。サービスインスタンスがアプリケーションにバインドされると[環境変数`VCAP_SERVICES`にJSON形式で接続情報](https://docs.pivotal.io/pivotalcf/devguide/deploy-apps/environment-variable.html#VCAP-SERVICES)が含まれるので、これ`jq`コマンドで加工する。
スクリプトは次のようになる。

``` sh
#!/bin/sh

CLEARDB=`echo $VCAP_SERVICES | grep "cleardb"`
PMYSQL=`echo $VCAP_SERVICES | grep "p-mysql"`

if [ "$CLEARDB" != "" ];then
	SERVICE="cleardb"
elif [ "$PMYSQL" != "" ]; then
	SERVICE="p-mysql"
fi

echo "detected $SERVICE"

HOSTNAME=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.hostname'`
PASSWORD=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.password'`
PORT=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.port'`
USERNAME=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.username'`
DATABASE=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.name'`

cat <<EOF > cf.hcl
ui = true
disable_mlock = true
storage "mysql" {
  username = "$USERNAME"
  password = "$PASSWORD"
  address = "$HOSTNAME:$PORT"
  database = "$DATABASE"
  table = "vault"
}
listener "tcp" {
 address = "0.0.0.0:8080"
 tls_disable = 1
}
EOF

echo "#### Starting Vault..."

./vault server -config=cf.hcl &
```

実行権限をつけておくこと。

```
chmod +x run.sh
```


> この記事で想定していないMySQLを使いたい場合は`run.sh`中の`HOSTNAME`、`PASSWORD`、`PORT`、`USERNAME`、`DATABASE`を設定している箇所を変更すれば良い。

#### manifest.ymlの作成

Cloud Foundryにデプロイするための`manifest.yml`は次の通り。今回はbinary_buildpackを使用する。メモリは32MBくらいで十分。
application名は重複するかもしれないので変えたほうが無難。

``` yaml
applications:
- name: cf-vault
  buildpack: binary_buildpack
  memory: 32m
  command: './run.sh'
  services:
  - vault-db
```

`manifest.yml`ができてしまえば、あとは

```
cf push
```

でデプロイ完了。

`cf logs`で次のようなログが出力される。

```
2020-02-23T19:49:13.61+0900 [CELL/0] OUT Cell 6d545859-b729-4434-8158-8d0ae0e3dd0a successfully created container for instance 2f6f4417-7bec-4292-5c1f-7252
2020-02-23T19:49:14.11+0900 [CELL/0] OUT Downloading droplet...
2020-02-23T19:49:18.60+0900 [CELL/0] OUT Downloaded droplet (45.5M)
2020-02-23T19:49:18.60+0900 [CELL/0] OUT Starting health monitoring of container
2020-02-23T19:49:29.21+0900 [APP/PROC/WEB/0] OUT detected cleardb
2020-02-23T19:49:30.04+0900 [APP/PROC/WEB/0] OUT #### Starting Vault...
2020-02-23T19:49:33.17+0900 [APP/PROC/WEB/0] OUT ==> Vault server configuration:
2020-02-23T19:49:33.20+0900 [APP/PROC/WEB/0] OUT                      Cgo: disabled
2020-02-23T19:49:33.31+0900 [APP/PROC/WEB/0] OUT               Listener 1: tcp (addr: "0.0.0.0:8080", cluster address: "0.0.0.0:8081", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
2020-02-23T19:49:33.32+0900 [APP/PROC/WEB/0] OUT                Log Level: info
2020-02-23T19:49:33.35+0900 [APP/PROC/WEB/0] OUT                    Mlock: supported: true, enabled: false
2020-02-23T19:49:33.37+0900 [APP/PROC/WEB/0] OUT            Recovery Mode: false
2020-02-23T19:49:33.39+0900 [APP/PROC/WEB/0] OUT                  Storage: mysql (HA disabled)
2020-02-23T19:49:33.40+0900 [APP/PROC/WEB/0] OUT                  Version: Vault v1.3.2
2020-02-23T19:49:33.44+0900 [APP/PROC/WEB/0] OUT ==> Vault server started! Log data will stream in below:
2020-02-23T19:49:33.44+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:49:31.994Z [INFO]  proxy environment: http_proxy= https_proxy= no_proxy=
2020-02-23T19:49:33.44+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:49:33.137Z [WARN]  no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set
2020-02-23T19:49:34.78+0900 [CELL/0] OUT Container became healthy
2020-02-23T19:50:30.30+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:30.306Z [ERROR] core: no seal config found, can't determine if legacy or new-style shamir
2020-02-23T19:50:30.46+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:30.460Z [INFO]  core: security barrier not initialized
2020-02-23T19:50:30.65+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:30.654Z [INFO]  core: security barrier initialized: stored=1 shares=5 threshold=3
2020-02-23T19:50:30.83+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:30.830Z [INFO]  core: post-unseal setup starting
2020-02-23T19:50:31.33+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:31.335Z [INFO]  core: loaded wrapping token key
2020-02-23T19:50:31.33+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:31.337Z [INFO]  core: successfully setup plugin catalog: plugin-directory=
2020-02-23T19:50:31.38+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:31.387Z [INFO]  core: no mounts; adding default mount table
2020-02-23T19:50:31.45+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:31.450Z [INFO]  core: successfully mounted backend: type=cubbyhole path=cubbyhole/
2020-02-23T19:50:31.47+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:31.460Z [INFO]  core: successfully mounted backend: type=system path=sys/
2020-02-23T19:50:31.51+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:31.510Z [INFO]  core: successfully mounted backend: type=identity path=identity/
2020-02-23T19:50:32.14+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.142Z [INFO]  core: successfully enabled credential backend: type=token path=token/
2020-02-23T19:50:32.14+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.142Z [INFO]  core: restoring leases
2020-02-23T19:50:32.14+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.143Z [INFO]  rollback: starting rollback manager
2020-02-23T19:50:32.18+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.184Z [INFO]  expiration: lease restore complete
2020-02-23T19:50:32.30+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.306Z [INFO]  identity: entities restored
2020-02-23T19:50:32.32+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.324Z [INFO]  identity: groups restored
2020-02-23T19:50:32.43+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.432Z [INFO]  core: post-unseal setup complete
2020-02-23T19:50:32.51+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.514Z [INFO]  core: root token generated
2020-02-23T19:50:32.51+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.514Z [INFO]  core: pre-seal teardown starting
2020-02-23T19:50:32.51+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.514Z [INFO]  rollback: stopping rollback manager
2020-02-23T19:50:32.51+0900 [APP/PROC/WEB/0] ERR 2020-02-23T10:50:32.519Z [INFO]  core: pre-seal teardown complete
```

`https://<アプリケーションのURL>/v1/`にアクセスして`"Vault is sealed"`と返って来ればOK。

Pivotal Web Servicesの場合、

```
$ curl https://<your application name>.cfapps.io/v1/
{"errors":["Vault is sealed"]}
```

> `manifest.yml`を作りたくない場合は、次のコマンドでもOK
> 
> ```
> cf push cf-vault -b binary_buildpack -m 32m -c './run.sh' --no-start
> cf bind-service cf-vault vault-db
> cf start cf-vault
> ```

### Vaultの初期化

起動した直後のVaultは"Sealed"な状態なので、"Unseal"する必要がある。`vault`コマンドで初期化する。

> Macからアクセスする場合は別途`vault`コマンドをインストールする必要がある。
>
> ```
> brew install vault
> ```

#### 初期化

```
export VAULT_ADDR=https://<アプリケーションのURL>
vault operator init
```

このコマンドを実行すると次のように出力される。

```
Unseal Key 1: USDincNpoViKoTGZJ4pX0uVi5iG+E9Avdp7Gcpv4IcUe
Unseal Key 2: f1DoeJsOaUgm4wScFaegeVmrXDYQURUaJr8bmP3AtvkQ
Unseal Key 3: xnBMnxlWucQwDa1IYYtUSwYaXudzvatITtMTnMtKNT+s
Unseal Key 4: gqcCx6iV1DXgeur9jU5+NfyoUMVfZRylo5VCPW3+jeT7
Unseal Key 5: PFJFSzRHhdCjkFpl6E3PwncG75qnuLNDQD4BA1MuJzLN
Initial Root Token: 06fdd16a-8941-1f78-2454-f25a13f6d55c

vault initialized with 5 keys and a key threshold of 3. Please
securely distribute the above keys. When the vault is re-sealed,
restarted, or stopped, you must provide at least 3 of these keys
to unseal it again.

Vault does not store the master key. Without at least 3 keys,
your vault will remain permanently sealed.
```

このUnseal KeyとRoot Tokenが重要。

#### Unseal

表示された5つのUnseal Keyのうち、3つを使ってUnsealする。

```
vault operator unseal USDincNpoViKoTGZJ4pX0uVi5iG+E9Avdp7Gcpv4IcUe
vault operator unseal f1DoeJsOaUgm4wScFaegeVmrXDYQURUaJr8bmP3AtvkQ
vault operator unseal xnBMnxlWucQwDa1IYYtUSwYaXudzvatITtMTnMtKNT+s
```

三回目の結果に`Sealed: false`が含まれるはず。

#### ログイン

Root Tokenを使ってログインする。

```
vault login 06fdd16a-8941-1f78-2454-f25a13f6d55c
```

`Successfully authenticated! You are now logged in.`が出力されればOK。

再度`/v1/`にアクセスすると、`errors`がなくなっている。

```
$ curl https://<your application name>.cfapps.io/v1/
{"errors":[""]}
```

これで自分専用のVaultが出来上がり、この環境で[VaultのGetting Started](https://www.vaultproject.io/intro/getting-started/first-secret.html)も始められる。

```
vault secrets enable -path=kv kv
vault secrets list
```

```
$ vault kv put kv/hello target=world
Success! Data written to: kv/hello
```

### 再起動時の自動Unseal

残る問題は再起動した際にVaultがまたSealされるので、再度`vault operator unseal`しないといけないこと。
セキュリティ上重要であるが、Cloud Foundryのようにコンテナがダウンしても自動復旧が行われると不都合である。
自動で復旧してほしいので、ここでは割り切って`run.sh`内で起動時に自動でUnsealするようにする。

`run.sh`を次のように修正。

``` sh
#!/bin/sh

CLEARDB=`echo $VCAP_SERVICES | grep "cleardb"`
PMYSQL=`echo $VCAP_SERVICES | grep "p-mysql"`

if [ "$CLEARDB" != "" ];then
	SERVICE="cleardb"
elif [ "$PMYSQL" != "" ]; then
	SERVICE="p-mysql"
fi

echo "detected $SERVICE"

HOSTNAME=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.hostname'`
PASSWORD=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.password'`
PORT=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.port'`
USERNAME=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.username'`
DATABASE=`echo $VCAP_SERVICES | jq -r '.["'$SERVICE'"][0].credentials.name'`

cat <<EOF > cf.hcl
disable_mlock = true
storage "mysql" {
  username = "$USERNAME"
  password = "$PASSWORD"
  address = "$HOSTNAME:$PORT"
  database = "$DATABASE"
  table = "vault"
}
listener "tcp" {
 address = "0.0.0.0:8080"
 tls_disable = 1
}
EOF

echo "#### Starting Vault..."

./vault server -config=cf.hcl &

###### !!!!ここから追加!!!!

if [ "$VAULT_UNSEAL_KEY1" != "" ];then
	export VAULT_ADDR='http://127.0.0.1:8080'
	echo "#### Waiting..."
	sleep 1
	echo "#### Unsealing..."
	if [ "$VAULT_UNSEAL_KEY1" != "" ];then
		./vault operator unseal $VAULT_UNSEAL_KEY1
	fi
	if [ "$VAULT_UNSEAL_KEY2" != "" ];then
		./vault operator unseal $VAULT_UNSEAL_KEY2
	fi
	if [ "$VAULT_UNSEAL_KEY3" != "" ];then
		./vault operator unseal $VAULT_UNSEAL_KEY3
	fi
fi
```


`manifest.yml`にUnseal Keyを設定

``` yaml
applications:
- name: cf-vault
  buildpack: binary_buildpack
  memory: 32m
  command: './run.sh'
  services:
  - vault-db
  env:
    VAULT_UNSEAL_KEY1: USDincNpoViKoTGZJ4pX0uVi5iG+E9Avdp7Gcpv4IcUe
    VAULT_UNSEAL_KEY2: f1DoeJsOaUgm4wScFaegeVmrXDYQURUaJr8bmP3AtvkQ
    VAULT_UNSEAL_KEY3: xnBMnxlWucQwDa1IYYtUSwYaXudzvatITtMTnMtKNT+s
```


これで再起動や再ステージング(コンテナイメージ再作成)しても、同じMySQLインスタンスを使う限り、そのまま`vault`コマンドでVaultにアクセスできる。

```
$ vault kv get kv/hello
===== Data =====
Key       Value
---       -----
target    world
vaule           	world
```

> このやり方は[おすすめされていない](https://www.vaultproject.io/docs/concepts/seal.html#unsealing)ので注意。Consulを使ったHAモードならうまくUnsealされる？

### まとめ

Cloud Foundryを使って、自動復旧可能な自分専用のVaultサーバーを持つことができた。
[Github](https://github.com/making/cf-vault)にもサンプルを置いた。

32MBメモリしか使わないので、[Pivotal Web Services](https://run.pivotal.io/)だと$0.675/month(だいたい月75円くらい)で運用できる。

$86分の無料ライセンスに含まれて気軽に試せるので、開発用のVaultが欲しい人はどうぞ。

次はこのVaultサーバーを使って、

* [Cloud Foundry Vault Service Broker](https://www.hashicorp.com/blog/cloud-foundry-vault-service-broker/)
* [Spring Cloud ServicesのVault & Multiple Backendサポート](https://content.pivotal.io/blog/spring-cloud-services-supports-vault-multiple-backends-use-the-right-config-repo-for-the-job)

を試したい。
