Demo code, runsheet and slides for the talk of the same name by Jeff Aven at KSUG.AI Australia #67, Melbourne (at AWS), 2 September 2026. 20 minutes plus 5 for Q&A.
The idea in one paragraph: StackQL treats cloud and SaaS
providers as data sources accessed via SQL. With the new
k8s provider (v26.08) the Kubernetes API is
one of them, so an agent can traverse the cloud control plane and the cluster in
one session: a GKE cluster and its node pools (google provider), the control
plane objects (deployments, services, RBAC) and the running workloads (pod
status, events, logs) through the k8s provider, with joins across the two in
a single statement. The StackQL MCP server is the
agent interface to that engine, with a fixed tool surface and server-side modes
that gate writes. The demo agent triages a broken workload across GKE and the
cluster, then repairs it under an elevated mode with a human approving each
write. The same server ships as an embeddable package for Go, Rust, Kotlin/JVM
and .NET.
Everything below runs from the repo root in one shell. Scripts are bash (macOS, Linux, Git Bash on Windows) and print the StackQL statement they are about to run so the room can read it.
| Time | Slides | What happens | Command |
|---|---|---|---|
| 0:00 | 1 | Title. Three things: k8s as a data source, agents as writers, a live triage. | |
| 0:45 | 2-4 | StackQL primer: providers as data sources, one engine, several ways in. | |
| 3:00 | 5-6 | The k8s provider: 20 API groups, 172 resources, subresources as tables, the API server is a WHERE clause. | |
| 4:30 | 7-10 | Cloud and cluster in one query, triage as SELECT, provisioning and change in SQL, the jsonnet aside. | |
| 7:00 | 11 | Act 1, live SQL: GKE to node pools, nodes, pods, events, logs, RBAC. | scripts/q.sh 01 ... 08 |
| 11:00 | 12-15 | Agentic platform ops: the four IaC assumptions inverted, the new contract. | |
| 12:45 | 16-18 | StackQL MCP: what it is, the 16 tools, the modes. | |
| 14:15 | 19-20 | Agentic patterns for Kubernetes; how the triage agent is wired. | (in the spare tab: scripts/drift.sh crash) |
| 15:15 | 21 | Act 2, the agent: triage read-only, then remediate in safe mode with two approvals at the terminal. |
python demo/agent.py triage / remediate |
| 18:45 | 22-23 | Takeaways, thank you, the repo. | |
| 20:00 | Q&A (5 min). Extras: --mode delete_safe, the pending scenario, ask, embedded MCP (Go, Rust, Kotlin/JVM, .NET). |
Speaker notes for every slide are in slides/notes.md.
The night before, on good wifi:
cp .env.example .env # GOOGLE_CREDENTIALS (service account JSON), ANTHROPIC_API_KEY
pip install -r requirements.txt # anthropic, mcp, google-auth, certifi
stackql exec --approot ~/.stackql "REGISTRY PULL google"
stackql exec --approot ~/.stackql "REGISTRY PULL k8s"
./scripts/check-env.sh # tools, packages, credentials, providers
./scripts/provision.sh # VPC, subnet, GKE cluster (about 7 min), then the workloads
python demo/agent.py triage # one warm run: all six controls PASS, about 45 s
./scripts/drift.sh crash && sleep 30 && python demo/agent.py remediate # rehearse the approvals (answer y twice)provision.sh is idempotent: it checks live state before every step and is the
right thing to run when anything looks off. The cluster is one e2-medium
zonal node in australia-southeast1-a, about $1.10 a day; scripts/teardown.sh
removes it and the VPC.
Ten minutes before walking on stage (the service-account token in the kubeconfig lasts 60 minutes):
./scripts/kube-connect.sh # fresh token, .kube/config, .kube/env
./scripts/proxy.sh # second tab: kubectl proxy on localhost:8011, leave it running
./scripts/check-env.sh # should end with "all good" and "ok kubectl proxy"
./scripts/heal.sh # cluster converged: checkout 1/1, web 2/2
python demo/agent.py preflight # server up, 16 tools, both planes answer; no model call
clearChecklist:
- two tabs:
demo(everything below) andspare(scripts/proxy.shrunning, plus drift between acts) -
.envpresent in the repo root (the scripts and the agent read it themselves) - terminal 80x24 visible, large font; the queries were chosen to fit
-
kubectl get pods -n shopshows 3/3 Running (cross-check for the room) - recording of a full run saved locally (the ultimate fallback)
Each q.sh call prints the rendered SQL, then runs it. Narrate the traversal:
cloud, node pool, node, pod, event, log.
# the cloud control plane: the cluster as a row
./scripts/q.sh 01
# its node pools (the list comes back as a nodePools array, json_each unpacks it)
./scripts/q.sh 02
# two planes, one statement: GKE node pools JOIN the nodes the API server reports
./scripts/q.sh 03
# the workloads: want vs ready
./scripts/q.sh 04
# pods with a container that is not ready, every namespace (empty right now)
./scripts/q.sh 05
# warning events in the namespace (empty right now)
./scripts/q.sh 06
# logs are a table with one column (the checkout pod's start-up line)
./scripts/q.sh 07
# the RBAC audit: who is cluster-admin
./scripts/q.sh 08
# same cluster, the way the room knows it
kubectl get pods -n shopSay: cluster_addr and protocol are columns. A fleet of clusters is one
table with a WHERE clause.
Between act 1 and act 2, while the MCP slides are up, in the spare tab:
./scripts/drift.sh crash # drops DB_HOST from checkout-config, bounces the podGive it 30 seconds: the replacement pod crash-loops with FATAL: DB_HOST is not set in its log and a BackOff warning event.
# read-only: the server refuses writes whatever the model does. Watch the SQL scroll on stderr.
# Expect: controls 3, 4, 5 FAIL; root cause DB_HOST missing; two statements proposed. About 50 s.
python demo/agent.py triageRead the report aloud: the config map is missing DB_HOST, the pod is
crash-looping, the log says why, the fix is one merge patch plus one pod
delete.
# safe mode: the server asks before every write, over MCP elicitation, answered here.
# Two prompts: the config map UPDATE and the pod DELETE. Read each statement aloud, answer y.
# It re-checks and reports all six controls PASS. About 60 s plus reading time.
python demo/agent.py remediateThen:
# every tool call, mode, decision and statement; never result rows
tail -n 5 stackql-mcp-audit.jsonl
# the deterministic view, no model
./scripts/q.sh 04If there is time, the modes table live: delete_safe lets creates and updates
through and asks only for deletes. Decline it with n:
python demo/agent.py sql --write --mode delete_safe \
"DELETE FROM k8s.core.pods WHERE name = 'not-a-real-pod' AND namespace = 'shop' AND cluster_addr = 'localhost:8011' AND protocol = 'http'"Takeaways, then the ask: pull the k8s provider, point it at your cluster
through kubectl proxy, and the first eight queries here work on any
conformant cluster. If embedded MCP comes up in Q&A: the server the agent just
used is the one that ships in the stackql-mcp crate, stackql-mcp-go,
io.stackql:stackql-mcp and StackQL.Mcp, as a sidecar or vendored into the
application.
./scripts/teardown.sh # cluster (a few minutes), subnet, GKE firewall rules, VPCThe pending scenario (cross-plane capacity): scripts/drift.sh pending asks
checkout for 4 CPUs on a 2 vCPU node. triage reads the node pool machine
type and the node's allocatable CPU and names the mismatch; remediate sends
the container back with the policy's requests (a merge patch replaces the
containers list, so the whole container goes in the patch). scripts/drift.sh all does both scenarios at once. scripts/heal.sh undoes everything with no
model in the loop.
Free-form questions, read-only:
python demo/agent.py ask "which pods in shop restarted in the last hour, and why?"The same server in Claude Code or Claude Desktop instead of the Python agent
(the k8s provider needs the proxy from scripts/proxy.sh and the google
provider reads GOOGLE_CREDENTIALS from the environment):
claude mcp add stackql -- stackql mcp --mcp.server.type=stdio --approot ~/.stackql \
--mcp.config '{"server": {"transport": "stdio", "mode": "safe"}}'Then ask it what demo/policy.md asks the agent.
Fallbacks:
- Anthropic slow or down:
scripts/heal.shconverges the cluster with no model;python demo/agent.py sql --write "<statement from the policy>"shows the approval prompt with no model in the path. - Proxy died or token expired (401s from the k8s provider):
scripts/kube-connect.sh, then restartscripts/proxy.sh. - Port 8011 taken:
KUBE_PROXY_PORT=8021 scripts/kube-connect.shand the same forproxy.sh; everything else reads.kube/env. - Wrong state at the start of act 2:
scripts/heal.sh, thenscripts/drift.sh crash. - Model wanders:
python demo/agent.py triage --max-turns 12, or--effort lowfor shorter runs. - No wifi: the recording. Everything here needs Google, the cluster and Anthropic reachable.
ksug-stackql-demo/
README.md this runsheet
CLAUDE.md working notes for the repo (conventions, quirks, constraints)
.env.example GOOGLE_CREDENTIALS, ANTHROPIC_API_KEY, optional overrides
requirements.txt anthropic, mcp, google-auth, certifi
infra/
gke/ network.iql, subnet.iql, cluster.iql: the GKE estate as INSERTs
k8s/ namespace, config map, two deployments, a service: the workloads as INSERTs
scripts/
_env.sh sourced by every script: .env, defaults, template context, sql helpers
check-env.sh tools, packages, credentials, providers, proxy
provision.sh network -> subnet -> cluster -> connect -> proxy -> workloads, idempotent
kube-connect.sh endpoint and CA from GKE via SQL, token from the SA key, kubeconfig, .kube/env
proxy.sh kubectl proxy on localhost:8011 (the k8s provider's transport)
workloads.sh applies infra/k8s, waits for the rollout
q.sh runs demo/queries/NN-*.iql, printing the rendered SQL first
drift.sh crash | pending | all: break things, one statement at a time
heal.sh the deterministic fix, no model
teardown.sh cluster, subnet, firewall rules, network
demo/
agent.py the triage agent: Anthropic SDK + MCP client + the StackQL server as a child
policy.md six controls and three allowed fixes, {{ PLACEHOLDERS }} from the environment
queries/ 01-cluster ... 08-rbac: the live SQL for act 1
slides/
build_slides.py first draft of the deck from the reference decks in ref/ (the deck is now mastered in Google Slides)
notes.md speaker notes, one bullet block per slide
Agentic Platform Engineering on Kubernetes.pptx
These were found building the demo and are encoded in the scripts and the agent's guidance.
- The k8s provider's server URL is
{protocol}://{cluster_addr}. In stackql 0.10.601 and 0.10.605 onlylocalhosthosts route (an IP or hostname getsanysdk router.FindRoute() failure: no matching operation was found), so the demo goes throughkubectl proxy, the provider's documented default. UPDATEon k8s resources is a JSON merge patch (RFC 7386): a key set tonullis removed and a list is replaced wholesale. Patching one container field means sending the whole container.google.container.node_pools(list) returns one row per cluster with anodePoolsJSON array;json_each(nodePools)unpacks it. The single-pool get (nodePoolsId = '...') has plain columns.- Cross-provider joins work when both sides are subqueries projecting plain
columns and the
ONclause compares those;json_extractin anONclause returns no rows, andjson_eachcannot sit inside a join. - A crash-looping pod reports phase
Running; testcoalesce(json_extract(status, '$.containerStatuses[0].ready'), 0) = 0. - Quotes inside JSON string values in an
.iqlfile are written\\": the SQL parser unescapes one backslash before the JSON is parsed. stackql exec -i file.iql -q context.jsonrenders{{ .values.NAME }}from a JSON or jsonnet data file;--var name=valuedoes not substitute into the template directly, it feedsstd.extVar("name")in a jsonnet data block or--iqldatafile.- Audit logging is configured under
server.audit.file.pathin--mcp.config; without it the server writesstackql_mcp_server_<ts>.login the working directory.
- k8s provider: https://k8s-provider.stackql.io (v26.08.00453)
- google provider (GKE): https://google-provider.stackql.io
- StackQL MCP server: https://stackql.io/docs/mcp and https://stackql.io/docs/command-line-usage/mcp
- Installing StackQL and the MCP server (npm, PyPI, Docker, mcpb, embedded packages): https://stackql.io/docs/installing-stackql
- Embedded packages:
stackql-mcp(crates.io),stackql-mcp-go,io.stackql:stackql-mcp,StackQL.Mcp(NuGet) - Sibling demos: rust-embedded-mcp-with-stackql, edgepilot