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.
Prerequisites
Section titled “Prerequisites”- 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 asrootwith it. - Its ID or fingerprint, from Settings > Security in the DigitalOcean control panel, or from
doctl compute ssh-key list. git.
Get the example
Section titled “Get the example”The configuration is the digitalocean example in the Conn3x repository:
git clone --depth 1 --branch v0.6.3 https://github.com/conn3xorg/conn3xcd conn3x/examples/digitaloceanmain.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
tlsprovider, 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
digitaloceanprovider, one for each name inlocal.servers. - Builds the circuit’s
hostsfrom the droplets. Each host is a host object with the droplet’s address and its pinnedhost_key. Conn3x accepts only that key, so even the first connection to a new droplet is verified, and you don’t need aknown_hostsentry. - Configures each droplet with two steps: a
userand anauthorized_key.
timeout = "5m" keeps Conn3x retrying while a new droplet boots, so there’s no separate wait step.
Set your credentials
Section titled “Set your credentials”Export your token, so the digitalocean provider can read it:
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:
cp ~/.ssh/id_ed25519.pub deploy.pubInitialize
Section titled “Initialize”conn3x initThis 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:
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" }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:
conn3x output addressesLog 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.
Remove one droplet
Section titled “Remove one droplet”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 the droplets
Section titled “Destroy the droplets”Destroy everything the configuration created, and type yes:
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:
rm -f terraform.tfstate terraform.tfstate.backupFor a configuration you keep, turn on state encryption or store state where you’d store a secret.
Next steps
Section titled “Next steps”- Reach hosts through a bastion, for servers without a public address.
- Create and configure a server in one plan adds nginx and a firewall to a single droplet.
- Hosts covers building
hostsfrom resources.