HCL Dili: Terraform’da Kaynak ve Değişken Tanımlama Rehberi

Terraform ile altyapı yönetimine adım atan her sysadmin’in er ya da geç karşılaştığı en temel engel, HCL sözdizimini kavramaktır. HashiCorp Configuration Language, ilk bakışta basit görünse de kaynak ve değişken tanımlama konusunda bilmeniz gereken birçok nüans barındırır. Bu yazıda HCL’nin temel yapı taşlarını, gerçek dünya senaryolarıyla birlikte ele alacağız.

HCL Nedir ve Neden Bu Kadar Önemli?

HCL, HashiCorp tarafından geliştirilen ve insan tarafından okunabilir olması için tasarlanmış bir konfigürasyon dilidir. JSON ile YAML arasında bir yerde duran bu dil, hem makine tarafından işlenebilir hem de sistem yöneticilerinin rahatça anlayabileceği bir sözdizimi sunar.

Terraform’da her şey HCL ile yazılır. Bir AWS EC2 instance’ı ayağa kaldırmak ister misiniz? HCL. Azure’da bir sanal ağ oluşturmak? HCL. Google Cloud’da Kubernetes cluster’ı yönetmek? Yine HCL. Dolayısıyla bu dili iyi anlamak, Terraform’u verimli kullanmanın olmazsa olmaz koşuludur.

Temel Sözdizimi ve Blok Yapısı

HCL’de her şey bloklar üzerine kuruludur. Bir bloğun temel yapısı şu şekildedir:

<BLOK_TIPI> "<TIP>" "<ISIM>" {
  arguman1 = deger1
  arguman2 = deger2
}

Bu yapıyı somutlaştıralım. Terraform’da en çok kullanacağınız blok tipleri şunlardır:

  • resource: Gerçek altyapı bileşenlerini tanımlar
  • variable: Giriş değişkenlerini tanımlar
  • output: Çıktı değerlerini tanımlar
  • locals: Yerel hesaplanmış değerleri tanımlar
  • data: Mevcut kaynakları sorgular
  • module: Modül çağrılarını tanımlar
  • provider: Sağlayıcı konfigürasyonlarını tanımlar

Şimdi bunları tek tek inceleyelim.

Resource Blokları: Altyapının Temeli

Resource bloğu, Terraform’un kalbini oluşturur. Bir kaynak tanımladığınızda Terraform’a “bu altyapı bileşenini yönet” diyorsunuz demektir.

resource "aws_instance" "web_sunucu" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.medium"

  tags = {
    Name        = "WebSunucu-Prod"
    Environment = "production"
    Owner       = "sysadmin-team"
  }
}

Burada dikkat etmeniz gereken birkaç nokak var. aws_instance provider ve kaynak tipini belirtir. web_sunucu ise bu kaynağa verdiğiniz isimdir ve Terraform konfigürasyonu içinde bu kaynağa referans verirken kullanırsınız. Bu iki parça birleşince aws_instance.web_sunucu şeklinde bir kaynak adresi oluşur.

Gerçek dünya senaryosunda bir üretim ortamı için EC2 instance’ını biraz daha detaylı yapılandırmak gerekir:

resource "aws_instance" "uygulama_sunucu" {
  ami                    = var.sunucu_ami
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.ozel_subnet.id
  vpc_security_group_ids = [aws_security_group.uygulama_sg.id]
  key_name               = var.ssh_key_name
  iam_instance_profile   = aws_iam_instance_profile.sunucu_profil.name

  root_block_device {
    volume_type           = "gp3"
    volume_size           = 50
    delete_on_termination = true
    encrypted             = true
  }

  user_data = templatefile("${path.module}/scripts/baslangic.sh", {
    db_endpoint = aws_db_instance.veritabani.endpoint
    app_version = var.uygulama_versiyonu
  })

  lifecycle {
    create_before_destroy = true
    ignore_changes        = [ami]
  }

  tags = local.ortak_etiketler
}

Bu örnekte birkaç önemli konsept bir arada kullanılıyor. var. prefix’i değişkenlere referans vermeyi, aws_subnet.ozel_subnet.id gibi ifadeler başka kaynaklara referans vermeyi, local. ise yerel değerlere erişmeyi sağlıyor.

