Skip to content

About

Resolve secrets from AWS SSM into LocalStack at runtime with the Envilder SDK, Testcontainers & Aspire examples in .NET, Python and TypeScript. No .env, nothing committed but a name→path map.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Envilder Examples

How much code does it take to feed a secret from AWS SSM into a LocalStack container, with nothing on disk and nothing committed?

This much:

const env = await Envilder.resolveFile('envilder.json');

const localstack = await new LocalstackContainer('localstack/localstack:stable')
  .withEnvironment(Object.fromEntries(env))
  .start();

Plus one committed file that maps names to paths. Paths, not values, so it's safe in Git:

{
  "LOCALSTACK_AUTH_TOKEN": "/envilder/development/localstack/authToken"
}

Important

envilder.json points at the SSM key /envilder/development/localstack/authToken. That key must exist in your AWS account and hold your LocalStack token, and you need the AWS CLI installed and logged in. See Store your LocalStack token.

That's the entire integration. Envilder resolves the token from your cloud at runtime: SSM → memory → container. No .env, no export, no bash script per stack. This repo shows it in five setups, for AWS SSM and Azure Key Vault.

What the tests prove

Every example tells the same two stories.

1. Envilder hands your containers their secrets

LocalStack refuses to start its Pro features without an auth token. Envilder fetches it from your real AWS account and passes it straight into the container:

flowchart LR
    map["📄 envilder.json<br/><code>LOCALSTACK_AUTH_TOKEN → /envilder/…/authToken</code>"]
    ssm[("☁️ AWS SSM<br/>(real)")]
    envilder(["⚙️ Envilder"])
    localstack["🐳 LocalStack"]

    map -- "which secret?" --> envilder
    ssm -- "the token" --> envilder
    envilder -- "env var, in memory" --> localstack
    localstack -. "is_license_activated: true ✅" .-> test["🧪 test"]
Loading

Test: Should_ActivateLicense_When_…StartsLocalStackWithTokenResolvedByEnvilder. It passes only if Envilder found the real token and LocalStack accepted it.

2. Envilder turns a map file into environment variables

The same test, once per cloud. It stores a random value where the map file points, asks Envilder to resolve the map file, and checks that the value matches:

sequenceDiagram
    autonumber
    participant T as 🧪 Test
    participant S as 🐳 Emulated store<br/>LocalStack for AWS · LocalStack for Azure
    participant E as ⚙️ Envilder

    Note over T: Arrange
    T->>S: put a random value at the path in envilder.test.*.json
    Note over T,E: Act
    T->>E: resolve envilder.test.*.json
    E->>S: get the secret
    S-->>E: random value
    E-->>T: { DEMO_SECRET: random value }
    Note over T: Assert
    T->>T: DEMO_SECRET == random value ✅
Loading

Tests: Should_ResolveSecretFromSsm_When_MapFilePointsToIt and Should_ResolveSecretFromKeyVault_When_MapFilePointsToIt. A fresh random value on every run means a pass can only come from what Envilder just read.

The one line that differs from production. In your app you write Env.ResolveFileAsync("envilder.json") and Envilder builds the cloud client for you. The tests build that client themselves, aimed at the emulator, and pass it to EnvilderClient. Everything after that line is the exact code your app runs.

Run any command with its secrets: envilder exec

No code at all? envilder exec resolves the map file and starts a command with the values in its environment. Nothing is written to disk, so there's no .env to gitignore or clean up:

# SSM -> memory -> container, in one line (from the repo root)
npx envilder exec docker run --rm --env LOCALSTACK_AUTH_TOKEN localstack/localstack:stable

That's it: no .env, no export, no script. --env LOCALSTACK_AUTH_TOKEN (no =value) tells Docker to copy the variable from the environment Envilder just built, so the token never touches the disk or the command line. Ctrl+C stops it and --rm cleans up the container. Needs Docker and the same SSM token as the rest of the repo.

Worth knowing, from the exec docs:

  • Fail-closed. If any secret is missing, empty or unreadable, the command is not started and Envilder exits with 125. Pull mode only warns.
  • Exit codes follow your command (n, or 128 + n for a signal), so Envilder's own failures (125) stay distinguishable.
  • Envilder options go first; the command starts at the first non-option, so -- is only needed when the command itself starts with a dash. No shell is involved: use envilder exec sh -c '...' if you need one.
  • Not a sandbox. The command and everything it starts can read the secrets.

The examples

Folder Stack Run it (from the folder)
typescript-testcontainers/ Vitest + Testcontainers npm install && npm test
python-testcontainers/ pytest + Testcontainers (uv) uv run pytest
dotnet-testcontainers/ xUnit v3 + Testcontainers dotnet test
dotnet-aspire/ Aspire AppHost + Aspire.Hosting.Testing cd AppHost.Tests && dotnet test
typescript-aspire/ Aspire TypeScript AppHost npm install && npm test *
cli/ npx envilder push, pull and exec, from a computer with nothing installed follow the README

