Contents

Build Kuberntes GRPC Health Probe with Pack

img/pod-loap.webp
Kuberntes Pod 生命周期

在 Kubernetes Pod 完整的生命周期包含了三個部份: Iinit container Pod Hook 健康檢查。這三部都會影響到 Pod 的生命周期,而本篇文章說明如何使用 pack 打包 grpc-health-probe 來支援 GRPC 健康檢查

Kubernetes livenessProbe & readinessProbe

Configure Liveness, Readiness and Startup Probes | Kubernetes

在 Kubernetes cluster 中我們可以通過配置 livenessProbereadinessProbe 二個探針來影響容器的生命周期

  • livenessProbe: 簡單的來說就是 Kubectl 通過 livenessProbe 來判斷容器是否存活 (Running),如果 livenessProbe 探針偵測到容器不健康,Kubectl 就會刪除容器,並依據容器的重啟策略來處理,如果容器不包含 livenessProbe 探針,Kubectl 預設就會認定 livenessProbe 探針回傳值永遠為 Success

  • readinessProbe: 簡單的來說就是 Kubectl 通過 livenessProbe 來判斷容器的可用性 (Ready),只有 Pod 下面所有容器的狀態都是就緒時,Kubectl 才會認定該 Pod 已經處於可工作狀態。如果該 Pod 執行過期中 Ready 狀態變成 False,系統會將其從 Service 的後端 Endpoints 列表中移除,等待 Pod Ready 狀態再度成為 True 時加為 Service Endpoints 列表,這樣可以確認流量不會被導至不可用的 Pod

在配置 livenessProbereadinessProbe 都有三種指定方式

  1. ExecAction: 在容器中執行指令,回傳值為 0 表示健康
  2. TCPSocketAction: 透過容器的 IP 及 Port 進行檢查,如果可以建立 TCP 連線表示健康
  3. HTTPGetAction: 透過容器的 IP 及 Port 進行 HTTP GET 檢查,如果回傳狀態碼介於 200 - 400 表示健康

Health checking gRPC servers on Kubernetes

