Terraform provider

Write your servers, disks and networks in a file and let Terraform build them. The provider talks to the same public API as the panel and the CLI, with the same sc_ token.

Setup

The provider is published on the public registry, so Terraform downloads it for you. Pin the major version and let patch releases through.

versions.tf
terraform {
  required_providers {
    scamp = {
      source  = "serverscamp/scamp"
      version = "~> 1.4"
    }
  }
}

Authentication

Create a token in the Cloud Panel under API tokens. It is shown once, starts with sc_, and carries the rights of the account that made it.

Keep it out of your files. With the environment variable set, the provider block stays empty. If you set both, the environment variable wins.

export SCAMP_TOKEN=sc_your_token_here

# main.tf
provider "scamp" {}
ArgumentEnvironmentDescription
tokenSCAMP_TOKENAPI token. Sensitive, never printed in a plan.
api_urlSCAMP_API_URLBase API URL. Defaults to https://platform.serverscamp.com/api/v1.

Your first server

Four lines of required configuration. The machine joins the default network of your account, and the platform generates a password if you do not set one.

main.tf
resource "scamp_vm" "web" {
  display_name    = "web-01"
  vm_class        = "bs-burst-xs"
  image           = "ubuntu-26.04"
  root_disk_class = "R1"
  root_disk_gb    = 25

  assign_public_ips = true
}

output "address" {
  value = scamp_vm.web.public_ip_v4
}
terraform init
terraform plan
terraform apply

Classes and images are taken by name, and a unique substring is enough: ubuntu resolves to the current Ubuntu image. Names live in the panel, or in the price list.

A fuller example

A server with a second network interface, an extra disk and a floating address. This is the configuration we run our own provider tests against.

main.tf
resource "scamp_network" "internal" {
  name = "app-internal"
  type = "private"
  cidr = "10.77.20.0/24"
}

resource "scamp_vm" "app" {
  display_name    = "app-01"
  vm_class        = "bs-burst-xs"
  image           = "ubuntu-26.04"
  root_disk_class = "R1"
  root_disk_gb    = 25

  assign_public_ips = true
  # primary_network_id omitted, so the VM joins default-network
}

# A second interface on the private network.
resource "scamp_vm_network_attachment" "app_internal" {
  vm_id      = scamp_vm.app.id
  network_id = scamp_network.internal.id
}

# An extra disk, attached at creation.
resource "scamp_volume" "data" {
  display_name   = "app-data"
  size_gb        = 50
  storage_class  = "R2"
  attached_vm_id = scamp_vm.app.id
}

# A public address that survives the machine it points at.
resource "scamp_floating_ip" "app" {
  display_name   = "app-public"
  attached_vm_id = scamp_vm.app.id
}

output "app" {
  value = {
    public_ip   = scamp_vm.app.public_ip_v4
    private_ip  = scamp_vm.app.ip_internal
    second_nic  = scamp_vm_network_attachment.app_internal.ip_internal
    floating_ip = scamp_floating_ip.app.ip_address
    volume_iops = scamp_volume.data.read_iops_limit
  }
}

Names you can use

Classes and images are written by name, not by number. Matching ignores case and accepts any unique substring, so ubuntu 26 and UBUNTU-26.04 both land on the same image. An ambiguous or unknown name fails the apply and prints the candidates.

VM classes

bs-xs   bs-s   bs-m   bs-l   bs-xl   bs-2xl   bs-4xl
bs-burst-xs   bs-burst-s   bs-burst-m   bs-burst-l   bs-burst-xl   bs-burst-2xl   bs-burst-4xl
hf-xxs   hf-xs   hf-s   hf-m   hf-l   hf-xl   hf-2xl
hf-burst-xxs   hf-burst-xs   hf-burst-s   hf-burst-m   hf-burst-l   hf-burst-xl   hf-burst-2xl

Cores, memory and the price of each are in the price list, and the scamp_vm_classes data source returns the same catalogue at plan time.

Disk classes

FamilyClassesUse
BasicR1 R2 R3 R4 R5Root disks and general volumes, replicated.
DatabaseW1 W2 W3 W4 W5Write-heavy workloads, replicated.
StorageS1Capacity at a lower price per GB.
SoloE1Single copy, no replica. Cheapest, and the one to avoid for anything you cannot lose.

