Kubespray と Terraform でクラスターを構築する
Terraform と Kubespray の境界 — 前はテンプレート、真ん中は包む、後ろは宣言
一言でいうと
Terraformは、「何があるべきか」をstateで記憶して、差分だけを変更するツールなので、kubesprayの前段ではインベントリを作る作業を、後段ではクラスター上のリソースを宣言してドリフトを捉える作業を担当し、中間のインストールはkubesprayに任せるのが、境界です。
なぜ必要なのか
kubesprayの入力はインベントリですが、インベントリのアドレスと名前は、ノードを作った側が知っています。クラウドでVMをTerraformで作ったなら、その出力(IP、名前、ロール)からインベントリを作るのが自然で、kubesprayリポジトリのcontrib/terraformにも、aws・gcp・openstack・vsphereのようなクラウドごとの例が入っています。反対側も同じです。クラスターができた後で、チームごとにネームスペースと権限と基本のアプリを用意する作業は繰り返されますし、誰かが手で変更したことに気づく必要があります。Terraformのstateとplanは、まさにその作業をします。問題は中間です。Kubernetesのインストールは、数百のタスクが順序を守る必要がある手順で、すでにkubesprayが冪等に行ってくれます。それをTerraformのリソースとして書き直すのは、二度手間です。
どう動くのか
前段: インベントリをテンプレートで。templatefile()は、ファイル1つを読み取って、変数で埋めます。ノードのマップ(name → {control_plane, worker, connection})を変数として置き、%{ for }と%{ if }のディレクティブでグループを埋めれば、ノードを増やす作業は、マップに1行を加える作業になります。local_fileがファイルを書き、stateにその内容が残ります。そのため、誰かがファイルを手で直すと、次のplanが元に戻すと出ます。このファイルの管理主体がTerraformになったという意味で、バージョン(kube_version)のように複数の場所に書かれやすい値の管理主体を1つに決める方法でもあります。
中段: ラップするだけ。terraform_dataは、インフラを何も作らない組み込みのリソースです。triggers_replaceに入れた値が変わると作り直され、作られるときにlocal-execプロビジョナーがコマンドを実行します。ここに、インベントリ・バージョン・host_varsのファイルの内容を入れて、コマンドとしてansible-playbook cluster.ymlを置くと、「宣言が変わったときにだけkubesprayを呼ぶ」ことになります。実行が失敗すると、リソースがtaintedのまま残り、次のapplyで再び呼びます。
限界も、ここで現れます。バージョンを1.35.8 → 1.36.4に変えると、planは、バージョンのファイルとterraform_dataを作り直すと言い、すると呼ばれるのはcluster.ymlです。kubesprayのアップグレードは、upgrade-cluster.yml(cordon・drain・1つずつ)である必要があります。Terraformは、「何が変わったか」は知っていても、「その変化をどんな手順で適用すべきか」は知りません。そのため、インストールはラップしても、アップグレードやノードの削除のように、手順が異なる作業は、パイプラインや人が、kubesprayのプレイブックを選んで呼び出すようにしておくのが、安全です。
後段: クラスター上を宣言で。kubernetesプロバイダーは、kubeconfigでAPIサーバーに接続して、kubernetes_namespace_v1・kubernetes_service_account_v1・kubernetes_role_v1・kubernetes_role_binding_v1のようなリソースを作り、helmプロバイダーはhelm_releaseでチャートをインストールします。helmプロバイダー3.xでは、Kubernetesの接続設定がkubernetes = { ... }属性で、値はset = [{ name, value }]のリストです。クラスターを作るルートモジュールと、このモジュールを分けるのがよいです。1つのモジュールでクラスターを作り、同じ実行でそのクラスターに対してプロバイダーを設定すると、最初のplanのときに、まだないkubeconfigを読み取る必要があり、順序が絡まります。
ドリフト。tofu planは、まず実際の状態を読み取ってstateを新しくし(refresh)、宣言と比較します。誰かがkubectl labelでネームスペースのラベルを変えると、planが「宣言のとおりに戻す」と出し、-detailed-exitcodeが2を返します。この終了コードを定期的な作業に組み込めば、ドリフトの通知になります。ただし、helm_releaseはリリースのメタデータ(チャートと値)を比較するので、チャートが作ったDeploymentを外で直したものまでは、必ず捉えるとは限りません。何をどのツールが監視しているかを、知っておく必要があります。
現場での姿
よくある設計は、3つに分けたリポジトリです。ノードを作るTerraform(クラウドのアカウントごと)、インベントリを受け取ってkubesprayを呼ぶパイプライン、クラスター上を宣言するTerraform(チームごと)です。このラボは、この3つを1台のVMに圧縮しました。ノードを作る最初の層は、ここでは行いません。このプラットフォームでその層は、VMを作る仮想化APIですが、ラボの中でそのAPIを呼ばせると、受講生間の分離が壊れるからです。現場では、クラウドプロバイダーや、vSphereやOpenStackのプロバイダーが、その位置に来ます。
stateも、注意すべき対象です。このラボはstateをローカルファイルに置きますが、チームで使うなら、リモートバックエンドとロックが必要で、stateの中にはファイルの内容とリソースの属性が平文で入るので、秘密を入れてはいけません。
次のラボですること
ノードのマップとテンプレートで、インベントリ・host_vars・バージョンのファイルを作ってapplyし、それらのファイルの管理主体がTerraformになることを見ます。terraform_dataでkubesprayをラップしてクラスターを構築し、バージョンを変えると何が再び呼ばれるかを、planで確認します。別のルートモジュールで、ネームスペース・RBAC・helmリリースを宣言し、外で変更したラベルをplanで捉えて元に戻します。