Skip to content

Provision and configure on DigitalOcean

In this tutorial one configuration creates three DigitalOcean droplets and sets each one up: a deploy account with your public key. The droplets, their SSH host keys and the steps on each droplet are planned and applied together, and live in one state file.

Time: 15 minutes.

  • Conn3x v0.6.3 installed.
  • A DigitalOcean account and a personal access token with write access.
  • An SSH key added to your DigitalOcean account, with its private half at ~/.ssh/id_ed25519. Conn3x logs in to the droplets as root with it.
  • Its ID or fingerprint, from Settings > Security in the DigitalOcean control panel, or from doctl compute ssh-key list.
  • git.

The configuration is the digitalocean example in the Conn3x repository:

Terminal window
git clone --depth 1 --branch v0.6.3 https://github.com/conn3xorg/conn3x
cd conn3x/examples/digitalocean

main.tf holds the whole configuration:

terraform {
required_providers {
digitalocean = { source = "digitalocean/digitalocean" }
}
}
variable "ssh_key_id" {
type = string
description = "ID or fingerprint of an SSH key already in your DigitalOcean account."
}
locals {
servers = toset(["app-1", "app-2", "app-3"])
}
# Each server's SSH host key, made here and installed by cloud-init,
# so Conn3x can check it on the very first connection.
resource "tls_private_key" "host" {
for_each = local.servers
algorithm = "ED25519"
}
resource "digitalocean_droplet" "app" {
for_each = local.servers
name = each.key
image = "rockylinux-10-x64"
region = "nyc3"
size = "s-1vcpu-1gb"
ssh_keys = [var.ssh_key_id]
user_data = "#cloud-config\n${yamlencode({
ssh_deletekeys = true
ssh_keys = {
ed25519_private = tls_private_key.host[each.key].private_key_openssh
ed25519_public = tls_private_key.host[each.key].public_key_openssh
}
})}"
}
circuit "app" {
hosts = {
for name, d in digitalocean_droplet.app : name => {
address = d.ipv4_address
host_key = tls_private_key.host[name].public_key_openssh
}
}
ssh {
user = "root"
private_key_file = "~/.ssh/id_ed25519"
timeout = "5m"
}
step "deploy_user" {
user {
name = "deploy"
shell = "/bin/bash"
}
}
step "deploy_key" {
authorized_key {
user = "deploy"
key = file("${path.module}/deploy.pub")
}
}
}
output "addresses" {
value = { for name, d in digitalocean_droplet.app : name => d.ipv4_address }
}

The configuration does four things:

  • Generates a host key for each droplet with the tls provider, and hands it to the droplet through cloud-init. The droplet starts with that key instead of making its own.
  • Creates the droplets with the digitalocean provider, one for each name in local.servers.
  • Builds the circuit’s hosts from the droplets. Each host is a host object with the droplet’s address and its pinned host_key. Conn3x accepts only that key, so even the first connection to a new droplet is verified, and you don’t need a known_hosts entry.
  • Configures each droplet with two steps: a user and an authorized_key.

timeout = "5m" keeps Conn3x retrying while a new droplet boots, so there’s no separate wait step.

Export your token, so the digitalocean provider can read it:

Terminal window
export DIGITALOCEAN_TOKEN=dop_v1_...

The deploy user gets the public key in deploy.pub, next to main.tf. Copy yours there, or any public key you want the account to accept:

Terminal window
cp ~/.ssh/id_ed25519.pub deploy.pub
Terminal window
conn3x init

This time init downloads two providers:

Initializing provider plugins...
- terraform.io/builtin/conn3x is built in to Conn3x
- Finding latest version of hashicorp/tls...
- Finding latest version of digitalocean/digitalocean...
- Installing hashicorp/tls v4.4.2...
- Installing digitalocean/digitalocean v2.106.0...

Your versions may be newer. init writes them to .terraform.lock.hcl.

Pass your SSH key’s ID or fingerprint as ssh_key_id:

Terminal window
conn3x plan -var ssh_key_id=<your key ID or fingerprint>

The plan covers both layers at once:

# conn3x_authorized_key.app__deploy_key["app-1"] will be created
# conn3x_authorized_key.app__deploy_key["app-2"] will be created
# conn3x_authorized_key.app__deploy_key["app-3"] will be created
# conn3x_user.app__deploy_user["app-1"] will be created
# conn3x_user.app__deploy_user["app-2"] will be created
# conn3x_user.app__deploy_user["app-3"] will be created
# digitalocean_droplet.app["app-1"] will be created
# digitalocean_droplet.app["app-2"] will be created
# digitalocean_droplet.app["app-3"] will be created
# tls_private_key.host["app-1"] will be created
# tls_private_key.host["app-2"] will be created
# tls_private_key.host["app-3"] will be created
Plan: 12 to add, 0 to change, 0 to destroy.

The droplets don’t exist yet, so each step’s connection details are unknown until apply:

+ ssh {
+ host = (known after apply)
+ host_key = (known after apply)
+ private_key_file = "~/.ssh/id_ed25519"
+ timeout = "5m"
+ user = "root"
}
Terminal window
conn3x apply -var ssh_key_id=<your key ID or fingerprint>

Type yes when asked. Conn3x generates the host keys, creates the droplets, waits for each one to answer on SSH, then creates the user and installs the key.

When it finishes, the addresses output lists each droplet’s public address:

Terminal window
conn3x output addresses

Log in as deploy to check the account. Your ~/.ssh/known_hosts doesn’t have the droplets’ keys, so ssh asks you to confirm the fingerprint. Conn3x didn’t need to: it already had the key from the plan.

Run conn3x plan -var ssh_key_id=<your key ID or fingerprint> again. It reads the droplets from DigitalOcean and the steps from each droplet, and reports no changes.

Take app-3 out of local.servers in main.tf:

locals {
servers = toset(["app-1", "app-2"])
}

Plan and apply again. The same plan destroys the app-3 droplet and its host key, and removes its two steps. The authorized_key step defaults to on_remove = "delete", so Conn3x deletes the deploy key from app-3 before the droplet goes. See Removal for each module’s default.

Destroy everything the configuration created, and type yes:

Terminal window
conn3x destroy -var ssh_key_id=<your key ID or fingerprint>

This deletes the droplets, which stops the charges. Check the Droplets page in the DigitalOcean control panel to confirm none are left. destroy doesn’t remove the SSH key from your DigitalOcean account, because the configuration didn’t create it.

The state file holds each droplet’s private host key, so delete it as well:

Terminal window
rm -f terraform.tfstate terraform.tfstate.backup

For a configuration you keep, turn on state encryption or store state where you’d store a secret.