iran@portfolio:~$ cat projects/azure-windows-vm-terraform-stack
~/projects/azure-windows-vm-terraform-stack
IaCTerraformAzure

Azure Windows VM Terraform Stack

A reusable, modular Terraform stack that stamps out consistent 3-server Azure environments, web, job, and a SQL Server database server, from a single map variable, replacing a manual Azure portal deployment wizard with per-server NIC-level security and per-deployment state isolation.

IaC · Factory patternProduction
// key highlights
[01]

Solely designed and built a reusable Terraform stack replacing manual Azure portal deployment for 3-server (web, job, SQL Server database) clusters

[02]

Designed a factory/stamping model using map(object(...)) input variables and for_each, so adding a server is a single variable entry instead of new resource code

[03]

Enforced NIC-level, per-server NSG rules instead of subnet-level policy, using a dynamic security_rule block to generate HTTP/HTTPS access only for the servers that need it

[04]

Configured SQL Server VMs with a public IP for stack consistency but zero open inbound NSG rules and private-only SQL connectivity through the SQL IaaS Agent, keeping the database unreachable from outside despite the public IP

[05]

Designed per-deployment state isolation on Azure Storage, bounding blast radius and enabling safe parallel deployments without lock contention

[06]

Documented the real tradeoffs of tfvars/pipeline-based secrets handling versus Key Vault, including the limits of sensitive = true, directly in the repo

[07]

Published a public-repository safety checklist alongside the design decisions, so the reasoning behind each choice is reviewable rather than tribal knowledge

// overview

A reusable, modular Terraform stack that stamps out consistent three-server Azure environments, a web server, a job server, and a SQL Server database server, from a single variable file, with per-server NIC-level security and per-deployment state isolation.

I designed and built this entirely on my own: the module composition, the factory/stamping pattern that drives server definitions from a single map variable, the per-deployment state isolation strategy, and the documented security tradeoffs behind every design decision, replacing a manual, error-prone portal-wizard process the team had been repeating for every new client cluster.

// problem

As the organization's client base grew, the team needed to deploy new server clusters, each made up of a web server, a job server, and a SQL Server database server, far more frequently than before. The standard process was manual: click through the Azure portal's deployment wizard and re-enter the same set of values for every server in the cluster. Deployments took time, and any mistake made in the wizard meant deleting the partially created resources and starting the whole cluster over.

Ad-hoc provisioning like this also produced inconsistent builds and a security posture that had to be re-derived by hand for every environment. A single shared Terraform state file compounded the risk further, turning every change into a potential blast radius across unrelated environments rather than a contained, reviewable one.

// approach

  1. Started by understanding exactly which Azure resources needed to be provisioned and how the three-server architecture, web, job, and database, needed to fit together, based on what was actually being deployed manually through the portal.

  2. Identified which configuration values were genuinely fixed across every deployment and which needed to vary by server, so the design could use predefined defaults where they made sense, for instance a base VM size per server role, while still allowing dynamic values where a deployment actually required them.

  3. Split the stack into composable modules with clear responsibilities: general (resource group), network (VNet and subnets, including the gateway subnet), win_server (NSGs, public IPs, NICs, and Windows VMs for the web and job roles, each with a 64 GiB Premium SSD data disk), and sql_server (SQL Server Marketplace VMs registered through the Azure SQL IaaS Agent, each with its own data and log disks sized independently and configured for OLTP storage), so each module owns its own security posture end to end and callers never need to know per-server port requirements.

  4. Drove server definitions through a map(object(...)) input variable and provisioned them with for_each, a factory/stamping model where adding a server is a single variable entry rather than new resource code. Because for_each keys resources by the map key instead of a numeric index, renaming or reordering server entries doesn't trigger unintended replacement.

  5. Enforced security at the network interface rather than the subnet: every VM gets its own NSG, with a dynamic security_rule block that inspects each server's key to open HTTP/HTTPS only for the web server. SQL Server VMs get a public IP for consistency with the rest of the stack, but their NSGs carry zero custom inbound rules, and I configured SQL connectivity itself as private through the SQL IaaS Agent, so there's no network path into the database regardless of the public IP being present.

  6. Stored state on an Azure Storage blob backend with a unique key per deployment rather than one shared state file, so blast radius stays bounded to a single deployment, parallel deployments can plan and apply without lock contention, and rollback can be scoped to just the affected environment. The tradeoff is that anything referencing another stack's outputs has to go through terraform_remote_state, which I accepted deliberately since it forces an explicit contract between stacks instead of implicit coupling.

  7. Supplied secrets, the Windows admin password, a separate SQL Server admin password, and the Azure credentials, through a gitignored terraform.tfvars file locally or CI/CD pipeline secret variables in automation, with sensitive = true set on every credential variable and output. I was explicit in the repo that sensitive = true only redacts CLI and log output, it doesn't encrypt the value in the Terraform state file itself, and that a Key Vault data source wouldn't fully close that gap either since resource attributes referencing the secret still land in state; the real mitigation in both cases is tightly scoped access control on the remote state storage account.

  8. Added prevent_destroy on the resource group as a guardrail against accidental teardown, and kept every Windows VM name to 15 characters or fewer, since Windows doesn't allow longer computer names.

  9. Documented a public-repository safety checklist alongside the design decisions themselves, covering things like never committing tfvars or state files, rotating any credential that's ever been committed, and reviewing NSG rules before publishing, since this repo is meant to be read and reused by other engineers.

// architecture

// outcome

Deploying a new three-server cluster, web, job, and SQL Server database, is now a single variable entry and a terraform apply, replacing a manual walk through the Azure portal's deployment wizard for every server. A mistake no longer means deleting a half-built cluster and starting over: the stack is repeatable and reviewable, and adding capacity is a one-line change rather than new resource code.

Each deployment is state-isolated behind its own backend key, so a problem with one environment's state can't affect another, and every server carries its own encapsulated NIC-level security posture, including the SQL Server database, which receives a public IP for stack consistency but is configured with zero open inbound rules and private-only SQL connectivity, rather than a shared, over-permissioned subnet policy. The module composition, the factory/stamping pattern, and every design tradeoff, per-deployment state, NIC-level NSGs, secrets handling, and prevent_destroy, are documented in the repo alongside a public-repository safety checklist, so the reasoning behind each decision is available for review rather than left as tribal knowledge.

// references