Skip to main content
Version: 0.3.0

Terraform — Azure

Version: 0.2.0

Provision a production-ready AKS cluster for DecisionBox using the included Terraform module.

What It Creates

ResourceDescription
Resource GroupDedicated RG for all DecisionBox resources
VNet + SubnetDedicated network with node subnet
NAT GatewayOutbound internet access for private nodes
NSGNetwork Security Group with SSH deny-by-default
AKS clusterWorkload Identity, OIDC issuer, Azure CNI, auto-scaling
Node poolAuto-scaling with configurable VM size and disk
Managed IdentitiesAPI identity (secrets read/write) + Agent identity (read-only)
Key VaultSecret storage with RBAC authorization (optional)
Log AnalyticsContainer Insights workspace (optional)

Prerequisites

  • Terraform 1.5+
  • Azure CLI authenticated (az login)
  • Azure subscription with sufficient quota
  • Sufficient permissions (Owner or Contributor + User Access Administrator)

Quick Start with Setup Wizard

The included setup wizard handles Terraform state, cluster provisioning, and Helm deployment in one flow:

cd terraform
./setup.sh # Full interactive setup
./setup.sh --dry-run # Generate config files only
./setup.sh --resume # Resume from Helm deploy

The wizard prompts for:

  1. Cloud provider (Azure)
  2. Secret namespace prefix
  3. Secret provider (Azure Key Vault or MongoDB)
  4. Azure subscription ID, region, cluster name
  5. Terraform state (Azure Storage Account, auto-creates if needed)
  6. VM size and node scaling
  7. Key Vault (optional)
  8. SECRET_ENCRYPTION_KEY (auto-generates or user-provided)

After provisioning, it automatically:

  • Configures kubectl credentials via az aks get-credentials
  • Creates the Kubernetes namespace and secrets
  • Deploys API and Dashboard via Helm

Manual Setup

1. Create State Backend

# Create resource group for Terraform state
az group create --name terraform-state-rg --location eastus

# Create storage account (name must be globally unique)
az storage account create \
--name decisionboxstate \
--resource-group terraform-state-rg \
--sku Standard_LRS \
--encryption-services blob

# Create blob container
az storage container create \
--name terraform \
--account-name decisionboxstate

2. Configure Variables

cd terraform/azure/prod
cp terraform.tfvars terraform.tfvars.local

Edit terraform.tfvars.local:

subscription_id     = "your-subscription-id"
location = "eastus"
cluster_name = "decisionbox-prod"
resource_group_name = "decisionbox-prod-rg"

# AKS node pool
vm_size = "Standard_D2s_v5"
min_node_count = 3
max_node_count = 3

# Workload Identity
k8s_namespace = "decisionbox"

# Optional features
enable_key_vault = true

# Optional: Restrict HTTP/HTTPS to specific IPs (empty = unrestricted)
# allowed_ip_ranges = ["203.0.113.0/24", "198.51.100.0/24"]

3. Initialize and Plan

terraform init \
-backend-config="resource_group_name=terraform-state-rg" \
-backend-config="storage_account_name=decisionboxstate" \
-backend-config="container_name=terraform" \
-backend-config="key=prod/terraform.tfstate"

terraform plan -var-file=terraform.tfvars.local

4. Apply (requires human approval)

terraform apply -var-file=terraform.tfvars.local

5. Configure kubectl

az aks get-credentials \
--resource-group decisionbox-prod-rg \
--name decisionbox-prod

6. Deploy DecisionBox via Helm

See Kubernetes deployment guide for Helm chart installation.

The Terraform outputs provide the values needed for Helm:

# Get managed identity client IDs for Workload Identity annotations
terraform output api_identity_client_id
terraform output agent_identity_client_id

# Get Key Vault URI (if enabled)
terraform output key_vault_uri

Module Reference

Variables

