3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Terraform + Kubespray で KVM 上に Kubernetes クラスタを構築

Last updated at Posted at 2023-08-01

TL;DR

構成を定義した後、次のコマンドで Kubernetes cluster が作成できる:

$ terraform init
$ terraform apply -auto-approve
$ docker run --rm -it \
  --mount type=bind,source="$(pwd)"/inventory,dst=/inventory \
  --mount type=bind,source="$(pwd)"/generate_inventory.py,dst=/kubespray/generate_inventory.py \
  --mount type=bind,source="$(pwd)"/terraform.tfstate,dst=/kubespray/terraform.tfstate \
  --mount type=bind,source="${HOME}"/.ssh/id_rsa,dst=/root/.ssh/id_rsa \
  quay.io/kubespray/kubespray:v2.23.1 bash

# Inside a container
$ ansible-playbook -i generate_inventory.py cluster.yml

サンプルコード:

概要

Terraform + Kubespray で KVM 上に Kubernetes クラスタを構築する。大まかな手順は次の通り。

  1. Terraform で VM を作成
  2. 生成された terraform.tfstate から inventory 情報を抽出
  3. inventory 情報と Kuberspray の Ansible playbooks から Kubernetes クラスタを作成

dynamic_inventory.drawio.png

ネットワーク構成は下図の通り:

network_architecture.drawio.png

クラスタの Public ネットワーク (eth0 側) と、Ansible が SSH ログインするためのネットワーク (eth1 側) を分けている。

前提条件

  • terraform
  • Container runtime (docker, podman, nerdctl, etc.)
  • KVM packages
    • qemu-kvm
    • libvirt
  • crdtools
  • nmcli
  • (Host OS: Ubuntu 22.04)

手順

Host の事前設定

ネットワーク設定

仮想ブリッジ br0 を作成し、既存のホストのネットワークインタフェース (例:enp1s0) を br0 に接続する。このようにしておくと、後に作成する VM を br0 に接続させることにより、LAN 内にある任意のホストから VM に直接アクセスできるようになる。

図で説明すると、次のように Before から After へ設定を変更する。

network-diff.drawio.png

次ようなスクリプトを作成し、パラメータを書き換え、ホスト上で実行する。

create_br0.sh
HOST_IP=192.168.8.10
CIDR_PREFIX=24
GATEWAY=192.168.8.1
DNS=192.168.8.1
NWIF=enp1s0

# ブリッジを作成
nmcli con add type bridge con-name br0 ifname br0

# ブリッジに既存の NIC 設定を与える
nmcli con modify br0 \
ipv4.method manual \
ipv4.addresses "$HOST_IP/$CIDR_PREFIX" \
ipv4.gateway "$GATEWAY" \
ipv4.dns $DNS

# enp1s0 のマスターを br0 にする
nmcli con add type bridge-slave ifname $NWIF master br0

# 既存の接続を無効化
nmcli con down $NWIF

# br0 を有効化
nmcli con up br0

libvirt の default pool の確認

libvirt が利用する様々なリソースは、 default pool と呼ばれるディレクトリ: /var/lib/libvirt/images 内で管理される。したがって、まずは default pool が存在することを確認する。

# virsh pool-list --all
 Name      State    Autostart
-------------------------------
 default   active   yes

default pool がなければ次のコマンドで作成しておく:

# mkdir -p /var/lib/libvirt/images
# chmod 755 /var/lib/libvirt/images

# virsh pool-define /dev/stdin <<EOF
<pool type='dir'>
  <name>default</name>
  <target>
    <path>/var/lib/libvirt/images</path>
  </target>
</pool>
EOF

# virsh pool-start default
# virsh pool-autostart default
# virsh pool-list --all

Linux image の入手

本記事では VM の OS として、CentOS の後継である Rocky Linux を用いる。download.rockylinux.org より配布のイメージファイルを、libvirt のデフォルトプール /var/lib/libvirt/images/ にダウンロードする:

# curl -L -o /var/lib/libvirt/images/Rocky-9-GenericCloud.latest.x86_64.qcow2 https://download.rockylinux.org/pub/rocky/9.2/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2

