core.cloud
Create, destroy, and resize cloud VMs through the provider's CloudDriver plugin.
core.cloud creates, destroys, and resizes cloud VMs by calling the provider's CloudDriver plugin (soul-cloud-<provider>) through PluginHost (gRPC-over-stdio). The step runs on the Keeper itself; the on: keeper dispatcher is required (otherwise the scenario fails validation): the side effects live at the provider (real billing) and in the Keeper's Postgres registries — the module does not touch the host filesystem. created is not idempotent (a repeat creates new VMs), destroyed cascade-updates the registries after a successful delete, resized may cause downtime. The SID of the created host = the FQDN the provider returned.
Requirements
- Rootnot required
- Side
keeper-side - Collection
soulstack.cloud - Category
cloud
States
core.cloud.created — Request count VMs from the provider: for each — a record in the souls registry (status: pending) and a bootstrap token.
Always: cloud-create is imperative and not idempotent at the module level — a repeat creates new VMs.
Never — the operation always runs.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
provider | string | required | Name of the cloud Provider in the registry (driver + credentials + region). |
profile | map | optional | VM profile parameters (backend-specific). |
count | int | optional | How many VMs to create (default 1, >= 1). |
userdata | string | optional | Cloud-init userdata (mutually exclusive with generate_userdata and self_onboard). |
generate_userdata | bool | optional | Generate userdata from keeper.yml cloud_init (ADR-017(h)). |
name | string | optional | Base name of the VM batch (self-onboard Variant T): Keeper predicts FQDN=<name>-<index>.<suffix>. Required when self_onboard is set. |
self_onboard | bool | optional | The VM onboards itself from cloud-init (Variant T, ADR-017(h)): Keeper predicts the FQDN and bakes per-VM tokens into userdata. Requires name + providers.fqdn_suffix. |
Example — Create VMs through the driver (spawn is optional via a when: guard)
- name: provision on: keeper when: has(input.spawn) module: core.cloud.created params: provider: "${ input.spawn.provider }" profile: "${ input.spawn.profile }" count: "${ input.spawn.count }"Output
| Field | Type | Description |
|---|---|---|
hosts | array<object> | one record per VM: {sid, vm_id, primary_ip, attributes?, bootstrap_token} |
count | number | number of VMs created |
vm_ids | array<string> | provider-side IDs of the created VMs |
action | created |
core.cloud.destroyed — Delete the VMs at the provider (PluginHost.Destroy(vm_ids)); then a cascade in a single PG transaction over the registries for the passed sids.
The provider returned a non-empty list of deleted VMs.
The provider returned an empty list — there was nothing to delete.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
provider | string | required | Name of the cloud Provider in the registry. |
vm_ids | list<string> | required | List of provider-vm-id to destroy. |
sids | list<string> | optional | SIDs for cascade update of the registries (souls/seeds/tokens). |
Output
| Field | Type | Description |
|---|---|---|
action | destroyed | |
vm_ids | array<string> | the VMs actually deleted by the provider |
sids | array<string> | echo of the passed sids |
destroyed_n | number | number of VMs deleted |
souls_updated / seeds_orphaned / tokens_burned | number | cascade counters (0 if sids is not passed) |
core.cloud.resized — Resize the resources of existing VMs (cpu / ram / disk in our units) through CloudDriver.Resize. The driver encapsulates the entire stop/start sequence; the souls registry is not touched.
At least one VM was actually changed. A driver without the Resizable capability → resize.unsupported.
No VM changed — the target parameters are already met.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
provider | string | required | Name of the cloud Provider in the registry. |
vm_ids | list<string> | required | provider-vm-id to resize (one target for the whole batch). |
desired | map | required | Target resources in our units: cpu_cores (cores) / ram_mb (MB) / disk_gb (GB). At least one > 0; fields set to 0 are left unchanged. |
allow_downtime | bool | optional | Allow downtime (stop/start) for cpu/ram resize. Required when changing cpu/ram; disk-only is online (default false). |
Output
| Field | Type | Description |
|---|---|---|
action | resized | |
results | array<object> | per VM: {vm_id, caused_downtime, error} |
Reference
Registering a Cloud-Provider and Cloud-Profile
For a scenario to reference a provider and profile, they are first registered in the Keeper's registries through the Operator API (REST / MCP / web-UI) — this is runtime data in PostgreSQL, not git.
| Registry | REST | Permission | Audit |
|---|---|---|---|
| Cloud-Provider | GET · POST /v1/providers · GET · DELETE /v1/providers/{name} | provider.read · provider.create · provider.delete | provider.created / provider.deleted |
| Cloud-Profile | GET · POST /v1/profiles · GET · DELETE /v1/profiles/{name} | profile.read · profile.create · profile.delete | profile.created / profile.deleted |
Notes
- Keeper-side, does not touch the host: the side effects live at the cloud provider and in the Keeper's registries (Postgres), not on the Soul host. The module declares no required_capabilities — this is a keeper-internal operation, and host root/capability do not apply (access is an operator keeper-permission, see secure_defaults).
- created is non-idempotent by construction (changed=true always): repeating the step creates NEW VMs rather than reconciling against existing ones. This is a real billing side effect — control repeats with a guard at the scenario level (when: / changed_when:), do not rely on module idempotency.
- destroyed is a destructive cascade: PluginHost.Destroy(vm_ids) physically destroys the instances; then (when sids is non-empty) a single PG transaction moves souls → destroyed, active soul_seeds → orphaned, bootstrap_tokens → burned. The cascade runs AFTER a successful cloud-destroy — if destroy fails the registries stay untouched (the host is still alive at the provider).
- resized may cause downtime: the driver itself decides whether a stop/start of the VM is needed for the resize (Keeper is agnostic to the sequence); in the output the per-VM caused_downtime field signals a reboot. The souls registry is not touched (resize does not change the set of souls). A driver without the Resizable capability → resize.unsupported.
- The SID of the created host = the FQDN the provider returned (VmInfo.fqdn); a VM without an fqdn fails the step — such a VM cannot be used as a SID.
- Provider is a cloud account (name, driver type, credentials_ref = a vault path; the secret is not resolved in the registry and is not stored in plaintext). Profile is a VM spec on top of a Provider (image, network, size). Deleting a Provider that has dependent Profiles → 409 (FK RESTRICT); POST Profile with a nonexistent provider → 422.
Security & defaults
- Keeper-side, not Soul-side: the step runs in the Keeper process (on: keeper), the side effects live at the provider and in the Postgres registries, not on the host, so root/capability semantics do not apply. Running such a scenario is governed by operator RBAC (a keeper-permission); the created souls records are written with CreatedByAID: null (a keeper-internal action).
- A real financial side effect (created): the step creates real VMs at the provider — this is billing, and it is not idempotent (a repeat creates new VMs rather than reconciling against existing ones). Control repeats with a guard at the scenario level, do not rely on module idempotency.
- destroyed is a destructive cascade operation: it physically destroys the instances, then moves the registry records to their final states. The caller holds the sid↔vm_id mapping — an error in it will destroy the wrong VM, so the source of vm_ids/sids must be trusted.
- Plain bootstrap-token in the register output (created): hosts[].bootstrap_token is a plain one-time token, deliberately in the output because the cloud-init flow must pass it to the VM on first boot (the only moment a plain token is visible; the DB stores only a hash, which cannot be reversed). Secrecy is held by the audit.MaskSecrets substring filter (the token fragment) on ALL output channels; you must not rename the bootstrap_token key without checking the filter — otherwise a one-time token leak.
- The mandatory cloud.provisioned audit event is written for all states; an audit failure FAILS the step (a compliance invariant — a destructive/billing operation must not pass silently). The audit payload holds provider / vm_ids / sids / cascade counters, but NOT plain tokens.