Using Terraform: Part 2

Introduction

Part 1 of this series gave a high-level overview of what Terraform is. Today'€™s post will demonstrate how to use Terraform to build out actual cloud resources using Cybera's Rapid Access Cloud.

Preparation

If you'd like to follow along, you'll need two things

  1. Terraform

  2. Access to a Rapid Access Cloud account (or any other OpenStack cloud)

Obtaining Terraform is easy enough. Just head over to its homepage and follow the Getting Started Guide.

Note: I should mention that Terraform is a command-line utility, so you should be proficient with working in a terminal to effectively use it.

For the cloud part, any OpenStack cloud will do. You should be able to follow along without having to make any modifications. This is especially true if you are using our Rapid Access Cloud. If you do have to make any modifications, they should be very minor. I'd be happy to assist anyone in the comments section.

The only resource you'll need from the OpenStack cloud is a copy of the openrc.sh file. This can be downloaded from the Access & Security menu, under the API Access tab, and clicking on the "Download OpenStack RC File", as seen in the following screenshot:

To keep everything organized, create a folder called "terraform-demo" and copy the openrc.sh file to that folder.

Creating Some Basic Resources

Security Groups

As shown on the Terraform documentation page, there are several different OpenStack resources available to create. Let's start by creating a security group.

Create a file called "demo.tf" with the contents: 

resource "openstack_compute_secgroup_v2" "my_secgroup" {
  name = "my_secgroup"
  description = "my security group"
  rule {
    from_port = 22
    to_port = 22
    ip_protocol = "tcp"
    cidr = "0.0.0.0/0"
  }
  rule {
    from_port = 80
    to_port = 80
    ip_protocol = "tcp"
    cidr = "0.0.0.0/0"
  }
}

This is going to create a security group called "my_group" with two open ports: 22 and 80.

After the contents have been saved, run the following on the command-line: 

$ source openrc.sh
Please enter your OpenStack Password:
$ terraform plan
+ openstack_compute_secgroup_v2.my_secgroup
    description:        "" => "my security group"
    name:               "" => "my_secgroup"
    region:             "" => "Calgary"
    rule.#:             "" => "2"
    rule.0.cidr:        "" => "0.0.0.0/0"
    rule.0.from_port:   "" => "22"
    rule.0.id:          "" => "<computed>"
    rule.0.ip_protocol: "" => "tcp"
    rule.0.self:        "" => "0"
    rule.0.to_port:     "" => "22"
    rule.1.cidr:        "" => "0.0.0.0/0"
    rule.1.from_port:   "" => "80"
    rule.1.id:          "" => "<computed>"
    rule.1.ip_protocol: "" => "tcp"
    rule.1.self:        "" => "0"
    rule.1.to_port:     "" => "80"

"terraform plan" is a command that reports outstanding tasks and changes that need to be made. Since nothing has been executed yet, all tasks are outstanding. The output therefore shows that a new security group needs to be created.

To create it, run:

$ terraform apply

If it was successful, you should see:

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

You can confirm this by running:

$ terraform show
openstack_compute_secgroup_v2.my_secgroup:
  id = 1985
  description = my security group
  name = my_secgroup
  region = Calgary
  rule.# = 2
  rule.0.cidr = 0.0.0.0/0
  rule.0.from_group_id =
  rule.0.from_port = 22
  rule.0.id =
  rule.0.ip_protocol = tcp
  rule.0.self = false
  rule.0.to_port = 22
  rule.1.cidr = 0.0.0.0/0
  rule.1.from_group_id =
  rule.1.from_port = 80
  rule.1.id =
  rule.1.ip_protocol = tcp
  rule.1.self = false
  rule.1.to_port = 80

What's interesting here is that some values exist which were not included in the "demo.tf" file — notably the "id" and the "region". Terraform obtained this information from OpenStack, and it's a very powerful feature. We'll use it more in the future.

Also, if you have the OpenStack command-line tools installed, you can confirm that a security group was, in fact, created:

$ nova secgroup-list
+------+-------------+------------------------------------+
| Id   | Name        | Description                        |
+------+-------------+------------------------------------+
| 1985 | my_secgroup | my security group                  |
+------+-------------+------------------------------------+

As well, you can see it in the dashboard:

And finally, running "terraform plan" again shows that nothing needs to be done:

$ terraform plan
Refreshing Terraform state prior to plan...
openstack_compute_secgroup_v2.my_secgroup: Refreshing state... (ID: 1977)
No changes. Infrastructure is up-to-date. This means that Terraform could not detect any differences between your configuration and the real physical resources that exist. As a result, Terraform doesn't need to do anything.

So that's the basic workflow of using Terraform. Let's move on and create more resources.

Floating IPs

A Floating IP is a public IP address. By default, instances (virtual machines) in OpenStack only have private IP addresses. A Floating IP will allow us to access the instance via a public IP.

Add the following to the "demo.tf" file (making sure to keep the existing content):

