Terraform meets MAAS

Terraform meets MAAS

I recently posted about provisioning a Virtual Machine on the MAAS system, using a web User Interface. You can read about it in the following post.

MAAS: Deploying a virtual machine
Some time ago, I built a tiny data center using old 2012 MacMinis and MAAS. It took a lot of steps and effort to succeed, but in the end, all the computers were successfully provisioned. As a next step, let’s try to deploy a Virtual Machine on that metal. When

The same effect can be achieved using the terminal as MAAS provides a command line interface tool. Nevertheless, it is alright when provisioning a single machine, but gets troublesome when a whole infrastructure with dozens of virtual machines is required. So why not automate such a process?

Infrastructure as Code

Infrastructure as Code (IaC) is the automated process of managing and provisioning an infrastructure through code instead of through manual processes. The IT infrastructure managed by this process comprises physical equipment, such as bare-metal servers, virtual machines, and associated configuration resources. Infrastructure specification is described by configuration files that can be stored in the version control system, easily edited, and distributed.

There are generally two approaches to IaC: declarative (functional) vs. imperative (procedural). The difference between the declarative and the imperative approach is essentially what versus how. The declarative approach defines the desired state and the system executes what needs to happen to achieve that desired state. Imperative defines specific commands that need to be executed in the appropriate order to end with the desired conclusion.

One of the tools that provide capabilities for managing infrastructure as a code is Terraform.

Terraform by HashiCorp
Terraform is an infrastructure as code tool that enables you to safely and predictably provision and manage infrastructure in any cloud.

What is Terraform?

Terraform is an infrastructure as a code tool that let you define both cloud and on-prem resources in human-readable configuration files. They can be versioned, reused, and shared. It enables consistent workflow to provision and manage all of the infrastructure throughout its lifecycle. Terraform can manage low-level as well as high-level components, like computing, storage, networking resources, DNS entries, etc.

Terraform workflow consists of three stages:

  • Write: You define resources. For instance, you might create a configuration to deploy a virtual machine and a load balancer.
  • Plan: Terraform creates an execution plan describing the infrastructure it will create, update, or destroy based on the existing infrastructure and your configuration.
  • Apply: On approval, Terraform performs the proposed operations maintaining correct order and dependencies.
source https://developer.hashicorp.com/terraform/intro

Terraform creates and manages resources on cloud platforms and other services through its application programming interfaces. Terraform Providers enable Terraform to work with any platform or service with an accessible API.

source https://developer.hashicorp.com/terraform/intro

Canonical provides its own Terraform Provider for MAAS that can be easily employed for provisioning resources on bare-metal servers.

GitHub - maas/terraform-provider-maas: Terraform MAAS provider
Terraform MAAS provider. Contribute to maas/terraform-provider-maas development by creating an account on GitHub.

Initializing Terraform

First of all, we have to install Terraform itself. It can be downloaded directly from the website or installed through the package manager. In my case I used Brew.

brew install terraform

Once installation is finished, we create a working directory to describe our infrastructure and start with defining the provider. Let's create providers.tf file and add the following content.

terraform {
  required_providers {
    maas = {
      source  = "maas/maas"
      version = "~>1.0"
    }
  }
}

Now, we can issue the first Terraform command.

❯ terraform plan
╷
│ Error: Inconsistent dependency lock file
│
│ The following dependency selections recorded in the lock file are inconsistent with the current configuration:
│   - provider registry.terraform.io/maas/maas: required by this configuration but no version is selected
│
│ To make the initial dependency selections that will initialize the dependency lock file, run:
│   terraform init

The output informs that we need properly initialize dependencies. Let's run it.

❯ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding maas/maas versions matching "~> 1.0"...
╷
│ Error: Failed to query available provider packages
│
│ Could not retrieve the list of available versions for provider maas/maas: provider registry registry.terraform.io does not have a provider named
│ registry.terraform.io/maas/maas
│
│ All modules should specify their required_providers so that external consumers will get the correct providers when using a module. To see which modules are
│ currently depending on maas/maas, run the following command:
│     terraform providers
╵

It turns out that MAAS Terraform Provider is not in the repository. Therefore, we have to handle this situation on our own. Let's clone the MAAS Terraform Provider repository.

