Azure PowerShell ile Yönetim Otomasyonu

Bulut ortamlarını elle yönetmek, belirli bir ölçeğin ötesinde gerçekten sürdürülemez hale geliyor. Onlarca sanal makine, yüzlerce kaynak grubu, periyodik yedekleme kontrolleri… Bunları Azure portalından tek tek yapmak hem zaman kaybı hem de insan hatasına açık kapı bırakmak demek. İşte tam bu noktada Azure PowerShell devreye giriyor ve hayatı ciddi ölçüde kolaylaştırıyor.

Bu yazıda Azure PowerShell’i sıfırdan kurup yapılandırmaktan başlayarak gerçek dünya senaryolarında nasıl kullanacağınızı adım adım anlatacağım. Script örnekleri mümkün olduğunca production ortamında işe yarar şeyler olacak, akademik “hello world” örneklerinden uzak duracağız.

Azure PowerShell Nedir ve Neden Kullanmalısınız?

Azure PowerShell, Microsoft’un Azure kaynaklarını yönetmek için geliştirdiği bir modül koleksiyonudur. Temel olarak iki sürümü var: eski AzureRM modülleri ve şu an aktif geliştirilen Az modülleri. Eğer hâlâ AzureRM kullanıyorsanız, o modüller artık bakım almıyor, bir an önce geçiş yapın.

Azure CLI ile karşılaştırıldığında PowerShell’in öne çıktığı noktalar şunlar:

  • Nesne tabanlı çıktı: Her komut string değil, gerçek .NET nesnesi döndürür. Bu sayede pipe ile işlem yapmak çok daha güçlü hale gelir.
  • Windows ekosistemi entegrasyonu: Active Directory, Exchange, Teams gibi Microsoft ürünleriyle aynı script içinde çalışabilirsiniz.
  • Hata yönetimi: Try/Catch blokları, özelleştirilmiş hata mesajları gibi tam bir programlama dili altyapısı.
  • Cross-platform: PowerShell 7+ ile Linux ve macOS’ta da çalışıyor.

Kurulum ve İlk Yapılandırma

Windows’a Kurulum

Windows’ta PowerShell 7+ üzerinde çalışmanızı öneririm. Windows PowerShell 5.1 de destekleniyor ama yeni özellikler için 7’yi tercih edin.

# PowerShell 7 kurulumu (winget ile)
winget install --id Microsoft.PowerShell --source winget

# Az modülünü yükle
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force

# Sadece belirli modülleri de yükleyebilirsiniz
Install-Module -Name Az.Compute -Scope CurrentUser
Install-Module -Name Az.Storage -Scope CurrentUser

Linux’a Kurulum (Ubuntu/Debian)

# PowerShell 7 kurulumu
sudo apt-get update
sudo apt-get install -y wget apt-transport-https software-properties-common

wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb"
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y powershell

# PowerShell başlatın ve Az modülünü yükleyin
pwsh
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force

Authentication Yöntemleri

Günlük kullanımda en çok karşılaşacağınız iki yöntem var:

# İnteraktif login (development için)
Connect-AzAccount

# Service Principal ile login (CI/CD ve otomasyon için)
$credential = New-Object System.Management.Automation.PSCredential(
    $env:AZURE_CLIENT_ID,
    (ConvertTo-SecureString $env:AZURE_CLIENT_SECRET -AsPlainText -Force)
)

Connect-AzAccount -ServicePrincipal `
    -Credential $credential `
    -Tenant $env:AZURE_TENANT_ID

# Managed Identity ile login (Azure VM veya Azure Functions üzerinde çalışıyorsa)
Connect-AzAccount -Identity

Production scriptlerinde kesinlikle Service Principal veya Managed Identity kullanın. Interactive login’i script içine gömmek güvenlik açığı demektir.

Birden fazla subscription yönetiyorsanız aktif subscription’ı şöyle ayarlarsınız:

# Subscription listesi
Get-AzSubscription

# Aktif subscription'ı değiştir
Set-AzContext -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# Ya da subscription adıyla
Set-AzContext -SubscriptionName "Production-Sub"

Temel Kaynak Yönetimi

Resource Group ve VM Yönetimi

Yeni bir environment ayağa kaldırmak için sıklıkla kullandığım bir temel script:

