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

【Go】踏み台サーバー経由 (ssh) で VPC エンドポイントの Elasticsearch Service にローカルから接続する【AWS】

やりたいこと

踏み台サーバー経由 (ssh) でプライベートサブネット内の RDS (MySQL) へ接続するのと同様にして、VPC エンドポイントの Elasticsearch Service にも踏み台サーバー経由で接続することが目標です。

ssh コマンドと curl コマンドを利用すれば次のように簡単にローカルから接続できますが、今回は Go プログラムで接続することを目標とします。

ssh -i <path/to/private-key> <username>@<hostname> curl -s '<ES_ENDPOINT>/_cat/indices?format=json&pretty'

前提

次のようなアーキテクチャを想定します。セキュリティグループはいい感じに設定されているものとします。

AWS (2019年) フレームワーク.jpeg

実装

Elasticsearch のクライアント用ライブラリとしては Elastic 社公式の elastic/go-elasticsearch を利用します。

実装のポイントは http.RoundTripper (http.Transport) の Dial に SSH Client の Dial を利用することです。

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "os"
    "time"

    "github.com/elastic/go-elasticsearch/v8"
    "github.com/elastic/go-elasticsearch/v8/esapi"
    "golang.org/x/crypto/ssh"
)

func main() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)

    var (
        sshUser       = os.Getenv("SSH_USER")
        sshHost       = os.Getenv("SSH_HOST")
        sshPort       = os.Getenv("SSH_PORT")
        sshPrivateKey = os.Getenv("SSH_PRIVATE_KEY")
        esEndpoint    = os.Getenv("ES_ENDPOINT")
    )

    // ------------------------------
    // 秘密鍵ファイルの読み込み
    // ------------------------------
    b, err := ioutil.ReadFile(sshPrivateKey)
    if err != nil {
        log.Fatal(err)
    }

    signer, err := ssh.ParsePrivateKey(b)
    if err != nil {
        log.Fatal(err)
    }

    // ------------------------------
    // SSH クライアントの生成
    // ------------------------------
    sshConf := ssh.ClientConfig{
        User: sshUser,
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(signer),
        },
        HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
            return nil
        },
        Timeout: 10 * time.Second,
    }

    sshClient, err := ssh.Dial("tcp", net.JoinHostPort(sshHost, sshPort), &sshConf)
    if err != nil {
        log.Fatal(err)
    }
    defer sshClient.Close()

    // ------------------------------
    // Elasticsearch クライアントの生成
    // ------------------------------
    esConf := elasticsearch.Config{
        Addresses: []string{esEndpoint},
        Transport: &http.Transport{
            Proxy:               http.ProxyFromEnvironment,
            Dial:                sshClient.Dial, // ここで SSH Client を利用
            TLSHandshakeTimeout: 10 * time.Second,
        },
    }

    es, err := elasticsearch.NewClient(esConf)
    if err != nil {
        log.Fatal(err)
    }

    // ------------------------------
    // リクエストを実行 (/_cat/indices)
    // ------------------------------
    req := esapi.CatIndicesRequest{
        Format: "json",
        Pretty: true,
    }

    ctx := context.Background()

    resp, err := req.Do(ctx, es)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    // ------------------------------
    // レスポンスを解析
    // ------------------------------
    if resp.IsError() {
        log.Fatal(resp.String())
    }

    body := io.TeeReader(resp.Body, os.Stdout) // debug

    var r []map[string]interface{}
    if err := json.NewDecoder(body).Decode(&r); err != nil {
        log.Fatal(err)
    }

    for i, obj := range r {
        fmt.Printf("\n[#%d]\n", i)
        for k, v := range obj {
            fmt.Println(k, v)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS EC2 の golang 開発環境構築 - 2020

事前準備

Golang インストール

List
$ amazon-linux-extras | grep golang
29  golang1.11=latest        enabled      \
Install
$ sudo amazon-linux-extras install -y golang1.11
$ go version
go version go1.13.4 linux/amd64

VSCode Plugin Install

1.png

Ctrl + Shift + P で ツールなどインストール

2.png
3.png

Hello World!

NewFile
$ cd $GOPATH
$ mkdir src
$ cd src
$ mkdir hello
$ cd hello
$ touch hello.go

VSCode で開く

Remote Development 機能使ってますが、省略します。
4.png
5.png

Coding

6.png

Execute

7.png

Debug

8.png
9.png

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

Golangでマルチプログレスバーを表示したい!!

Golangの十八番と言えば並行処理なわけですが、それぞれの処理がどのくらいの進捗なのか知りたい!っということで、pbとuiprogressと言う人気のパッケージを使って動かしてみました。

Golangのプログレスバーを実装するパッケージについては、こちらの記事で紹介しています。
Golangでプログレスバーを表示するためのパッケージ3選

pbでやってみた

main.go
package main

import (
    "log"
    "math/rand"
    "sync"
    "time"

    "github.com/cheggaaa/pb"
)

func processing(wg *sync.WaitGroup, b *pb.ProgressBar) {
    defer wg.Done()
    for n := 0; n < 200; n++ {
        b.Increment()
        rand.Seed(time.Now().UnixNano())
        time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
    }
    b.Finish()
}

func main() {
    process_1 := pb.New(200).Prefix("process_1: ").SetMaxWidth(80)
    process_2 := pb.New(200).Prefix("process_2: ").SetMaxWidth(80)
    process_3 := pb.New(200).Prefix("process_3: ").SetMaxWidth(80)

    pool, err := pb.StartPool(process_1, process_2, process_3)
    if err != nil {
        log.Fatal(err)
    }

    var wg sync.WaitGroup
    for _, bar := range []*pb.ProgressBar{process_1, process_2, process_3} {
        wg.Add(1)
        go processing(&wg, bar)
    }
    wg.Wait()
    pool.Stop()
}

実行してみるとこんな感じ
pb_multi.gif

uiprogressでやってみた

main.go
package main

import (
    "math/rand"
    "sync"
    "time"

    "github.com/gosuri/uiprogress"
    "github.com/gosuri/uiprogress/util/strutil"
)

func processing(process_name string, wg *sync.WaitGroup, u *uiprogress.Bar) {
    defer wg.Done()
    u.PrependFunc(func(b *uiprogress.Bar) string {
        return strutil.Resize(process_name + ": ", 11)
    })
    for u.Incr() {
        rand.Seed(time.Now().UnixNano())
        time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
    }
}

func main() {
    uiprogress.Start()
    process_1 := uiprogress.AddBar(100).AppendCompleted().PrependElapsed()
    process_2 := uiprogress.AddBar(300).AppendCompleted().PrependElapsed()
    process_3 := uiprogress.AddBar(200).AppendCompleted().PrependElapsed()

    var wg sync.WaitGroup

    wg.Add(1)
    go processing("process_1", &wg, process_1)
    wg.Add(1)
    go processing("process_2", &wg, process_2)
    wg.Add(1)
    go processing("process_3", &wg, process_3)

    wg.Wait()
}

実行してみるとこんな感じ
uiprogress_multi.gif

まとめ

なんか実際動かしてみると、Dockerでイメージをpullする時みたいでエモいですねw

どちらのパッケージも使いやすいので、馴染みのいい方を選んでカスタムするといいと思います。

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

【Go】 ユニットテストでflagへ引数を渡す際のハマりどころ

はじめに

はじめまして。Go初学者です。
Gopher道場#8や自身の勉強にて、気づいた学びを共有しています。

今回は、あるCLIツールのユニットテストを実装する際のflag引数の渡し方と、 flag.Parse() の実行位置によってテストが落ちる問題、またその理由についてまとめました。

結論から

  • ユニットテスト時は flag.CommandLine.Set(name, value string) error を利用すると実行時引数を渡す事ができる
  • ただし、func init()flag.Parse() を行っていると、テスト時の実行順序の関係上、期待しない動きをする事がある。

testing

is 何

詳細は公式ドキュメントへ

goのテストを書く際に利用する標準パッケージです。テストは go test コマンドで実行します。
一般的に、テスト対象となるファイル ex) main.go の末尾に _test.goを付けて ex) main_test.go テストコードを記載しています。

