はじめに
Kubernetesクラスターをオンプレやクラウド上の仮想マシンの上に構築する場合、Kubernetes The Hard Wayのようにイチから全てを構築するか、構築用のツールを利用する方法があります。Kubernetesクラスターを構築するツールは複数存在し、代表的なものとしては以下のようなものがあります。
- Kubeadm: Kubernetesが公式で提供するツール。
- Kops: Kubernetes Operations。現在はAWSを公式にサポートするツール。
- Rancher: Rancher Labsの提供するマルチクラスター構成ツール。
- Kubespray: Ansibleを利用してKubernetesクラスターを構築する。
このうち今回紹介するのはKubesprayになります。
KubesprayはAnsibleを利用するクラスター構築ツールです。Ansibleは構成管理ツールとしてとても有名ですが、私はこれまで触れたことがありませんでした。今回はKubesprayを通じてAnsibleに触れ、合わせてKubesprayの特徴を紹介したいと思います。
Kubesprayとは
Kubesprayは以下のような特徴を備えたKubernetesクラスター管理ツールであり、本番環境で利用するクラスターを構築する場合にも利用できます。
- 構成管理ツールであるAnsibleを利用
- パブリッククラウドからベアメタルまで様々な環境に対応
- HAクラスターを容易に構成可能
- 一般的なLinuxディストリビューションでの利用をカバー。
- 複数のNetwork pluginに対応
各特徴について、もう少し見ていきます。
Ansibleを利用
Ansibleは、いわゆる構成管理自動化ツールの一つです。AnsibleはPythonで書かれており、他の構成管理ツールと比べて以下のような特徴を備えています。
- YAMLファイルを利用することでコンフィグファイルの可読性が高い
- 対象の機器にSSHでアクセスできれば適用可能であり、エージェントレス
- モジュールが豊富に備わっており、様々なレイヤーに適用させた自動化を実現できる
- シンプルな処理を実行するのに向いている
※参考: Ansible実践ガイド第3版 (impress top gearシリーズ
幅広い環境にクラスターを構築できる
KubesprayはAnsibleを利用してKubernetesクラスターを構築します。Ansibleは各サーバーに対してSSH接続できる環境であれば適用可能なため、クラウドからオンプレまで幅広く適用できます。公式ドキュメントでは、利用できる環境について、以下のように紹介しています。
HAクラスターの構築
Kubernetesクラスターを構築する場合、etcd・kube-apiserverを持つクラスターを複数台用意する必要があります。上記コンポーネントはMasterノードが保持する場合が多く、Kubesprayでは構築時に利用するYAMLファイルに必要なMasterノードを記載するだけで複数台のMasterノードを構築することができます。またetcdはMasterノードとは別に構築することも可能です。 ただし、kube-apiserverを複数用意する場合、Kubernetesコンポーネントがkube-apiserverにアクセスするためにロードバランサーを用意する必要があります。KubesprayではNginxベースのサポートを用意しており、デフォルトでそちらを利用することができます。
Kubeadmなどではkube-apiserver用のロードバランサーを構築者が用意する必要があるため、この辺りはKubesprayを利用することで得られるメリットと言えそうです。
様々なLinuxディストリビューションに対応
Kubesprayは、多くのLinuxディストリビューションに対応しています。Upstart/SysVinitベースのOSには対応していませんが、以下のLinuxディストリビューションをサポートしています。
- Container Linux
- Debian Buster, Jessie, Stretch, Wheezy
- Ubuntu 16.04, 18.04
- CentOS/RHEL 7
- Fedora 28
- Fedora/CentOS Atomic
- openSUSE Leap 42.3/Tumbleweed
- Oracle Linux 7
複数のNetwork Pluginに対応
Kubernetesクラスターを利用する際、異なるノードに配置されたPod間で通信を行うため、Network pluginをデプロイする必要があります。Kubesprayでは複数のNetwork pluginをサポートしています。
- flannel
- calico
- canal
- cilium
- contiv
- weave
- kube-ovn
- kube-router
- macvlan
- multus
※参考リンク:
Kubernetes公式ドキュメント - Cluster Networking
Kubesprayと別ツールとの比較
最初に書いた通り、Kubernetesクラスターを自前で用意する方法はKubesprayだけではありません。Kubesprayと他のツールとの違いは何なのか、公式ドキュメントで紹介しています。ここではkopsとKubeadmに対する違いを説明しており、以下にその内容を紹介します。
Kopsとの比較
KubesprayはAnsibleを利用してクラスターの構築・構成管理を行うため、クラウドからベアメタルまで様々な環境に適用することができます。それに対してKopsは、kops自身が構築・管理を行い、Kubesprayに比べると適用できる範囲は狭くなってしまいます(この記事を書いている時点ではAWSを公式サポートとしています)。
Ansibleに慣れ親しんでいるユーザー、あるいはマルチプラットフォーム上でKubernetesクラスターを構築したいユーザーにとっては、Kubesprayの方が良い選択肢となるでしょう。一方、Kopsはサポートするクラウドプロバイダーとの連携がより密接なため、クラスター構築時の各種リソースのプロビジョニング、クラウドプロバイダーの提供サービスとKubernetesとの連携強化などを望めます。上記理由から、将来的に一つのプラットフォーム上でKubernetesクラスターを利用する場合、Kopsの方が良い選択肢となります。
※参考リンク:Kops公式ページ
Kubeadmとの比較
KubeadmはKubernetesクラスターのライフサイクルマネジメント(Kubernetes自身がKubernetesを管理するself-hosted layout、動的なディスカバリサービスなど)に関するナレッジを提供します。現在のKubernetes Operatorが普及した世界に当てはめれば、Kubeadmの事を「Kubernetes cluster Operator」と名付けることができるかもしれません。一方でKubesprayは「OS Operator」とも呼べるAnsibleを利用し、クラスターを構築する際はより一般的な構成管理タスクを実行します。
Kubesprayではver 2.3からKubeadmをサポートしており、クラスター構築の際はKubeadmの機能を利用しています。またver 2.8からはKubeadmを利用しないクラスター構築の機能を廃止する動きを進めつつ、最終的には両者からの恩恵を受けることを考えているようです。この記事を書いている時点ではすでにver 2.12になっており、Kubeadmへの統合はかなり進んでいると予想されます。
※参考リンク:
Creating a single control-plane cluster with kubeadm
Kubernetes Meetup Tokyo #8 Self-Hosted Kubernetesを調べてみた
Kubesprayを使ってクラスターを構築
ここからKubesprayを使ったクラスター構築を進めます。
Kubesprayを利用する条件
Kubesprayを実行するにあたり、実行の条件を確認します。 公式ドキュメントのこちらのページを参照すると、各種条件が記載されています。
Ansibleコマンド用マシン
- Ansible: v2.7.8
- python-netaddr
- Jinja: 2.9以上
Kubernetesクラスター用マシン
- Dockerイメージをpullするため、インターネットに接続できる
- IPv4 Forwardingを許可
- 操作用マシンとSSH通信を行うため、各サーバにSSH keyがコピーされている
- Firewallが無効化されている(推奨)
- rootユーザー以外でで実行する場合は権限の追加が必要
- マシン性能
- Masterノード: メモリ1.5GB以上
- Workerノード: メモリ1.0GB以上
検証環境
検証環境のついての情報を紹介します。今回はAzure上に仮想マシンを構築し、そこにKubesprayを使ってKubernetesクラスターを構築します。
項目1 | 項目2 | 内容 |
---|---|---|
仮想マシン | ||
OS | CentOS 7.7 | |
インスタンスサイズ | D2s v3 | |
Kubernetesクラスター | ||
Masterノード数 | 1 | |
Workerノード数 | 2 | |
Network plugin | Calico (デフォルト) | |
Ansible用サーバー | ||
サーバー台数 | 1 |
構築手順
ここから実際の構築手順を紹介します。手順はこちらの公式ドキュメントを参照しながら進めます。
構築①:各ノードでの構築準備
Ansibleコマンドを実行する仮想マシン上で以下の作業を行います。
yum update
- IPv4フォワーディングの有効化
- FireWallの無効化
- SELinuxの無効化
- 発行したsshキーの公開鍵情報をコピーする (
ssh-copy-id
コマンドなど)- 以降の手順では、Ansible実行サーバーから各マシンのrootユーザーにssh接続ができる必要があります
構築②:Ansible実行サーバーの準備
Ansibleコマンドを実行するサーバーの準備を行います。その前に、Ansibleを実行する上で何が必要かを紹介します。
Ansibleに必要なもの
Ansibleはコントロールノードからターゲットノードに対してSSHで処理内容を送り、実行します。今回の場合、コントロールノードはAnsibleコマンド実行用に用意したサーバー、ターゲットノードはKubernetesクラスター用のサーバーになります。
Ansibleを実行するには2種類のファイルを用意する必要があり、それぞれInventoryファイルとPlaybookファイルになります。
Inventoryファイル:ターゲットノードのリストを記載するファイル。ターゲットへの接続情報としてホスト名やIPアドレスなどを記載します。
Playbookファイル:ターゲットノードへの処理内容を記載するファイル。タスクを定義し、Inventoryファイルで定義したホストのグループと紐付けます。実行時、Playbookで定義されたタスクを、紐付けたホストグループに対して実行します。PlaybookはYAML形式で記述します。
Module:Playbookで定義するタスクは、モジュールを呼び出して実行します。モジュールは各作業内容を実行するために必要な処理をまとめたユニットです。Ansibleが公式に利用するモジュールに加え、ユーザーがモジュールを作成することもできます。
Ansible実行サーバーでの準備
ここからKubesprayを実行する上で必要なファイルを用意します。
まずはGitHubリポジトリから必要なファイルを取得し、Kubesprayの実行に必要なファイルを取得します。またAnsibleを実行するために必要なモジュールをインストールするため、リポジトリに含まれるrequirements.txt
に書かれたモジュールをインストールします。
なお、検証時点ではCentOS7はデフォルトでPython2.7を利用しており、pipコマンドを実行できません。そのため、必要に応じてPython3のインストールを行います。
# git clone [root@kubespray-ansible ~]# git clone https://github.com/kubernetes-sigs/kubespray.git # pip install [root@kubespray-ansible kubespray]# pip3 install -r requirements.txt (中略) Successfully installed MarkupSafe-1.1.1 PyYAML-5.2 ansible-2.7.12 bcrypt-3.1.7 certifi-2019.11.28 cffi-1.13.2 chardet-3.0.4 cryptography-2.8 hvac-0.8.2 idna-2.8 jinja2-2.10.1 jmespath-0.9.4 netaddr-0.7.19 paramiko-2.7.1 pbr-5.2.0 pycparser-2.19 pynacl-1.3.0 requests-2.22.0 ruamel.yaml-0.15.96 six-1.13.0 urllib3-1.25.7 # インストール後の確認 [root@kubespray-ansible kubespray]# ansible --version ansible 2.7.12 config file = /root/kubespray/ansible.cfg configured module search path = ['/root/kubespray/library'] ansible python module location = /usr/local/lib/python3.6/site-packages/ansible executable location = /usr/local/bin/ansible python version = 3.6.8 (default, Aug 7 2019, 17:28:10) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
Inventory/Playbookの内容確認・書き換え
次にクラスターインストール時に利用するInventory・Playbookを書き換えます。はじめに各ファイルの内容を確認します。
まずはInventoryとして利用するhosts.yml
ファイルです。Kubesprayではinventory.py
を実行することで生成されます。以下にMasterノード・Workerノード1つずつの例を載せます。
all: hosts: node1: ansible_host: <node1のIPアドレス> ip: <node1のIPアドレス> access_ip: <node1のIPアドレス> node2: ansible_host: <node2のIPアドレス> ip: <node2のIPアドレス> access_ip: <node2のIPアドレス> children: kube-master: hosts: node1: kube-node: hosts: node2: etcd: hosts: node1: k8s-cluster: children: kube-master: kube-node: calico-rr: hosts: {}
上記ファイルを眺めると、以下のような階層構造になっていることがわかります。
all | --hosts | --children | |--kube-master |--kube-node |--etcd |--k8s-cluster | | | --children | | | --kube-master | --kube-worker |--calico-rr
Inventoryではグループを階層構造で表すことができます。上位のグループが下位のグループを束ねることで階層構造を作り、複数のグループに共通のタスクを割り当てることができます。
hosts
にはKubernetesのノードとして利用するマシンのIPアドレスを指定し、children
以下で各hostに与えるタスクを指定します。
今回node1
はMasterノードとして利用するのでkube-master
etcd
にて指定し、node2
はWorkerノードとして利用するのでkube-worker
にて指定します。
k8s-cluster
ではKubernetesのノードとして利用するホストをグループで指定します(ここではkube-master
kube-worker
)。
なおcalico-rr
は通常利用することはありませんが、例えば大規模クラスターを構築し、一部ノード間での通信を許可したくない場合などに利用するようです。詳細は公式ドキュメントのCalicoのパートをご覧ください。
またInventoryとして利用できるファイルはhosts.yml
だけではありません。Kubesprayではあらかじめinventory.ini
ファイルを用意しています。以下にファイルの内容を載せます。
# ## Configure 'ip' variable to bind kubernetes services on a # ## different ip than the default iface # ## We should set etcd_member_name for etcd cluster. The node that is not a etcd member do not need to set the value, or can set the empty string value. [all] # node1 ansible_host=95.54.0.12 # ip=10.3.0.1 etcd_member_name=etcd1 # node2 ansible_host=95.54.0.13 # ip=10.3.0.2 etcd_member_name=etcd2 # node3 ansible_host=95.54.0.14 # ip=10.3.0.3 etcd_member_name=etcd3 # node4 ansible_host=95.54.0.15 # ip=10.3.0.4 etcd_member_name=etcd4 # node5 ansible_host=95.54.0.16 # ip=10.3.0.5 etcd_member_name=etcd5 # node6 ansible_host=95.54.0.17 # ip=10.3.0.6 etcd_member_name=etcd6 # ## configure a bastion host if your nodes are not directly reachable # bastion ansible_host=x.x.x.x ansible_user=some_user [kube-master] # node1 # node2 [etcd] # node1 # node2 # node3 [kube-node] # node2 # node3 # node4 # node5 # node6 [calico-rr] [k8s-cluster:children] kube-master kube-node calico-rr
上記ファイルを眺めると、hosts.yml
と同じようなカテゴリーが並んでいることが分かるかと思います。それぞれのカテゴリーの関係性は上記階層構造と同様になります。
続いてPlaybookとして利用するcluster.yml
ファイルは以下の通りです。
--- - hosts: localhost gather_facts: false become: no tasks: - name: "Check ansible version >=2.7.8" assert: msg: "Ansible must be v2.7.8 or higher" that: - ansible_version.string is version("2.7.8", ">=") tags: - check vars: ansible_connection: local - hosts: bastion[0] gather_facts: False roles: - { role: kubespray-defaults} - { role: bastion-ssh-config, tags: ["localhost", "bastion"]} - hosts: k8s-cluster:etcd any_errors_fatal: "{{ any_errors_fatal | default(true) }}" gather_facts: false roles: - { role: kubespray-defaults} - { role: bootstrap-os, tags: bootstrap-os} - hosts: k8s-cluster:etcd any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes/preinstall, tags: preinstall } - { role: "container-engine", tags: "container-engine", when: deploy_container_engine|default(true) } - { role: download, tags: download, when: "not skip_downloads" } environment: "{{ proxy_env }}" - hosts: etcd any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - role: etcd tags: etcd vars: etcd_cluster_setup: true etcd_events_cluster_setup: "{{ etcd_events_cluster_enabled }}" when: not etcd_kubeadm_enabled| default(false) - hosts: k8s-cluster any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - role: etcd tags: etcd vars: etcd_cluster_setup: false etcd_events_cluster_setup: false when: not etcd_kubeadm_enabled| default(false) - hosts: k8s-cluster any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes/node, tags: node } environment: "{{ proxy_env }}" - hosts: kube-master any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes/master, tags: master } - { role: kubernetes/client, tags: client } - { role: kubernetes-apps/cluster_roles, tags: cluster-roles } - hosts: k8s-cluster any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes/kubeadm, tags: kubeadm} - { role: network_plugin, tags: network } - { role: kubernetes/node-label } - hosts: calico-rr any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: network_plugin/calico/rr, tags: ['network', 'calico_rr']} - hosts: kube-master[0] any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes-apps/rotate_tokens, tags: rotate_tokens, when: "secret_changed|default(false)" } - { role: win_nodes/kubernetes_patch, tags: ["master", "win_nodes"]} - hosts: kube-master any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes-apps/network_plugin, tags: network } - { role: kubernetes-apps/policy_controller, tags: policy-controller } - { role: kubernetes-apps/ingress_controller, tags: ingress-controller } - { role: kubernetes-apps/external_provisioner, tags: external-provisioner } - hosts: kube-master any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes-apps, tags: apps } environment: "{{ proxy_env }}" - hosts: k8s-cluster any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes/preinstall, when: "dns_mode != 'none' and resolvconf_mode == 'host_resolvconf'", tags: resolvconf, dns_late: true }
cluster.yml
にはKubernetesクラスターを構築する手順を記載しています。各タスクはKubesprayのroles
フォルダにて定義されています。
# rolesフォルダの内容確認 [root@ansible-eastus roles]# pwd /root/kubespray/roles [root@ansible-eastus roles]# ls -l total 4 drwxr-xr-x. 5 root root 47 Dec 18 12:23 adduser drwxr-xr-x. 4 root root 36 Dec 18 12:23 bastion-ssh-config drwxr-xr-x. 5 root root 65 Dec 18 12:23 bootstrap-os drwxr-xr-x. 7 root root 85 Dec 18 12:23 container-engine drwxr-xr-x. 6 root root 64 Dec 18 12:23 download drwxr-xr-x. 7 root root 80 Dec 18 12:23 etcd drwxr-xr-x. 9 root root 111 Dec 18 12:23 kubernetes drwxr-xr-x. 17 root root 4096 Dec 18 12:23 kubernetes-apps drwxr-xr-x. 5 root root 47 Dec 18 12:23 kubespray-defaults drwxr-xr-x. 15 root root 189 Dec 18 12:23 network_plugin drwxr-xr-x. 6 root root 71 Dec 18 12:23 recover_control_plane drwxr-xr-x. 4 root root 43 Dec 18 12:23 remove-node drwxr-xr-x. 4 root root 35 Dec 18 12:23 reset drwxr-xr-x. 4 root root 45 Dec 18 12:23 upgrade drwxr-xr-x. 3 root root 30 Dec 18 12:23 win_nodes [root@ansible-eastus roles]#
構築③:Inventory/Playbookファイルの準備
では、実際に利用するInventoryファイルを用意します。ここでは公式ドキュメントの手順に従います。
# サンプル用ファイルのコピー [root@ansible-eastus kubespray]# cp -r inventory/sample inventory/mycluster # inventory/myclusterの内容確認 [root@ansible-eastus kubespray]# ls -l inventory/mycluster/ total 4 drwxr-xr-x 4 root root 52 Dec 20 16:07 group_vars -rw-r--r-- 1 root root 994 Dec 20 16:07 inventory.ini # hosts.ymlファイルの用意 ## ノードとして利用するマシンのIPアドレスを指定 [root@ansible-eastus kubespray]# declare -a IPS=(10.3.0.6 10.3.0.7 10.3.0.8) ## hosts.ymlファイルを生成するpythonコマンドの実行 [root@ansible-eastus kubespray]# CONFIG_FILE=inventory/mycluster/hosts.yml python3 contrib/inventory_builder/inventory.py ${IPS[@]} DEBUG: Adding group all DEBUG: Adding group kube-master DEBUG: Adding group kube-node DEBUG: Adding group etcd DEBUG: Adding group k8s-cluster DEBUG: Adding group calico-rr DEBUG: adding host node1 to group all DEBUG: adding host node2 to group all DEBUG: adding host node3 to group all DEBUG: adding host node1 to group etcd DEBUG: adding host node2 to group etcd DEBUG: adding host node3 to group etcd DEBUG: adding host node1 to group kube-master DEBUG: adding host node2 to group kube-master DEBUG: adding host node1 to group kube-node DEBUG: adding host node2 to group kube-node DEBUG: adding host node3 to group kube-node
上記コマンドによって生成されたhosts.yml
ファイルが以下になります。
all: hosts: node1: ansible_host: 10.3.0.6 ip: 10.3.0.6 access_ip: 10.3.0.6 node2: ansible_host: 10.3.0.7 ip: 10.3.0.7 access_ip: 10.3.0.7 node3: ansible_host: 10.3.0.8 ip: 10.3.0.8 access_ip: 10.3.0.8 children: kube-master: hosts: node1: node2: kube-node: hosts: node1: node2: node3: etcd: hosts: node1: node2: node3: k8s-cluster: children: kube-master: kube-node: calico-rr: hosts: {}
上記ファイルを見ると、デフォルトでは指定したIPアドレスを持つマシンの多くがkube-master
kube-worker
etcd
のすべてのタスクを実行するよう指定されています。
またホストを指定する際に利用する名前がnode1
node2
node3
となっていますが、ここには各マシンのホスト名を指定します。デフォルトの状態でKubesprayを構築すると、指定したIPアドレスを持つマシンのホスト名がnode1
node2
node3
となるため、正しいホスト名を指定します。
今回は10.3.0.6
のIPアドレスを持つマシンをMasterノード、それ以外をWorkerノードとして利用するため、以下のように書き換えます。
all: hosts: k8s-master: # ホスト名の修正 ansible_host: 10.3.0.6 ip: 10.3.0.6 access_ip: 10.3.0.6 k8s-worker01: # ホスト名の修正 ansible_host: 10.3.0.7 ip: 10.3.0.7 access_ip: 10.3.0.7 k8s-worker02: # ホスト名の修正 ansible_host: 10.3.0.8 ip: 10.3.0.8 access_ip: 10.3.0.8 children: kube-master: hosts: # Masterノードとして利用するホストのみ指定 k8s-master: kube-node: hosts: # Workerノードとして利用するホストのみ指定 k8s-worker01: k8s-worker02: etcd: hosts: # Masterノードとして利用するホストのみ指定 k8s-master: k8s-cluster: children: kube-master: kube-node: calico-rr: hosts: {}
以上でInventoryの準備は完了です。
なお、上記手順で利用するinventory.py
は以下の通りです。
inventory.py
#!/usr/bin/env python3 # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # # Usage: inventory.py ip1 [ip2 ...] # Examples: inventory.py 10.10.1.3 10.10.1.4 10.10.1.5 # # Advanced usage: # Add another host after initial creation: inventory.py 10.10.1.5 # Add range of hosts: inventory.py 10.10.1.3-10.10.1.5 # Add hosts with different ip and access ip: # inventory.py 10.0.0.1,192.168.10.1 10.0.0.2,192.168.10.2 10.0.0.3,192.168.1.3 # Add hosts with a specific hostname, ip, and optional access ip: # inventory.py first,10.0.0.1,192.168.10.1 second,10.0.0.2 last,10.0.0.3 # Delete a host: inventory.py -10.10.1.3 # Delete a host by id: inventory.py -node1 # # Load a YAML or JSON file with inventory data: inventory.py load hosts.yaml # YAML file should be in the following format: # group1: # host1: # ip: X.X.X.X # var: val # group2: # host2: # ip: X.X.X.X from collections import OrderedDict from ipaddress import ip_address from ruamel.yaml import YAML import os import re import sys ROLES = ['all', 'kube-master', 'kube-node', 'etcd', 'k8s-cluster', 'calico-rr'] PROTECTED_NAMES = ROLES AVAILABLE_COMMANDS = ['help', 'print_cfg', 'print_ips', 'print_hostnames', 'load'] _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, '0': False, 'no': False, 'false': False, 'off': False} yaml = YAML() yaml.Representer.add_representer(OrderedDict, yaml.Representer.represent_dict) def get_var_as_bool(name, default): value = os.environ.get(name, '') return _boolean_states.get(value.lower(), default) # Configurable as shell vars start CONFIG_FILE = os.environ.get("CONFIG_FILE", "./inventory/sample/hosts.yaml") KUBE_MASTERS = int(os.environ.get("KUBE_MASTERS_MASTERS", 2)) # Reconfigures cluster distribution at scale SCALE_THRESHOLD = int(os.environ.get("SCALE_THRESHOLD", 50)) MASSIVE_SCALE_THRESHOLD = int(os.environ.get("SCALE_THRESHOLD", 200)) DEBUG = get_var_as_bool("DEBUG", True) HOST_PREFIX = os.environ.get("HOST_PREFIX", "node") # Configurable as shell vars end class KubesprayInventory(object): def __init__(self, changed_hosts=None, config_file=None): self.config_file = config_file self.yaml_config = {} if self.config_file: try: self.hosts_file = open(config_file, 'r') self.yaml_config = yaml.load(self.hosts_file) except OSError: pass if changed_hosts and changed_hosts[0] in AVAILABLE_COMMANDS: self.parse_command(changed_hosts[0], changed_hosts[1:]) sys.exit(0) self.ensure_required_groups(ROLES) if changed_hosts: changed_hosts = self.range2ips(changed_hosts) self.hosts = self.build_hostnames(changed_hosts) self.purge_invalid_hosts(self.hosts.keys(), PROTECTED_NAMES) self.set_all(self.hosts) self.set_k8s_cluster() etcd_hosts_count = 3 if len(self.hosts.keys()) >= 3 else 1 self.set_etcd(list(self.hosts.keys())[:etcd_hosts_count]) if len(self.hosts) >= SCALE_THRESHOLD: self.set_kube_master(list(self.hosts.keys())[ etcd_hosts_count:(etcd_hosts_count + KUBE_MASTERS)]) else: self.set_kube_master(list(self.hosts.keys())[:KUBE_MASTERS]) self.set_kube_node(self.hosts.keys()) if len(self.hosts) >= SCALE_THRESHOLD: self.set_calico_rr(list(self.hosts.keys())[:etcd_hosts_count]) else: # Show help if no options self.show_help() sys.exit(0) self.write_config(self.config_file) def write_config(self, config_file): if config_file: with open(self.config_file, 'w') as f: yaml.dump(self.yaml_config, f) else: print("WARNING: Unable to save config. Make sure you set " "CONFIG_FILE env var.") def debug(self, msg): if DEBUG: print("DEBUG: {0}".format(msg)) def get_ip_from_opts(self, optstring): if 'ip' in optstring: return optstring['ip'] else: raise ValueError("IP parameter not found in options") def ensure_required_groups(self, groups): for group in groups: if group == 'all': self.debug("Adding group {0}".format(group)) if group not in self.yaml_config: all_dict = OrderedDict([('hosts', OrderedDict({})), ('children', OrderedDict({}))]) self.yaml_config = {'all': all_dict} else: self.debug("Adding group {0}".format(group)) if group not in self.yaml_config['all']['children']: self.yaml_config['all']['children'][group] = {'hosts': {}} def get_host_id(self, host): '''Returns integer host ID (without padding) from a given hostname.''' try: short_hostname = host.split('.')[0] return int(re.findall("\\d+$", short_hostname)[-1]) except IndexError: raise ValueError("Host name must end in an integer") def build_hostnames(self, changed_hosts): existing_hosts = OrderedDict() highest_host_id = 0 try: for host in self.yaml_config['all']['hosts']: existing_hosts[host] = self.yaml_config['all']['hosts'][host] host_id = self.get_host_id(host) if host_id > highest_host_id: highest_host_id = host_id except Exception: pass # FIXME(mattymo): Fix condition where delete then add reuses highest id next_host_id = highest_host_id + 1 all_hosts = existing_hosts.copy() for host in changed_hosts: if host[0] == "-": realhost = host[1:] if self.exists_hostname(all_hosts, realhost): self.debug("Marked {0} for deletion.".format(realhost)) all_hosts.pop(realhost) elif self.exists_ip(all_hosts, realhost): self.debug("Marked {0} for deletion.".format(realhost)) self.delete_host_by_ip(all_hosts, realhost) elif host[0].isdigit(): if ',' in host: ip, access_ip = host.split(',') else: ip = host access_ip = host if self.exists_hostname(all_hosts, host): self.debug("Skipping existing host {0}.".format(host)) continue elif self.exists_ip(all_hosts, ip): self.debug("Skipping existing host {0}.".format(ip)) continue next_host = "{0}{1}".format(HOST_PREFIX, next_host_id) next_host_id += 1 all_hosts[next_host] = {'ansible_host': access_ip, 'ip': ip, 'access_ip': access_ip} elif host[0].isalpha(): if ',' in host: try: hostname, ip, access_ip = host.split(',') except Exception: hostname, ip = host.split(',') access_ip = ip if self.exists_hostname(all_hosts, host): self.debug("Skipping existing host {0}.".format(host)) continue elif self.exists_ip(all_hosts, ip): self.debug("Skipping existing host {0}.".format(ip)) continue all_hosts[hostname] = {'ansible_host': access_ip, 'ip': ip, 'access_ip': access_ip} return all_hosts def range2ips(self, hosts): reworked_hosts = [] def ips(start_address, end_address): try: # Python 3.x start = int(ip_address(start_address)) end = int(ip_address(end_address)) except Exception: # Python 2.7 start = int(ip_address(str(start_address))) end = int(ip_address(str(end_address))) return [ip_address(ip).exploded for ip in range(start, end + 1)] for host in hosts: if '-' in host and not host.startswith('-'): start, end = host.strip().split('-') try: reworked_hosts.extend(ips(start, end)) except ValueError: raise Exception("Range of ip_addresses isn't valid") else: reworked_hosts.append(host) return reworked_hosts def exists_hostname(self, existing_hosts, hostname): return hostname in existing_hosts.keys() def exists_ip(self, existing_hosts, ip): for host_opts in existing_hosts.values(): if ip == self.get_ip_from_opts(host_opts): return True return False def delete_host_by_ip(self, existing_hosts, ip): for hostname, host_opts in existing_hosts.items(): if ip == self.get_ip_from_opts(host_opts): del existing_hosts[hostname] return raise ValueError("Unable to find host by IP: {0}".format(ip)) def purge_invalid_hosts(self, hostnames, protected_names=[]): for role in self.yaml_config['all']['children']: if role != 'k8s-cluster' and self.yaml_config['all']['children'][role]['hosts']: # noqa all_hosts = self.yaml_config['all']['children'][role]['hosts'].copy() # noqa for host in all_hosts.keys(): if host not in hostnames and host not in protected_names: self.debug( "Host {0} removed from role {1}".format(host, role)) # noqa del self.yaml_config['all']['children'][role]['hosts'][host] # noqa # purge from all if self.yaml_config['all']['hosts']: all_hosts = self.yaml_config['all']['hosts'].copy() for host in all_hosts.keys(): if host not in hostnames and host not in protected_names: self.debug("Host {0} removed from role all".format(host)) del self.yaml_config['all']['hosts'][host] def add_host_to_group(self, group, host, opts=""): self.debug("adding host {0} to group {1}".format(host, group)) if group == 'all': if self.yaml_config['all']['hosts'] is None: self.yaml_config['all']['hosts'] = {host: None} self.yaml_config['all']['hosts'][host] = opts elif group != 'k8s-cluster:children': if self.yaml_config['all']['children'][group]['hosts'] is None: self.yaml_config['all']['children'][group]['hosts'] = { host: None} else: self.yaml_config['all']['children'][group]['hosts'][host] = None # noqa def set_kube_master(self, hosts): for host in hosts: self.add_host_to_group('kube-master', host) def set_all(self, hosts): for host, opts in hosts.items(): self.add_host_to_group('all', host, opts) def set_k8s_cluster(self): k8s_cluster = {'children': {'kube-master': None, 'kube-node': None}} self.yaml_config['all']['children']['k8s-cluster'] = k8s_cluster def set_calico_rr(self, hosts): for host in hosts: if host in self.yaml_config['all']['children']['kube-master']: self.debug("Not adding {0} to calico-rr group because it " "conflicts with kube-master group".format(host)) continue if host in self.yaml_config['all']['children']['kube-node']: self.debug("Not adding {0} to calico-rr group because it " "conflicts with kube-node group".format(host)) continue self.add_host_to_group('calico-rr', host) def set_kube_node(self, hosts): for host in hosts: if len(self.yaml_config['all']['hosts']) >= SCALE_THRESHOLD: if host in self.yaml_config['all']['children']['etcd']['hosts']: # noqa self.debug("Not adding {0} to kube-node group because of " "scale deployment and host is in etcd " "group.".format(host)) continue if len(self.yaml_config['all']['hosts']) >= MASSIVE_SCALE_THRESHOLD: # noqa if host in self.yaml_config['all']['children']['kube-master']['hosts']: # noqa self.debug("Not adding {0} to kube-node group because of " "scale deployment and host is in kube-master " "group.".format(host)) continue self.add_host_to_group('kube-node', host) def set_etcd(self, hosts): for host in hosts: self.add_host_to_group('etcd', host) def load_file(self, files=None): '''Directly loads JSON to inventory.''' if not files: raise Exception("No input file specified.") import json for filename in list(files): # Try JSON try: with open(filename, 'r') as f: data = json.load(f) except ValueError: raise Exception("Cannot read %s as JSON, or CSV", filename) self.ensure_required_groups(ROLES) self.set_k8s_cluster() for group, hosts in data.items(): self.ensure_required_groups([group]) for host, opts in hosts.items(): optstring = {'ansible_host': opts['ip'], 'ip': opts['ip'], 'access_ip': opts['ip']} self.add_host_to_group('all', host, optstring) self.add_host_to_group(group, host) self.write_config(self.config_file) def parse_command(self, command, args=None): if command == 'help': self.show_help() elif command == 'print_cfg': self.print_config() elif command == 'print_ips': self.print_ips() elif command == 'print_hostnames': self.print_hostnames() elif command == 'load': self.load_file(args) else: raise Exception("Invalid command specified.") def show_help(self): help_text = '''Usage: inventory.py ip1 [ip2 ...] Examples: inventory.py 10.10.1.3 10.10.1.4 10.10.1.5 Available commands: help - Display this message print_cfg - Write inventory file to stdout print_ips - Write a space-delimited list of IPs from "all" group print_hostnames - Write a space-delimited list of Hostnames from "all" group Advanced usage: Add another host after initial creation: inventory.py 10.10.1.5 Add range of hosts: inventory.py 10.10.1.3-10.10.1.5 Add hosts with different ip and access ip: inventory.py 10.0.0.1,192.168.10.1 10.0.0.2,192.168.10.2 10.0.0.3,192.168.10.3 Add hosts with a specific hostname, ip, and optional access ip: first,10.0.0.1,192.168.10.1 second,10.0.0.2 last,10.0.0.3 Delete a host: inventory.py -10.10.1.3 Delete a host by id: inventory.py -node1 Configurable env vars: DEBUG Enable debug printing. Default: True CONFIG_FILE File to write config to Default: ./inventory/sample/hosts.yaml HOST_PREFIX Host prefix for generated hosts. Default: node SCALE_THRESHOLD Separate ETCD role if # of nodes >= 50 MASSIVE_SCALE_THRESHOLD Separate K8s master and ETCD if # of nodes >= 200 ''' # noqa print(help_text) def print_config(self): yaml.dump(self.yaml_config, sys.stdout) def print_hostnames(self): print(' '.join(self.yaml_config['all']['hosts'].keys())) def print_ips(self): ips = [] for host, opts in self.yaml_config['all']['hosts'].items(): ips.append(self.get_ip_from_opts(opts)) print(' '.join(ips)) def main(argv=None): if not argv: argv = sys.argv[1:] KubesprayInventory(argv, CONFIG_FILE) if __name__ == "__main__": sys.exit(main())
今回はMasterノードを1つ、Workerノードを2つ用意しましたが、HA構成を組む場合はMasterノードを「奇数」個分用意し、hosts.yml
に記載するだけで構築されます。
なお、Master(というかetcd)ノードを奇数個用意しなければならない理由は、こちらの記事に詳しく書かれています。etcdではRaftという分散合意アルゴリズムを利用しています。Raftの都合上、3台以上のノードを形成しない場合、障害が発生しても耐えられるノード数(failure tolerance)が0になってしまい、冗長構成の効果がなくなります。また偶数台の場合、アルゴリズムの性質上全断が発生してしまう場合があります。そのため、etcdノードを冗長化する場合は3、5、7・・・台用意することが推奨されています。
KubesprayでMasterを偶数個指定してクラスターを構築しようとすると、以下のようなエラーメッセージが表示されます。
TASK [kubernetes/preinstall : Stop if even number of etcd hosts] ******************************************************************************************************************** Sunday 22 December 2019 08:54:42 +0000 (0:00:00.315) 0:00:15.956 ******* fatal: [ks-master01]: FAILED! => { "assertion": "groups.etcd|length is not divisibleby 2", "changed": false, "evaluated_to": false, "msg": "Assertion failed" } fatal: [ks-master02]: FAILED! => { "assertion": "groups.etcd|length is not divisibleby 2", "changed": false, "evaluated_to": false, "msg": "Assertion failed" }
※参考リンク:
Raft - Kubernetes(etcd)のHA構成はなぜ3台以上?
構築④:ansible-playbookコマンドの実行
それではAnsibleを実行してクラスターを構築します。
Kubesprayを実行する場合はansible-playbook
コマンドを実行します。Ansibleを実行する際、ansible
コマンドの他にansible-playbook
コマンドを実行することが可能です。前述の通りansible-playbook
コマンドは、InventoryとPlaybookファイルを指定します。
一方ansible
コマンドでは、タスクを実行するホストとモジュールを個別に指定して実行します。開発したモジュールの試験や、一つの処理だけを実行したい場合に利用します。
ansible-playbookコマンドの実行
それではansible-playbook
コマンドを実行します。
[root@ansible-eastus kubespray]# ansible-playbook -i inventory/mycluster/hosts.yml cluster.yml -b -v
各オプションは以下の用途で利用します。
-i
:Inventoryファイルを指定-b
:--become-method
。接続ユーザー以外で対象ホストを操作するために使用する-v
:詳細な情報を出力
上記コマンドを実行し、15分ほど待つと完了します。
出力ログの内容
ansible-playbook
コマンドの実行ログは大量に出力されますが、Kubeadmで実行する内容に加え、OS周りの処理やオプションの追加、各タスク実行前の確認などを行っているように見受けられます。
Kubeadmの処理内容については、以前私がQiitaでまとめたものなどがあるのでそちらをご覧ください。
最後にansible-playbook
実行後に出力されるログのうち、処理結果の要約を表す「PLAY RECAP」の部分だけ載せておきます。
PLAY RECAP *************************************************************************************************************************************** k8s-master : ok=613 changed=133 unreachable=0 failed=0 k8s-worker01 : ok=442 changed=86 unreachable=0 failed=0 k8s-worker02 : ok=406 changed=84 unreachable=0 failed=0 localhost : ok=1 changed=0 unreachable=0 failed=0 Friday 20 December 2019 16:41:25 +0000 (0:00:00.113) 0:12:13.186 ******* =============================================================================== container-engine/docker : ensure docker packages are installed --------------------------------------------------------------------------- 31.71s kubernetes/master : kubeadm | Initialize first master ------------------------------------------------------------------------------------ 28.12s kubernetes/kubeadm : Join to cluster ----------------------------------------------------------------------------------------------------- 22.46s container-engine/docker : ensure service is started if docker packages are already present ----------------------------------------------- 22.26s download : download_container | Download image if required ------------------------------------------------------------------------------- 12.58s download : download_container | Download image if required ------------------------------------------------------------------------------- 11.65s download : download_container | Download image if required ------------------------------------------------------------------------------- 10.55s download : download_container | Download image if required -------------------------------------------------------------------------------- 9.68s download : download_container | Download image if required -------------------------------------------------------------------------------- 9.43s kubernetes-apps/ansible : Kubernetes Apps | Start Resources ------------------------------------------------------------------------------- 9.12s etcd : Configure | Check if etcd cluster is healthy --------------------------------------------------------------------------------------- 8.69s download : download_container | Download image if required -------------------------------------------------------------------------------- 7.26s download : download_container | Download image if required -------------------------------------------------------------------------------- 7.10s kubernetes/preinstall : Install packages requirements ------------------------------------------------------------------------------------- 6.22s network_plugin/calico : Start Calico resources -------------------------------------------------------------------------------------------- 5.96s download : download_container | Download image if required -------------------------------------------------------------------------------- 5.91s etcd : wait for etcd up ------------------------------------------------------------------------------------------------------------------- 5.69s download : download | Download files / images --------------------------------------------------------------------------------------------- 5.57s kubernetes-apps/ansible : Kubernetes Apps | Lay Down CoreDNS Template --------------------------------------------------------------------- 5.35s download : download_container | Download image if required -------------------------------------------------------------------------------- 4.74s
構築完了後の確認
ansible-playbook
コマンドが完了したら、Masterノードにログインし、Kubernetesが構築されたことを確認します。
# kubectlコマンドによるノードの確認 [root@k8s-master ~]# kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-master Ready master 6m11s v1.16.3 k8s-worker01 Ready <none> 5m7s v1.16.3 k8s-worker02 Ready <none> 5m7s v1.16.3 # Podの確認 [root@k8s-master ~]# kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE kube-system calico-kube-controllers-86df5ff779-ptv75 1/1 Running 0 4m41s kube-system calico-node-77fpw 1/1 Running 1 5m20s kube-system calico-node-jsmtg 1/1 Running 1 5m20s kube-system calico-node-vsdx6 1/1 Running 1 5m20s kube-system coredns-58687784f9-6264j 1/1 Running 0 4m25s kube-system coredns-58687784f9-f7x2s 1/1 Running 0 4m18s kube-system dns-autoscaler-79599df498-q8r7r 1/1 Running 0 4m20s kube-system kube-apiserver-k8s-master 1/1 Running 0 6m20s kube-system kube-controller-manager-k8s-master 1/1 Running 0 6m20s kube-system kube-proxy-fnxdk 1/1 Running 0 5m41s kube-system kube-proxy-tsm7b 1/1 Running 0 6m26s kube-system kube-proxy-xx2zj 1/1 Running 0 5m41s kube-system kube-scheduler-k8s-master 1/1 Running 0 6m20s kube-system kubernetes-dashboard-556b9ff8f8-qrqpt 1/1 Running 0 4m16s kube-system nginx-proxy-k8s-worker01 1/1 Running 0 5m41s kube-system nginx-proxy-k8s-worker02 1/1 Running 0 5m41s kube-system nodelocaldns-5fmv7 1/1 Running 0 4m18s kube-system nodelocaldns-njxmm 1/1 Running 0 4m18s kube-system nodelocaldns-qrhk8 1/1 Running 0 4m18s
上記結果を眺めると、Kubernetesコンポーネントの他にdns-autoscaler
nginx-proxy
nodelocaldns
などのPodが確認できます。
dns-autoscaler
はKubernetes内部で利用するCoreDNSなどのDNSのHorizontal Autoscaleを自動的に行うために作成されるPodです。詳細はKubernetes公式ドキュメントをご覧ください。
nginx-proxy
はWorkerノードに配置され、kube-apiserverとのヘルスチェックを行うPodです。
nodelocaldns
はCluster内部の名前解決を効率的に行うために作成されるDNSコンテナです。詳細はKubernetesの公式ドキュメントをご覧ください。
構築⑤:Workerノードの追加
最後に、構築済みクラスターにWorkerノードを追加します。KubesprayではInventoryを更新しPlaybookを実行するだけでノードを追加することができます。
まずはInventoryファイルであるhosts.yml
を以下のように更新します。
all: hosts: k8s-master: ansible_host: 10.3.0.6 ip: 10.3.0.6 access_ip: 10.3.0.6 k8s-worker01: ansible_host: 10.3.0.7 ip: 10.3.0.7 access_ip: 10.3.0.7 k8s-worker02: ansible_host: 10.3.0.8 ip: 10.3.0.8 access_ip: 10.3.0.8 k8s-worker03: # 追加ノード ansible_host: 10.3.0.9 ip: 10.3.0.9 access_ip: 10.3.0.9 children: kube-master: hosts: k8s-master: kube-node: hosts: k8s-worker01: k8s-worker02: k8s-worker03: # 追加 etcd: hosts: k8s-master: k8s-cluster: children: kube-master: kube-node: calico-rr: hosts: {}
そしてansible-playbook
コマンドを実行します。ここではPlaybookにscale.yml
というスケールアップ専用のファイルを指定します。scale.yml
の内容は以下の通りです。
--- - hosts: localhost gather_facts: False become: no tasks: - name: "Check ansible version >=2.7.8" assert: msg: "Ansible must be v2.7.8 or higher" that: - ansible_version.string is version("2.7.8", ">=") tags: - check vars: ansible_connection: local - hosts: bastion[0] gather_facts: False roles: - { role: kubespray-defaults} - { role: bastion-ssh-config, tags: ["localhost", "bastion"]} - name: Bootstrap any new workers hosts: kube-node any_errors_fatal: "{{ any_errors_fatal | default(true) }}" gather_facts: false roles: - { role: kubespray-defaults} - { role: bootstrap-os, tags: bootstrap-os} - name: Generate the etcd certificates beforehand hosts: etcd any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: etcd, tags: etcd, etcd_cluster_setup: false } - name: Target only workers to get kubelet installed and checking in on any new nodes hosts: kube-node any_errors_fatal: "{{ any_errors_fatal | default(true) }}" roles: - { role: kubespray-defaults} - { role: kubernetes/preinstall, tags: preinstall } - { role: container-engine, tags: "container-engine", when: deploy_container_engine|default(true) } - { role: download, tags: download, when: "not skip_downloads" } - { role: etcd, tags: etcd, etcd_cluster_setup: false, when: "not etcd_kubeadm_enabled|default(false)" } - { role: kubernetes/node, tags: node } - { role: kubernetes/kubeadm, tags: kubeadm } - { role: network_plugin, tags: network } - { role: kubernetes/node-label } environment: "{{ proxy_env }}"
それではansible-playbook
コマンドを実行します。
[root@ansible-eastus kubespray]# ansible-playbook -i inventory/mycluster/hosts.yml scale.yml -b -v
こちらも処理結果の要約 (PLAY RECAP)を載せておきます。
PLAY RECAP ************************************************************************************************************************************************************************** k8s-master : ok=35 changed=3 unreachable=0 failed=0 k8s-worker01 : ok=330 changed=21 unreachable=0 failed=0 k8s-worker02 : ok=299 changed=20 unreachable=0 failed=0 k8s-worker03 : ok=319 changed=81 unreachable=0 failed=0 localhost : ok=1 changed=0 unreachable=0 failed=0 Sunday 22 December 2019 02:01:56 +0000 (0:00:00.584) 0:07:11.674 ******* =============================================================================== container-engine/docker : ensure docker packages are installed -------------------------------------------------------------------------------------------------------------- 31.06s container-engine/docker : ensure service is started if docker packages are already present ---------------------------------------------------------------------------------- 21.94s download : download_container | Download image if required ------------------------------------------------------------------------------------------------------------------- 9.28s download : download_container | Download image if required ------------------------------------------------------------------------------------------------------------------- 8.64s kubernetes/kubeadm : Join to cluster ----------------------------------------------------------------------------------------------------------------------------------------- 7.54s download : download_container | Download image if required ------------------------------------------------------------------------------------------------------------------- 7.45s download : download_container | Download image if required ------------------------------------------------------------------------------------------------------------------- 7.21s kubernetes/preinstall : Install packages requirements ------------------------------------------------------------------------------------------------------------------------ 6.41s download : download | Download files / images -------------------------------------------------------------------------------------------------------------------------------- 4.57s download : download_container | Download image if required ------------------------------------------------------------------------------------------------------------------- 4.24s download : download_container | Download image if required ------------------------------------------------------------------------------------------------------------------- 3.93s download : download | Sync files / images from ansible host to nodes --------------------------------------------------------------------------------------------------------- 3.75s kubernetes/preinstall : Hosts | populate inventory into hosts file ----------------------------------------------------------------------------------------------------------- 2.92s download : download_file | Download item ------------------------------------------------------------------------------------------------------------------------------------- 2.49s download : download | Download files / images -------------------------------------------------------------------------------------------------------------------------------- 2.46s download : download | Sync files / images from ansible host to nodes --------------------------------------------------------------------------------------------------------- 2.37s kubernetes/preinstall : install growpart ------------------------------------------------------------------------------------------------------------------------------------- 2.30s etcd : Gen_certs | Copy certs on nodes --------------------------------------------------------------------------------------------------------------------------------------- 2.10s download : download_file | Download item ------------------------------------------------------------------------------------------------------------------------------------- 2.09s container-engine/docker : check if container-selinux is available ------------------------------------------------------------------------------------------------------------ 2.02s
実行後にMasterノードからkubectl
コマンドを実行し、確認します。
# ノードが追加されたことを確認 [root@k8s-master ~]# kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-master Ready master 33h v1.16.3 k8s-worker01 Ready <none> 33h v1.16.3 k8s-worker02 Ready <none> 33h v1.16.3 k8s-worker03 Ready <none> 5m7s v1.16.3 # Podが稼働しているかの確認 [root@k8s-master ~]# kubectl get pods -n kube-system -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES calico-kube-controllers-86df5ff779-ptv75 1/1 Running 0 33h 10.3.0.7 k8s-worker01 <none> <none> calico-node-77fpw 1/1 Running 1 33h 10.3.0.7 k8s-worker01 <none> <none> calico-node-jsmtg 1/1 Running 1 33h 10.3.0.6 k8s-master <none> <none> calico-node-pk24d 1/1 Running 2 5m22s 10.3.0.9 k8s-worker03 <none> <none> calico-node-vsdx6 1/1 Running 1 33h 10.3.0.8 k8s-worker02 <none> <none> coredns-58687784f9-6264j 1/1 Running 0 33h 10.233.111.1 k8s-master <none> <none> coredns-58687784f9-f7x2s 1/1 Running 0 33h 10.233.117.1 k8s-worker02 <none> <none> dns-autoscaler-79599df498-q8r7r 1/1 Running 0 33h 10.233.111.2 k8s-master <none> <none> kube-apiserver-k8s-master 1/1 Running 0 33h 10.3.0.6 k8s-master <none> <none> kube-controller-manager-k8s-master 1/1 Running 0 33h 10.3.0.6 k8s-master <none> <none> kube-proxy-868x2 1/1 Running 0 5m22s 10.3.0.9 k8s-worker03 <none> <none> kube-proxy-fnxdk 1/1 Running 0 33h 10.3.0.7 k8s-worker01 <none> <none> kube-proxy-tsm7b 1/1 Running 0 33h 10.3.0.6 k8s-master <none> <none> kube-proxy-xx2zj 1/1 Running 0 33h 10.3.0.8 k8s-worker02 <none> <none> kube-scheduler-k8s-master 1/1 Running 0 33h 10.3.0.6 k8s-master <none> <none> kubernetes-dashboard-556b9ff8f8-qrqpt 1/1 Running 0 33h 10.233.125.1 k8s-worker01 <none> <none> nginx-proxy-k8s-worker01 1/1 Running 0 33h 10.3.0.7 k8s-worker01 <none> <none> nginx-proxy-k8s-worker02 1/1 Running 0 33h 10.3.0.8 k8s-worker02 <none> <none> nginx-proxy-k8s-worker03 1/1 Running 0 5m22s 10.3.0.9 k8s-worker03 <none> <none> nodelocaldns-5fmv7 1/1 Running 0 33h 10.3.0.6 k8s-master <none> <none> nodelocaldns-5zsg9 1/1 Running 0 5m22s 10.3.0.9 k8s-worker03 <none> <none> nodelocaldns-njxmm 1/1 Running 0 33h 10.3.0.7 k8s-worker01 <none> <none> nodelocaldns-qrhk8 1/1 Running 0 33h 10.3.0.8 k8s-worker02 <none> <none> [root@k8s-master ~]#
最後に
今回はKubesprayについて紹介しました。
KubesprayではAnsibleを利用するため幅広い環境に適用することができます。またクラスター構築時に様々なオプションを選択できるため拡張性もあり、HAクラスターの構築も可能です。またクラスター構築後のノード追加も、少なくともWorkerノードについては簡単に実行することができます。クラスター構築後の追加作業が容易なことも考えると、オンプレ環境など自前でクラスター構築が必要な場面では、第一候補として利用を検討するべきツールではないかと感じました。
一方、Masterノードの追加方法については、この記事を書いている時点ではわかりませんでした。単純にInventoryファイルを更新してPlaybookを実行すれば良いわけではないようで、別の設定変更作業が必要なようです。以下のissueリンク等を参考にして引き続き検証したいと思います。
Document node adding/removing/restoring #1122
Unable to add new master/etcd node to cluster #3471