Rust kube-rsハンズオン

:::note info
この記事は[ZOZO AdventCalender 2023](https://qiita.com/advent-calendar/2023/zozo)シリーズ9の4日目の記事です。
:::

# はじめに
Rust初心者の筆者がRustの環境構築から始めて、Rust実装のkubenetes clientである[kube-rs/kube](https://github.com/kube-rs/kube)を使ってpodを立ち上げて`Hello, kube-rs!`するまでをハンズオンしていきます。  
kubernetesの環境はEKS上に既に立ち上げてある環境を使用しますが、今回の記事ではEKSの設定などには触れません。  
開発環境は以下の通りです。  
- M2 Mac
- zsh
- AstroNvimを入れたNeovim

# Rustの公式情報
まずは参考となる情報を探します。  
一番始めに確認すべきは公式情報なので、Rustの公式サイトを確認しましょう。 
ありがたいことに日本語訳も用意されています。  
- https://www.rust-lang.org/ja/

### 学ぶ
[学ぶ](https://www.rust-lang.org/ja/learn)のページに色々と参考になりそうな情報が揃っています。  
標準ライブラリのAPIガイドもここから飛べそうです。  
その他にも[The Rust Programming Language](https://doc.rust-lang.org/book/)という書籍もオンラインで読めるようです。  
非公式ではあるようですが、[日本語訳](https://doc.rust-jp.rs/book-ja/)もありました。  

これらの情報を参考にしつつ進めていきます。

# Rustの環境構築
まずはrustの環境構築から始めます。  
筆者の環境はM2Macなのでmacを前提に進めていきます。  
少し調べてみるとRustではrustupというツールを使ってツールチェーンの管理を行うようです。  
macならHomebrewでもインストールできるようですが、ここは[公式ドキュメント](https://www.rust-lang.org/ja/tools/install)に従ってcurlでshellスクリプトを取得してインストールしていきたいと思います。  
実行コマンドは以下の通りです。
```zsh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

公式ドキュメントによると、
>Rustの開発環境において、全てのツールは~/.cargo/binディレクトリにインストールされ、ここにrustc、cargo、rustupを含むRustのツールチェーンが置かれます。
よって、このディレクトリをPATH環境変数に含めるのが、Rustの開発者にとっての通例となっています。

とあるので`~/.cargo/bin`をPATHに追加するように`~/.zshrc`に以下を追加します。
```zsh
export PATH="$HOME/.cargo/bin:$PATH"
```

これでひとまず最低限rustを実行する環境が整いました。  

## その他の開発ツール
公式でも紹介されている[The Rust Programming Language 日本語版](https://doc.rust-jp.rs/book-ja/)に、付録として[便利な開発ツール](https://doc.rust-jp.rs/book-ja/appendix-04-useful-development-tools.html)が載っています。  
ここで紹介されている各種ツールはとりあえず入れておきましょう。  
- rustfmt
  - コードフォーマッタ
- rustfix
  - rustをインストールすると同梱されているツールでコンパイラの警告を自動で直してくれる。`cargo fix`で実行する
- clippy
  - いわゆる静的解析ツール
- rust-analyzer
  - LSP(LanguageServerProtocol)に対応したServer。IDEと連携して自動補完・定義ジャンプ・インラインのエラー表示ができるようになる。
  - 書籍で紹介されているのはrlsですが、[rlsのREADME](https://github.com/rust-lang/rls#%EF%B8%8F-rls-is-no-longer-supported)に`rust-analyzer`を使うように書いてあるのでrust-analyzerを入れていきます。

## nvim設定
保存時にフォーマットを自動で実行するように設定をします。  
[rustfmtのREADME](https://github.com/rust-lang/rustfmt#running-rustfmt-from-your-editor)に各種editorでの設定方法へのリンクがあるので、  
[vimの設定](https://github.com/rust-lang/rust.vim#formatting-with-rustfmt)に飛ぶと`rust.vim`というvimのpluginが用意されてるようなので入れていきましょう。  
筆者は[AstroNvim](https://astronvim.com/)を使っているので、`~/.config/nvim/lua/user/init.lua`に設定を入れていきます。
```lua
{
  "rust-lang/rust.vim",
  lazy = false,
  config = function()
    vim.g.rustfmt_autosave = 1
  end
}
```
これで保存時に自動でフォーマットされるようになります。

LSP実装の`rust-analyzer`も準備しておきます。  
AstroNvimであればmason-lspconfigがあるので`:LspInstall rust`でインストールできるのでサクッと入れれます。

ひととおり環境を整えたらまずは`Hello, World!`してみます。  
[The Rust Programming Language 日本語版のHello, World!](https://doc.rust-jp.rs/book-ja/ch01-02-hello-world.html)に従ってやっていきます。
```rust:main.rs
fn main() {
    println!("Hello, world!")
}
```
コードをコンパイルして実行してみます。
```zsh
$ rustc main.rs
$ ./main
Hello, world!
$
```
実行できました！  
rustのコンパイルと実行が確認できたので、早速podを立ち上げるコードを書いていきたいと思います。

# Hello, Kube-rs
まずは新しいプロジェクトを作成します。  
RustにはCargoというビルドシステム兼パッケージマネージャがあるので、`cargo`コマンドを使ってプロジェクトを作成します。  
```zsh
$ cargo new hello_kubers
     Created binary (application) `hello_kubers` package
$ 
```
このコマンドを実行すると以下の構成でプロジェクトが作成されます。
gitディレクトリとgitignoreも生成されるようです。
```zsh
$ tree -a
.
├── .git
│   ├── HEAD
│   ├── config
│   ├── description
│   ├── hooks
│   │   └── README.sample
│   ├── info
│   │   └── exclude
│   ├── objects
│   │   ├── info
│   │   └── pack
│   └── refs
│       ├── heads
│       └── tags
├── .gitignore
├── Cargo.toml
└── src
    └── main.rs

11 directories, 8 files
$
```

それでは本題のkube-rsを使ってkubernetesクラスタにpodを立ち上げるまでをやっていきたいと思います。  

## kube-rs Install
[kube-rsのREADME](https://github.com/kube-rs/kube#installation)に従ってインストールします。  
`Cargo.toml`に下記を追加しましょう。
非同期処理を行うためのライブラリである`tokio`も一緒に入れておきます。
```toml:Cargo.toml
[dependencies]
kube = { version = "0.87.1", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.20.0", features = ["latest"] }
tokio = { version = "1", features = ["full"] }
```

## podを立ち上げてHello, kube-rs!する
リポジトリを眺めているとexamplesに色々とサンプルコードが載っています。  
その中にある[pod_attach.rs](https://github.com/kube-rs/kube/blob/main/examples/pod_attach.rs)がちょうど今回やりたいことのサンプルになっているのでそのまま使わせてもらいましょう。
とりあえずコードを全てコピーして、Podを作成するためのリソース定義を下記のように変更します。
```rust:main.rs
    let p: Pod = serde_json::from_value(serde_json::json!({
        "apiVersion": "v1",
        "kind": "Pod",
        "metadata": { "name": "test-kane8n" },
        "spec": {
            "containers": [{
                "name": "test-kane8n",
                "image": "alpine",
                "command": ["sh", "-c", "echo \"Hello, kube-rs!\" && sleep 10"],
            }],
        }
    }))?;
```
またpod名を指定している箇所も適宜変更します。  
ビルドは`cargo build`コマンドでやりましょう。  
この時点でビルドしてみると大量のエラーを吐いてビルドが失敗します。  
行数が多いのでここに載せるのは省きますが、どうやら色々と依存関係の設定なんかが足りてないようなので[examplesのCargo.toml](https://github.com/kube-rs/kube/blob/main/examples/Cargo.toml)を参考に足りてなさそうなものを足していきます。  
最終的には以下のようにすることでビルドができました。
```toml:Cargo.toml
[package]
name = "hello_kubers"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["ws"]
ws = ["kube/ws"]

[dependencies]
kube = { version = "0.87.1", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.20.0", features = ["latest"] }
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1.36"
tracing-subscriber = "0.3.3"
anyhow = "1.0.44"
futures = "0.3.17"
```

いったんビルドができたので、試しに実行してみましょう。
ビルドしたバイナリは`target/debug/hello_kubers`にあります。
```zsh
$ ./target/debug/hello_kubers
2023-11-30T16:03:34.812853Z  INFO hello_kubers: Creating a Pod that outputs "Hello, kube-rs!"
2023-11-30T16:03:34.919201Z  INFO hello_kubers: Added test-kane8n
2023-11-30T16:03:37.319434Z  INFO hello_kubers: Ready to attach to test-kane8n
2023-11-30T16:03:46.967688Z  INFO hello_kubers: Status { code: None, details: None, message: None, metadata: ListMeta { continue_: None, remaining_item_count: None, resource_version: None, self_link: None }, reason: None, status: Some("Success") }
$
```
podを起動するのに成功しました！  
ちなみにkubernetesに接続するためのconfigですが、`Client::try_default()`を使用する場合、`kube-rs`はデフォルトの`kubeconfig`ファイルの場所を探し、その場所からConfigオブジェクトを生成して使用するみたいです。  
configを指定したい場合は`Client::try_from_kubeconfig(kubeconfig_path)`を使えば良いです。

しかし標準出力しているはずの`Hello, kube-rs!`が出力されていません。（`combined_output`で出力される想定だったのですが・・・
ここは一旦深掘りするのは置いておいて、もう一度examplesを眺めてみると[log_stream.rs](https://github.com/kube-rs/kube/blob/main/examples/log_stream.rs)に良さそうなサンプルがあるので試してみます。  
```rust
info!("Fetching logs for test-kane8n");
let mut logs = pods
    .log_stream(
        "test-kane8n",
        &LogParams {
            follow: true,
            container: None,
            tail_lines: None,
            since_seconds: None,
            timestamps: false,
            ..LogParams::default()
        },
    )
    .await?
    .lines();

while let Some(line) = logs.try_next().await? {
    println!("{}", line);
}
```
[doc.rs](https://docs.rs/kube/latest/kube/struct.Api.html#method.log_stream)によると、`AsyncBufRead`でログをストリームできるもののようです。  
このコードを入れて試してみます。  
`futures::{AsyncBufReadExt}`と`kube::{api::{LogParams}}`をuseするのも忘れずに
```zsh
$ ./target/debug/hello_kubers   
2023-11-30T16:33:08.929252Z  INFO hello_kubers: Creating a Pod that outputs "Hello, kube-rs!"
2023-11-30T16:33:09.056016Z  INFO hello_kubers: Added test-kane8n
2023-11-30T16:33:11.571052Z  INFO hello_kubers: Ready to attach to test-kane8n
2023-11-30T16:33:21.116735Z  INFO hello_kubers: Status { code: None, details: None, message: None, metadata: ListMeta { continue_: None, remaining_item_count: None, resource_version: None, self_link: None }, reason: None, status: Some("Success") }
2023-11-30T16:33:21.116922Z  INFO hello_kubers: Fetching logs for test-kane8n
Hello, kube-rs!
$ 
```

`Hello, kube-rs`が取得できました！  

Rust初心者ですが、公式の情報とkube-rsのサンプルコードが充実していたのでそこまでハマることなく実装できたかなという所感です。  

最後に今回実装したコードをGithubに上げておいたので載せておきます。  
- https://github.com/kane8n/hello-kubers/tree/main

以上、Rustでkube-rsハンズオンでした。