CloudInit in Azure: Zero-Touch Automation with Bicep

Learn how CloudInit enables automated, zero-touch VM configuration in Azure

When deploying virtual machines (VMs) in Azure, the last thing you want is to SSH in manually every time to configure packages, users, and services. This is where CloudInit steps in — enabling zero-touch provisioning of VMs through declarative configuration. In this post, we’ll explore what CloudInit is, how it works in Azure, and how to integrate it with Bicep to automate deployments.

What is CloudInit?

CloudInit is an industry-standard method of automating the initial configuration of Linux VMs. It allows you to:

  • Install packages
  • Configure users and SSH keys
  • Set up disk mounts
  • Write configuration files
  • Run scripts at first boot

This eliminates the need for post-deployment scripts or manual SSH configuration, especially in large-scale environments. If you want to find out more information you can check the official Microsoft Docs Here

For a deeper dive into the CloudInit configuration format, see the official documentation on cloud-config-data.

Why Use CloudInit in Azure?

Azure supports CloudInit natively for most Linux distributions. That means you can provide a customData field at deployment time containing your CloudInit YAML. Azure injects this into the VM at boot time, where the Linux guest agent processes and applies it.

Benefits:

  • Repeatable Deployments – Same config every time
  • Zero-Touch – No manual post-deployment steps
  • Security – Avoids passing secrets via shell scripts
  • Scalability – Ideal for scaling out fleets of VMs

Using CloudInit with Bicep

For those who want a ready-made example you can check out the virtualMachineCloudInit folder

To use CloudInit in Bicep, you need to add the value customData to the Virtual Machine module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
module createVirtualMachine 'br/public:avm/res/compute/virtual-machine:0.16.0' = {
  name: 'create-virtual-machine'
  scope: resourceGroup(resourceGroupName)
  params: {
    name: vmHostName
    adminUsername: vmUserName
    adminPassword: vmUserPassword
    location: location
    osType: 'Linux'
    vmSize: 'Standard_B2ms'
    customData: cloudInitData
    availabilityZone: 1
    bootDiagnostics: true
    secureBootEnabled: true
    encryptionAtHost: true
    vTpmEnabled: true
    securityType: 'TrustedLaunch'
    imageReference: {
      publisher: 'Canonical'
      offer: 'ubuntu-24_04-lts'
      sku: 'server'
      version: 'latest'
    }
    nicConfigurations: [
      {
        ipConfigurations: [
          {
            name: 'ipconfig01'
            pipConfiguration: {
              name: '${vmHostName}-pip-01'
            }
            subnetResourceId: createVirtualNetwork.outputs.subnetResourceIds[0]
          }
        ]
        nicSuffix: '-nic-01'
        enableAcceleratedNetworking: false
      }
    ]
    osDisk: {
      caching: 'ReadWrite'
      diskSizeGB: 128
      managedDisk: {
        storageAccountType: 'Premium_LRS'
      }
    }
    tags: tags
  }
  dependsOn: [
    createVirtualNetwork
  ]
}

Once defined in the module, there are two methods of providing the CloudInit value: using an external YAML file or defining it inline within the Bicep file. Choose the method that best fits your workflow and version control needs.

Method One: External File

Use this method if you want to keep your CloudInit configuration separate from your Bicep code, which can make versioning and reuse easier.

1
2
3
4
5
6
7
@description('Cloud-init configuration as a string')
@allowed([
  'cloudInit.yaml'
])
param cloudInitFile string = 'cloudInit.yaml'

var cloudInitData = loadTextContent(cloudInitFile)

Method Two: Inline CloudInit

Use this method for quick prototyping or when you want the configuration to be self-contained within the Bicep file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
var cloudInitData = '''
#cloud-config
package_update: true
package_upgrade: true
packages:
  - nginx
runcmd:
  - systemctl enable nginx
  - systemctl start nginx
write_files:
  - path: /var/www/html/index.html
    permissions: '0644'
    content: |
      <html>
        <head>
          <title>Welcome to Nginx on Ubuntu 24.04 LTS!</title>
        </head>
        <body>
          <h1>It works!</h1>
        </body>
      </html>
'''

Test and Validate

To troubleshoot or test CloudInit deployments:
Review logs at /var/log/cloud-init.log and /var/log/cloud-init-output.log
Use SSH to verify file creation and service status Use Azure Serial Console if boot fails

Final Thoughts

CloudInit is a game-changer for zero-touch VM provisioning in Azure. When paired with Bicep, it becomes a powerful part of your Infrastructure-as-Code toolkit — simplifying, scaling, and securing your deployments.

Whether you’re spinning up test environments or deploying hardened production Linux VMs, CloudInit helps ensure your infrastructure starts life exactly how you intend.

Share with your network!

Built with Hugo - Theme Stack designed by Jimmy