PodCreate icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Patch icon
Schedule icon

Resume a forgotten paused Deployment rollout with approval

Detect an opted-in paused Kubernetes Deployment, approve a conditional resume and verify the intended template reaches Ready replicas.

Categories
Infrastructure

Safety

kubectl rollout pause is meant to be temporary. A Deployment left paused after maintenance accepts every new template but never rolls it out: CI reports a successful deploy while the old pods keep serving. Resuming it starts that rollout immediately, so this flow refuses by default and every guard below is load bearing. Each one was proven by its own run.

Guard What it stops
Opt-in annotation kestra.io/resume-paused-rollout: "true" resuming a Deployment nobody enrolled
Hold annotation kestra.io/hold-paused-rollout resuming a pause that is intentional
Grace window on the DeploymentPaused condition resuming a pause that is still part of a maintenance window
Pending template check resuming when no new template is waiting, which would do nothing useful
Replica bounds (max_replicas, zero replicas refused) a resume that starts a large or empty rollout
auto_remediate false, dry_run true, Pause three separate gates before one patch
JSON Patch test on UID, generation, annotations and the full spec a patch landing on a Deployment that changed while the approval waited
Rollout verification after the patch a patch that returned 200 being reported as a fix

How it works

  1. inventory (io.kestra.plugin.kubernetes.core.PodCreate) runs a short-lived reader pod with a get/list-only ServiceAccount. It reads the Deployment with its ReplicaSets and pods. It classifies the target as not_paused, not_opted_in, intentional_pause_protected, deleting, no_pause_timestamp, within_grace, replica_bound, no_existing_replicaset, no_pending_template or eligible. It saves the UID, generation, annotations and full spec of the plan.
  2. report logs the verdict, age and ReplicaSets. eligible (If) continues only for eligible and posts the plan to Slack with notify_plan.
  3. may_change (If) requires auto_remediate: true and dry_run: false. Otherwise safe_mode logs and the run ends.
  4. approve_resume (Pause) waits for a person. approved reads onResume.approved ?? false, so a resume with no answer is a refusal.
  5. recheck reads the Deployment again and fails if it is no longer eligible or if anything in the saved snapshot changed.
  6. resume (io.kestra.plugin.kubernetes.kubectl.Patch, JSON Patch) sends test operations for the UID, generation, annotations and full spec, then replaces only spec.paused. The API server rejects the whole patch if any test fails.
  7. verify reads again until verify_seconds: spec.paused false, generation up by exactly one, observedGeneration caught up, updated, available and total replicas equal to the desired count. That many pods must be Ready, owned by the intended ReplicaSet and running the intended images. Anything less fails the run. notify_failure posts failures to Slack.

A Schedule ships disabled: true and forces dry_run: true with auto_remediate: false. Flow concurrency is 1.

What you get

  • A daily report of whether each enrolled Deployment is stuck paused with a template waiting.
  • A resume that only happens on the exact object that was approved.
  • Proof that the intended template is Ready. When it is not, the run fails and says so.
  • Deployments paused on purpose or never enrolled are left alone.

Who it's for

Platform and SRE teams who pause rollouts during maintenance or incidents and need to know none was left paused.

Why orchestrate this with Kestra

A forgotten pause has no alert of its own: the Deployment reports healthy and CI reports success. Kestra makes it a scheduled check, holds the change at a Pause for a person, records the approved snapshot next to the conditional patch. The rollout verdict stays in the same execution. Kubernetes enforces the snapshot with JSON Patch tests.

Tested end to end

Kestra 2.0.5, Kubernetes plugin 1.11.4, Slack 2.1.0, k3s v1.31.5+k3s1 in Docker. Validate was clean: no constraints, warnings or deprecations. Every state below was read with kubectl, not from the flow's output.

The fixture: a Deployment target with 2 replicas serving template original, paused with kubectl rollout pause, then given a new template pending. The new template has no ReplicaSet, so it never rolls out.

