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.
Every example tells the same two stories.
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"]
Test: Should_ActivateLicense_When_…StartsLocalStackWithTokenResolvedByEnvilder. It passes only if Envilder found the real token and LocalStack accepted it.
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 ✅
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 toEnvilderClient. Everything after that line is the exact code your app runs.
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:stableThat'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, or128 + nfor 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: useenvilder exec sh -c '...'if you need one. - Not a sandbox. The command and everything it starts can read the secrets.
| 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).
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).
| 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.
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 |
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.
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/authTokenUsing 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"
}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-tokenRun 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.
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.