Skip to content

Blog

Crossplane 2.2: What's New (and Why It Matters)

Banner image Banner image

Crossplane 2.2: What's New (and Why It Matters)

Here's a scenario that'll feel familiar if you've been writing Crossplane composition functions for a while. You've got a multi-step pipeline, something's failing, and you're staring at the Composite Resource conditions trying to figure out which function in the chain produced the wrong output. You can see that it failed. You can't see what was sent to the function or what came back. So you add debug logging, redeploy, reproduce the failure, squint at the logs, and eventually piece it together. It's not broken, exactly. It's just slower than it needs to be.

Crossplane 2.2 fixes that with the Pipeline Inspector. And honestly that's the headline — the rest of the release is the kind of solid incremental work that makes a platform more reliable without asking you to change much. Composition functions got smarter about version-awareness, a meaningful bug fix landed for function selectors, and the MRD controller got quieter in busy clusters. Worth knowing what changed before you upgrade.

DevEx Metrics That Matter (and How to Automate Them)

Banner image Banner image

DevEx Metrics That Matter (and How to Automate Them)

Here's a scenario I find genuinely unsettling: a platform team that's shipping consistently, 95% test coverage, zero critical vulnerabilities, clean backlog. By every measure they're tracking, things are good.

Lead time is 14 days. Nobody's asking why.

Fourteen days from first commit to production isn't good — it's a quiet disaster. Developers are sitting on their changes for two weeks. Feedback loops are long, so rework costs compound. Every release is large, which makes deployment anxiety real and rollbacks terrifying. And the platform team has no visibility into any of it, because they're measuring the wrong things.

That's not a made-up story. It's distressingly common.

DORA metrics exist precisely to break this pattern. Four numbers — lead time, deployment frequency, change failure rate, and MTTR — that tell you whether software delivery is actually getting better or quietly getting worse. They're not perfect. But they're the closest thing the industry has converged on, and they measure outcomes instead of activity. That distinction matters enormously.

FluxCD 2.8 GA: Helm v4, Server-Side Apply, and a Release Inventory That Actually Works

Banner image Banner image

You know that moment when you're staring at a HelmRelease that says Ready: True and you're still not quite sure what's actually running in the cluster? You've got the chart, you've got the values, but the definitive answer to "what Kubernetes resources did this release create?" has always required either running helm get manifest or squinting at the chart templates until something clicked. It's a gap. And it matters more than it sounds because drift detection, auditing, and debugging all depend on being able to compare what Flux says it deployed against what's actually there.

FluxCD 2.8, released in February 2026, fixes that with the HelmRelease resource inventory — .status.inventory now tracks every resource a release created. That's the headline for me. But there's more: Helm v4 is now the default, which brings server-side apply and kstatus health checking, and the way those work has implications for how upgrades and health assessment behave. Not scary changes, but worth understanding before you upgrade.

Gemini API Flex vs Priority: The Tier Decision You're Probably Getting Wrong

Banner image Banner image

Here's a scenario I've seen play out more than once. A platform team starts building AI pipelines — embeddings, document classification, a bit of customer-facing chat. They use the default API tier for everything because it works and they're moving fast. Then the bill arrives. It's not just higher than expected — it's three times what it needed to be. They were running nightly batch jobs through the same tier as live user requests, paying premium pricing for work nobody was watching.

The flip side happens too. A cost-conscious team spots the 50% saving on Flex tier, switches everything across in an afternoon, and calls it a win. Then peak hours hit, and customer-facing chat starts throwing 4xx errors at random. Support tickets pile up. The Flex tier is doing exactly what it's designed to do — prioritising capacity for higher tiers when things get busy. But nobody told the team that.

Here's what I actually believe: tier selection isn't a billing detail. It's an architecture decision, and it deserves the same care you'd give to any other reliability tradeoff. The good news is the mental model is simple once you have it. One question does most of the work: will a human notice if this takes three times longer? If yes, pay for Priority or Standard. If no, use Flex and pocket the saving.

Gemma 4 at the Edge: Agentic Skills in Production

Banner image Banner image

Most conversations about running AI agents start with "which cloud provider?" That's the wrong starting question for a growing number of use cases. Sometimes the data can't leave the building. Sometimes there's no reliable connection. Sometimes you're deploying to a Jetson Orin on a factory floor where a 200ms API round-trip is unacceptable, let alone a network timeout.

Google's Gemma 4, released in April 2026 under Apache 2.0, is the first open model family I've seen that makes genuinely capable agentic workloads viable at the edge without heroic engineering. Not "we ran inference locally" viable — actually capable of planning, tool use, and multi-step task execution, on hardware that fits in a laptop bag. That changes something.

GitOps + AI Drift Detection: Catch It Before Prod

Banner image Banner image

