Creating DigitalOcean Droplets with Terraform

Published on
Authors

In this post, you will find a tutorial about getting started with Terraform and DigitalOcean.

Terraform is a declarative open-source tool. Terraform allows you to define infrastructure as code, it means that you only have to define once the resources you want to create, and then you can use this code multiple times to create those resources.

You can find all the code of this post in this GitHub repository,

Get your DigitalOcean API token

Terraform needs a Digital Ocean API token to be able to create and destroy resources. To get this token, you can follow their instructions about How to Create a Personal Access Token.

Once you have the token you need to export it as an environment variable with the following command:

export TF_VAR_do_token=6aa82264bb900dd56909d6cb8432439b55211c996cb16da733679d960b5ef1e1

Every environment variable you export with the format TF_VAR_xyz will be available inside Terraform as xyz. Using an environment variable keeps the token out of config files, so secrets are not accidentally committed to the git repository and published.

Get your SSH Key fingerprint

As soon as Terraform creates the droplets we will want to access them using SSH. To avoid using passwords the best (and most secure) way is to use SSH keys. In DigitalOcean you need to add your SSH key to your account. You can do it following their instructions about How to Upload SSH Public Keys to a DigitalOcean Account.

After adding the SSH key to DigitalOcean you need to get your SSH key fingerprint. You can obtain this fingerprint from the list of your SSH keys on your DigitalOcean account. If you prefer you can also get the fingerprint running this command:

ssh-keygen -E md5 -lf ~/.ssh/id_rsa.pub | awk '{print $2}'|cut -d ':' -f2-

Now that you have you SSH key fingerprint you need to export it as an environment variable with the following command:

export TF_VAR_ssh_fingerprint=a5:20:d7:1a:51:83:17:bc:2d:0c:4b:51:26:ac:42:15

Creating a single Droplet and a Firewall

In this first example, we are going to use the DigitalOcean provider available in Terraform to create the following infrastructure.

  • A Droplet to be used as a web server
  • A Firewall to allow only incoming SSH and HTTP(s) traffic to the Droplet.

DigitalOcean SSH and Web Firewall and Droplet

Note: These resources will be created in your default project in DigitalOcean.

Terraform uses HCL (HashiCorp configuration language) to define resources, which is a declarative language. The first file we need to create is the variables.tf file. This file declares and defines the list of variables that we will use later in our main.tf file.

variable "do_token" {
  description = "DigitalOcean API token"
}
variable "ssh_fingerprint" {
  description = "Fingerprint of your SSH key"
}
variable "droplet_image" {
  description = "Image identifier of the OS in DigitalOcean"
  default     = "ubuntu-20-04-x64"
}
variable "droplet_region" {
  description = "Droplet region identifier where the droplet will be created"
  default     = "sfo3"
}
variable "droplet_size" {
  description = "Droplet size identifier"
  default     = "s-1vcpu-1gb"
}

In this file we have defined the following variables

  • do_token: This is the token that we have previously created in DigitalOcean. It doesn't have a default value because we need to provide it using the TF_VAR_do_token environment variable.
  • ssh_fingerprint: This is our SSH key fingerprint. We can provide it using an environment variable (TF_VAR_ssh_fingerprint) or we could add our fingerprint here as the default value.
  • droplet_image: This is the identifier of the image (operating system) that droplets will run. I've chosen the last Ubuntu (20.04 LTS)
  • droplet_region: This is the identifier of the region where the droplets will run. The best is to choose the nearest possible location in order to minimize ping. In this example, I've chosen sfo3 which is located in San Francisco.
  • droplet_size: This is the droplet size identifier. In this example, I've chosen s-1vcpu-1gb, which is the smallest Droplet that DigitalOcean offers with 1 vCPU and 1GB of RAM.

Now that we have seen the variables is time to see the file that defines the infrastructure resources: main.tf

The first part of the file tells Terraform to use the DigitalOcean provider with our DigitalOcean API token.

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
    }
  }
}

provider "digitalocean" {
  token = var.do_token
}

Then there is a digitalocean_droplet resource which will create a new Droplet in DigitalOcean with "webserver" as name and our previously defined image, region and size. Also monitoring, private networking and backups are enabled. The last line in the resource injects our SSH key into the droplet, so we can log in via SSH as root.

# Droplet
resource "digitalocean_droplet" "web" {
  image              = var.droplet_image
  name               = "webserver"
  region             = var.droplet_region
  size               = var.droplet_size
  backups            = true
  monitoring         = true
  private_networking = true
  ssh_keys = [
    var.ssh_fingerprint
  ]
}

The last part of this file defines a digitalocean_firewall resource. This firewall allows inbound SSH, HTTP and HTTPS traffic to the webserver Droplet. It also allows any kind of ICMP, TCP and UDP outbound traffic.

# Firewall
resource "digitalocean_firewall" "web" {
  name = "only-allow-ssh-http-and-https"

  droplet_ids = [digitalocean_droplet.web.id]

  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "80"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "443"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  inbound_rule {
    protocol         = "icmp"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "udp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "icmp"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
}

Tip: At any moment you can run the command terraform fmt to format the code.

Now the first command we need to run is terraform init. This command needs to be run only once. What this command does is to download the DigitalOcean external provider. This provider is not included in Terraform by default.

$ terraform init

Initializing the backend...

Initializing provider plugins...
Finding latest version of digitalocean/digitalocean...
Installing digitalocean/digitalocean v1.22.2...
Installed digitalocean/digitalocean v1.22.2 (signed by a HashiCorp partner, key ID F82037E524B9C0E8)

...

* digitalocean/digitalocean: version = "~> 1.22.2"

Terraform has been successfully initialized!

Before applying changes we can invoke the terraform plan command to know which resources will be created.

$ terraform plan

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # digitalocean_droplet.web will be created
  + resource "digitalocean_droplet" "web" {
      ...
    }

  # digitalocean_firewall.web will be created
  + resource "digitalocean_firewall" "web" {
      ...
    }

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

------------------------------------------------------------------------

As we can see Terraform is planning to create 2 resources: a Droplet and a Firewall, so everything seems to be right. Now it's time to create the resources running the terraform apply command. Terraform will ask if we want to perform the action:

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

...

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

digitalocean_droplet.web: Creating...
digitalocean_droplet.web: Still creating... [10s elapsed]
digitalocean_droplet.web: Still creating... [20s elapsed]
digitalocean_droplet.web: Still creating... [30s elapsed]
digitalocean_droplet.web: Creation complete after 36s [id=207248896]
digitalocean_firewall.web: Creating...
digitalocean_firewall.web: Creation complete after 2s [id=854955d2-56a9-423e-83f4-fba75be71b3d]

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

If we check DigitalOcean control panel we will see that our resources exist:

Digitalocean webserver

Digitalocean Firewall

Now we can start setting up the droplet installing all the software we need, but that won't be described here as it isn't the purpose of this post.

If we want to destroy these resources we just have to run terraform destroy. That's all.

Multiple Droplets, Firewalls, and a Load Balancer

In the previous example, we have explored two kinds of Terraform DigitalOcean resources: droplets and firewalls. In this example, we are going to add more resources to have a more realistic scenario.

  • A Droplet to be used as a Database (MySQL) server.
  • Two Droplets to be used as web servers
  • A Load Balancer to evenly distribute the incoming HTTP traffic across the web servers.
  • A Firewall to allow only incoming SSH traffic and any kind of TCP/UDP outgoing traffic.
  • A Firewall to allow only incoming HTTP/HTTPs traffic from the Load balancer to the web servers.
  • A Firewall to allow only incoming MySQL traffic from the web servers to the MySQL server.

DigitalOcean Droplets Load Balancer and Firewalls

The content of the variables.tf file is the same as in the first example. This is the content of the main.tf file. The first part is the same as we have already seen.

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
    }
  }
}

provider "digitalocean" {
  token = var.do_token
}

In this part we create three droplets (two web servers and one database). As we want to create two web servers with the same specs we use the count statement in Terraform to create both of them without repeating the same code.

# Droplets
resource "digitalocean_droplet" "webserver" {
  count              = 2
  image              = var.droplet_image
  name               = "webserver-${count.index}"
  region             = var.droplet_region
  size               = var.droplet_size
  backups            = true
  monitoring         = true
  private_networking = true
  ssh_keys = [
    var.ssh_fingerprint
  ]
}

resource "digitalocean_droplet" "database" {
  image              = var.droplet_image
  name               = "dbserver"
  region             = var.droplet_region
  size               = var.droplet_size
  backups            = true
  monitoring         = true
  private_networking = true
  ssh_keys = [
    var.ssh_fingerprint
  ]
}

Next we create a Load Balancer which will distribute our web traffic. Using the droplets_ids property we can specify the Droplets where the Load Balancer will send traffic.

resource "digitalocean_loadbalancer" "public" {
  name   = "loadbalancer"
  region = var.droplet_region

  forwarding_rule {
    entry_port     = 80
    entry_protocol = "http"

    target_port     = 80
    target_protocol = "http"
  }

  healthcheck {
    port     = 22
    protocol = "tcp"
  }

  droplet_ids = digitalocean_droplet.webserver.*.id
}

Now the only remaining resources are the three Firewalls:

resource "digitalocean_firewall" "ssh-icmp-and-outbound" {
  name = "allow-ssh-and-icmp"

  droplet_ids = concat(digitalocean_droplet.webserver.*.id, [digitalocean_droplet.database.id])

  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  inbound_rule {
    protocol         = "icmp"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "udp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "icmp"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
}

resource "digitalocean_firewall" "http-https" {
  name = "allow-http-and-https"

  droplet_ids = digitalocean_droplet.webserver.*.id

  inbound_rule {
    protocol                  = "tcp"
    port_range                = "80"
    source_load_balancer_uids = [digitalocean_loadbalancer.public.id]
  }

  inbound_rule {
    protocol                  = "tcp"
    port_range                = "443"
    source_load_balancer_uids = [digitalocean_loadbalancer.public.id]
  }
}

resource "digitalocean_firewall" "mysql" {
  name = "allow-mysql-traffic-form-webservers"

  droplet_ids = [digitalocean_droplet.database.id]

  inbound_rule {
    protocol           = "tcp"
    port_range         = "3306"
    source_droplet_ids = digitalocean_droplet.webserver.*.id
  }
}

As with the previous example, we have to run terraform init to initialize and then terraform apply to create the resources.

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # digitalocean_droplet.database will be created
  + resource "digitalocean_droplet" "database" {
      ...
    }

  # digitalocean_droplet.webserver[0] will be created
  + resource "digitalocean_droplet" "webserver" {
      ...
    }

  # digitalocean_droplet.webserver[1] will be created
  + resource "digitalocean_droplet" "webserver" {
      ...
    }

  # digitalocean_firewall.http-https will be created
  + resource "digitalocean_firewall" "http-https" {
      ...
    }

  # digitalocean_firewall.mysql will be created
  + resource "digitalocean_firewall" "mysql" {
      ...
    }

  # digitalocean_firewall.ssh-icmp-and-outbound will be created
  + resource "digitalocean_firewall" "ssh-icmp-and-outbound" {
      ...
    }

  # digitalocean_loadbalancer.public will be created
  + resource "digitalocean_loadbalancer" "public" {
      ...
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

digitalocean_droplet.webserver[1]: Creating...
digitalocean_droplet.webserver[0]: Creating...
digitalocean_droplet.database: Creating...
digitalocean_droplet.database: Still creating... [10s elapsed]
digitalocean_droplet.webserver[1]: Still creating... [10s elapsed]
digitalocean_droplet.webserver[0]: Still creating... [10s elapsed]
digitalocean_droplet.webserver[0]: Still creating... [20s elapsed]
digitalocean_droplet.database: Still creating... [20s elapsed]
digitalocean_droplet.webserver[1]: Still creating... [20s elapsed]
digitalocean_droplet.webserver[1]: Still creating... [30s elapsed]
digitalocean_droplet.webserver[0]: Still creating... [30s elapsed]
digitalocean_droplet.database: Still creating... [30s elapsed]
digitalocean_droplet.webserver[1]: Creation complete after 34s [id=207351077]
digitalocean_droplet.webserver[0]: Creation complete after 35s [id=207351078]
digitalocean_loadbalancer.public: Creating...
digitalocean_droplet.database: Creation complete after 35s [id=207351079]
digitalocean_firewall.mysql: Creating...
digitalocean_firewall.ssh-icmp-and-outbound: Creating...
digitalocean_firewall.mysql: Creation complete after 1s [id=39334ce2-b4d4-4a48-9930-a72dbdae8765]
digitalocean_firewall.ssh-icmp-and-outbound: Creation complete after 2s [id=bd50b05c-d792-42a9-ab44-44be8a47ad6d]
digitalocean_loadbalancer.public: Still creating... [10s elapsed]
digitalocean_loadbalancer.public: Still creating... [20s elapsed]
digitalocean_loadbalancer.public: Still creating... [30s elapsed]
digitalocean_loadbalancer.public: Still creating... [40s elapsed]
digitalocean_loadbalancer.public: Still creating... [50s elapsed]
digitalocean_loadbalancer.public: Still creating... [1m0s elapsed]
digitalocean_loadbalancer.public: Still creating... [1m10s elapsed]
digitalocean_loadbalancer.public: Still creating... [1m20s elapsed]
digitalocean_loadbalancer.public: Creation complete after 1m20s [id=b1304ac3-735d-49aa-b5f3-42a927b58de4]
digitalocean_firewall.http-https: Creating...
digitalocean_firewall.http-https: Creation complete after 1s [id=b7bb6df2-6b67-4a41-8554-38e55291d582]

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

And then we will see in DigitalOcean that everything has been created successfully.

Load Balancer and Droplets

Multiple Droplets Networking Firewalls

Load Balancer details

In the the Load Balancer details page, we can see the Status column of every webserver in Healthy status. It means that the Load Balancer knows that every webserver is running and able to receive traffic. In the Terraform file we defined that the Load Balancer will perform the health checks using the TCP port 22 (SSH).

Recap

In this post, you have learned how to easily define and create resources like Droplets, Firewalls, and Load Balancers in DigitalOcean with Terraform.

If you want to find out more types of available resources you can check the Terraform DigitalOcean Provider documentation page. There you will find other interesting resources like Domains, Droplet Snapshots, Projects, DNS Records, Spaces Buckets, Volumes, and much more!