Infrastructure as Code with Terraform

Setting up new infrastructure can be a tedious process. It doesn’t matter whether it is on-premise or in the cloud. Many organisations have a long process of filling forms, obtaining budget clearance, asking for priority and verifying everything is set up correctly. The cloud promises us to make things better, and at least it got a lot faster. But still, we can make many mistakes in this manual process, and if we want to duplicate a deliberately crafted setup things become even harder….

One approach to solve this is using “Infrastructure as Code”. Instead of manually clicking in the web portal of your cloud provider, you write code that lets you create or recreate a set of infrastructural resources.

Of course, this is easier to verify than verifying ones manual work. But it also makes it easier to make many copies of the same infrastructure. Each team within your company could have its own testing environment, yet every environment could be equal to the others.

Imperative vs. Declarative

The simplest approach is scripting, where we use a command-line interface (CLI) provided by our cloud provider to create the desired state of the infrastructure. This is “imperative”: we tell the provider what to do in a simple, step-by-step approach.

The alternative is a declarative approach. In this approach, we describe what we want to have, and we let some tools figure out how exactly to achieve that.

In this post, I’m showing you this latter approach. I’ll be using Oracle Cloud in these demo’s, but the concept applies to each major cloud provider. Terraform has extensive documentation for each “provider”. That documentation includes instructions on how to set up the initial connection, and what kind of resources you can configure through Terraform.

Installing Terraform

Before we can do anything, we must install Terraform. Head to terraform.io, and click “Download CLI”. Alternatively, you can use your platforms package manager and see if it has a Terraform package. For macOS, brew install terraform should get you started in minutes.

Generating a key pair

To interact with the Oracle Cloud, you need RSA key pair in PEM format (minimum 2048 bits). We can generate a key pair using these commands:

# First, generate the private key.
openssl genrsa -out oracle-cloud.pem -aes256 2048
# Next, generate the corresponding public key.
openssl rsa -pubout -in oracle-cloud.pem -out oracle-cloud-public.pem
# Calculate a key fingerprint.
openssl rsa -pubout -outform DER -in oracle-cloud.pem | openssl md5 -c

The first step asks you to enter a password. I advise you to generate a password just for this purpose and store it secretly, for example in a password manager.

The oracle-cloud-public.pem file is the public key. As the name suggests, it is not a secret - you can share it with anyone and we’ll soon share it with Oracle.

The oracle-cloud.pem is the private key. This file is a secret. Consider it as secret as a password: don’t share it with anyone, even when you’re asked to. Together with the password that you entered with the first instruction, it’s the key to your identity as far as Oracle Cloud is concerned.

Registering the key pair

Next, we need to tell the Oracle cloud about this key pair. First, log in to the Cloud Console, then click the user icon in the top-right corner.

User icon
User icon

From that menu, click “User Settings” and scroll down to “API Keys”. Click “Add Public Key” and paste the contents of the public key in the box. Those contents should look like

-----BEGIN PUBLIC KEY-----
...
...
...
-----END PUBLIC KEY-----

After clicking OK, the Cloud Console should show you a Fingerprint for the key, which should match with the fingerprint that we’ve calculated earlier.

While we’re at it, we need a few other values. Those values are Oracle Cloud IDs (or OCIDs), which is an Oracle-assigned unique ID. On the same page, under User Information, you’ll find the OCID for your user.

Navigate to the “Tenancy Details” by clicking the top-left hamburger; in the bottom of that menu you’ll find “Administration”, and that has “Tenancy Details”.

Hamburger menu
Hamburger menu

Under “Tenancy Information”, you’ll find the OCID for your tenancy.

Finally, we need our “Compartment ID”. We can find that by navigating to the “Compartments” by again clicking the top-left hamburger; in the bottom of that menu you’ll find “Identity”, and there you’ll find Compartments.

If you’re working in a larger Oracle Cloud account, it may have been set up with Compartments. Think of them as “account with an account”. You may have access to the whole account, which is the case if you’re starting with the “Free Tier”. In that case, you’re looking for the “root” compartment - its name is equal to your Tenancy name. In corporate environments, it’s more likely that you only have access to a subset of it. In that case, you select the name of the desired Compartment. On the Compartment detail page, you’ll find your “Compartment ID”.

To keep things tidy, create a new Compartment where we’ll create all the resources needed for this demo. Note down the Compartment OCID, we’ll need it later.

Preparing variables

Now we can create a file named terraform.tfvars which the following contents:

compartment_ocid     = "ocid1.tenancy...." # This is usually the same as the Tenancy ID.
user_ocid            = "ocid1.user...."
tenancy_ocid         = "ocid1.tenancy...."
fingerprint          = "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx"
private_key_path     = "/full/path/to/generated/oracle.pem"
private_key_password = "that's a secret!"
region               = "...." # I'm using eu-frankfurt-1

A note on version control

Since this file contains a password, we should never check it in to version control. You might want to share some of the values with colleagues or team members. In that case, you could create a terraform.tfvars.example file that lists all the above values except for the key and the password.

This declares a handful of variables, but we’re not using them… yet!

Declaring infrastructure

Now that we can have Terraform talk to Oracle Cloud, we can start declaring the infrastructure that we would like to have. In this example, I’ll boot up a virtual machine.

The full code for that is on GitHub, so I’ll highlight the most important parts here. If you want to follow along, clone that repository to have an easy start.

First, we need to tell Terraform we’re going to use the variables we’ve just created. Open variables.tf, where you’ll find

variable "user_ocid" {}
variable "tenancy_ocid" {}
variable "fingerprint" {}
variable "private_key_path" {}
variable "private_key_password" {}
variable "region" {}

This snippet says we need those variables, but it doesn’t assign values to them - that what we have the terraform.tfvars file for. Take the terraform.tfvars file you’ve created before, and copy it to this directory. Edit it and add two lines

demo_compartment_ocid = "ocid1.compartment...." # the OCID of your freshly created compartment
ssh_authorized_key    = "ssh-rsa...." # the public SSH key you want for login

Now open virtual.tf which declares the virtual machine we’re going to create. There are a few interesting parts.

shape               = "VM.Standard.E2.1.Micro"

This part describes the “shape” of the virtual machine. Think of it as the amount of assigned hardware. The full list is in the Oracle Cloud Infrastructure Documentation. Within the Free Tier, you can have two VM.Standard.E2.1.Micro virtual machines with either Ubuntu, Oracle Linux or CentOS.

source_details {
  source_id   = "ocid1.image.oc1.eu-frankfurt-1...."
  source_type = "image"
}

This part describes the base image for the virtual machine. In the Oracle Cloud Infrastructure Documentation, you’ll find a list of supported VM images. For this demo, I’m picking the latest Ubuntu 20.04 release, which is 20.04-2020.05.18-0 at the time of writing. In the list with “Image OCIDs” you can find the OCID of your preferred image for your region.

metadata = {
  ssh_authorized_keys = var.ssh_authorized_key
}

In this section, we’re configuring who is authorised to log on to the virtual machine. The username depends on the base image that we chose, so it’ll be ubuntu in this case.

It may be interesting to open networking.tf, too. You’ll find a bunch of networking related stuff. The most important parts are the oci_core_security_list: a list of firewall rules that describes what traffic can come in and go out of your machine.

ingress_security_rules {
  protocol = "6" # TCP
  source   = "0.0.0.0/0"
  tcp_options {
    min = 22
    max = 22
  }
}

This rule says that any inbound traffic is allowed, from any location, as long as it’s TCP traffic that targets port 22, the default for SSH.

egress_security_rules {
  protocol    = "all"
  destination = "0.0.0.0/0"
}

This rule says that any outbound traffic is allowed, regardless of its destination and protocol.

Ready, Set, Go!

Before executing the plan, let’s make sure everything is as we expect. Run terraform plan and inspect the generated plan. If all looks okay, run terraform apply, confirm with yes, and patiently wait. It may take a minute or two to create all the desired resources. If all went well, the output will be something like

Terraform output
Terraform output

The most important part here is, of course, the public IP address that our virtual machine got assigned. Now log on to this fresh virtual machine by issuing ssh ubuntu@130.xxx.yyy.zzz and start looking around.

Next Steps

If you want to use your new virtual for something else than showing off, you’ll probably want to allow more incoming connections. Apart from the operating system firewall, it is important to also adopt the oci_core_security_list. For instance, to accept incoming HTTPS traffic (you aren’t using HTTP without TLS, are you?!), add this to networking.tf:

ingress_security_rules {
  protocol = "6" # TCP
  source   = "0.0.0.0/0"
  tcp_options {
    min = 443
    max = 443
  }
}

Other useful things may be to setup Logcheck or Apticron.

For now, enjoy your brand new free virtual machine. Let me know in the comments what are you going to do with it!