Zenn
😊

Ansible で作るおうちラボ HA Kubernetes クラスタ

2025/03/05に公開

イントロ

私はたびたびおうちラボを作っては全部真っ新にして作り直すということをしています。直近の環境の変化として、ハイパーバイザーとして動かせる程度の強力なマシンが仲間入りしましたので、以下を課題として新たにおうちラボを構築しなおすことにしました。

  • サーバ
    • より多くのVMを立てて多様なLinuxディストロを試す
  • 構築・運用
    • AnsibleやGitLab CI/CDで手作業を減らす
  • Kubernetesクラスタのデザイン
    • 外部Etcdトポロジー採用
    • Control planeをkeepalivedとhaproxyで冗長化
    • Calicoの代わりとしてCiliumを試す
      • LoadbalancerおよびL2広報機能に関して、MetalLBをお役御免に
      • Gateway API実装に関して、NGINX fabric gatewayをお役御免に
    • Volume provisioningに関して、MinIO DirectPVの代わりとしてLonghornを試す
    • データベースに関連し、Bytebaseの代わりとしてPercona Everestを試す

この内、今回はAnsibleを用いたKubernetesクラスタの構築と、CiliumのL2AnnouncementのデモとしてCiliumのHubble UIへのアクセスを用意するところまでを記事にしたので、よろしければご覧ください。

本記事でカバーしている点

本記事で触れる内容について、次の通りです:

  • サーバをAnsible管理用にオンボードさせる
  • ちょっとしたAnsibleの紹介としてgather_factsの実行や、単純なパッケージアップグレードや再起動をAnsible playbookとして実行
  • Dockerで動かすDNSサーバをAnsibleで用意
  • Kubernetesクラスタへの参加ノードとしての必要事前準備をAnsibleで実行
  • Etcdクラスタ構築、Kubernetesクラスタ構築をAnsibleで実行
  • Ciliumのインストール
  • Ciliumのデモ

このプロジェクトの公開レポジトリはこちらに用意してあります: https://github.com/pkkudo/homelab-v3

本記事でカバーしていない点

サーバを立てるところはスキップしています。本記事はすでに稼働しているサーバがある状態から始めていきます。

実際の構築とは別に、本記事用にテスト環境をProxmoxとHyper-Vで用意したのですが、通常のインストーラーからOSインストールが完了したところから、あるいはCloud-initイメージからVMを立ち上げたところから本記事の作業手順が進んでいきます。

また今回、RedHat Enterprise Linuxもサーバリストに含めていますが、これはパッケージレポジトリが利用できる状態までしてから (rhc connectだけでしょうか) 本記事でカバーしている作業を進めています。

Kubernetesクラスタのデザインの説明

まず、初めて私が構築したKubernetesクラスタと、続いて今回構築するKubernetesクラスタのデザインについて触れていきます。

ベーシックなKubernetesクラスタ

最初に構築したKubernetesクラスタはx1 Control plane、x2 Workersの、三つのノードからなっているものでした。サーバを三台、Kubernetesの要件に合うよう設定変更し、パッケージをインストールし、control planeでクラスタ構築、worker二台をクラスタに参加させるといった手順で構築しました。

Control planeノードはオペレータやKubernetesのコンポーネント自身らがクラスタとやり取りするための唯一の宛先です。

クラスタ情報のメモリストアであるetcdも同じControl plane上で稼働している重要サービスです。

今回構築するKubernetesクラスタ

今回はせっかくたくさんのVMを立ち上げられるようになったので外部etcdクラスタ用に三台、KubernetesクラスタのControl planeとして三台、そしてWorkerとして一台用意することにしました。

EtcdサービスはこれでControl planeとは独立して存在することとなり、Control Planeから見ると利用できるEtcdサービスが冗長化されていることとなります。

Control Plane自体も冗長化されます。

それぞれのControl Planeノードはkeepalivedサービスで高可用なVirtual IP Address (VIP)を設け、haproxyサービスでkube-apiserverへのロードバランサを設けます。

KeepalivedでVIPを設ける

Keepalivedの簡単な図です。Control Plane各ノード上のKeepalivedサービスはお互いに連携し、誰かしらがVIP (ここでは192.0.2.8)を担当するよう調整します。

Haproxyでロードバランサを設ける

Haproxyの図です。Control Plane各ノード上のHaproxyサービスは8443ポートで通信を受け付け、生きているどれかのkube-apiserverへ通信を流します。

Kube-apiserverはControl Plane上でデフォルトでは6443ポートでサービス応答している、Control Planeに対するメインコンタクトポイントです。今回構築するKubernetesクラスタではクラスタエンドポイント、クラスタへの通信先をVIP:8443とし (192.0.2.8:8443)、その通信はその時点でVIPを担当しているControl Plane上のHaproxyサーバが受け取り、そしてHaproxyはその時点でヘルスチェックに合格しているkube-apiserverへ通信を流します。

サーバリストとIPアドレス

以下が本記事で登場するサーバ一覧です。

記事上およびレポジトリ上ではドキュメント用の192.0.2.0/24サブネットを用いていますが、実際は普通のプライベートIPアドレスレンジを使っています。

ドメインについても同様で、今回はlab.example.netとしてあります。

そしてディストロはDebian, Ubuntu, RHEL, Rocky, OracleLinuxなどいろいろ混ざっていますが、今回用意してあるAnsible playbookは少なくともこれらのOSでは通るように作られています。

hostname ipaddr role os cpu memory disk hypervisor
lab-cp1 192.0.2.1 kubernetes control plane debian 4 4GB 64GB hyper-v
lab-cp2 192.0.2.2 kubernetes control plane rocky 4 4GB 64GB proxmox
lab-cp3 192.0.2.3 kubernetes control plane ubuntu 4 4GB 64GB proxmox
lab-worker1 192.0.2.4 kubernetes worker node rhel 4 4GB 64GB hyper-v
lab-etcd1 192.0.2.5 etcd node debian 2 4GB 64GB hyper-v
lab-etcd2 192.0.2.6 etcd node debian 2 4GB 64GB proxmox
lab-etcd3 192.0.2.7 etcd node oracle 2 4GB 64GB proxmox
lab-ns1 192.0.2.16 run dns server using docker rhel 1 1GB 32GB proxmox
lab-ns2 192.0.2.17 run dns server using docker debian 1 2GB 10GB hyper-v

サーバ以外に少しだけIPアドレスが登場します。

  • VIP、192.0.2.8
    • エンドポイントはlab-kube-endpoint.lab.example.net:8443となる
      • DNSで名前解決され、192.0.2.8となる
      • 192.0.2.8:8443への通信はkeepalived + haproxyでいずれかのControl Planeで受け取られ、いずれかのControl Plane上でデフォルトでは6443ポートへと流れる
  • 192.0.2.9、本記事最後の方で取り上げるHubble UIへのアクセス、CiliumのL2Announcementで使われる

プロジェクトの準備

プロジェクトの準備を進めましょう。大まかな作業ステップは次の通りです:

  • レポジトリをgit cloneする
  • Pythonを用意し、Ansibleをインストールする
  • Ansible collectionをインストールする
  • 今回用意したAnsibleのコンフィグについて説明
  • Ansibleのインベントリ、変数、Ansible Vaultを用いた暗号化について説明
  • Ansibleマスターのための名前解決手段をhostsファイルないしDNSで用意
  • Ansibleユーザで用いるSSHキーペアの用意
  • Ansibleインベントリ、変数などの見直し、更新必要部分の対応
    • kube-endpoint用のVIP
    • nameserversリスト
    • DNSレコード用のテンプレートファイルなどなど

