ResourcesΒ·Tutorials

Provision a RareCloud VPS with Terraform

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

By RareCloud Team Β· 9 min read Β· 5/20/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 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:

  • plan and region are stable strings from the catalog. Run rarecloud catalog plans (CLI) or curl https://api.rarecloud.io/v1/catalog/products to see the full list.
  • 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.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.

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?
Renaming is a non-destructive in-place update, no recreate. Changing the immutable fields (region, image, or ssh_public_key) is what triggers a destroy + recreate, while changing plan does an in-place resize. 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