main.go
package main

func square(i int) int {
    return i * i
}

func main() {}
main_test.go
package main

import "testing"

func Test_square(t *testing.T) {
    tests := []struct {
        name string
        i int
        want int
    }{
        {name: "1", i: 1, want: 1 },
        {name: "2", i: 2, want: 4 },
        {name: "3", i: 3, want: 9 },
        {name: "4", i: 4, want: 16 },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := square(tt.i); got != tt.want {
                t.Errorf("square() = %v, want %v", got, tt.want)
            }
        })
    }
}

The Go Playgroundのデフォルトセットにも、サンプルがありますね。

flag

is 何

詳細は公式ドキュメントへ

コマンドラインフラグを扱う際に利用する標準パッケージです。
./square -target=10 といった形で、 -targetに対して値を渡す形でコマンドを実行したい場合に利用します。
以下は、flag.IntVarを利用してコマンドラインフラグを指定し、flag.Parse()を実行することで指定した値を受け取り、パッケージ変数 i へ格納しています。

main.go
package main

import (
    "flag"
    "fmt"
    "os"
)

var (
    i int
)

func init() {
    flag.IntVar(&i, "target", 0, "Enter an integer to the square (not 0)")
}

func square(i int) int {
    return i * i
}

func run() int {
    flag.Parse()
    if i == 0 {
        fmt.Println("input 0")
        return 1
    }
    fmt.Println(square(i))
    return 0
}

func main() {
    os.Exit(run())
}

ユニットテストでflagへ引数を渡す

前述したソースの run() 関数をテストします。
テスト関数内で、 flag.Commandline.Set(name, value string) errorを利用します。
その後、テスト対象の run() 関数内で flag.Parse() が実行される事で、パッケージ変数に値を格納することができます。

main_test.go
func Test_run(t *testing.T) {
    tests := []struct {
        name string
        i    int
        want int
    }{
        {name: "input0", i: 0, want: 1},
        {name: "input1", i: 1, want: 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            flag.CommandLine.Set("target", strconv.Itoa(tt.i)) // -target=iと指定したかの様に設定できる
            if got := run(); got != tt.want {
                t.Errorf("run() = %v, want %v", got, tt.want)
            }
        })
    }

ユニットテスト時のハマリどころ

上記のflagの例ですが、サラっとinit()関数を利用しています。
こちらはパッケージの初期化の際に動く関数で、パッケージ変数の複雑な初期化などに利用します。 詳細は公式ドキュメントへ
今回はコマンドラインフラグ -targetを指定する必要があったため、init()関数に記載しています。
当初は以下の通り init()を記述していました。

main.go
func init() {
    flag.IntVar(&i, "target", 0, "Enter an integer to the square (not 0)")
    flag.Parse()
}

この様に記述した場合、後述する実行順序の関係で、下記の通りtestそのものが動作しなくなってしまいます。

$ go test
flag provided but not defined: -test.timeout
Usage of /var/folders/53/j681dgkd1wb7jjyvg8vkwf8h0000gp/T/go-build009487560/b001/aggreagtemyqiita.test:
  -target int
        Enter an integer to the square (not 0)
exit status 2
FAIL    aggregate-my-qiita/cmd/aggreagtemyqiita 0.300s

エラー内容を見る限り、どうやらflagパッケージが関係していそうですね。

テストの実行順序について

こちらの記事に詳細が書かれています。
go testを実行する際は、メイン関数そのものを実行しテストフラグを付与する事で、テストを実行しているようです。

また、Go1.13リリースノートに以下の通り記載されていました。

Testing flags are now registered in the new Init function, which is invoked by the generated main function for the test. As a result, testing flags are now only registered when running a test binary, and packages that call flag.Parse during package initialization may cause tests to fail.

テスト用に生成されたメイン関数によって呼び出される新しい Init関数にテストフラグが登録されるようになりました。その結果、テストフラグはテストバイナリの実行時にのみ登録されるようになり、パッケージの初期化中に flag.Parse を呼び出すパッケージではテストが失敗する可能性があります。

つまり、私のハマったポイントとしては、init() 関数に flag.Parse() を記載してしまったため、該当のパッケージのテストにおいて、
テスト用のメイン関数のflagセットの前に flag.Parse() が実行されてしまったため、前述のエラーが出たと解釈しました。
間違っていたらご指摘ください。

おわりに

ハマった事で、 go testが何をしているか、Goの初期化から実行までの順序について学ぶ事ができました。
GoはGoで書かれているので、いずれソースコード上で上記の流れを追ってみたいと思います。

参考

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

Goでクッキーをブラウザに渡したり取得したりしてみる

Goの標準パッケージを使ってクッキーの操作をやってみました。

クッキーをブラウザに渡す

コードはこんな感じです。

main.go
package main

import (
    "net/http"
    "log"
)

func setCookie(w http.ResponseWriter,r *http.Request){
    c1:=http.Cookie{
        Name: "cookie1",
        Value:"Hello World!",
        HttpOnly: true,
    }
    c2:=http.Cookie{
        Name:"cookie2",
        Value:"Hello Quita!",
    }
    http.SetCookie(w,&c1)
    http.SetCookie(w,&c2)
}

func main(){
    http.HandleFunc("/set_cookie",setCookie)
    log.Fatal(http.ListenAndServe(":8080",nil))
}

実行して localhost:8080/set_cookie をブラウザで開くと一見なにも変化がないこのような画面になります。
スクリーンショット 2020-07-13 20.41.59.png

しかし、Chromeの検証でApplication>Storage>Cookiesを見るとこのようにクッキーがブラウザに渡せていることがわかります。
次は逆に渡したクッキーをブラウザから取得し、画面に出力してみます。
スクリーンショット 2020-07-13 20.44.41.png

クッキーをブラウザから取得

コードはこちらです。

main.go
package main

import (
    "fmt"
    "log"
    "net/http"
)

func getCookie(w http.ResponseWriter,r *http.Request){
    //名前が"coockie1"であるクッキーを取得
    c1,err:=r.Cookie("cookie1")
    if err!=nil{
        log.Fatal(err)
    }

    //c1を出力
    fmt.Fprintln(w,c1)

    //クッキーをスライスで全取得
    cSlice:=r.Cookies()

    //cSliceを出力
    fmt.Fprintln(w,cSlice)


}

func main(){
    http.HandleFunc("/get_cookie",getCookie)
    log.Fatal(http.ListenAndServe(":8080",nil))
}

http://localhost:8080/get_cookie にアクセスするとこのように先ほどブラウザに渡したクッキーを取得できていることがわかります。
メソッドCoockies()はすべてのクッキーをスライス形で返します。
スクリーンショット 2020-07-13 21.06.12.png

参考

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