Kubernetes'e Geçmeden Önce Compose Yeter mi
Tek host, birkaç servis, henüz scheduler yok

Üç konteyner, bir sanal makine, bir compose dosyası. API, worker, Postgres. Restart policy, bind mount, gece yedek. Bu düzen iki yıl kimseyi uyandırmadı. 'Kubernetes'e geçelim' cümlesi bir konferanstan sonra geldi. Gerekçe: olgunluk. Olgunluk, scheduler demek değildi. Slayt demekti.
Compose'u küçümsemek kolay. YAML'ı da küçümsemek kolay. İkisi de dosya. Fark, kontrol düzleminin nerede olduğu. Compose, Docker engine'e 'bunları aç' der. Kubernetes, bir API sunucusuna 'istenilen durum bu' der, sonra o durumu birçok makinede arar. İkinci cümleyi hak etmeyen bir kurulumda birinci cümle yeter.
Compose'un dürüst sınırı
Tek host. Servis sayısı bir insanın kafasındaki grafı aşmıyor. Yayın, imaj çek + compose up. Geri dönüş, önceki digest. Log, journald veya bir dosya. Bu listede eksik olan şeyler var: çok bölgeli planlama, pod disruption, servis mesh, CRD. Eksik olduğu için yanlış değil. İhtiyaç yoksa eksik, sade.
- Tek makine kabul: disk, ağ, yedek o makineye bağlı.
- Yatay büyüme 'bir instance daha' değil, başka bir compose veya başka bir VM.
- Secret dosyası veya engine secret; kube Secret nesnesi yok.
- Rolling update senin sıran: önce worker, sonra api, health bak, devam.
Bu sınırları yazıp imzalamak, 'henüz kube yok' utancını bitirir. Yazmayınca her kesinti bir kanıt gibi kullanılır: bak, orchestrator yok. Kesinti çoğu zaman yedek, disk, veya latest tag'dir. Orchestrator onları otomatik düzeltmez.
Ne zaman dosya yetmez
İkinci host'u yük için değil, kesinti için istediğimde compose incindi. Aktif-aktif iki compose, Postgres'i çoğaltmaz. Aktif-pasif, DNS veya IP kaydırma ister. Bunu script ile yapmak mümkün. Script, kontrol düzlemi olmaya başlar. O anda kube bir seçenek, henüz zorunluluk değil. Zorunluluk, script'in test edilmeyen dalları çoğalınca gelir.
Üçüncü işaret: yayın sırasında eski ve yeni replica aynı anda yaşasın, readiness ile trafik kaysın, başarısızsa otomatik geri dönsün. Bunu compose + nginx + el ile yaptım. Çalıştı. İkinci ekip üyesi izinliyken çalışmadı. Runbook, kişiye bağlı orchestrator'dır. Kişiye bağlı olan şey, küme değil, risk.
Dördüncü: kaynak kavgası. API burst'ü Postgres'i boğuyor, cgroup elle ayarlı, birisi limit'i unutuyor. kube burada cgroup'u nesne yapar. Compose da deploy.resources veya mem_limit bilir. Unutmamak süreç işi. Araç, unutmayı zorlaştırır; düşünmeyi devretmez.
Aynı iş, iki dosya
Karşılaştırma için aynı servisi iki yerde yazıyorum. Amaç 'kube daha uzun' demek değil. Amaç, eklenen her satırın bir operasyon karşılığı olduğunu görmek.
services:
api:
image: api:1.8.0@sha256:7c2d1b0e4a9f2c8b1d6e0a4f3c9b8e7d6c5b4a3928170654433221100ffeedd
restart: unless-stopped
env_file: /etc/api/api.env
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health/ready"]
interval: 10s
timeout: 2s
retries: 6
ports:
- "8080:8080"
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
db:
image: postgres:16.2-bookworm
restart: unless-stopped
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/pg
secrets:
- pg
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 10
secrets:
pg:
file: /etc/api/pg.password
volumes:
pgdata:
Bu dosya bir ekibin okuyabileceği kadar kısa. Secret dosyası host'ta, imaj pinli, log dönüyor, health var. Eksik: ikinci node, pod anti-affinity, sertifika döndürme nesnesi, HPA. Bunlar yok diye dosya oyuncak değil. Ürün bunları istemiyorsa dosya doğru boyutta.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
containers:
- name: api
image: api@sha256:7c2d1b0e4a9f2c8b1d6e0a4f3c9b8e7d6c5b4a3928170654433221100ffeedd
ports:
- containerPort: 8080
envFrom:
- secretRef: { name: api-env }
readinessProbe:
httpGet: { path: /health/ready, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /health/live, port: 8080 }
periodSeconds: 15
resources:
requests: { cpu: "100m", memory: "256Mi" }
limits: { memory: "512Mi" }
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector: { app: api }
ports:
- port: 80
targetPort: 8080
Deployment + Service, compose'taki bir servis bloğunun karşılığı bile değil. Eksik olan Ingress, ServiceAccount, NetworkPolicy, PDB, HPA, Secret, ServiceMonitor. Her biri ayrı YAML, ayrı RBAC, ayrı 'kim kırdı' sorusu. Bunu yazabilmek olgunluk. Bunu işletmek ayrı vardiya.
Gizli maliyet
Küme: kontrol düzlemi, etcd yedeği, sürüm sıçraması, CNI, storage class. Managed küme bunları faturalandırır, silmez. Node drain bir Cuma akşamı öğrenilmez. kubectl bilgisi, compose bilgisine ek. Yerine geçmez. 'Artık Docker bilmeme gerek yok' cümlesi, image ve runtime'ı kümenin yuttuğunu sanmaktır. Pod, yine bir konteyner.
Gözlem: kube olmadan da Prometheus çalışır. kube ile de çalışmayabilir — scrape ayarı yoksa. Geçiş, metrik üretmez. Geçiş, hedef keşfini değiştirir. Bunu planlamadan 'daha görünür olacağız' demek, dashboard sayısıyla körlüğü karıştırmaktır.
Ertelediğim geçiş
Erteledim çünkü ikinci bir availability zone yoktu. Tek zone'da iki node, tek disk sınıfı, tek NAT. kube o gerçeği dağıtmaz. Dağıtılmış gibi gösterir. Tek zone kesintisinde etcd de, pod da, compose da aynı binada durur. Önce ağı ve yedeği çoğalt. Sonra kontrol düzlemini.
Erteledim çünkü yayın sıklığı günde bir. Rolling update otomasyonu, günde bir yayın için pahalı. Haftada bir için daha pahalı. Compose up + health + önceki digest, bu sıklıkta okunabilir. Sıklık artınca, gece yayınınca, üç ekip aynı dosyaya basınca tekrar konuşurum.
Erteledim çünkü secret rotasyonu hâlâ bir dosya değişimi. kube Secret bunu nesne yapar, şifreleme ve RBAC ekler. Dosyayı Git'e koymuyorsan (koymuyorsun), host permission da bir kontroldür. Secret'ı Git'e koyan bir ekibe kube vermek, Secret'ı YAML olarak Git'e koydurtur. Araç, alışkanlığı düzeltmez.
Geçeceksem ne taşırım
- İmaj zaten digest. latest yok. Bunu küme çözmez, sen çözersin.
- Health uçları live/ready ayrılmış. Probe kopyalar.
- Kaynak limiti ölçülmüş. Talep uydurma değil.
- Log stdout. Küme dosya toplayıcı değil, senin sürecin yazar.
- Rollback: önceki ReplicaSet / önceki digest. Elle SSH yok.
Bu liste compose'ta da durur. Listeyi tamamlamadan kube, eksikleri YAML ile çoğaltır. Multi-stage ve tag pin bu yüzden serinin başında. Temel imaj hareket ederken, image 1.4 GB iken, health yokken küme kurmak, bozuk kutuyu çoğaltmaktır.
Karar cümlesi
Compose yeter mi? Tek host, az servis, okunabilir yayın, kabul edilen kesinti penceresi: evet. İkinci zone, kişiye bağlı runbook, aynı anda yaşayan replica, kaynak kavgası ve birden fazla ekip: hayır, veya 'henüz hayır ama tarih yaz'. Tarih, 'olgunlaştık' değil. Tarih, listedeki hangi maddenin geldiği.
Ortadaki seçenekler
k3s veya bir managed tek node, compose ile kube arasında durur. Tek node k3s, scheduler oyunu oynar, etcd yine o diskte durur. Öğrenme için iyi. Kesinti bütçesi için compose'dan üstün değil. 'Küme kurduk' cümlesi tek node'da boş. İki node, iki zone, backup alınmış etcd: o zaman cümle dolu.
Compose profiles ortam böler: dev, worker, obs. Hepsini her host'ta açmam. Observability profili ikinci bir compose projesi de olabilir. Proje adı -p ile ayrılır. İki proje, iki ağ, yanlışlıkla aynı Postgres'e yazmama. kube namespace'inin ucuz hali.
# compose.obs.yml
services:
loki:
image: grafana/loki:3.0.0
profiles: ["obs"]
restart: unless-stopped
command: ["-config.file=/etc/loki/local-config.yaml"]
promtail:
image: grafana/promtail:3.0.0
profiles: ["obs"]
volumes:
- /var/log:/var/log:ro
Bunu kube stack'ine çevirmek bir öğleden sonra değil, bir çeyrek. Chart, değer, kalıcılık, ingress. Compose'ta üç servis. İhtiyacın üç servis ise dosya durur. İhtiyacın 'her ekip kendi namespace'i' ise dosya yetmez. İhtiyacı yazmadan chart kopyalamak, serinin başındaki latest kadar hareketli bir zemin.
İnsan saati
Geçişin maliyeti lisans değil. Drain denemesi, RBAC yanlışlığı, storage class'ın tek zone'a bağlı olduğu Cuma 18:40. Bunları compose'ta da yaşarsın ama yüzeyi küçük. Kümede aynı hata, üç dashboard ve bir 'hangisi kaynak' tartışması üretir. İkinci kişiyi işe almadan küme kurmak, o kişiyi incident'te aramaktır.
Yayın sıklığı günde on olunca compose up sıkıcı olur, evet. O sıkıcılık bir sinyal. Sinyali 'YAML çoğalt' diye okumak yerine 'istenen durum nedir' diye oku. İstenen durum iki replica ve otomatik geri dönüşse kube. İstenen durum bir kutu ve bir digest'se compose. İkisini aynı cümlede istemek, ikisini de kötü işletir.
Geri dönüş senaryosu
Compose host'ta geri dönüş: kayıttaki digest'i dosyaya yaz, docker compose up -d. Kümede: kubectl rollout undo. İkisi de önceki blob'u ister. Blob yoksa ikisi de ölür. GC politikası bu yüzden geçişten önce konuşulur, sonra değil. 'Küme kurtarır' cümlesi, silinmiş katmanı geri getirmez.
Secret rotasyonu compose'ta dosya + up. kube'da Secret nesnesi + pod restart. İkisinde de uygulama env'i process start'ta okur. Canlı reload yoksa nesneyi değiştirmek yetmez. Bunu unutup 'kube'da secret döndürdük' demek, eski env'li pod'ları bırakır. Compose'ta da aynı: dosyayı değiştirip up etmeden kutular eski değeri taşır.
Ingress ve TLS, compose'ta genelde bir nginx veya Caddy. kube'da Ingress + cert-manager. İkincisi otomatik yeniler. Birincisi cron veya elle. Sertifika süresi 90 gün, yenilemeyi unutmak her iki dünyada da aynı 02:00 alarmı. Araç, takvimi silmez. Takvim yoksa geçiş gerekçe değildir.
# compose.prod.yml — still one host, still enough
services:
api:
image: api:1.8.0@sha256:7c2d1b0e4a9f2c8b1d6e0a4f3c9b8e7d6c5b4a3928170654433221100ffeedd
restart: unless-stopped
env_file: /etc/api/api.env
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health/ready"]
interval: 10s
timeout: 2s
retries: 6
ports: ["8080:8080"]
logging:
driver: json-file
options: { max-size: "20m", max-file: "5" }
worker:
image: api:1.8.0@sha256:7c2d1b0e4a9f2c8b1d6e0a4f3c9b8e7d6c5b4a3928170654433221100ffeedd
command: ["dotnet", "Worker.dll"]
restart: unless-stopped
env_file: /etc/api/api.env
depends_on:
db:
condition: service_healthy
db:
image: postgres:16.2-bookworm
restart: unless-stopped
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/pg
secrets: [pg]
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 10
secrets:
pg:
file: /etc/api/pg.password
volumes:
pgdata:
Karar defterine üç tarih yazdım. Bir: ikinci zone geldiğinde. İki: yayın günde üçe çıkınca. Üç: runbook'u ikinci kişi tek başına uygulayamayınca. Üçünden biri gelmeden YAML çoğaltmıyorum. Gelince bu yazıyı yeniden okuyup hangi maddenin geldiğine bakacağım. Slayt değil, madde. Madde gelmeden küme kurmak, serinin ilk üç yazısını atlamaktır.
Atlamamak için check list'im compose dosyasının yanında duruyor: digest pinli mi, health var mı, ignore var mı, restart policy health ile evli mi, yayın kaydı digest yazıyor mu. Beşi evetse kube konuşulur. Biri hayırsa konuşma ertelenir. Ertelemek utanç değil, sıra. Sıra bozulunca YAML çoğalır, kutu aynı kalır.
O zamana kadar compose dosyası utanç kaynağı değil. Pinli imaj, health, ignore, digest kaydı. Serinin geri kalanı bunları sıkıcı tutmak için duruyor. Sıkıcı olan şey, kümeden önce bitmeli. Bitmeden geçmek, bozuk kutuyu çoğaltmaktır — bunu tekrar yazıyorum çünkü unutuluyor.
Bu yazı kube karşıtı değil. Karşı olduğum şey, compose dosyasından utanmak. Utanç kötü mimari üretir. Sınır yazmak iyi mimari. Sunucu birken küme kurmadım. Sunucu çoğalıp script dallanınca tekrar ölçeceğim. O ölçek bir rozet değil, bir iş emri.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap