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 in RareCloud's European cloud region (our first, in Bucharest): an SSH key, two cloud VMs, and a load balancer fronting them. The rarecloud_server resource creates cloud VMs only; classic VPS are ordered from the console or the API.
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 console: Account, then API tokens, then New token. Give it services:read and services:write for the servers and the load balancer, plus account:read and account:write for the SSH key. Scopes match exactly, so :write does not include :read, and Terraform needs both to read its resources back. Copy the token (it is 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.3"
}
}
}
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 = "g-2vcpu-8gb"
region = "bucharest-ro"
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 <product-sku>(CLI) orcurl https://api.rarecloud.io/v1/catalog/productsto see the full list.tagsare optional labels. Change them later and Terraform updates the VM in place; reorder them and nothing changes.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.3"...
- Installing rarecloudio/rarecloud v0.4.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 = [
"203.0.113.12",
"203.0.113.18",
]
lb_ip = "203.0.113.30"
Two cloud VMs, an SSH key, a load balancer fronting them, in a few minutes. The apply waits until each server is active and the load balancer has finished building, so the addresses in the outputs are ready to use. If the API's answer to a create is lost on the way, the provider retries with the same idempotency key, so a flaky connection does not leave you with an extra server.
Reading drift
A week later someone deletes one of the servers in the console. terraform plan notices it is gone:
$ terraform plan
# rarecloud_server.api[1] will be created
+ resource "rarecloud_server" "api" {
+ name = "api-prod-2"
+ plan = "g-2vcpu-8gb"
+ region = "bucharest-ro"
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Two paths: run apply to put the server back, or accept the change by lowering count in main.tf. The provider does not read a server's plan back from the API, so a resize made in the console does not show up here: make plan changes in main.tf and let Terraform apply them.
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.