Show examples as

YAML manifest reference

kcore supports declarative resource creation via YAML manifests. Every resource that can be created with kctl CLI flags can also be expressed as a YAML manifest, making manifests the preferred way to manage infrastructure as code.

All manifest kinds are auto-detected and applied with a single command:

kctl apply -f <file.yaml>

You are browsing in CLI mode. This page is the YAML manifest reference—switch the toggle above to YAML for the intended view, or open the kctl CLI reference for commands only.

VM manifest (kind: VM)

FieldTypeDescription
kindstringMust be "VM"
metadata.namestringName of the VM
spec.cpuintegerNumber of vCPUs (default 2)
spec.memoryBytesstring | integerRAM size, e.g. "4G" or raw bytes
spec.desiredStatestringrunning | stopped. Omit to preserve the current state on re-apply.
spec.storageBackendstringfilesystem | lvm | zfs
spec.storageSizeBytesinteger | stringRoot disk size in bytes or human-readable, e.g. "20G"
spec.targetNodestringPin VM to a specific node (optional)
spec.dcstringTarget datacenter for scheduling (optional, mutually exclusive with targetNode)
spec.sshKeysstring[]List of SSH key names to inject (must exist in the cluster)
spec.cloudInitUserDatastringFull cloud-init user-data (multi-line YAML block)
spec.disks[]arrayList of disk objects
spec.disks[].backendHandlestringURL or local path to disk image
spec.disks[].sha256stringHex-encoded SHA-256 checksum
spec.disks[].formatstringqcow2 | raw
spec.nics[]arrayList of network interfaces (supports multiple NICs)
spec.nics[].networkstringNetwork name (default "default")
spec.nics[].modelstringNIC model (default "virtio")
spec.imageSha256stringTop-level alternative to disks[].sha256
spec.imageFormatstringTop-level alternative to disks[].format

VM examples

LVM-backed VM with SSH key injection:

kind: VM
metadata:
  name: web-server
spec:
  cpu: 4
  memoryBytes: "4G"
  desiredState: running
  storageBackend: lvm
  storageSizeBytes: "20G"
  targetNode: kvm-node-01
  sshKeys:
    - my-deploy-key
  nics:
    - network: vxlan-prod
    - network: nat-mgmt
  disks:
    - image: https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
      sha256: "<sha256>"
      format: qcow2

ZFS-backed VM with cloud-init:

kind: VM
metadata:
  name: db-server
spec:
  cpu: 8
  memoryBytes: "16G"
  storageBackend: zfs
  storageSizeBytes: "100G"
  dc: dc-eu-west
  sshKeys:
    - ops-team
  cloudInitUserData: |
    #cloud-config
    packages:
      - postgresql-15
    runcmd:
      - systemctl enable postgresql
      - systemctl start postgresql
  nics:
    - network: vxlan-prod
  disks:
    - image: https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
      sha256: "<sha256>"
      format: qcow2

Network manifest (kind: Network)

FieldTypeDescription
kindstringMust be "Network"
metadata.namestringName of the network
spec.typestringnat | vxlan | bridge
spec.gatewayIpstringGateway IP for the internal network
spec.externalIpstringExternal IP for NAT/bridge (optional)
spec.internalNetmaskstringNetmask for internal subnet (default "255.255.255.0")
spec.vlanIdintegerVLAN tag (required for bridge networks)
spec.enableOutboundNatbooleanEnable outbound NAT masquerade (default true for nat/vxlan)
spec.targetNodestringPin network to a specific node (optional, NAT only)
spec.allowedTcpPortsinteger[]TCP ports to open on the network (optional)
spec.allowedUdpPortsinteger[]UDP ports to open on the network (optional)

Network examples

VXLAN overlay (multi-node):

kind: Network
metadata:
  name: vxlan-prod
spec:
  type: vxlan
  gatewayIp: 10.200.0.1
  internalNetmask: "255.255.255.0"
  enableOutboundNat: true

NAT network (single node):

kind: Network
metadata:
  name: nat-mgmt
spec:
  type: nat
  gatewayIp: 10.100.0.1
  internalNetmask: "255.255.255.0"
  targetNode: kvm-node-01

Bridge network with VLAN:

kind: Network
metadata:
  name: bridge-vlan100
spec:
  type: bridge
  vlanId: 100
  externalIp: 192.168.100.1
  gatewayIp: 192.168.100.1
  internalNetmask: "255.255.255.0"
  enableOutboundNat: false

SshKey manifest (kind: SshKey)

FieldTypeDescription
kindstringMust be "SshKey"
metadata.namestringName of the SSH key (referenced by VM manifests)
spec.publicKeystringThe full SSH public key string

Example

kind: SshKey
metadata:
  name: my-deploy-key
spec:
  publicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample... ops@company.com"

Container manifest (kind: Container)

FieldTypeDescription
kindstringMust be "Container"
metadata.namestringName of the container
spec.imagestringOCI image reference (e.g. docker.io/library/nginx:latest)
spec.networkstringNetwork name to attach to (optional)
spec.portsstring[]Port mappings, e.g. "80:80"
spec.envmapEnvironment variables as key-value pairs
spec.commandstring[]Override container entrypoint command (optional)
spec.desiredStatestringrunning | stopped. Omit to preserve the current state on re-apply.

Example

kind: Container
metadata:
  name: nginx-web
spec:
  image: docker.io/library/nginx:latest
  network: nat-mgmt
  ports:
    - "80:80"
    - "443:443"
  env:
    NGINX_HOST: example.com

SecurityGroup manifest (kind: SecurityGroup)

FieldTypeDescription
kindstringMust be "SecurityGroup"
metadata.namestringName of the security group
spec.descriptionstringHuman-readable description
spec.rules[]arrayList of firewall rules
spec.rules[].protocolstringtcp | udp
spec.rules[].hostPortintegerPort on the host side
spec.rules[].targetPortintegerPort inside the VM/container (optional, defaults to hostPort)
spec.rules[].sourceCidrstringAllowed source CIDR (default "0.0.0.0/0")
spec.rules[].targetVmstringTarget VM name (optional)
spec.rules[].enableDnatbooleanEnable DNAT for this rule
spec.attachments[]arrayResources this group is attached to
spec.attachments[].kindstringvm | network
spec.attachments[].targetstringName or ID of the target resource
spec.attachments[].targetNodestringNode name (required when kind is network)

Example

kind: SecurityGroup
metadata:
  name: web-sg
spec:
  description: Allow HTTP and HTTPS traffic
  rules:
    - protocol: tcp
      hostPort: 80
      targetPort: 80
      sourceCidr: "0.0.0.0/0"
    - protocol: tcp
      hostPort: 443
      targetPort: 443
      sourceCidr: "0.0.0.0/0"
      enableDnat: true
      targetVm: web-01
  attachments:
    - kind: vm
      target: web-01

DiskLayout manifest (kind: DiskLayout)

A DiskLayout manifest declares day-2 partitioning for a node via the controller. Set exactly one of spec.diskLayout (structured YAML that kctl expands to disko Nix), spec.layoutNix (inline Nix), or spec.layoutNixFile (path relative to the manifest). Full workflow and classifier behaviour are documented in Storage day-2 operations.

FieldTypeDescription
kindstringMust be "DiskLayout"
metadata.namestringName of this layout resource
spec.nodeIdstringTarget node id in the controller
spec.diskLayoutobjectStructured disk / GPT / partition tree (mutually exclusive with the other two)
spec.diskLayout.disks[]arrayEach item: name, device (/dev/…), gpt.partitions[]
spec.diskLayout.disks[].gpt.partitions[].contentobjectTag type: filesystem | lvm_pv | zfs with the matching fields
spec.diskLayout.lvmVolumeGroups[]arrayOptional stubs: name for each volume group referenced by lvm_pv
spec.diskLayout.zfsPools[]arrayOptional stubs: name for each pool referenced by zfs partition content
spec.layoutNixstringMulti-line Nix defining disko.devices (mutually exclusive)
spec.layoutNixFilestringPath to a .nix file (mutually exclusive)

DiskLayout example (spec.diskLayout)

kind: DiskLayout
metadata:
  name: prod-data-pool
spec:
  nodeId: kvm-node-192-168-40-105
  diskLayout:
    disks:
      - name: data1
        device: /dev/nvme1n1
        gpt:
          partitions:
            - name: kcore0
              size: "100%"
              content:
                type: filesystem
                format: ext4
                mountpoint: /var/lib/kcore/volumes1

Apply: kctl diff -f disk-layout.yaml then kctl apply -f disk-layout.yaml. Other commands: kctl get disk-layouts, kctl describe disk-layout <name>, kctl delete disk-layout <name>.

ClusterUpdate manifest (kind: ClusterUpdate)

A ClusterUpdate manifest declares a flake-pinned host OS rollout for one or more nodes. It is submitted with kctl update cluster, not kctl apply. See Cluster & node upgrades.

