この記事は Z Lab Advent Calendar 2018 の 17 日目の記事です.
Kubernetes クラスタを自前で作ろうとするとき, みなさんはどんなツールを使っているでしょうか? kubeadm ですかね? まぁ, そうですよね. でも今回はもっと硬派に bootkube を使ってみたいと思います.
bootkube
この記事では bootkube を利用して, packet という bare-metal のクラウドサービス上に Kubernetes クラスタを作る手順を紹介したいと思います.
こちらのガイドによると bootkube を利用して bare-metal の Kubernetes クラスタを作る方法はざっくりと次の通りのようです.
- matchbox をセットアップする
- Typhoon から提供されている Terraform モジュールに必要な設定を記述する
- その Terraform モジュールを利用して bootkube を実行する
簡単そうですね!!
matchbox のセットアップ
matchbox とは bare-metal マシンに Container Linux をセットアップするための仕組みを提供するサービスで, PXE という network booting のためのエンドポイントを提供します.
bootkube ではクラスタに参加させるマシンの MAC アドレスと, それらに対応するコンフィグレーション (Ignition や Cloud-Config など) を matchbox に登録しておくことで, 個別の OS イメージを用意することなくクラスタを作れるようにしています.
matchbox 自身のセットアップ方法は次のリンクにまとめられています.
この記事ではこのインストラクションに従って matchbox を構築したものとして進めていきますが. 実際に構築する上では次のことに注意してください.
- この matchbox のサービスは packet 上にある bare-metal マシンからアクセスできる必要があるので, インターネットからアクセスできるようにしておくこと
- 後述する Terraform モジュールで利用する必要があるため HTTP API だけでなく gRPC API も有効にしておくこと
- 同じく Terraform モジュールで利用するため CA 証明書とクライアント証明書を保存しておくこと
参考までにこの記事を書くときに利用した Terraform ファイルを置いておきます.
packet への bare-metal マシンの用意
matchbox がセットアップできたところで, 早速 Kubernetes クラスタを構成する bare-metal マシンを packet に用意します.
上述したように bootkube では利用するマシンの MAC アドレスを事前に知っておく必要があります. packet では作成するマシンの MAC アドレスを前もって知ることができないため, PXE による network booting を行う前に, 一度適当な OS イメージを利用して起動して MAC アドレスを確認します.
公式の CLI を利用して確認していきます.
はじめに SSH 公開鍵を登録します.
$ ssh-key create --key "$(cat ~/.ssh/id_rsa.pub)" --label default
+--------------------------------------+---------+----------------------+
| ID | LABEL | CREATED |
+--------------------------------------+---------+----------------------+
| ********-****-****-****-************ | default | 2018-12-05T15:17:16Z |
+--------------------------------------+---------+----------------------+
プロジェクトを作成します.
$ packet project create --name bootkube
+--------------------------------------+----------+----------------------+
| ID | NAME | CREATED |
+--------------------------------------+----------+----------------------+
| ********-****-****-****-************ | bootkube | 2018-12-05T14:43:20Z |
+--------------------------------------+----------+----------------------+
ここで得られた ID を環境変数 PROJECT_ID
として, controller-001 と worker-001 という二つのマシンを作ります. --ipxe-script-url
オプションに指定する値は, 構築した matchbox のエンドポイントに応じて修正してください.
$ packet device create \
--hostname controller-001 \
--project-id ${PROJECT_ID} \
--facility nrt1 \
--plan t1.small.x86 \
--operating-system custom_ipxe \
--ipxe-script-url http://matchbox.example.com:8080/boot.ipxe
+--------------------------------------+----------------+--------------------------+--------+----------------------+
| ID | HOSTNAME | OS | STATE | CREATED |
+--------------------------------------+----------------+--------------------------+--------+----------------------+
| ********-****-****-****-************ | controller-001 | Container Linux - Stable | queued | 2018-12-05T15:18:38Z |
+--------------------------------------+----------------+--------------------------+--------+----------------------+
$ packet device create \
--hostname worker-001 \
--project-id ${PROJECT_ID} \
--facility nrt1 \
--plan t1.small.x86 \
--operating-system custom_ipxe \
--ipxe-script-url http://matchbox.example.com:8080/boot.ipxe
+--------------------------------------+------------+--------------------------+--------+----------------------+
| ID | HOSTNAME | OS | STATE | CREATED |
+--------------------------------------+------------+--------------------------+--------+----------------------+
| ********-****-****-****-************ | worker-001 | Container Linux - Stable | queued | 2018-12-05T15:19:37Z |
+--------------------------------------+------------+--------------------------+--------+----------------------+
マシンの状態がアクティブになったら, 次のようなコマンドで IP アドレスと MAC アドレスが確認できます.
$ packet device get \
--project-id ${PROJECT_ID} \
--json \
| jq -M '.[] | {id:.id, hostname:.hostname, ip:.ip_addresses[0].address, mac:.network_ports[1].data.mac}'
{
"id": "********-****-****-****-************",
"hostname": "worker-001",
"ip": "***.***.***.***",
"mac": "**:**:**:**:**:**"
}
{
"id": "********-****-****-****-************",
"hostname": "controller-001",
"ip": "***.***.***.***",
"mac": "**:**:**:**:**:**"
}
ここで, 少し面倒なのですが bootkube ではマシンにアクセスするための DNS 名が必要なため, それぞれのマシンの IP アドレスを利用して DNS 名を登録しておきます. 以降は次のように DNS レコードを登録したものとして進めたいと思います.
DNS | A レコード | 用途 |
---|---|---|
controller-001.packet.example.com | controller-001 の IP アドレス | controller-001 へのアクセスに利用 |
worker-001.packet.example.com | worker-001 の IP アドレス | worker-001 へのアクセスに利用 |
k8s.packet.example.com | controller-001 の IP アドレス | kube-apiserver へのアクセスに利用 |
ちなみのこの時点では matchbox にコンフィグレーションが登録されていないため, 作成したマシンはいつまで経っても起動しません. matchbox サービスのログを確認すると次のようなメッセージが出力されていて, 対応するプロファイル (コンフィグレーションと MAC アドレスの紐づけ) が無いことが分かります.
time="2018-12-07T08:16:53Z" level=info msg="HTTP GET /boot.ipxe"
time="2018-12-07T08:16:53Z" level=info msg="HTTP GET /ipxe?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**&domain=&hostname=controller-001&serial=***************"
time="2018-12-07T08:16:53Z" level=info msg="No matching profile" labels=map[uuid:00000000-0000-0000-0000-000000000000 mac:**:**:**:**:**:** domain: hostname:controller-001 serial:***************]
time="2018-12-07T08:17:02Z" level=info msg="HTTP GET /boot.ipxe"
time="2018-12-07T08:17:02Z" level=info msg="HTTP GET /ipxe?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**&domain=&hostname=worker-001&serial=***************"
time="2018-12-07T08:17:02Z" level=info msg="No matching profile" labels=map[hostname:worker-001 serial:*************** uuid:00000000-0000-0000-0000-000000000000 mac:**:**:**:**:**:** domain:]
bootkube の準備
ここまででクラスタを設定するのに必要な情報が手に入ったので, いよいよ bootkube を使っていきます. ここでは matchbox のリポジトリにある examples/terraform/bootkube-install というディレクトリにある Terraform ファイルがほとんどそのまま使えるので, これを利用したいと思います.
このディレクトリにある terraform.tfvars.example ファイルを元に, これまでに手に入れた情報から次のような terraform.tfvars ファイルを作ります.
matchbox_http_endpoint = "http://matchbox.example.com:8080" # matchbox の HTTP エンドポイント
matchbox_rpc_endpoint = "matchbox.example.com:8081" # matchbox の gRPC エンドポイント
ssh_authorized_key = "ADD ME" # マシンへの SSH ログインに使う公開鍵
cluster_name = "my-cluster" # クラスタ名
os_channel = "coreos-stable"
os_version = "1911.4.0"
# Machines
# controller/worker のホスト名, MAC アドレス, DNS 名をそれぞれ指定します.
controller_names = [ "controller-001" ]
controller_macs = [ "**:**:**:**:**:**" ]
controller_domains = [ "controller-001.packet.example.com" ]
worker_names = [ "worker-001" ]
worker_macs = [ "**:**:**:**:**:**" ]
worker_domains = [ "worker-001.packet.example.com" ]
# Bootkube
k8s_domain_name = "k8s.packet.example.com" # クラスタの DNS 名 (kube-apiserver へアクセスする際に利用)
asset_dir = "assets" # kubeconfig や種証明書ファイルなどの出力先ディレクトリ
また, matchbox 構築時に作成したクライアント証明書 (ca.crt, client.crt, client.key) を $HOME/.matchbox/
ディレクトリに置きます. (これは今回利用する Terraform ファイルの仕様です.)
さらに, 追加で必要な Terraform プロバイダをインストールしておきます. go get
でソースコードからバイナリを作ってから,
$ go get -u github.com/coreos/terraform-provider-matchbox
$ go get -u github.com/coreos/terraform-provider-ct
$HOME/.terraformrc を追加, 編集して Terraform CLI から利用できるようにします.
providers {
matchbox = "$GOPATH/bin/terraform-provider-matchbox"
ct = "$GOPATH/bin/terraform-provider-ct"
}
最後に, Terraform の実行時のそれぞれのマシンへの SSH 接続を可能にするため, ssh-agent に秘密鍵を登録しておきます.
$ ssh-add ~/.ssh/id_rsa
これで準備はできました!
bootkube の実行
では早速, 実行してみましょう.
$ terraform init
$ terraform plan
$ terraform apply
Terraform を実行するとクラスタに必要な証明書 (etcd や kube-apiserver などの TLS 通信に利用される) の作成や, matchbox へのプロファイルの登録などが行われますが, 証明書を各マシンへデプロイする処理で先へ進まなくなると思います.
...
module.cluster.null_resource.copy-worker-secrets: Still creating... (2m30s elapsed)
module.cluster.null_resource.copy-controller-secrets: Still creating... (2m30s elapsed)
module.cluster.null_resource.copy-worker-secrets: Still creating... (2m40s elapsed)
module.cluster.null_resource.copy-controller-secrets: Still creating... (2m40s elapsed)
module.cluster.null_resource.copy-worker-secrets: Still creating... (2m50s elapsed)
module.cluster.null_resource.copy-controller-secrets: Still creating... (2m50s elapsed)
...
これは各マシンがまだ起動していないため当然の結果です. そこで各マシンをもう一度 network booting させることにより Terraform の処理を先に進めます. 先程は matchbox にプロファイルが登録されていなかったため起動に失敗していましたが今度は成功するはずです. そこで Terraform とは別のシェルから各マシンに対して次のようなコマンドを実行します.
curl https://api.packet.net/devices/${DEVICE_ID}/actions \
-v \
--header "X-Auth-Token: ${PACKET_TOKEN}" \
--header 'Content-Type: application/json' \
--data @- <<EOT
{
"type": "reinstall",
"force_delete": false,
"operating_system": "custom_ipxe",
"ipxe_script_url": "http://matchbox.example.com:8080/boot.ipxe"
}
EOT
ここでは CLI によって network booting を行わせられるための方法が見つからなかったため, packet の REST API を直接実行しています. 環境変数 DEVICE_ID
には controller-001/worker-001 を作成したときのレスポンスに含まれていた ID を, PACKET_TOKEN
には API のアクセストークンを指定してください. 成功すると matchbox に登録したプロファイルを利用して各マシンが起動するはずです. 成功したときの matchbox のログは次のような感じです.
time="2018-12-08T03:24:12Z" level=info msg="HTTP GET /"
time="2018-12-08T03:25:52Z" level=info msg="HTTP GET /boot.ipxe"
time="2018-12-08T03:25:52Z" level=info msg="HTTP GET /ipxe?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**&domain=&hostname=controller-001&serial=***************"
time="2018-12-08T03:25:52Z" level=debug msg="Matched an iPXE config" labels=map[domain: hostname:controller-001 serial:*************** uuid:00000000-0000-0000-0000-000000000000 mac:**:**:**:**:**:**] profile=example-container-linux-install-controller-001
time="2018-12-08T03:26:09Z" level=info msg="HTTP GET /boot.ipxe"
time="2018-12-08T03:26:09Z" level=info msg="HTTP GET /ipxe?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**&domain=&hostname=worker-001&serial=***************"
time="2018-12-08T03:26:09Z" level=debug msg="Matched an iPXE config" labels=map[mac:**:**:**:**:**:** domain: hostname:worker-001 serial:*************** uuid:00000000-0000-0000-0000-000000000000] profile=example-container-linux-install-worker-001
time="2018-12-08T03:26:43Z" level=info msg="HTTP GET /ignition?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**"
time="2018-12-08T03:26:43Z" level=debug msg="Matched an Ignition or Container Linux Config template" group=install-controller-001 labels=map[uuid:00000000-0000-0000-0000-000000000000 mac:**:**:**:**:**:**] profile=example-container-linux-install-controller-001
time="2018-12-08T03:27:03Z" level=info msg="HTTP GET /ignition?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**"
time="2018-12-08T03:27:03Z" level=debug msg="Matched an Ignition or Container Linux Config template" group=install-worker-001 labels=map[mac:**:**:**:**:**:** uuid:00000000-0000-0000-0000-000000000000] profile=example-container-linux-install-worker-001
time="2018-12-08T03:27:37Z" level=info msg="HTTP GET /ignition?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**&os=installed"
time="2018-12-08T03:27:37Z" level=debug msg="Matched an Ignition or Container Linux Config template" group=example-controller-001 labels=map[os:installed uuid:00000000-0000-0000-0000-000000000000 mac:**:**:**:**:**:**] profile=example-controller-controller-001
time="2018-12-08T03:27:37Z" level=warning msg="warning parsing Ignition JSON: error: incorrect config version (too new)"
time="2018-12-08T03:27:57Z" level=info msg="HTTP GET /ignition?uuid=00000000-0000-0000-0000-000000000000&mac=**-**-**-**-**-**&os=installed"
time="2018-12-08T03:27:57Z" level=debug msg="Matched an Ignition or Container Linux Config template" group=example-worker-001 labels=map[os:installed uuid:00000000-0000-0000-0000-000000000000 mac:**:**:**:**:**:**] profile=example-worker-worker-001
time="2018-12-08T03:27:57Z" level=warning msg="warning parsing Ignition JSON: error: incorrect config version (too new)"
すると止まっていた Terraform による証明書のデプロイ処理も進み始め, Terraform による bootkube の実行まで完了するはずです. bootkube の実行は Terraform によって controller マシンの bootkube.service という Systemd サービスにより実行されます.
module.cluster.null_resource.bootkube-start: Still creating... (3m0s elapsed)
module.cluster.null_resource.bootkube-start: Still creating... (3m10s elapsed)
module.cluster.null_resource.bootkube-start: Still creating... (3m20s elapsed)
module.cluster.null_resource.bootkube-start: Still creating... (3m30s elapsed)
module.cluster.null_resource.bootkube-start: Creation complete after 3m32s (ID: 359689546407774373)
Apply complete! Resources: 59 added, 0 changed, 0 destroyed.
Terraform の実行が完了すると assets/auth/
ディレクトリ内に kubeconfig ファイルができているので kubectl で確認してみます.
$ kubectl --kubeconfig=assets/auth/kubeconfig get nodes
NAME STATUS ROLES AGE VERSION
controller-001.packet.example.com Ready controller,master 5m38s v1.12.3
worker-001.packet.example.com Ready node 5m38s v1.12.3
$ kubectl --kubeconfig=assets/auth/kubeconfig get all --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system pod/coredns-7bbc5998b9-tz9mm 1/1 Running 0 5m4s
kube-system pod/coredns-7bbc5998b9-wr6tk 1/1 Running 0 5m4s
kube-system pod/flannel-d7shx 2/2 Running 2 5m4s
kube-system pod/flannel-jw9px 2/2 Running 0 5m4s
kube-system pod/kube-apiserver-r87qg 1/1 Running 2 4m38s
kube-system pod/kube-controller-manager-64b8b56ffd-bcdc7 1/1 Running 0 5m4s
kube-system pod/kube-controller-manager-64b8b56ffd-ktg6k 1/1 Running 2 5m4s
kube-system pod/kube-proxy-85l6s 1/1 Running 0 5m4s
kube-system pod/kube-proxy-9r2hd 1/1 Running 0 5m4s
kube-system pod/kube-scheduler-769d859ff5-4c7j7 1/1 Running 0 5m3s
kube-system pod/kube-scheduler-769d859ff5-vhhct 1/1 Running 0 5m3s
kube-system pod/pod-checkpointer-9nhkn 1/1 Running 0 4m38s
kube-system pod/pod-checkpointer-9nhkn-controller-001.packet.example.com 1/1 Running 0 3m51s
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default service/kubernetes ClusterIP 10.3.0.1 <none> 443/TCP 5m18s
kube-system service/coredns ClusterIP 10.3.0.10 <none> 53/UDP,53/TCP 5m14s
NAMESPACE NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
kube-system daemonset.apps/flannel 2 2 2 2 2 <none> 5m14s
kube-system daemonset.apps/kube-apiserver 1 1 1 1 1 node-role.kubernetes.io/master= 5m14s
kube-system daemonset.apps/kube-proxy 2 2 2 2 2 <none> 5m14s
kube-system daemonset.apps/pod-checkpointer 1 1 1 1 1 node-role.kubernetes.io/master= 5m14s
NAMESPACE NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kube-system deployment.apps/coredns 2 2 2 2 5m14s
kube-system deployment.apps/kube-controller-manager 2 2 2 2 5m14s
kube-system deployment.apps/kube-scheduler 2 2 2 2 5m14s
NAMESPACE NAME DESIRED CURRENT READY AGE
kube-system replicaset.apps/coredns-7bbc5998b9 2 2 2 5m4s
kube-system replicaset.apps/kube-controller-manager-64b8b56ffd 2 2 2 5m4s
kube-system replicaset.apps/kube-scheduler-769d859ff5 2 2 2 5m4s
クラスタができていますね! Yay!!
まとめ
- bootkube を利用して packet 上に bare-metal な Kubernetes クラスタを作ることに成功しました.
- このとき, OS のセットアップには matchbox というサービスを利用して PXE による network booting を行いました.