Terraform and OpenTofu on DigitalOcean: Droplets, VPC and Firewall
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 #
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:
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 #
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
}imageis a slug such asubuntu-24-04-x64.sizeis a slug that sets CPU, memory and price, such ass-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 listanddoctl compute region list. - You connect as
root:ssh root@<ip>. Create another user with cloud-init (theuser_dataargument 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:
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.