This repository contains both:
- Azure Function application code (
src/function_app) - Terraform infrastructure code (
infra)
Infrastructure changes are executed through Terraform Cloud remote runs. GitHub Actions orchestrates plans/applies and application delivery.
Identity is separated by environment and privilege.
- Dev deploy identity (
AZURE_CLIENT_ID_DEV_DEPLOY): can deploy non-release packages to the dedicated dev Function App only. - Deploy identity (
AZURE_CLIENT_ID_DEPLOY): can deploy zip artifacts to thestageslot only. - Promotion identity (
AZURE_CLIENT_ID_PROMOTION): can perform slot swap/promotion only.
- Dev plan token (
TF_API_TOKEN_DEV_PLAN): plan access to dev workspace only. - Dev apply token (
TF_API_TOKEN_DEV_APPLY): apply access to dev workspace only. - Prod plan token (
TF_API_TOKEN_PROD_PLAN): plan access to prod workspace only. - Prod apply token (
TF_API_TOKEN_PROD_APPLY): apply access to prod workspace only.
Production credentials/tokens must be stored only in the GitHub production environment.
Application delivery uses two paths: non-release dev deployment and release promotion.
app-ci.yml(PR): lint, dependency install, local runtime smoke test using Azure Functions Core Tools, and packaging validation.app-deploy-dev.yml(manual): build temporary package from selected git ref and deploy directly to dedicated dev Function App (no stage slot).app-release.yml(push tomain): build zip fromsrc/function_appand publish versioned artifact.app-deploy-stage.yml(manual): deploy selected release artifact version to the production pre-production slot (PROD_STAGE_SLOT_NAME) using stage deploy identity.app-swap-slots.yml(manual): swapPROD_STAGE_SLOT_NAMEintoproductionusing promotion identity.
Production traffic is never deployed directly.
- Dev environment uses a single dedicated Function App without deployment slots (
enable_stage_slot = false). - Production environment uses swap-based promotion with a stage slot (
enable_stage_slot = true,stage_slot_name = "stage"). app-deploy-stage.ymlandapp-swap-slots.ymlare for production promotion path only.- Slot swap hardening settings are applied in app configuration:
WEBSITE_OVERRIDE_STICKY_DIAGNOSTICS_SETTINGS=0WEBSITE_OVERRIDE_STICKY_EXTENSION_VERSIONS=0
Use the repository Makefile to align local checks with CI:
make install-devinstalls app and dev tooling dependencies.make lintruns Python lint checks used by CI.make testruns sample unit tests.make packagebuilds the app zip artifact.
Terraform is executed remotely in Terraform Cloud using official HashiCorp GitHub actions.
- Local
terraform plan/applyis not part of delivery. - One workspace per environment:
<repo-name>-dev<repo-name>-prod
- Same Terraform code is used across environments; environment-specific values live in:
infra/env/dev.tfvarsinfra/env/prod.tfvars
- Resource groups are expected to be pre-created by subscription bootstrap and referenced by
resource_group_name. - Plan/apply workflows copy the matching env tfvars file into
infra/terraform.tfvarsbefore uploading configuration to Terraform Cloud. - Remote runs upload from
infraas the Terraform root module. - Provider/module versions are pinned in code and
infra/.terraform.lock.hclis committed for deterministic provider selection.
Infrastructure is provisioned via Azure/avm-ptn-function-app-storage-private-endpoints/azurerm.
- Function App and stage slot use:
- system-assigned managed identity
- HTTPS only
- minimum TLS 1.2
- FTPS disabled
- HTTP/2 enabled
- Storage account uses:
- secure storage defaults from the AVM module
- Optional Key Vault is provisioned via
Azure/avm-res-keyvault-vault/azurermwith secure defaults:public_network_access_enabled = false- private endpoint enabled by default when Key Vault is enabled (
enable_key_vault_private_endpoint = true)
Optional network controls are available per environment via tfvars:
- VNet integration (Function App):
enable_vnet_integrationfunction_app_integration_subnet_id
- Private endpoints:
- shared subnet input:
private_endpoint_subnet_id - storage blob:
enable_storage_private_endpoint,storage_private_dns_zone_id - function app:
enable_function_app_private_endpoint,function_app_private_dns_zone_id - key vault:
enable_key_vault,enable_key_vault_private_endpoint,key_vault_private_dns_zone_id
- shared subnet input:
All are disabled by default and can be enabled per environment. Subnet IDs are expected to reference existing bootstrap network resources.
infra-validate.yml(PR):terraform fmtandterraform validateonly.infra-plan-dev.yml(PR): Terrascan IaC scan + speculative remote plan in dev workspace.infra-plan-prod.yml(PR): Terrascan IaC scan + speculative remote plan in prod workspace.infra-apply-dev.yml(push tomain): remote apply in dev workspace.infra-apply-prod.yml(manual): remote apply in prod workspace, gated byproductionenvironment.
Application workflows:
app-ci.yml: pull request quality gate for app code only (no deploy).app-deploy-dev.yml: manual direct deploy to dedicated dev Function App using non-release package.app-release.yml: builds and publishes versioned app artifact onmain.app-deploy-stage.yml: manual deploy of selected artifact version to the fixed production pre-production slot target.app-swap-slots.yml: manual fixed pre-production slot toproductionswap.
Infrastructure workflows:
infra-validate.yml: PR formatting and validation checks only.infra-plan-dev.yml: PR speculative remote plan for dev workspace.infra-plan-prod.yml: PR speculative remote plan for prod workspace.infra-apply-dev.yml: automatic remote apply on merge tomainfor dev.infra-apply-prod.yml: manual remote apply for prod, gated byproduction.
Promotion to production is a controlled, manual step.
- Build and publish a versioned artifact from
main. - Manually deploy that artifact to the pre-production slot (
PROD_STAGE_SLOT_NAME). - Validate behavior in the pre-production slot.
- Manually run slot swap to promote that slot to production.
This process provides deterministic promotion and a clear audit trail.
Operational procedure reference: docs/production-promotion-runbook.md
Complete the following before running workflows.
Create environments:
devproduction
Configure production with:
- Required reviewers for manual approvals.
- Restricted environment secrets.
- Promotion and prod-apply credentials only.
Repository-level (or dev environment where preferred):
AZURE_TENANT_IDAZURE_SUBSCRIPTION_IDAZURE_CLIENT_ID_DEV_DEPLOY(dev app deploy only)AZURE_CLIENT_ID_DEPLOY(non-prod scope where possible)TF_API_TOKEN_DEV_PLANTF_API_TOKEN_DEV_APPLYTF_API_TOKEN_PROD_PLAN(restrict appropriately)
production environment only:
AZURE_CLIENT_ID_PROMOTIONTF_API_TOKEN_PROD_APPLY- Any additional prod-only credentials
Set these in the production environment to hard-lock deployment targets:
PROD_FUNCTION_APP_NAMEPROD_RESOURCE_GROUP_NAMEPROD_STAGE_SLOT_NAME(must match Terraformstage_slot_namefor production)
Configure branch protection on main to require:
- Pull request review approval
- Required status checks for CI and Terraform plan workflows
- No direct pushes by default
Reference baseline: .github/branch-protection.md
version-bump-check.ymlenforces thatVERSIONmust be updated in any PR that changessrc/**orinfra/**.
- Create organization and workspaces matching naming convention:
<repo-name>-dev<repo-name>-prod
- Configure workspace permissions/tokens to enforce plan/apply separation.
- Ensure workspace variable sets do not duplicate env-specific tfvars values.
- Ensure pre-created resource groups exist and are set in
infra/env/*.tfvars. - Storage account naming is deterministic and globally unique per environment/subscription:
- prefix derives from
project_name+ workspace - suffix derives from hash(workspace + subscription ID)
- prefix derives from
- If enabling network isolation features, ensure required VNet/subnets/private DNS zones already exist and pass their IDs in tfvars.
- Configure federated credentials for GitHub OIDC on deploy and promotion app registrations.
- Scope role assignments to least privilege:
- deploy identity: stage slot deployment permissions only
- promotion identity: slot swap permissions only
CODEOWNERS enforces separation of responsibilities:
infra/**owned by cloud engineeringsrc/**andtests/**owned by application engineering.github/workflows/**andCODEOWNERSprotected by shared ownership