Terraform and NREC: Part II - Additional resources

Last changed: 2024-09-17

This document describes how to create and manage several instances (virtual machines) using Terraform. This document builds on Terraform and NREC: Part I - Basics. While part 1 relied on preexisting resources such as SSH key pairs and security groups, in this example we create everything from scratch.

The example file can be downloaded here: advanced.tf.

The examples in this document have been tested and verified with Terraform version 1.9.5:

Terraform v1.9.5
on linux_amd64
+ provider registry.terraform.io/terraform-provider-openstack/openstack v2.1.0

Image Name

In Part 1 we used image_name to specify our preferred image. By itself this is usually not a good idea, unless for testing purposes. The “GOLD” images provided in NREC are renewed (e.g. replaced) each month, and Terraform uses the image ID in its state. If using Terraform as a oneshot utility to spin up instances, this isn’t a problem.

The consequence of using image_name to specify the image is that Terraform’s own state becomes outdated when the NREC image is renamed. When using Terraform at a later time to make changes in the virtual infrastructure, it will destroy all running instances and create new ones, in order to comply with the configuration. This is probably not what you want. Running terraform plan in this scenario would output:

image_name:     "Outdated (Alma Linux 9)" => "GOLD Alma Linux 9" (forces new resource)

In order to combat this, we add the following code snippet to our openstack_compute_instance_v2 resource:

advanced.tf
1  lifecycle {
2    ignore_changes = [image_name,image_id]
3  }

This lifecycle meta-argument makes Terraform ignore changes to the image name. Another approach would be to specify image_id instead of image_name. We find the correct image_id by using the Openstack CLI:

$ openstack image list --status active

Multiple instances

Building on the basic.tf file discussed in Part 1:

basic.tf
 1# Define required providers
 2terraform {
 3  required_version = ">= 1.0"
 4  required_providers {
 5    openstack = {
 6      source  = "terraform-provider-openstack/openstack"
 7    }
 8  }
 9}
10
11# Configure the OpenStack Provider
12# Empty means using environment variables "OS_*". More info:
13# https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs
14provider "openstack" {}
15
16# Create a server
17resource "openstack_compute_instance_v2" "test-server" {
18  name = "test-server"
19  image_name = "GOLD Alma Linux 9"
20  flavor_name = "m1.small"
21
22  key_pair = "mykey"
23  security_groups = [ "default", "ssh_icmp_login.uio.no" ]
24
25  network {
26    name = "IPv6"
27  }
28}

This file provisions a single instance. We can add a count directive to specify how many we want to provision. When doing so, we should also make sure that the instances have unique names, and we accomplish that by using the count when specifying the instance name:

 1# Define required providers
 2terraform {
 3  required_version = ">= 1.0"
 4  required_providers {
 5    openstack = {
 6      source  = "terraform-provider-openstack/openstack"
 7    }
 8  }
 9}
10
11# Configure the OpenStack Provider
12# Empty means using environment variables "OS_*". More info:
13# https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs
14provider "openstack" {}
15
16#---------------------------------------------------------------------
17# Instances
18#---------------------------------------------------------------------
19resource "openstack_compute_instance_v2" "instance" {
20  count = 5
21  name = "test-${count.index}"
22  image_name = "GOLD Alma Linux 9"
23  flavor_name = "m1.small"
24
25  key_pair = "my-terraform-key"
26  security_groups = [ "default", "uio-ssh-icmp" ]
27
28  network {
29    name = "IPv6"
30  }
31
32  lifecycle {
33    ignore_changes = [image_name,image_id]
34  }
35}

When running this file with terraform apply, a total of 5 instances are created, as expected:

$ openstack server list
+--------------------------------------+--------+--------+----------------------------------------+-------------------+----------+
| ID                                   | Name   | Status | Networks                               | Image             | Flavor   |
+--------------------------------------+--------+--------+----------------------------------------+-------------------+----------+
| 73c746ca-98a9-46f7-9bfd-22e334fe3cb1 | test-1 | ACTIVE | IPv6=10.2.1.4, 2001:700:2:8201::15be   | GOLD Alma Linux 9 | m1.small |
| 243af083-4ff8-4212-8449-b4d977fed61c | test-3 | ACTIVE | IPv6=10.2.1.191, 2001:700:2:8201::121e | GOLD Alma Linux 9 | m1.small |
| b138d167-7158-4838-96d7-7b18083c1294 | test-0 | ACTIVE | IPv6=10.2.2.135, 2001:700:2:8201::1204 | GOLD Alma Linux 9 | m1.small |
| b65042e0-1692-4d3b-86f6-a308770b6c4f | test-2 | ACTIVE | IPv6=10.2.0.163, 2001:700:2:8201::1691 | GOLD Alma Linux 9 | m1.small |
| db8e0326-cf16-491a-b9cb-973d6ae1cfff | test-4 | ACTIVE | IPv6=10.2.1.199, 2001:700:2:8201::107c | GOLD Alma Linux 9 | m1.small |
+--------------------------------------+--------+--------+----------------------------------------+-------------------+----------+

Key pairs

In the previous examples, we relied on the key pair already existing in NREC. If we don’t already have a key pair in NREC, or we wish to use another one, we can use Terraform to manage this part.

We can have Terraform automatically create a key pair for us, instead of relying on a preexisting key pair. This is accomplished by creating a resource block for a key pair:

 1#---------------------------------------------------------------------
 2# SSH key
 3#---------------------------------------------------------------------
 4resource "openstack_compute_keypair_v2" "my-terraform-key" {
 5  name = "my-terraform-key"
 6  public_key = file("~/.ssh/id_ed25519.pub")
 7}
 8
 9#---------------------------------------------------------------------
10# Instances
11#---------------------------------------------------------------------
12resource "openstack_compute_instance_v2" "instance" {
13  count = 5
14  name = "test-${count.index}"
15  image_name = "GOLD Alma Linux 9"
16  flavor_name = "m1.small"
17
18  key_pair = "my-terraform-key"
19  security_groups = [ "default", "uio-ssh-icmp" ]
20
21  network {
22    name = "IPv6"
23  }
24
25  lifecycle {
26    ignore_changes = [image_name,image_id]
27  }
28}

The public key file must exist on disk with the given path, Terraform will not create it for us. After running Terraform, we can verify that the key has been created:

$ openstack keypair list
+------------------+-------------------------------------------------+
| Name             | Fingerprint                                     |
+------------------+-------------------------------------------------+
| my-terraform-key | e2:2e:26:7f:5d:98:9e:8f:5e:fd:c7:d5:d0:6b:44:e7 |
| mykey            | e2:2e:26:7f:5d:98:9e:8f:5e:fd:c7:d5:d0:6b:44:e7 |
+------------------+-------------------------------------------------+

Security groups

In all the previous examples, we use existing security groups when provisioning instances. We can use Terraform to create security groups on the fly for us to use:

 1#---------------------------------------------------------------------
 2# Security groups
 3#---------------------------------------------------------------------
 4resource "openstack_networking_secgroup_v2" "instance_access" {
 5  name = "uio-ssh-icmp"
 6  description = "Allow SSH and ICMP access from UiO"
 7}
 8
 9# Allow ssh from IPv4 net
10resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv4" {
11  direction = "ingress"
12  ethertype = "IPv4"
13  protocol  = "tcp"
14  port_range_min = 22
15  port_range_max = 22
16  remote_ip_prefix = "129.240.0.0/16"
17  security_group_id = openstack_networking_secgroup_v2.instance_access.id
18}
19
20# Allow ssh from IPv6 net
21resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv6" {
22  direction = "ingress"
23  ethertype = "IPv6"
24  protocol  = "tcp"
25  port_range_min = 22
26  port_range_max = 22
27  remote_ip_prefix = "2001:700:100::/41"
28  security_group_id = openstack_networking_secgroup_v2.instance_access.id
29}
30
31# Allow icmp from IPv4 net
32resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv4" {
33  direction = "ingress"
34  ethertype = "IPv4"
35  protocol  = "icmp"
36  remote_ip_prefix = "129.240.0.0/16"
37  security_group_id = openstack_networking_secgroup_v2.instance_access.id
38}
39
40# Allow icmp from IPv6 net
41resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv6" {
42  direction = "ingress"
43  ethertype = "IPv6"
44  protocol = "ipv6-icmp"
45  remote_ip_prefix = "2001:700:100::/41"
46  security_group_id = openstack_networking_secgroup_v2.instance_access.id
47}
48
49#---------------------------------------------------------------------
50# Instances
51#---------------------------------------------------------------------
52resource "openstack_compute_instance_v2" "instance" {
53  count = 5
54  name = "test-${count.index}"
55  image_name = "GOLD Alma Linux 9"
56  flavor_name = "m1.small"
57
58  key_pair = "my-terraform-key"
59  security_groups = [ "default", "uio-ssh-icmp" ]
60
61  network {
62    name = "IPv6"
63  }
64
65  lifecycle {
66    ignore_changes = [image_name,image_id]
67  }
68}

There is a lot of new stuff here:

  1. Line 4-7 contains a resource for a security group. This is pretty straightforward and only contains a name and description

  2. Line 10-47 contains 4 security group rules. They are all ingress rules (e.g. incoming traffic) and allows for SSH and ICMP from the UiO IPv4 and IPv6 networks.

  3. The security_group_id is a required field which specifies the security group where the rule shall be applied, and we use the Terraform object notation to specify the security group we created earlier.

As before, 5 instances are created. In addition a new security group is created, with the name and description as specified in the Terraform file:

$ openstack security group list -c Name -c Description
+-------------------+------------------------------------+
| Name              | Description                        |
+-------------------+------------------------------------+
| ssh_icmp_from_uio | Allows ssh and ping from all UiO   |
| default           | Default security group             |
| uio-ssh-icmp      | Allow SSH and ICMP access from UiO |
+-------------------+------------------------------------+

We can also inspect the security group uio-ssh-icmp that we created, to verify that the specified rules are present:

$ openstack security group rule list --long uio-ssh-icmp
+--------------------------------------+-------------+-------------------+------------+-----------+-----------+-----------------------+
| ID                                   | IP Protocol | IP Range          | Port Range | Direction | Ethertype | Remote Security Group |
+--------------------------------------+-------------+-------------------+------------+-----------+-----------+-----------------------+
| 391b4869-e900-44d8-9b7e-77318b9484ba | ipv6-icmp   | 2001:700:100::/41 |            | ingress   | IPv6      | None                  |
| 6f06e10a-99d8-4e3b-9dd3-6b20ff43aa28 | tcp         | 2001:700:100::/41 | 22:22      | ingress   | IPv6      | None                  |
| 88e6d5cf-1479-45a7-aa3f-8921ef84b939 | None        | None              |            | egress    | IPv4      | None                  |
| 98827cd8-7461-42c1-af8d-209813a15507 | icmp        | 129.240.0.0/16    |            | ingress   | IPv4      | None                  |
| 9c37c84c-e06c-4a0f-8f8e-24799210ec99 | tcp         | 129.240.0.0/16    | 22:22      | ingress   | IPv4      | None                  |
| bf54f0b2-844e-41a7-8f90-a11fb2c808c0 | None        | None              |            | egress    | IPv6      | None                  |
+--------------------------------------+-------------+-------------------+------------+-----------+-----------+-----------------------+

Volumes

Creating volumes is often required, and Terraform can do that as well. In order to create a volume you will define the resource:

1resource "openstack_blockstorage_volume_v3" "volume" {
2  name = "my-volume"
3  size = "10"
4}

Here, we create a volume named “my-volume” with a size of 10 GB. We also want to attach the volume to one of our instances:

1resource "openstack_compute_volume_attach_v2" "volumes" {
2  instance_id = openstack_compute_instance_v2.instance.0.id
3  volume_id   = openstack_blockstorage_volume_v3.volume.id
4}

In this example, we choose to attach the volume to instance number 0, which is the instance named “test-0”. We can inspect using Openstack CLI:

$ openstack volume list
+--------------------------------------+--------------+-----------+------+---------------------------------+
| ID                                   | Name         | Status    | Size | Attached to                     |
+--------------------------------------+--------------+-----------+------+---------------------------------+
| b5240613-404d-4b85-a28b-8ad32f8b0652 | my-volume    | in-use    |   10 | Attached to test-0 on /dev/sdb  |
+--------------------------------------+--------------+-----------+------+---------------------------------+

Complete example

A complete listing of the example file advanced.tf used in this document is provided below.

advanced.tf
  1# Define required providers
  2terraform {
  3  required_version = ">= 1.0"
  4  required_providers {
  5    openstack = {
  6      source  = "terraform-provider-openstack/openstack"
  7    }
  8  }
  9}
 10
 11# Configure the OpenStack Provider
 12# Empty means using environment variables "OS_*". More info:
 13# https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs
 14provider "openstack" {}
 15
 16#---------------------------------------------------------------------
 17# SSH key
 18#---------------------------------------------------------------------
 19resource "openstack_compute_keypair_v2" "my-terraform-key" {
 20  name = "my-terraform-key"
 21  public_key = file("~/.ssh/id_ed25519.pub")
 22}
 23
 24#---------------------------------------------------------------------
 25# Security groups
 26#---------------------------------------------------------------------
 27resource "openstack_networking_secgroup_v2" "instance_access" {
 28  name = "uio-ssh-icmp"
 29  description = "Allow SSH and ICMP access from UiO"
 30}
 31
 32# Allow ssh from IPv4 net
 33resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv4" {
 34  direction = "ingress"
 35  ethertype = "IPv4"
 36  protocol  = "tcp"
 37  port_range_min = 22
 38  port_range_max = 22
 39  remote_ip_prefix = "129.240.0.0/16"
 40  security_group_id = openstack_networking_secgroup_v2.instance_access.id
 41}
 42
 43# Allow ssh from IPv6 net
 44resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv6" {
 45  direction = "ingress"
 46  ethertype = "IPv6"
 47  protocol  = "tcp"
 48  port_range_min = 22
 49  port_range_max = 22
 50  remote_ip_prefix = "2001:700:100::/41"
 51  security_group_id = openstack_networking_secgroup_v2.instance_access.id
 52}
 53
 54# Allow icmp from IPv4 net
 55resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv4" {
 56  direction = "ingress"
 57  ethertype = "IPv4"
 58  protocol  = "icmp"
 59  remote_ip_prefix = "129.240.0.0/16"
 60  security_group_id = openstack_networking_secgroup_v2.instance_access.id
 61}
 62
 63# Allow icmp from IPv6 net
 64resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv6" {
 65  direction = "ingress"
 66  ethertype = "IPv6"
 67  protocol = "ipv6-icmp"
 68  remote_ip_prefix = "2001:700:100::/41"
 69  security_group_id = openstack_networking_secgroup_v2.instance_access.id
 70}
 71
 72#---------------------------------------------------------------------
 73# Instances
 74#---------------------------------------------------------------------
 75resource "openstack_compute_instance_v2" "instance" {
 76  count = 5
 77  name = "test-${count.index}"
 78  image_name = "GOLD Alma Linux 9"
 79  flavor_name = "m1.small"
 80
 81  key_pair = "my-terraform-key"
 82  security_groups = [ "default", "uio-ssh-icmp" ]
 83
 84  network {
 85    name = "IPv6"
 86  }
 87
 88  lifecycle {
 89    ignore_changes = [image_name,image_id]
 90  }
 91}
 92
 93#---------------------------------------------------------------------
 94# Volumes
 95#---------------------------------------------------------------------
 96resource "openstack_blockstorage_volume_v3" "volume" {
 97  name = "my-volume"
 98  size = "10"
 99}
100
101# Attach volume
102resource "openstack_compute_volume_attach_v2" "volumes" {
103  instance_id = openstack_compute_instance_v2.instance.0.id
104  volume_id   = openstack_blockstorage_volume_v3.volume.id
105}