3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

googleのsubcommandsでGCPのSecret Managerへの読み書きをラップ

Posted at

お題

GCPにはSecret Managerというのがある。
Kubernetes知ってる人は「Secretsみたいなものか」思うかも。ただk8sのSecretsはBase64エンコードなので、(例えば元データの管理の名目としても)PublicなGitHubリポジトリにアップするわけにいかないけど、Secret Managerの方は暗号化されてるので(鍵を知らなければ)まさにSecret。

その他、ウリみたいなものについては以下参照。
https://cloud.google.com/blog/ja/products/identity-security/introducing-google-clouds-secret-manager

上記参考記事にも記載してあるけど、Secret Managerに限らずGCPのサービス群はCloud SDKを使えば、用意されたコマンドを叩くだけで簡単にGCP上のリソースを操作できる。
なので、今回のお題にあるように、わざわざプログラムでラッパーを書く必要はまったくない。
今回は単にgoogleのsubcommandsというライブラリを使って適当なコマンドラインツールを書く題材としてSecret Managerを操作する機能を簡易ラップしただけ。

想定する読者

  • GCPについては知っている。
  • Golangもそれなりに書ける。

前提

  • ローカルにGoの開発環境構築済み。
  • GCP契約済み。
  • ローカルでCloud SDKのセットアップ済み。
  • ローカルの環境変数GOOGLE_APPLICATION_CREDENTIALSに(必要な権限を全て有したサービスアカウントの)鍵JSONファイルパス設定済み。

開発環境

# OS - Linux(Ubuntu)

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"

# バックエンド

# 言語 - Golang

$ go version
go version go1.15.2 linux/amd64

IDE - Goland

GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020

今回の全ソース

個別ソース

解説を書こうと思ったけど、googleのsubcommandsを使う上でお約束の記述と、Secret ManagerのSDKを使う上でお約束の記述ばかり(つまり、それぞれのサイトに記載のある情報)なので、ほぼ説明レス。

main.go

機能として、Secretを作成するための「create」コマンドと、作成済みのSecret一覧を表示する「list」コマンドだけ用意。

package main

import (
	"context"
	"flag"
	"os"

	"github.com/google/subcommands"
)

func main() {
	os.Exit(int(execMain()))
}

func execMain() subcommands.ExitStatus {
	subcommands.Register(subcommands.HelpCommand(), "")
	subcommands.Register(newCreateCmd(), "create")
	subcommands.Register(newListCmd(), "list")
	flag.Parse()
	return subcommands.Execute(context.Background())
}

create.go

package main

import (
	"context"
	"flag"
	"fmt"
	"io/ioutil"
	"log"

	secretmanager "cloud.google.com/go/secretmanager/apiv1"
	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"

	"github.com/google/subcommands"
)

type createCmd struct {
	projectID, key, value, path string
}

func newCreateCmd() *createCmd {
	return &createCmd{}
}

func (*createCmd) Name() string {
	return "create"
}

func (*createCmd) Synopsis() string {
	return "create secret"
}

func (*createCmd) Usage() string {
	return `usage: create secret`
}

func (cmd *createCmd) SetFlags(f *flag.FlagSet) {
	f.StringVar(&cmd.projectID, "p", "", "project id")
	f.StringVar(&cmd.key, "k", "", "key")
	f.StringVar(&cmd.value, "v", "", "value")
	f.StringVar(&cmd.path, "f", "", "file path")
}

func (cmd *createCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
	if cmd.projectID == "" || cmd.key == "" || (cmd.value == "" && cmd.path == "") {
		log.Println("need -p [gcp project id] -k [secret key] -v [secret value] or -f [secret file path]")
		return subcommands.ExitFailure
	}

	var client *secretmanager.Client
	{
		var err error
		client, err = secretmanager.NewClient(ctx)
		if err != nil {
			log.Fatalf("failed to setup client: %v", err)
		}
	}

	// Create the request to create the secret.
	createSecretReq := &secretmanagerpb.CreateSecretRequest{
		Parent:   fmt.Sprintf("projects/%s", cmd.projectID),
		SecretId: cmd.key,
		Secret: &secretmanagerpb.Secret{
			Replication: &secretmanagerpb.Replication{
				Replication: &secretmanagerpb.Replication_Automatic_{
					Automatic: &secretmanagerpb.Replication_Automatic{},
				},
			},
		},
	}

	var secret *secretmanagerpb.Secret
	{
		var err error
		secret, err = client.CreateSecret(ctx, createSecretReq)
		if err != nil {
			log.Fatalf("failed to create secret: %v", err)
		}
	}

	// Declare the payload to storage.
	var payload []byte
	if cmd.value != "" {
		payload = []byte(cmd.value)
	}
	if cmd.path != "" {
		ba, err := ioutil.ReadFile(cmd.path)
		if err != nil {
			log.Fatalf("failed to read secret file: %+v", err)
		}
		payload = ba
	}
	if payload == nil {
		log.Fatal("payload is nil")
	}

	// Build the request.
	addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{
		Parent: secret.Name,
		Payload: &secretmanagerpb.SecretPayload{
			Data: payload,
		},
	}

	var accessRequest *secretmanagerpb.AccessSecretVersionRequest
	{
		// Call the API.
		version, err := client.AddSecretVersion(ctx, addSecretVersionReq)
		if err != nil {
			log.Fatalf("failed to add secret version: %v", err)
		}

		// Build the request.
		accessRequest = &secretmanagerpb.AccessSecretVersionRequest{
			Name: version.Name,
		}
	}

	// Call the API.
	result, err := client.AccessSecretVersion(ctx, accessRequest)
	if err != nil {
		log.Fatalf("failed to access secret version: %v", err)
	}

	// Print the secret payload.
	//
	// WARNING: Do not print the secret in a production environment - this
	// snippet is showing how to access the secret material.
	log.Printf("Plaintext: %s", result.Payload.Data)

	return subcommands.ExitSuccess
}

list.go

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"strings"

	secretmanager "cloud.google.com/go/secretmanager/apiv1"
	"google.golang.org/api/iterator"
	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"

	"github.com/google/subcommands"
)

type listCmd struct {
	projectID string
}

func newListCmd() *listCmd {
	return &listCmd{}
}

func (*listCmd) Name() string {
	return "list"
}

func (*listCmd) Synopsis() string {
	return "list secrets"
}

func (*listCmd) Usage() string {
	return `usage: list secrets`
}

func (cmd *listCmd) SetFlags(f *flag.FlagSet) {
	f.StringVar(&cmd.projectID, "p", "", "project id")
}

func (cmd *listCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
	if cmd.projectID == "" {
		log.Println("need -p [gcp project id]")
		return subcommands.ExitFailure
	}

	client, err := secretmanager.NewClient(ctx)
	if err != nil {
		log.Printf("failed to create secretmanager client: %v", err)
		return subcommands.ExitFailure
	}

	// Build the request.
	req := &secretmanagerpb.ListSecretsRequest{
		Parent: fmt.Sprintf("projects/%s", cmd.projectID),
	}

	// Call the API.
	it := client.ListSecrets(ctx, req)
	for {
		resp, err := it.Next()
		if err == iterator.Done {
			break
		}

		if err != nil {
			log.Printf("failed to list secret versions: %v", err)
			return subcommands.ExitFailure
		}

		names := strings.Split(resp.Name, "/")
		reqName := fmt.Sprintf("projects/%s/secrets/%s/versions/%s", names[1], names[3], "latest")

		// Build the request.
		req := &secretmanagerpb.AccessSecretVersionRequest{
			Name: reqName,
		}

		// Call the API.
		result, err := client.AccessSecretVersion(ctx, req)
		if err != nil {
			log.Printf("failed to access secret version: %v", err)
			return subcommands.ExitFailure
		}

		log.Printf("Found secret %s ... got value: %s\n", resp.Name, string(result.Payload.Data))
	}
	return subcommands.ExitSuccess
}

実践

Secretを追加

$ go run ./*.go create -p XXXXXXXX -k rdb-host -v localhost
2020/10/11 23:03:33 Plaintext: localhost
$ go run ./*.go create -p XXXXXXXX -k rdb-port -v 12345
2020/10/11 23:04:05 Plaintext: 12345
$ go run ./*.go create -p XXXXXXXX -k rdb-user -v user1
2020/10/11 23:04:24 Plaintext: user1
$ go run ./*.go create -p XXXXXXXX -k rdb-pass -v pass1234
2020/10/11 23:04:47 Plaintext: pass1234

XXXXXXXXの部分は自分が持っているGCPプロジェクトのID

GCPのコンソールマネージャーで見ると、こんな感じで作成されている。
screenshot-console.cloud.google.com-2020.10.11-23_05_07.png

Secret一覧を表示

$ go run ./*.go list -p fs-work-21
2020/10/11 23:09:34 Found secret projects/999999999999/secrets/rdb-host ... got value: localhost
2020/10/11 23:09:35 Found secret projects/999999999999/secrets/rdb-pass ... got value: pass1234
2020/10/11 23:09:35 Found secret projects/999999999999/secrets/rdb-port ... got value: 12345
2020/10/11 23:09:35 Found secret projects/999999999999/secrets/rdb-user ... got value: user1

まとめ

Secret Managerで管理させたSecret情報って、たとえば何に使うのか?
用途はいろいろあると思うけど、自分の場合だと、Cloud Runに載せるサービスに環境変数伝いでDBパスワードとかを渡す時かな。
ビルド用のシェルの中でgcloudコマンドでSecret Managerから取得したDBパスワードをset envしてる。
具体的なやり方うんぬんは、また後ほど。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?