Skip to main content

Terraform - Hetzer Cloud: Deploy Servers with SSH Key, Deploy Firewall & Firewall Rules, Connect VMs to Firewall, Cloud-init Example

1531 words·
Terraform Hetzner Cloud Cloud-init
Terraform - This article is part of a series.
Part 2: This Article

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
Terraform - This article is part of a series.
Part 2: This Article