Read the full technical breakdown on my blog: Infrastructure as Code: Terraform for Fintech Compliance on Microsoft Azure View the architecture and project details: Fintech Compliance Platform on Microsoft Azure
Terraform-managed cloud infrastructure for a regulated financial services platform on Microsoft Azure. Built to satisfy PCI DSS, SOC 2, and New Zealand financial compliance requirements including the RBNZ BS11 Outsourcing Policy and Privacy Act 2020 from the first infrastructure decision. Every resource operates within a private network boundary with no public endpoints, zero hardcoded credentials, and a tamper-proof audit trail enforced at the infrastructure level.
Compliance is a design constraint in this architecture, not a retrofit. When a regulator asks how encryption is guaranteed or how access is controlled, the answer is Terraform code committed to version control, not a policy document assembled after the fact.
Building this platform required solving six consecutive security and compliance problems. Here is the logical progression of how the architecture was designed:
Financial infrastructure in New Zealand operates under a layered compliance regime. This architecture encodes the following regulatory requirements directly into infrastructure code:
| Framework | Requirement | Terraform Control |
|---|---|---|
| PCI DSS | Network segmentation, encryption, audit logging | Hub and Spoke topology, TDE, WORM storage |
| Privacy Act 2020 | Encryption, access control, data residency | Private Endpoints, RBAC, region-pinned resources |
| RBNZ BS11 | Auditability, outsourcing risk control | Immutable logs, Git-tracked deployment history |
| AML/CFT Act | Transaction recording and monitoring | Service Bus session ordering, SQL audit logs |
| SOC 2 CC7 | Tamper-proof audit trail | 365-day WORM immutability policy |
| ISO 27001 | Least-privilege access, secrets management | Managed Identity, Key Vault RBAC |
Internet
│
▼
┌─────────────────────────────┐
│ Application Gateway (WAF) │ Terminates public HTTPS, blocks OWASP threats
└─────────────┬───────────────┘
│ Clean traffic only, rt-appgw routes to Internet
▼
┌───────────────────────────────────────────────────────────────────┐
│ SPOKE VNet 10.0.0.0/16 │
│ │
│ ┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ AKS Cluster │ │ SQL Subnet │ │ AppGW Subnet │ │
│ │ snet-aks │───▶│ snet-sql │ │ snet-appgw │ │
│ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │ 10.0.3.0/24 │ │
│ └────────┬────────┘ └──────────────┘ └──────────────────┘ │
│ │ rt-spoke: 0.0.0.0/0 → Firewall IP │
└───────────┼───────────────────────────────────────────────────────┘
│ VNet Peering (bidirectional, allow forwarded traffic)
┌───────────┼───────────────────────────────────────────────────────┐
│ HUB VNet 10.10.0.0/16 │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
│ │ Azure Firewall │ │ Azure Bastion │ │
│ │ AzureFirewall │ │ AzureBastionSubnet │ │
│ │ Subnet │ │ Secure RDP/SSH only │ │
│ └─────────────────────┘ └──────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
This single transaction traces every layer of the architecture from the moment a customer clicks Transfer to the moment the money moves.
CUSTOMER BROWSER
│
│ POST /api/transfer {"from": "ACC001", "to": "ACC002", "amount": 5000}
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 1 Application Gateway (WAF) │
│ │
│ Public IP receives the HTTPS request. │
│ WAF Policy (OWASP 3.2, Prevention Mode) scans: │
│ ✓ No SQL injection patterns │
│ ✓ No XSS payloads │
│ ✓ No malformed headers │
│ │
│ Request is clean. Forwarded internally. │
└──────────────────────┬───────────────────────────────────┘
│ Private IP routing (snet-appgw to snet-aks)
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 2 AKS Pod (Transfer Service) │
│ │
│ The transfer-service pod receives the request. │
│ It needs the database password to write the record. │
│ It calls Azure Key Vault using its Managed Identity │
│ token. No username, no password hardcoded anywhere. │
│ │
│ Key Vault returns: sql-admin-password = "xK9#mP2..." │
└──────────────────────┬───────────────────────────────────┘
│ Private Endpoint (pe-kv-dev, inside snet-sql)
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 3 Azure SQL (Private Endpoint) │
│ │
│ Pod connects to sql-fintech-dev.database.windows.net │
│ This resolves to a private IP inside snet-sql. │
│ TDE encrypts the row on disk automatically. │
│ │
│ SQL writes: INSERT INTO transfers VALUES (...) │
│ Status: PENDING │
└──────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 4 Service Bus (Ordered Queue) │
│ │
│ Pod publishes event to Service Bus queue. │
│ Queue: transactions-ordered │
│ SessionId: "ACC001" (guarantees per-account FIFO order) │
│ Message: { transferId: "TXN-9981", amount: 5000 } │
│ │
│ Decouples API response from actual processing. │
│ Customer receives instant Transfer Submitted receipt. │
└──────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 5 AKS Pod (Settlement Worker) │
│ │
│ A separate worker pod picks up the message from │
│ Service Bus in guaranteed FIFO order per account. │
│ It deducts from ACC001, credits ACC002. │
│ Updates SQL: Status = COMPLETED │
│ │
│ Container image pulled from ACR (private registry) │
│ using AKS Managed Identity. No Docker credentials. │
└──────────────────────┬───────────────────────────────────┘
│ Egress: any outbound call (e.g. external rate API)
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 6 Azure Firewall (Egress Gate) │
│ │
│ Route table on snet-aks forces all egress through │
│ Azure Firewall in the Hub VNet. │
│ │
│ Firewall checks Application Rule Collection: │
│ ✓ *.azure.com ALLOW │
│ ✗ *.unknown-domain.com DENY and LOG │
│ │
│ Only pre-approved Azure APIs are reachable. │
└──────────────────────┬───────────────────────────────────┘
│ All events written to:
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 7 Immutable Audit Log (WORM Storage) │
│ │
│ Every firewall decision, every Key Vault secret read, │
│ every SQL transaction is logged to Storage Account. │
│ │
│ Immutability Policy: 365 days, locked. │
│ No principal, including subscription Owners, can │
│ delete these logs. Satisfies PCI DSS Requirement 10. │
└──────────────────────────────────────────────────────────┘
This single transaction touched every service in the architecture: Application Gateway WAF → AKS → Key Vault → SQL → Service Bus → AKS Worker → Azure Firewall → Audit Storage.
When a request arrives at the public IP, the Application Gateway WAF Policy (OWASP 3.2, Prevention Mode) inspects it for SQL injection, XSS, and all OWASP Top 10 attack patterns. Only clean requests are forwarded over the private network to the AKS backend pool. Malicious requests are dropped at the edge before they reach the application.
The AKS cluster runs with private_cluster_enabled = true, meaning the Kubernetes API server has no public IP. It uses a SystemAssigned Managed Identity, so no passwords exist for the cluster itself. Azure grants it access to ACR and Key Vault via RBAC role assignments scoped to exactly those resources.
Every outbound packet from a pod hits the Route Table on snet-aks which has a single rule directing all traffic to the Azure Firewall Private IP. Traffic crosses the VNet Peering into the Hub VNet and is inspected by Azure Firewall against the Application Rule Collection. Only pre-approved Azure API hostnames are allowed. Everything else is denied and logged.
Terraform generates a random 20-character SQL admin password. That password is stored as a Key Vault Secret. Key Vault is accessible only via a Private Endpoint inside snet-sql. The AKS Managed Identity is granted the Key Vault Secrets User RBAC role — it can read the secret at runtime but cannot modify or delete it. No passwords appear in code, config files, or environment variables.
Azure SQL has public_network_access_enabled = false. Its DNS resolves only to a private IP inside the VNet via a Private Endpoint. All data on disk is encrypted via Transparent Data Encryption. TLS 1.2 is enforced at the protocol level. Daily backups run at 23:00 and are retained for 30 days via Recovery Services Vault.
Financial transactions are published to a Service Bus Premium queue with requires_session = true. This guarantees per-account FIFO ordering so debits are always processed before credits for the same account. The Premium SKU supports Private Endpoints so message payloads never cross the public internet.
The CI/CD pipeline pushes Docker images to ACR with admin_enabled = false. The AKS cluster is assigned the AcrPull role and authenticates using its Managed Identity token. public_network_access_enabled = false means image pulls happen entirely within the private network.
All diagnostic logs land in a Storage Account with a 365-day immutability policy (Write Once Read Many). Once written, no principal including subscription Owners can delete or modify those logs. This satisfies PCI DSS Requirement 10 and SOC 2 CC7 for tamper-proof audit trails.
.
├── environments/
│ └── dev/
│ └── terraform.tfvars # Dev environment variable values
├── modules/
│ ├── acr/ # Azure Container Registry + AKS role assignment
│ ├── aks/ # Private AKS cluster + network profile
│ ├── appgw/ # Application Gateway v2 + standalone WAF Policy
│ ├── hub/ # Azure Firewall + Bastion + Hub VNet
│ ├── keyvault/ # Key Vault + random password + RBAC + Private Endpoint
│ ├── network/ # Spoke VNet, subnets, NSG, UDR route tables
│ ├── observability/ # Immutable Storage + Recovery Services Vault
│ ├── servicebus/ # Service Bus Premium + ordered session queue
│ └── sql/ # SQL Server + Database + Private Endpoint
├── main.tf # Root module wiring all components and VNet peerings
├── provider.tf # AzureRM and Random provider configuration
├── variables.tf # Root variable declarations
└── README.md
| Control | Implementation |
|---|---|
| No public endpoints | public_network_access_enabled = false on SQL, ACR, Key Vault, Service Bus |
| No hardcoded secrets | random_password stored in Key Vault, retrieved at runtime via Managed Identity |
| Egress filtering | UDR on snet-aks and snet-sql forces all outbound traffic through Azure Firewall |
| WAF protection | Standalone WAF Policy (OWASP 3.2, Prevention mode) linked to Application Gateway |
| Encryption at rest | TDE on SQL, automatic Storage Account encryption |
| Encryption in transit | TLS 1.2 minimum enforced across all services |
| Immutable audit trail | WORM storage with 365-day lock satisfies PCI DSS Requirement 10 |
| Least-privilege identity | SystemAssigned Managed Identity with scoped RBAC role assignments |
| Private DNS resolution | Private Endpoints on SQL and Key Vault provide private IPs inside the VNet |
| AppGW subnet routing | Dedicated route table with 0.0.0.0/0 next hop Internet, required for AGW v2 |
Terraform CLI 1.5 or higher and Azure CLI installed, with an active Azure subscription where the deploying principal holds Owner or Contributor plus User Access Administrator roles.
Authenticate to Azure:
az login
az account set --subscription "YOUR_SUBSCRIPTION_ID"
Initialize Terraform:
terraform init
Review the execution plan:
terraform plan -var-file="environments/dev/terraform.tfvars"
Apply the infrastructure (full deployment takes approximately 15 to 20 minutes):
terraform apply -var-file="environments/dev/terraform.tfvars"
Destroy when done:
terraform destroy -var-file="environments/dev/terraform.tfvars"
| Variable | Description | Example |
|---|---|---|
location |
Azure region for all resources | australiaeast |
environment |
Environment tag appended to all resource names | dev |
resource_group_name |
Resource group containing all deployed resources | rg-fintech-dev |
vnet_address_space |
Spoke VNet CIDR block | ["10.0.0.0/16"] |
aks_node_count |
Number of AKS worker nodes | 2 |
aks_vm_size |
VM size for each worker node | Standard_D2s_v3 |