LimaでK3sのクラスター環境を構築する

[Lima](https://github.com/lima-vm/lima) の仮想マシン内に [K3s](https://k3s.io/) のクラスターを構築してみました。

# 1. 仮想マシンの実行

今回は arm64 環境を使っているため、下記の設定で仮想マシンを実行します。
containerd を rootful モードで実行するために ```containerd.system``` を true に設定しています。

```yaml:k3s_cl.yaml
images:
- location: "https://cloud-images.ubuntu.com/releases/23.10/release/ubuntu-23.10-server-cloudimg-arm64.img"
  arch: "aarch64"

mounts:
- location: "~/temp/k3s_cl"
  writable: true

containerd:
  system: true
  user: false
```

仮想マシンを実行して、以降の作業は仮想マシン内で実施します。（作業ディレクトリは ~/temp/k3s_cl）

```sh:仮想マシンの実行
% limactl start --tty=false k3s_cl.yaml
```

```sh:仮想マシンへ入る
% limactl shell k3s_cl
```

# 2. プライベートレジストリの実行

K3s のクラスター環境に必須というわけではありませんが、自前のサンプルアプリケーションを実行したいので Docker のプライベートレジストリを実行しておきます。

```sh:プライベートレジストリ実行
$ sudo nerdctl run -d --name reg1 -p 5000:5000 registry:2
```

これで、HTTP 接続用のプライベートレジストリが起動します。

このプライベートレジストリからコンテナイメージを取得させるには、K3s の Server や Agent の実行時に下記のようなファイルを ```/etc/rancher/k3s/registries.yaml``` にマウントする必要があります。

```yaml:registries.yaml
mirrors:
  localhost:5000:
    endpoint:
    - "http://reg1:5000/v2"
```

この設定により、リポジトリ名が ```localhost:5000``` のコンテナイメージを ```http://reg1:5000/v2``` から取得するようになります。

# 3. K3s Server 実行

K3s Server を実行します。

kubectl コマンドで ```https://127.0.0.1:6443``` へ接続する事になるので 6443 ポートで接続できるようにしておきます。

なお、特権モードが必要そうなので ```--privileged``` を付けて実行します。

```sh:K3s Server 実行
$ sudo nerdctl run -d --privileged --name server-1 -p 6443:6443 -v $(pwd)/registries.yaml:/etc/rancher/k3s/registries.yaml rancher/k3s server
```

## kubectl の準備

kubectl コマンドで操作できるように、K3s Server（server-1）の ```/etc/rancher/k3s/k3s.yaml``` ファイルを ```~/.kube/config``` としてコピーしておきます。

```sh:k3s.yaml のコピー
$ mkdir ~/.kube
$ sudo nerdctl cp server-1:/etc/rancher/k3s/k3s.yaml ~/.kube/config
$ sudo chown $USER ~/.kube/config
```

kubectl コマンドをインストールします。

```sh:kubectl インストール
$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl"
$ chmod +x kubectl
$ sudo mv kubectl /usr/local/bin/
```

これで kubectl コマンドを使用できるようになったので、kubectl で Node を確認します。

```sh:Node 確認
$ kubectl get node
NAME           STATUS   ROLES                  AGE   VERSION
d5f7e7bfd478   Ready    control-plane,master   12m   v1.29.1+k3s2
```

# 4. K3s Agent 実行

次に、K3s Agent を実行して Node を 1つ追加してみます。

K3s Server への接続にトークンが必要になるため、K3s Server の ```/var/lib/rancher/k3s/server/token``` ファイルをコピーしておきます。

```sh:token 取得
$ sudo nerdctl cp server-1:/var/lib/rancher/k3s/server/token ./
```

token ファイルや K3s Server の URL を下記の環境変数へそれぞれ設定して Agent を実行します。

|環境変数|内容|
|---|---|
|K3S_URL|K3s Server の URL|
|K3S_TOKEN|token ファイルの内容|

```sh:K3s Agent 実行
$ sudo nerdctl run -d --privileged --name agent-1 -e K3S_URL=https://server-1:6443 -e K3S_TOKEN=$(cat ./token) -v $(pwd)/registries.yaml:/etc/rancher/k3s/registries.yaml rancher/k3s agent
```

これで Node が 2つになりました。

```sh:Node 確認
$ kubectl get node
NAME           STATUS   ROLES                  AGE   VERSION
d5f7e7bfd478   Ready    control-plane,master   16m   v1.29.1+k3s2
dd20bb3b3071   Ready    <none>                 19s   v1.29.1+k3s2
```

# 5. 動作確認

K3s のクラスターを構築できたので、次のようなサンプルアプリケーションを実行して動作確認します。

```go:sampleapp/main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
)

func main() {
	port := os.Getenv("APP_PORT")

	if strings.TrimSpace(port) == "" {
		port = "3000"
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		log.Printf("url query: %s", r.URL.RawQuery)
		fmt.Fprintf(w, "ok")
	})

	log.Fatal(http.ListenAndServe(":"+port, nil))
}
```

## ビルド

Dockerfile を用意してビルドします。

```dockerfile:sampleapp/Dockerfile
FROM golang:1.21-bullseye AS build

WORKDIR /work
COPY main.go ./
RUN CGO_ENABLED=0 go build -o sampleapp main.go

FROM scratch

COPY --from=build /work/sampleapp /app/sampleapp
CMD ["/app/sampleapp"]
```

ビルド後にプライベートレジストリへ push しておきます。

```sh:ビルド
$ sudo nerdctl build -t localhost:5000/sampleapp ./sampleapp
```

```sh:プライベートレジストリへ push
$ sudo nerdctl push localhost:5000/sampleapp
```

なお、```localhost``` のような特定のリポジトリ名を使うと HTTP で push するのでこれで問題ありませんが、それ以外のリポジトリ名（例えば registry.example.com:5000）にすると HTTPS で push しようとするので ```--insecure-registry``` オプションが必要になります。

## 実行

下記のマニフェストで Pod を実行します。

```yaml:sampleapp.yaml
kind: Service
apiVersion: v1
metadata:
  name: sampleapp
  labels:
    app: sampleapp
spec:
  selector:
    app: sampleapp
  ports:
  - protocol: TCP
    port: 3001
  type: LoadBalancer

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sampleapp
  labels:
    app: sampleapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sampleapp
  template:
    metadata:
      labels:
        app: sampleapp
    spec:
      containers:
      - name: sampleapp
        image: localhost:5000/sampleapp
        env:
        - name: APP_PORT
          value: "3001"
        ports:
        - containerPort: 3001
```

```sh:リソース作成（Pod 実行）
$ kubectl apply -f sampleapp.yaml
```

Pod を確認すると、次のようにそれぞれの Node で 1つずつ Pod が実行されていました。

```sh:Pod 確認
$ kubectl get pod -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
sampleapp-845b8c8fb5-x8gmj   1/1     Running   0          70s   10.42.0.10   d5f7e7bfd478   <none>           <none>
sampleapp-845b8c8fb5-g5lks   1/1     Running   0          70s   10.42.1.4    dd20bb3b3071   <none>           <none>
```

Service を確認します。

```sh:Service 確認
$ kubectl get svc
NAME         TYPE           CLUSTER-IP    EXTERNAL-IP           PORT(S)          AGE
kubernetes   ClusterIP      10.43.0.1     <none>                443/TCP          26m
sampleapp    LoadBalancer   10.43.90.28   10.4.0.24,10.4.0.25   3001:31343/TCP   116s
```

sampleapp の EXTERNAL-IP へ接続して、アプリケーションの処理を呼び出してみます。

```sh:アプリケーション呼び出し
$ curl "http://10.4.0.24:3001/?a"
ok

$ curl "http://10.4.0.24:3001/?b"
ok
```

Pod のログはそれぞれ次のようになっており、問題なく動作しているようです。

```sh:Pod ログ確認
$ kubectl logs sampleapp-845b8c8fb5-x8gmj
2024/02/07 13:39:54 url query: a

$ kubectl logs sampleapp-845b8c8fb5-g5lks
2024/02/07 13:39:58 url query: b
```