Terraform による VM のプロビジョニング

cloud-init の設定

cloud-init は、多くの Linux distribution で採用されている VM の初期設定を自動化するための仕組みである。設定は次の2つの設定ファイルを用意して与えることができる。

  • cloud_init.cfg: ユーザ設定を記述
  • network_config.cfg: ネットワーク設定を記述
cloud_init.cfg
#cloud-config
users:
  - name: root
    ssh-authorized-keys:
      - "<YOUR_SSH_KEY>"

ネットワーク設定は network_config.cfg から与える。

VM ごとに異なる値(例:静的 IP アドレスなど)は、${foo} のようなプレースホルダーで記述したテンプレートを作成しておく。Terraform のテンプレート機能によって値が代入され、VM ごとの network_config.cfg を生成することができる。

network_config.cfg.tpl
version: 2
ethernets:
  eth0:
    dhcp4: no
    addresses: [${ip}]
    gateway4: ${gateway}
    nameservers:
      addresses: ${nameservers}

Terraform ファイルの用意

:::note
本節は、Kubespray 用の VM を構築するための Terraform コードの実装方法について解説します。実装するのではなく、import して使いたい場合は、本記事内 TIPS の 『本記事の-Terraform-コードを-module-として使う』 を参照してください。

ファイルは次の4つからなる:

  • provider.tf
  • main.tf
  • variables.tf
  • output.tf

provider.tf

Terraform Libvirt Provider の使用を宣言する。

main.tf
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.7.1"
    }
  }
}

provider "libvirt" {
  uri = var.libvirt_uri
}

main.tf

主に VM の作成に関する処理を記述する。

main.tf
locals {
  cluster_cidr_splitted      = split("/", var.cidr)
  cluster_cidr_subnet        = local.cluster_cidr_splitted[0]
  cluster_cidr_prefix        = local.cluster_cidr_splitted[1]
  cluster_nameservers_string = "[\"${join("\", \"", var.nameservers)}\"]"

  # Auto-calculate mac address from IP 
  cluster_ips_parts = [for vm in var.vms : split(".", vm.public_ip)]
  cluster_mac_addrs = [
    for ip_parts in local.cluster_ips_parts : format(
      "52:54:00:%02X:%02X:%02X",
      tonumber(ip_parts[1]),
      tonumber(ip_parts[2]),
      tonumber(ip_parts[3])
    )
  ]
  private_ips_parts = [for vm in var.vms : split(".", vm.private_ip)]
  private_mac_addrs = [
    for ip_parts in local.private_ips_parts : format(
      "52:54:00:%02X:%02X:%02X",
      tonumber(ip_parts[1]),
      tonumber(ip_parts[2]),
      tonumber(ip_parts[3])
    )
  ]
}

data "template_file" "user_data" {
  count    = length(var.vms)
  template = file(var.vms[count.index].cloudinit_file)
}

data "template_file" "network_config" {
  count    = length(var.vms)
  template = file("${path.module}/network_config.cfg")
  vars = {
    ip          = var.vms[count.index].public_ip
    cidr_prefix = local.cluster_cidr_prefix
    gateway     = var.gateway
    nameservers = local.cluster_nameservers_string
  }
}

resource "libvirt_cloudinit_disk" "commoninit" {
  count          = length(var.vms)
  name           = "commoninit_${var.vms[count.index].name}.iso"
  user_data      = data.template_file.user_data[count.index].rendered
  network_config = data.template_file.network_config[count.index].rendered
}

locals {
  volume_list      = { for vm in var.vms : "${vm.name}" => flatten([for volume in vm.volumes : volume]) }
  volume_name_list = [for vm, volumes in local.volume_list : [for volume in volumes : { "name" : "${vm}_${volume.name}", "disk" : volume.disk }]]
  volumes          = flatten(local.volume_name_list)
  volumes_indexed  = { for index, volume in local.volumes : volume.name => index }
}

