Infrastructure as Code Nedir? Terraform, OpenTofu, Bicep, Pulumi ve Ne Zaman Hangisi
Infrastructure as Code, altyapıyı kodla tarif edip bir aracın onu istenen duruma getirmesidir; aynı kodu ikinci kez çalıştırmak hiçbir şeyi değiştirmez, elle yapılan değişiklik de bir sonraki plan'da drift olarak görünür. Bir bash script'iyle Terraform'u aynı işte karşılaştırıyor, kod-plan-apply-state döngüsünü görüyor, Terraform, OpenTofu, Bicep, Pulumi ve Ansible'ı konumlandırıyor ve BSL, OpenTofu ve CDKTF'nin emekliliğinden sonra ekip için seçim kriterlerini çıkarıyoruz.
Sürüm notu: Bu yazıdaki örnekler Terraform 1.17.2, OpenTofu 1.13.9, kreuzwerker/docker provider 4.7.0 ve Docker Engine 29 (Docker Desktop) ile denenmiştir.
Önce Tahmin Et
Elinde iki şey var. Birincisi, bir Docker network'ü ve bir API container'ı açan beş satırlık bir bash script'i. İkincisi, aynı iki şeyi tarif eden yirmi satırlık bir Terraform dosyası. İkisini de art arda iki kez çalıştırıyorsun. Script ikinci çalışmada ne yapar? Terraform ne yapar? Bir de üçüncü soru: biri container'ı Docker Desktop'tan elle silerse, ikisinden hangisi bunu fark eder? Cevaplar yazının ortasında, gerçek çıktılarla.
Portal'da Tıklayarak Kurmanın Bedeli
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) bir gün Azure'a taşınıyor. Emre portal'a giriyor, bir resource group açıyor, içine bir App Service Plan, bir Web App, bir Azure SQL Server ve BookstoreDb veritabanını, kapak resimleri için de bir Storage Account kuruyor. Her şey iki saatte ayağa kalkıyor. Altı ay sonra staging ortamı lazım olduğunda Emre izinde. Ayşe aynı kurulumu yapmaya çalışıyor ve şu sorulara cevap bulamıyor: Web App'te hangi app setting'ler vardı? SQL Server'ın firewall'unda hangi IP aralıkları açıktı? Storage'da minimum TLS (trafiği şifreleyen protokol) sürümü neydi? Bunları portal'da ekran ekran gezip bulabilir, ama bulduğu şey bugünkü durumdur; kimin, ne zaman, neden değiştirdiğini göremez.
Portal'da tıklayarak altyapı kurmaya sektörde "click-ops" deniyor ve sorunları hep aynı yerde çıkıyor:
- Tekrar edilemezlik: aynı ortamı ikinci kez kurmak, ilkini kuran kişinin hafızasına bağlı. Staging ile production birbirinden habersizce ayrışır.
- Drift: biri production'da bir ayarı "geçici olarak" değiştirir, geri almayı unutur. Kayıtlı bir hedef durum olmadığı için bu farkı fark edecek bir mekanizma da yoktur.
- Review'suz değişiklik: uygulama kodundaki tek satırlık değişiklik pull request'ten ve CI'dan geçerken, veritabanının firewall kuralını kimse görmeden açmak mümkündür.
- Felaket anında hız: bir region çöktüğünde ya da subscription yanlışlıkla silindiğinde, sıfırdan kurulum günler sürer.
Infrastructure as Code, kısaca IaC (altyapıyı elle kurmak yerine kodla tanımlayıp araçla kurdurmak), bu sorunlara verilen cevap. Altyapı metin dosyalarında tanımlanır, Git'te durur, pull request ile review edilir ve bir araç bu tanımı gerçek dünyaya uygular. Ayşe'nin aradığı bütün cevaplar bir dosyanın içindedir ve git log kimin neyi neden değiştirdiğini söyler.
Imperative ve Declarative: Aynı İşi İki Kez Çalıştırmak
Altyapıyı kodla kurmanın iki yolu var. Imperative yaklaşımda "ne yapılacağını" adım adım yazarsın; script'ler böyledir. Declarative yaklaşımda "sonunda ne olmasını istediğini" yazarsın, oraya nasıl gidileceğine araç karar verir. Farkı görmek için yerel bir deneme yapalım. Bookstore API'sinin küçük bir sürümünü önce image olarak build ediyoruz (Docker serisinde anlatılan standart multi-stage Dockerfile; seri boyunca snapshot'taki app/Bookstore.Api klasöründe duruyor):
docker build -t bookstore-api:1.0.0 app/Bookstore.Api
Sonra imperative script:
#!/usr/bin/env bash
set -euo pipefail
docker network create tfseries-dev
docker run -d --name tfseries-dev-api --network tfseries-dev \
-p 18480:8080 -e Bookstore__Environment=dev bookstore-api:1.0.0
Script'i iki kez çalıştırınca:
$ ./setup.sh
992f5138d615ef7d1ce0289a7f9f4b0c5b3ca043c80a1cc73ffdafca009d67ba
0bab9cd89b64080a64557362a52fbd526cba2e23b6073ce32c5e15debaac7dc9
$ ./setup.sh
Error response from daemon: network with name tfseries-dev already exists
İlk çalışma network'ü ve container'ı açtı. İkincisi ilk satırda düştü, çünkü script "network oluştur" diyor; "network yoksa oluştur" demiyor. Bunu düzeltmek için her adımın önüne "zaten var mı, varsa ayarları aynı mı" kontrolü eklemen gerekir. Container'ın port'u ya da environment variable'ı değiştiyse onu durdurup yeniden açan kodu da sen yazarsın. Script büyüdükçe asıl iş bu kontrollere döner.
Aynı hedefin Terraform'daki karşılığı:
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.7"
}
}
}
provider "docker" {}
resource "docker_network" "bookstore" {
name = "tfseries-dev"
}
resource "docker_container" "api" {
name = "tfseries-dev-api"
image = "bookstore-api:1.0.0"
env = ["Bookstore__Environment=dev"]
networks_advanced {
name = docker_network.bookstore.name
}
ports {
internal = 8080
external = 18480
}
}
Bu dosya HCL (HashiCorp Configuration Language; Terraform için geliştirilmiş configuration dili) ile yazılmış. Hiçbir satırı bir komut değil; her resource block'u "dünyada böyle bir şey olsun" diyor. terraform init provider'ı indirdikten sonra terraform apply'ı iki kez çalıştırıyoruz. Docker'daki nesnelerin adı, bu serinin deneme ortamı olduğu belli olsun diye tfseries- ile başlıyor. İlk çalışmanın sonu:
docker_network.bookstore: Creating...
docker_network.bookstore: Creation complete after 2s [id=2c7682d8293a661c453ea92809589c7d60564fec3cde46232b1b0ce8c99d84d5]
docker_container.api: Creating...
docker_container.api: Creation complete after 0s [id=c91b708673da334de0189f09efdc4f5109e97cf4d069d55be069a2b2fc0cfa82]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
İkinci çalışma:
docker_network.bookstore: Refreshing state... [id=2c7682d8293a661c453ea92809589c7d60564fec3cde46232b1b0ce8c99d84d5]
docker_container.api: Refreshing state... [id=c91b708673da334de0189f09efdc4f5109e97cf4d069d55be069a2b2fc0cfa82]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
İlk sorunun cevabı bu. Terraform ikinci çalışmada hiçbir şey yapmadı, çünkü istenen durum ile gerçek durum aynıydı. Bu özelliğe idempotency (aynı işlemi kaç kez çalıştırırsan çalıştır sonucun değişmemesi) deniyor ve IaC'nin temel vaadi bu: kodu her çalıştırdığında altyapı, kodda tarif edilen desired state'e (istenen duruma) getirilir. Zaten oradaysa dokunulmaz.
Bir de ters durumu deneyelim. Script'in açtığı network ve container dururken Terraform'u ilk kez çalıştırırsan:
Error: Unable to create network: Error response from daemon: network with name tfseries-dev already exists
with docker_network.bookstore,
on main.tf line 12, in resource "docker_network" "bookstore":
12: resource "docker_network" "bookstore" {
Terraform, kendi kurmadığı bir nesneyi kendiliğinden sahiplenmez. Portal'da elle kurulmuş bir altyapıyı Terraform'a almanın ayrı bir yolu var (import block'ları); 8. yazının konusu bu.
Kod, Plan, Apply ve State Döngüsü
Terraform'un ikinci çalışmada "değişiklik yok" diyebilmesi için neyi kendisinin kurduğunu hatırlaması gerekiyor. Bu hafıza state dosyası. Apply'dan sonra klasöre bakınca terraform.tfstate dosyasını görürsün ve içinde neler olduğunu şöyle listelersin:
$ terraform state list
docker_container.api
docker_network.bookstore
Her terraform plan ve apply aynı döngüyü izler:

Terraform önce .tf dosyalarını okur ve istenen durumu çıkarır. Sonra state'teki her kaynak için provider'a "bu nesne şu an nasıl" diye sorar; buna refresh denir. Provider, Docker'ın ya da Azure Resource Manager'ın API'sini çağıran ayrı bir programdır. Terraform istenen durumla gerçek durumu karşılaştırıp bir plan çıkarır: ne oluşturulacak, ne değişecek, ne silinecek. Apply bu planı provider üzerinden uygular ve sonucu state'e yazar.
Üçüncü sorunun cevabı da bu döngüde. Container'ı Docker'dan elle siliyoruz (docker rm -f tfseries-dev-api) ve sadece plan alıyoruz:
docker_network.bookstore: Refreshing state... [id=2c7682d8293a661c453ea92809589c7d60564fec3cde46232b1b0ce8c99d84d5]
docker_container.api: Refreshing state... [id=c91b708673da334de0189f09efdc4f5109e97cf4d069d55be069a2b2fc0cfa82]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# docker_container.api will be created
+ resource "docker_container" "api" {
+ env = [
+ "Bookstore__Environment=dev",
]
+ image = "bookstore-api:1.0.0"
+ name = "tfseries-dev-api"
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Refresh sırasında provider container'ı bulamadı, Terraform da onu yeniden oluşturmayı önerdi. Kodla gerçek dünya arasındaki bu farka drift deniyor. Script bunu hiçbir zaman fark etmez; Terraform her plan'da fark eder. Plan çıktısındaki ... satırı benim kısaltmam: provider'ın varsayılan değerleriyle dolu otuz kadar satırı çıkardım. Gerçek plan'ı okumayı 2. yazıda ayrıntılı göreceğiz.
Araç Haritası: Terraform, OpenTofu, Bicep, Pulumi
IaC araçları aynı soruya farklı cevaplar veriyor. Bookstore ekibinin karşılaşacağı adaylar aşağıdaki tabloda. Lisans sütunundaki MIT (en serbest açık kaynak lisanslardan biri), Apache 2.0, MPL (Mozilla Public License; değiştirilen dosyaların açık kalmasını isteyen lisans) ve GPL (GNU General Public License; türetilen yazılımın da aynı lisansla açılmasını şart koşan lisans) açık kaynak lisanslar. BSL (Business Source License; kaynak kodu açık ama rakip ticari ürünlerde kullanımı kısıtlayan lisans) ise birazdan anlatacağım lisans değişikliğinin sonucu. Ansible satırındaki YAML (girintiye dayalı, insan tarafından okunabilir veri formatı) playbook'lar Ansible'ın görev dosyaları.
| Araç | Dil | Hedef | State | Lisans |
|---|---|---|---|---|
| Terraform | HCL | Provider'ı olan her platform | State dosyası, backend'de | BSL 1.1 (1.6'dan beri) |
| OpenTofu | HCL | Aynı provider ekosistemi | State dosyası, istemci tarafı şifreleme seçeneği | MPL 2.0 |
| Bicep | Bicep | Yalnız Azure | Ayrı state yok, Azure'un kendisi | MIT |
| Pulumi | C#, TypeScript, Python, Go, Java | Çok cloud | State, Pulumi Cloud'da ya da kendi storage'ında | Apache 2.0 |
| Ansible | YAML playbook | Sunucunun içi | Yok | GPL 3.0 |
Terraform bu alanın en yaygın aracı. Gücü provider ekosisteminden geliyor: Azure, AWS (Amazon Web Services; Amazon'un sunduğu cloud platformu) ve Google Cloud'un yanında GitHub repo'ları, DNS kayıtları, Kubernetes nesneleri ve buradaki gibi Docker container'ları aynı dille ve aynı plan/apply akışıyla yönetilir.
Bicep, Microsoft'un Azure'a özel dili. Bicep dosyası ARM (Azure Resource Manager; Azure'da her kaynağı oluşturan ve yöneten katman) template'ine derlenir ve deploy'u Azure'un kendisi yapar. Ayrı bir state dosyası yoktur, plan'ın karşılığı az deployment group what-if komutudur. Sadece Azure kullanan ve öyle kalacak bir ekip için en az hareketli parçalı seçenek; ama GitHub ayarlarını ya da başka bir cloud'u yönetemez.
Pulumi aynı declarative modeli genel amaçlı programlama dilleriyle kurar. Bookstore ekibi altyapıyı C# ile yazabilir, döngüleri ve abstraction'ları dilin kendisiyle kurar ve unit test'leri xUnit ile yazar. Bedeli, altyapı kodunun da uygulama kodu kadar karmaşıklaşabilmesi. Terraform tarafında bu boşluğu dolduran CDKTF (Cloud Development Kit for Terraform; Terraform'u TypeScript, C# gibi dillerle yazmayı sağlayan araç) ise 10 Aralık 2025'te deprecate edildi ve repository arşivlendi. README, kullanıcılara cdktf synth --hcl ile HCL üretip standart Terraform'a geçmeyi öneriyor. Bugün CDKTF ile yeni proje başlatmak için bir sebep yok.
Ansible ise başka bir katmanın aracı. Terraform sunucuyu, network'ü, veritabanını oluşturur; Ansible var olan bir sunucunun içine paket kurar, dosya kopyalar, servis ayarlar. Bu işe configuration management deniyor. Container ve PaaS kullanan Bookstore gibi bir uygulamada sunucunun içini yönetme ihtiyacı büyük ölçüde ortadan kalkıyor: image'ı Docker build eder, çalıştıran platformu Terraform kurar. Ansible bu serinin kapsamında değil.
Lisans ve Ekosistem: BSL, OpenTofu ve IBM
Ağustos 2023'te HashiCorp, Terraform'un lisansını MPL 2.0'dan BSL 1.1'e geçirdi. Değişiklik 1.6 sürümüyle başladı; 1.5.x serisi MPL ile yayınlanan son seri oldu. BSL, Terraform'u kendi altyapın için kullanmanı engellemiyor; HashiCorp'la rekabet eden ticari bir ürünün içine gömmeyi kısıtlıyor. Bookstore ekibi gibi bir kullanıcı için pratik bir fark yok.
Lisans değişikliğine tepki olarak topluluk, son MPL kodundan OpenTofu'yu çatalladı. OpenTofu Eylül 2023'te Linux Foundation projesi oldu, Nisan 2025'te de CNCF'e (Cloud Native Computing Foundation; Kubernetes'in de bağlı olduğu vakıf) sandbox projesi olarak kabul edildi. HashiCorp tarafında ise IBM, şirketi satın alma işlemini 27 Şubat 2025'te tamamladı. Terraform artık bir IBM ürünü.
İki araç başlangıçta aynıydı, artık her sürümde biraz daha ayrışıyorlar. OpenTofu 1.7'den beri state dosyasını istemci tarafında şifreleyebiliyor; Terraform'da böyle bir özellik yok. Terraform ise 1.14 ile gelen terraform query ve 1.15 ile gelen module source'ta variable kullanımı gibi özellikleri kendi takviminde ekliyor. Bu serideki kodu OpenTofu ile de denedim. Yukarıdaki main.tf'i ayrı bir klasöre kopyalayıp tofu init dediğimde provider OpenTofu'nun kendi registry'sinden geldi:
Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 4.7"...
- Installing kreuzwerker/docker v4.7.0 to the shared cache directory...
- Installed kreuzwerker/docker v4.7.0 (signed, key ID 0DCE698927DAF8EC)
- Using kreuzwerker/docker v4.7.0 from the shared cache directory
tofu validate "Success! The configuration is valid." dedi, tofu plan da Plan: 2 to add, 0 to change, 0 to destroy. sonucunu verdi. İki kaynak eklemek istemesinin sebebi, yeni klasörün kendi boş state'iyle başlaması; state klasöre aittir. Basit kod iki araçta da çalışıyor. Sürümle gelen yeni özellikleri kullandıkça bu uyum azalır, ayrıntısı 12. yazıda.
Ekip İçin Seçim: Hangi Durumda Hangisi
Bookstore ekibi için soruyu şöyle sordum: altyapı yalnız Azure'da mı kalacak, yoksa GitHub, DNS ya da ikinci bir cloud da kodla yönetilecek mi? Ekip HCL öğrenmeye mi, altyapıyı C# ile yazmaya mı daha yakın? Lisans ve tedarikçi riski şirket için bir karar kriteri mi?
- Yalnız Azure, küçük ekip, ek araç istemiyorsan: Bicep.
- Birden fazla platform, geniş topluluk, hazır module ve örnek bolluğu istiyorsan: Terraform.
- Aynı ekosistemi açık kaynak lisans ve vakıf yönetimi altında istiyorsan ya da state şifreleme şartsa: OpenTofu.
- Altyapıyı uygulama diliyle yazmak ve test etmek ekip için gerçek bir kazanç sağlıyorsa: Pulumi.
Bu seri Terraform'u anlatıyor. Bookstore'un hedef altyapısı Azure'da: resource group, Linux App Service Plan ve container çalıştıran Web App, Azure SQL ve BookstoreDb, Storage ve Key Vault. Seçim kadar seçimin gerekçesini yazmak da önemli; bunun yolu bir ADR (Architecture Decision Record; mimari bir kararı bağlamı, seçenekleri ve sonuçlarıyla kaydeden kısa doküman). Architecture Decision'lar serisinin "ADR Yazmak" yazısındaki şablonla iki paragraflık bir kayıt yeterli: hangi seçenekleri tarttık, neden Terraform, hangi koşul değişirse kararı yeniden açarız.
Bu Seride Örnekler Nasıl Çalışıyor
Her yazıda iki tür kod göreceksin. infra/azure klasöründeki kod Bookstore'un gerçek hedefi; AzureRM provider 5.x ile yazılıyor. Bu kodu yazarken bir Azure hesabı kullanmadım: init, fmt ve validate gerçekten çalıştı, ama Azure'a plan ve apply yapılmadı. Azure'a özgü davranışları dokümandan aktarıyorum ve bunu her seferinde açıkça söylüyorum. infra/local klasöründeki kod ise aynı kavramları Docker provider ile senin bilgisayarında çalıştıran yerel bir eş; yazıdaki plan, apply ve hata çıktılarının hepsi oradan, gerçek koşudan. Terraform'un mekaniği (state, plan, dependency graph, lifecycle, module) provider'dan bağımsız, bu yüzden Docker'da öğrendiğin her şey Azure'da aynen geçerli.
Üretimde Dikkat
IaC'ye geçmek "her şeyi bir kerede koda dökmek" demek değil. Önce yeni kurulan ortamları kodla kur, var olanları adım adım import et. Production'da elle değişiklik yapma yetkisini daralt; portal'da herkesin Contributor rolü olduğu bir subscription'da drift kaçınılmazdır.
Plan'ı review sürecinin parçası yap. Kodun diff'i ne değişeceğini her zaman tam söylemez; bir variable'ın değişmesi yirmi kaynağı etkileyebilir. Pull request'te kodun yanında plan çıktısını da görmek, 11. yazıda kuracağımız pipeline'ın ana amacı.
State dosyası artık altyapının bir parçası. İçinde kaynak ID'leri, bağlantı bilgileri ve bazen düz metin secret'lar durur. Bu yüzden laptop'ta değil, erişimi kısıtlanmış bir remote backend'de tutulur; 4. yazının konusu.
Sık Düşülen Tuzaklar
IaC'yi bir kere çalıştırılıp unutulan bir script sanmak. Terraform kodu ilk kurulumda değil, sonraki yıllarda değer üretir: her değişiklik koddan geçtiği sürece drift görünür olur. Kurulumdan sonra portal'a dönüp ayar değiştiren bir ekip, kodu birkaç ay içinde gerçekle uyumsuz bir belgeye çevirir.
Aynı altyapıyı hem portal'dan hem koddan yönetmek. Terraform'un yönettiği bir kaynakta portal'dan yapılan değişikliği bir sonraki apply sessizce geri alır. Bu doğru davranış, ama habersiz yakalanan ekip için sürpriz olur.
Aracı lisans tartışmasına göre değil ihtiyaca göre seçmemek. Ekibin büyük kısmı yalnız Azure'la çalışıyorsa Bicep'i hiç değerlendirmeden Terraform'a geçmek, ya da tersine çok platformlu bir ihtiyacı Bicep'e sığdırmaya çalışmak, ileride pahalıya patlar.
Küçük Bir Deneme
★☆☆ Yazıdaki main.tf'i bir klasöre koy, terraform init ve terraform apply çalıştır, sonra http://localhost:18480/health adresine git. Apply'ı ikinci kez çalıştırıp "No changes." çıktısını kendin gör.
★★☆ Container'ı docker stop tfseries-dev-api ile durdur ve terraform plan al. Plan container'ı yerinde mi değiştirmek istiyor, yeniden mi oluşturmak? Hangi satırların yanında forces replacement yazıyor ve neden?
★★★ Kendi ekibin için yarım sayfalık bir ADR yaz: Terraform, OpenTofu ve Bicep arasında hangisini, hangi gerekçeyle seçerdin ve hangi koşul değişirse kararı yeniden açardın?
Bir sonraki yazıda Terraform'u Windows'a kuruyor, required_version, provider sürüm kısıtları ve .terraform.lock.hcl ile çalışıyor, ardından Bookstore'un ilk Azure kaynaklarını (resource group ve Storage Account) yazıyoruz. Plan çıktısındaki +, ~ ve -/+ işaretlerini de orada tek tek okuyacağız.