Terraform y Microsoft Fabric en la práctica: un medallion en un solo apply
Demo paso a paso con vídeo: una capacity, 9 workspaces, 36 grupos de Entra ID y sus roles en Microsoft Fabric con Terraform. Plan, apply, drift y destroy.
En el artículo anterior vimos la teoría: qué es Infrastructure as Code, cómo se escribe un recurso, qué es el estado y por qué hay que leer el plan antes de cada apply.
Hoy vamos con la demo. El encargo: el medallion de un proyecto que se llama «ventas». Tres capas (bronze, silver y gold) por tres entornos (dev, test y prod), cada workspace con sus cuatro grupos de seguridad y sus cuatro roles. Y la capacity debajo, que tampoco existe todavía.
A mano serían nueve workspaces, 36 grupos y 36 asignaciones de roles. Con Terraform es un apply.
Este es el vídeo de la demo que hicimos Alfonso Ming y yo en la NetCoreConf.
El vídeo no tiene sonido: te explico cada paso aquí debajo.
El código está en github.com/marlizana/FabricAsCode.
Paso 0: no hay nada
Antes de empezar, comprobamos que el punto de partida está vacío:
- Fabric: filtras la lista de workspaces por «ventas». No sale ninguno.
- Azure: en Fabric capacities no hay ninguna capacity del proyecto.
- Entra ID: buscas los grupos
sg-fbc-ventas. Ninguno.
Paso 1: el código de Terraform, de arriba abajo
El repo tiene esta pinta:
FabricAsCode/
├── versions.tf # providers y versiones
├── main.tf # conecta las piezas
├── variables.tf
├── outputs.tf
├── environments/ # un .tfvars por escenario
├── modules/
│ ├── capacity/
│ ├── workspace-with-roles/
│ └── templates/medallion-cicd/
└── scripts/demo.ps1 # atajos para la demo
versions.tf: los providers
terraform {
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }
azuread = { source = "hashicorp/azuread", version = "~> 3.0" }
fabric = { source = "microsoft/fabric", version = "~> 1.14" }
}
}
provider "azurerm" { features {} }
provider "azuread" {}
provider "fabric" {}
azurerm crea la capacity, azuread los grupos y fabric los workspaces y los roles. Los bloques provider van vacíos a propósito: las credenciales del service principal llegan por variables de entorno.
environments/ventas.tfvars: el encargo
capacity_name = "fbcventas"
capacity_sku = "F2"
region = "spaincentral"
template = "medallion-cicd"
project_name = "ventas"
layers = ["bronze", "silver", "gold"]
environments = ["dev", "test", "prod"]
admin_group_members = [
"mar@contoso.onmicrosoft.com",
"alfonso@contoso.onmicrosoft.com",
]
Aquí está todo lo que cambia de un proyecto a otro: el tamaño de la capacity, la región, las capas, los entornos y quién entra en los grupos Admin. Para un proyecto nuevo se copia este fichero y el código no se toca.
main.tf: une las piezas
Llama al módulo de la capacity y al template medallion-cicd, y le pasa al template el ID de la capacity.
modules/capacity/main.tf: la capacity de Fabric
Un resource group y un azurerm_fabric_capacity con SKU F2. Y un detalle que me costó un plan fallido:
# El ID de Azure no es el ID de Fabric: esta data source hace de puente.
data "fabric_capacity" "this" {
display_name = azurerm_fabric_capacity.this.name
}
azurerm devuelve el ID de recurso de Azure, pero un workspace espera el GUID de Fabric. La data source busca la capacity por nombre y devuelve el ID que necesita el workspace.
modules/templates/medallion-cicd/main.tf: las combinaciones
locals {
workspace_specs = {
for pair in setproduct(var.layers, var.environments) :
"${pair[0]}-${pair[1]}" => { layer = pair[0], env = pair[1] }
}
}
module "workspace" {
source = "../../workspace-with-roles"
for_each = local.workspace_specs
workspace_name = "${var.project_name}-${each.value.layer}-${each.value.env}" # ventas-bronze-dev
ad_group_prefix = "sg-fbc-${var.project_name}-${each.value.layer}-${each.value.env}" # sg-fbc-ventas-bronze-dev
capacity_id = var.capacity_id
admin_members = var.admin_members
}
setproduct combina las dos listas y salen nueve parejas capa-entorno. Para cada una se llama al mismo módulo con su nombre y su prefijo de grupos.
modules/workspace-with-roles/main.tf: workspaces, grupos y roles
resource "fabric_workspace" "this" {
display_name = var.workspace_name
capacity_id = var.capacity_id
}
resource "azuread_group" "roles" {
for_each = var.roles # Admin, Contributor, Member, Viewer
display_name = "${var.ad_group_prefix}-${lower(each.value)}"
security_enabled = true
members = each.value == "Admin" ? var.admin_members : null
}
resource "fabric_workspace_role_assignment" "this" {
for_each = azuread_group.roles
workspace_id = fabric_workspace.this.id
role = each.key
principal = { id = each.value.object_id, type = "Group" }
}
Un workspace, un grupo por rol y una asignación que ata cada grupo a su rol. Este módulo, llamado nueve veces, es el que genera los 36 grupos y las 36 asignaciones.
Paso 2: terraform plan
Con las credenciales en variables de entorno:
terraform init
terraform plan "-var-file=environments/ventas.tfvars" "-out=ventas.tfplan"
Ojo: en Windows PowerShell 5.1, entrecomilla los argumentos que llevan un punto. Sin comillas,
-var-file=environments/ventas.tfvarsse parte en dos y Terraform se queja de «Too many command line arguments».
En el plan conviene pararse en cuatro sitios: la capacity, un workspace, un grupo y una asignación de rol. Y luego en la última línea:
Plan: 84 to add, 0 to change, 0 to destroy.
Las 84 son nueve workspaces, 36 grupos, 36 asignaciones, la capacity, su resource group y el sufijo aleatorio de su nombre. Ningún cambio y ningún borrado, que es lo que esperas la primera vez.
Paso 3: terraform apply
terraform apply ventas.tfplan
Primero se crea la capacity, que tarda un par de minutos. Después, en paralelo, los grupos y los workspaces. Al final:
Apply complete! Resources: 84 added, 0 changed, 0 destroyed.
Mientras corre, da tiempo a un café.
Paso 4: lo que ha creado
- Azure: la capacity, en su propio resource group.
- Entra ID: buscas
sg-fbc-ventasy salen 36 grupos. Ensg-fbc-ventas-silver-prod-admin, pestaña Members, estamos Alfonso y yo. - Fabric: filtras por «ventas» y salen los nueve workspaces. En
ventas-silver-prod, Manage access muestra los cuatro grupos, cada uno con su rol. Y en Workspace settings → License, la capacity.
Los nueve workspaces siguen el mismo patrón de nombres y tienen exactamente los mismos permisos.
Paso 5: volver a lanzar el plan
¿Qué pasa si vuelvo a hacer plan sin cambiar nada?
No changes. Your infrastructure matches the configuration.
Nada. Como contaba en la teoría, apply se comporta como un MERGE: lo puedes lanzar diez veces y sigues teniendo nueve workspaces.
Paso 6: drift, alguien ha tocado el portal
Ahora hago de compañera con prisa: entro en ventas-silver-prod, Manage access, y quito el grupo sg-fbc-ventas-silver-prod-viewer. «Un momentito, para probar».
Plan otra vez:
# module.medallion_cicd[0].module.workspace["silver-prod"].fabric_workspace_role_assignment.this["Viewer"] will be created
+ resource "fabric_workspace_role_assignment" "this" { ... }
Plan: 1 to add, 0 to change, 0 to destroy.
Terraform ha consultado Fabric, ha visto que esa asignación ya no existe y propone volver a crearla. Un apply y el grupo vuelve a tener su rol.
Paso 7: un entorno más en una línea
¿Y si mañana hace falta QA? Se añade al .tfvars:
environments = ["dev", "test", "qa", "prod"]
Y el plan:
Plan: 27 to add, 0 to change, 0 to destroy.
Tres workspaces (bronze, silver y gold de QA), doce grupos y doce asignaciones, con el mismo patrón que el resto. Los entornos que ya existían no se tocan.
Paso 8: terraform destroy
Cuando el proyecto, la demo o la prueba de concepto se acaba:
terraform plan -destroy "-var-file=environments/ventas.tfvars" "-out=borrar.tfplan"
terraform apply borrar.tfplan
Lee el plan antes: tiene que decir 84 to destroy y nada más. Se borran los workspaces, los grupos y la capacity, y con ella deja de facturarse.
Ojo: con la capacity pausada, el provider de Fabric no deja hacer ni plan, ni apply, ni destroy. Reanúdala antes y espera a que esté Active.
El script de la demo
En el repo verás scripts/demo.ps1. Envuelve estos mismos comandos para no tener que teclearlos delante de 200 personas.
| Atajo | Qué hace |
|---|---|
. .\scripts\demo.ps1 env | Carga las credenciales en la sesión (con el punto delante) |
.\scripts\demo.ps1 init | terraform init y el workspace de estado |
.\scripts\demo.ps1 plan | Plan con el .tfvars del escenario |
.\scripts\demo.ps1 apply | Plan a fichero, confirmación y apply de ese fichero |
.\scripts\demo.ps1 drift | Plan para ver qué cambió fuera de Terraform |
.\scripts\demo.ps1 qa / sin-qa | Añade o quita el entorno de QA |
.\scripts\demo.ps1 pausa / reanuda | Pausa o reanuda la capacity |
.\scripts\demo.ps1 destroy | Plan de borrado, confirmación y destroy |
Antes y después
| Antes | Después |
|---|---|
| Una tarde de clics por proyecto | Un apply de unos minutos |
| Un Word desactualizado como documentación | El código describe cómo está la plataforma |
| QA y pro parecidos, pero no iguales | Los entornos salen del mismo módulo |
| «¿Quién quitó este rol?» sin respuesta | El drift aparece en el siguiente plan |
El siguiente proyecto empieza copiando un .tfvars en vez de abriendo el portal. Si lo pruebas con el repo, cuéntame en los comentarios qué tal te ha ido.
Pris, mi gata, ha visto el vídeo entero y su única aportación ha sido sentarse en el teclado durante el apply. Por suerte ya estaba aplicando el plan guardado.
Enlaces de interés
¿Te ha servido? Aplaude, sin cuenta. Puedes hacerlo varias veces.
Comentarios