Skip to content

Attest the state file a failed apply rewrote - #36

Merged
gsavage merged 2 commits into
mainfrom
attest-state-written-by-failed-apply
Oct 7, 2026
Merged

gsavage merged 2 commits into
mainfrom
attest-state-written-by-failed-apply

Conversation

@JonJagger

@JonJagger JonJagger commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

terraform apply writes the state file to S3 even when it fails
part-way, but apply.yml only attests the state file in
reset-drift-detection, which needs the apply job to succeed. A failed
CI apply therefore leaves a state file Kosli has no provenance for,
and drift detection reports it exactly as if terraform had been run
from a laptop. It stays that way until the same component next
applies successfully.

base.yml now records the state file's ETag before Apply and, only if
Apply fails, attests the state file when its ETag has changed. A state
file the failed apply did not change is left unattested, since it may
have been written outside CI. The bucket and key come from the backend
config tf plan initialised, so they are the ones terraform used. The
ETag step is continue-on-error so it can never block an apply.

A cancelled apply, or one whose AWS credentials have expired, attests
nothing, so it is still reported as drift. A failed apply also does
not reset the drift plan, so infrastructure it changed is reported as
drift by the next detect-drift run until an apply succeeds. The README
says so.

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com

  terraform apply writes the state file to S3 even when it fails
  part-way, but apply.yml only attests the state file in
  reset-drift-detection, which needs the apply job to succeed. A failed
  CI apply therefore leaves a state file Kosli has no provenance for,
  and drift detection reports it exactly as if terraform had been run
  from a laptop. It stays that way until the same component next
  applies successfully.

  This happened on 2026-10-06 to the cyber-dojo prod apply of
  custom-start-points: the apply timed out waiting for the ECS service
  to stabilise after writing the state file, and
  aws-prod-terraform-drift-detection stayed non-compliant until a
  forced redeployment the next day.
  https://github.com/cyber-dojo/aws-prod-co-promotion/actions/runs/37458113081/job/112253462088

  base.yml now records the state file's ETag before Apply and, only if
  Apply fails, attests the state file when its ETag has changed. A state
  file the failed apply did not change is left unattested, since it may
  have been written outside CI. The bucket and key come from the backend
  config tf plan initialised, so they are the ones terraform used. The
  ETag step is continue-on-error so it can never block an apply.

  A cancelled apply, or one whose AWS credentials have expired, attests
  nothing, so it is still reported as drift. A failed apply also does
  not reset the drift plan, so infrastructure it changed is reported as
  drift by the next detect-drift run until an apply succeeds. The README
  says so.

  Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JonJagger

JonJagger commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

On 2026-10-06 this happened to the prod apply of cyber-dojo's custom-start-points,
and its state file has been reported as having no provenance ever since.

  1. co-promotion run 37458113081, job "Prod apply custom-start-points /
    apply / terraform":
    https://github.com/cyber-dojo/aws-prod-co-promotion/actions/runs/37458113081/job/112253462088
    The Apply step created the new task definition at
    2026-10-06 11:50:26 UTC, then failed at 2026-10-06 12:00:28 UTC with:
    Error: waiting for ECS Service (...service/app/custom-start-points)
    update: timeout while waiting for state to become 'tfSTABLE'
    (last state: 'tfPENDING', timeout: 10m0s)
    "Kosli attest apply" was skipped, and so was the job
    "Prod apply custom-start-points / reset-drift-detection", the only
    place apply.yml attests the state file.

  2. Snapshot #107 of aws-prod-terraform-drift-detection
    (2026-10-06 12:03:29 UTC) replaced custom-start-points-state
    2a0f5321... (attested by terraform-apply-prod-aws-prod-co-promotion)
    with c6bca2ae245a6b7eb308d2ab7b9a3751968c95e6ae5ad22e14978f65ec4935d1,
    which no flow has attested. custom-start-points-drift stayed d38ec34c
    (attested). The provenance rule failed, so the environment went
    non-compliant:
    https://app.kosli.com/cyber-dojo/environments/aws-prod-terraform-drift-detection/snapshots/107

  3. The next promotions (run 37463179108 at 2026-10-06 12:25:49 UTC and
    run 37596945840 at 2026-10-07 08:54:41 UTC) found no new
    custom-start-points image to deploy, so its apply did not run again
    and the state stayed unattested.

  4. We forced a no-behaviour-change redeployment of custom-start-points.
    co-promotion run 37598639214 (started 2026-10-07 09:09:19 UTC):
    https://github.com/cyber-dojo/aws-prod-co-promotion/actions/runs/37598639214
    "Prod apply custom-start-points / apply / terraform" succeeded, then
    "Prod apply custom-start-points / reset-drift-detection" attested the
    new state file at 2026-10-07 09:16:01 UTC. Snapshot #109
    (2026-10-07 09:23:31 UTC) replaced c6bca2ae... with
    cd84e62ac1340f4f42c88fa37f3e1f0d572dd9c042eac15bffc149f6f7204f5d,
    attested in flow terraform-apply-prod-custom-start-points, and the
    environment is compliant again:
    https://app.kosli.com/cyber-dojo/environments/aws-prod-terraform-drift-detection/snapshots/109
    (The flow name differs from item 2 because aws-prod-co-promotion
    commit 300be81 started passing component_name to apply.yml.)

