Cloud Architecture·8 min read·

Idempotent VPS Provisioning from a REST API

Wrap Netbay purchase calls in client-side idempotency so retries, CI jobs, and incident runbooks never double-buy a VPS when a request is replayed.

NB

Netbay Developer Relations

Netbay Engineering

On this page

Provisioning is the one API call people are afraid to retry. POST /api/v1/services/purchase creates a billable VPS in Lucknow DC01 in under 60 seconds. If the HTTP client times out, you do not know whether the server committed. Clicking again buys a second instance. CI that "helps" by rerunning the job buys a third. Idempotency is the property you add around that call so the second run is a no-op that returns the same instance you already have. Netbay's public surface is small and honest: account, plans, purchase. You own the replay log.

Treat purchase as a once-token, not a loop

A provisioner should take a key you choose (a hostname, a ticket id, a git SHA) and guarantee that key maps to at most one VPS. Store that map locally in git-ignored state or in your own database before you call purchase. On every run: if the key exists and the instance is still yours, exit 0. If the key is new, purchase, then record. If purchase succeeds but the record write fails, the next run must detect the orphan by listing what you already own, not by buying again.

Do not invent extra platform features. There is no managed cluster endpoint and no catalog of one-click apps to hide behind. The loop is: authenticate, list plans, purchase one plan, record the result. Power and status follow after you have a service id.

Replay-safe purchase CI / runbook key=web-prod local lock + map state/web-prod.json HIT: return existing id no second purchase MISS: POST purchase then write the map api.netbayhosts.in — X-API-Key + X-API-Secret Lucknow DC01 instance appears in under 60 seconds

A shell wrapper that will not double-buy

Keep credentials in the environment, never in the repo. The state file is the idempotency store. flock makes two overlapping CI jobs serialize instead of racing two purchases.

bash
#!/usr/bin/env bash
# provision-web-prod.sh — safe to run twice
set -euo pipefail
KEY=web-prod
STATE_DIR=/var/lib/netbay-provision
STATE=$STATE_DIR/$KEY.json
API=https://api.netbayhosts.in
install -d -m 0750 "$STATE_DIR"
exec 9>"$STATE_DIR/$KEY.lock"
flock 9

if [ -f "$STATE" ]; then
  echo "already provisioned: $(cat $STATE)"
  exit 0
fi

curl -fsS "$API/api/v1/account"   -H "X-API-Key: $API_KEY" -H "X-API-Secret: $API_SECRET" >/dev/null

PLAN_JSON=$(curl -fsS "$API/api/v1/plans"   -H "X-API-Key: $API_KEY" -H "X-API-Secret: $API_SECRET")
echo "$PLAN_JSON" | grep -q vps-4gb

RESP=$(curl -fsS -X POST "$API/api/v1/services/purchase"   -H "X-API-Key: $API_KEY" -H "X-API-Secret: $API_SECRET"   -H "Content-Type: application/json"   -d '{"plan":"vps-4gb","os":"ubuntu-24-04"}')
echo "$RESP" > "$STATE.tmp"
mv "$STATE.tmp" "$STATE"
echo "purchased: $(cat $STATE)"

The mv of a temp file is the commit. If curl fails, no state file appears and the next run retries. If curl succeeds and the process dies before mv, you may retry a purchase; that is the remaining gap. Close it by writing a "pending" marker with the key before curl, and on restart ask a human or an account listing before buying again. Never delete the state file to "try once more" unless you intend a second VPS.

Python for CI: explicit keys, explicit exits

Shell is fine for a runbook. CI usually wants a function that returns the same dict on every replay. Use string concatenation, not templates, and fail closed on missing env.

python
# provision.py
import json, os, sys, urllib.request

API = "https://api.netbayhosts.in"
KEY = os.environ["PROVISION_KEY"]
STATE = "/var/lib/netbay-provision/" + KEY + ".json"
HDR = {
    "X-API-Key": os.environ["API_KEY"],
    "X-API-Secret": os.environ["API_SECRET"],
    "Content-Type": "application/json",
}

def call(method, path, body=None):
    data = None if body is None else json.dumps(body).encode()
    req = urllib.request.Request(API + path, data=data, headers=HDR, method=method)
    with urllib.request.urlopen(req, timeout=60) as resp:
        return json.load(resp)

if os.path.exists(STATE):
    print(open(STATE).read())
    sys.exit(0)
call("GET", "/api/v1/account")
call("GET", "/api/v1/plans")
result = call("POST", "/api/v1/services/purchase",
              {"plan": "vps-4gb", "os": "ubuntu-24-04"})
tmp = STATE + ".tmp"
open(tmp, "w").write(json.dumps(result))
os.replace(tmp, STATE)
print(json.dumps(result))

Pay-as-you-go INR billing makes a double purchase painful in a very concrete way. Idempotency is cheaper than a support ticket. Point the new instance at Lucknow DC01 by construction — that is the only Netbay location — then configure it with the same systemd layers as the rest of this series. The API's job is to give you a VPS once. Your job is to not ask twice.

Timeouts, pending markers, and human gates

A 60 second HTTP timeout on purchase is not long enough if you retry blindly at 59. Write a pending marker with the key, the timestamp, and the plan name before the POST. If the process dies, the next run sees pending and stops: it prints the marker and asks a human, or it lists account services and matches hostname or create time before it buys. Automating that match is worth a few extra lines; automating a second purchase is not.

Keep API keys in a systemd EnvironmentFile or a CI secret store, never in the state JSON. The state file is not secret but it is unique; back it up with /etc/app. Rotate keys by writing new ones, running GET /api/v1/account as a probe, then deleting the old pair. A provisioner that 401s should fail, not fall through to a dashboard click that bypasses the map. CI should run the wrapper on every main-branch deploy of the infrastructure repo, including the runs where nothing should happen. Those no-op runs are the proof the lock works.

You can generate API keys in your Netbay account, run this wrapper from CI, and have an Ubuntu 24.04 VPS ready in under 60 seconds without a second invoice — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance