Terraform and UH-IaaS: Part III - Dynamics

Last changed: 2019-08-05

This document builds on Terraform Part 1 and Part 2, and extends the code base to make it more dynamic. We also make use of more advanced functionality in Terraform such as output handling and local variables.

The goal with this document is to show how Terraform can be used to set up a real environment on UH-IaaS. We will create:

  • An SSH key pair
  • Four web servers running CentOS
  • One database server running Ubuntu
  • A volume that is attached to the database server
  • Security groups that allow access to the different servers, as well as allowing the web servers to access the database server

The files used in this document can be downloaded:

Variables file

In order to keep the management of the Terraform infrastructure easy and intuitive, it is a good idea to consolidate definitions and variables into a single file or a small set of files. Terraform supports a concept of default values for variables, which can be overridden. In our example, we are opting for a single file that contains all variables, with default values, used throughout the code:

variables.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
# Variables
variable "region" { }
variable "name" { default = "myproject" }
variable "ssh_public_key" { default = "~/.ssh/id_rsa.pub" }
variable "network" { default = "IPv6" }
variable "volume_size" { default = 20 }

# Security group defaults
variable "allow_ssh_from_v6"   { type = "list" default = [] }
variable "allow_ssh_from_v4"   { type = "list" default = [] }
variable "allow_http_from_v6"  { type = "list" default = [] }
variable "allow_http_from_v4"  { type = "list" default = [] }
variable "allow_mysql_from_v6" { type = "list" default = [] }
variable "allow_mysql_from_v4" { type = "list" default = [] }

# Mapping between role and image
variable "role_image" {
  type = "map"
  default = {
    "web" = "4756b700-9489-4d59-bfd6-24d3b8b4167b"  # GOLD CentOS 7
    "db"  = "974d7df1-d845-4bf0-a3c0-d95d85267d43"  # GOLD Ubuntu 18.04 LTS
  }
}

# Mapping between role and flavor
variable "role_flavor" {
  type = "map"
  default = {
    "web" = "m1.small"
    "db"  = "m1.medium"
  }
}

# Mapping between role and number of instances (count)
variable "role_count" {
  type = "map"
  default = {
    "web" = 4
    "db"  = 1
  }
}

Notice that the region variable (highlighted) is empty and doesn’t have a default value. For this reason, the region must always be specified in some way when running Terraform:

$ ~/terraform plan
var.region
  Enter a value:

As shown above, when a default value isn’t specified in the code Terraform will ask for it interactively.

Also note that the allow_ssh_from_v6, allow_ssh_from_v4 etc. (highlighted) variables are empty lists. It is expected that we specify these in the local.tfvars file, explained in the next section.

Local variables file

Terraform supports specification of local variables that completes or overrides the variable set given in variables.tf. We can do this on command line:

terraform -var 'region=bgo'

This does not scale, however. Terraform has an option -var-file that takes one argument, a variables file:

terraform -var-file <file>

An example file local.tfvars that complements our variables.tf could look like this:

local.tfvars
 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
# Region
region = "osl"

# This is needed to access the instance over ssh
allow_ssh_from_v6 = [
  "2001:700:100:202::202:12/128",
  "2001:700:100:118::67/128"
]
allow_ssh_from_v4 = [
  "129.240.202.12/32",
  "129.240.118.67/32"
]

# This is needed to access the instance over http
allow_http_from_v6 = [
  "2001:700:100::/40"
]
allow_http_from_v4 = [
  "129.240.0.0/16"
]

# This is needed to access the instance over the mysql port
allow_mysql_from_v6 = [
  "2001:700:100:202::202:12/128"
]
allow_mysql_from_v4 = [
  "129.240.202.12/32"
]

Here, we specify the region and the addresses to be used for the security group. We would use this file like this:

$ terraform plan -var-file local.tfvars
$ terraform apply -var-file local.tfvars

Using variables

Terraform supports a variety of different variable types, and should be familiar to anyone who has used modern programming languages. We’re using string, list (array) and map (hash) variables. In this example, we have divided our original one-file setup into 3 files, in addition to the local variables file:

main.tf Our main file.
secgroup.tf Since the security group definitions are rather verbose, we have separated these from the main file.
variables.tf Variable definitions with default values.
local.tfvars Local variables.

We’ll take a look at main.tf. The first part, containing the SSH key pair resource, is as before but using variables:

main.tf
1
2
3
4
5
6
7
8
provider "openstack" {}

