AZURE TERRAFORM AZTFEXPORT DEVOPS

Importing an Existing Azure Static Web App into Terraform: A Real-World Walkthrough

How I brought a live Azure Static Web App under Terraform with aztfexport: the errors, two near-misses on production, and the fixes that worked.

By Ravindrakumar Narayanan · Microsoft Learn credentials · · 6 min read

Why I'm Writing This

Most Terraform import guides show the happy path. This one doesn't. I had a Static Web App running in production and wanted it under Terraform. The same resource group also had an Automation Account, and aztfexport pulled that in too. It took a lot of cleanup to get to a clean plan, and I nearly broke production twice on the way.

The screenshots are redrawn from my terminal output. Resource group, site, domain and repo names are replaced with placeholders.

Prerequisites

PowerShell showing Terraform v1.15.9 with azurerm v4.80.0 and az account show confirming the subscription

Step 1 — az webapp list Returned Nothing

I started with the command I always use:

az webapp list --query "[].{name:name, rg:resourceGroup}" -o table

Empty table. No error, just a 32-bit Python warning from the cryptography package.

PowerShell showing an empty az webapp list result and a 32-bit Python cryptography warning

The reason: az webapp list only covers App Service (Microsoft.Web/sites). Static Web Apps are Microsoft.Web/staticSites and need their own extension:

az extension add --name staticwebapp
az staticwebapp list --query "[].{name:name, rg:resourceGroup}" -o table

That found it.

Step 2 — Install aztfexport

winget install aztfexport

Restart your terminal after the install. Otherwise the command isn't on PATH yet and you get "not recognised".

winget install aztfexport completing successfully

Step 3 — Export the Resource Group

I exported the whole resource group, not just the Static Web App:

aztfexport resource-group rg-example

The TUI shows every resource and the Terraform type it maps to. Everything is selected by default. I pressed w.

aztfexport interactive mode listing 239 resources in the resource group

What I missed was the Automation Account in that group. I only use it for one runbook, but Azure creates a lot of defaults with every Automation Account: 196 PowerShell modules, six runtime environments, three connection types and two tutorial runbooks. aztfexport exported all of it. 239 resources in total.

Step 4 — terraform plan: 200+ Errors

terraform plan

Over 200 errors, in three groups:

  1. azurerm_automation_module and azurerm_automation_powershell72_module: "at least 1 module_link blocks are required". Azure doesn't return a content_uri for its built-in modules.
  2. azurerm_automation_runtime_environment_package: "expected content_uri url to not be empty". Same cause, from the built-in PowerShell-5.1 runtime environment.
  3. azurerm_static_web_app: repository_branch, repository_token and repository_url must be set together. Azure returns the branch and URL, but never the GitHub token.

Lesson: exporting a resource group gives you everything in it, including Azure defaults that can't be written as valid Terraform.

Step 5 — Remove the Automation Defaults

PowerShell has no grep. Select-String does the same job:

terraform state list | Select-String automation_module | ForEach-Object { terraform state rm $_.Line }
terraform state list | Select-String automation_powershell72_module | ForEach-Object { terraform state rm $_.Line }
terraform state list | Select-String automation_runtime_environment | ForEach-Object { terraform state rm $_.Line }
terraform state rm removing 110 automation modules, 86 PowerShell 7.2 modules and 31 runtime environment resources

Gotcha: terraform state rm only removes from state. The resource blocks stay in main.tf, and if you leave them there the next plan tries to create them all. So I also removed the matching resource blocks from main.tf, over 200 of them.

A much shorter terraform plan after the default Automation blocks were removed from main.tf

Step 6 — The repository_token Near-Miss

For the token error, I first deleted repository_branch and repository_url from the config. Bad idea. The plan set both to null, which would have unlinked GitHub Actions from my production site.

terraform plan setting repository_branch and repository_url on azurerm_static_web_app to null

What worked: put the real branch and URL back, add a placeholder token (the schema needs all three), and wrap them in ignore_changes so Terraform never touches them.

resource "azurerm_static_web_app" "swa" {
  # ...
  repository_branch = "main"
  repository_url    = "https://github.com/example-org/example-prod-site"
  repository_token  = "managed-outside-terraform"

  lifecycle {
    ignore_changes = [repository_branch, repository_url, repository_token]
  }
}

Step 7 — Custom Domain validation_type

Both custom domains (example.com and www.example.com) were exported with an empty validation_type. I set "cname-delegation". The plan wanted to destroy and recreate both live domains (-/+ must be replaced).

Then I removed validation_type completely. Terraform wouldn't plan, because it's a required argument.

terraform plan error: The argument validation_type is required, but no definition was found

Azure doesn't return validation_type once a domain is validated, so there's no safe value to put in. I removed both domains from state and config and left them outside Terraform.

Step 8 — Connection Type Field Ordering

The three default connection types (Azure, AzureClassicCertificate, AzureServicePrincipal) showed changes on every plan. The field blocks looked like they were being renamed.

terraform plan showing field block names swapping on azurerm_automation_connection_type

Nothing had changed. The provider treats field blocks as an ordered list, and Azure returns them in a different order from the HCL. These are Azure defaults I never configured, so they went the same way as the modules: terraform state rm for all three, and their blocks removed from main.tf. The next plan no longer showed them.

Step 9 — Clean Plan

Plan: 0 to add, 1 to change, 0 to destroy.

The one change is whitespace in the stored content of Azure's graphical tutorial runbook. Harmless.

terraform plan: 0 to add, 1 to change, 0 to destroy
terraform apply
terraform apply completing successfully

Can This Rebuild From Scratch?

No, not as it stands. This config manages what already exists. Two things would fail on an empty subscription:

The resource group, Automation Account, runbooks and identity would rebuild fine. Azure recreates its own defaults on a new Automation Account.

Lessons Learned

Where I Landed

main.tf now has seven resources, down from 239: the resource group, the Automation Account, my runbook, Azure's two tutorial runbooks, the user-assigned identity, and the Static Web App with its GitHub link protected.

terraform state list showing the final seven managed resources

Ravindrakumar Narayanan — Senior Infrastructure Engineer specialising in Azure, VMware and automation.

View Microsoft Learn transcript → · ← Back to all blogs