レポジトリ

公開されているこちらのレポジトリをクローンしてきてください。

git clone -b zenn-post https://github.com/pkkudo/homelab-v3
cd homelab-v3

# optionally, set your own origin
git remote remove origin
git remote add origin {your_repo}

Ansibleのインストール

MiseやPoetryといったツールで用意するのもよいですが、以下の手順でインストールできる最新のもので差し支えないはずです。

sudo apt install python3 python3-venv
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip ansible
ansible-galaxy collection install -r requirements.yml

実際に私が本手順を通しでやり直した際にはcloud-init Debian 12イメージでやったのですが、その際はmiseをインストールしてpython 3.12を用意し、poetryをインストールしてpoetry経由でAnsibleをインストールする、といった以下の手順を踏んでいます。

# install mise
curl https://mise.run | sh
echo "eval \"\$(/home/YOUR_USERNAME_HERE/.local/bin/mise activate bash)\"" >> ~/.bashrc

# re-logon and come back to the cloned homelab-v3 directory
# and have mise trust .mise.toml file in this project directory
mise trust
# install python version as described in .mise.toml file
mise up

# create venv at .venv and load the environment
python -m venv ~/homelab-v3/.venv
source .venv/bin/activate

# enable mise experimental feature to have mise automatically activate python venv
mise settings experimental=true

# get latest pip, install poetry, and install ansible using poetry
pip install -U pip
pip install poetry

# install ansible as defined in pyproject.toml file
poetry install --no-root

# install ansible collections
ansible-galaxy collection install -r requirements.yml

もしsshpassがなければ

少し後の手順となりますが、もしAnsible playbook実行時に以下のようなエラーが出た場合はsshpassをインストールするなどして解消できます (sudo apt install sshpassなど)。

lab-etcd1 | FAILED! => {
    "msg": "to use the 'ssh' connection type with passwords or pkcs11_provider, you must install the sshpass program"
}

Ansibleのコンフィグファイル

本プロジェクトで用意したAnsibleコンフィグについてです。

[genera]セクション:

  • inventoryはデフォルトで使うインベントリファイルの指定
  • vault_password_fileはデフォルトで使うAnsible Vault用パスワードファイルの指定
  • roles_pathはAnsible Rolesのパス指定
  • collections_pathも同様にAnsibleのCollectionsのパス指定

[ssh_connection]セクション:

  • ssh_argsはリモートホストへsshアクセスする際に用いる引数の設定で、セキュアではないがおうちラボ規模で便利に使うには良い設定

本来もっと膨大な数のコンフィグが用意されていますが、それに関しては公式を参照してください。すべてのコンフィグを含んだサンプルコンフィグファイルが簡単に生成できます。

https://docs.ansible.com/ansible/latest/reference_appendices/config.html#generating-a-sample-ansible-cfg-file

Ansibleのインベントリ、変数、暗号化など

インベントリファイルはAnsibleで扱うホストのリストが含まれています。複数のホストをグループ化することもできます。Ansibleのタスク実行時、対象をホストおよびグループで指定できます。

# list "all" hosts defined in the inventory file at ./inventory/hosts.yml
ansible -i inventory/hosts.yml all --list-hosts

# since the default inventory file is set in the ansible config file, you can omit -i option to specify which inventory file to use
ansible all --list-hosts

# confirm hosts in different groups
ansible lab --list-hosts
ansible lab_kubernetes --list-hosts
ansible lab_k8s_cp --list-hosts
ansible lab_k8s_worker --list-hosts
ansible lab_etcd --list-hosts

各グループごとおよびホスト単位でも、変数を設定することができます。例として./inventory/group_vars/labをみてください。ここで指定された変数はlabグループのホスト全てに適用されます。

例えばdomain_suffixlab.example.netと設定されており、ansible_host"インベントリホスト名".domain_suffixと設定されています。すると例えばlab-cp1ホストの場合、そのansible_hostの値はlab-cp1.lab.example.netとなります。

# ansible remote access
ansible_user: "{{ vault_ansible_user }}"
ansible_username: "{{ vault_ansible_user }}" # required by bootstrap playbook
ansible_ssh_private_key_file: "{{ playbook_dir }}/files/ssh/id_ed25519_ansible"
ansible_ssh_pubkey: "{{ playbook_dir }}/files/ssh/id_ed25519_ansible.pub"
domain_suffix: "lab.example.net"
ansible_host: "{{ inventory_hostname_short }}.{{ domain_suffix }}"

そして、ansible_userはまた別の変数、vault_ansible_userを指しています。ansible_usernameも同様です。これはAnsibleプロジェクトにおいて重要な情報を暗号化して保持する方法の一つです。./inventory/group_vars/lab/vars.ymlファイル上で、これらの変数が設定されていることが分かりやすいようここに記載しており、更にその実際の内容はvault_*と銘打った別の暗号化されたファイルにあることを明示しています。

本プロジェクトではAnsible Vaultのパスワードは./.vault_passというファイルを用いるよう設定ファイルにも書いており、(当然実際はパブリックな場所に出すべきではないですが)パスワードファイルもレポジトリに含めてあるので、以下の暗号化された出力、複合化された出力が同じようにみられると思います。

$ cat inventory/group_vars/lab/vault.yml
$ANSIBLE_VAULT;1.1;AES256
30656633313135653464373834663137646662633137376564653864653565333661333339616661
6133336638316131643831333033353334366435613062610a373234333434313734333333346164
35353339343862666466653233353961663661363762353939616662356237626163323634363630
3132653733616464390a316332316332663331656236386331656630623364343061393835393038
63623466383437633162353533373036623038346433616361363839643433343563636466363363
34313264623833626363363834393837366465666534623465626237383532623939356664353261
373531613066666539643962393862323761

$ ansible-vault view inventory/group_vars/lab/vault.yml
# ansible remote access
vault_ansible_user: "ansible-hlv3"

Ansibleマスターのための名前解決の用意について

Ansibleマスターはタスク実行のために、リモートホストへsshアクセスする必要があります。ただ、インベントリファイルを見ると記載されているのはlab-cp1やlab-worker1などのホスト名だけです。実際にAnsibleが宛先として用いるのはansible_hostであり、この値はlabグループの変数ファイルで小細工することによりlab-cp1の場合はlab-cp1.lab.example.netとなり、lab-worker1の場合はlab-worker1.lab.example.netとなるようにしています。

名前解決で悩みたくないならば、一応labグループ変数ファイルからansible_hostの行を削除し、インベントリファイルにIPアドレスベタ打ちといったやり方でもいけるとは思います。

lab:
  children:
    lab_kubernetes:
      children:
        lab_k8s_cp:
          hosts:
            192.0.2.1:  # instead of lab-cp1
        lab_k8s_worker:
          hosts:
            192.0.2.4:  # instead of lab-worker1

ただ本記事では、ひとまずの対応策としてAnsibleマスターのhostsファイルの書き換え、そしてのちのステップでDNSサーバを設けることとします。

用意されている./inventory/hosts-list.txt.exampleを元にAnsibleインベントリのホストの一時的な名前解決用にhostsファイルを用意しましょう。自身の環境にあるサーバの実際のIPアドレスで用意すると良いです。

cp inventory/hosts-list.txt.example inventory/hosts-list.txt

# edit inventory/hosts-list.txt
# and set the actual IP addresses for the hosts

# append the hosts-list.txt lines to the /etc/hosts file
sudo tee -a /etc/hosts < inventory/hosts-list.txt

# test
ping lab-cp2
ping lab-cp3.lab.example.net.

ちなみに、もちろんドメイン名も好きに変更して本手順をなぞっていただけるので、labグループの変数ファイルなどでdomain_suffixの値を更新してください。

次に、準備の一環としてのちに用意するDNSサーバ用のレコードのテンプレートファイルも用意しておきましょう。フォーマットは先と同様、見たままかと思います。各行各ホスト用にIPアドレスを書き換えてください。

なおDNSサーバ用のこちらのファイルではホスト以外のレコードがあるのに気づくと思います。一つはlab-kube-endpoint.lab.example.netであり、これはControl PlaneノードらのVIP用です。もう一つはhubble-ui.lab.example.netであり、Ciliumのデモで使います。

cp roles/dns/templates/a-records.j2.example roles/dns/templates/a-records.j2

# edit this a-records.j2 file
# to match the actual environment
#
# for example, if your homelab subnet is 10.8.0.0/24, 
# some of your records might look as follows:
# local-data: "lab-cp1.{{ domain_suffix }}. IN A 10.8.0.20"
# local-data: "lab-cp2.{{ domain_suffix }}. IN A 10.8.0.21"

Ansibleユーザ用SSHキーペア

次です。Ansibleユーザ用にSSHキーペアを用意しましょう。ユーザ名はlabグループ変数ファイル(の暗号化されたvault.ymlファイルの方で指定されている)"ansible-hlv3"となります。

私のおうちラボバージョン3ということで仮にこのような名前にしていますが、ぜひ変更してください。変更される場合はせっかくですのでAnsible Vaultのキーも更新してみましょう。

手順としては:

  • ./.vault_passおよび./inventory/group_vars/lab/vault.ymlファイルを削除
  • 新たに./.vault_passファイルを用意
  • ansible-vault create inventory/group_vars/lab/vault.ymlで新たに暗号化された変数ファイルを作成し、vault_ansible_userをセットする

なお参考までに、私は以下のコマンドで./.vault_passファイルを用意しています。

# generating random 31 characters alpha-numerical string used as ansible vault password
tr -dc '[:alnum:]' < /dev/urandom | head -c 31 > .vault_pass

ユーザ名に関しては以上とし、SSHキーペアの話題に戻りましょう。ssh-keygenコマンドで生成できますので、新たなキーペアを./playbooks/files/ssh以下に用意しましょう。

元々用意したサーバで用いているものを設置するのでも全く問題ありません。ファイル名に関してはlabグループ変数ファイルで指定しているので、用意したファイル名と変数ファイルで指定しているファイル名が合致するようにしてください。

# prepare playbooks dir
# and files/ssh directory to place ssh key pair used by ansible master
mkdir -p playbooks/files/ssh
cd playbooks/files/ssh
ssh-keygen -t ed25519 -f id_ed25519_ansible

こうしてansible_hostansible_ssh_private_key_file変数および用意したファイルが組み合わさることによって、Ansibleマスターは例えばlab-cp1ホストにアクセスしに行く際、実質的にssh ansible-hlv3@lab-cp1.lab.example.net -i playbooks/files/ssh/id_ed25519_ansibleとしてアクセスすることになります。

そのほかに設定すべき変数について

実際にAnsibleを実行し始める前に、もういくつか変数を確認・更新する必要があります。

Nameservers

本プロジェクトではのちにlab-ns1とlab-ns2ホストがDNSサーバとして構築されます。これら2サーバのIPアドレスで./roles/dns/defaults/main.yml内で指定されているnameservers変数を更新してください。仮で["192.0.2.16", "192.0.2.17"]あたりが設定されています。

のちに実行するplaybookで、各ホストのnameserverをここで指定した宛先に書き替えることになります。

なお、例えばrolesではなくlabグループの変数ファイルなどでnameservers変数を設定するのでも大丈夫です。そしてのちのち、例えばdevグループなどが作られたとして、そのグループには別のnameserversの値をセットするといったこともできます。

デフォルトでは./roles/dns/defaults/main.ymlで指定されているもの、そしてグループごとにそれぞれの変数ファイルで上書き指定することもできる、とイメージして頂ければ良いです。

kube-endpoint用のVIP

kube-endpoint用のVIPを./roles/kubernetes/defaults/main.yml内で指定してください。デフォルトで192.0.2.8となっているので、ご自身の環境のサブネットよりVIPとして割り当てたい値をセットしてください。これは先にDNSサーバのテンプレートファイルで触れたlab-kube-endpoint.lab.example.net.と同じものです。

ちなみにこちらもnameserversと同様に、グループ変数ファイルなどの方で設定するのでも問題ありません。

kube_endpoint: "lab-kube-endpoint.{{ domain_suffix }}"
kube_endpoint_port: 8443
kube_endpoint_vip: 192.0.2.8  # CHANGE THIS

本プロジェクトで設けられている他の変数について

本プロジェクトではこれまで触れた以外にもあちこちで変数がセットされています。

それら全て説明していくより、一般的に気になるであろう、のちのち変更したいと思うであろう変数を以下にリストします:

  • version of kubernetes, cni, runc, containerd, etcd found in ./roles/kubernetes/defaults/main.yml
    • 1.32.2 for kubernetes
    • 2.0.2 for containerd
    • 1.2.5 for runc
    • 1.6.2 for cni
    • 3.5.18 for etcd
  • image and image tag for keepalived and haproxy found in ./roles/kubernetes/defaults/main.yml
    • osixia/keepalived:stable for keepalived last updated in 2020
      • 古い!?
      • 私の実環境では、自分でビルドし、プライベートのイメージレジストリに載せているバージョン2.3.2のイメージを使っています
      • これに関して、できたら今後別記事で紹介したいです
    • haproxy:3.1.5-alpine for haproxy
  • unbound DNS image and tag in ./roles/dns/templates/env.j2
    • mvance/unbound:1.21.1
  • docker version in ./roles/docker/defaults/main.yml
    • 27.5.1
  • ansible collections and their versions in ./requirements.yml

上記いずれも大本のサービスのURLをファイル内に記載してあるので、そちらから最新バージョンが確認できます。

最初のplaybook - bootstrap

準備作業がやっと終わりました。最初のplaybookを実行して、リモートホストに対するアクセスクレデンシャルを整理・更新しましょう。

実行するタスクは次の通りです:

  • ansible-adminグループの作成
  • ansible_useransible-adminグループのメンバーとして作成
  • sudoのインストールおよびansible-adminグループに対してパスワードレスsudo実行許可
  • ansible_userにsshアクセス許可用の公開鍵をセット

ちなみに私のテストランで用いたVMのうちcloud-initから用意したものは、作成時点ですでにユーザ、ssh公開鍵セットが済んでいるため、以下ではそれに応じた実行コマンドを例として記載しています。

# bootstrap on cloud-init VMs
ansible-playbook playbooks/bootstrap.yml --limit lab-cp2:lab-cp3:lab-etcd2:lab-etcd3:lab-ns1

# bootstrap on the other VMs
# specifying username and password
# if your existing username to use is happyansibleuser, "-e ansible_user=happyansibleuser"
# if ssh key logon is not setup for that user, you can omit "-e ansible_ssh_private_key_file=..."
# if no remote host is going to ask for logon password, you can omit "-k" option
# the capital "-K" option is always needed to enter privilege password to execute tasks that require sudo or su
ansible-playbook playbooks/bootstrap.yml --limit lab-cp1:lab-etcd1:lab-worker1:lab-ns2 -e ansible_user=$USER -e ansible_ssh_private_key_file=~/.ssh/id_ed25519 -k -K
# ansible will ask you for "-k" password and "-K" password

タスクが実施されると、指定のユーザ名およびSSHキーで対象ホストにアクセスできるようになっています。

$ ssh ansible-hlv3@lab-etcd3 -i playbooks/files/ssh/id_ed25519_ansible cat /etc/hostname /etc/os-release
lab-etcd3
NAME="Oracle Linux Server"
VERSION="9.5"
ID="ol"
ID_LIKE="fedora"
VARIANT="Server"
VARIANT_ID="server"
VERSION_ID="9.5"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Oracle Linux Server 9.5"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:oracle:linux:9:5:server"
HOME_URL="https://linux.oracle.com/"
BUG_REPORT_URL="https://github.com/oracle/oracle-linux"

ORACLE_BUGZILLA_PRODUCT="Oracle Linux 9"
ORACLE_BUGZILLA_PRODUCT_VERSION=9.5
ORACLE_SUPPORT_PRODUCT="Oracle Linux"
ORACLE_SUPPORT_PRODUCT_VERSION=9.5

$ ssh ansible-hlv3@lab-cp2 -i playbooks/files/ssh/id_ed25519_ansible id && cat /etc/hostname /etc/os-release
uid=1000(ansible-hlv3) gid=1000(ansible-hlv3) groups=1000(ansible-hlv3),1001(admin-ansible) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
crocus
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

また、例えば以下のコマンドでちょっとしたピンポンチェックが実施できます。

# targeting hosts in "lab" group
ansible lab -m ping -e ansible_ssh_private_key_file=playbooks/files/ssh/id_ed25519_ansible

Gathering Facts

次に、Ansibleのgather_factsタスクを走らせて、Ansibleが対象ホストについてどれだけの情報を確認しているのか見てみましょう。

用意されているplaybookを実行するとローカルにディレクトリとファイルが作られます。ファイルにはホストごとの情報が含まれています。それらのファイルを見てみると、例えばパッケージマネージャ、サービスマネジャ、OSファミリー、ディストロ情報やNIC情報など、他にも膨大な情報があることが分かります。

# run the playbook
ansible-playbook playbooks/gather_facts.yml

# facts gathered are stored locally in ./playbooks/facts/
ls -1 playbooks/facts

出力例:

