내 개인 웹사이트 구축: 아이디어에서 자동 배포까지 (Part 2)

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

Source: Dev.to

개요

시리즈 첫 번째 파트에서는 개인 웹사이트를 구축하기 위해 선택한 고수준 아키텍처와 도구들을 소개했습니다. 이번 글에서는 기술 구현에 대해 더 깊이 파고들며, 최소한의 Hetzner Cloud 인프라를 배포하기 위해 필요한 Terraform 모듈들을 살펴봅니다.

필요한 구성 요소는 다음과 같습니다:

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

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

Network

terraform-hcloud-network 모듈은 Hetzner Cloud용 포괄적인 네트워크 관리를 제공하며, 새 네트워크 생성 옵션, 다중 서브넷 지원, 사용자 정의 라우트 및 일관된 출력값을 포함합니다.

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"
    }
  }
}

가격이 저렴한 eu-central 네트워크 영역을 선택했습니다. 이 설정은 /16 CIDR 블록(10.100.0.0/16)과 단일 /24 서브넷(10.100.1.0/24)을 생성하며, 단일 서버를 운영하기에 충분합니다.

Firewall

terraform-hcloud-firewall 모듈을 사용하면 사용자 정의 인바운드·아웃바운드 규칙을 가진 여러 방화벽을 만들 수 있습니다. 여기서는 Cloudflare IP 주소에서 오는 HTTP/HTTPS 트래픽과 내 홈 IP에서 오는 SSH 접근만 허용하도록 설정했습니다.

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 토글을 활성화하세요.

SSH Key

terraform-hcloud-ssh-key 모듈은 SSH 키 관리를 담당하며, 자동 키 생성, 로컬 저장 및 기존 키 가져오기를 지원합니다.

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
}

기본적으로 ED25519 키 쌍이 생성되어 로컬에 저장되며, 이는 안전하고 권장되는 인증 방법입니다.

Server

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 타입은 Hetzner에서 가장 저렴한 옵션(헬싱키 기준 약 $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 ra */
Back to Blog

관련 글

더 보기 »

Day-12: AWS Terraform 함수

Terraform 고급 함수 Day 11에서 다룬 기본 함수들을 기반으로, 오늘은 Terraform을 더욱 강화하는 보다 전문화된 함수들을 살펴봅니다.

S3에 Terraform 상태 저장

S3를 Terraform 백엔드로 구성하기 Terraform은 상태를 S3 버킷에 저장할 수 있습니다. 아래는 S3 백엔드를 설정하는 최소 구성 예시입니다: hcl terrafor...