Terraform Getting Started¶
First steps with Terraform - Complete guide to setting up Terraform for Kavi infrastructure (planned implementation).
Planned Architecture
This documentation describes the planned Terraform setup. Terraform is not yet implemented but this guide shows how to set it up when ready.
Table of Contents¶
- Prerequisites
- Installation
- Project Setup
- Provider Authentication
- Backend Configuration
- First Resource
- Essential Commands
- Troubleshooting
Prerequisites¶
Required Accounts¶
Before starting with Terraform, ensure you have:
| Service | Purpose | How to Get |
|---|---|---|
| Hetzner Cloud | VPS provisioning | https://console.hetzner.cloud |
| Cloudflare | DNS management | https://dash.cloudflare.com |
| Hetzner Storage Box | Terraform state storage | Already have: u522581.your-storagebox.de |
| Infisical | Secret storage | Already deployed: https://secrets.kua.cl |
Required API Tokens¶
Store these in Infisical before starting:
Hetzner Cloud API Token:
# Generate at: https://console.hetzner.cloud/projects → Security → API Tokens
# Permissions: Read & Write
# Store in Infisical:
infisical secrets set HCLOUD_TOKEN="<token>" --env=dev --projectId=personal-vault
Cloudflare API Token:
# Generate at: https://dash.cloudflare.com/profile/api-tokens
# Template: "Edit zone DNS"
# Zone Resources: Include → Specific zone → kua.cl
# Store in Infisical:
infisical secrets set CLOUDFLARE_API_TOKEN="<token>" --env=dev --projectId=personal-vault
Storage Box Credentials:
# From Hetzner Robot: https://robot.hetzner.com/storage
# Store in Infisical:
infisical secrets set STORAGEBOX_ACCESS_KEY="<username>" --env=dev --projectId=personal-vault
infisical secrets set STORAGEBOX_SECRET_KEY="<password>" --env=dev --projectId=personal-vault
System Requirements¶
Terraform CLI: - Version: >= 1.6.0 - Installation: Already in Docker dev environment
Development Environment: - SSH access to Hetzner VPS (100.80.53.55) - dev() container access - Git for version control
Installation¶
Verify Terraform in Docker Image¶
Terraform should be pre-installed in the my-dev-env:latest image:
Install Terraform (if not in image)¶
If Terraform is not pre-installed:
Option A: Add to Docker Image (recommended)
# In Dockerfile
RUN wget https://releases.hashicorp.com/terraform/1.6.5/terraform_1.6.5_linux_amd64.zip && \
unzip terraform_1.6.5_linux_amd64.zip && \
mv terraform /usr/local/bin/ && \
rm terraform_1.6.5_linux_amd64.zip && \
terraform version
Option B: Install in Container (temporary)
# In dev() container
wget https://releases.hashicorp.com/terraform/1.6.5/terraform_1.6.5_linux_amd64.zip
unzip terraform_1.6.5_linux_amd64.zip
sudo mv terraform /usr/local/bin/
terraform version
Project Setup¶
Create Project Directory¶
Initialize Git Repository¶
# Initialize Git
git init
# Create .gitignore
cat > .gitignore << 'EOF'
# Terraform
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.*
*.tfvars
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Secrets
.env
secrets.auto.tfvars
# IDE
.vscode/
.idea/
*.swp
*.swo
EOF
# Initial commit
git add .gitignore
git commit -m "chore: initial Terraform project setup"
Create Basic Structure¶
# Create core files
touch terraform.tf providers.tf variables.tf outputs.tf README.md
# Create resource files
touch hetzner-vps.tf dns.tf networks.tf
# Verify structure
tree
# .
# ├── .gitignore
# ├── README.md
# ├── dns.tf
# ├── hetzner-vps.tf
# ├── networks.tf
# ├── outputs.tf
# ├── providers.tf
# ├── terraform.tf
# └── variables.tf
Provider Authentication¶
Define Providers¶
providers.tf:
# Hetzner Cloud provider
provider "hcloud" {
token = var.hcloud_token
}
# Cloudflare provider
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# Docker provider (for networks)
provider "docker" {
host = "unix:///var/run/docker.sock"
}
Define Variables¶
variables.tf:
# Hetzner Cloud API token
variable "hcloud_token" {
description = "Hetzner Cloud API token (from Infisical)"
type = string
sensitive = true
}
# Cloudflare API token
variable "cloudflare_api_token" {
description = "Cloudflare API token (from Infisical)"
type = string
sensitive = true
}
# Cloudflare Zone ID
variable "cloudflare_zone_id" {
description = "Cloudflare zone ID for kua.cl"
type = string
}
# Storage Box credentials
variable "storagebox_access_key" {
description = "Storage Box S3 access key (username)"
type = string
sensitive = true
}
variable "storagebox_secret_key" {
description = "Storage Box S3 secret key (password)"
type = string
sensitive = true
}
Inject Variables from Infisical¶
Option A: Environment Variables (recommended)
# In dev() container (secrets already loaded from Infisical)
# Terraform reads from TF_VAR_* environment variables
export TF_VAR_hcloud_token="$HCLOUD_TOKEN"
export TF_VAR_cloudflare_api_token="$CLOUDFLARE_API_TOKEN"
export TF_VAR_cloudflare_zone_id="$CLOUDFLARE_ZONE_ID"
export TF_VAR_storagebox_access_key="$STORAGEBOX_ACCESS_KEY"
export TF_VAR_storagebox_secret_key="$STORAGEBOX_SECRET_KEY"
# Or create a helper script
cat > load-terraform-vars.sh << 'EOF'
#!/bin/bash
export TF_VAR_hcloud_token="$HCLOUD_TOKEN"
export TF_VAR_cloudflare_api_token="$CLOUDFLARE_API_TOKEN"
export TF_VAR_cloudflare_zone_id="$CLOUDFLARE_ZONE_ID"
export TF_VAR_storagebox_access_key="$STORAGEBOX_ACCESS_KEY"
export TF_VAR_storagebox_secret_key="$STORAGEBOX_SECRET_KEY"
EOF
chmod +x load-terraform-vars.sh
source ./load-terraform-vars.sh
Option B: terraform.tfvars File (less secure, not recommended)
# terraform.tfvars (gitignored)
hcloud_token = "..." # Don't hardcode, defeats Infisical purpose
cloudflare_api_token = "..." # Don't do this
Recommended: Use environment variables so secrets stay in Infisical only.
Backend Configuration¶
Configure S3 Backend¶
terraform.tf:
terraform {
# Terraform version
required_version = ">= 1.6.0"
# Remote state on Hetzner Storage Box (S3-compatible)
backend "s3" {
bucket = "terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-east-1" # Dummy value for compatibility
# Storage Box S3 endpoint
endpoint = "https://u522581.your-storagebox.de"
# Credentials (from variables, set via environment)
# Note: Can't use var.* in backend config, must use -backend-config flags
# S3 compatibility flags
skip_credentials_validation = true
skip_region_validation = true
skip_metadata_api_check = true
force_path_style = true
}
# Required providers
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.20.0"
}
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.0"
}
}
}
Initialize with Backend¶
# Initialize Terraform with backend credentials
terraform init \
-backend-config="access_key=$STORAGEBOX_ACCESS_KEY" \
-backend-config="secret_key=$STORAGEBOX_SECRET_KEY"
# Expected output:
# Initializing the backend...
# Successfully configured the backend "s3"!
#
# Initializing provider plugins...
# - Finding hetznercloud/hcloud versions matching "~> 1.45.0"...
# - Finding cloudflare/cloudflare versions matching "~> 4.20.0"...
# - Installing hetznercloud/hcloud v1.45.0...
# - Installing cloudflare/cloudflare v4.20.0...
#
# Terraform has been successfully initialized!
Troubleshooting init:
If initialization fails:
# Check Storage Box credentials
echo $STORAGEBOX_ACCESS_KEY # Should show username
echo $STORAGEBOX_SECRET_KEY # Should show password (or *** if hidden)
# Test S3 access manually
s3cmd --access_key=$STORAGEBOX_ACCESS_KEY \
--secret_key=$STORAGEBOX_SECRET_KEY \
--host=u522581.your-storagebox.de \
--host-bucket=u522581.your-storagebox.de \
ls s3://terraform-state/
# If bucket doesn't exist, create it
s3cmd --access_key=$STORAGEBOX_ACCESS_KEY \
--secret_key=$STORAGEBOX_SECRET_KEY \
--host=u522581.your-storagebox.de \
mb s3://terraform-state
First Resource¶
Create SSH Key Resource¶
Let's create our first Terraform-managed resource: an SSH key in Hetzner Cloud.
hetzner-vps.tf:
# Upload MacBook SSH public key to Hetzner Cloud
resource "hcloud_ssh_key" "macbook" {
name = "macbook"
public_key = file("/root/.ssh/id_ed25519_macbook.pub")
}
# Output the SSH key ID
output "macbook_ssh_key_id" {
description = "Hetzner Cloud SSH key ID for MacBook"
value = hcloud_ssh_key.macbook.id
}
Plan the Change¶
# Preview what Terraform will do
terraform plan
# Expected output:
# Terraform will perform the following actions:
#
# # hcloud_ssh_key.macbook will be created
# + resource "hcloud_ssh_key" "macbook" {
# + fingerprint = (known after apply)
# + id = (known after apply)
# + labels = (known after apply)
# + name = "macbook"
# + public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... kavi@macbook"
# }
#
# Plan: 1 to add, 0 to change, 0 to destroy.
Review carefully:
- ✅ + resource means CREATE (green)
- ⚠️ ~ resource means UPDATE (yellow)
- ❌ - resource means DESTROY (red)
Apply the Change¶
# Apply the plan
terraform apply
# Terraform shows plan again and prompts:
# Do you want to perform these actions?
# Terraform will perform the actions described above.
# Only 'yes' will be accepted to approve.
#
# Enter a value: yes
# Type: yes
# Expected output:
# hcloud_ssh_key.macbook: Creating...
# hcloud_ssh_key.macbook: Creation complete after 1s [id=12345678]
#
# Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
#
# Outputs:
# macbook_ssh_key_id = 12345678
Verify in Hetzner Console¶
# Via CLI
hcloud ssh-key list
# Expected:
# ID NAME FINGERPRINT
# 12345678 macbook aa:bb:cc:dd:...
# Or check Hetzner Console:
# https://console.hetzner.cloud/projects → Security → SSH Keys
# Should see "macbook" key
Congratulations! You've created your first Terraform-managed resource.
Essential Commands¶
terraform init¶
Purpose: Initialize working directory, download providers, configure backend
terraform init
# When to run:
# - First time in new project
# - After adding new providers
# - After changing backend configuration
# - After cloning repository
terraform plan¶
Purpose: Preview changes before applying
terraform plan
# Shows:
# + Resources to create
# ~ Resources to update
# - Resources to destroy
# Always run before apply!
Save plan to file:
# Save plan
terraform plan -out=tfplan
# Apply saved plan (no confirmation needed)
terraform apply tfplan
terraform apply¶
Purpose: Apply changes to infrastructure
terraform apply
# Interactive: Shows plan, prompts for confirmation
# Enter: yes
# Auto-approve (dangerous, use with caution):
terraform apply -auto-approve
terraform destroy¶
Purpose: Destroy all managed infrastructure
terraform destroy
# Shows what will be destroyed, prompts for confirmation
# Enter: yes
# ⚠️ WARNING: This destroys EVERYTHING
# Use with extreme caution
Destroy specific resource:
terraform show¶
Purpose: Show current state
terraform state list¶
Purpose: List all managed resources
terraform state list
# Example output:
# hcloud_ssh_key.macbook
# hcloud_server.hetzner_vps
# cloudflare_record.root
terraform output¶
Purpose: Display output values
terraform output
# Shows all outputs
terraform output macbook_ssh_key_id
# Shows specific output
# 12345678
terraform fmt¶
Purpose: Format Terraform files
# Format all .tf files
terraform fmt
# Check formatting (returns non-zero if not formatted)
terraform fmt -check
terraform validate¶
Purpose: Validate configuration syntax
terraform validate
# Expected:
# Success! The configuration is valid.
# Or if errors:
# Error: Missing required argument
# on hetzner-vps.tf line 5:
# ...
Troubleshooting¶
"Error: Backend configuration changed"¶
Symptom:
Error: Backend configuration changed
A change in the backend configuration has been detected, which may require
migrating existing state.
Solution:
# Reinitialize with migration
terraform init -migrate-state
# Or reconfigure without migration
terraform init -reconfigure
"Error: Failed to get existing workspaces"¶
Symptom:
Cause: Wrong Storage Box credentials
Solution:
# Check credentials
echo $STORAGEBOX_ACCESS_KEY
echo $STORAGEBOX_SECRET_KEY
# Reinitialize with correct credentials
terraform init \
-backend-config="access_key=$STORAGEBOX_ACCESS_KEY" \
-backend-config="secret_key=$STORAGEBOX_SECRET_KEY" \
-reconfigure
"Error: Invalid provider configuration"¶
Symptom:
Cause: Missing TF_VAR_hcloud_token environment variable
Solution:
# Load variables from Infisical
source ./load-terraform-vars.sh
# Or export manually
export TF_VAR_hcloud_token="$HCLOUD_TOKEN"
# Verify
env | grep TF_VAR
"Error: Error creating server"¶
Symptom:
Cause: Invalid or expired Hetzner API token
Solution:
# Check token in Infisical
infisical secrets get HCLOUD_TOKEN --env=dev --projectId=personal-vault
# Generate new token at Hetzner Console
# Update in Infisical
infisical secrets set HCLOUD_TOKEN="new-token" --env=dev --projectId=personal-vault
# Reload dev() to get new token
exit
dev terraform
State Lock Errors¶
Symptom:
Error: Error acquiring the state lock
Lock Info:
ID: abc-123
Path: terraform-state/infrastructure/terraform.tfstate
Cause: Another terraform process is running, or previous run crashed
Solution:
# If you're sure no other process is running:
terraform force-unlock abc-123
# Use lock ID from error message
Summary¶
Terraform Setup Steps:
1. ✅ Store API tokens in Infisical
2. ✅ Create project directory structure
3. ✅ Configure providers and backend
4. ✅ Initialize with terraform init
5. ✅ Create first resource
6. ✅ Plan with terraform plan
7. ✅ Apply with terraform apply
Essential Commands:
- terraform init - Initialize project
- terraform plan - Preview changes
- terraform apply - Apply changes
- terraform destroy - Destroy resources
- terraform show - Show current state
- terraform state list - List resources
Security: - ✅ Store tokens in Infisical (not .tfvars files) - ✅ Use environment variables (TF_VAR_) - ✅ Gitignore .tfstate and *.tfvars - ✅ Remote state on Storage Box (encrypted at rest)
What's Next: - Learn State Management - critical state concepts - Learn Providers - Hetzner, Cloudflare, Docker setup - Learn VPS Management - provision Hetzner VPS - Learn Workflow - daily operations and best practices