feat: Add opt-in upgrade_drain_timeout to drain control nodes before an upgrade - #21
feat: Add opt-in upgrade_drain_timeout to drain control nodes before an upgrade#21Aureliolo wants to merge 1 commit into
Conversation
termination_grace_period_seconds cannot keep jobs alive across a version upgrade. Once the first pod running the new version registers as an instance, every control node still on the old version stops its own services, because cluster_node_heartbeat shuts a node down as soon as it sees a peer reporting a higher version. That happens from inside the container, so it preempts the PreStop hook and the jobs that hook was waiting for fail with "Task was canceled due to receiving a shutdown signal." Drain before the new version is applied instead. When the application image changes, disable each control node so that newly submitted jobs stay pending rather than being scheduled onto a pod that is about to be replaced, then wait for the jobs already running there to finish. The wait sits ahead of the schema migration, so draining nodes never run old code against a migrated database. Reaching the timeout does not abort the upgrade: it proceeds, and any jobs still running fail as they do today. Opt-in. upgrade_drain_timeout defaults to 0, which keeps the current behavior.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in upgrade_drain_timeout mechanism to proactively drain AWX control nodes before applying new deployment resources during an upgrade, preventing running jobs from being cut short by AWX’s version-skew self-shutdown behavior. This fits into the installer/operator reconcile flow by inserting a drain step immediately before “Apply deployment resources”, and extends the CRD + docs + Molecule coverage to support the new field.
Changes:
- Introduces
upgrade_drain_timeout(default0) and wires it into the installer reconcile to conditionally run a new drain task before upgrading deployments. - Adds
drain_control_nodes.ymlto disable task pods (instances) and wait for their running jobs to finish prior to rollout (without aborting the upgrade on timeout). - Updates CRD schema, user documentation, and Molecule scenario/spec + verification to include/assert the new field and ensure ordinary reconciles don’t disable instances.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| roles/installer/tasks/resources_configuration.yml | Calls the new drain task before applying deployment resources when upgrade_drain_timeout > 0. |
| roles/installer/tasks/drain_control_nodes.yml | Implements the pre-upgrade control-node disable + wait logic. |
| roles/installer/defaults/main.yml | Adds the new upgrade_drain_timeout default setting (opt-in). |
| molecule/default/verify.yml | Includes the new Molecule verification task file. |
| molecule/default/templates/awx_cr_molecule.yml.j2 | Sets upgrade_drain_timeout in the Molecule AWX CR template. |
| molecule/default/tasks/upgrade_drain_test.yml | Verifies that a reconcile with unchanged image does not disable instances even when upgrade_drain_timeout is set. |
| docs/user-guide/advanced-configuration/pods-termination-grace-period.md | Documents upgrade behavior and the new upgrade_drain_timeout option. |
| config/crd/bases/awx.ansible.com_awxs.yaml | Adds upgrade_drain_timeout to the CRD schema with default 0. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| command: awx-manage disable_instance --hostname {{ item }} --wait --retry=1 --retry_sleep=1 | ||
| loop: '{{ _control_hostnames }}' | ||
| register: _drain_result | ||
| until: _drain_result.return_code | default(1) == 0 |
There was a problem hiding this comment.
until retries per loop item, and during those retries the registered variable holds
that item's own result. The .results aggregation only exists once the loop has
finished, which is after until has been evaluated, so return_code is the right
reference here.
Checked on ansible-core 2.18.18 (the version in the operator image) and on 2.21.1,
using the same condition shape as this task, including the | default(1) fallback
that would have masked the problem if it were real:
- ansible.builtin.command: /bin/true
loop: [one, two]
register: r
until: r.rc | default(1) == 0
retries: 5
delay: 1
item=one rc=0 attempts=1
item=two rc=0 attempts=1Both items satisfy the condition on the first attempt. If it were reading the
aggregate, r.rc would be undefined, default(1) would evaluate it to false, and
each item would show attempts=5 followed by a "Failed until condition" failure.
Leaving the task as is.
SUMMARY
termination_grace_period_secondsdoes not keep jobs alive across a version upgrade, which is the case its own documentation says it covers ("AWX upgrade by the operator").As soon as the first pod running the new version registers as an instance, every control node still on the old version stops its own services, because
cluster_node_heartbeatshuts a node down when it sees a peer reporting a higher version:That shutdown comes from inside the container, so it preempts the
PreStophook, which gates only kubelet's SIGTERM. Observed on a production cluster during a 25.4.0 -> 25.5.1 upgrade, from one pod's logs: the drain hook was already waiting on the right job when the node killed it seven seconds later.The drain machinery works, it just runs too late. This adds
upgrade_drain_timeout, which drains ahead of the rollout instead: when the application image changes, each control node is disabled so that newly submitted jobs staypendingrather than being scheduled onto a pod about to be replaced, and the operator then waits for the jobs already running there.The drain is placed immediately before
Apply deployment resources, which is the last point where the cluster is still wholly on the old version.migrate_schema.ymlruns later ininstall.yml, so draining nodes never run old code against a migrated database. That ordering is deliberate and is why this lives in the operator rather than incluster_node_heartbeat.Reaching the timeout does not abort the upgrade. It proceeds and any jobs still running fail as they do today, so the worst case is current behaviour.
Opt-in:
upgrade_drain_timeoutdefaults to0, which changes nothing for existing users. An alternative is to key this offtermination_grace_period_seconds, since anyone who set that has already asked for jobs to be respected on termination. Happy to switch if you prefer one knob to two.ISSUE TYPE
COMPONENT NAME
ADDITIONAL INFORMATION
Skipped unless all of the following hold, so ordinary reconciles are unaffected:
upgrade_drain_timeout > 0awx-manageinAll control nodes are disabled before any of them is waited on: an instance that is still enabled keeps accepting work, so draining them one at a time would not converge. The wait uses
disable_instance --wait --retry=1as a probe and retries from Ansible, rather than holding a singlek8s_execstream open for the whole drain.Known trade-off: if the operator dies between the drain and the apply, the instances stay disabled until a reconcile completes. The next reconcile drains and applies, and the rollout replaces those pods with freshly registered, enabled ones. Note also there is no
awx-manage enable_instance; recovery in that window is aPATCHon the instance.Verified against a live operator built from this branch on a throwaway cluster:
upgrade_drain_timeout=60on the CR)false_condition: "_deployed_image != _image"), with zerodisable_instancecallsyamllint -c .yamllintclean;ansible-lintreports only rules that existing files in the repo also reportmolecule verifydid not complete in my environment: the deployment never became ready there for reasons unrelated to this change (a pod networking fault in my local cluster), so the added test's final assertion, that an ordinary reconcile leaves every control node enabled, did not execute. The scenario also never changes the application image, so it does not exercise the drain body itself. Flagging both rather than implying coverage I did not get.