TL;DR) Terraform is a powerful tool to manage an Infrastructure using a description language. In this article, we show how to manage existing resources such as Compute instances and Firewalling Security Groups; as well as how to apply changes to them. It relies on a plugin specially tailored to work with Exoscale infrastructure.

article cover

Introduction

Previously published articles showed how to deploy new infrastructure like a Kubernetes cluster, OpenShift.io, or HAProxy using Ansible or the CloudStack API client. Most of the time though, we are managing existing setups, instances, security groups and what not.

With the latest release of our Terraform provider, it’s easier than ever to handle the Infrastructure as Code (IaC). This post details how one can import and manage their existing infrastructure setup in Terraform.

Setup

Once you’ve installed Terraform, install the provider built for Exoscale. As of yet, it’s not part of the official list of providers but we are working on it.

Installation of the Terraform provider

  1. Create a folder
  2. Download the binary provider from GitHub;
  3. Create a folder for the plugins .terraform/plugins/(darwin|linux|windows)_amd64/ (or globally into ~/.terraform, or %APPDATA%\terraform\plugins);
  4. Place the provider into it;
  5. Initialize the plugin with terraform init.
$ terraform version
Terraform v0.11.2
+ provider.exoscale v0.9.3

Configuration

Managing the infrastructure uses the CloudStack API. In order to do so, we have to configure the provider with the API credentials linked with our account. Into the Portal, navigate to Account > Profile > API Keys. Put your API Key and Secret Key into a file called terraform.tfvars.

# terraform.tfvars
token = "EXO..." # API Key
secret = "..."   # Secret Key

Terraform uses its own format called the HashiCorp Configuration Language (HCL). Although, the extension used is often .tf. HCL is related to JSON with the addition of comments and references.

Those values will get imported into input variables.

# variables.tf
variable "token" {}
variable "secret" {}

Now, we are ready to configure the freshly installed provider.

# config.tf
provider "exoscale" {
  version = "~> 0.9"
  token = "${var.token}"
  secret = "${var.secret}"
}

Tip: having separate files makes it easier to keep the sensitive information out of a version control system such a Git or Mercurial (hg).

Importing a Compute instance

For the sake of the exercise, we will create a crash-test dummy Compute instance, modestly called after a famous computer scientist, Ada Byron, Countess of Lovelace.

exoscale console showing the instance information

In Terraform, we manage resources of various kinds. A Compute instance is of the exoscale_compute kind. ada-lovelace represents the hostname. Before importing a resource, it’s important to create an empty resource.

# machines.tf
resource "exoscale_compute" "ada" {
  # ...
}

The import requires two pieces of information: the resource name composed of its kind (exoscale_compute) and label (ada) as well as the identifier (Instance ID).

$ terraform import exoscale_compute.ada 7fd56f8b-e8cb-4014-b8bd-e9d9821ff91f

exoscale_compute.ada: Importing from ID "7fd56f8b-e8cb-4014-b8bd-e9d9821ff91f"...
exoscale_compute.ada: Import complete!

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.

Then, the show command displays the imported details of our resources.

$ terraform show
exoscale_compute.ada:
  id = 7fd56f8b-e8cb-4014-b8bd-e9d9821ff91f
  affinity_groups.# = 0
  disk_size = 10
  display_name = ada-lovelace
  ip_address = 159.100.241.221
  key_pair = yoan@exo
  name = VM-7fd56f8b-e8cb-4014-b8bd-e9d9821ff91f
  security_groups.# = 1
  security_groups.3814588639 = default
  size = Tiny
  state = Running
  template = Linux Debian 9 64-bit
  zone = ch-gva-2

Now comes the tedious part, we have to accordingly update our file. Some values are computed meaning you can read them but cannot change them, e.g. the ip_address which is provided.

# machines.tf
resource "exoscale_compute" "ada" {
  display_name = "ada-lovelace"
  key_pair = "yoan@exo"
  disk_size = 10
  size = "Tiny"
  template = "Linux Debian 9 64-bit"
  zone = "ch-gva-2"

  security_groups = ["default"]
}

Once done, trying to apply the plan should not do anything.

$ terraform plan

exoscale_compute.ada: Refreshing state... (ID: ea4d2802-62cb-42d8-a25c-b9b856c7778d)

No changes. Infrastructure is up-to-date.

Very good! The definition matches the reality. Our infrastructure has been put into code.

Importing an existing security group

As a Compute instance must belong to a Security Group, we will also manage the provided default Security Group with Terraform. In the Firewalling section, we will find its identifier.

default security group

# security_groups.tf
resource "exoscale_security_group" "default" {
  # ...
}

And, just like for the Compute instance, import the Security Group.

$ terraform import exoscale_security_group.default 21a14a60-be48-4bde-8509-ae6ed0231ed0
exoscale_security_group.default: Import complete!
  Imported exoscale_security_group (ID: 21a14a60-be48-4bde-8509-ae6ed0231ed0)
  Imported exoscale_security_group_rule (ID: 96366510-f356-42b2-91e8-5fd0b2a36c3f)
  Imported exoscale_security_group_rule (ID: 6b7d3e9b-b7a6-4bcb-9c98-145751bf2d99)

Import successful!

It imported three resources, the Security Group as well as the two rules it contains.

$ terraform show
exoscale_security_group.default:
  id = 21a14a60-be48-4bde-8509-ae6ed0231ed0
  description = "Default Security Group"
  name = "default"

exoscale_security_group_rule.default:
   ...

exoscale_security_group_rule.default-1:
   ...

We can now fill their description. Depending on the protocol or the rule, some bits are not required. E.g., a TCP rule only needs to have a port range and does not require the ICMP type and code details.

# security_groups.tf
resource "exoscale_security_group" "default" {
  name = "default"
  description = "Default Security Group"
}

resource "exoscale_security_group_rule" "default" {
  type = "INGRESS"
  security_group_id = "${exoscale_security_group.default.id}"
  protocol = "ICMP"
  icmp_type = 8
  icmp_code = 0
  cidr = "0.0.0.0/0"
}

resource "exoscale_security_group_rule" "default-1" {
  type = "INGRESS"
  security_group_id = "${exoscale_security_group.default.id}"
  protocol = "TCP"
  start_port = 22
  end_port = 22
  cidr = "0.0.0.0/0"
}

Verify that no mistakes were made. The ordering of the rules may differ from the given example as default-1 could be the ICMP rule instead of the TCP one.

$ terraform plan

No changes. Infrastructure is up-to-date.

As you’ve probably noticed, we replaced the security_group_id with a computed value instead of the real ones. Doing so informs Terraform that those resources are linked and must be managed together.

Linking the Security Group to the Compute instance

We should also link the Compute instance with the Security Group in a similar fashion than we did for the rules. In this use case, we are free to use the .name or the .id property.

 # machines.tf
 resource "exoscale_compute" "ada" {
   # ...

-  security_groups = ["default"]
+  security_groups = ["${exoscale_security_group.default.name}"]
 }

Verify that the previous change didn’t change anything.

$ terraform plan

No changes.

Terraform can show how the resources are linked with one another.

$ terraform graph | dot -Tpng > graph.png

Terraform graph of dependencies

Altering the Security Group

Before any drastic operations like scaling the Compute instance, we will modify the rules by adding a description to rules of the default Security Group.

 # security_groups.tf
 resource "exoscale_security_group_rule" "default" {
   ...
   protocol = "ICMP"
+  description = "Ping"
   ...
 }

 resource "exoscale_security_group_rule" "default-1" {
   ...
   protocol = "TCP"
+  description = "SSH Access"
   ...
 }

Now comes the time to apply those changes.

$ terraform apply

...

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

As you’ve experienced in the web interface, a rule cannot be altered and must be recreated. How pleasant is this way of doing it?

Scaling up the Compute instance

Another feature we can use here is to give our machine more Disk space as well as more CPU and memory power.

 # machines.tf
 resource "exoscale_compute" "ada" {
   key_pair = "yoan@exo"
-  disk_size = 10
-  size = "Tiny"
+  disk_size = 42
+  size = "Medium"
   template = "Linux Debian 9 64-bit"
   zone = "ch-gva-2"

   security_groups = ["${exoscale_security_group.default.name}"]
 }

NB: the disk space can only be expanded, made bigger. But the size can go either way.

When applying the new plan, the Compute instance will stop and start again using the new configuration.

And stopping it

Last but not least, you can manage the state of the instance.

resource "exoscale_compute" "ada" {
  # ...

  state = "Stopped"
}

Let’s stop it.

$ terraform apply

  ~ exoscale_compute.ada
      state: "Running" => "Stopped"


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

Scaling horizontally?

Terraform is also able to scale horizontally using the count parameter.

Let’s explore how we would import our machines if we had two instances: ada-lovelace-0 and ada-lovelace-1.

# machines.tf
resource "exoscale_compute" "ada" {
  count = 2
  display_name = "ada-lovelace-${count.index}"
  ...
}

And then, by specifying which one is which, Terraform will link them to the Compute resource.

$ terraform import "exoscale_compute.ada[0]" <ID>

$ terraform import "exoscale_compute.ada[1]" <ID>

Conclusion

We’ve only scratched the surface of what can be done with Terraform. Most of the resources that are accessible through the Portal can be managed using the exoscale provider. E.g. DNS Domains, Anti-Affinity Groups, IP Addresses, etc.