Terraform and UH-IaaS: Part II - Additional resources

Last changed: 2019-10-21

This document describes how to create and manage several instances (virtual machines) using Terraform. This document builds on Terraform and UH-IaaS: 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.

Image ID

In Part 1 we used image_name to specify our preferred image. This is usually not a good idea, unless for testing purposes. The “GOLD” images provided in UH-IaaS 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. But if you rely on Terraform to maintain your virtual infrastructure over time, switching to image_id is encouraged.

The consequence of using image_name to specify the image is that Terraform’s own state becomes outdated. 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 (CentOS)" => "GOLD CentOS 7" (forces new resource)

We find the correct image_id by using the Openstack CLI:

$ openstack image list --status active
+--------------------------------------+-----------------------------------+--------+
| ID                                   | Name                              | Status |
+--------------------------------------+-----------------------------------+--------+
| ea951bf5-9bda-4aef-af1a-0cecba3089fc | GOLD CentOS 6                     | active |
| 4756b700-9489-4d59-bfd6-24d3b8b4167b | GOLD CentOS 7                     | active |
| 108b6b0c-d88f-4683-9f44-3ca7329674dd | GOLD Debian 9                     | active |
| a15f6150-4ab3-409a-a5b4-3cb69bd7b409 | GOLD Fedora 28                    | active |
| be5bce21-3346-4bb9-bc22-3a780d77b4d3 | GOLD Ubuntu 16.04 LTS             | active |
| 974d7df1-d845-4bf0-a3c0-d95d85267d43 | GOLD Ubuntu 18.04 LTS             | active |
| b2d189c0-a5b4-4660-8007-555f34dcd4c4 | GOLD Windows Server 2016 Standard | active |
| b7047043-8d00-4ab5-8db5-8b2688d0d74b | GOLD Windows Server 2019 Core     | active |
| 72568f04-d909-4809-8b0a-279679c054de | GOLD Windows Server 2019 Standard | active |
+--------------------------------------+-----------------------------------+--------+

Instead of specifying image_name as in Part 1:

basic.tf
1
    image_name = "GOLD CentOS 7"

We use the image_id for the “GOLD CentOS 7” image found using Openstack CLI above:

advanced.tf
1
    image_id = "4756b700-9489-4d59-bfd6-24d3b8b4167b"

Multiple instances

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

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
provider "openstack" {}

# Instances
resource "openstack_compute_instance_v2" "instance" {
    count = 5
    name = "test-${count.index}"
    image_id = "4756b700-9489-4d59-bfd6-24d3b8b4167b"
    flavor_name = "m1.small"

    key_pair = "my-terraform-key"
    security_groups = [ "default", "ssh-and-icmp" ]

    network {
        name = "IPv6"
    }
}

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

$ openstack server list -c Name -c Networks -c Image -c Flavor
+--------+---------------------------------------+---------------+----------+
| Name   | Networks                              | Image         | Flavor   |
+--------+---------------------------------------+---------------+----------+
| test-4 | IPv6=2001:700:2:8201::1033, 10.2.0.51 | GOLD CentOS 7 | m1.small |
| test-0 | IPv6=2001:700:2:8201::1029, 10.2.0.68 | GOLD CentOS 7 | m1.small |
| test-2 | IPv6=2001:700:2:8201::1009, 10.2.0.62 | GOLD CentOS 7 | m1.small |
| test-3 | IPv6=2001:700:2:8201::1027, 10.2.0.36 | GOLD CentOS 7 | m1.small |
| test-1 | IPv6=2001:700:2:8201::101a, 10.2.0.21 | GOLD CentOS 7 | m1.small |
+--------+---------------------------------------+---------------+----------+

Key pairs

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
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# SSH key
resource "openstack_compute_keypair_v2" "keypair" {
    name = "my-terraform-key"
    public_key = "${file("~/.ssh/id_rsa.pub")}"
}

# Instances
resource "openstack_compute_instance_v2" "instance" {
    count = 5
    name = "test-${count.index}"
    image_id = "4756b700-9489-4d59-bfd6-24d3b8b4167b"
    flavor_name = "m1.small"

    key_pair = "my-terraform-key"
    security_groups = [ "default", "ssh-and-icmp" ]

    network {
        name = "IPv6"
    }
}

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
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# Security group
resource "openstack_networking_secgroup_v2" "instance_access" {
    name = "ssh-and-icmp"
    description = "Security group for allowing SSH and ICMP access"
}

# Allow ssh from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv4" {
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "129.240.0.0/16"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Allow ssh from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv6" {
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "2001:700:100::/40"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Allow icmp from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv4" {
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "icmp"
    remote_ip_prefix = "129.240.0.0/16"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Allow icmp from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv6" {
    direction = "ingress"
    ethertype = "IPv6"
    protocol = "icmp"
    remote_ip_prefix = "2001:700:100::/40"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Instances
resource "openstack_compute_instance_v2" "instance" {
    count = 5
    name = "test-${count.index}"
    image_id = "4756b700-9489-4d59-bfd6-24d3b8b4167b"
    flavor_name = "m1.small"

    key_pair = "my-terraform-key"
    security_groups = [ "default", "ssh-and-icmp" ]

    network {
        name = "IPv6"
    }
}