VariableTypeDefaultDescription
subscription_idstringAzure subscription ID (required)
locationstringeastusAzure region
cluster_namestringdecisionbox-prodAKS cluster name
resource_group_namestringdecisionbox-prod-rgResource group name
create_vnetbooltrueCreate a new VNet
vnet_cidrstring10.0.0.0/16VNet CIDR range
node_subnet_cidrstring10.0.0.0/20Node subnet CIDR
enable_nat_gatewaybooltrueCreate NAT Gateway
create_clusterbooltrueCreate a new AKS cluster
kubernetes_versionstringnullK8s version (null = latest stable)
sku_tierstringFreeAKS SKU (Free or Standard)
vm_sizestringStandard_D2s_v5Node VM size
os_disk_size_gbnumber50OS disk size
min_node_countnumber3Min nodes (auto-scaling)
max_node_countnumber3Max nodes (auto-scaling)
private_cluster_enabledboolfalsePrivate API server
api_server_authorized_rangeslist(string)[]API server allow-list
allowed_ip_rangeslist(string)[]CIDR blocks allowed for HTTP/HTTPS. Empty = unrestricted (NSG allows Internet).
k8s_namespacestringdecisionboxK8s namespace
k8s_service_accountstringdecisionbox-apiAPI K8s SA name
k8s_agent_service_accountstringdecisionbox-agentAgent K8s SA name (read-only)
enable_key_vaultboolfalseCreate Key Vault
enable_oms_agentbooltrueEnable Container Insights
tagsmap(string){}Resource tags

Outputs

OutputDescription
cluster_nameAKS cluster name
cluster_fqdnAKS cluster FQDN
resource_group_nameResource group name
api_identity_client_idAPI managed identity client ID
agent_identity_client_idAgent managed identity client ID
key_vault_uriKey Vault URI (empty if not enabled)
key_vault_enabledWhether Key Vault was created

Architecture

Azure Subscription
├── Resource Group (decisionbox-prod-rg)
│ ├── VNet (decisionbox-prod-vnet)
│ │ └── Subnet (decisionbox-prod-nodes)
│ │ └── NSG (SSH deny-by-default)
│ ├── NAT Gateway + Public IP
│ ├── AKS Cluster (decisionbox-prod)
│ │ ├── Default Node Pool (Standard_D2s_v5, 3 nodes)
│ │ ├── Workload Identity (OIDC issuer)
│ │ └── Container Insights → Log Analytics
│ ├── Key Vault (decisionbox-prod-kv)
│ │ ├── API identity → Secrets Officer
│ │ └── Agent identity → Secrets User (read-only)
│ └── Managed Identities
│ ├── decisionbox-prod-api (federated to K8s SA)
│ └── decisionbox-prod-agent (federated to K8s SA)

└── Storage Account (Terraform state)
└── terraform/prod/terraform.tfstate

Using an Existing VNet

Set create_vnet = false and provide the existing resource IDs:

create_vnet        = false
existing_vnet_id = "/subscriptions/.../resourceGroups/.../providers/Microsoft.Network/virtualNetworks/my-vnet"
existing_subnet_id = "/subscriptions/.../resourceGroups/.../providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/aks-subnet"

The existing subnet must have sufficient address space for AKS nodes.

Using an Existing Cluster

Set create_cluster = false to skip cluster creation. Terraform will only create managed identities and Key Vault:

create_cluster = false
cluster_name = "my-existing-aks"

The existing cluster must have:

  • OIDC issuer enabled (--enable-oidc-issuer)
  • Workload Identity enabled (--enable-workload-identity)

Security Defaults

  • Private nodes: Nodes have no public IPs (outbound via NAT Gateway)
  • SSH denied: NSG denies SSH by default (configurable via nsg_allowed_ssh_cidrs)
  • RBAC: Key Vault uses Azure RBAC (not access policies)
  • Least privilege: Agent identity gets read-only Key Vault access
  • Workload Identity: No long-lived credentials — pods authenticate via federated tokens
  • Soft delete: Key Vault soft delete with configurable retention
  • Purge protection: Disabled by default for development flexibility

Cost Optimization

ResourceDefaultMonthly Estimate
AKS control plane (Free tier)Free$0
3x Standard_D2s_v5 nodes2 vCPU, 8 GB each~$210
NAT GatewayStandard~$32
Public IP (NAT)Standard/Static~$4
Key VaultStandard~$0.03/10k ops
Log Analytics30-day retention~$2.76/GB
Total~$250/mo

Scale down for development:

vm_size        = "Standard_B2s"       # Burstable, ~$30/mo each
min_node_count = 1
max_node_count = 1
enable_oms_agent = false # Skip Log Analytics
sku_tier = "Free" # No uptime SLA

Next Steps