# SSH key
resource "openstack_compute_keypair_v2" "keypair" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}"
    public_key = "${file(var.ssh_public_key)}"
}

Next, we’ll look at our security groups in secgroup.tf. We now have three of them:

secgroup.tf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Security group SSH + ICMP
resource "openstack_networking_secgroup_v2" "instance_ssh_access" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}-ssh"
    description = "Security group for allowing SSH and ICMP access"
}

# Security group HTTP
resource "openstack_networking_secgroup_v2" "instance_web_access" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}-web"
    description = "Security group for allowing HTTP access"
}

# Security group MySQL
resource "openstack_networking_secgroup_v2" "instance_db_access" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}-db"
    description = "Security group for allowing MySQL access"
}

Since these are web and database serves, we create a security group for allowing HTTP for the web servers and port 3306 for the database server, in addition to allowing SSH and ICMP. The security group rules for SSH and ICMP are pretty much the same as before, but using variables:

secgroup.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
# Allow ssh from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "${var.allow_ssh_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

# Allow ssh from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "${var.allow_ssh_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

# Allow icmp from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "icmp"
    remote_ip_prefix = "${var.allow_ssh_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

# Allow icmp from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol = "icmp"
    remote_ip_prefix = "${var.allow_ssh_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

Notice that we now use implicit iteration over the number of entries listed in the “allow_from” variables, which are empty lists in variables.tf but are properly defined in local.tfvars.

Let’s take a look at the security group rules defined for HTTP and MySQL access:

secgroup.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
# Allow HTTP from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_http_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_http_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 80
    port_range_max = 80
    remote_ip_prefix = "${var.allow_http_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
}

# Allow HTTP from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_http_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_http_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 80
    port_range_max = 80
    remote_ip_prefix = "${var.allow_http_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
}

# Allow MySQL from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_mysql_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_ip_prefix = "${var.allow_mysql_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}

# Allow MYSQL from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_mysql_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_ip_prefix = "${var.allow_mysql_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}

The resource definition for the HTTP access, as well as the first two resource definitions for MySQL access, follows the same logic as that of the SSH and ICMP rules. The last two MySQL rules are different:

secgroup.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
# Allow MYSQL from web servers (IPv4)
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_from_web_access_ipv4" {
    region = "${var.region}"
    count = 1
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}

# Allow MYSQL from web servers (IPv6)
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_from_web_access_ipv6" {
    region = "${var.region}"
    count = 1
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}

Here, we use rather advanced functionality for security groups in Openstack. We can allow IP addresses from other security groups (source groups) access by specifying remote_group_id rather than remote_ip_prefix. It is possible to achieve the same using remote_ip_prefix, however it is less elegant [1].

We’ll circle back to main.tf:

main.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
# Web servers
resource "openstack_compute_instance_v2" "web_instance" {
    region = "${var.region}"
    count = "${lookup(var.role_count, "web", 0)}"
    name = "${var.region}-web-${count.index}"
    image_id = "${lookup(var.role_image, "web", "unknown")}"
    flavor_name = "${lookup(var.role_flavor, "web", "unknown")}"

    key_pair = "${terraform.workspace}-${var.name}"
    security_groups = [
        "default",
        "${terraform.workspace}-${var.name}-ssh",
        "${terraform.workspace}-${var.name}-web"
    ]

    network {
        name = "IPv6"
    }

    depends_on = [
        "openstack_networking_secgroup_v2.instance_ssh_access",
        "openstack_networking_secgroup_v2.instance_web_access"
    ]
}

# Database servers
resource "openstack_compute_instance_v2" "db_instance" {
    region = "${var.region}"
    count = "${lookup(var.role_count, "db", 0)}"
    name = "${var.region}-db-${count.index}"
    image_id = "${lookup(var.role_image, "db", "unknown")}"
    flavor_name = "${lookup(var.role_flavor, "db", "unknown")}"

    key_pair = "${terraform.workspace}-${var.name}"
    security_groups = [
        "default",
        "${terraform.workspace}-${var.name}-ssh",
        "${terraform.workspace}-${var.name}-db"
    ]

    network {
        name = "IPv6"
    }

    depends_on = [
        "openstack_networking_secgroup_v2.instance_ssh_access",
        "openstack_networking_secgroup_v2.instance_db_access"
    ]
}

We now define two different instance resources. One for web servers and one for the database server. They use different values defined in variables.tf for image, flavor etc. Lastly, we define a volume resource and attach this volume to the database server:

main.tf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Volume
resource "openstack_blockstorage_volume_v2" "volume" {
    name = "database"
    size = "${var.volume_size}"
}