Variable Blokları: Esneklik ve Yeniden Kullanılabilirlik

Değişkenler, Terraform konfigürasyonlarını esnek ve yeniden kullanılabilir hale getiren temel mekanizmadır. Bir değişken tanımladığınızda, o konfigürasyonu farklı ortamlarda farklı değerlerle çalıştırabilirsiniz.

Temel Değişken Tanımlama

variable "region" {
  description = "AWS bölgesi"
  type        = string
  default     = "eu-west-1"
}

variable "instance_sayisi" {
  description = "Kaç adet instance oluşturulacak"
  type        = number
  default     = 2
}

variable "production_ortami" {
  description = "Production ortamı mı?"
  type        = bool
  default     = false
}

Değişken tanımlarken kullanabileceğiniz parametreler:

  • description: Değişkenin ne işe yaradığını açıklar, ekip çalışmasında kritik önem taşır
  • type: Değişkenin veri tipini belirtir, string/number/bool/list/map/object/any olabilir
  • default: Değişken için varsayılan değer atar, bu parametreyi kullanırsanız değişken opsiyonel hale gelir
  • validation: Değişken değeri için doğrulama kuralları tanımlar
  • sensitive: true olarak ayarlandığında değer loglarda ve çıktılarda gizlenir

Karmaşık Değişken Tipleri

Gerçek projelerde basit string veya number değişkenlerin ötesine geçmeniz gerekir. Liste ve map tipleri çok daha güçlü yapılar oluşturmanızı sağlar:

variable "izin_verilen_portlar" {
  description = "Güvenlik grubuna izin verilecek portlar"
  type        = list(number)
  default     = [80, 443, 8080]
}

variable "ortam_etiketleri" {
  description = "Kaynaklara uygulanacak etiketler"
  type        = map(string)
  default = {
    Environment = "staging"
    Project     = "altyapi-modernizasyon"
    ManagedBy   = "terraform"
  }
}

variable "veritabani_konfig" {
  description = "Veritabanı konfigürasyonu"
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    allocated_storage = number
    multi_az       = bool
  })
  default = {
    engine            = "postgres"
    engine_version    = "14.7"
    instance_class    = "db.t3.medium"
    allocated_storage = 100
    multi_az          = false
  }
}

Değişken Doğrulama

Validation bloğu, değişkenlere geçersiz değer girilmesini engellemenin en temiz yoludur. Büyük ekiplerde çalışırken bu özellik gereksiz hataların önüne geçer:

variable "instance_type" {
  description = "EC2 instance tipi"
  type        = string
  default     = "t3.medium"

  validation {
    condition = contains([
      "t3.small",
      "t3.medium",
      "t3.large",
      "m5.large",
      "m5.xlarge"
    ], var.instance_type)
    error_message = "Gecersiz instance tipi. Izin verilen tipler: t3.small, t3.medium, t3.large, m5.large, m5.xlarge"
  }
}

variable "ortam_adi" {
  description = "Deployment ortamı"
  type        = string

  validation {
    condition     = can(regex("^(dev|staging|production)$", var.ortam_adi))
    error_message = "Ortam adi sadece 'dev', 'staging' veya 'production' olabilir."
  }
}

Locals: Hesaplanmış Değerlerin Gücü

Locals, değişkenlerden farklı olarak dışarıdan değer almaz. Konfigürasyon içinde hesaplanan, türetilen değerleri tanımlamak için kullanılır. Kodun tekrar kullanılabilirliği ve okunabilirliği açısından son derece değerlidir.

locals {
  uygulama_adi = "webapi"
  
  # Ortama göre dinamik isim oluşturma
  kaynak_prefix = "${local.uygulama_adi}-${var.ortam_adi}"
  
  # Hesaplanmış değerler
  toplam_disk_kapasitesi = var.instance_sayisi * var.disk_boyutu_gb
  
  # Koşullu değer ataması
  instance_tipi = var.production_ortami ? "m5.large" : "t3.medium"
  
  # Tüm kaynaklara uygulanacak ortak etiketler
  ortak_etiketler = merge(var.ortam_etiketleri, {
    Application = local.uygulama_adi
    ManagedBy   = "terraform"
    LastUpdated = timestamp()
  })
  
  # Port listesinden dinamik kural oluşturma
  guvenlik_kurallari = {
    for port in var.izin_verilen_portlar :
    "port_${port}" => {
      from_port = port
      to_port   = port
      protocol  = "tcp"
    }
  }
}