FieldTypeDescription
kindstringMust be "ClusterUpdate"
metadata.namestringStable name for this rollout
spec.target.versionstringHuman version label
spec.target.flake_refstringFlake URI (e.g. github:org/repo)
spec.target.flake_revstringImmutable Git revision
spec.target.nixpkgs_revstringOptional nixpkgs pin
spec.target.system_profilestringOptional system profile attribute path
spec.selectorobjectnode_ids, all_nodes, controllers_only, labels, datacenters
spec.strategyobjectstrategy_type, max_unavailable, batch_size
spec.drain_vmsbooleanDrain intent (consult release notes)
spec.drain_timeout_secondsintegerDrain timeout
spec.activation.modestringtest | switch | boot | auto
spec.activation.reboot_policystringOpaque; interpreted by node-agent
spec.approval_policystringmanual | auto-non-disruptive | auto
spec.automatic_rollbackbooleanRollback behaviour hint

ClusterUpdate example

kind: ClusterUpdate
metadata:
  name: release-0-4-0
spec:
  target:
    version: "0.4.0"
    flake_ref: github:kcorehypervisor/kcore
    flake_rev: "<full-git-sha>"
  selector:
    all_nodes: true
  strategy:
    strategy_type: one-at-a-time
  approval_policy: manual

Plan: kctl update cluster plan -f rollout.yaml. Apply: kctl update cluster apply -f rollout.yaml. Other: kctl update cluster get, list, approve, cancel, rollback.

Cluster manifest (kind: Cluster)

A Cluster manifest creates the cluster PKI on the operator workstation. This is a local operation that does not require a running controller.

FieldTypeDescription
kindstringMust be "Cluster"
metadata.namestringContext name for this cluster (stored in ~/.kcore/config)
spec.controllerstringAddress of the first controller, e.g. 192.168.40.107:9090
spec.certsDirstringDirectory to write PKI files (default ~/.kcore/<context-name>)
spec.forcebooleanOverwrite existing PKI (default false)

Cluster example

kind: Cluster
metadata:
  name: prod
spec:
  controller: 192.168.40.107:9090

Apply: kctl apply -f cluster.yaml

NodeInstall manifest (kind: NodeInstall)

A NodeInstall manifest drives the ISO installer on a booted node. It talks to the live-ISO node-agent and optionally contacts a controller for certificate issuance.

FieldTypeDescription
kindstringMust be "NodeInstall"
metadata.namestringNode identifier (optional)
spec.nodestringNode-agent address, e.g. 192.168.40.107:9091
spec.osDiskstringTarget OS disk (required), e.g. /dev/sda
spec.dataDisksstring[]Additional data disks (optional)
spec.runControllerbooleanStart a controller process on this node (default false)
spec.joinControllersstring[]Existing controller addresses to join (repeatable)
spec.storageBackendstringfilesystem | lvm | zfs
spec.dataDiskModestringStorage mode for data disks: filesystem, lvm, zfs
spec.lvmVgNamestringLVM volume group name (only with lvm backend)
spec.lvmLvPrefixstringLVM logical volume prefix
spec.zfsPoolNamestringZFS pool name (only with zfs backend)
spec.zfsDatasetPrefixstringZFS dataset prefix
spec.dcIdstringDatacenter identifier (default DC1)
spec.hostnamestringOverride hostname (optional)
spec.nodeIdstringSet a specific node UUID (optional)
spec.disableVxlanbooleanDisable VXLAN overlay on this node (default false)
spec.insecurebooleanSkip TLS verification (default true; required for live ISO)

NodeInstall examples

First controller node:

kind: NodeInstall
metadata:
  name: kvm-node-107
spec:
  node: 192.168.40.107:9091
  osDisk: /dev/sda
  runController: true
  dcId: DC1
  insecure: true

HA second controller joining the first:

kind: NodeInstall
metadata:
  name: kvm-node-105
spec:
  node: 192.168.40.105:9091
  osDisk: /dev/sda
  runController: true
  joinControllers:
    - 192.168.40.107:9090
  storageBackend: lvm
  dcId: DC1
  insecure: true

Agent-only worker node:

kind: NodeInstall
metadata:
  name: kvm-node-151
spec:
  node: 192.168.40.151:9091
  osDisk: /dev/sda
  dataDisks:
    - /dev/sdb
  joinControllers:
    - 192.168.40.107:9090
    - 192.168.40.105:9090
  storageBackend: zfs
  zfsPoolName: tank0
  dcId: DC1
  insecure: true

