Kubesprayを使ったオフラインインストール

本記事は、[Kubespray](https://kubespray.io) を使った Kubernetes のオフラインインストール(オンプレミス向け)を行う手順について記載したものです。

また必要なスクリプト類も作りました。[kubespray-offline](https://github.com/kubespray-offline/kubespray-offline) で公開しています。

'23/9/9: Kubespray 2.23.0 対応のため一部修正

# 公式手順

Kubespray 自体はオフラインインストールをサポートしています。公式サイトの [Air-Gap Installation](https://kubespray.io/#/docs/offline-environment) に記載があります。
これによると、以下のものが必要になります。

1. スタティックファイル(zip/バイナリ)を提供するための HTTPリバースプロキシ/キャッシュ/ミラーサーバ
2. OSパッケージ用の内部 Yum/Deb リポジトリ
3. Kubesprayが使用するコンテナイメージをを登録するための内部コンテナイメージレジストリ。必要なイメージのリストはセットアップ内容に依存する
4. [Optional] Kubespray の Python Package を提供するための 内部 PyPI サーバ (requirements.txt に記載されている Python パッケージを OS が提供していない場合)
5. [Optional] 内部 Helm レジストリ (helm_enabled=true の場合にだけ必要)

これだけのものが必要ですが、これらを**どうやって用意すれば良いかは全く記載されていません**。これだけをみて実施できる人がいるとはあまり思えないのですが。。。

# ファイル取得とサーバの構築について

まずはファイル取得とサーバの構築方法について検討します。

最初に用語について定義。

* 準備ノード: オフラインファイルの取得などの準備を行うノード。Internet への接続性が必要。
* 実行ノード: Kubespray デプロイを実行するノード。オフライン環境なので Internet への接続性は無い。

ここからは主に準備ノードでの作業になります。

## a) HTTP サーバ / プライベートレジストリサーバ

実行ノードで HTTP サーバと、プライベートコンテナレジストリサーバを立てる必要があります。どちらもコンテナとして立てるのが良いでしょう。それぞれ nginx:latest と registry:2 のコンテナイメージを準備ノード側で docker pull で取得し、docker save で tar ファイルに保存しておきます。これを実行ノードに持ち込んで containerd で起動することにします。

'22/3/6補足: これらコンテナを動作させるノード自体にも Kubernetes をインストールする場合、port forward を使うのではなく、コンテナはホストネットワークモード(--network host)で起動する必要があります。ブリッジモードで起動してしまうと、デプロイ後にノードを再起動すると port forward 経由でもコンテナに接続できなくなる模様。

## b) スタティックファイルの取得

スタティックファイルの収集を考える必要があります。これは比較的容易で、Kubespray の [contrib/offline](https://github.com/kubernetes-sigs/kubespray/tree/master/contrib/offline) にある `generate_list.sh` を使えば良いです(なおこのスクリプトの改修には私も関わっています)。このスクリプトを実行すると、以下の2つのファイルが生成されます。

* `files.list`: 必要なファイルの URL
* `images.list`: 必要なコンテナイメージ名 (repo/tag)

ファイルについては、files.list に応じて curl などでファイルを取得します。こちらは HTTP サーバで提供することになります。

コンテナイメージは images.list に応じて、docker pull で取得し docker save で .tar に書き出せば OK です。これを実行環境に持ち込んで nerdctl load でロードし、後ほど構築するコンテナイメージレジストリに登録することになります。

## c) Yum/Deb リポジトリ

Yum/Debリポジトリを構築する必要があります。リポジトリ自体は HTTP サーバで提供すれば良いのですが、どうやってファイルを取得し、リポジトリ構造を作るかが問題になります。

まず最初に必要なのは、必要な RPM / DEB ファイルをリストアップすることです。これが一番面倒くさく、試行錯誤が必要になります。

* Kubespray が必要とするもの。これは主に [common_required_pkgs](https://github.com/kubernetes-sigs/kubespray/blob/master/roles/kubernetes/preinstall/defaults/main.yml) と required_pkgs ([Ubuntu](https://github.com/kubernetes-sigs/kubespray/blob/master/roles/kubernetes/preinstall/vars/ubuntu.yml), [RHEL](https://github.com/kubernetes-sigs/kubespray/blob/master/roles/kubernetes/preinstall/vars/redhat.yml) など)に記載されています。
    - common_required_pkgs: 全OS共通のもの。curl, rsync, socat, unzip, e2fsprogs, xfsprogs, ebtables, bash-completion。
    - required_pkgs: OS固有のもの。conntrack など。
* Python3, venv: Ansible を動かすために
* その他もろもろ、必要に応じて: ipvsadm, ipset, ...

特定したら、次は必要な RPM / DEB ファイルをダウンロードします。使うツールは以下の通り。

* ~~RHEL/CentOS 7 の場合: repotrack~~
* RHEL 8 / AlmaLinux 8 の場合: dnf download
* Ubuntu の場合: apt cache, apt download

ダウンロード後にリポジトリを構成する必要があります。

* RHEL/CentOS 系: createrepo
    - RHEL8系 の場合はこれに加えて repo2module が必要
* Ubuntu: apt-ftparchive

具体的な手順まで説明すると大変なので、詳細は後ほど紹介するスクリプトを見てください。

# d) PyPI ミラー

Ansible に必要な Python パッケージを取得します。取得したパッケージは上記のHTTPサーバで提供することになります。

パッケージの取得は `pip download -d download_dir -r requirements.txt` でできます。簡単ですね。

、、、ですが、この方法だとソースパッケージしか取得されません。これを使って Ansible をインストールしようとすると、Ansible が依存する cffi や cryptography にネイティブコードが含まれるため、コンパイルが必要になってしまいます。このため、gcc, libffi-dev, libssl-dev などの Yum/Deb パッケージも取得して事前インストールが必要になってしまい、面倒です。

これを避けるためには、バイナリパッケージ(wheel) も取得しておく必要があります。これは pip download に `--binary-only :all:` をつければ良いです。が、Ansible はバイナリパッケージを提供していないためこれは pip download がエラーになってしまいます。なので requirements.txt から Ansible を抜いて、さらに Ansible が暗黙的に依存する PyYAML を追加してバイナリ取得する必要があります。

なお、バイナリパッケージは Python バージョンによってものが違います。なのでどの環境でも動くようにしたければ `--python-version` オプションを 3.8〜3.10 あたりでそれぞれ指定して取得する必要があります。

また、パッケージを取得したら PyPI ミラーとしてディレクトリ構成を行う必要があります。これは [pypi-mirror](https://pypi.org/project/python-pypi-mirror/) でできます。

### ちょっと補足

Kubespray で推奨されている方法は、Kubespray のコンテナイメージを使う、というものです。コンテナを使えば PyPI ミラーは当然不要です(コンテナに全部入っているので)。コンテナを使うほうはまだ試してないです。

また、Python の venv 環境をまるごと tar に固めてそれを使う、という方法もありますが、バイナリが含まれるので Python のバージョンが違うと動きません。Ubuntu で作った環境は RHEL では動かないです。オフラインファイル作成環境と実行環境のOSバージョンが完全に同じならいけると思います。

# Kubernetesデプロイ(オフライン)の実行

ここから実行ノードの作業になります。

上記のファイルをすべて実行ノードにコピーします。VPNで転送するなり、USBメモリで転送するなどしてください。ただサイズが5GBは余裕で超えますので注意。

containerd をインストールし、Nginx, Registry サーバを起動します。スタティックファイルは Nginx コンテナで提供し、コンテナイメージは Registry サーバに登録します。

補足: なお containerd ではなく Docker を使ってもいいのですが、実行ノード自身に Kubernetes をデプロイしようとすると不具合が発生してしまいます。というのは kubespray 2.18.0 以降ではデフォルトで Docker を消して containerd を入れてしまうため、デプロイ最中に Nginx, Registry サーバが止まり、デプロイも止まってしまうため。なので、最初から containerd を使うほうが良いです。

このあとは Kubespray を展開し、オフラインインストールを実行します。インベントリディレクトリの `group_vars/all/offline.yml` にオフライン定義を投入する必要があります。こちらの詳細は [Air-Gap Installation](https://kubespray.io/#/docs/offline-environment) の 「Configure Inventory」に記載されている通りで、`registry_host` を Registry サーバを指すように、また `files_repo`, `yum_repo`, `debian_repo`, `ubuntu_repo` を Nginx サーバを指すように設定しておきます。

なお、各ターゲットノードではオフライン yum/deb リポジトリを使用するように設定が必要です。これは Kubespray ではやってくれないので自前で設定が必要です。

# 自動化スクリプト

さて上記手順をいちいち手でやっていたら日がくれるので、自動化スクリプトを作りました。[kubespray-offline](https://github.com/tmurakam/kubespray-offline) で公開していますので、お使いください。本スクリプトは上記の手順をスクリプト化したものになっています。

サポートしているのは ~~RHEL/CentOS 7,~~ RHEL/AlmaLinux 8, Ubuntu 20.04/22.04 のみです。Kubespray のバージョンはデフォルトで 2.23.0 です。

## 準備ノードの作業

### Docker CE のインストール

最初に Docker CE をインストールしておきます。`install-docker.sh` でインストールできます。

ここは Docker ではなく containerd を使っても良いのですが、準備ノードでは Docker を使うことにしました。開発用などで入ってるケースが多いと思いますので。

### オフラインファイルの取得

まず、準備ノードで `download-all.sh` を実行します。すると以下が自動的に実行されます。

* 必要なパッケージ(yum/apt)のインストール
* Python venv 設定と必要な Python パッケージのインストール
* Kubespray をダウンロードして展開
* PyPIミラーファイルを取得
* スタティックファイル・コンテナイメージをすべて取得
* RPM/DEBファイルを取得しローカルレポジトリを作成

`outputs` ディレクトリに成果物が生成されますので、この内容をすべて実行ノード(オフライン)に転送します。

## Kubernetes デプロイ(オフライン)

`outputs` ディレクトリにあるスクリプトを使用して実行ノード上でサーバをセットアップ・起動します。

* `setup-container.sh`:containerd のインストールと、nginx, registry イメージのロードを行います。
* `start-nginx.sh` : Nginx を起動します。outputs ディレクトリをそのまま http://localhost:8080 で公開します。
* `setup-offline.sh` : オフラインリポジトリを設定します。Yum/Apt のローカルリポジトリを Nginx を指すように設定します。また pip も Nginx を指すように設定します。
* `start-registry.sh`: プライベートレジストリを起動します
* `load-push-all-images.sh` : コンテナイメージ(.tar.gz) をすべてロードし、プライベートレジストリに push します。

次に、Kubespray の tarball を展開します。

次に Ansible 用にインベントリファイルと `offline.yml` を作成します。

`offline.yml` はこんな感じで作成します。`YOUR_HOST` のところだけ書き換えてください。

```yaml
http_server: "http://YOUR_HOST:8080/"
registry_host: "YOUR_HOST:35000"

# Insecure registries for containerd
containerd_registries_mirrors:
  - prefix: "{{ registry_host }}"
    mirrors:
      - host: "http://{{ registry_host }}"
        capabilities: ["pull", "resolve"]
        skip_verify: true

files_repo: "{{ http_server }}/files"
yum_repo: "{{ http_server }}/rpms"
ubuntu_repo: "{{ http_server }}/debs"

# Registry overrides
kube_image_repo: "{{ registry_host }}"
gcr_image_repo: "{{ registry_host }}"
docker_image_repo: "{{ registry_host }}"
quay_image_repo: "{{ registry_host }}"

# Download URLs: See roles/download/defaults/main.yml of kubespray.
kubeadm_download_url: "{{ files_repo }}/kubernetes/{{ kube_version }}/kubeadm"
kubectl_download_url: "{{ files_repo }}/kubernetes/{{ kube_version }}/kubectl"
kubelet_download_url: "{{ files_repo }}/kubernetes/{{ kube_version }}/kubelet"
# etcd is optional if you **DON'T** use etcd_deployment=host
etcd_download_url: "{{ files_repo }}/kubernetes/etcd/etcd-{{ etcd_version }}-linux-amd64.tar.gz"
cni_download_url: "{{ files_repo }}/kubernetes/cni/cni-plugins-linux-{{ image_arch }}-{{ cni_version }}.tgz"
crictl_download_url: "{{ files_repo }}/kubernetes/cri-tools/crictl-{{ crictl_version }}-{{ ansible_system | lower }}-{{ image_arch }}.tar.gz"
# If using Calico
calicoctl_download_url: "{{ files_repo }}/kubernetes/calico/{{ calico_ctl_version }}/calicoctl-linux-{{ image_arch }}"
# If using Calico with kdd
calico_crds_download_url: "{{ files_repo }}/kubernetes/calico/{{ calico_version }}.tar.gz"

runc_download_url: "{{ files_repo }}/runc.{{ image_arch }}"
nerdctl_download_url: "{{ files_repo }}/nerdctl-{{ nerdctl_version }}-{{ ansible_system | lower }}-{{ image_arch }}.tar.gz"
containerd_download_url: "{{ files_repo }}/containerd-{{ containerd_version }}-linux-{{ image_arch }}.tar.gz"
```

公式サイトに乗っている [offline.yml の例](https://kubespray.io/#/docs/offline-environment) には docker や containerd のリポジトリも書いてありますが、これは使わないので設定不要です。

つぎに、全ターゲットノードでオフライン yum/deb リポジトリを使用するように Ansible で設定します。

```bash
$ cp -r ${outputs_dir}/playbook ${kubespray_dir}
$ cd ${kubespray_dir}
$ ansible-playbook -i ${your_inventory_file} offline-repo.yml
```

最後に Ansible で Kubespray をデプロイします。

```bash
$ ansible-playbook -i ${your_inventory_file} --become --become-user=root cluster.yml
```

## 注意事項など

* Ubuntu 20.04 だと CoreDNS が CrashLoopBackoff 状態になりますが、ノードを reboot すれば直ります。おそらく /etc/resolv.conf を systemd で管理している影響と思いますが、原因追求はまだしていません。
* ~~RHEL/CentOS 7 の場合、firewalld を停止しておく必要があります。~~

## おまけ: テストとか

kubespray-offline では `test` ディレクトリにテスト用のスクリプトを入れてあります。

Vagrant で仮想環境を作り、デプロイテストを行うようにしてあります。オフライン環境については ip route del default でデフォルトゲートウェイを削除することで模擬しています。