Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Agentic Platform Engineering on Kubernetes: MCP and Infrastructure-as-Context

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.

Runsheet

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.

Before the talk

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
clear

Checklist:

  • two tabs: demo (everything below) and spare (scripts/proxy.sh running, plus drift between acts)
  • .env present 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 shop shows 3/3 Running (cross-check for the room)
  • recording of a full run saved locally (the ultimate fallback)

Act 1: traversing the control plane (about 4 minutes, slide 11)

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 shop

Say: 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 pod

Give it 30 seconds: the replacement pod crash-loops with FATAL: DB_HOST is not set in its log and a BackOff warning event.

Act 2: triage, then a scoped remediation (about 3.5 minutes, slide 21)

# 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 triage

Read 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 remediate

Then:

# 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 04

If 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'"

Close (slides 22-23)

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.

After the talk

./scripts/teardown.sh              # cluster (a few minutes), subnet, GKE firewall rules, VPC

Extras and fallbacks

The 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.sh converges 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 restart scripts/proxy.sh.
  • Port 8011 taken: KUBE_PROXY_PORT=8021 scripts/kube-connect.sh and the same for proxy.sh; everything else reads .kube/env.
  • Wrong state at the start of act 2: scripts/heal.sh, then scripts/drift.sh crash.
  • Model wanders: python demo/agent.py triage --max-turns 12, or --effort low for shorter runs.
  • No wifi: the recording. Everything here needs Google, the cluster and Anthropic reachable.

Repo layout

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

Provider quirks worth knowing

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 only localhost hosts route (an IP or hostname gets anysdk router.FindRoute() failure: no matching operation was found), so the demo goes through kubectl proxy, the provider's documented default.
  • UPDATE on k8s resources is a JSON merge patch (RFC 7386): a key set to null is 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 a nodePools JSON 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 ON clause compares those; json_extract in an ON clause returns no rows, and json_each cannot sit inside a join.
  • A crash-looping pod reports phase Running; test coalesce(json_extract(status, '$.containerStatuses[0].ready'), 0) = 0.
  • Quotes inside JSON string values in an .iql file are written \\": the SQL parser unescapes one backslash before the JSON is parsed.
  • stackql exec -i file.iql -q context.json renders {{ .values.NAME }} from a JSON or jsonnet data file; --var name=value does not substitute into the template directly, it feeds std.extVar("name") in a jsonnet data block or --iqldata file.
  • Audit logging is configured under server.audit.file.path in --mcp.config; without it the server writes stackql_mcp_server_<ts>.log in the working directory.

References

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages