Skip to content
CSA Loom — the Microsoft Fabric experience for Azure tenants where Fabric isn't yet available: lakehouses, warehouses, notebooks, semantic models, Activator rules, Data Agents, across Commercial, GCC, GCC-High, and DoD IL5

Deploy CSA Loom via Terraform (azurerm_resource_group_template_deployment)

Comparative positioning note

This document is written from the perspective of Microsoft Azure, Cloud Scale Analytics, and CSA Loom. Any description of third-party or competing products, services, pricing, or capabilities is derived from publicly available documentation and sources believed accurate at the time of writing, and is provided for general comparison only. We do not claim expertise in, or authority over, any non-Microsoft product or service; the respective vendor's official documentation is the authoritative source for their offerings, which may change over time. Nothing here is intended to disparage any vendor — where a competing product has genuine advantages, we aim to note them honestly. Verify all third-party details against the vendor's current official documentation before making decisions.

For organizations standardized on Terraform / OpenTofu. The Bicep template (platform/fiab/bicep/main.bicep) compiles to ARM JSON which Terraform can drive via the azurerm_subscription_template_deployment resource — no separate Terraform module rewrite needed.

Why this pattern

  • One source of truth — Bicep stays the canonical template (so upgrades inherit cleanly from upstream releases)
  • Terraform state — drift detection + plan/apply lifecycle remains intact for the wrapper resource
  • OpenTofu friendlyazurerm works identically under OpenTofu

If you want a pure Terraform module that mirrors the Loom resource graph, that's tracked in the backlog (no ETA — Bicep is the locked canonical per LD-15).

Prerequisites

Item Notes
Terraform 1.6+ or OpenTofu 1.6+
azurerm provider 3.100+
az CLI for compiling the Bicep to ARM az bicep build runs locally during the apply
SP or workload identity authenticating the provider OIDC supported

Module layout

terraform/
├── main.tf              # the wrapper resource
├── variables.tf
├── outputs.tf
└── arm/
    └── main-template.json   # generated by az bicep build (gitignored)

main.tf

terraform {
  required_version = ">= 1.6"
  required_providers {
    azurerm = { source = "hashicorp/azurerm", version = "~> 3.100" }
  }
}

provider "azurerm" {
  features {}
  use_oidc        = true
  client_id       = var.azure_client_id
  tenant_id       = var.azure_tenant_id
  subscription_id = var.azure_subscription_id
}

# Compile the Loom Bicep to ARM JSON every plan (vendored copy in arm/)
# Run this once before terraform plan:
#   az bicep build --file ../platform/fiab/bicep/main.bicep \
#     --outfile arm/main-template.json
data "local_file" "arm_template" {
  filename = "${path.module}/arm/main-template.json"
}

resource "azurerm_subscription_template_deployment" "loom" {
  name              = "loom-${var.environment}-${formatdate("YYYYMMDDhhmm", timestamp())}"
  location          = var.location
  template_content  = data.local_file.arm_template.content

  parameters_content = jsonencode({
    loomAdminGroupObjectId = { value = var.admin_group_object_id }
    boundary               = { value = var.boundary }
    capacitySku            = { value = var.capacity_sku }
    deploymentMode         = { value = var.deployment_mode }
    hubVnetCidr            = { value = var.hub_vnet_cidr }
  })

  lifecycle {
    ignore_changes = [name]  # don't churn on timestamp suffix
  }
}

variables.tf

variable "azure_client_id"        { type = string }
variable "azure_tenant_id"        { type = string }
variable "azure_subscription_id"  { type = string }

variable "environment" {
  type        = string
  description = "dev | stage | prod"
}

variable "location" {
  type    = string
  default = "eastus2"
}

variable "boundary" {
  type        = string
  default     = "commercial"
  validation {
    condition     = contains(["commercial", "gcc", "gcc-high"], var.boundary)
    error_message = "boundary must be commercial | gcc | gcc-high"
  }
}

variable "capacity_sku"        { type = string,  default = "F8" }
variable "deployment_mode"     { type = string,  default = "single-sub" }
variable "hub_vnet_cidr"       { type = string,  default = "10.0.0.0/16" }
variable "admin_group_object_id" { type = string }

outputs.tf

output "deployment_name" {
  value = azurerm_subscription_template_deployment.loom.name
}

output "outputs" {
  value     = azurerm_subscription_template_deployment.loom.output_content
  sensitive = true
}

Run

# One-time: compile Bicep → ARM
az bicep build --file ../platform/fiab/bicep/main.bicep \
  --outfile arm/main-template.json

# Standard Terraform flow
terraform init
terraform plan  -var environment=dev -var admin_group_object_id=<group-oid>
terraform apply -var environment=dev -var admin_group_object_id=<group-oid>

For OpenTofu, swap terraform for tofu in every command.

Post-deploy bootstrap

Terraform won't run the Power BI / Databricks / Dataverse bootstrap. Either chain it as a null_resource with local-exec, or run it out-of-band:

bash ../scripts/csa-loom/bootstrap-all.sh \
  --boundary "$TF_VAR_boundary" \
  --environment "$TF_VAR_environment"