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:
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:
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:
Line 4-7 contains a resource for a security group. This is pretty straightforward and only contains a name and description
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.
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.
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}