You know that moment when someone patches a ConfigMap at 2am, the fix works, everyone goes back to sleep, and nobody opens a PR? Three weeks later the next deploy reverts it. The incident repeats. And nobody immediately knows why, because the change lived only in the cluster, never in Git.

That's drift. And honestly, it's not a discipline problem — it's a systems problem. Every team that gives engineers direct cluster access alongside automated sync will hit this eventually. The gap between what Git says and what's actually running is invisible until it hurts.

AI makes that gap visible. Not by adding more dashboards, but by turning drift detection into an automated loop that classifies what it finds and opens a targeted PR when something needs fixing.

C4 Architecture Diagram

Quick takeaways

  • Drift is rarely all-or-nothing — most clusters have harmless and risky divergence coexisting at any given moment
  • Classification matters more than detection — knowing which drift to act on is the genuinely hard part
  • Fix PRs should be targeted, not bulk — one drift, one PR, one reviewer who actually has the context
  • The loop runs on a schedule, not just on deploy — because drift doesn't wait for you to ship

What drift actually looks like

ArgoCD will tell you an Application is OutOfSync. What it won't tell you is whether that's because:

  • someone patched a resource directly (risky — it'll silently revert on the next sync, and surprise everyone)
  • a controller updated status fields (harmless — this is expected behaviour, leave it alone)
  • a Helm chart generated slightly different output because a values file changed (worth reviewing, but not panicking about)
  • a sidecar injector added annotations at admission (harmless — Istio and Linkerd do this constantly)

The OutOfSync noise problem

Without classification, every OutOfSync looks the same. Teams start ignoring the alerts. Real drift hides in the noise. And then the 2am ConfigMap situation happens again. Detection without classification is worse than useless — it trains your team to treat real signals as noise.


The detection loop

Run this on a schedule — every 30 minutes in non-prod, every 15 in prod. Set it and let it run.

1. Query ArgoCD for OutOfSync Applications
2. For each application: run kubectl diff against live cluster
3. Feed the diff to an AI classifier
4. Classifier labels each diff: harmless / needs-review / risky
5. For risky or needs-review: open a targeted PR with the fix
6. Post a summary to Slack

Apply this: why PRs, not auto-sync

Auto-sync reverts drift immediately. But it doesn't capture why it happened. The PR approach creates a record of what diverged and forces an actual conversation: is the cluster wrong, or is Git wrong? Sometimes Git is wrong. You want that question asked, and you want the answer in writing.


1) Querying ArgoCD for drift

#!/bin/bash
# scripts/detect-drift.sh

ARGOCD_SERVER=${ARGOCD_SERVER:-"argocd-server.argocd.svc"}
OUT_OF_SYNC=$(argocd app list --output json \
  --server $ARGOCD_SERVER \
  | jq -r '.[] | select(.status.sync.status == "OutOfSync") | .metadata.name')

for APP in $OUT_OF_SYNC; do
  echo "Checking drift for: $APP"
  argocd app diff $APP --server $ARGOCD_SERVER > /tmp/drift-$APP.diff
done

2) AI classification of the diff

# scripts/classify-drift.py
import anthropic
import sys

client = anthropic.Anthropic()

CLASSIFIER_PROMPT = """
You are a Kubernetes drift classifier. You will receive a kubectl diff showing
the difference between Git state (desired) and live cluster state (actual).

Classify each changed resource as one of:
- HARMLESS: controller-managed fields, status updates, injected annotations
- NEEDS_REVIEW: values changed by Helm or config, could be intentional or drift
- RISKY: manual edits to spec fields, security settings, resource limits, replicas

Output JSON: {"resources": [{"name": "...", "kind": "...", "verdict": "...", "reason": "..."}]}
"""

def classify_drift(diff_content: str, app_name: str) -> dict:
    message = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"Application: {app_name}\n\nDiff:\n{diff_content}"
        }],
        system=CLASSIFIER_PROMPT
    )
    return message.content[0].text

if __name__ == "__main__":
    app_name = sys.argv[1]
    diff_file = sys.argv[2]
    with open(diff_file) as f:
        diff = f.read()
    print(classify_drift(diff, app_name))

Three classification buckets

HARMLESS (skip), NEEDS_REVIEW (open a GitHub Issue), RISKY (open a PR immediately). Setting a confidence threshold below which you open an Issue instead of a PR keeps the signal clean — reviewers only see PRs for things the classifier is confident about.


3) Opening a targeted fix PR

When the classifier returns RISKY, the agent opens a PR that restores the Git state for that specific resource. Just that resource. Nothing else gets touched, nothing gets auto-applied. The PR is the decision point.

# scripts/open-fix-pr.py
import subprocess
import json
from github import Github

