Terraform works fine for Azure Static Web Apps, but it means managing a state file/backend for a resource that Azure already tracks itself. Bicep plus the Azure Developer CLI (azd) gives the same deploy/destroy/redeploy workflow without that overhead, and it fits naturally into a repo that already ships its own build output.
Why Bicep + azd instead of Terraform
- No state file to store, lock, or lose — ARM tracks the deployed resource state server-side.
azd up/azd downmap directly to provision+deploy / teardown, which is exactly the deploy-to-save-costs-then-redeploy cycle a personal blog needs.- A Static Web App is stateless: content is rebuilt from the repo on every deploy, so tearing the resource down between updates loses nothing.
Files
infra/main.bicep declares the Static Web App resource:
resource staticWebApp 'Microsoft.Web/staticSites@2023-12-01' = { name: staticWebAppName location: location tags: tags sku: { name: sku tier: sku } properties: { buildProperties: { skipGithubActionWorkflowGeneration: true } }} output staticWebAppDeploymentToken string = staticWebApp.listSecrets().properties.apiKeyoutput staticWebAppHostname string = staticWebApp.properties.defaultHostnameinfra/main.bicepparam supplies the default parameter values (name, region, SKU) so the template can be deployed without extra flags.
azure.yaml is the azd project file. It points at the compiled dist/ output and runs the project's own build before packaging:
resourceGroup: rg-fullswing-blog hooks: preprovision: shell: sh run: ./scripts/ensure-resource-group.sh postdown: shell: sh run: ./scripts/delete-resource-group.sh services: web: project: . language: js host: staticwebapp dist: dist hooks: prepackage: shell: sh run: npm run buildOne-time setup
azd auth loginazd env new fullswing-blogazd up— creates or reusesrg-fullswing-blog, provisions the Static Web App frominfra/main.bicep, and deploys the current build
Everyday workflow
azd up— redeploy after content changes; reuses or recreatesrg-fullswing-blogas neededazd down --purge— tear down the app and then deleterg-fullswing-blogentirely to stop paying for it between updatesazd upagain later — same Bicep template, same parameters, a fresh Static Web App with the latest build
CI
azd pipeline config wires up GitHub OIDC federated credentials automatically, so CI never needs a stored client secret. The workflow ensures rg-fullswing-blog exists before azd up, and on destroy it tears down the app and then deletes that resource group:
- name: Deploy run: | ./scripts/ensure-resource-group.sh azd up --no-prompt - name: Destroy if: inputs.action == 'down' run: | azd down --force --purge ./scripts/delete-resource-group.shazd pipeline config walkthrough
Run the following command to use Federated User Managed Identity (MSI + OIDC) to handle the Github action authentication to Azure
Running azd pipeline config --provider github is interactive; here's what it asks for and why:
Azure login — prompts to log in if you aren't already. If your tenant enforces MFA, sign in against the specific tenant first:
azd auth login --tenant-id <tenant>.Environment name — creates (or reuses) an
azdenvironment, e.g.dev. This maps to theAZURE_ENV_NAMEvalue used byazd up/azd down.Azure subscription and location — pick the subscription and region the Static Web App (and supporting identity) should live in.
Resource group — Use
rg-fullswing-blog. If you use another value, you will need to update the yaml filesMissing workflow file — if
.github/workflows/deploy.ymldoesn't exist yet,azdoffers to generate a starter workflow.GitHub CLI login — if you're not authenticated with
gh, it walks through a device-code browser login.Pipeline auth method — choose Federated User Managed Identity (MSI + OIDC) to avoid storing any client secret.
azdcreates the MSI, assigns it the needed roles, and adds federated credentials scoped to the repo'smainbranch and pull requests.Repo variables —
azdsetsAZURE_CLIENT_ID,AZURE_ENV_NAME,AZURE_LOCATION,AZURE_SUBSCRIPTION_ID, andAZURE_TENANT_IDas GitHub Actions repo variables so the workflow can authenticate via OIDC.Commit and push — optionally commits the new workflow file and pushes to
originto kick off the first run. Declining just means you push manually later:git add .git commit -m "Add azd GitHub Actions pipeline config"git push --set-upstream origin main
After this, every push to main runs the workflow using the federated identity — no long-lived secrets in GitHub.
What not to put in this template
Keep anything that should survive a teardown — a database, storage account, custom domain binding — in a separate Bicep module or resource group. That way azd down for the blog's Static Web App never touches it.