# kind(Kubernetes)を触ってみた

## 前提

- 環境
  - Ubuntu 22.04
  - kind version 0.27.0
    - Kunertes の Local 版
  - podman version 4.9.3
  - Openwebui
  - Dify
  - ollama

## はじめに

最近、Cloud 関連の記事をあさっていると、多くの企業はコンテナやサーバレス環境で新しいアプリケーションを稼働させているという記事を見ました。というのも、インフラ関連の管理が容易になる点や迅速な開発が可能になる点が、コンテナやサーバレス採用の決め手となっているそうです。筆者自身、Docker といったコンテナは使用したことがあるものの、コンテナの管理を行う Kubernetes や Openshift などは、一回触ったことがありませんでした。
この機会に、kind(Local で Kubernetes クラスタを実行するためのツール)を使用して、クラスタの操作をしてみました。

:::note warn
警告
掲載内容はあくまで私自身の見解であり、私の所属団体・企業における立場、戦略、意見を代表するものではありません。
:::

## この記事の対象読者

- Docker の基本は理解しているが、Kubernetes は初めて触れる方
- ローカル環境で Kubernetes を試してみたい方
- 生成 AI 開発環境の構築に興味がある方

## この記事のゴール

- この記事では、生成 AI の開発環境（ollama、Openweb UI、Dify）を構築する過程を解説します。
  - オーケストレーションなどの操作は行わずに、クラスタの構築とコンテナのスケジュールをメインに扱っていきます。
  - 生成 AI の開発環境を選択した理由は、LocalLLM を試したかったことと、生成 AI 関連で気になっていたアプリケーション(Open WebUI, Dify)を触りたかったためです。
  - 下記の画像の番号は、扱うセクションの番号を示しています。
![スクリーンショット 2025-05-14 124010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4069049/dfb47383-4347-4506-a212-273fc53877ff.png)


### 1. Local でクラスタの構築

> このセクションでは、以下のことをします。
>
> - Kubernetes や Kind を操作するツールである`kubectl`のインストール
> - kind を使用して、Local にクラスタを構築

