Repository navigation
State and Backends
What this module expects of your Terraform state, and what is in it.
versions.tf contains:
terraform {
backend "azurerm" {}
}An empty block means every backend setting is supplied at init time. There is no
local-state fallback and no committed backend file — .gitignore excludes both
backend.tf and backend.tfvars.
Copy backend.tf.example to backend.tfvars, fill it in, and pass it to init:
terraform init -backend-config=backend.tfvars
The four keys it expects are resource_group_name, storage_account_name,
container_name and key.
The state account should live outside the ReportMate resource group so a mistaken destroy of the application resources cannot take the state with it.
az group create --name rg-terraform-state --location canadacentral
az storage account create --name examplestatestore --resource-group rg-terraform-state --sku Standard_LRS
az storage container create --name terraform-state --account-name examplestatestore
Enable blob versioning and soft delete on that account. Nothing in this module does it for you, and the state is the only record of which random-suffixed resources belong to this deployment.
Terraform state holds every value a resource reads back, including sensitive ones. For this module that includes at minimum:
- The PostgreSQL administrator password, and the full connection string built from it.
- The storage account access key and connection string.
- The Application Insights instrumentation key and connection string.
- The generated Entra ID client secret and the generated NextAuth secret.
-
client_passphrases,api_internal_secret,kiosk_tokens, and every Teams webhook URL.
Treat the state container as a secret store. Restrict it with RBAC, do not copy
state files around, and do not paste terraform show -json output anywhere.
The Key Vault this module creates does not change that: the vault is populated
from Terraform variables, so the values pass through state on their way in.
The containers module comments are explicit about this being a known
trade-off rather than an oversight.
Two resources carry prevent_destroy:
module.database.azurerm_postgresql_flexible_server.pgmodule.database.azurerm_postgresql_flexible_server_database.db
Any plan that would replace or destroy either fails outright. That includes changes to create-time-only attributes — see Upgrading for which ones force replacement.
Three modules use count, so their state addresses are indexed:
| Module | Address | Present when |
|---|---|---|
key_vault |
module.key_vault[0] |
enable_key_vault = true |
functions |
module.functions[0] |
enable_functions = true |
networking |
module.networking[0] |
enable_custom_domain = true and custom_domain_name != ""
|
Flipping one of those flags off does not orphan resources — it plans their
destruction. Turning enable_key_vault off destroys the vault (subject to purge
protection and soft-delete retention) and strips NEXTAUTH_SECRET and
AZURE_AD_CLIENT_SECRET from the dashboard container.
main.tf ends with:
removed {
from = module.auth.azurerm_key_vault_secret.nextauth_secret
lifecycle {
destroy = false
}
}This drops a resource from state without deleting the underlying vault secret,
which the key_vault module still manages. It requires Terraform 1.7 or later
in practice even though required_version says >= 1.5. Leave it in place
until every state file that ever held that resource has been applied at least
once; removing it early re-introduces the duplicate writer that rotated the
session secret on every apply.
Several attributes are deliberately not owned by Terraform, so a manual change to them will not show up as drift — and reverting them by hand will be undone. The full list is in Architecture; the ones that surprise people most are the container image tags (owned by the deploy pipeline) and the PostgreSQL storage size (owned by auto-grow).
Conversely, the Function App's app_settings are owned outright. A setting
added with az functionapp config appsettings set is erased by the next apply.
Use function_app_additional_settings instead.
The auth module's null_resources use always_run = timestamp() triggers, so
they execute on every apply and shell out to az ad app update. One always
exists; the second exists only when enable_oidc_api is true. This means:
- The apply host needs a working Azure CLI login, not just provider credentials.
- Plans are never fully clean — those resources always show as changing.
The provisioners use on_failure = continue, so a transient Entra failure does
not fail the apply; the next run retries.
MIT licensed — reportmate.app