Skip to content

Provision a cloud Resource

The other tutorials work with tenancy — Domains, Projects, identities — and one works with the mesh. This one works with provisioning: the broker that turns a declared Resource into a real, enrolled member of a Domain's mesh. You will hand the platform a single resource create and then watch it do everything else — mint a bootstrap token, stand up the substrate, enrol the node, and report back — until the Resource reaches its terminal Ready phase.

The lean local stack has no cloud account, so this lesson uses a cloudless blueprint: a dev-only blueprint whose substrate is rendered entirely in-cluster. The flow you drive is identical to a real cloud provisioning — only the blueprint differs.

This lesson is played by two people, because provisioning deliberately splits two concerns:

  • the platform operator curates the shared catalog — the Clouds, Credentials, and Blueprints the platform can provision against — and decides whose credential pays;
  • the project owner consumes the catalog — creates a Project, requests a credential for it, and declares the Resource.

You will:

  • as the platform operator, read the seeded Cloud, find its CloudCredential, and read a blueprint's version id,
  • as the project owner, create a Project and request a credential assignment for it,
  • as the platform operator, approve that assignment — the two-party governance that keeps "who may provision" and "whose credential pays" separate,
  • as the project owner, resource create a provisioned Resource and get back 202 + a Location, and
  • poll the Resource through Pending → Provisioning → Enrolling → Ready.

By the end you will understand the credential-assignment governance, the shape of a provisioned Resource, and the phases the broker drives it through to make it real.

This lesson takes about fifteen minutes — most of it spent watching the broker work.

Before you start

This lesson is standalone: it does not build on the explore, build, or Group lessons. You only need a running, logged-in stack from Set up your local plexsphere, plus jq on your $PATH to read fields out of the JSON responses.

This lesson uses the two seeded human identities from the tutorials overview; you act as each in turn:

IdentityEmailProfileRole in this lesson
Platform operatoroperator@example.comoperatorreads the catalog and approves the assignment
Project owneradmin@example.comdefaultcreates the Project, requests the assignment, and provisions

Both fixture accounts sign in with the dev password password. The split is real: operator owns the seeded Credential and may read the platform catalog, while admin owns Projects but not the platform catalog — so each command below is run by the identity that is actually allowed to run it, and a principal may never approve its own request.

You switch between these two identities several times. The project owner is the identity you already use everywhere else, so it stays on your default profile — the one Set up your local plexsphere logged you into. The platform operator is a second identity: sign it into its own named operator profile once, and select it with --profile operator on just the operator's commands. The owner's commands carry no flag and resolve to the default profile as usual. That is two device-code sign-ins for the whole lesson, with no re-login when you switch back and forth.

Recreate the shell environment and read the Acme Corp Domain id and its active IdP binding with make dev-ids — you will switch identities several times, so it helps to have both ids in hand:

bash
export PATH="$PWD/bin:$PATH"
export PLEXSPHERE_URL=http://localhost:8080

eval "$(make -s dev-ids)"
echo "domain=$DOMAIN_ID binding=$BINDING_ID"

The demo Cloud's id is deterministic:

bash
CLOUD_ID=019100c0-0d00-7000-8000-000000000001
CLOUDLESS_BLUEPRINT=01910051-0051-7000-8051-000000000006

Step 1 — As the platform operator, inspect the catalog

Sign in as the platform operator and save the session under the operator profile — they curate the catalog, so they are the identity allowed to read it. Run the login below, then complete sign-in in the browser as operator@example.com with the password password:

bash
plexctl login --profile-name operator --domain-id "$DOMAIN_ID" --idp-binding-id "$BINDING_ID"

A Cloud is a provider account plexsphere can provision against; a CloudCredential is the (vaulted) secret that pays for it. Read the seeded Cloud:

bash
plexctl cloud get "$CLOUD_ID" --profile operator --output json | jq '{slug, display_name, provider}'
json
{
  "slug": "demo-cloud",
  "display_name": "Demo Cloud",
  "provider": "aws"
}

The CloudCredential's id is minted at boot, so find it by its seeded display name demo-credential:

bash
CREDENTIAL_ID=$(plexctl cloud credential list --cloud-id "$CLOUD_ID" --profile operator --output json \
  | jq -r '.items[] | select(.display_name == "demo-credential") | .id')
echo "$CREDENTIAL_ID"
text
019ed553-bf6c-7289-9dcb-4fb9917409b9

The list returns metadata only — the credential material lives in the secrets engine and never crosses this surface.

A Blueprint is a reusable recipe; a BlueprintVersion is a concrete, immutable revision of it. Read the cloudless blueprint and inspect the served version's parameter schema:

