Building My Personal Website: From Idea to Automated Deployment (Part 2)

Published: (December 4, 2025 at 10:50 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Overview

In the first part of this series I covered the high‑level architecture and the tools I chose for building my personal website. This post dives deeper into the technical implementation, starting with the Terraform modules needed to deploy a minimal Hetzner Cloud infrastructure.

The required components are:

  • Network – private network for internal communication
  • Firewall – security rules to restrict traffic
  • SSH Key – authentication for server access
  • Server – the compute instance

Each component is managed by its own Terraform module.

Network

The terraform-hcloud-network module provides comprehensive network management for Hetzner Cloud, including optional creation of a new network, support for multiple subnets, custom routes, and consistent outputs.

module "network" {
  source  = "danylomikula/network/hcloud"
  version = "1.0.0"

  create_network = true
  name           = local.project_slug
  ip_range       = "10.100.0.0/16"

  labels = local.common_labels

  subnets = {
    web = {
      type         = "cloud"
      network_zone = "eu-central"
      ip_range     = "10.100.1.0/24"
    }
  }
}

I chose the eu-central network zone for its pricing. The configuration creates a /16 CIDR block (10.100.0.0/16) with a single /24 subnet (10.100.1.0/24), which is ample for a single server.

Firewall

The terraform-hcloud-firewall module allows creation of multiple firewalls with custom inbound and outbound rules. In this setup I only allow HTTP/HTTPS traffic from Cloudflare IP addresses and SSH access from my home IP.

module "firewall" {
  source  = "danylomikula/firewall/hcloud"
  version = "1.0.0"

  firewalls = {
    "${local.resource_names.website}" = {
      rules = [
        {
          direction   = "in"
          protocol    = "tcp"
          port        = "22"
          source_ips  = [var.my_homelab_ip]
          description = "allow ssh"
        },
        {
          direction   = "in"
          protocol    = "tcp"
          port        = "80"
          source_ips  = local.cloudflare_all_ips
          description = "allow http from cloudflare"
        },
        {
          direction   = "in"
          protocol    = "tcp"
          port        = "443"
          source_ips  = local.cloudflare_all_ips
          description = "allow https from cloudflare"
        },
        {
          direction   = "in"
          protocol    = "icmp"
          source_ips  = ["0.0.0.0/0", "::/0"]
          description = "allow ping"
        }
      ]
      labels = {
        service = "firewall"
      }
    }
  }

  common_labels = local.common_labels
}

Dynamic Cloudflare IPs

Cloudflare publishes its IP ranges publicly. They are fetched dynamically with Terraform’s http data source, ensuring the firewall stays up‑to‑date.

data "http" "cloudflare_ips_v4" {
  url = "https://www.cloudflare.com/ips-v4"
}

data "http" "cloudflare_ips_v6" {
  url = "https://www.cloudflare.com/ips-v6"
}

locals {
  cloudflare_ipv4_cidrs = split("\n", trimspace(data.http.cloudflare_ips_v4.response_body))
  cloudflare_ipv6_cidrs = split("\n", trimspace(data.http.cloudflare_ips_v6.response_body))
  cloudflare_all_ips    = concat(local.cloudflare_ipv4_cidrs, local.cloudflare_ipv6_cidrs)
}

Note: Enable the Proxy toggle on your A and AAAA records in Cloudflare DNS settings for this configuration to work correctly.

SSH Key

The terraform-hcloud-ssh-key module handles SSH key management, supporting automated key generation, local storage, and importing existing keys.

module "ssh_key" {
  source  = "danylomikula/ssh-key/hcloud"
  version = "1.0.0"

  create_key = true
  name       = local.project_slug

  save_private_key_locally = true
  local_key_directory      = path.module

  labels = local.common_labels
}

By default an ED25519 key pair is generated and saved locally, providing a secure and recommended authentication method.

Server

The terraform-hcloud-server module creates the compute instance and ties together the previously defined resources.

module "servers" {
  source  = "danylomikula/server/hcloud"
  version = "1.0.0"

  servers = {
    "${local.resource_names.website}" = {
      server_type  = "cx23"
      location     = "hel1"
      image        = data.hcloud_image.rocky.name
      user_data    = local.cloud_init_config
      ssh_keys     = [module.ssh_key.ssh_key_id]
      firewall_ids = [module.firewall.firewall_ids[local.resource_names.website]]
      networks = [{
        network_id = module.network.network_id
        ip         = "10.100.1.10"
      }]
      labels = {
        service = "website"
      }
    }
  }

  common_labels = local.common_labels
}

The cx23 type is the cheapest Hetzner offering (≈ $5 / month in Helsinki) and is more than sufficient for a static website. All identifiers—SSH key ID, firewall ID, and network ID—are passed dynamically from the respective module outputs, eliminating manual configuration and reducing error risk.

Full Terraform Configuration

Below is the complete Terraform configuration that brings together all the pieces described above.

locals {
  project_slug = "mikula-dev"

  common_labels = {
    environment = "production"
    project     = local.project_slug
    managed_by  = "terraform"
  }

  resource_names = {
    website = "${local.project_slug}-web"
  }

  cloud_init_config = templatefile("${path.module}/cloud-init.tpl", {
    ansible_ssh_public_key = var.ansible_user_ssh_public_key
  })

  cloudflare_ipv4_cidrs = split("\n", trimspace(data.http.cloudflare_ips_v4.response_body))
  cloudflare_ipv6_cidrs = split("\n", trimspace(data.http.cloudflare_ips_v6.response_body))
  cloudflare_all_ips    = concat(local.cloudflare_ipv4_cidrs, local.cloudflare_ipv6_cidrs)
}

/* Fetch Cloudflare IP ra
Back to Blog

Related posts

Read more »

Terraform Project: Simple EC2 + Security Group

Project Structure terraform-project/ │── main.tf │── variables.tf │── outputs.tf │── providers.tf │── terraform.tfvars │── modules/ │ └── ec2/ │ ├── main.tf │...

Saving Terraform State in S3

Configuring S3 as a Terraform Backend Terraform can store its state in an S3 bucket. Below is a minimal configuration that sets up the S3 backend: hcl terrafor...