Skip to content

Amazon S3

The Amazon S3 backend persists state to an object in an S3 bucket. This is ideal for a shared backend for a team or CI, where many runners need a single source of truth.

Declare the plugin, then point a STATE statement at it:

PLUGIN s3 (
source = 'NSchema.Aws',
version = '[5.0,6.0)'
);
STATE s3 (
bucket = 'my-bucket',
key = 'env/state.json'
);

The label (s3 here) is yours to choose; the STATE statement selects the plugin by referencing it. The package is resolved and locked by init and restored on first use.

Attribute Type Description
bucket string The S3 bucket that holds the state object.
key string The S3 object key for the state file within the bucket.
force_path_style boolean Use path-style addressing (endpoint/bucket/key) instead of the host (bucket.endpoint/key). Defaults to false.

The endpoint, credentials, and region are resolved through the standard AWS SDK chain: environment variables (AWS_ENDPOINT_URL, AWS_REGION, etc.), shared config/credentials files, and instance/role profiles, so they’re not part of the statement. In CI, supply them the same way you would for any other AWS tooling.

To target an S3-compatible store such as MinIO, point AWS_ENDPOINT_URL_S3 at it and set force_path_style = true in the statement. Path-style addressing is the one setting with no standard environment variable, so it lives on the statement; everything else still comes from the ambient AWS configuration:

STATE s3 (
bucket = 'my-bucket',
key = 'env/state.json',
force_path_style = true
);

Reach for S3 when more than one machine needs to read and write the same state: a team sharing a project, or a CI pipeline whose plan and apply steps run on different runners. For a single operator, the simpler local file state is usually enough.

The S3 backend coordinates exclusive access through a sibling lock object (the state key plus a .lock suffix), created atomically with an S3 conditional write, so a second writer is rejected while another operation holds the lock. If a run is interrupted and leaves the lock object behind, clear it with lock release. See Locking.

Operation Required permissions
apply / refresh / destroy (writes state) s3:GetObject, s3:PutObject, s3:DeleteObject
plan / state show / drift (reads state) s3:GetObject

The lock object lives beside the state object, so write operations also need s3:PutObject and s3:DeleteObject on the .lock key. A policy scoped to both:

{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": [
"arn:aws:s3:::my-bucket/nschema/state.json",
"arn:aws:s3:::my-bucket/nschema/state.json.lock"
]
}