내 개인 웹사이트 구축: 아이디어에서 자동 배포까지 (Part 2)
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 */