お題
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のコンソールマネージャーで見ると、こんな感じで作成されている。
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してる。
具体的なやり方うんぬんは、また後ほど。