2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kubernetesを学ぶ9日間 - Day 3: Terraform × Hyper-VでTalos Linuxクラスタ構築

2
Posted at

はじめに

「ジャンルなしオンラインもくもく会 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-IP10.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

確認事項

  1. Hyper-V VMが起動しているか

    Get-VM | Where-Object {$_.State -eq 'Running'}
    
  2. ネットワーク疎通確認

    ping 192.168.1.100
    
  3. 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の基盤セットアップまで完了しました。

重要ポイント

  1. ✅ Infrastructure as Code で再現性の高い構築
  2. ✅ MBPからWindowsへのリモート管理
  3. ✅ Talos Linuxのブートストラップ手順
  4. ✅ MetalLB(LoadBalancer)のセットアップ
  5. ✅ Ingress NGINX Controllerのセットアップ
  6. ✅ 監視専用ノードの準備(ラベル + Taint)

所感

  • ネットワーク設計は慎重に: とはいえ、まずはDHCPで構築してから作り直すほうが現実的です。最初から完璧を目指すと沼にハマります。
  • Generation 1 VMでの構築: 筆者の機材が古いためGeneration 1世代での構築となりましたが、UEFIが使える環境ならGeneration 2で構築可能です。
  • CNI選択の伏線: 後日CNIをFlannelにしたことで失敗するとは、このときは思わず...(詳細は後の記事で)
  • Terraformの待機時間: terraform applyterraform planの待機時間がかなり長く、Windows側のリソースなのか別要因なのかは後日調べたいところです(優雅にお茶入れてました😂)

参考リンク

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?