Terraform provider for the SpaceWeb (sweb.ru) hosting API.
Manage VPS instances declaratively. Built on the
Terraform Plugin Framework
over sweb-go-sdk.
terraform {
required_providers {
sweb = {
source = "sanchpet/sweb"
}
}
}
provider "sweb" {
login = var.sweb_login # or $SWEB_LOGIN
password = var.sweb_password # or $SWEB_PASSWORD
}
resource "sweb_vps" "infra_hub" {
cpu = 2
ram = 6
disk = 15
category = 1 # 1=nvme, 2=hdd, 3=turbo
distributive = 164 # debian-13
datacenter = 1 # 1=spb, 2=msk, 3=ams
alias = "infra-hub"
}SpaceWeb issues short-lived session tokens (no refresh-token flow). Two modes:
| Mode | Config | Env | Behaviour |
|---|---|---|---|
| Credentials (recommended) | login + password |
SWEB_LOGIN / SWEB_PASSWORD |
SDK re-exchanges for a fresh token transparently when the session expires |
| Token | token |
SWEB_TOKEN |
One-off; fails once the session expires |
endpoint (or SWEB_ENDPOINT) overrides the API root for staging/testing.
Provision either via the configurator (cpu + ram + disk [+ category])
or a ready-made plan id — the two are mutually exclusive. Common inputs:
distributive, datacenter, alias, optional ssh_key, ip_count. Computed:
billing_id (= resource id), uid, name, ip, running.
Attaches an existing VPS to the account private (local) network — declaratively,
no re-create (via addLocal/removeLocal on /vps/ip). SpaceWeb assigns the
local IP; the guest OS still needs the private NIC configured with it.
resource "sweb_vps_local_network" "infra_01" {
billing_id = "login_vps_10"
}
# → .local_ip / .mask / .mac (computed)Resolves a configurator spec to a plan id, so HCL reads by resources instead of a magic number — and an imported plan-mode node stays clean (the data source re-derives the same id, no mode switch, no resize):
data "sweb_plan" "infra" {
cpu = 2
ram = 6 # GB
disk = 15 # GB
category = 1 # 1=NVMe (default), 2=HDD, 3=Turbo
}
resource "sweb_vps" "infra_hub" {
alias = "infra-hub"
plan = data.sweb_plan.infra.id
distributive = 164
datacenter = 1
}It calls the same getConstructorPlanId resolver as the resource. The id is
resolved dynamically each plan, so a catalog remap on SpaceWeb's side could
change it — pin a literal plan if you need a frozen id.
For a domain already on the account (registration itself is deliberately out
of the provider — a paid, irreversible purchase; terraform destroy must never
cancel a domain), the provider manages the parts that map cleanly onto Terraform:
resource "sweb_subdomain" "shop" {
domain = "example.com"
machine = "shop" # -> shop.example.com; destroy removes it
}
resource "sweb_domain_redirect" "example" {
domain = "example.com"
url = "https://example.org" # destroy clears the redirect
}
data "sweb_domain" "example" {
domain = "example.com" # expiry, registrar, autoprolong, docroot, redirect
}DNS records (A, AAAA, CNAME, MX, TXT, NS) are managed per-record with
sweb_dns_record — bring an existing zone under Terraform and edit it there:
resource "sweb_dns_record" "www" {
domain = "example.com"
type = "A"
name = "www" # empty or "@" for the apex
value = "203.0.113.10"
}
resource "sweb_dns_record" "mail" {
domain = "example.com"
type = "MX"
value = "mx1.example.com."
priority = 10
}The SpaceWeb API addresses records by a per-type index that shifts as the zone changes, so the record is identified by its content (type + host + value) and the index is re-derived on each read/delete; every attribute forces replacement, so a value change is a delete+create.
SRV records have their own resource, sweb_dns_srv_record, because of their
distinct shape (service/protocol/target/port/weight):
resource "sweb_dns_srv_record" "autodiscover" {
domain = "example.com"
service = "autodiscover"
protocol = "tcp"
target = "autodiscover.example.com."
port = 443
priority = 5
}Mailboxes on a mail domain are managed with sweb_mailbox:
resource "sweb_mailbox" "info" {
domain = "example.com"
name = "info" # -> info@example.com
password = var.mailbox_password
antispam = "medium" # hard | medium | soft | off
spf = true
comment = "shared inbox"
}Like a DNS record, a mailbox has no stable server id: it is identified by its
content — the mail domain plus the local-part name — so those two force
replacement, while password, antispam, spf and comment update in place.
quota is read-only (the API assigns it and exposes no create/update control).
Three more content-addressed resources cover the shared-hosting surface — a MySQL database, a website, and a crontab entry:
resource "sweb_database" "app" {
name = "appdb"
password = var.database_password
comment = "application database"
}
resource "sweb_site" "shop" {
alias = "shop"
doc_root = "shop"
domain = "example.com"
}
resource "sweb_cron_task" "nightly" {
minute = 30
hour = 3
day = 1
month = 12
weekday = 7
command = "/usr/bin/php /home/example/backup.php"
}Each has no stable server id, so identity is its content: the database name
(the API may store it account-prefixed, exposed as full_name), the site
doc_root, the cron entry's raw crontab line. A sweb_database updates
password/comment in place; a sweb_site renames its alias in place; a
sweb_cron_task is fully replace-on-change (the API edits a task by replacing its
whole line). Cron positions are integers only — "*", ranges and steps aren't
expressible through the API.
A free Let's Encrypt certificate is managed with sweb_letsencrypt:
resource "sweb_letsencrypt" "site" {
domain = "example.com"
autoprolong = true
}It is identified by the domain it covers; the issuance inputs
(wildcard/virtdom/ip/challenge) force replacement, while autoprolong
toggles in place. Issuance is asynchronous, so apply waits for the certificate
to appear (bounded by the create timeout, default 10m).
Beyond VPS, the cloud panel exposes four more resources:
sweb_balancer— a cloud load balancer with nestedserver/ruleblocks. Ordering bills; provisioning is asynchronous (waits until idle).datacenterandplan_idforce replacement; the algorithm, toggles and backends update in place. Identified by its billing id.sweb_dbaas_instance— a managed-database cluster with nesteduserblocks. Ordering bills and is asynchronous;engine_type/engine_versionforce replacement,plan_id/display_name/users update in place. Identified by its billing id.sweb_monitoring_check— an uptime check (typeforces replacement; target, interval, contacts and options update in place;enabledtoggles activation).sweb_monitoring_contact— a notification contact (email/phone/telegram).
resource "sweb_monitoring_contact" "ops" {
type = "email"
value = "ops@example.com"
name = "Ops"
}
resource "sweb_monitoring_check" "site" {
type = "http"
target = "https://example.com"
name = "example.com"
interval = 60
contact_ids = [sweb_monitoring_contact.ops.id]
}Like VPS, sweb_balancer and sweb_dbaas_instance correlate the newly ordered
object via a List-diff (the create call returns no usable id) and serialize
creates behind a mutex — so apply never orphans or mis-attributes a billed
resource.
terraform import sweb_vps.infra_hub petrovpet2_vps_10The id is the billing_id (login_vps_N) shown by sweb vps list. Import
reconstructs a plan-mode config (the resolved plan id is always available
from the API). Notes:
- Switching the imported resource to the configurator (
cpu/ram/disk) is fine — those update in place (resize), so it does not force a replace. ssh_keyis create-only and not recoverable from the API; re-state it in HCL.- Use
terraform plan -generate-config-out=...to materialise matching HCL.
Importing a whole DNS zone at once. Rather than hand-write an import {} block
per record, pipe a zone dump through the bundled tf-dns-import helper — it emits
correct, unique ids for every record (round-robin, DKIM TXT, apex, SRV):
export TF_VAR_sweb_token="$(sweb token --profile hosting)" # the account that owns the domain
sweb dns records example.com -o json | tf-dns-import example.com > imports.tf
terraform plan -generate-config-out=generated.tfSee the Importing an existing DNS zone guide for the full walkthrough, including the multi-account credential gotcha.
- In-place:
alias(rename) andplan/cpu/ram/disk(resize viachangePlan) update without a replacement. The resize is asynchronous — the provider waits until it settles (Modify → ExtIpAdd → …). - Disk grows only: the API refuses shrinking a disk; the provider rejects a disk decrease at apply with a clear error.
- Forces replacement:
category(storage tier),distributive(OS),datacenter,ssh_keyandip_count. - 24h delete lock: a freshly created VPS cannot be destroyed for 24h; the provider surfaces a clear error and keeps the resource in state.
mise install # Go + golangci-lint + terraform + pre-commit (pinned)
mise run build # go build ./...
mise run test # unit tests
mise run testacc # mock-acceptance: full TF lifecycle vs an httptest backend
mise run lint
pre-commit install && pre-commit run -aAcceptance tests run against an in-memory mock of the SpaceWeb API — they never touch the real service (which bills and locks deletes for 24h).
MIT © Aleksandr Petrov