Hetzner Cloud Sunucu Yönetimi: Terraform ile Altyapı Otomasyonu

Hetzner Cloud, özellikle Avrupa merkezli projelerde maliyet-performans dengesi açısından ciddi bir rakip haline geldi. Saatlik 3-5 Euro’luk sunuculardan başlayan fiyatlarıyla hem hobi projelerinde hem de production ortamlarında sıkça tercih ediliyor. Ama birden fazla sunucu yönetmeye başladığında, her şeyi Hetzner Cloud Console üzerinden elle yapmak hızla kaosa dönüşüyor. İşte tam bu noktada Terraform devreye giriyor.

Bu yazıda Hetzner Cloud altyapısını Terraform ile nasıl yöneteceğimizi, gerçek dünya senaryolarıyla birlikte ele alacağız. Tek bir sunucudan başlayıp load balancer, firewall, SSH key yönetimi ve volume ekleme gibi konulara kadar ilerleyeceğiz.

Neden Terraform ile Hetzner Cloud?

Elle yapılan altyapı yönetiminde kaçınılmaz olarak şu sorular baş gösterir: “Bu sunucuyu neden açtım?”, “Hangi firewall kuralı neden eklendi?”, “Staging ile production arasındaki fark neydi?” Terraform bu soruların cevabını kod olarak saklar.

Hetzner, resmi bir Terraform provider sunuyor. hetznercloud/hcloud provider’ı Terraform Registry’de mevcut ve aktif olarak geliştiriliyor. Bu sayede Hetzner’in neredeyse tüm kaynaklarını Terraform üzerinden yönetebiliyorsunuz.

Avantajları şöyle sıralayabiliriz:

  • Tekrar üretilebilirlik: Aynı altyapıyı staging ve production için dakikalar içinde klonlayabilirsiniz
  • Değişiklik geçmişi: Git history üzerinden altyapı değişikliklerini takip edebilirsiniz
  • Ekip çalışması: Altyapı değişiklikleri pull request sürecine dahil edilebilir
  • Maliyet kontrolü: Aktif kaynakları kod üzerinden görebilir, gereksiz olanları hızla temizleyebilirsiniz

Başlamadan Önce: Gereksinimler

Öncelikle birkaç şeyin hazır olması gerekiyor:

  • Terraform CLI kurulu olmalı (1.5+ önerilen)
  • Hetzner Cloud hesabı ve API token’ı
  • Temel Terraform bilgisi

Hetzner Cloud Console’a girip Security > API Tokens bölümünden bir token oluşturun. Read/Write yetkisi vermeniz gerekiyor. Bu token’ı güvenli bir yerde saklayın, bir daha göremeyeceksiniz.

Provider Yapılandırması

İlk adım olarak proje dizinimizi oluşturup provider ayarlarını yapalım.

mkdir hetzner-terraform && cd hetzner-terraform
touch main.tf variables.tf outputs.tf

main.tf dosyamıza provider tanımını ekleyelim:

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.44"
    }
  }
  required_version = ">= 1.5.0"
}

provider "hcloud" {
  token = var.hcloud_token
}

variables.tf dosyasına token değişkenini tanımlayalım:

variable "hcloud_token" {
  description = "Hetzner Cloud API Token"
  type        = string
  sensitive   = true
}

variable "location" {
  description = "Sunucu lokasyonu (nbg1, fsn1, hel1, ash, hil)"
  type        = string
  default     = "nbg1"
}

variable "server_type" {
  description = "Sunucu tipi"
  type        = string
  default     = "cx22"
}

Token’ı environment variable olarak set etmek en temiz yöntem:

export TF_VAR_hcloud_token="buraya_token_gelecek"
terraform init

terraform init komutu provider’ı indirir ve başlangıç dosyalarını oluşturur. Her şey yolundaysa “Terraform has been successfully initialized!” mesajını görürsünüz.

İlk Sunucuyu Oluşturmak

Basit bir web sunucusu senaryosuyla başlayalım. Önce SSH key’imizi Hetzner’e tanıtalım, sonra sunucuyu oluşturalım.

# SSH key kaynağı
resource "hcloud_ssh_key" "default" {
  name       = "terraform-key"
  public_key = file("~/.ssh/id_rsa.pub")
}

# Temel web sunucusu
resource "hcloud_server" "web" {
  name        = "web-server-01"
  image       = "ubuntu-22.04"
  server_type = var.server_type
  location    = var.location
  ssh_keys    = [hcloud_ssh_key.default.id]

  labels = {
    environment = "production"
    role        = "web"
    managed_by  = "terraform"
  }

  user_data = <<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y nginx
    systemctl enable nginx
    systemctl start nginx
    echo "Terraform tarafindan olusturuldu" > /var/www/html/index.html
  EOF
}

