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:
cx22yerinecpx11ARM tabanlı sunucular genellikle daha ucuz ve birçok iş yükü için yeterli - Lokasyon seçimi:
ash(Ashburn, ABD) vehil(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 yerelb31aç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.
