🚢

vagrant-libvirt と Ansible で自宅鯖prod環境に近い複数ノードKubernetes環境をローカルに作る

2023/09/20に公開

本記事は IaC (Infrastructure as Code) をベースに複数ノードの Kubernetes 環境をローカルPC内に構築することを目指します. 簡易staging環境を求めている人や, Kubernetes のクラスタ構築をやってみたいという方の参考になれば幸いです.

2023/09/28 編集

きっかけ

学習も兼ねて実際に自宅鯖で k8s を動かしていると, ノードの追加・削除やバージョン・構成変更時等によく壊れます (個人談). 壊れると環境を戻すのが非常に面倒なので, いろんな実験を躊躇うケースがありました.
そこで手軽に試せる「簡易staging環境 (以降, 単にstagingと呼称)」のためのスクリプトを組むことにしました.

記事の内容

Vagrant, KVM, Ansible を用いて複数ノード・複数control-plane構成の Kubernetes 環境をstaging用に構築します.

staging用VMはVagrant経由で起動し, prodに寄せたアクセスができるよう設計しました. prod は元々 Ansible で展開していたため, inventory ファイルを切り替えるだけで staging, prod を使い分けができるようにしています.

この記事で肝となるのはDNSサーバーを用いることでIPアドレスを手動で渡す、または利用する必要が一切無いことです. 仕組みは以下に示す通りです.

  • Vagrant が自動でIPアドレスを付与
  • Ansible の dynamic inventory (後述) でIPアドレスを取得し, ドメイン名との対応を保存
  • DNSサーバーに登録することで, ドメイン名をベースとしたアクセスが可能

構成図・環境

以下が prod と今回作る staging それぞれの構成図になります. どちらにもLAN内DNSサーバーを置き, ドメイン名で相互アクセスできるようにしています.

Image from Gyazo

Local PC: Windows 11 Pro (WSL)

名称 役割 OS
vm-dns.vagrant.home DNS server / Load Balancer Ubuntu 22.04
vm01.vagrant.home k8s control-plane node Ubuntu 22.04
vm02.vagrant.home k8s control-plane node Ubuntu 22.04
vm03.vagrant.home k8s control-plane node Ubuntu 22.04
vm04.vagrant.home k8s worker node Ubuntu 22.04
vm05.vagrant.home k8s worker node Ubuntu 22.04

注意: 冗長性

本記事の構成は control-plane node を2つ用意していますが, ロードバランサーを用意していないため不完全です.
Load Balancer を追加したため, 1台Control-Plane に対しては冗長性を持っています.

コード

https://github.com/pollenjp/sample-vagrant-libvirt-ansible-kubernetes/tree/v2023.9.28

上記のリポジトリに本記事用に対応したコードを載せています.
各アプリケーションを用意したあとに以下のコマンドで一発で構築できます.
※自分の手元では初回構築に1時間~1時間半ほどかかりました.

make debug-k8s-setup

or

go run ./tools/cmd setup-vagrant-k8s

基礎知識

今回は IaC (Infrastructure as Code) として全てすることも目的の一つであり, 以下のような領域で使い分けています. どれも有名なツールですので既知の人は読み飛ばしてください.

Tool 用途 設定ファイルフォーマット
Vagrant VMの作成・起動・停止・削除 Ruby
Ansible 各OSのセットアップ YAML
Kubernetes アプリケーションの展開 (本記事の範囲外) YAML

Vagrant

VagrantはVMを管理することができるツールになります.
構成ファイル (Vagrantfile) に起動したいVMのスペックやOSを記述し, 裏で Virtualbox や KVM などの仮想化ソフトウェアの操作し, VMの作成・起動を行っています.

Vagrantは Vagrant Cloud に様々なOSのBoxが公開されており, セットアップ済みのVM環境を利用することができます.
vagrant ユーザー作成やssh等も予め設定されているため頻繁にVMを作成・削除する際に向いています.

vagrant-libvirt

Vagrantの仮想化ソフトウェアとしてデフォルトでは VirtualBox が想定されていますが, vagrant-libvirt というプラグインを用いることで KVM (libvirt) を利用することができます.
Vagrant Cloud でも libvirt 用の Box が用意されています.

今回は自分の好みや手元で VirtualBox と Hyper-V の共存が難しい等の理由もあって KVM を利用しています.

Ansible

