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)
| Field | Type | Description |
|---|---|---|
kind | string | Must be "VM" |
metadata.name | string | Name of the VM |
spec.cpu | integer | Number of vCPUs (default 2) |
spec.memoryBytes | string | integer | RAM size, e.g. "4G" or raw bytes |
spec.desiredState | string | running | stopped. Omit to preserve the current state on re-apply. |
spec.storageBackend | string | filesystem | lvm | zfs |
spec.storageSizeBytes | integer | string | Root disk size in bytes or human-readable, e.g. "20G" |
spec.targetNode | string | Pin VM to a specific node (optional) |
spec.dc | string | Target datacenter for scheduling (optional, mutually exclusive with targetNode) |
spec.sshKeys | string[] | List of SSH key names to inject (must exist in the cluster) |
spec.cloudInitUserData | string | Full cloud-init user-data (multi-line YAML block) |
spec.disks[] | array | List of disk objects |
spec.disks[].backendHandle | string | URL or local path to disk image |
spec.disks[].sha256 | string | Hex-encoded SHA-256 checksum |
spec.disks[].format | string | qcow2 | raw |
spec.nics[] | array | List of network interfaces (supports multiple NICs) |
spec.nics[].network | string | Network name (default "default") |
spec.nics[].model | string | NIC model (default "virtio") |
spec.imageSha256 | string | Top-level alternative to disks[].sha256 |
spec.imageFormat | string | Top-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)
| Field | Type | Description |
|---|---|---|
kind | string | Must be "Network" |
metadata.name | string | Name of the network |
spec.type | string | nat | vxlan | bridge |
spec.gatewayIp | string | Gateway IP for the internal network |
spec.externalIp | string | External IP for NAT/bridge (optional) |
spec.internalNetmask | string | Netmask for internal subnet (default "255.255.255.0") |
spec.vlanId | integer | VLAN tag (required for bridge networks) |
spec.enableOutboundNat | boolean | Enable outbound NAT masquerade (default true for nat/vxlan) |
spec.targetNode | string | Pin network to a specific node (optional, NAT only) |
spec.allowedTcpPorts | integer[] | TCP ports to open on the network (optional) |
spec.allowedUdpPorts | integer[] | 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)
| Field | Type | Description |
|---|---|---|
kind | string | Must be "SshKey" |
metadata.name | string | Name of the SSH key (referenced by VM manifests) |
spec.publicKey | string | The 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)
| Field | Type | Description |
|---|---|---|
kind | string | Must be "Container" |
metadata.name | string | Name of the container |
spec.image | string | OCI image reference (e.g. docker.io/library/nginx:latest) |
spec.network | string | Network name to attach to (optional) |
spec.ports | string[] | Port mappings, e.g. "80:80" |
spec.env | map | Environment variables as key-value pairs |
spec.command | string[] | Override container entrypoint command (optional) |
spec.desiredState | string | running | 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)
| Field | Type | Description |
|---|---|---|
kind | string | Must be "SecurityGroup" |
metadata.name | string | Name of the security group |
spec.description | string | Human-readable description |
spec.rules[] | array | List of firewall rules |
spec.rules[].protocol | string | tcp | udp |
spec.rules[].hostPort | integer | Port on the host side |
spec.rules[].targetPort | integer | Port inside the VM/container (optional, defaults to hostPort) |
spec.rules[].sourceCidr | string | Allowed source CIDR (default "0.0.0.0/0") |
spec.rules[].targetVm | string | Target VM name (optional) |
spec.rules[].enableDnat | boolean | Enable DNAT for this rule |
spec.attachments[] | array | Resources this group is attached to |
spec.attachments[].kind | string | vm | network |
spec.attachments[].target | string | Name or ID of the target resource |
spec.attachments[].targetNode | string | Node 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.
| Field | Type | Description |
|---|---|---|
kind | string | Must be "DiskLayout" |
metadata.name | string | Name of this layout resource |
spec.nodeId | string | Target node id in the controller |
spec.diskLayout | object | Structured disk / GPT / partition tree (mutually exclusive with the other two) |
spec.diskLayout.disks[] | array | Each item: name, device (/dev/…), gpt.partitions[] |
spec.diskLayout.disks[].gpt.partitions[].content | object | Tag type: filesystem | lvm_pv | zfs with the matching fields |
spec.diskLayout.lvmVolumeGroups[] | array | Optional stubs: name for each volume group referenced by lvm_pv |
spec.diskLayout.zfsPools[] | array | Optional stubs: name for each pool referenced by zfs partition content |
spec.layoutNix | string | Multi-line Nix defining disko.devices (mutually exclusive) |
spec.layoutNixFile | string | Path 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.
| Field | Type | Description |
|---|---|---|
kind | string | Must be "ClusterUpdate" |
metadata.name | string | Stable name for this rollout |
spec.target.version | string | Human version label |
spec.target.flake_ref | string | Flake URI (e.g. github:org/repo) |
spec.target.flake_rev | string | Immutable Git revision |
spec.target.nixpkgs_rev | string | Optional nixpkgs pin |
spec.target.system_profile | string | Optional system profile attribute path |
spec.selector | object | node_ids, all_nodes, controllers_only, labels, datacenters |
spec.strategy | object | strategy_type, max_unavailable, batch_size |
spec.drain_vms | boolean | Drain intent (consult release notes) |
spec.drain_timeout_seconds | integer | Drain timeout |
spec.activation.mode | string | test | switch | boot | auto |
spec.activation.reboot_policy | string | Opaque; interpreted by node-agent |
spec.approval_policy | string | manual | auto-non-disruptive | auto |
spec.automatic_rollback | boolean | Rollback 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.
| Field | Type | Description |
|---|---|---|
kind | string | Must be "Cluster" |
metadata.name | string | Context name for this cluster (stored in ~/.kcore/config) |
spec.controller | string | Address of the first controller, e.g. 192.168.40.107:9090 |
spec.certsDir | string | Directory to write PKI files (default ~/.kcore/<context-name>) |
spec.force | boolean | Overwrite 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.
| Field | Type | Description |
|---|---|---|
kind | string | Must be "NodeInstall" |
metadata.name | string | Node identifier (optional) |
spec.node | string | Node-agent address, e.g. 192.168.40.107:9091 |
spec.osDisk | string | Target OS disk (required), e.g. /dev/sda |
spec.dataDisks | string[] | Additional data disks (optional) |
spec.runController | boolean | Start a controller process on this node (default false) |
spec.joinControllers | string[] | Existing controller addresses to join (repeatable) |
spec.storageBackend | string | filesystem | lvm | zfs |
spec.dataDiskMode | string | Storage mode for data disks: filesystem, lvm, zfs |
spec.lvmVgName | string | LVM volume group name (only with lvm backend) |
spec.lvmLvPrefix | string | LVM logical volume prefix |
spec.zfsPoolName | string | ZFS pool name (only with zfs backend) |
spec.zfsDatasetPrefix | string | ZFS dataset prefix |
spec.dcId | string | Datacenter identifier (default DC1) |
spec.hostname | string | Override hostname (optional) |
spec.nodeId | string | Set a specific node UUID (optional) |
spec.disableVxlan | boolean | Disable VXLAN overlay on this node (default false) |
spec.insecure | boolean | Skip 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 |
|---|---|
storageBackend | storage_backend |
storageSizeBytes | storage_size_bytes |
targetNode | target_node |
targetDc | target_dc |
sshKeys | ssh_keys |
cloudInitUserData | cloud_init_user_data |
backendHandle | backend_handle |
imageSha256 | image_sha256 |
imageFormat | image_format |
gatewayIp | gateway_ip |
externalIp | external_ip |
internalNetmask | internal_netmask |
enableOutboundNat | enable_outbound_nat |
publicKey | public_key |
certsDir | certs_dir |
osDisk | os_disk |
dataDisks | data_disks |
runController | run_controller |
joinControllers | join_controllers |
dataDiskMode | data_disk_mode |
lvmVgName | lvm_vg_name |
lvmLvPrefix | lvm_lv_prefix |
zfsPoolName | zfs_pool_name |
zfsDatasetPrefix | zfs_dataset_prefix |
dcId | dc_id |
disableVxlan | disable_vxlan |
nodeId | node_id |
Supported manifest kinds
| Kind | Apply command | Description |
|---|---|---|
Cluster | kctl apply -f | Create cluster PKI (local operation, no controller needed) |
NodeInstall | kctl apply -f | Install a node from the live ISO |
VM | kctl apply -f or kctl create vm -f | Create a virtual machine |
Network | kctl apply -f | Create a NAT, VXLAN, or bridge network |
SshKey | kctl apply -f | Register an SSH public key |
Container | kctl apply -f | Create an OCI container |
SecurityGroup | kctl apply -f or kctl sg apply -f | Create and attach firewall rules |
DiskLayout | kctl diff -f / kctl apply -f | Day-2 disk layout (YAML or disko Nix) |
ClusterUpdate | kctl update cluster plan -f / kctl update cluster apply -f | Host 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.
| Kind | Mutable | Immutable (rejects with InvalidArgument) |
|---|---|---|
VM | cpu, memoryBytes, desiredState | disks, nics, storageBackend, storageSizeBytes, targetNode, sshKeys, cloudInitUserData, image_* |
Container | desiredState | image, command, network, env, ports, storageBackend, storageSizeBytes, mountTarget |
Network | — (none in v1) | all fields |
SshKey | — (public key immutable) | publicKey |
SecurityGroup | description, rules, attachments | name |
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:
- Create (Terraform) / reconcile loop (Crossplane) → gRPC
CreateXwith the desired spec. - Update on drift → same gRPC
CreateX. The controller does the diff server-side and either applies the mutable fields or returnsInvalidArgument, which the provider surfaces as a diagnostic telling the user to destroy and recreate. - Delete → existing gRPC
DeleteX.
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.