Terraform seems to be the right choice when you speak about infrastructure as a code and many companies want to gain the advantage of the powerfull automation it can provide.
Lately I was playing with Terraform and cloud-init to deploy Ubuntu and RHEL VMs in a vSphere environment. In this article I will show you how to use Terraform and Cloud-init to deploy a VM and customize its network settings. You can do much more with it, like install packages or configure users, these being only few example. For a full list of the available features, refer to cloud-init documentation.
You will need a machine where you have installed Terraform. I won’t cover the installation steps here. For the purpose of this article, I am asuming you already have installed Terraform in a machine. For installation steps refer to this oficial documentation.
Step 1. Prepare a VM template.
I will show you the steps to prepare a template with cloud-init for both Ubuntu and RHEL VMs along with cloud-init installation and configuration.
I used Ubuntu version 22.04 to create a template for Ubuntu server. I won’t cover here the steps to install Ubuntu server in a VM but I will cover the steps required to prepare the OS for template along with installation and configuration of cloud-init.
Once you have created and installed Ubuntu server in a VM, you need to run some command to make it template ready.
1. Set hostname to localhost
hostnamectl set-hostname localhost
2. Check if cloud-init is installed
apt list ---installed | grep cloud-init
ubuntuuser@cloud-test:~$ apt list --installed | grep cloud-init
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
cloud-init/jammy-updates,now 24.3.1-0ubuntu0~22.04.1 all [installed]
2.1 Install cloud-init if is not already installed
sudo apt-get install cloud-init -y
3. Configure cloud-init to accept VMware source data
Run below command, select VMware and press “OK” to save the configuration.
sudo dpkg-reconfigure cloud init
4. Clean cloud-init to make sure it will run on next boot
sudo cloud-init clean
5. Delete netplan files
ls /etc/netplan/
Delete all files under netplan directory.
rm /etc/netplan/FILENAME
6. Delete shell history
history -c
7. Shutdown VM
shutdown -h now
These are very basic steps. Depending on your requirements you might need aditional steps and better hardening for your template image.
For RHEL, I installed version 9.4 in a VM and did follow the steps below before I converted to template.
1. Set hostname to localhost
hostnamectl set-hostname localhost
2. Install cloud init
sudo yum install cloud-init -y
3. Update or delete default cloud.cfg file
After installing cloud-init, it will create a default cloud.cfg file locate in /etc/cloud/ directory. This will have some default configuration, like disable root account and disable password login. You can change “disable_root” to “false” and “ssh_pwdauth” to “true” or delete the file.
disable_root: false
ssh_pwauth: true
For my example, I just deleted the file.
rm /etc/cloud/cloud.cfg
4. Delete shell history
history -c
5. Shutdown VM
shutdown -h now
These are very basic steps. Depending on your requirements you might need aditional steps and better hardening for your template image.
Step 2. Convert VM to template
In vCenter ( or ESXi webclient ) right click on VM > Template and click on Convert to template.
This will convert your VM into a template which we will see later to deploy a VM.
Step 3. Prepare cloud-init yaml input file
You can use cloud-init to configure networking for a VM, create users, install packages and many more. For more details you can refer to cloud-init documentation.
In this example I will use cloud-init to only configure network details on the VM.( Check line 7 if you use RHEL )
#cloud-metadata
instance-id: cloud-test-01
local-hostname: cloud-test
network:
version: 2
ethernets:
id0: # replace "id0" with "ens33" if you use RHEL
match:
name: ens33
dhcp4: false
addresses:
- 192.168.1.1
nameservers:
search: [home.lab]
addresses: [192.168.1.105]
gateway4: 192.168.1.1
The above code will be used during the boot to configure the network for the VM. Copy the code, update it according to your needs and save it into a file called “cloud-init.yaml”. Put this file into a separate folder where we will also create a file with terraform code.
Step 4. Prepare terraform code
provider "vsphere" {
user = "administrator@vsphere.local" # vCenter user
password = "PASSWORD" # password
vsphere_server = "VCENTER-IP" # vCenter IP for FQDN
allow_unverified_ssl = true
api_timeout = 10
}
data "vsphere_datacenter" "datacenter" {
name = "HomeDC" # DC name
}
data "vsphere_datastore" "datastore" {
name = "RAID5_Datastore" # datastore name
datacenter_id = data.vsphere_datacenter.datacenter.id
}
data "vsphere_compute_cluster" "cluster" {
name = "Home-Cluster01" # cluster name
datacenter_id = data.vsphere_datacenter.datacenter.id
}
data "vsphere_distributed_virtual_switch" "vds" {
name = "Nested-Lab-Switch" # distributed switch name
datacenter_id = data.vsphere_datacenter.datacenter.id
}
data "vsphere_network" "network" {
name = "VM Network" # port group name
datacenter_id = data.vsphere_datacenter.datacenter.id
distributed_virtual_switch_uuid = data.vsphere_distributed_virtual_switch.vds.id
}
data "vsphere_virtual_machine" "template" {
name = "ubuntu-cloudinit" # template name
datacenter_id = "${data.vsphere_datacenter.datacenter.id}"
}
resource "vsphere_virtual_machine" "vm" {
name = "ubuntu-from-terraform" # VM name
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_id = data.vsphere_datastore.datastore.id
num_cpus = 1 # CPU count
memory = 1024 # ram in MB
guest_id = "ubuntu64Guest" # guest OS version configuration ( replace with rhel9_64Guest for rhel 9 )
firmware = "efi"
network_interface {
network_id = data.vsphere_network.network.id
}
disk {
label = "disk0"
unit_number = 0
size = 50 # disk size
}
clone {
template_uuid = "${data.vsphere_virtual_machine.template.id}"
extra_config = {
"guestinfo.metadata" = base64encode(file("cloud-init.yaml")) # path to cloud-init metadata file
"guestinfo.metadata.encoding" = "base64"
}
}
Copy the code above and place it into a file called “deployvm.tf” inside same folder with “cloud-init.yaml” file.
In the terraform file, check and replace the values to match your environment. I added comment on all lines which you should pay attention to.
Once you have both files ready, run “terraform init” to install all the requirements for the code to run. You should have a similar output to the one below.
[root@terraform-vm Terraform-Tech-hype]# terraform init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/vsphere from the dependency lock file
- Using previously-installed hashicorp/vsphere v2.10.0
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
[root@terraform-vm Terraform-Tech-hype]#
Once everything is initialized, you can run “terraform plan” to see what will be changed in your infrastructure. This command will just show you the plan which will help you understand what changes will be done in your infrastructure. At this stage, nothing is going to be changed in the infrastructure so plan can be run safely without worrying of unwanted changes.
Best practice is to check the plan properly to understand each update which will be done in your infrastructure. You will see a summary in the end of the plan, where you can see how many objects will be create, changed or destroyed. ( I didn’t include the plan details in the code below, yours will look different )
[root@terraform-vm Terraform-Tech-hype]# terraform plan
.
.
.
Plan: 1 to add, 0 to change, 0 to destroy.
Once you checked the plan, you can run “terraform apply”. This command will ask you if you want to apply the changes or not. ( I didn’t include plan details in the code below, yours will look different )
[root@terraform-vm Terraform-Tech-hype]# terraform apply
.
.
.
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Now this is going to create, change or destroy objects in your infrastructure. In our case, we have only 1 object to add as you can see in the plan.
Lines 61 and 62 from terraform code are the important ones for cloud-init implementation. Terraform will encode the file using base64 format and add it as a parameter in VM configuration file. You can look for it after VM is deployed. VM needs to be powered off to see the guestinfo.metadata, if the VM is powered on, it will show only “TRUE”.
Troubleshooting
If you encounter problems with cloud-init and you don’t see the configuration applied, you might need to do some troubleshooting.
First, you can check cloud-init status by running the command “cloud-init status”
[root@localhost ~]# cloud-init status
status: done
Status “done” will mean that cloud init did run at boot and it completed the configuration. If you see status “not run” it means there was an issue and cloud-init didnt run.
Logs for cloud init are located in /var/log/. You will find 2 log files, cloud-init.log and cloud-init-output.log which you can analyze.
[root@localhost ~]# ls /var/log/cloud-init
cloud-init.log cloud-init-output.log
Leave a Reply