WSL2 に kind と MetalLB v0.13.7 を入れてcurlで通信してみた

## 概要
次の構成でGoアプリに対する通信ができるところまで試してみた。

なお、Qiita で既に記事を書いていそうなら載せないつもりだったけど、[MetalLB v0.13で破壊的変更](https://github.com/metallb/metallb/issues/1473)もあり、そのまま動かなかったので、備忘録として載せる。

### 環境

* Windows 10
* Ubuntu 20.04 LTS(WSL2)
  * go1.19 (go version)
  * Docker version 20.10.17, build 100c701
* Kind .. v0.17.0
* MetalLB .. v0.13.7

## 前提

* go インストール済み
* dockerインストール済み

## インストール(KIND関連)

備忘録として、今回の手順を載せるが、実際やるときは一次情報源の本家のドキュメントをベースに行ってください。

1. ~/go/bin にパスを通しておく

   **~/.bashrcへの追記例**

   ```shell
   test -d $HOME/go/bin && export PATH="$HOME/go/bin:$PATH"
   ```

1. KINDのインストール

   ```shell
   go install sigs.k8s.io/kind@v0.17.0
   ```
   **インストール確認**

   ```console
   $ $(go env GOPATH)/bin/kind version
   kind v0.17.0 go1.19 linux/amd64
   ```

1. クラスタの作成

   ```shell
   $(go env GOPATH)/bin/kind create cluster --name kind-1
   ```

   **確認**

   ```console
   $ kind get clusters
   kind-1
   ```

1. kubectl の導入

   KINDを使う分には kubectl が必須でないようで、必要なら導入すする。

   ```shell
   curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s 
   https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
   sudo install -m 755 kubectl /usr/local/bin/kubectl
   rm kubectl
   ```

### MetalLBのインストール

[MetalLB > Installation](https://metallb.universe.tf/installation/)より。

1. インストール

   ```console
   $ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.7/config/manifests/metallb-native.yaml
   ```

2. アドレスプールの設定

    次の注意点がある

    * MetalLB v0.13 で非互換があり、configMapの指定は認識されない
    * kindで使用しているの docker のIPを指定しないと、IPの割り当てが成功しても通信ができない

    **Dockerのネットワーク上のIPを調べる**

    ```console
    $ docker network inspect kind --format '{{json (index .IPAM.Config 0).Subnet}}'
    "172.18.0.0/16"
    ```

    **metallb-IPAddressPool.yaml**

    ```yaml
    apiVersion: metallb.io/v1beta1
    kind: IPAddressPool
    metadata:
    name: first-pool
    namespace: metallb-system
    spec:
    addresses:
    - 172.18.254.240/28  # ★環境ごとに書き換える
    autoAssign: true     # ★true: 自動割り当て, false: 手動割り当て(loadBalancerIPで指定)
    ---
    apiVersion: metallb.io/v1beta1
    kind: L2Advertisement
    metadata:
    name: l2-ad
    namespace: metallb-system
    spec:
    ipAddressPools:
    - first-pool
    ```

1. デプロイ

   ```console
   $ kubectl apply -f metallb-IPAddressPool.yaml
   ipaddresspool.metallb.io/first-pool created
   l2advertisement.metallb.io/l2-ad created
   ```

上記の手順については次のドキュメントにあるが、2022-08-28現在、ConfigMapを使う例となっており、IPAddressPoolとして読み替えが必要。

* [kind – LoadBalancer](https://kind.sigs.k8s.io/docs/user/loadbalancer/)

## Goアプリケーションのデプロイ

/api/greet/John に応答するだけのアプリ。

### main.go

```go
package main

import (
        "encoding/json"
        "fmt"
        "net/http"
        "path/filepath"
        "strings"
)

func greetHandler(w http.ResponseWriter, r *http.Request) {
        sub := strings.TrimPrefix(r.URL.Path, "/api/greet")
        _, name := filepath.Split(sub)
        var m any
        if name != "" {
                m = map[string]interface{}{
                        "message": fmt.Sprintf("Hello, %s!", name),
                }
        } else {
                m = map[string]interface{}{
                        "status": 404,
                }
        }

        resJson, err := json.Marshal(m)
        if err != nil {
                panic(err.Error())
        }

        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, "%s", string(resJson))
}

func main() {
        mux := http.NewServeMux()
        mux.HandleFunc("/api/greet/", greetHandler)

        http.ListenAndServe(":8080", mux)
}
```

### Dockerfile
容量を削減する目的でマルチステージビルドを使用している。

```dockerfile
FROM golang:1.19-alpine3.16 as builder
COPY ./src/main.go ./
RUN go build -o /go-app ./main.go

FROM alpine:3.16 as runtime
EXPOSE 8080
COPY --from=builder /go-app .
ENTRYPOINT ["./go-app"]
```

### Deploy

1. Build a image

   ```shell
   docker image build --tag greet-go:0.1 .
   ```

1. KIND にイメージをPushする

    ```shell
    kind --name kind-1 load docker-image greet-go:0.1
    ```

    **push確認**
    go-hello-app が一覧に含まれることが確認できる。

    ```console
    $ docker container ls --format "table {{.Image}}\t{{.State}}\t{{.Names}}"
    IMAGE                  STATE     NAMES
    kindest/node:v1.25.3   running   kind-1-control-plane
    $ docker exec -it kind-1-control-plane crictl images
    IMAGE                                      TAG                  IMAGE ID            SIZE
    docker.io/kindest/kindnetd                 v20221004-44d545d1   d6e3e26021b60       25.8MB
    docker.io/kindest/local-path-helper        v20220607-9a4d8d2a   d2f902e939cc3       2.86MB
    docker.io/kindest/local-path-provisioner   v0.0.22-kind.0       4c1e997385b8f       17.4MB
    docker.io/library/greet-go                 0.1                  abd18c430d5ec       12.5MB
    quay.io/metallb/controller                 v0.13.7              e73361dabfb86       25.6MB
    quay.io/metallb/speaker                    v0.13.7              738c5d221d601       46.9MB
    registry.k8s.io/coredns/coredns            v1.9.3               5185b96f0becf       14.8MB
    registry.k8s.io/etcd                       3.5.4-0              a8a176a5d5d69       102MB
    registry.k8s.io/kube-apiserver             v1.25.3              4bc1b1e750e34       76.5MB
    registry.k8s.io/kube-controller-manager    v1.25.3              580dca99efc3b       64.5MB
    registry.k8s.io/kube-proxy                 v1.25.3              86063cd68dfc9       63.3MB
    registry.k8s.io/kube-scheduler             v1.25.3              5225724a11400       51.9MB
    registry.k8s.io/pause                      3.7                  221177c6082a8       311kB
    ```

1. KIND にデプロイする

    今回はイメージがローカルにしかないので `imagePullPolicy` に `Never` を設定する必要があることに注意する。

    **greet-go.deployment.yaml**

    ```yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: greet-go-app
    spec:
      selector:
        matchLabels:
          app: greet-go-app
      replicas: 2
      template:
        metadata:
          labels:
            app: greet-go-app
        spec:
          containers:
          - name: greet-go
            image: greet-go:0.1
            imagePullPolicy: Never
            ports:
            - containerPort: 8080
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: greet-go-service
    spec:
      type: LoadBalancer
      selector:
        app: greet-go-app
      ports:
        - name: http
          port: 3000
          targetPort: 8080
          protocol: TCP
    ```

    **デプロイ**
    ```shell
    $ kubectl apply -f greet-go.deployment.yaml
    deployment.apps/greet-go-app created
    service/greet-go-service created
    ```

### 動作確認

#### Pod IPに対して直接通信する

Pod がデプロイできていれば行える確認。Serviceがなくとも動く。

1. Pod のIPを確認する

    ```console
    $ kubectl get pods -l app=greet-go-app -o custom-columns="Pod IP":.status.podIP,"Container port":.spec.containers[0].ports[].containerPort
    Pod IP       Container port
    10.244.0.7   8080
    10.244.0.6   8080
    ```

1. Control plane から curl を叩く

    ```console
    $ docker ps --format "table {{.Names}}\t{{.Image}}" --filter name=kind
    NAMES                  IMAGE
    kind-1-control-plane   kindest/node:v1.25.3
    $ docker container exec -it kind-1-control-plane curl -s http://10.244.0.7:8080/api/greet/John | python3 -m json.tool
    {
        "message": "Hello, John!"
    }
    ```

1. Internal DNSでPodに通信する

   以下は Service で2つのPodのいずれかの名前解決を行う。
   この例では Pod側のポートは 8080 だが Service側のポートは 3000 であることに注意する(この辺りの理解がごちゃ混ぜにならないように敢えて変えている)。

    ```
    $ kubectl get services greet-go-service
    NAME               TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)          AGE
    greet-go-service   LoadBalancer   10.96.94.74   172.18.254.240   3000:30485/TCP   65m
    $ docker container exec -it kind-1-control-plane curl -s http://greet-go-service.default.svc.cluster.local:3000/api/greet/John | python3 -m json.tool
    curl: (6) Could not resolve host: greet-go-service.default.svc.cluster.local
    $ kubectl run -q -n default -it curl --image=curlimages/curl --rm --restart=Never --wait=true -- -s -L http://greet-go-service.default.svc.cluster.local:3000/api/greet/John | python3 -m json.tool
    {
        "message": "Hello, John!"
    }
    ```

    **補足**

    * kubectl run

      * `-q` は `pod "curl" deleted` などの `kubectl run` の出力を削るため
      * `--wait=true` は特定の警告メッセージを出すのを抑制する
         `kubectl run` が即終わってしまうと次のメッセージが出る

        ```
        warning: couldn't attach to pod/curl, falling back to streaming logs: unable to upgrade connection: container curl not found in pod curl_default
        ```

#### ポート転送して確認する

1. Pod name を確認する

    ```console
    $ kubectl get pods -l app=greet-go-app
    NAME                            READY   STATUS    RESTARTS   AGE
    greet-go-app-78cc5cfdd8-4q6lt   1/1     Running   0          13m
    greet-go-app-78cc5cfdd8-kdpdl   1/1     Running   0          13m
    ```

1. 別の端末でポート転送を行う
   下記は、Ctrl-Cを打つまで端末を占有するのでWSL内の別端末で確認する。以下のように実行できた。

    ```console
    $ POD_NAME="greet-go-app-78cc5cfdd8-4q6lt"
    $ kubectl port-forward $POD_NAME 3000:8080
    Forwarding from 127.0.0.1:3000 -> 8080
    Forwarding from [::1]:3000 -> 8080
    ...(注:Ctrl-Cを打つまで復帰しない)
    ```

1. Curlでレスポンスを確認する

    ```console
    $ curl -s http://localhost:3000/api/greet/John \
    --header "Content-Type: application/json" \
    --request "GET" | python3 -m json.tool
    {
        "message": "Hello, John!"
    }
    ```

#### External IP経由の確認

この確認が本命となる。

1. EXTERNAL-IP の確認をする
   うまくいっていれば次のように、EXTERNAL-IPを取得できる

    ```console
    $ kubectl get services
    NAME               TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)          AGE
    greet-go-service   LoadBalancer   10.96.94.74   172.18.254.240   3000:30485/TCP   17m
    kubernetes         ClusterIP      10.96.0.1     <none>           443/TCP          65
    ```

1. Curlでレスポンスを確認する

    ```console
    $ curl -s http://172.18.254.240:3000/api/greet/John \
    --header "Content-Type: application/json" \
    --request "GET" | python3 -m json.tool
    {
        "message": "Hello, John!"
    }
    ```

なお、この時点でWindows 10のホスト側からの通信は失敗。多分、追加の設定が必要。

## 補足: トラブルシューティング
いくつかはまった点を書いておく。

### curl で Failed to connect to 172.18.xxx.yyy port 8080: No route to host と怒られる

この記事は Deployment を使った記事で、試しに Pod で作ろうとしたときに発生した。

**状況**

* `EXTERNAL-IP` は取得できている
* arp コマンドの結果、 `HWaddress` が `(incomplete)` になっている
* `sudo arping 172.18.xxx.yyy -c 3` ですべてタイムアウトする

**原因**

結果的には、`selector` で Pod が見つけられない状態にあった。
labels に `app: ～` を追加し問題は解消した。

* **go-hello-app.pod.yaml**

    ```yaml
    apiVersion: v1
    kind: Pod
    metadata:
    name: go-hello-pod
    labels:                # 追加
        app: go-hello-app  # 追加
    spec:
    containers:
    - name: go-hello-app
        image: greet-go:0.1
        imagePullPolicy: Never
        ports:
        - containerPort: 8080
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: go-hello-service
    spec:
    type: LoadBalancer
    selector:
        app: go-hello-pod # pod の labelsを見ている
    ports:
        - name: http
        port: 3000
        targetPort: 8080
        protocol: TCP
    ```

### OS再帰動詞、WSLを再開後に IPAddressPool の更新ができなくなる

**原因**

* metallb の Controller が Unhelthy になっている

**暫定対処**

* kind のクラスタを再作成したらうまくいった

## 参考

* [KIND](https://kind.sigs.k8s.io/)
* MetalLB
  * Address Pool 関連 
    * [MetalLB v0.13以降はConfigmapでの設定ができない - 動かざることバグの如し](https://thr3a.hatenablog.com/entry/20220718/1658127951)

    * [MetalLB > ADVANCED ADDRESSPOOL CONFIGURATION](https://metallb.universe.tf/configuration/_advanced_ipaddresspool_config/)
    * [(1) Heads up: breaking changes in 0.13.x · Issue #1473 · metallb/metallb](https://github.com/metallb/metallb/issues/1473)
    * [RFC1918 日本語訳](https://www.nic.ad.jp/ja/translation/rfc/1918.html)
  * L2関連
    * [MetalLB > ADVANCED L2 CONFIGURATION](https://metallb.universe.tf/configuration/_advanced_l2_configuration/)
 
 * その他
   * [【手順あり】MetalLBの使い方から動きまで解説します - フラミナル](https://blog.framinal.life/entry/2020/04/16/022042) 
   * [Accessing network applications with WSL | Microsoft Docs](https://docs.microsoft.com/en-us/windows/wsl/networking) 
   * [WSL2で開発中のWebアプリを同じLANのスマホで動作確認する方法](https://zenn.dev/solufa/articles/accessing-wsl2-from-mobile)