自宅サーバ(2) - 設計のコード化

概要

Proxmox VE を GUI で操作し、LXC/VM を手動構築する段階を受けて、構成管理をコード化した際の設計と実装をまとめたものです。

背景

物理マシンへの Proxmox VE 導入、LXC/VM の手動作成、サービスごとの個別セットアップを実施しました。学習としては分かりやすい反面、手順が人の記憶に依存しやすい・変更履歴を追いにくい・障害時の復旧や別環境への複製に時間がかかるといった課題があります。

このため、インフラ定義とアプリ設定をコードとして管理し、状態を再現しやすい運用へ移行しました。

方針

構成管理は二層に分離しています。ひとつは Terraform によるインフラリソースの宣言管理、もうひとつは Ansible によるミドルウェア・アプリ設定の投入です。

リポジトリ構成

構成は次のようになっています。

homelab/
├── Makefile
├── terraform/
│   ├── .terraform.lock.hcl
│   ├── provider.tf
│   ├── variables.tf
│   ├── main.tf
│   ├── outputs.tf
│   └── terraform.tfvars
└── ansible/
  ├── ansible.cfg
  ├── inventory.yaml
  ├── site.yaml
  └── roles/
    ├── common/
    ├── adguard/
    ├── cloudflared/
    ├── tailscale/
    ├── nginx/
    ├── webserver/
    ├── docker/
    ├── obsidian/
    └── github-runner/

terraform/ は Proxmox の VM と Cloudflare 側の公開経路を宣言し、ansible/ は作成済みホストへ role を適用して期待した状態にそろえる構成です。

全体の流れ

この構成では、Terraform、Cloud-Init、Ansible を順番に使います。Terraform は VM や Tunnel/DNS の枠を作り、同時に Cloud-Init へ初期値を渡します。Cloud-Init は初回起動時にネットワークと SSH ログインを成立させ、Ansible が接続できる最小状態を作ります。最後に Ansible がミドルウェアやアプリ設定を投入し、ホストを期待した状態にします。

Terraform

provider.tf で Proxmox と Cloudflare の接続設定を持ち、variables.tf で入力を受け、main.tf で VM と Tunnel/DNS を定義し、outputs.tf で後段が参照する値を返す構成です。terraform.tfvars はローカル環境の値を注入するために使います。Provider は bpg/proxmoxcloudflare/cloudflare を利用しています。

VM は Cloud-Init テンプレートから clone し、initialization で IP、gateway、SSH 公開鍵を渡します。これにより、作成直後のノードへ Ansible を可能にします。

設定の一部です。

terraform {
  required_providers {
    proxmox = {
      source  = "bpg/proxmox"
      version = "~> 0.99"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 5.17"
    }
  }
}

resource "proxmox_virtual_environment_vm" "nodes" {
  for_each  = var.vms
  name      = each.value.hostname
  node_name = var.proxmox_node_name
  vm_id     = each.value.vm_id

  clone {
    vm_id = var.vm_template_id
  }

  initialization {
    user_account {
      keys = [var.ssh_public_key]
    }
    ip_config {
      ipv4 {
        address = "${var.network_prefix}.${each.value.vm_id}/24"
        gateway = var.gateway
      }
    }
  }
}

resource "cloudflare_zero_trust_tunnel_cloudflared" "main" {
  account_id = var.cloudflare_account_id
  name       = "ggme-tunnel"
  config_src = "cloudflare"
}

Cloud-Init テンプレート作成

Cloud-Init のテンプレートは、qcow2 を取り込んで一度 VM を組み立て、テンプレート化してから Terraform で clone 元として使います。ここでは VM ID 9000 を例にしています。

# 1) テンプレートの土台VMを作成
qm create 9000 --name "debian-13-template" --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0

# 2) qcow2をインポートして接続
qm importdisk 9000 debian-13-genericcloud-amd64.qcow2 local-lvm
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0

# 3) Cloud-Initドライブと起動設定
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --boot c --bootdisk scsi0
qm set 9000 --serial0 socket --vga serial0

# 4) テンプレート化
qm template 9000

Ansible

ansible.cfg で実行設定を集約し、inventory.yaml でホストグループを定義し、site.yaml で適用順を管理します。role は共通設定、DNS、トンネル、VPN、Web、Docker、Obsidian、GitHub Runner のように責務で分割しています。

playbook は、まず全ホストへ共通設定を入れ、次にグループごとの role を順に適用する構成です。

- name: Apply common configuration
  hosts: all
  become: true
  roles:
    - common

- name: Setup AdGuard Home
  hosts: dns
  become: true
  roles:
    - adguard

- name: Setup Web Servers
  hosts: web
  become: true
  roles:
    - nginx
    - webserver