20201011のGoに関する記事は5件です。

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

お題

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

今回の全ソース

https://github.com/sky0621/gcp-toolbox/tree/v0.1.0/secret-manager

個別ソース

解説を書こうと思ったけど、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してる。
具体的なやり方うんぬんは、また後ほど。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[GoLand]コメントの先頭にスペースを設定する

1.はじめに

ググっても解決策(GoLandの公式ヘルプ)がなかなかヒットしなかったので書きました。
個人的にはGoLandを別のPCにインストールしたときという、たまにの機会に忘れていて結構困るためです。

1-1.この記事を読んだらどうなるのか

フォーマット時にコメントの先頭にスペースが自動的に入るようになって、楽になれる。

1-2.対象読者

GoLandでコメントの先頭にスペースを自動でいれられずに苦しんでいる人。

2.GoLandの設定

こちらが公式のヘルプ
公式を引用させてもらいます。

1.設定ウィンドウ(ファイル | 設定)を開きます。
2.エディター | コード・スタイル | Goをクリックします。
3.その他タブをクリックします。
4.コメントに先行スペースを追加するチェックボックスを選択します。
5.コメントのスペースを除いてフィールドで、追加アイコン(the Add button)をクリックします。
6.例外として追加する接頭辞の名前を入力します(たとえば、easyjson)。
7.OKをクリックします。
code_style_add_leading_space_to_comments.png

これでフォーマット時にコメントの先頭に空行が入るようになります。

参考

公式ヘルプ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語で見かける「...(ピリオド3つ)」の使い方

1.はじめに

これからGoを始める人のためになればと思って記事を書きました。
ソースコードで「...」を初めて見たときに、ググっても日本語で情報がまとまっているものがあまり無いので、私が業務でよく使っている「...」の使い方をまとめようとおもいました。

1-1.この記事を読んだらどうなるのか

「...」の使い方とポイントを理解できる

1-2.対象読者

Goの「...」がわからない人

2.「...」の使い方

2-1.関数のパラメータを可変長にしたいとき

関数のパラメータを可変長にすることができます。
たとえば、関数で複数のintを可変で受け取りたいときがあるとしたら、以下のように書くことができます。
ポイントは ...int のように型の前に...を書くことです。

// WriteInt はintを可変で受け取って出力する関数
func WriteInt(nums ...int) {
    for _, v := range nums {
        fmt.Println(v)
    }
}

実行すると、以下の結果になります

func main() {
    WriteInt(1, 2, 3)
}

// 実行結果
1
2
3

Go Playgroud

2-2.可変長引数の関数に値を全て渡す

前の章で説明した可変長引数の関数に対して、「...」を使うことで一気に渡すことができます。
ポイントは s... のように渡したい値の後に...を書くことです。

func main() {
    s := []int{1, 2, 3}

    // sが持つ値を...を使って、全てWriteIntに渡している
    WriteInt(s...)
}

func WriteInt(nums ...int) {
    for _, v := range nums {
        fmt.Println(v)
    }
}

Go Playgroud

sliceをappendするときにも便利です。

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{4, 5, 6}
    s3 := append(s1, s2...)
    fmt.Println(s3)
}

// 実行結果
[1 2 3 4 5 6]

参考

公式ドキュメント Passing_arguments_to_..._parameters
公式ドキュメント append
3 dots in 4 places
Goで可変引数の関数にスライスを展開して渡す
★ Ultimate Guide to Go Variadic Functions

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Go]「...(ピリオド3つ)」の使い方

1.はじめに

これからGoを始める人のためになればと思って記事を書きました。
ソースコードで「...」を初めて見たときに、ググっても日本語で情報がまとまっているものがあまり無いので、私が業務でよく使っている「...」の使い方をまとめようとおもいました。

1-1.この記事を読んだらどうなるのか

「...」の使い方とポイントを理解できる

1-2.対象読者

Goの「...」がわからない人

2.「...」の使い方

2-1.関数のパラメータを可変長にしたいとき

関数のパラメータを可変長にすることができます。
たとえば、関数で複数のintを可変で受け取りたいときがあるとしたら、以下のように書くことができます。
ポイントは ...int のように型の前に...を書くことです。

// WriteInt はintを可変で受け取って出力する関数
func WriteInt(nums ...int) {
    for _, v := range nums {
        fmt.Println(v)
    }
}

実行すると、以下の結果になります

func main() {
    WriteInt(1, 2, 3)
}

// 実行結果
1
2
3

Go Playgroud

2-2.可変長引数の関数に値を全て渡す

前の章で説明した可変長引数の関数に対して、「...」を使うことで一気に渡すことができます。
ポイントは s... のように渡したい値の後に...を書くことです。

func main() {
    s := []int{1, 2, 3}

    // sが持つ値を...を使って、全てWriteIntに渡している
    WriteInt(s...)
}

func WriteInt(nums ...int) {
    for _, v := range nums {
        fmt.Println(v)
    }
}

Go Playgroud

sliceをappendするときにも便利です。

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{4, 5, 6}
    s3 := append(s1, s2...)
    fmt.Println(s3)
}

// 実行結果
[1 2 3 4 5 6]

参考

公式ドキュメント Passing_arguments_to_..._parameters
公式ドキュメント append
3 dots in 4 places
Goで可変引数の関数にスライスを展開して渡す
★ Ultimate Guide to Go Variadic Functions

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

QiitaのtagフィードをCLIで見るためのCLIツールをGo言語で書いた

