Skip to content

core.cloud

← Module catalog

keeper-sidesoulstack.cloudcloud

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
  • Sidekeeper-side
  • Collectionsoulstack.cloud
  • Categorycloud

States

core.cloud.created — Request count VMs from the provider: for each — a record in the souls registry (status: pending) and a bootstrap token.

Changes when

Always: cloud-create is imperative and not idempotent at the module level — a repeat creates new VMs.

Stays unchanged when

Never — the operation always runs.

Parameters

ParamTypeRequired / defaultDescription
providerstringrequiredName of the cloud Provider in the registry (driver + credentials + region).
profilemapoptionalVM profile parameters (backend-specific).
countintoptionalHow many VMs to create (default 1, >= 1).
userdatastringoptionalCloud-init userdata (mutually exclusive with generate_userdata and self_onboard).
generate_userdatabooloptionalGenerate userdata from keeper.yml cloud_init (ADR-017(h)).
namestringoptionalBase name of the VM batch (self-onboard Variant T): Keeper predicts FQDN=<name>-<index>.<suffix>. Required when self_onboard is set.
self_onboardbooloptionalThe 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

FieldTypeDescription
hostsarray<object>one record per VM: {sid, vm_id, primary_ip, attributes?, bootstrap_token}
countnumbernumber of VMs created
vm_idsarray<string>provider-side IDs of the created VMs
actioncreated

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.

RegistryRESTPermissionAudit
Cloud-ProviderGET · POST /v1/providers · GET · DELETE /v1/providers/{name}provider.read · provider.create · provider.deleteprovider.created / provider.deleted
Cloud-ProfileGET · POST /v1/profiles · GET · DELETE /v1/profiles/{name}profile.read · profile.create · profile.deleteprofile.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.

See also