20201104のGoに関する記事は6件です。

Microsoft AzureのGo紹介

2020年11月3日(火)にオンラインで「合同勉強会 in 大都会岡山 -2020 Winter Online-」が開催されました。今回は初のオンライン開催でしたが、バラエティ豊かなセッションが揃った、これまで通りの雰囲気だったので安心しました。

2020年、これまでの「Azure SDK for Go」の学習の過程で私が知り得たMicrosoft Azureな情報のお裾分けと、裏テーマとしてお世話になった方々への恩返しとして、「Microsoft AzureのGo紹介」にて発表させて頂きました。

直前まで何回リハをしても予定の20分を超えてしまいどうなるかと心配していましたが、いざ本番、ほぼ時間通りに終われたということで、ひと安心。

現時点での日本のMicrosoft、Azure、コミュニティの情報と、今回は少しとなってしまった関連するGoの情報を紹介しています。あなたの知らないAzureがあるかも!?

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

go の json 変換の挙動まとめ

go の json 挙動を色々と試したのでそれをまとめます。

json への変換の仕方

まずは、基本的なjsonへの変換の仕方です。 最も簡単な方法としては、以下のような感じで変換を行ないます。

import "encoding/json"
//...
jsonout, err := json.Marshal(対象のオブジェクト)

このやり方は分かりやすいですが、実際は以下のような形で変換することが多いと思います。

encoder := json.NewEncoder(jsonの書きこみ先のio.Writer)
err := encoder.Encode(対象のオブジェクト)

上記のやり方だと、 io.Writer のインターフェイスであれば何でもいけるので、ファイル(os.File)に書きこんだり、httpのレスポンス(http.ResponseWriter)に書きこんだり、バッファ(bufio.Writer)に書きこんだりと自由度が高いです。

基本的な出力

次に基本的な構造体の出力を見ます。 以下のような普通の構造体の場合のjsonの出力内容は以下のようになります。

func main() {
  type Sample struct {
    IDString string
  }
  st := Sample{IDString: "xxfff"}
  out, _ := json.Marshal(st)
  fmt.Println(string(out)) // []byte型なのでstringに変換
  // Output:
  // {"IDString":"xxfff"}
}

構造体がそのままjsonに変換されるイメージです。直感的です。しかし、jsonの命名規則を考えると実用的かと言われるかと微妙です。 多くの人は json のキー名はローワーキャメルで、スネークケースにすることが多いと思います。そのためには、キー名を変更する必要があります。キー名を変更するには tag 文字を使用して変更します。 キー名を変更したケースとしては、以下のようになります。

func main() {
  type Sample struct {
    IDString string `json:"id_string"`
  }

  st := Sample{IDString: "xxfff"}
  out, _ := json.Marshal(st)
  fmt.Println(string(out)) // []byte型なのでstringに変換
  // Output:
  // {"id_string":"xxfff"}
}

いちいちタグ文字を入れるのが面倒と思う人は多いです。しかし、公式のjsonライブラリにはいい感じのオプションはないため、ツールを使ったりして、みなさん頑張っています。

jsonタグについて

json タグのフォーマットは以下のようになります。

`(...) json:"[<key>][,<flag1>[,<flag2>]]" (...)`

上記のように、json タグはキー名を変える以外にも指定できるものがあります。指定できる項目としては以下です。

  • キー名
  • omitempty
  • string

キー名

この項目はその名の通り json の キー名のフィールドになります。後述しますが、キー名の指定の中でも最も優先度の高いものとなります。 - を指定した場合は、そのフィールドをスキップすることになります。 何も書かず,のみの場合は、通常通り構造体のフィールドの名前がそのまま使われます。

omitempty

この項目を指定した場合は、値がゼロ値のさいにスキップされます。go 言語の場合は、初期値がゼロ値として扱われるため、ポインタ型以外の型ではこの項目は使わないかなって思います。 イメージとしては以下のような感じです。