Zennのこちらの記事(フィードを取得する Go 言語パッケージ) を読んで、gofeedという便利なツールがあることを知り、触ってみたくなったので、QiitaのtagフィードをCLI上で表示するだけの、シンプルなツールを作ってみました。

qiita-tag-feed-reader-cli - CLI reader of Qiita tag feed.

qiita-tag-feed-reader-cliの使い方

上のリンク先にあるREADMEのままの説明になりますが、下記のとおりです。

# install
go get github.com/shinshin86/qiita-tag-feed-reader-cli

# ランダムにタグを指定してフィードを表示する
qiita-tag-feed-reader-cli

# 指定したタグのフィードを表示する(この場合は"Go"でも"go"でもOK)
qiita-tag-feed-reader-cli Go

# 勿論、日本語も使えます
qiita-tag-feed-reader-cli セキュリティ

完全にgofeed触りたかったのと、Go自体の勉強目的のために作ったようなツールですが、意外とビルド中などのちょっとした空き時間に使えるようなツールになったので、一応こちらでもご報告させていただいた次第です。
(なにより Qiita に関連したもののため)

ソース自体は、非常に小規模なため、どういうことをやっているかは直接ソースコードを読んで頂くのが一番良いかと思います。
が、一応、Qiitaに書いているわけだし、技術ポイント(と言えるほどじゃないですが)を書き残しておきます。

gofeedについては解説しません

なお、ここまで話しておいてちゃぶ台をひっくり返すようですが、gofeed自体の使い方はgofeedのREADMEと、上に貼ったZennのポストを見れば分かるレベルなので、割愛します。

以前自分でもGoで、取得したfeedをパースするコードを書いたことがありましたが、gofeedがあれば、そこらへんの処理を一気にすっ飛ばせるので、便利だなと思いました。使いやすいツールです。

CLIならではの表示順序

実際にコマンドを叩いていただくと分かると思いますが、情報は下記のような並びで表示しています。
これは、CLI上で使う故、下から上へと目線が動いていくことを考慮したものとなっています。

・
・
・
----------------------------------------------------
<feed items>
-----------------------
<feed items>
-----------------------
<feed items>
-----------------------
<feed title>
<feed type> <feed version>
======================

フィードに関する情報

gofeedを使いたかったということもあり、tagのフィードを取得しています。
このフィードURLについては下記のQiitaポストを参考にさせていただきました。

Qiita 記事/ユーザ/Organization/タグ のフィード URL(フォローしたいユーザーやタグの XML/ATOM の URL) - タグ・フィードの URL

取得したHTMLのHTMLタグを除去する処理

HTMLタグを除去する処理は下記のコードを参考に実装させていただいています。
(というか、HTMLコメントを対応した以外はほとんど同じだ...)
https://gist.github.com/g10guang/04f11221dadf1ed019e0d3cf3e82caf3

実際に qiita-tag-feed-reader-cli 内で書いてあるHTMLタグ除去のソースコードを下記に載せます。

func removeHTMLTag(html string) string {
    const pattern = `(<\/?[a-zA-A!-]+?[^>]*\/?>)*`
    r := regexp.MustCompile(pattern)
    groups := r.FindAllString(html, -1)

    // Replace the long string first
    sort.Slice(groups, func(i, j int) bool {
        return len(groups[i]) > len(groups[j])
    })

    for _, group := range groups {
        if strings.TrimSpace(group) != "" {
            html = strings.ReplaceAll(html, group, "")
        }
    }
    return html
}

参照元のコードを見ながら実装していたときに、なるほどなーと思ったのは、上のコードのコメントにもあるように、文字列を長い順から置換していっているところです。

というのも、こちらの正規表現でタグを抜き出した場合、取得したタグのパターンとしては、例えば下記のような2パターンのタグが取得できます。

</span></div>
</span>

strings.ReplaceAllでHTMLを置換していく際、</span>から先に置換された場合、 </span></div>は取り残されてしまうため、長い文字列から置換を行うようにしています。
これですべてのHTMLタグの除去に成功します。

本当はこの挙動をちゃんと証明するためのテストコードも必要ですが、早く形にしたかったので横着しました。
せっかくなので、後日テストコードも追加しようと思います。
→追記: テストを追加しました。

Qiitaのtagをランダムに取得する部分について

これは技術ポイントではないのですが、このツールを引数無しで実行した場合、Qiitaのtagをランダムに選択してfeedを表示しようとします。
このQiitaのtagについては下記のコードを参照して、人気順にtagを取得しています。

(Qiita API v2 活用) Qiita のタグ情報を API 経由で取得する方法 - タグを指定しないで取得するサンプルコード

ソースコードを読んでいただいた方はもうお分かりかと思いますが、私が実装した時点での人気タグ100個をGoファイル内で管理して、それを実行時に呼び出しています。
そのため、私がこのツールを作成した当時の人気のタグ100個の中からランダムに選ばれる形となっています。

実行時にリアルタイムで100個のタグを選択して〜、というのは勿論可能ですが、元々gofeed試したい目的、ということもあり、ここらへんも完全に横着しています。
あと、Qiita APIは認証していないとすぐに上限が来てしまうと思うので、そこらへんのことも考慮したくないという気持ちもありました。

おわり

というわけで、もしよろしければ使ってみてください。
自分的にはこのツールのシンプルさが、ちょっとした空き時間と相性良くて、意外と時間が潰せるツールになりました。ビルドの待ち時間などにぜひぜひ。

実際に使ってみたところ↓
qiita-tag-feed-reader-cli demo gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む