π¨ Important Notice: This template is currently under active development and should be considered a DRAFT version. Features, configurations, and documentation may change without notice. Use in production environments is not recommended at this time.
A production-ready, secure, and compliant infrastructure template for deploying containerized applications to Azure Landing Zone environments. This template follows Azure Landing Zone security guardrails and BC Government cloud deployment best practices.
- Full-stack containerized application: NestJS backend + React/Vite frontend
- Secure Azure infrastructure: Landing Zone compliant with proper network isolation
- Database management: PostgreSQL with Flyway migrations and optional CloudBeaver admin UI
- CI/CD pipeline: GitHub Actions with OIDC authentication
- Infrastructure as Code: Terraform with Terragrunt for multi-environment management
- Monitoring & observability: Azure Monitor, Application Insights, and comprehensive logging
- Security best practices: Managed identities, private endpoints, and network security groups
- Azure CLI v2.50.0+ - Installation Guide
- GitHub CLI v2.0.0+ - Installation Guide
- Terraform v1.5.0+ - Installation Guide
- Docker or Podman - Docker Installation
- BCGOV Azure account with appropriate permissions - Registry Link
- GitHub repository with Actions enabled
- Azure subscription with Owner or Contributor role
- Access to Azure Landing Zone with network connectivity configured
/quickstart-azure-containers
βββ .github/ # GitHub Actions CI/CD workflows
β βββ workflows/
β βββ pr-open.yml # PR validation and deployment
β βββ pr-close.yml # PR cleanup
β βββ pr-validate.yml # Code quality checks
β βββ prune-env.yml # Environment cleanup
βββ infra/ # Terraform infrastructure code
β βββ main.tf # Root configuration
β βββ providers.tf # Azure provider configuration
β βββ variables.tf # Global variables
β βββ outputs.tf # Infrastructure outputs
β βββ modules/ # Reusable infrastructure modules
β βββ backend/ # App Service for NestJS API
β βββ frontend/ # App Service for React SPA
β βββ postgresql/ # PostgreSQL Flexible Server
β βββ flyway/ # Database migration service
β βββ network/ # VNet, subnets, NSGs
β βββ monitoring/ # Log Analytics, App Insights
β βββ frontdoor/ # Azure Front Door CDN
βββ backend/ # NestJS TypeScript API
β βββ src/ # API source code
β β βββ users/ # User management module
β β βββ common/ # Shared utilities
β β βββ middleware/ # Request/response middleware
β βββ prisma/ # Database ORM configuration
β β βββ schema.prisma # Database schema definition
β βββ test/ # E2E API tests
β βββ Dockerfile # Container build configuration
βββ frontend/ # React + Vite SPA
β βββ src/ # Frontend source code
β β βββ components/ # React components
β β βββ routes/ # Application routes
β β βββ services/ # API integration
β β βββ interfaces/ # TypeScript interfaces
β βββ e2e/ # Playwright end-to-end tests
β βββ public/ # Static assets
β βββ Dockerfile # Container build configuration
βββ migrations/ # Flyway database migrations
β βββ sql/ # SQL migration scripts
β βββ Dockerfile # Migration runner container
β βββ entrypoint.sh # Migration execution script
βββ docker-compose.yml # Local development stack
βββ initial-azure-setup.sh # Azure setup automation script
βββ package.json # Monorepo configuration
# Use this template to create a new repository
gh repo create my-azure-app --template bcgov/quickstart-azure-containers --public
# Clone your new repository
git clone https://github.com/your-org/my-azure-app.git
cd my-azure-appThe initial-azure-setup.sh script automates the complete Azure environment setup with OIDC authentication for GitHub Actions.
- Azure CLI logged in (
az login) - GitHub CLI (optional, for automatic secret creation)
- Azure subscription with appropriate permissions
- Existing Azure Landing Zone resource group
# Make the setup script executable
chmod +x initial-azure-setup.sh- follow the instruction in the header section of the file.
π Identity & Authentication:
- Creates a user-assigned managed identity in your Landing Zone resource group
- Configures OIDC federated identity credentials for GitHub Actions
- Sets up environment-specific authentication (no secrets stored in Azure)
πΎ Terraform State Management:
- Creates a secure Azure storage account for Terraform state files
- Enables blob versioning for state file protection
- Configures appropriate access permissions for the managed identity
π GitHub Integration:
- Automatically creates GitHub environment if
--create-github-secretsis used - Sets up required secrets in your GitHub repository:
AZURE_CLIENT_IDAZURE_TENANT_IDAZURE_SUBSCRIPTION_IDVNET_NAME(derived from resource group)VNET_RESOURCE_GROUP_NAME
β‘ Azure Permissions:
- Assigns security group to the managed identity aligned with landing zone policy.
- Configures storage-specific permissions for Terraform state management
- Validates all configurations and provides verification
After running the script, verify the setup:
# Check managed identity was created
az identity show --name "my-app-github-identity" --resource-group "ABCD-dev-networking"
# Verify federated credentials
az identity federated-credential list --identity-name "my-app-github-identity" --resource-group "ABCD-dev-networking"
# Test GitHub Actions authentication (in your repository)
gh workflow run test-azure-connection # if you have a test workflowIf you didn't use the --create-github-secrets flag, manually add the following secrets to your GitHub repository (Settings > Secrets and variables > Actions > Environment secrets):
AZURE_CLIENT_ID=<managed-identity-client-id>
AZURE_TENANT_ID=<your-azure-tenant-id>
AZURE_SUBSCRIPTION_ID=<your-azure-subscription-id>
VNET_NAME=<landing-zone-vnet-name>
VNET_RESOURCE_GROUP_NAME=<landing-zone-rg-name>π‘ Tip: The setup script outputs the exact values to use for these secrets if you didn't use auto-creation.
# Install dependencies for all packages
npm install
# Start local development environment
docker-compose up -d
# Run database migrations
docker-compose exec migrations flyway migrate
# Start backend development server
cd backend && npm run start:dev
# Start frontend development server (in new terminal)
cd frontend && npm run devAccess your local application:
- Frontend: http://localhost:5173
- Backend API: http://localhost:3000 (default; see
docker-compose.ymlfor overrides) - Database: localhost:5432 (postgres/default)
The repository includes comprehensive CI/CD workflows:
# Triggered on: Pull Request creation
# Actions:
# 1. Build and test frontend/backend containers
# 2. Run security scans and linting
# 3. Plan Terraform infrastructure changes
# 4. Ability to manually deploy for testing to tools
# 5. Run end-to-end tests# Triggered on: Merge to main branch
# Actions:
# 1. Build and push production containers
# 2. Deploy to staging environment
# 3. Run full test suite
# 4. Deploy to production (with approval)# Navigate to environment configuration
cd terragrunt/dev # or test/prod
# Initialize and plan
terragrunt init
terragrunt plan
# Apply changes
terragrunt applyThe template uses Flyway for database schema management:
-- V1.0.0__init.sql
CREATE SCHEMA IF NOT EXISTS app;
CREATE TABLE app.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);# Local development
docker-compose exec migrations flyway migrate
# Production (via container)
docker run --rm \
-v $(pwd)/migrations/sql:/flyway/sql:ro \
-e FLYWAY_URL=jdbc:postgresql://your-db:5432/app \
-e FLYWAY_USER=your-user \
-e FLYWAY_PASSWORD=your-password \
flyway/flyway:11-alpine migrateOptional CloudBeaver container provides web-based database management:
- Access:
https://your-app-cloudbeaver.azurewebsites.net - Features: Query editor, schema browser, data export/import
- Private endpoints for all Azure services
- Network Security Groups with least-privilege rules
- Azure Front Door with WAF protection
- VNet integration for App Services
- Managed identities for service-to-service authentication
- OIDC authentication for GitHub Actions (no stored credentials)
- HTTPS everywhere with TLS 1.3 minimum
- Security headers (HSTS, CSP, X-Frame-Options)
- Container scanning in CI/CD pipeline
resource "azurerm_linux_web_app" "backend" {
# ... other configuration
site_config {
minimum_tls_version = "1.3"
ftps_state = "Disabled"
# IP restrictions for enhanced security
ip_restriction {
service_tag = "AzureFrontDoor.Backend"
action = "Allow"
priority = 100
headers {
x_azure_fdid = [var.frontend_frontdoor_resource_guid]
}
}
ip_restriction {
name = "DenyAll"
action = "Deny"
priority = 500
ip_address = "0.0.0.0/0"
}
}
}resource "azurerm_application_insights" "main" {
name = "${var.app_name}-appinsights"
location = var.location
resource_group_name = var.resource_group_name
application_type = "web"
workspace_id = azurerm_log_analytics_workspace.main.id
}Azure PostgreSQL Flexible Server automatically supports point-in-time restore (PITR) to any moment within the configured backup retention window (postgres_backup_retention_period).
Key points:
- PITR window = retention days (7β35) you set in Terraform.
- Geo-redundant backup (
postgres_geo_redundant_backup_enabled = true) improves DR but adds cost. - Restores create a new server; you then repoint apps / rotate connection strings.
Restore example (CLI):
az postgres flexible-server restore \
--resource-group <rg> \
--name <new-server-name> \
--source-server <current-server-name> \
--restore-time "2025-08-12T15:04:05Z"Variables controlling verbosity:
postgres_enable_server_logs: Master toggle for connection / duration logging.postgres_log_statement_mode: none | ddl | mod | all (default ddl). Avoidallin production unless debugging.postgres_log_min_duration_statement_ms: Slow query threshold (default 500 ms). Lower value = more logs & cost.postgres_track_io_timing: Enables IO timing (slight overhead, useful for perf diagnostics).postgres_pg_stat_statements_max: Controls number of statements tracked; higher values consume more memory.
Recommendations:
| Scenario | log_statement | log_min_duration_statement_ms | Notes |
|---|---|---|---|
| Prod steady state | ddl | 500β1000 | Focus on schema changes + slow queries |
| Perf investigation | mod or all | 100β250 | Temporarily increase verbosity |
| Heavy cost pressure | none | 1000β2000 | Minimize ingestion volume |
If you disable full statement logging (none/ddl) ensure slow query threshold captures problematic queries (set <= 1000 ms initially).
Metric alerts are enabled when postgres_alerts_enabled = true. Customize or add alerts via postgres_metric_alerts map. Default keys: cpu_percent, storage_used, active_connections.
Example override in terraform.tfvars:
postgres_alerts_enabled = true
postgres_alert_emails = ["[email protected]", "[email protected]"]
postgres_metric_alerts = {
cpu_percent = {
metric_name = "cpu_percent"
operator = "GreaterThan"
threshold = 75
aggregation = "Average"
description = "CPU > 75% (tuned)"
}
failed_connections = {
metric_name = "connections_failed"
operator = "GreaterThan"
threshold = 5
aggregation = "Total"
description = "Failed connections spike"
}
}Supported metric names (common): cpu_percent, storage_used, active_connections, connections_failed, deadlocks, serverlog_storage_percent.
Action Group:
- Created only if
postgres_alert_emailsis non-empty. - Add/remove emails without recreating alerts (resource uses dynamic receivers).
If postgres_ha_enabled = true, Terraform validates that postgres_sku_name starts with GP_ or MO_ (General Purpose / Memory Optimized). Adjust SKU before enabling HA to avoid apply failure.
resource "azurerm_log_analytics_workspace" "main" {
name = "${var.app_name}-log-analytics"
location = var.location
resource_group_name = var.resource_group_name
sku = var.log_analytics_sku
retention_in_days = var.log_analytics_retention_days
}Access monitoring through:
- Azure Portal: Resource group > Monitoring
- Application Insights: Performance, failures, dependencies
- Log Analytics: Custom queries and alerts
- Azure Monitor: Infrastructure metrics and alerts
The GitHub Actions workflows include:
- Unit tests for frontend and backend
- Integration tests with test database
- E2E tests in containerized environment
- Security scanning of dependencies and containers
- Performance testing with load simulation
The template supports multiple environments with Terragrunt:
terragrunt/
βββ terragrunt.hcl # Root configuration
βββ dev/
β βββ terragrunt.hcl # Development overrides
βββ test/
β βββ terragrunt.hcl # Testing overrides
βββ prod/
β βββ terragrunt.hcl # Production overrides
βββ tools/
βββ terragrunt.hcl # Tools/utilities environment
include "root" {
path = find_in_parent_folders()
}
inputs = {
app_service_sku_name_backend = "B1"
app_service_sku_name_frontend = "B1"
postgres_sku_name = "B_Standard_B1ms"
backend_autoscale_enabled = false
enable_cloudbeaver = true
}include "root" {
path = find_in_parent_folders()
}
inputs = {
app_service_sku_name_backend = "P1V3"
app_service_sku_name_frontend = "P1V3"
postgres_sku_name = "GP_Standard_D2s_v3"
backend_autoscale_enabled = true
enable_cloudbeaver = false
postgres_ha_enabled = true
}Issue: OIDC authentication fails
Error: No subscription found. Run 'az account set' to select a subscription.
Solution:
- Verify
AZURE_CLIENT_ID,AZURE_TENANT_ID, andAZURE_SUBSCRIPTION_IDsecrets - Ensure managed identity has proper federated credentials
- Check that repository URL matches federated identity configuration
Issue: State file conflicts or locks
Error: Error acquiring the state lock
Solution:
# Force unlock (use with caution)
terragrunt force-unlock <lock-id>
# Or check Azure storage account permissions
az storage blob list --account-name your-storage --container-name tfstateIssue: App Service fails to pull container (if using ACR)
Error: Failed to pull image: unauthorized
Solution:
- Verify managed identity has
AcrPullrole on container registry - Check container registry URL in app settings
- Ensure container image exists and is accessible
Issue: Backend cannot connect to PostgreSQL
Error: getaddrinfo ENOTFOUND your-postgres-server
Solution:
- Verify VNet integration and private endpoint configuration
- Check PostgreSQL firewall rules
- Ensure connection string environment variables are correct
- if you are using pgpool make sure you have this line
ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false,
# Enable debug logging
az config set core.log_level=debug
# Check resource status
az webapp show --name your-app --resource-group your-rg
# View app service logs
az webapp log tail --name your-app --resource-group your-rg- Terraform Azure Provider
- Terragrunt Documentation
- NestJS Documentation
- React + Vite Documentation
- Prisma Documentation
We welcome contributions to improve this template! Please see our Contributing Guidelines for details.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Built with β€οΈ by the NRIDS Team