func main() {
  type Sample struct {
    ID      string  `json:",omitempty"`
    Pointer *string `json:",omitempty"`
  }
  s := Sample{ID: "", Pointer: nil}
  out, _ := json.Marshal(s)
  fmt.Println(string(out)) // []byte型なのでstringに変換
  // Output:
  // {}
}

string

この項目は個人的に結構特殊な項目で、値をstring型に変更します。対応しているのは以下の組み込み型となっています。

  • string
  • byte
  • rune
  • int
  • int8
  • int16
  • int32
  • int64
  • uint
  • uint8
  • uint16
  • uint32
  • uint64
  • float32
  • float64
  • bool
  • uintptr

また、string型も、string型に直されるため、 " がエスケープされて出力されます。

func main() {
  type Sample struct {
    ID string `json:",string"`
  }
  s := Sample{ID: "xxffid"}
  out, _ := json.Marshal(s)
  fmt.Println(string(out)) // []byte型なのでstringに変換
  // Output:
  // {"ID":"\"xxffid\""}
}

出力順

出力順は構造体のフィールドの順番と同様になります。しかし、map のような順番が定まっていないものは、キー名のアルファベット順にソートがかけられます。何でこの順番と思いましたが、テストのしやすさを考えるとそんなもんかなぁという気持ちになります。

func main() {
    s := map[string]string{
        "cup":    "one",
        "apple":  "two",
        "banana": "three",
    }
    out, _ := json.Marshal(s)
    fmt.Println(string(out)) // []byte型なのでstringに変換
    // Output:
    // {"apple":"two","banana":"three","cup":"one"}
}

埋め込み型

go 言語の構造体には埋め込み型が可能です。この型はフィールド名を指定ない型のためキー名も存在しません。そのため、基本的には埋め込まれた構造体のフィールドが展開されて出力されます。

func main() {
  type Emb struct {
    Content string
  }
  type Sample struct {
    Emb
  }
  s := Sample{Emb: Emb{Content: "string"}}
  out, _ := json.Marshal(s)
  fmt.Println(string(out)) // []byte型なのでstringに変換
  // Output: {"Content":"string"}
}

基本的と言ったように勿論例外があります。go言語では配列に対して型宣言することが可能です。この場合、展開を行なうと、配列が展開されることになるため、キーの名前が分かりません。そのため、配列の埋め込み型の場合は例外的に、型名がキー名となります。

func main() {
  type Emb []string
  type Sample struct {
    Emb
  }
  s := Sample{Emb: Emb{"content1", "content2"}}
  out, _ := json.Marshal(s)
  fmt.Println(string(out)) // []byte型なのでstringに変換
  // Output:
  // {"Emb":["content1","content2"]}
}

キー名の優先順位

これまでの説明で、キー名は様々な要因で決まることがわかります。しかし、それではキーの衝突が起こってしまいます。そのため、goのjson変換には優先順位が定められています。 優先順位は以下で示すものです。

  1. tag で指定したキー名
  2. 通常のフィールド名
  3. 埋め込み型(埋め込み型のフィールドにも同じ優先順位が適用されます)

もし同じ優先度のものが複数あった場合は、どのキーの値も出力はされませんし、エラーも出力されません。埋め込み型を展開するさいなどに、気をつけないと嵌りそうな挙動です。

宣伝

色々と挙動がありますが、全部覚えるのも一々考えるのも面倒という人は多いと思います。そこで、構造体がどのような json を出力するか調べるツールを作成しました。 https://github.com/komem3/stout

使い方としては、以下のようにファイルパスと、構造体名を指定するだけです。

stout -path ./define.go SimpleStruct

この宣伝のために書いたのでした。

参考文献

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

golang × SQLite3 でLIKE句を使う

はじめに

Go言語の基礎を1~2週間ほど学び、CRUD機能を持つ簡単なwebアプリを作成しました。
その過程のデーターベース操作で躓き(LIKE句)調べても中々解決に繋がる記事を見つけられなかったので
この記事を書くことにしました。

https://qiita.com/__init__/items/2edfc7acf11234e5b1aa
上記の記事にて基本的なデータベース操作は書かれています。
初めてQiita記事を書くのでとりあえず今回は自分が躓いたLIKE句のことのみ記事にしてみます。

環境

バージョン:go 1.15.3

db:SQLite3

PC:Mac

事前準備

golang、SQLite3、go-sqlite3のインストール

※先ほどの記事でインストール手順も詳しく書かれています。
https://qiita.com/__init__/items/2edfc7acf11234e5b1aa

テーブル作成と中身

まずはテーブル作成

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
)

var DbConnection *sql.DB

type Uset struct {
    Id   int
    Name string
    Age  int
}

func main() {
    DbConnection, err := sql.Open("sqlite3", "../example.sql")
    if err != nil {
        log.Fatalln(err)
    }
    defer DbConnection.Close()

    cmd := `CREATE TABLE IF NOT EXISTS user(id INTEGER PRIMARY KEY,
    name STRING,
    age INT)`
    _, err = DbConnection.Exec(cmd)
    if err != nil {
        log.Fatalln(err)
    }
}

結果
$ sqlite3 example.sql
SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
sqlite> .table
user
sqlite> 
データの中身
sqlite> select * from user;
1|yamada|20
2|yamamoto|22
3|suzuki|19
4|tanaka|15
5|miyamoto|30
sqlite> 

レコードを5つ入れておきました。
左から、id name age です

LIKE句

うまくいったコード