Ansible はプッシュベース型の設定管理ツールです. サーバー側で実行させたい処理を予め YAML 形式で記述 (playbook) しておき, 実行時にはコントロールクライアントからSSH経由でコマンドを逐次実行してくれます.
Chef や Puppet といったプルベース型の設定管理ツールとは異なり, クライアント側にエージェントをインストールする必要が無くシンプルです.

inventory ファイルという設定ファイルに接続先のサーバー情報や変数情報を記述しておき, その情報を元に playbook を実行することができます. prod用, staging用として異なる inventory ファイルを用意し 切り替えることで, 同じ処理 (playbook) を異なるサーバー構成に対して簡単に実行することができます.

inventory ファイルは ini 形式と YAML 形式などのように静的に記述することもできますが, dynamic inventory の機能を使うことでPythonコードとして動的に生成することもできます.

rye

今回は Python の Package Manager として rye を利用しています.
ryeはパッケージ管理だけではなく, 任意のPythonバージョンのインストールも行ってくれるため, 最近の言語の潮流に乗っており便利です. pyenvやpoetry が既知であれば, よく pyenv + poetry のように表現されることが多いです.
サンプルプロジェクトでは Ansible やその他のツールの依存関係解決のために利用しています.
本記事では rye run <some command> は venv 環境化でコマンドを叩いているという理解でも問題ないです.

解説

冒頭と同じ図を以下に持ってきました.

Image from Gyazo

全体の流れは以下のようになります.

  • Vagrant で VM を起動
  • DNSサーバーに各VMのIPアドレスを登録 (Ansible)
  • Kubernetes のセットアップ (Ansible)
    • kubeadm init でクラスターを初期化
    • kubeadm join でクラスターに追加

Vagrant で VM を起動

今回のVagrantfile では以下のようなスペック構成にしています.
左から順に name, cpu, memory, box, provider, description という順番で, ドメイン名アクセスと同じアクセスができるように .vagrant.home を後ろに追加しています.

VM_SPEC_ARR = [
  VmSpecData.new('vm-dns.vagrant.home', 2, 2048, VAGRANT_BOX, 'libvirt', 'dns node'),
  VmSpecData.new('vm01.vagrant.home', 4, 4096, VAGRANT_BOX, 'libvirt', 'cp01 node'),
  VmSpecData.new('vm02.vagrant.home', 2, 4096, VAGRANT_BOX, 'libvirt', 'cp02 node'),
  VmSpecData.new('vm03.vagrant.home', 2, 2048, VAGRANT_BOX, 'libvirt', 'worker01 node'),
  VmSpecData.new('vm04.vagrant.home', 2, 2048, VAGRANT_BOX, 'libvirt', 'worker02 node')
  VmSpecData.new('vm05.vagrant.home', 2, 2048, VAGRANT_BOX, 'libvirt', 'worker02 node')
].freeze

vagrant up

このまま vagrant up で起動できればよいのですが, 自分の環境下では一気に立ち上げようとするとメモリ割当等に失敗するケースによく遭遇しました.
そんな時は一台ずつ起動してあげれば大丈夫なはずです (時間はかかりますが...).

Makefile には以下のように記述しており, 起動時には make vagrant-up 等で起動するのが良いでしょう.

.PHONY: vagrant-up
vagrant-up:  ##
        vagrant box update
        vagrant up vm-dns.vagrant.home
        vagrant up vm01.vagrant.home
        vagrant up vm02.vagrant.home
        vagrant up vm03.vagrant.home
        vagrant up vm04.vagrant.home
        vagrant up vm05.vagrant.home

vgrant ssh-config

Ansible で VM にアクセスする際にはsshの設定がそのまま使われるため ~/.ssh/config を参照します.
しかし, 今回は先程起動したVMにアクセスしたいので vgrant ssh-config によって生成した ssh_config 設定を利用します.

環境変数ANSIBLE_SSH_ARG を利用して ssh のオプションを渡すことができるため, 以下のように実行することで見た目上, たとえば vm01.vagrant.home のドメインにアクセスしているように扱うことができます.

Makefile

.PHONY: debug
debug:
	${MAKE} vagrant-up
	-vagrant ssh-config > inventory/vagrant.ssh_config
	ANSIBLE_SSH_ARGS='-F inventory/vagrant.ssh_config' \
		rye run ansible-playbook \
			-i inventory/vagrant.py \
			playbooks/debug.yml