Locals’ı özellikle karmaşık ifadeleri bir kez yazıp defalarca kullanmak için tercih edin. Hem kodu temiz tutar hem de bir değişiklik yapmanız gerektiğinde tek bir yerden düzenlemenize olanak tanır.

Data Sources: Mevcut Kaynakları Sorgulamak

Data source’lar, Terraform dışında veya başka bir state’te yönetilen kaynakları sorgulamanızı sağlar. Mevcut altyapıyla entegrasyon kurarken vazgeçilmez bir araçtır.

# Mevcut VPC'yi sorgula
data "aws_vpc" "mevcut_vpc" {
  filter {
    name   = "tag:Name"
    values = ["production-vpc"]
  }
}

# En güncel Amazon Linux 2 AMI'sini bul
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Mevcut IAM rolünü çek
data "aws_iam_role" "mevcut_rol" {
  name = "existing-app-role"
}

# Bu data source'ları kullanmak
resource "aws_instance" "uygulama" {
  ami       = data.aws_ami.amazon_linux.id
  subnet_id = data.aws_vpc.mevcut_vpc.id

  tags = {
    Name = "${local.kaynak_prefix}-instance"
  }
}

Output Blokları: Değerleri Dışa Aktarmak

Output’lar iki amaçla kullanılır. Birincisi, terraform apply sonrasında size kritik bilgileri göstermek için. İkincisi, bir modülün çıktılarını başka modüllere veya root konfigürasyona aktarmak için.

output "web_sunucu_ip" {
  description = "Web sunucusunun public IP adresi"
  value       = aws_instance.uygulama_sunucu.public_ip
}

output "veritabani_endpoint" {
  description = "RDS veritabani endpoint adresi"
  value       = aws_db_instance.veritabani.endpoint
  sensitive   = true
}

output "yuk_dengeleyici_dns" {
  description = "Application Load Balancer DNS adresi"
  value       = aws_lb.main.dns_name
}

output "olusturulan_kaynaklar" {
  description = "Bu deployment ile olusturulan tum kaynak ID'leri"
  value = {
    instance_id    = aws_instance.uygulama_sunucu.id
    sg_id          = aws_security_group.uygulama_sg.id
    subnet_ids     = aws_subnet.ozel_subnet[*].id
  }
}

Gerçek Dünya Senaryosu: Çok Ortamlı Yapılandırma

Şimdiye kadar öğrendiklerimizi bir araya getirelim. Birçok şirketin ihtiyaç duyduğu dev/staging/production ortam ayrımını HCL ile nasıl yönetirsiniz?

Proje yapısı şu şekilde olabilir:

  • main.tf: Ana kaynak tanımları
  • variables.tf: Değişken tanımları
  • outputs.tf: Çıktı tanımları
  • locals.tf: Yerel değerler
  • environments/dev.tfvars: Dev ortamı değerleri
  • environments/prod.tfvars: Production ortamı değerleri

variables.tf dosyası:

variable "ortam" {
  description = "Deployment ortami (dev/staging/production)"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "production"], var.ortam)
    error_message = "Gecerli ortamlar: dev, staging, production"
  }
}

variable "sunucu_sayisi" {
  description = "Her ortam icin sunucu sayisi"
  type        = number
}

variable "enable_yedekleme" {
  description = "Otomatik yedekleme aktif mi?"
  type        = bool
  default     = false
}

variable "izin_verilen_cidr_bloklar" {
  description = "SSH erisimi icin izin verilen CIDR bloklar"
  type        = list(string)
  default     = []
}

environments/prod.tfvars dosyası:

ortam                    = "production"
sunucu_sayisi            = 4
enable_yedekleme         = true
izin_verilen_cidr_bloklar = ["10.0.0.0/8", "172.16.0.0/12"]

