kubeadmでovn-kubernetesのクラスタを構築

Table of Contents

1. はじめに

今回は、我が家の自宅サーバーで ovn-kubernetes を使ったKubernetesクラスタを構築した話をしようと思います!

kubeadm, Ansibleを使った自前のKubernetesクラスタ構築や、ovn-kubernetesに興味がある方の参考になれば幸いです。

2. 環境構成

今回の構築環境はこんな感じです。 Nodeは全てProxmox VE上の仮想マシンを利用しています。

  • Node 構成
    • Control Plane Node: 1台(2vCPU、4GiB メモリ)
    • Worker Node: 2台(4vCPU、8GiB メモリ)
  • OS: Ubuntu 24.04 Noble
  • Kubernetes: v1.33
  • コンテナランタイム: containerd 2.1.4
  • ネットワークアドオン: OVN-Kubernetes v1.1.0

Nodeに必要な要件は公式ドキュメントの kubeadmのインストール#始める前に に記載されているので、構築する際はそちらも参考にしてください。

3. 構築手順

実際の構築は全てAnsible Playbookで記述しました。 Proxmox VMを起動して2つのPlaybookを順番に実行するだけで、Kubernetesクラスタが構築されるようになっています。

  1. cluster_bootstrap.yml: Control Planeの初期化とOVN-Kubernetesのインストール
  2. worker_join.yml: Worker NodeをClusterに参加させる

3.1 Playbook構成

Ansible Playbookは以下のような構成になっています。

.
├── group_vars
│   └── all.yml
│── roles/
│   ├── common/              # 全ノード共通の設定
│   │   ├── tasks/
│   │   │   ├── main.yml
│   │   │   ├── swap_off.yml # swapの無効化
│   │   │   ├── resolve.yml  # systemd-resolved設定
│   │   │   ├── containerd.yml # containerdインストール
│   │   │   ├── runc.yml     # runcインストール
│   │   │   ├── cni.yml      # CNIプラグインインストール
│   │   │   └── kube.yml     # Kubernetes関連パッケージ
│   ├── cluster_bootstrap/   # Control Plane初期化
│   │   ├── tasks/
│   │   │   ├── main.yml
│   │   │   ├── kube.yml     # kubeadm init実行
│   │   │   ├── helm.yml     # Helmインストール
│   │   │   └── ovn_kubernetes.yml # OVN-Kubernetes設定
│   └── worker_join/        # Worker Nodeをクラスタに参加
│       └── tasks/
│           └── main.yml     # kubeadm joinの実行
├── cluster_bootstrap.yml
├── inventory.yml
├── requirements.txt
├── requirements.yml
└── worker_join.yml

ここからは、各タスクについて抜粋して解説していきます。

3.2 必要パッケージのインストール

まず、各ノードで必要なパッケージをインストールするところから始まります。

# roles/common/tasks/main.yml

- name: Install packages
  ansible.builtin.apt:
    name:
      # Utilities
      - tree
      - jq
      - yq
      # Kubernetes dependencies
      - apt-transport-https
      - ca-certificates
      - curl
      - gpg
      # Helm
      - apt-transport-https
      - python3-kubernetes
    state: present
    update_cache: true

インストールするのはホスト上での操作をする際に便利なコマンド群や、Kubernetes, Helm等の依存パッケージです。

3.3 swapの無効化

Kubernetesではswapを無効にすることが求められています。 そのためswapoffを実行し、/etc/fstabからも削除します。

Linuxでswapを無効化・有効化する #Ubuntu - Qiita

# roles/common/tasks/swap_off.yml

- name: Remove swap in fstab
  ansible.posix.mount:
    name: swap
    fstype: swap
    state: absent

- name: Swapoff
  ansible.builtin.command:
    cmd: swapoff -a
  when: ansible_swaptotal_mb > 0
  register: swapoff_result
  changed_when: false
  failed_when: swapoff_result.rc != 0

ただし最近のKubernetesでは設定次第でswapを利用することもできるみたいです。

