Cloud-Init With Terraform
Did you know that you could use cloud-init with Terraform? I didn’t realize until recently. No, I am not talking about a template and passing it via user data, but defining a cloud-init template in Terraform. Let’s get into it.
The Plan
Pick your favorite cloud provider to try this, as all you need is a Linux VM. I will be using Azure to spin up an Ubuntu server and apply a cloud-init template. I will have cloud-init install the tool HTTPie.
Initial Terraform
We are going to get started like we always do by defining a Terraform block and our provider.
terraform {
required_version = ">= 0.14.3"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "2.44.0"
}
}
}
provider "azurerm" {
features {}
}
I am using the Azure CLI to login and set my subscription.
Define our VM
Let’s now create our VM.
resource "azurerm_resource_group" "cloudinit" {
name = "cloudinit-resources"
location = "East US"
}
resource "azurerm_virtual_network" "cloudinit" {
name = "cloudinit-network"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.cloudinit.location
resource_group_name = azurerm_resource_group.cloudinit.name
}
resource "azurerm_subnet" "cloudinit" {
name = "internal"
resource_group_name = azurerm_resource_group.cloudinit.name
virtual_network_name = azurerm_virtual_network.cloudinit.name
address_prefixes = ["10.0.2.0/24"]
}
resource "azurerm_public_ip" "cloudinit" {
name = "cloudinit-pip"
location = azurerm_resource_group.cloudinit.location
resource_group_name = azurerm_resource_group.cloudinit.name
allocation_method = "Dynamic"
}
resource "azurerm_network_security_group" "cloudinit" {
name = "cloudinit-sg"
location = azurerm_resource_group.cloudinit.location
resource_group_name = azurerm_resource_group.cloudinit.name
security_rule {
name = "SSH"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_network_interface" "cloudinit" {
name = "cloudinit-nic"
location = azurerm_resource_group.cloudinit.location
resource_group_name = azurerm_resource_group.cloudinit.name
ip_configuration {
name = "cloudinit-nic-config"
subnet_id = azurerm_subnet.cloudinit.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.cloudinit.id
}
}
resource "azurerm_network_interface_security_group_association" "cloudinit" {
network_interface_id = azurerm_network_interface.cloudinit.id
network_security_group_id = azurerm_network_security_group.cloudinit.id
}
resource "azurerm_linux_virtual_machine" "cloudinit" {
name = "cloudinit-machine"
resource_group_name = azurerm_resource_group.cloudinit.name
location = azurerm_resource_group.cloudinit.location
size = "Standard_B1s"
admin_username = "cloudinit"
admin_password = "HKKRoD24XLBzxdD"
# This is where we pass our cloud-init.
custom_data = ""
disable_password_authentication = false
network_interface_ids = [
azurerm_network_interface.cloudinit.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
}
output "public_ip" {
value = azurerm_linux_virtual_machine.cloudinit.public_ip_address
}
Creating our cloud-init
The resource documentation can be found here. We are going to create this in our Terraform after the provider since it is a data block, and that is the convention that I use.
data "template_cloudinit_config" "config" {
gzip = true
base64_encode = true
# Main cloud-config configuration file.
part {
content_type = "text/cloud-config"
content = "packages: ['httpie']"
}
}
Now we can go back to our VM resource and pass that to the custom_data property.
resource "azurerm_linux_virtual_machine" "cloudinit" {
name = "cloudinit-machine"
resource_group_name = azurerm_resource_group.cloudinit.name
location = azurerm_resource_group.cloudinit.location
size = "Standard_B1s"
admin_username = "cloudinit"
admin_password = "HKKRoD24XLBzxdD"
# This is where we pass our cloud-init.
custom_data = data.template_cloudinit_config.config.rendered
disable_password_authentication = false
# Abbreviated
}
That’s it, and we can now execute it.
Creating and validating the VM
Time to initialize our Terraform and apply.
$ terraform init
Terraform has been successfully initialized!
$ terraform apply -auto-approve
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
With our VM created successfully, we can use the Azure CLI to check to see if it has a status of running.
$ az vm get-instance-view /
-n cloudinit-machine /
-g cloudinit-resources /
--query instanceView.statuses[1] /
-o table
Code Level DisplayStatus
------------------ ------- ---------------
PowerState/running Info VM running
Once the status reports that it’s running, we need to ssh into the server and verify the HTTPie installation. We will need the public ip of the instance, which we can get from Terraform.
$ terraform output publc_ip
"XX.XX.XXX.XXX"
Now we can SSH in and check HTTPie.
$ ssh cloudinit@XX.XX.XXX.XXX
$ cloudinit@cloudinit-machine:~$ http --version
0.9.8
$ cloudinit@cloudinit-machine:~$ exit
logout
Connection to XX.XX.XXX.XXX closed.
Conclusion
That’s it; that is how you use cloud-init to configure your VM upon creation. Don’t forget to clean up your resources.
Thanks for reading,
Jamie
If you enjoy the content, then consider buying me a coffee.