deployment  paused  generation  updated  available  template running
target      true    4           0        2          original (2 Ready pods)
unrelated   false   1           2        2          original
Case Execution Result
Healthy (not paused) EjLu9PR2rqS2NIBvurgqD SUCCESS. not_paused, nothing written
Dry run 2nzVpxSaLglXpXJugiCclP SUCCESS. eligible, still paused, generation 4, full spec unchanged, no patch
Declined approval 3NknStzfaACfGA76467Wrc SUCCESS. UID, generation and spec unchanged
Remediation 7929nKgPfakOSpjn50nvzR SUCCESS. paused false, generation 4 to 5, observedGeneration 5, 2 of 2 Ready pods on the pending template. unrelated UID and spec unchanged
Repeat 5SzhNoz6MECM9ttm92jT5w SUCCESS. Nothing to resume, no patch
Ineffective rollout 3rsHLOD0v3reijyXpbsh5W FAILED as intended. The patch succeeded, but the new template had a broken readiness probe: 1 new replica, 0 Ready, the 2 old pods still available. Verification failed the run
Template changed during the Pause 1xoRLgOyIUA7rLeh0hCNlL FAILED as intended. recheck refused, no patch sent, still paused with the operator's new template kept
Patch refused by RBAC 4LU0njiEoj9W4FYx8fvIds FAILED as intended. Token without patch: reads succeeded, the API returned 403, still paused, spec unchanged
Retry with a permitted token lpBqL9HgOEDu5ECHAsts3 SUCCESS. Generation up by exactly one, 2 Ready pods
Hold annotation dV7closPAhWx2T8OADkec SUCCESS. intentional_pause_protected, nothing written
No opt-in 4At4rTppOJfgAMEAleFscB SUCCESS. not_opted_in, nothing written
Within grace 4hSSwrdTm338Xv2yBxQP0 SUCCESS. within_grace, nothing written
Zero replicas 4aMjKbiUixQ455tMRepBRT SUCCESS. replica_bound, nothing written
Above the replica ceiling 6Q5ceDC9N137JQ8A4Do1IF SUCCESS. replica_bound, nothing written
Paused with no pending template 31wfkyJddQ1WLaHpOzKQk3 SUCCESS. no_pending_template, nothing written

The remediation set was repeated in a fresh namespace with the same results. An enabled copy of the Schedule (3heHM0sZhavmVFWpz4I8Lx) ran the same tasks in safe mode. The shipped trigger stays disabled. Slack was a local HTTP sink. The pause age was set by patching the DeploymentPaused condition timestamp, so the 30-minute grace did not have to elapse. Pause state and templates were real spec changes.

Prerequisites

  • Kubernetes with the apps/v1 Deployment API. Tested on k3s 1.31.5 only.
  • Kestra 2.0.5 with the Kubernetes and Slack plugins.
  • The Deployment selector must use matchLabels.
  • Minimum permissions, scoped to the target namespace:
    • reader ServiceAccount (reader_service_account): get, list on deployments, replicasets and pods. It has no write permission;
    • Kestra token (K8S_TOKEN): create, get, list, watch, delete on pods, create on pods/exec, get on pods/log, list on events, plus get, patch on the target Deployment.

Kubernetes RBAC cannot limit which ServiceAccount a created pod uses, so run the flow in a dedicated namespace or behind an admission policy.

Secrets

  • K8S_MASTER_URL: Kubernetes API server URL.
  • K8S_TOKEN: bearer token with the permissions above.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for findings and failures.

Inputs

Input Default Purpose
target_namespace production Namespace of the Deployment
deployment_name none The one Deployment to check
reader_service_account kestra-rollout-reader Read-only ServiceAccount for the reader pod
min_paused_age_minutes 30 Grace before a pause is treated as forgotten
max_replicas 10 Largest Deployment the flow will resume
max_objects 100 Read bound for ReplicaSets and pods
verify_seconds 120 Time allowed for the rollout to become Ready
dry_run true Report only
auto_remediate false Ask for approval to resume
trust_certs false Keep false with a trusted API certificate
reader_image, file_sidecar_image pinned by digest Reader and output sidecar images

Quick start

  1. Create the reader ServiceAccount and the token permissions above. Add the three secrets.
  2. Annotate the Deployment: kubectl annotate deploy/<name> kestra.io/resume-paused-rollout=true.
  3. Execute with deployment_name set and the defaults. Read the verdict and the pending template.
  4. Execute with dry_run: false and auto_remediate: true, check the template, then approve the Pause.
  5. Read the verification lines: generation, Ready pods and image references.
  6. Set deployment_name in the Schedule, then enable it for a daily check.

How to extend

  • Add an application health check after Kubernetes readiness, for example an HTTP probe against the service.
  • To cover several Deployments, run one execution per target, so each keeps its own approval and snapshot.
  • Support matchExpressions selectors with the same bounded, owned-resource checks.

Limits: the flow verifies Kubernetes readiness, not application behaviour or image provenance. It spares a Deployment that is already unpaused even if its rollout is failing. It never rolls back, because the approved template is the one the team pushed. Flow concurrency serializes this flow only. The JSON Patch tests protect the write itself, not later changes by other controllers.

Links

This blueprint was created by zkasuran.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.