Apply: kctl apply -f node.yaml

Field naming

Both camelCase (recommended) and snake_case are accepted for most fields:

camelCase (preferred)snake_case alternative
storageBackendstorage_backend
storageSizeBytesstorage_size_bytes
targetNodetarget_node
targetDctarget_dc
sshKeysssh_keys
cloudInitUserDatacloud_init_user_data
backendHandlebackend_handle
imageSha256image_sha256
imageFormatimage_format
gatewayIpgateway_ip
externalIpexternal_ip
internalNetmaskinternal_netmask
enableOutboundNatenable_outbound_nat
publicKeypublic_key
certsDircerts_dir
osDiskos_disk
dataDisksdata_disks
runControllerrun_controller
joinControllersjoin_controllers
dataDiskModedata_disk_mode
lvmVgNamelvm_vg_name
lvmLvPrefixlvm_lv_prefix
zfsPoolNamezfs_pool_name
zfsDatasetPrefixzfs_dataset_prefix
dcIddc_id
disableVxlandisable_vxlan
nodeIdnode_id

Supported manifest kinds

KindApply commandDescription
Clusterkctl apply -fCreate cluster PKI (local operation, no controller needed)
NodeInstallkctl apply -fInstall a node from the live ISO
VMkctl apply -f or kctl create vm -fCreate a virtual machine
Networkkctl apply -fCreate a NAT, VXLAN, or bridge network
SshKeykctl apply -fRegister an SSH public key
Containerkctl apply -fCreate an OCI container
SecurityGroupkctl apply -f or kctl sg apply -fCreate and attach firewall rules
DiskLayoutkctl diff -f / kctl apply -fDay-2 disk layout (YAML or disko Nix)
ClusterUpdatekctl update cluster plan -f / kctl update cluster apply -fHost OS rollout (flake-pinned); not kctl apply

Idempotency and upsert

kctl apply -f and kctl create -f are idempotent. The controller performs a server-side upsert: it looks up the existing resource, diffs it against your manifest, and then creates, updates the fields that are safe to change, rejects changes to fields that would require a rebuild, or does nothing when the stored state already matches.

Every create command prints what happened:

$ kctl create vm -f vm.yaml
updated VM 'web-01' (fields: cpu, memory_bytes, desired_state)
  ID:   vm-…
  Node: node-a
  CPU:  4 cores
  Mem:  8.0 GiB

$ kctl create vm -f vm.yaml
unchanged VM 'web-01'

Mutable vs immutable fields

A change to an immutable field is rejected with InvalidArgument. The fix is always: kctl delete … then re-apply.

KindMutableImmutable (rejects with InvalidArgument)
VMcpu, memoryBytes, desiredStatedisks, nics, storageBackend, storageSizeBytes, targetNode, sshKeys, cloudInitUserData, image_*
ContainerdesiredStateimage, command, network, env, ports, storageBackend, storageSizeBytes, mountTarget
Network— (none in v1)all fields
SshKey— (public key immutable)publicKey
SecurityGroupdescription, rules, attachmentsname

Rationale: v1 rejects any change that would require recreating the workload or re-provisioning a disk, so reconciliation stays predictable. Future versions can promote more fields to mutable as controlled rebuild paths are added.

Note: targetDc is not rejected by the diff today — the controller has no per-VM DC field to compare against. Placement is enforced once at create time via the placement preflight; changing targetDc on an existing VM is silently a no-op (the VM stays where it was originally placed).

Terraform and Crossplane

Because every Create* RPC is already a declarative upsert, a Terraform provider or Crossplane composition only needs one verb per resource:

No client-side diff, no client-side tracking of which fields are mutable — the controller is the single source of truth.

Applying manifests

Use kctl apply -f for most resource kinds; the kind field is auto-detected. Host OS rollouts use kctl update cluster apply -f rollout.yaml instead — see Cluster & node upgrades.

# Bootstrap (local / ISO operations)
kctl apply -f cluster.yaml
kctl apply -f node-install.yaml

# Cluster resources (require controller)
kctl apply -f vm.yaml
kctl apply -f network.yaml
kctl apply -f ssh-key.yaml
kctl apply -f container.yaml
kctl apply -f security-group.yaml
kctl apply -f disk-layout.yaml

# Host OS upgrade (separate command — kind: ClusterUpdate)
kctl update cluster plan -f rollout.yaml
kctl update cluster apply -f rollout.yaml

Manifests are the recommended way to manage kcore resources. They provide a reproducible, version-controllable description of your infrastructure.