Hetzner Cloud Prerequisites #
Create API Token #
-
Open the console: https://console.hetzner.cloud/
-
Select a project, mine is called “terraform-playground”
-
Go to : “Security” > “API tokens”
-
Click “Generate API token”
-
Define a description like
terraform
-
Permissions: “Read & Write”
-
Click “Generate API token”
# Copy the token
VNLTBL5nJN7Kr-my-api-token
Install Terraform #
Installation Script #
Use the following script to install Terraform ob Debian based Linux distributions:
# Install the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
# Verify the GPG key fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
# Add the official HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Terraform
sudo apt update && sudo apt-get install terraform
Verify Installation #
# Verify the installation / check version
terraform version
# Shell output:
Terraform v1.9.1
on linux_amd64
Terraform Project #
Main Configuration #
Project Folder & variables.tf #
# Create a folder for the project and create the "variables.tf" file
TF_PROJECT_NAME=terraform-playground
TF_HETZNER_API_TOKEN=VNLTBL5nJN7Kr-my-api-token
mkdir $TF_PROJECT_NAME
cat << EOF >> "$TF_PROJECT_NAME/variables.tf"
variable "hcloud_token" {
sensitive = true
default = "$TF_HETZNER_API_TOKEN"
}
EOF
variables.tf should look like this:
#variables.tf
variable "hcloud_token" {
sensitive = true
type = string
default = "VNLTBL5nJN7Kr-my-api-token"
}
Terraform Provider terraform.tf #
# Create "terraform.tf" file
cat << EOF >> "$TF_PROJECT_NAME/terraform.tf"
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
EOF
terraform.tf should look like this:
# terraform.tf
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
Initialize Terraform Project #
This will download and install the Hetzner Cloud provider defined in the terraform.tf file with “hetznercloud/hcloud”, as well as setting up the configuration files in the project directory.
# Initialize the Terraform project
terraform init
# Shell output:
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Terraform SSH Key #
SSH Key Configuration #
# Create the "ssh-key.tf" SSH key configuration
cat << EOF >> "$TF_PROJECT_NAME/ssh-key.tf"
# Create SSH private key
resource "tls_private_key" "generic-ssh-key" {
algorithm = "RSA"
rsa_bits = 4096
}
# Add an output definition to expose the private key
output "private_ssh_key" {
value = tls_private_key.generic-ssh-key.private_key_pem
sensitive = true
}
# Create SSH public key on Hetzner Cloud
resource "hcloud_ssh_key" "primary-ssh-key" {
name = "primary-ssh-key"
public_key = tls_private_key.generic-ssh-key.public_key_openssh
}
EOF
The SSH key configuration looks like this:
# Create SSH private key
resource "tls_private_key" "generic-ssh-key" {
algorithm = "RSA"
rsa_bits = 4096
}
# Add an output definition to expose the private key
output "private_ssh_key" {
value = tls_private_key.generic-ssh-key.private_key_pem
sensitive = true
}
# Create SSH public key on Hetzner Cloud
resource "hcloud_ssh_key" "primary-ssh-key" {
name = "primary-ssh-key"
public_key = tls_private_key.generic-ssh-key.public_key_openssh
}
Apply Resources #
# Update the necessary providers and modules
terraform init -upgrade
# Apply resources
terraform apply
Extract the SSH Key #
Extract the private SSH key, so that it can be used to access the Hetzner Cloud VMs:
# Extract the private key
terraform output -raw private_ssh_key > ~/.ssh/id_hetzner
# Set the permissions
chmod 600 ~/.ssh/id_hetzner
List Server Images #
# List available VM images on Hetzner Cloud
curl \
-H "Authorization: Bearer $TF_HETZNER_API_TOKEN" \
'https://api.hetzner.cloud/v1/images'
# List available VM types on Hetzner Cloud
curl \
-H "Authorization: Bearer $TF_HETZNER_API_TOKEN" \
'https://api.hetzner.cloud/v1/server_types'
Create Example Servers #
Server Manifest #
# Create the server manifest "main.tf"
cat << EOF >> "$TF_PROJECT_NAME/main.tf"
resource "hcloud_server" "vm1" {
name = "vm1"
server_type = "cax11"
image = "debian-11"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.primary-ssh-key.name]
labels = {
purpose = "Terraform-Playground"
}
}
resource "hcloud_server" "vm2" {
name = "vm2"
server_type = "cax11"
image = "debian-11"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.primary-ssh-key.name]
labels = {
purpose = "Terraform-Playground"
}
}
EOF
The server manifest looks like this:
# main.tf
resource "hcloud_server" "vm1" {
name = "vm1"
server_type = "cax11"
image = "debian-11"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.primary-ssh-key.name]
labels = {
purpose = "Terraform-Playground"
}
}
resource "hcloud_server" "vm2" {
name = "vm2"
server_type = "cax11"
image = "debian-11"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.primary-ssh-key.name]
labels = {
purpose = "Terraform-Playground"
}
}
Manifest Explanation #
# main.tf
resource "hcloud_server" "vm1" {
name = "vm1"
-
Resource Label:
"hcloud_server" "vm1"
Used within the Terraform configuration to refer to this specific resource. -
Resource Property:
name = "vm1"
Actual setting for the Hetzner Cloud server resource. It defines the hostname or server name within Hetzner Cloud, visible in the Hetzner dashboard.
Server IP output.tf #
Create a “output.tf” file to output the IP addresses of the Hetzner Cloud servers:
# Create the server IP manifest "output.tf"
cat << EOF >> "$TF_PROJECT_NAME/output.tf"
# vm1 IP address
output "vm1_ip" {
value = hcloud_server.vm1.ipv4_address
description = "Public IP of vm1"
}
# vm2 IP address
output "vm2_ip" {
value = hcloud_server.vm2.ipv4_address
description = "Public IP of vm2"
}
EOF
The “output.tf” manifest looks like this:
# vm1 IP address
output "vm1_ip" {
value = hcloud_server.vm1.ipv4_address
description = "Public IP of vm1"
}
# vm2 IP address
output "vm2_ip" {
value = hcloud_server.vm2.ipv4_address
description = "Public IP of vm2"
}
Apply the Servers #
When the servers are deployed, their IP addresses are automatically listed in the shell:
# Apply resources
terraform apply
# Shell output:
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
private_ssh_key = <sensitive>
vm1_ip = "78.47.99.45"
vm2_ip = "188.245.47.237"
Manually Print IP Output #
You can also manually list the server IP addresses:
# Manually print VM IP
terraform output vm1_ip &&
terraform output vm2_ip
# Shell output:
"78.47.99.45"
"188.245.47.237"
SSH Into Servers #
# SSH into VM1
ssh -i ~/.ssh/id_hetzner root@78.47.99.45
# SSH into VM2
ssh -i ~/.ssh/id_hetzner root@188.245.47.237
Firewall Example #
Firewall Manifest #
# Create the firewall manifest "firewall.tf"
cat << EOF >> "$TF_PROJECT_NAME/firewall.tf"
resource "hcloud_firewall" "playground-firewall" {
name = "playground firewall"
# SSH port 22
rule {
description = "Allow SSH traffic"
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0", # Allow from any IP
"::/0"
]
}
# HTTP port 80
rule {
description = "Allow HTTP traffic"
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0", # Allow from any IP
"::/0"
]
}
# HTTPS port 443
rule {
description = "Allow HTTPS traffic"
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0", # Allow from any IP
"::/0"
]
}
}
EOF
The “firewall.tf” manifest looks like this:
resource "hcloud_firewall" "playground-firewall" {
name = "playground firewall"
# SSH port 22
rule {
description = "Allow SSH traffic"
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0", # Allow from any IP
"::/0"
]
}
# HTTP port 80
rule {
description = "Allow HTTP traffic"
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0", # Allow from any IP
"::/0"
]
}
# HTTPS port 443
rule {
description = "Allow HTTPS traffic"
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0", # Allow from any IP
"::/0"
]
}
}
Attach Firewall to VM #
Adopt the “main.tf” manifest and add the firewall to the VMs:
# main.tf
resource "hcloud_server" "vm1" {
name = "vm1"
server_type = "cax11"
image = "debian-11"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.primary-ssh-key.name]
firewall_ids = [hcloud_firewall.playground-firewall.id] # Add firewall
labels = {
purpose = "Terraform-Playground"
}
}
resource "hcloud_server" "vm2" {
name = "vm2"
server_type = "cax11"
image = "debian-11"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.primary-ssh-key.name]
firewall_ids = [hcloud_firewall.playground-firewall.id] # Add firewall
labels = {
purpose = "Terraform-Playground"
}
}
Apply Firewall Resources #
Deploy the firewall and add the servers to the firewall:
# Apply resources
terraform apply
Delete Resources #
Destroy VMs #
# Destroy specific VM resource: vm1
terraform destroy -target=hcloud_server.vm1
# Destroy specific VM resource: vm2
terraform destroy -target=hcloud_server.vm2
Destroy Firewall #
# Delete Firewall
terraform destroy -target=hcloud_firewall.playground-firewall
Destroy all Resources #
# Delete all resources
terraform destroy
Terraform VM Options #
Server with Cloud-Init #
main.tf #
# main.tf
resource "hcloud_server" "vm1" {
name = "vm1"
server_type = "cax11"
image = "debian-11"
location = "nbg1"
ssh_keys = [hcloud_ssh_key.primary-ssh-key.name]
user_data = "${file("user-data.yml")}" # Cloud-init manifest
labels = {
purpose = "Terraform-Playground"
}
}
user-data.yml #
#cloud-config
ssh_pwauth: no
# Set TimeZone
timezone: Europe/Vienna
# Install packages
packages:
- nginx
# Update/Upgrade & Reboot if necessary
package_update: true
package_upgrade: true
package_reboot_if_required: true
Deploy Resources #
# Shell output:
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
private_ssh_key = <sensitive>
vm1_ip = "78.47.99.45"
SSH Into VM #
# SSH into VM1
ssh -i ~/.ssh/id_hetzner root@78.47.99.45
# Verify the Nginx status
sudo systemctl status nginx
# Verify the time zone
timedatectl | grep "Time zone"
Links #
# Terraform Hetzner Cloud Provider
https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs
# Terraform Hetzner Server
https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs/resources/server.html
# Hetzner Cloud VM Images
https://docs.hetzner.cloud/#images