resource "openstack_compute_floatingip_v2" "test" {
  pool = "nova"
}

Running "terraform plan" will show that a Floating IP needs to be created, so run "terraform apply" to create it:

$ terraform apply
When complete, run:
$ terraform show
openstack_compute_floatingip_v2.test:
  id = 368
  address = 199.116.235.123
  fixed_ip =
  instance_id =
  pool = nova
  region = Calgary

Note the "address" field. This is the Floating IP that OpenStack allocated to us. Just like with the "id" and "region" in the security group, the "address" is not something we specified in the "demo.tf" file.

Building An Instance

Next, let's build an instance (or virtual machine). Add the following to the same "demo.tf" file:

resource "openstack_compute_instance_v2" "my_instance" {
  name = "my_instance"
  image_name = "Ubuntu 14.04"
  flavor_name = "m1.small"
  security_groups = ["${openstack_compute_secgroup_v2.my_secgroup.name}"]
  floating_ip = "${openstack_compute_floatingip_v2.test.address}"
}

Note the "security_groups" and "floating_ip" settings. Their values are "variable", meaning they are not hard-coded or fixed. When we next apply Terraform, they will be replaced with "my_secgroup" for the "security_groups" and "199.116.235.123" for the "floating_ip". We could have hard-coded these values, but making them variable does two things:

  1. It creates an implicit relationship between the instance, the security group, and the floating ip. Terraform now knows that the instance cannot exist before the security group or floating IP are created.

  2. It allows us to use data that Terraform obtains from OpenStack as configuration data. My floating IP was 199.116.235.123, but your Floating IP will be something totally different. Similarly, if I do this exercise again tomorrow, I could end up with a totally different Floating IP. By making these non-permanent settings variable, the same "tf" file can be used over and over without having to update it. This is the foundation of cloud application orchestration.

Run "terraform apply" and the instance will be created. If you now run a "terraform show", a lot of new information will be revealed: 

openstack_compute_instance_v2.my_instance:
  id = 2dd6e0bc-5dc6-4d07-a72a-4cb66edba22a
  access_ip_v4 = 199.116.235.123
  access_ip_v6 = [2605:fd00:4:1000:f816:3eff:fe08:56cc]
  flavor_id = 2
  flavor_name = m1.small
  floating_ip = 199.116.235.123
  image_id = 5f645a5c-9aae-41ba-897a-dbe47ca7de6d
  image_name = Ubuntu 14.04
  name = my_instance
  network.# = 1
  network.0.fixed_ip_v4 = 10.1.1.149
  network.0.fixed_ip_v6 = [2605:fd00:4:1000:f816:3eff:fe08:56cc]
  network.0.mac = fa:16:3e:08:56:cc
  network.0.name = cybera
  network.0.port =
  network.0.uuid =
  region = Calgary
  security_groups.# = 1
  security_groups.0 = my_secgroup

All of this information can now be used with other resources.

Building a Simple Web Server

For a final demonstration, we'll have the instance install the Apache web server. Begin by tearing everything down — we'll start with a clean slate:

$ terraform destroy

Next, create a new file called "bootstrap.sh" with the following contents:

#!/bin/bash
apt-get update
apt-get install -y apache2

Now edit "demo.tf" and change the instance declaration to look like the following:

resource "openstack_compute_instance_v2" "my_instance" {
  name = "my_instance"
  image_name = "Ubuntu 14.04"
  flavor_name = "m1.small"
  security_groups = ["${openstack_compute_secgroup_v2.my_secgroup.name}"]
  floating_ip = "${openstack_compute_floatingip_v2.test.address}"
  user_data = "${file("bootstrap.sh")}"
}

The "user_data" section will read the contents of "bootstrap.sh" and execute them on the newly created instance.

Let's see it in action:

$ terraform apply

After a few seconds the command will finish and it'll look like nothing happened. But if you have the OpenStack tools installed, you can view the console log and see that it is, in fact, installing Apache:

$ nova console-log my_instance
'€¦
Reading state information...
The following extra packages will be installed:
  apache2-bin apache2-data libapr1 libaprutil1 libaprutil1-dbd-sqlite3
  libaprutil1-ldap ssl-cert
'€¦

After two or three minutes, everything should be finished. You should now be able to type the Floating IP into a web browser and get the default Apache web page:

Congratulations! You've just automated the deployment of a simple web server by simply declaring (not programming) some cloud resources in a file. While this was just a small and simple example, the possibilities for this are quite amazing: Imagine building out an entire web application with a load balancer, multiple application servers, and multiple database servers simply by declaring everything in a file. That file can then be shared with others to deploy the same type of application stack.

Conclusion

Part two of this series covered how to begin using Terraform, with hands-on examples. We started by building some simple cloud resources, such as a security group, and moved on to deploying a fully functional web server.

Future blog posts will cover how to create more complex, multi-server environments with Terraform. Until then, and as a little bonus, here's a Terraform file with an accompanying shell script that will deploy a fully functional Minecraft server.