This how-to will help you deploy a production-ready infrastructure on Digital Ocean using Terraform.
Pre-requisites
- Install Terraform
- Create a Digital Ocean account if you don't already have one (Use this link to get $100 credit)
- Generate a Personal Access Token for your Digital Ocean account to access the DigitalOcean API. Go to API => Tokens/Keys => Generate New Token. Save thestringgenerated.
- Create a domain for your project. Go to Networking => Domains => Add domain
Initial setup terraform files
- Open your terminal and create a new project directory, and open with you code editor.
$ mkdir minimal-prod`
$ cd minimal-prod`
$ code .
- Create the following terraform files:
$ touch versions.tf
$ touch main.tf
$ touch variables.tf
- In versions.tf, specify the Digital Ocean terraform provider as follows:
terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "2.25.2"
    }
  }
}
- In main.tf, add the token required by the provider like this:
provider "digitalocean" {
  token = var.do_token
}
- In variables.tf, create a new variable calleddo_token.
variable "do_token" {
  type        = string
  description = "Digital Ocean personal access token"
  default     = "<token_string>"
}
If you wish to commit
variables.tfto your version control system, you might want to use a different file for more sensitive information, such as your Digital Ocean's personal access token. Create a new fileterraform.tfvarsand save the following to it:
do_token = "<digital_ocean_token>"
Replace
<digital_ocean_token>with the actual value generated.
⚠️ Make sure *.tfvars is in your .gitignore file.
- Now prepare your working directory for other commands by running the following command:
$ terraform init
Architecture Diagram
Below is a diagram representing the architecture that will be produced by executing the Terraform files at the end of this tutorial.
                       |                                                   
                     https                                                
                       |                                                   
                       v                                                    
              +--------------------+                                        
              |    Load Balancer   |                                        
 +--------------------------------------------------+                          
 |            |                    |                |                     
 |            +--------------------+                |                     
 |                     |                            |                     
 |                     |                            |                     
 |                    http              +---------+ |                     
 |                     |                |         | |                     
 |                     |                |         | |                     
 |                     +-------SSH------| Bastion |<---SSH---
 |                     |                |         | |                     
 |                     |                |         | |                     
 |                     |                +---------+ |                     
 |          +----------+---------+                  |                     
 |          |          |         |                  |                     
 |          v          v         v                  |                     
 |      +-------+  +-------+  +-------+             |                     
 |      |  web  |  |  web  |  |  web  |             |                     
 |      +---+---+  +---+---+  +---+---+             |                     
 |          |          |          |                 |                     
 |          |          v          |                 |                     
 |          |     +----------+    |                 |                     
 |          |     |          |    |                 |                     
 |          +---->| database |<---+                 |                     
 |                |          |                      |                     
 |                +----------+                      |                     
 +--------------------------------------------------+  
Virtual Private Cloud (VPC) setup
- Create a file network.tfand add the following to build the VPC
resource "digitalocean_vpc" "web" {
  name     = "${var.name}-vpc"
  region   = var.region
  ip_range = var.ip_range
}
- Add new variables to variables.tf
...
variable "name" {
  type        = string
  description = "Infrastructure project name"
  default     = "minimal-prod"
}
variable "region" {
  type    = string
  default = "ams2"
}
variable "ip_range" {
  type        = string
  description = "IP range for VPC"
  default     = "192.168.22.0/24"
}
- Run the command below to see the execution plan so far
$ terraform plan
Web Servers setup
- Add a few more variables to variables.tfto be used in the future
...
variable "droplet_count" {
  type    = number
  default = 1
}
variable "image" {
  type        = string
  description = "OS to install on the servers"
  default     = "ubuntu-20-04-x64"
}
variable "droplet_size" {
  type    = string
  default = "s-1vcpu-1gb"
}
variable "ssh_key" {
  type = string
}
variable "subdomain" {
  type = string
}
variable "domain_name" {
  type = string
}
- Create a new file data.tfand add a data resource for our ssh key which will be pulled from Digital Ocean directly:
data "digitalocean_ssh_key" "main" {
  name = var.ssh_key
}
- Create a file servers.tfhold all the resources we'll be creating for our web servers as follows:
resource "digitalocean_droplet" "web" {
  count     = var.droplet_count
  image     = var.image
  name      = "web-${var.name}-${var.region}-${count.index + 1}"
  region    = var.region
  size      = var.droplet_size
  ssh_keys  = [data.digitalocean_ssh_key.main.id]
  vpc_uuid  = digitalocean_vpc.web.id
  tags      = ["${var.name}-webserver"]
  user_data = <<EOF
  #cloud-config
  packages:
    - nginx
    - postgresql
    - postgresql-contrib
  runcmd:
    - [ sh, -xc, "echo '<h1>web-${var.region}-${count.index + 1}</h1>' >> /var/www/html/index.html"]
  EOF
  lifecycle {
    create_before_destroy = true
  }
}
- Update terraform.tfvarswith the following variables for our environment:
...
region        = "ams3"
droplet_count = 3
ssh_key       = "<ssh_key_name_on_digitalocean>"
domain_name   = "<domain_added_on_digitalocean>"
subdomain     = "app"
- Add a value for the domain you want to use in data.tf
...
data "digitalocean_domain" "web" {
  name = var.domain_name
}
- Create a lets encrypt certificate to be used by the load balancer. Add the following to the servers.tffile
resource "digitalocean_certificate" "web" {
  name    = "${var.name}-certificate"
  type    = "lets_encrypt"
  domains = ["${var.subdomain}.${data.digitalocean_domain.web.name}"]
  lifecycle {
    create_before_destroy = true
  }
}
You can use the Digital Ocean terraform provider to provide your own certificate if you want to.
- Next, we create our load balancer with the correct forwarding rules and the firewall setup in servers.tf. This is to block all inbound traffic directly to the web servers from the internet.
...
resource "digitalocean_loadbalancer" "web" {
  name                   = "web-${var.region}"
  region                 = var.region
  droplet_ids            = digitalocean_droplet.web.*.id
  vpc_uuid               = digitalocean_vpc.web.id
  redirect_http_to_https = true
  forwarding_rule {
    entry_port       = 443
    entry_protocol   = "https"
    target_port      = 80
    target_protocol  = "http"
    certificate_name = digitalocean_certificate.web.name
  }
  forwarding_rule {
    entry_port       = 80
    entry_protocol   = "http"
    target_port      = 80
    target_protocol  = "http"
    certificate_name = digitalocean_certificate.web.name
  }
  lifecycle {
    create_before_destroy = true
  }
}
resource "digitalocean_firewall" "web" {
  name        = "${var.name}-only-vpc-traffic"
  droplet_ids = digitalocean_droplet.web.*.id
  inbound_rule {
    protocol         = "tcp"
    port_range       = "1-65535"
    source_addresses = [digitalocean_vpc.web.ip_range]
  }
  inbound_rule {
    protocol         = "udp"
    port_range       = "1-65535"
    source_addresses = [digitalocean_vpc.web.ip_range]
  }
  inbound_rule {
    protocol         = "icmp"
    source_addresses = [digitalocean_vpc.web.ip_range]
  }
  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = [digitalocean_vpc.web.ip_range]
  }
  outbound_rule {
    protocol              = "udp"
    port_range            = "1-65535"
    destination_addresses = [digitalocean_vpc.web.ip_range]
  }
  outbound_rule {
    protocol              = "icmp"
    destination_addresses = [digitalocean_vpc.web.ip_range]
  }
  outbound_rule {
    protocol              = "udp"
    port_range            = "53"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
  outbound_rule {
    protocol              = "tcp"
    port_range            = "80"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
  outbound_rule {
    protocol              = "tcp"
    port_range            = "443"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
  outbound_rule {
    protocol              = "icmp"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
}
- Next, we create the record for the subdomain
...
resource "digitalocean_record" "web" {
  domain = data.digitalocean_domain.web.name
  type   = "A"
  name   = var.subdomain
  value  = digitalocean_loadbalancer.web.ip
  ttl    = 30
}
Database resource
- Next, we add a few more variables to be used to setup our database in variables.tfas follows:
...
variable "db_count" {
  type    = number
  default = 1
}
variable "database_size" {
  type    = string
  default = "db-s-1vcpu-1gb"
}
- Next, create database.tfto build the database. For this example we will be creating a Postgres database.
resource "digitalocean_database_cluster" "postgres-cluster" {
  name                 = "${var.name}-database-cluster"
  engine               = "pg"
  version              = "11"
  size                 = var.database_size
  region               = var.region
  node_count           = var.db_count
  private_network_uuid = digitalocean_vpc.web.id
}
resource "digitalocean_database_firewall" "postgress-cluster-firewall" {
  cluster_id = digitalocean_database_cluster.postgres-cluster.id
  rule {
    type  = "tag"
    value = "${var.name}-webserver"
  }
}
Jump server (Bastion) for accessing our infrastructure
- Next, we need to create a jump server (Bastion) to access our infrastructure. Create a bastion.tfwith the following:
resource "digitalocean_droplet" "bastion" {
  image    = var.image
  name     = "bastion-${var.name}-${var.region}"
  region   = var.region
  size     = "s-1vcpu-1gb"
  ssh_keys = [data.digitalocean_ssh_key.main.id]
  vpc_uuid = digitalocean_vpc.web.id
  tags     = ["${var.name}-webserver"]
  lifecycle {
    create_before_destroy = true
  }
}
resource "digitalocean_record" "bastion" {
  domain = data.digitalocean_domain.web.name
  type   = "A"
  name   = "bastion-${var.name}-${var.region}"
  value  = digitalocean_droplet.bastion.ipv4_address
  ttl    = 300
}
resource "digitalocean_firewall" "bastion" {
  name        = "${var.name}-only-ssh-bastion"
  droplet_ids = [digitalocean_droplet.bastion.id]
  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }
  outbound_rule {
    protocol              = "tcp"
    port_range            = "22"
    destination_addresses = [digitalocean_vpc.web.ip_range]
  }
  outbound_rule {
    protocol              = "icmp"
    destination_addresses = [digitalocean_vpc.web.ip_range]
  }
}
- Now you can apply the files and let terraform create the infrastructure on Digital Ocean
$ terraform apply
You can use the
--auto-approveflag to let terraform automatically continue without asking you for approval.
Access the server through the bastion host
- Copy the domain name of the bastion host and ssh into it from your terminal.
$ ssh -A root@<FQDN of bastion host>
- Copy the IP address of any of the web servers and ssh into it from the bastion host
$ ssh root@<private_ip_address_of_server>
Delete infrastructure
This will undo everything. You can delete the infrastructure by running the following command:
$ terraform destroy
That's it.
Thanks for the time reading this!
Let me know if you there's a way you think this infrastructure can be improved.
👍
 
 
              

 
    
Top comments (2)
Great article, this is well written and very informative.
On top of making sure to add
.tfvarsextension to your.gitinore, you should also make sure your tfstate file is encrypted. Because when Terraform use a secret for creating resources, it writes the value to the tfstate as plain-text.I totally agree with you, thanks for pointing this out. 👏