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 planile “bu deployment ne değiştirecek” sorusunu net cevaplamaya başladık- Bir şeyler bozulunca
git revert+terraform applyile 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.
