ホームクラスターの構築 2: Terraformを用いたクラスター構成の自動化
Proxmox VE上でTerraformを使ったVM作成からクラスター構築まで
前回のホームクラスターのハードウェア選択と構成に関する記事に続いて、今回はTerraformを利用してProxmox VEノードにクラスター構成を自動化する過程を扱います。
Terraformとは?
Terraformはインフラをコードで管理できるようにするIaC(Infrastructure as Code)ツールです。
以前、AWS環境やホームラボを運営し、インフラを直接管理した経験があります。サーバーを運営するために手動でシェルにアクセスし必要な依存関係とインフラを構成し、AWSの場合はコンソールでインスタンスの仕様をいちいち調整するなど、手動作業に多くの時間を費やしました。
その後、Terraformに触れたことで、自動化と利便性を極限まで高められるツールだと判断しました。特にKubernetesクラスターやProxmox VEのようなハイパーバイザー、AWSのようなクラウドインフラを管理する際、ダッシュボードで直接制御する方法よりもコードでインフラを管理する方が拡張性の面でずっと有利であり、今後規模が大きいまたは複雑なインフラを構築する際に変更履歴を追跡したり、繰り返し作業を防げるという利点が大きかったです。

(Terraformワークフロー)
Terraformは大きく3つのステージで構成されます。
**Write**: 設定ファイルでインフラ構成要素を定義します。
**Plan**: 現在のインフラ構成と比較し変更点を確認します。
**Apply**: インフラに実際に設定を適用し、stateファイルを更新します。
宣言的にインフラが定義され管理されることで、一貫性のある便利なインフラ構成が可能になります。また、モジュール構造を使用して繰り返し作業なしで構成が可能になります。
$ tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│ ├── nestedA/
│ │ ├── README.md
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ ├── nestedB/
│ ├── .../
├── examples/
│ ├── exampleA/
│ │ ├── main.tf
│ ├── exampleB/
│ ├── .../
Terragruntとは?
Terraformは単体でインフラを簡単に構成できる非常に良いツールです。しかし、複数のデプロイ環境ごとに同じコードを繰り返し作成する必要があり、宣言的に記述されるツールであるため、コンポーネント間に依存関係がある場合には問題が複雑になることがあります。TerragruntはTerraformのラッパーツールであり、これらの問題を解決します。
Unit構造
ユニットとは`terragrunt.hcl` ファイルを含むディレクトリを指し、Terragruntでデプロイ可能な最小の単位です。
terraform {
# Deploy version v0.0.3 in stage
source = "git::git@github.com:foo/modules.git//app?ref=v0.0.3"
}
inputs = {
instance_count = 3
instance_type = "t4g.micro"
}上記のように宣言され、事前に作成されたモジュールに入力値を設定できるように構成されています。
Includes
複数の環境で繰り返しProvider設定をする代わりに、環境ごとまたは共通で設定できるように構成する方法です。
一般的にルートに`root.hcl` を作成し、グローバルな設定を行います。
# root.hcl
remote_state {
backend = "s3"
config = {
bucket = "my-tofu-state"
key = "${path_relative_to_include()}/tofu.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
assume_role {
role_arn = "arn:aws:iam::0123456789:role/terragrunt"
}
}
EOF
}
# app/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}上記の構造だけでなく、root.hclのようなファイルを環境ごとに(prod/env)宣言し、環境に応じて設定を行う方法もあります。
include "root" {
path = find_in_parent_folders("root.hcl")
}
include "k8s-providers" {
path = find_in_parent_folders("k8s-providers.hcl")
}
# Storage needs cert-manager for CSI webhook validation
dependency "security" {
config_path = "../05_security"
skip_outputs = true
}
terraform {
source = "${get_repo_root()}/modules/rook-ceph"
}
inputs = {
kubeconfig_path = "${get_repo_root()}/.cache/production/kubeconfig"
}State Backend
Terraformでstateはローカルで管理され、作業環境やチーム間で管理するには不適切な場合があります。State Backendを利用すると、S3などの外部ストレージと連携してリモートでStateを管理でき、変数や関数、式をサポートして安定的で一貫性のあるState管理が可能になります。
remote_state {
backend = "s3"
config = {
bucket = "my-tofu-state"
key = "${path_relative_to_include()}/tofu.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}インフラ構成の適用
現在構築中のホームクラスターは、Proxmox上で複数の仮想マシンを生成し、Talos Linuxを利用して構成されたKubernetesクラスターです。
複数の仮想マシンを使用するため、自動化や一貫性が必要で、上記のようなIaCツールを使用したことでこの部分が解決できました。
このとき、将来ArgoCDを通じてサービスがデプロイされる予定で、別途プロジェクトとして構成したかったため、インフラ領域とサービス領域を次のように決定しました。
**インフラ領域** - クラスター運営に必要な構成
**サービス領域** - 構成されたクラスター上で動作するサービス
モジュール構造
仮想マシン、CNI、ストレージ(Ceph)、シークレットマネージャー、ArgoCDなどクラスター上でサービスを動作させるために必要なものをインフラ領域とみなし、次のようなTerraformモジュールを構成しました。
> tree -L 1 modules/
modules/
├── bootstrap
├── cluster
├── gitops
├── network
├── secrets
└── storage
> cat modules/network/main.tf
resource "helm_release" "cilium" {
name = "cilium"
repository = "https://helm.cilium.io/"
chart = "cilium"
version = var.cilium_version
namespace = "kube-system"
wait = true
wait_for_jobs = true
timeout = 600
set = [
{
name = "kubeProxyReplacement"
value = "true"
},
{
name = "k8sServiceHost"
value = "localhost"
},
{
name = "k8sServicePort"
value = "7445"
},
{
name = "ipam.mode"
value = "kubernetes"
},
{
name = "cgroup.autoMount.enabled"
value = "false"
},
{
name = "cgroup.hostRoot"
value = "/sys/fs/cgroup"
},
...
> cat modules/network/outputs.tf
output "cilium_status" {
description = "Status metadata for the Cilium Helm release"
value = {
release = helm_release.cilium.status
version = helm_release.cilium.version
namespace = helm_release.cilium.namespace
}
}
output "cilium_lb_pool_name" {
description = "Name of the Cilium LoadBalancer IPPool"
value = kubectl_manifest.cilium_loadbalancer_ip_pool.name
}
output "cilium_l2_policy_name" {
description = "Name of the Cilium L2 Announcement Policy"
value = kubectl_manifest.cilium_l2_announcement_policy.name
}上記のように責任別に各モジュールを構成し、結果を次のモジュールで利用できるように`outputs.tf`を作成しました。
Terragrunt階層構造
各モジュールから出力された結果を次のモジュールに伝え、全体または環境別に設定を指定するためにはTerragruntが必要です。
また、このようなインフラプロジェクトでは各コードが**直観的**で**予測可能**に動作する必要があるため、次のように順次適用できるようフォルダ構造と依存性を設定しました。
> tree -L 2 environments/
environments/
└── production
├── 01_bootstrap
├── 02_cluster
├── 03_network
├── 04_storage
├── 05_secrets
├── 06_gitops
├── env.hcl
└── k8s-providers.hcl
> cat environments/production/03_network/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
include "k8s-providers" {
path = find_in_parent_folders("k8s-providers.hcl")
}
# CNI must deploy after cluster is bootstrapped
dependency "cluster" {
config_path = "../02_cluster"
skip_outputs = true
}
terraform {
source = "${get_repo_root()}/modules/network"
}
inputs = {
kubeconfig_path = "${get_repo_root()}/.cache/production/kubeconfig"
cilium_version = "1.19.4"
cilium_lb_pool = "192.168.0.80-192.168.0.99"
}
まとめ
今回の記事ではTerraformおよびTerragruntを用いたクラスター構成の自動化方法についてご紹介しました。次の記事ではArgoCDを用いたデプロイ戦略について扱います。
また今後、Cilium CNIを用いたネットワーク構成やAWS OIDCとの連携方法について追加で説明いたします。
ありがとうございます :)