SuperStackDevelopment Studio
Work
DevOps Engineering·

Fintech Automation Platform

Kubernetes Migration & Event-Driven Scaling for a Browser Agent Fleet

Migrated stateful browser automation workloads from StatefulSets to Deployments, wired KEDA event-driven auto-scaling to queue depth, and fixed a CI pipeline tagging bug that was silently shipping stale images to production.

KubernetesKEDAEKSBitbucket Pipelines

Overview

Browser automation at scale is deceptively hard. Each agent carries live session state and per-agent configuration that make the workload feel stateful — but it is actually stateless once you design around it correctly. This client had reached a point where their StatefulSet-based agent fleet was a bottleneck: scaling up required manual intervention, and scaling down risked evicting agents mid-session.

The engagement covered two parallel tracks: rearchitecting the workload topology, and fixing a shipping problem in the Bitbucket Pipelines CI/CD setup that had been causing stale image tags to reach production.

The Problem

The agent fleet was deployed as a Kubernetes StatefulSet — the standard choice when pods need stable network identities or persistent local storage. In this case, neither requirement actually applied once the architecture was re-examined. The stateful framing had been inherited from an early prototype and never revisited.

Consequences:

  • No horizontal auto-scaling: HPAs cannot target StatefulSets with rolling pod management effectively; KEDA support for StatefulSets is limited.
  • Slow scale-out: new pods had to be brought up sequentially, not in parallel.
  • Eviction risk: when scaling down, Kubernetes has no built-in way to protect a pod that is actively processing a job.
  • CI confusion: a latest-tag pinning bug in the pipeline meant Kubernetes was sometimes pulling a cached older image rather than the newly built one, causing silent production mismatches.

The Solution

StatefulSet → Deployment Migration

The migration required verifying that no pod identity assumptions existed in the application code — no $(POD_NAME) hostnames, no persistent volume claims per pod, no ordinal-indexed configuration. Once confirmed, the workload was converted to a standard Deployment.

This unlocked:

  • Parallel scale-out: new replicas come up simultaneously rather than one by one.
  • KEDA compatibility: KEDA's ScaledObject can target Deployments natively across all scaler types.
  • Normal rolling updates: RollingUpdate strategy with proper maxSurge/maxUnavailable tuning.

KEDA Event-Driven Auto-Scaling

KEDA (Kubernetes Event-Driven Autoscaler) was introduced to drive replica count from queue depth rather than CPU/memory averages — a much more accurate signal for a job-processing workload.

# ScaledObject targeting the agent Deployment
spec:
  scaleTargetRef:
    name: agent-worker
  minReplicaCount: 2
  maxReplicaCount: 20
  triggers:
    - type: rabbitmq
      metadata:
        queueName: agent-jobs
        queueLength: "5"

At idle the fleet holds a minimum warm pool; as the queue grows, KEDA scales out to cap latency; as it drains, it scales back in.

Pod Deletion Cost — Protecting Active Agents

The remaining problem was controlled scale-in. When KEDA reduces replicas, Kubernetes picks pods to terminate based on various heuristics. An agent mid-session should not be the one to go.

The solution uses the controller.kubernetes.io/pod-deletion-cost annotation, which the agent process writes to itself via the Downward API and a small patch call to the Kubernetes API server:

# agent sets a high deletion cost when actively processing
patch_pod_annotation(
    pod_name=os.environ["POD_NAME"],
    annotation={"controller.kubernetes.io/pod-deletion-cost": "1000"},
)

# and resets it when idle / between jobs
patch_pod_annotation(
    pod_name=os.environ["POD_NAME"],
    annotation={"controller.kubernetes.io/pod-deletion-cost": "0"},
)

Kubernetes prefers to terminate lower-cost pods, so idle agents get evicted first and active sessions are left to finish cleanly.

CI/CD Pipeline Fix — Deterministic Image Tags

The Bitbucket Pipelines config was tagging every build image as latest in addition to the commit SHA tag, and the Kubernetes manifests were referencing latest. The result: kubectl rollout would succeed but pods would sometimes pull a cached latest from the node, silently running old code.

Fix: remove the latest tag from all manifests and deploy scripts; pin every deployment to the explicit commit SHA tag built in that pipeline run.

# before — unreliable
image: registry.example.com/agent-worker:latest

# after — deterministic
image: registry.example.com/agent-worker:${BITBUCKET_COMMIT}

This alone eliminated a class of "why is my fix not working in prod" incidents.

Observability and Day-2 Operations

Scaling behaviour you can't see is scaling behaviour you can't trust. The platform runs a full observability stack across a dual-cluster EKS setup (UAT and production):

  • Prometheus scraping cluster, KEDA and application metrics — queue depth, replica counts and per-agent job timings on shared dashboards
  • Grafana dashboards tracking the scale-out/scale-in cycle against queue pressure, so capacity tuning is a data decision
  • Loki for centralised logs across the agent fleet — one query instead of kubectl logs archaeology across twenty pods
  • Alertmanager routing that pages on symptoms (queue latency breaching SLO) rather than causes (a single pod restart)

The same stack underpins incident response across the wider platform — the difference between "users are reporting failures" and "queue latency crossed threshold four minutes ago, here's the deploy that did it."

  • Kubernetes
  • KEDA
  • AWS EKS
  • Bitbucket Pipelines
  • Prometheus / Grafana
  • Loki / Alertmanager
  • RabbitMQ
  • Python

Outcome

  • Agent fleet scales automatically from a minimum warm pool to 20 replicas based on queue depth, with no manual intervention required.
  • Pod deletion cost scheduling prevents active browser sessions from being terminated mid-job during scale-in events.
  • The CI/CD pipeline now ships a deterministic image reference on every deploy, eliminating silent image staleness.
  • Rolling deploys to the agent fleet are zero-downtime and take roughly 90 seconds end-to-end.
  • Queue-depth, scaling and latency metrics live on shared Grafana dashboards — capacity decisions are made from data, and incidents are caught by alerts rather than user reports.

Need a hand with this? Tell us what's broken.

30 minutes, no slide deck. If we're not the right fit, we'll say so.