cmd := "SELECT * FROM user WHERE name LIKE ?"
rows, err := Dbconnection.Query(cmd, "%yamada%")
if err != nil {
    log.Println(err)
}
defer rows.Close()
var users []User
for rows.Next() {
    var user User
    err = rows.Scan(&user.Id, &m.Name, &m.Age)
    if err != nil {
        log.Println(err)
    }
users = append(users, user)

fmt.Println(users)
結果
$ go run main.go
[{1 yamada 20}]


うまくいかなかったコード

cmd := "SELECT * FROM user WHERE name LIKE '%?%'" //←ここ
rows, err := DbConnection.Query(cmd, "yamada")

// 結果
 go run main.go
[]

『?』 の部分に値を渡すのですが
先に『%%』で囲っておくのではなく『"%yamada%"』のように『%』と一緒に値を渡さないとダメでした。
Python×SQLiteのLIKE句の記事を見つけ参考にしました。


ターミナルなどでは

sqlite> select * from user where name like '%yamada%';
1|yamada|20

でいけます。

整数も同じようにいけます

cmd := "SELECT * FROM user WHERE age LIKE ?"
rows, err := DbConnection.Query(cmd, "%1%")

// 結果
[{3 suzuki 19} {4 tanaka 15}]

他の使用パターン

任意の文字列から始まるレコードを取得するとき

cmd := "SELECT * FROM user WHERE name LIKE ?"
rows, err := DbConnection.Query(cmd, "y%")

// 結果
[{1 yamada 20} {2 yamamoto 22}]

『m』が二回くるデータを取得

cmd := "SELECT * FROM user WHERE name LIKE ?"
rows, err := DbConnection.Query(cmd, "%m%m%")

// 結果
[{2 yamamoto 22} {5 miyamoto 30}]

任意の長さのカラムを持つデータを取得(サンプルでは6文字)

cmd := "SELECT * FROM user WHERE name LIKE ?"
rows, err := DbConnection.Query(cmd, "______")

// 結果
[{1 yamada 20} {3 suzuki 19} {4 tanaka 15}]

最後に

同じように躓く方がいるかわかりませんが、誰かの参考になると幸いです。(コードの詳細まで書きたかったのですが不慣れなせいかここまでかなり時間がかかってしまったのですいません。)
実際に簡単なアプリを作成してみてgolangはRailsのように情報量が豊富ではないと実感したので
今後golangの学習の中で気づいたことなどを記事にしていけたらと思います。
最後まで見ていただきありがとうございました。

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

Docker環境のgRPC/goでハローワールド

はじめに

goのお勉強しはじめた。gRPCのお勉強しはじめた。まず、これを試してみた。
思ったより、つまづいたので記録を残しておく。
Quick start – gRPC

goもgRPCもよくわかってない。

環境は、このdockerイメージを使った。名前的に、欲しいの入ってそうやったので。
grpc/go - Docker Hub

Docker

こんな感じで立ち上げた。Dockerよくわからん 。

docker pull grpc/go
docker run --name grpc-go -it grpc/go /bin/bash 

Quick Start

Quick Startにそって順番に実行していく。

下記コマンドを実行すると、

$ go run greeter_server/main.go

こんな感じのエラーが出る。

cannot find package "golang.org/x/net/http2"

とか

undefined: "github.com/golang/protobuf/proto".ProtoPackageIsVersion4

必要そうなのを入れたり、下記コマンドで入れ直したりした。

go get golang.org/x/sys/unix
go get golang.org/x/net/http2
go get google.golang.org/genproto/googleapis/rpc/status
go get -u github.com/golang/protobuf/proto

最後のやつはコピペもとを真似して-uをつけたけど、よくわかってない。
updateっぽいけど。

go - The Go Programming Language

The -u flag instructs get to update modules providing dependencies of packages named on the command line to use newer minor or patch releases when available. Continuing the previous example, 'go get -u A' will use the latest A with B v1.3.1 (not B v1.2.3). If B requires module C, but C does not provide any packages needed to build packages in A (not including tests), then C will not be updated.

上記エラーが出た時に、下記コマンドうったりして、消して入れ直したりしてたから、バージョンとかがよくわからなくなった。
そのうち、わかるようになりたい。rmしたけど、そういえばバイナリファイルは消してない。どうなったんやろう?上書きされたんかな?

rm -rf /go/src/github.com/golang/protobuf/proto
rm -rf /go/src/google.golang.org/grpc/
git clone https://github.com/grpc/grpc-go

エラー出た時、こことかを参考にした。
Go Frequently Asked Questions  |  Protocol Buffers  |  Google Developers

上記対応で、最初の、serverとclientの方は動いた。
その後、SayHelloAgainをすると、undefinedと言われた。

$ go run greeter_client/main.go 
# command-line-arguments
greeter_client/main.go:59:12: c.SayHelloAgain undefined (type helloworld.GreeterClient has no field or method SayHelloAgain)

main.goにあるpb "google.golang.org/grpc/examples/helloworld/helloworld"が怪しかったので、パスを辿って見てみると、/go/src/google.golang.org/grpc/examples/helloworld/helloworldhelloworld.protoの古いやつがあった。

どうするのが良いのかよくわからんかったけど、修正後の~/grpc-go/examples/helloworld/helloworldを上記パスに上書きコピーした。
再度、serverとclientを実行すると期待通り動いた。
とりあえず、よかった。hello world できた。

環境

最終的に動いた時の環境はこんな感じ。
go getで入れたバージョンの確認方法がよくわからん 。
GOPATHとかはdocker pullしたイメージでデフォルトで入ってた。
.bashrcは空っぽでも動いてる。

# go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/go"
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build743505095=/tmp/go-build -gno-record-gcc-switches"
# go version
go version go1.10.4 linux/amd64
# protoc --version
libprotoc 3.6.0

最後に

ハローワールド的なことできた。よくわからんことだらけなので、ちょっとずつ調べながら学んでこう。

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

Goで排他制御の仕組みを作ってみた

概要

2度目の投稿です。
Goで排他制御の仕組みを書く機会があったので、記録していきます。
今回は2種類の方法で排他制御の仕組みを作っていきます。

各種ツール

  • go 1.14
  • VSCode(1.50.1)
  • golang.org/x/sync v0.0.0-20201020160332

シナリオ

今回はあるテーブルを共有資源として、テーブルに対して複数のプロセスから処理が行われたときに、先行のプロセスが終わるまで、後続のプロセスがテーブルを操作することが出来ないようにする仕組みを作っていきます。

まずは排他制御を行わず、ゴルーチンをつかって同時にテーブルをupdateしてみます。
(sync.WaitGroupはメインのgoroutineを待機させるためのものです。詳しくはこちら。)

var w sync.WaitGroup

func main() {
    w.Add(2)
    go update()
    go update()
    w.Wait()
}

func update() {
    defer w.Done()
    for i := 0; i <= 10; i += 5 {
        fmt.Println("tbl update:", i*10, "%")
        time.Sleep(time.Second)
    }
}

// tbl update: 0 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 100 %

2つのプロセスが同時にテーブルのupdateをしている様子がわかります。
今回はこれを先行のupdateが終わってから、後続のupdateが行われるように、下記のような出力を得られる仕組みを作っていきます。

// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %

sync.Mutexを使ったパターン

まずは、sync.Mutexからです。

// メイン関数は上記と同様

var m sync.Mutex

func update() {
    defer w.Done()
    defer m.Unlock()
    m.Lock()
    for i := 0; i <= 10; i += 5 {
        fmt.Println("tbl update:", i*10, "%")
        time.Sleep(time.Second)
    }
}
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %

update()の冒頭でUnLock()およびLock()を宣言します。
後続のゴルーチンは先行のゴルーチンがUnLock()するまで(関数の処理が終わるまで)Lock()をかけることが出来ず、結果として先行ゴルーチンの処理の終了を待つことになります。
狙った通りの出力が得られていることが分かります。

sync.Mutexは非常にシンプルで分かりやすいですが、処理を行えるプロセスは一つになります。
今回のシナリオとは少し違いますが、例えば「サーバーへの負荷を抑えるため、1000以上のプロセスが同時に実行しないようにする」などの仕組みを作ることは難しそうです。

セマフォを使ったパターン

筆者はセマフォを使うのが初めてなので、セマフォがなんなのかってとこから書いていきます。

セマフォとは?

セマフォは排他制御の仕組みの一つで、鉄道線路の運行をモデル化したものです。
(semaphoreは日本語で信号機の意)
線路という共有資源を同時に使うことがないよう制御するために作られました。
セマフォはセマフォ変数、P操作、V操作の3つで成り立ちます。

  • セマフォ変数
    • 共有資源にアクセスできるプロセスの数
    • 負にはならない
    • セマフォ変数が0 or 1しかとらないものを2値セマフォ(バイナリセマフォ)、0からNをとるものをゼネラルセマフォと呼ぶ
  • P操作
    • セマフォ変数をデクリメントする
    • あるプロセスが共有資源を確保する
  • V操作
    • セマフォ変数をインクリメントする
    • あるプロセスが共有資源を解放する

今回のシナリオは2値セマフォで、update()の冒頭でP操作を、終わりにV操作を行う必要がありそうです。

実装

golang.org/x/syncを使って実装します。

const semaphoreCnt = 1

var s *semaphore.Weighted = semaphore.NewWeighted(semaphoreCnt)

func update() {
    defer w.Done()
    defer s.Release(1)           //V操作 セマフォ変数を1増やす
    s.Acquire(context.TODO(), 1) //P操作 セマフォ変数を1減らす
    for i := 0; i <= 10; i += 5 {
        fmt.Println("tbl update:", i*10, "%")
        time.Sleep(time.Second)
    }
}
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %

golang.org/x/syncAcquire()がP操作に,Release()がV操作にあたります。
semaphore.Weightedでセマフォ変数を定義し、Acquire()及びRelease()を用いて、増減させることで排他制御を実現しています。

また、golang.org/x/syncでは、Acquire(),Release()でセマフォ変数をいくつ増減させるかを調節することが出来ます。(今回は2値セマフォのため、それぞれ1増減となっています。)
これにより、各プロセスの処理の重みを定義することができます。

golang.org/x/syncを使うことでsync.Mutexと比べ、より複雑な排他制御の仕組みを作ることができます。

参考

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

[GKE] Deploymentのreplicasを0にしてGoアプリにOSシグナル SIGTERM を通知

お題

表題の通り。
実際の動きとしてそうなることを確認したかっただけ。

前提

  • GCP環境は用意済み。
  • GCPローカル設定済み。(gcloudコマンドが使用できる状態になっている。)
  • kubectlコマンドが使用できる状態になっている。
  • GKEクラスタ作成済み。

開発環境

# OS - Linux(Ubuntu)

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

# gcloud

$ gcloud version
Google Cloud SDK 312.0.0

# kubectl

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.9", GitCommit:"4fb7ed12476d57b8437ada90b4f93b17ffaeed99", GitTreeState:"clean", BuildDate:"2020-07-15T16:18:16Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"17+", GitVersion:"v1.17.12-gke.2502", GitCommit:"974eff7a63e05b7eb05c9aded92fae8a3ce14521", GitTreeState:"clean", BuildDate:"2020-10-19T17:01:32Z", GoVersion:"go1.13.15b4", Compiler:"gc", Platform:"linux/amd64"}