# Attach volume
resource "openstack_compute_volume_attach_v2" "attach_vol" {
    instance_id = "${openstack_compute_instance_v2.db_instance.id}"
    volume_id   = "${openstack_blockstorage_volume_v2.volume.id}"
}

Making changes

Terraform maintains the state of the infrastructure it manages in the workspace directory. It is possible to make simple changes just by updating and applying the code. If we wanted to scale down the number of web servers from 4 to 2, we would change this line in variables.tf:

variables.tf
1
2
3
4
5
6
7
8
# Mapping between role and number of instances (count)
variable "role_count" {
  type = "map"
  default = {
    "web" = 4
    "db"  = 1
  }
}

After changing the count from 4 to 2 here (the highlighted line), we can run terraform plan:

$ terraform plan -var-file local.tfvars
...
Terraform will perform the following actions:

  - openstack_compute_instance_v2.web_instance[2]

  - openstack_compute_instance_v2.web_instance[3]


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

Applying this with terraform apply will then destroy two of the web servers. Similarly, if we were to increase the web server count from 4 to 5, Terraform would add a new web server.

Complete example

A complete listing of the example files used in this document is provided below.

main.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
provider "openstack" {}

# SSH key
resource "openstack_compute_keypair_v2" "keypair" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}"
    public_key = "${file(var.ssh_public_key)}"
}

# Web servers
resource "openstack_compute_instance_v2" "web_instance" {
    region = "${var.region}"
    count = "${lookup(var.role_count, "web", 0)}"
    name = "${var.region}-web-${count.index}"
    image_id = "${lookup(var.role_image, "web", "unknown")}"
    flavor_name = "${lookup(var.role_flavor, "web", "unknown")}"

    key_pair = "${terraform.workspace}-${var.name}"
    security_groups = [
        "default",
        "${terraform.workspace}-${var.name}-ssh",
        "${terraform.workspace}-${var.name}-web"
    ]

    network {
        name = "IPv6"
    }

    depends_on = [
        "openstack_networking_secgroup_v2.instance_ssh_access",
        "openstack_networking_secgroup_v2.instance_web_access"
    ]
}

# Database servers
resource "openstack_compute_instance_v2" "db_instance" {
    region = "${var.region}"
    count = "${lookup(var.role_count, "db", 0)}"
    name = "${var.region}-db-${count.index}"
    image_id = "${lookup(var.role_image, "db", "unknown")}"
    flavor_name = "${lookup(var.role_flavor, "db", "unknown")}"

    key_pair = "${terraform.workspace}-${var.name}"
    security_groups = [
        "default",
        "${terraform.workspace}-${var.name}-ssh",
        "${terraform.workspace}-${var.name}-db"
    ]

    network {
        name = "IPv6"
    }

    depends_on = [
        "openstack_networking_secgroup_v2.instance_ssh_access",
        "openstack_networking_secgroup_v2.instance_db_access"
    ]
}

# Volume
resource "openstack_blockstorage_volume_v2" "volume" {
    name = "database"
    size = "${var.volume_size}"
}

# Attach volume
resource "openstack_compute_volume_attach_v2" "attach_vol" {
    instance_id = "${openstack_compute_instance_v2.db_instance.id}"
    volume_id   = "${openstack_blockstorage_volume_v2.volume.id}"
}
secgroup.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
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# Security group SSH + ICMP
resource "openstack_networking_secgroup_v2" "instance_ssh_access" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}-ssh"
    description = "Security group for allowing SSH and ICMP access"
}

# Security group HTTP
resource "openstack_networking_secgroup_v2" "instance_web_access" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}-web"
    description = "Security group for allowing HTTP access"
}

# Security group MySQL
resource "openstack_networking_secgroup_v2" "instance_db_access" {
    region = "${var.region}"
    name = "${terraform.workspace}-${var.name}-db"
    description = "Security group for allowing MySQL access"
}

# Allow ssh from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "${var.allow_ssh_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

# Allow ssh from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 22
    port_range_max = 22
    remote_ip_prefix = "${var.allow_ssh_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

# Allow icmp from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "icmp"
    remote_ip_prefix = "${var.allow_ssh_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

# Allow icmp from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_ssh_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol = "icmp"
    remote_ip_prefix = "${var.allow_ssh_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_ssh_access.id}"
}

# Allow HTTP from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_http_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_http_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 80
    port_range_max = 80
    remote_ip_prefix = "${var.allow_http_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
}

