DEV Community

Aurelia Peters
Aurelia Peters

Posted on

Setting Up The Home Lab: Terraform and Cloud-Init

In my last article I talked about getting Terraform set up on Proxmox VE. In this article I want to talk about how I got Cloud-Init set up to use with my Terraform templates.

To begin with, I needed a cloud-init base VM. While I could use the cloud image that Ubuntu provides, I found a nifty article that shows you how to roll your own base image.

NOTE: The Proxmox VE cloud-init documentation suggests adding a serial console next. I have found that not to be necessary with the Ubuntu cloud image, so I'm not going to do it.

Now that we've got the base template set up (turns out I was mistaken in my last post when I said it needed to be a VM and not a template) let's set up an actual VM. I'll have a single virtual Ethernet interface that gets its IP address via DHCP, 32 GB of virtual disk, 2048 GB of RAM, and 2 processor cores.

Note here that I've broken my Terraform config into several files to make it more manageable. As long as all of the Terraform files (i.e. the ones ending in .tf or .tfvars) are in the same directory, Terraform will process them in the same way as if they were one big file.

# provider.tf - This is where I define my providers

terraform {
  required_providers {
    proxmox = {
      source = "telmate/proxmox"
      #latest version as of 16 July 2024
      version = "3.0.1-rc3"
    }
  }
}

provider "proxmox" {
  # References our vars.tf file to plug in the api_url
  pm_api_url = "https://${var.proxmox_host}:8006/api2/json"
  # Provided in a file, secrets.tfvars containing secret terraform variables
  pm_api_token_id = var.token_id 
  # Also provided in secrets.tfvars
  pm_api_token_secret = var.token_secret
  # Defined in vars.tf
  pm_tls_insecure = var.pm_tls_insecure
  pm_log_enable = true
  # this is useful for logging what Terraform is doing
  pm_log_file   = "terraform-plugin-proxmox.log"
  pm_log_levels = {
    _default    = "debug"
    _capturelog = ""
  }
}
Enter fullscreen mode Exit fullscreen mode
# cloud-init.tf - This is where I store cloud-init configuration

# Source the Cloud Init Config file. NB: This file should be located 
# in the "files" directory under the directory you have your Terraform
# files in.
data "template_file" "cloud_init_test1" { 
  template  = "${file("${path.module}/files/test1.cloud_config")}"

  vars = {
    ssh_key = file("~/.ssh/id_ed25519.pub")
    hostname = var.vm_name
    domain = "scurrilous.foo"
  }
}

# Create a local copy of the file, to transfer to Proxmox.
resource "local_file" "cloud_init_test1" {
  content   = data.template_file.cloud_init_test1.rendered
  filename  = "${path.module}/files/user_data_cloud_init_test1.cfg"
}

# Transfer the file to the Proxmox Host
resource "null_resource" "cloud_init_test1" {
  connection {
    type    = "ssh"
    user    = "root"
    private_key = file("~/.ssh/id_ed25519")
    host    = var.proxmox_host
  }

  provisioner "file" {
    source       = local_file.cloud_init_test1.filename
    destination  = "/var/lib/vz/snippets/cloud_init_test1.yml"
  }
}
Enter fullscreen mode Exit fullscreen mode
# main.tf - This is where I define the VMs I want to deploy with Terraform

resource "proxmox_vm_qemu" "cloudinit-test" {
    name = var.vm_name
    desc = "Testing Terraform and cloud-init"
    depends_on = [ null_resource.cloud_init_test1 ]
    # Node name has to be the same name as within the cluster
    # this might not include the FQDN
    target_node = var.proxmox_host

    # The template name to clone this vm from
    clone = var.template_name
    # Activate QEMU agent for this VM
    agent = 1

    os_type = "cloud-init"
    cores = 2
    sockets = 1
    vcpus = 0
    cpu = "host"
    memory = 2048
    scsihw = "virtio-scsi-single"

    # Setup the disk
    disks {
        ide {
            ide2 {
                cloudinit {
                    storage = "containers-and-vms"
                }
            }
        }
        scsi {
            scsi0 {
                disk {
                  size     = "32G"
                  storage  = "containers-and-vms"
                  discard  = true
                  iothread = true
                }
            }
        }
    }

    network {
        model = "virtio"
        bridge = var.nic_name
    tag = -1
    }

    # Setup the ip address using cloud-init.
    boot = "order=scsi0"
    # Keep in mind to use the CIDR notation for the ip.
    ipconfig0 = "ip=192.168.1.80/24,gw=192.168.1.1,ip6=dhcp"
    skip_ipv6 = true

    lifecycle {
      ignore_changes = [
        ciuser,
        sshkeys,
        network
      ]
    }
    cicustom = "user=local:snippets/cloud_init_test1.yml"
}
Enter fullscreen mode Exit fullscreen mode

In addition to the Terraform files, we also need the cloud-config file (cloud_init_test1.yml) that we're referencing in main.tf.

IMPORTANT If you specify a value for cicustom as I did here, the ciuser and sshkeys fields in the template definition (e.g. main.tf) are ignored in favor of whatever is in the cloud-config file, even when nothing is there. This also trumps whatever is in the base template. You must specify your SSH keys in your cloud-config file.

#cloud-config

ssh_authorized_keys:
  - <ssh public key 1>
  - <ssh public key 2>

runcmd:
  - apt-get update
  - apt-get install -y nginx

write_files:
  - content: |
      #!/bin/bash
      echo "ZOMBIES RULE BELGIUM?"
    path: /usr/local/bin/my-script
    permissions: '0755'

scripts-user:
  - /usr/local/bin/my-script
Enter fullscreen mode Exit fullscreen mode

So you can see here that you can run arbitrary commands at first boot with runcmd, and you can also run a custom Bash script with scripts-user and write-files. (See this writeup from SaturnCloud for more information).

You might notice that the Terraform template definition is pretty close in structure to the one I used in my last article. That's intentional - I set up the last one with cloud-init, but didn't do much with it. This one actually provisions the VM with cloud-init. You can also use Ansible playbooks to provision a VM, and I might talk about that in a future post, but in my next post I'm going to talk about doing something actually useful in my home infrastructure and setting up Plex.

Once again, we execute terraform plan. The plan looks good, so we apply it with terraform apply, wait a couple of minutes, and boom! We've got ourselves a VM with both cloud-init and QEMU Guest Agent. Pretty cool! Next time I'll show you how to use Ansible playbooks to provision your VMs

Top comments (2)

Collapse
 
piya__c204c9e90 profile image
Piya

Great read, very well explained!

Collapse
 
popefelix profile image
Aurelia Peters

That's very kind of you to say. Thank you!