bash
plexctl blueprint get "$CLOUDLESS_BLUEPRINT" --profile operator --output json \
  | jq '{slug, version: .versions[0].version, parameter_schema: .versions[0].parameter_schema}'
json
{
  "slug": "kubernetes-cloudless-node",
  "version": "v1alpha1",
  "parameter_schema": []
}

The cloudless blueprint declares no parameters — the node it stands up needs no operator input — so the create call later passes an empty object.

resource create takes the BlueprintVersion's id, the broker's internal handle for that revision. That id is not surfaced on the read API, so read it straight from the database:

bash
VERSION_ID=$(kubectl exec statefulset/postgres -- \
  env PGPASSWORD=plexsphere psql -U plexsphere -d plexsphere -tAc \
  "SELECT id FROM plexsphere.blueprint_versions WHERE blueprint_id='$CLOUDLESS_BLUEPRINT'")
echo "$VERSION_ID"
text
019ed6a1-8638-779f-9c18-4910165c1e1a

You now hold the two ids the project owner needs: $CREDENTIAL_ID and $VERSION_ID. They are shell variables, so they survive the identity switches ahead.

Step 2 — As the project owner, create a Project

Switch to the project owner — your default profile, the same identity every other lesson uses. Sign in a second time to refresh it, completing sign-in in the browser as admin@example.com with the password password:

bash
plexctl login --domain-id "$DOMAIN_ID" --idp-binding-id "$BINDING_ID"

Create a Project under Acme Corp to hold your provisioned fleet. The owner's commands carry no --profile flag — they resolve to the default profile as usual:

bash
PROJECT_ID=$(plexctl project create \
  --domain "$DOMAIN_ID" \
  --slug provision-demo \
  --display-name "Provision demo" \
  --output json | jq -r '.id')
echo "$PROJECT_ID"
text
019ed555-b826-739d-a97f-e158acfa4bea

Give the grant a moment to land. Your admin access to a new Project reaches the authorization mirror a moment after the Project is created. If the next command returns Permission Denied, wait a second and re-run it.

Step 3 — Request a credential assignment

Before the broker may spend a CloudCredential on your Project, the Project must be assigned that credential — and the assignment is governed by two parties. As the Project owner, you request it, naming the credential id the operator handed you:

bash
ASSIGNMENT_ID=$(plexctl credential assignment request \
  --project-id "$PROJECT_ID" \
  --cloud-credential-id "$CREDENTIAL_ID" \
  --output json | jq -r '.id')
echo "$ASSIGNMENT_ID"

The assignment lands in the requested state. You cannot approve your own request — the platform deliberately splits the two halves.

Step 4 — As the platform operator, approve the assignment

Approval is the credential owner's call, not the requester's. The seeded operator@example.com owns the demo credential, so switch back to that identity with --profile operator — no second sign-in, the profile you saved in Step 1 still holds the operator's token.

First, list the Project's credential assignments and pick out the ones still awaiting a decision. The operator may read them as a domain auditor on Acme Corp, even though it does not own the Project:

bash
plexctl credential assignment list --project-id "$PROJECT_ID" --profile operator --output json \
  | jq '.items[] | select(.state == "requested") | {id, state, cloud_credential_id}'
json
{
  "id": "019ed556-9f1c-7d83-a0b4-6e2c1f7a4d92",
  "state": "requested",
  "cloud_credential_id": "019ed553-bf6c-7289-9dcb-4fb9917409b9"
}

That id is the $ASSIGNMENT_ID requested in Step 3, sitting in the requested state with no live binding yet. Approve it:

bash
plexctl credential assignment approve "$ASSIGNMENT_ID" --profile operator --output json \
  | jq '{state, materialised}'
json
{
  "state": "approved",
  "materialised": true
}

materialised: true means the broker now has a usable credential handle for this Project. The separation of duties you just exercised — owner requests, credential holder approves, and never the same principal — is enforced server-side, even on this single-tenant dev stack.

Step 5 — As the project owner, create the provisioned Resource

Switch back to the project owner — drop the --profile flag and your commands resolve to the default profile again. You have everything the broker needs: a Project, an approved credential, and a blueprint version. Declare the Resource:

bash
RESOURCE_ID=$(plexctl resource create \
  --project-id "$PROJECT_ID" \
  --kind node \
  --blueprint-version-id "$VERSION_ID" \
  --cloud-credential-id "$CREDENTIAL_ID" \
  --parameters '{}' \
  --output json | jq -r '.id')
echo "$RESOURCE_ID"

Another grant to land. The approval you just made writes the Project's use of the credential to the authorization mirror a moment later. If resource create returns Permission Denied: project not authorised to use the named credential, wait a second and re-run it — the same eventual-consistency window as the Project grant in Step 2.

