Linked Labs¶
Some things can only be practised with two systems: an app logging in against an external identity provider, a client calling a service in another cluster, network policy between "sites". A lab can run extra kind clusters side by side with its own — declared under runtime.kind.clusters in the same config.yaml — and knows how to reach them.
The worked reference is examples/linked-labs-01: an idp cluster serving a token endpoint, and a task to log in against it from the lab's own cluster. astrona init lab <dir> --linked scaffolds a lab like it.
Declaring linked clusters¶
metadata:
name: auth-lab
runtime:
type: kind
kind:
preloadImages: [curlimages/curl:8.11.1] # the lab's own cluster
clusters:
- name: idp # kind cluster astro-auth-lab-idp
preloadImages: [nginx:1.27-alpine]
nodes: {workers: 0}
addons: {certManager: true}
bootstrap:
manifests:
- {name: idp, type: file, source: clusters/idp/bootstrap/idp.yaml}
waitFor:
- {resource: deploy/idp, namespace: auth}
testing: # its part of the reference solution
init:
- {name: seed users, type: file, source: clusters/idp/testing/seed.sh}
Each entry takes the same cluster fields as runtime.kind (version, nodes, networking, addons, preloadImages, …) plus its own bootstrap and testing. Up to 5 per lab; names are lowercase letters, digits and - (max 20 characters). See runtime.kind.clusters.
Dependencies and start order¶
By default clusters are created one at a time: every linked cluster first, then the lab's own. Each one is only created once the previous is fully ready — its bootstrap done and its waitFor gates passed. Use dependsOn when one linked cluster needs another:
clusters:
- name: db
bootstrap: {manifests: [{name: db, type: file, source: clusters/db/bootstrap/db.yaml}],
waitFor: [{resource: statefulset/db, namespace: data}]}
- name: idp
dependsOn: [db] # created only once db is ready
bootstrap:
init:
- {name: configure, type: file, source: clusters/idp/bootstrap/configure.sh} # gets $ASTRONA_CLUSTER_DB_HOST
- Dependencies are created first; otherwise clusters start in the order listed.
- A cluster's scripts get the
ASTRONA_CLUSTER_*addresses of the clusters it depends on (the lab's own scripts get all of them). - If a cluster fails, the run stops there: nothing that depends on it — and not the lab itself — is started, and the error says what was skipped.
astrona destroycleans up what was created. - Unknown names, a cluster depending on itself, and cycles are rejected by
astrona validate. astrona testapplies the clusters'testingblocks in the same order;astrona startrestarts them in it.
Creating clusters in parallel¶
--parallel N (on run, reset and test, max 5) creates up to N linked clusters at once — faster on a machine with memory to spare:
dependsOnstill holds: a cluster starts as soon as everything it depends on is ready, so independent clusters come up together.- The
kind create clusterstep itself runs one at a time, so your kubectl current-context is still restored correctly and~/.kube/configis never written by two creates at once; everything after it (addons, bootstrap, readiness) runs in parallel. - After the first failure nothing new starts; clusters already being created finish, and the error lists what was never started.
astrona destroycleans up what exists. - Each cluster logs to its own run log (
~/.astrona/logs/linked-<cluster>-…); the screen shows one line per cluster as it becomes ready or fails, with the log path on failure. - The lab's own cluster is still created last, after all of them.
Folder layout¶
Paths are relative to config.yaml, like everywhere else. By convention each linked cluster keeps its files in its own folder under clusters/, next to docs/ and solution/:
config.yaml
docs/ # student docs
solution/ # the lab's own reference solution
clusters/
idp/
bootstrap/ # sets up the idp cluster
testing/ # the idp cluster's part of the reference solution
teardown/ # its teardown scripts
Renamed in v0.3¶
astrona v0.2.1 called these names differently. The old ones still work, so existing labs keep running — switch when convenient:
| v0.2.1 | Now |
|---|---|
runtime.kind.labs |
runtime.kind.clusters — labs is still read, with a deprecation warning from run and validate |
ASTRONA_LINK_<NAME>_* |
ASTRONA_CLUSTER_<NAME>_* — both are set |
ConfigMap astrona-links |
ConfigMap astrona-clusters — both are published, with the same data |
folders under labs/ |
clusters/ — only a convention; your paths keep working |
Reaching a linked cluster¶
All kind clusters share one container network, so a pod can reach a linked cluster's node. Publish what the lab should reach as a NodePort Service with a fixed nodePort:
apiVersion: v1
kind: Service
metadata: {name: idp, namespace: auth}
spec:
type: NodePort
selector: {app: idp}
ports: [{port: 80, targetPort: 80, nodePort: 30080}]
From any cluster of the lab: http://idp.astrona.internal:30080/. A ClusterIP Service name or a pod IP from the other cluster is not reachable — each cluster has its own service and pod network.
Stable names: <name>.astrona.internal¶
Every linked cluster gets a fixed DNS name, <name>.astrona.internal, that resolves to its node in every cluster of the lab — the lab's own and the other linked clusters. It's the same name under astrona run and astrona test (where the container names differ), so manifests and configs can simply contain it: an OIDC issuer URL, a database host, a redirect URI.
- Astrona looks up each node's IPv4 and writes it into each cluster's CoreDNS config (a marked block in the
corednsConfigMap), refreshing it afterstartandreset --cluster, when IPs can change. .internalis reserved for private use. (*.localhostwouldn't work: curl and other clients resolve it to127.0.0.1without asking DNS.)- A lab that replaces CoreDNS's config with one lacking a
.:53server block keeps the container names ($ASTRONA_CLUSTER_<NAME>_HOST) instead.
The same URL on your machine: forward the service with hostPort equal to its nodePort (see below) and add the name to /etc/hosts once — then http://idp.astrona.internal:30080 works in your browser and in the cluster, which is what an identity provider's issuer URL needs:
(Astrona never edits your host /etc/hosts. Isolated Kind terminals get these
addresses in their own container hosts file.)
Everything astrona publishes¶
Astrona tells the lab where each linked cluster is:
| Where | What |
|---|---|
| Every cluster's DNS | <name>.astrona.internal |
The lab's bootstrap/testing/teardown scripts and command checks |
ASTRONA_CLUSTER_<NAME>_HOSTNAME (the stable name), _HOST (the node's container name), _CONTEXT, _KUBECONFIG (<NAME> upper-cased, - → _) |
| In the lab's cluster | ConfigMap astrona-clusters in namespace default: <name>.hostname, <name>.host, <name>.context |
astrona shell |
Every cluster's kubeconfig: kubectl --context "$ASTRONA_CLUSTER_IDP_CONTEXT" … works, plus the env vars. astrona shell <lab> --cluster idp makes the idp cluster the default context; astrona kubeconfig <lab> --cluster idp prints its kubeconfig |
A linked cluster's own bootstrap/testing scripts run with KUBECONFIG pointing at that cluster.
Use the stable name, not the container name. Under astrona test every cluster has a different container name (astro-test-…) — <name>.astrona.internal doesn't change.
Reaching a linked cluster from your machine¶
A port forward reaches a linked cluster with cluster: — the student's browser can open the identity provider's login page, a database client can connect to the db cluster:
runtime:
portForwards:
- name: idp
cluster: idp # forwarded from the idp cluster
resource: svc/idp
namespace: auth
targetPort: 80
hostPort: 30080 # = the NodePort, so one URL works everywhere
scheme: http
It belongs to the lab like any forward: started after every cluster is up, paused and resumed by stop/start, removed by destroy, listed by astrona port-forward list. Like all forwards it binds 127.0.0.1 only.
TLS between clusters: a shared CA¶
An identity provider is usually reached over HTTPS, and its clients must trust its certificate. runtime.kind.sharedCA: true gives the lab its own certificate authority, trusted by every cluster of the lab:
runtime:
type: kind
kind:
sharedCA: true
clusters:
- name: idp
addons: {certManager: true}
bootstrap:
manifests:
- {name: idp, type: file, source: clusters/idp/bootstrap/idp.yaml}
waitFor:
- {resource: certificate/idp, namespace: auth, condition: Ready, timeout: 3m}
What every cluster (the lab's own and each linked one) gets:
ConfigMap astrona-ca in default |
ca.crt — mount it in a pod to trust the CA (curl --cacert /ca/ca.crt …, SSL_CERT_FILE, a Java truststore, …) |
Secret astrona-ca (kubernetes.io/tls) |
The CA's certificate and key — in cert-manager when the cluster has the certManager addon, in default otherwise |
ClusterIssuer astrona-ca |
Only with the certManager addon — issue certificates from the lab CA with issuerRef: {kind: ClusterIssuer, name: astrona-ca} |
A certificate for the stable name then verifies from any cluster of the lab:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: {name: idp, namespace: auth}
spec:
secretName: idp-tls
dnsNames: [idp.astrona.internal]
issuerRef: {kind: ClusterIssuer, name: astrona-ca}
- The CA is created per lab run (ECDSA P-256, can only sign leaf certificates) at
~/.astrona/kind/<lab>/ca.crt(keyca.key, mode0600) and deleted with the lab;astrona testcopies get their own. - Scripts, command checks and
astrona shellget$ASTRONA_CA_CERT— the path ofca.crt— forcurl --cacert "$ASTRONA_CA_CERT" …from your machine (with the/etc/hostsline and a port forward). - astrona never adds the CA to your system's trust store.
sharedCAis lab-wide: set it onruntime.kind, not on a linked cluster.
Simulating a remote site¶
A linked cluster can behave like a site across a WAN — slow, lossy, bandwidth-limited:
astrona applies it with tc netem on every node of that cluster, after the cluster is set up (its bootstrap and readiness gates run at full speed), and re-applies it after astrona start — a restart drops it. It delays what the cluster sends, so the latency is added once per round trip; it affects all its traffic, including kubectl from your machine.
Changing conditions live: astrona net¶
For exercises like "the identity provider got slow" or "the site is down — what breaks?":
astrona net my-lab --cluster idp --latency 400ms --jitter 50ms --loss 10%
astrona net my-lab --cluster idp --partition # nothing it sends gets through
astrona net my-lab --cluster idp --heal # back to the config's wan (or none)
astrona net my-lab --cluster idp --show # what each node has now
Without --cluster it acts on the lab's own cluster. Changes last until --heal, astrona start (which re-applies the config) or destroy. While a cluster is partitioned astrona can't reach its API either — --heal still works, it runs inside the node containers. astrona status shows each linked cluster's configured conditions.
Grading across clusters¶
A check grades the lab's own cluster unless it names a linked one with cluster::
validation:
checks:
- name: idp runs 2 replicas
cluster: idp
type: jsonpath
resource: deploy/idp -n auth
jsonpath: "{.status.readyReplicas}"
expect: "2"
Works for every check type except http (which runs from your machine). A command check with cluster: gets that cluster's KUBECONFIG.
Lifecycle¶
Linked clusters belong to the lab:
| Command | What happens |
|---|---|
astrona run |
Creates the linked clusters (bootstrap included), then the lab. Refused while the lab is running — astrona reset starts it over |
astrona list / status |
A linked cluster is listed as linked cluster of <lab>; status shows each one's state. Commands that pick "the only running lab" ignore linked clusters |
astrona stop / start |
Stops/starts them with the lab (start brings them up first) |
astrona reset --soft |
Keeps every cluster; puts each back to its just-bootstrapped state (lab namespaces deleted, bootstrap re-run, linked clusters first) — seconds instead of minutes. --soft --cluster idp does only that one |
astrona reset --cluster idp |
Rebuilds only that linked cluster of the running lab: its teardown, destroy, then create + bootstrap again (with the addresses of what it dependsOn). The lab and its other clusters are left alone; port forwards into it are restarted. Same name, so the lab's ASTRONA_CLUSTER_*/astrona-clusters stay valid |
astrona reset / destroy |
Runs the lab's teardown, then each linked cluster's teardown.init (reverse start order, KUBECONFIG pointing at it), then removes everything. teardown.keepCluster keeps them all |
astrona test |
Creates test copies of every cluster, applies each linked cluster's testing, then the lab's, grades, and tears everything down — even on failure |
astrona check |
Counts their memory in the lab's estimate |
astrona diagnose / test --diagnostics |
Collects every linked cluster too, under clusters/<cluster>/ in the bundle |
Before bootstrap scripts run, astrona run and astrona test wait for the cluster's DNS (CoreDNS) — a script resolving a linked cluster's host in the first seconds after a cluster is created would otherwise fail with bad address.
astrona bundle packs linked clusters too — each must pin its node image (version or image); see Offline Bundles.
Planning resources¶
Each linked cluster is a full kind cluster: budget memory for all of them together (roughly 1–1.5 GiB per single-node cluster before workloads) — astrona check -c <lab> estimates it. A linked cluster's gateway addon gets no host ports, so it never clashes with the lab's own.