Examples¶
Questions kubectl cannot answer in one command, answered in one query.
Every query here runs in CI against a small shop namespace with a few things
wrong in it, and the output under each is from that run. They assume you have
finished Initialize, so server prod exists and
its kinds are imported into schema k8s.
Which pods keep restarting?¶
CrashLoopBackOff is not a pod phase: a crash-looping pod is Running. It is
not even a steady state of the container, which alternates between
CrashLoopBackOff and the error from its last attempt, so filtering on it
misses the pod half the time. The restart count only grows, and it lives on
each container, a nested field kubectl cannot filter on.
SELECT namespace, name AS pod, phase, c->>'name' AS container,
(c->>'restartCount')::int AS restarts,
coalesce(c->'state'->'waiting'->>'reason',
c->'lastState'->'terminated'->>'reason') AS why
FROM k8s.core_pods,
jsonb_array_elements(status->'containerStatuses') c
WHERE (c->>'restartCount')::int > 0
ORDER BY restarts DESC;
namespace | pod | phase | container | restarts | why
-----------+-----------------+---------+-----------+----------+-------------------
shop | checkout-worker | Running | worker | 3 | RunContainerError
(1 row)
Why is that pod not running?¶
The pod's state and the latest warning that explains it, on one row.
kubectl get pods and kubectl get events are two commands and a squint.
SELECT DISTINCT ON (p.namespace, p.name)
p.namespace, p.name AS pod, p.phase, e.reason, e.message
FROM k8s.core_pods p
JOIN k8s.core_events e ON e.namespace = p.namespace
AND e.involved_object->>'kind' = 'Pod'
AND e.involved_object->>'name' = p.name
WHERE p.phase <> 'Running' AND e.type = 'Warning'
ORDER BY p.namespace, p.name,
coalesce(e.last_timestamp, e.event_time, e.creation_timestamp) DESC;
namespace | pod | phase | reason | message
-----------+---------------------------+---------+------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
shop | checkout-6b889d49cf-jt7bz | Pending | FailedScheduling | 0/1 nodes are available: 1 Insufficient cpu. no new claims to deallocate, preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.
shop | report | Pending | Failed | Failed to pull image "registry.invalid/shop/report:1.4": failed to pull and unpack image "registry.invalid/shop/report:1.4": failed to resolve reference "registry.invalid/shop/report:1.4": failed to do request: Head "https://registry.invalid/v2/shop/report/manifests/1.4": dial tcp: lookup registry.invalid on 172.18.0.1:53: no such host
(2 rows)
Which rollouts are stuck, and why?¶
Four kinds in one statement: each Deployment short of its replicas, through its ReplicaSet to the pod that will not start, and the event that says why.
SELECT DISTINCT ON (d.namespace, d.name, p.name)
d.namespace, d.name AS deployment,
coalesce(d.ready_replicas, 0) || '/' || d.replicas AS ready,
p.name AS pod, e.message AS why
FROM k8s.apps_deployments d
JOIN k8s.apps_replicasets r ON r.namespace = d.namespace
AND r.metadata->'ownerReferences'->0->>'name' = d.name
JOIN k8s.core_pods p ON p.namespace = r.namespace
AND p.metadata->'ownerReferences'->0->>'name' = r.name
LEFT JOIN k8s.core_events e ON e.namespace = p.namespace
AND e.involved_object->>'kind' = 'Pod'
AND e.involved_object->>'name' = p.name
AND e.type = 'Warning'
WHERE coalesce(d.ready_replicas, 0) < d.replicas
AND p.phase <> 'Running'
ORDER BY d.namespace, d.name, p.name,
coalesce(e.last_timestamp, e.event_time, e.creation_timestamp) DESC NULLS LAST;
namespace | deployment | ready | pod | why
-----------+------------+-------+---------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------
shop | checkout | 0/1 | checkout-6b889d49cf-jt7bz | 0/1 nodes are available: 1 Insufficient cpu. no new claims to deallocate, preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.
(1 row)
Find it and fix it in one statement¶
A query can also write. This annotates each Deployment in shop using under a fifth of
the memory it requests, so the finding sits on the object where kubectl
describe will show it. Each row is a real Kubernetes update.
WITH used AS (
SELECT m.namespace, m.name AS pod, sum(axiom_quantity(c->'usage'->>'memory')) AS bytes
FROM k8s.metrics_k8s_io_pods m, jsonb_array_elements(m.containers) c
GROUP BY 1, 2),
requested AS (
SELECT p.namespace, p.name AS pod,
r.metadata->'ownerReferences'->0->>'name' AS deployment,
sum(axiom_quantity(c->'resources'->'requests'->>'memory')) AS bytes
FROM k8s.core_pods p
JOIN k8s.apps_replicasets r
ON r.namespace = p.namespace AND r.name = p.metadata->'ownerReferences'->0->>'name',
jsonb_array_elements(p.spec->'containers') c
GROUP BY 1, 2, 3),
ratio AS (
SELECT q.namespace, q.deployment, round(100 * sum(u.bytes) / sum(q.bytes)) AS pct
FROM requested q JOIN used u USING (namespace, pod)
GROUP BY 1, 2
HAVING sum(q.bytes) > 0)
UPDATE k8s.apps_deployments d
SET annotations = coalesce(d.annotations, '{}')
|| jsonb_build_object('axiom/memory-used-pct', r.pct::text)
FROM ratio r
WHERE d.namespace = r.namespace AND d.name = r.deployment AND r.pct < 20
AND d.namespace = 'shop' -- one namespace at a time: this writes to the cluster
RETURNING d.namespace, d.name, r.pct;
namespace | name | pct
-----------+---------+-----
shop | catalog | 0
shop | web | 1
(2 rows)
Changing the cluster covers the grant this needs, and what happens when two writers race.
Join the cluster to your own data¶
Axiom runs inside your application's Postgres, so the cluster joins to your own tables with no export in between. Given which customer owns which namespace:
CREATE TABLE tenants (namespace text PRIMARY KEY, customer text, plan text);
INSERT INTO tenants VALUES ('shop', 'Acme Corp', 'enterprise');
Which customers are affected by a failing pod right now, and why:
SELECT DISTINCT ON (p.namespace, p.name)
t.customer, t.plan, p.name AS pod, e.reason
FROM tenants t
JOIN k8s.core_pods p ON p.namespace = t.namespace
JOIN k8s.core_events e ON e.namespace = p.namespace
AND e.involved_object->>'kind' = 'Pod'
AND e.involved_object->>'name' = p.name
WHERE e.type = 'Warning'
ORDER BY p.namespace, p.name,
coalesce(e.last_timestamp, e.event_time, e.creation_timestamp) DESC;
customer | plan | pod | reason
-----------+------------+---------------------------+------------------
Acme Corp | enterprise | checkout-6b889d49cf-jt7bz | FailedScheduling
Acme Corp | enterprise | checkout-worker | BackOff
Acme Corp | enterprise | report | Failed
(3 rows)
More patterns¶
Aggregate. Where pods are failing, and how:
SELECT namespace, phase, count(*)
FROM k8s.core_pods
WHERE phase <> 'Running'
GROUP BY namespace, phase
ORDER BY count(*) DESC;
namespace | phase | count
-----------+---------+-------
shop | Pending | 2
(1 row)
Filter on a promoted column. Deployments that never finished rolling out.
The replica counts are bigint, so they compare as numbers with no cast, and a
missing field is NULL rather than 0:
SELECT namespace, name, replicas, ready_replicas
FROM k8s.apps_deployments
WHERE coalesce(ready_replicas, 0) < replicas;
namespace | name | replicas | ready_replicas
-----------+----------+----------+----------------
shop | checkout | 1 |
(1 row)
Filter on anything, through raw. kubectl offers label selectors and a
few field selectors. Anything no column promotes is still in raw, such as
every container that sets no memory limit:
SELECT d.namespace, d.name AS deployment, c->>'name' AS container
FROM k8s.apps_deployments d,
jsonb_array_elements(d.raw->'spec'->'template'->'spec'->'containers') c
WHERE c->'resources'->'limits'->'memory' IS NULL;
namespace | deployment | container
--------------------+------------------------+------------------------
kube-system | metrics-server | metrics-server
local-path-storage | local-path-provisioner | local-path-provisioner
shop | checkout | checkout
shop | web | web
(4 rows)
Ask several clusters at once. Each cluster is its own server and its own
schema, so one question across them is a UNION ALL. This needs a second
cluster and gateway, so it is the shape rather than something to paste:
-- after CREATE SERVER stage ... and IMPORT FOREIGN SCHEMA k8s FROM SERVER stage INTO stage
SELECT 'prod' AS cluster, namespace, name, phase FROM k8s.core_pods WHERE phase <> 'Running'
UNION ALL
SELECT 'stage', namespace, name, phase FROM stage.core_pods WHERE phase <> 'Running';
Bigger examples¶
- Capacity and risk review. Per workload: what it uses, what it reserved, whether it is bounded at all, and what is failing. Usage, spec, ownership and events in one statement.
- Changing the cluster.
INSERT,UPDATEandDELETEas real API calls, what Axiom refuses to do, and why. - Manage Postgres with Postgres. One Postgres creating and scaling another through CloudNativePG, with no YAML.
- A silly Kubernetes operator in SQL. The reconcile
step is one
UPDATE … FROMa table you own;NOTIFYwakes it. - Your deployment, in chronological order. A Helm release's Deployment, ReplicaSet, Pods, events and conditions, in order, from one query.
- Did your patch regress? pgbench across Postgres 16, 17 and 18.
CloudNativePG clusters created from a table, a
pgbenchJob against each, and each run's throughput beside the CPU its Postgres used.
Custom resources and writes may need a grant
The shipped RBAC in deploy/k8s/gateway-rbac.yaml reads broadly, covering
workloads, nodes, networking, storage, events and metrics, so most examples
work as imported. It never reads Secrets, and it writes only ConfigMaps and
the example CRD.
A custom resource is readable if its operator ships an
aggregate-to-view role. Otherwise, label a read-only ClusterRole for its
API group axiom.dhilipkumars.github.io/aggregate-to-gateway: "true"
(Restricting access shows
one). To make a kind writable, add its verbs to the axiom-gateway
ClusterRole. Neither needs a gateway restart; only revoking a grant does.
Either way, re-import:
-- foreign tables are catalog objects; they do not follow an RBAC change
IMPORT FOREIGN SCHEMA k8s FROM SERVER prod INTO k8s;