repl.info

consul-templateでVaultのシークレットをソースにしている場合、更新してもすぐにはレンダリングされない場合がある

consul-templateでVaultのKVSからデータを取得してファイルに書き出したいことがある。Consulとconsul-templateを組み合わせた時と同様、Vault上のシークレットを更新するとすぐに反映されると思っていた。しかし、そうではなく、KVSの場合そのシークレットのリース期間(設定されていない場合は5分)の85%~95%が過ぎたあたりで更新を試みるようだった。

検証に使ったconsul-templateのコンフィグとテンプレートはこんな感じ。

config.hcl:

log_level = "debug"



vault {

  address = "http://localhost:8200"

  token = "roottoken"

  renew_token = false



  ssl {

    enabled = false

  }

}



template {

  source = "./template.ctmpl"

  destination = "./test.txt"

  command = "echo rendered"

}

template.ctmpl:

{{- with secret "kv-v2/foo" }}

a: {{ .Data.data.a }}

b: {{ .Data.data.b }}

{{- end }}

kv-v2/fooにデータを書き込んでおく。

$ vault kv put kv-v2/foo a=111 b=222

Key Value

--- -----

created_time 2020-03-13T00:55:12.045597Z

deletion_time n/a

destroyed false

version 2



$ vault kv get kv-v2/foo

====== Metadata ======

Key Value

--- -----

created_time 2020-03-13T00:55:12.045597Z

deletion_time n/a

destroyed false

version 2



== Data ==

Key Value

--- -----

a 111

b 222

consul-templateを起動すると、test.txtに書き込まれる。

2020/03/13 00:55:24.545585 [INFO] (runner) creating watcher

2020/03/13 00:55:24.546436 [INFO] (runner) starting

2020/03/13 00:55:24.546455 [DEBUG] (runner) running initial templates

2020/03/13 00:55:24.546462 [DEBUG] (runner) initiating run

2020/03/13 00:55:24.546520 [DEBUG] (runner) checking template dc39c8fc3e10d93709e0aabdf4a1b566

2020/03/13 00:55:24.546827 [DEBUG] (runner) missing data for 1 dependencies

2020/03/13 00:55:24.546846 [DEBUG] (runner) missing dependency: vault.read(kv-v2/foo)

2020/03/13 00:55:24.546853 [DEBUG] (runner) add used dependency vault.read(kv-v2/foo) to missing since isLeader but do not have a watcher

2020/03/13 00:55:24.546858 [DEBUG] (runner) was not watching 1 dependencies

2020/03/13 00:55:24.546860 [DEBUG] (watcher) adding vault.read(kv-v2/foo)

2020/03/13 00:55:24.546870 [DEBUG] (runner) diffing and updating dependencies

2020/03/13 00:55:24.546874 [DEBUG] (runner) watching 1 dependencies

2020/03/13 00:55:24.657740 [DEBUG] (runner) receiving dependency vault.read(kv-v2/foo)

2020/03/13 00:55:24.657783 [DEBUG] (runner) initiating run

2020/03/13 00:55:24.657791 [DEBUG] (runner) checking template dc39c8fc3e10d93709e0aabdf4a1b566

2020/03/13 00:55:24.658390 [DEBUG] (runner) rendering "./template.ctmpl" => "./test.txt"

2020/03/13 00:55:24.672384 [INFO] (runner) rendered "./template.ctmpl" => "./test.txt"

2020/03/13 00:55:24.672419 [DEBUG] (runner) appending command "echo rendered" from "./template.ctmpl" => "./test.txt"

2020/03/13 00:55:24.672428 [DEBUG] (runner) diffing and updating dependencies

2020/03/13 00:55:24.672439 [DEBUG] (runner) vault.read(kv-v2/foo) is still needed

2020/03/13 00:55:24.672445 [INFO] (runner) executing command "echo rendered" from "./template.ctmpl" => "./test.txt"

2020/03/13 00:55:24.672552 [INFO] (child) spawning: echo rendered

rendered

2020/03/13 00:55:24.678989 [DEBUG] (runner) watching 1 dependencies

2020/03/13 00:55:24.679010 [DEBUG] (runner) all templates rendered

2020/03/13 00:55:24.679049 [DEBUG] (cli) receiving signal "child exited"

ここで、Vault側を更新するとconsul-templateがtest.txtを更新してくれるのかと思ったが、そうではなかった。

Consul-template not update file when data in Vault is updated によると、Vaultはシークレットの更新をウォッチするブロッキングクエリをサポートしていないらしい。ではどうやって更新するかというと、シークレットのリース期間の半分が過ぎると更新を試みるらしい。なるほど、ではリース期間から待ち時間を算出するロジックを探してみよう。

https://github.com/hashicorp/consul-template/blob/v0.24.1/dependency/vault_common.go#L126-L167 のleaseCheckWaitが待ち時間を算出する関数のようだ。

// leaseCheckWait accepts a secret and returns the recommended amount of

// time to sleep.

func leaseCheckWait(s *Secret) time.Duration {

	// Handle whether this is an auth or a regular secret.

	base := s.LeaseDuration

	if s.Auth != nil && s.Auth.LeaseDuration > 0 {

		base = s.Auth.LeaseDuration

	}



	// Handle if this is a certificate with no lease

	if certInterface, ok := s.Data["certificate"]; ok && s.LeaseID == "" {

		if certData, ok := certInterface.(string); ok {

			newDuration := durationFromCert(certData)

			if newDuration > 0 {

				log.Printf("[DEBUG] Found certificate and set lease duration to %d seconds", newDuration)

				base = newDuration

			}

		}

	}



	// Ensure we have a lease duration, since sometimes this can be zero.

	if base <= 0 {

		base = int(VaultDefaultLeaseDuration.Seconds())

	}



	// Convert to float seconds.

	sleep := float64(time.Duration(base) * time.Second)



	if vaultSecretRenewable(s) {

		// Renew at 1/3 the remaining lease. This will give us an opportunity to retry

		// at least one more time should the first renewal fail.

		sleep = sleep / 3.0



		// Use some randomness so many clients do not hit Vault simultaneously.

		sleep = sleep * (rand.Float64() + 1) / 2.0

	} else {

		// For non-renewable leases set the renew duration to use much of the secret

		// lease as possible. Use a stagger over 85%-95% of the lease duration so that

		// many clients do not hit Vault simultaneously.

		sleep = sleep * (.85 + rand.Float64()*0.1)

	}



	return time.Duration(sleep)

}

標準的なシークレットの場合、LeaseDurationがあればそれを、なければVaultDefaultLeaseDurationをリース期間として使っている(VaultDefaultLeaseDurationは5分)。そして、シークレットがリニュー可能かどうかでロジックは異なるが、待ち時間を計算して返却する。

kv-v2/fooをみると、lease_durationは0なのでVaultDefaultLeaseDurationが使われる。

{

  "request_id": "c8ffdb88-1b66-10d1-b417-3061e843914f",

  "lease_id": "",

  "lease_duration": 0,

  "renewable": false,

  "data": {

    "data": {

      "a": "111",

      "b": "222",

      "ttl": "60s"

    },

    "metadata": {

      "created_time": "2020-03-13T01:02:53.685381Z",

      "deletion_time": "",

      "destroyed": false,

      "version": 4

    }

  },

  "warnings": null

}

なお、証明書の場合は証明書の発行から失効までの時間をリース期間としている。

consul-templateでVaultと連携してあれこれできて便利だが、更新のタイミングには注意しないといけないということがわかった。