- 今回は、Local でクラスタを作成できる[kind](https://kubernetes.io/ja/docs/setup/learning-environment/kind/)を使用しました。
  - 他にも似たようなツールで、Minikube や kubeadm がマニュアルでは紹介されています。（初めは、minikube を検討していましたが、クラスタの構築がうまくいかずに断念しました。今思うと、rootless の Podman を使用していたからかもしれませんが、原因はわからず）

#### 1.1 kubectl の install

- kubectl とは、Kunertes のコマンドラインツールで、クラスタの中身を操作（Pod のデプロイや Log の出力など）するときに使用します。
- [公式マニュアル](https://kubernetes.io/docs/tasks/tools/#kubectl)に従いインストールします。

#### 1.2 kind クラスタの定義

- 今回作成するクラスタの構成は以下の通りです。これを yaml ファイルに記述します。
  - control-plane x 1
  - Node x 2

```yaml
# クラスタの構成
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings: # 1. Port
      - containerPort: 30080
        hostPort: 8085
        protocol: TCP
      - containerPort: 30443
        hostPort: 8443
        protocol: TCP
  - role: worker
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "label_name=llm_worker"　# 2. Nodeラベル
  - role: worker
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "label_name=llm_dev"    # 2. Nodeラベル
```

##### yaml 内の詳細

1. port について
   - portmapping を control-plane に設定しています。これは、Local からクラスタの Pod に向けてアクセスするときに使用します。kind にアクセスすると、Control-plane のなかにある kube-proxy がアクセスを受け、クラスタ内の Port(30080 や 30443)へとルーティングしてくれます。なので、Worker node ではなく、control-plane の Port に穴を開けます。
2. node-labels について
   - Node にはラベルを付与し、別々の値を設定しました。このラベルを使用して、Pod を任意の Node にスケジュールします。（ラベルの値は、Pod を定義するときに使用します。）つまり、今回の構成でいうとラベルを使用して、Open WebUI と Dify、Ollama を別 Node にデプロイします。

- クラスタの構築
  - 下記のコマンドでクラスタを構築します。
  - 注意：podman などの rootless を使用するときは、`Delegate=yes`を設定することが必要です。[詳細はこちら](https://kind.sigs.k8s.io/docs/user/rootless/)

```shell
systemd-run --scope --user -p "Delegate=yes" kind create cluster --config <1.1のyamlファイル>.yaml
```

<details ><summary>クラスタ作成時のコンソールLog</summary>

```コマンドラインLog
Running as unit: run-r3e6adfccad5b49329534d5e9bb58ba11.scope; invocation ID: 06933af9926b41939278f8daf6087f28
using podman due to KIND_EXPERIMENTAL_PROVIDER
enabling experimental podman provider
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.32.2) 🖼
 ✓ Preparing nodes 📦 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂

```

</details >

- これでクラスターが構築できました。
  - `podman ps`コマンドで確認すると、control-plane と２つの node ができています。

```shell
CONTAINER ID  IMAGE                                                                                           COMMAND     PORTS                                                                        NAMES
f38821e29738  docker.io/kindest/node@sha256:f226345927d7e348497136874b6d207e0b32cc52154ad8323129352923a3142f                                                                                           kind-worker2
f8be4b1d254c  docker.io/kindest/node@sha256:f226345927d7e348497136874b6d207e0b32cc52154ad8323129352923a3142f              127.0.0.1:33793->6443/tcp, 0.0.0.0:8085->30080/tcp, 0.0.0.0:8443->30443/tcp  kind-control-plane
49d5ae5bb31b  docker.io/kindest/node@sha256:f226345927d7e348497136874b6d207e0b32cc52154ad8323129352923a3142f                                                                                           kind-worker
```

- 作成したクラスタに対して、kubectl で Node 情報を確認します。
  - クラスタ上のリソースの状態を確認したいときは、`kubectl get <resource type> <resource名> -n <namespace>`コマンドを実行します。
  - Worker node に Label も付与されていることが確認できました。

```shell
kubectl get node --show-labels
```

- 作成されてからの秒数(AGE)は、意図的に省いています。

```shell
NAME                STATUS  ROLES          VERSION  LABELS
kind-control-plane  Ready   control-plane  v1.32.2  beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=kind-control-plane,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node.kubernetes.io/exclude-from-external-load-balancers=
kind-worker         Ready   <none>         v1.32.2  beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=kind-worker,kubernetes.io/os=linux,label_name=llm_worker
kind-worker2        Ready   <none>         v1.32.2  beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=kind-worker2,kubernetes.io/os=linux,label_name=llm_dev

```

- yaml ファイルの定義通りに Node ができてますね。

### 2. ollama、Openwebui、Dify のデプロイ

> このセクションでは、構築したクラスタに対して、以下の操作をしていきます。
>
> - namespace の作成
> - ollama のデプロイとモデルのインストール
> - Open WebUI のデプロイと ollama との接続確認
> - Dify のデプロイ

#### 2.1 namspace の作成

- クラスタを構築した段階では、下記の名前空間があります。

```shell
kubectl get namespace

NAME                 STATUS
default              Active
kube-node-lease      Active
kube-public          Active
kube-system          Active
local-path-storage   Active
```

- default に dify や ollama を入れていってもいいのですが、実際の運用では使わないような気がするので、今回は新に`ai-dev`という namespace を作成して、そこにデプロイしていきます。
  - namespace の作成やデプロイなどクラスタ内の操作には、1.1 でインストールした kubectl を使用します。

```shell
kubectl create namespace ai-dev

# namespace 作成後
NAME                 STATUS
ai-dev               Active
default              Active
kube-node-lease      Active
kube-public          Active
kube-system          Active
local-path-storage   Active
```

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

- 各アプリケーションのデプロイを行っていきます。Helm と呼ばれるマニュフェストのリポジトリがあり、そちらをカスタムしてデプロイする方法が断然に楽です。しかし、Helm で実行すると、なあなあになってしまうかなと思い、マニュフェストを作成しました。（ベースを生成 AI に作成させて、必要部分を自分で改変しています。）

#### 2.2.1 ollama のデプロイ

- ollama のデプロイで行うことは２つです。
  - ollama をデプロイ
  - model を pull
- ollama を定義した際のマニュフェストは、以下です。

<details ><summary>ollamaのマニュフェスト</summary>

```yaml
# ollama.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ollama
  namespace: ai-dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ollama
  template:
    metadata:
      labels:
        app: ollama
    spec:
      nodeSelector:
        label_name: llm_worker
      containers:
        - name: ollama
          image: ollama/ollama:latest
          ports:
            - containerPort: 11434
              protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: ollama-service
  namespace: ai-dev
  labels:
    app: ollama
spec:
  selector:
    app: ollama
  ports:
    - port: 11434
      targetPort: 11434
      protocol: TCP
```

</details >

- マニュフェストには、deployment リソースと service リソースを定義しています。
- deployment リソース

  - kind 項目にしているものです。Pod の更新戦略を定義したもので、更新戦略の方法は、Recreate(再作成)と RollingUpdate があります。今回は Update をすることがないと思っているので、そこまで考えていないです。
  - この deployment は、ReplicaSet というリソースを作成します。
  - ReplicaSet リソース
    - マニュフェストで指定した数、Pod を作成するリソースです。
    - replicas 項目に、Pod の Replica 数を定義しています。今回は１つにしていますが、複製する場合はこの数を増やします。

- Service リソース

  - 各 Pod への適切なルーティングを行ってくれるリソースです。
  - これがないと各 Pod の IP アドレスを指定してアクセスする必要があり、RollingUpdate 中にアクセスしていた場合、接続が途切れてしまいます。
  - targetPort は、利用するコンテナが開放している Port を指定します。

- 簡単に説明したところで、マニュフェストのデプロイを行っていきます。
- マニュフェストを直接デプロイする時は、`kubectl apply -f <filepath>`コマンドを使用します。

```shell
kubectl apply -f ollama.yaml
```

- apply 後、pod と service、deployment ができていることが確認できました。

```shell
kubectl get pod,service,deployment -n ai-dev
NAME                         READY      STATUS         RESTARTS
pod/ollama-585b774f56-7wr99  1/1        Running        0
NAME                         TYPE       CLUSTER-IP     EXTERNAL-IP  PORT(S)
service/ollama-service       ClusterIP  10.96.238.170  <none>       11434/TCP
NAME                         READY      UP-TO-DATE     AVAILABLE
deployment.apps/ollama       1/1        1              1
```

- ここでマニュフェストで定義した nodeselecter について、確認します。
- nodeselecter とは、Pod のスケジュールに使用するもので、特定の Node に Pod を配置したい時に使用します。このラベルを見て、kubernetes のスケジューラー が目的の Node にスケジュールしてくれます。
  - この Pod のスケジュール方法については、Node の情報ををもとにスケジュールする方法（Node selector や Node Affinity）や Node 上で稼働している他の Pod をもとにスケジュールする方法（Pod Affinity や Pod Anti-Affinity）があります。これらを使用することで、Pod を柔軟に配置できます。
- では、確認していきましょう。
  - 今回は、`kubectl describe pod` コマンドを使用します。
  - このコマンドを使用することで、Pod の詳細な情報や Pod の event 情報を確認することができます。特に、Pod がうまくデプロイできなかった時、原因究明に役に立つコマンドです。

```shell
kubectl describe pod ollama-585b774f56-7wr99 -n ai-dev
```

```shell
Name:             ollama-585b774f56-7wr99
Namespace:        ai-dev
Priority:         0
Service Account:  default
Node:             kind-worker/10.89.0.5
Start Time:       Sun, 20 Apr 2025 12:01:43 +0900
〜〜〜割愛〜〜〜
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  13m   default-scheduler  Successfully assigned ai-dev/ollama-585b774f56-7wr99 to kind-worker
  Normal  Pulling    13m   kubelet            Pulling image "ollama/ollama:latest"
  Normal  Pulled     10m   kubelet            Successfully pulled image "ollama/ollama:latest" in 3m3.995s (3m3.995s including waiting). Image size: 1771606532 bytes.
  Normal  Created    10m   kubelet            Created container: ollama
  Normal  Started    10m   kubelet            Started container ollama
```

- Evetns のはじめの行で Scheduled があり、kind-worker にスケジュールされています。

  - これは、Control-plane の中にあるスケジューラーがマニュフェストを解析して、NodeSelector と Node label をみて、ユーザーの要求に合わせて Pod を目的の Node に割り当ててくれています。
  - そのド、指示を受け取った Node 上の kubelet がイメージの pull からデプロイまで担当します。
  - ちなみに Node 欄でも、kind-worker/10.89.0.5 にデプロイされていることがわかります。

- 立ち上げた ollama コンテナの中に入り、モデルをダウンロードします。
  - これは、Docker などのコマンドと同じようなやり方でできます。（本当は、マニュフェストの中に定義したかったのですが、うまく行かなかった、、、）

```shell
kubectl exec -it ollama-585b774f56-7wr99 -n ai-dev -- ollama run gemma3:1b
```

```shell
pulling manifest
pulling 7cd4618c1faf... 100% ▕████████████████████████████████████████████████████████████████▏ 815 MB
pulling e0a42594d802... 100% ▕████████████████████████████████████████████████████████████████▏  358 B
pulling dd084c7d92a3... 100% ▕████████████████████████████████████████████████████████████████▏ 8.4 KB
pulling 3116c5225075... 100% ▕████████████████████████████████████████████████████████████████▏   77 B
pulling 120007c81bf8... 100% ▕████████████████████████████████████████████████████████████████▏  492 B
verifying sha256 digest
writing manifest

success
>>>
```

- model のダウンロードができました。
- pod の Log を確認するときは、`kubectl logs`コマンドを使用します。

```shell
kubectl logs ollama-585b774f56-7wr99 -n ai-dev
```

- ollama は立ち上がっていそうです。

```shell
Couldn't find '/root/.ollama/id_ed25519'. Generating new private key.
Your new public key is:

ssh-ed25519 ***

2025/04/20 03:04:47 routes.go:1231: INFO server config env="map[CUDA_VISIBLE_DEVICES: GPU_DEVICE_ORDINAL: HIP_VISIBLE_DEVICES: HSA_OVERRIDE_GFX_VERSION: HTTPS_PROXY: HTTP_PROXY: NO_PROXY: OLLAMA_CONTEXT_LENGTH:2048 OLLAMA_DEBUG:false OLLAMA_FLASH_ATTENTION:false OLLAMA_GPU_OVERHEAD:0 OLLAMA_HOST:http://0.0.0.0:11434 OLLAMA_INTEL_GPU:false OLLAMA_KEEP_ALIVE:5m0s OLLAMA_KV_CACHE_TYPE: OLLAMA_LLM_LIBRARY: OLLAMA_LOAD_TIMEOUT:5m0s OLLAMA_MAX_LOADED_MODELS:0 OLLAMA_MAX_QUEUE:512 OLLAMA_MODELS:/root/.ollama/models OLLAMA_MULTIUSER_CACHE:false OLLAMA_NEW_ENGINE:false OLLAMA_NOHISTORY:false OLLAMA_NOPRUNE:false OLLAMA_NUM_PARALLEL:0 OLLAMA_ORIGINS:[http://localhost https://localhost http://localhost:* https://localhost:* http://127.0.0.1 https://127.0.0.1 http://127.0.0.1:* https://127.0.0.1:* http://0.0.0.0 https://0.0.0.0 http://0.0.0.0:* https://0.0.0.0:* app://* file://* tauri://* vscode-webview://* vscode-file://*] OLLAMA_SCHED_SPREAD:false ROCR_VISIBLE_DEVICES: http_proxy: https_proxy: no_proxy:]"
〜割愛〜
```

#### 2.2.2 openwebui のデプロイ

- openwebui のデプロイでやることは、以下の３つです。
  - 環境変数を configMap に格納（今回は扱いませんが、API_KEY など暗号化が必要なものは、secret に保存します）
  - openwebui をデプロイ
  - Local の Web ブラウザからアクセス

1. configMap の作成

- 変数名とその値を定義したマニュフェストを作成します。
- `kubectl apply -f <マニュフェストパス>`でデプロイ

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: open-webui-config
  namespace: ai-dev
data:
  # ここに環境変数として設定したいキーと値を記述
  OLLAMA_BASE_URL: "http://ollama-service.ai-dev.svc.cluster.local:11434"
  WEBUI_AUTH: "False"
```

- `kubectl get configMap -n ai-dev`で確認すると、Data が２になっているので、定義できていそうです。

```log
NAME               DATA
kube-root-ca.crt   1
open-webui-config  2
```

- 中身を確認します。ollama の pod を確認したときと同様に、`kubectl describe <resource>`コマンドで確認します。
  - 環境変数が定義されていました。

```
〜〜〜割愛〜〜〜
Data
====
OLLAMA_BASE_URL:
----
http://ollama-service.ai-dev.svc.cluster.local:11434

WEBUI_AUTH:
----
False

BinaryData
====

Events:  <none>
```

2. openwebui も ollama と同様にマニュフェストを書いて、kind クラスタに apply します。ollama との差分は、以下です。
   1. デプロイ先の Node。マニュフェストの中に Nodelabel を指定して、worker2 にスケジュールします。
   2. deployment リソース内で、configMap から環境変数を取得
   3. service リソースの Type を NodePort とする。（これは、ブラウザからアクセスするためです。）

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: open-webui
  namespace: ai-dev
  labels:
    app: open-webui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: open-webui
  template:
    metadata:
      labels:
        app: open-webui
    spec:
      # 1. deploy先のNodeをlabelで指定
      nodeSelector:
        label_name: llm_dev
      containers:
        - name: open-webui
          image: ghcr.io/open-webui/open-webui:main
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          # 2. 作成したconfigMapの名前を指定
          envFrom:
            - configMapRef:
                name: open-webui-config
---
apiVersion: v1
kind: Service
metadata:
  name: open-webui-service
  namespace: ai-dev
  labels:
    app: open-webui
spec:
  # 3. serviceリソースのTypeを指定
  type: NodePort
  selector:
    app: open-webui
  ports:
    - name: http
      # 3. nodePortは、クラスタの設定で定義したcontainerPort番号を指定
      nodePort: 30080
      port: 8080
      protocol: TCP
```

- `kubectl apply -f <マニュフェストパス>`でデプロイした後、Pod の中身を確認します。
  - Node 欄にデプロイ先の Node が記載されています。workerNode にデプロイできていそうですね
  - Environment 欄に、configMap のリソース名が記載されています。環境変数も CofigMap から取れていそうです。

```shell
Name:             open-webui-649bff6844-zpt5v
Namespace:        ai-dev
Priority:         0
Service Account:  default
Node:             kind-worker2/10.89.0.6
Start Time:       Wed, 23 Apr 2025 06:56:45 +0900
Labels:           app=open-webui
                  pod-template-hash=649bff6844
〜〜〜割愛〜〜〜
    Environment Variables from:
      open-webui-config  configMap  Optional: false
    Environment:         <none>
〜〜〜割愛〜〜〜
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  20m   default-scheduler  Successfully assigned ai-dev/open-webui-649bff6844-zpt5v to kind-worker2
  Normal  Pulled     20m   kubelet            Container image "ghcr.io/open-webui/open-webui:main" already present on machine
  Normal  Created    20m   kubelet            Created container: open-webui
  Normal  Started    20m   kubelet            Started container open-webui
```

- Openwebui にアクセスしましょう。
  - service Type で指定した NodePort とは、簡単にいうと、クラスタの外から Node の Pod にアクセスするために Port を開ける設定です。
  - NodePort に使用できる Port 番号は、デフォルトで 30000 ～ 32767 みたいです。[詳細はこちら](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport)
  - 加えて、クラスタの構築時に、extraPortMappings を指定しています。これは、LocalPC の Port とクラスタの Port を Bind する設定です。
  - なのでブラウザで、`http://localhost:8085`を開くと Openwebui にアクセスできるはずです。
    - 実際には、NodePort ではなく、Ingress を使用して外部からのアクセスをルーティングします。実際にためしたものの Ingress へアクセスできず、PD も完了していないので、NodePort を使用しました。
    - Ingress とは、いわば LoadBalancer です。外部からのアクセスに対して、Path などで適切にルーティングしてくれます。なので、NodePort でいちいち Port を開ける必要がないですし、https 通信などのセキュリティ面でも、Ingress を使用するべきだと考えています。
      ![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4069049/62a21086-be7c-47c2-9dc3-323588d93f64.png)
      無事にアクセスできました！
      また、configMap で指定していた ollama の API 接続先も Openwebui の環境変数として定義していました。
      ![image-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4069049/0ea53a8c-1c61-43c5-b6eb-259c7a18702d.png)
- ollama には、gemma3:1b を入れていました。モデルとの疎通も成功です。
  ![image-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4069049/46d0fbdb-c35a-4b33-a339-2f86f511a847.png)
- 以上で、Openwebui のデプロイが完了です。

#### 2.2.3 dify

- dify の導入は、ややこしかったので、Helm を使用します。
  - helm の説明は、またいずれかで、簡単に手順の Log を残します。
    1. helm cli のインストール
    2. <https://github.com/BorisPolonsky/dify-helm>の chart を使用
    3. Nodeselecter と service(NodePort タイプ)を dify-values.yaml として定義
    4. ３で作成した yaml ファイルをもとにデプロイする。
       `helm install dify-release dify/dify -f dify-values.yaml --namespace ai-dev`
    5. 完了
    6. `http://localhost:8443`をブラウザで開いてアクセス

## まとめ

今回は、ai 関連のアプリケーションを Kind（Kubernetes）にデプロイしてみました。この環境で、生成 AI を使用したアプリケーションを簡単に作成することができます！
今回は、Ingress や Storage の設定は行っていないので、完全とは言えませんが、基本的な流れはつかめたと思います。コンテナでアプリケーションを運用するには、Kubernetes や Openshift などのオーケストレーションサービスは、かなり有効です。スケーリングなどを試してはいないので、またの機会に実施できたらいいなと思います。