Skip to content

fix(ci): break orphaned TF state lock before UAT teardown - #2049

Merged
mchmarny merged 2 commits into
NVIDIA:mainfrom
njhensley:ci/uat-teardown-break-stale-tf-lock
Aug 5, 2026
Merged

mchmarny merged 2 commits into
NVIDIA:mainfrom
njhensley:ci/uat-teardown-break-stale-tf-lock

Conversation

@njhensley

Copy link
Copy Markdown
Member

Summary

Break the orphaned Terraform state lock/lease before the UAT Destroy Cluster retry loop, so a run cancelled mid-bringup can still tear down its cluster instead of leaking the GPU node.

Motivation / Context

GKE UAT run 30948013962 was cancelled mid-bringup and then failed teardown with Cluster aicr-30948013962 destroy failed after 3 attempts; GPU node may be leaking, leaving a 5-node cluster + 9 VPC networks running (cleaned up manually).

Root cause: GitHub cancels a job by SIGKILLing the running step's process tree after a ~7.5s grace period. The Bringup Infra step's terraform apply (a multi-minute GKE/AKS provision inside the actuator container) is killed before Terraform can release its backend state lock, orphaning it. The always()-gated Destroy Cluster step then can never acquire the lock — and because a stale lock is persistent, all 3 retry attempts fail identically. The retry loop is designed for transient failures and cannot recover here.

Fix: by the time the teardown step runs, the Bringup container has already exited, so any lock present has no live holder and is safe to break. Clear it best-effort before the retry loop.

Fixes: N/A
Related: run 30948013962

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • Build/CI/tooling

Component(s) Affected

  • Other: UAT teardown workflows (.github/workflows/uat-{gcp,azure,aws}.yaml)

Implementation Notes

Handling differs per cloud because the backend lock mechanism differs — verified by inspecting the pinned actuator images' terraform/backend.tf and the -backend-config flags baked into each cluster binary:

  • GCP (GCS backend): takes a default.tflock object lock; acquire fails with HTTP 412 conditionNotMet. Fix: gcloud storage rm …/default.tflock (teardown SA already owns the state bucket).
  • Azure (azurerm backend): holds an infinite blob lease on the state blob (always on — not opt-in). Fix: az storage blob lease break on deployments/<location>/<id>/terraform.tfstate in the clst<sub-hex> account, using the account key (the actuator's backend authenticates the same way). xtrace is disabled around the key so it never lands in the build log.
  • AWS (S3 backend): the EKS actuator's -backend-config sets no dynamodb_table and no use_lockfile, so the backend does no state locking and cannot orphan a lock. No code change — documented inline so the asymmetry is intentional, with a pointer to add an equivalent break if the actuator ever enables S3 locking.

All three breaks are best-effort no-ops on the happy path (the lock/lease is absent once a successful apply releases it) and are scoped to this run's own deployment.id, so they cannot disturb another run's state.

Testing

yamllint .github/workflows/uat-{gcp,azure,aws}.yaml   # clean
actionlint .github/workflows/uat-{gcp,azure,aws}.yaml # no new findings (pre-existing info-level SC2016 only)
shellcheck -x <extracted Azure block>                 # clean

The GCP path is validated end-to-end: this exact gcloud storage rm …/default.tflock + actuator re-destroy sequence is what recovered the leaked cluster from run 30948013962 manually. The Azure lease-break is reasoned from the actuator's verified state layout/auth but has not yet been exercised against a live cancelled-mid-bringup Azure run.

Risk Assessment

  • Low — Additive, best-effort, scoped to the run's own state object; no-op on the happy path and easy to revert.

Rollout notes: N/A — CI-only workflow change.

Checklist

  • Linter passes (make lint — yamllint/actionlint clean on the edited steps)
  • I did not skip/disable tests to make CI green
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S)
@njhensley
njhensley requested a review from a team as a code owner August 4, 2026 22:56
@njhensley njhensley added the theme/ci-dx CI pipelines, developer experience, and build tooling label Aug 4, 2026
@njhensley njhensley self-assigned this Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
���️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: f71507a4-7156-49f5-9f62-db65b0bdd35d

📥 Commits

Reviewing files that changed from the base of the PR and between 0cd7577 and afb3dbb.

📒 Files selected for processing (3)
  • .github/workflows/uat-aws.yaml
  • .github/workflows/uat-azure.yaml
  • .github/workflows/uat-gcp.yaml

📝 Walkthrough

Walkthrough

The AWS UAT workflow documents that its S3 backend does not use state locking. The Azure UAT workflow retrieves the storage account key and breaks stale state-blob leases before cluster destruction. The GCP UAT workflow removes the deployment’s default.tflock object before each destroy attempt.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: mchmarny

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: breaking orphaned Terraform state locks before UAT teardown.
Description check ✅ Passed The description explains the orphaned lock issue, cloud-specific fixes, scope, testing, and rollout risk.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/uat-azure.yaml:
- Around line 903-912: Update the stale-lease cleanup block around STATE_KEY and
the az storage blob lease break command to stop retrieving or passing the
storage account key on the command line. Authenticate the blob operation with
--auth-mode login instead, ensuring the workflow identity has only the required
Blob lease permissions.

