opentofu-experiments #4

Merged
torjus merged 3 commits from opentofu-experiments into master 2026-01-31 22:07:23 +00:00
14 changed files with 519 additions and 0 deletions
Showing only changes of commit 3a464bc323 - Show all commits

10
.gitignore vendored
View File

@@ -1,2 +1,12 @@
.direnv/ .direnv/
result result
# Terraform/OpenTofu
terraform/.terraform/
terraform/.terraform.lock.hcl
terraform/*.tfstate
terraform/*.tfstate.*
terraform/terraform.tfvars
terraform/*.auto.tfvars
terraform/crash.log
terraform/crash.*.log

View File

@@ -44,6 +44,15 @@ nix develop
Secrets are handled by sops. Do not edit any `.sops.yaml` or any file within `secrets/`. Ask the user to modify if necessary. Secrets are handled by sops. Do not edit any `.sops.yaml` or any file within `secrets/`. Ask the user to modify if necessary.
### Git Commit Messages
Commit messages should follow the format: `topic: short description`
Examples:
- `flake: add opentofu to devshell`
- `template2: add proxmox image configuration`
- `terraform: add VM deployment configuration`
## Architecture ## Architecture
### Directory Structure ### Directory Structure
@@ -143,6 +152,57 @@ Configured in `/system/autoupgrade.nix`:
- Auto-reboot after successful upgrade - Auto-reboot after successful upgrade
- Systemd service: `nixos-upgrade.service` - Systemd service: `nixos-upgrade.service`
### Proxmox VM Provisioning with OpenTofu
The repository includes automated workflows for building Proxmox VM templates and deploying VMs using OpenTofu (Terraform).
#### Building and Deploying Templates
Template VMs are built from `hosts/template2` and deployed to Proxmox using Ansible:
```bash
# Build NixOS image and deploy to Proxmox as template
nix develop -c ansible-playbook -i playbooks/inventory.ini playbooks/build-and-deploy-template.yml
```
This playbook:
1. Builds the Proxmox image using `nixos-rebuild build-image --image-variant proxmox`
2. Uploads the `.vma.zst` image to Proxmox at `/var/lib/vz/dump`
3. Restores it as VM ID 9000
4. Converts it to a template
Template configuration (`hosts/template2`):
- Minimal base system with essential packages (age, vim, wget, git)
- Cloud-init configured for NoCloud datasource (no EC2 metadata timeout)
- DHCP networking on ens18
- SSH key-based root login
- `prepare-host.sh` script for cleaning machine-id, SSH keys, and regenerating age keys
#### Deploying VMs with OpenTofu
VMs are deployed from templates using OpenTofu in the `/terraform` directory:
```bash
cd terraform
tofu init # First time only
tofu apply # Deploy VMs
```
Configuration files:
- `main.tf` - Proxmox provider configuration
- `variables.tf` - Provider variables (API credentials)
- `vm.tf` - VM resource definitions
- `terraform.tfvars` - Actual credentials (gitignored)
Example VM deployment includes:
- Clone from template VM
- Cloud-init configuration (SSH keys, network, DNS)
- Custom CPU/memory/disk sizing
- VLAN tagging
- QEMU guest agent
OpenTofu outputs the VM's IP address after deployment for easy SSH access.
### Adding a New Host ### Adding a New Host
1. Create `/hosts/<hostname>/` directory 1. Create `/hosts/<hostname>/` directory

View File

@@ -172,6 +172,22 @@
sops-nix.nixosModules.sops sops-nix.nixosModules.sops
]; ];
}; };
template2 = nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit inputs self sops-nix;
};
modules = [
(
{ config, pkgs, ... }:
{
nixpkgs.overlays = commonOverlays;
}
)
./hosts/template2
sops-nix.nixosModules.sops
];
};
http-proxy = nixpkgs.lib.nixosSystem { http-proxy = nixpkgs.lib.nixosSystem {
inherit system; inherit system;
specialArgs = { specialArgs = {

View File

@@ -0,0 +1,75 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [
./hardware-configuration.nix
../../system/sshd.nix
];
# Root user with no password but SSH key access for bootstrapping
users.users.root = {
hashedPassword = "";
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAwfb2jpKrBnCw28aevnH8HbE5YbcMXpdaVv2KmueDu6 torjus@gunter"
];
};
# Proxmox image-specific configuration
# Configure storage to use local-zfs instead of local-lvm
image.modules.proxmox = {
proxmox.qemuConf.virtio0 = lib.mkForce "local-zfs:vm-9999-disk-0";
proxmox.qemuConf.boot = lib.mkForce "order=virtio0";
proxmox.cloudInit.defaultStorage = lib.mkForce "local-zfs";
};
# Configure cloud-init to only use NoCloud datasource (no EC2 metadata service)
services.cloud-init.settings = {
datasource_list = [ "NoCloud" ];
datasource = {
NoCloud = {
fs_label = "cidata";
};
};
};
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/vda";
networking.hostName = "nixos-template2";
networking.domain = "home.2rjus.net";
networking.useNetworkd = true;
networking.useDHCP = false;
services.resolved.enable = true;
systemd.network.enable = true;
systemd.network.networks."ens18" = {
matchConfig.Name = "ens18";
networkConfig.DHCP = "ipv4";
linkConfig.RequiredForOnline = "routable";
};
time.timeZone = "Europe/Oslo";
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
nix.settings.tarball-ttl = 0;
environment.systemPackages = with pkgs; [
age
vim
wget
git
];
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
networking.firewall.enable = false;
system.stateVersion = "25.11";
}

View File

@@ -0,0 +1,9 @@
{ ... }:
{
imports = [
./hardware-configuration.nix
./configuration.nix
./scripts.nix
../../system/packages.nix
];
}

View File

@@ -0,0 +1,36 @@
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"virtio_pci"
"virtio_scsi"
"sd_mod"
"sr_mod"
];
boot.initrd.kernelModules = [ "dm-snapshot" ];
boot.kernelModules = [
"ptp_kvm"
"virtio_rng" # Provides entropy from host for fast SSH key generation
];
boot.extraModulePackages = [ ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.ens18.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}

View File

@@ -0,0 +1,33 @@
{ pkgs, ... }:
let
prepare-host-script = pkgs.writeShellScriptBin "prepare-host.sh"
''
echo "Removing machine-id"
rm -f /etc/machine-id || true
echo "Removing SSH host keys"
rm -f /etc/ssh/ssh_host_* || true
echo "Restarting SSH"
systemctl restart sshd
echo "Removing temporary files"
rm -rf /tmp/* || true
echo "Removing logs"
journalctl --rotate || true
journalctl --vacuum-time=1s || true
echo "Removing cache"
rm -rf /var/cache/* || true
echo "Generate age key"
rm -rf /var/lib/sops-nix || true
mkdir -p /var/lib/sops-nix
${pkgs.age}/bin/age-keygen -o /var/lib/sops-nix/key.txt
'';
in
{
environment.systemPackages = [ prepare-host-script ];
users.motd = "Prepare host by running 'prepare-host.sh'.";
}

View File

@@ -0,0 +1,101 @@
---
- name: Build and deploy NixOS Proxmox template
hosts: localhost
gather_facts: false
vars:
template_name: "template2"
nixos_config: "template2"
proxmox_node: "pve1.home.2rjus.net" # Change to your Proxmox node name
proxmox_host: "pve1.home.2rjus.net" # Change to your Proxmox host
template_vmid: 9000 # Template VM ID
storage: "local-zfs"
tasks:
- name: Build NixOS image
ansible.builtin.command:
cmd: "nixos-rebuild build-image --image-variant proxmox --flake .#template2"
chdir: "{{ playbook_dir }}/.."
register: build_result
changed_when: true
- name: Find built image file
ansible.builtin.find:
paths: "{{ playbook_dir}}/../result"
patterns: "*.vma.zst"
recurse: true
register: image_files
- name: Fail if no image found
ansible.builtin.fail:
msg: "No QCOW2 image found in build output"
when: image_files.matched == 0
- name: Set image path
ansible.builtin.set_fact:
image_path: "{{ image_files.files[0].path }}"
- name: Extract image filename
ansible.builtin.set_fact:
image_filename: "{{ image_path | basename }}"
- name: Display image info
ansible.builtin.debug:
msg: "Built image: {{ image_path }} ({{ image_filename }})"
- name: Deploy template to Proxmox
hosts: proxmox
gather_facts: false
vars:
template_name: "template2"
template_vmid: 9000
storage: "local-zfs"
tasks:
- name: Get image path and filename from localhost
ansible.builtin.set_fact:
image_path: "{{ hostvars['localhost']['image_path'] }}"
image_filename: "{{ hostvars['localhost']['image_filename'] }}"
- name: Set destination path
ansible.builtin.set_fact:
image_dest: "/var/lib/vz/dump/{{ image_filename }}"
- name: Copy image to Proxmox
ansible.builtin.copy:
src: "{{ image_path }}"
dest: "{{ image_dest }}"
mode: '0644'
- name: Check if template VM already exists
ansible.builtin.command:
cmd: "qm status {{ template_vmid }}"
register: vm_status
failed_when: false
changed_when: false
- name: Destroy existing template VM if it exists
ansible.builtin.command:
cmd: "qm destroy {{ template_vmid }} --purge"
when: vm_status.rc == 0
changed_when: true
- name: Import image
ansible.builtin.command:
cmd: "qmrestore {{ image_dest }} {{ template_vmid }}"
changed_when: true
- name: Convert VM to template
ansible.builtin.command:
cmd: "qm template {{ template_vmid }}"
changed_when: true
- name: Clean up uploaded image
ansible.builtin.file:
path: "{{ image_dest }}"
state: absent
- name: Display success message
ansible.builtin.debug:
msg: "Template VM {{ template_vmid }} created successfully on {{ storage }}"

5
playbooks/inventory.ini Normal file
View File

@@ -0,0 +1,5 @@
[proxmox]
pve1.home.2rjus.net
[proxmox:vars]
ansible_user=root

37
terraform/README.md Normal file
View File

@@ -0,0 +1,37 @@
# OpenTofu Configuration for Proxmox
This directory contains OpenTofu configuration for managing Proxmox VMs.
## Setup
1. **Create a Proxmox API token:**
- Log into Proxmox web UI
- Go to Datacenter → Permissions → API Tokens
- Click Add
- User: `root@pam`, Token ID: `terraform`
- Uncheck "Privilege Separation"
- Save the token secret (shown only once)
2. **Configure credentials:**
```bash
cd terraform
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your Proxmox details
```
3. **Initialize OpenTofu:**
```bash
tofu init
```
4. **Test connection:**
```bash
tofu plan
```
## Files
- `main.tf` - Provider configuration and test data source
- `variables.tf` - Variable definitions
- `terraform.tfvars.example` - Example credentials file
- `terraform.tfvars` - Your actual credentials (gitignored)

18
terraform/main.tf Normal file
View File

@@ -0,0 +1,18 @@
terraform {
required_version = ">= 1.0"
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "3.0.2-rc07"
}
}
}
provider "proxmox" {
pm_api_url = var.proxmox_api_url
pm_api_token_id = var.proxmox_api_token_id
pm_api_token_secret = var.proxmox_api_token_secret
pm_tls_insecure = var.proxmox_tls_insecure
}
# Provider configured - ready to add resources

View File

@@ -0,0 +1,7 @@
# Copy this file to terraform.tfvars and fill in your values
# terraform.tfvars is gitignored to keep credentials safe
proxmox_api_url = "https://your-proxmox-host.home.2rjus.net:8006/api2/json"
proxmox_api_token_id = "root@pam!terraform"
proxmox_api_token_secret = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
proxmox_tls_insecure = true

22
terraform/variables.tf Normal file
View File

@@ -0,0 +1,22 @@
variable "proxmox_api_url" {
description = "Proxmox API URL (e.g., https://proxmox.home.2rjus.net:8006/api2/json)"
type = string
}
variable "proxmox_api_token_id" {
description = "Proxmox API Token ID (e.g., root@pam!terraform)"
type = string
sensitive = true
}
variable "proxmox_api_token_secret" {
description = "Proxmox API Token Secret"
type = string
sensitive = true
}
variable "proxmox_tls_insecure" {
description = "Skip TLS verification (set to true for self-signed certs)"
type = bool
default = true
}

90
terraform/vm.tf Normal file
View File

@@ -0,0 +1,90 @@
# Example VM configuration - clone from template
# Before using this, you need to:
# 1. Upload the NixOS image to Proxmox
# 2. Restore it as a template VM (e.g., ID 9000)
# 3. Update the variables below
variable "target_node" {
description = "Proxmox node to deploy to"
type = string
default = "pve1"
}
variable "template_name" {
description = "Template VM name to clone from"
type = string
default = "nixos-25.11.20260128.fa83fd8"
}
variable "ssh_public_key" {
description = "SSH public key for root user"
type = string
default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAwfb2jpKrBnCw28aevnH8HbE5YbcMXpdaVv2KmueDu6 torjus@gunter"
}
# Example test VM
resource "proxmox_vm_qemu" "test_vm" {
name = "nixos-test-tofu"
target_node = var.target_node
# Clone from template
clone = var.template_name
# Full clone (not linked)
full_clone = true
# Boot configuration
boot = "order=virtio0"
scsihw = "virtio-scsi-single"
# VM settings
cpu {
cores = 2
}
memory = 2048
# Network
network {
id = 0
model = "virtio"
bridge = "vmbr0"
tag = 13
}
# Disk settings
disks {
virtio {
virtio0 {
disk {
size = "20G"
storage = "local-zfs"
}
}
}
}
# Start on boot
start_at_node_boot = true
# Agent
agent = 1
# Cloud-init configuration
ciuser = "root"
sshkeys = var.ssh_public_key
ipconfig0 = "ip=dhcp"
nameserver = "10.69.13.5 10.69.13.6"
searchdomain = "home.2rjus.net"
# Skip IPv6 since we don't use it
skip_ipv6 = true
rng {
source = "/dev/urandom"
period = 1000
}
}
output "test_vm_ip" {
value = proxmox_vm_qemu.test_vm.default_ipv4_address
}