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 it with a BACKEND s3 config block:

BACKEND s3 (
version = '4.0.0',
bucket = 'my-bucket',
key = 'env/state.json'
);

The NSchema.Aws plugin is restored automatically from the pinned version the first time you run a command. (To embed the engine as a library, see Using the library.)

Attribute Type Description
version string Required. The version of the NSchema.Aws plugin package to restore.
source string Optional. A NuGet package id to load the backend from instead of NSchema.Aws.
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 config block. 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 block. Path-style addressing is the one setting with no standard environment variable, so it lives on the block; everything else still comes from the ambient AWS configuration:

BACKEND s3 (
version = '4.0.0',
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 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. If a run is interrupted and leaves the lock object behind, clear it with lock release. 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"]!;
});

Each overload also accepts an optional Action<AmazonS3Config> to configure the underlying client directly:

builder.UseS3StateStore("my-bucket", "nschema/state.json", config =>
{
config.ServiceURL = "http://localhost:9000";
config.ForcePathStyle = true;
});

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");
Operation Required 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"
]
}