initial scaffold
This commit is contained in:
commit
5c2080a73b
31 changed files with 1244 additions and 0 deletions
33
terraform/cloud-init/agent.yaml.tpl
Normal file
33
terraform/cloud-init/agent.yaml.tpl
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#cloud-config
|
||||
hostname: ${hostname}
|
||||
manage_etc_hosts: true
|
||||
|
||||
users:
|
||||
- name: k3s
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
ssh_authorized_keys:
|
||||
- ${ssh_public_key}
|
||||
|
||||
package_update: true
|
||||
packages:
|
||||
- curl
|
||||
|
||||
write_files:
|
||||
- path: /usr/local/bin/join-k3s.sh
|
||||
permissions: '0755'
|
||||
content: |
|
||||
#!/bin/sh
|
||||
# Server may still be booting; retry the join until it answers.
|
||||
until curl -sk https://${server_ip}:6443/ping >/dev/null 2>&1; do
|
||||
echo "waiting for k3s server at ${server_ip}..."
|
||||
sleep 5
|
||||
done
|
||||
curl -sfL https://get.k3s.io | \
|
||||
K3S_URL="https://${server_ip}:6443" \
|
||||
K3S_TOKEN="${k3s_token}" \
|
||||
sh -s - agent
|
||||
|
||||
runcmd:
|
||||
- /usr/local/bin/join-k3s.sh
|
||||
8
terraform/cloud-init/network-config.yaml.tpl
Normal file
8
terraform/cloud-init/network-config.yaml.tpl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
version: 2
|
||||
ethernets:
|
||||
eth0:
|
||||
addresses:
|
||||
- ${ip}/${prefix_length}
|
||||
gateway4: ${gateway}
|
||||
nameservers:
|
||||
addresses: [${gateway}]
|
||||
30
terraform/cloud-init/server.yaml.tpl
Normal file
30
terraform/cloud-init/server.yaml.tpl
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#cloud-config
|
||||
hostname: ${hostname}
|
||||
manage_etc_hosts: true
|
||||
|
||||
users:
|
||||
- name: k3s
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
ssh_authorized_keys:
|
||||
- ${ssh_public_key}
|
||||
|
||||
package_update: true
|
||||
packages:
|
||||
- curl
|
||||
|
||||
write_files:
|
||||
- path: /etc/rancher/k3s/config.yaml
|
||||
permissions: '0600'
|
||||
content: |
|
||||
token: "${k3s_token}"
|
||||
tls-san:
|
||||
- "${hostname}"
|
||||
- "${k8s_api_hostname}"
|
||||
|
||||
runcmd:
|
||||
- curl -sfL https://get.k3s.io | sh -s - server
|
||||
- mkdir -p /home/k3s/.kube
|
||||
- k3s kubectl config view --raw > /home/k3s/.kube/config
|
||||
- chown -R k3s:k3s /home/k3s/.kube
|
||||
95
terraform/main.tf
Normal file
95
terraform/main.tf
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
locals {
|
||||
server_node = [for name, n in var.nodes : n if n.role == "server"][0]
|
||||
server_ip = local.server_node.ip
|
||||
prefix_length = split("/", var.network_cidr)[1]
|
||||
}
|
||||
|
||||
resource "libvirt_pool" "k3s" {
|
||||
name = var.storage_pool
|
||||
type = "dir"
|
||||
path = var.storage_pool_path
|
||||
}
|
||||
|
||||
resource "libvirt_volume" "base" {
|
||||
name = "k3s-base.qcow2"
|
||||
pool = libvirt_pool.k3s.name
|
||||
source = var.base_image_url
|
||||
format = "qcow2"
|
||||
}
|
||||
|
||||
# A dedicated, isolated NAT network so this project can't collide with
|
||||
# anything else already using the host's default libvirt network. DHCP is
|
||||
# off — every node gets a static IP via cloud-init instead.
|
||||
resource "libvirt_network" "k3s" {
|
||||
name = "k3s-homelab"
|
||||
mode = "nat"
|
||||
domain = "k3s.local"
|
||||
addresses = [var.network_cidr]
|
||||
|
||||
dhcp {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
dns {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "libvirt_volume" "node" {
|
||||
for_each = var.nodes
|
||||
name = "${each.key}.qcow2"
|
||||
pool = libvirt_pool.k3s.name
|
||||
base_volume_id = libvirt_volume.base.id
|
||||
size = each.value.disk_gb * 1024 * 1024 * 1024
|
||||
format = "qcow2"
|
||||
}
|
||||
|
||||
resource "libvirt_cloudinit_disk" "node" {
|
||||
for_each = var.nodes
|
||||
name = "${each.key}-cloudinit.iso"
|
||||
pool = libvirt_pool.k3s.name
|
||||
|
||||
user_data = templatefile("${path.module}/cloud-init/${each.value.role}.yaml.tpl", {
|
||||
hostname = each.key
|
||||
ssh_public_key = var.ssh_public_key
|
||||
k3s_token = var.k3s_token
|
||||
server_ip = local.server_ip
|
||||
k8s_api_hostname = var.k8s_api_hostname
|
||||
})
|
||||
|
||||
network_config = templatefile("${path.module}/cloud-init/network-config.yaml.tpl", {
|
||||
ip = each.value.ip
|
||||
prefix_length = local.prefix_length
|
||||
gateway = var.gateway_ip
|
||||
})
|
||||
}
|
||||
|
||||
resource "libvirt_domain" "node" {
|
||||
for_each = var.nodes
|
||||
name = each.key
|
||||
vcpu = each.value.vcpu
|
||||
memory = each.value.memory
|
||||
|
||||
cloudinit = libvirt_cloudinit_disk.node[each.key].id
|
||||
|
||||
network_interface {
|
||||
network_id = libvirt_network.k3s.id
|
||||
wait_for_lease = false
|
||||
}
|
||||
|
||||
disk {
|
||||
volume_id = libvirt_volume.node[each.key].id
|
||||
}
|
||||
|
||||
# Serial console only, no display — this box doesn't need a GUI hop for
|
||||
# a couple of small demo VMs.
|
||||
console {
|
||||
type = "pty"
|
||||
target_type = "serial"
|
||||
target_port = "0"
|
||||
}
|
||||
|
||||
# Terraform brings all VMs up in parallel; the agent cloud-init script
|
||||
# (cloud-init/agent.yaml.tpl) retries the join until the server's API is
|
||||
# reachable, so node boot order doesn't matter.
|
||||
}
|
||||
8
terraform/outputs.tf
Normal file
8
terraform/outputs.tf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
output "server_ip" {
|
||||
description = "IP of the k3s server node — fetch its kubeconfig from here"
|
||||
value = local.server_ip
|
||||
}
|
||||
|
||||
output "node_ips" {
|
||||
value = { for name, n in var.nodes : name => n.ip }
|
||||
}
|
||||
14
terraform/terraform.tfvars.example
Normal file
14
terraform/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Copy to terraform.tfvars and fill in. terraform.tfvars is gitignored —
|
||||
# never commit real secrets.
|
||||
|
||||
# Default (qemu:///system) assumes you're running tofu on the T630 itself
|
||||
# as the 'k8s' user - see docs/SETUP.md step 2. Leave commented out unless
|
||||
# you're running Terraform from a separate workstation instead.
|
||||
# libvirt_uri = "qemu+ssh://k8s@t630.lan/system?keyfile=/home/you/.ssh/k3s_homelab"
|
||||
|
||||
ssh_public_key = "ssh-ed25519 AAAA... you@workstation"
|
||||
k3s_token = "generate-with: openssl rand -hex 32"
|
||||
|
||||
# Defaults in variables.tf (network_cidr, gateway_ip, node IPs/sizing) are
|
||||
# fine as-is for a first apply — override here only if they clash with
|
||||
# something else on the T630.
|
||||
68
terraform/variables.tf
Normal file
68
terraform/variables.tf
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
variable "libvirt_uri" {
|
||||
description = "libvirt connection URI. Default assumes tofu/kubectl/flux all run directly on the T630 as the 'k8s' user (see docs/SETUP.md step 2) - simplest option, since node IPs (network_cidr) are only directly reachable from the T630 itself. Use qemu+ssh://k8s@t630.lan/system?keyfile=... instead if you'd rather run Terraform from a separate workstation (you'll then need an SSH tunnel for kubectl/flux to reach node IPs - see step 6)."
|
||||
type = string
|
||||
default = "qemu:///system"
|
||||
}
|
||||
|
||||
variable "base_image_url" {
|
||||
description = "Cloud image libvirt clones for every node's disk (downloaded once, cached in the pool)"
|
||||
type = string
|
||||
default = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
|
||||
}
|
||||
|
||||
variable "storage_pool" {
|
||||
description = "Name of the libvirt storage pool this project's disks live in (created if missing)"
|
||||
type = string
|
||||
default = "k3s-homelab"
|
||||
}
|
||||
|
||||
variable "storage_pool_path" {
|
||||
description = "Host filesystem path backing the storage pool"
|
||||
type = string
|
||||
default = "/var/lib/libvirt/images/k3s-homelab"
|
||||
}
|
||||
|
||||
variable "network_cidr" {
|
||||
description = "Subnet for the dedicated NAT network this project's VMs live on (isolated from any other libvirt networks already on the host)"
|
||||
type = string
|
||||
default = "10.20.30.0/24"
|
||||
}
|
||||
|
||||
variable "gateway_ip" {
|
||||
description = "Gateway address within network_cidr (libvirt itself, on the host)"
|
||||
type = string
|
||||
default = "10.20.30.1"
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "Public key injected into each VM via cloud-init for the 'k3s' admin user"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "k3s_token" {
|
||||
description = "Shared cluster token agents use to join the k3s server"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "k8s_api_hostname" {
|
||||
description = "LAN-only hostname for the k3s API server, added to the server's TLS SAN list so client-cert kubeconfigs validate against it. Resolve it via local DNS only (never a public record) - see docs/SETUP.md step 13."
|
||||
type = string
|
||||
default = "k8s-api.boglabob.com"
|
||||
}
|
||||
|
||||
variable "nodes" {
|
||||
description = "k3s nodes to provision. Sizing is deliberately small (2 vCPU/2GB each = 6GB total) so this stays a demo, not a resource hog, alongside the T630's other services."
|
||||
type = map(object({
|
||||
role = string # "server" or "agent"
|
||||
ip = string # e.g. "10.20.30.11" - must be inside network_cidr
|
||||
vcpu = number
|
||||
memory = number # MiB
|
||||
disk_gb = number
|
||||
}))
|
||||
default = {
|
||||
"k3s-server-1" = { role = "server", ip = "10.20.30.11", vcpu = 2, memory = 2048, disk_gb = 20 }
|
||||
"k3s-agent-1" = { role = "agent", ip = "10.20.30.12", vcpu = 2, memory = 2048, disk_gb = 20 }
|
||||
"k3s-agent-2" = { role = "agent", ip = "10.20.30.13", vcpu = 2, memory = 2048, disk_gb = 20 }
|
||||
}
|
||||
}
|
||||
17
terraform/versions.tf
Normal file
17
terraform/versions.tf
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
libvirt = {
|
||||
source = "dmacvicar/libvirt"
|
||||
version = "~> 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# qemu+ssh:// so this can be run from your workstation against the T630;
|
||||
# requires the connecting user to be in the T630's `libvirt` and `kvm`
|
||||
# groups (see docs/SETUP.md step 2) — no sudo needed after that.
|
||||
provider "libvirt" {
|
||||
uri = var.libvirt_uri
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue