Bilko Azure IaC — Terraform azurerm (rg-bilko-demo)
Bilko Azure IaC — Terraform azurerm (rg-bilko-demo)
MC: #103720 (child of #103715)
Status: Converged (terraform plan = no changes)
Branch: feat/103715-azure-terraform-iac → PR #380
Date: 2026-06-16
Overview
Context: GCP project billing exhausted → Bilko migrated to Azure. Azure side had ZERO IaC (14 resources hand-created via az CLI). This created drift risk, manual errors, and no audit trail.
Solution: Entire rg-bilko-demo resource group (~23 resources) now under Terraform azurerm provider. Infrastructure is declarative, version-controlled, and reproducible.
Current state: terraform plan returns "No changes. Your infrastructure matches the configuration." (fully converged).
Architecture
Azure Topology
- Subscription: 5b0b4d9b-e677-464e-abf0-5170cbce3b8e
- Resource Group: rg-bilko-demo
- Region: swedencentral
Resource Inventory
| Resource | Name | Type/SKU | Notes |
|---|---|---|---|
| Resource Group | rg-bilko-demo | - | Parent container |
| ACA Environment | bilko-demo-env | Consumption | Shared environment for all container apps |
| Container Registry | bilkodemo | ACR | Private image registry |
| PostgreSQL | bilko-demo-pg | Flexible, B_Standard_B1ms, PG16, zone 1 | Main database |
| Key Vault | kv-bilko-demo2 | - | 2 access policies: managed_identity + terraform_user |
| Managed Identity | mi-bilko-demo | - | For ACA → Key Vault |
| App Insights | appi-bilko | - | + action group appi-bilko-alerts |
| Availability Alert | - | - | → [email protected] |
| 5xx Metric Alert | - | - | → [email protected] |
| Container App | bilko-api-demo | ACA | Adopted (ignore_changes) |
| Container App | bilko-web-demo | ACA | Adopted (ignore_changes) |
| Container App | bilko-unleash | ACA | Adopted (public Docker Hub image) |
| Container App | bilko-api-stage | ACA | Adopted (ignore_changes) |
| Container App | bilko-web-stage | ACA | Adopted (ignore_changes) |
| Firewall Rule | - | Postgres | FORGE runner 10.0.0.2/32 |
Module Map
Repo: infrastructure/azure/terraform/
graph LR
ENV[envs/demo] --> RG[module: resource-group]
ENV --> LA[module: log-analytics]
ENV --> ACR[module: acr]
ENV --> MI[module: managed-identity]
ENV --> KV[module: keyvault]
ENV --> PG[module: postgres]
ENV --> ACAENV[module: aca-environment]
ENV --> ACA1[module: aca-app bilko-api-demo]
ENV --> ACA2[module: aca-app bilko-web-demo]
ENV --> ACA3[module: aca-app bilko-unleash]
ENV --> ACA4[module: aca-app bilko-api-stage]
ENV --> ACA5[module: aca-app bilko-web-stage]
ENV --> AI[module: app-insights]
ACAENV --> LA
ACA1 --> ACAENV
ACA2 --> ACAENV
ACA3 --> ACAENV
ACA4 --> ACAENV
ACA5 --> ACAENV
9 modules:
- resource-group
- log-analytics
- acr
- managed-identity
- keyvault
- postgres
- aca-environment
- aca-app (reusable, 5 instances)
- app-insights
State Backend
Provider: azurerm (NOT GCS — gcloud out of the loop)
Backend config:
backend "azurerm" {
storage_account_name = "stbilkotfstate"
container_name = "tfstate"
key = "demo.terraform.tfstate"
}
Ops Access
To run terraform plan/apply manually:
export ARM_ACCESS_KEY=$(az storage account keys list -g rg-bilko-demo -n stbilkotfstate --query "[0].value" -o tsv)
cd infrastructure/azure/terraform/envs/demo
terraform plan
CI/CD
Workflow: azure-infra.yml
New workflow (added in this PR):
- Trigger: PR with paths
infrastructure/azure/**→ runsterraform plan - Apply: ONLY via manual
workflow_dispatch+confirm="APPLY"input (never auto-apply — ZAKON PI2, live customer demo) - Runner: self-hosted FORGE
- Auth: AZURE_CREDENTIALS SP (alai-cli-deployer f2a3b94b, Contributor role)
- Backend auth: ARM_ACCESS_KEY from storage account key
Boundary: Infra vs. App Rollout
CRITICAL: Infrastructure = Terraform; APP ROLLOUT stays imperative.
| Concern | Tool | Location |
|---|---|---|
| Resource creation/config | Terraform | azure-infra.yml |
| App image rollout | az containerapp update --image | azure-stage.yml / azure-deploy.yml |
Do NOT move rollout to Terraform. The aca-app module uses lifecycle { ignore_changes } on container image to preserve imperative rollout.
Adopt-vs-Managed Pattern
The aca-app module has TWO modes:
1. Managed (greenfield)
Full Terraform control of env vars, secrets, image, traffic weight.
ignore_env_secrets = false
2. Adopted (existing apps)
Terraform imports existing resource but ignores runtime config (env/secrets/image/revision_mode/custom_domain/traffic_weight). Used for the 5 hand-built apps adopted as-is.
ignore_env_secrets = true
lifecycle {
ignore_changes = [
template[0].container[0].image,
template[0].container[0].env,
secret,
ingress[0].custom_domain,
ingress[0].traffic_weight,
template[0].revision_suffix
]
}
All 5 current apps use adopted mode:
- bilko-api-demo
- bilko-web-demo
- bilko-unleash
- bilko-api-stage
- bilko-web-stage
Gotchas & Lessons
1. Adopted ACA updates trigger NEW REVISION
Issue: Even with ignore_changes, any Terraform change to an adopted container_app triggers a new revision (graceful zero-downtime rolling restart) — NOT a silent no-op.
Mitigation: Minimize unnecessary Terraform changes to adopted apps. Review plan carefully before apply.
2. ACA environment force-replacement bug
Issue: azurerm 3.x tries to force-replace ACA environment on unchanged log_analytics_workspace_id.
Fix: Added ignore_changes = [log_analytics_workspace_id] to aca-environment module.
3. Postgres zone must be pinned
Issue: Azure blocks zone changes on existing Postgres Flexible servers.
Fix: Hardcode zone = "1" + ignore_changes = [zone].
4. Public-image apps (unleash) must NOT get ACR registry block
Issue: Unleash pulls from Docker Hub, not ACR. If module tries to set ACR registry, plan fails.
Fix: Dynamic registry block gated on registry_username != null:
dynamic "registry" {
for_each = var.registry_username != null ? [1] : []
content { ... }
}
5. workload_profile_name drift
Issue: Imported apps have workload_profile_name = "Consumption". If not set in Terraform, drifts to null.
Fix: Explicitly set workload_profile_name = "Consumption" for adopted apps.
6. NEVER commit .terraform/ or local tfstate
Issue: .terraform/ contains 273MB provider binary. Local tfstate can leak secrets.
Fix: Added to .gitignore.
Runbook: Safe Plan/Apply
Local Development
# 1. Authenticate
az login
export ARM_ACCESS_KEY=$(az storage account keys list -g rg-bilko-demo -n stbilkotfstate --query "[0].value" -o tsv)
# 2. Navigate
cd infrastructure/azure/terraform/envs/demo
# 3. Plan
terraform init # first time only
terraform plan
# 4. Apply (if safe)
terraform apply
# 5. Verify
az containerapp list -g rg-bilko-demo --query "[].{name:name, status:properties.provisioningState}" -o table
CI Apply (Production)
- Open PR with infrastructure changes
- Review
terraform planoutput in PR checks - Merge PR to main
- Go to Actions → azure-infra.yml → Run workflow
- Set
confirminput toAPPLY - Monitor run
- Verify resources in Azure Portal
ZAKON PI2: Never auto-apply. Always manual approval for live customer demo environment.
Open Follow-ups
| MC | Priority | Description |
|---|---|---|
| #103745 | M | Migrate ACA secrets → Key Vault (live ACA secrets are write-only/unreadable; adopted apps still use manual secrets) |
| TBD | L | Narrow azure-stage.yml paths filter (coordinate with MC #103579) |
| TBD | M | Rotate live Unleash DB credentials (weak cred still active on running app) |
MC #103720 (child of #103715) — ZAKON-PLAN mandatory documentation task
No comments to display
No comments to display