resource create returns 202 Accepted with a Location header pointing at the new Resource — provisioning is asynchronous, so the call accepts your declaration rather than waiting for the substrate. The Resource is created at phase Pending:

bash
plexctl resource get "$RESOURCE_ID" --output json | jq '{kind, origin, phase: .provisioning.phase}'
json
{
  "kind": "node",
  "origin": "Provisioned",
  "phase": "Pending"
}

Step 6 — Watch it reach Ready

From here you do nothing — the broker reconcile loop drives the Resource forward on its own ticker. Poll its phase until it is Ready:

bash
while true; do
  PHASE=$(plexctl resource get "$RESOURCE_ID" --output json | jq -r '.provisioning.phase')
  echo "$(date +%T) $PHASE"
  case "$PHASE" in Ready|Failed) break ;; esac
  sleep 10
done
text
14:31:02 Pending
14:31:12 Provisioning
14:31:42 Enrolling
14:33:12 Ready

Each phase is a real milestone the broker observes, never a timer:

  • Pending → Provisioning — the Project's management-fleet namespace became ready, so the broker minted a bootstrap token, bound it to this Resource, and applied the blueprint's Composite Resource.
  • Provisioning → Enrolling — the substrate composed and reported ready; the enrolment workload is running.
  • Enrolling → Ready — the node redeemed its bootstrap token at POST /v1/register, resolved this Resource straight from the token, and was anchored as a live mesh peer. Only then does the broker flip the phase.

That is a Resource provisioned end to end — no cloud account, no manual substrate, no operator wiring between the steps. The same resource create against a real cloud blueprint would have stood up a real machine.

Step 7 — Deprovision the Resource

A provisioned Resource is torn down the way it was built — you declare the intent and the broker reconciles it. Delete it; the --yes gate guards the destructive act:

bash
plexctl resource delete "$RESOURCE_ID" --yes

Like create, this is asynchronous: the call returns 202 Accepted and queues the teardown rather than blocking on it. Poll the phase the same way you did on the way up, now until it reaches Deleted:

bash
while true; do
  PHASE=$(plexctl resource get "$RESOURCE_ID" --output json | jq -r '.provisioning.phase')
  echo "$(date +%T) $PHASE"
  case "$PHASE" in Deleted|Failed) break ;; esac
  sleep 10
done
text
14:40:03 Deregistering
14:40:33 Deprovisioning
14:41:13 Deleted

Teardown runs the provisioning arc in reverse, and the order is an invariant rather than a convenience:

  • Ready → Deregistering — the broker drains the plexd node out of the mesh first, holding this phase until the node is no longer observed registered.
  • Deregistering → Deprovisioning — only once the node is gone does the broker delete the substrate (the Crossplane Composite Resource and its ProviderConfig), so a node is never stranded against a substrate that has already vanished.
  • Deprovisioning → Deleted — both the node and the substrate are gone; Deleted is terminal.

Unlike provisioning, teardown needs no credential assignment and no two-party approval — a single resource delete with the delete relation on the Resource is enough. Separation of duties guards standing spend up, not tearing it down.

What you learned

  • Provisioning splits two roles. The platform operator curates the catalog — Clouds, Credentials, Blueprints — and a project owner consumes it. A domain admin owns Projects but not the platform catalog, which is why the catalog reads were the operator's to make.
  • A Resource is a declaration the broker reconciles. resource create returns 202 and a Location because provisioning is asynchronous; the Resource then advances through observed phases, not a fixed schedule.
  • Credential assignment is two-party. The Project owner requests and the credential owner approves, and a principal may never approve its own request — separation of duties holds even on a single dev stack.
  • A blueprint version is the unit you provision against. The Blueprint is the recipe; the immutable BlueprintVersion id is what resource create takes, and its parameter schema tells you what input it needs.
  • The phases are facts, not a clock. Provisioning waits on the substrate being applied, Enrolling on it reporting ready, and Ready on the node actually registering and becoming a live peer.
  • The token carries the Resource. A broker-provisioned node has no operator-chosen handle, so its bootstrap token names the Resource it binds to — that is how enrolment resolves which Resource the new node becomes, with no human in the loop.
  • Teardown is the provisioning arc in reverse — and ordered.resource delete is asynchronous like create; the broker deregisters the node from the mesh before it deletes the substrate, so a machine is never stranded — and, unlike standing one up, tearing it down needs no approval.

Where to go next

  • Keep learning by doingReach your Resource issues short-lived, mediated sessions — a kubeconfig, a TCP forward, an SSH login — to the Node you just provisioned.

Or pick the quadrant that matches what you need now: