/ Devops

VirtOps #2: Managing existing infrastructure with Terraform

In the previous tutorial, we used Terraform to provision and update a new VM on Xen Orchestra. This hopefully demonstrated the power of Terraform but how can Terraform be used for existing infrastructure outside of Terraform's management?

Fortunately, Terraform was designed to interoperate with existing infrastructure to make these transitions easy. In this post, we will discuss Terraform's import functionality and walk through the process for importing an existing VM. Bringing infrastructure under Terraform's management follows these main steps:

  1. Identify the existing infrastructure to import
  2. Import the infrastructure into Terraform's state
  3. Write the Terraform configuration that matches the infrastructure
  4. Review the Terraform plan to ensure the configuration matches the expected state and infrastructure.
  5. Repeat steps 3 and 4 until Terraform's plan is a noop
  6. Apply Terraform to update the state

The code for this tutorial can be found on GitHub and we will assume you have a VM outside of Terraform's management. The following VM will be used throughout the rest of these steps.

Screen-Capture_select-area_20201117221511

While your terraform code will be different, it will follow the same process as outlined below.

The first step is to identify the ID associated with the VM and decide on the name for the resource. The example below imports the VM with id 5019156b-f40d-bc57-835b-4a259b177be1 into the xenorchestra_vm.imported resource.

$ terraform import xenorchestra_vm.imported 5019156b-f40d-bc57-835b-4a259b177be1
Error: resource address "xenorchestra_vm.imported" does not exist in the configuration.

Before importing this resource, please create its configuration in the root module. For example:

resource "xenorchestra_vm" "imported" {
  # (resource arguments)
}

Terraform will let us know that the resource hasn't been defined in code yet. Create a vm.tf file with the following placeholder for the resource and run terraform import again.

# vm.tf
resource "xenorchestra_vm" "imported" {
}

Terraform will now successfully import the resource.

$ terraform import xenorchestra_vm.imported 5019156b-f40d-bc57-835b-4a259b177be1
xenorchestra_vm.imported: Importing from ID "5019156b-f40d-bc57-835b-4a259b177be1"...
xenorchestra_vm.imported: Import prepared!
  Prepared xenorchestra_vm for import
xenorchestra_vm.imported: Refreshing state... [id=5019156b-f40d-bc57-835b-4a259b177be1]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

Terraform found the VM from the Xen Orchestra API and stored the relevant information about it in its state (more details on state can be found here). The state that Terraform retrieved can be displayed with the terraform state show command.

Note: your state will be dependent on the specific VM you imported and will likely not match the details below

$ terraform state show xenorchestra_vm.imported
# xenorchestra_vm.imported:
resource "xenorchestra_vm" "imported" {
    auto_poweron     = false
    cpus             = 2
    id               = "5019156b-f40d-bc57-835b-4a259b177be1"
    memory_max       = 2147483648
    name_description = "Debian 10 Cloudinit ready template from XO Hub"
    name_label       = "Debian 10 Cloudinit self-service"

    disk {
        attached   = true
        name_label = "debian root"
        position   = "0"
        size       = 4294967296
        sr_id      = "86a9757d-9c05-9fe0-e79a-8243cb1f37f3"
        vbd_id     = "981bf2ef-01ff-5430-8f8b-cdfdbe49f9cd"
        vdi_id     = "062a3cae-cf00-4008-9c96-2d840b2c22f8"
    }

    network {
        attached    = true
        device      = "0"
        mac_address = "c2:03:35:47:79:8f"
        network_id  = "6c4e1cdc-9fe0-0603-e53d-4790d1fce8dd"
    }

    timeouts {}
}

Running a terraform plan will show that the missing arguments cause an error.

