Samsun’da 320 çalışanlı bir gıda üreticisi, 24 ay önce “tıklayarak Azure portal’dan kurma” yönteminden Infrastructure as Code’a geçiş başlattı. Bugün 220+ resource Terraform + Bicep ile yönetiliyor, deployment süresi 4 saat → 12 dakika, environment parity (dev/test/prod) sağlandı. Bu yazı projenin sahadaki teknik notları.
Terraform mu Bicep mi?
| Kriter | Terraform | Bicep |
|---|---|---|
| Multi-cloud | Evet (AWS, GCP, Azure) | Sadece Azure |
| Azure özellik desteği (yeni) | 1-2 hafta gecikme | Day 1 |
| State management | Backend (Azure Storage) | Stateless (Azure Resource Manager) |
| Syntax | HCL | DSL (JSON benzeri) |
| Modül ekosistemi | Çok geniş (Terraform Registry) | Daha sınırlı (Bicep registry yeni) |
| Öğrenme eğrisi | Orta | Düşük (Azure native) |
Karar: hibrit yaklaşım. Multi-cloud + 3rd party servisler Terraform; Azure-spesifik karmaşık (App Service, Functions, AKS configürasyonu) Bicep.
Repository Yapısı
infra/
├── terraform/
│ ├── modules/
│ │ ├── azure-rg/
│ │ ├── azure-vnet/
│ │ ├── azure-aks/
│ │ ├── azure-sql/
│ │ └── github-actions-runner/
│ ├── environments/
│ │ ├── dev/
│ │ │ ├── main.tf
│ │ │ ├── terraform.tfvars
│ │ │ └── backend.tf
│ │ ├── test/
│ │ └── prod/
│ └── README.md
├── bicep/
│ ├── modules/
│ │ ├── app-service.bicep
│ │ ├── function-app.bicep
│ │ └── private-endpoint.bicep
│ ├── main.dev.bicepparam
│ ├── main.test.bicepparam
│ ├── main.prod.bicepparam
│ └── main.bicep
└── .github/workflows/
├── tf-plan.yml
├── tf-apply.yml
├── bicep-validate.yml
└── bicep-deploy.yml
Terraform Backend (Remote State)
terraform {
backend "azurerm" {
resource_group_name = "rg-tfstate"
storage_account_name = "tfstateprod"
container_name = "tfstate"
key = "prod.tfstate"
use_oidc = true # GitHub Actions OIDC
}
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 3.110" }
}
required_version = ">= 1.7"
}
provider "azurerm" {
features {}
use_oidc = true
}
State Azure Storage’da, locking otomatik (blob lease). 3 environment için 3 ayrı state.
Modül Örneği: AKS
module "aks" {
source = "../../modules/azure-aks"
name = "aks-prod"
resource_group_name = module.rg.name
location = "northeurope"
system_node_count = 3
system_node_size = "Standard_D4s_v5"
user_node_pools = {
apps = {
vm_size = "Standard_D8s_v5"
min_count = 3
max_count = 12
mode = "User"
}
spot = {
vm_size = "Standard_D8s_v5"
min_count = 0
max_count = 8
mode = "User"
spot = true
}
}
network_plugin = "azure"
network_policy = "calico"
private_cluster = true
identity = {
type = "SystemAssigned"
}
tags = local.common_tags
}
Bicep: App Service Detayları
param appName string
param location string
param skuName string = 'P1v3'
param vnetSubnetId string
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: 'asp-${appName}'
location: location
sku: { name: skuName, tier: 'PremiumV3' }
properties: { reserved: true }
}
resource webApp 'Microsoft.Web/sites@2023-12-01' = {
name: appName
location: location
identity: { type: 'SystemAssigned' }
properties: {
serverFarmId: appServicePlan.id
virtualNetworkSubnetId: vnetSubnetId
httpsOnly: true
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
minTlsVersion: '1.2'
ftpsState: 'Disabled'
vnetRouteAllEnabled: true
ipSecurityRestrictionsDefaultAction: 'Deny'
}
}
}
output appServiceUrl string = 'https://${webApp.properties.defaultHostName}'
CI/CD: Plan/Apply Discipline
name: tf-plan
on:
pull_request:
paths: ['infra/terraform/**']
jobs:
plan:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: terraform init
run: terraform init
working-directory: infra/terraform/environments/prod
- name: terraform plan
run: terraform plan -out=tfplan -no-color
working-directory: infra/terraform/environments/prod
- uses: actions/upload-artifact@v4
with:
name: tfplan
path: infra/terraform/environments/prod/tfplan
PR açıldığında plan otomatik, output PR yorumuna düşüyor. Reviewer plan’ı görüp onaylıyor. Merge sonrası apply workflow tetikleniyor.
Drift Detection
Manuel portal’dan değişiklik (acil müdahale, hızlı düzeltme) drift yaratır. Haftalık terraform plan cron job, drift varsa Slack notify.
on:
schedule:
- cron: '0 6 * * MON'
jobs:
drift-check:
runs-on: ubuntu-latest
steps:
- run: |
terraform plan -detailed-exitcode -no-color > plan.out
if [ $? -eq 2 ]; then
gh issue create --title "IaC Drift Detected" --body "$(cat plan.out)"
fi
Sonuçlar (8 Ay)
| Metrik | Önce | Sonra |
|---|---|---|
| Yeni environment kurulum süresi | ~~4 saat (manuel) | ~~12 dk (one-click) |
| Dev/test/prod parity | %~~60 (drift’li) | %~~98 |
| Deployment hata oranı | ~~%18 | ~~%2 |
| Audit trail (kim ne değiştirdi) | Yok | Tam (Git history) |
| Rollback süresi | 2-4 saat | ~~8 dk |
Sahada Düşülen Üç Tuzak
- State’i Git’e commit etmek: Secret leak, lock yok. Remote backend (Azure Storage) şart.
- Manuel portal değişikliklerini engellemek yerine ignore etmek: Drift birikir, IaC anlamsızlaşır. Drift detection + sıkı governance şart.
- Module versioning yapmamak: Module değişince tüm environment etkileniyor. Semantic versioning (v1.2.0) ve pinned reference şart.
CloudSpark olarak Terraform + Bicep IaC programı kurulumu, modül kütüphanesi tasarımı, CI/CD entegrasyon ve drift detection projeleri için danışmanlık veriyoruz.