/posts/build-kubernetes-grpc-health-probe-with-pack/img/grpc_health_probe.png
grpc_health_probe
(ref: https://kubernetes.io/blog/2018/10/01/health-checking-grpc-servers-on-kubernetes/)

本篇文章因為要檢查 GRPC 服務是否健康,則屬於第一種 ExecAction 的範籌。Health checking gRPC servers on Kubernetes | Kubernetes 文章也說明如何使用 grpc-health-probe 工具來檢查 GRPC 是否健康

在 Pod 中我們可以配置 readinessProbelivenessProbe

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
spec:
  containers:
  - name: server
    image: "[YOUR-DOCKER-IMAGE]"
    ports:
      - containerPort: 10021
    readinessProbe:
      exec:
        command: ["/layers/cage1016_github-assets-cnb/github-assets/bin/grpc_health_probe", "-addr=:10021"]
      initialDelaySeconds: 5
      periodSeconds: 10
    livenessProbe:
      exec:
        command: ["/layers/cage1016_github-assets-cnb/github-assets/bin/grpc_health_probe", "-addr=:10021"]
      initialDelaySeconds: 10
      periodSeconds: 20

在 exec ommand 中 grpc_health_probe 的執行檔是 /layers/cage1016_github-assets-cnb/github-assets/bin/grpc_health_probe 而非 grpc-health-probe 中看到的 /bin/grpc_health_probe 則是本篇的重點,我們慢慢說明

GPRC Server 健康檢查準備

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// HealthServer is the server API for Health service.
type HealthServer interface {
	// If the requested service is unknown, the call will fail with status
	// NOT_FOUND.
	Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
	// Performs a watch for the serving status of the requested service.
	// The server will immediately send back a message indicating the current
	// serving status.  It will then subsequently send a new message whenever
	// the service's serving status changes.
	//
	// If the requested service is unknown when the call is received, the
	// server will send a message setting the serving status to
	// SERVICE_UNKNOWN but will *not* terminate the call.  If at some
	// future point, the serving status of the service becomes known, the
	// server will send a new message with the service's serving status.
	//
	// If the call terminates with status UNIMPLEMENTED, then clients
	// should assume this method is not supported and should not retry the
	// call.  If the call terminates with any other status (including OK),
	// clients should retry the call with appropriate exponential backoff.
	Watch(*HealthCheckRequest, Health_WatchServer) error
}

在要使用 grpc_health_probe 來檢查 GRCP 狀態是否健康,在 Server 端也需要進行一些配合,實作二個方法 CheckWatch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  // imports
  "google.golang.org/grpc/health"
	healthgrpc "google.golang.org/grpc/health/grpc_health_v1"

  // health server
	hs := health.NewServer()
	hs.SetServingStatus(cfg.ServiceName, healthgrpc.HealthCheckResponse_SERVING)

  // register healdh grpc server
  server = grpc.NewServer(grpc.UnaryInterceptor(kitgrpc.Interceptor))
  healthgrpc.RegisterHealthServer(server, hs)

我們直接使用 google.golang.org/grpc/health 提供的方法進行配置就可以快速完成 GRPC Server 端的準備工具,詳細的程式碼可以至 cage1016/ms-demo/cmd/add/main.go#L76-L77cage1016/ms-demo/cmd/add/main.go#L190-L192

GPRC client 健康檢查準備

grpc-ecosystem/grpc-health-probe: A command-line tool to perform health-checks for gRPC applications in Kubernetes etc.

1
2
grpc_health_probe -addr=localhost:5000
healthy: SERVING

在 local 的部份可以下載 grpc_health_probe 進行測試

建構 Container Image

本篇文章的重點就是如何使用 Pack 來建構含有 grpc_health_probe 功能的 container image

方法一 Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
FROM gcr.io/gcp-runtimes/go1-builder:1.14 AS builder
WORKDIR /src

# restore dependencies
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -gcflags='-N -l' -o /exe cmd/add/main.go

# Adding the grpc_health_probe
RUN GRPC_HEALTH_PROBE_VERSION=v0.3.2 && \
    wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \
    chmod +x /bin/grpc_health_probe

FROM gcr.io/distroless/base:latest
COPY --from=builder /exe .
COPY --from=builder /bin/grpc_health_probe ./grpc_health_probe
ENTRYPOINT ["/exe"]

在建構 Container image 時下載 grpc_health_probe 執行檔至 /bin/grpc_health_probe,所以在 Kubernetes Pod livenessProbereadinessProbe 中的 command 才會是 command: ["/grpc_health_probe", "-addr=:5000"]

在之前的文章

都有分享使用 Pack 來建構 container image。那我們如何使用 Pack 提供的方式在建構 container image 時如同寫 Dockerfile 去下載我們所需的 grpc_health_probe 檔案呢?

Github Asset Buildpack

cage1016/github-assets-cnb: A Cloud Native Buildpack that Download Github Assets

buildpack cage1016/[email protected] 提供了一個簡易的方式讓你透過 pack 建構 container image 時動態下載所需的 Github Assets,在這一次的使用情境也派上了用場

  1. 建立一個 project.toml 並配置 buildpack cage1016/github-assets-cnb 所需的參數

    • repo: Github Repo
    • asset: Github Repo asset name
    • tag: Release tag name, default set to “latest”
    • token_env: (optional), Please assign ENV name for private repo
    • destination: download asset destination path to, bin/<your-asset> for application/x-executable asset
    • strip_components: x-tar, gzip, x-zx suuport StripComponents feature.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    cat <<EOF >> project.toml
    # assign token
    [[build.env]]
    name = "APITEST_TOOLCHAIN_TOKEN"
    value = "<github-token>"
    
    [[metadata.githubassets]]
    repo = "kkdai/youtube"
    asset = "youtubedr_2.7.0_linux_arm64.tar.gz"
    destination = "bin"
    
    [[metadata.githubassets]]
    repo = "qeek-dev/apitest-toolchain"
    token_env = "APITEST_TOOLCHAIN_TOKEN"
    asset = "apitest-toolchain-linux-amd64"
    destination = "bin/apitest-toolchain"
    tag = "v0.1.0"
    
    [[metadata.githubassets]]
    repo = "stedolan/jq"
    asset = "jq-linux64"
    destination = "bin/jq"
    EOF
    
  2. 明確的指定所需的 buildpack 給 Pack 指令。一般來說,Pack 會跟據 builder 中的配置自動偵測目標目錄下需要載入那幾個 buildpacks

    buildpacks/builder.toml at main · GoogleCloudPlatform/buildpacks

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    ######
    # Go #
    ######
    
    [[order]]
    
      [[order.group]]
        id = "google.go.runtime"
    
      [[order.group]]
        id = "google.go.functions-framework"
    
      [[order.group]]
        id = "google.go.build"
    
      [[order.group]]
        id = "google.config.entrypoint"
        optional = true
    
      [[order.group]]
        id = "google.go.clear_source"
        optional = true
    
      [[order.group]]
        id = "google.utils.label"
    

    以 GoogleCloudPlatform/buildpacks builder.toml 所定義的來說,針對 Golang 語言定義了 6 個 buildpack,而這些 buildpack 也會在 builder 動態偵測目標目錄所需,並不是 Golang 就一定是 6 個

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    ...
    Status: Image is up to date for gcr.io/buildpacks/gcp/run:v1
    ===> DETECTING
    3 of 6 buildpacks participating
    google.go.runtime  0.9.1
    google.go.build    0.9.0
    google.utils.label 0.0.1
    ===> ANALYZING
    ===> RESTORING
    ===> BUILDING
    === Go - Runtime ([email protected]) ===
    Using runtime version from go.mod: 1.14
    Installing Go v1.14
    ...
    

    我們可以看到使用 gcr.io/buildpacks/builder:v1 作為 builder 對 Golang 言語基本上就會載入 3 個 buildpack,google.go.runtime google.go.build google.utils.label

    最後因為我們沒有客製自己的 builder,所以必需明確指定 buildpack,加上下載 Github Asset 用的 cage1016/github-assets-cnb,原來 google.go.runtime google.go.build google.utils.label 共記 4 個

  3. project.toml 增加明確指定所需要的 4 個 buildpack 及 Github Asset 相關的 metadata

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    cat <<EOF >> project.toml
    [[build.buildpacks]]
    id = "google.go.runtime"
    version = "0.9.1"
    
    [[build.buildpacks]]
    id = "google.go.build"
    version = "0.9.0"
    
    [[build.buildpacks]]
    id = "google.utils.label"
    version = "0.0.1"
    
    [[metadata.githubassets]]
    repo = "grpc-ecosystem/grpc-health-probe"
    asset = "grpc_health_probe-linux-amd64"
    destination = "bin/grpc_health_probe"
    
    [[build.buildpacks]]
    id = "cage1016/github-assets-cnb"
    version = "2.1.1"
    EOF
    
    1
    2
    3
    
    pack build aa -B gcr.io/buildpacks/builder:v1 \
                  -d=project.toml \
                  --env GOOGLE_BUILDABLE=cmd/add/main.go
    

    我們可以看到 DETECTING 的階段有正確偵測到我們明確指定的 4 個 buildpack,並在 BUILDING 階段也有正確下載 Github grpc-ecosystem/grpc-health-probe asset 至 container image 中

    /posts/build-kubernetes-grpc-health-probe-with-pack/img/dive-0.png
    grpc-health-probe asset container image

    而依照 cage1016/github-assets-cnb 中實作 buildpack sepc 所提供的目錄為 /layers/cage1016_github-assets-cnb/grpc-ecosystem_grpc-health-prob/bin/grpc_health_probe 如圖所示,而這個路徑也是我們在 Kubernetes Pod 在 livenessProbereadinessProbe 探針指令執行檔所在

  4. 基本上二種方式的結果都是一樣的,就看你喜歡那一種

心得

Dockerfile 中使用 wget 動態去下載所需要的檔案算是一種常規的作法。反之在 buildapck 的架構之下要下載一個檔案卻有一點複雜,也是常常有需要下載 Github Assets 的剛性需求,特別寫了一個符合 Cloud Native Buildpack 的 cage1016/github-assets-cnb buildpack 來滿足這個需求,當這一個生態越來越豐富時,就會慢慢感覺像是在疊責木一樣

如同 cage1016/ms-demo 這個 gokit microserives demo 一樣,AddTictac 服務基本上都改用 pack 來建構 container image,為了在 Kubernetes Pod 新增 livenessProbereadinessProbe 探針並使用 grpc_health_probe 來檢查 GRPC 服務的健康狀況,其實就是新增了 project.toml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cat <<EOF >> project.toml
# [[build.env]]
# optional, github token for private assets
# name = "TOKEN"
# value = "<github-token>"

# skaffold
[[build.env]]
# required
name = "REPO"
value = "grpc-ecosystem/grpc-health-probe"

[[build.env]]
# required
name = "FILE"
value = "grpc_health_probe-linux-amd64"

[[build.env]]
# optional, default set to FILE value
name = "TARGET"
value = "grpc_health_probe"

# [[build.env]]
# # optional, default set to 'latest'
# name = "VERSION"
# value = "v1.22.0"
EOF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
ports:
  - containerPort: 10021
readinessProbe:
  exec:
    command: ["/layers/cage1016_github-assets-cnb/grpc-ecosystem_grpc-health-prob/bin/grpc_health_probe", "-addr=:10021"]
  initialDelaySeconds: 5
livenessProbe:
  exec:
    command: ["/layers/cage1016_github-assets-cnb/grpc-ecosystem_grpc-health-prob/bin/grpc_health_probe", "-addr=:10021"]
  initialDelaySeconds: 10
...

剩下的都不用動,Pack 就會操作 builder 按照所載入的 buildpack 完成對應的動作。慢慢體會這種抽換的方便性