[GKE] Deploymentのreplicasを０にして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のログは出ないことも確認する。

```go: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。

```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](https://cloud.google.com/container-registry?hl=ja)を使う。

```yaml: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'
```

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

```shell: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は３つ。
コンテナポートは8080（別に今回は使わないけど）。

```yaml: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` で書き換え。

```shell: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数を書き換えるためのシェル

```shell: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/43c72cc3-4b7c-af85-827e-b5e816d812a2.png)

### GKEにデプロイ

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

Podが３つ。

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

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

![screenshot-console.cloud.google.com-2020.11.04-04_52_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/68f6daa4-76ab-13dc-07c3-8c30f11a336a.png)

### Pod数を０に変更

```
$ ./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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/79d57aae-7aff-da49-7102-4f697e72d378.png)

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