outputs.tf dosyasına da sunucu IP’sini ekleyelim:

output "web_server_ip" {
  description = "Web sunucusunun public IP adresi"
  value       = hcloud_server.web.ipv4_address
}

output "web_server_ipv6" {
  description = "Web sunucusunun IPv6 adresi"
  value       = hcloud_server.web.ipv6_address
}

Şimdi planı inceleyelim ve uygulayalım:

terraform plan
terraform apply

apply komutu çalıştıktan sonra çıktıda sunucunuzun IP adresini göreceksiniz. Yaklaşık 30-60 saniye içinde sunucu hazır olacak ve user_data scriptiniz çalışmış olacak.

Firewall Yönetimi

Sunucu oluşturuldu ama henüz güvenlik kuralları yok. Gerçek dünyada her zaman firewall kurallarıyla birlikte deploy etmek gerekir. Hetzner Cloud Firewall, sunucu düzeyinde ağ filtresi sağlar.

resource "hcloud_firewall" "web_firewall" {
  name = "web-firewall"

  # SSH erişimi - sadece belirli IP'lerden
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "22"
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }

  # HTTP
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "80"
    source_ips = ["0.0.0.0/0", "::/0"]
  }

  # HTTPS
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "443"
    source_ips = ["0.0.0.0/0", "::/0"]
  }

  # ICMP (ping)
  rule {
    direction  = "in"
    protocol   = "icmp"
    source_ips = ["0.0.0.0/0", "::/0"]
  }
}

# Firewall'u sunucuya bağla
resource "hcloud_firewall_attachment" "web_fw_attach" {
  firewall_id = hcloud_firewall.web_firewall.id
  server_ids  = [hcloud_server.web.id]
}

Production ortamında SSH portunu kısıtlamak güvenlik açısından kritik. Yukarıdaki örnekte 0.0.0.0/0 kullandım ama gerçek senaryoda bunu kendi ofis/VPN IP’lerinizle değiştirmelisiniz.

Private Network ile Çok Sunuculu Senaryo

Gerçek dünyada genellikle birden fazla sunucu birbirleriyle haberleşmesi gerekir. Bir web sunucusu ve bir veritabanı sunucusunun private network üzerinden iletişim kurduğu klasik bir senaryoya bakalım.

# Private network oluştur
resource "hcloud_network" "private_net" {
  name     = "private-network"
  ip_range = "10.0.0.0/16"
}

# Network subnet'i
resource "hcloud_network_subnet" "private_subnet" {
  network_id   = hcloud_network.private_net.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = "10.0.1.0/24"
}

# Web sunucusu
resource "hcloud_server" "web" {
  name        = "web-01"
  image       = "ubuntu-22.04"
  server_type = "cx22"
  location    = "nbg1"
  ssh_keys    = [hcloud_ssh_key.default.id]

  network {
    network_id = hcloud_network.private_net.id
    ip         = "10.0.1.10"
  }

  depends_on = [hcloud_network_subnet.private_subnet]
}

# Veritabanı sunucusu - public IP olmadan
resource "hcloud_server" "database" {
  name        = "db-01"
  image       = "ubuntu-22.04"
  server_type = "cx32"
  location    = "nbg1"
  ssh_keys    = [hcloud_ssh_key.default.id]

  network {
    network_id = hcloud_network.private_net.id
    ip         = "10.0.1.20"
  }

  depends_on = [hcloud_network_subnet.private_subnet]

  user_data = <<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y postgresql
    # PostgreSQL sadece private network'te dinlesin
    sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '10.0.1.20'/" /etc/postgresql/14/main/postgresql.conf
    systemctl restart postgresql
  EOF
}

Bu yapıda veritabanı sunucusu sadece private network üzerinden erişilebilir. Web sunucusu 10.0.1.20 adresinden veritabanına bağlanır. Public IP ataması public_net bloğuyla kontrol edilebilir ama varsayılan olarak sunucular public IP alır. Veritabanı sunucusuna public IP atanmasını engellemek için ayrıca düzenleme yapmanız gerekebilir.

Volume (Kalıcı Depolama) Yönetimi

Veritabanı sunucuları ve dosya depolama gerektiren uygulamalar için Hetzner Volume kullanımı şart. Volume’lar sunucu silinse bile verilerinizi korur.

# 50GB volume oluştur
resource "hcloud_volume" "db_storage" {
  name      = "database-storage"
  size      = 50
  location  = "nbg1"
  format    = "ext4"

  labels = {
    environment = "production"
    purpose     = "database"
  }
}

# Volume'u sunucuya bağla
resource "hcloud_volume_attachment" "db_vol_attach" {
  volume_id = hcloud_volume.db_storage.id
  server_id = hcloud_server.database.id
  automount = true
}