https://kubernetes.io/docs/concepts/cluster-administration/swap-memory-management/#

3.4 カーネルモジュールとsysctl設定

コンテナネットワークに必要なカーネルモジュールの有効化とsysctl設定を行います。

コンテナランタイム | Kubernetes #IPv4フォワーディングを有効化し、iptablesからブリッジされたトラフィックを見えるようにする

# roles/common/tasks/main.yml

- name: Enable modules
  community.general.modprobe:
    name: "{{ item }}"
    persistent: present
  loop:
    - overlay
    - br_netfilter

- name: Update sysctl
  ansible.posix.sysctl:
    name: "{{ item }}"
    value: "1"
    sysctl_file: /etc/sysctl.d/k8s.conf
    sysctl_set: true
  loop:
    - net.bridge.bridge-nf-call-iptables
    - net.bridge.bridge-nf-call-ip6tables
    - net.ipv4.ip_forward

3.5 containerdのインストール

コンテナランタイムとしてcontainerdをインストールします。公式のバイナリを直接ダウンロードして配置しています。

# roles/common/tasks/containerd.yml

- name: Create containerd dir
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: root
    group: root
    mode: "0755"
  loop:
    - "{{ containerd.src_dir }}"
    - "{{ containerd.service_dir }}"
    - /etc/containerd

- name: Get containerd sha256
  ansible.builtin.uri:
    url: "{{ containerd.release_url }}/v{{ containerd.version }}/containerd-{{ containerd.version }}-{{ os }}-{{ arch }}.tar.gz.sha256sum"
    return_content: true
  register: containerd_sha256

- name: Set containerd sha256
  ansible.builtin.set_fact:
    containerd_sha256: "{{ (containerd_sha256.content | regex_search('([a-f0-9]{64})\\s+containerd-' + containerd.version + '-' + os + '-' + arch + '\\.tar\\.gz', '\\1'))[0] }}"

- name: Get containerd package
  ansible.builtin.get_url:
    url: "{{ containerd.release_url }}/v{{ containerd.version }}/containerd-{{ containerd.version }}-{{ os }}-{{ arch }}.tar.gz"
    dest: "{{ containerd.src_dir }}/containerd.tar.gz"
    mode: "0644"
    checksum: "sha256:{{ containerd_sha256 }}"

- name: Extract containerd package
  ansible.builtin.unarchive:
    remote_src: true
    src: "{{ containerd.src_dir }}/containerd.tar.gz"
    dest: /usr/local
  notify: Restart containerd

- name: Get containerd service file
  ansible.builtin.get_url:
    url: "https://raw.githubusercontent.com/containerd/containerd/refs/tags/v{{ containerd.version }}/containerd.service"
    dest: "{{ containerd.service_dir }}/containerd.service"
    mode: "0644"
  notify: Restart containerd

- name: Deploy containerd config
  ansible.builtin.copy:
    src: containerd-config.toml
    dest: /etc/containerd/config.toml
    owner: root
    group: root
    mode: "0644"
  notify: Restart containerd

- name: Start containerd service
  ansible.builtin.systemd_service:
    name: containerd
    state: started
    enabled: true

ここではcontainerdがsystemd cgroupドライバーを利用するように設定しています。

https://kubernetes.io/ja/docs/setup/production-environment/container-runtimes/#containerd-systemd

# containerd-config.toml

