rust で grpc サーバの healthcheck を試す

# 概要

自分メモ用

rust で grpc サーバを実装する
実装するサーバは以下のような機能を持つ

- カスタム定義のサービス
    - healthcheck の挙動確認が主のためこの部分の実装はかなり適当
- Healthcheck サービス

このような grpc サーバを実装し、k8s 上にデプロイして grpc の readinessProbe が成功する様子を確認する

以下のレポジトリでサンプルを保存

grpc-healthcheck (https://github.com/kronos1209/grpc-healthcheck)

# 前提

以下の環境で確認
それぞれのツールの導入については省略

- wsl (Ubuntu 22.04)
- docker
- minikube
    - k8s クラスタ作成用
    - kind などでもよいがダッシュボードの追加を容易にできるため minikube がおすすめ
- kubectl

# 実装

## proto 定義

以下のようなサービスを定義してコンパイルする

```proto
syntax = "proto3";
package my_service;

message MyRequest {}
message MyResponse {}

service MyService {
    rpc Get(MyRequest) returns(MyResponse);
}
```

`proto` のコンパイル用の `build.rs`

```rust:build.rs
use std::path::PathBuf;

fn main() {
    // 指定したディレクトリを再帰的に検索し、proto ファイルを探す
    let proto_dir = vec!["proto"];
    let protos: Vec<_> = proto_dir.iter().fold(vec![], |mut acc, base| {
        acc.append(&mut find_protos(base));
        acc
    });

    tonic_build::configure()
        .build_server(true)
        .build_client(true)
        .compile_protos(&protos, &vec![""])
        .unwrap();
}

fn find_protos(base: &str) -> Vec<PathBuf> {
    let mut protos = Vec::new();
    for entry in walkdir::WalkDir::new(base).into_iter() {
        let entry = entry.unwrap();

        // Directory はスキップ
        if entry.path().is_dir() {
            continue;
        }

        // 拡張子が proto であるならば返り値に追加する
        let path = entry.into_path();
        if path.extension().unwrap() == "proto" {
            protos.push(path)
        }
    }
    protos
}
```

コンパイルした `proto` をライブラリに追加

```rust: my_service.rs
tonic::include_proto!("my_service");
```

## grpc サーバ実装

前段でコンパイルしたコードを利用して grpc サーバを実装する。

healthcheck サービスは `tonic-health` クレートが実装を提供してくれているため、そちらを利用する


```rust:main.rs
#[tokio::main]
async fn main() {
    // ヘルスチェックのステータスマップに MyService を追加する
    // この時点では MyService は NotServing として登録する
    let (mut reporter, health_server) = tonic_health::server::health_reporter();
    reporter
        .set_not_serving::<MyServiceServer<MyService>>()
        .await;

    let my_service = MyService {};
    let my_server =
        proto_definition::proto::my_service::my_service_server::MyServiceServer::new(my_service);

    // 30秒後にサービスが利用可能になっているようにする
    let mut clone_reporter = reporter.clone();
    tokio::spawn(async move {
        sleep(Duration::from_secs(30)).await;
        clone_reporter
            .set_serving::<MyServiceServer<MyService>>()
            .await;
    });

    // サーバーを起動する
    let addr = "0.0.0.0:8080".parse().unwrap();
    tonic::transport::Server::builder()
        .add_service(my_server)
        .add_service(health_server)
        .serve(addr)
        .await
        .unwrap();
}
```

## k8s リソース

### イメージ化

まずは実装した grpc サーバをイメージ化する

```dockerfile
FROM rust as build

WORKDIR /app

# ビルドに必要な protoc をインストールする
RUN apt update \
    && apt upgrade -y \
    && apt install -y protobuf-compiler libprotobuf-dev

# ビルド
COPY . .
RUN cargo build --release --package grpc-server --bin server

#############################################################################
# scrach イメージしたかったが、うまく実行できなかったため ubuntu イメージを利用
# おそらくリンクとかの問題？
FROM ubuntu as runtime
WORKDIR /server

RUN apt update \
    && apt upgrade -y 

COPY --from=build /app/target/release/server /server

ENTRYPOINT [ "/server/server" ]
```

### イメージを利用した pod マニフェスト
pod マニフェストを作成する

`readinessProbe` は `spec.containers[].readinessProbe` で定義できる

ここで重要なのは `readinessProbe.grpc.service` で grpc サーバで実装されているサービスのうち、特定のサービスが利用可能になっているかどうかを確認することができる 

HealthService はハッシュマップで各サービスが準備完了状態であるかどうか管理しており、今回は MyService だけ30秒間立たないと準備完了状態にならないため、\
`service` を指定していない場合は一回目の readinessProbe で成功し、\
`service` を`my_service.MyServie` に指定すると一定期間 readinessProbe は失敗する 

また、ここで指定するサービス名というのは tonic_build で proto をコンパイルした際に `{サービス名}Server` という構造体が自動で生成されており、この構造体に実装されている NamedService トレイトの `SERVICE_NAME` がそれにあたる

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: myapp
  labels:
    name: myapp
spec:
  containers:
  - name: myapp
    image: test:v1
    imagePullPolicy: IfNotPresent
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"
    ports:
    - containerPort: 8080
    livenessProbe:
      grpc:
        port: 8080
        service: my_service.MyService
      initialDelaySeconds: 60
    # MyService grpc サービスが準備完了状態になっているかどうか healtchcheck を行う
    readinessProbe:
      grpc:
        port: 8080
        # NamedService のサービス名の指定する
        service: my_service.MyService
      initialDelaySeconds: 10
      periodSeconds: 10
```

## 挙動確認

挙動を確認するためにリソースをデプロイしていく。

### デプロイ

1. サンプルイメージの作成

    ```bash
    docker build . -f ./grpc-server/dockerfile  -t test:v1
    ```

1. k8s クラスタの用意
    `minkube dashboard` を実行したときに、ダッシュボードにアクセスするための url が表示されるためのそれを使ってダッシュボードにアクセスする
    
    ```bash
    # クラスタ作成
    minikube start

    #クラスタ内にイメージを取り込む
    minikube image load test:v1

    # ダッシュボードの起動
    minikube dashboard
    ```

1. k8s リソース追加

    ```bash
    kubectl apply -f ./mamifest/pod.yaml
    ```

### 確認

k8s のダッシュボードからポッドのページを開き、`myapp` のポッドのステータスを確認する

正常に動作していれば、以下のような ReadinessProbe 失敗のログが3回発生したのち、ポッドのステータスが準備完了状態に移行しているはず

> Readiness probe failed: service unhealthy (responded with "NOT_SERVING")

# まとめ

rust で grpc-healtch-check を実装するには `tonic-health` クレートを使えばよさそう。
ただし、[crate.io](https://crates.io/crates/tonic-health) の概要を見ると　grpc-health-check の仕様をすべて実装しているわけではないようなので要調査

k8s のポッドの grpc の readinessProbe は k8s にビルドインで実装されている。
以前見たときは別途 [grpc-health-probe](https://github.com/grpc-ecosystem/grpc-health-probe?tab=readme-ov-file) をイメージに含めないといけなかったが k8a 内で完結できそうなので良かった

# 参考文献
[tonic-health](https://crates.io/crates/tonic-health)
[Configure Liveness, Readiness and Startup Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-grpc-liveness-probe)