Every class has its own minimum volume size, and a smaller size_gb is refused when the apply reaches the platform, not while planning. The scamp_storage_class data source reports that minimum along with the IOPS and bandwidth limits.

Images

ubuntu-26.04   ubuntu-24.04   debian-13   alpine-3.23
rocky-10   centos-stream-10   centos-stream-9

These three lists are what the platform offered when this page was written. The live answer is always https://platform.serverscamp.com/api/v1/vm-classes, /storage-classes and /vm-templates, or the matching data sources.

Resources

scamp_vm

A virtual machine. Only root_disk_gb is required, but in practice you set the class and the image as well.

ArgumentTypeValuesDescription
root_disk_gbRequired10 to 1000Root disk size, 10 to 1000 GB.
vm_classOptionalAny class nameClass by name, such as bs-burst-xs or hf-m. Use this or vm_class_id.
imageOptionalAny image slugImage by slug or display name, such as ubuntu-26.04. Use this or vm_template_id.
root_disk_classOptionalAny disk classStorage class of the root disk, such as R1.
display_nameOptionalFree textName in the panel, up to 100 characters.
assign_public_ipsOptionaltrue, false. Default falseGive the machine public IPv4 and IPv6. Defaults to false.
primary_network_idOptionalNetwork UUIDNetwork the machine sits on. Left out, it joins the default network of the account.
security_group_idOptionalSecurity group UUIDSecurity group to join. Left out, the platform uses the default group.
ssh_key_idOptionalSSH key id, a numberSSH key to inject at build time.
os_passwordOptional8 to 64 charactersPassword for the OS user, 8 to 64 characters. Generated when absent. Sensitive.
tags, descriptionOptionalFree textKept in state only, not sent to the platform.

Exported: id, public_ip_v4, public_ip_v6, ip_internal, ipv6_address, os_user, cpu_cores, memory_mb, vm_name, state, status, created_at.

scamp_volume

A block device you can attach to a machine.

ArgumentTypeValuesDescription
size_gbRequired1 to 10000, and never below the minimum of the classSize in GB. Each class has its own minimum, and a smaller size is rejected on apply.
storage_classOptionalAny disk classClass by name, such as R2 or W3. Use this or storage_class_id.
attached_vm_idOptionalMachine UUIDMachine to attach to. Left out, the disk is created and left free.
display_nameOptionalFree textName in the panel.

Exported: id, state, sds_pool_name, read_iops_limit, write_iops_limit, read_bandwidth_limit, write_bandwidth_limit, created_at.

scamp_network

A network inside your account. Private networks are isolated, public networks hang off a router.

ArgumentTypeValuesDescription
typeRequiredprivate or publicprivate or public.
router_uuidOptionalRouter UUIDRouter to attach to. Required when the type is public.
cidrOptionalAny private range, such as 10.50.0.0/24Address range, such as 10.50.0.0/24. Generated when absent.
name, description, tagsOptionalName is 1 to 64 charactersName is 1 to 64 characters and is generated when absent.

Exported: id, status, created_at.

scamp_vm_network_attachment

A second interface. It puts an existing machine on another network.

ArgumentTypeValuesDescription
vm_idRequiredMachine UUIDThe machine.
network_idRequiredNetwork UUIDThe network to join.
is_primaryOptionaltrue, false. Default falseMove the default route to this interface. Leave it alone unless that is what you mean.

Exported: id, ip_internal, mac_address.

scamp_floating_ip

A public address that belongs to the account rather than to one machine, so you can move it during a release.

ArgumentTypeValuesDescription
attached_vm_idOptionalMachine UUIDMachine the address points at. Left out, it is reserved and idle.
display_nameOptionalFree textName in the panel.

Exported: id, ip_address, ipv6_address, status, created_at.

scamp_router

The gateway a public network needs to reach the internet.

ArgumentTypeValuesDescription
name, description, tagsOptionalName is 1 to 64 charactersName is 1 to 64 characters and is generated when absent.

Exported: id, ipv4_address, ipv6_address, status, created_at.

scamp_ssh_key

Either upload a public key or have one generated for you.

ArgumentTypeValuesDescription
public_keyOptionalAn OpenSSH public key lineKey in OpenSSH format. Use this or generate.
generateOptionaltrue, false. Default falseGenerate an Ed25519 pair. Mutually exclusive with public_key.
key_nameOptionalUp to 255 charactersName in the panel, generated when absent.

Exported: id, which is a number here and not a UUID, plus fingerprint, key_type, created_at, and private_key for generated keys.

A generated private key is handed back once, at creation, and lands in your state file. We never keep a copy, so if you lose it the pair is gone and you make a new one.

Data sources

Look things up instead of hardcoding identifiers.

Data sourceLooks up
scamp_vm_classOne class by name, with cores, memory and price per hour.
scamp_vm_classesEvery class available to the account.
scamp_storage_classOne storage class by name, with its IOPS, bandwidth, replica count and size limits.
scamp_storage_classesEvery storage class.
scamp_vm_templateOne image by OS type, such as Ubuntu or Alpine.
scamp_vm_templatesEvery image on offer.
scamp_vmAn existing machine.
scamp_volumeAn existing disk.
scamp_networkAn existing network.
scamp_routerAn existing router.
scamp_ssh_keyAn existing SSH key.
data "scamp_storage_class" "fast" {
  name = "R3"
}

resource "scamp_volume" "data" {
  size_gb          = 100
  storage_class_id = data.scamp_storage_class.fast.id
}

output "iops" {
  value = data.scamp_storage_class.fast.write_iops_limit
}

Recipes

A fleet from one block

Three machines that differ only in name and size. for_each keeps their addresses stable in state, so adding a fourth later does not disturb the other three.

locals {
  nodes = {
    web-01 = "bs-burst-s"
    web-02 = "bs-burst-s"
    db-01  = "hf-m"
  }
}

resource "scamp_vm" "node" {
  for_each = local.nodes

  display_name    = each.key
  vm_class        = each.value
  image           = "debian-13"
  root_disk_class = "R1"
  root_disk_gb    = 25

  assign_public_ips = true
}

output "addresses" {
  value = { for k, vm in scamp_vm.node : k => vm.public_ip_v4 }
}

A generated SSH key

The platform makes the pair and hands the private half back once. Read it straight after the apply, because nothing stores it afterwards.

resource "scamp_ssh_key" "deploy" {
  key_name = "deploy"
  generate = true
}

resource "scamp_vm" "web" {
  display_name    = "web-01"
  vm_class        = "bs-burst-xs"
  image           = "ubuntu-26.04"
  root_disk_class = "R1"
  root_disk_gb    = 25
  ssh_key_id      = scamp_ssh_key.deploy.id

  assign_public_ips = true
}

output "private_key" {
  value     = scamp_ssh_key.deploy.private_key
  sensitive = true
}

output "address" {
  value = scamp_vm.web.public_ip_v4
}
terraform output -raw private_key > ~/.ssh/deploy
chmod 600 ~/.ssh/deploy
ssh -i ~/.ssh/deploy $(terraform output -raw address)

To use a key you already have, drop generate and pass the public half instead: public_key = file("~/.ssh/id_ed25519.pub").

An address that survives a release

Changing the machine a floating address points at is one of the few things updated in place. Build the new machine, move the address, drop the old one, and the address itself never changes.

variable "live" {
  description = "Which machine serves traffic, blue or green."
  type        = string
  default     = "blue"
}

resource "scamp_vm" "blue" {
  display_name    = "app-blue"
  vm_class        = "bs-burst-s"
  image           = "ubuntu-26.04"
  root_disk_class = "R1"
  root_disk_gb    = 25
}

resource "scamp_vm" "green" {
  display_name    = "app-green"
  vm_class        = "bs-burst-s"
  image           = "ubuntu-26.04"
  root_disk_class = "R1"
  root_disk_gb    = 25
}

resource "scamp_floating_ip" "app" {
  display_name   = "app-live"
  attached_vm_id = var.live == "blue" ? scamp_vm.blue.id : scamp_vm.green.id
}

terraform apply -var live=green moves the address across. The plan shows an update, not a replacement.

Data that outlives the machine

A machine is replaced on almost any change, so keep anything you care about on its own volume. The volume is a separate resource, and only its attachment follows the new machine.

resource "scamp_vm" "db" {
  display_name    = "db-01"
  vm_class        = "hf-m"
  image           = "debian-13"
  root_disk_class = "R1"
  root_disk_gb    = 25
}

resource "scamp_volume" "pgdata" {
  display_name   = "pgdata"
  size_gb        = 100
  storage_class  = "W3"
  attached_vm_id = scamp_vm.db.id

  lifecycle {
    prevent_destroy = true
  }
}

prevent_destroy makes Terraform refuse any plan that would delete the disk. The disk cannot be resized from here either, so pick the size with room to spare.

Checking the price before you build

The catalogue is readable at plan time, so a configuration can say what it is about to spend.

data "scamp_vm_class" "picked" {
  name = "hf-m"
}

data "scamp_storage_class" "root" {
  name = "R1"
}

output "cost_per_month_eur" {
  value = format("%.2f", (
    data.scamp_vm_class.picked.price_per_hour_millicents +
    data.scamp_storage_class.root.price_per_gb_hour_millicents * 25
  ) * 730 / 10000)
}

Prices come back in millicents per hour, where 10000 is one euro. The catalogue lookups are exact and case sensitive, unlike the friendly matching used by vm_class and image on the resources themselves.

Two machines on a private network

Only one of them faces the internet. The other is reachable over the private network and through it alone.

resource "scamp_network" "back" {
  name = "back"
  type = "private"
  cidr = "10.90.0.0/24"
}

resource "scamp_vm" "front" {
  display_name      = "front-01"
  vm_class          = "bs-burst-xs"
  image             = "ubuntu-26.04"
  root_disk_class   = "R1"
  root_disk_gb      = 25
  assign_public_ips = true
}

resource "scamp_vm" "worker" {
  display_name    = "worker-01"
  vm_class        = "bs-burst-s"
  image           = "ubuntu-26.04"
  root_disk_class = "R1"
  root_disk_gb    = 25
  # no public addresses at all
}

resource "scamp_vm_network_attachment" "front_back" {
  vm_id      = scamp_vm.front.id
  network_id = scamp_network.back.id
}

resource "scamp_vm_network_attachment" "worker_back" {
  vm_id      = scamp_vm.worker.id
  network_id = scamp_network.back.id
}

output "worker_private_ip" {
  value = scamp_vm_network_attachment.worker_back.ip_internal
}

Both machines still sit on the account default network as their first interface. Traffic between them inside the region is not charged.

Importing what already exists

Machines built in the panel can be adopted into a configuration. The identifier is the UUID shown in the panel or returned by the API.

terraform import scamp_vm.web 75279184-491e-44cb-af17-838ff438a8f3
terraform import scamp_volume.data 687cb4d5-4862-4fac-a33c-35c24fb39d6b
terraform import scamp_ssh_key.main 42

# An interface takes two ids, the machine and the interface itself.
terraform import scamp_vm_network_attachment.internal <vm-uuid>/<interface-id>

After importing a machine, the next plan is empty: the provider translates the numeric class and template back into the names you would have written, and works out whether public addresses were asked for.

What to expect

Worth knowing before you write the first plan.

  • A machine is never changed in place. Class, image, disk size, network, SSH key, security group: change any of them and Terraform destroys the machine and builds a new one. Read the plan before you apply it, and keep data you care about on a separate volume.
  • Disks do not grow through Terraform. Size and class are fixed at creation, and changing either replaces the volume. Only the machine it is attached to can be changed in place. The panel can extend a disk without replacing it, and the provider will follow later.
  • Names are set once. A changed display_name, name or key_name is accepted into state but never sent, so the panel keeps showing the old one. Same for description and tags on machines, networks and routers: those live in your state file only.
  • Flipping assign_public_ips later does nothing. It is read at creation. Changing it afterwards updates the state and leaves the machine as it was.
  • Creation is asynchronous, and the provider waits. An apply returns when the machine is really running and its addresses are known, not when the request was accepted. Budget a few minutes per machine, and up to ten for a destroy.
  • The first hour is charged at creation. An apply followed straight away by a destroy still costs that hour, which adds up while you are getting a file right.
  • State holds secrets. Generated OS passwords and generated private keys live in the state file. Keep it in a backend you trust.

What the provider covers

Seven resources and eleven data sources, all listed above: machines, volumes, networks, interfaces, routers, floating addresses and SSH keys.

Everything else is managed from the panel or the API for now. That includes security groups and their rules, which Terraform can point a machine at by UUID but cannot create, along with snapshots, backups, load balancers, object storage and managed databases.

Next