# バックエンド

# 言語 - Golang

$ go version
go version go1.15.2 linux/amd64

実践

ソース一式

https://github.com/sky0621/study-k8sOnGKE/tree/v0.1.0/try01

ソース

Golang

適当にWebサーバを立てておいて、OSシグナル(SIGTERM)を受信したらログ(GOT_NOTIFY)を吐く。
deferでもログを仕込んでおいて、OSシグナル受信時に、deferのログは出ないことも確認する。

main.go
package main

import (
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    fmt.Println("APP_START")
    defer fmt.Println("DEFER")

    // OSシグナル(SIGTERM)の受信を待ち受ける Goroutine
    go func() {
        fmt.Println("BEFORE_NOTIFY")
        q := make(chan os.Signal, 1)
        signal.Notify(q, syscall.SIGTERM)
        <-q
        fmt.Println("GOT_NOTIFY")

        os.Exit(-1)
    }()

    // 適当にHTTPサーバーを立ち上げておく
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if _, err := fmt.Fprint(w, "Hello"); err != nil {
            fmt.Printf("HANDLE_ERROR_OCCURRED: %+v", err)
        }
    })
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Printf("SERVE_ERROR_OCCURRED: %+v", err)
    }

    fmt.Println("APP_END")
}

Dockerfile

