Terraform ile Docker Altyapı Yönetimi

Altyapıyı kod olarak yönetmek, bir zamanlar sadece büyük şirketlerin lüksüydü. Şimdi ise Terraform sayesinde iki kişilik bir startup bile production ortamını tamamen versiyon kontrollü, tekrarlanabilir ve otomatik bir şekilde yönetebiliyor. Bu yazıda özellikle Docker altyapısını Terraform ile nasıl yöneteceğimize, gerçek dünya senaryolarında nasıl sorunları çözdüğüne ve dikkat etmeniz gereken noktalara bakacağız.

Neden Terraform + Docker?

Docker zaten kendi CLI’ı ve Compose araçlarıyla gayet iyi çalışıyor diyebilirsiniz. Haklısınız da. Ama şöyle bir senaryo düşünün: 10 farklı sunucuda, 3 farklı ortamda (dev, staging, prod) Docker container’ları yönetiyorsunuz. Her ortamda hangi image’ın, hangi volume’ün, hangi network’ün olduğunu takip etmeye çalışıyorsunuz. Bir değişikliği staging’e uyguladınız ama production’a unuttuğunuz oluyor. İşte tam bu noktada Terraform devreye giriyor.

Terraform’un Docker provider’ı, container’lardan image’lara, volume’lerden network’lere kadar her şeyi HCL (HashiCorp Configuration Language) ile tanımlamanıza izin veriyor. State dosyası sayesinde “ne var, ne olması gerekiyor” karşılaştırmasını otomatik yapıyor. Ve en önemlisi, bu konfigürasyonu Git’e atıp ekibinizle paylaşabiliyorsunuz.

Kurulum ve İlk Adımlar

Önce gerekli araçları kuralım. Terraform’u HashiCorp’un resmi reposundan kurmak en sağlıklı yol:

# Ubuntu/Debian için
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# macOS için
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Versiyon kontrolü
terraform version

Docker’ın kurulu ve çalışır durumda olduğunu varsayıyorum. Şimdi proje yapısını oluşturalım:

mkdir terraform-docker-demo
cd terraform-docker-demo

# Temel dosya yapısı
touch main.tf
touch variables.tf
touch outputs.tf
touch terraform.tfvars

# Ortam bazlı klasörler (ilerleyen bölümde kullanacağız)
mkdir -p environments/dev
mkdir -p environments/prod
mkdir -p modules/webapp

Provider Konfigürasyonu

Her Terraform projesinin bir provider konfigürasyonuna ihtiyacı var. Docker için bu şu şekilde görünüyor:

# main.tf
terraform {
  required_version = ">= 1.0"
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

# Uzak bir Docker host'a bağlanmak için:
# provider "docker" {
#   host = "tcp://remote-docker-host:2376"
#   cert_path = "/path/to/certs"
# }

terraform init komutuyla provider’ı indirin:

terraform init

Bu komut çalıştığında .terraform klasörü oluşur ve provider binary’si indirilir. Bu klasörü .gitignore‘a eklemeyi unutmayın.

Temel Kaynaklar: Image, Container, Network, Volume

Şimdi gerçek bir senaryo üzerinde gidelim. Bir web uygulaması + PostgreSQL veritabanı + Nginx reverse proxy kombinasyonu kuracağız. Bu yapı birçok şirketin staging ortamı için kullandığı klasik bir senaryo.

Önce variables.tf dosyasını dolduralım:

# variables.tf
variable "environment" {
  description = "Deployment ortami (dev, staging, prod)"
  type        = string
  default     = "dev"
}

variable "app_image" {
  description = "Uygulama Docker image'i"
  type        = string
  default     = "nginx:alpine"
}

variable "postgres_password" {
  description = "PostgreSQL root sifresi"
  type        = string
  sensitive   = true
}

variable "app_port" {
  description = "Uygulamanin dinleyecegi port"
  type        = number
  default     = 8080
}

variable "container_count" {
  description = "Calistirilacak uygulama container sayisi"
  type        = number
  default     = 1
}

Şimdi ana kaynak tanımlarına geçelim:

# main.tf (devam)

# Docker Network
resource "docker_network" "app_network" {
  name   = "${var.environment}-app-network"
  driver = "bridge"

  ipam_config {
    subnet  = "172.20.0.0/16"
    gateway = "172.20.0.1"
  }

  labels {
    label = "environment"
    value = var.environment
  }
}

# PostgreSQL Volume
resource "docker_volume" "postgres_data" {
  name = "${var.environment}-postgres-data"

  labels {
    label = "managed-by"
    value = "terraform"
  }
}

# PostgreSQL Image
resource "docker_image" "postgres" {
  name         = "postgres:15-alpine"
  keep_locally = true
}

# PostgreSQL Container
resource "docker_container" "postgres" {
  name  = "${var.environment}-postgres"
  image = docker_image.postgres.image_id

  env = [
    "POSTGRES_DB=appdb",
    "POSTGRES_USER=appuser",
    "POSTGRES_PASSWORD=${var.postgres_password}"
  ]

  volumes {
    volume_name    = docker_volume.postgres_data.name
    container_path = "/var/lib/postgresql/data"
  }

  networks_advanced {
    name         = docker_network.app_network.name
    ipv4_address = "172.20.0.10"
  }

  restart = "unless-stopped"

  healthcheck {
    test         = ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
    interval     = "10s"
    timeout      = "5s"
    retries      = 5
    start_period = "30s"
  }
}

# Nginx Image
resource "docker_image" "nginx" {
  name         = var.app_image
  keep_locally = true
}

# Uygulama Container'lari (birden fazla)
resource "docker_container" "app" {
  count = var.container_count
  name  = "${var.environment}-app-${count.index + 1}"
  image = docker_image.nginx.image_id

  env = [
    "ENVIRONMENT=${var.environment}",
    "DB_HOST=172.20.0.10",
    "DB_NAME=appdb",
    "DB_USER=appuser"
  ]

  ports {
    internal = 80
    external = var.app_port + count.index
  }

  networks_advanced {
    name = docker_network.app_network.name
  }

  depends_on = [docker_container.postgres]

  restart = "unless-stopped"

  labels {
    label = "environment"
    value = var.environment
  }
}

Bu konfigürasyon oldukça fazla şey yapıyor. count parametresi ile birden fazla container ayağa kaldırabiliyoruz, her birinin portu otomatik olarak artıyor. depends_on ile PostgreSQL’in hazır olmasını bekliyoruz.

Outputs Tanımları

# outputs.tf
output "app_urls" {
  description = "Uygulama erisim adresleri"
  value = [
    for container in docker_container.app :
    "http://localhost:${container.ports[0].external}"
  ]
}

output "network_id" {
  description = "Olusturulan Docker network ID"
  value       = docker_network.app_network.id
}

output "postgres_container_id" {
  description = "PostgreSQL container ID"
  value       = docker_container.postgres.id
}

output "volume_name" {
  description = "PostgreSQL data volume adi"
  value       = docker_volume.postgres_data.name
}

Ortam Değişkenlerini Güvenli Yönetmek

Hassas bilgileri terraform.tfvars dosyasına koyabilirsiniz, ama bu dosyayı asla Git’e commit etmeyin:

# terraform.tfvars (gitignore'a ekleyin!)
postgres_password = "super-secret-password-123"
environment       = "dev"
app_port          = 8080
container_count   = 2
# .gitignore
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
terraform.tfvars
*.auto.tfvars

Daha iyi bir yaklaşım, değerleri environment variable olarak geçmek:

export TF_VAR_postgres_password="super-secret-password-123"
export TF_VAR_environment="staging"
terraform apply

CI/CD pipeline’larında bu yöntem çok daha güvenli. GitHub Actions veya GitLab CI’da secret olarak tanımlayıp environment variable üzerinden Terraform’a geçiriyorsunuz.

Modüler Yapı: Gerçek Dünya Yaklaşımı

Küçük projelerde tek dosya yeterli, ama büyüdükçe modüler yapıya geçmek şart. İşte bir webapp modülü örneği:

# modules/webapp/main.tf
variable "environment" {}
variable "app_name" {}
variable "image" {}
variable "port" {}
variable "network_name" {}
variable "env_vars" {
  type    = list(string)
  default = []
}

resource "docker_image" "app" {
  name         = var.image
  keep_locally = true
}

resource "docker_container" "app" {
  name  = "${var.environment}-${var.app_name}"
  image = docker_image.app.image_id

  env = var.env_vars

  ports {
    internal = var.port
    external = var.port
  }

  networks_advanced {
    name = var.network_name
  }

  restart = "unless-stopped"

  labels {
    label = "app"
    value = var.app_name
  }
  labels {
    label = "environment"
    value = var.environment
  }
}

output "container_name" {
  value = docker_container.app.name
}

output "container_id" {
  value = docker_container.app.id
}

Bu modülü çağırmak:

# environments/prod/main.tf
module "frontend" {
  source       = "../../modules/webapp"
  environment  = "prod"
  app_name     = "frontend"
  image        = "mycompany/frontend:v2.1.0"
  port         = 3000
  network_name = docker_network.prod_network.name
  env_vars     = [
    "NODE_ENV=production",
    "API_URL=http://backend:4000"
  ]
}

module "backend" {
  source       = "../../modules/webapp"
  environment  = "prod"
  app_name     = "backend"
  image        = "mycompany/backend:v1.8.0"
  port         = 4000
  network_name = docker_network.prod_network.name
  env_vars     = [
    "NODE_ENV=production",
    "DB_URL=postgresql://appuser:${var.db_password}@postgres:5432/appdb"
  ]
}

Plan, Apply ve Destroy Workflow’u

Günlük kullanımda şu komutları çok sık kullanacaksınız:

# Değişiklikleri önce inceleyin
terraform plan

# Sadece belirli bir kaynağa odaklanmak için
terraform plan -target=docker_container.postgres

# Değişiklikleri uygula
terraform apply

# Onay sormadan uygula (CI/CD için)
terraform apply -auto-approve

# Belirli bir variable geçerek uygula
terraform apply -var="container_count=3"

# Altyapıyı tamamen yık (dikkatli kullanın!)
terraform destroy

# Sadece belirli bir kaynağı yık
terraform destroy -target=docker_container.app

terraform plan çıktısını okumayı öğrenin. + yeni kaynak ekleniyor, - kaynak siliniyor, ~ kaynak değiştiriliyor, -/+ kaynak yeniden oluşturuluyor demek. Özellikle -/+ işaretini görünce dikkatli olun, container’ın komple silinip yeniden oluşturulacağı anlamına geliyor, bu downtime demek.

State Yönetimi: Takım Çalışması İçin Remote Backend

Tek başınıza çalışıyorsanız lokal state dosyası yeterli. Ama bir takımda çalışıyorsanız, remote backend şart. Aksi halde iki kişi aynı anda terraform apply çalıştırdığında state dosyası çakışır ve işler karışır.

Docker altyapısını yönetirken genellikle Terraform Cloud veya S3 backend kullanılıyor. S3 + DynamoDB kombinasyonu en yaygın yöntem:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "docker-infra/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

Lokal test ortamı için Terraform Cloud’un ücretsiz planı da iyi bir seçenek. Hem state yönetimi hem de temel CI/CD özelliklerini veriyor.

Lifecycle Kuralları: İnce Ayarlar

Bazı durumlarda Terraform’un varsayılan davranışını değiştirmeniz gerekiyor. lifecycle bloğu burada devreye giriyor:

resource "docker_container" "app" {
  name  = "${var.environment}-app"
  image = docker_image.nginx.image_id

  # ...

  lifecycle {
    # Image değişince container'ı yeniden oluştur
    replace_triggered_by = [docker_image.nginx]

    # Bu container'ı hiçbir zaman Terraform ile silme
    # (production veritabanı container'ları için kullanışlı)
    prevent_destroy = true

    # Dışarıdan yapılan değişiklikleri yoksay
    ignore_changes = [
      labels,
      env
    ]
  }
}

prevent_destroy = true production veritabanı container’larınız için çok önemli. Yanlışlıkla terraform destroy çalıştırdığınızda sizi kurtarır.

ignore_changes ise otomatik deployment’larla manuel değişikliklerin çakışmasını önler. Örneğin uygulamanız kendi container label’larını güncelleyebiliyor, Terraform her apply‘da bu değişikliği geri almak isteyecektir. ignore_changes ile bunu engelleyebilirsiniz.

CI/CD Pipeline Entegrasyonu

GitLab CI ile basit bir pipeline örneği:

# .gitlab-ci.yml içeriğini bash formatında gösteriyoruz
# stages: validate, plan, apply

# Validate aşaması: format ve syntax kontrolü
terraform fmt -check -recursive
terraform validate

# Plan aşaması: değişiklikleri kaydet
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

# Apply aşaması: sadece main branch'te çalışsın
# Manuel onay gerektirsin
terraform apply tfplan

GitHub Actions için de benzer bir yapı kurabilirsiniz. Önemli olan plan çıktısını bir artifact olarak saklamak ve apply aşamasında o plan dosyasını kullanmak. Bu sayede “plan’da gördüğüm şey uygulandı” garantisi alıyorsunuz.

Sık Karşılaşılan Sorunlar ve Çözümleri

Problem: terraform apply sonrası container hep yeniden oluşturuluyor. Çözüm: Genellikle image ID’si değişiyor ya da env listesinin sırası tutarsız. ignore_changes = [env] ile çözebilirsiniz, ama asıl nedeni bulmak daha iyi.

Problem: State dosyası ile gerçek durum uyumsuz hale geldi. Çözüm: terraform refresh ile state’i güncelle. Eğer kaynaklar state dışında manuel silindiyse terraform state rm resource.name ile state’den de kaldır.

Problem: İki farklı ortam aynı port’u kullanıyor, çakışma var. Çözüm: Variable ile port offset kullanın ya da her ortam için farklı bir port aralığı tanımlayın.

Problem: docker_container resource’u her apply’da değişiyor gibi görünüyor. Çözüm: keep_locally = true ayarını image resource’una ekleyin ve image ID yerine image adını kullanın.

# State'i incelemek için faydalı komutlar
terraform state list
terraform state show docker_container.app
terraform state show docker_volume.postgres_data

# State'den kaynak kaldırmak (silmeden sadece Terraform takibinden çıkarmak)
terraform state rm docker_container.old_container

# Mevcut bir kaynağı Terraform kontrolüne almak
terraform import docker_container.existing container_id_here

Gerçek Dünya Senaryosu: Mikroservis Geçişi

Bir e-ticaret şirketinde çalıştığımı düşünün. Monolitik uygulamayı mikroservislere taşıyoruz. 5 farklı servis var: auth, product, order, payment, notification. Her birinin kendi veritabanı bağlantısı, kendi port’u, kendi restart politikası var.

Docker Compose ile başladık ama ortam sayısı arttıkça yönetemez hale geldik. Terraform’a geçince şunları kazandık:

  • Her servis versiyonu Git tag’i ile takip edilir hale geldi
  • Staging’e yeni bir servis eklemek, değişikliği PR açıp review’dan geçirmek anlamına geldi
  • terraform plan ile “bu deployment ne değiştirecek” sorusunu net cevaplamaya başladık
  • Bir şeyler bozulunca git revert + terraform apply ile 5 dakikada eski haline döndük

Bu geçiş 3 ay sürdü ve en zorlu kısım mevcut container’ları terraform import ile Terraform kontrolüne almaktı. Her container için ID’yi bulmak, import etmek, Terraform konfigürasyonunu gerçekle uyumlu hale getirmek. Sabır isteyen bir süreç ama bir kez tamamlayınca değiyor.

Sonuç

Terraform ile Docker yönetimi başta gereksiz bir karmaşıklık gibi görünebilir. Özellikle tek bir sunucu ve birkaç container varken. Ama altyapı büyüdükçe, ortam sayısı arttıkça, takım genişledikçe bu yatırımın değeri ortaya çıkıyor.

En kritik noktaları özetlemek gerekirse: state dosyasını mutlaka remote backend’de tutun, hassas değerleri asla kod içine yazmayın, terraform plan çıktısını apply öncesi dikkatlice okuyun, ve lifecycle bloklarını production kaynakları için doğru konfigüre edin.

Docker provider’ı Terraform ekosisteminin en olgun provider’larından biri. Dokümantasyonu kapsamlı, community büyük, sorunlarla karşılaştığınızda kaynak bulmak zor değil. Küçük bir test projesiyle başlayın, modüler yapıyı öğrenin ve zamanla bütün Docker altyapınızı kod olarak yönetir hale gelin. Birkaç ay sonra “eskiden nasıl yönetiyorduk” diye düşüneceksiniz.

Bir yanıt yazın

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