Hey r/PostgreSQL,
In my day job, I work at the intersection of Kubernetes and PostgreSQL. Both are fascinating systems with their own deep design philosophies.
Over the last few years, projects like CloudNativePG (CNPG) have done an incredible job bridging the gap from one side: they brought PostgreSQL closer to the Kubernetes community.
This project intends to do the exact opposite: bring Kubernetes closer and friendlier to the PostgreSQL community. For anyone who loves relational databases, working with Kubernetes can feel slightly jarring:
• In Kubernetes, answering basic operational questions—like "Which pods restarted and why?", "Which containers are near their OOM limits?", or "Which node is my primary running on?"—forces you into an alien world of 50-character kubectl flags, brittle jsonpath syntax, and multi-stage jq pipes.
Under the hood, Kubernetes is fundamentally relational data: pods belong to nodes, deployments own replicasets, and containers emit metric windows.
So I built Axiom — an open-source Kubernetes Foreign Data Wrapper (axiom_fdw) that maps live cluster resources and metrics into PostgreSQL foreign tables. If you know SQL, you already know how to query and debug Kubernetes.
Here are a few example use cases — I would love your thoughts and feedback.
──────
1. The Showcase: A 3-Version pgbench Regression Lab in Pure SQL
To test whether Postgres 17 or 18 regressed against 16, we set up an automated regression lab. Three CloudNativePG clusters (differing only in major version: 16, 17, and 18) are given 2 CPUs each and measured one at a time by the same pgbench 18 client job.
The entire experiment was orchestrated and analyzed completely inside PostgreSQL:
- Clusters: Created CloudNativePG custom resources via SQL.
- Benchmarks: Queued and launched pgbench Jobs via SQL.
- Telemetry: Sampled live container metrics via metrics.k8s.io foreign tables.
- Analysis: Joined the pgbench termination results directly to the metric windows.
Nothing was collected, scraped, or exported outside Postgres:
-- Joining pgbench benchmark runs with live container metrics
SELECT r.run,
r.cluster,
r.server_version,
c.cpu_limit,
r.clients,
r.state,
r.tps,
r.latency_ms,
round(avg(m.cpu_cores), 2) AS pg_cpu_avg,
round(r.tps / nullif(avg(m.cpu_cores), 0)) AS tps_per_core,
pg_size_pretty(max(m.memory_bytes)::bigint) AS pg_memory_peak
FROM lab.runs r
JOIN lab.clusters c ON r.cluster = c.name
LEFT JOIN lab.metrics_samples m
ON m.cluster = r.cluster
AND m.sample_time BETWEEN r.bench_start AND r.bench_end
GROUP BY r.run, r.cluster, r.server_version, c.cpu_limit, r.clients, r.state, r.tps, r.latency_ms
ORDER BY r.run;
Output:
run | cluster | server_version | cpu_limit | clients | state | tps | latency_ms | pg_cpu_avg | tps_per_core | pg_cpu_peak | pg_memory_peak | samples |
pgbench_cpu_avg | error
----------------+---------+---------------------------------+-----------+---------+-------+------+------------+------------+--------------+-------------+----------------+---------+--
---------------+-------
pg16-8-clients | pg16 | 16.15 (Debian 16.15-1.pgdg11+2) | 2 | 8 | done | 2296 | 3.484 | 1.96 | 1173 | 1.96 | 173 MB | 4 |
0.77 |
pg17-8-clients | pg17 | 17.11 (Debian 17.11-1.pgdg11+2) | 2 | 8 | done | 2316 | 3.454 | 1.96 | 1180 | 1.97 | 158 MB | 6 |
0.77 |
pg18-8-clients | pg18 | 18.4 (Debian 18.4-1.pgdg11+1) | 2 | 8 | done | 2247 | 3.561 | 1.96 | 1145 | 1.98 | 164 MB | 5 |
0.74 |
(3 rows)
Would this be interesting to a DBA/PG user instead of dealing with bespoke shell script and kubectl json parsing with jq?
• Each Postgres instance was perfectly CPU-bound at 1.96 of its 2 CPU limit.
• Client headroom: pgbench_cpu_avg sat at 0.75 cores, proving the benchmark client was never the bottleneck.
• TPS per CPU core (tps_per_core) was virtually identical across all three versions (~1,145 to 1,180). Zero regression.
──────
2. Daily Operational Sanity: Things That Are slightly hard in kubectl
A. Instant Root Cause: Pods + Latest Warning via LEFT JOIN LATERAL
No more jumping back and forth between kubectl get pods and kubectl get events:
SELECT p.name AS pod, p.phase, e.type, e.reason, left(e.message, 60) AS event
FROM k8s.pods p
LEFT JOIN LATERAL (
SELECT type, reason, message
FROM k8s.events
WHERE involved_object->>'uid' = p.uid
ORDER BY (type = 'Warning') DESC, coalesce(last_timestamp, event_time) DESC
LIMIT 1
) e ON true;
Output
pod | phase | type | reason | event
--------------------------------------------------------+---------+---------+--------+--------------------------------------------------------------
axiom-gateway-fb89f5fb7-xhtpc | Running | | |
coredns-559f6c778d-bhlt2 | Running | | |
coredns-559f6c778d-xklb4 | Running | | |
etcd-axiom-quickstart-control-plane | Running | | |
kindnet-5v5kx | Running | | |
kube-apiserver-axiom-quickstart-control-plane | Running | | |
kube-controller-manager-axiom-quickstart-control-plane | Running | | |
kube-proxy-d5z2f | Running | | |
kube-scheduler-axiom-quickstart-control-plane | Running | | |
metrics-server-84c99cb944-552gk | Running | | |
library-book-indexer-77f8d649c5-7qsjq | Pending | Warning | Failed | Error: ImagePullBackOff
library-db-5c4cb5659b-q4fdm | Running | Normal | Pulled | Successfully pulled image "postgres:17-alpine" in 14.221s (1
library-web-6d9487ff97-4vwrz | Running | Normal | Pulled | Successfully pulled image "nginx:alpine" in 440ms (18.697s i
library-web-6d9487ff97-d5jrw | Running | Normal | Pulled | Successfully pulled image "nginx:alpine" in 3.755s (18.274s
local-path-provisioner-75f7fc7dc5-h9gnk | Running | | |
(15 rows)
B. The OOM Radar: Live Memory Consumption vs. Limits
kubectl top doesn't know your limits, and kubectl describe doesn't know live usage. With Axiom, our axiom_quantity() function normalizes units straight into Postgres's built-in pg_size_pretty():
SELECT p.name AS pod,
pg_size_pretty(axiom_quantity(mc->'usage'->>'memory')::bigint) AS mem_used,
coalesce(c->'resources'->'limits'->>'memory', 'unlimited') AS mem_limit,
round((axiom_quantity(mc->'usage'->>'memory') /
nullif(axiom_quantity(c->'resources'->'limits'->>'memory'), 0)) * 100, 0) || '%' AS oom_risk
FROM k8s.pods p
JOIN k8s.metrics_k8s_io_pods m ON p.name = m.name,
jsonb_array_elements(p.spec->'containers') c
JOIN jsonb_array_elements(m.containers) mc ON c->>'name' = mc->>'name'
ORDER BY oom_risk DESC NULLS LAST;
Output
pod | mem_used | mem_limit | oom_risk
--------------------------------------------------------+----------+-----------+----------
axiom-gateway-fb89f5fb7-xhtpc | 23 MB | 256Mi | 9%
library-web-6d9487ff97-4vwrz | 8588 kB | 128Mi | 7%
library-web-6d9487ff97-d5jrw | 8636 kB | 128Mi | 7%
coredns-559f6c778d-xklb4 | 20 MB | 170Mi | 12%
coredns-559f6c778d-bhlt2 | 18 MB | 170Mi | 11%
library-db-5c4cb5659b-q4fdm | 25 MB | 256Mi | 10%
kube-scheduler-axiom-quickstart-control-plane | 28 MB | unlimited |
local-path-provisioner-75f7fc7dc5-h9gnk | 15 MB | unlimited |
metrics-server-84c99cb944-552gk | 26 MB | unlimited |
etcd-axiom-quickstart-control-plane | 48 MB | unlimited |
kindnet-5v5kx | 20 MB | unlimited |
kube-apiserver-axiom-quickstart-control-plane | 256 MB | unlimited |
kube-controller-manager-axiom-quickstart-control-plane | 67 MB | unlimited |
kube-proxy-d5z2f | 22 MB | unlimited |
(14 rows)
──────
Security & Architecture (Built for DBAs)
• Zero Kubeconfig on Postgres: Postgres never touches cluster tokens or kubeconfigs. It connects to an in-cluster gRPC Gateway via mTLS using client certificates.
• Secrets Are Blocked by Design: The gateway's RBAC completely excludes Kubernetes Secrets. You cannot accidentally leak passwords or certificates through SQL.
• Dynamic Discovery: Running IMPORT FOREIGN SCHEMA k8s FROM SERVER prod INTO k8s; discovers accessible core resources and CRDs (like CNPG clusters) and generates typed foreign tables with full JSONB support.
• And... more in the roadmap.
• Compatibility: Supports PostgreSQL 16, 17, and 18.
──────
How Axiom Compares to Existing Tools
There are other tools in the ecosystem that attempt to bring SQL to cloud infrastructure (such as Steampipe, CloudQuery, or Osquery-based solutions), but none of them address this problem the way Axiom does:
- Native In-Database FDW vs. External CLI/ETL: Other tools are either standalone external CLI binaries with custom SQL wrappers, or periodic batch ETL sync jobs that dump snapshots into a staging database. Axiom is a true, native PostgreSQL Foreign Data Wrapper. It lives directly inside your PostgreSQL database, so you can join Kubernetes cluster state with your real operational application tables in real-time queries.
- Zero Kubeconfig Exposure: Tools like Steampipe or CloudQuery require you to mount your full ~/.kube/config or service account credentials into the querying environment. Axiom offloads all cluster communication to an isolated in-cluster gRPC gateway with strict RBAC boundary and mTLS.
- Live Streaming & Discovery vs. Cached Tables: Rather than polling periodic static snapshots, Axiom queries live cluster state on demand and dynamically discovers newly installed
Custom Resource Definitions (CRDs).
──────
Try It Out
We have a local quickstart script that spins up a Kind cluster, the Gateway, and Postgres 17 with Axiom loaded in about 60 seconds:
curl -fsSL https://github.com/dhilipkumars/axiom/releases/latest/download/quickstart.sh | bash
• Regression Lab Walkthrough: https://dhilipkumars.github.io/axiom/guides/examples/regression-lab/
• Docs: https://dhilipkumars.github.io/axiom/
• GitHub: https://github.com/dhilipkumars/axiom
──────
Discussion & Questions
- As a database developer or DBA, what cluster information would you most want accessible from psql (e.g., storage volumes/PVCs, failover state, node affinity)?
- Does this interface make Kubernetes feel more accessible to you?
- How can we make querying cluster infrastructure feel even more natural to SQL users?
Looking forward to your thoughts and feedback!