Simple VPS with Terraform
The scamp_vps resource of our provider makes and changes Simple VPS from a plan. It works through the same API as the Cloud Panel and scli.
Setup
The provider is on the Terraform Registry, so terraform init downloads it. Simple VPS needs version 1.5 or newer:
terraform {
required_providers {
scamp = {
source = "serverscamp/scamp"
version = "~> 1.5"
}
}
}Give it an API token from the Cloud Panel, Settings, API tokens. In CI keep it in your secrets:
export SCAMP_TOKEN=sc_your_token_hereThe provider also manages Cloud VMs, disks and networks; that is on the Terraform provider page.
A first server
Save your public key and make the server with it. terraform apply returns when the server is running, usually within half a minute.
resource "scamp_ssh_key" "laptop" {
key_name = "laptop"
public_key = file("~/.ssh/id_ed25519.pub")
}
resource "scamp_vps" "web" {
name = "web-1"
plan = "vps2-1"
image = "debian-13"
ssh_key_ids = [scamp_ssh_key.laptop.id]
backups = true
}
output "web_ipv4" {
value = scamp_vps.web.ipv4
}scamp_vps
Set ssh_key_ids, password or both, so that you can sign in.
| Argument | What it is |
|---|---|
name | Required. 3 to 24 lowercase letters, digits and hyphens, starting with a letter. Changing it replaces the server |
plan | Required. Plan id such as vps2-1. Changed in place to a bigger plan; a smaller one is refused because the disk cannot shrink |
image | Required. Image id such as debian-13. Changing it replaces the server |
ssh_key_ids | IDs of scamp_ssh_key keys. The keys go onto the server once, when it is made; changing the list replaces the server |
password | Root password, 8 to 128 characters. Sensitive. Changing it replaces the server |
backups | Daily backups, false by default. Turning them off deletes every backup of the server |
reverse_dns | Reverse DNS of the IPv4 and IPv6 addresses. Without it the default name is used |
| Attribute | What it is |
|---|---|
id | UUID of the server |
ipv4 | Public IPv4 address |
ipv6_prefix | The IPv6 /56 of the server |
hostname | The default name, NAME-xxxxxx.vm.scamp.pro |
status | running, stopped, paused, unpaid or error |
cpu_model | CPU model of the host |
created_at | When the server was made |
Plans and images
The value for plan is the id in the first column. Prices are a month; the server is billed by the hour from your balance.
| plan | vCPU | Memory | Disk | Traffic a month | Price |
|---|---|---|---|---|---|
vps2-075 | 1 | 768 MB | 10 GB | 500 GB | €1.00 |
vps2-1 | 1 | 1 GB | 20 GB | 1 TB | €1.50 |
vps2-2 | 2 | 2 GB | 40 GB | 2 TB | €3.00 |
vps2-4 | 2 | 4 GB | 80 GB | 3 TB | €6.00 |
vps2-8 | 4 | 8 GB | 160 GB | 4 TB | €12.00 |
vps2-16 | 8 | 16 GB | 320 GB | 5 TB | €24.00 |
vps2-32 | 8 | 32 GB | 640 GB | 6 TB | €48.00 |
The value for image:
| image | System |
|---|---|
ubuntu-26.04 | Ubuntu 26.04 LTS |
debian-13 | Debian 13 |
rocky-10 | Rocky Linux 10 |
almalinux-10 | AlmaLinux 10 |
Daily backups add a share of the plan price; the price list has it per plan.
Root password
A root password is optional when you give SSH keys, and needed when you do not. Keep it out of the code: pass it in a sensitive variable, so Terraform never prints it in a plan.
variable "root_password" {
type = string
sensitive = true
}
resource "scamp_vps" "db" {
name = "db-1"
plan = "vps2-2"
image = "debian-13"
password = var.root_password
}Set the variable from the environment, for example from your CI secrets. Give the same value every time: a new password is a change, and a change of the password replaces the server.
export TF_VAR_root_password="$ROOT_PASSWORD" # the same value on every run
terraform applyOr let Terraform make one with the random provider and read it later with terraform output -raw root_password:
resource "random_password" "root" {
length = 24
}
resource "scamp_vps" "db" {
# ...
password = random_password.root.result
}
output "root_password" {
value = random_password.root.result
sensitive = true
}The password is 8 to 128 characters. It ends up in the Terraform state like any value a resource gets, so keep the state somewhere only you can read: an encrypted backend, not a public repository. Changing the password replaces the server; to change it on a running server, use passwd on the server and leave the config as it is.
What a change does
plan, backups and reverse_dns change on the running server. A bigger plan restarts it, and the apply returns once it has started on the new plan; the system inside may need a few more seconds before SSH answers.
name, image, ssh_key_ids and password replace the server: the old one and its disk are deleted and a new one is made, with new addresses. The plan shows this as must be replaced before anything happens.
SSH keys go onto the server once, when it is made, as with other clouds. If editing a key should not replace the server, tell Terraform to leave the list alone:
resource "scamp_vps" "web" {
# ...
lifecycle {
ignore_changes = [ssh_key_ids]
}
}Before it makes a server, the provider checks that every key in ssh_key_ids is in your account and stops if one is not.
Plans in code
The same lists are in two data sources, for a config that picks a plan by its resources or shows the prices: scamp_vps_plans and scamp_vps_images.
data "scamp_vps_plans" "all" {}
data "scamp_vps_images" "all" {}
output "plans" {
value = { for p in data.scamp_vps_plans.all.items : p.id => "${p.vcpu} vCPU, ${p.ram_gb} GB, ${p.disk_gb} GB disk, EUR ${p.price_monthly}" }
}
output "images" {
value = [for i in data.scamp_vps_images.all.items : i.id]
}SSH after apply
The apply returns when the server is running, before the system has finished booting. To run commands on it from Terraform, give the connection a timeout; Terraform tries again until SSH answers:
resource "scamp_vps" "web" {
# ...
connection {
host = self.ipv4
user = "root"
private_key = file("~/.ssh/id_ed25519")
timeout = "5m"
}
provisioner "remote-exec" {
inline = ["apt-get update", "apt-get install -y nginx"]
}
}Import
A server made in the Cloud Panel or with scli comes under Terraform by its id, shown on the server's page and by scli vps ls:
terraform import scamp_vps.web 12d46f8c-d66e-4163-99aa-1245d5b06e5bWrite the config as the server is before the first apply. If it has daily backups, set backups = true: without it the apply turns them off and deletes every backup. The API never returns the password or the SSH keys, so after an import a value you set for them is only recorded and does not replace the server.