In this article, we will take a look at how to use Infrastructure-as-code (IaC) with Microsoft’s Azure cloud platform. First, we will look at what IaC is, and how it works on Azure. We will take a look at why you would want to use it and the associated benefits of IaC, before looking at the available tools you can use. We will dive into a few examples and share some best practices you’ll want to follow when using IaC with Azure.
IaC allows developers and operations teams to define, deploy, and manage infrastructure through machine-readable script files rather than through physical hardware configuration or interactive configuration tools, with the benefits of achieving the end result in a repeatable and automated manner, improving efficiency, consistency, and reliability.
Popular IaC tools include Terraform, Ansible, Puppet, Chef, Pulumi, Azure Resource Manager (ARM) templates, Azure Bicep, and AWS CloudFormation. Each of these tools utilizes one or more particular script languages or DSLs (Domain-Specific Languages) to define infrastructure components, dependencies, and configurations.
IaC relies on declarative rather than imperative configuration. The declarative configuration specifies the desired end state of the infrastructure without detailing the step-by-step process to achieve it.
Infrastructure code is treated like any other software code and is typically stored in version control systems (e.g., Git). This enables versioning, collaboration, and tracking changes over time. Developers can use branches, pull requests, and other version control features to manage infrastructure changes, also enabling automation of infrastructure provisioning, configuration, and management processes.
Another key characteristic of IaC is idempotence. This means applying the same configuration multiple times has the same result as applying it once. IaC encourages the creation of modular and reusable code components. This allows you to define infrastructure patterns, templates, or modules that can be reused across different projects or environments.
Read more: Infrastructure as Code — Best Practices, Benefits & Examples
The general steps involved in using IaC on Azure are as follows:
- Choose a tool (See list below! Take into account the pros and cons of each and select the one most appropriate for you. In most cases, Terraform, Bicep, or ARM templates will be most appropriate).
- Define infrastructure in configuration files.
- Store your files in a version control system (VCS).
- Deploy your infrastructure — this usually involves authenticating with Azure and then running a series of commands. These commands can be automated in the form of a CI/CD pipeline using a DevOps tool, such as Azure DevOps, GitLab, Terraform Cloud, or Spacelift.
- Update, maintain, or destroy your infrastructure by changing the configuration files as required and re-running your deployment.
Azure provides native support for IaC through services like Azure Resource Manager (ARM) templates and Azure Bicep. These tools are designed specifically for provisioning and managing Azure resources, offering tight integration with the Azure ecosystem.
Terraform also has Azure providers that are well maintained (most commonly azurerm). However, one disadvantage of using Terraform is that they slightly lag behind the latest feature releases of Azure, whereas the natively supported Bicep and ARM templates always support the latest and greatest.
There are lots of IaC tools that integrate well with Azure. These are the most common:
1. Terraform
Terraform is an IaC tool developed by HashiCorp. It is cloud-agnostic and supports multiple providers, including Azure. Terraform uses a declarative configuration language and has a large and active community.
2. OpenTofu
OpenTofu is a fork of Terraform that is open-source, community-driven, and managed by the Linux Foundation. The goal of the project is to provide a well-known and widely-accepted license that companies can trust, that won’t suddenly change in the future, and isn’t subject to the whims of a single vendor. It was created in response to HashiCorp’s switch from an open-source license to the BUSL (Business Source License).
Read more: What is OpenTofu?
3. ARM Templates
ARM Templates are native templates provided by Azure for describing and deploying Azure resources. These templates use JSON syntax and are specifically designed for Azure.
4. Bicep Templates
Bicep Templates are a more concise, readable, and maintainable alternative to writing ARM templates directly in JSON. Bicep is a domain-specific language (DSL).
5. Ansible
Ansible is an open-source automation tool that supports configuration management, application deployment, and infrastructure orchestration. Ansible includes Azure modules that allow you to manage Azure resources.
6. Pulumi
Pulumi is an IaC tool that allows you to use familiar programming languages (such as Python, JavaScript, or TypeScript) to define and deploy infrastructure. Pulumi supports Azure and other cloud providers.
7. Chef
Chef is a configuration management tool that can be used for automating infrastructure and application deployment. Chef Infra has cookbooks and resources for managing Azure resources. Chef is usually used to define and enforce the desired state of servers and applications on those servers and is not typically used for creating and managing infrastructure in the same way as IaC tools like Terraform, ARM templates, or Pulumi.
8. SaltStack
SaltStack is a configuration management and automation tool that allows for managing IaC. It has support for Azure and can be used for provisioning and managing Azure resources.
To further show the differences between the tools, In the following examples, we will show configuration files for the the different tools that all create the same infrastructure, a resource group with a VNET that includes a subnet with an NSG attached to the subnet. We will include the steps below:
- Create a virtual network (
myVnet
) with an address space of “10.0.0.0/16”. - Define a subnet (
mySubnet
) within the virtual network with an address space of “10.0.1.0/24” and attaches the NSG (myNSG
) to the subnet. - Create a Network Security Group (
myNSG
) with a rule (AllowAll
) allowing all traffic.
1. Terraform
The configuration file for Terraform with Azure should look like this:
provider "azurerm" {
features = {}
}
# Resource Group
resource "azurerm_resource_group" "example" {
name = "myResourceGroup"
location = "UK South"
}
# Virtual Network
resource "azurerm_virtual_network" "example" {
name = "myVnet"
resource_group_name = azurerm_resource_group.example.name
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.example.location
}
# Subnet
resource "azurerm_subnet" "example" {
name = "mySubnet"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
# Network Security Group
resource "azurerm_network_security_group" "example" {
name = "myNSG"
resource_group_name = azurerm_resource_group.example.name
}
# Associate NSG with Subnet
resource "azurerm_subnet_network_security_group_association" "example" {
subnet_id = azurerm_subnet.example.id
network_security_group_id = azurerm_network_security_group.example.id
}
2. Bicep
For Bicep with Azure:
param location string = 'UK South'
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: 'myResourceGroup'
location: location
}
resource vnet 'Microsoft.Network/virtualNetworks@2021-04-01' = {
name: 'myVnet'
location: rg.location
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
}
}
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-04-01' = {
parent: vnet
name: 'mySubnet'
properties: {
addressPrefix: '10.0.1.0/24'
}
}
resource nsg 'Microsoft.Network/networkSecurityGroups@2021-04-01' = {
name: 'myNSG'
location: rg.location
}
resource subnetAssociation 'Microsoft.Network/virtualNetworks/subnets/networkSecurityGroupAssociations@2021-04-01' = {
parent: subnet
properties: {
networkSecurityGroupId: nsg.id
}
}
3. ARM Template
The ARM Template configuration for Azure:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"resources": [
{
"type": "Microsoft.Resources/resourceGroups",
"apiVersion": "2021-04-01",
"name": "myResourceGroup",
"location": "East US",
"properties": {}
},
{
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2021-04-01",
"name": "myVnet",
"location": "[resourceGroup().location]",
"dependsOn": [
"myResourceGroup"
],
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.0.0.0/16"
]
},
"subnets": [
{
"name": "mySubnet",
"properties": {
"addressPrefix": "10.0.1.0/24",
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', 'myNSG')]"
}
}
}
]
}
},
{
"type": "Microsoft.Network/networkSecurityGroups",
"apiVersion": "2021-04-01",
"name": "myNSG",
"location": "[resourceGroup().location]",
"dependsOn": [
"myResourceGroup"
],
"properties": {
"securityRules": [
{
"name": "AllowAll",
"properties": {
"priority": 100,
"direction": "Inbound",
"access": "Allow",
"protocol": "*",
"sourceAddressPrefix": "*",
"sourcePortRange": "*",
"destinationAddressPrefix": "*",
"destinationPortRange": "*"
}
}
]
}
}
]
}
4. Ansible
Ansible playbook that uses the Azure Resource Manager (ARM) Ansible module. You will need to have the ansible
and ansible[azure]
packages installed.
---
- name: Deploy Azure VNET with Subnet and NSG
hosts: localhost
gather_facts: no
tasks:
- name: Ensure Azure CLI is installed
ansible.builtin.package:
name: azure-cli
state: present
- name: Log in to Azure
ansible.builtin.azure_rm_login:
client_id: "{{ azure_client_id }}"
secret: "{{ azure_client_secret }}"
tenant: "{{ azure_tenant_id }}"
subscription_id: "{{ azure_subscription_id }}"
- name: Create Resource Group
ansible.builtin.azure_rm_resourcegroup:
name: "myResourceGroup"
location: "UK South"
state: present
- name: Create Network Security Group
ansible.builtin.azure_rm_securitygroup:
resource_group: "myResourceGroup"
name: "myNSG"
rules:
- name: AllowAll
protocol: "*"
source_address_prefix: "*"
destination_address_prefix: "*"
access: Allow
direction: Inbound
priority: 100
state: present
- name: Create Virtual Network with Subnet
ansible.builtin.azure_rm_virtualnetwork:
resource_group: "myResourceGroup"
name: "myVnet"
address_prefixes:
- "10.0.0.0/16"
subnets:
- name: "mySubnet"
address_prefix: "10.0.1.0/24"
security_group: "myNSG"
state: present
- name: Log out of Azure
ansible.builtin.azure_rm_logout:
5. Pulumi
Using Typescript — you will need Nodejs installed:
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";
const resourceGroupName = "myResourceGroup";
const location = "East US";
// Create an Azure Resource Group
const resourceGroup = new azure.core.ResourceGroup(resourceGroupName, {
location: location,
});
// Create a Network Security Group
const nsg = new azure.network.NetworkSecurityGroup("myNSG", {
resourceGroupName: resourceGroup.name,
location: resourceGroup.location,
securityRules: [{
name: "AllowAll",
priority: 100,
direction: "Inbound",
access: "Allow",
protocol: "*",
sourceAddressPrefix: "*",
destinationAddressPrefix: "*",
destinationPortRange: "*",
}],
});
// Create a Virtual Network
const vnet = new azure.network.VirtualNetwork("myVnet", {
resourceGroupName: resourceGroup.name,
location: resourceGroup.location,
addressSpaces: ["10.0.0.0/16"],
subnets: [{
name: "mySubnet",
addressPrefix: "10.0.1.0/24",
securityGroup: nsg.id,
}],
});
// Export the Virtual Network information
export const vnetName = vnet.name;
export const subnetName = vnet.subnets[0].name;
export const nsgName = nsg.name;
Pulumi configuration for Azure using the GO SDK:
package main
import (
"github.com/pulumi/pulumi-azure/sdk/go/azure/core"
"github.com/pulumi/pulumi-azure/sdk/go/azure/network"
"github.com/pulumi/pulumi/sdk/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Create an Azure Resource Group
resourceGroup, err := core.NewResourceGroup(ctx, "myResourceGroup", &core.ResourceGroupArgs{
Location: pulumi.String("East US"),
})
if err != nil {
return err
}
// Create a Network Security Group
nsg, err := network.NewNetworkSecurityGroup(ctx, "myNSG", &network.NetworkSecurityGroupArgs{
ResourceGroupName: resourceGroup.Name,
SecurityRules: network.NetworkSecurityGroupSecurityRuleArray{
&network.NetworkSecurityGroupSecurityRuleArgs{
Name: pulumi.String("AllowAll"),
Priority: pulumi.Int(100),
Direction: pulumi.String("Inbound"),
Access: pulumi.String("Allow"),
Protocol: pulumi.String("*"),
SourceAddressPrefix: pulumi.String("*"),
DestinationAddressPrefix: pulumi.String("*"),
DestinationPortRange: pulumi.String("*"),
},
},
})
if err != nil {
return err
}
// Create a Virtual Network with Subnet
vnet, err := network.NewVirtualNetwork(ctx, "myVnet", &network.VirtualNetworkArgs{
ResourceGroupName: resourceGroup.Name,
AddressSpaces: pulumi.StringArray{
pulumi.String("10.0.0.0/16"),
},
Subnets: network.SubnetArray{
&network.SubnetArgs{
Name: pulumi.String("mySubnet"),
AddressPrefix: pulumi.String("10.0.1.0/24"),
SecurityGroup: nsg.ID(),
},
},
})
if err != nil {
return err
}
// Export the Virtual Network information
ctx.Export("vnetName", vnet.Name)
ctx.Export("subnetName", vnet.Subnets[0].Name)
ctx.Export("nsgName", nsg.Name)
return nil
})
}
You should also consider the use of a Continuous Integration/Continuous Delivery (CI/CD) platform for these tools. The most flexible platforms, such as Spacelift, support multiple tools, including Terraform, OpenTofu Ansible, Pulumi, and AWS CloudFormation. See our documentation to find out more about how Azure integrates with Spacelift. Create a free account today, or book a demo with one of our engineers.
Be sure to get yourself familiar with these best practices associated with IaC. By following these best practices, you can create a solid foundation for managing your infrastructure in Azure using IaC, promoting consistency, security, and collaboration within your development and operations teams.
1. Version Control
- Store your IaC files in a version control system (e.g., Git).
- Use branches for different environments (e.g., development, staging, production).
- Tag releases to track changes and facilitate rollbacks.
2. Separation of concerns
- Organize your IaC code into modular and reusable components.
- Use separate files or modules for different resource types (e.g., networking, compute).
3. Parameterization
- Parameterize your IaC templates to make them reusable across environments.
- Externalize configuration parameters to quickly adapt to different scenarios.
4. Resource naming conventions
- Establish consistent naming conventions for resources to enhance clarity and traceability.
- Include environment indicators, resource type, and other relevant information in resource names.
5. Tagging
- Use resource tagging to label resources for organizational, billing, and monitoring purposes.
- Implement a consistent tagging strategy across resources.
6. Environment consistency
- Aim for consistency between your IaC and actual environments.
- Use the same versions of IaC tools and libraries across environments.
7. Immutable infrastructure
- Treat infrastructure as immutable: avoid making changes to existing resources; instead, deploy new versions.
- Replace resources rather than updating them to minimize configuration drift.
8. Security best practices
- Implement security best practices such as restricting network access, using managed identities, and encrypting sensitive data.
- Leverage Azure Policy to enforce compliance and security standards.
9. Testing
- Implement automated testing for your IaC code to catch errors early.
- Test in a sandbox or staging environment before deploying to production.
10. Monitoring and logging
- Integrate monitoring and logging solutions to track changes and monitor the health of your infrastructure.
- Leverage Azure Monitor and Azure Log Analytics for insights.
11. Secrets management
- Avoid hardcoding secrets in your IaC code.
- Use Azure Key Vault or other secure solutions to store and manage secrets.
12. Continuous Integration/Continuous Deployment (CI/CD)
- Implement CI/CD pipelines for automated testing and deployment.
- Trigger deployments automatically on code changes to ensure consistency.
Learn how to use Terraform in Azure DevOps CI/CD pipelines.
13. Governance
- Establish governance policies using Azure Policy to enforce organizational standards and compliance.
- Leverage Azure Blueprints for managing and controlling resource deployment.
14. Documentation
- Include inline comments and documentation in your IaC code to explain configurations and decisions.
- Maintain a README file with instructions for deploying and managing the infrastructure.
15. Education and training
- Ensure team members are trained on IaC best practices and Azure services.
- Stay updated on new features and improvements in Azure.
In this article, we have given an overview of what IaC is, why you should use it with Azure, and some example configuration files to show the differences between the available tools you can use.
Utilizing IaC on Azure brings many benefits to the process of managing and provisioning infrastructure. Today, IaC is a foundational practice in modern DevOps and cloud computing, offering a range of benefits that contribute to increased efficiency, reliability, and agility in managing infrastructure.
Continuous Integration and Deployment for your IaC
Spacelift allows you to automate, audit, secure, and continuously deliver your infrastructure. It helps overcome common state management issues and adds several must-have features for infrastructure management.