Skip to content

Azure

Azure

Azure plays two distinct roles in devhome: a Terraform-managed footprint (terraform/azure/) covering DNS, offsite backup storage, this documentation site's hosting, and — since the Entra ID identity work — the tenant-side identity platform every app in the lab authenticates against; and a larger hybrid architecture (Hub VNet, Azure Arc, private AKS, ACR) reached over the IPsec VPN tunnel — see Architecture Overview for where that sits in the overall topology.

What's actually in terraform/azure/

No longer the small two-resource footprint it started as — four distinct things now live here, each with its own file and its own reason for being in this stack rather than somewhere else:

DNS zone (devhome.cloud)

resource "azurerm_dns_zone" "devhome_dns_zone" {
  name                = "devhome.cloud"
  resource_group_name = azurerm_resource_group.dns.name
}

Azure Public DNS is authoritative for devhome.cloud (name servers delegated from the registrar). This is also the zone pfSense's ACME package validates against via DNS-01 — see ACME Certificates.

One dynamic A record lives here: openvpn.devhome.cloud, pointed at the home connection's public IP. Since that IP isn't static, it's kept current by an Azure DevOps pipeline (see Pipelines) that looks up the current public IP and calls az network dns record-set a add-record on a schedule — the same public-IP lookup pattern (dig +short myip.opendns.com @resolver1.opendns.com) is reused directly in terraform/azure/plan.sh so a manual terraform plan always has the current address too.

Offsite backup storage account

A single storage account (devhomesa<random-suffix> — storage account names must be globally unique across all of Azure, hence the random suffix) with two purposes:

  • backups container — offsite copies of config backups produced by ansible/backup-playbook.yml, written as backups/devhome_backup_<YYYY-MM-DD>/<file>. A lifecycle management policy prunes anything older than 30 days, scoped specifically to that prefix so it can never touch anything else in the account.
  • cloudshell file share — Azure Cloud Shell's persistent $HOME, sized to Cloud Shell's own 5 GiB default so it can be selected as the existing share during Cloud Shell's setup wizard rather than provisioning a second one.

Entra ID identity platform (entra.tf)

By far the largest thing in this directory now — every OIDC app registration in the lab (ArgoCD, Rancher, Vault, vCenter) plus the break-glass accounts Cloud Sync and interactive portal access both depend on. This gets its own dedicated page rather than being described here — see Entra for the tenant-side setup and My Apps for how each app ends up launchable from myapps.microsoft.com. On-Prem Active Directory covers the on-prem half this all builds on.

Lives in terraform/azure/ (not its own stack) because it authenticates via a second, separate provider block in this same directory — see Credentials below.

Documentation site hosting (docs-static-site.tf)

This documentation site itself — docs.devhome.cloud — is an Azure Static Web App (Free tier), chosen over the earlier Storage-static-website + CDN + Key Vault BYOC-cert design: classic Azure CDN no longer accepts new profile creation on this API version, and Front Door's ~$35/mo minimum wasn't worth it for a personal docs site. Static Web Apps' free tier does the same job at $0/mo, at the cost of using Azure's own managed certificate for this one domain instead of the lab's own *.devhome.cloud wildcard.

Terraform only provisions the Static Web App and its custom domain/DNS binding once — deploying content is a separate step, run every time the docs change:

make deploy-docs

builds the site with mkdocs and pushes it via the Azure Static Web Apps CLI, using a deployment token this stack generates and publishes to devhome/docs-site in Vault (the one thing terraform/azure writes to Vault rather than only reading from — a resource publishing its own derived credential for a downstream consumer, same pattern entra.tf uses for devhome/entra-sso).

Credentials

Two separate credential sets, two separate provider blocks — terraform/azure/provider.tf:

Provider Vault path Scope
azurerm devhome/azure (arm_subscription_id/arm_client_id/arm_client_secret/arm_tenant_id) DNS, storage, Static Web App — general Azure Resource Manager access
azuread devhome/entra (entra_client_id/entra_client_secret/entra_tenant_id) Entra ID app registrations/groups only — a separate app scoped to Microsoft Graph (Application.ReadWrite.All, Group.ReadWrite.All, etc.), not the ARM service principal above. See Entra for how this app itself gets bootstrapped.

Both are read via vault_kv_secret_v2 data sources, same pattern as every other Terraform stack (see Vault Consumers). The backup storage account/container names are themselves stored back into devhome/azure by terraform/vault's state — this directory only ever reads that secret, never writes it, so there's exactly one owner of that value.

The hybrid pieces (Hub VNet, Arc, AKS, ACR)

The Hub Resource Group side of the architecture — Virtual Network Gateway, Hub VNet (172.16.0.0/16), Azure DNS Resolver, Arc-connected Kubernetes/Machines, private AKS, and private ACR — is reached from the lab via the IPsec tunnel (see VPN) and pfSense's Domain Overrides for the private-link DNS zones. This is not managed from terraform/azure/ in this repo.