何の変哲もないマルチステージビルドなDockerfile。

FROM golang:1.15 as builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o server

FROM gcr.io/distroless/base
COPY --from=builder /app/server /server
CMD ["/server"]

Cloud Build設定

DockerイメージはContainer Registryを使う。

cloudbuild.yaml
steps:
  - name: 'gcr.io/cloud-builders/docker'
    args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/golang-app-try01', '.' ]
images:
  - 'gcr.io/$PROJECT_ID/golang-app-try01'

上記を使ってビルドする用のシェルは下記。

build.sh
#!/usr/bin/env bash
set -euox pipefail
SCRIPT_DIR=$(dirname "$0")
cd "${SCRIPT_DIR}"

gcloud builds submit --config cloudbuild.yaml .

デプロイ設定

Container RegistryからDockerイメージを取得する。
Podは3つ。
コンテナポートは8080(別に今回は使わないけど)。

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: golang-app-try01
spec:
  replicas: 3
  selector:
    matchLabels:
      app: golang-app-try01
  template:
    metadata:
      labels:
        app: golang-app-try01
    spec:
      containers:
        - name: golang-app-try01
          image: gcr.io/MY_GCP_PROJECT_ID/golang-app-try01
          ports:
            - containerPort: 8080

上記を使ってデプロイするシェルは下記。
自分が使っているGCPプロジェクトのIDが必要で、それ自体はローカル環境でgcloudコマンドから拾えるのだけど、
k8sのYamlに直接書かずにGCPプロジェクトIDを指定する方法(※ConfigMapやSecret経由ならできるのかもだけど、出来れば手軽に)を調べるのが面倒だったので、sed で書き換え。

