k3sでアプリケーションをデプロイし、外部からアクセスしてみた

k3s とは軽量 Kubernetes です。しばしばメモリ消費量が少ない、バイナリが小さいなど軽量であることに注目されますが、シンプルで簡単に Kubernetes の機能を使い始められ、学習にも向いたツールです。

今回は k3s で Kubernetes クラスターを構築します。そのうえで、適当な Web アプリケーションをクラスターにデプロイし、インターネットからアクセスできるようにすることを目標とします。  
私は Kubernetes 初心者なので、k3s を使って Kubernetes を学習していきたいと思います。

せっかくなので、node は 2 つにします。k3s だとそのような構成も簡単にできます。  
また、各 node 間の通信は VPN で暗号化します。これも k3s だと簡単です。

### バージョン情報など

```bash
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
```

```bash
$ k3s --version
k3s version v1.27.3+k3s1 (fe9604ca)
go version go1.20.5
```

```bash
$ sudo k3s kubectl version --short
Flag --short has been deprecated, and will be removed in the future. The --short output will become the default.
Client Version: v1.27.3+k3s1
Kustomize Version: v5.0.1
Server Version: v1.27.3+k3s1
```

本記事の内容に影響しないはずですが、動作確認は AWS EC2 の t3.medium インスタンスで行いました。2vCPU でメモリは 4GB です。このスペックで十分動作しました。これより小さいサーバーでも大丈夫だと思います。  
なお、AWS EC2 は簡単にサーバーが作成できるから利用しただけで AWS であることは本記事の内容には関係ないはずです。

### 前提

#### 最低限のスペック