[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
  ...
  [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
    SystemdCgroup = true

ちなみにこちら箇所の日本語ドキュメントは自分が先日翻訳させていただいたものです。 詳しくはこちらをご覧ください。

3.6 runcのインストール

コンテナランタイムの低レベルコンポーネントであるruncをインストールします。

# roles/common/tasks/runc.yml

- name: Create runc dir
  ansible.builtin.file:
    path: "{{ runc.src_dir }}"
    state: directory
    owner: root
    group: root
    mode: "0755"

- name: Get runc sha256
  ansible.builtin.uri:
    url: "{{ runc.release_url }}/v{{ runc.version }}/runc.sha256sum"
    return_content: true
  register: runc_sha256

- name: Set runc sha256
  ansible.builtin.set_fact:
    runc_sha256: "{{ (runc_sha256.content | regex_search('([a-f0-9]{64})\\s+runc\\.' + arch, '\\1'))[0] }}"

- name: Get runc package
  ansible.builtin.get_url:
    url: "{{ runc.release_url }}/v{{ runc.version }}/runc.{{ arch }}"
    dest: "{{ runc.src_dir }}/runc"
    mode: "0755"
    checksum: "sha256:{{ runc_sha256 }}"

- name: Install runc
  ansible.builtin.copy:
    remote_src: true
    src: "{{ runc.src_dir }}/runc"
    dest: /usr/local/sbin/runc
    owner: root
    group: root
    mode: "0755"

3.7 CNIプラグインのインストール

Container Network Interfaceプラグインをダウンロードして配置します。

- name: Create cni dirs
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: root
    group: root
    mode: "0755"
  loop:
    - "{{ cni.src_dir }}"
    - "{{ cni.bin_dir }}"

- name: Get cni sha256
  ansible.builtin.uri:
    url: "{{ cni.release_url }}/v{{ cni.version }}/cni-plugins-{{ os }}-{{ arch }}-v{{ cni.version }}.tgz.sha256"
    return_content: true
  register: cni_sha256

- name: Set cni sha256
  ansible.builtin.set_fact:
    cni_sha256: "{{ (cni_sha256.content | regex_search('([a-f0-9]{64})\\s+cni-plugins-' + os + '-' + arch + '-v' + cni.version + '\\.tgz', '\\1'))[0] }}"

- name: Get cni package
  ansible.builtin.get_url:
    url: "{{ cni.release_url }}/v{{ cni.version }}/cni-plugins-{{ os }}-{{ arch }}-v{{ cni.version }}.tgz"
    dest: "{{ cni.src_dir }}/cni-plugins.tgz"
    mode: "0644"
    checksum: "sha256:{{ cni_sha256 }}"

- name: Extract cni package
  ansible.builtin.unarchive:
    remote_src: true
    src: "{{ cni.src_dir }}/cni-plugins.tgz"
    dest: "{{ cni.bin_dir }}"

3.8 Kubernetesパッケージのインストール

kubeadm、kubelet、kubectlをインストールします。

kubeadmのインストール | Kubernetes #kubeadm、kubelet、kubectlのインストール

- name: Download Kubernetes GPG key
  ansible.builtin.uri:
    url: "https://pkgs.k8s.io/core:/stable:/v{{ kubernetes.version }}/deb/Release.key"
    return_content: true
  register: kubernetes_gpg_key

- name: Convert GPG key to binary format
  ansible.builtin.shell:
    cmd: "gpg --dearmor > {{ kubernetes.gpg_path }}"
    creates: "{{ kubernetes.gpg_path }}"
  args:
    stdin: "{{ kubernetes_gpg_key.content }}"

- name: Add Kubernetes repository
  ansible.builtin.apt_repository:
    repo: "deb [signed-by={{ kubernetes.gpg_path }}] https://pkgs.k8s.io/core:/stable:/v{{ kubernetes.version }}/deb/ /"
    state: present
    filename: kubernetes

- name: Install Kubernetes packages
  ansible.builtin.apt:
    name:
      - kubelet
      - kubeadm
      - kubectl
    state: present
    update_cache: true

- name: Hold Kubernetes packages
  ansible.builtin.dpkg_selections:
    name: "{{ item }}"
    selection: hold
  loop:
    - kubelet
    - kubeadm
    - kubectl

- name: Create kubelet config to use systemd-resolved upstream
  ansible.builtin.copy:
    dest: /etc/default/kubelet
    content: "KUBELET_EXTRA_ARGS=--resolv-conf={{ resolve_conf_path }}"
    owner: root
    group: root
    mode: "0644"
    backup: true

最後の/etc/default/kubeletの設定は、kubelet実行時のオプションでスタブリゾルバじゃないリゾルバに向けさせるためのものです。 /etc/default/kubeletに環境変数を書いておくことで、systemdがkubelet起動時に引数に渡してくれるようになっていたので、その仕組みを利用しました。

https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/kubelet-integration/#the-kubelet-drop-in-file-for-systemd

また、公式の日本語ドキュメントにはDocker以外のコンテナランタイムを使う際には KUBELET_EXTRA_ARGS=--cgroup-driver=<value> を設定するように書かれていますが、こちらのページにある通り、v1.22以降ではデフォルトでsystemd cgroupドライバーが使われるようになっているため、特に設定する必要はありませんでした。

3.9 Control Plane初期化(cluster_bootstrap Role)

ここからはControl Planeノードでのみ実行される処理です。クラスタを初期化します。

# roles/cluster_bootstrap/tasks/kube.yml

- name: Init cluster
  when: ansible_hostname == groups['masters'][0]
  block:
    - name: Kubeadm init
      ansible.builtin.command:
        # Skip kube-proxy addon, as we will use OVN-Kubernetes for networking.
        cmd: >
          kubeadm init
            --control-plane-endpoint {{ kubernetes.control_plane_endpoint }}
            --apiserver-advertise-address {{ ansible_host }}
            --skip-phases=addon/kube-proxy
        creates: /etc/kubernetes/admin.conf
      register: kubeadm_init
      run_once: true
      failed_when: kubeadm_init.rc != 0

    - name: Create .kube directory
      ansible.builtin.file:
        path: "{{ item.dest }}"
        state: directory
        owner: "{{ item.owner }}"
        group: "{{ item.group }}"
        mode: "0755"
      loop:
        - dest: /root/.kube
          owner: root
          group: root
        - dest: "/home/{{ ansible_user }}/.kube"
          owner: "{{ ansible_user }}"
          group: "{{ ansible_user }}"

    - name: Copy admin.conf to .kube/config
      ansible.builtin.copy:
        src: /etc/kubernetes/admin.conf
        dest: "{{ item.dest }}"
        owner: "{{ item.owner }}"
        group: "{{ item.group }}"
        mode: "0600"
        remote_src: true
      loop:
        - dest: /root/.kube/config
          owner: root
          group: root
        - dest: "/home/{{ ansible_user }}/.kube/config"
          owner: "{{ ansible_user }}"
          group: "{{ ansible_user }}"

when: ansible_hostname == groups['masters'][0] を指定することで、間違えて他のNodeで実行しても処理がスキップされるようにしています。

また、今回はKubernetes Serviceのネットワーク処理もovn-kubernetesで行われるため、--skip-phases=addon/kube-proxy オプションでkube-proxyのインストールをスキップしています。

3.10 OVN-Kubernetesのインストール

HelmでOVN-Kubernetesをインストールします。

# roles/cluster_bootstrap/tasks/ovn_kubernetes.yml

- name: Clone ovn-kubernetes repository
  ansible.builtin.git:
    repo: https://github.com/ovn-kubernetes/ovn-kubernetes.git
    dest: /tmp/ovn-kubernetes
    version: "{{ ovn_kubernetes.version }}"
    force: true

- name: Create ovn-kubernetes namespace
  kubernetes.core.k8s:
    name: "{{ ovn_kubernetes.namespace }}"
    api_version: v1
    kind: Namespace
    state: present

- name: Install ovn-kubernetes helm chart
  kubernetes.core.helm:
    name: ovn-kubernetes
    chart_ref: /tmp/ovn-kubernetes/helm/ovn-kubernetes
    release_namespace: "{{ ovn_kubernetes.namespace }}"
    values_files:
      - /tmp/ovn-kubernetes/helm/ovn-kubernetes/values-no-ic.yaml
    values:
      k8sAPIServer: "https://{{ kubernetes.control_plane_endpoint }}:6443"
      global:
        image:
          repository: ghcr.io/ovn-kubernetes/ovn-kubernetes/ovn-kube-ubuntu
          tag: "{{ ovn_kubernetes.image_version }}"

ovn-kubernetesのHelm installは公式のガイドがあったためそちらを参考にしつつ、バージョンだけちゃんと設定するようにしています。

https://ovn-kubernetes.io/installation/launching-ovn-kubernetes-with-helm/

インストールが完了すると、ovn-kubernetesのPodが起動していることが確認できます。

$ kubectl -n ovn-kubernetes get pods
NAME                              READY   STATUS    RESTARTS   AGE
ovnkube-db-894fd95bd-z2nvt        2/2     Running   0          19h
ovnkube-identity-dcfmg            1/1     Running   0          19h
ovnkube-master-669cfc9d94-zg45m   2/2     Running   0          19h
ovnkube-node-7gvnt                3/3     Running   0          19h
ovnkube-node-897g9                3/3     Running   0          19h
ovnkube-node-tk9sk                3/3     Running   0          19h
ovs-node-m2wg2                    1/1     Running   0          19h
ovs-node-ncc52                    1/1     Running   0          19h
ovs-node-zk7nz                    1/1     Running   0          19h

3.11 Worker Nodeの参加(worker_join Role)

続いてWorker Nodeをクラスタに参加させます。 Control Plane Nodeでjoinコマンドを生成して、それを各Worker Nodeで実行するようにしています。

# roles/worker_join/tasks/main.yml

- name: Generate join tokens
  ansible.builtin.command:
    cmd: kubeadm token create --print-join-command
  when: ansible_hostname == groups['masters'][0]
  changed_when: false
  register: kubeadm_join_command

- name: Join cluster
  when: "'workers' in group_names"
  block:
    - name: Join the node to the Kubernetes cluster
      ansible.builtin.command:
        cmd: >
          {{ hostvars[groups['masters'][0]].kubeadm_join_command.stdout }}
        creates: /etc/kubernetes/kubelet.conf
      register: join_result
      failed_when: join_result.rc != 0

    - name: Create .kube directory
      ansible.builtin.file:
        path: "{{ item.dest }}"
        state: directory
        owner: "{{ item.owner }}"
        group: "{{ item.group }}"
        mode: "0755"
      loop:
        - dest: /root/.kube
          owner: root
          group: root
        - dest: "/home/{{ ansible_user }}/.kube"
          owner: "{{ ansible_user }}"
          group: "{{ ansible_user }}"

    - name: Copy admin.conf to .kube/config
      ansible.builtin.copy:
        src: /etc/kubernetes/kubelet.conf
        dest: "{{ item.dest }}"
        owner: "{{ item.owner }}"
        group: "{{ item.group }}"
        mode: "0600"
        remote_src: true
      loop:
        - dest: /root/.kube/config
          owner: root
          group: root
        - dest: "/home/{{ ansible_user }}/.kube/config"
          owner: "{{ ansible_user }}"
          group: "{{ ansible_user }}"

hostvars[groups['masters'][0]].kubeadm_join_command.stdout のように参照することで、Control Plane Nodeで生成されたjoinコマンドをWorker Nodeで実行することができます。 ただし、ストラテジープラグインで “free” や “host_pinned” を使っている場合、Control Plane Nodeが常に先に実行されるとは限らないため注意が必要です。 デフォルトのストラテジープラグインである “linear” を使うのが無難でしょう。

ここまでの手順でクラスタを構築することで、Control Plane Node1台、Worker Node2台の合計3台のNodeがあることを確認することができます。

$ kubectl get nodes
NAME                STATUS   ROLES           AGE   VERSION
home-k8s-master01   Ready    control-plane   19h   v1.33.4
home-k8s-worker01   Ready    <none>          19h   v1.33.4
home-k8s-worker02   Ready    <none>          18h   v1.33.4

4. ハマりポイント

構築時にハマったポイントを一つ紹介します。

今回最初にクラスタを構築した際、なぜかNode上で名前解決が一切できなくなってしまう問題が発生しました。 直接正しいネームサーバーを指定してdigを実行すると名前解決ができるため、スタブリゾルバーが何か悪さをしていそうです。

調べてみると、公式ドキュメントにもこのような記載がありました。

Some Linux distributions (e.g. Ubuntu) use a local DNS resolver by default (systemd-resolved). Systemd-resolved moves and replaces /etc/resolv.conf with a stub file that can cause a fatal forwarding loop when resolving names in upstream servers. This can be fixed manually by using kubelet’s –resolv-conf flag to point to the correct resolv.conf (With systemd-resolved, this is /run/systemd/resolve/resolv.conf). kubeadm automatically detects systemd-resolved, and adjusts the kubelet flags accordingly.

Systemd-resolvedがスタブリゾルバを使っているせいで、名前解決のフォワーディングループが発生してしまうようです。kubeadm automatically detects systemd-resolved, and adjusts the kubelet flags accordingly. と書いてありますが、自動で解決はしてくれなかったので以下のような設定を入れました:

# roles/common/tasks/resolve.yml

- name: Create systemd-resolved configuration
  ansible.builtin.copy:
    src: resolved.conf
    dest: /etc/systemd/resolved.conf
    owner: root
    group: root
    mode: "0644"
    backup: true
  notify: Restart systemd-resolved

- name: Link resolv.conf to systemd-resolved upstream
  ansible.builtin.file:
    src: "{{ resolve_conf_path }}" # /run/systemd/resolve/resolv.conf
    dest: /etc/resolv.conf
    state: link
    force: true
# resolved.conf

#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it under the
#  terms of the GNU Lesser General Public License as published by the Free
#  Software Foundation; either version 2.1 of the License, or (at your option)
#  any later version.
#
# Entries in this file show the compile time defaults. Local configuration
# should be created by either modifying this file (or a copy of it placed in
# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in
# the /etc/systemd/resolved.conf.d/ directory. The latter is generally
# recommended. Defaults can be restored by simply deleting the main
# configuration file and all drop-ins located in /etc/.
#
# Use 'systemd-analyze cat-config systemd/resolved.conf' to display the full config.
#
# See resolved.conf(5) for details.

[Resolve]
# Some examples of DNS servers which may be used for DNS= and FallbackDNS=:
# Cloudflare: 1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com
# Google:     8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google
# Quad9:      9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net
#DNS=
#FallbackDNS=
#Domains=
#DNSSEC=no
#DNSOverTLS=no
#MulticastDNS=no
#LLMNR=no
#Cache=no-negative
#CacheFromLocalhost=no
DNSStubListener=no
#DNSStubListenerExtra=
#ReadEtcHosts=yes
#ResolveUnicastSingleLabel=no
#StaleRetentionSec=0

こちらの設定により、kubeletがスタブリゾルバではない /run/systemd/resolve/resolv.conf を参照するようにしました。また、念のためsystemd-resolvedの方でもスタブリゾルバーを使わないようにしています。(DNSStubListener=no)

5. 動作確認

クラスタが正常に動作することを確認するため、簡単なDeploymentとNodePort Serviceを作成してテストしました。

$ kubectl create deployment nginx --image=nginx:latest
deployment.apps/nginx created

$ kubectl expose deployment nginx --type=NodePort --port=80
service/nginx exposed

$ kubectl get pods,svc
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-54c98b4f84-fgn5b   1/1     Running   0          2m41s

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
service/kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        19h
service/nginx        NodePort    10.105.216.4   <none>        80:32643/TCP   2m29s

$ curl kube.home.tomokon.net:32643
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

正常に動作することが確認できました。 ovn-kubernetesでServiceのネットワーク処理もちゃんとできているようです。

6. まとめ

今回は自宅サーバ上でkubeadmとOVN-Kubernetesを使ったKubernetesクラスタ構築をしてみました! 自前のKubernetes構築は楽しいし勉強にもなりますね。今後はControl PlaneのHA構成や、構築したKubernetesクラスタ上でのOpenStackや監視基盤の構築に取り組んでいきたいと思います。

最後まで読んでいただき、ありがとうございました!