はじめに
「ジャンルなしオンラインもくもく会 Advent Calendar 2025」の5日目の記事です。
本記事は、Kubernetesクラスタ構築シリーズの第3回です。今回は、Terraformを使ってHyper-V上にTalos Linuxクラスタを実際に構築します。
本シリーズの全体構成
前回のおさらい
本記事で学べること
- Terraform Provider for Hyper-Vの使い方
- Talos Linuxのブートストラップ手順
- MBP(Mac)からWindowsへのリモート管理
- クラスタ構成のInfrastructure as Code化
IPアドレスについて: 本シリーズでは
10.0.0.x体系のIPアドレスを使用しています。Terraform設定例では汎用的な値(192.168.x.x)を示していますが。ご自身の環境に合わせて読み替えてください。
1. 全体アーキテクチャ
1.1 構築するクラスタ構成
2. 事前準備
2.1 Windows側の準備
Hyper-Vの有効化
# PowerShell(管理者権限)で実行
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All
# 再起動が必要
Restart-Computer
Virtual Switchの作成
筆者環境ではHyper-Vマネージャー(GUI)で作成済みのため、以下は参考です。
参考: PowerShellでの作成
# External Virtual Switch(外部通信用)
New-VMSwitch -Name "LAB" -NetAdapterName "Ethernet" -AllowManagementOS $true
# Internal Virtual Switch(管理通信用)
New-VMSwitch -Name "LAB-Internal" -SwitchType Internal
Note:
-NetAdapterNameは環境によって異なります(例: "Ethernet 2", "Wi-Fi"など)。Get-NetAdapterで確認してください。
Terraform用のWindows専用アカウント準備
Terraformから安全にHyper-Vを操作するため、専用のWindowsアカウントを作成することを推奨します。WinRMの詳細な設定手順はMicrosoft公式ドキュメントを参照してください。
ポイント
- 管理者権限を持つ専用アカウントを作成
- WinRM HTTPS接続を有効化(ポート5986)
- 認証情報は
terraform.tfvarsで管理し、.gitignoreに追加
2.2 Mac側の準備
必要なツールのインストール
# Homebrew経由でインストール
brew install terraform
brew install siderolabs/tap/talosctl
brew install kubectl
バージョン確認
$ terraform version
Terraform v1.13.4
$ talosctl version
Client:
Tag: v1.11.5
$ kubectl version --client
Client Version: v1.34.1
2.3 Talos ISOイメージのダウンロード
方法1: ブラウザでダウンロード(推奨)
Talos Linux Releases から metal-amd64.iso をダウンロードし、C:\ISO\ に保存。
方法2: SSH経由でダウンロード
# MacからWindowsへSSH接続してダウンロード
ssh user@windows-host "mkdir C:\ISO 2>nul & curl -Lo C:\ISO\metal-amd64.iso https://github.com/siderolabs/talos/releases/download/v1.11.5/metal-amd64.iso"
3. Terraformでのインフラ定義
3.1 ディレクトリ構成
talos-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
└── configs/
├── controlplane.yaml
└── worker.yaml
3.2 main.tf
terraform {
required_version = ">= 1.13.0"
required_providers {
hyperv = {
source = "taliesins/hyperv"
version = "~> 1.0"
}
}
}
# Hyper-V Provider設定
provider "hyperv" {
host = var.hyperv_host
port = var.hyperv_port
user = var.hyperv_user
password = var.hyperv_password
https = true
insecure = true
}
# Control Plane VM
resource "hyperv_machine_instance" "talos_controlplane" {
name = "talos-cp-1"
generation = 1
processor_count = 2
memory_startup_bytes = 4294967296 # 4GB
network_adaptors {
name = "eth0"
switch_name = var.virtual_switch_name
}
hard_disk_drives {
controller_type = "Scsi"
controller_number = 0
path = "C:\\VMs\\talos-cp-1\\disk.vhdx"
disk_size_gb = 20
}
dvd_drives {
controller_number = 0
path = var.talos_iso_path
}
vm_firmware {
enable_secure_boot = "Off"
}
state = "Running"
}
# Worker Node 1
resource "hyperv_machine_instance" "talos_worker_1" {
name = "talos-worker-1"
generation = 1
processor_count = 2
memory_startup_bytes = 8589934592 # 8GB
network_adaptors {
name = "eth0"
switch_name = var.virtual_switch_name
}
hard_disk_drives {
controller_type = "Scsi"
controller_number = 0
path = "C:\\VMs\\talos-worker-1\\disk.vhdx"
disk_size_gb = 50
}
dvd_drives {
controller_number = 0
path = var.talos_iso_path
}
vm_firmware {
enable_secure_boot = "Off"
}
state = "Running"
}
# Worker Node 2
resource "hyperv_machine_instance" "talos_worker_2" {
name = "talos-worker-2"
generation = 1
processor_count = 2
memory_startup_bytes = 8589934592 # 8GB
network_adaptors {
name = "eth0"
switch_name = var.virtual_switch_name
}
hard_disk_drives {
controller_type = "Scsi"
controller_number = 0
path = "C:\\VMs\\talos-worker-2\\disk.vhdx"
disk_size_gb = 50
}
dvd_drives {
controller_number = 0
path = var.talos_iso_path
}
vm_firmware {
enable_secure_boot = "Off"
}
state = "Running"
}
# Worker Node 3 (監視専用: スペック高め)
resource "hyperv_machine_instance" "talos_worker_3" {
name = "talos-worker-3"
generation = 1
processor_count = 4
memory_startup_bytes = 17179869184 # 16GB
network_adaptors {
name = "eth0"
switch_name = var.virtual_switch_name
}
hard_disk_drives {
controller_type = "Scsi"
controller_number = 0
path = "C:\\VMs\\talos-worker-3\\disk.vhdx"
disk_size_gb = 100
}
dvd_drives {
controller_number = 0
path = var.talos_iso_path
}
vm_firmware {
enable_secure_boot = "Off"
}
state = "Running"
}
3.3 variables.tf
variable "hyperv_host" {
description = "Hyper-V ホストのIPアドレス"
type = string
}
variable "hyperv_port" {
description = "WinRM ポート"
type = number
}
variable "hyperv_user" {
description = "Hyper-V ホストのユーザー名"
type = string
}
variable "hyperv_password" {
description = "Hyper-V ホストのパスワード"
type = string
sensitive = true
}
variable "virtual_switch_name" {
description = "Virtual Switch名"
type = string
}
variable "talos_iso_path" {
description = "Talos ISOのパス"
type = string
}
3.4 terraform.tfvars
hyperv_host = "192.168.1.100"
hyperv_port = 5986
hyperv_user = "Administrator"
hyperv_password = "YourSecurePassword"
virtual_switch_name = "LAB"
talos_iso_path = "C:\\ISO\\metal-amd64.iso"
注意 terraform.tfvarsは.gitignoreに追加してください。
3.5 outputs.tf
output "controlplane_name" {
value = hyperv_machine_instance.talos_controlplane.name
}
output "worker_names" {
value = [
hyperv_machine_instance.talos_worker_1.name,
hyperv_machine_instance.talos_worker_2.name,
hyperv_machine_instance.talos_worker_3.name,
]
}
4. Terraformの実行
4.1 初期化
cd talos-terraform
terraform init
出力例
Initializing the backend...
Initializing provider plugins...
- Installing taliesins/hyperv v1.0.3...
Terraform has been successfully initialized!
4.2 プラン確認
terraform plan
出力例
Terraform will perform the following actions:
# hyperv_machine_instance.talos_controlplane will be created
+ resource "hyperv_machine_instance" "talos_controlplane" {
+ name = "talos-cp-1"
+ generation = 1
+ processor_count = 2
+ memory_startup_bytes = 4294967296
...
}
Plan: 4 to add, 0 to change, 0 to destroy.
4.3 適用
terraform apply
確認プロンプト
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
完了メッセージ
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
controlplane_name = "talos-cp-1"
worker_names = [
"talos-worker-1",
"talos-worker-2",
"talos-worker-3",
]
5. Talos Linuxの設定とブートストラップ
5.1 Talos設定ファイルの生成
# シークレットファイルの生成
talosctl gen secrets -o secrets.yaml
# 設定ファイルの生成
talosctl gen config my-cluster https://192.168.1.100:6443 \
--with-secrets secrets.yaml \
--output-dir ./configs
生成されるファイル
configs/
├── controlplane.yaml
├── worker.yaml
└── talosconfig
5.2 Control Planeへの設定適用
# Control Planeに設定を適用
talosctl apply-config \
--insecure \
--nodes 192.168.1.100 \
--file configs/controlplane.yaml
出力例
applied configuration to node 192.168.1.100
5.3 Bootstrap(初回のみ)
# talosconfig の設定
export TALOSCONFIG=./configs/talosconfig
# Control PlaneでKubernetesクラスタをブートストラップ
talosctl bootstrap --nodes 192.168.1.100
待機時間 約3-5分
確認
# ノードの状態確認
talosctl --nodes 192.168.1.100 health
# 出力例:
waiting for etcd to be healthy: OK
waiting for kubelet to be healthy: OK
waiting for all services to be healthy: OK
5.4 Worker Nodeへの設定適用
# Worker Node 1
talosctl apply-config \
--insecure \
--nodes 192.168.1.101 \
--file configs/worker.yaml
# Worker Node 2
talosctl apply-config \
--insecure \
--nodes 192.168.1.102 \
--file configs/worker.yaml
# Worker Node 3 (監視専用)
talosctl apply-config \
--insecure \
--nodes 192.168.1.103 \
--file configs/worker.yaml
5.5 kubeconfigの取得
# kubeconfigをマージ
talosctl kubeconfig --nodes 192.168.1.100
# または、別ファイルに保存
talosctl kubeconfig --nodes 192.168.1.100 --force ./kubeconfig
export KUBECONFIG=./kubeconfig
6. クラスタの確認
6.1 ノード一覧
kubectl get nodes
出力例
NAME STATUS ROLES AGE VERSION
talos-cp-1 Ready control-plane 5m v1.34.1
talos-worker-1 Ready <none> 3m v1.34.1
talos-worker-2 Ready <none> 3m v1.34.1
talos-worker-3 Ready <none> 3m v1.34.1
6.2 システムPodの確認
kubectl get pods -n kube-system
出力例
NAME READY STATUS RESTARTS AGE
coredns-76f75df574-abcde 1/1 Running 0 5m
coredns-76f75df574-fghij 1/1 Running 0 5m
etcd-talos-cp-1 1/1 Running 0 5m
kube-apiserver-talos-cp-1 1/1 Running 0 5m
kube-controller-manager-talos-cp-1 1/1 Running 0 5m
kube-flannel-ds-xxxxx 1/1 Running 0 5m
kube-proxy-xxxxx 1/1 Running 0 5m
kube-scheduler-talos-cp-1 1/1 Running 0 5m
6.3 監視専用ノードのラベル付け
明日(12/6)の監視スタック構築のため、事前にラベルを設定します。
# 監視専用ノードにラベル付与
kubectl label node talos-worker-3 node-role.kubernetes.io/monitoring=true
# Taint設定(監視ワークロードのみ配置)
kubectl taint node talos-worker-3 dedicated=monitoring:NoSchedule
確認
kubectl get node talos-worker-3 --show-labels
出力例
NAME STATUS ROLES AGE VERSION LABELS
talos-worker-3 Ready monitoring 5m v1.34.1 node-role.kubernetes.io/monitoring=true,...
7. ISO除去(重要!)
Bootstrap完了後、必ずISO イメージを除去してください。
7.1 PowerShell(Windows側)
# Control Plane
Set-VMDvdDrive -VMName "talos-cp-1" -ControllerNumber 0 -Path $null
# Worker Nodes
Set-VMDvdDrive -VMName "talos-worker-1" -ControllerNumber 0 -Path $null
Set-VMDvdDrive -VMName "talos-worker-2" -ControllerNumber 0 -Path $null
Set-VMDvdDrive -VMName "talos-worker-3" -ControllerNumber 0 -Path $null
7.2 Terraform(推奨: 自動化)
Hyper-V Providerのdvd_drivesブロックを削除または空にすることで、次回のterraform apply時にISOが自動的に除去されます。
# 初回構築後は dvd_drives ブロックを削除またはコメントアウト
resource "hyperv_machine_instance" "talos_controlplane" {
name = "talos-cp-1"
# ... 他の設定 ...
# dvd_drives {
# controller_number = 0
# path = var.talos_iso_path
# }
}
その後、terraform applyを実行すると、DVD driveが除去されます。
8. LoadBalancer(MetalLB)のセットアップ
Kubernetesクラスタを実用的に運用するには、外部からのアクセスを受け付けるLoadBalancerが必須です。クラウド環境では自動で提供されますが、ベアメタル環境ではMetalLBを使って実装します。
これにより、後続の記事で紹介する監視UI(Grafana)やアプリケーションへのアクセスがスムーズになります。
8.1 なぜMetalLBが必要か
Kubernetesの type: LoadBalancer サービスは、クラウドプロバイダーのLoadBalancerを自動で作成しますが、ベアメタル環境では外部IPが <pending> のまま割り当てられません(MetalLB公式ドキュメント、Ingress-Nginx Bare-metal考慮事項参照)。
Kubernetesサービスタイプの用途
| 方法 | 特徴 | 本来の用途 |
|---|---|---|
| ClusterIP | クラスタ内部のみアクセス可能 | マイクロサービス間通信(本番標準) |
| NodePort | ノードIP:ポートで外部公開 | シンプルな外部公開、外部LBの背後 |
| LoadBalancer | 外部IPを自動割り当て | クラウド環境での外部公開(本番標準) |
本記事ではブラウザからドメインでアクセスしたいため、LoadBalancer(MetalLB)を採用します。クラウド環境ではLoadBalancerが自動でプロビジョニングされますが、ベアメタル環境ではMetalLBで同等の機能を実現します。
Note: 自宅環境のネットワーク構成によっては、IPアドレス範囲の調整やルーター設定が必要になる場合があります。うまくいかない場合は NodePort での運用も検討してください。
MetalLBは以下の機能を提供します。
- L2モード(ARP): 宅内ネットワークでLoadBalancer IPを提供
- 自動IP割り当て: 事前定義したIPアドレスプールから自動割り当て
- 複数サービス対応: 複数のLoadBalancerサービスに異なるIPを割り当て
8.2 IPアドレスプールの設計
宅内ネットワーク10.0.0.0/24から、以下の範囲をMetalLB用に確保します。
| 用途 | IP範囲 | 説明 |
|---|---|---|
| MetalLB Pool | 10.0.0.200-220 | LoadBalancer用(21個のIP) |
選定理由
- ノードIP(10.0.0.100番台)と重複しない範囲
- DHCPサーバーの割り当て範囲外
8.3 マニフェスト作成
ディレクトリ構成
kubernetes/
└── infrastructure/
└── metallb/
├── kustomization.yaml
└── config.yaml
kubernetes/infrastructure/metallb/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: metallb-system
resources:
# MetalLB本体は公式のマニフェストから取得(namespace含む)
- https://raw.githubusercontent.com/metallb/metallb/v0.14.0/config/manifests/metallb-native.yaml
- config.yaml
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/part-of: homelab-infra
kubernetes/infrastructure/metallb/config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 10.0.0.200-10.0.0.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default-l2
namespace: metallb-system
spec:
ipAddressPools:
- default-pool
設定内容
-
IPAddressPool: 割り当て可能なIPアドレス範囲を定義 -
L2Advertisement: L2モード(ARP)でIPアドレスをアドバタイズ
8.4 デプロイと確認
デプロイ
kubectl apply -k kubernetes/infrastructure/metallb/
Pod起動確認
kubectl get pods -n metallb-system
期待される出力
NAME READY STATUS RESTARTS AGE
controller-86576c4f8c-xxxxx 1/1 Running 0 30s
speaker-xxxxx 1/1 Running 0 30s
IPAddressPool確認
kubectl get ipaddresspool -n metallb-system
期待される出力
NAME AUTO ASSIGN AVOID BUGGY IPS ADDRESSES
default-pool true false ["10.0.0.200-10.0.0.220"]
9. Ingress Controller(Nginx)のセットアップ
MetalLBで外部IPが割り当てられるようになりましたが、HTTPのルーティング(ホスト名やパスベース)にはIngress Controllerが必要です。
9.1 Ingress Controllerの役割
Ingress Controllerは以下の機能を提供します。
- HTTPルーティング: ホスト名やパスに基づいて異なるサービスにルーティング
- TLS終端: HTTPS通信の終端処理
- 単一エントリポイント: 複数のアプリケーションを1つのIPで公開
例:
http://grafana.homelab.local → Grafanaサービス
http://argocd.homelab.local → ArgoCDサービス
9.2 マニフェスト作成
kubernetes/infrastructure/ingress-nginx/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ingress-nginx
resources:
# Ingress NGINX公式マニフェスト
- https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/cloud/deploy.yaml
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/part-of: homelab-infra
patchesStrategicMerge:
- |-
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
# MetalLBで特定のIPを割り当てる
loadBalancerIP: 10.0.0.210
ポイント
-
loadBalancerIP: 10.0.0.210で固定IP割り当て - MetalLBのIPプール範囲内のIPを指定
9.3 デプロイと確認
デプロイ
kubectl apply -k kubernetes/infrastructure/ingress-nginx/
Controller起動確認
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/component=controller \
-n ingress-nginx --timeout=120s
LoadBalancer IP確認
kubectl get svc -n ingress-nginx ingress-nginx-controller
期待される出力
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
ingress-nginx-controller LoadBalancer 10.96.48.100 10.0.0.210 80:30588/TCP,443:31778/TCP
✅ EXTERNAL-IP が 10.0.0.210 になっていれば成功です。
10. 動作確認
10.1 LoadBalancer IP割り当て確認
全てのLoadBalancerサービスのIPを確認
kubectl get svc --all-namespaces -o wide | grep LoadBalancer
期待される出力例
ingress-nginx ingress-nginx-controller LoadBalancer 10.96.48.100 10.0.0.210 80:30588/TCP,443:31778/TCP
10.2 テストアプリケーションのデプロイ
簡単なnginxアプリをデプロイして、LoadBalancerとIngressの動作を確認します。
kubernetes/apps/nginx-test/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
kubernetes/apps/nginx-test/service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-test
namespace: default
spec:
type: LoadBalancer
selector:
app: nginx-test
ports:
- port: 80
targetPort: 80
デプロイ
kubectl apply -f kubernetes/apps/nginx-test/deployment.yaml
kubectl apply -f kubernetes/apps/nginx-test/service.yaml
LoadBalancer IP確認
kubectl get svc nginx-test
期待される出力
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
nginx-test LoadBalancer 10.96.62.135 10.0.0.211 80:30334/TCP
✅ MetalLBが 10.0.0.211 を自動で割り当てました(プール範囲内)。
10.3 アクセステスト
LoadBalancer経由でのアクセス
# MBPまたはWindows Host から
curl http://10.0.0.211
期待される出力
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
Ingress経由でのアクセス(オプション)
Ingressリソースを作成すれば、ホスト名ベースのルーティングも可能
curl -H "Host: demo.homelab.local" http://10.0.0.210
11. トラブルシューティング
11.1 ノードが起動しない
症状
talosctl --nodes 192.168.1.100 health
# Timeout
確認事項
-
Hyper-V VMが起動しているか
Get-VM | Where-Object {$_.State -eq 'Running'} -
ネットワーク疎通確認
ping 192.168.1.100 -
Virtual Switchの設定確認
Get-VMSwitch
11.2 Bootstrap が進まない
症状
talosctl bootstrap --nodes 192.168.1.100
# Hanging...
解決策
# etcdログの確認
talosctl --nodes 192.168.1.100 logs etcd
# API Serverログの確認
talosctl --nodes 192.168.1.100 logs kubelet
11.3 kubeconfigが取得できない
症状
talosctl kubeconfig
# Error: failed to fetch kubeconfig: rpc error: ...
解決策
# talosconfigの確認
cat $TALOSCONFIG
# エンドポイントの確認
talosctl config info
12. 次回: 監視スタック構築
明日(12/6)は、構築したクラスタに監視スタックをデプロイします。
学べること
- kube-prometheus-stackをHelmでインストール
- Kustomizeでの環境別カスタマイズ
- AlertManager → Discord Webhook連携
- 専用ノードへのデプロイ(Taint & Toleration)
準備しておくこと
- Discord Webhookの作成
- Helmのインストール
brew install helm
まとめ
本記事では、Terraform × Hyper-VでTalos Linuxクラスタを構築し、LoadBalancerとIngress Controllerの基盤セットアップまで完了しました。
重要ポイント
- ✅ Infrastructure as Code で再現性の高い構築
- ✅ MBPからWindowsへのリモート管理
- ✅ Talos Linuxのブートストラップ手順
- ✅ MetalLB(LoadBalancer)のセットアップ
- ✅ Ingress NGINX Controllerのセットアップ
- ✅ 監視専用ノードの準備(ラベル + Taint)
所感
- ネットワーク設計は慎重に: とはいえ、まずはDHCPで構築してから作り直すほうが現実的です。最初から完璧を目指すと沼にハマります。
- Generation 1 VMでの構築: 筆者の機材が古いためGeneration 1世代での構築となりましたが、UEFIが使える環境ならGeneration 2で構築可能です。
- CNI選択の伏線: 後日CNIをFlannelにしたことで失敗するとは、このときは思わず...(詳細は後の記事で)
-
Terraformの待機時間:
terraform applyやterraform planの待機時間がかなり長く、Windows側のリソースなのか別要因なのかは後日調べたいところです(優雅にお茶入れてました😂)