Each folder has its own README with the order to read its files in. Each folder is self-contained: there is no package.json at the repo root, so run the command inside the folder. The two .NET examples also share a solution, so dotnet test from the repo root runs both.

Testcontainers and Aspire are alternative orchestrators; pick the folder that matches your project. (Aspire AppHosts can also orchestrate Python and JavaScript apps.)

* Needs the Aspire CLI 13.5+. There's no TypeScript Aspire.Hosting.Testing yet, so the tests spawn aspire run and wait for the emulators to answer. The first run generates .modules/ and .aspire/ (both git-ignored).

The map files

All examples share the map files at the repo root:

File Maps Points at
envilder.json LOCALSTACK_AUTH_TOKEN your real AWS SSM or Azure Key Vault (options)
envilder.test.aws.json DEMO_SECRET → an SSM path LocalStack's SSM
envilder.test.azure.json DEMO_SECRET → a Key Vault secret name LocalStack for Azure

The two test files only differ in $config.provider and in the shape of the identifier: SSM uses paths (/a/b), Key Vault uses names (a-b).

The emulators

Cloud Emulator Needs
AWS SSM LocalStack an auth token (fetched by Envilder, see story 1)
Azure Key Vault LocalStack for Azure the same auth token (fetched by Envilder, see story 1)

LocalStack for Azure starts empty, so the tests create a vault through its Resource Manager API and sign in with a placeholder service principal, through a small local token endpoint. Each folder's README lists the few lines that deal with that. You won't need them against real Azure.

Before you run (once)

Install only what the folders you want need:

Tool Version Used by
Docker any recent all
AWS CLI, logged in v2 all (to read the LocalStack token from SSM)
Node.js 24 LTS or newer (see .nvmrc) typescript-*
uv any recent; it installs Python 3.14 for you python-testcontainers
.NET SDK 10.0 (see global.json) dotnet-*
Aspire CLI 13.5+ typescript-aspire

Store your LocalStack token (once)

You need a LocalStack auth token in your own cloud. Envilder resolves it from AWS SSM or Azure Key Vault: pick one, write the matching envilder.json, and push the token with one command.

Option A: AWS SSM Parameter Store (default)

Needs the AWS CLI installed and logged in (aws configure or aws sso login), so Envilder can read and write /envilder/development/localstack/authToken.

{
  "LOCALSTACK_AUTH_TOKEN": "/envilder/development/localstack/authToken"
}
npx envilder --push \
  --key=LOCALSTACK_AUTH_TOKEN \
  --value=<your-token> \
  --secret-path=/envilder/development/localstack/authToken

Using a named AWS profile? Add it to $config, and Envilder uses it for both the push and the tests:

{
  "$config": { "provider": "aws", "profile": "my-profile" },
  "LOCALSTACK_AUTH_TOKEN": "/envilder/development/localstack/authToken"
}

Option B: Azure Key Vault

Needs an Azure login (az login) with permission to read and write secrets in your vault. Key Vault secret names allow only letters, digits and dashes, so the identifier is a name, not a path:

{
  "$config": {
    "provider": "azure",
    "vaultUrl": "https://<your-vault>.vault.azure.net"
  },
  "LOCALSTACK_AUTH_TOKEN": "localstack-auth-token"
}
npx envilder --push \
  --key=LOCALSTACK_AUTH_TOKEN \
  --value=<your-token> \
  --secret-path=localstack-auth-token

Run the push from the repo root: the CLI reads $config from envilder.json and sends the token to your vault.

Nothing else changes. The tests and AppHosts call Envilder.resolveFile('envilder.json'), and the $config block decides where the token comes from. See providers.

Updating dependencies

Versions are pinned in one place per ecosystem, so an upgrade is one command each:

Ecosystem Where versions live Upgrade
.NET Directory.Packages.props (NuGet), global.json (SDK + Aspire.AppHost.Sdk) dotnet outdated envilder-examples.slnx -u (dotnet-outdated)
Python pyproject.toml + uv.lock uv lock --upgrade
Node each folder's package.json + package-lock.json npx npm-check-updates -u && npm install
Aspire (TypeScript) aspire.config.json aspire update, then delete .modules/ so it regenerates

When you bump Aspire, update the Aspire.AppHost.Sdk version in global.json and Aspire.Hosting.Testing in Directory.Packages.props together.

Links

About

Resolve secrets from AWS SSM into LocalStack at runtime with the Envilder SDK, Testcontainers & Aspire examples in .NET, Python and TypeScript. No .env, nothing committed but a name→path map.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages