내 개인 웹사이트 구축: 아이디어부터 자동 배포까지 (파트 2)

발행: (2025년 12월 5일 오전 12:50 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

개요

시리즈의 첫 번째 부분에서는 개인 웹사이트를 구축하기 위해 선택한 고수준 아키텍처와 도구들을 다루었습니다. 이번 포스트에서는 기술 구현을 보다 깊이 파고들며, 최소한의 Hetzner Cloud 인프라를 배포하는 데 필요한 Terraform 모듈부터 시작합니다.

필요한 구성 요소

  • Network – 내부 통신을 위한 사설 네트워크
  • Firewall – 트래픽을 제한하는 보안 규칙
  • SSH Key – 서버 접근을 위한 인증
  • Server – 컴퓨팅 인스턴스

각 구성 요소는 자체 Terraform 모듈에 의해 관리됩니다.

네트워크

terraform-hcloud-network 모듈은 Hetzner Cloud에 대한 포괄적인 네트워크 관리를 제공하며, 다음을 포함합니다:

  • 새 네트워크를 선택적으로 생성
  • 다중 서브넷 지원
  • 사용자 정의 라우트
  • 일관된 출력

사용 예시

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

  # Create a new network
  create_network = true
  name           = local.project_slug
  ip_range       = "10.100.0.0/16"

  # Optional labels
  labels = local.common_labels

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

참고 사항

  • 네트워크 영역: 가격 이점을 위해 eu-central을 선택했습니다.
  • CIDR 블록:
    • 네트워크 CIDR: 10.100.0.0/16
    • 서브넷 CIDR: 10.100.1.0/24 (단일 서버에 충분)

필요에 따라 추가 서브넷이나 라우트를 자유롭게 추가하세요.

방화벽

terraform-hcloud-firewall 모듈을 사용하면 사용자 정의 인바운드 및 아웃바운드 규칙을 가진 여러 방화벽을 생성할 수 있습니다. 이 예제에서는 다음을 수행합니다:

  • HTTP/HTTPS 트래픽을 Cloudflare IP 주소에서만 허용합니다.
  • SSH 접근을 개인 홈 IP에서만 허용합니다.
  • ICMP(ping)를 어디서든 허용합니다.
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
}

동적 Cloudflare IP

Cloudflare는 IP 범위를 공개합니다. Terraform의 http 데이터 소스로 이를 가져오면 방화벽 규칙이 자동으로 최신 상태를 유지합니다.

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: 이 구성이 올바르게 작동하려면 Cloudflare DNS 설정에서 A 및 AAAA 레코드에 대한 Proxy 토글을 활성화하십시오.

Source:

SSH Key

terraform-hcloud-ssh-key 모듈은 Hetzner Cloud용 SSH 키를 관리합니다. 이 모듈은 다음을 수행할 수 있습니다.

  • 새 키 쌍을 자동으로 생성합니다.
  • 개인 키를 로컬에 저장합니다(선택 사항).
  • 직접 관리하고 싶다면 기존 키를 가져오기합니다.

기본적으로 모듈은 ED25519 키 쌍을 생성하는데, 이는 권장되는 안전한 알고리즘입니다.

사용 예시

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

  # Create a new key pair
  create_key = true
  name       = local.project_slug

  # Save the private key locally (optional)
  save_private_key_locally = true
  local_key_directory      = path.module

  # Apply common labels to the key resource
  labels = local.common_labels
}
  • create_keytrue 로 설정하면 새 키 쌍을 생성합니다.
  • name – Hetzner Cloud에서 키에 할당할 이름입니다.
  • save_private_key_locallytrue 로 설정하면 개인 키가 local_key_directory에 기록됩니다.
  • local_key_directory – 개인 키 파일이 저장될 디렉터리(기본값은 모듈 디렉터리).
  • labels – 키 리소스에 붙일 선택적 라벨 맵입니다.

Source:

서버

terraform-hcloud-server 모듈은 컴퓨트 인스턴스를 생성하고 이전에 정의한 리소스들을 연결합니다.

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
}
  • 인스턴스 유형: cx23 – 헷즈너에서 제공하는 가장 저렴한 옵션(헬싱키 기준 월 약 $5)이며 정적 웹사이트를 운영하기에 충분합니다.
  • 동적 식별자: SSH 키 ID, 방화벽 ID, 네트워크 ID가 각각의 모듈 출력값에서 자동으로 가져와져 수동 설정을 없애고 오류 발생 위험을 줄여줍니다.

전체 Terraform 구성

아래는 위에서 설명한 모든 요소를 하나로 모은 전체 Terraform 구성입니다.

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 ranges for firewall rules */
data "http" "cloudflare_ips_v4" {
  url = "https://www.cloudflare.com/ips-v4"
}

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

/* Network */
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"
    }
  }
}

/* SSH Key */
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
}

/* Firewall */
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
}

/* Server */
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
}
Back to Blog

관련 글

더 보기 »