Cuando empecé el proyecto, elegí Terraform. Era la opción obvia: ecosistema maduro, providers para cualquier cloud, HCL como lenguaje declarativo estándar, gran comunidad. Seis meses después, documenté ADR-0013: reemplazar Terraform por Bicep.
Este post es esa decisión explicada en detalle — incluyendo lo que Bicep no puede hacer y por qué igual valió la pena migrar.
Por qué empecé con Terraform
Terraform en 2024 es el estándar de facto para IaC multi-cloud. Las razones para elegirlo:
- Provider ecosystem: un provider para cada recurso Azure, GCP, AWS, y la mayoría de SaaS
- State management: sabe qué recursos existen y qué cambió, no solo qué debe existir
- Plan antes de apply: ver qué va a cambiar antes de que cambie
- HCL: lenguaje legible, módulos reutilizables, variables tipadas
El primer mes funcionó bien. Los recursos básicos (Resource Groups, Storage Accounts, Container Apps Environments) estaban bien soportados en el provider. El pipeline de CD hacía terraform plan en PR y terraform apply en merge a main.
Los problemas que aparecieron
El state file es infraestructura adicional
Terraform necesita guardar su state en algún lado. Para equipos, el backend remoto es obligatorio — el state local no se puede compartir.
En Azure, el backend remoto es un Storage Account con un blob container:
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstate12345"
container_name = "tfstate"
key = "proyecto.terraform.tfstate"
}
}
El storage account para el state tiene que existir antes de poder crear el storage account con Terraform. El bootstrap del estado es manual o con un script separado. Y el state necesita locking — Azure Storage Blob leases hacen el trabajo, pero si el pipeline se interrumpe en mitad de un apply, el lock puede quedar activo y el siguiente run falla con “state is locked”.
Costo operacional: un Storage Account dedicado, locking que a veces falla, bootstrapping manual.
Plan/apply cycle agrega latencia al CD
Un deploy típico con Terraform:
terraform init (~30s — descarga providers)
terraform plan (~45s — compara state con cloud)
terraform apply (~2-5min — aplica cambios)
Para un cambio de una variable de entorno, el ciclo completo toma 3-6 minutos solo en overhead de Terraform. Esto es tolerable si deployeas raramente. En un pipeline de CD que corre varias veces al día, el tiempo se acumula.
El terraform init descarga providers en cada run si no hay cache. El plan hace API calls a Azure para comparar el estado actual con el deseado.
Recursos Azure-específicos con soporte rezagado
Azure Container Apps environment con managed identity y Dapr sidecar en Terraform (circa 2023-2024):
resource "azurerm_container_app" "app" {
name = "mi-app"
container_app_environment_id = azurerm_container_app_environment.env.id
resource_group_name = var.resource_group_name
revision_mode = "Single"
template {
container {
name = "mi-app"
image = var.image
cpu = 0.25
memory = "0.5Gi"
}
}
# identity block requerido para managed identity
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.app.id]
}
}
El problema: algunas propiedades de Container Apps (scaling rules específicas, Dapr config avanzada) no estaban disponibles en el provider de Azure hasta versiones posteriores. O requerían usar el bloque lifecycle { ignore_changes } para que Terraform no sobreescribiera configuración que Azure manejaba automáticamente.
El provider azurerm siempre está un poco detrás de lo que Azure soporta nativamente.
Bicep: lo que gané
Sin state file
Bicep no tiene state. Cada deploy es idempotente por diseño — declara el estado deseado y Azure Deployment Engine lo reconcilia. Si el recurso ya existe con las propiedades correctas, no hace nada. Si cambió algo, lo actualiza.
No hay Storage Account para estado. No hay locking. No hay bootstrapping.
Soporte día-0 para recursos Azure
Bicep es desarrollado por Microsoft. Cuando Azure lanza un recurso nuevo o una feature nueva, el soporte en Bicep es casi inmediato. Container Apps con todas las propiedades de scaling, Workload Profiles, Dapr:
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
name: appName
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
properties: {
environmentId: containerAppEnv.id
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: true
targetPort: 8080
}
}
template: {
scale: {
minReplicas: 1
maxReplicas: 10
rules: [
{
name: 'http-scaling'
http: {
metadata: {
concurrentRequests: '100'
}
}
}
]
}
containers: [
{
name: appName
image: image
resources: {
cpu: json('0.25')
memory: '0.5Gi'
}
}
]
}
}
}
Todas las propiedades disponibles, documentación oficial de Microsoft, ningún workaround con lifecycle.
Pipeline simplificado
- name: Deploy infrastructure
run: |
az deployment group create \
--resource-group ${{ env.RESOURCE_GROUP }} \
--template-file infra/main.bicep \
--parameters @infra/parameters.json \
--mode Incremental
Un comando. Sin init, sin plan separado, sin state. El --mode Incremental actualiza solo los recursos que cambiaron.
Para preview antes de deploy (equivalente al plan de Terraform):
az deployment group what-if \
--resource-group $RESOURCE_GROUP \
--template-file infra/main.bicep \
--parameters @infra/parameters.json
what-if muestra qué va a crear, modificar o eliminar. No tan detallado como terraform plan, pero suficiente para la mayoría de casos.
Lo que Terraform hace mejor
Siendo honesto sobre las limitaciones de Bicep:
Multi-cloud: Si algún día necesitas recursos en GCP o AWS además de Azure, Bicep no te sirve. Terraform tiene providers para todo. Bicep es Azure-only.
State como fuente de verdad: Terraform sabe que un recurso existe aunque hayas cambiado su nombre en el código. Bicep no tiene estado — si renombras un recurso en Bicep, el deploy crea uno nuevo y el viejo queda huérfano.
Módulos de la comunidad: Terraform Registry tiene miles de módulos compartidos. Bicep tiene el módulo registry de Microsoft, más pequeño y menos diverso.
Destroy controlado: terraform destroy elimina todos los recursos del state de forma controlada. Con Bicep, para eliminar recursos tienes que hacerlo manualmente o con az group delete.
La decisión como ADR
La decisión la documenté como ADR-0013 (reemplaza ADR-0007):
# ADR-0013: Reemplazar Terraform por Bicep como IaC principal
## Estado
Reemplaza ADR-0007
## Contexto
ADR-0007 estableció Terraform como IaC. En práctica:
- State file requiere backend remoto con locking manual
- Plan/apply cycle agrega 3-6min por deploy
- Recursos Azure recientes tienen soporte rezagado en azurerm provider
- El proyecto es Azure-only — portabilidad multi-cloud no es requerimiento
## Opciones consideradas
- **Mantener Terraform:** conocido, multi-cloud futuro posible
- **Pulumi (TypeScript):** IaC en lenguaje de aplicación, state igual que Terraform
- **Bicep (elegida):** Azure-native, sin state, soporte día-0, pipeline más simple
## Decisión
Bicep como IaC para todos los recursos Azure. Terraform descontinuado.
## Consecuencias
Positivas:
- Sin state file que gestionar
- Soporte inmediato para features Azure nuevas
- Pipeline CD más rápido (-3min por deploy)
Negativas:
- Azure-only — si hay multi-cloud en el futuro, hay que migrar de nuevo
- El equipo tiene que aprender Bicep (curva menor que HCL)
- Sin equivalente de terraform destroy
## Decisión de migración
Recursos existentes: importados manualmente al estado deseado en Bicep.
Recursos Terraform: no destruidos, sino "adoptados" por Bicep en el próximo deploy (idempotente).
Lo que aprendí
Herramientas con estado son deuda operacional. El state file de Terraform es potente pero requiere mantenimiento. Para proyectos Azure-only, Bicep elimina esa deuda sin costo en funcionalidad.
ADR-0007 no fue un error. Fue la decisión correcta con la información disponible en ese momento. ADR-0013 no lo niega — lo reemplaza con el contexto actualizado.
La portabilidad multi-cloud es una trampa frecuente. “Podríamos necesitar GCP algún día” es el argumento que justifica Terraform sobre Bicep. En la práctica, si el stack es Azure, migrar el IaC cuando llegue ese día es menos costoso que mantener la abstracción todos los días hasta entonces.
El what-if de Bicep es suficiente para la mayoría de casos. La falta de un plan detallado como Terraform es una pérdida real, pero el 90% de los casos se manejan con az deployment group what-if.