TTerramantle
Get Started Free
Docs · Scan lifecycle

Scan lifecycle: when a published module version becomes consumable

You published the module and terraform init says there is no such version. Nothing is broken. The scan hasn't finished, and an unscanned version isn't served. That rule is easy to defend and surprising the first time it costs you a pipeline run, so this page sets out when each artefact becomes consumable, what consumers see while it isn't, and why modules and state are gated in opposite directions.

How a module version becomes published

A module version is stored on publish and served on scan. Publishing writes the archive and creates the version as pending. The scan runs out of band. Only when results come back does the version become published, and only published versions appear in the registry protocol or resolve to bytes. Those are two separate events, and most of the confusion comes from the gap between them.

State of the versionWhat a consumer sees
pending: scan queued or runningAbsent from the versions list, and 404 Version not found on download. A version constraint cannot resolve to it, so terraform init reports no matching version rather than a scanning state.
published, policy cleanListed and downloadable. This is the only state in which bytes are served.
published, failing an enforcing policyListed, but marked non-consumable, so the download returns 403 naming the policies that failed. Visible and refused, rather than quietly missing.
failed: the scan reported a failure, or never reportedTreated as unpublished: still 404. A scan that never settles is failed by a reaper 90 minutes after it was enqueued, so nothing stays pending indefinitely.
manually blocked, or module blocked for the org403 with the block reason. A manual block outranks a policy exemption, so an admin kill switch cannot be overridden from below.

Making the publish step wait, or poll

Make the publish step wait for the verdict, or make it poll, so it never has to assume. The failure mode to design out is a pipeline that reports a successful publish and a downstream job that can't find the version thirty seconds later. The publish response tells you which of the two things happened, so read it.

A pipeline that assumes:

# publish, then immediately consume
terramantle modules publish
terraform init   # 404: no matching version

# the publish returned 201 with
# { "status": "pending" }

One that waits for the verdict:

# hold the request open until the scan
# settles (up to 15s), verdict in the body
curl -u "terramantle-bot:$TOKEN" \
  -X PUT --data-binary @module.tar.gz \
  "$REG/v1/modules/acme/vpc/aws/1.4.0?wait=true"

# 201 published  -> consumable
# 202 pending    -> poll below

And one that polls the status:

curl -u "terramantle-bot:$TOKEN" \
  "$REG/v1/modules/acme/vpc/aws/1.4.0/status"

# { "status": "published",
#   "scan_started_at": ...,
#   "scan_completed_at": ... }

The CLI reads the same distinction and says so: a settled publish prints its verdict, and a publish whose scan is still running prints pending (scan still running). A version that came back failed is not retried by republishing the same number, because versions are immutable. Rescan it, or publish the next version.

How state and plans are gated

State is gated the other way round: reads are never withheld. A state read returns the newest version for the workspace immediately, whatever its scan status. Pull your state one second after an apply and you get it. The scan runs behind you, and its verdict is applied at the next lock instead of at the next read.

Reads are not gated because Terraform reads an empty state response as no state at all and plans to create your entire estate from scratch. A registry that withheld state during a scan would convert a pending finding into a proposed rebuild of production. No finding is worth that, so the read path has no scan predicate in it.

The verdict lands at the lock. When the latest state version of a workspace fails an enforcing policy, the workspace is flagged and the next lock attempt is refused with 423, before any apply runs. Plan artefacts raise the same flag by the same route. The gate sits at the start of the next change, which is the first point at which refusing is useful rather than destructive.

What this design does not give you

Module scanning is a true gate: unscanned code is never served, so a backlog costs you availability and never silently costs you a check. State scanning is a different kind of control. The apply that wrote the offending state has already run by the time the scanner looks at it, so all you get is a fast stop on the next change. If the state scan queue is backed up, that stop arrives later, and the workspace stays applyable in the meantime. Pre-apply enforcement belongs to plan-stage checks, which run against the plan before it is applied rather than the state it produced.

For what the scanners look for across modules, provider binaries and live state, see supply-chain scanning. A pending version is invisible rather than listed and refused because the registry protocol has no way to express "exists but not ready", so leaving it out of the versions list is the only accurate answer.

Scan lifecycle FAQ

Why does terraform init say my module version does not exist right after publishing it?

Because the scan hasn't finished. A module version is stored the moment you publish it, but it's recorded as pending, and every consumer-facing read filters on published. The versions endpoint omits it and the download returns 404 until the scan reports back. This is deliberate: a version that hasn't been scanned has no verdict, and serving unverified code while promising to scan it would make the promise meaningless.

How long does a module scan take, and what happens if the scanner never reports?

Normally seconds, and the publish call can wait for it. If a scan never settles, a reaper marks the version failed 90 minutes after it was enqueued, so a version cannot sit pending forever with nobody told. Failed is terminal for that version: it stays unconsumable until you rescan it or publish a new one.

What is the difference between the 404 and the 403 I get on a module download?

A 404 means there is no published version at that address, which includes a version whose scan is still running or has failed. A 403 means the version is published and something decided you should not have it: a failing enforcing policy, a manual block on the version, or an organisation-level block on the module. The 403 comes with the reason attached.

Does a scan ever block terraform state pull?

No. A state read always returns the newest version for the workspace, with no reference to its scan status. Withholding state would be worse than any finding it could reveal, because Terraform reads an empty response as no state at all and plans to recreate your entire estate.

What does state scanning block?

The next apply, at the lock. When the latest state version of a workspace violates an enforcing policy, the workspace is flagged, and the next lock attempt is refused with 423 before any apply can proceed. Plans work the same way. The apply that produced the offending state has already happened by then, and that's the limit of scanning after the fact.

Can I make my pipeline wait for the verdict instead of polling?

Yes. Publish with wait enabled and the request holds open until the scan settles, up to 15 seconds, then returns the verdict with the version detail. Past that it returns 202 with a pending status and you poll the status endpoint. The CLI does this for you and prints the pending state explicitly rather than reporting a publish as done when the verdict is still outstanding.

Last reviewed

Terramantle is a private Terraform and OpenTofu registry. See pricing or read the FAQ.