# Değişkenler
$resourceGroup = "prod-webapp-rg"
$location = "westeurope"
$vmName = "prod-web-01"
$vmSize = "Standard_D2s_v3"

# Resource group oluştur
New-AzResourceGroup -Name $resourceGroup -Location $location -Tag @{
    Environment = "Production"
    Owner       = "[email protected]"
    CostCenter  = "IT-OPS"
}

# VM oluştur
$credential = Get-Credential -Message "VM admin şifresini girin"

New-AzVM -ResourceGroupName $resourceGroup `
    -Name $vmName `
    -Location $location `
    -VirtualNetworkName "prod-vnet" `
    -SubnetName "web-subnet" `
    -SecurityGroupName "$vmName-nsg" `
    -PublicIpAddressName "$vmName-pip" `
    -Credential $credential `
    -Size $vmSize `
    -Image "Win2022Datacenter"

Write-Host "VM başarıyla oluşturuldu: $vmName" -ForegroundColor Green

Toplu VM Durumu Kontrolü

Birden fazla subscription’daki tüm VM’lerin durumunu tek seferde görmek için:

function Get-AllVMStatus {
    param(
        [string[]]$SubscriptionIds
    )

    $results = @()

    foreach ($subId in $SubscriptionIds) {
        Set-AzContext -SubscriptionId $subId | Out-Null
        $subName = (Get-AzContext).Subscription.Name

        $vms = Get-AzVM -Status

        foreach ($vm in $vms) {
            $powerState = ($vm.Statuses | Where-Object { $_.Code -like "PowerState/*" }).DisplayStatus

            $results += [PSCustomObject]@{
                Subscription    = $subName
                ResourceGroup   = $vm.ResourceGroupName
                VMName          = $vm.Name
                Size            = $vm.HardwareProfile.VmSize
                PowerState      = $powerState
                Location        = $vm.Location
                OSType          = $vm.StorageProfile.OsDisk.OsType
            }
        }
    }

    return $results
}

# Kullanım
$subscriptions = @(
    "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
)

$vmStatus = Get-AllVMStatus -SubscriptionIds $subscriptions
$vmStatus | Where-Object { $_.PowerState -eq "VM running" } | 
    Sort-Object Subscription, ResourceGroup |
    Format-Table -AutoSize

# Çalışan VM sayısını subscription bazında özetle
$vmStatus | Group-Object Subscription | ForEach-Object {
    Write-Host "`nSubscription: $($_.Name)" -ForegroundColor Cyan
    $_.Group | Group-Object PowerState | ForEach-Object {
        Write-Host "  $($_.Name): $($_.Count) VM"
    }
}

Gerçek Dünya Senaryosu: Maliyet Optimizasyonu Otomasyonu

En sık karşılaştığım problemlerden biri: dev/test ortamlarındaki VM’ler mesai saatleri dışında çalışmaya devam ediyor ve gereksiz maliyet yaratıyor. Bu scripti hafta içi akşam 20:00’de çalışacak şekilde Task Scheduler veya Azure Automation’a ekleyebilirsiniz:

# dev-test-shutdown.ps1
# Bu scripti Azure Automation Runbook olarak kullanabilirsiniz

param(
    [string]$TargetTag = "AutoShutdown",
    [string]$TagValue = "true",
    [bool]$WhatIf = $false
)

# Managed Identity ile bağlan (Azure Automation'da)
Connect-AzAccount -Identity

# Tüm subscription'ları dolaş
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" }

$shutdownCount = 0
$errorCount = 0

foreach ($sub in $subscriptions) {
    Set-AzContext -SubscriptionId $sub.Id | Out-Null

    # Tag'e göre çalışan VM'leri bul
    $vms = Get-AzVM -Status | Where-Object {
        $_.Tags[$TargetTag] -eq $TagValue -and
        ($_.Statuses | Where-Object { $_.Code -eq "PowerState/running" })
    }

    foreach ($vm in $vms) {
        try {
            if ($WhatIf) {
                Write-Output "WhatIf: $($vm.ResourceGroupName)/$($vm.Name) kapatılacaktı"
            } else {
                Stop-AzVM -ResourceGroupName $vm.ResourceGroupName `
                    -Name $vm.Name `
                    -Force `
                    -NoWait
                Write-Output "Kapatıldı: $($vm.ResourceGroupName)/$($vm.Name)"
                $shutdownCount++
            }
        }
        catch {
            Write-Error "Hata - $($vm.Name): $_"
            $errorCount++
        }
    }
}

Write-Output "Tamamlandi. Kapatilan: $shutdownCount, Hata: $errorCount"

Storage Hesabı Yönetimi ve Blob Operasyonları

Storage operasyonları da sık ihtiyaç duyulan alanlardan. Özellikle backup dosyalarını taşımak, eski blob’ları silmek gibi işler için:

# storage-cleanup.ps1
# 90 günden eski backup blob'larını sil

param(
    [string]$ResourceGroupName = "backup-rg",
    [string]$StorageAccountName = "companybackupsa",
    [string]$ContainerName = "vm-backups",
    [int]$RetentionDays = 90,
    [switch]$DryRun
)

# Storage context oluştur
$storageAccount = Get-AzStorageAccount `
    -ResourceGroupName $ResourceGroupName `
    -Name $StorageAccountName

$ctx = $storageAccount.Context

# Tüm blob'ları listele
$allBlobs = Get-AzStorageBlob -Container $ContainerName -Context $ctx
$cutoffDate = (Get-Date).AddDays(-$RetentionDays)

$oldBlobs = $allBlobs | Where-Object { 
    $_.LastModified.DateTime -lt $cutoffDate 
}

$totalSize = ($oldBlobs | Measure-Object -Property Length -Sum).Sum
$totalSizeMB = [math]::Round($totalSize / 1MB, 2)

Write-Host "Silinecek blob sayisi: $($oldBlobs.Count)" -ForegroundColor Yellow
Write-Host "Toplam boyut: $totalSizeMB MB" -ForegroundColor Yellow

if ($DryRun) {
    Write-Host "DRY RUN modu - hicbir sey silinmedi" -ForegroundColor Cyan
    $oldBlobs | Select-Object Name, LastModified, Length | Format-Table
    exit 0
}

$deletedCount = 0
foreach ($blob in $oldBlobs) {
    try {
        Remove-AzStorageBlob -Blob $blob.Name `
            -Container $ContainerName `
            -Context $ctx `
            -Force
        $deletedCount++
        Write-Verbose "Silindi: $($blob.Name)"
    }
    catch {
        Write-Warning "Silinemedi: $($blob.Name) - $($_.Exception.Message)"
    }
}

Write-Host "Tamamlandi. $deletedCount blob silindi, $totalSizeMB MB temizlendi." -ForegroundColor Green

Azure Key Vault ile Secret Yönetimi

Script’lerde şifre ve connection string’leri plaintext tutmak en büyük güvenlik açıklarından biri. Key Vault kullanımını bir alışkanlık haline getirin:

# keyvault-helper.ps1
# Sık kullanılan Key Vault işlemleri için yardımcı fonksiyonlar

function Get-SecretValue {
    param(
        [Parameter(Mandatory)]
        [string]$VaultName,
        [Parameter(Mandatory)]
        [string]$SecretName
    )

    try {
        $secret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName
        return $secret.SecretValue | ConvertFrom-SecureString -AsPlainText
    }
    catch {
        Write-Error "Secret alinirken hata: $VaultName/$SecretName - $_"
        return $null
    }
}

function Set-SecretValue {
    param(
        [Parameter(Mandatory)]
        [string]$VaultName,
        [Parameter(Mandatory)]
        [string]$SecretName,
        [Parameter(Mandatory)]
        [string]$Value,
        [DateTime]$ExpiresOn = (Get-Date).AddYears(1)
    )

    $secureValue = ConvertTo-SecureString $Value -AsPlainText -Force
    
    Set-AzKeyVaultSecret -VaultName $VaultName `
        -Name $SecretName `
        -SecretValue $secureValue `
        -Expires $ExpiresOn
    
    Write-Host "Secret guncellendi: $SecretName (Gecerlilik: $ExpiresOn)" -ForegroundColor Green
}

# Süresi dolmak üzere olan secretları raporla
function Get-ExpiringSecrets {
    param(
        [string]$VaultName,
        [int]$DaysWarning = 30
    )

    $secrets = Get-AzKeyVaultSecret -VaultName $VaultName
    $warningDate = (Get-Date).AddDays($DaysWarning)

    $expiring = $secrets | Where-Object { 
        $_.Expires -ne $null -and $_.Expires -lt $warningDate 
    }

    if ($expiring.Count -eq 0) {
        Write-Host "Surecek: Onumuzdeki $DaysWarning gun icinde dolan secret yok." -ForegroundColor Green
        return
    }

    Write-Host "UYARI: $($expiring.Count) secret yakinda doluyor!" -ForegroundColor Red
    foreach ($s in $expiring) {
        $daysLeft = ($s.Expires - (Get-Date)).Days
        Write-Host "  - $($s.Name): $daysLeft gun kaldi ($(($s.Expires).ToString('yyyy-MM-dd')))" `
            -ForegroundColor $(if ($daysLeft -le 7) { "Red" } else { "Yellow" })
    }
}

# Kullanim ornekleri
# $dbPassword = Get-SecretValue -VaultName "prod-keyvault" -SecretName "db-admin-password"
# Get-ExpiringSecrets -VaultName "prod-keyvault" -DaysWarning 30

Network Güvenlik Grupları Denetimi

NSG kurallarını düzenli olarak denetlemek, güvenlik açısından kritik. Özellikle 0.0.0.0/0 kaynaklı izinleri tespit etmek için:

# nsg-audit.ps1
# Tehlikeli NSG kurallarını raporla

function Get-RiskyNSGRules {
    param(
        [string]$ResourceGroupName = "*"
    )

    if ($ResourceGroupName -eq "*") {
        $nsgs = Get-AzNetworkSecurityGroup
    } else {
        $nsgs = Get-AzNetworkSecurityGroup -ResourceGroupName $ResourceGroupName
    }

    $riskyRules = @()

    foreach ($nsg in $nsgs) {
        $inboundRules = $nsg.SecurityRules | Where-Object {
            $_.Direction -eq "Inbound" -and
            $_.Access -eq "Allow" -and
            ($_.SourceAddressPrefix -eq "*" -or $_.SourceAddressPrefix -eq "0.0.0.0/0" -or
             $_.SourceAddressPrefix -eq "Internet") -and
            $_.DestinationPortRange -in @("22", "3389", "1433", "3306", "5432", "27017", "*")
        }

        foreach ($rule in $inboundRules) {
            $riskyRules += [PSCustomObject]@{
                NSGName         = $nsg.Name
                ResourceGroup   = $nsg.ResourceGroupName
                RuleName        = $rule.Name
                Priority        = $rule.Priority
                Port            = $rule.DestinationPortRange
                Source          = $rule.SourceAddressPrefix
                RiskLevel       = if ($rule.DestinationPortRange -in @("3389","22")) { "KRITIK" } else { "YUKSEK" }
            }
        }
    }

    return $riskyRules
}

$riskyRules = Get-RiskyNSGRules

if ($riskyRules.Count -gt 0) {
    Write-Host "`n=== RISKLI NSG KURALLARI ===" -ForegroundColor Red
    $riskyRules | Sort-Object RiskLevel, NSGName | ForEach-Object {
        $color = if ($_.RiskLevel -eq "KRITIK") { "Red" } else { "Yellow" }
        Write-Host "[$($_.RiskLevel)] $($_.NSGName) - $($_.RuleName) | Port: $($_.Port) | Kaynak: $($_.Source)" -ForegroundColor $color
    }

    # CSV'ye de kaydet
    $riskyRules | Export-Csv -Path "nsg-audit-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation -Encoding UTF8
    Write-Host "`nRapor kaydedildi: nsg-audit-$(Get-Date -Format 'yyyyMMdd').csv"
} else {
    Write-Host "Tehlikeli NSG kurali bulunamadi." -ForegroundColor Green
}

Azure Automation ile Scheduling

Scriptleri elle çalıştırmak yerine Azure Automation Runbook olarak zamanlamak, tam anlamıyla “set and forget” otomasyonu sağlar.

Runbook oluştururken dikkat etmeniz gerekenler:

  • Managed Identity: Automation account’a sistem assigned managed identity verin, Service Principal yerine bu daha güvenli ve yönetimi kolay.
  • Hata yönetimi: Runbook’larda $ErrorActionPreference = "Stop" ile başlayın, sessiz hatalar peşinizi bırakmaz.
  • Output: Write-Output kullanın, Write-Host Runbook log’larında görünmeyebilir.
  • Parametre doğrulama: [Parameter(Mandatory)] kullanarak zorunlu parametreleri belirtin.
# azure-automation-runbook-template.ps1
# Azure Automation Runbook için temel şablon

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [string]$TargetSubscription = "",
    
    [Parameter(Mandatory = $false)]
    [bool]$SendNotification = $true
)