def open_drift_fix_pr(app_name: str, resource: dict, diff: str):
    """Open a PR for a single drifted resource."""
    branch = f"drift-fix/{app_name}/{resource['kind']}-{resource['name']}"

    # Create branch
    subprocess.run(["git", "checkout", "-b", branch], check=True)

    # The fix is recorded in the PR body, not applied as a code change
    # (the sync itself is the fix - this PR is the review gate)
    pr_body = f"""## Drift detected: {app_name}

**Resource:** `{resource['kind']}/{resource['name']}`  
**Verdict:** {resource['verdict']}  
**Reason:** {resource['reason']}

### What changed in the cluster

```diff
{diff}

Review whether the live state represents an intentional change that should be committed to Git, or whether the cluster should be synced back to Git state.

  • If the cluster change was intentional: update the Git config and close this PR
  • If it was accidental: approve this PR to acknowledge, then trigger ArgoCD sync

Generated by drift-detection workflow """

g = Github(os.environ["GITHUB_TOKEN"])
repo = g.get_repo(os.environ["GITHUB_REPO"])

pr = repo.create_pull(
    title=f"[Drift] {app_name}: {resource['kind']}/{resource['name']}",
    body=pr_body,
    head=branch,
    base="main",
    draft=False
)
return pr.html_url

``` If you're wondering what governs these PRs before they merge — Kyverno admission policies, OPA Terraform checks, the whole validation layer — that's covered in Policy as Code + Agents.


4) GitHub Actions workflow

```yaml

.github/workflows/drift-detection.yml

name: GitOps Drift Detection on: schedule: - cron: '/30 * * * ' # every 30 minutes workflow_dispatch: jobs: detect-drift: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install ArgoCD CLI run: | curl -sSL -o /usr/local/bin/argocd \ https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 chmod +x /usr/local/bin/argocd - name: Detect drift env: ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }} ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }} run: bash scripts/detect-drift.sh - name: Classify and open PRs env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPO: ${{ github.repository }} run: python scripts/classify-and-pr.py - name: Post summary to Slack if: always() env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} run: python scripts/post-drift-summary.py ```


What harmless drift looks like (skip these)

Some patterns are always safe to ignore. Hard-code them into your classifier so they never make it into a PR:

  • status.* fields (controller-managed — you didn't write these, you don't own them)
  • metadata.resourceVersion, metadata.uid, metadata.creationTimestamp
  • metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"]
  • Sidecar containers injected by Istio or Linkerd
  • spec.nodeName on Pods

Apply this ignore list from day one

If a field isn't in your Git manifests, a controller added it. That's not your concern. Don't waste reviewer attention on it. Building this ignore list into the classifier prompt is what keeps your PR volume manageable — without it, every Istio-injected annotation becomes a drift alert.


Common objections

"Won't this create too many PRs?"
Only if you have too much drift — which is actually the point. A noisy drift detector is telling you something real: your cluster isn't being managed the way you think it is. Don't silence the noise. Fix what's causing it.

"What if the PR-opener gets it wrong?"
Nothing happens to the cluster because a PR was opened. It's a review gate, not an auto-apply. Engineers look at it, decide, and act. If you want to understand the three-tier approval model for which of these PRs actually needs a human before merging, Agentic Change Management covers that in detail.

"ArgoCD auto-sync already handles this."
It reverts. But it doesn't explain. Auto-sync can't tell you whether the cluster was wrong or Git was wrong — it just picks Git. Sometimes that's right. Sometimes it quietly reverts an intentional hotfix. The PR forces the question to be asked, and gets the answer into the record.


Frequently asked questions

What is GitOps drift and how does it happen?

It's when the live state of your cluster diverges from what Git says it should be. Happens all the time: someone applies a change manually at 2am (bypassing GitOps entirely), a controller overwrites a field it owns, or an external process modifies a resource without anyone creating a corresponding commit. The scary part isn't that it happens — it's that it's invisible until something breaks.

How do you detect drift in ArgoCD automatically?

Query ArgoCD's API for anything with OutOfSync status on a schedule — every 30 minutes is a reasonable starting point. For each out-of-sync resource, pull the diff between desired and live state and run it through an AI classifier. The classifier tells you whether this needs a fix PR or whether you can safely ignore it. That classification step is what separates signal from noise.

What's the difference between harmless and dangerous drift?

Harmless drift is fields Kubernetes manages automatically and you'd never put in a manifest yourself — resourceVersion, uid, creationTimestamp, status fields, HPA-managed replica counts. Dangerous drift is the stuff that matters: manually applied security group changes, RBAC modifications, or config values that silently differ from what's in Git. The risk with dangerous drift is that it reverts on the next sync with no warning.

How do you prevent an AI agent from opening incorrect fix PRs?

Set a confidence threshold and don't open PRs below it. For medium-confidence cases, open a GitHub Issue for human review instead of a PR. And include the full diff plus the model's reasoning in every PR body — reviewers should be able to verify the classification themselves without trusting the agent blindly.

