Skip to main content
  1. Posts/

Auto Shutdown Azure Container Apps

·4 mins·
Table of Contents

Dalam banyak skenario, Anda mungkin ingin mengotomatiskan penghentian (auto-stop) Container Apps untuk mengurangi biaya atau mengoptimalkan penggunaan resource, terutama jika aplikasi hanya diperlukan pada waktu-waktu tertentu.

Terdapat dua pendekatan utama untuk melakukan Auto-Stop Container Apps:

  1. Function App Menjalankan kode PowerShell untuk menghentikan Container App berdasarkan jadwal tertentu menggunakan Timer Trigger.

  2. Time-Based Scaling dengan KEDA Mengonfigurasi KEDA agar melakukan scale down ke 0 replika di luar jam operasional.

Function App
#

1. Buat Resource Group
#

az group create \
  --name <resourceGroupName> \
  --location <REGION>

2. Buat Storage Account
#

az storage account create \
  --name <STORAGE_NAME> \
  --resource-group <resourceGroupName> \
  --location <REGION> \
  --sku Standard_RAGRS \
  --kind StorageV2 \
  --min-tls-version TLS1_2 \
  --allow-shared-key-access true

3. Buat User Assigned Managed Identity dan Tambahkan Role
#

Tambahkan role berikut ke identity:

  • Storage Blob Data Owner
  • Contributor
output=$(az identity create \
  --name "demo-app" \
  --resource-group <resourceGroupName> \
  --location <REGION> \
  --query "{userId:id, principalId: principalId, clientId: clientId}" -o json)

userId=$(echo $output | jq -r '.userId')
principalId=$(echo $output | jq -r '.principalId')
clientId=$(echo $output | jq -r '.clientId')

storageId=$(az storage account show \
  --resource-group <resourceGroupName> \
  --name <STORAGE_NAME> \
  --query 'id' -o tsv)

TARGET_RESOURCE_GROUP=$(az group show \
  --name <resourceGroupName> \
  --query 'id' -o tsv)

az role assignment create \
  --assignee-object-id $principalId \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Owner" \
  --scope $storageId

az role assignment create \
  --assignee-object-id $principalId \
  --assignee-principal-type ServicePrincipal \
  --role "Contributor" \
  --scope $TARGET_RESOURCE_GROUP

4. Buat Function App (Consumption Plan)
#

IDENTITY_ID=$(az identity show \
  --name demo-app \
  --resource-group <resourceGroupName> \
  --query 'id' -o tsv)

az functionapp create \
  --resource-group <resourceGroupName> \
  --consumption-plan-location <REGION> \
  --runtime powershell \
  --functions-version 4 \
  --os-type Windows \
  --name <APP_NAME> \
  --storage-account <STORAGE_NAME> \
  --assign-identity $IDENTITY_ID

5. Konfigurasi Environment Variables pada Function App
#

az functionapp config appsettings set \
  --name <APP_NAME> \
  --resource-group <resourceGroupName> \
  --settings \
    CONTAINER_APP_NAME=<TARGET_CONTAINER_APP> \
    RESOURCE_GROUP=<TARGET_RESOURCE_GROUP> \
    AZURE_CLIENT_ID=$(az identity show --name demo-app --resource-group <resourceGroupName> --query clientId -o tsv) \
    SUBSCRIPTION_ID=$(az account show --query id -o tsv) \
    AzureFunctionsJobHost__logging__console__isEnabled=true

6. Inisialisasi Proyek Function
#

Buat folder proyek lalu jalankan func init:

mkdir stop-container-app-function && cd stop-container-app-function
func init --worker-runtime powershell

Buat function dengan template TimerTrigger:

func new --name StopContainerApp --template "TimerTrigger"

7. Update File ./StopContainerApp/run.ps1
#

param($Timer)

$ErrorActionPreference = "Stop"

# 🔍 Load environment variables secara aman
$clientId         = [System.Environment]::GetEnvironmentVariable('AZURE_CLIENT_ID')
$containerAppName = [System.Environment]::GetEnvironmentVariable('CONTAINER_APP_NAME')
$resourceGroup    = [System.Environment]::GetEnvironmentVariable('RESOURCE_GROUP')
$subscriptionId   = [System.Environment]::GetEnvironmentVariable('SUBSCRIPTION_ID')

# ✅ Validasi environment variables
$requiredVars = @{
    AZURE_CLIENT_ID     = $clientId
    CONTAINER_APP_NAME  = $containerAppName
    RESOURCE_GROUP      = $resourceGroup
    SUBSCRIPTION_ID     = $subscriptionId
}

foreach ($key in $requiredVars.Keys) {
    if (-not $requiredVars[$key]) {
        throw "Environment variable '$key' tidak di-set"
    }
}

try {
    Write-Information "🔐 Autentikasi menggunakan Managed Identity..."
    Connect-AzAccount -Identity -AccountId $clientId

    Write-Information "🔎 Mengecek status Container App: $containerAppName"

    $currentState = Get-AzContainerAppRevision -ContainerAppName $containerAppName `
                                       -ResourceGroupName $resourceGroup `
                                       -SubscriptionId $subscriptionId

    if ($null -eq $currentState) {
        throw "Container App '$containerAppName' tidak ditemukan di resource group '$resourceGroup'"
    }

    $runningState = $($currentState.RunningState)
    Write-Information "đŸ“Ļ Status saat ini: $runningState"

    # Debugging output
    # Write-Information "📋 Dumping Get-AzContainerAppRevision output:"
    # $currentState | ConvertTo-Json -Depth 10

    if ($runningState -eq 'Running') {
        Write-Information "🛑 Menghentikan Container App: $containerAppName"
        Stop-AzContainerApp -Name $containerAppName `
                            -ResourceGroupName $resourceGroup `
                            -SubscriptionId $subscriptionId
        Write-Information "✅ Container App berhasil dihentikan"
    } else {
        Write-Information "â„šī¸ Container App sudah dalam keadaan '$runningState'"
    }

    Write-Information "⏰ Function dijalankan pada: $($Timer.ScheduleStatus.Last)"

} catch {
    Write-Error "❌ Terjadi error: $($_.Exception.Message)"
    throw
}

8. Update requirements.psd1
#

@{
    'Az.App'      = '2.0.0'
    'Az.Accounts' = '5.0.0'
}

9. Update Jadwal Timer pada ./StopContainerApp/function.json
#

{
  "bindings": [
    {
      "name": "Timer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 17 * * *"
    }
  ]
}

10. Deploy Function App
#

func azure functionapp publish <APP_NAME>

Time-Based Scaling
#

Konfigurasi KEDA Cron Scaler agar Container App melakukan scale down ke 0 replika di luar jam kerja.

# Scale down setelah jam kerja (17:00–23:59)
az containerapp update \
  --name demo-app \
  --resource-group <NAMA_RESOURCE_GROUP> \
  --scale-rule-name "afterhours" \
  --scale-rule-type "cron" \
  --scale-rule-metadata \
    "timezone=Asia/Jakarta" \
    "start=0 0 17 * * *" \
    "end=59 59 23 * * *" \
    "desiredReplicas=0"

# Scale down pada dini hari (00:00–08:59)
az containerapp update \
  --name demo-app \
  --resource-group <NAMA_RESOURCE_GROUP> \
  --scale-rule-name "earlyhours" \
  --scale-rule-type "cron" \
  --scale-rule-metadata \
    "timezone=Asia/Jakarta" \
    "start=0 0 0 * * *" \
    "end=59 59 8 * * *" \
    "desiredReplicas=0"

Referensi:

Related