Lab Config Schema¶
The full shape of config.yaml (internal/config.LabConfig). Every field is optional unless noted — an empty/omitted block simply means that stage does nothing.
Editor support and validation¶
Add this first line to a lab's config.yaml for autocompletion, hover docs and inline errors in VS Code (with the Red Hat YAML extension) and any other editor using yaml-language-server:
The JSON Schema is generated from astrona's own config types, so it always matches what the CLI accepts — every object rejects unknown keys, and closed value sets (runtime.type, type: file|folder|url, check types, …) are enums. It expects the documented spelling (podReady); the CLI itself also accepts other letter case for check and source types.
From the command line:
checks the config without creating anything and exits non-zero on any problem — good as a CI step for lab repositories:
✗ line 3 unknown field "waitfor" in bootstrap/testing
fix: did you mean "waitFor"?
✗ validation.checks[1] unsupported type 'httpGet' (resourceExists, podReady or command)
✗ teardown.init[0] cleanup local file source does not exist: …/cleanup.sh
Unknown fields (typos like waitfor or manifest) used to be silently ignored, leaving a lab without the gates or manifests its author meant to configure. Now astrona validate and astrona check fail on them, and run/test/submit print a warning with a did-you-mean (they still run, so a lab that has always worked keeps working).
Top level¶
apiVersion: astrona.io/v1 # config format (optional; a newer one is refused)
astronaVersion: ">=0.2.0" # which astrona releases may run this lab (optional) — see Astrona Versions
metadata: {} # MetadataConfig
runtime: {} # RuntimeConfig — omit entirely for a kind cluster
bootstrap: {} # BootstrapConfig
testing: {} # BootstrapConfig (same shape, CI-only)
validation: {} # ValidationConfig
teardown: {} # TeardownConfig
resources: [] # LabResource — optional; everything in resources/ counts without it
metadata¶
| Field | Type | Description |
|---|---|---|
name |
string | Lab name — becomes the cluster/VM name, prefixed astro-. Must start with a letter or digit and contain only letters, digits, ., _ and - (no /, ,, spaces or ..) — see Names |
docs.question |
string | The task statement. Default: question.md next to config.yaml, if it exists |
docs.solution |
string | The full step-by-step walkthrough. Default: solution.md |
docs.prerequisites |
string | What to know before starting. Default: prerequisites.md |
docs.caseStudy |
string | A softer, hint-driven version of the same task. Default: case-study.md |
timeLimit |
duration | Playgrounds only: how long a module's playground may run before its clock stops and its cluster is removed, written like 90m, 2h or 1h30m; between 5m and 24h. Empty: the site's default (2h). Ignored for labs, which have no such limit — see Playgrounds |
docs is optional: a doc it doesn't list is found by its default file name next to config.yaml, so a lab with question.md and solution.md beside its config needs no docs block at all. The older names examQuestion (= question) and guide (= solution) still work; setting both names of the same doc to different files is an error. Doc paths are relative to config.yaml and must stay inside the lab directory (absolute paths are rejected); students read them with astrona docs.
Names¶
metadata.name, runtime.qemu[].name and runtime.kind.clusters[].name become directory names under ~/.astrona/ (state that astrona destroy deletes) and parts of qemu command lines, so they must match ^[A-Za-z0-9][A-Za-z0-9._-]*$ and must not contain ... Every command checks this when it loads the config and refuses the lab otherwise, e.g. metadata.name: invalid name 'x/../y': must start with a letter or digit …. (runtime.kind.clusters[].name has a stricter rule of its own, below.) astrona bundle load applies the same rule to the lab name recorded in a bundle.
runtime¶
| Field | Type | Description |
|---|---|---|
type |
string | ""/"kind" (default) or "qemu" |
kind |
Kind cluster | kind only — node image, node count, networking. Omit for kind's defaults |
qemu |
list of QEMU VM | Required when type: qemu; always a list, even for one VM |
networks |
list | {name, cidr} — named virtual network segments VMs can join |
portForwards |
list of PortForward | kind only — host-side port forwards started by astrona run |
runtime.portForwards[N]¶
kind only (rejected for type: qemu). Each entry becomes a background, auto-restarting kubectl port-forward on 127.0.0.1, started once bootstrap (scripts, manifests and waitFor gates) has finished. See Port forwards.
| Field | Type | Description |
|---|---|---|
name |
string | Required. Unique, lowercase DNS label (a-z, 0-9, -) — shown in astrona port-forward list |
resource |
string | Required. <kind>/<name>, kind one of svc/service, pod/po, deploy/deployment, sts/statefulset, rs/replicaset — e.g. svc/frontend |
namespace |
string | Namespace of resource (default default) |
hostPort |
int | Required. Local port, 1024–65535, unique within the lab. Always bound to 127.0.0.1 |
targetPort |
int | Required. Port on the service/pod, 1–65535 |
scheme |
string | http | https | tcp (default) — only changes the URL printed after run |
description |
string | Optional, printed next to the URL after run |
cluster |
string | Forward from a linked cluster (its name) instead of the lab's own — e.g. an identity provider's login page |
runtime:
type: kind
portForwards:
- name: web
resource: svc/frontend
hostPort: 8080
targetPort: 80
scheme: http
description: Frontend UI
- name: db
resource: svc/postgres
namespace: data
hostPort: 5432
targetPort: 5432
runtime.qemu[N]¶
| Field | Type | Description |
|---|---|---|
name |
string | Required once there's more than one entry; leave empty for a single-VM lab. Same character rule as metadata.name — see Names |
image.type |
string | "file" | "url" | "oci" |
image.source |
string | Path, URL, or OCI reference, depending on type |
image.checksum |
string | e.g. sha256:... — recommended |
image.checksums |
map | Alternative: per-algorithm checksum map |
arch |
string | Guest architecture |
cpus |
int | vCPU count |
memoryMB |
int | Memory in MB |
diskSizeGB |
int | Overlay disk size in GB |
extraDisks |
list | {sizeGB, format, serial} — additional blank disks |
networks |
list | {name, ipv4} — attaches a NIC to a runtime.networks segment |
sshAccess |
list of strings | Other VM names this VM can SSH into passwordlessly (multi-VM only) |
sshPort |
int | Host-side forwarded SSH port |
sshPasswordAuth |
bool | Enable password auth in addition to key auth, for the student account only — the astrona automation account (see Runtimes) is always key-auth only |
display |
bool | Show a QEMU display window |
bootstrap |
BootstrapConfig | Runs only against this VM, after the shared root bootstrap (multi-VM only) |
validation |
ValidationConfig | Runs only against this VM, after the shared root validation (multi-VM only) |
See Runtimes for single- vs multi-VM behavior.
Disk device naming¶
Disks are attached in a fixed order, so the guest device names are stable:
| Guest device | Disk |
|---|---|
/dev/vda |
Main overlay disk (root filesystem, diskSizeGB). Always the boot disk. |
/dev/vdb |
extraDisks[0] |
/dev/vdc |
extraDisks[1] |
| ... | ... |
The cloud-init seed disk is also attached but carries no useful mount for a
lab. /dev/vda is guaranteed to be the boot disk regardless of how many
extraDisks entries are present.
For labs that must not hard-code a /dev/vd* name, set a serial on the
extraDisks entry and reference it by the stable
/dev/disk/by-id/virtio-<serial> path instead.
runtime.kind¶
kind only (rejected for type: qemu). Omit it and the lab gets a plain kind create cluster: one control-plane node, kind's default node image, kindnet CNI. Every field is optional.
| Field | Type | Description |
|---|---|---|
version |
string | Kubernetes version, e.g. v1.31.2 — boots kindest/node:<version>. Mutually exclusive with image |
image |
string | Full node image reference. Pin by digest for reproducible labs: kindest/node:v1.31.2@sha256:… |
dnsTimeout |
string | CoreDNS readiness timeout before bootstrap; default 3m, allowed 1s–30m. Also supported on linked clusters. A longer timeout helps slow startup but cannot repair an unhealthy control plane |
nodes.controlPlanes |
int | 1–3 (default 1). More than one adds kind's load balancer container |
nodes.workers |
int | 0–6 (default 0) |
networking.disableDefaultCNI |
bool | Don't install kindnet — the lab installs its own CNI (Calico, Cilium, …) in bootstrap. Nodes stay NotReady until it does |
networking.kubeProxyMode |
string | iptables | ipvs | nftables | none |
networking.ipFamily |
string | ipv4 | ipv6 | dual |
networking.podSubnet |
string | Pod CIDR, e.g. 192.168.0.0/16 (Calico's default) |
networking.serviceSubnet |
string | Service CIDR; must not overlap podSubnet |
featureGates |
map[string]bool | Kubernetes feature gates, e.g. InPlacePodVerticalScaling: true |
runtimeConfig |
map[string]string | API groups to enable/disable ("true"/"false"), e.g. "resource.k8s.io/v1beta1": "true" |
addons |
Addons | Cluster components installed right after the cluster is created |
preloadImages |
list of strings | Images loaded into every node before addons/bootstrap, e.g. [nginx:1.27-alpine] — explicit tag or digest required (not :latest), max 30. See Preloaded images |
clusters |
list of KindCluster | Extra kind clusters running side by side with the lab's own (max 5). Called labs before v0.3 — still accepted, with a deprecation warning |
sharedCA |
bool | Give the lab its own certificate authority, installed in its cluster and every linked cluster: ConfigMap astrona-ca (ca.crt) in default; with the certManager addon a ClusterIssuer astrona-ca. Scripts, command checks and astrona shell get $ASTRONA_CA_CERT. See TLS between clusters |
runtime:
type: kind
kind:
version: v1.31.2
nodes:
workers: 2
networking:
disableDefaultCNI: true
podSubnet: 192.168.0.0/16
runtime.kind.addons¶
Installed in this order, right after the cluster is created and before bootstrap; each one is waited on until ready. Each is pinned to one upstream release whose manifest checksum is built into astrona — see Addons.
| Field | Type | Description |
|---|---|---|
cni |
string | calico (Calico v3.32.2). Implies networking.disableDefaultCNI: true and pod subnet 192.168.0.0/16 (set podSubnet to that or leave it out). Enforces NetworkPolicy |
certManager |
bool | cert-manager v1.21.2 |
metricsServer |
bool | metrics-server v0.9.0 (kubectl top, HPA), with --kubelet-insecure-tls for kind's self-signed kubelet certs |
gatewayAPI |
string | envoy — Gateway API CRDs + Envoy Gateway v1.9.2, with GatewayClass eg |
gatewayPorts.http |
int | Host port for Gateway listeners on port 80 (default 8080, on 127.0.0.1) |
gatewayPorts.https |
int | Host port for Gateway listeners on port 443 (default 8443, on 127.0.0.1) |
runtime:
kind:
nodes: { workers: 1 }
addons:
cni: calico
certManager: true
metricsServer: true
gatewayAPI: envoy # Gateways with gatewayClassName: eg
runtime.kind.clusters[N]¶
Extra kind clusters that run side by side with the lab's own — e.g. an identity provider, a database, a second "site". They belong to the lab: astrona run creates them first, and stop, start, reset, destroy and test handle them together with the lab. See the Linked Labs guide.
| Field | Type | Description |
|---|---|---|
name |
string | Required. How the lab refers to the cluster: ASTRONA_CLUSTER_<NAME>_* env vars, astrona-clusters ConfigMap keys, cluster: on checks. Lowercase letters, digits, -; max 20 characters; unique. The kind cluster is astro-<lab>-<name> |
version, image, nodes, networking, featureGates, runtimeConfig, addons, preloadImages |
Same as for the lab's own cluster (above). A gateway addon gets no host ports here | |
bootstrap |
BootstrapConfig | Sets this cluster up — runs before the lab's own bootstrap |
testing |
BootstrapConfig | This cluster's part of the reference solution — astrona test applies it before the lab's own testing |
teardown.init |
list of ResourceItem | Best-effort scripts run before the lab is destroyed: after the lab's own teardown, in reverse start order, with KUBECONFIG pointing at this cluster. (Whether clusters are kept is the lab's teardown.keepCluster) |
wan.latency / wan.jitter |
string | Simulate a remote site: latency added per round trip (e.g. 80ms) and its variation (10ms, needs latency) — see Simulating a remote site |
wan.loss |
string | Share of packets dropped, 0%–100% |
wan.rate |
string | Bandwidth cap, e.g. 10mbit (kbit, mbit, gbit) |
dependsOn |
list of strings | Other linked clusters (names) that must be up and ready — bootstrap and waitFor done — before this one is created. Its scripts get their ASTRONA_CLUSTER_* addresses (and its cluster their astrona-clusters ConfigMap). Unknown names, self-dependencies and cycles are rejected |
Paths are relative to the lab's config, like everywhere else; by convention each cluster keeps its files in clusters/<name>/ (clusters/idp/bootstrap/, clusters/idp/testing/).
Clusters are created one at a time: dependencies first, otherwise in the order listed; the lab's own cluster last. The first one that fails stops the run — nothing depending on it is started.
runtime:
type: kind
kind:
clusters:
- name: idp
preloadImages: [nginx:1.27-alpine]
bootstrap:
manifests:
- {name: idp, type: file, source: clusters/idp/bootstrap/idp.yaml}
waitFor:
- {resource: deploy/idp, namespace: auth}
The node image must exist for the kind version installed on the student's machine — each kind release lists the node images built for it. Kind config features that reach outside the cluster (extraMounts, extraPortMappings, kubeadmConfigPatches) are deliberately not exposed — see Runtimes.
bootstrap / testing¶
Both use the same shape (BootstrapConfig) — testing only runs under astrona test.
| Field | Type | Description |
|---|---|---|
init |
list of ResourceItem | Scripts run in order at start |
manifests |
list of ResourceItem | Applied via kubectl apply (requires a kubectl-reachable cluster) |
waitFor |
list of WaitFor | Readiness gates checked in order after manifests (kind only) |
WaitFor¶
One readiness gate. Target exactly one of: a named object (resource: deploy/web), every object matching a label selector (resource: pod + selector), or every object of a kind (resource: nodes + all: true).
| Field | Type | Description |
|---|---|---|
name |
string | Optional label shown while waiting (defaults to the target) |
resource |
string | Required. <kind>/<name>, or a bare <kind> with selector/all. CRD kinds work too: certificates.cert-manager.io/web-tls |
selector |
string | Label selector, e.g. app=web or tier in (fe,be) |
all |
bool | Every object of resource's kind |
namespace |
string | Default default (ignored for cluster-scoped kinds like nodes) |
condition |
string | rollout (kubectl rollout status) or a status condition for kubectl wait --for=condition=<X> (Ready, Available, Complete, Established, …) |
timeout |
string | Go duration, 1s–30m (default 2m) |
Default condition by kind — any other kind must set one:
| Kind | Default condition |
|---|---|
deploy/deployment, sts/statefulset, ds/daemonset |
rollout |
pod, node |
Ready |
job |
Complete |
crd/customresourcedefinition |
Established |
bootstrap:
waitFor:
- resource: nodes
all: true # e.g. after bootstrap installed a CNI
timeout: 3m
- resource: deploy/web
namespace: shop
- name: database
resource: pod
selector: app=postgres
namespace: shop
timeout: 5m
While a gate's target doesn't exist yet (NotFound, no matching resources, kind not registered yet) it's retried every 2s until the timeout; any other kubectl failure fails the gate immediately. rollout only works for deployments, statefulsets and daemonsets, and needs a name or selector.
validation¶
| Field | Type | Description |
|---|---|---|
checks |
list of ValidationCheck | Declarative checks |
script |
ResourceItem | Single custom pass/fail script |
scripts |
list of ResourceItem | Additional scripts, run after script, in order |
passPercent |
int | 1–100: pass once the score reaches this percentage (e.g. 66). Unset: every check and script must pass |
Validation scripts also take hint and points (see ValidationCheck).
ValidationCheck¶
| Field | Type | Description |
|---|---|---|
name |
string | Label shown in the Proctor's report |
type |
string | "resourceExists" | "podReady" | "command" | "jsonpath" | "count" | "http" — see Grading |
resource |
string | kubectl get arguments (for resourceExists/podReady/jsonpath/count), e.g. deploy/web -n shop or pods -l app=web -n shop |
command |
string | Shell words to run directly, no shell interpolation (for command) |
expect |
string | Exact expected value (trimmed stdout / JSONPath value / body) |
contains |
string | Value must contain this |
expectRegex |
string | Value must match this regular expression (Go syntax) |
jsonpath |
string | jsonpath checks: kubectl JSONPath, e.g. {.spec.replicas} |
min / max |
int | count checks: bounds on the number of objects (at least one required) |
url |
string | http checks: http:// or https:// URL |
expectStatus |
int | http checks: expected status (default 200) |
hint |
string | Shown to the student only when this check fails — a nudge, not the answer |
points |
int | Weight in the score (default 1) |
cluster |
string | Grade the check in this linked cluster (a runtime.kind.clusters name) instead of the lab's own. Not for http checks |
exam¶
Turns the lab into a timed exam. Omit it for normal practice labs.
| Field | Type | Description |
|---|---|---|
timeLimit |
string | Go duration, 1m–24h, e.g. 2h. The clock starts when astrona run (or reset) has the lab ready |
hideHints |
bool | astrona submit never shows check hints |
strict |
bool | A submission after the time limit can't pass |
See Grading → Exam mode.
teardown¶
| Field | Type | Description |
|---|---|---|
init |
list of ResourceItem | Best-effort scripts run before the environment is destroyed |
keepCluster |
bool | Skip destroying the environment (linked clusters included) after teardown scripts run |
resources¶
Files and folders a student shows, copies or runs with astrona resource instead of copying commands or file contents out of the lab's docs. They live in a resources/ folder next to config.yaml; every file or folder directly in it is a resource even without an entry here. An entry adds a description and says how it runs:
resources:
- file: setup-db.sh # resources/setup-db.sh — runs with bash (from .sh)
description: Creates the demo database the task starts from
- file: broken-deployment.yaml
description: The Deployment you'll fix in step 2
type: file # only shown or copied, never applied
- file: load-test # a folder
description: Generates traffic so you can watch the HPA
run: go run . # run in the folder
| Field | Type | Description |
|---|---|---|
file |
string | Required. Path inside resources/ (a file or folder; may be nested, e.g. tools/check.sh). Absolute paths and .. are refused |
description |
string | One line shown when the lab starts and in astrona resource |
run |
string | Command astrona resource run starts, in the resource's folder. Without it: .sh → bash, .yaml/.yml → kubectl apply -f against the lab (kind labs), an executable with #! → itself, anything else (and folders) → a file |
type |
file |
Never run — only shown or copied (run must be empty) |
vm |
string | qemu labs: the VM it runs in (a runtime.qemu[].name) |
A resource's short name is its file name without the extension (setup-db.sh → setup-db); the full relative filename also works in resource commands. Short names must be unique and use letters, digits, ., _, -. Hidden files (.DS_Store, .gitkeep) are skipped; symlinks are refused anywhere — the file itself, a linked folder in the middle of its path (home/.ssh/id_ed25519 with home a link) or anything inside a folder resource — and every read stays inside resources/. Also refused: more than 1000 files, a file over 50 MB or more than 200 MB in all.
astrona run / reset copy the resources to ~/.astrona/resources/<lab>/ (owner-only) before the lab is built — nothing in them runs — and list them once the lab is ready. A new run replaces the copy; astrona destroy removes it (--keep-resources keeps it). A config fetched from a URL has no folder, so it can't have resources — use a local lab or --git. astrona validate checks them. Students use them with astrona resource.
ResourceItem¶
Shared shape for every script/manifest reference (bootstrap.init, bootstrap.manifests, testing.*, teardown.init, validation.script, validation.scripts).
| Field | Type | Description |
|---|---|---|
name |
string | Label printed as the step runs |
description |
string | Optional, printed alongside name |
type |
string | "file" | "folder" | "url" (manifests support all three; scripts too) |
source |
string | Path (relative to the lab's base directory) or URL, depending on type. A url source must be https:// (redirects to http:// are refused); scripts and manifests alike are downloaded to a size-capped temp file (50 MiB) and run/applied from there |
hint |
string | Validation scripts only: shown when the script fails |
points |
int | Validation scripts only: weight in the score (default 1) |
Full example¶
metadata:
name: "k8s-basics-01"
docs:
prerequisites: "docs/prerequisites.md"
question: "docs/exam-question.md"
caseStudy: "docs/case-study.md"
solution: "docs/step-by-step-guide.md"
bootstrap:
init:
- name: "echo"
type: "file"
source: "hello.sh"
manifests: []
testing:
manifests:
- name: "solution"
type: "folder"
source: "solution"
validation:
checks:
- name: "lab-ns namespace exists"
type: "resourceExists"
resource: "namespace/lab-ns"
script:
type: "file"
source: "validate.sh"
scripts:
- name: "check-network"
type: "file"
source: "validate-network.sh"
teardown:
init:
- name: "dump-logs"
type: "file"
source: "teardown/dump-logs.sh"
keepCluster: false
For the CLI's own flags and commands, see the CLI Reference.