❯ git clone git@github.com:maas/terraform-provider-maas.git
Cloning into 'terraform-provider-maas'...
remote: Enumerating objects: 888, done.
remote: Counting objects: 100% (292/292), done.
remote: Compressing objects: 100% (192/192), done.
remote: Total 888 (delta 140), reused 150 (delta 89), pack-reused 596
Receiving objects: 100% (888/888), 298.28 KiB | 1.20 MiB/s, done.
Resolving deltas: 100% (491/491), done.

Then we have to navigate into the directory and run a build.

❯ cd terraform-provider-maas
❯ make install
mkdir -p /Users/slysj/terraform-provider-maas/bin
go build -o /Users/slysj/terraform-provider-maas/bin/terraform-provider-maas
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/maas/maas/1.0.1/$(go env GOOS)_$(go env GOARCH)
mv /Users/slysj/terraform-provider-maas/bin/terraform-provider-maas ~/.terraform.d/plugins/registry.terraform.io/maas/maas/1.0.1/$(go env GOOS)_$(go env GOARCH)
make and golang must be present in the system to execute a build.

Now, we can rerun the initialization.

❯ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding maas/maas versions matching "~> 1.0"...
- Installing maas/maas v1.0.1...
- Installed maas/maas v1.0.1 (unauthenticated)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

╷
│ Warning: Incomplete lock file information for providers
│
│ Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers:
│   - maas/maas
│
│ The current .terraform.lock.hcl file only includes checksums for darwin_amd64, so Terraform running on another platform will fail to install these
│ providers.
│
│ To calculate additional checksums for another platform, run:
│   terraform providers lock -platform=linux_amd64
│ (where linux_amd64 is the platform to generate)
╵

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.

We have successfully initialized the working directory!

Now, we have to update the providers.tf file to include the MAAS API key and MAAS URL. API key has to be obtained from MAAS UI.

Click your username at the top right (in my case it is kuba), and pick API keys. Copy the API key and paste it into providers.tf file.

terraform {
  required_providers {
    maas = {
      source  = "maas/maas"
      version = "~>1.0"
    }
  }
}

provider "maas" {
  api_version = "2.0"
  api_key = "<YOUR API KEY>"
  api_url = "http://127.0.0.1:5240/MAAS"
}

The updated file should include your API key in place of <YOUR API KEY>, and your MAAS address instead of http://127.0.0.1:5240/MAAS. Now, we are ready to write a configuration file to deploy the Virtual Machine instances.

Provisioning with Terraform

Let's define main.tf file with maas_vm_host_machine and maas_instance to provision a virtual machine with an operating system.

resource "maas_vm_host_machine" "test-vm" {
  vm_host = "macmini01"
  cores = 2
  memory = 2048
  hostname = "test-vm"
  storage_disks {
    size_gigabytes = 50
  }
  pool = "default"
}

resource "maas_instance" "test-vm" {
  allocate_params {
    min_cpu_count = 2
    min_memory = 2048
  }
  deploy_params {
    distro_series = "focal"
  }
  depends_on = [maas_vm_host_machine.test-vm]
}

The maas_vm_host_machine test-vm resource will provision a virtual machine on bare-metal macmini01 host. This machine will have 2 vCores, 2048 GB of RAM, and 50 GB of storage. The maas_instance test-vm will match the characteristics of maas_vm_host_machine and deploy Ubuntu "focal" on the machine. depends_on section means we have to create a virtual machine first before we can install an operating system on it. Now we are ready to apply the configuration!

❯ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # maas_instance.test-vm will be created
  + resource "maas_instance" "test-vm" {
      + cpu_count    = (known after apply)
      + fqdn         = (known after apply)
      + hostname     = (known after apply)
      + id           = (known after apply)
      + ip_addresses = (known after apply)
      + memory       = (known after apply)
      + pool         = (known after apply)
      + tags         = (known after apply)
      + zone         = (known after apply)

      + allocate_params {
          + min_cpu_count = 2
          + min_memory    = 2048
          + tags          = []
        }

      + deploy_params {
          + distro_series = "focal"
        }
    }

  # maas_vm_host_machine.test-vm will be created
  + resource "maas_vm_host_machine" "test-vm" {
      + cores    = 2
      + domain   = (known after apply)
      + hostname = "test-vm"
      + id       = (known after apply)
      + memory   = 2048
      + pool     = "default"
      + vm_host  = "macmini01"
      + zone     = (known after apply)

      + storage_disks {
          + size_gigabytes = 50
        }
    }

Plan: 2 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:

We can review the changes, type yes and hit Enter.

maas_vm_host_machine.test-vm: Creating...
maas_vm_host_machine.test-vm: Still creating... [10s elapsed]
...
maas_vm_host_machine.test-vm: Still creating... [2m0s elapsed]
...
maas_vm_host_machine.test-vm: Still creating... [2m10s elapsed]
...
maas_vm_host_machine.test-vm: Still creating... [3m0s elapsed]
maas_vm_host_machine.test-vm: Creation complete after 3m4s [id=aqn8qt]
maas_instance.test-vm: Creating...
...
maas_instance.test-vm: Still creating... [3m0s elapsed]
...
maas_instance.test-vm: Still creating... [5m30s elapsed]
maas_instance.test-vm: Creation complete after 5m34s [id=aqn8qt]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

test-vm has been created, it is visible on MAAS UI and we can now ssh to it.

❯ ssh test-vm.maas
The authenticity of host 'test-vm.maas (192.168.111.114)' can't be established.
ED25519 key fingerprint is SHA256:nUNPVw+kSN2Gro8a2aggy2Qbolzq5C8PORUMz7i+IiE.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'test-vm.maas' (ED25519) to the list of known hosts.
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-137-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sat Jan 28 11:50:21 UTC 2023

  System load:  0.0               Processes:             111
  Usage of /:   9.7% of 45.53GB   Users logged in:       0
  Memory usage: 11%               IPv4 address for ens4: 192.168.111.114
  Swap usage:   0%

43 updates can be applied immediately.
36 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable



The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@test-vm:~$

Once the provisioned infrastructure is not needed anymore, it can be easily disposed of by running destroy.

❯ terraform destroy
maas_vm_host_machine.test-vm: Refreshing state... [id=aqn8qt]
maas_instance.test-vm: Refreshing state... [id=aqn8qt]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # maas_instance.test-vm will be destroyed
  - resource "maas_instance" "test-vm" {
      - cpu_count    = 2 -> null
      - fqdn         = "test-vm.maas" -> null
      - hostname     = "test-vm" -> null
      - id           = "aqn8qt" -> null
      - ip_addresses = [
          - "192.168.111.114",
        ] -> null
      - memory       = 2048 -> null
      - pool         = "default" -> null
      - tags         = [
          - "pod-console-logging",
          - "virtual",
        ] -> null
      - zone         = "default" -> null

      - allocate_params {
          - min_cpu_count = 2 -> null
          - min_memory    = 2048 -> null
          - tags          = [] -> null
        }

      - deploy_params {
          - distro_series = "focal" -> null
        }
    }

  # maas_vm_host_machine.test-vm will be destroyed
  - resource "maas_vm_host_machine" "test-vm" {
      - cores    = 2 -> null
      - domain   = "maas" -> null
      - hostname = "test-vm" -> null
      - id       = "aqn8qt" -> null
      - memory   = 2048 -> null
      - pool     = "default" -> null
      - vm_host  = "macmini01" -> null
      - zone     = "default" -> null

      - storage_disks {
          - size_gigabytes = 50 -> null
        }
    }

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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value:

Review the changes, type yes, and hit Enter to run it.

maas_instance.test-vm: Destroying... [id=aqn8qt]
maas_instance.test-vm: Still destroying... [id=aqn8qt, 10s elapsed]
maas_instance.test-vm: Destruction complete after 13s
maas_vm_host_machine.test-vm: Destroying... [id=aqn8qt]
maas_vm_host_machine.test-vm: Destruction complete after 3s

Conclusion

Infrastructure as a code eases maintaining IT infrastructure throughout its full lifecycle. Terraform is the tool that implements this approach. Thanks to the notion of providers, Terraform is easily extendable and can automate the provisioning process on different clouds. MAAS Terraform provider enables provisioning on Canonical MAAS. The provider is pretty useful and usable but has plenty of glitches, therefore still requires developers' attention.