resource "libvirt_domain" "vm" {
  count  = length(var.vms)
  name   = var.vms[count.index].name
  vcpu   = var.vms[count.index].vcpu
  memory = var.vms[count.index].memory

  disk {
    volume_id = libvirt_volume.system[count.index].id
  }

  cloudinit = libvirt_cloudinit_disk.commoninit[count.index].id
  autostart = true

  # Public network
  network_interface {
    bridge    = var.bridge
    addresses = [var.vms[count.index].public_ip]
    mac       = local.cluster_mac_addrs[count.index]
  }

  # Private network
  network_interface {
    network_name = "default"
    addresses    = [var.vms[count.index].private_ip]
    mac          = local.private_mac_addrs[count.index]
  }

  qemu_agent = true

  cpu {
    mode = "host-passthrough"
  }

  graphics {
    type        = "vnc"
    listen_type = "address"
  }

  # Makes the tty0 available via `virsh console`
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }
}

resource "libvirt_volume" "system" {
  count          = length(var.vms)
  name           = "${var.vms[count.index].name}_system.qcow2"
  pool           = var.pool
  format         = "qcow2"
  base_volume_id = var.vm_base_image_uri
  size           = var.vms[count.index].disk
}

variables.tf

変数を宣言する。

variables.tf
variable "libvirt_uri" {
  type = string
}

variable "vm_base_image_uri" {
  type = string
}

variable "bridge" {
  type = string
}

variable "gateway" {
  type = string
}

variable "cidr" {
  type = string
}

variable "nameservers" {
  type = list(string)
}

variable "pool" {
  type    = string
  default = "default"
}

variable "vms" {
  type = list(
    object({
      name           = string
      vcpu           = number
      memory         = number
      disk           = number
      public_ip      = string
      private_ip     = string
      cloudinit_file = string

      kube_control_plane = bool
      kube_node          = bool
      etcd               = bool
    })
  )
}

output.tf

この後の工程で Ansible に渡すホストの情報を出力する。

output.tf
locals {
  kubespray_hosts_keys = ["name", "kube_control_plane", "kube_node", "etcd"]
  kubespray_hosts = [for vm in var.vms :
    merge(
      {
        for key, value in vm : key => value if contains(local.kubespray_hosts_keys, key)
      },
      {
        ip        = vm.public_ip
        access_ip = vm.private_ip
    })
  ]
}

output "kubespray_hosts" {
  value = local.kubespray_hosts
}

Terraform の実行

Terraform を実行し、VM をプロビジョニングする。

$ terraform init
$ terraform apply -auto-approve

VM が作成され、稼働していることを確認する。

$ virsh list --all
 Id   Name           State
------------------------------
 1    k8s-master-1   running
 2    k8s-worker-1   running
 3    k8s-worker-2   running

次のコマンドで、作成した VM がbr0 に接続されていることが確認できる。

$ ip link show master br0
bridge name     bridge id               STP enabled     interfaces
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master br0 state UP mode DEFAULT group default qlen 1000
    link/ether 48:21:0b:57:b2:52 brd ff:ff:ff:ff:ff:ff
5: vnet4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether fe:54:00:00:00:04 brd ff:ff:ff:ff:ff:ff
6: vnet5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether fe:54:00:00:00:02 brd ff:ff:ff:ff:ff:ff
7: vnet6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether fe:54:00:00:00:03 brd ff:ff:ff:ff:ff:ff
8: vnet7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether fe:54:00:00:00:01 brd ff:ff:ff:ff:ff:ff

Kubespray による kubernetes cluster の作成

Kubespray は、Kubenetes クラスタの構築を自動化をする Ansible playbook を提供するオープンソースのプロジェクトである。手順は kubespray/docs/setting-up-your-first-cluster.md at master · kubernetes-sigs/kubespray に従う。

まず kubespray のリポジトリをクローンする。

$ git clone git@github.com:kubernetes-sigs/kubespray.git
$ cd kubespray

次に 既存のサンプルをコピーし、設定ファイルの土台を作成する。

$ git checkout release-2.23
$ cp -rfp inventory/sample inventory/mycluster

本記事ではコンテナランタイムに crio を使うことにする(デフォルトだと containerd になっている)。group_vars/k8s_cluster/k8s_cluster.ymlcontainer_manager の値を crio に書き換える:

