この記事は Kubernetes Advent Calendar 2019 の5日目の記事です。

最近(というほどでもないけど)CentOS 8がでたのでKubernetesクラスタを作った話。

結論から言うと、CentOS 7と比べて、pythonコマンドが無くなったのと、iptablesがデフォルトでnf_tablesを使うようになったのと、YumがDNFになったのに対応するくらいでいけた。

ついでに、RHEL 8でDockerパッケージが配布されなくなったので、そのあたりを見越して脱Dockerもした。

CentOS 8

CentOSの最新メジャーバージョンである8が2019年9月24日にリリースされた

カーネルバージョンは4.18.0-80。

CentOS 8での変更点のうち、今回の作業に影響があったものは3つ。

pythonコマンドが無い

Kubernetesクラスタをつくるときは、いつもOSはMinimalインストールしてAnsible(ansible-k8s)を実行するんだけど、CentOS 8の場合それだとpythonコマンドがインストールされない。

CentOS 7以前はPython(の2系)がいろいろなシステムツールに使われていたため、Pythonが入っていて当然だった。 CentOS 8でもまあPythonがシステムツールに使われているみたいなんだけど、それ用のはPATHの通っていない/usr/libexec/platform-pythonに置かれるようになり、ユーザが使ってはいけないことになった。 しかもこれがPython 3。

$ /usr/libexec/platform-python --version
Python 3.6.8

そんなわけなので、CentOS 8をMinimalインストールしたマシンにAnsibleで環境構築しようとすると、Ansibleがpythonコマンドを叩こうとしてエラーになる。

fatal: [k8s_master]: FAILED! => {"changed": false, "module_stderr": "/bin/sh: /usr/bin/python: No such file or directory\n", "module_stdout": "", "msg": "MODULE FAILURE", "rc": 127}

で、yum install -y python3してから再度Ansible実行してみると、/usr/bin/python3しかインストールされていないので、また同じエラーになってへこむ。 どうもCentOS 8では、pythonというコマンドを実行したときに2系が走るのか3系が走るのかはユーザが明示的に指定すべきという方針らしい。 なので、alternatives --set python /usr/bin/python3をしてpythonpython3へのリンクにしておくか、Ansibleの設定でansible_python_interpreter: /usr/bin/python3としておく必要がある。


Minimalインストールじゃない場合はpython3パッケージがデフォルトでインストールされる。 けどやっぱりpythonコマンドじゃなくてpython3コマンドしかないので注意。

YUMがダンディになった

CentOS 7のYumはバージョン3系で、Python 2で書かれているため、Python 3化したCentOS 8では廃止された。 代わりに、YumのフォークであるDNF(Dandified Yum)が標準のパッケージマネージャになっている。 とはいえ、yumコマンドもDNFへのリンクとして残っていて、従来と変わらないインターフェースで使える。

$ ls -l /usr/bin/yum
lrwxrwxrwx. 1 root root 5 May 14  2019 /usr/bin/yum -> dnf-3

ただしAnsibleでパッケージインストールする場合、AnsibleのyumモジュールはPython 2が前提なので、使うと以下のようなエラーになる。

fatal: [k8s_master]: FAILED! => {"changed": false, "msg": "The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead."}

なので代わりにdnfモジュールを使わないといけない。

iptablesがnftablesになった。

CentOS 8ではiptablesのパッケージバージョンが1.8になって、iptablesの皮をかぶったnftablesになった。 iptablesコマンドは従来通りあるんだけど、

$ iptables --version
iptables v1.8.2 (nf_tables)
$ ls -l /usr/sbin/iptables
lrwxrwxrwx. 1 root root 17 Jul  2 00:41 /usr/sbin/iptables -> xtables-nft-multi

というように本体はnftablesであることが分かる。 xtables-nft-multiというのはnftablesを従来のiptables(など)と同じインターフェースで扱うためのもので、いろんな*tablesからリンクされている。

find /usr/sbin/ -name "*tables*" | xargs ls -l
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ebtables -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ebtables-restore -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ebtables-save -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ip6tables -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ip6tables-restore -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ip6tables-restore-translate -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ip6tables-save -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/ip6tables-translate -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/iptables -> xtables-nft-multi
-rwxr-xr-x. 1 root root   3512 Jul  2 00:41 /usr/sbin/iptables-apply
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/iptables-restore -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/iptables-restore-translate -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/iptables-save -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/iptables-translate -> xtables-nft-multi
lrwxrwxrwx. 1 root root     17 Jul  2 00:41 /usr/sbin/xtables-monitor -> xtables-nft-multi
-rwxr-xr-x. 1 root root 240296 Jul  2 00:42 /usr/sbin/xtables-nft-multi

この新しいiptablesコマンド(xtables-nft-multi)は、従来のやつの完全な上位互換で後方互換で、スケーラブルで高性能とうたわれているんだけど、バグがあったりしていつもすぐに移行できるわけでもないっぽい。 なので、Ubuntuでは従来通りのiptablesコマンド(iptables-legacy)も提供されていて使えるんだけど、CentOSのYUMリポジトリではlegacyなやつが配布されていないような…


で、DockerやKubernetes(のkube-proxy)はコンテナ間通信のためにiptablesをディープに使っていて、上記変更の影響をもろに受ける。

Dockerの対応iptables-legacyがあればそれを優先で使うというもの。 Kubernetesの方のissueとかを見た感じだと、システム内でlegacyとlegacyじゃないのとが混ざってるとダメみたいなんだけど、この対応でいいんだろうか… firewalldとかがlegacyじゃないの使ってたりしたらダメなんじゃなかろうか…

Kubernetesのコミュニティでは、(Ubuntu前提で)システムで使うiptablesを全部legacyにしちゃう派が多いように見えるけど、2019年11月にマージされた修正により、kube-proxyコンテナ内からホストのiptablesがどちらかを判定して、それに合わせたiptablesを使ってくれるようになっている。 ちょっと判定ロジックが怪しく見えるけど、とにかく、ホストと合ってさえいれば、nftablesでもいけるってことだ。

(因みに、OpenShiftでは、kube-proxyコンテナにホストのルートディレクトリをマウントさせて、ホストのiptablesコマンドをコンテナ内から実行するという豪快なソリューションを採用している。)


CNIプラグインもiptablesを使うので、そのコンテナ内のiptablesコマンドがlegacyかどうかも気を付けなければいけない

Weave NetのコンテナはAlpine Linuxベース。 Alpine Linuxの最新版の3.10だとiptablesが1.8になっているんだけど、デフォルトではなぜかlegacyな方が有効になっている。 もちろんnftablesを使うiptablesコマンドに切り替えはできるけど、Weave Netの公式コンテナはlegacyな方のままになっている。

Calicoは知らない…

RHEL 8でDockerが配布されなくなった問題

CentOS 8の気になる変更点は上記3つなんだけど、もう一点、CentOSのアップストリームであるRHELの方のバージョン8でインパクトの大きい変更があったので、それにも触れる。

RHEL 7では、Red HatがビルドしてRHELでの動作を保証・サポートしてくれる版のDockerが、Red HatのYumリポジトリから配布されていたんだけど、RHEL 8ではそれが廃止されてしまった。 Red Hatとしては、代わりに自製のPodmanを使えといっているんだけど、PodmanはCRIを実装していないっぽいし、Dockerに対するdockershimみたいなものもないので、Kubernetesからは使えないはず。

Docker Enterprise EditionはRHEL 8をサポートしているんだけど、当然有償だし、売り飛ばされちゃったし、ちょっと手を出したくない感じ。

Docker Community Edition使えばいいじゃんと思うかもしれないけど、Docker Community EditionはRHELをサポートしてない…。 動くんだろうけど。

で、RHEL 8でKubernetes動かすなら、コンテナランタイムにDockerを使い難いのでCRI-Oを使うかcontainerdを使うか、という感じになるんだけど、いずれにせよDockerに比べて情報が少ないし設定も多いし面倒。

Kubernetesでcontainerdを使う設定

コンテナランタイムは、今回の作業に関しては結局のところ、CentOSなのでDocker Community Editionでもいいんだけど、RHEL 8の上記の動きがあるし、KubernetesディストリビューションもOpenShiftやMicroK8sやk3sなど結構脱Dockerを済ませているので、それらを追ってcontainerdにしてみる。

containerdをインストールしてKubernetesから使うセットアップ方法については、KubernetesのマニュアルcontainerdのマニュアルcontainerdのCRI Pluginのマニュアルあたりを見ればなんとなくわかる。

containerdとruncのバイナリをダウンロードしてどっかに置いて、containerdの設定ファイル(i.e. /etc/containerd/config.toml)と起動のためのユニットファイルを書いて、kubeletの起動オプション--container-runtime=remote--container-runtime-endpoint=unix:///run/containerd/containerd.sockを付けてあげればいいだけ。

containerdのconfig.tomlはバージョンによって結構変わるので、CRI Pluginのバージョンと合わせてよく確認しないと嵌る。

また、Dockerと同居させようとすると、Dockerとともにインストールされるcontainerdにバイナリやユニットファイルが上書きされたり、ソケットファイル(i.e. /run/containerd/containerd.sock)が競合したりして残念なことになるので、慣れないうちはやめておくべし。

ようやくKubernetesクラスタ構築

前置きが長くなったけど、CentOS 8でシングルノード(マスタコンポとノードコンポ同居)のKubernetseクラスタを構築する。

構築に使うのは、いろいろカスタマイズするために自作したAnsibleプレイブックのansible-k8s。 今回のために、DNFがあればYumの代わりにDNFを使うようにエンハンスした。 コンテナランタイムにDockerの代わりにcontainerdを使うようにもした

また、iptablesはせっかくなので新しいやつを使いたくて、nftables版のWeave Netコンテナも作ってDockerHubに挙げておいた

  1. OSインストール

    VMware PlayerのVM作って、CentOS 8のインストールイメージを適当にダウンロードして、適当にMinimalインストール。

  2. python3インストール

    CentOSのインストールが完了したら、OSにログインして、yum install -y python3

  3. ansible-k8sダウンロード、インベントリ設定

    git clone --recursive https://github.com/kaitoy/ansible-k8s.git でダウンロードして、インベントリファイルであるansible-k8s/productionを環境に合わせて編集する。

    ansible_python_interpreter: /usr/bin/python3はこのインベントリに書いておく。

  4. nftables版のWeave Netコンテナを使う設定

    ansible-k8s/extra_vars.ymlに以下を書いておく。

    weave_net__weave_kube_image: docker.io/kaitoy/weave-kube:2.6.0-nftables
    weave_net__weave_npc_image: docker.io/kaitoy/weave-npc:2.6.0-nftables
  5. ansible-k8s実行

    Ansibleはすでに使えるものとして、以下のコマンドでansible-k8sをキック。

    $ cd ansible-k8s
    $ ./play.sh production k8s_single_node_cluster.yml

    ansible-k8sの中の処理は昔書いた内容と大きく変わっていない。


以上でKubernetesクラスタが構築出来て、Weave NetとCoreDNSがデプロイされた状態になる。

$ cat /etc/centos-release
CentOS Linux release 8.0.1905 (Core)
$ kubectl get node
NAME               STATUS   ROLES    AGE   VERSION
k8s-master.local   Ready    <none>   23d   v1.16.0
$ kubectl get po --all-namespaces
NAMESPACE     NAME                      READY   STATUS    RESTARTS   AGE
kube-system   coredns-77b79c856-b6dqh   1/1     Running   1          23d
kube-system   coredns-77b79c856-nv46g   1/1     Running   1          23d
kube-system   weave-net-9msnk           2/2     Running   3          23d

Dockerはいなくて、代わりにcontainerdが動いている。(ctrはcontainerdのクライアント。TASKはコンテナにあたるもの。)

$ docker ps
-bash: docker: command not found
$ ctr -a /run/k8s-containerd/containerd.sock -n k8s.io task ls
TASK                                                                PID     STATUS
00dd015d67e40e4a8879edc56fd94fc9c5c0d68dbcb3f0576be3c31096ce20a6    2236    RUNNING
b93a71c250f40f68742300464be7b6950648417e15d0425d2544192199e1b86b    2425    RUNNING
1fb23ce08081f99388a3260bd4258fe3ad391ace1964a6b3c3823a528f1a7b5d    2477    RUNNING
307bdc92fdb0e392c68ce18a04685296e10040f29f2d519630c8ee92af4d9b4b    1449    RUNNING
e30a1ee0316bf299c74ae0e3bd026bb88a3ca78d133da419226ea04824de828b    1901    RUNNING
ebd5d32043d5d3db2f933162329353ce5db801db0bb78f6ca57e509e68dc7910    1559    RUNNING
e23c6600bd232a8a08b77798a62c236979b166caa92aa33eadc130390ae60fb2    2187    RUNNING

iptablesもnftablesでちゃんと動いているっぽい。

$ iptables --version
iptables v1.8.2 (nf_tables)
$ iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
KUBE-FIREWALL  all  --  anywhere             anywhere
WEAVE-NPC-EGRESS  all  --  anywhere             anywhere
WEAVE-IPSEC-IN  all  --  anywhere             anywhere

(snip)