Skip to content

Policies

A policy inspects one node's pending change and passes or fails it. Policies run at plan time over the changeset, so a violation appears in the plan, before any apply starts.

from atlantide.policy import enforce

enforce("require-tags", keys=["env", "owner"])
enforce("deny-destroy-in-protected", stacks=["prod"])
enforce("require-secret-refs")

Call enforce() during config evaluation, conventionally at the top of the file, outside any Stack block. Bindings are recorded in the IR and round-trip through a .atlas artifact, so a plan built from an artifact enforces the same policies as the config that produced it.

Levels

from atlantide.core import PolicyLevel

enforce("require-tags", keys=["env"], level=PolicyLevel.ADVISORY)
Level Effect of a violation
MANDATORY (default) The plan is blocked. plan and apply exit 1.
ADVISORY Reported as a warning; the run proceeds.

Narrowing by type

types= restricts a binding to the named resource types. A single string is one type name, not a sequence of characters:

enforce("require-tags", types="aws.S3Bucket", keys=["owner"])
enforce("require-tags", types=["aws.S3Bucket", "aws.SqsQueue"], keys=["owner"])

A policy bound more than once is evaluated separately with each binding's arguments, for example one deny-destroy-in-protected binding per environment.

What a policy sees

Only actionable changes are evaluated; NOOP nodes are not passed to policies. For each change the policy receives the node id, the pending action, the stack, the desired resource and the binding's arguments. The desired resource is None for a pure DELETE, so the built-in policies that inspect the resource pass such nodes.

Built-in policies

require-tags

Requires the tag keys listed in keys. With no keys, requires only that the resource has at least one tag.

enforce("require-tags")                         # any tags
enforce("require-tags", keys=["env", "owner"])  # these keys

A key with an empty value counts as missing. Resource types without a tags field are skipped.

require-secret-refs

Every field marked sensitive must hold a SecretRef handle, not a literal. Takes no arguments.

A secret referenced by name stays out of the config, the IR, state and the .atlas artifact; a literal value is written to all four. A field declared with secret() is typed SecretRef | None, so pydantic already rejects a literal. This policy covers fields declared mutable(..., sensitive=True) on a plain str, which accept plaintext without error.

deny-destroy-in-protected

Denies destructive actions (DELETE, and the destroy half of a REPLACE) in the stacks listed in stacks.

enforce("deny-destroy-in-protected", stacks=["prod", "shared"])

With no stacks, it passes everything; there is no default protected stack.

deny-destroy-in-prod is an alias.

Policy vs. prevent_destroy

Lifecycle(prevent_destroy=True) guards one resource and is checked by the planner. This policy guards every resource in a stack and is checked by the policy pass. They are independent; a resource can be covered by both.

Argument errors

A binding with an argument the policy cannot use fails the plan with a PolicyConfigError naming the policy and the parameter:

require-tags: `keys` must be a name or a list of them, got int

Binding a policy to a resource type

Provider authors can attach a policy to a resource type so that every instance carries it:

from atlantide.policy import policy

@policy("require-tags")
class Gadget(AcmeResource):
    ...

The binding applies to that type only and is evaluated alongside the config's enforce() bindings. Config cannot use the decorator, because Atlas-lang accepts no classes other than an EnvSchema of annotated fields. It is an API for Python code: providers and the libraries that publish components.

Where policies sit in the pipeline

Policy evaluation is the last step of stage 7, after the prevent_destroy guard and after --target/--replace selection, so a policy sees the narrowed changeset that would run. See How it works.