Ansible Inventory

今回の構成では inventory/vagrant.py として dynamic inventory の機能を利用しています. Vagrant で起動したマシンの IP アドレスを取得し動的に inventory を生成するためです.

試行錯誤しながら雑に書いているため見づらいかもしれませんが単に以下のような設定 (JSON) を吐き出しているだけです.

rye run python ./inventory/vagrant.py --list | jq

フルログ

{
  // 略
  "k8s_cp_master": {
    "hosts": [
      "vm01.vagrant.home"
    ],
    "children": []
  },
  "k8s_other_nodes": {
    "hosts": [
      "vm02.vagrant.home",
      "vm03.vagrant.home",
      "vm04.vagrant.home"
      "vm05.vagrant.home"
    ],
    "children": []
  },
  // 略
}

DNSサーバーに各VMのIPアドレスを登録 (Ansible)

prodでは専用のDNSサーバーを建てて名前解決をするようなことが多いと思います. そのため, 手元でも専用のDNSを建てて本番同様に名前解決できる環境を作ります.

playbooks/dns_server.yml にそのための設定を書いています.

playbooks/roles/dns_server/tasks/main.yml

  • DNSサーバー用のVMに BIND をインストール

  • 設定ファイルを編集し起動

    • 変数としてドメインと各VMのIPアドレスとの対応などを与えておき, template として zone ファイルを形成しています.
      playbooks/roles/dns_server/templates/etc/bind/etc/bind/template.zone

      $TTL	1d
      @	IN	SOA	ns1 root.localhost. (
              202309100	; Serial (size:uint32) (YYYYMMDDX: date+1桁index)
                      60	; 1w Refresh
                      30	; 1d Retry
                     120	; 4w Expire
                      30	; 1d Negative Cache TTL
            )
      @	IN	NS	ns1
      
      {% for v4_conf in item.value.ipv4  %}
      {% for name, addr in v4_conf.addresses.items()  %}
      {{ name }}	IN	A	{{ v4_conf.network_component }}.{{ addr }}
      {% endfor %}
      {% endfor %}
      
      {% for v6_conf in item.value.ipv6  %}
      {% for name, addr in v6_conf.addresses.items()  %}
      {{ name }}	IN	AAAA	{{ v6_conf.network_component }}{{ addr }}
      {% endfor %}
      {% endfor %}
      
      {% for name, actual_name in item.value.cnames.items()  %}
      {{ name }}	IN CNAME	{{ actual_name }}
      {% endfor %}
      

      inventoryの一部 (例)

      "network_configs": {
        "name_server": "192.168.121.214",
        "dns": {
          "acl": {
            "internal_network": [
              "localhost",
              "192.168.121.0/24"
            ]
          },
          "domains": {
            "vagrant.home": {
              "ipv4": [
                {
                  "network_component": "192.168.121",
                  "addresses": {
                    "vm02": 124,
                    "vm03": 142,
                    "vm04": 132,
                    "vm05": 121,
                    "vm01": 29,
                    "vm-dns": 214,
                    "ns1": 214
                  }
                }
              ],
              "ipv6": [],
              "cnames": {
                "k8s-cp-endpoint": "vm-dns"
              }
            }
          }
        }
      }
      
  • 各VMのネームサーバーの向き先を vm-dns.vagrant.home のIPに向ける

Load Balancer (Nginx)

Kubernetes のセットアップ (Ansible)

Kubernetes の構築は kubeadm を利用して行います.

必要な操作が kubeadm initkubeadm join で異なるので別の playbook に分けています.

Makefile

.PHONY: debug-k8s-setup
debug-k8s-setup:  ## debug the playbook (vagrant)
#	略
	ANSIBLE_SSH_ARGS='-F inventory/vagrant.ssh_config' \
		${MAKE} run \
			INVENTORY_FILE="inventory/vagrant.py" \
			PLAYBOOK="playbooks/k8s-setup-control-plane.yml"
	ANSIBLE_SSH_ARGS='-F inventory/vagrant.ssh_config' \
		${MAKE} run \
			INVENTORY_FILE="inventory/vagrant.py" \
			PLAYBOOK="playbooks/k8s-setup-join-node.yml"

k8s-setup-control-plane.yml

