ホームラボでK3sを構築しよう!RHEL・Podman・Ansibleで作るKubernetes環境

ホームラボでK3sを構築しよう!RHEL・Podman・Ansibleで作るKubernetes環境

AIによるこの記事の要約
RHEL上のrootful PodmanコンテナでシングルノードK3sを動かせるか検証しました。Ansibleによる構築、systemdでの自動起動、firewalldとSELinuxを有効にした状態での稼働、LAN内PCからのkubectl接続を確認し、Node名の変更で遭遇した事象も記録しています。

はじめに

自宅のホームラボでアプリケーションやコンテナ運用を試すため、Kubernetesの検証環境が欲しくなりました。今回は手元にあるRHELサーバーでシングルノード構成を試します。また、今回の環境とは別に、将来は複数台のRaspberry Piを使った物理クラスターも構築したいと考えています。

将来イメージしているのは、単にKubernetesを一時的に動かす環境ではありません。レバテックLABの「なぜ自宅に Kubernetes クラスタを持つのか —— 本番相当に運用して得られる学び(前編)」で紹介されているように、複数台のRaspberry Piで独立したクラスターを組み、監視、障害対応、アップデート、GitOpsなどを継続的に試せる環境です。台数やストレージ、ネットワークの具体的な構成はこれから検討しますが、「作って終わり」ではなく、長く運用しながら育てるホームラボを目指しています。

なぜK3sを選んだのか

候補として比較したのが K3sminikube です。

minikubeは、PC上にローカルKubernetes環境を素早く用意できるため、Kubernetesの学習やアプリケーションの動作確認に向いています。複数Node構成も作成できますが、公式にはローカルでの開発・学習を主な用途としており、リモートネットワークへ公開する使い方は推奨されていません。

一方、K3sはエッジ、ホームラボ、IoT、ARMのシングルボードコンピューターを想定した軽量なKubernetesディストリビューションです。x86_64に加えてarmhfとarm64/aarch64をサポートしているため、今回のRHEL環境で構築や運用の基本を確認しておけば、将来別途構築するRaspberry Piクラスターにも知識を生かせると判断しました。

比較項目K3sminikube
主な用途ホームラボ、エッジ、IoTなどの継続運用ローカルでの学習・開発・一時的な動作確認
複数Node別のマシンをserver/agentとして参加させる構成に向く複数Nodeに対応するが、基本的にはローカル環境内で構成
ARM対応armhf、arm64/aarch64を公式にサポートarm64環境でも利用可能だが、利用できるドライバーなどの確認が必要
今回の目的との相性RHELで得た知識を、将来別途構築するRaspberry Piクラスターにも生かせる手軽な単体検証には適するが、将来目指す物理クラスターとは用途が異なる

このため、単にKubernetesを触るだけならminikubeも有力ですが、将来Raspberry Piによる独立した物理マルチノードクラスターを組み、継続運用するための予備検証として、今回はK3sを選びました。今回のRHEL環境をそのままRaspberry Piクラスターへ拡張するのではなく、それぞれ独立したクラスターとして運用する想定です。

また、今回使うRHELサーバーにはすでにPodmanを導入しています。K3sをホストへ直接インストールせず、導入や削除のライフサイクルを分けて管理したかったため、まずはPodmanコンテナ上でどこまで動かせるかを検証します。

本記事では、K3sRHEL(Red Hat Enterprise Linux) 上のPodmanコンテナとして構築し、その結果を確認します。

今回確認したいのは、次の4点です。

  1. rootful Podman上でK3sのコントロールプレーンとワークロードを起動できるか
  2. Ansibleで同じ構成を再現し、systemdから自動起動できるか
  3. firewalldとSELinuxを有効にしたRHEL上でPodネットワークが動作するか
  4. LAN内の作業PCからKubernetes APIへ接続できるか

以降では、実際に使った構成と確認結果、検証中に遭遇した問題を順に記録します。

検証環境と確認項目

今回の検証環境は以下の通りです。

