kubeadmでovn-kubernetesのクラスタを構築
Table of Contents
1. はじめに
今回は、我が家の自宅サーバーで ovn-kubernetes を使ったKubernetesクラスタを構築した話をしようと思います!
kubeadm, Ansibleを使った自前のKubernetesクラスタ構築や、ovn-kubernetesに興味がある方の参考になれば幸いです。
2. 環境構成
今回の構築環境はこんな感じです。 Nodeは全てProxmox VE上の仮想マシンを利用しています。
- Node 構成
- Control Plane Node: 1台(2vCPU、4GiB メモリ)
- Worker Node: 2台(4vCPU、8GiB メモリ)
- OS: Ubuntu 24.04 Noble
- Kubernetes: v1.33
- コンテナランタイム: containerd 2.1.4
- ネットワークアドオン: OVN-Kubernetes v1.1.0
Nodeに必要な要件は公式ドキュメントの kubeadmのインストール#始める前に に記載されているので、構築する際はそちらも参考にしてください。
3. 構築手順
実際の構築は全てAnsible Playbookで記述しました。 Proxmox VMを起動して2つのPlaybookを順番に実行するだけで、Kubernetesクラスタが構築されるようになっています。
cluster_bootstrap.yml
: Control Planeの初期化とOVN-Kubernetesのインストールworker_join.yml
: Worker NodeをClusterに参加させる
3.1 Playbook構成
Ansible Playbookは以下のような構成になっています。
.
├── group_vars
│ └── all.yml
│── roles/
│ ├── common/ # 全ノード共通の設定
│ │ ├── tasks/
│ │ │ ├── main.yml
│ │ │ ├── swap_off.yml # swapの無効化
│ │ │ ├── resolve.yml # systemd-resolved設定
│ │ │ ├── containerd.yml # containerdインストール
│ │ │ ├── runc.yml # runcインストール
│ │ │ ├── cni.yml # CNIプラグインインストール
│ │ │ └── kube.yml # Kubernetes関連パッケージ
│ ├── cluster_bootstrap/ # Control Plane初期化
│ │ ├── tasks/
│ │ │ ├── main.yml
│ │ │ ├── kube.yml # kubeadm init実行
│ │ │ ├── helm.yml # Helmインストール
│ │ │ └── ovn_kubernetes.yml # OVN-Kubernetes設定
│ └── worker_join/ # Worker Nodeをクラスタに参加
│ └── tasks/
│ └── main.yml # kubeadm joinの実行
├── cluster_bootstrap.yml
├── inventory.yml
├── requirements.txt
├── requirements.yml
└── worker_join.yml
ここからは、各タスクについて抜粋して解説していきます。
3.2 必要パッケージのインストール
まず、各ノードで必要なパッケージをインストールするところから始まります。
# roles/common/tasks/main.yml
- name: Install packages
ansible.builtin.apt:
name:
# Utilities
- tree
- jq
- yq
# Kubernetes dependencies
- apt-transport-https
- ca-certificates
- curl
- gpg
# Helm
- apt-transport-https
- python3-kubernetes
state: present
update_cache: true
インストールするのはホスト上での操作をする際に便利なコマンド群や、Kubernetes, Helm等の依存パッケージです。
3.3 swapの無効化
Kubernetesではswapを無効にすることが求められています。 そのためswapoffを実行し、/etc/fstabからも削除します。
Linuxでswapを無効化・有効化する #Ubuntu - Qiita
# roles/common/tasks/swap_off.yml
- name: Remove swap in fstab
ansible.posix.mount:
name: swap
fstype: swap
state: absent
- name: Swapoff
ansible.builtin.command:
cmd: swapoff -a
when: ansible_swaptotal_mb > 0
register: swapoff_result
changed_when: false
failed_when: swapoff_result.rc != 0
ただし最近のKubernetesでは設定次第でswapを利用することもできるみたいです。
https://kubernetes.io/docs/concepts/cluster-administration/swap-memory-management/#
3.4 カーネルモジュールとsysctl設定
コンテナネットワークに必要なカーネルモジュールの有効化とsysctl設定を行います。
コンテナランタイム | Kubernetes #IPv4フォワーディングを有効化し、iptablesからブリッジされたトラフィックを見えるようにする
# roles/common/tasks/main.yml
- name: Enable modules
community.general.modprobe:
name: "{{ item }}"
persistent: present
loop:
- overlay
- br_netfilter
- name: Update sysctl
ansible.posix.sysctl:
name: "{{ item }}"
value: "1"
sysctl_file: /etc/sysctl.d/k8s.conf
sysctl_set: true
loop:
- net.bridge.bridge-nf-call-iptables
- net.bridge.bridge-nf-call-ip6tables
- net.ipv4.ip_forward
3.5 containerdのインストール
コンテナランタイムとしてcontainerdをインストールします。公式のバイナリを直接ダウンロードして配置しています。
- https://kubernetes.io/ja/docs/setup/production-environment/container-runtimes/
- https://github.com/containerd/containerd/blob/main/docs/getting-started.md
# roles/common/tasks/containerd.yml
- name: Create containerd dir
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ containerd.src_dir }}"
- "{{ containerd.service_dir }}"
- /etc/containerd
- name: Get containerd sha256
ansible.builtin.uri:
url: "{{ containerd.release_url }}/v{{ containerd.version }}/containerd-{{ containerd.version }}-{{ os }}-{{ arch }}.tar.gz.sha256sum"
return_content: true
register: containerd_sha256
- name: Set containerd sha256
ansible.builtin.set_fact:
containerd_sha256: "{{ (containerd_sha256.content | regex_search('([a-f0-9]{64})\\s+containerd-' + containerd.version + '-' + os + '-' + arch + '\\.tar\\.gz', '\\1'))[0] }}"
- name: Get containerd package
ansible.builtin.get_url:
url: "{{ containerd.release_url }}/v{{ containerd.version }}/containerd-{{ containerd.version }}-{{ os }}-{{ arch }}.tar.gz"
dest: "{{ containerd.src_dir }}/containerd.tar.gz"
mode: "0644"
checksum: "sha256:{{ containerd_sha256 }}"
- name: Extract containerd package
ansible.builtin.unarchive:
remote_src: true
src: "{{ containerd.src_dir }}/containerd.tar.gz"
dest: /usr/local
notify: Restart containerd
- name: Get containerd service file
ansible.builtin.get_url:
url: "https://raw.githubusercontent.com/containerd/containerd/refs/tags/v{{ containerd.version }}/containerd.service"
dest: "{{ containerd.service_dir }}/containerd.service"
mode: "0644"
notify: Restart containerd
- name: Deploy containerd config
ansible.builtin.copy:
src: containerd-config.toml
dest: /etc/containerd/config.toml
owner: root
group: root
mode: "0644"
notify: Restart containerd
- name: Start containerd service
ansible.builtin.systemd_service:
name: containerd
state: started
enabled: true
ここではcontainerdがsystemd cgroupドライバーを利用するように設定しています。
https://kubernetes.io/ja/docs/setup/production-environment/container-runtimes/#containerd-systemd
# containerd-config.toml
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
...
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
SystemdCgroup = true
ちなみにこちら箇所の日本語ドキュメントは自分が先日翻訳させていただいたものです。 詳しくはこちらをご覧ください。
3.6 runcのインストール
コンテナランタイムの低レベルコンポーネントであるruncをインストールします。
# roles/common/tasks/runc.yml
- name: Create runc dir
ansible.builtin.file:
path: "{{ runc.src_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Get runc sha256
ansible.builtin.uri:
url: "{{ runc.release_url }}/v{{ runc.version }}/runc.sha256sum"
return_content: true
register: runc_sha256
- name: Set runc sha256
ansible.builtin.set_fact:
runc_sha256: "{{ (runc_sha256.content | regex_search('([a-f0-9]{64})\\s+runc\\.' + arch, '\\1'))[0] }}"
- name: Get runc package
ansible.builtin.get_url:
url: "{{ runc.release_url }}/v{{ runc.version }}/runc.{{ arch }}"
dest: "{{ runc.src_dir }}/runc"
mode: "0755"
checksum: "sha256:{{ runc_sha256 }}"
- name: Install runc
ansible.builtin.copy:
remote_src: true
src: "{{ runc.src_dir }}/runc"
dest: /usr/local/sbin/runc
owner: root
group: root
mode: "0755"
3.7 CNIプラグインのインストール
Container Network Interfaceプラグインをダウンロードして配置します。
- name: Create cni dirs
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ cni.src_dir }}"
- "{{ cni.bin_dir }}"
- name: Get cni sha256
ansible.builtin.uri:
url: "{{ cni.release_url }}/v{{ cni.version }}/cni-plugins-{{ os }}-{{ arch }}-v{{ cni.version }}.tgz.sha256"
return_content: true
register: cni_sha256
- name: Set cni sha256
ansible.builtin.set_fact:
cni_sha256: "{{ (cni_sha256.content | regex_search('([a-f0-9]{64})\\s+cni-plugins-' + os + '-' + arch + '-v' + cni.version + '\\.tgz', '\\1'))[0] }}"
- name: Get cni package
ansible.builtin.get_url:
url: "{{ cni.release_url }}/v{{ cni.version }}/cni-plugins-{{ os }}-{{ arch }}-v{{ cni.version }}.tgz"
dest: "{{ cni.src_dir }}/cni-plugins.tgz"
mode: "0644"
checksum: "sha256:{{ cni_sha256 }}"
- name: Extract cni package
ansible.builtin.unarchive:
remote_src: true
src: "{{ cni.src_dir }}/cni-plugins.tgz"
dest: "{{ cni.bin_dir }}"
3.8 Kubernetesパッケージのインストール
kubeadm、kubelet、kubectlをインストールします。
kubeadmのインストール | Kubernetes #kubeadm、kubelet、kubectlのインストール
- name: Download Kubernetes GPG key
ansible.builtin.uri:
url: "https://pkgs.k8s.io/core:/stable:/v{{ kubernetes.version }}/deb/Release.key"
return_content: true
register: kubernetes_gpg_key
- name: Convert GPG key to binary format
ansible.builtin.shell:
cmd: "gpg --dearmor > {{ kubernetes.gpg_path }}"
creates: "{{ kubernetes.gpg_path }}"
args:
stdin: "{{ kubernetes_gpg_key.content }}"
- name: Add Kubernetes repository
ansible.builtin.apt_repository:
repo: "deb [signed-by={{ kubernetes.gpg_path }}] https://pkgs.k8s.io/core:/stable:/v{{ kubernetes.version }}/deb/ /"
state: present
filename: kubernetes
- name: Install Kubernetes packages
ansible.builtin.apt:
name:
- kubelet
- kubeadm
- kubectl
state: present
update_cache: true
- name: Hold Kubernetes packages
ansible.builtin.dpkg_selections:
name: "{{ item }}"
selection: hold
loop:
- kubelet
- kubeadm
- kubectl
- name: Create kubelet config to use systemd-resolved upstream
ansible.builtin.copy:
dest: /etc/default/kubelet
content: "KUBELET_EXTRA_ARGS=--resolv-conf={{ resolve_conf_path }}"
owner: root
group: root
mode: "0644"
backup: true
最後の/etc/default/kubelet
の設定は、kubelet実行時のオプションでスタブリゾルバじゃないリゾルバに向けさせるためのものです。
/etc/default/kubelet
に環境変数を書いておくことで、systemdがkubelet起動時に引数に渡してくれるようになっていたので、その仕組みを利用しました。
また、公式の日本語ドキュメントにはDocker以外のコンテナランタイムを使う際には KUBELET_EXTRA_ARGS=--cgroup-driver=<value>
を設定するように書かれていますが、こちらのページにある通り、v1.22以降ではデフォルトでsystemd cgroupドライバーが使われるようになっているため、特に設定する必要はありませんでした。
3.9 Control Plane初期化(cluster_bootstrap Role)
ここからはControl Planeノードでのみ実行される処理です。クラスタを初期化します。
# roles/cluster_bootstrap/tasks/kube.yml
- name: Init cluster
when: ansible_hostname == groups['masters'][0]
block:
- name: Kubeadm init
ansible.builtin.command:
# Skip kube-proxy addon, as we will use OVN-Kubernetes for networking.
cmd: >
kubeadm init
--control-plane-endpoint {{ kubernetes.control_plane_endpoint }}
--apiserver-advertise-address {{ ansible_host }}
--skip-phases=addon/kube-proxy
creates: /etc/kubernetes/admin.conf
register: kubeadm_init
run_once: true
failed_when: kubeadm_init.rc != 0
- name: Create .kube directory
ansible.builtin.file:
path: "{{ item.dest }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "0755"
loop:
- dest: /root/.kube
owner: root
group: root
- dest: "/home/{{ ansible_user }}/.kube"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
- name: Copy admin.conf to .kube/config
ansible.builtin.copy:
src: /etc/kubernetes/admin.conf
dest: "{{ item.dest }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "0600"
remote_src: true
loop:
- dest: /root/.kube/config
owner: root
group: root
- dest: "/home/{{ ansible_user }}/.kube/config"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
when: ansible_hostname == groups['masters'][0]
を指定することで、間違えて他のNodeで実行しても処理がスキップされるようにしています。
また、今回はKubernetes Serviceのネットワーク処理もovn-kubernetesで行われるため、--skip-phases=addon/kube-proxy
オプションでkube-proxyのインストールをスキップしています。
3.10 OVN-Kubernetesのインストール
HelmでOVN-Kubernetesをインストールします。
# roles/cluster_bootstrap/tasks/ovn_kubernetes.yml
- name: Clone ovn-kubernetes repository
ansible.builtin.git:
repo: https://github.com/ovn-kubernetes/ovn-kubernetes.git
dest: /tmp/ovn-kubernetes
version: "{{ ovn_kubernetes.version }}"
force: true
- name: Create ovn-kubernetes namespace
kubernetes.core.k8s:
name: "{{ ovn_kubernetes.namespace }}"
api_version: v1
kind: Namespace
state: present
- name: Install ovn-kubernetes helm chart
kubernetes.core.helm:
name: ovn-kubernetes
chart_ref: /tmp/ovn-kubernetes/helm/ovn-kubernetes
release_namespace: "{{ ovn_kubernetes.namespace }}"
values_files:
- /tmp/ovn-kubernetes/helm/ovn-kubernetes/values-no-ic.yaml
values:
k8sAPIServer: "https://{{ kubernetes.control_plane_endpoint }}:6443"
global:
image:
repository: ghcr.io/ovn-kubernetes/ovn-kubernetes/ovn-kube-ubuntu
tag: "{{ ovn_kubernetes.image_version }}"
ovn-kubernetesのHelm installは公式のガイドがあったためそちらを参考にしつつ、バージョンだけちゃんと設定するようにしています。
https://ovn-kubernetes.io/installation/launching-ovn-kubernetes-with-helm/
インストールが完了すると、ovn-kubernetesのPodが起動していることが確認できます。
$ kubectl -n ovn-kubernetes get pods
NAME READY STATUS RESTARTS AGE
ovnkube-db-894fd95bd-z2nvt 2/2 Running 0 19h
ovnkube-identity-dcfmg 1/1 Running 0 19h
ovnkube-master-669cfc9d94-zg45m 2/2 Running 0 19h
ovnkube-node-7gvnt 3/3 Running 0 19h
ovnkube-node-897g9 3/3 Running 0 19h
ovnkube-node-tk9sk 3/3 Running 0 19h
ovs-node-m2wg2 1/1 Running 0 19h
ovs-node-ncc52 1/1 Running 0 19h
ovs-node-zk7nz 1/1 Running 0 19h
3.11 Worker Nodeの参加(worker_join Role)
続いてWorker Nodeをクラスタに参加させます。 Control Plane Nodeでjoinコマンドを生成して、それを各Worker Nodeで実行するようにしています。
# roles/worker_join/tasks/main.yml
- name: Generate join tokens
ansible.builtin.command:
cmd: kubeadm token create --print-join-command
when: ansible_hostname == groups['masters'][0]
changed_when: false
register: kubeadm_join_command
- name: Join cluster
when: "'workers' in group_names"
block:
- name: Join the node to the Kubernetes cluster
ansible.builtin.command:
cmd: >
{{ hostvars[groups['masters'][0]].kubeadm_join_command.stdout }}
creates: /etc/kubernetes/kubelet.conf
register: join_result
failed_when: join_result.rc != 0
- name: Create .kube directory
ansible.builtin.file:
path: "{{ item.dest }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "0755"
loop:
- dest: /root/.kube
owner: root
group: root
- dest: "/home/{{ ansible_user }}/.kube"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
- name: Copy admin.conf to .kube/config
ansible.builtin.copy:
src: /etc/kubernetes/kubelet.conf
dest: "{{ item.dest }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "0600"
remote_src: true
loop:
- dest: /root/.kube/config
owner: root
group: root
- dest: "/home/{{ ansible_user }}/.kube/config"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
hostvars[groups['masters'][0]].kubeadm_join_command.stdout
のように参照することで、Control Plane Nodeで生成されたjoinコマンドをWorker Nodeで実行することができます。
ただし、ストラテジープラグインで “free” や “host_pinned” を使っている場合、Control Plane Nodeが常に先に実行されるとは限らないため注意が必要です。
デフォルトのストラテジープラグインである “linear” を使うのが無難でしょう。
ここまでの手順でクラスタを構築することで、Control Plane Node1台、Worker Node2台の合計3台のNodeがあることを確認することができます。
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
home-k8s-master01 Ready control-plane 19h v1.33.4
home-k8s-worker01 Ready <none> 19h v1.33.4
home-k8s-worker02 Ready <none> 18h v1.33.4
4. ハマりポイント
構築時にハマったポイントを一つ紹介します。
今回最初にクラスタを構築した際、なぜかNode上で名前解決が一切できなくなってしまう問題が発生しました。 直接正しいネームサーバーを指定してdigを実行すると名前解決ができるため、スタブリゾルバーが何か悪さをしていそうです。
調べてみると、公式ドキュメントにもこのような記載がありました。
Some Linux distributions (e.g. Ubuntu) use a local DNS resolver by default (systemd-resolved). Systemd-resolved moves and replaces /etc/resolv.conf with a stub file that can cause a fatal forwarding loop when resolving names in upstream servers. This can be fixed manually by using kubelet’s –resolv-conf flag to point to the correct resolv.conf (With systemd-resolved, this is /run/systemd/resolve/resolv.conf). kubeadm automatically detects systemd-resolved, and adjusts the kubelet flags accordingly.
Systemd-resolvedがスタブリゾルバを使っているせいで、名前解決のフォワーディングループが発生してしまうようです。kubeadm automatically detects systemd-resolved, and adjusts the kubelet flags accordingly.
と書いてありますが、自動で解決はしてくれなかったので以下のような設定を入れました:
# roles/common/tasks/resolve.yml
- name: Create systemd-resolved configuration
ansible.builtin.copy:
src: resolved.conf
dest: /etc/systemd/resolved.conf
owner: root
group: root
mode: "0644"
backup: true
notify: Restart systemd-resolved
- name: Link resolv.conf to systemd-resolved upstream
ansible.builtin.file:
src: "{{ resolve_conf_path }}" # /run/systemd/resolve/resolv.conf
dest: /etc/resolv.conf
state: link
force: true
# resolved.conf
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# Entries in this file show the compile time defaults. Local configuration
# should be created by either modifying this file (or a copy of it placed in
# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in
# the /etc/systemd/resolved.conf.d/ directory. The latter is generally
# recommended. Defaults can be restored by simply deleting the main
# configuration file and all drop-ins located in /etc/.
#
# Use 'systemd-analyze cat-config systemd/resolved.conf' to display the full config.
#
# See resolved.conf(5) for details.
[Resolve]
# Some examples of DNS servers which may be used for DNS= and FallbackDNS=:
# Cloudflare: 1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com
# Google: 8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google
# Quad9: 9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net
#DNS=
#FallbackDNS=
#Domains=
#DNSSEC=no
#DNSOverTLS=no
#MulticastDNS=no
#LLMNR=no
#Cache=no-negative
#CacheFromLocalhost=no
DNSStubListener=no
#DNSStubListenerExtra=
#ReadEtcHosts=yes
#ResolveUnicastSingleLabel=no
#StaleRetentionSec=0
こちらの設定により、kubeletがスタブリゾルバではない /run/systemd/resolve/resolv.conf
を参照するようにしました。また、念のためsystemd-resolvedの方でもスタブリゾルバーを使わないようにしています。(DNSStubListener=no
)
5. 動作確認
クラスタが正常に動作することを確認するため、簡単なDeploymentとNodePort Serviceを作成してテストしました。
$ kubectl create deployment nginx --image=nginx:latest
deployment.apps/nginx created
$ kubectl expose deployment nginx --type=NodePort --port=80
service/nginx exposed
$ kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/nginx-54c98b4f84-fgn5b 1/1 Running 0 2m41s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 19h
service/nginx NodePort 10.105.216.4 <none> 80:32643/TCP 2m29s
$ curl kube.home.tomokon.net:32643
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
正常に動作することが確認できました。 ovn-kubernetesでServiceのネットワーク処理もちゃんとできているようです。
6. まとめ
今回は自宅サーバ上でkubeadmとOVN-Kubernetesを使ったKubernetesクラスタ構築をしてみました! 自前のKubernetes構築は楽しいし勉強にもなりますね。今後はControl PlaneのHA構成や、構築したKubernetesクラスタ上でのOpenStackや監視基盤の構築に取り組んでいきたいと思います。
最後まで読んでいただき、ありがとうございました!