
ホームラボでK3sを構築しよう!RHEL・Podman・Ansibleで作るKubernetes環境
はじめに
自宅のホームラボでアプリケーションやコンテナ運用を試すため、Kubernetesの検証環境が欲しくなりました。今回は手元にあるRHELサーバーでシングルノード構成を試します。また、今回の環境とは別に、将来は複数台のRaspberry Piを使った物理クラスターも構築したいと考えています。
将来イメージしているのは、単にKubernetesを一時的に動かす環境ではありません。レバテックLABの「なぜ自宅に Kubernetes クラスタを持つのか —— 本番相当に運用して得られる学び(前編)」で紹介されているように、複数台のRaspberry Piで独立したクラスターを組み、監視、障害対応、アップデート、GitOpsなどを継続的に試せる環境です。台数やストレージ、ネットワークの具体的な構成はこれから検討しますが、「作って終わり」ではなく、長く運用しながら育てるホームラボを目指しています。
なぜK3sを選んだのか
候補として比較したのが K3s と minikube です。
minikubeは、PC上にローカルKubernetes環境を素早く用意できるため、Kubernetesの学習やアプリケーションの動作確認に向いています。複数Node構成も作成できますが、公式にはローカルでの開発・学習を主な用途としており、リモートネットワークへ公開する使い方は推奨されていません。
一方、K3sはエッジ、ホームラボ、IoT、ARMのシングルボードコンピューターを想定した軽量なKubernetesディストリビューションです。x86_64に加えてarmhfとarm64/aarch64をサポートしているため、今回のRHEL環境で構築や運用の基本を確認しておけば、将来別途構築するRaspberry Piクラスターにも知識を生かせると判断しました。
| 比較項目 | K3s | minikube |
|---|---|---|
| 主な用途 | ホームラボ、エッジ、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コンテナ上でどこまで動かせるかを検証します。
本記事では、K3s を RHEL(Red Hat Enterprise Linux) 上のPodmanコンテナとして構築し、その結果を確認します。
今回確認したいのは、次の4点です。
- rootful Podman上でK3sのコントロールプレーンとワークロードを起動できるか
- Ansibleで同じ構成を再現し、systemdから自動起動できるか
- firewalldとSELinuxを有効にしたRHEL上でPodネットワークが動作するか
- LAN内の作業PCからKubernetes APIへ接続できるか
以降では、実際に使った構成と確認結果、検証中に遭遇した問題を順に記録します。
検証環境と確認項目
今回の検証環境は以下の通りです。
| 項目 | 設定値 / 環境 |
|---|---|
| Kubernetesディストリビューション | K3s v1.36.1+k3s1 |
| ホストOS | Red 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 を実行できることとしました。
今回使用した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検証環境の構成
つまり、外側の 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)によって以下の処理が順次実行されます。
- OS検証: 対象OSが Red Hat 系であることを確認
- パッケージの準備: Podman のインストールと準備
- ディレクトリ生成: 永続化用データ領域(
/var/lib/rancher/k3s)および設定領域(/etc/rancher/k3s)を作成 - 環境変数ファイルの配置: クラスタートークンなどを保持する
/etc/sysconfig/k3s-podmanをパーミッション0600で配置 - systemdユニットの配置: Podmanコンテナを管理・起動するためのsystemdユニットファイルを配置
- firewalld の設定: Kubernetes API 用ポート(
6443/tcp)の許可 - ネットワークゾーン登録: Pod CIDR (
10.42.0.0/16) と Service CIDR (10.43.0.0/16) をtrustedゾーンへ登録 - サービス起動:
k3s-podman.serviceの有効化 (enable) と起動 (start) - ヘルスチェック待機: APIポートの待ち受けとkubeconfigファイルの出力完了を待機
- 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
長時間進まない場合や ImagePullBackOff、FailedMount などへ変化した場合は、以下のコマンドでイベントや詳細ログを確認します。
# イベントログの確認
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
主に FailedMount や FailedCreatePodSandBox、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名切り替えの手順
Ansible変数で Node 名を定義します。
k3s_podman_node_name: homelab-k3sPlaybook を再実行して
k3s-podman.serviceを更新・再起動します。ansible-playbook k3s.yml今回の環境では、古い
localhostと新しいhomelab-k3sの 2つのNodeが一時的に併存 しました。NAME STATUS ROLES VERSION homelab-k3s NotReady control-plane v1.36.1+k3s1 localhost NotReady control-plane v1.36.1+k3s1新しい Node (
homelab-k3s) がReadyになるまで監視します。watch -n 5 'sudo podman exec k3s kubectl get nodes'新しい 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 nodes で Ready |
| システム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上のマニフェストをクラスターへ反映するところまで検証します。