k3s は軽量ですが、いくつかの最低要件があります。  
詳細は[Requirements](https://docs.k3s.io/installation/requirements)に記載されていますが、下記は特に注意と思われます。

| Spec | Minimum | Recommended |
| :--- | :------ | :---------- |
| CPU  | 1 core  | 2 cores     |
| RAM  | 512 MB  | 1 GB        |

#### ポートを開ける

| Protocol | Port      | Source    | Destination | Description                                   |
| :------- | :-------- | :-------- | :---------- | :-------------------------------------------- |
| TCP      | 2379-2380 | Servers   | Servers     | Required only for HA with embedded etcd       |
| TCP      | 6443      | Agents    | Servers     | K3s supervisor and Kubernetes API Server      |
| UDP      | 8472      | All nodes | All nodes   | Required only for Flannel VXLAN               |
| TCP      | 10250     | All nodes | All nodes   | Kubelet metrics                               |
| UDP      | 51820     | All nodes | All nodes   | Required only for Flannel Wireguard with IPv4 |
| UDP      | 51821     | All nodes | All nodes   | Required only for Flannel Wireguard with IPv6 |

## クラスターを作成

### WireGuard の準備

k3s のドキュメント（[Network Options](https://docs.k3s.io/installation/network-options)）によると

> The default backend for Flannel is vxlan. To enable encryption, use the wireguard-native backend.

とあります。というわけで、コンテナ間通信を暗号化するために、WireGuard を使います。
上記のドキュメントからリンクのある[WireGuard Install Guide](https://www.wireguard.com/install/)に従い、その準備をします。と言っても ubuntu だと apt でインストールするだけです。

```bash
sudo apt install wireguard
```

### クラスターの立ち上げ

k3s のドキュメント（[Quick-Start Guide](https://docs.k3s.io/)）に従い、クラスターを作成します。  
今回は次のようなコマンドをクラスターを立ち上げたいサーバー内で実行しました。

```bash
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server" sh -s - --flannel-backend wireguard-native
```

クラスターを立ち上げる最小のコマンドは次のとおりです。

```bash
curl -sfL https://get.k3s.io | sh -
```

これにいくつかのオプションを追加しています。

`INSTALL_K3S_EXEC` は k3s のインストール時につけるフラグを指定します。今回は `server` を指定しています。`server`、`agent`は k3s 用語らしいです。`server`は[Kubernetes のコントロールプレーン](https://kubernetes.io/ja/docs/concepts/overview/components/)が動いているノードで`agent`はコントロールプレーンがないノードのことです。（参考：[Architecture](https://docs.k3s.io/architecture)）  
`INSTALL_K3S_EXEC`に`server`を指定することで、`server`としてノードを立ち上げています。（なお、指定しなくてもデフォルトで server として立ち上がるようではあります）  
`--flannel-backend wireguard-native` は Flannel のバックエンドに WireGuard を使うように指定しています。これで Kubernetes のネットワークが暗号化されます。

続けて、もう一つのサーバーに`agent`を立ち上げます。

```bash
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="agent --server https://xxx:6443 --token yyy" sh -s -
```

今回も`INSTALL_K3S_EXEC`でオプションを指定しています。  
`agent`を含めることで、`agent`としてノードを立ち上げています。  
さらに`agent`は`server`に接続するために`--server`と`--token`を指定しています。  
`--server`は`server`のアドレスを指定します。  
`--token`は`server`に接続するためのトークンを指定します。`--token`に指定する値は`server`ノードの`/var/lib/rancher/k3s/server/node-token`に書かれています。

オプションの指定の仕方はいろいろあるので、下記も参照してください。

- [Configuration Options](https://docs.k3s.io/installation/configuration)
- [Network Options](https://docs.k3s.io/installation/network-options)

#### 動作確認

`server`ノードで次のコマンドを実行してみます。

```bash
$ sudo k3s kubectl get node
```

すると、`agent`ノードも含めて 2 つのノードが表示されるはずです。

### ローカルから kubectl コマンドを実行する

k3s は kubeconfig ファイルを`/etc/rancher/k3s/k3s.yaml`に作成しているのでこれをローカルにコピーしてきます。  
kubectl コマンドが利用する kubeconfig ファイルの指定方法はいくつかありますが、例えば`KUBECONFIG`環境変数に指定します。

なお、`Unable to connect to the server: x509: certificate is valid for`というエラーが出る場合は、`--insecure-skip-tls-verify`オプションをつけます。証明書の確認をスキップしますが、今回は実運用するわけではなく、学習目的なので、一旦このオプションで解決しても良いと思います。

```bash
$ export KUBECONFIG=k3s.yaml
$ kubectl get node --insecure-skip-tls-verify
```

ノードが 2 つ表示されるはずです。  
これでローカルからクラスターを操作できるようになりました。

## namespace の作成

アプリケーションをデプロイするための Namespace を作成します。  
まず、`kubectl get namespace`で既存の Namespace を確認します。

```bash
$ kubectl get namespace
NAME              STATUS   AGE
kube-system       Active   9m13s
default           Active   9m13s
kube-public       Active   9m13s
kube-node-lease   Active   9m13s
```

のように`kube`ではじまる kube システム用の Namespace や`default`が見つかるはずです。  
ここに Namespace を追加します。  
`kubectl create namespace`コマンドで作成することもできますが、ここでは YAML ファイルを作成して`kubectl apply`コマンドで作成します。次のような YAML ファイルを作成します。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: app
  labels:
    name: app
```

そして、`kubectl apply`コマンドで YAML ファイルを Kubernetes に適用します。

```bash
$ # （namespace.yamlに上記のYAMLファイルを保存しているとして、）
$ kubectl apply -f namespace.yaml
```

`kubectl get namespace`を再度実行することで、`app`が追加されていることがわかります。

## deployment の作成

次に deployment を作成します。  
再度 YAML ファイルを作成します。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-app
  name: sample-app
  namespace: app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - image: containous/whoami:v1.5.0
          name: whoami
```

ここでは[whoami](https://github.com/traefik/whoami)という動作確認用のイメージを使います。

`kubectl apply`コマンドで YAML ファイルを Kubernetes に適用します。

```bash
$ # （deployment.yamlに上記のYAMLファイルを保存しているとして、）
$ kubectl apply -f deployment.yaml
```

Kubernetes で deployment を作成すると Pod と ReplicaSet が作成されます。次のコマンドで確認できます。

```bash
$ kubectl get pod -n app
$ kubectl get rs -n app
```

`-n`は Namespace を指定しています。

## service の作成

次に service を作成します。  
service とは Pod に関するネットワーク関連の設定です。

service の設定は次のような YAML ファイルで行います。

```yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: sample-app
  name: sample-app
  namespace: app
spec:
  ports:
    - name: "whoami"
      port: 18080
      protocol: TCP
      targetPort: 80
  selector:
    app: sample-app
  type: ClusterIP
```

次のコマンドで service を作成します。

```bash
$ # （service.yamlに上記のYAMLファイルを保存しているとして、）
$ kubectl apply -f service.yaml
```

service が作成されたことは

```bash
$ kubectl get svc -n app
```

で確認できます。  
なお、`type: ClusterIP`なので、service はクラスター内からしかアクセスできません。現時点でサーバーに curl などでアクセスしようとしてもできないはずです。

`port`は何でも良いはずですが、traefik が 80 番ポートを使っているので、`targetPort`は 80 以外の適当な値にしています。  
現時点ではサーバー内外から curl などでアクセスしようとしてもできません。

## ingress の作成

最後に ingress を作成します。

ingress の設定は次のような YAML ファイルで行います。  
ingress とは、クラスター外からのクラスター内の service に対するアクセスを管理するものです。

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sample-ingress
  namespace: app
spec:
  rules:
  - http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: sample-app
            port:
              number: 18080
```

`path`は`/api`としています。  
whoami コンテナは`/`にアクセスすると text で結果を返します。一方で、`/api`にアクセスすると JSON で結果を返します。  
今回は練習のために`/`ではアクセスできず、`/api`にアクセスする必要があるように設定してみます。  
これで ingress が「Service に対する外部からのアクセス(主に HTTP)を管理」していることが確認できるのではないかと思います。

これを次のコマンドで適用します。

```bash
$ # （ingress.yamlに上記のYAMLファイルを保存しているとして、）
$ kubectl apply -f ingress.yaml
```

ingress が作成されたことは

```bash
$ kubectl get ingress -n app
```

で確認できます。

### ingress の確認

ingress が作成されると、traefik が自動的に ingress を認識してルーティングを行います。  
traefik は k3s に同梱された k3s とは別の OSS です。

curl でリクエストを送信して動作確認してみます。  
もし`SSL certificate problem`などと証明書関連のエラーが発生すれば一旦`-k`オプションを付けて無視します。  
どの node にリクエストを送信しても whoami コンテナのレスポンスを取得できます。whoami コンテナはコンテナがあるサーバーの IP アドレスなども返してくれるので動作確認で便利です。  
また、今回は ingress に`/api`と指定しているので、`/`にアクセスしてもレスポンスを取得できないことも確認できます。

```bash
$ curl -k https://aaa.bbb.ccc.ddd/api
```

これで k3s でアプリをデプロイし、外部からアクセスできるように設定できました。

## 参考

k3s のドキュメント

https://docs.k3s.io/

k3s 日本語ドキュメント

https://techiemedia.booth.pm/items/2201461