Declarative infrastructure beats clicking buttons. When you describe what you want, two servers behind a load balancer with health checks, Terraform figures out how to get there. And when something drifts (someone changes a setting in the dashboard), terraform plan tells you exactly what.
This tutorial provisions a small but realistic stack on RareCloud: an SSH key, two VPS servers, and a load balancer fronting them.
Install Terraform
If you don't have it yet:
macOS:
brew install terraform
Linux / Windows: download from terraform.io/downloads.
Confirm:
$ terraform -version
Terraform v1.9.0
Get an API token
In the dashboard: Account β API tokens β New token. Scope it to services:write. Copy it (shown once).
Export it for Terraform:
export RARECLOUD_TOKEN="rc_pat_..."
(Or put it in a .env file ignored by git, then source .env.)
Write the config
Create a directory for your infra and a file called main.tf:
terraform {
required_providers {
rarecloud = {
source = "rarecloudio/rarecloud"
version = "~> 0.2"
}
}
}
provider "rarecloud" {
# token read from RARECLOUD_TOKEN env var by default
}
# Upload your SSH key once, reference it everywhere.
resource "rarecloud_ssh_key" "deploy" {
name = "deploy-key"
public_key = file("~/.ssh/id_ed25519.pub")
}
# Two API workers.
resource "rarecloud_server" "api" {
count = 2
name = "api-prod-${count.index + 1}"
plan = "plus-vps"
region = "frankfurt-de"
image = "ubuntu-24.04"
ssh_public_key = rarecloud_ssh_key.deploy.public_key
tags = ["prod", "api"]
}
# Balance HTTPS across both workers.
resource "rarecloud_load_balancer" "api" {
name = "api-prod-lb"
port = 443
member_server_ids = rarecloud_server.api[*].id
health_check = true
}
# Outputs, useful when scripting downstream.
output "api_ips" {
value = rarecloud_server.api[*].ipv4
}
output "lb_ip" {
value = rarecloud_load_balancer.api.ipv4
}
A few things worth noting:
planandregionare stable strings from the catalog. Runrarecloud catalog plans(CLI) orcurl https://api.rarecloud.io/v1/catalog/productsto see the full list.ssh_public_keytakes the key material directly. Reading it fromrarecloud_ssh_key.deploy.public_keykeeps a single source of truth and lets Terraform manage the dependency for you.member_server_idsreferences each server'sid.rarecloud_server.api[*].idexpands to the whole fleet, so the load balancer always tracks both workers.countis the simplest form of multi-resource declaration. For more complex topologies usefor_eachwith a map.
Plan and apply
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding rarecloudio/rarecloud versions matching "~> 0.2"...
- Installing rarecloudio/rarecloud v0.2.0...
Terraform has been successfully initialized!
$ terraform plan
Plan: 4 to add, 0 to change, 0 to destroy.
$ terraform apply
...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
api_ips = [
"85.121.241.12",
"85.121.241.18",
]
lb_ip = "85.121.241.30"
Two servers, an SSH key, a load balancer fronting them. ~60 seconds end to end (server provisioning dominates).
Reading drift
A week later someone resizes one of the servers in the dashboard. terraform plan flags it:
$ terraform plan
# rarecloud_server.api[0] will be updated in-place
~ resource "rarecloud_server" "api" {
id = "srv_01H8E9..."
~ plan = "pro-vps" -> "plus-vps"
...
}
Plan: 0 to add, 1 to change, 0 to destroy.
Two paths: revert the dashboard change by running apply, or accept the drift by updating main.tf to match.
Tear it all down
$ terraform destroy
... Plan: 0 to add, 0 to change, 4 to destroy.
Destroy complete! Resources: 4 destroyed.
Use this for ephemeral environments, staging branches, load-test fleets, CI runners.
Where to go next
- Combine with the CLI for hybrid workflows: Terraform owns the steady-state resources, the CLI handles the ephemeral ones.
- Module library, extract repeating server + load balancer + firewall patterns into your own reusable Terraform modules.
- The provider and its full schema reference live on the Terraform Registry.