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

Pocket

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と連携してあれこれできて便利だが、更新のタイミングには注意しないといけないということがわかった。

Leave a Reply

Your email address will not be published. Required fields are marked *