There is a lot of new stuff here:

  1. Line 3-5 contains a resource for a security group. This is pretty straightforward and only contains a name and description
  2. Line 9-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                                     |
+--------------+-------------------------------------------------+
| RDP          |                                                 |
| ssh-and-icmp | Security group for allowing SSH and ICMP access |
| SSH and ICMP |                                                 |
| default      | Default security group                          |
+--------------+-------------------------------------------------+

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

$ openstack security group show ssh-and-icmp
+-----------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Field           | Value                                                                                                                                                                                                                                                  |
+-----------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| created_at      | 2019-04-24T12:24:35Z                                                                                                                                                                                                                                   |
| description     | Security group for allowing SSH and ICMP access                                                                                                                                                                                                        |
| id              | 43863c7f-d105-47a5-afe2-22d74f7a4623                                                                                                                                                                                                                   |
| name            | ssh-and-icmp                                                                                                                                                                                                                                           |
| project_id      | b56e80c7c777419585b13ebafe024330                                                                                                                                                                                                                       |
| revision_number | 6                                                                                                                                                                                                                                                      |
| rules           | created_at='2019-04-24T12:24:35Z', direction='egress', ethertype='IPv6', id='53bfef03-fea6-4504-a996-69c12f5c00bd', updated_at='2019-04-24T12:24:35Z'                                                                                                  |
|                 | created_at='2019-04-24T12:24:35Z', direction='egress', ethertype='IPv4', id='7565bdf1-827a-4736-ba1c-dab822037c4b', updated_at='2019-04-24T12:24:35Z'                                                                                                  |
|                 | created_at='2019-04-24T12:24:36Z', direction='ingress', ethertype='IPv4', id='93458178-15b1-4ae5-bee0-225ae56aeeef', port_range_max='22', port_range_min='22', protocol='tcp', remote_ip_prefix='129.240.0.0/16', updated_at='2019-04-24T12:24:36Z'    |
|                 | created_at='2019-04-24T12:24:36Z', direction='ingress', ethertype='IPv4', id='9d1724ae-c375-4b64-98ec-43d0f6b58383', protocol='icmp', remote_ip_prefix='129.240.0.0/16', updated_at='2019-04-24T12:24:36Z'                                             |
|                 | created_at='2019-04-24T12:24:36Z', direction='ingress', ethertype='IPv6', id='b0d110ad-8e43-4493-a178-a3ef56854c20', protocol='icmp', remote_ip_prefix='2001:700:100::/40', updated_at='2019-04-24T12:24:36Z'                                          |
|                 | created_at='2019-04-24T12:24:37Z', direction='ingress', ethertype='IPv6', id='e7131d6e-9a56-43ca-819d-bd3428013b44', port_range_max='22', port_range_min='22', protocol='tcp', remote_ip_prefix='2001:700:100::/40', updated_at='2019-04-24T12:24:37Z' |
| updated_at      | 2019-04-24T12:24:37Z                                                                                                                                                                                                                                   |
+-----------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

Volumes

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

1
2
3
4
5
# Volume
resource "openstack_blockstorage_volume_v2" "volume" {
    name = "my-volume"
    size = "10"
}

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:

1
2
3
4
5
# Attach volume
resource "openstack_compute_volume_attach_v2" "volumes" {
    instance_id = "${openstack_compute_instance_v2.instance.0.id}"
    volume_id   = "${openstack_blockstorage_volume_v2.volume.id}"
}

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                     |
+--------------------------------------+-----------+--------+------+---------------------------------+
| b75b654e-bd74-4796-9405-27ca2e056e96 | 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
provider "openstack" {}

# SSH key
resource "openstack_compute_keypair_v2" "keypair" {
    name = "my-terraform-key"
    public_key = "${file("~/.ssh/id_rsa.pub")}"
}

# Security group
resource "openstack_networking_secgroup_v2" "instance_access" {
    name = "ssh-and-icmp"
    description = "Security group for allowing SSH and ICMP access"
}

# Allow ssh from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv4" {
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "129.240.0.0/16"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Allow ssh from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv6" {
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "2001:700:100::/40"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Allow icmp from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv4" {
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "icmp"
    remote_ip_prefix = "129.240.0.0/16"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Allow icmp from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv6" {
    direction = "ingress"
    ethertype = "IPv6"
    protocol = "icmp"
    remote_ip_prefix = "2001:700:100::/40"
    security_group_id = "${openstack_networking_secgroup_v2.instance_access.id}"
}

# Instances
resource "openstack_compute_instance_v2" "instance" {
    count = 5
    name = "test-${count.index}"
    image_id = "4756b700-9489-4d59-bfd6-24d3b8b4167b"
    flavor_name = "m1.small"

    key_pair = "my-terraform-key"
    security_groups = [ "default", "ssh-and-icmp" ]

    network {
        name = "IPv6"
    }
}

# Volume
resource "openstack_blockstorage_volume_v2" "volume" {
    name = "my-volume"
    size = "10"
}

# Attach volume
resource "openstack_compute_volume_attach_v2" "volumes" {
    instance_id = "${openstack_compute_instance_v2.instance.0.id}"
    volume_id   = "${openstack_blockstorage_volume_v2.volume.id}"
}