In this recipe, we'll build from scratch a fully working CoreOS cluster on Digital Ocean in their New York region, using Terraform and cloud-init. We'll add some latency monitoring as well with StatusCake, so we have a good foundation of using Terraform on Digital Ocean.
To step through this recipe, you will need the following:
Let's start by creating the digitalocean
provider (it only requires an API token) in a file named providers.tf
:
provider "digitalocean" { token = "${var.do_token}" }
Declare the do_token
variable in a file named variables.tf
:
variable "do_token" { description = "Digital Ocean Token" }
Also, don't forget to set it in a private terraform.tfvars
file:
do_token = "a1b2c3d4e5f6"
We know that we'll need an SSH key to log into the cluster members. With Digital Ocean, the resource is named digitalocean_ssh_key
. I propose that we name the SSH key file iac_admin_sshkey
in the keys
directory, but as you might prefer something else, let's use a variable for that as well. Let's write this in a keys.tf
file:
resource "digitalocean_ssh_key" "default" { name = "Digital Ocean SSH Key" public_key = "${file("${var.ssh_key_file}.pub")}" }
Create the related variable in variables.tf
, with our suggested default:
variable "ssh_key_file" { default = "keys/iac_admin_sshkey" description = "Default SSH Key file" }
It's now time to effectively override the value in the terraform.tfvars
file if you feel like it:
ssh_key_file = "./keys/my_own_key"
Here's the core of our infrastructure: three nodes running in the New York City data center NYC1, with private networking enabled, no backups activated (set it to true
if you feel like it!), the SSH key we previously created, and a cloud-init file to initiate configuration. A virtual machine at Digital Ocean is named a droplet, so the resource to launch a droplet is digitalocean_droplet
. All variables' names relate to what we just enumerated:
resource "digitalocean_droplet" "coreos" { image = "${var.coreos_channel}" count = "${var.cluster_nodes}" name = "coreos-${count.index+1}" region = "${var.do_region}" size = "${var.do_droplet_size}" ssh_keys = ["${digitalocean_ssh_key.default.id}"] private_networking = true backups = false user_data = "${file("cloud-config.yml")}" }
Declare all the variables in the variables.tf
file, with some good defaults (the smallest 512 MB droplet, a three-node cluster), and some defaults we'll want to override (AMS3 data center or the stable CoreOS channel):
variable "do_region" { default = "ams3" description = "Digital Ocean Region" } variable "do_droplet_size" { default = "512mb" description = "Droplet Size" } variable "coreos_channel" { default = "coreos-stable" description = "CoreOS Channel" } variable "cluster_nodes" { default = "3" description = "Number of nodes in the cluster" }
Here are our overridden values in terraform.tfvars
(but feel free to put your own values, such as using another data center or CoreOS release):
do_region = "nyc1" coreos_channel = "coreos-beta"
It would be awesome to automatically have a few auto-documented lines on how to connect to our CoreOS cluster. As we can do that with the Terraform outputs, let's use this example for a start, in outputs.tf
. This is constructing an SSH command line with dynamic information from Terraform that we'll be able to use easily (it's simply iterating over every digitalocean_droplet.coreos.*
available):
output "CoreOS Cluster Members" { value = "${formatlist("ssh core@%v -i ${var.ssh_key_file}", digitalocean_droplet.coreos.*.ipv4_address)}" }
The output will look like this:
CoreOS Cluster Members = [ ssh [email protected] -i ./keys/iac_admin_sshkey, ssh [email protected] -i ./keys/iac_admin_sshkey, ssh [email protected] -i ./keys/iac_admin_sshkey ]
One of the attractive features of Digital Ocean is the easy DNS integration. For example, if our domain is infrastructure-as-code.org
and we launch a blog droplet, we'll end up registering it automatically under the public DNS name blog.infrastructure-as-code.org
. Pretty easy and dynamic! To give Digital Ocean power on our domain, we need to go to our registrar (where we bought our domain), and configure our domain to be managed by Digital Ocean, using their own nameservers, which are as follows:
ns1.digitalocean.com
ns2.digitalocean.com
ns3.digitalocean.com
This prerequisite being done, let's declare our domain in the dns.tf
file using the digitalocean_domain
resource, automatically using a cluster_domainname
variable for the domain name, and an initial IP address matching, that we can either set to a value you already know or to an arbitrary droplet:
resource "digitalocean_domain" "cluster_domainname" { name = "${var.cluster_domainname}" ip_address = "${digitalocean_droplet.coreos.0.ipv4_address}" }
Add the new variable in variables.tf
:
variable "cluster_domainname" { default = "infrastructure-as-code.org" description = "Domain to use" }
Don't forget to override it as necessary in terraform.tfvars
.
The next step is to register automatically every droplet in the DNS. By iterating over each droplet, and extracting their name
and ipv4_address
attributes, we'll add this digitalocean_record
resource into the mix:
resource "digitalocean_record" "ipv4" { count = "${var.cluster_nodes}" domain = "${digitalocean_domain.cluster_domainname.name}" type = "A" name = "${element(digitalocean_droplet.coreos.*.name, count.index)}" value = "${element(digitalocean_droplet.coreos.*.ipv4_address, count.index)}" }
This will automatically register every droplet under the name core-[1,2,3].mydomain.com, for easier access and reference.
If you like, you can access the fqdn
attribute of this resource right from the outputs (outputs.tf
):
output "CoreOS Cluster Members DNS" { value = "${formatlist("ssh core@%v -i ${var.ssh_key_file}", digitalocean_record.ipv4.*.fqdn)}" }
We need to build a fully working cloud-config.yml
file for our CoreOS cluster. Refer to the cloud-init part of this book in Chapter 5, Provisioning the Last Mile with Cloud-Init for more information on the cloud-config.yml
file, and especially on configuring CoreOS with it.
What we need for a fully usable CoreOS cluster are the following:
$private_ipv4
)$private_ipv4
)Fleet is a distributed init system. You can think of it as systemd for a whole cluster
To configure etcd, we first need to obtain a new token. This token is unique and can be distributed through different channels. It can be easily obtained through the https://coreos.com/os/docs/latest/cluster-discovery.html etcd service. Then we'll start 2 units—etcd and fleet.
$ curl -w " " 'https://discovery.etcd.io/new?size=3' https://discovery.etcd.io/b04ddb7ff454503a66ead486b448afb7
Note this URL carefully and copy paste it in the following cloud-config.yml
file:
#cloud-config # https://coreos.com/validate/ coreos: etcd2: discovery: "https://discovery.etcd.io/b04ddb7ff454503a66ead486b448afb7" advertise-client-urls: "http://$private_ipv4:2379" initial-advertise-peer-urls: "http://$private_ipv4:2380" listen-client-urls: http://0.0.0.0:2379 listen-peer-urls: http://$private_ipv4:2380 units: - name: etcd2.service command: start - name: fleet.service command: start fleet: public-ip: "$public_ipv4" metadata: "region=ams,provider=digitalocean"
This will be enough to start an etcd + fleet cluster on CoreOS. Chapter 5, Provisioning the Last
Mile with Cloud-Init, for in-depth details on cloud-init.
We can reuse our knowledge from previous chapters to easily integrate full latency monitoring to the hosts of our CoreOS cluster, using a free StatusCake account (https://statuscake.com).
Start by configuring the provider in providers.tf
:
provider "statuscake" { username = "${var.statuscake_username}" apikey = "${var.statuscake_apikey}" }
Declare the required variables in variables.tf
:
variable "statuscake_username" { default = "changeme" description = "StatusCake Account Username" } variable "statuscake_apikey" { default = "hackme" description = "StatusCake Account API Key" }
Also, override with your own values in terraform.tfvars
.
Now we can use the statuscake_test
resource to activate immediate latency (ping) monitoring on every droplet by iterating over each digitalocean_droplet.coreos.*
resource value:
resource "statuscake_test" "coreos_cluster" { count = "${var.cluster_nodes}" website_name = "${element(digitalocean_droplet.coreos.*.name, count.index)}.${var.cluster_domainname}" website_url = "${element(digitalocean_droplet.coreos.*.ipv4_address, count.index)}" test_type = "PING" check_rate = 300 paused = false }
It's time to terraform apply
this:
$ terraform apply [...] CoreOS Cluster Members = [ ssh [email protected] -i ./keys/iac_admin_sshkey, ssh [email protected] -i ./keys/iac_admin_sshkey, ssh [email protected] -i ./keys/iac_admin_sshkey ] CoreOS Cluster Members DNS = [ ssh [email protected] -i ./keys/iac_admin_sshkey, ssh [email protected] -i ./keys/iac_admin_sshkey, ssh [email protected] -i ./keys/iac_admin_sshkey ]
Confirm that we can connect to a member using the command line from the output:
$ ssh [email protected] -i ./keys/iac_admin_sshkey
Verify the etcd cluster health:
$ core@coreos-1 ~ $ etcdctl cluster-health member 668f889d5f96b578 is healthy: got healthy result from http://10.136.24.178:2379 member c8e8906e0f3f63be is healthy: got healthy result from http://10.136.24.176:2379 member f3b53735aca3062e is healthy: got healthy result from http://10.136.24.177:2379 cluster is healthy
Check that all fleet members are all right:
core@coreos-1 ~ $ fleetctl list-machines MACHINE IP METADATA 24762c02... 159.203.189.146 provider=digitalocean,region=ams 3b4b0792... 159.203.189.142 provider=digitalocean,region=ams 59e15b88... 159.203.189.131 provider=digitalocean,region=ams
Enjoy, in less than a minute, you're ready to use a CoreOS cluster with basic monitoring, using only fully automated Terraform code!
3.149.234.188