@gsavage

gsavage commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Is this new behaviour desirable?

As terraform apply runs, the statefile is written many times. If an apply command changes ten things, then there will be up-to ten separate writes to the statefile (I believe Terraform does some batching, but I don't fully understand the rules).

If an apply changes five things and then fails, the statefile will be updated from the initial state. The statefile at that point will not have provenance in Kosli, and so the next time an environment snapshot runs, we'll see drift (exactly as though someone had run Terraform from their laptop).

Therefore, the environment in Kosli goes red and everyone runs around flapping their arms. And someone (an engineer, an auditor) asks -- why did the environment go non-compliant and the answer is "because a deploy failed part way through leaving us in an inconsistent state". It is inconsistent precisely because merged code is not fully deployed.

With the change in this PR, the partially successful apply is attested, and so the environment remains green, and no-one knows there was a problem.

Perhaps this is a philosophical question -- if we only apply 50% of a change that's on main should the environment be compliant or not? My vote is for "not"

@gsavage

gsavage commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

With the specific CyberDojo case you described, it looks like the tf apply failed because the ecs update took longer than the timeout configured in GitHub.

Attesting the statefile after that timeout is problematic because we don't know what the state of the infrastructure is -- will the deployment succeed in AWS in a few minutes? Or will AWS eventually decide that the ECS Service couldn't be stabilised and roll it back to the previous release? At the time at which tf apply stopped, we have no way to know which of those future states will be reached.

I would argue that the problem is in step 3 of your description -- that you were unable to run tf apply again within GitHub because of a check for whether or not an image had changed. If you were able to run tf apply again, Terraform would run, it would probably find that the deployment had succeeded, it would determine there was no further action to take, and it would attest the statefile for you, putting you back into a compliant state.

@gsavage gsavage 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.

I'm marking as "Request changes" so that we don't accidentally merge this without discussing the comments I've posted on the PR.

I don't think the change here is the right change, but I can be persuaded to view it differently

  Attesting the state file a failed apply rewrote would report a
  partly-applied commit as compliant. Terraform writes the state file as
  it goes, so an apply that fails part-way leaves infrastructure only
  partly at the merged commit. That is an inconsistency worth seeing,
  and drift detection reporting it is the right outcome, not a false
  positive.

  So this drops the base.yml change from the previous commit, and the
  README now says that a failed apply attests nothing on purpose, and
  that the environment stays non-compliant until an apply of the same
  component succeeds, which is the remedy once the failure's cause is
  fixed.

  Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gsavage
gsavage merged commit 480f6b5 into main Oct 7, 2026
1 check passed
@gsavage
gsavage deleted the attest-state-written-by-failed-apply branch October 7, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants