Post-Tyranny-Tech-Infrastru.../tofu
Pieter 9eb6f2028a feat: Use Hetzner Volumes for Nextcloud data storage (issue #18)
Implement persistent block storage for Nextcloud user data, separating application and data layers:

OpenTofu Changes:
- tofu/volumes.tf: Create and attach Hetzner Volumes per client
  - Configurable size per client (default 100 GB for dev)
  - ext4 formatted, attached but not auto-mounted
- tofu/variables.tf: Add nextcloud_volume_size to client config
- tofu/terraform.tfvars: Set volume size for dev client (100 GB ~€5.40/mo)

Ansible Changes:
- ansible/roles/nextcloud/tasks/mount-volume.yml: New mount tasks
  - Detect volume device automatically
  - Format if needed, mount at /mnt/nextcloud-data
  - Add to fstab for persistence
  - Set correct permissions for www-data
- ansible/roles/nextcloud/tasks/main.yml: Include volume mounting
- ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2:
  - Use host mount /mnt/nextcloud-data/data instead of Docker volume
  - Keep app code in Docker volume (nextcloud-app)
  - User data now on Hetzner Volume

Scripts:
- scripts/resize-client-volume.sh: Online volume resizing
  - Resize via Hetzner API
  - Expand filesystem automatically
  - Show cost impact
  - Verify new size

Documentation:
- docs/storage-architecture.md: Complete storage guide
  - Architecture diagrams
  - Volume specifications
  - Sizing guidelines
  - Operations procedures
  - Performance considerations
  - Troubleshooting guide

- docs/volume-migration.md: Step-by-step migration
  - Safe migration from Docker volumes
  - Rollback procedures
  - Verification checklist
  - Timeline estimates

Benefits:
 Data independent from server instance
 Resize storage without rebuilding server
 Easy data migration between servers
 Better separation of concerns (app vs data)
 Simplified backup strategy
 Cost-optimized (pay for what you use)

Volume Pricing:
- 50 GB: ~€2.70/month
- 100 GB: ~€5.40/month
- 250 GB: ~€13.50/month
- Resizable online, no downtime

Note: Existing clients require manual migration
Follow docs/volume-migration.md for safe migration procedure

Closes #18

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-17 21:07:48 +01:00
..
dns.tf Deploy Zitadel identity provider with DNS automation (#3) (#8) 2026-01-05 16:40:37 +01:00
main.tf feat: Implement per-client SSH key isolation 2026-01-17 19:50:30 +01:00
outputs.tf Implement OpenTofu infrastructure provisioning (#1) 2025-12-27 13:48:42 +01:00
README.md Implement OpenTofu infrastructure provisioning (#1) 2025-12-27 13:48:42 +01:00
terraform.tfvars.example feat: Implement per-client SSH key isolation 2026-01-17 19:50:30 +01:00
variables.tf feat: Use Hetzner Volumes for Nextcloud data storage (issue #18) 2026-01-17 21:07:48 +01:00
versions.tf Deploy Zitadel identity provider with DNS automation (#3) (#8) 2026-01-05 16:40:37 +01:00
volumes.tf feat: Use Hetzner Volumes for Nextcloud data storage (issue #18) 2026-01-17 21:07:48 +01:00

OpenTofu Configuration for Hetzner Cloud

This directory contains Infrastructure as Code using OpenTofu to provision VPS instances on Hetzner Cloud.

Quick Start

1. Prerequisites

  • OpenTofu installed (brew install opentofu)
  • Hetzner Cloud account
  • Domain registered and added to Hetzner DNS

2. Get Hetzner API Tokens

Cloud API Token:

  1. Go to https://console.hetzner.cloud/
  2. Select your project (or create one)
  3. Navigate to SecurityAPI tokens
  4. Click Generate API token
  5. Name: infrastructure-provisioning
  6. Permissions: Read & Write
  7. Copy the token (shown only once!)

DNS API Token:

  1. Go to https://dns.hetzner.com/
  2. Click on your account name → API Tokens
  3. Click Create access token
  4. Name: infrastructure-dns
  5. Copy the token

Note

: You can use the same token for both if it has the necessary permissions.

3. Add Your Domain to Hetzner DNS

  1. Go to https://dns.hetzner.com/
  2. Click Add new zone
  3. Enter your domain (e.g., platform.nl)
  4. Update your domain registrar's nameservers to:
    • hydrogen.ns.hetzner.com
    • oxygen.ns.hetzner.com
    • helium.ns.hetzner.de

4. Configure OpenTofu

Create terraform.tfvars from the example:

cd tofu
cp terraform.tfvars.example terraform.tfvars

Edit terraform.tfvars with your values:

hcloud_token     = "YOUR_ACTUAL_HETZNER_CLOUD_TOKEN"
hetznerdns_token = "YOUR_ACTUAL_HETZNER_DNS_TOKEN"

# Your SSH public key (e.g., from ~/.ssh/id_ed25519.pub)
ssh_public_key = "ssh-ed25519 AAAA... user@hostname"

# Your domain registered in Hetzner DNS
base_domain = "your-domain.com"

# Start with one test client
clients = {
  test = {
    server_type = "cx22"        # 2 vCPU, 4 GB RAM - €6.25/month
    location    = "fsn1"        # Falkenstein, Germany
    subdomain   = "test"        # Will create test.your-domain.com
    apps        = ["zitadel", "nextcloud"]
  }
}

enable_snapshots = true

5. Initialize OpenTofu

tofu init

This downloads the Hetzner provider plugins.

6. Plan Infrastructure

tofu plan

Review what will be created:

  • SSH key resource
  • Firewall rules
  • VPS server(s)
  • DNS records (A, AAAA, wildcard)

7. Apply Configuration

tofu apply

Type yes when prompted. This will:

  • Upload your SSH key to Hetzner
  • Create firewall rules
  • Provision VPS instance(s)
  • Create DNS records

8. View Outputs

tofu output

Shows:

  • Client IP addresses
  • FQDNs
  • Complete client information

Server Sizes

Type vCPU RAM Disk Price/month Use Case
cx22 2 4 GB 40 GB €6.25 Small clients (1-10 users)
cx32 4 8 GB 80 GB €12.50 Medium clients (10-50 users)
cx42 8 16 GB 160 GB €24.90 Large clients (50+ users)

Locations

  • fsn1 - Falkenstein, Germany
  • nbg1 - Nuremberg, Germany
  • hel1 - Helsinki, Finland

Important Files

  • terraform.tfvars - GITIGNORED - Your secrets and configuration
  • versions.tf - Provider versions
  • variables.tf - Input variable definitions
  • main.tf - Server and firewall resources
  • dns.tf - DNS record management
  • outputs.tf - Output values for Ansible

Adding a New Client

Edit terraform.tfvars and add to the clients map:

clients = {
  existing-client = { ... }

  new-client = {
    server_type = "cx22"
    location    = "fsn1"
    subdomain   = "newclient"
    apps        = ["zitadel", "nextcloud"]
  }
}

Then run:

tofu plan   # Review changes
tofu apply  # Provision new server

Removing a Client

Remove the client from terraform.tfvars, then:

tofu plan   # Verify what will be destroyed
tofu apply  # Remove server and DNS records

Warning: This permanently deletes the server. Ensure backups are taken first!

State Management

OpenTofu state is stored locally in terraform.tfstate (gitignored).

For production with multiple team members, consider:

  • Remote state backend (S3, Terraform Cloud, etc.)
  • State locking
  • Encrypted state storage

Troubleshooting

"Zone not found" error

  • Ensure your domain is added to Hetzner DNS
  • Wait for DNS propagation (can take 24-48 hours)
  • Verify zone name matches exactly (no trailing dot)

SSH key errors

  • Ensure ssh_public_key is the public key content
  • Format: ssh-ed25519 AAAA... comment or ssh-rsa AAAA... comment
  • No newlines or extra whitespace

API token errors

  • Ensure Read & Write permissions
  • Check token hasn't expired
  • Verify correct project selected in Hetzner console

Next Steps

After provisioning:

  1. SSH to server: ssh root@<server-ip>
  2. Run Ansible configuration: cd ../ansible && ansible-playbook playbooks/setup.yml
  3. Applications will be deployed via Ansible