Error: Missing required argument

  on template.tf line 36, in resource "xenorchestra_vm" "imported":
  36: resource "xenorchestra_vm" "imported" {

The argument "template" is required, but no definition was found.


Error: Missing required argument

  on template.tf line 36, in resource "xenorchestra_vm" "imported":
  36: resource "xenorchestra_vm" "imported" {

The argument "name_label" is required, but no definition was found.


Error: Missing required argument

  on template.tf line 36, in resource "xenorchestra_vm" "imported":
  36: resource "xenorchestra_vm" "imported" {

The argument "cpus" is required, but no definition was found.

Now it's our responsibility to write the Terraform code that matches the infrastructure. Since terraform is able to accurately "diff" the state of the infrastructure against the current code, we will iteratively write code until the terraform plan shows no changes. At that point, the code will accurately reflect the VM.

Since the terraform state show printed out HCL, we can copy the following lines and update vm.tf with the following content. I also added a fabricated template attribute since the error message from the provider indicated those fields were required.

resource "xenorchestra_vm" "imported" {
    auto_poweron     = false
    cpus             = 2
    memory_max       = 2147483648
    name_description = "Debian 10 Cloudinit ready template from XO Hub"
    name_label       = "Debian 10 Cloudinit self-service"
    template = "template"

    disk {
        attached   = true
        name_label = "debian root"
        size       = 4294967296
        sr_id      = "86a9757d-9c05-9fe0-e79a-8243cb1f37f3"
    }

    network {
        attached    = true
        mac_address = "c2:03:35:47:79:8f"
        network_id  = "6c4e1cdc-9fe0-0603-e53d-4790d1fce8dd"
    }
}

This leads to the following terraform plan

Terraform will perform the following actions:

  # xenorchestra_vm.imported must be replaced
-/+ resource "xenorchestra_vm" "imported" {
        auto_poweron     = false
      + core_os          = false
      + cpu_cap          = 0
      + cpu_weight       = 0
        cpus             = 2
      ~ id               = "5019156b-f40d-bc57-835b-4a259b177be1" -> (known after apply)
        memory_max       = 2147483648
        name_description = "Debian 10 Cloudinit ready template from XO Hub"
        name_label       = "Debian 10 Cloudinit self-service"
      + template         = "template" # forces replacement

      ~ disk {
            attached   = true
            name_label = "debian root"
          ~ position   = "0" -> (known after apply)
            size       = 4294967296
            sr_id      = "86a9757d-9c05-9fe0-e79a-8243cb1f37f3"
          ~ vbd_id     = "981bf2ef-01ff-5430-8f8b-cdfdbe49f9cd" -> (known after apply)
          ~ vdi_id     = "062a3cae-cf00-4008-9c96-2d840b2c22f8" -> (known after apply)
        }

      ~ network {
            attached    = true
          ~ device      = "0" -> (known after apply)
            mac_address = "c2:03:35:47:79:8f"
            network_id  = "6c4e1cdc-9fe0-0603-e53d-4790d1fce8dd"
        }

      - timeouts {}
    }

Plan: 1 to add, 0 to change, 1 to destroy.

As seen from the plan above, Terraform thinks it needs to recreate the resource. This is unexpected since we want Terraform to think our infrastructure is the source of truth rather than recreating it!

Since the template attribute was a placeholder and changing that attribute creates a new resource, we must instruct Terraform to ignore that field. We could provide the correct value, but the Terraform provider isn't yet able to identify what template the VM originated from (see this improvement for more details).

Add the following to the xenorchestra_vm.imported resource to have Terraform ignore the attribute change.

resource "xenorchestra_vm" "imported" {
...
...

  lifecycle {
    ignore_changes = [
      template,
    ]
  }
}

Terraform now thinks it needs to edit the resource rather than re-create it!

$ terraform plan

...

  # xenorchestra_vm.imported will be updated in-place
  ~ resource "xenorchestra_vm" "imported" {
        auto_poweron     = false
      + core_os          = false
      + cpu_cap          = 0
      + cpu_weight       = 0
        cpus             = 2
        id               = "5019156b-f40d-bc57-835b-4a259b177be1"
      + memory_max       = 1073741824
        name_description = "Debian 10 Cloudinit ready template from XO Hub"
        name_label       = "Debian 10 Cloudinit self-service"

        disk {
            attached   = true
            name_label = "debian root"
            position   = "0"
            size       = 4294967296
            sr_id      = "86a9757d-9c05-9fe0-e79a-8243cb1f37f3"
            vbd_id     = "981bf2ef-01ff-5430-8f8b-cdfdbe49f9cd"
            vdi_id     = "062a3cae-cf00-4008-9c96-2d840b2c22f8"
        }

        network {
            attached    = true
            device      = "0"
            mac_address = "c2:03:35:47:79:8f"
            network_id  = "6c4e1cdc-9fe0-0603-e53d-4790d1fce8dd"
        }

        timeouts {}
    }

Plan: 0 to add, 1 to change, 0 to destroy.

------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform can't guarantee
that exactly these actions will be performed if
"terraform apply" is subsequently run.

Continue to update your terraform code to reflect the changes terraform is trying to make. Eventually the plan will be a noop and you can proceed to apply the change.

Note: ignore_changes should be used sparingly because it means your infrastructure is only partially defined in code.*

The same process can be used for importing any infrastructure that Terraform supports. If you are interested in learning more about Terraform's import functionality, check out Hashicorp's import guide which explains more details about the process.

Up next in the devops series, we will explore how to create and manage VM templates with Packer, another popular Hashicorp tool, and deploy VMs from these templates with Terraform. This allows you to specify your machine images in code as well as making the images easily reproduced.