項目設定値 / 環境
KubernetesディストリビューションK3s v1.36.1+k3s1
ホストOSRed Hat Enterprise Linux 10.2 / 192.168.1.xxx
ホスト側コンテナエンジンrootful Podman 5.8.2
Kubernetes Node名homelab-k3s
Kubernetes API エンドポイントhttps://192.168.1.xxx:6443
構成管理ツールansible-core 2.16.16
操作環境手元の作業PC(Mac / Linux)
作業PC側の kubeconfig~/.kube/homelab-k3s.yaml

※ IPアドレス 192.168.1.xxx はお使いの環境のサーバーIP(例: 192.168.1.100 等)に適宜読み替えてください。

判定条件は、Nodeが Ready になり、K3s標準のシステムPodが Running または Completed へ遷移し、作業PCから kubectl get nodes を実行できることとしました。

検証範囲
今回はホームラボ向けのシングルノード構成です。高可用性、障害復旧、性能、K3sやRHELの商用サポート可否までは検証していません。また、K3sをコンテナで動かす方法は公式ドキュメントにも例がありますが、本記事ではDockerの例をPodmanへ置き換えています。

今回使用したRHEL 10では、K3s公式要件で kernel-modules-extra の導入が案内されています。検証前に、ホストへ必要なカーネルモジュールが導入されていることを確認してください。

検証する構成

今回対象とするRHEL環境では、ホスト側の標準コンテナエンジンとして Podman を利用します。 ただし、K3sがホスト側のPodmanをKubernetesのCRI(Container Runtime Interface)として直接操作するわけではありません。

具体的なレイヤー構造は以下のようになります。

RHEL(Host OS)
└── systemd
    └── rootful Podman
        └── 特権付きK3sコンテナ (--privileged)
            ├── K3s server
            ├── kubelet
            └── 組み込み containerd(KubernetesのCRI)

図1. RHEL・Podman・K3s検証環境の構成

図1. RHEL・Podman・K3s検証環境の構成

つまり、外側の K3sコンテナ全体をPodmanで管理 し、K3s内で動作する各Podの実行には K3sに同梱されている組み込み containerd を使用します。

今回の構成ではK3sコンテナ内でcontainerd、ネットワーク、cgroupを扱うため、K3s公式のDocker実行例と同様に --privileged を付け、rootful Podmanで起動します。これは強い権限を与える構成なので、汎用的なアプリケーション分離や強固なセキュリティ境界を目的としたものではありません。

Ansibleで検証環境を構築する

手動でコマンドを入力してコンテナを起動することも可能ですが、再現性と保守性を確保するため Ansible ロールを作成して自動化しました。

1. インベントリの設定

今回の構築では、対象のRHELサーバー自身にログインし、Ansible Playbookを直接実行(ローカル実行)しています。 インベントリファイル(hosts.yml)には local グループを定義し、接続方式を local に設定しています。

all:
  children:
    local:
      hosts:
        localhost:
      vars:
        ansible_connection: local
        ansible_python_interpreter: /usr/bin/python3

※ この設定は「Playbookを実行しているマシン自身」が対象となるため、手元の作業PCで間違えて実行しないよう、必ずRHELサーバー側で実行します。

2. K3s構築用 Playbook とディレクトリ構成

メインのPlaybook (k3s.yml) とロール構造は以下のようになっています。

k3s.yml

---
- name: Run k3s on localhost with Podman
  hosts: local
  become: true
  gather_facts: true
  roles:
    - k3s_podman

ロールのディレクトリ構成

roles/k3s_podman/
├── defaults/main.yml
├── handlers/main.yml
├── tasks/main.yml
└── templates/
    ├── k3s-podman.env.j2
    └── k3s-podman.service.j2

3. デフォルト変数 (defaults/main.yml)

ロールで定義している主要な変数は以下の通りです。

k3s_podman_container_name: k3s
k3s_podman_image: docker.io/rancher/k3s:v1.36.1-k3s1
k3s_podman_service_name: k3s-podman
k3s_podman_node_name: "{{ inventory_hostname }}"

k3s_podman_data_dir: /var/lib/rancher/k3s
k3s_podman_config_dir: /etc/rancher/k3s
k3s_podman_env_file: /etc/sysconfig/k3s-podman
k3s_podman_kubeconfig: /etc/rancher/k3s/k3s.yaml

k3s_podman_token: ""
k3s_podman_tls_sans: []
k3s_podman_extra_server_args: []

k3s_podman_configure_firewalld: true
k3s_podman_api_port: 6443
k3s_podman_pod_cidr: 10.42.0.0/16
k3s_podman_service_cidr: 10.43.0.0/16

バージョンを latest タグのままにせず v1.36.1-k3s1 のように明示して固定することで、予期せぬアップデートを防ぎ、環境の再現性を維持しています。

4. ホームラボ固有変数の定義 (group_vars/local.yml)

LAN内の作業PCからKubernetes API(6443/tcp)へTLS接続できるよう、サーバーのLAN内IPアドレスを証明書の SAN (Subject Alternative Name) に追加します。 また、検証の初回起動ではNode名が localhost になったため、再検証時には homelab-k3s を明示しました。

k3s_podman_tls_sans:
  - 192.168.1.xxx  # ご自身のサーバーIPを指定
k3s_podman_node_name: homelab-k3s

5. ロールが自動実行する一連の処理

k3s_podman ロールを実行すると、タスク(tasks/main.yml)によって以下の処理が順次実行されます。

  1. OS検証: 対象OSが Red Hat 系であることを確認
  2. パッケージの準備: Podman のインストールと準備
  3. ディレクトリ生成: 永続化用データ領域(/var/lib/rancher/k3s)および設定領域(/etc/rancher/k3s)を作成
  4. 環境変数ファイルの配置: クラスタートークンなどを保持する /etc/sysconfig/k3s-podman をパーミッション 0600 で配置
  5. systemdユニットの配置: Podmanコンテナを管理・起動するためのsystemdユニットファイルを配置
  6. firewalld の設定: Kubernetes API 用ポート(6443/tcp)の許可
  7. ネットワークゾーン登録: Pod CIDR (10.42.0.0/16) と Service CIDR (10.43.0.0/16) を trusted ゾーンへ登録
  8. サービス起動: k3s-podman.service の有効化 (enable) と起動 (start)
  9. ヘルスチェック待機: APIポートの待ち受けとkubeconfigファイルの出力完了を待機
  10. Ready状態確認: コンテナ内で kubectl get --raw=/readyz を実行し API の健全性を検証

systemd と Podman の連携設定

Podmanコンテナをホスト起動時に自動開始させるため、systemdユニットを作成します。

以下はテンプレートから生成されるsystemdユニット(/etc/systemd/system/k3s-podman.service)のコア部分です。

[Service]
Type=simple
Restart=on-failure
RestartSec=5s
TimeoutStartSec=0
TimeoutStopSec=90s
Delegate=yes
TasksMax=infinity
LimitNOFILE=1048576

ExecStartPre=-/usr/bin/podman rm -f k3s
ExecStart=/usr/bin/podman run \
  --name k3s \
  --hostname homelab-k3s \
  --privileged \
  --network host \
  --cgroupns=host \
  --volume /var/lib/rancher/k3s:/var/lib/rancher/k3s:Z \
  --volume /etc/rancher/k3s:/etc/rancher/k3s:Z \
  --volume /lib/modules:/lib/modules:ro \
  --env-file /etc/sysconfig/k3s-podman \
  --pull=missing \
  docker.io/rancher/k3s:v1.36.1-k3s1 \
  server \
  --node-name=homelab-k3s \
  --write-kubeconfig-mode=0640 \
  --tls-san=192.168.1.xxx
ExecStop=/usr/bin/podman stop -t 90 k3s
ExecStopPost=-/usr/bin/podman rm -f k3s

設定の重要ポイント

  • --privileged: K3s内部でcontainerdやネットワークを動かすため、コンテナへ広い権限を与えます。
  • --network host: K3sのネットワーク空間をホスト側と共有し、ポートフォワーディングの手間をなくします。
  • --cgroupns=host & Delegate=yes: コンテナ内からの cgroup リソース制御を正しく許可します。
  • :Z オプション(ボリュームマウント): SELinux環境で、このコンテナ専用のラベルをホストディレクトリへ付与します。
  • /lib/modules のマウント: コンテナからホストのカーネルモジュールを参照できるようにします。必要なモジュール自体はホスト側に用意されている必要があります。
  • --node-name & --hostname: コンテナを削除・再作成した場合でも、Kubernetesへ同じNode名で登録されるよう明示します。
  • ExecStartPre / ExecStopPost: 停止済みの同名コンテナが次回起動を妨げないよう削除します。K3sの状態はホストへマウントしたディレクトリに残します。

Playbookを実行して結果を確認する

Gitリポジトリから最新コードを取得し、RHELサーバー上で Playbook を実行します。

cd ~/ansible-playbooks
git pull --ff-only
ansible-playbook k3s.yml

実行後、コンテナとサービスの稼働状態を確認します。

# OS、SELinux、firewalldの状態を記録
cat /etc/redhat-release
getenforce
sudo firewall-cmd --state

# systemd サービスの状態確認
sudo systemctl status k3s-podman --no-pager

# Podman コンテナ一覧の確認
sudo podman ps

# コンテナ内部の kubectl を使って Node を確認
sudo podman exec k3s kubectl get nodes

初回検証では、Nodeが次のように Ready になりました。ただし、Node名は意図した homelab-k3s ではなく、Ansibleのインベントリ名を引き継いだ localhost でした。この点は後半で再検証します。

NAME        STATUS   ROLES           AGE   VERSION
localhost   Ready    control-plane   6s    v1.36.1+k3s1

初回起動時の ContainerCreating 状態について

起動直後に kubectl get pods -A を実行すると、システム系 Pod(CoreDNS や Traefik 等)が ContainerCreating のまま並ぶことがあります。

NAMESPACE     NAME                                      READY   STATUS
kube-system   coredns-...                               0/1     ContainerCreating
kube-system   helm-install-traefik-...                  0/1     ContainerCreating
kube-system   helm-install-traefik-crd-...              0/1     ContainerCreating
kube-system   local-path-provisioner-...                0/1     ContainerCreating
kube-system   metrics-server-...                        0/1     ContainerCreating

今回の検証では、内部containerdによるイメージ取得が完了すると、各Podは Running または Completed へ遷移しました。起動時間は回線速度やレジストリの状態にも左右されるため、まずウォッチコマンドで変化を確認します。

sudo podman exec k3s kubectl get pods -A -w

長時間進まない場合や ImagePullBackOffFailedMount などへ変化した場合は、以下のコマンドでイベントや詳細ログを確認します。

# イベントログの確認
sudo podman exec k3s kubectl get events -A --sort-by=.lastTimestamp

# 特定 Pod の詳細情報取得
sudo podman exec k3s kubectl describe pod -n kube-system <POD_NAME>

# systemd / コンテナのログ確認
sudo journalctl -u k3s-podman -n 200 --no-pager

主に FailedMountFailedCreatePodSandBox、SELinuxのアクセス拒否、cgroup関連のエラーが出ていないかを確認します。

手元のPCから kubectl で接続する

K3sが生成した管理者用 kubeconfig は、RHELサーバーの /etc/rancher/k3s/k3s.yaml に保存されています。これを手元の作業PCに取得してリモート操作を可能にします。

1. kubeconfig の安全な転送

サーバー側で元ファイルの所有権を変更せず、一時的にユーザー権限でアクセスできる作業ディレクトリにコピーします。

# RHELサーバー上で実行
sudo install -m 0600 -o ansible -g ansible \
  /etc/rancher/k3s/k3s.yaml \
  /tmp/k3s.yaml

ここで指定している ansible は今回使用したSSHユーザー名です。実際の環境に合わせて、ユーザー名とグループ名を読み替えてください。

手元の作業PCから SCP を使って取得します。

# 手元の作業PC上で実行
mkdir -p ~/.kube

scp ansible@192.168.1.xxx:/tmp/k3s.yaml ~/.kube/homelab-k3s.yaml

コピー完了後、サーバー側の一時ファイルは削除しておきます。