output "volume_linux_device" {
  value = hcloud_volume.db_storage.linux_device
}

automount = true seçeneği volume’u otomatik olarak /dev/disk/by-id/ altında mount eder ama gerçek production senaryolarında user_data ile özel bir mount point ayarlamanızı öneririm:

user_data = <<-EOF
  #!/bin/bash
  # Volume hazır olana kadar bekle
  while [ ! -e /dev/disk/by-id/scsi-0HC_Volume_* ]; do
    sleep 1
  done
  
  # Mount point oluştur
  mkdir -p /var/lib/postgresql
  
  # fstab'a ekle
  VOLUME_ID=$(ls /dev/disk/by-id/scsi-0HC_Volume_*)
  echo "$VOLUME_ID /var/lib/postgresql ext4 defaults 0 0" >> /etc/fstab
  mount -a
  
  # PostgreSQL dizin izinleri
  chown -R postgres:postgres /var/lib/postgresql
EOF

Load Balancer Yapılandırması

Trafik artmaya başladığında tek web sunucusu yetmez. Hetzner’in managed load balancer’ı Terraform ile kolayca yönetilebilir.

# Birden fazla web sunucusu - count ile
resource "hcloud_server" "web" {
  count       = 3
  name        = "web-${count.index + 1}"
  image       = "ubuntu-22.04"
  server_type = "cx22"
  location    = "nbg1"
  ssh_keys    = [hcloud_ssh_key.default.id]

  labels = {
    role = "web"
    tier = "frontend"
  }

  user_data = <<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y nginx
    echo "Web Server ${count.index + 1}" > /var/www/html/index.html
    systemctl enable nginx && systemctl start nginx
  EOF
}

# Load Balancer
resource "hcloud_load_balancer" "web_lb" {
  name               = "web-load-balancer"
  load_balancer_type = "lb11"
  location           = "nbg1"

  labels = {
    environment = "production"
  }
}

# Load Balancer ile network bağlantısı
resource "hcloud_load_balancer_network" "lb_network" {
  load_balancer_id = hcloud_load_balancer.web_lb.id
  network_id       = hcloud_network.private_net.id
  ip               = "10.0.1.100"
}

# Backend servisleri - label selector ile
resource "hcloud_load_balancer_service" "http_service" {
  load_balancer_id = hcloud_load_balancer.web_lb.id
  protocol         = "http"
  listen_port      = 80
  destination_port = 80

  health_check {
    protocol = "http"
    port     = 80
    interval = 10
    timeout  = 5
    retries  = 3

    http {
      path         = "/"
      status_codes = ["200"]
    }
  }
}

# Label selector ile target ekle
resource "hcloud_load_balancer_target" "web_targets" {
  type             = "label_selector"
  load_balancer_id = hcloud_load_balancer.web_lb.id
  label_selector   = "role=web"
  use_private_ip   = true

  depends_on = [hcloud_load_balancer_network.lb_network]
}

output "load_balancer_ip" {
  value = hcloud_load_balancer.web_lb.ipv4
}

Bu yapıda label_selector kullanımı çok pratik. Yeni web sunucuları eklediğinizde role=web label’ı koymanız yeterli, load balancer otomatik olarak onları da dahil eder.

State Yönetimi: Remote Backend

Tek başınıza çalışıyorsanız local state dosyası yeterli olabilir ama ekip ortamında mutlaka remote backend kullanmalısınız. Terraform state’i Hetzner Object Storage’da (S3 uyumlu) saklamak mümkün.

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.44"
    }
  }

  backend "s3" {
    endpoint = "https://fsn1.your-objectstorage.com"
    bucket   = "terraform-state-bucket"
    key      = "production/terraform.tfstate"

    # Hetzner Object Storage S3 uyumlu
    region                      = "main"
    skip_credentials_validation = true
    skip_metadata_api_check     = true
    skip_region_validation      = true
    force_path_style            = true
  }
}

Alternatif olarak GitLab veya Terraform Cloud backend’lerini de kullanabilirsiniz. Production ortamlarında state dosyasının şifrelenmesi ve versiyonlanması kritik öneme sahip.

Modüler Yapı: Gerçek Dünya Projesi

Büyük projelerde her şeyi tek dosyaya koymak yönetilemez bir hal alır. Modüler yapıya geçmek için proje dizinini şöyle organize edebilirsiniz:

hetzner-infra/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars.example
├── modules/
│   ├── web-server/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── database/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── network/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── environments/
    ├── staging/
    │   ├── main.tf
    │   └── terraform.tfvars
    └── production/
        ├── main.tf
        └── terraform.tfvars