group_vars/k8s_cluster/k8s_cluster.yml
## Container runtime
## docker for docker, crio for cri-o and containerd for containerd.
## Default: containerd
container_manager: crio

より細かいオプションを設定する場合は、inventory/mycluster/group_vars/ 内のファイルを修正する(*補足:プロキシの設定)。

Inventory の作成

次の2通りの方法を紹介するが、どちらか一方を選べば良い。

  1. Dynamic Inventory を用いる方法
  2. 手動で hosts.yaml を編集する方法
(方法1) Dynamic Inventory を用いる

terraform 実行後に生成される terraform.tfstate から動的に inventory 情報を抽出する。(

これを行うには、JSON で記述された inventory 情報を出力だけのするスクリプト (実装例:./generate_inventory.py) を用意すればよい。下記は、terraform.state.outputs を読み込み、インベントリを作成するスクリプトの実装例である:

generate_inventory.py
#!/usr/bin/env python3

import json
import re


def main():
    output = get_outputs()
    hosts = output['kubespray_hosts']['value']
    libvirt_uri = output['libvirt_uri']['value']

    hostvars = {}
    kube_control_plane = []
    kube_node = []
    etcd = []

    for host in hosts:
        name = host['name']
        ip = host['ip']
        access_ip = host['access_ip']
        hostvars.update({
          name: {
              "ansible_host": access_ip,
              "ip": ip,
          }
        })

        regex = r"^qemu(\+ssh)?://([^/]*)/.*"
        res = re.match(regex, libvirt_uri)
        if res:
          hostname = res[2]
          if hostname != "":
            hostvars[name].update({
              "ansible_ssh_common_args": f"-J {hostname}"
          })

        if host["kube_control_plane"]:
            kube_control_plane.append(name)
        if host["kube_node"]:
            kube_node.append(name)
        if host["etcd"]:
            etcd.append(name)

    inventory = {
        "_meta": {
            "hostvars": hostvars,
        },
        "kube_control_plane": kube_control_plane,
        "kube_node": kube_node,
        "etcd": etcd,
        "k8s_cluster": {
            "children": [
                "kube_control_plane",
                "kube_node",
            ]
        }
    }


    print(json.dumps(inventory))


def get_outputs():
    tfstate_path = './terraform.tfstate'
    with open(tfstate_path) as f:
        tfstate = json.load(f)
    return tfstate['outputs']


main()

この ./generate_inventory.py を実行すると次のような JSON が出力される:

{
  "_meta": {
    "hostvars": {
      "storage1": {
        "ansible_host": "192.168.122.201",
        "ip": "192.168.8.201"
      },
      "storage2": {
        "ansible_host": "192.168.122.202",
        "ip": "192.168.8.202"
      },
      "storage3": {
        "ansible_host": "192.168.122.203",
        "ip": "192.168.8.203"
      }
    }
  },
  "kube_control_plane": [
    "storage1"
  ],
  "kube_node": [
    "storage1",
    "storage2",
    "storage3"
  ],
  "etcd": [
    "storage1"
  ],
  "k8s_cluster": {
    "children": [
      "kube_control_plane",
      "kube_node"
    ]
  }
}

これをインベントリとして指定し、Kubernetes クラスタを作成する:

$ docker pull quay.io/kubespray/kubespray:v2.23.1
$ docker run --rm -it \
  --mount type=bind,source="$(pwd)"/inventory,dst=/inventory \
  --mount type=bind,source="$(pwd)"/generate_inventory.py,dst=/kubespray/generate_inventory.py \
  --mount type=bind,source="$(pwd)"/terraform.tfstate,dst=/kubespray/terraform.tfstate \
  --mount type=bind,source="${HOME}"/.ssh/id_rsa,dst=/root/.ssh/id_rsa \
  quay.io/kubespray/kubespray:v2.23.1 bash

# Inside a container
$ ansible-playbook -i generate_inventory.py cluster.yml
…
PLAY RECAP *****************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master-1               : ok=765  changed=138  unreachable=0    failed=0    skipped=1263 rescued=0    ignored=8   
k8s-worker-1               : ok=583  changed=104  unreachable=0    failed=0    skipped=795  rescued=0    ignored=2   
k8s-worker-2               : ok=523  changed=82   unreachable=0    failed=0    skipped=763  rescued=0    ignored=1   

Tuesday 01 August 2023  13:16:52 +0000 (0:00:00.041)       0:59:01.983 ********                                                                                      ===============================================================================      

なお、途中で失敗するなどしてインストールをやり直したくなった場合、次でリセットできる。

$ ansible-playbook -i ./generate_inventory.py reset.yml

Kubespray における Dynamic Inventory の詳細は、Kubespray 公式レポジトリの https://github.com/kubernetes-sigs/kubespray/blob/master/docs/aws.md#dynamic-inventory も参照。

(方法2) 手動で hosts.yaml を編集する

kubespray のコンテナを立ち上げる。

$ docker pull quay.io/kubespray/kubespray:v2.23.1
$ docker run --rm -it \
  --mount type=bind,source="$(pwd)"/inventory/mycluster,dst=/inventory \
  --mount type=bind,source="${HOME}"/.ssh/id_rsa,dst=/root/.ssh/id_rsa \
  quay.io/kubespray/kubespray:v2.23.1 bash

コンテナの中に入るので、次のコマンドを実行する。

$ declare -a IPS=(192.168.8.201 192.168.8.202 192.168.8.203)
$ CONFIG_FILE=/inventory/hosts.yaml python3 contrib/inventory_builder/inventory.py ${IPS[@]}

inventory ファイル /inventory/mycluster/hosts.yaml が生成されるので、適宜、ホスト情報を修正する。本記事では下記の設定を用いる:

hosts.yaml
all:
  hosts:
    storage1:
      ansible_host: 192.168.122.201
      ip: 192.168.8.201
    storage2:
      ansible_host: 192.168.122.202
      ip: 192.168.8.202
    storage3:
      ansible_host: 192.168.122.203
      ip: 192.168.8.203
  children:
    kube_control_plane:
      hosts:
        storage1:
    kube_node:
      hosts:
        storage1:
        storage2:
        storage3:
    etcd:
      hosts:
        storage1:
    k8s_cluster:
      children:
        kube_control_plane:
        kube_node:
$ docker run --rm -it \
  --mount type=bind,source="$(pwd)"/inventory,dst=/inventory \
  --mount type=bind,source="$(pwd)"/cluster.yml,dst=/kubespray/cluster.yml \
  --mount type=bind,source="${HOME}"/.kube,dst=/root/.kube \
  --mount type=bind,source="${HOME}"/.ssh/id_rsa,dst=/root/.ssh/id_rsa \
  quay.io/kubespray/kubespray:v2.23.1 bash

# Inside a container
$ ansible-playbook -i /inventory/hosts.yaml cluster.yml

Kubernetes cluster へのアクセス

いずれかの master node から認証情報 admin.conf を取ってくる。

$ mkdir -p ~/.kube
$ scp root@192.168.8.201:/etc/kubernetes/admin.conf ~/.kube

admin.conf 内の clusters.cluster.server の IP は、127.0.0.1 になっているので、Public IP (例:192.168.8.201) に修正する。

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: <CERTIFICATE_AUTHORITY_DATA>
    server: https://192.168.8.201:6443
  name: cluster.local
...

.bashrc などで次の環境変数を設定する。

$ export KUBECONFIG=$HOME/.kube/admin.conf 

以上で kubectl でクラスタにアクセスできるようになる。

$ kubectl cluster-info
Kubernetes control plane is running at https://192.168.8.201:6443

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

$ kubectl get nodes
NAME           STATUS   ROLES           AGE   VERSION
k8s-master-1   Ready    control-plane   70m   v1.26.5
k8s-worker-1   Ready    <none>          68m   v1.26.5
k8s-worker-2   Ready    <none>          68m   v1.26.5

TIPS

Kubespray で proxy を設定する

proxy を設定する場合は、inventory/mycluster/group_vars/all/all.ymlの次の項目を編集する。

http_proxy: 'http://your-proxy-server:8080'
https_proxy: 'http://your-proxy-server:8080'
no_proxy: 'localhost,127.0.0.1,.yourdomain.com'

Kubespray に独自の playbook を組み込む

例えば、cluster.yml の適用後に独自の追加処理を実行させる場合について。

Kubespray の cluster.yml の内容 を見ると、単に playbooks/cluster.yml を import しているだけなので、追加のタスクを行う場合は cluster.yml の末尾に独自の処理を追加した、次のような cusotmized_cluster.yml を作成する。

customized_cluster.yml
---
# This role assumes to call Kubespray's `playbooks/cluster`.
- name: Install Kubernetes
  ansible.builtin.import_playbook: playbooks/cluster.yml

- name: Registernqualified
  hosts: all
  tasks:
    - ansible.builtin.file:
        path: /etc/containers/registries.conf.d
        state: directory
        mode: '0755'
    - ansible.builtin.copy:
        dest: /etc/containers/registries.conf.d/01-unqualified.conf
        content: |

          unqualified-search-registries = ['docker.io', 'quay.io']

- name: Download amind.conf to localhost
  hosts: kube_control_plane
  run_once: true
  tasks:
    - ansible.builtin.fetch:
        src: /etc/kubernetes/admin.conf
        dest: ~/.kube/admin.conf
        flat: yes
    - delegate_to: localhost
      ansible.builtin.replace:
        path: ~/.kube/admin.conf
        regexp: '127.0.0.1'
        replace: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}"

上の例では、クラスタ作成後の次のタスク:

  • Unqualified registries (docker.ioquay.io) の追加
  • admin.conf をローカルにダウンロードし、API サーバのアドレスを Public IP に置換

を自動化する。あとは Kubespray の cluster.yml をこれに置き換え、通常通り playbook を実行すればよい。

$ docker run --rm -it \
  --mount type=bind,source="$(pwd)"/inventory,dst=/inventory \
  --mount type=bind,source="$(pwd)"/.terraform/modules/kubernetes/kubernetes/generate_inventory.py,dst=/kubespray/generate_inventory.py \
  --mount type=bind,source="$(pwd)"/terraform.tfstate,dst=/kubespray/terraform.tfstate \
  --mount type=bind,source="$(pwd)"/cluster.yml,dst=/kubespray/cluster.yml \
  --mount type=bind,source="${HOME}"/.kube,dst=/root/.kube \
  --mount type=bind,source="${HOME}"/.ssh/id_rsa,dst=/root/.ssh/id_rsa \
  quay.io/kubespray/kubespray:v2.23.1 bash

# Inside a container
ansible-playbook -i generate_inventory.py cluster.yml

公式のガイド kubespray/docs/integration.md at master · kubernetes-sigs/kubespray · GitHub) も参照。

オフライン環境で Terraform を実行する

通常、Provider はインターネット経由で自動的にダウンロードされるが、オフライン環境の場合は、ホームディレクトリ下に ~/terraform.d/providers を用意し、そこに Provider をおく。

例として、Terraform Libvirt Provider 使う場合、Releases · dmacvicar/terraform-provider-libvirt からバイナリをダウンロードし次のように配置する:

~/terraform.d/providers/
└── providers/
    └── registry.terraform.io/
        └── dmacvicar/
            └── libvirt/
                └── 0.7.1/
                    └── linux_amd64/
                        ├── CHANGELOG.md
                        ├── LICENSE
                        ├── README.md
                        └── terraform-provider-libvirt_v0.7.4

リモートホストに対して Terraform を適用する

ローカルから Terraform を実行し、リモートホスト上に VM を作成することができる。

まずはリモートホスト上で、Host の事前設定 を完了させた上で、libvirtd が稼働していることを確認する:

# Remote side
$ systemctl status libvirtd

クライアント側から qemu+ssh:// プロトコルを用いて接続を確認する:

# Client side
$ virsh -c qemu+ssh://<user>@<remote_ip>/system list
 Id   Name   State
--------------------

確認できたら、main.tf で下記のようにリモートホストを指定し、クライアント側で terraform apply を実行する。

main.tf
locals {
  user_home_directory = pathexpand("~")
}