$ grep -C2 family playbooks/facts/lab-etcd2.json
    ],
    "nodename": "lab-etcd2",
    "os_family": "Debian",
    "pkg_mgr": "apt",
    "proc_cmdline": {

$ grep distribution playbooks/facts/lab-etcd2.json
    "distribution": "Debian",
    "distribution_file_parsed": true,
    "distribution_file_path": "/etc/os-release",
    "distribution_file_variety": "Debian",
    "distribution_major_version": "12",
    "distribution_minor_version": "9",
    "distribution_release": "bookworm",
    "distribution_version": "12.9",

$ grep mgr playbooks/facts/lab-etcd2.json
    "pkg_mgr": "apt",
    "service_mgr": "systemd",

$ grep -A8 default_ipv4 playbooks/facts/lab-etcd2.json
    "default_ipv4": {
        "address": "REDACTED",
        "alias": "eth0",
        "broadcast": "REDACTED",
        "gateway": "REDACTED",
        "interface": "eth0",
        "macaddress": "bc:24:11:00:df:33",
        "mtu": 1500,
        "netmask": "255.255.255.0",

単純なパッケージアップグレードや再起動タスク

レポジトリ上には、単純なパッケージアップグレードを実行するplaybookが用意されています。

# run package upgrades, apt or dnf
ansible-playbook playbooks/pkg_upgrade.yml

再起動するplaybookもあります。対象ホストを一斉に再起動したい時に使えます。

# reboots hosts
ansible-playbook playbooks/reboot.yml

# reboot just specified targets
ansible-playbook playbooks/reboot.yml --limit lab-ns1

Docker実行ホストの用意

DNSサーバとして動かす予定のlab-ns1とlab-ns2に、Dockerをインストールするplaybookを実行しましょう。

なおこれまでに使ったplaybookとは違い、./playbooks/docker.ymlはタスク実行対象を"docker"グループと"lab_docker"グループとしてハードコードされています。

# run the playbook
ansible-playbook playbooks/docker.yml --tags cleaninstall

# test docker by running hello-world
ansible-playbook playbooks/docker.yml --tags test

# see running docker container list
ansible-playbook playbooks/docker.yml --tags status

最後の--tags statusに関しては、その時点で稼働しているDockerコンテナはないため何も出力されません。次のセクションでDNSサーバを動かした後にまた実行すると違う結果が見られます。

DockerでDNSサーバを走らせる

では次にDockerでDNSサーバを立ち上げるplaybookを実行しましょう。

先の準備セクションですでに必要なDNSサーバのレコード用ファイルは./roles/dns/templates/a-records.j2に用意してあります。今回用意するDNSサーバは、そのファイルに含まれるレコードは名前解決し、それ以外の問い合わせはCloudflareのDNSサーバを頼るよう設定されています。

Playbookのタスクとしては次の通りです:

  • もしすでにDNSサービスのコンテナが動いていれば止める
  • Ansibleマスターから最新のコンフィグファイルをアップロードする
  • DNSサービスを動かす (docker compose up -d)
  • hostコマンドかdigコマンドを使ってAnsibleマスタからサーバに対して名前解決のクエリを試す
# run the playbook
ansible-playbook playbooks/dns.yml --tags start

名前解決のテスト

レポジトリに含まれているバージョンではlabグループを対象としたときにしか動作しません。

具体的なタスクは次の通りです:

  • Ansibleマスター上にhostコマンドがあるか確認
  • Ansibleマスター上にdigコマンドがあるか確認
  • 名前解決テスト試行 (コマンドがない場合はスキップ)
    • cloudflare.com. SOAレコードを問い合わせ (外部の名前解決)
    • labグループ内のランダムなホストのAレコードの名前解決、例えばlab-cp1.lab.example.net.
    • happyansibleuser.lab.example.net.の名前解決 (NXDOMAIN結果を期待)

例えばansible-playbook playbooks/dns.yml --tags start --limit lab-ns2 -vというコマンドで実際の問い合わせ結果が確認できます。

hostdigで名前解決テストする際、問い合わせるサーバを明示的に指定しているので、Ansibleマスターのhostsファイルがどうであろうと、設定されているnameserversが何であろうと関係なく、playbookの対象ホスト上に関してテストできるようにしています。

DNSサーバのコンフィグ

いずれかのホストにログインし、docker execコマンドでDNSサーバのコンテナ内のファイルを確認できます。

# list of configuration files
docker exec dns ls -1 /opt/unbound/etc/unbound

# forwarder config
# where you can find the forwarder settings to use TLS for root ".",
# and that the upstream servers are 1.1.1.1 and 1.0.0.1
docker exec dns cat /opt/unbound/etc/unbound/forward-records.conf

稼働しているコンテナリストの確認

先ほど登場したコマンド、ansible-playbook playbooks/docker.yml --tags statusを実行すると、以下のようにその時対象ホストで稼働しているコンテナリストが確認できます。

TASK [Container status] ********************************************************************************************************************************************************************
included: docker for lab-ns1, lab-ns2

TASK [docker : Gather information on running containers] ***********************************************************************************************************************************
ok: [lab-ns2]
ok: [lab-ns1]

TASK [docker : Show information on running containers if any] ******************************************************************************************************************************
ok: [lab-ns1] => (item={'Id': '83836f4f4abace42234143e547c4b8447d58013fdb9b39bf1dcc49cdf57dbc5c', 'Image': 'mvance/unbound:1.21.1', 'Command': '/unbound.sh', 'Created': 1741051361, 'Status': 'Up 27 minutes (healthy)', 'Ports': [{'IP': '0.0.0.0', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'tcp'}, {'IP': '::', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'tcp'}, {'IP': '0.0.0.0', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'udp'}, {'IP': '::', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'udp'}], 'Names': ['/dns']}) => {
    "msg": [
        "Image: mvance/unbound:1.21.1",
        "Status: Up 27 minutes (healthy)"
    ]
}
ok: [lab-ns2] => (item={'Id': 'c9e524ea4ec1f55cab26722fd6c133fbb33c16ad206fa637e3b3a495692c3ae1', 'Image': 'mvance/unbound:1.21.1', 'Command': '/unbound.sh', 'Created': 1741052118, 'Status': 'Up 25 minutes (healthy)', 'Ports': [{'IP': '0.0.0.0', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'tcp'}, {'IP': '::', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'tcp'}, {'IP': '0.0.0.0', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'udp'}, {'IP': '::', 'PrivatePort': 53, 'PublicPort': 53, 'Type': 'udp'}], 'Names': ['/dns']}) => {
    "msg": [
        "Image: mvance/unbound:1.21.1",
        "Status: Up 25 minutes (healthy)"
    ]
}

Systemd上でDNSサービスをEnable状態に

オプショナルですが、--tags enableでsystemdのサービスユニットファイルを用意し、サーバ再起動時などに自動的にDNSサービスが立ち上がるようsystemdに登録することができます。

ansible-playbook playbooks/dns.yml --tags enable

Nameserversの更新

DNSサーバが用意できたので、各ホストのnameservers設定を更新しましょう。以下を実行するとnameservers変数として指定したサーバらを使うよう、各ホストの設定が更新されます。

./roles/dns/defaults/main.ymlなり、labグループの変数ファイルなりでlab-ns1とlab-ns2のIPアドレスが指定されていれば完璧です。

# update nameservers settings on each host
ansible-playbook playbooks/nameservers.yml --tags update

このplaybookは以下のネットワークサービスに対応しています:

  • networking, 通常の/etc/resolv.confファイルの更新
  • NetworkManager (多くのRedHatディストロといくつかのDebianディストロ)
    • もしDNS設定がNetworkManager側の設定に含まれていない場合は、/etc/resolv.confが更新される
  • netplan (だいたいUbuntuで見られる)

Kubernetes用ホストのセットアップ

では残りのホストをKubernetesレディなホストにセットアップするためのplaybookを実行しましょう。

たくさんの設定変更、たくさんのパッケージインストールが実施されますが、まずは確認用のタスクを実施して現時点の状態を確認してみましょう。

生成されるファイルを見ると、kubernetes関連パッケージのバージョンやcontainerdなどのバージョン、swapメモリの有無などなどが確認できます。

# check the host and generate report
ansible-playbook playbooks/kubernetes.yml --tags check

# see the report
cat playbooks/files/kubernetes/lab.md
cat playbooks/files/kubernetes/lab-etcd.md

次に実際にセットアップタスクを実行して、変更後の結果も見てましょう。

# prepare kubernetes-ready hosts
ansible-playbook playbooks/kubernetes.yml --tags prepare

# run another check and see what's in the report
ansible-playbook playbooks/kubernetes.yml --tags check
cat playbooks/files/kubernetes/lab.md
cat playbooks/files/kubernetes/lab-etcd.md

各ホストはKubernetesクラスタを構築するために必要なものすべてのインストール、設定がなされた状態になっています。この時点で、もし単一Control Planeのクラスタを立ち上げるならば、
Control Planeでkubeadm initを実行してクラスタを立ち上げ、Workerとなるホストでkubeadm joinすれば完成です。

今回は更に準備を進めて冗長化されたKubernetesクラスタを構築していきます。

Etcdクラスタの構築

次にetcdクラスタを構築します。タスクリストとしては次の通りです:

  • etcdctlのインストール (あとでヘルスチェックに使用)
  • etcdサービスを管理するようkubeletを設定
  • etcdクラスタ用のCA生成
  • etcdメンバー同士やKubernetesのControl Planeがetcdとやり取りするために用いるその他のTLS証明書、鍵を生成
  • etcdクラスタを走らせるためのstatic podマニフェストファイルを用意
  • (するとkubeletが自動的にetcdのコンテナを立ち上げ、各ホストのetcdが連携し一つのクラスタとして動作するようになる)
# run the playbook to spin up an etcd cluster
ansible-playbook playbooks/etcd.yml --tags cluster

# run manual etcd cluster check tasks to verify
ansible-playbook playbooks/etcd.yml --tags healthcheck

Kubernetesクラスタの構築

次のplaybookでKubernetesクラスタを立ち上げます。タスクリストは次の通りです:

  • クラスタのコンフィグファイルの用意
  • keepalivedのコンフィグファイルおよびヘルスチェックスクリプトの用意
  • haproxyのコンフィグファイルの用意
  • keepalivedとhaproxyのstatic podマニフェストファイルの用意
  • Etcdクラスタ構築時に用意したTLS証明書や鍵ファイルをアップロード
  • Control Planeの一台でkubeadm initを実行してクラスタを立ち上げ
  • 立ち上げ時に生成されたKubernetesクラスタの証明書、鍵ファイルを他のControl Planeにもコピーし、クラスタにControl Planeとして参加させる
  • Workerノードらもクラスタに参加させる
# form a cluster on one control plane node
# and join other control plane nodes
ansible-playbook playbooks/kubernetes.yml --tags cluster

# join worker nodes to the cluster
ansible-playbook playbooks/kubernetes.yml --tags worker

# see the output of "kubectl get nodes" taken after the tasks above
cat playbooks/files/kubernetes/kubectl_get_nodes.txt

Kubernetesクラスタ立ち上げ直後

クラスタ立ち上げ後、kubectlなどのツールでクラスタとやり取りするために必要な/etc/kubernetes/admin.confファイルを~/.kube/configにコピーするタスクも実行されています。

crictl pskubectl get podsコマンドなどで稼働しているコンテナを確認できます。

# output example of crictl ps
$ ssh ansible-hlv3@lab-cp2 -i playbooks/files/ssh/id_ed25519_ansible sudo crictl ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD                               NAMESPACE
6308f99d17515       d04966a100a7a       31 minutes ago      Running             keepalived                0                   a7f6f6d6b1025       keepalived-lab-cp2                kube-system
9a22ab5f6fc82       cf865d0b2bcd1       31 minutes ago      Running             haproxy                   0                   e1079c84cf6d1       haproxy-lab-cp2                   kube-system
9bb4f91523f67       85b7a174738ba       31 minutes ago      Running             kube-apiserver            0                   b557f1c7c4ef5       kube-apiserver-lab-cp2            kube-system
541b4d84f26c8       b6a454c5a800d       31 minutes ago      Running             kube-controller-manager   0                   62c785292829b       kube-controller-manager-lab-cp2   kube-system
3dfb7d3646135       d8e673e7c9983       31 minutes ago      Running             kube-scheduler            0                   b204de296f5a8       kube-scheduler-lab-cp2            kube-system

# output example of kubectl get pods, node ipaddr edited
$ ssh ansible-hlv3@lab-cp1 -i playbooks/files/ssh/id_ed25519_ansible kubectl get pods -n kube-system -o wide
NAME                              READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
coredns-668d6bf9bc-bbldg          0/1     Pending   0          31m   <none>         <none>    <none>           <none>
coredns-668d6bf9bc-qtcrw          0/1     Pending   0          31m   <none>         <none>    <none>           <none>
haproxy-lab-cp1                   1/1     Running   0          31m   192.0.2.1      lab-cp1   <none>           <none>
haproxy-lab-cp2                   1/1     Running   0          31m   192.0.2.2      lab-cp2   <none>           <none>
haproxy-lab-cp3                   1/1     Running   0          31m   192.0.2.3      lab-cp3   <none>           <none>
keepalived-lab-cp1                1/1     Running   0          31m   192.0.2.1      lab-cp1   <none>           <none>
keepalived-lab-cp2                1/1     Running   0          31m   192.0.2.2      lab-cp2   <none>           <none>
keepalived-lab-cp3                1/1     Running   0          31m   192.0.2.3      lab-cp3   <none>           <none>
kube-apiserver-lab-cp1            1/1     Running   0          31m   192.0.2.1      lab-cp1   <none>           <none>
kube-apiserver-lab-cp2            1/1     Running   0          31m   192.0.2.2      lab-cp2   <none>           <none>
kube-apiserver-lab-cp3            1/1     Running   0          31m   192.0.2.3      lab-cp3   <none>           <none>
kube-controller-manager-lab-cp1   1/1     Running   0          31m   192.0.2.1      lab-cp1   <none>           <none>
kube-controller-manager-lab-cp2   1/1     Running   0          31m   192.0.2.2      lab-cp2   <none>           <none>
kube-controller-manager-lab-cp3   1/1     Running   0          31m   192.0.2.3      lab-cp3   <none>           <none>
kube-scheduler-lab-cp1            1/1     Running   0          31m   192.0.2.1      lab-cp1   <none>           <none>
kube-scheduler-lab-cp2            1/1     Running   0          31m   192.0.2.2      lab-cp2   <none>           <none>
kube-scheduler-lab-cp3            1/1     Running   0          31m   192.0.2.3      lab-cp3   <none>           <none>

# kubectl get nodes
$ ssh ansible-hlv3@lab-cp3 -i playbooks/files/ssh/id_ed25519_ansible kubectl get nodes -o wide
NAME                                 STATUS     ROLES           AGE   VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE                              KERNEL-VERSION                 CONTAINER-RUNTIME
lab-cp1                              NotReady   control-plane   38m   v1.32.2   192.0.2.1   <none>        Debian GNU/Linux 12 (bookworm)        6.1.0-31-amd64                 containerd://2.0.2
lab-cp2                              NotReady   control-plane   37m   v1.32.2   192.0.2.2   <none>        Rocky Linux 9.5 (Blue Onyx)           5.14.0-503.26.1.el9_5.x86_64   containerd://2.0.2
lab-cp3                              NotReady   control-plane   37m   v1.32.2   192.0.2.3   <none>        Ubuntu 24.04.2 LTS                    6.8.0-54-generic               containerd://2.0.2
lab-worker1                          NotReady   <none>          37m   v1.32.2   192.0.2.4   <none>        Red Hat Enterprise Linux 9.5 (Plow)   5.14.0-503.26.1.el9_5.x86_64   containerd://2.0.2

もし何かがうまくいかない場合は

自分で確認のために手順を再走した時には、何度かVIPの変数を192.0.2.8のままにしてしまっていたことがあります。

この変数の値が更新されていることを確認しましょう。

grep endpoint roles/kubernetes/defaults/main.yml

他には、Ansibleのplaybook実行のコンソール出力を見ていれば何でひっかかっているのか分かるかもしれません。

また、もしkubeadm initでひっかかっている場合は、stdoutとstderr出力がいつもAnsibleマスター上に保存されているので確認してみてください。

# if things go wrong during the "kubeadm init" task,
# see the stdout and stderr logs available here
cat playbooks/files/kubernetes/kubeadm.log
cat playbooks/files/kubernetes/kubeadm.err

もしやりなおししたい場合は、--tags resetが用意されているのでご利用ください。kubeadm resetやディレクトリのクリーンアップが実行されます。なお全ステップのetcdクラスタ構築や設定変更、パッケージインストールは巻き戻されません。

ansible-playbook playbooks/kubernetes.yml --tags reset

ネットワークアドオンのインストール - Cilium

クラスタ構築後のチェックの出力から見て取れるように、参加ノードはしっかり認識されているしいろいろなpodももう立ち上がっています。しかし同時に、全てのノードは"NotReady"状態ですし、CoreDNSのpodは"pending state"でスタックしています。

次にやることはネットワークアドオンのインストールです。ここではCiliumをインストールします。

Kubernetesクラスタはネットワークコンポーネントがインストールされてようやく正常稼働できる状態になります。ネットワークアドオンにはflannel, calico, Cisco ACI, VMware NSX-Tなどなどたくさんの選択肢があります。

詳しく触れていませんが、クラスタ構築タスクでは、CiliumおよびCiliumの特定の機能を利用するための要件に合うようにクラスタコンフィグを調整してました。

https://docs.cilium.io/en/stable/installation/k8s-install-external-etcd/#requirements

https://docs.cilium.io/en/stable/network/l2-announcements/#prerequisites

https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/#prerequisites

そしてCiliumのインストール時にもそういったカスタム設定を用意する必要があります。今回はhelmを用いて、valuesファイル上で必要なカスタム設定を指定してインストールすることにします。

カスタム内容がファイルとして手元に、そしてVCSなどに載せられるというのはとても便利だと思います。

Helmを用いたCiliumのインストール

インストール作業はKubernetesクラスタのオペレートに使用するホストから実施する必要があります。Ansibleマスターとしているホストでも良いですし、Control Planeのどれか一台でも良いです。要はkubectlなどでクラスタを操作できる状態である必要があります。

タスクリストとしては次の通りです:

  • helmのインストール
  • インストールするciliumのバージョンを確認
  • そのバージョンのciliumのvaluesファイルをダウンロードする
  • valuesファイル上で必要なカスタム設定をする
  • 用意したvaluesファイルを用いてciliumのhelm chartをKubernetesクラスタにインストールする

実行したコマンドは次の通りです:

# on one of the control plane node
# for example...
# ssh ansible-hlv3@lab-cp1 -i playbooks/files/ssh/id_ed25519_ansible

# install helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# add cilium repository on helm
helm repo add cilium https://helm.cilium.io/

# confirm the latest version of cilium
helm search repo cilium
helm search repo cilium -l # to see all available versions

# download the values file for version 1.17.1
helm show values cilium/cilium --version 1.17.1 > values.yaml

# edit the values file

# OR, you can use one prepared in the repository
# scp -i playbooks/files/ssh/id_ed25519_ansible playbooks/files/cilium/values.yaml ansible-hlv3@lab-cp1:/home/ansible-hlv3/.

# STILL, MAKE SURE TO EDIT THE ETCD ENDPOINTS
# as they point to the doc ipaddr 192.0.2.x

# nl values.yaml | grep -C4 "endpoints:$"

# create secret for cilium containing etcd cert files
sudo cp /etc/kubernetes/pki/etcd/ca.crt .
sudo cp /etc/kubernetes/pki/apiserver-etcd-client.crt client.crt
sudo cp /etc/kubernetes/pki/apiserver-etcd-client.key client.key

sudo chown $USER:$USER *.crt
sudo chown $USER:$USER *.key

kubectl create secret generic -n kube-system cilium-etcd-secrets \
    --from-file=etcd-client-ca.crt=ca.crt \
    --from-file=etcd-client.key=client.key \
    --from-file=etcd-client.crt=client.crt

sudo rm *.crt *.key

# install
helm install cilium cilium/cilium --version 1.17.1 --values values.yaml -n kube-system

Cilium 1.17.1のValuesファイルへの変更リスト

変更点は次の通りです。また、変更後のファイルはレポジトリ上に含まれているのでよければご利用ください。

要注意点はetcdのendpointsです。実際のetcdクラスタのメンバーのIPアドレスに更新してください。

ファイル: ./playbooks/files/cilium/values.yaml

  • k8sServiceHost: lab-kube-endpoint.lab.example.net
  • k8sServicePort: "8443"
  • k8sClientRateLimit.qps: 33
  • k8sClientRateLimit.burst: 50
  • kubeProxyReplacement: "true"
  • kubeProxyReplacementHealthzBindAddr: "0.0.0.0:10256"
  • l2announcements.enabled: true
  • l2announcements.leaseDuration: 3s
  • l2announcements.leaseRenewDeadline: 1s
  • l2announcements.leaseRetryPeriod: 200ms
  • externalIPs.enabled: true
  • gatewayAPI.enabled: true
  • etcd.enabled: true
  • etcd.ssl: true
  • etcd.endpoints: ["https://192.0.2.5:2379", "https://192.0.2.6:2379", "https://192.0.2.7:2379"]
  • hubble.ui.enabled: true
  • hubble.relay.enabled: true
  • hubble.peerService.clusterDomain: lab.example.net

Ciliumインストール後の状態

# Nodes are in Ready state
$ ssh ansible-hlv3@lab-cp2 -i playbooks/files/ssh/id_ed25519_ansible kubectl get nodes
NAME                                 STATUS   ROLES           AGE   VERSION
lab-cp1                              Ready    control-plane   69m   v1.32.2
lab-cp2                              Ready    control-plane   69m   v1.32.2
lab-cp3                              Ready    control-plane   69m   v1.32.2
lab-worker1                          Ready    <none>          69m   v1.32.2


# CoreDNS and all the other pods are ready and runnning
$ ssh ansible-hlv3@lab-cp3 -i playbooks/files/ssh/id_ed25519_ansible kubectl get all -n kube-system
NAME                                  READY   STATUS    RESTARTS   AGE
pod/cilium-6npvn                      1/1     Running   0          5m54s
pod/cilium-dzflv                      1/1     Running   0          5m54s
pod/cilium-envoy-c6wnr                1/1     Running   0          5m54s
pod/cilium-envoy-mh4h4                1/1     Running   0          5m54s
pod/cilium-envoy-nprtn                1/1     Running   0          5m54s
pod/cilium-envoy-zrzl5                1/1     Running   0          5m54s
pod/cilium-l5dq9                      1/1     Running   0          5m54s
pod/cilium-m5pbg                      1/1     Running   0          5m54s
pod/cilium-operator-5f59576-qnxzv     1/1     Running   0          5m54s
pod/cilium-operator-5f59576-txw7h     1/1     Running   0          5m54s
pod/coredns-668d6bf9bc-bbldg          1/1     Running   0          67m
pod/coredns-668d6bf9bc-qtcrw          1/1     Running   0          67m
pod/haproxy-lab-cp1                   1/1     Running   0          67m
pod/haproxy-lab-cp2                   1/1     Running   0          67m
pod/haproxy-lab-cp3                   1/1     Running   0          67m
pod/hubble-relay-7bf4fc498b-rnfkq     1/1     Running   0          5m54s
pod/hubble-ui-69d69b64cf-hv7mk        2/2     Running   0          5m54s
pod/keepalived-lab-cp1                1/1     Running   0          67m
pod/keepalived-lab-cp2                1/1     Running   0          67m
pod/keepalived-lab-cp3                1/1     Running   0          67m
pod/kube-apiserver-lab-cp1            1/1     Running   0          67m
pod/kube-apiserver-lab-cp2            1/1     Running   0          67m
pod/kube-apiserver-lab-cp3            1/1     Running   0          67m
pod/kube-controller-manager-lab-cp1   1/1     Running   0          67m
pod/kube-controller-manager-lab-cp2   1/1     Running   0          67m
pod/kube-controller-manager-lab-cp3   1/1     Running   0          67m
pod/kube-scheduler-lab-cp1            1/1     Running   0          67m
pod/kube-scheduler-lab-cp2            1/1     Running   0          67m
pod/kube-scheduler-lab-cp3            1/1     Running   0          67m

NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
service/cilium-envoy   ClusterIP   None            <none>        9964/TCP                 5m54s
service/hubble-peer    ClusterIP   10.96.232.5     <none>        443/TCP                  5m54s
service/hubble-relay   ClusterIP   10.96.122.84    <none>        80/TCP                   5m54s
service/hubble-ui      ClusterIP   10.96.208.145   <none>        80/TCP                   5m54s
service/kube-dns       ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP,9153/TCP   67m

NAME                          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/cilium         4         4         4       4            4           kubernetes.io/os=linux   5m54s
daemonset.apps/cilium-envoy   4         4         4       4            4           kubernetes.io/os=linux   5m54s

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/cilium-operator   2/2     2            2           5m54s
deployment.apps/coredns           2/2     2            2           67m
deployment.apps/hubble-relay      1/1     1            1           5m54s
deployment.apps/hubble-ui         1/1     1            1           5m54s

NAME                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/cilium-operator-5f59576   2         2         2       5m54s
replicaset.apps/coredns-668d6bf9bc        2         2         2       67m
replicaset.apps/hubble-relay-7bf4fc498b   1         1         1       5m54s
replicaset.apps/hubble-ui-69d69b64cf      1         1         1       5m54s

完成

完成です!!Etcdクラスタが立ち上がっており、Kubernetesクラスタも立ち上がっておりネットワークアドオンもインストールされ、全て正常動作している状態です。ハッピーです。

以上が外部Etcdトポロジー採用の冗長構成Kubernetesクラスタの構築作業です。Etcdノードの一個や二個クラッシュしたとしても、Kubernetesクラスタは応答し、正常稼働し続けます。Control Planeノードが落ちてもKubernetesクラスタは応答・機能し続けます。

今回利用したレポジトリで用意されている設定では、lab-cp1のkeepalivedが基本的にマスターとしてVIPの応答責任者となります。lab-cp1をシャットダウンすると、他のいずれかのControl PlaneノードがVIPをテイクオーバーし、そのノードのhaproxyがkube-endpoint宛の通信を受け取り、そしてHaproxyはその時点でヘルスチェックに合格しているlab-cp2かlab-cp3いずれかのkube-apiserverへ通信を流します。

Cilium L2Announcementのデモンストレーション

最後にサッとCiliumのL2Announcement機能のデモをします。

OPTIONAL) Control Planeにもワークロードをスケジュールする

デフォルトでは通常のワークロードはControl Planeノードにはスケジュールされません。

エンタープライズ環境ではなく個人のおうちラボ規模ですので、私はいつもこの設定を変更してControl Planeのリソースも活用するようにしています。

# on lab-cp1 or any node that can run kubectl to manage the cluster
kubectl taint nodes lab-cp1 node-role.kubernetes.io/control-plane:NoSchedule-
kubectl taint nodes lab-cp2 node-role.kubernetes.io/control-plane:NoSchedule-
kubectl taint nodes lab-cp3 node-role.kubernetes.io/control-plane:NoSchedule-

# to confirm the taint settings of a node
kubectl describe node lab-cp1 | grep Taints

OPTIONAL) CoreDNSのコンフィグの更新