inventory で k8s_cp_master グループに含めているものに対して処理していきます.

  • install kubernetes

  • kubeadm init

    • kubeadm_config.yaml

      ---
      # <https://kubernetes.io/docs/reference/config-api/kubeadm-config.v1beta3/>
      apiVersion: kubeadm.k8s.io/v1beta3
      kind: InitConfiguration
      localAPIEndpoint:
          advertiseAddress: "{{ k8s_kubeadm_init_role__local_api_endpoint__advertise_address }}"
          bindPort: {{ k8s_kubeadm_init_role__local_api_endpoint__bind_port }}
      ---
      apiVersion: kubeadm.k8s.io/v1beta3
      kind: ClusterConfiguration
      networking:
          serviceSubnet: "10.96.0.0/16" # default
          # --pod-network-cidr=10.244.0.0/16 is required by flannel
          podSubnet: "10.244.0.0/16"
          dnsDomain: "cluster.local" # default
      controlPlaneEndpoint: "{{ k8s_kubeadm_init_role__control_plane_endpoint__address }}:{{ k8s_kubeadm_init_role__control_plane_endpoint__port }}"
      
    • kubeadm init

      - name: Initialize kubeadm
        ansible.builtin.command:
          cmd: >-
            kubeadm init
              --skip-token-print
              --config /tmp/kubeadm_config.yaml
      
  • その他のインストール

    • flannel
      • deploy時の設定をGit管理する意味も含めてmanifestは flannel-config.yml に保存しています.
      • playbook
    • helm

今回は vm01.vagrant.home を最初の control-plane とするため, inventory で k8s_cp_master グループに指定しています.

rye run python ./inventory/vagrant.py --list | jq
{
  // 略
  "k8s_cp_master": {
    "hosts": [
      "vm01.vagrant.home"
    ],
    "children": []
  },
  // 略
}

k8s-setup-join-node.yml

playbooks/k8s-setup-join-node.yml

基本的にはinventory で k8s_other_nodes グループに含めているものに対して処理していきますが, 一部 k8s_cp_master グループ上で諸々の情報を取得しています.

  • k8s_other_nodes: install kubernetes
  • k8s_cp_master: vm01.vagrant.home で kubeadm の token, 証明書, certificate-key を作成・取得
  • k8s_other_nodes: token, 証明書, certificate-key を用いて kubeadm join

control-plane にするか否かは inventory で指定しています.

rye run python ./inventory/vagrant.py --list | jq
{
  // 略
  "_meta": {
    "hostvars": {
      "vm-dns.vagrant.home": {},
      "vm01.vagrant.home": {},
      "vm02.vagrant.home": { "k8s_is_control_plane": true },
      "vm03.vagrant.home": { "k8s_is_control_plane": true },
      "vm04.vagrant.home": { "k8s_is_control_plane": false },
      "vm05.vagrant.home": { "k8s_is_control_plane": false }
    }
  }
}

全て実行

以下のコマンドのいずれかでできます.

make debug-k8s-setup

or

go run ./tools/cmd setup-vagrant-k8s

確認

vagrant@vm02:~$ kubectl get nodes
NAME   STATUS   ROLES           AGE   VERSION
vm01   Ready    control-plane   43m   v1.28.2
vm02   Ready    control-plane   21m   v1.28.2
vm03   Ready    control-plane   20m   v1.28.2
vm04   Ready    <none>          20m   v1.28.2
vm05   Ready    <none>          20m   v1.28.2

停止・削除

普通の vagrant コマンドとほぼ同じですが, 以下のコマンドで一括で停止・削除できます.

Makefile

停止だけ

make vagrant-halt

停止+削除

make vagrant-destroy
# make clean でもよい
.PHONY: vagrant-halt
vagrant-halt:  ##
	vagrant halt

.PHONY: vagrant-destroy
vagrant-destroy:  ##
	-${MAKE} vagrant-halt
	vagrant destroy -f

おわりに

今回, 自分としてはとりあえずで動くものを作れたので満足です.
同じく簡易staging環境を作りたいという方の参考になれば幸いです.
ただ, まだ以下の問題が残っているので余力があるときに追加・改善したいと思います.

  • VMの数や名前等が変わったときに複数箇所を修正する必要があるので1つの設定を参照させる
    • ( 簡易staging なのでそこまでする必要はない気もする )

本記事の誤字脱字に関してはプルリクの方も受け付けております.

GitHubで編集を提案

Discussion