deploy.sh
#!/usr/bin/env bash
set -euox pipefail
SCRIPT_DIR=$(dirname "$0")
cd "${SCRIPT_DIR}"

project=$(gcloud config get-value project)
if [[ -z "${project}" ]]; then
  echo -n "need project"
  exit 1
fi
echo "${project}"

sed -i -e "s/MY_GCP_PROJECT_ID/${project}/" deployment.yaml

kubectl apply -f deployment.yaml

sed -i -e "s/${project}/MY_GCP_PROJECT_ID/" deployment.yaml

Pod数を書き換えるためのシェル

replica_n.sh
#!/usr/bin/env bash
set -euox pipefail
SCRIPT_DIR=$(dirname "$0")
cd "${SCRIPT_DIR}"

num=${1:-}

if [ -z "${num}" ]; then
  echo -n "input replicas number: "
  read num
fi

kubectl scale deployment golang-app-try01 --replicas="${num}"

動作確認

アプリのビルド(Dockerイメージを作成してContainer Registryに格納)

$ ./build.sh 
++ dirname ./build.sh
+ SCRIPT_DIR=.
+ echo .
.
+ cd .
+ gcloud builds submit --config cloudbuild.yaml .
Creating temporary tarball archive of 6 file(s) totalling 1.7 KiB before compression.
 ・
 ・
 ・
DONE
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

ID                                    CREATE_TIME                DURATION  SOURCE                                                                                   IMAGES                                        STATUS
6452c516-cfbf-4497-b536-378023cbc34d  2020-11-03T19:29:14+00:00  29S       gs://XXXXXXXX_cloudbuild/source/1604431752.38075-ccb069fbb0d0413382dc79d42e5c618a.tgz  gcr.io/XXXXXXXX/golang-app-try01 (+1 more)  SUCCESS

screenshot-console.cloud.google.com-2020.11.04-05_33_09.png

GKEにデプロイ

$ ./deploy.sh 
++ dirname ./deploy.sh
+ SCRIPT_DIR=.
+ echo .
.
+ cd .
 ・
 ・
 ・
+ kubectl apply -f deployment.yaml
deployment.apps/golang-app-try01 created
 ・
 ・
 ・

Podが3つ。

$ kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
golang-app-try01   3/3     3            3           4m19s

この時点でコンテナログを見ると、3つのPodそれぞれで、アプリ起動時とOSシグナル待受開始のログが出ていることがわかる。

screenshot-console.cloud.google.com-2020.11.04-04_52_58.png

Pod数を0に変更

$ ./replica_n.sh 0
++ dirname ./replica_n.sh
+ SCRIPT_DIR=.
+ echo .
.
+ cd .
+ num=0
+ '[' -z 0 ']'
+ kubectl scale deployment golang-app-try01 --replicas=0
deployment.apps/golang-app-try01 scaled

OSシグナル受信時のログ(GOT_NOTIFY)がそれぞれのPodのログとして出た。
deferで仕込んでいた方のログ(DEFER)は出ない。

screenshot-console.cloud.google.com-2020.11.04-04_53_28.png

まとめ

GKEに載せるなら、アプリ停止時に確実に処理させたい内容は defer でなく、OSシグナル(SIGTERM)受信用の Goroutine を別途立てて対応。

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