Skip to content

Upgrading

Rod Christiansen edited this page Oct 8, 2026 · 3 revisions

Upgrading

Version constraints, how the application itself gets upgraded, and which variable changes are more expensive than they look.

Version constraints

Component Constraint Declared in
Terraform >= 1.5 declared; 1.7 or later needed for the removed block versions.tf, main.tf
hashicorp/azurerm ~> 4.33 versions.tf, and again in modules/key_vault, modules/auth, modules/maintenance
hashicorp/azuread ~> 2.0 at the root, ~> 2.47 in modules/auth versions.tf, modules/auth/main.tf
hashicorp/random ~> 3.1 versions.tf, modules/key_vault, modules/auth
hashicorp/null ~> 3.1 versions.tf
hashicorp/time ~> 0.9 modules/auth/main.tf

The effective azuread constraint is the intersection, so ~> 2.47. The ~> on a two-part version means minor upgrades within 4.x and 2.x are accepted; a 5.x azurerm or 3.x azuread requires editing the constraints.

.terraform.lock.hcl is gitignored, so provider versions are not pinned across machines. Two operators can end up on different provider patch versions. If you run this from CI, commit a lock file in your own fork or pin exact versions.

Upgrading the application, not the infrastructure

The running container images are not upgraded by terraform apply. Both container apps declare:

lifecycle {
  ignore_changes = [template[0].container[0].image]
}

The deploy pipeline owns the image, pushing an immutable tag with az containerapp update. Bumping frontend_image_tag or api_image_tag in tfvars changes nothing on an existing deployment — those variables only take effect the first time a container app is created. Roll a new version through your pipeline, not through Terraform.

The same is true of the maintenance job's image, which is hardcoded to <registry>/reportmate-maintenance:latest.

Database schema migrations

Terraform creates the database and stops there. The schemas/ directory holds the numbered migration SQL and two runners (run-migrations.ps1, run_migration.py), and the API applies migrations at startup. An infrastructure apply is not a schema upgrade and a schema upgrade is not an infrastructure apply — sequence them yourself.

Changes that force a replacement or a restart

Change Consequence
db_geo_redundant_backup Create-time only on PostgreSQL Flexible Server. Changing it is a rebuild, which prevent_destroy blocks
postgres_server_name Renames the server, i.e. replaces it. Blocked by prevent_destroy
db_name Replaces the database. Blocked by prevent_destroy
db_max_connections Static Postgres parameter. Applying it restarts the server — do it in a maintenance window, not alongside an unrelated change
db_storage_mb Effectively inert after create; it is in ignore_changes. Storage can only grow, and auto-grow does the growing
db_sku_name Scales the server in place, with a brief outage
function_app_sku The plan is named <app>-flex-plan. Y1 to FC1 is not an in-place SKU change, so a deployment that holds an older <app>-plan gets a fresh plan and the old one destroyed
storage_account_name Replaces the storage account, losing the ingest queue
key_vault_name Replaces the vault. Purge protection means the old name is unusable for the soft-delete retention period
container_environment_name Replaces the Container Apps environment and therefore both container apps
enable_custom_domain to false Destroys the Front Door profile and the managed certificate. Re-enabling means re-validating the domain

Raising the connection budget safely

api_db_pool_max * api_max_replicas must stay at or below db_max_connections minus the containers module's db_connection_reserve (default 15). The API container app enforces this with a precondition, so getting it wrong fails the plan rather than the fleet.

To scale the API out, raise db_max_connections first — in its own apply, accepting the server restart — and only then raise api_max_replicas or api_db_pool_max. The precondition compares variable values only, so a single apply that raises both passes it, while the API can scale out against the old ceiling before the server restart completes.

Note that api_db_pool_max also sets the API's HTTP scale rule concurrency, so changing it moves the point at which a second replica spins up.

Turning OIDC bearer auth on

enable_oidc_api defaults to false and the whole api.tf file is a no-op until it is true. Enabling it in one apply:

  1. Creates a second Entra application and service principal.
  2. Grants the roles listed in oidc_reader_principal_ids, oidc_ingest_principal_ids and oidc_admin_principal_ids.
  3. Sets ENABLE_OIDC_AUTH=true plus the issuer and audience on the API container, which triggers a new revision.

Because app_role_assignment_required is true on the API service principal, nothing can obtain a token until you have populated at least one of the three principal lists. Assign the roles in the same apply that flips the flag.

Migrating an existing deployment onto a newer module

  1. terraform init -upgrade with your backend config.
  2. terraform plan and read every replacement carefully. Anything touching the PostgreSQL server or database will fail on prevent_destroy rather than proceed — that is the intended behaviour, not a bug to work around.
  3. Expect the identifier-URI null_resources to always show as changing.
  4. If the existing deployment includes a Web PubSub service from an older module version, the plan destroys it together with its role assignment and the webpubsub-connection-string vault secret. The current module has no messaging module, and nothing reads those resources.
  5. Apply, then confirm the running image tags did not move. If they did, the ignore_changes blocks were removed and the apply rolled the running image back to the tfvars value.

Clone this wiki locally