Skip to content

Amazon S3 backend

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

Declare it with a BACKEND s3 config block:

BACKEND s3 (
bucket = 'my-bucket',
key = 'env/state.json'
);
AttributeTypeDescription
bucketstringThe S3 bucket that holds the state object.
keystringThe S3 object key for the state file within the bucket.

Any attribute not listed here is rejected — a typo surfaces as an error rather than being silently ignored.

AWS credentials and region are resolved through the standard AWS SDK chain — environment variables, shared config/credentials files, and instance/role profiles — so they’re not part of the config block. In CI, supply them the same way you would for any other AWS tooling.

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 backend 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, rather than silently overwriting it. If a run is interrupted and leaves the lock object behind, clear it with force-unlock. See Locking.

When embedding the engine instead of the CLI, register the S3 backend with the NSchema.Aws package:

Terminal window
dotnet add package NSchema.Core
dotnet add package NSchema.Aws

UseS3StateStore has three overloads:

// 1. Bucket and key directly.
builder.UseS3StateStore("my-bucket", "nschema/state.json");
// 2. Configure options via a delegate.
builder.UseS3StateStore(o =>
{
o.Bucket = configuration["NSchema:Bucket"]!;
o.Key = configuration["NSchema:Key"]!;
});
// 3. As above, with access to the IServiceProvider.
builder.UseS3StateStore((o, sp) =>
{
var config = sp.GetRequiredService<IConfiguration>();
o.Bucket = config["NSchema:Bucket"]!;
o.Key = config["NSchema:Key"]!;
});

If no IAmazonS3 is registered, a default AmazonS3Client using ambient credentials (ECS task role, Lambda execution role, environment variables, …) is registered automatically. To bring your own client, register it (e.g. via AWSSDK.Extensions.NETCore.Setup) before calling UseS3StateStore:

builder.Services.AddAWSService<IAmazonS3>();
builder.UseS3StateStore("my-bucket", "nschema/state.json");
OperationRequired permissions
apply / refresh / destroy (writes state)s3:GetObject, s3:PutObject, s3:DeleteObject
plan / 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"
]
}