Automating cluster creation with Terraform on DigitalOcean

In our previous introduction to devops tools, we introduced terraform but did not go into a lot of details. So let’s dig a little deeper and setup a simple three node cluster with terraform. Terraform, as with many of Hashicorp’s software, is a single binary. You can download terraform for your platform and add it to your path. Or if you are on a mac, you can brew install terraform.

We are going to use DigitalOcean as the cloud platform. They have been adding a lot of features to their platform like Private Networking, Load balancers, S3 compatible object store, CDN etc. You would need an API key that DigitalOcean calls a Personal Access Token to interact with the DO API.

Lets create an empty folder and put in the following in a file called main.tf. The file can be called anything but main.tf seems to be the convention. Terraform process all the files in the current directory to construct the object graph.

# main.tf
provider "digitalocean" {}

The DigitalOcean provider requires that you set the token as a part of the provider configuration or expects an environment variable called DIGITALOCEAN_TOKEN to be present. I prefer setting this sort of configuration as an environment variable. Now you can run terraform init on the command line. It will detect that you need the DigitalOcean Provider plugin and download it into the .terraform folder.

Now before we create an droplet, we need to import or upload our SSH key. I have created a new SSH keypair using ssh-keygen. I have created an SSH key with the name dotf and its corresponding public key dotf.pub.

variable "ssh_pub_key" {}
resource "digitalocean_ssh_key" "tfsshkey" {
name = "tfkey"
public_key = "${file("${var.ssh_pub_key}")}"
}

I have setup the ssh_pub_key variable to point to the path to my .pub file by setting the environment variable TF_VAR_ssh_pub_key. Notice that I have interpolated the ssh_pub_key variable and used the file() function to read its contents for the tfsshkey resource. It does seem a little weird to nest double quotes though.

Now that we have imported the SSH key, let’s create the droplets. In this example, we’ll be using the blr1 region (Bangalore) and a Ubuntu 16.04 image. I am also referring to the SSH key we created earlier so that we’ll be able to log into the machine.

resource "digitalocean_droplet" "controller" {
image = "ubuntu-16-04-x64"
name = "controller"
region = "blr1"
ssh_keys = ["${digitalocean_ssh_key.tfsshkey.id}"]
size = "4gb"
private_networking = "true"
}

This creates a machine with name controller and sets up private networking. Unfortunately, DigitalOcean does not allow you to create a VPC like Amazon and hence we cannot specify the private ip address ahead of time.

Let’s also create a resource for our nodes. We need two of them. So, we can use the count argument to specify the number of droplets we need.

resource "digitalocean_droplet" "nodes" {
count = 2
image = "ubuntu-16-04-x64"
name = "node-0${count.index+1}"
region = "blr1"
ssh_keys = ["${digitalocean_ssh_key.tfsshkey.id}"]
size = "4gb"
private_networking = "true"
}

Notice that we have interpolated the name argument with the index. So this will create two droplets with names node-01and node-02.

We are almost done. We now need to instruct terraform to store the state in DigitalOcean Spaces. DigitalOcean Spaces is a S3 compatible service to store objects. You can create your Spaces API keys from the same page that you created your Personal Access Token. We can simply use the S3 backend to store our state into DigitalOcean secrets. You need to store the key and the secret it generates to AWS_ACCESS_KEY_ID and the AWS_SECRET_ACCESS_KEY environment variables. Our backend configuration looks a bit like this.

terraform {
backend "s3" {
bucket = "<bucketname>"
region = "us-east-1" # required but totally ignored
endpoint = "https://sgp1.digitaloceanspaces.com"
key = "state-store/tf.tfstate"

# Hey DO Spaces is only S3 compatible not exactly S3
skip_credentials_validation = true
skip_get_ec2_platforms = true
skip_requesting_account_id = true
skip_metadata_api_check = true
}
}

Now to access the IP addresses of our terraform script temporarily, we’ll configure them as outputs.

output "controller_ip_address" {
value = "${digitalocean_droplet.controller.ipv4_address}"
}
output "controller_private_ip_address" {
value = "${digitalocean_droplet.controller.ipv4_address_private}"
}
output "node_ip_address" {
value = "${digitalocean_droplet.nodes.*.ipv4_address}"
}
output "node_private_ip_address" {
value = "${digitalocean_droplet.nodes.*.ipv4_address_private}"
}

You can now run the following on the console.

$ terraform init
...
$ terraform plan
...
$ terraform apply
...

Now this will create the SSH key and three droplets. You can test if the setup is working by SSH’ing into the machines using the following command. You can access the outputs using the terraform output command. Note that dotf is my private key that I created earlier using ssh-keygen.

ssh -i dotf -l root  $(terraform output controller_ip_address)

Our terraform output in JSON looks a bit like this.

$ terraform output -json
{
"controller_ip_address": {
"sensitive": false,
"type": "string",
"value": "142.93.216.119"
},
"controller_private_ip_address": {
"sensitive": false,
"type": "string",
"value": "10.139.80.8"
},
"node_ip_address": {
"sensitive": false,
"type": "list",
"value": [
"142.93.210.32",
"142.93.216.89"
]
},
"node_private_ip_address": {
"sensitive": false,
"type": "list",
"value": [
"10.139.80.13",
"10.139.80.3"
]
}
}

Now we can SSH into it by referring to the node IP using jq.

ssh -i dotf -l root  $(terraform output -json node_ip_address | jq -r .value[0])

Next in the series, we’ll setup an ansible script to setup an etcd cluster and demonstrate how it can be integrated with terraform. Follow the publication to get notified when we publish the next article.