provider "libvirt" {
  uri = "qemu+ssh://<remote-user>@<remote_host>/system?keyfile=${local.user_home_directory}/.ssh/id_rsa&known_hosts_verify=ignore"
}

踏み台サーバを経由する場合

踏み台サーバを経由してリモートにアクセスする場合、リモートの22番ポートをローカルの適当なポート (例:50000) にフォワーディングする。

# Client side
$ ssh -C -N -f -L 50000:<remote-user>@<remote-host>:22 <bastion-host> -p <bastion-port>
$ virsh -c qemu+ssh://<remote-user>@localhost:50000/system list
 Id   Name   State
--------------------

main.tf
locals {
  user_home_directory = pathexpand("~")
}

provider "libvirt" {
  uri = "qemu+ssh://<remote-user>@localhost:50000/system?keyfile=${local.user_home_directory}/.ssh/id_rsa&known_hosts_verify=ignore"
}

本記事の Terraform コードを module として使う

下記ような main.tf を用意する。例では1台の master node と 2台の worker nodes からなる合計3台のVMが作成されるが、所望の構成になるよう適宜パラメータを修正する。

main.tf
output "kubespray_hosts" {
  value = module.kubernetes.kubespray_hosts
}

output "libvirt_uri" {
  value = module.kubernetes.libvirt_uri
}

locals {
  user_home_directory = pathexpand("~")
}

module "kubernetes" {
  source = "github.com/sawa2d2/k8s-on-kvm//kubernetes/"

  ## Localhost:
  # libvirt_uri = "qemu:///system"
  ## Remote:
  # libvirt_uri = "qemu+ssh://<user>@<remote-host>/system?keyfile=${local.user_home_directory}/.ssh/id_rsa&known_hosts_verify=ignore"
  ## Remote via bastion:
  ##   Forward port in advance.
  ##   $ ssh -C -N -f -L 50000:<remote-user>@<remote-host>:22 <bastion-host> -p <bastion-port>
  # libvirt_uri = "qemu+ssh://<remote-user>@localhost:50000/system?keyfile=${local.user_home_directory}/.ssh/id_rsa&known_hosts_verify=ignore"
  libvirt_uri = "qemu:///system"

  # Download the image by:
  #   sudo curl -L -o /var/lib/libvirt/images/Rocky-9-GenericCloud.latest.x86_64.qcow2 https://download.rockylinux.org/pub/rocky/9.2/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2 
  vm_base_image_uri = "/var/lib/libvirt/images/Rocky-9-GenericCloud.latest.x86_64.qcow2"
  pool              = "default"

  # Cluster network
  bridge      = "br0"
  cidr        = "192.168.8.0/24"
  gateway     = "192.168.8.1"
  nameservers = ["192.168.8.1"]

  vms = [
    {
      name           = "k8s-master-1"
      vcpu           = 4
      memory         = 16000                    # in MiB
      disk           = 100 * 1024 * 1024 * 1024 # 100 GB
      public_ip      = "192.168.8.101"
      private_ip     = "192.168.122.201"
      cloudinit_file = "cloud_init.cfg"
      volumes        = []

      kube_control_plane = true
      kube_node          = true
      etcd               = true
    },
    {
      name           = "k8s-worker-1"
      vcpu           = 4
      memory         = 16000                    # in MiB
      disk           = 100 * 1024 * 1024 * 1024 # 100 GB
      public_ip      = "192.168.8.102"
      private_ip     = "192.168.122.202"
      cloudinit_file = "cloud_init.cfg"
      volumes        = []

      kube_control_plane = false
      kube_node          = true
      etcd               = false
    },
    {
      name           = "k8s-worker-2"
      vcpu           = 2
      memory         = 8000                     # in MiB
      disk           = 100 * 1024 * 1024 * 1024 # 100 GB
      public_ip      = "192.168.8.103"
      private_ip     = "192.168.122.203"
      cloudinit_file = "cloud_init.cfg"
      volumes        = []

      kube_control_plane = false
      kube_node          = true
      etcd               = false
    },
  ]
}

参考資料

Terraform および Dynamic Inventory 関連

libvirt の設定関連

Kuberspray 関連

ブリッジ設定関連

関連記事

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?