Why IaC Is a Security Control, Not Just a DevOps Practice
Infrastructure as Code is not just a convenience for operations teams. For Azure network security, it is one of the most impactful controls you can implement. Here's why:
- Configuration drift is eliminated. Manual changes in the Azure portal are not tracked, are hard to audit, and accumulate into environments that no longer match what was originally designed or approved.
- Security is embedded in the deployment pipeline. Policy checks, NSG rule validation, and compliance scanning happen before resources are deployed — not after a security review finds problems in production.
- Every change is reviewed. When your network configuration lives in Git, changes go through pull requests. A second pair of eyes reviews every NSG rule addition, every peering change, every firewall policy update before it reaches Azure.
- Environments are reproducible. Disaster recovery, new region deployments, and test environment creation become trivial when your entire network is defined in code.
Most Azure security incidents I have seen in customer environments involve a configuration that was changed manually months earlier — no ticket, no review, no record. IaC doesn't just make deployments faster. It closes the audit gap that manual configuration leaves wide open.
The IaC Tools for Azure Networking
There are three main tools used to define Azure network infrastructure as code. Each has a distinct philosophy, learning curve, and best-fit use case.
Bicep is Microsoft's purpose-built IaC language for Azure. It compiles down to ARM JSON at deployment time, giving you the full power of ARM with dramatically cleaner syntax. If you're Azure-only and starting fresh, Bicep is the right default choice in 2025.
Bicep has first-class support in Visual Studio Code via the Bicep extension, which gives you IntelliSense for every Azure resource type, real-time validation, and one-click deployment. The language is declarative, concise, and tightly integrated with the Azure resource model.
- +Azure-native — supports new resource types immediately
- +Cleaner syntax than ARM JSON
- +Excellent VS Code extension with full IntelliSense
- +Free — no licensing cost
- +Backed by Microsoft — long-term supported
- +Direct integration with Azure Policy and Blueprints
- −Azure only — no multi-cloud support
- −Smaller community than Terraform
- −State management handled by Azure (less flexible)
Terraform is the most widely used IaC tool across the industry. Its Azure provider (maintained by HashiCorp with Microsoft collaboration) is comprehensive and mature. If your organisation runs workloads across Azure and AWS, or if your team already has Terraform expertise, it's the natural choice.
Terraform's state management — where it tracks what it has deployed and what needs to change — is more sophisticated than ARM/Bicep's approach. Remote state in Azure Storage or Terraform Cloud gives you a reliable source of truth for your infrastructure. The HashiCorp Configuration Language (HCL) is readable and well-documented.
- +Multi-cloud — one toolset for Azure, AWS, and GCP
- +Largest IaC community and module ecosystem
- +Sophisticated state management
- +Excellent VS Code extension (HashiCorp Terraform)
- +Terraform plan previews changes before applying
- +Strong CI/CD pipeline integration
- −Azure resource support sometimes lags behind Bicep
- −State file management adds operational overhead
- −BSL licence change (2023) — consider OpenTofu for open-source
- −Higher learning curve than Bicep for Azure-specific teams
ARM Templates are the original Azure IaC format — raw JSON files that define Azure resources. Bicep compiles to ARM JSON, so understanding ARM is still useful for reading compiled output and working with older codebases. For new projects in 2025, Bicep supersedes ARM in every way.
You will still encounter ARM templates in Azure Quickstart Templates, exported resource definitions, and older enterprise codebases. The VS Code ARM Tools extension provides some help but the experience is significantly worse than Bicep or Terraform.
- +Foundation of Azure — every resource type supported
- +Large existing template library (Azure Quickstart)
- +No compilation step — deploys directly to ARM
- −Extremely verbose JSON — difficult to read and maintain
- −No native modularity — workarounds are clunky
- −Superseded by Bicep for all new work
- −Poor developer experience compared to alternatives
Pulumi takes a different approach — instead of a domain-specific language, you write infrastructure code in TypeScript, Python, Go, C#, or Java. For developer-led organisations or teams that want to write infrastructure tests in the same language as their application code, Pulumi is a compelling option.
Its Azure support is solid and it integrates well with Azure DevOps and GitHub Actions. It's less common in pure network security contexts but worth knowing as it grows in adoption.
Setting Up Visual Studio Code for Azure IaC
Visual Studio Code is the standard editor for Azure IaC work. The right extensions transform it from a text editor into a full Azure development environment with real-time validation, IntelliSense, and one-click deployment.
Essential VS Code Extensions
Install these extensions from the VS Code Marketplace:
Open VS Code, press Ctrl+Shift+X (or Cmd+Shift+X on Mac) to open the Extensions panel, and search for each of the following:
- Bicep (
ms-azuretools.vscode-bicep) — IntelliSense, validation, and visualiser for Bicep files. Shows you a graphical diagram of your resource dependencies as you write. - HashiCorp Terraform (
hashicorp.terraform) — Syntax highlighting, IntelliSense, and formatting for HCL files. Required for Terraform development. - Azure Resource Manager Tools (
msazurermtools.azurerm-vscode-tools) — Syntax support for ARM JSON templates. Only needed if you're maintaining legacy ARM templates. - Azure CLI Tools (
ms-vscode.azurecli) — Run Azure CLI commands directly from VS Code terminal with IntelliSense. - Azure Account (
ms-vscode.azure-account) — Sign in to Azure from VS Code and deploy directly from the editor. - GitLens (
eamodio.gitlens) — Essential for tracking who changed what in your IaC repository. Shows blame annotations inline with your code. - YAML (
redhat.vscode-yaml) — Needed for Azure Pipelines and GitHub Actions workflow files.
Recommended VS Code Settings for IaC Work
Add these to your VS Code settings.json for a cleaner IaC development experience:
Bicep in Practice — Deploying an NSG with Security Rules
Here is a complete Bicep module that deploys a Network Security Group with hardened inbound and outbound rules, following the deny-all-by-default pattern. This is the kind of code that replaces clicking through the Azure portal and provides a repeatable, auditable configuration.
Deploy this module from the command line using the Azure CLI:
The az deployment group what-if command shows you exactly what will be created, modified, or deleted before you apply the change. Make it a habit — especially when modifying existing NSG rules in production.
Terraform in Practice — Azure Firewall with Policy
Here is a Terraform configuration that deploys Azure Firewall with a Firewall Policy, including a network rule collection to control east-west traffic between spokes. This pattern is the foundation of a secure hub-spoke architecture managed entirely as code.
IaC Best Practices for Azure Network Security
Everything in Git, nothing in the portal
If a change wasn't made through your IaC pipeline, it didn't happen. Enforce this with Azure Policy to deny manual portal changes in production subscriptions.
Use parameter files for environment differences
Never hardcode IP ranges, names, or environment-specific values. Use Bicep parameter files or Terraform variable files — one per environment. Promotes code reuse and prevents inconsistency.
Modularise your network components
VNets, NSGs, Azure Firewall, and route tables should each be separate modules. Compose them in a root module. This makes testing, reuse, and change management significantly easier.
Run what-if / plan in every PR
Configure your CI pipeline to run az deployment what-if or terraform plan automatically on every pull request. Reviewers see exactly what the infrastructure change will do before approving.
Scan for security misconfigurations before deployment
Integrate Checkov or tfsec into your pipeline to scan Bicep and Terraform code for security issues before deployment. Catches open NSG rules, missing encryption, and insecure defaults automatically.
Tag every resource consistently
Always include environment, owner, cost centre, and managedBy: IaC tags in your modules. Resources without the IaC tag are drift candidates — use Azure Policy to alert on them.
Never store secrets in IaC code
Passwords, connection strings, and API keys must never appear in Bicep or Terraform files — even in private repos. Reference Azure Key Vault secrets at deployment time instead.
Lock production deployments to main branch only
Configure your pipeline so only the main/master branch can deploy to production. Feature branches deploy to dev/test only. This prevents untested code reaching production environments.
CI/CD Pipeline for Azure Network IaC
Writing IaC code is only half the story. The deployment pipeline is what enforces the security and consistency guarantees you're trying to achieve. Here is a recommended GitHub Actions workflow for Bicep deployments:
Real-World Scenarios
Deploying a Hardened Hub-Spoke Network from Scratch
An organisation is migrating from on-premises to Azure and needs to deploy a hub-spoke network with Azure Firewall, NSGs on every subnet, Private Endpoints for all PaaS services, and User Defined Routes forcing traffic through the hub firewall.
Approach: Three Bicep modules — one for the hub VNet and Azure Firewall, one for spoke VNets (reused for each spoke), and one for Private Endpoints. A root main.bicep composes them. Parameters files define each spoke's address space and workload requirements. The entire network deploys in under 20 minutes from a single pipeline run.
Security benefit: Every NSG rule, every firewall policy, and every route table entry is in Git. Any change — even a single NSG rule addition — goes through a pull request with what-if output reviewed before approval. Configuration drift is impossible because the pipeline re-applies the desired state on every deployment.
Enforcing NSG Compliance Across 40 Subscriptions
A large enterprise has 40 Azure subscriptions managed by different teams. Security audits keep finding open management ports (22, 3389) in NSGs that teams have added manually for convenience. The security team needs to stop this at scale.
Approach: A Terraform module defines a deny-management-ports NSG rule that must be present on every subnet. An Azure Policy definition (also written as Bicep and deployed via the pipeline) enforces that all NSGs include this rule. Checkov scans any IaC code that touches NSGs. Teams can still deploy their own NSG rules — they just cannot override the security baseline.
Security benefit: The combination of IaC enforcement plus Azure Policy means neither manual portal changes nor IaC code can remove the baseline rule. Compliance reporting shows 100% of subnets compliant within one sprint of rollout.
Zero Trust Network Deployment via IaC Pipeline
An organisation is implementing Zero Trust and needs to deploy Microsoft Entra Private Access connectors, configure Conditional Access policies, and update NSGs to remove legacy VPN access routes — all consistently across dev, staging, and production environments.
Approach: Bicep handles the network layer (NSG changes, Private Endpoint configurations, UDR updates). Terraform manages the Entra ID configuration (Conditional Access policies, application registrations). Both run in the same pipeline with environment-specific parameter files. Dev deploys first, validation gates block staging deployment if security scans fail.
Security benefit: The Zero Trust configuration is identical across all environments — no inconsistencies between dev and production that create false confidence during testing. Rollback is a Git revert away if something goes wrong.
Choosing the Right Tool
| Scenario | Recommended Tool | Reason |
|---|---|---|
| Azure-only, greenfield deployment | Bicep | Native, concise, best VS Code experience |
| Multi-cloud (Azure + AWS) | Terraform | Single toolset across both clouds |
| Existing Terraform codebase | Terraform | Consistency — don't split your state management |
| Existing ARM templates | Migrate to Bicep | Bicep decompiles ARM JSON automatically |
| Developer-led teams (TypeScript/Python) | Pulumi | Write infra in your existing language |
| Quick prototype or one-off deployment | Azure CLI / Portal | For throwaway resources only — never production |
If you have existing ARM templates, you don't have to rewrite them from scratch. Run az bicep decompile --file yourtemplate.json to automatically convert ARM JSON to Bicep. The output won't be perfect but gives you a 90% starting point that you can clean up and modularise.
What to remember from this article
- →IaC is a security control — it eliminates configuration drift, enforces review, and creates an audit trail for every network change
- →Bicep is the right default for Azure-only environments — cleaner than ARM, native Azure support, excellent VS Code integration
- →Terraform wins for multi-cloud — if you run Azure and AWS, use one tool for both with consistent state management
- →ARM Templates are legacy — migrate to Bicep using the decompile command, don't write new ARM JSON
- →Always run what-if / plan before applying — see exactly what will change before it changes in production
- →Scan before you deploy — integrate Checkov or tfsec into every pipeline to catch security misconfigurations in code
- →Never store secrets in IaC code — reference Key Vault secrets at deployment time