In @.github/workflows/uat-gcp.yaml:
- Around line 703-707: Update the cleanup logic in
.github/workflows/uat-gcp.yaml lines 703-707 to treat only a genuinely missing
default.tflock object as a successful no-op; preserve and retry or fail on
authentication, permission, network, and wrong-URI errors instead of reporting
them as “no lock.” Update the corresponding cleanup flow in
.github/workflows/uat-azure.yaml lines 906-920 to distinguish missing
account/blob/lease results from account lookup or key retrieval failures, and
remove any || true behavior that suppresses operational errors before the
destroy loop.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 4b17f744-ee37-417a-a3f7-5b7a4fbde0d5

📥 Commits

Reviewing files that changed from the base of the PR and between b7c83bb and 9f08563.

📒 Files selected for processing (3)
  • .github/workflows/uat-aws.yaml
  • .github/workflows/uat-azure.yaml
  • .github/workflows/uat-gcp.yaml
Comment thread .github/workflows/uat-azure.yaml Outdated
Comment thread .github/workflows/uat-gcp.yaml Outdated
@njhensley
njhensley force-pushed the ci/uat-teardown-break-stale-tf-lock branch from 9f08563 to e42b4eb Compare August 4, 2026 23:08
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/uat-azure.yaml:
- Around line 945-947: The teardown command using az storage blob lease break
must not expose AZURE_STORAGE_KEY through the process environment. Update the
teardown flow around the lease-break invocation to authenticate via --auth-mode
login with narrowly scoped Blob permissions, or move the operation behind a
trusted teardown boundary, while preserving the existing account, container,
blob, error-file, and return-code behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 3ed0594c-cf0f-4a54-a064-968d94ead9c8

📥 Commits

Reviewing files that changed from the base of the PR and between b7c83bb and e42b4eb.

📒 Files selected for processing (3)
  • .github/workflows/uat-aws.yaml
  • .github/workflows/uat-azure.yaml
  • .github/workflows/uat-gcp.yaml
Comment thread .github/workflows/uat-azure.yaml
mchmarny
mchmarny previously approved these changes Aug 4, 2026
@mchmarny
mchmarny self-requested a review August 4, 2026 23:20
A UAT run cancelled mid-bringup SIGKILLs the actuator's terraform apply
before it can release the backend state lock, orphaning it. The
always()-gated Destroy Cluster step then can never acquire the lock, so
all 3 destroy attempts fail identically and the GPU node leaks (observed
on GKE run 30948013962: 'destroy failed after 3 attempts').

The Bringup container has already exited by the time the teardown step
runs, so any lock present has no live holder and is safe to break. Clear
it at the top of each destroy attempt, per backend:

- GCP (GCS): delete the default.tflock object. HTTP 412 on acquire.
- Azure (azurerm): break the infinite blob lease on the state blob. The
  account key reaches az via AZURE_STORAGE_KEY rather than --account-key
  so it stays out of argv (world-readable through /proc/<pid>/cmdline),
  and xtrace is off across the helper so it never hits the build log.
  --auth-mode login is not usable: the pipeline identity holds
  subscription Contributor, which grants listKeys but no data-plane blob
  role, and the ABAC condition on its RBAC Administrator grant forbids
  self-assigning one.
- AWS (S3): no change — the EKS actuator's -backend-config sets no
  dynamodb_table / use_lockfile, so the backend does no state locking
  and cannot orphan a lock. Documented inline for future readers.

Only a genuinely-absent lock (GCS) or an unheld lease / never-written
state blob (azurerm) counts as the happy path; auth, permission, and
network failures are surfaced as ::warning:: instead of being collapsed
into "no lock", and retry along with the destroy attempt they precede.
Cleanup is deliberately non-fatal — aborting the step would skip the
destroy entirely and guarantee the leak it exists to prevent, so the
loop's terminal exit 1 stays the gate on a real teardown failure.

Backend mechanics confirmed by inspecting the pinned actuator images'
terraform backend.tf and -backend-config flags.

Signed-off-by: Nathan Hensley <nhensley@nvidia.com>
@njhensley
njhensley force-pushed the ci/uat-teardown-break-stale-tf-lock branch from e42b4eb to 0cd7577 Compare August 4, 2026 23:23
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@mchmarny mchmarny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE - 0 blockers at head 0cd7577.

Reviewed the stale-Terraform-lock break added to the GCP and Azure teardowns, plus the AWS "deliberately none" note.

An earlier head (e42b4eb) failed tests/uat two ways, both of which this head fixes:

  • Azure: the az storage account list / az storage account keys list invocations split a quoted --query across a \ continuation, which stripShellComment (run per physical line) read as an unterminated quote. Now one line each.
  • GCP: not ?found in the grep -qiE pattern tripped hasUnsupportedShellGlob, and shellEnablesXtrace fails closed, so the credential-bearing step was reported as enabling xtrace. Now not found|notfound.

Verified locally against this head: go test ./tests/uat/ passes with these workflows overlaid on main. CI is green on 0cd7577.

Spot-checked the safety argument: both breaks run only under always() after the Bringup container has exited, and every UAT run serializes on the per-reservation lease owned by uat-run.yaml, so there is no live lock holder to race. The Azure account key never reaches argv or the xtrace log (AZURE_STORAGE_KEY plus set +x around the helper), and every non-happy-path outcome is a ::warning:: rather than a swallowed || true.

Merge-ordering note: this touches the same Destroy Cluster steps as #2042, so expect a conflict depending on which lands first.

@mchmarny
mchmarny merged commit c7fa85a into NVIDIA:main Aug 5, 2026
2 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci size/M theme/ci-dx CI pipelines, developer experience, and build tooling

2 participants