Is this compatible with multi-cluster ArgoCD setups?

Yes. The detection script runs against ArgoCD's API, which can manage multiple clusters. You scope the classifier per cluster by including the cluster name and environment in the classifier prompt — this lets it apply different risk thresholds (staging vs production) and different ignore lists per environment.


What you get

  • Drift is visible within 30 minutes of occurring, not on the next 2am incident
  • Every manual cluster edit creates a PR that forces someone to actually decide what to do
  • The audit trail is in Git, where it belongs and where anyone can find it
  • Engineers stop treating OutOfSync as noise because it's now classified and genuinely actionable

Walkthrough files

  • scripts/detect-drift.sh — query ArgoCD for out-of-sync apps
  • scripts/classify-drift.py — AI classification of kubectl diff output
  • scripts/open-fix-pr.py — targeted PR creation per drifted resource
  • .github/workflows/drift-detection.yml — scheduled detection workflow

For the change management layer that controls how these fix PRs actually get reviewed and merged, see Agentic Change Management.

GitOps Policy‑as‑Code with Argo CD + Kyverno

Banner image Banner image

We caught it in a post-incident review. A deployment had been running in production for three weeks without resource limits — not because anyone removed them deliberately, but because a PR that skipped them went through the GitOps workflow exactly like a compliant one. ArgoCD synced it. The CI pipeline passed. Nobody noticed until a memory spike took the pod down during a load test.

GitOps solves the consistency problem beautifully. Everything in Git, every change a PR, every deploy auditable. What it doesn't solve is the correctness problem. A PR that removes resource limits, exposes a service to the public internet, or skips required labels goes through the exact same workflow as a perfectly compliant one. ArgoCD doesn't know the difference — it syncs what Git says.

Kyverno is how you add the correctness layer. It's a Kubernetes-native policy engine — policies are Kubernetes resources, no Rego required — and it runs in the cluster as an admission controller. Pair it with ArgoCD and you get deployment consistency and policy enforcement, without adding a separate policy system that lives outside your GitOps workflow.

From Alert to Root Cause: HolmesGPT in Production

Banner image Banner image

You know the moment. PagerDuty fires at 2am. You're pulling up kubectl, squinting at pod logs, trying to remember which namespace this service actually lives in. Someone's pinging Slack. The on-call channel is filling up. You spend the first twelve minutes just getting oriented — what's broken, where, and why — before you've even formed a hypothesis.

HolmesGPT collapses that twelve minutes into eleven seconds. Not by replacing your judgement. By doing the orientation work for you.

Kubernetes v1.36: Migrate Off ingress-nginx Before You Read the Rest

Banner image Banner image

Here's the thing about ingress-nginx: it's everywhere, it's boring, and it works. That's exactly why this is hard. The CNCF moved it to end-of-life in March 2026. The sig-network team is no longer cutting releases. There will be no security patches. And if you've been running it quietly in the background — which most of us have — it's been accumulating unpatched CVEs since then with nobody upstream doing anything about it. The project repository still exists. The community hasn't vanished overnight. But the organised maintenance is gone, and that means your ingress layer is now in the same category as anything else you'd describe as "unsupported dependency." That's not a comfortable place for traffic handling.

Look, I know migration from ingress-nginx sounds like a weekend you don't have. It's not usually a single Helm value change. But the risk calculus has shifted fundamentally. Before March 2026, a known CVE in ingress-nginx meant "there'll be a patch release in a few days." After March 2026, it means "you're carrying this forever unless you move." That's a different situation, and it deserves a different response than "we'll get to it."

The good news — and there's genuinely good news in v1.36 — is that once you've dealt with the ingress situation, this is a solid release. User Namespaces goes GA. SELinux volume mounting goes GA. DRA device taints graduate to beta. If you're running multi-tenant clusters or SELinux-enforcing nodes, there are real operational wins here. But sort the ingress-nginx situation first.

OpenClaw 2026.4 Release: What Changed

Banner image Banner image

Something I didn't expect: the most useful thing in OpenClaw 2026.4 isn't one of the headline features. It's a fix to a problem that was driving me quietly mad.

My OpenClaw instance runs across two contexts — a work Slack and a personal WhatsApp. After a few weeks, the memory model had blended them into a single soup. It knew about both my team's sprint retrospective and a conversation with my family about where to go on holiday, and every now and then something from one context would surface in the other in a way that felt off. Not wrong, exactly. Just.. the wrong kind of thing to remember for this conversation.

The 2026.4 memory filters fix that. You can now scope what each channel remembers and reads, which sounds like a small thing until you've run an AI assistant across mixed personal and professional channels for a few months.

That said, there's more here than memory: four new stable channels, meaningful run-steering improvements, and the groundwork for Bedrock and NVIDIA inference backends landing in 2026.5. Here's what actually changed and what's worth configuring before you upgrade.