TT Lab
はじめる
学ぶ 学習パス コース

Kubespray と Terraform でクラスターを構築する

Terraform がインベントリを作り、Kubespray を呼び、その上にデプロイする

TT Labで続きを見る

目標

同じVMの中で、OpenTofuがkubesprayのインベントリをテンプレートで作り、kubesprayの実行をラップしてクラスターを構築し、構築したクラスターに対して、kubernetesプロバイダーとhelmプロバイダーで、ネームスペース・RBAC・アプリケーションを宣言します。外で変更されたものをplanで捉えて、元に戻します。

なぜ重要なのか

現場のクラスターのライフサイクルは、普通は3つの層です。ノードを作る層(クラウド・仮想化)、ノードの上にKubernetesをインストールする層(kubespray)、クラスターの上にチームの領域と権限とアプリを用意する層です。Terraformは、最初の層と3つ目の層に強く、2つ目の層はAnsibleに任せるのが一般的です。このラボは、その境界を1台のVMの中でつなぎ合わせてみます。何をTerraformのstateとして持ち、何をkubesprayの冪等性に任せ、宣言と実際が食い違ったとき、誰がそれに気づくのかです。 ノードを作る最初の層は、このVMでは行いません。このプラットフォームの仮想化APIをラボの中で呼ばせると、分離が壊れるからです。インストールを含めて、約20分待ちます。

ステップ

  1. hashicorp/localプロバイダーで3種類のlocal_fileを宣言してください(書き込み先: /root/ks/tf/cluster/main.tf)。inventory(/root/ks/inventory/lab/inventory.iniを、/root/ks/tf/cluster/inventory.tftplのテンプレートとnodes変数で作る)、host_vars(ノードごとに/root/ks/inventory/lab/host_vars/<노드>.ymlにansible_connectionを書く)、version(/root/ks/inventory/lab/group_vars/k8s_cluster/zz-terraform.ymlにkube_version: <변수>を書く)です(プレースホルダーはノード名と変数です)。kube_version変数のデフォルト値は1.35.8、nodesのデフォルト値はnode1の1台(コントロールプレーンとワーカーを兼ね、local接続)です。tofu validateが通過する必要があります。
  2. /root/ks/tf/clusterでtofu applyを使って、3つのファイルを作成してください。作成された/root/ks/inventory/lab/inventory.iniが、stateのlocal_file.inventoryの内容と同じで、ansible-inventoryで読み取ったときに、node1がkube_control_plane・etcd・kube_nodeにあり、kube_versionが1.35.8と展開される必要があります。
  3. terraform_data "kubespray"を加えて(対象: /root/ks/tf/cluster/main.tf)、インベントリ・バージョン・host_varsのファイルの内容が変わったときにだけ作り直されるように(triggers_replace)し、作られるときにlocal-execで/opt/ks/kubesprayからansible-playbook -i /root/ks/inventory/lab/inventory.ini cluster.ymlを実行して、出力を残すようにしてください(保存先: /root/ks/logs/tf-cluster.log。HOME=/rootを渡します)。tofu applyでインストールまで終わったら(約7分)、stateにterraform_data.kubesprayがあり、ログのPLAY RECAPがfailed=0で、node1がReadyである必要があります。
  4. /root/ks/tf/clusterでtofu plan -detailed-exitcodeをそのままで1回、-var kube_version=1.36.4を付けて1回実行してみてください(適用しません)。次のフィールドを書いてください(書き込み先: /root/ks/tf/plan.json)。steady_exit(最初のplanの終了コード)、bump_exit(2回目の終了コード)、bump_replaces(2回目のplanが変更または作り直すと言っているリソースのアドレスのソートした配列)です。
  5. hashicorp/kubernetes(3.2.1)とhashicorp/helm(3.3.0)のプロバイダーを/root/.kube/configで設定し、ネームスペースteam-a(ラベルowner=platform)、ServiceAccountdeployer、deploymentsを作成して直せて、podsとservicesは読み取りだけを許可するRoledeployerとそのRoleBinding、そしてローカルチャート/opt/ks/charts/helloをteam-aにレプリカ2でインストールするhelm_release "hello"を宣言して、tofu applyしてください(書き込み先: /root/ks/tf/apps/main.tf)。deployerは、team-aでdeploymentsを作成でき、ノードは削除できない必要があり、helloのPod2つがReadyである必要があります。
  6. kubectl label ns team-a owner=someone-else --overwriteでネームスペースのラベルを外で変更してから、/root/ks/tf/appsでtofu plan -detailed-exitcodeを使って差分を捉え、出力を残してください(保存先: /root/ks/tf/drift-plan.txt)。次のフィールドを書きます(書き込み先: /root/ks/tf/drift.json)。exit_code(そのplanの終了コード)、drifted(変更すると出てきたリソースのアドレスのソートした配列)です。まだ適用しません。
  7. /root/ks/tf/appsでtofu applyを使って、差分を元に戻してください。終わったら、team-aのownerラベルがplatformで、tofu plan -detailed-exitcodeが0である必要があります。そして、/root/ks/tf/clusterのplanも0である必要があります(クラスター側の宣言もそのまま)。

