This repository is a learning template, not a production-ready baseline. It demonstrates patterns for Azure Functions, Terraform, and GitHub Actions governance. Validate security, compliance, reliability, and operational controls before using any part of this in production.
Template version: 0.2.11 (see VERSION and CHANGELOG.md).
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.
Local workflow files in this repo are wrappers; executable workflow logic is centralized in github_pipeline_governance.
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.cd tests/terraform && go test -v ./... -timeout 30mruns Terraform Terratest checks used byinfra-terratest.yml.
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 wrappers pass env-specific tfvars and Terraform root inputs to centralized reusable workflows.
- 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 wrapper): invokes centralized Terraform validate reusable workflow.infra-terratest.yml(PR/workflow_dispatch wrapper): invokes centralized Terraform Terratest reusable workflow (github_pipeline_governance/.github/workflows/infra-terratest.yml@v1.0.1) for repository-local tests undertests/terraform.infra-plan-dev.yml(PR wrapper): invokes centralized dev speculative plan reusable workflow.infra-plan-prod.yml(PR wrapper): invokes centralized prod speculative plan reusable workflow.infra-apply-dev.yml(push wrapper): invokes centralized dev apply reusable workflow.infra-apply-prod.yml(manual wrapper): invokes centralized prod apply reusable workflow, gated byproductionenvironment.
Application workflows:
app-ci.yml: PR wrapper for centralized CI checks.app-deploy-dev.yml: manual wrapper for centralized dev deploy workflow.app-release.yml: push-to-main wrapper for centralized artifact build/publish workflow.app-deploy-stage.yml: manual wrapper for centralized stage deploy workflow, including release provenance checks.app-swap-slots.yml: manual wrapper for centralized slot swap workflow.
Infrastructure workflows:
infra-validate.yml: PR wrapper for centralized formatting and validation checks.infra-terratest.yml: PR/workflow_dispatch wrapper for centralized Terraform Terratest checks.infra-plan-dev.yml: PR wrapper for centralized dev speculative plan.infra-plan-prod.yml: PR wrapper for centralized prod speculative plan.infra-apply-dev.yml: push wrapper for centralized dev apply.infra-apply-prod.yml: manual wrapper for centralized prod apply.
- Auto apply wrappers are used for lower-risk environments (for example
dev) and trigger onpushtomain. - Manual apply wrappers are used for higher-risk environments (for example
prod) and trigger onworkflow_dispatch. - Wrapper workflows provide environment targeting by passing
environment_name, workspace, and tfvars inputs to centralized reusable workflows. - To add new environments such as
qa,stage, orpreprod, follow the blueprint indocs/infra-environment-expansion.md.
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
Workflows under .github/workflows are wrapper workflows that call reusable workflows from a centralized governance repository pinned by version tag.
Behavior changes should be made in the governance repository and consumed here by tag upgrade.
Migration details: docs/pipeline-governance-migration.md