ワークロードの名前解決の挙動は、どのノード上で動作しているかによって変わりうることがあります。差異ができるだけないよう、CoreDNSのコンフィグを変更します。

# retrieve the live configmap from the cluster
kubectl get configmap coredns -n kube-system -o yaml > cm-coredns.yaml

# edit the configmap and then apply it
kubectl replace -f cm-coredns.yaml

# restart coredns deployment to recreate coredns pods with the updated configmap
kubectl -n kube-system rollout restart deployment coredns

更新した箇所は"forward ."の5行です。root forwarderとして、Dockerで今回構築したDNSサーバ同様、CloudflareのDNSサービスを利用するよう設定しています。

なお私の実際の環境では自分で管理している別のDNSサーバへ向けていますし、このあたりはご自身の環境で利用できる名前解決サービスに応じて好きに変更することができます。

data:
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        ready
        kubernetes lab.example.net in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        prometheus :9153
        forward . tls://1.1.1.1 tls://1.0.0.1 {
           tls_servername cloudflare-dns.com
           health_check 5s
           max_concurrent 1000
        }
        cache 30 {
           disable success lab.example.net
           disable denial lab.example.net
        }
        loop
        reload
        loadbalance
    }

Cilium L2Announcement

クラスタ上で走っているサービスへは、クラスタに参加しているノードからはアクセスできます。

# on lab-cp1
$ kubectl get svc -n kube-system
NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
cilium-envoy   ClusterIP   None            <none>        9964/TCP                 24m
hubble-peer    ClusterIP   10.96.232.5     <none>        443/TCP                  24m
hubble-relay   ClusterIP   10.96.122.84    <none>        80/TCP                   24m
hubble-ui      ClusterIP   10.96.208.145   <none>        80/TCP                   24m
kube-dns       ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP,9153/TCP   86m

# works on kubernetes cluster nodes, but the network is inaccessible from other hosts on the homelab subnet
$ host kubernetes.default.svc.lab.example.net. 10.96.0.10
Using domain server:
Name: 10.96.0.10
Address: 10.96.0.10#53
Aliases:

kubernetes.default.svc.lab.example.net has address 10.96.0.1

しかし、ここで見えている"10.96.."といったアドレスへは、クラスタ外からはアクセスできません。これをアクセスできるようにする一つのオプションが、L2広報です。

今回のデモでは、Ciliumインストール時に有効にしたHubble UIを取り上げます。ゴールとしてはクラスタ外からも (おうちラボのネットワーク上から)このHubble UIへアクセスできるようにすることです。

まずはhubble-uiポッドの識別に適当なラベルが何か確認します。

# looking at the defined labels on the hubble-ui deployment
$ kubectl get deploy hubble-ui -n kube-system -o jsonpath='{.spec.template.metadata.labels}'
{"app.kubernetes.io/name":"hubble-ui","app.kubernetes.io/part-of":"cilium","k8s-app":"hubble-ui"}o