参考

インベントリをテンプレートで作る

hashicorp/localプロバイダーで3種類のlocal_fileを宣言してください(書き込み先: /root/ks/tf/cluster/main.tf)。inventory(/root/ks/inventory/lab/inventory.iniを、/root/ks/tf/cluster/inventory.tftplのテンプレートとnodes変数で作る)、host_vars(ノードごとに/root/ks/inventory/lab/host_vars/<노드>.ymlにansible_connectionを書く)、version(/root/ks/inventory/lab/group_vars/k8s_cluster/zz-terraform.ymlにkube_version: <변수>を書く)です(プレースホルダーはノード名と変数です)。kube_version変数のデフォルト値は1.35.8、nodesのデフォルト値はnode1の1台(コントロールプレーンとワーカーを兼ね、local接続)です。tofu validateが通過する必要があります。

templatefile()は、%{ for } … %{ endfor }ディレクティブで繰り返し、~は改行を消費します。ノードのロールを変数(マップ)に置けば、ノードを増やすとき、マップに1行を加えるだけで、インベントリとhost_varsが一緒に変わります。レシピがk8s-cluster.ymlに書いておいたバージョンの行は、削除してあります。バージョンの管理主体を、Terraform 1つにするためです。OpenTofuは、terraformという名前でも呼び出せます。

ファイルだけを先にapply

/root/ks/tf/clusterでtofu applyを使って、3つのファイルを作成してください。作成された/root/ks/inventory/lab/inventory.iniが、stateのlocal_file.inventoryの内容と同じで、ansible-inventoryで読み取ったときに、node1がkube_control_plane・etcd・kube_nodeにあり、kube_versionが1.35.8と展開される必要があります。

stateには、Terraformが作ったファイルの内容がそのまま入っています。誰かがinventory.iniを手で直すと、次のplanがそれを元に戻すと出ます。このファイルの管理主体が、今やTerraformだという意味です。tofu state show local_file.inventoryで内容を見られます。

terraform_dataでkubesprayを呼ぶ

terraform_data "kubespray"を加えて(対象: /root/ks/tf/cluster/main.tf)、インベントリ・バージョン・host_varsのファイルの内容が変わったときにだけ作り直されるように(triggers_replace)し、作られるときにlocal-execで/opt/ks/kubesprayからansible-playbook -i /root/ks/inventory/lab/inventory.ini cluster.ymlを実行して、出力を残すようにしてください(保存先: /root/ks/logs/tf-cluster.log。HOME=/rootを渡します)。tofu applyでインストールまで終わったら(約7分)、stateにterraform_data.kubesprayがあり、ログのPLAY RECAPがfailed=0で、node1がReadyである必要があります。

local-execは、Terraformを動かす場所(このVM)でコマンドを実行します。コマンドが失敗すると、リソースがtaintedのまま残り、次のapplyが再び呼びます。kubesprayのkubeモジュールは、HOMEが空だとkubeconfigを見つけられないので、environmentで渡します。applyが7分間ターミナルを占有するので、systemd-runやtmuxで起動するほうが安全です。

変更がなければ何もしない: バージョンを変えるとどうなるか

/root/ks/tf/clusterでtofu plan -detailed-exitcodeをそのままで1回、-var kube_version=1.36.4を付けて1回実行してみてください(適用しません)。次のフィールドを書いてください(書き込み先: /root/ks/tf/plan.json)。steady_exit(最初のplanの終了コード)、bump_exit(2回目の終了コード)、bump_replaces(2回目のplanが変更または作り直すと言っているリソースのアドレスのソートした配列)です。

-detailed-exitcodeは、変わるものがなければ0を、あれば2を返します。バージョンを変えると何が作り直されるかを見てください。terraform_dataが作り直されると、cluster.ymlが再び呼ばれます。kubesprayのアップグレードは、cluster.ymlではなくupgrade-cluster.ymlです。Terraformは、その違いを知りません。tofu plan -jsonのresource_changesが、アドレスと動作(updateやreplace)を教えてくれます。

構築したクラスターに宣言でデプロイする

hashicorp/kubernetes(3.2.1)とhashicorp/helm(3.3.0)のプロバイダーを/root/.kube/configで設定し、ネームスペースteam-a(ラベルowner=platform)、ServiceAccountdeployer、deploymentsを作成して直せて、podsとservicesは読み取りだけを許可するRoledeployerとそのRoleBinding、そしてローカルチャート/opt/ks/charts/helloをteam-aにレプリカ2でインストールするhelm_release "hello"を宣言して、tofu applyしてください(書き込み先: /root/ks/tf/apps/main.tf)。deployerは、team-aでdeploymentsを作成でき、ノードは削除できない必要があり、helloのPod2つがReadyである必要があります。

クラスターを構築するルートモジュールと、その上にデプロイするルートモジュールを分ける理由があります。1つのモジュールでクラスターを作り、同じapplyでそのクラスターに対してプロバイダーを設定すると、最初のplanのときに、まだないkubeconfigを読み取る必要があります。helmプロバイダー3.xでは、kubernetesの設定がブロックではなくkubernetes = {{ ... }}属性で、setもリストの属性です。権限は、kubectl auth can-i ... --as=system:serviceaccount:team-a:deployerで確認します。

誰かが外で変更した

kubectl label ns team-a owner=someone-else --overwriteでネームスペースのラベルを外で変更してから、/root/ks/tf/appsでtofu plan -detailed-exitcodeを使って差分を捉え、出力を残してください(保存先: /root/ks/tf/drift-plan.txt)。次のフィールドを書きます(書き込み先: /root/ks/tf/drift.json)。exit_code(そのplanの終了コード)、drifted(変更すると出てきたリソースのアドレスのソートした配列)です。まだ適用しません。

planは、まず実際の状態を読み取って(refresh)stateと比較し、そのあと宣言と比較します。外で変更されたものが宣言と異なれば、宣言どおりに戻す計画が出ます。これがドリフトの検知で、定期的にplanを実行して、終了コード2を通知につなげるのが、よくある運用方式です。

宣言に戻す

/root/ks/tf/appsでtofu applyを使って、差分を元に戻してください。終わったら、team-aのownerラベルがplatformで、tofu plan -detailed-exitcodeが0である必要があります。そして、/root/ks/tf/clusterのplanも0である必要があります(クラスター側の宣言もそのまま)。

applyは、planと同じ比較をもう一度行って、差分を宣言のとおりに合わせます。ドリフトを元に戻すか、それとも、外で変更したものが正しいので宣言を直すかは、人が判断することです。今回は、宣言が正しいと判断して、元に戻します。