$ErrorActionPreference = "Stop"
$VerbosePreference = "Continue"

try {
    # Managed Identity ile bağlan
    Write-Output "Azure'a baglaniliyor..."
    Connect-AzAccount -Identity | Out-Null
    
    if ($TargetSubscription -ne "") {
        Set-AzContext -SubscriptionId $TargetSubscription | Out-Null
    }
    
    $context = Get-AzContext
    Write-Output "Baglanti basarili. Subscription: $($context.Subscription.Name)"
    
    # Ana islemleri buraya ekleyin
    # ...
    
    Write-Output "Runbook basariyla tamamlandi: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
}
catch {
    Write-Error "Runbook hatasi: $($_.Exception.Message)"
    Write-Error "Stack Trace: $($_.ScriptStackTrace)"
    
    # Hata bildirimini burada yapabilirsiniz
    # Send-MailMessage veya Teams webhook ile
    
    exit 1
}

İpuçları ve En İyi Pratikler

Yıllarca Azure PowerShell kullanan biri olarak şu noktalara dikkat edin:

  • Modülleri güncel tutun: Update-Module -Name Az komutunu düzenli çalıştırın. Eski versiyonlarda bug’lar ve eksik özellikler sizi çıldırtabilir.
  • WhatIf parametresini kullanın: Destructive komutları çalıştırmadan önce -WhatIf ile simüle edin. Silme, durdurma gibi işlemlerde hayat kurtarır.
  • Parallel işlem: PowerShell 7’de ForEach-Object -Parallel ile büyük VM listelerini çok daha hızlı işleyebilirsiniz.
  • Profil dosyası: Sık kullandığınız fonksiyonları $PROFILE dosyasına ekleyin, her session’da yeniden tanımlamaktan kurtulursunuz.
  • Az.Accounts cache: Connect-AzAccount her seferinde token alıyor. Disconnect-AzAccount sonrası tekrar bağlanmak zorunda kalmamak için Enable-AzContextAutosave kullanın.
  • Rate limiting: Büyük ortamlarda binlerce API çağrısı yapıyorsanız Azure’un throttle limitine takılabilirsiniz. Döngülerde Start-Sleep -Milliseconds 100 gibi küçük beklemeler eklemek bu sorunu önler.
  • Log yönetimi: Kritik scriptlerde Start-Transcript ile tüm çıktıyı dosyaya kaydedin, sorun çıktığında neyin ne zaman olduğunu anlayın.

Sonuç

Azure PowerShell, sadece birkaç tıklama işlemini script haline getirmekten çok daha fazlasını sunuyor. Doğru kullanıldığında multi-subscription yönetimi, otomatik uyumluluk kontrolleri, maliyet optimizasyonu ve güvenlik denetimleri gibi karmaşık senaryoları tamamen otomatik hale getirmenizi sağlıyor.

Bu yazıda anlattığım scriptleri doğrudan production’a atmayın tabii. Her ortamın kendine özgü gereksinimleri var. Önce test subscription’ında -WhatIf ve -DryRun modlarıyla çalıştırın, sonuçlara bakın, sonra production’a taşıyın.

Bir sonraki adım olarak Azure Automation, Logic Apps veya GitHub Actions ile bu scriptleri tam bir pipeline’a dönüştürmeyi düşünebilirsiniz. Özellikle maliyet yönetimi ve güvenlik denetimleri için haftalık otomatik raporlar oluşturmak, pek çok küçük problemi büyümeden yakalamanızı sağlıyor.

Sorularınız veya farklı bir senaryo için örnek script isterseniz yorumlarda belirtin.

Bir yanıt yazın

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