# double check that the label works
$ kubectl get pods -l 'k8s-app=hubble-ui' -n kube-system
NAME                        READY   STATUS    RESTARTS   AGE
hubble-ui-68bb47466-6gkwb   2/2     Running   0          100m

次にhubble-ui用のサービスをもう一つ自前で作ります。作るのはLoadBalancerタイプのサービスです。

# create this service "l2-hubble-ui" on kube-system namespace
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: l2-hubble-ui
  namespace: kube-system
  labels:
    app.kubernetes.io/name: l2-hubble-ui
spec:
  type: LoadBalancer
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8081
  selector:
    k8s-app: hubble-ui
EOF

既存のものと同様にhubble-uiへアクセスするためのサービスが作れました。

# the created service with "pending" external IP address allocation
$ kubectl get svc l2-hubble-ui -n kube-system
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
l2-hubble-ui   LoadBalancer   10.96.81.203   <pending>     80:32334/TCP   36s

次に、作った"l2-hubble-ui"サービスのためのCilium IP Poolを用意します。

この時、設定するIPアドレスはDNSサーバのレコード用に用意したテンプレートファイルにあったhubble-ui.lab.example.netのIPアドレスを同じにするのが良いでしょう。

# CHANGE the IP address from 192.0.2.24 to whichever IP address you want to assign for hubble-ui
# the IP address you set in the DNS server configuration file should be the one
cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "ippool-hubble-ui"
spec:
  blocks:
    - start: "192.0.2.24"
      stop: "192.0.2.24"
  serviceSelector:
    matchExpressions:
      - { key: app.kubernetes.io/name, operator: In, values: [l2-hubble-ui] }
EOF

するとExternal IPとして用意したプールよりIPアドレスが割り当てられます。

$ kubectl get svc l2-hubble-ui -n kube-system
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)        AGE
l2-hubble-ui   LoadBalancer   10.96.81.203   192.0.2.24     80:32334/TCP   4m

OKです!もうアクセスできるでしょうか?まだです。

まだどのKubernetesクラスタのノードも、ネットワーク上に自身がそのIPアドレスを担当しているとネットワーク上に広報していないためです。ですので次に必要なのは、L2Announcementのポリシーを作成することです。

この時、クラスタに参加しているノードのインタフェース名に応じてinterfacesのリストを更新してください。おおよそこの内容でどんなインタフェース名でもマッチできるとは思います。

cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
  name: l2-hubble-ui
spec:
  serviceSelector:
    matchLabels:
      app.kubernetes.io/name: l2-hubble-ui
  interfaces:
    - ^eth[0-9]+
    - ^eno[0-9]+
    - ^enp[0-9]s[0-9]+
  loadBalancerIPs: true
EOF

L2AnnouncementポリシーができるとIPアドレスがsubnet上に広報され、Kubernetesクラスタ外の端末のウェブブラウザなどからhttp://hubble-ui.lab.example.netがアクセスできるようになっています。

ちなみにクラスタ外のその端末でhubble-ui.lab.example.netが名前解決できるようになっていなければ、IPアドレスでアクセスするのでも大丈夫です。
http://192.0.2.24

hubble-ui

Hubble UI

https://github.com/cilium/hubble-ui

Observability & Troubleshooting for Kubernetes Services

これはクラスタ上で何が起こっているのか見るためのツールですので、何か走らせましょう。

Cilium公式ドキュメントのインストール手順のページには、インストール後に実行するテストマニフェストが紹介されています。これをこのまま動かしましょう。

https://docs.cilium.io/en/latest/installation/k8s-install-helm/#validate-the-installation

https://github.com/cilium/cilium/blob/main/examples/kubernetes/connectivity-check/connectivity-check.yaml

やることは簡単で、namespaceを作る、マニフェストをapplyする、Hubble UIで確認してみる、(用が済めば)namespaceを削除する、といったステップで完了します。

# on lab-cp1 or any control plane node

# create the namespace cilium-test
kubectl create ns cilium-test

# run the connecitvity check pods in the cilium-test namespace
kubectl apply -n cilium-test -f https://raw.githubusercontent.com/cilium/cilium/1.17.1/examples/kubernetes/connectivity-check/connectivity-check.yaml

# clean up
kubectl delete ns cilium-test

こちらが画面のキャプチャです。

hubble-ui-cilium-test-namespace

終わりに

以上となります。

Ciliumで利用するGateway APIなども、今後続編として投稿するかもしれません。

以前試したNginx Gateway Fabricに関してはMetalLBも使っているパターンで、zenn上で記事にしていました。もしよろしければご覧ください。

Gateway APIとNGINX Gateway Fabricを自前のKubernetesクラスタで

また、CertManagerも組み合わせてTLS自動化楽々設定にした記録はCloudflarePages上でホストしているこちらにありますので、もしよろしければご覧ください。

building homelab cluster part 5

またCilium以外では、本記事の構築作業中に少し触れたkeepalivedが古いという点で、 新しいバージョンのイメージの構築の仕方の記事なりパブリックなイメージレジストリでの公開なり、何か用意出来たら更新したいと思います。

Discussion

ログインするとコメントできます