larstobi / terraform-provider-multipass

Terraform provider for the Canonical Multipass virtual machine manager
MIT License
60 stars 13 forks source link

Feature request: accept cloudinit data inline #12

Open mark-rushakoff opened 2 years ago

mark-rushakoff commented 2 years ago

I am trying to use cloudinit_config with many part blocks to compose a MIME document to use for cloudinit against a multipass resource.

The cloudinit_config resource only exposes a rendered value. I am new to terraform, so maybe I'm missing something, but I don't see a way to write that rendered value to a file, and then provide the path of the file to the multipass resource.

I would like to be able to write:

data "cloudinit_config" "cloudinit_ubuntu" {
  part {
    content = file("./cloudinit/groups.yml")
  }
  part {
    content = file("./cloudinit/users.yml")
  }
  # etc.
}

resource "multipass_instance" "ubuntu" {
  name = "ubuntu"
  disk = "3072MiB"
  memory = "512MiB"

  # cloudinit_data would be mutually exclusive with cloudinit_file
  cloudinit_data = data.cloudinit_ubuntu.rendered
}

AFAICS, it should be okay to hold cloudinit_data in memory, and then pass it as stdin to multipass launch --cloud-init=-, but it could just as well go into a temporary file managed by the multipass provider if necessary.

larstobi commented 2 years ago

Hi, many thanks for your feature request!

Your feature request is a valid one, and is technically possible with multipass. One way to solve it is to have the provider write temporary files with the cloudinit content, and another is to use stdin to pass the content in directly, by using this feature: https://github.com/canonical/multipass/pull/628. Adding this feature is on my horizon. :-)

multipass doesn't provide a way to see the cloudinit file after it has been launched, nor does it give any checksum value for the contents, so it will be impossible to check if any content has changed from its side. So, we can spot any content diffs by using the Terraform state file only.

One way you can work around this provider's lack of a cloudinit_data parameter with Terraform is by using the hashicorp/local provider[1]. It provides you with a local_file resource that you can use to write files.

data "cloudinit_config" "cloudinit_ubuntu" {
  part {
    content = file("./cloudinit/groups.yml")
  }
  part {
    content = file("./cloudinit/users.yml")
  }
  # etc.
}

resource "local_file" "cloudinit_ubuntu" {
  filename = "ubuntu-cloudinit.yml"
  content  = data.cloudinit_config.cloudinit_ubuntu.rendered
}

resource "multipass_instance" "ubuntu" {
  name           = "ubuntu"
  disk           = "3072MiB"
  memory         = "512MiB"
  cloudinit_file = local_file.cloudinit_ubuntu.filename
}

[1] https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file

mark-rushakoff commented 2 years ago

Thanks for teaching me how to use the local_file resource properly. That approach correctly passes the rendered file to multipass, but unfortunately, multipass does not (yet) support multipart/mime cloudinit files: https://github.com/canonical/multipass/issues/1892

I can find another way to combine the YAML segments, which will be sufficient if I end up only needing to combine plain cloud config data.

Schachte commented 8 months ago

This is how I solved this (adjacent cloud-init template yaml inside of ./template/cloud-init.yaml.tpl) edit: Just realized it's fairly similar to @larstobi, but this features variable interpolation using template files, so slightly different.

I think passing this in via stdin would be awesome and happy to work on that PR if you haven't already!

resource "local_file" "cloudinit" {
  for_each = { for i, name in var.instance_names : name => {
    ip_address = var.ip_addresses[i]
  } }

  filename = "${path.module}/cloud-init-${each.key}.yaml"
  content = templatefile("${path.module}/templates/cloud-init.yaml.tpl", {
    ip_address = each.value.ip_address
  })
}

resource "multipass_instance" "dev_vm" {
  for_each = { for i, name in var.instance_names : name => {
    ip_address = var.ip_addresses[i]
  } }

  name   = each.key
  cpus   = var.cpus
  memory = var.memory
  disk   = var.disk
  image  = var.image

  cloudinit_file = local_file.cloudinit[each.key].filename
}