Add Terraform VM-provisioning skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe4228fb38
commit
9a8181ef18
16 changed files with 491 additions and 0 deletions
13
terraform/README.md
Normal file
13
terraform/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# terraform/
|
||||
|
||||
Infrastructure provisioning. Terraform owns **VM existence only** — creating and
|
||||
destroying Proxmox VMs. It writes no DNS records and configures nothing inside a
|
||||
VM; Ansible owns all of that.
|
||||
|
||||
- `modules/proxmox_vm/` — reusable VM module (Proxmox only).
|
||||
- `environments/{staging,production}/` — separate state per environment. Add a VM by
|
||||
editing `local.vms` in that env's `main.tf`, then `make tf-plan` → `tf-apply` →
|
||||
`tf-inventory`.
|
||||
|
||||
Rationale: **ADR-006**. Handoff to Ansible: **ADR-009**. Secrets via `TF_VAR_*`
|
||||
only — never in `.tfvars`. Not yet `terraform init`ed — see `STATUS.md`.
|
||||
17
terraform/environments/production/backend.tf
Normal file
17
terraform/environments/production/backend.tf
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
terraform {
|
||||
backend "http" {
|
||||
# Forgejo HTTP state backend.
|
||||
# Replace <owner> and <repo> with your Forgejo organisation and repository name.
|
||||
# Verify the exact path format against your running Forgejo instance's API docs.
|
||||
# Authentication: set TF_HTTP_USERNAME (Forgejo username) and
|
||||
# TF_HTTP_PASSWORD (Forgejo personal access token) as environment variables.
|
||||
#
|
||||
# If Forgejo's HTTP state endpoint is unavailable, remove this block entirely
|
||||
# to fall back to local state on the control node.
|
||||
address = "https://git.baobab.band/api/v1/repos/<owner>/<repo>/raw/terraform/state/production.tfstate"
|
||||
lock_address = "https://git.baobab.band/api/v1/repos/<owner>/<repo>/raw/terraform/state/production.tfstate/lock"
|
||||
unlock_address = "https://git.baobab.band/api/v1/repos/<owner>/<repo>/raw/terraform/state/production.tfstate/lock"
|
||||
lock_method = "POST"
|
||||
unlock_method = "DELETE"
|
||||
}
|
||||
}
|
||||
41
terraform/environments/production/main.tf
Normal file
41
terraform/environments/production/main.tf
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# production/main.tf — Production VM definitions
|
||||
# Add entries to local.vms to provision VMs; remove to destroy them.
|
||||
# ALWAYS run `make tf-plan TF_ENV=production` and review before `make tf-apply TF_ENV=production`.
|
||||
|
||||
locals {
|
||||
vms = {
|
||||
# control01 = {
|
||||
# ip = "192.168.1.10/24"
|
||||
# group = "control"
|
||||
# cores = 2
|
||||
# memory_mb = 2048
|
||||
# }
|
||||
# docker01 = {
|
||||
# ip = "192.168.1.11/24"
|
||||
# group = "docker_hosts"
|
||||
# cores = 4
|
||||
# memory_mb = 4096
|
||||
# }
|
||||
}
|
||||
}
|
||||
|
||||
module "vms" {
|
||||
for_each = local.vms
|
||||
source = "../../modules/proxmox_vm"
|
||||
|
||||
vm_name = each.key
|
||||
target_node = var.proxmox_node
|
||||
clone_template_id = var.vm_template_id
|
||||
datastore_id = var.vm_datastore_id
|
||||
ip_address = each.value.ip
|
||||
gateway = var.gateway
|
||||
dns_servers = var.dns_servers
|
||||
dns_domain = var.dns_domain
|
||||
ssh_public_keys = var.ssh_public_keys
|
||||
cores = each.value.cores
|
||||
memory_mb = each.value.memory_mb
|
||||
tags = ["production", each.value.group]
|
||||
}
|
||||
|
||||
# Internal DNS records are NOT managed here. Terraform owns VM existence only;
|
||||
# the Ansible `dns` role renders the internal zone from inventory. See ADR-009.
|
||||
9
terraform/environments/production/outputs.tf
Normal file
9
terraform/environments/production/outputs.tf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
output "vms" {
|
||||
description = "Map of hostname to IP and Ansible group — consumed by make tf-inventory"
|
||||
value = {
|
||||
for name, cfg in local.vms : name => {
|
||||
ip = split("/", cfg.ip)[0]
|
||||
group = cfg.group
|
||||
}
|
||||
}
|
||||
}
|
||||
16
terraform/environments/production/providers.tf
Normal file
16
terraform/environments/production/providers.tf
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
terraform {
|
||||
required_version = ">= 1.9"
|
||||
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "bpg/proxmox"
|
||||
version = "~> 0.70"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "proxmox" {
|
||||
endpoint = var.proxmox_endpoint
|
||||
api_token = var.proxmox_api_token
|
||||
insecure = var.proxmox_insecure
|
||||
}
|
||||
23
terraform/environments/production/terraform.tfvars.example
Normal file
23
terraform/environments/production/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Production environment — non-secret values
|
||||
# Copy to terraform.tfvars and fill in your values.
|
||||
#
|
||||
# Secrets must be exported as environment variables before running Terraform:
|
||||
# export TF_VAR_proxmox_api_token="terraform@pve!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
#
|
||||
# Forgejo backend credentials:
|
||||
# export TF_HTTP_USERNAME="your-forgejo-username"
|
||||
# export TF_HTTP_PASSWORD="your-forgejo-personal-access-token"
|
||||
|
||||
proxmox_endpoint = "https://pve01.baobab.band:8006/"
|
||||
proxmox_insecure = false
|
||||
proxmox_node = "pve01"
|
||||
vm_template_id = 9000 # Proxmox VM ID of the Debian 13 cloud-init template
|
||||
vm_datastore_id = "local-lvm"
|
||||
|
||||
gateway = "10.20.0.1"
|
||||
dns_servers = ["10.20.0.10", "10.20.0.11"]
|
||||
dns_domain = "boma.baobab.band"
|
||||
|
||||
ssh_public_keys = [
|
||||
# "ssh-ed25519 AAAA... user@host",
|
||||
]
|
||||
58
terraform/environments/production/variables.tf
Normal file
58
terraform/environments/production/variables.tf
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# ── Proxmox ───────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "proxmox_endpoint" {
|
||||
description = "Proxmox API URL, e.g. https://pve01.baobab.band:8006/"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "proxmox_api_token" {
|
||||
description = "Proxmox API token (user@realm!tokenid=secret) — set via TF_VAR_proxmox_api_token"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proxmox_insecure" {
|
||||
description = "Skip TLS verification for the Proxmox API (true while using a self-signed cert)"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "proxmox_node" {
|
||||
description = "Default Proxmox node name to place VMs on"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vm_template_id" {
|
||||
description = "Proxmox VM ID of the Debian 13 cloud-init template to clone"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "vm_datastore_id" {
|
||||
description = "Proxmox datastore for VM disks and cloud-init drives"
|
||||
type = string
|
||||
default = "local-lvm"
|
||||
}
|
||||
|
||||
variable "gateway" {
|
||||
description = "Default IPv4 gateway for all VMs"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "dns_servers" {
|
||||
description = "DNS servers provided to VMs via cloud-init"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "dns_domain" {
|
||||
description = "Search domain provided to VMs via cloud-init"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_public_keys" {
|
||||
description = "Public SSH keys provisioned for the ansible user on each VM"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
# Note: `dns_servers` / `dns_domain` above are the cloud-init *resolver* settings
|
||||
# (which DNS server a VM queries). Terraform does not write DNS *records* — the
|
||||
# Ansible `dns` role owns the internal zone. See ADR-009.
|
||||
17
terraform/environments/staging/backend.tf
Normal file
17
terraform/environments/staging/backend.tf
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
terraform {
|
||||
backend "http" {
|
||||
# Forgejo HTTP state backend.
|
||||
# Replace <owner> and <repo> with your Forgejo organisation and repository name.
|
||||
# Verify the exact path format against your running Forgejo instance's API docs.
|
||||
# Authentication: set TF_HTTP_USERNAME (Forgejo username) and
|
||||
# TF_HTTP_PASSWORD (Forgejo personal access token) as environment variables.
|
||||
#
|
||||
# If Forgejo's HTTP state endpoint is unavailable, remove this block entirely
|
||||
# to fall back to local state on the control node.
|
||||
address = "https://git.baobab.band/api/v1/repos/<owner>/<repo>/raw/terraform/state/staging.tfstate"
|
||||
lock_address = "https://git.baobab.band/api/v1/repos/<owner>/<repo>/raw/terraform/state/staging.tfstate/lock"
|
||||
unlock_address = "https://git.baobab.band/api/v1/repos/<owner>/<repo>/raw/terraform/state/staging.tfstate/lock"
|
||||
lock_method = "POST"
|
||||
unlock_method = "DELETE"
|
||||
}
|
||||
}
|
||||
35
terraform/environments/staging/main.tf
Normal file
35
terraform/environments/staging/main.tf
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# staging/main.tf — Staging VM definitions
|
||||
# Add entries to local.vms to provision VMs; remove to destroy them.
|
||||
# Run `make tf-plan TF_ENV=staging` before `make tf-apply TF_ENV=staging`.
|
||||
|
||||
locals {
|
||||
vms = {
|
||||
# staging01 = {
|
||||
# ip = "192.168.1.20/24"
|
||||
# group = "docker_hosts"
|
||||
# cores = 2
|
||||
# memory_mb = 2048
|
||||
# }
|
||||
}
|
||||
}
|
||||
|
||||
module "vms" {
|
||||
for_each = local.vms
|
||||
source = "../../modules/proxmox_vm"
|
||||
|
||||
vm_name = each.key
|
||||
target_node = var.proxmox_node
|
||||
clone_template_id = var.vm_template_id
|
||||
datastore_id = var.vm_datastore_id
|
||||
ip_address = each.value.ip
|
||||
gateway = var.gateway
|
||||
dns_servers = var.dns_servers
|
||||
dns_domain = var.dns_domain
|
||||
ssh_public_keys = var.ssh_public_keys
|
||||
cores = each.value.cores
|
||||
memory_mb = each.value.memory_mb
|
||||
tags = ["staging", each.value.group]
|
||||
}
|
||||
|
||||
# Internal DNS records are NOT managed here. Terraform owns VM existence only;
|
||||
# the Ansible `dns` role renders the internal zone from inventory. See ADR-009.
|
||||
9
terraform/environments/staging/outputs.tf
Normal file
9
terraform/environments/staging/outputs.tf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
output "vms" {
|
||||
description = "Map of hostname to IP and Ansible group — consumed by make tf-inventory"
|
||||
value = {
|
||||
for name, cfg in local.vms : name => {
|
||||
ip = split("/", cfg.ip)[0]
|
||||
group = cfg.group
|
||||
}
|
||||
}
|
||||
}
|
||||
16
terraform/environments/staging/providers.tf
Normal file
16
terraform/environments/staging/providers.tf
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
terraform {
|
||||
required_version = ">= 1.9"
|
||||
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "bpg/proxmox"
|
||||
version = "~> 0.70"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "proxmox" {
|
||||
endpoint = var.proxmox_endpoint
|
||||
api_token = var.proxmox_api_token
|
||||
insecure = var.proxmox_insecure
|
||||
}
|
||||
23
terraform/environments/staging/terraform.tfvars.example
Normal file
23
terraform/environments/staging/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Staging environment — non-secret values
|
||||
# Copy to terraform.tfvars and fill in your values.
|
||||
#
|
||||
# Secrets must be exported as environment variables before running Terraform:
|
||||
# export TF_VAR_proxmox_api_token="terraform@pve!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
#
|
||||
# Forgejo backend credentials:
|
||||
# export TF_HTTP_USERNAME="your-forgejo-username"
|
||||
# export TF_HTTP_PASSWORD="your-forgejo-personal-access-token"
|
||||
|
||||
proxmox_endpoint = "https://pve01.baobab.band:8006/"
|
||||
proxmox_insecure = true # set false once a valid TLS cert is in place
|
||||
proxmox_node = "pve01"
|
||||
vm_template_id = 9000 # Proxmox VM ID of the Debian 13 cloud-init template
|
||||
vm_datastore_id = "local-lvm"
|
||||
|
||||
gateway = "10.20.0.1"
|
||||
dns_servers = ["10.20.0.10", "10.20.0.11"]
|
||||
dns_domain = "boma.baobab.band"
|
||||
|
||||
ssh_public_keys = [
|
||||
# "ssh-ed25519 AAAA... user@host",
|
||||
]
|
||||
58
terraform/environments/staging/variables.tf
Normal file
58
terraform/environments/staging/variables.tf
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# ── Proxmox ───────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "proxmox_endpoint" {
|
||||
description = "Proxmox API URL, e.g. https://pve01.baobab.band:8006/"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "proxmox_api_token" {
|
||||
description = "Proxmox API token (user@realm!tokenid=secret) — set via TF_VAR_proxmox_api_token"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proxmox_insecure" {
|
||||
description = "Skip TLS verification for the Proxmox API (true while using a self-signed cert)"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "proxmox_node" {
|
||||
description = "Default Proxmox node name to place VMs on"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vm_template_id" {
|
||||
description = "Proxmox VM ID of the Debian 13 cloud-init template to clone"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "vm_datastore_id" {
|
||||
description = "Proxmox datastore for VM disks and cloud-init drives"
|
||||
type = string
|
||||
default = "local-lvm"
|
||||
}
|
||||
|
||||
variable "gateway" {
|
||||
description = "Default IPv4 gateway for all VMs"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "dns_servers" {
|
||||
description = "DNS servers provided to VMs via cloud-init"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "dns_domain" {
|
||||
description = "Search domain provided to VMs via cloud-init"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_public_keys" {
|
||||
description = "Public SSH keys provisioned for the ansible user on each VM"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
# Note: `dns_servers` / `dns_domain` above are the cloud-init *resolver* settings
|
||||
# (which DNS server a VM queries). Terraform does not write DNS *records* — the
|
||||
# Ansible `dns` role owns the internal zone. See ADR-009.
|
||||
66
terraform/modules/proxmox_vm/main.tf
Normal file
66
terraform/modules/proxmox_vm/main.tf
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
resource "proxmox_virtual_environment_vm" "this" {
|
||||
name = var.vm_name
|
||||
node_name = var.target_node
|
||||
tags = var.tags
|
||||
|
||||
clone {
|
||||
vm_id = var.clone_template_id
|
||||
full = true
|
||||
}
|
||||
|
||||
agent {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
cpu {
|
||||
cores = var.cores
|
||||
type = "x86-64-v2-AES"
|
||||
}
|
||||
|
||||
memory {
|
||||
dedicated = var.memory_mb
|
||||
}
|
||||
|
||||
disk {
|
||||
datastore_id = var.datastore_id
|
||||
interface = "scsi0"
|
||||
size = var.disk_size_gb
|
||||
file_format = "raw"
|
||||
discard = "on"
|
||||
|
||||
lifecycle {
|
||||
# Proxmox disallows disk shrinks; ignore if disk was grown outside Terraform.
|
||||
ignore_changes = [size]
|
||||
}
|
||||
}
|
||||
|
||||
network_device {
|
||||
bridge = "vmbr0"
|
||||
vlan_id = var.vlan_tag
|
||||
}
|
||||
|
||||
initialization {
|
||||
datastore_id = var.datastore_id
|
||||
|
||||
ip_config {
|
||||
ipv4 {
|
||||
address = var.ip_address
|
||||
gateway = var.gateway
|
||||
}
|
||||
}
|
||||
|
||||
dns {
|
||||
domain = var.dns_domain
|
||||
servers = var.dns_servers
|
||||
}
|
||||
|
||||
user_account {
|
||||
username = "ansible"
|
||||
keys = var.ssh_public_keys
|
||||
}
|
||||
}
|
||||
|
||||
operating_system {
|
||||
type = "l26"
|
||||
}
|
||||
}
|
||||
14
terraform/modules/proxmox_vm/outputs.tf
Normal file
14
terraform/modules/proxmox_vm/outputs.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
output "vm_id" {
|
||||
description = "Proxmox VM ID"
|
||||
value = proxmox_virtual_environment_vm.this.vm_id
|
||||
}
|
||||
|
||||
output "ipv4_address" {
|
||||
description = "VM's static IPv4 address (without prefix length)"
|
||||
value = split("/", var.ip_address)[0]
|
||||
}
|
||||
|
||||
output "hostname" {
|
||||
description = "VM hostname"
|
||||
value = var.vm_name
|
||||
}
|
||||
76
terraform/modules/proxmox_vm/variables.tf
Normal file
76
terraform/modules/proxmox_vm/variables.tf
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
variable "vm_name" {
|
||||
description = "Hostname and Proxmox display name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "target_node" {
|
||||
description = "Proxmox node name to place the VM on"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "clone_template_id" {
|
||||
description = "Proxmox VM ID of the cloud-init template to clone"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "cores" {
|
||||
description = "CPU cores"
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "memory_mb" {
|
||||
description = "RAM in MB"
|
||||
type = number
|
||||
default = 2048
|
||||
}
|
||||
|
||||
variable "disk_size_gb" {
|
||||
description = "Boot disk size in GB"
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "datastore_id" {
|
||||
description = "Proxmox datastore for disk and cloud-init drive"
|
||||
type = string
|
||||
default = "local-lvm"
|
||||
}
|
||||
|
||||
variable "ip_address" {
|
||||
description = "Static IPv4 address in CIDR notation, e.g. 192.168.1.10/24"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "gateway" {
|
||||
description = "Default IPv4 gateway"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "dns_servers" {
|
||||
description = "DNS server addresses passed to cloud-init"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "dns_domain" {
|
||||
description = "Search domain passed to cloud-init"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "ssh_public_keys" {
|
||||
description = "Public SSH keys provisioned for the ansible user via cloud-init"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "vlan_tag" {
|
||||
description = "802.1q VLAN tag; null disables tagging"
|
||||
type = number
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Proxmox UI tags"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue