Terraform and OpenTofu on DigitalOcean: Droplets, VPC and Firewall

· 2 min read · Terraform & OpenTofu Tutorials

DigitalOcean with Terraform #

DigitalOcean is a cloud with a short list of services and simple pricing, popular for small and medium projects. Terraform and OpenTofu manage it with the digitalocean provider. The workflow is the same as for AWS, but there is much less to configure: a virtual machine is called a Droplet, and a Droplet can be running in a few lines.

API token #

In the DigitalOcean control panel open API, Generate New Token, and give it the scopes you need (read and write for this tutorial). The provider reads the token from an environment variable, so it stays out of the code:

$ export DIGITALOCEAN_TOKEN="<your token>"

Provider #

providers.tf
terraform {
  required_version = ">= 1.6"

  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

provider "digitalocean" {
  # the token is read from DIGITALOCEAN_TOKEN
}

variable "region" {
  type        = string
  default     = "fra1"
  description = "Region slug, for example fra1 (Frankfurt) or nyc3 (New York)"
}

variable "admin_cidr" {
  type        = string
  description = "Address range that can connect with SSH, for example your public IP as x.x.x.x/32"
}

variable "ssh_public_key_path" {
  type    = string
  default = "~/.ssh/id_ed25519.pub"
}

SSH key and VPC #

Upload your public key once and refer to it from the Droplet. A VPC is a private network for the Droplets of one region:

network.tf
resource "digitalocean_ssh_key" "main" {
  name       = "ditwl-demo"
  public_key = file(pathexpand(var.ssh_public_key_path))
}

resource "digitalocean_vpc" "main" {
  name     = "ditwl-demo"
  region   = var.region
  ip_range = "10.10.10.0/24"
}

The ip_range must not overlap with other networks in your account. If you omit it, DigitalOcean picks a range.

A Droplet #

droplet.tf
resource "digitalocean_droplet" "web" {
  name     = "ditwl-demo-web"
  image    = "ubuntu-24-04-x64"
  region   = var.region
  size     = "s-1vcpu-1gb"
  vpc_uuid = digitalocean_vpc.main.id
  ssh_keys = [digitalocean_ssh_key.main.fingerprint]
  tags     = ["web"]
}

output "web_public_ip" {
  value = digitalocean_droplet.web.ipv4_address
}
  • image is a slug such as ubuntu-24-04-x64. size is a slug that sets CPU, memory and price, such as s-1vcpu-1gb. Not every size exists in every region.
  • List the valid slugs with the command line client doctl compute image list --public, doctl compute size list and doctl compute region list.
  • You connect as root: ssh root@<ip>. Create another user with cloud-init (the user_data argument of the Droplet) and disable root login on a real server.

A cloud firewall #

A DigitalOcean cloud firewall runs outside the Droplet. Attach it by tag, so every Droplet with the tag web is protected, even those created later:

firewall.tf
resource "digitalocean_firewall" "web" {
  name = "ditwl-demo-web"
  tags = ["web"]

  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = [var.admin_cidr]
  }

  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"]
  }

  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"]
  }
}

The outbound rules allow all outgoing traffic, so the Droplet can download packages and updates. Remove the inbound ports that you do not serve.

Run it #

$ tofu init
$ tofu plan -var admin_cidr=203.0.113.25/32
$ tofu apply -var admin_cidr=203.0.113.25/32
$ tofu destroy -var admin_cidr=203.0.113.25/32

Use your own public IP in admin_cidr, and terraform instead of tofu if you prefer HashiCorp Terraform. A Droplet is billed while it exists, even when it is powered off, so destroy test resources when you are done.

Compare with AWS #

AWS DigitalOcean
EC2 instance Droplet
VPC VPC (regional, per region)
Security group Cloud firewall (attached by Droplet ID or tag)
Key pair SSH key
S3 Spaces
EKS DigitalOcean Kubernetes (DOKS)

The same provider manages Spaces, managed databases, load balancers and Kubernetes clusters. Read next project structure, remote state and best practices.

#Terraform #OpenTofu #Digitalocean