Go製ソフトウェアの開発中にインポートしているパッケージを修正して手元で動作確認する方法

Goで書かれたソフトウェアを触っていると、そのソフトウェアがインポートしているパッケージを修正して手元で動作確認したいことがある。Go Modulesのreplaceを使うと手軽に実現できるので、その方法について記録しておく。

例としてhashicorp/vaultを挙げる。vaultは機能ごとにプラグインとして外部パッケージに切り出していて、例えばシークレットのKVはhashicorp/vault-plugin-secrets-kv だったりする。この時、vault-plugin-secrets-kv側を変更して、その変更をVaultとして動作確認したいというわけである。

ではどうするかというと、Go Modulesにあるreplaceを使う。

replace github.com/hashicorp/vault-plugin-secrets-kv => ../vault-plugin-secrets-kv

このようにgithub.com/hashicorp/vault-plugin-secrets-kvを相対パスで指定することで、修正内容が含まれるパッケージでビルドすることができる。

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

Ubuntuでユーザーディレクトリを再作成するには mkhomedir_helper を使うと良い

Ubuntuでユーザーディエレクトリを消した後に/etc/skelをベースに再作成したい時があると思うのだけど、そういう時は mkhomedir_helper が使える。

Usage: /sbin/mkhomedir_helper <username> [<umask> [<skeldir>]]

こういう感じでユーザー名を指定するとその名前でユーザーディレクトリが作られる。

Goのtext/templateで、”{{“と”}}”を出力する

Goのtext/templateで、delimiterとして使われる”{{“と”}}”が含まれる文字列を出力したい。

まずは基本的な使い方を確認。”{{“と”}}”で囲んだ箇所は評価される。

https://play.golang.org/p/o0Y5CaZpIMN

package main

import (
	"log"
	"os"
	"text/template"
)


func main() {
	t := `
{{ .Foo }}
`
	tpl, err := template.New("t").Parse(t)
	if err != nil {
		log.Fatal(err)
	}
	data := map[string]interface{}{
		"Foo": "aaa",
	}
	tpl.Execute(os.Stdout, data)
}

結果は次の通り。

aaa

さて、ここからが本題。text/templateを使って、{{ foo }} というテキストを得たい場合どうするか。

素朴にエスケープしてもうまくいかない。

https://play.golang.org/p/loznU_SgNU2

package main

import (
	"log"
	"os"
	"text/template"
)

func main() {
	t := `
\{\{ foo \}\}
`
	tpl, err := template.New("t").Parse(t)
	if err != nil {
		log.Fatal(err)
	}
	data := map[string]interface{}{
		"Foo": "aaa",
	}
	tpl.Execute(os.Stdout, data)
}

\が結果に含まれてしまうため、これでは駄目。

\{\{ foo \}\}

ではどうするかというと、次のようにdelimiterの中で文字列を評価することで実現できる。

https://play.golang.org/p/ko6-P7p86fo

package main

import (
	"log"
	"os"
	"text/template"
)

func main() {
	t := `
{{ "{{ foo }}" }}
`
	tpl, err := template.New("t").Parse(t)
	if err != nil {
		log.Fatal(err)
	}
	data := map[string]interface{}{
		"Foo": "aaa",
	}
	tpl.Execute(os.Stdout, data)
}

次のように、期待した結果が出力される。テンプレートがややこしくなり、読みにくくなるが…

{{ foo }}

delimiterを変えることでも実現できる。

https://play.golang.org/p/IPUxNVPrw5Y

package main

import (
	"log"
	"os"
	"text/template"
)

func main() {
	t := `
{{ foo }}
`
	tpl, err := template.New("t").Delims("[[", "]]").Parse(t)
	if err != nil {
		log.Fatal(err)
	}
	data := map[string]interface{}{
		"Foo": "aaa",
	}
	tpl.Execute(os.Stdout, data)
}

この場合でも、期待した結果を得られる。テンプレートはスッキリするが、delimiterが普段と違うとそれはそれで混乱する気がする。

{{ foo }}

知っているとすぐ読めるけど、知らないとどうなってるのこれ?とハマりそうだな、これ。

Vaultをインメモリで起動してテストに使うテクニックの紹介

Hashicorp Vaultを扱うコードのテストを書く場合、Vaultへのアクセス部分をモックするかテスト時にVaultサーバーを起動するかのどちらかを選ぶことになる。今回は、テスト前にVaultをインメモリで起動する方法を紹介する。

テストのためのVaultのリポジトリの中には、テスト用のサーバーを起動する関数がいくつかある。これを使うことで、UnsealeされたVaultサーバーを使うことができる。

package foo

import (
  "testing"
  vhttp "github.com/hashicorp/vault/http"
  "github.com/hashicorp/vault/vault"
)

func Test_vault(t *testing.T) {
  core, _, token := vault.TestCoreUnsealed(t)
  ln, addr := vhttp.TestServer(t, core)
  defer ln.Close()

  // ここにVaultのAPIを使うテストを書く

}

ただし、この場合はKVSは使えるがPKIは使えない。PKIに関するテストを行いたい場合、vault.CoreConfigでバックエンドとしてPKIを指定する必要がある。

package foo

import (
  "testing"
  vhttp "github.com/hashicorp/vault/http"
  "github.com/hashicorp/vault/vault"
)

func Test_vault_with_pki(t *testing.T) {
  coreConfig := &vault.CoreConfig{
    LogicalBackends: map[string]logical.Factory{
      "pki": pki.Factory,
    },
  }
  core, _, token := vault.TestCoreUnsealedWithConfig(t, coreConfig)
  ln, addr := vhttp.TestServer(t, core)

  // ここにVaultのAPIを使うテストを書く

}

このように、インメモリでVaultを起動すればより本番環境に近い形でテストを行うこともできるし、別途Vaultを頑張って起動する必要もなくなる。便利なのでお試しください。