environments/production/main.tf içinde modülleri şöyle çağırabilirsiniz:

module "network" {
  source      = "../../modules/network"
  environment = "production"
  ip_range    = "10.0.0.0/16"
}

module "web_servers" {
  source       = "../../modules/web-server"
  server_count = 3
  server_type  = "cx32"
  network_id   = module.network.network_id
  environment  = "production"
}

module "database" {
  source      = "../../modules/database"
  server_type = "cx42"
  volume_size = 200
  network_id  = module.network.network_id
  environment = "production"
}

Bu yapıyla staging ortamı için server_count = 1 ve daha küçük server_type kullanıp aynı altyapıyı farklı boyutlarda deploy edebilirsiniz.

Yaygın Sorunlar ve Çözümleri

Sunucu silinmesini önlemek: Terraform’da yanlışlıkla terraform destroy çalıştırıldığında üretim sunucularını korumak için lifecycle bloğu kullanın.

resource "hcloud_server" "production_db" {
  # ... diğer ayarlar

  lifecycle {
    prevent_destroy = true
    ignore_changes  = [user_data]
  }
}

prevent_destroy = true: Bu kaynağı silmeye çalışırsanız Terraform hata verir ignore_changes = [user_data]: user_data değişirse sunucuyu yeniden oluşturma

Rate limiting: Çok fazla kaynak oluştururken Hetzner API rate limitine takılabilirsiniz. Bu durumda terraform apply komutuna -parallelism=5 ekleyerek eşzamanlı işlem sayısını düşürebilirsiniz.

terraform apply -parallelism=5

Drift detection: Birisi Console üzerinden manuel değişiklik yaparsa Terraform state ile gerçek durum arasında fark oluşur. Bunu tespit etmek için:

terraform plan -detailed-exitcode
# Exit code 2 = değişiklik var demektir

Bu komutu CI/CD pipeline’ınıza ekleyip drift olduğunda alarm oluşturabilirsiniz.

CI/CD Pipeline Entegrasyonu

GitHub Actions ile otomatik deploy pipeline’ı kurmak oldukça basit:

name: Terraform Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  terraform:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0
      
      - name: Terraform Init
        run: terraform init
        env:
          TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
      
      - name: Terraform Plan
        run: terraform plan -out=tfplan
        env:
          TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
      
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve tfplan
        env:
          TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}

Pull request’lerde sadece plan çalışır ve sonucu PR comment olarak görebilirsiniz. main branch’e merge edilince apply otomatik devreye girer.

Maliyet Optimizasyonu İpuçları

Hetzner’in Terraform provider’ı üzerinden maliyet optimizasyonu için bazı pratikler:

  • Server type seçimi: cx22 yerine cpx11 ARM tabanlı sunucular genellikle daha ucuz ve birçok iş yükü için yeterli
  • Lokasyon seçimi: ash (Ashburn, ABD) ve hil (Hillsboro, ABD) lokasyonları bazen Avrupa lokasyonlarından farklı fiyatlandırmaya sahip
  • Volume boyutu: Başlangıçta küçük volume açıp Terraform ile büyütebilirsiniz, ama küçültemezsiniz
  • Load balancer tipi: Küçük projeler için lb11 çoğunlukla yeterli, gereksiz yere lb31 açmayın

Terraform ile altyapı maliyetini infracost aracıyla takip edebilirsiniz:

# infracost kurulumu sonrası
infracost breakdown --path .

Bu komut Terraform planınızdaki tüm kaynakların aylık maliyetini hesaplar.

Sonuç

Hetzner Cloud ile Terraform kombinasyonu, özellikle Avrupa’daki projelerde son derece güçlü ve ekonomik bir altyapı yönetim çözümü sunuyor. Başlangıçta basit bir web sunucusuyla başlayıp modüler yapıya geçiş, private network entegrasyonu, load balancer ve CI/CD pipeline entegrasyonuna kadar her adımda Terraform’un sağladığı “altyapı as code” yaklaşımının faydalarını net biçimde görüyorsunuz.

En önemli üç pratik tavsiye olarak şunları söyleyebilirim: State dosyasını mutlaka remote backend’de saklayın, production kaynaklarına prevent_destroy ekleyin ve token gibi hassas bilgileri asla .tf dosyalarına yazmayın. Bu üç kurala dikkat ettiğinizde Hetzner ve Terraform ile çok stabil ve yönetilebilir bir altyapı işletebilirsiniz.

Terraform state’ini doğru yönetmek başlangıçta karmaşık gelebilir ama bir kez oturduktan sonra geri dönmek istemeyeceğiniz bir rahatlamayı beraberinde getiriyor. Console’u artık sadece izleme amacıyla açıyorsunuz, değişiklikler için değil.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir