前提
- 環境
- 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 クラスタを実行するためのツール)を使用して、クラスタの操作をしてみました。
警告
掲載内容はあくまで私自身の見解であり、私の所属団体・企業における立場、戦略、意見を代表するものではありません。
この記事の対象読者
- Docker の基本は理解しているが、Kubernetes は初めて触れる方
- ローカル環境で Kubernetes を試してみたい方
- 生成 AI 開発環境の構築に興味がある方
この記事のゴール
- この記事では、生成 AI の開発環境(ollama、Openweb UI、Dify)を構築する過程を解説します。
1. Local でクラスタの構築
このセクションでは、以下のことをします。
- Kubernetes や Kind を操作するツールである
kubectl
のインストール- kind を使用して、Local にクラスタを構築
- 今回は、Local でクラスタを作成できるkindを使用しました。
- 他にも似たようなツールで、Minikube や kubeadm がマニュアルでは紹介されています。(初めは、minikube を検討していましたが、クラスタの構築がうまくいかずに断念しました。今思うと、rootless の Podman を使用していたからかもしれませんが、原因はわからず)
1.1 kubectl の install
- kubectl とは、Kunertes のコマンドラインツールで、クラスタの中身を操作(Pod のデプロイや Log の出力など)するときに使用します。
- 公式マニュアルに従いインストールします。
1.2 kind クラスタの定義
- 今回作成するクラスタの構成は以下の通りです。これを yaml ファイルに記述します。
- control-plane x 1
- Node x 2
# クラスタの構成
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 内の詳細
- port について
- portmapping を control-plane に設定しています。これは、Local からクラスタの Pod に向けてアクセスするときに使用します。kind にアクセスすると、Control-plane のなかにある kube-proxy がアクセスを受け、クラスタ内の Port(30080 や 30443)へとルーティングしてくれます。なので、Worker node ではなく、control-plane の Port に穴を開けます。
- node-labels について
- Node にはラベルを付与し、別々の値を設定しました。このラベルを使用して、Pod を任意の Node にスケジュールします。(ラベルの値は、Pod を定義するときに使用します。)つまり、今回の構成でいうとラベルを使用して、Open WebUI と Dify、Ollama を別 Node にデプロイします。
- クラスタの構築
- 下記のコマンドでクラスタを構築します。
- 注意:podman などの rootless を使用するときは、
Delegate=yes
を設定することが必要です。詳細はこちら
systemd-run --scope --user -p "Delegate=yes" kind create cluster --config <1.1のyamlファイル>.yaml
クラスタ作成時のコンソール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 🙂
- これでクラスターが構築できました。
-
podman ps
コマンドで確認すると、control-plane と2つの node ができています。
-
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 も付与されていることが確認できました。
- クラスタ上のリソースの状態を確認したいときは、
kubectl get node --show-labels
- 作成されてからの秒数(AGE)は、意図的に省いています。
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 の作成
- クラスタを構築した段階では、下記の名前空間があります。
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 を使用します。
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 のデプロイで行うことは2つです。
- ollama をデプロイ
- model を pull
- ollama を定義した際のマニュフェストは、以下です。
ollamaのマニュフェスト
# 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
-
マニュフェストには、deployment リソースと service リソースを定義しています。
-
deployment リソース
- kind 項目にしているものです。Pod の更新戦略を定義したもので、更新戦略の方法は、Recreate(再作成)と RollingUpdate があります。今回は Update をすることがないと思っているので、そこまで考えていないです。
- この deployment は、ReplicaSet というリソースを作成します。
- ReplicaSet リソース
- マニュフェストで指定した数、Pod を作成するリソースです。
- replicas 項目に、Pod の Replica 数を定義しています。今回は1つにしていますが、複製する場合はこの数を増やします。
-
Service リソース
- 各 Pod への適切なルーティングを行ってくれるリソースです。
- これがないと各 Pod の IP アドレスを指定してアクセスする必要があり、RollingUpdate 中にアクセスしていた場合、接続が途切れてしまいます。
- targetPort は、利用するコンテナが開放している Port を指定します。
-
簡単に説明したところで、マニュフェストのデプロイを行っていきます。
-
マニュフェストを直接デプロイする時は、
kubectl apply -f <filepath>
コマンドを使用します。
kubectl apply -f ollama.yaml
- apply 後、pod と service、deployment ができていることが確認できました。
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 がうまくデプロイできなかった時、原因究明に役に立つコマンドです。
- 今回は、
kubectl describe pod ollama-585b774f56-7wr99 -n ai-dev
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 などのコマンドと同じようなやり方でできます。(本当は、マニュフェストの中に定義したかったのですが、うまく行かなかった、、、)
kubectl exec -it ollama-585b774f56-7wr99 -n ai-dev -- ollama run gemma3:1b
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
コマンドを使用します。
kubectl logs ollama-585b774f56-7wr99 -n ai-dev
- ollama は立ち上がっていそうです。
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 のデプロイでやることは、以下の3つです。
- 環境変数を configMap に格納(今回は扱いませんが、API_KEY など暗号化が必要なものは、secret に保存します)
- openwebui をデプロイ
- Local の Web ブラウザからアクセス
- configMap の作成
- 変数名とその値を定義したマニュフェストを作成します。
-
kubectl apply -f <マニュフェストパス>
でデプロイ
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 が2になっているので、定義できていそうです。
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>
- openwebui も ollama と同様にマニュフェストを書いて、kind クラスタに apply します。ollama との差分は、以下です。
- デプロイ先の Node。マニュフェストの中に Nodelabel を指定して、worker2 にスケジュールします。
- deployment リソース内で、configMap から環境変数を取得
- service リソースの Type を NodePort とする。(これは、ブラウザからアクセスするためです。)
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 から取れていそうです。
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 みたいです。詳細はこちら
- 加えて、クラスタの構築時に、extraPortMappings を指定しています。これは、LocalPC の Port とクラスタの Port を Bind する設定です。
- なのでブラウザで、
http://localhost:8085
を開くと Openwebui にアクセスできるはずです。- 実際には、NodePort ではなく、Ingress を使用して外部からのアクセスをルーティングします。実際にためしたものの Ingress へアクセスできず、PD も完了していないので、NodePort を使用しました。
- Ingress とは、いわば LoadBalancer です。外部からのアクセスに対して、Path などで適切にルーティングしてくれます。なので、NodePort でいちいち Port を開ける必要がないですし、https 通信などのセキュリティ面でも、Ingress を使用するべきだと考えています。
無事にアクセスできました!
また、configMap で指定していた ollama の API 接続先も Openwebui の環境変数として定義していました。
- ollama には、gemma3:1b を入れていました。モデルとの疎通も成功です。
- 以上で、Openwebui のデプロイが完了です。
2.2.3 dify
- dify の導入は、ややこしかったので、Helm を使用します。
- helm の説明は、またいずれかで、簡単に手順の Log を残します。
- helm cli のインストール
- https://github.com/BorisPolonsky/dify-helmの chart を使用
- Nodeselecter と service(NodePort タイプ)を dify-values.yaml として定義
- 3で作成した yaml ファイルをもとにデプロイする。
helm install dify-release dify/dify -f dify-values.yaml --namespace ai-dev
- 完了
-
http://localhost:8443
をブラウザで開いてアクセス
- helm の説明は、またいずれかで、簡単に手順の Log を残します。
まとめ
今回は、ai 関連のアプリケーションを Kind(Kubernetes)にデプロイしてみました。この環境で、生成 AI を使用したアプリケーションを簡単に作成することができます!
今回は、Ingress や Storage の設定は行っていないので、完全とは言えませんが、基本的な流れはつかめたと思います。コンテナでアプリケーションを運用するには、Kubernetes や Openshift などのオーケストレーションサービスは、かなり有効です。スケーリングなどを試してはいないので、またの機会に実施できたらいいなと思います。