Resources·Tutorials

Provision a RareCloud VPS with Terraform

Use the official rarecloudio/rarecloud Terraform provider to declare cloud infrastructure as code: SSH keys, cloud VMs, a load balancer. Plan, apply, destroy, the same workflow you know from AWS or Hetzner.

By RareCloud Team · 9 min read · 20 May 2026 · Updated 9 Oct 2026

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:

  • plan and region are stable strings from the catalog. Run rarecloud catalog plans <product-sku> (CLI) or curl https://api.rarecloud.io/v1/catalog/products to see the full list.
  • tags are optional labels. Change them later and Terraform updates the VM in place; reorder them and nothing changes.
  • ssh_public_key takes the key material directly. Reading it from rarecloud_ssh_key.deploy.public_key keeps a single source of truth and lets Terraform manage the dependency for you.
  • member_server_ids references each server's id. rarecloud_server.api[*].id expands to the whole fleet, so the load balancer always tracks both workers.
  • count is the simplest form of multi-resource declaration. For more complex topologies use for_each with 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.

Frequently Asked Questions

Do I need Terraform Cloud?
No. Terraform OSS (the open-source binary) works against any backend, local state, S3, Postgres, whatever. We support every state backend Terraform itself supports.
What happens to my server if I rename it in Terraform?
Nothing on the server. The provider does not send a new name to the API, so the server keeps its old name and the next plan shows the difference again. Pick the name once. Changing region, image, ssh_public_key, root_password or vpc_id destroys and recreates the server; changing plan resizes it in place. Always check terraform plan output before applying.
Can I change a server's plan with Terraform?
Yes. Edit the plan attribute and apply, the provider issues an async resize (cold-migrate + reboot) via POST /v1/services/{id}/resize and the VM settles on the next status poll. No destroy/recreate. Always check terraform plan output before applying.

Related