This Terraform configuration creates a foundational landing zone infrastructure on Hetzner Cloud with Consul Service Mesh, providing a secure, observable, and well-architected base for deploying microservices and applications.
📝 Note: A cloud-init syntax error was discovered and fixed in this version. See CLOUD-INIT-FIX.md for details. Future deployments will work correctly.
- Architecture Overview
- Prerequisites
- Quick Start
- Make Commands Reference
- Consul Service Mesh
- Outputs
- Customization
- Security Best Practices
- Troubleshooting
- Project Structure
- Cost Estimation
- Resources
The landing zone includes:
- Main Network: 172.16.0.0/16 VPC with multiple subnets
- Management Subnet (172.16.0.0/24): Bastion host and management tools
- Application Subnet (172.16.1.0/24): Application servers and workloads
- Services Subnet (172.16.2.0/24): Databases and shared services
- DMZ Subnet (172.16.10.0/24): Public-facing services
- VPN Network: 192.168.100.0/24 for WireGuard VPN clients
- Consul Server: Centralized service registry on bastion
- Consul Agents: On all VMs with Envoy sidecar proxies
- Service Discovery: Services find each other by name, not IP
- Automatic mTLS: Encrypted service-to-service communication
- Service Intentions: Zero-trust access policies
- Health Checking: Automatic monitoring and failover
- Consul UI: Web interface at http://:8500
- Bastion Host: Secure jump host with WireGuard VPN and Consul server
- Firewall Rules: Three tiers of security + Consul mesh ports
- Bastion Firewall: SSH, WireGuard VPN, and Consul management
- Application Firewall: HTTP/HTTPS, internal SSH, and mesh traffic
- Database Firewall: Restricted access only from application tier
- SSH Key Management: Centralized SSH key deployment
- Fail2ban: Automatic brute-force protection on bastion
- Service Mesh Security: mTLS encryption and intention-based policies
- Placement Groups: Ensures resources are spread across physical servers
- Management placement group
- Application placement group
- Database placement group
- Consul Service Mesh: Automatic failover and load balancing
- Hetzner Cloud Account: Sign up at https://console.hetzner.cloud/
- Terraform: Version 1.5.0 or higher
- SSH Key Pair: For accessing the infrastructure
# Quick setup (generates SSH key, creates config, initializes Terraform)
make quick-start
# Edit terraform.tfvars with your Hetzner API token
vim terraform.tfvars
# Preview changes
make plan
# Deploy infrastructure
make apply
# View all available commands
make helpssh-keygen -t ed25519 -f ./id_ed25519_hetzner_cloud_k3s -C "hetzner-landing-zone"# Copy the example configuration
cp terraform.tfvars.example terraform.tfvars
# Edit with your values
vim terraform.tfvarsAdd your Hetzner Cloud API token:
hcloud_token = "your-token-here"# Initialize Terraform
terraform init
# Review the plan
terraform plan
# Apply the configuration
terraform applyAfter deployment, connect to your bastion host:
# Get the SSH command from outputs
terraform output bastion_ssh_command
# Or connect directly
ssh -i ./id_ed25519_hetzner_cloud_k3s root@<bastion-ip>Retrieve WireGuard configuration:
# Get WireGuard info
terraform output wireguard_info_command | sh
# Or SSH and get the config
ssh -i ./id_ed25519_hetzner_cloud_k3s root@<bastion-ip> 'cat /etc/wireguard/wg0.conf'Create a client configuration:
[Interface]
PrivateKey = <generate-with-wg-genkey>
Address = 10.10.10.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <server-public-key-from-bastion>
Endpoint = <bastion-public-ip>:51820
AllowedIPs = 10.0.0.0/16, 10.10.10.0/24
PersistentKeepalive = 25The infrastructure includes Consul service mesh pre-configured:
# Check Consul cluster status
make consul-status
# List registered services
make consul-services
# View service intentions (access policies)
make consul-intentions
# Configure service mesh policies
make consul-setup
# Open Consul UI in browser
make open-consul-ui
# View comprehensive service mesh info
make mesh-summary# Wait 30 seconds after terraform apply for services to register
# Check Consul status
BASTION_IP=$(terraform output -raw bastion_public_ip)
ssh -i ./id_ed25519_hetzner_cloud_k3s admin@$BASTION_IP 'consul members'
# Configure service mesh policies
ssh -i ./id_ed25519_hetzner_cloud_k3s admin@$BASTION_IP 'sudo /usr/local/bin/setup-consul-intentions.sh'
# Access Consul UI
terraform output consul_ui_url
# Opens: http://<bastion-ip>:8500/ui
# Use management script
./consul-manage.sh $BASTION_IP- Service Discovery:
postgres.service.consulinstead of hardcoded IPs - Automatic mTLS: All service-to-service traffic encrypted
- Health Checks: Unhealthy instances automatically removed
- Zero-Trust Policies: Explicitly allow service communication
# View current policies
ssh admin@$BASTION_IP 'consul intention list'
# Allow web to connect to postgres
ssh admin@$BASTION_IP 'consul intention create -allow web postgres'
# Enable zero-trust (deny all by default)
ssh admin@$BASTION_IP 'consul intention create -deny "*" "*"'- CONSUL-QUICKSTART.md - Complete guide and troubleshooting
- MICRO-SEGMENTATION-GUIDE.md - Option 3: Consul implementation
- SERVICE-MESH-VMS.md - Architecture details
After deployment, you'll have access to:
network_id: Main network ID for deploying additional resourcesbastion_public_ip: Public IP of the bastion hostsubnet_*_ip_range: IP ranges for each subnetfirewall_ids: IDs of created firewallsplacement_group_ids: IDs of placement groupsconsul_ui_url: URL to access Consul web UIconsul_management_commands: Common Consul commandsservice_mesh_summary: Overview of deployed services
This project includes a comprehensive Makefile for easy management. Run make help to see all available commands.
make quick-start # Quick setup for new deployments
make init # Initialize Terraform
make plan # Show deployment plan
make apply # Deploy infrastructure
make destroy # Destroy all resources
make validate # Validate configuration
make fmt # Format Terraform filesmake output # Show all outputs
make info # Show deployment information
make list-ips # List all server IPs
make state-list # List Terraform resources
make state-backup # Backup Terraform statemake ssh-bastion # SSH to bastion host
make test-ssh # Test SSH connection
make wireguard-info # Get WireGuard configmake consul-status # Check Consul status
make consul-services # List services
make consul-intentions # View access policies
make consul-setup # Configure mesh policies
make open-consul-ui # Open UI in browser
make mesh-summary # Show deployment summary
make health-check # Check all servicesmake logs-bastion # View bastion logs
make logs-app # View app server logs
make logs-db # View database logs
make health-check # Overall health checkmake docs # List documentation
make cost # Show cost estimate
make debug # Show debug info
make clean # Clean Terraform cache# Initial setup
make quick-start
vim terraform.tfvars # Add your Hetzner API token
# Deploy
make plan # Review changes
make apply # Deploy infrastructure
# Verify
make info # Check deployment
make consul-status # Verify Consul
make open-consul-ui # Open web UI
# Manage
make health-check # Check health
make logs-bastion # View logs
make consul-setup # Configure policies
# Cleanup
make destroy # Tear down (when done)Edit main.tf and modify the hcloud_firewall resources to match your requirements.
resource "hcloud_network_subnet" "custom" {
type = "cloud"
network_id = hcloud_network.main.id
network_zone = var.network_zone
ip_range = "10.0.3.0/24"
}Modify the server_type in the bastion resource or when creating new servers:
cx22: 2 vCPU, 4 GB RAM (default)cx32: 4 vCPU, 8 GB RAMcx42: 8 vCPU, 16 GB RAM
- Restrict SSH Access: Update
allowed_ssh_ipsto your actual IP address - Use WireGuard VPN: Connect through VPN instead of exposing SSH publicly
- Regular Updates: Keep the bastion host updated
- Key Rotation: Regularly rotate SSH keys
- Enable 2FA: Use Hetzner's 2FA for console access
- Audit Logs: Regularly review access logs
Once the landing zone is deployed, you can reference its resources:
# In another Terraform configuration
data "hcloud_network" "landing_zone" {
name = "landing-zone-prod-network"
}
resource "hcloud_server" "app" {
name = "my-app"
server_type = "cx22"
image = "ubuntu-24.04"
location = "nbg1"
network {
network_id = data.hcloud_network.landing_zone.id
ip = "10.0.1.10"
}
}Approximate monthly costs:
- Bastion Host (cx22): ~€5.29/month
- Network: Free
- Firewalls: Free
- Placement Groups: Free
Total: ~€5.29/month for the base landing zone
make debug # Show debug information
make health-check # Check service health
make test-ssh # Test SSH connection# Check deployment status
make output
# Test SSH connection
make test-ssh
# Manual check
ssh -i ./id_ed25519_hetzner_cloud_k3s admin@$(terraform output -raw bastion_public_ip)Common causes:
- Check firewall rules in Hetzner console
- Verify your IP is in
allowed_ssh_ips - Ensure SSH key is correct (
id_ed25519_hetzner_cloud_k3s)
# Check Consul status
make consul-status
# View logs
make logs-bastion
# Get UI URL
make consul-uiSolution: See CONSUL-UI-FIX.md for firewall configuration.
# Check services
make consul-services
# View application logs
make logs-app
make logs-db
# Check Consul members
make consul-statusWait time: Services may take 30-60 seconds to register after deployment.
# Get WireGuard info
make wireguard-info
# SSH to bastion and check
make ssh-bastion
sudo systemctl status wg-quick@wg0
sudo ufw status
sudo wg show- Verify subnet configuration matches network zone
- Check that
network_idreferences are correct - Run
make state-listto see all resources
# Backup state before changes
make state-backup
# List all resources
make state-list
# If state is corrupted, restore from backup
cp terraform.tfstate.backup terraform.tfstateTo destroy all resources:
terraform destroyThis project follows Terraform best practices with a modular structure:
.
├── main.tf # Core infrastructure resources
├── variables.tf # Input variable declarations
├── outputs.tf # Output declarations
├── versions.tf # Terraform & provider versions
├── locals.tf # Local computed values
├── data.tf # Data source declarations
├── terraform.tfvars.example # Example configuration
├── Makefile # Automation commands
├── templates/ # Cloud-init templates
│ ├── bastion-cloud-init.tftpl # Bastion host configuration
│ ├── application-cloud-init.tftpl # App server configuration
│ └── database-cloud-init.tftpl # Database server configuration
├── README.md # This file
├── README-REFACTORING.md # Refactoring documentation
├── REFACTORING-SUMMARY.md # Quick refactoring reference
├── CONSUL-QUICKSTART.md # Consul setup guide
├── CONSUL-UI-FIX.md # Consul UI access fix
├── FIX-SUMMARY.md # Recent fixes summary
├── consul-manage.sh # Consul management script
└── setup-vpn-client.sh # VPN client setup script
- main.tf: Core resources (network, servers, firewalls, placement groups)
- variables.tf: All configurable parameters with validation
- outputs.tf: Exported values for reference and automation
- templates/: Separated cloud-init configurations for maintainability
- Makefile: Convenient commands for common operations
See README-REFACTORING.md for details on the code organization.
After deploying the landing zone:
- Set up monitoring (Prometheus, Grafana)
- Deploy application servers in the application subnet
- Set up databases in the services subnet
- Configure DNS records
- Implement backup strategy
- Set up CI/CD pipelines
MIT
For issues and questions:
- Hetzner Support: https://www.hetzner.com/support
- Terraform Documentation: https://www.terraform.io/docs