locals.tf dosyası:

locals {
  # Ortama gore instance boyutu belirle
  instance_boyutu = {
    dev        = "t3.small"
    staging    = "t3.medium"
    production = "m5.large"
  }

  secilen_instance = local.instance_boyutu[var.ortam]

  # Ortama gore backup retention suresi
  backup_retention = var.ortam == "production" ? 30 : 7

  # Tum kaynaklara uygulanacak etiketler
  global_etiketler = {
    Environment = var.ortam
    ManagedBy   = "terraform"
    Team        = "platform-engineering"
  }
}

Sık Yapılan Hatalar ve Kaçınma Yolları

HCL yazarken en çok karşılaşılan hatalar ve bunlardan nasıl kaçınabileceğiniz:

Döngüsel bağımlılıklar: Kaynak A, kaynak B’ye; kaynak B de kaynak A’ya referans verirse Terraform apply çalışmaz. Bunu önlemek için bağımlılıkları dikkatli tasarlayın, gerekirse depends_on kullanın ama bunu son çare olarak düşünün.

Sensitive değerlerin output’lara eklenmesi: Veritabanı şifresi veya API anahtarı gibi değerleri output olarak tanımlayacaksanız mutlaka sensitive = true ekleyin.

Hardcoded değerler: Region, account ID veya resource ARN gibi değerleri direkt koda yazmak, konfigürasyonu yeniden kullanılamaz hale getirir. Bunları her zaman değişken veya data source üzerinden alın.

Değişken tipi belirtmemek: type parametresi olmayan değişkenler any tipini varsayar. Bu esneklik sağlar ama aynı zamanda beklenmedik hatalara yol açar. Mümkün olduğunca açık tip belirtin.

Uzun ve karmaşık locals: Bir locals bloğu içine çok fazla mantık koymak kodu takip edilmez hale getirir. Gerekirse mantığı birden fazla locals bloğuna bölün ya da helper modüller oluşturun.

terraform.tfvars ve Değişken Geçirme Yöntemleri

Değişkenlere değer geçirmenin birden fazla yolu vardır ve bunların öncelik sırası önemlidir. En düşükten en yükseğe doğru sıralama:

  • Environment variable (TF_VAR_ prefix): export TF_VAR_ortam=production
  • terraform.tfvars dosyası: Otomatik yüklenir
  • .auto.tfvars uzantılı dosyalar: Otomatik ve alfabetik sırayla yüklenir
  • -var-file parametresi: terraform apply -var-file="environments/prod.tfvars"
  • -var parametresi: terraform apply -var="instance_sayisi=3"
# terraform.tfvars ornegi
ortam            = "dev"
sunucu_sayisi    = 1
enable_yedekleme = false

# Cok satira yayilan liste
izin_verilen_cidr_bloklar = [
  "192.168.1.0/24",
  "10.10.0.0/16"
]

CI/CD pipeline’larında genellikle environment variable veya -var-file yöntemi tercih edilir. Hassas değerleri asla .tfvars dosyalarına yazmayın ve bu dosyaları git repository’sine commit etmeyin.

Sonuç

HCL’yi kavramak, Terraform yolculuğunuzun en kritik adımıdır. Resource blokları altyapınızın iskeletini oluştururken, variable ve locals mekanizmaları bu iskeletin farklı ortam ve koşullara uyum sağlamasını mümkün kılar. Data source’lar mevcut altyapıyla köprü kurmanızı, output’lar ise değerli bilgileri dışarıya aktarmanızı sağlar.

Pratik olarak şunu öneririm: Yeni bir Terraform projesi başlatırken önce değişken tanımlarınızı yazın, sonra locals ile iş mantığınızı kodlayın, en son kaynak tanımlarına geçin. Bu sıralama, kodu daha okunabilir ve bakımı daha kolay hale getirir.

Bir sonraki adım olarak Terraform modüllerini ve remote state yönetimini incelemenizi tavsiye ederim. HCL temellerini oturdurduktan sonra bu konular çok daha anlamlı gelecek. Sorularınız veya farklı senaryolar için yorum bölümünü kullanabilirsiniz.

Bir yanıt yazın

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