# Allow HTTP from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_http_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_http_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 80
    port_range_max = 80
    remote_ip_prefix = "${var.allow_http_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
}

# Allow MySQL from IPv4 net
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_access_ipv4" {
    region = "${var.region}"
    count = "${length(var.allow_mysql_from_v4)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_ip_prefix = "${var.allow_mysql_from_v4[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}

# Allow MYSQL from IPv6 net
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_access_ipv6" {
    region = "${var.region}"
    count = "${length(var.allow_mysql_from_v6)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_ip_prefix = "${var.allow_mysql_from_v6[count.index]}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}

# Allow MYSQL from web servers (IPv4)
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_from_web_access_ipv4" {
    region = "${var.region}"
    count = 1
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}

# Allow MYSQL from web servers (IPv6)
resource "openstack_networking_secgroup_rule_v2" "rule_mysql_from_web_access_ipv6" {
    region = "${var.region}"
    count = 1
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_group_id = "${openstack_networking_secgroup_v2.instance_web_access.id}"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
}
variables.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
# Variables
variable "region" { }
variable "name" { default = "myproject" }
variable "ssh_public_key" { default = "~/.ssh/id_rsa.pub" }
variable "network" { default = "IPv6" }
variable "volume_size" { default = 20 }

# Security group defaults
variable "allow_ssh_from_v6"   { type = "list" default = [] }
variable "allow_ssh_from_v4"   { type = "list" default = [] }
variable "allow_http_from_v6"  { type = "list" default = [] }
variable "allow_http_from_v4"  { type = "list" default = [] }
variable "allow_mysql_from_v6" { type = "list" default = [] }
variable "allow_mysql_from_v4" { type = "list" default = [] }

# Mapping between role and image
variable "role_image" {
  type = "map"
  default = {
    "web" = "4756b700-9489-4d59-bfd6-24d3b8b4167b"  # GOLD CentOS 7
    "db"  = "974d7df1-d845-4bf0-a3c0-d95d85267d43"  # GOLD Ubuntu 18.04 LTS
  }
}

# Mapping between role and flavor
variable "role_flavor" {
  type = "map"
  default = {
    "web" = "m1.small"
    "db"  = "m1.medium"
  }
}

# Mapping between role and number of instances (count)
variable "role_count" {
  type = "map"
  default = {
    "web" = 4
    "db"  = 1
  }
}
local.tfvars
 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
# Region
region = "osl"

# This is needed to access the instance over ssh
allow_ssh_from_v6 = [
  "2001:700:100:202::202:12/128",
  "2001:700:100:118::67/128"
]
allow_ssh_from_v4 = [
  "129.240.202.12/32",
  "129.240.118.67/32"
]

# This is needed to access the instance over http
allow_http_from_v6 = [
  "2001:700:100::/40"
]
allow_http_from_v4 = [
  "129.240.0.0/16"
]

# This is needed to access the instance over the mysql port
allow_mysql_from_v6 = [
  "2001:700:100:202::202:12/128"
]
allow_mysql_from_v4 = [
  "129.240.202.12/32"
]

Footnotes

[1]

An alternative way to dynamically add access for the web servers, by using remote_ip_prefix, would be to make use of the computed value for the instance IPv4 and IPv6 addresses given to the web servers when provisioned:

secgroup alternative
 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
# Allow MYSQL from web servers (IPv4)
resource "openstack_networking_secgroup_rule_v2" "rule2_mysql_access_ipv4" {
    region = "${var.region}"
    count = "${lookup(var.role_count, "web", 0)}"
    direction = "ingress"
    ethertype = "IPv4"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_ip_prefix = "${element(openstack_compute_instance_v2.web_instance.*.access_ip_v4, count.index)}/32"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
    depends_on = [ "openstack_compute_instance_v2.web_instance" ]
}

# Allow MYSQL from web servers (IPv6)
resource "openstack_networking_secgroup_rule_v2" "rule2_mysql_access_ipv6" {
    region = "${var.region}"
    count = "${lookup(var.role_count, "web", 0)}"
    direction = "ingress"
    ethertype = "IPv6"
    protocol  = "tcp"
    port_range_min = 3306
    port_range_max = 3306
    remote_ip_prefix = "${replace(element(openstack_compute_instance_v2.web_instance.*.access_ip_v6, count.index), "/[\\[\\]]/", "")}/128"
    security_group_id = "${openstack_networking_secgroup_v2.instance_db_access.id}"
    depends_on = [ "openstack_compute_instance_v2.web_instance" ]
}