# RHELサーバー上で実行
rm /tmp/k3s.yaml

2. kubeconfig の接続先設定とパーミッション変更

デフォルトの kubeconfig では、API サーバーの接続先が 127.0.0.1(ループバックアドレス)になっています。

server: https://127.0.0.1:6443

手元の作業PCから直接接続できるように、RHELサーバーのLAN内IPアドレス(192.168.1.xxx)に置き換えます。

# 手元の作業PC上で実行
sed -i.bak \
  's|https://127\.0\.0\.1:6443|https://192.168.1.xxx:6443|' \
  ~/.kube/homelab-k3s.yaml

# 所有者以外アクセスできないようパーミッションを0600にする
chmod 600 ~/.kube/homelab-k3s.yaml
chmod 600 ~/.kube/homelab-k3s.yaml.bak
セキュリティ上の注意
kubeconfig にはクラスターの最高管理者権限(system:admin)の証明書と秘密鍵が含まれています。誤ってGitなどに公開しないよう注意し、ファイルパーミッションは必ず 0600 に設定してください。

また、K3sは起動時にサーバー上の管理者証明書を更新します。手元へコピーしたkubeconfigは自動更新されないため、証明書更新後は再取得が必要です。

3. 手元PCからの動作確認

設定完了後、--kubeconfig フラグを指定して接続確認を行います。

kubectl --kubeconfig ~/.kube/homelab-k3s.yaml get nodes

環境変数 KUBECONFIG に設定しておくと、フラグなしで操作できます。

export KUBECONFIG="$HOME/.kube/homelab-k3s.yaml"
kubectl get nodes

~/.zshrc~/.bashrc に追記しておくと次回以降もスムーズにアクセスできます。

echo 'export KUBECONFIG="$HOME/.kube/homelab-k3s.yaml"' >> ~/.zshrc
source ~/.zshrc

追加検証:Node名をlocalhostから変更する

構築直後、Kubernetes の Node 名が Ansible インベントリ名(localhost)を引き継いで登録されてしまうケースがあります。

そこで、Node名を homelab-k3s に変えて再起動した場合の挙動も確認しました。結果として、設定を書き換えてPlaybookを再実行するだけでは、古いNodeオブジェクトが残りました

Node名切り替えの手順

  1. Ansible変数で Node 名を定義します。

    k3s_podman_node_name: homelab-k3s
    
  2. Playbook を再実行して k3s-podman.service を更新・再起動します。

    ansible-playbook k3s.yml
    
  3. 今回の環境では、古い localhost と新しい homelab-k3s2つのNodeが一時的に併存 しました。

    NAME          STATUS     ROLES           VERSION
    homelab-k3s   NotReady   control-plane   v1.36.1+k3s1
    localhost     NotReady   control-plane   v1.36.1+k3s1
    
  4. 新しい Node (homelab-k3s) が Ready になるまで監視します。

    watch -n 5 'sudo podman exec k3s kubectl get nodes'
    
  5. 新しい Node が Ready になったことを確認したら、残ってしまった旧 Node を手動で削除します。

    sudo podman exec k3s kubectl delete node localhost
    

最終的に意図した Node 名のみが登録された状態になります。

NAME          STATUS   ROLES           AGE    VERSION
homelab-k3s   Ready    control-plane   111s   v1.36.1+k3s1

今回の変更ではAPIエンドポイントとTLS SANを変えていないため、手元のPCに配布したkubeconfigはそのまま利用できました。なお、ワークロードを載せた後のNode名変更は、Podの再配置やローカルストレージへの影響を伴う可能性があります。構築時点でNode名を確定しておく方が安全です。

検証結果

今回の環境では、次の結果になりました。

確認項目結果確認方法
Kubernetes APIの起動成功/readyz が成功
Nodeの登録成功kubectl get nodesReady
システムPodの起動成功CoreDNS、Traefikなどが Running / Completed
systemdからの管理成功systemctl status k3s-podman で稼働を確認
LAN内PCからの操作成功配布したkubeconfigで kubectl get nodes を実行
Node名の初期設定要修正初回は localhost で登録された

運用時によく使うコマンド集

日常的な運用・トラブルシューティングでよく利用するコマンドをまとめました。

RHELサーバー上での操作

# Node 一覧の確認
sudo podman exec k3s kubectl get nodes

# 全 Pod の詳細状態取得
sudo podman exec k3s kubectl get pods -A -o wide

# イベントログの時系列確認
sudo podman exec k3s kubectl get events -A --sort-by=.lastTimestamp

# systemd サービス状態確認
sudo systemctl status k3s-podman --no-pager

# K3s コンテナのリアルタイムログ出力
sudo journalctl -u k3s-podman -f

手元の作業PC上での操作

# Node 状態の確認
kubectl get nodes

# 全 Pod 状態の確認
kubectl get pods -A

# クラスタ情報の確認
kubectl cluster-info

セキュリティ・運用上の考慮点

  • 特権コンテナのリスク: --privileged はホストへ強い権限を与えます。信頼できるイメージだけを使い、用途を検証環境に限定します。
  • バージョン固定: イメージタグは latest を使用せず、特定のバージョンを明示して運用します。
  • 機密情報の管理: クラスタートークンや秘密鍵などの機密情報をGitにコミットしないよう徹底し、必要に応じてAnsible Vaultなどで暗号化します。
  • パーミッションの制御: kubeconfig や環境変数定義ファイルのパーミッションは 0600 に設定します。
  • 一時ファイルの削除: SCP用に作成した /tmp/k3s.yaml などの一時ファイルは作業後即座に削除します。
  • ネットワークアクセス制御: Kubernetes API (6443/tcp) へアクセスできる送信元 IP アドレスを firewalld やルーター側で必要に応じて制限します。

構築してみた感想

今回、K3sそのものだけでなく、Podman、systemd、cgroup、SELinux、firewalld、TLS証明書などを一つずつ設定してみて、改めてマネージドKubernetesの便利さを実感しました。

GKE、AKS、Amazon EKSであれば、もちろんネットワークや権限、Node構成などを考える必要はありますが、管理コンソールから項目を選んでいくだけでクラスターの作成を始められます。自前で構築してみた後だと、あの「画面からポチポチ」でコントロールプレーンが用意される体験は、かなり楽です。

その分、利用するクラウドリソースに応じて料金がかかるため、気軽なホームラボ用途ではコストが悩ましいところです。自前構築は手間がかかりますが、その手間も含めて仕組みを理解できるのがホームラボの面白さだと感じました。

まとめ

今回の範囲では、RHEL上の rootful Podman × systemd × Ansible という構成で、シングルノードK3sを起動できました。NodeとシステムPodが正常状態になり、LAN内の作業PCからも kubectl で操作できたため、ホームラボの検証基盤として使えることを確認できました。

K3sのデータと設定をホストへマウントしているため、コンテナのライフサイクルとクラスターデータを分離できる点も確認できました。一方で、--privileged が必要なこと、管理者kubeconfigの取り扱い、Node名を後から変更した際に旧Nodeが残ることには注意が必要です。

kubectl --kubeconfig ~/.kube/homelab-k3s.yaml get nodes
NAME          STATUS   ROLES           VERSION
homelab-k3s   Ready    control-plane   v1.36.1+k3s1

これで、ホームラボ上にKubernetesを試すための土台ができました。ただ、クラスターを作っただけでは、アプリケーションのデプロイやマニフェストの変更管理はまだ手作業です。次はGitリポジトリを正しい状態の基準とし、その内容をクラスターへ継続的に反映するGitOpsを試してみます。

次回は「ホームラボでArgo CDを触ってみよう。K3sで学ぶGitOps」として、今回構築したK3sへArgo CDを導入し、Git上のマニフェストをクラスターへ反映するところまで検証します。

参考資料

K3s公式ドキュメント

minikube公式ドキュメント

関連記事

@urchin_hat
Written by
@urchin_hat

とある領域のSaaSサービスでSREとして活動中。現在はインフラの正常化とAIを活用した運用効率化(AIOps)に注力しています。

Back to Memo