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.
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" {}
| Argument | Environment | Description |
|---|---|---|
token | SCAMP_TOKEN | API token. Sensitive, never printed in a plan. |
api_url | SCAMP_API_URL | Base 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.
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.
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
| Family | Classes | Use |
|---|---|---|
| Basic | R1 R2 R3 R4 R5 | Root disks and general volumes, replicated. |
| Database | W1 W2 W3 W4 W5 | Write-heavy workloads, replicated. |
| Storage | S1 | Capacity at a lower price per GB. |
| Solo | E1 | Single 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.
| Argument | Type | Values | Description |
|---|---|---|---|
root_disk_gb | Required | 10 to 1000 | Root disk size, 10 to 1000 GB. |
vm_class | Optional | Any class name | Class by name, such as bs-burst-xs or hf-m. Use this or vm_class_id. |
image | Optional | Any image slug | Image by slug or display name, such as ubuntu-26.04. Use this or vm_template_id. |
root_disk_class | Optional | Any disk class | Storage class of the root disk, such as R1. |
display_name | Optional | Free text | Name in the panel, up to 100 characters. |
assign_public_ips | Optional | true, false. Default false | Give the machine public IPv4 and IPv6. Defaults to false. |
primary_network_id | Optional | Network UUID | Network the machine sits on. Left out, it joins the default network of the account. |
security_group_id | Optional | Security group UUID | Security group to join. Left out, the platform uses the default group. |
ssh_key_id | Optional | SSH key id, a number | SSH key to inject at build time. |
os_password | Optional | 8 to 64 characters | Password for the OS user, 8 to 64 characters. Generated when absent. Sensitive. |
tags, description | Optional | Free text | Kept 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.
| Argument | Type | Values | Description |
|---|---|---|---|
size_gb | Required | 1 to 10000, and never below the minimum of the class | Size in GB. Each class has its own minimum, and a smaller size is rejected on apply. |
storage_class | Optional | Any disk class | Class by name, such as R2 or W3. Use this or storage_class_id. |
attached_vm_id | Optional | Machine UUID | Machine to attach to. Left out, the disk is created and left free. |
display_name | Optional | Free text | Name 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.
| Argument | Type | Values | Description |
|---|---|---|---|
type | Required | private or public | private or public. |
router_uuid | Optional | Router UUID | Router to attach to. Required when the type is public. |
cidr | Optional | Any private range, such as 10.50.0.0/24 | Address range, such as 10.50.0.0/24. Generated when absent. |
name, description, tags | Optional | Name is 1 to 64 characters | Name 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.
| Argument | Type | Values | Description |
|---|---|---|---|
vm_id | Required | Machine UUID | The machine. |
network_id | Required | Network UUID | The network to join. |
is_primary | Optional | true, false. Default false | Move 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.
| Argument | Type | Values | Description |
|---|---|---|---|
attached_vm_id | Optional | Machine UUID | Machine the address points at. Left out, it is reserved and idle. |
display_name | Optional | Free text | Name in the panel. |
Exported: id, ip_address, ipv6_address, status, created_at.
scamp_router
The gateway a public network needs to reach the internet.
| Argument | Type | Values | Description |
|---|---|---|---|
name, description, tags | Optional | Name is 1 to 64 characters | Name 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.
| Argument | Type | Values | Description |
|---|---|---|---|
public_key | Optional | An OpenSSH public key line | Key in OpenSSH format. Use this or generate. |
generate | Optional | true, false. Default false | Generate an Ed25519 pair. Mutually exclusive with public_key. |
key_name | Optional | Up to 255 characters | Name 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 source | Looks up |
|---|---|
scamp_vm_class | One class by name, with cores, memory and price per hour. |
scamp_vm_classes | Every class available to the account. |
scamp_storage_class | One storage class by name, with its IOPS, bandwidth, replica count and size limits. |
scamp_storage_classes | Every storage class. |
scamp_vm_template | One image by OS type, such as Ubuntu or Alpine. |
scamp_vm_templates | Every image on offer. |
scamp_vm | An existing machine. |
scamp_volume | An existing disk. |
scamp_network | An existing network. |
scamp_router | An existing router. |
scamp_ssh_key | An 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,nameorkey_nameis accepted into state but never sent, so the panel keeps showing the old one. Same fordescriptionandtagson machines, networks and routers: those live in your state file only. - Flipping
assign_public_ipslater 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
- Full schema for every resource, generated from the provider itself, on the Terraform Registry.
- The same operations over HTTP in the API reference.
- Class names, disk classes and prices in the price list.