<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marçal Albert</title>
    <description>The latest articles on DEV Community by Marçal Albert (@macalbert).</description>
    <link>https://dev.to/macalbert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1290970%2Fdf8026b9-d325-4e43-b614-e1b57b3038c4.png</url>
      <title>DEV Community: Marçal Albert</title>
      <link>https://dev.to/macalbert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/macalbert"/>
    <language>en</language>
    <item>
      <title>LocalStack integration tests without .env files</title>
      <dc:creator>Marçal Albert</dc:creator>
      <pubDate>Sun, 05 Jul 2026 01:12:45 +0000</pubDate>
      <link>https://dev.to/macalbert/localstack-integration-tests-without-env-files-b75</link>
      <guid>https://dev.to/macalbert/localstack-integration-tests-without-env-files-b75</guid>
      <description>&lt;p&gt;&lt;em&gt;A follow-up to &lt;a href="https://dev.to/macalbert/how-i-run-localstack-without-committing-localstackauthtoken-34gk"&gt;How I run LocalStack without committing LOCALSTACK_AUTH_TOKEN&lt;/a&gt;. That post used Docker Compose and a generated &lt;code&gt;.env&lt;/code&gt;. This one removes the file entirely.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;LocalStack's unified image changed the shape of local integration tests. Since &lt;a href="https://blog.localstack.cloud/localstack-for-aws-release-2026-03-0/" rel="noopener noreferrer"&gt;LocalStack for AWS moved to a unified image that requires &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;&lt;/a&gt;, getting realistic AWS behavior means you need a real token &lt;em&gt;before&lt;/em&gt; the container even starts.&lt;/p&gt;

&lt;p&gt;So the test fixture needs a secret before the test environment exists.&lt;/p&gt;

&lt;p&gt;The usual fix is a generated &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;get the token into a secret store in the first place&lt;/li&gt;
&lt;li&gt;generate the file before tests&lt;/li&gt;
&lt;li&gt;gitignore it&lt;/li&gt;
&lt;li&gt;load it into Docker, Testcontainers, or Aspire&lt;/li&gt;
&lt;li&gt;keep local and CI aligned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;But now there is a file whose only job is to carry a secret into your tests: something to generate, gitignore, keep in sync and never commit by accident. One more moving part that lives next to your real configuration instead of inside it.&lt;/p&gt;

&lt;p&gt;When a tool expects a file (Docker Compose, shell scripts, legacy tooling), a &lt;code&gt;.env&lt;/code&gt; is a good bridge.&lt;/p&gt;

&lt;p&gt;When the consumer is &lt;em&gt;code&lt;/em&gt;, the bridge is unnecessary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://testcontainers.com/" rel="noopener noreferrer"&gt;Testcontainers&lt;/a&gt; and &lt;a href="https://aspire.dev/" rel="noopener noreferrer"&gt;Aspire&lt;/a&gt; start LocalStack from inside your test process, and code can resolve the secret directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSM -&amp;gt; memory -&amp;gt; container environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing to generate, nothing to gitignore, nothing to go stale.&lt;/p&gt;

&lt;p&gt;This is not meant to replace your secrets manager. It just removes the generated &lt;code&gt;.env&lt;/code&gt; hop when the consumer is already code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just call SSM directly?
&lt;/h2&gt;

&lt;p&gt;You can. A direct &lt;code&gt;ssm.getParameter(...)&lt;/code&gt; in the fixture is perfectly reasonable, and if your app is AWS-only and already loads Parameter Store through the native config provider (&lt;code&gt;builder.Configuration.AddSystemsManager(...)&lt;/code&gt; in .NET), you probably do not need anything else in runtime. Reading secrets is a solved problem.&lt;/p&gt;

&lt;p&gt;The part that stays implicit with direct calls is the &lt;em&gt;contract&lt;/em&gt;: which logical keys this project needs, where each one lives, and what has to exist before it boots. That knowledge ends up scattered across C# code, SSM paths, GitHub Actions secrets and a stale README. Nobody has the full list in one place.&lt;/p&gt;

&lt;p&gt;A committed map surfaces it. It is the list, versioned and reviewable in a PR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LOCALSTACK_AUTH_TOKEN -&amp;gt; /demo/localstack/auth-token
DATABASE_URL          -&amp;gt; /demo/db/url            # new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a key and everyone sees the project grew a dependency. Because the code depends on the logical name and every consumer reads the same map, rotating a value, changing a path, or pointing at a different provider is an edit in one place rather than a hunt across call sites. The consumer still has to &lt;em&gt;use&lt;/em&gt; the value it wants; it just never has to learn how to fetch it. In a monorepo, where each service commits its own &lt;code&gt;envilder.json&lt;/code&gt;, that visibility compounds: you can see at a glance what every service depends on without reading its code.&lt;/p&gt;

&lt;p&gt;That is the trade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;direct SDK call -&amp;gt; resolution logic per call site, contract stays implicit
shared contract -&amp;gt; one versioned map, every consumer resolves the same thing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdp3f82klquzov3n45jea.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdp3f82klquzov3n45jea.jpg" alt="One map for all, all from one map" width="600" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One contract, many consumers: the .NET fixture, the Python one, the TypeScript setup and every pipeline all resolve the same committed map. It is the same instinct as infrastructure as code, applied to the secret contract: make the implicit explicit and put it under version control. That is a win on a single stack, before any multi-provider story enters the picture.&lt;/p&gt;

&lt;p&gt;And if you already centralize this in one config layer, good: that layer &lt;em&gt;is&lt;/em&gt; the contract. Envilder just makes it explicit and provider-agnostic instead of hand-rolled per stack.&lt;/p&gt;

&lt;p&gt;One honest caveat while we are here: removing the &lt;code&gt;.env&lt;/code&gt; is not a stronger runtime secret boundary. The token still ends up in the container environment because LocalStack needs it there. The improvement is operational: there is no intermediate &lt;code&gt;.env&lt;/code&gt; file to generate, ignore, sync, clean up or accidentally commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract
&lt;/h2&gt;

&lt;p&gt;One file, committed at the repo root. Paths, not values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LOCALSTACK_AUTH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/demo/localstack/auth-token"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://envilder.com" rel="noopener noreferrer"&gt;Envilder&lt;/a&gt; SDK resolves it at test time. Same pattern in every stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  TypeScript
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Envilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../envilder.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;localstack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LocalstackContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localstack/localstack:stable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full test: &lt;a href="https://github.com/macalbert/envilder-examples/blob/main/typescript-testcontainers/localstack.test.ts" rel="noopener noreferrer"&gt;&lt;code&gt;typescript-testcontainers/localstack.test.ts&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalStackContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localstack/localstack:stable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Envilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAP_FILE&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waiting_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LogMessageWaitStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ready\.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;DockerContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit wait for the &lt;code&gt;Ready.&lt;/code&gt; log line is there because the built-in wait does not match the consolidated 2026 LocalStack image.&lt;/p&gt;

&lt;p&gt;Full test: &lt;a href="https://github.com/macalbert/envilder-examples/blob/main/python-testcontainers/test_localstack.py" rel="noopener noreferrer"&gt;&lt;code&gt;python-testcontainers/test_localstack.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  .NET
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResolveFileAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"envilder.json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;_container&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LocalStackBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localstack/localstack:stable"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full fixture: &lt;a href="https://github.com/macalbert/envilder-examples/blob/main/dotnet-testcontainers/LocalStackFixture.cs" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet-testcontainers/LocalStackFixture.cs&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Same pattern, different orchestrator: .NET Aspire
&lt;/h2&gt;

&lt;p&gt;In .NET, you might not be using Testcontainers directly. &lt;a href="https://aspire.dev/" rel="noopener noreferrer"&gt;Aspire&lt;/a&gt; can also orchestrate containers, and it has its own &lt;a href="https://github.com/localstack-dotnet/dotnet-aspire-for-localstack" rel="noopener noreferrer"&gt;LocalStack integration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The AppHost resolves the token and injects it into the LocalStack resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"LocalStack:UseLocalStack"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;localstack&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLocalStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localstack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configureContainer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContainerImageTag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"stable"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"LocalStack is disabled."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResolveFileAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"envilder.json"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;localstack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha: without that &lt;code&gt;UseLocalStack&lt;/code&gt; config flag, &lt;code&gt;AddLocalStack&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;. That is the package's fallback-to-real-AWS mechanism.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aspire run&lt;/code&gt; and you have a local AWS, token included, with nothing to export first.&lt;/p&gt;

&lt;p&gt;And it is just as testable: &lt;code&gt;Aspire.Hosting.Testing&lt;/code&gt; boots the same AppHost from an xUnit fixture, so the round-trip test works identically to the Testcontainers one.&lt;/p&gt;

&lt;p&gt;Aspire is not .NET-only either. The same AppHost can orchestrate &lt;a href="https://aspire.dev/integrations/frameworks/python/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; and &lt;a href="https://aspire.dev/integrations/frameworks/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; apps next to LocalStack, so a FastAPI service resolving its secrets with the Envilder Python SDK fits in the same graph.&lt;/p&gt;

&lt;p&gt;Full AppHost and its test suite: &lt;a href="https://github.com/macalbert/envilder-examples/tree/main/dotnet-aspire" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet-aspire/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the whole integration. The token travels from AWS SSM into the container's environment without being written to a generated file. When it rotates, I update it in SSM once per environment and every suite picks it up on the next run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recursion
&lt;/h2&gt;

&lt;p&gt;This is exactly how &lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;Envilder's own test suite&lt;/a&gt; works. The SDK resolves the token that starts the LocalStack container used to test the SDK against an emulated SSM.&lt;/p&gt;

&lt;p&gt;If Envilder cannot fetch its own secret, the suite fails before a single assertion runs. It is the most honest smoke test in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI
&lt;/h2&gt;

&lt;p&gt;The mechanism does not change. The secret does.&lt;/p&gt;

&lt;p&gt;Locally, the &lt;code&gt;profile&lt;/code&gt; in &lt;code&gt;$config&lt;/code&gt; resolves my credentials from &lt;code&gt;~/.aws/credentials&lt;/code&gt;, and the map points to my developer LocalStack token.&lt;/p&gt;

&lt;p&gt;In GitHub Actions, OIDC through &lt;code&gt;aws-actions/configure-aws-credentials&lt;/code&gt; feeds the default credential chain, and the map can point to a &lt;a href="https://docs.localstack.cloud/aws/getting-started/auth-token/" rel="noopener noreferrer"&gt;CI Auth Token&lt;/a&gt; stored in SSM. LocalStack developer tokens are not meant for pipelines, so CI should use a CI-specific token.&lt;/p&gt;

&lt;p&gt;Same contract, different credentials, different secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Minimal, runnable examples live in &lt;strong&gt;&lt;a href="https://github.com/macalbert/envilder-examples" rel="noopener noreferrer"&gt;envilder-examples&lt;/a&gt;&lt;/strong&gt;, one folder per stack and orchestrator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/macalbert/envilder-examples/tree/main/typescript-testcontainers" rel="noopener noreferrer"&gt;&lt;code&gt;typescript-testcontainers&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/macalbert/envilder-examples/tree/main/python-testcontainers" rel="noopener noreferrer"&gt;&lt;code&gt;python-testcontainers&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/macalbert/envilder-examples/tree/main/dotnet-testcontainers" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet-testcontainers&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/macalbert/envilder-examples/tree/main/dotnet-aspire" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet-aspire&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/macalbert/envilder-examples/tree/main/typescript-aspire" rel="noopener noreferrer"&gt;&lt;code&gt;typescript-aspire&lt;/code&gt;&lt;/a&gt;: Aspire's &lt;a href="https://devblogs.microsoft.com/aspire/aspire-typescript-apphost/" rel="noopener noreferrer"&gt;TypeScript AppHost&lt;/a&gt;, where &lt;a href="https://github.com/macalbert/envilder-examples/blob/main/typescript-aspire/apphost.ts" rel="noopener noreferrer"&gt;&lt;code&gt;apphost.ts&lt;/code&gt;&lt;/a&gt; resolves the token with the same &lt;code&gt;@envilder/sdk&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one resolves the token, boots LocalStack, and does a &lt;code&gt;SecureString&lt;/code&gt; round-trip against emulated SSM.&lt;/p&gt;

&lt;p&gt;Seeding the token is one command. That is the "get the token into a secret store" step from the top of this post, handled without any manual setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx envilder &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LOCALSTACK_AUTH_TOKEN &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-token&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/demo/localstack/auth-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;Envilder started as a fix for my own problem: I wanted the secret mapping in Git and the values out of it, resolved by whatever consumes them. This post is one concrete case where that helped, nothing more. If it turns out other people hit the same wall, there is obvious room to grow it (validating a map in CI, diffing a map against the store, and so on), but that is speculative and feedback-driven, not a promised roadmap. For now it does one thing, and this is that one thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secret values outside Git
secret mappings inside Git
resolution at runtime
no generated file when the consumer is code
one contract across every call site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; files are the right bridge when a tool needs a file. Test fixtures are code. Let them resolve secrets through the same contract as everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Example repo: &lt;a href="https://github.com/macalbert/envilder-examples" rel="noopener noreferrer"&gt;https://github.com/macalbert/envilder-examples&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Envilder: &lt;a href="https://envilder.com/" rel="noopener noreferrer"&gt;https://envilder.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Testcontainers: &lt;a href="https://testcontainers.com/" rel="noopener noreferrer"&gt;https://testcontainers.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LocalStack Auth Token docs: &lt;a href="https://docs.localstack.cloud/aws/getting-started/auth-token/" rel="noopener noreferrer"&gt;https://docs.localstack.cloud/aws/getting-started/auth-token/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Thanks again to the &lt;a href="https://localstack.cloud/" rel="noopener noreferrer"&gt;LocalStack&lt;/a&gt; team for supporting Envilder through their Open Source program.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>testing</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I run LocalStack without committing LOCALSTACK_AUTH_TOKEN</title>
      <dc:creator>Marçal Albert</dc:creator>
      <pubDate>Mon, 08 Jun 2026 23:13:33 +0000</pubDate>
      <link>https://dev.to/macalbert/how-i-run-localstack-without-committing-localstackauthtoken-34gk</link>
      <guid>https://dev.to/macalbert/how-i-run-localstack-without-committing-localstackauthtoken-34gk</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;Using &lt;a href="https://envilder.com/" rel="noopener noreferrer"&gt;Envilder&lt;/a&gt; to keep the LocalStack auth token out of Git while preserving a reproducible local Docker Compose setup.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;I wanted a reproducible LocalStack setup without committing &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; to the repository.&lt;/p&gt;

&lt;p&gt;That sounds simple, but it touches a common local development problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;How do you make a project easy to run without putting real secrets in Git?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LocalStack &lt;a href="https://docs.localstack.cloud/aws/getting-started/auth-token/" rel="noopener noreferrer"&gt;expects the auth token&lt;/a&gt; to be available through the &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; environment variable when starting it with Docker, Docker Compose, the LocalStack CLI, or CI workflows.&lt;/p&gt;

&lt;p&gt;Since &lt;a href="https://blog.localstack.cloud/localstack-for-aws-release-2026-03-0/" rel="noopener noreferrer"&gt;LocalStack for AWS 2026.03.0&lt;/a&gt;, it ships as a consolidated Docker image and requires an auth token to start, so managing &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; cleanly is now part of the normal setup path, not just a Pro-only edge case.&lt;/p&gt;

&lt;p&gt;That token is a secret, so I did not want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commit it to the repository&lt;/li&gt;
&lt;li&gt;paste it into &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;duplicate it across helper scripts&lt;/li&gt;
&lt;li&gt;rely on a manual &lt;code&gt;export LOCALSTACK_AUTH_TOKEN=...&lt;/code&gt; step every time&lt;/li&gt;
&lt;li&gt;make the setup work only on my machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted instead was a small, repeatable contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;this project needs LOCALSTACK_AUTH_TOKEN
resolve it from the secret store
inject it locally when needed
do not commit the value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the pattern I ended up using with &lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;Envilder&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;secret values stay in a real secret store&lt;/li&gt;
&lt;li&gt;the repository contains only the mapping&lt;/li&gt;
&lt;li&gt;local development generates a &lt;code&gt;.env&lt;/code&gt; file from that mapping&lt;/li&gt;
&lt;li&gt;Docker Compose reads the generated &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the token never gets committed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, the secret is &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In another project, it could be a database password, webhook secret, API key, or service credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the Secret Mapping
&lt;/h2&gt;

&lt;p&gt;Here is a minimal &lt;code&gt;envilder.json&lt;/code&gt; example using AWS SSM Parameter Store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://envilder.com/schema/map-file.v1.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LOCALSTACK_AUTH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/envilder/localstack/auth-token"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is safe to commit because it does not contain the token.&lt;/p&gt;

&lt;p&gt;It only says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;To produce &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;, resolve this value from the secret store.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The actual token remains outside the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the Local &lt;code&gt;.env&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before starting LocalStack, generate the local environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx envilder &lt;span class="nt"&gt;--map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;envilder.json &lt;span class="nt"&gt;--envfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces a local &lt;code&gt;.env&lt;/code&gt; file like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LOCALSTACK_AUTH_TOKEN=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file should be ignored by Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the project contains the setup contract, but not the secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing the Token to LocalStack
&lt;/h2&gt;

&lt;p&gt;A Docker Compose service can reference the variable as usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;localstack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localstack/localstack:stable&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4566:4566"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4510-4559:4510-4559"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;LOCALSTACK_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${LOCALSTACK_AUTH_TOKEN:?Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;envilder&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;first}"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Compose automatically reads a &lt;code&gt;.env&lt;/code&gt; file from the project directory for variable interpolation.&lt;/p&gt;

&lt;p&gt;If your generated env file lives somewhere else, you can pass it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; path/to/.env up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff is what a new contributor sees after cloning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx envilder &lt;span class="nt"&gt;--map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;envilder.json &lt;span class="nt"&gt;--envfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two commands, no Slack message asking "where do I get the token?", no secret in Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not direnv, 1Password, or chamber?
&lt;/h2&gt;

&lt;p&gt;For one developer, &lt;code&gt;export LOCALSTACK_AUTH_TOKEN=...&lt;/code&gt; works, but it creates hidden local state the repo never documents. The interesting comparison is against the tools that already resolve secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;direnv&lt;/strong&gt; loads env vars when you enter a directory, but &lt;code&gt;.envrc&lt;/code&gt; is just shell. You still write &lt;code&gt;export LOCALSTACK_AUTH_TOKEN=$(aws ssm get-parameter ...)&lt;/code&gt; yourself, per variable, per project. It is a loader, not a resolver, and the mapping lives in bespoke shell instead of a declared contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1Password &lt;code&gt;op run&lt;/code&gt;&lt;/strong&gt; injects secrets at runtime and is excellent, but it assumes 1Password is your store. If your secrets already live in AWS SSM or Azure Key Vault, you are adding a vendor, not removing one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chamber&lt;/strong&gt; is the closest match for the AWS-only case:
&lt;code&gt;chamber exec service -- docker compose up&lt;/code&gt; reads straight from SSM. If you never leave AWS and only need secrets at the command line, chamber or a three-line script is genuinely enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Envilder's narrow bet is different on three points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The mapping is an explicit &lt;code&gt;name -&amp;gt; path&lt;/code&gt; contract committed as JSON, not a service prefix or shell glue.&lt;/li&gt;
&lt;li&gt;The store is your own cloud. No third-party infrastructure holds the secret.&lt;/li&gt;
&lt;li&gt;The same map drives the CLI here, CI, and in-process resolution through the SDKs, so local dev and runtime read one contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you only ever touch AWS from a shell, you may not need it. The case shows up when you have more than one provider, more than one language, or want the same declared contract locally, in CI, and at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Slightly Meta Part
&lt;/h2&gt;

&lt;p&gt;I ran into this while working on Envilder.&lt;/p&gt;

&lt;p&gt;Envilder uses LocalStack for integration testing.&lt;/p&gt;

&lt;p&gt;LocalStack needs &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I used Envilder to inject the token needed to start the LocalStack environment used to test Envilder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2tfkdag8d7ske6xb59ta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2tfkdag8d7ske6xb59ta.png" alt="Envilder uses Envilder to inject LocalStack token"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tool became its own first customer, which is always a useful kind of feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on LocalStack Auth Tokens
&lt;/h2&gt;

&lt;p&gt;LocalStack's documentation is clear that &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; should be kept confidential and should not be committed to source control.&lt;/p&gt;

&lt;p&gt;This post is not about bypassing that requirement.&lt;/p&gt;

&lt;p&gt;It is about treating the token like any other secret:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store it safely&lt;/li&gt;
&lt;li&gt;resolve it when needed&lt;/li&gt;
&lt;li&gt;avoid leaking it into Git&lt;/li&gt;
&lt;li&gt;keep the project setup reproducible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For open source maintainers, this matters. A project can require credentials for local or CI workflows and still provide a clean setup path for contributors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; is just one example of a broader problem.&lt;/p&gt;

&lt;p&gt;Projects often need secrets during local development, CI/CD, tests, and runtime. The hard part is not only storing those secrets safely. It is keeping the mapping between "what the app needs" and "where the secret lives" clear and consistent.&lt;/p&gt;

&lt;p&gt;The pattern I like is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secret values outside Git
secret mappings inside Git
one contract for local dev, CI/CD, and runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the reason I am building Envilder.&lt;/p&gt;

&lt;p&gt;Not to add another place to store secrets.&lt;/p&gt;

&lt;p&gt;Just to make secret resolution explicit, repeatable, and versionable.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>localstack</category>
      <category>docker</category>
      <category>secrets</category>
    </item>
    <item>
      <title>One year of Envilder: from a CLI script to SDKs, push mode, and a website</title>
      <dc:creator>Marçal Albert</dc:creator>
      <pubDate>Thu, 07 May 2026 00:11:10 +0000</pubDate>
      <link>https://dev.to/macalbert/one-year-of-envilder-from-a-cli-script-to-sdks-push-mode-and-a-website-h49</link>
      <guid>https://dev.to/macalbert/one-year-of-envilder-from-a-cli-script-to-sdks-push-mode-and-a-website-h49</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a follow-up to my &lt;a href="https://dev.to/macalbert/stop-hardcoding-secrets-generate-env-files-from-aws-ssm-with-a-simple-cli-f77"&gt;first post about Envilder&lt;/a&gt;, where I introduced it as a simple CLI to generate &lt;code&gt;.env&lt;/code&gt; files from AWS SSM. A year later, it has grown quite a bit, and I want to share what changed, why, and what I learned building it almost entirely with AI.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where we left off
&lt;/h2&gt;

&lt;p&gt;A year ago, Envilder did one thing: pull secrets from AWS SSM Parameter Store and write them to a &lt;code&gt;.env&lt;/code&gt; file. That was already useful. It removed the "what's the DB password?" Slack messages and kept onboarding fast.&lt;/p&gt;

&lt;p&gt;But real usage exposed real gaps. If you haven't seen Envilder before, here's the basic flow: create secrets via CLI and consume them in your app:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1189935282" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The map file: one contract, every context
&lt;/h2&gt;

&lt;p&gt;Before getting into features, this is the core idea behind Envilder and what makes it different from ad-hoc scripts.&lt;/p&gt;

&lt;p&gt;Everything in Envilder revolves around a single JSON file: &lt;code&gt;envilder.json&lt;/code&gt;. It's a declarative contract that decouples your application from the secret provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://envilder.com/schema/map-file.v1.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-profile"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DB_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/my-app/prod/db-password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/my-app/prod/api-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"STRIPE_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/my-app/prod/stripe-secret"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Left side: the environment variable name your app expects. Right side: the path in your secret provider. The &lt;code&gt;$config&lt;/code&gt; block declares the provider and credentials.&lt;/p&gt;

&lt;p&gt;The key point: &lt;strong&gt;your application never knows where secrets come from&lt;/strong&gt;. It depends on &lt;code&gt;envilder.json&lt;/code&gt;, not on AWS SSM, not on Azure Key Vault. If you migrate from SSM to Key Vault tomorrow, you change the &lt;code&gt;$config&lt;/code&gt; block. Your app code, your CI/CD pipelines, your onboarding scripts don't change at all. The SDKs even let you override the provider at runtime via a fluent builder, without touching the file.&lt;/p&gt;

&lt;p&gt;At the SDK level, this goes further: the code depends on an &lt;code&gt;ISecretProvider&lt;/code&gt; abstraction, not on AWS or Azure directly. The map file feeds that abstraction. Switch provider in &lt;code&gt;$config&lt;/code&gt;, your app code stays the same.&lt;/p&gt;

&lt;p&gt;And because &lt;code&gt;envilder.json&lt;/code&gt; contains paths, not values, it &lt;strong&gt;lives in your repo&lt;/strong&gt;, versionable, reviewable, shared across the team. When someone joins the project, they run one command and get every secret they need. When someone adds a new secret, the diff in &lt;code&gt;envilder.json&lt;/code&gt; is the documentation. The same file is consumed by the CLI, all three SDKs, and the GitHub Action. One contract, every context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed (and why)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Push mode: stop switching tools
&lt;/h3&gt;

&lt;p&gt;The first friction: every time someone needed to add a new secret, they had to open the AWS Console or use the AWS CLI directly. Envilder was a read-only tool.&lt;/p&gt;

&lt;p&gt;Now you can push secrets to the cloud directly from Envilder. The powerful part: it accepts an &lt;code&gt;.env&lt;/code&gt; file and your &lt;code&gt;envilder.json&lt;/code&gt;, and pushes everything in one go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envilder &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env &lt;span class="nt"&gt;--map-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;envilder.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads the &lt;code&gt;.env&lt;/code&gt; values, maps them through &lt;code&gt;envilder.json&lt;/code&gt; to the right provider paths, and creates (or updates) all secrets at once. New project? Define your &lt;code&gt;envilder.json&lt;/code&gt;, write your &lt;code&gt;.env&lt;/code&gt; locally, push. Done. No clicking through the AWS Console, no running 15 &lt;code&gt;aws ssm put-parameter&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;You can also push a single key when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envilder &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DB_PASSWORD &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12345 &lt;span class="nt"&gt;--ssm-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/my-app/db/password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One tool for reading and writing. No context switching.&lt;/p&gt;

&lt;p&gt;One detail worth noting about pull: when generating your &lt;code&gt;.env&lt;/code&gt;, Envilder only overwrites the keys declared in &lt;code&gt;envilder.json&lt;/code&gt;. Any other variables already in your &lt;code&gt;.env&lt;/code&gt; are left untouched. This means you can safely mix Envilder-managed secrets with local-only variables (like &lt;code&gt;DEBUG=true&lt;/code&gt; or &lt;code&gt;LOG_LEVEL=verbose&lt;/code&gt;) without losing them on every pull.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SDKs: because &lt;code&gt;.env&lt;/code&gt; files are a liability
&lt;/h3&gt;

&lt;p&gt;Generating &lt;code&gt;.env&lt;/code&gt; files was the original goal, but it's inherently insecure: secrets land on disk, they get accidentally committed, they go stale. The safer approach is resolving secrets at runtime, directly in your application.&lt;/p&gt;

&lt;p&gt;Envilder now has SDKs for three platforms:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt; (&lt;code&gt;pip install envilder&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;envilder&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Envilder&lt;/span&gt;

&lt;span class="n"&gt;Envilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;envilder.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# secrets are now in os.environ, no file on disk
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;.NET&lt;/strong&gt; (&lt;code&gt;dotnet add package Envilder&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Via IConfiguration (ASP.NET)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ConfigurationBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEnvilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"envilder.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Or one-liner&lt;/span&gt;
&lt;span class="n"&gt;Envilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"envilder.json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The .NET SDK targets .NET Standard 2.0, so it works from .NET Framework 4.6.1 all the way to .NET 10. It integrates natively with &lt;code&gt;IConfiguration&lt;/code&gt; and &lt;code&gt;IServiceCollection&lt;/code&gt;. It feels like a first-class .NET library, not a wrapper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript/JavaScript&lt;/strong&gt; (&lt;code&gt;@envilder/sdk&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Envilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@envilder/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Envilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;envilder.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three SDKs support a fluent builder for runtime overrides (switch profile, provider, or vault URL without touching the map file), environment-based loading, and secret validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Azure Key Vault support
&lt;/h3&gt;

&lt;p&gt;This is the payoff of the abstraction. The SDK architecture already had a provider interface (&lt;code&gt;ISecretProvider&lt;/code&gt;), so adding Azure Key Vault was a natural extension. Same &lt;code&gt;envilder.json&lt;/code&gt;, different &lt;code&gt;$config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"azure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vaultUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://my-vault.vault.azure.net"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DB_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db-password"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app code doesn't change. You can even implement your own provider (HashiCorp Vault, GCP Secret Manager) by plugging in the interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. LocalStack supports Envilder, and Envilder uses itself
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://localstack.cloud/" rel="noopener noreferrer"&gt;LocalStack&lt;/a&gt; supports the Envilder project, which means the acceptance tests run against a local AWS emulation with full SSM support.&lt;/p&gt;

&lt;p&gt;The fun part: LocalStack now requires an auth token to run. How does Envilder manage that token? &lt;strong&gt;With itself.&lt;/strong&gt; The CI pipeline uses Envilder to resolve the &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; from SSM before running the tests that validate Envilder against LocalStack. It references itself as a dependency of its own test infrastructure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I wrote up this pattern in full, from the &lt;code&gt;envilder.json&lt;/code&gt; mapping to the Docker Compose wiring that keeps the token out of Git: &lt;a href="https://dev.to/macalbert/how-i-run-localstack-without-committing-localstackauthtoken-34gk"&gt;How I run LocalStack without committing LOCALSTACK_AUTH_TOKEN&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5. GitHub Action
&lt;/h3&gt;

&lt;p&gt;CI/CD was always the primary use case, so there's now an official GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macalbert/envilder/github-action@v0.7.9&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;map-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;envilder.json&lt;/span&gt;
    &lt;span class="na"&gt;env-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. A website
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://envilder.com" rel="noopener noreferrer"&gt;envilder.com&lt;/a&gt;: documentation, schema references, and a landing page. Nothing fancy, but it makes the project feel real and gives the SDKs a proper home.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Envilder fits
&lt;/h2&gt;

&lt;p&gt;Doppler and Infisical are full platforms with their own infrastructure. Your secrets are stored on their servers, managed through their dashboards. That's a valid choice, but it comes with cost (Doppler Team at $21/user/month, Infisical Pro at $18/identity/month) and a trust trade-off.&lt;/p&gt;

&lt;p&gt;Envilder has no infrastructure. Zero. Your secrets stay in your cloud (AWS SSM or Azure Key Vault) and never pass through a third party. Audit trails? CloudTrail or Azure Monitor. Access control? IAM or Azure RBAC. Encryption at rest, versioning? Already there. You're not giving anything up. Those capabilities come from the provider you already pay for.&lt;/p&gt;

&lt;p&gt;Envilder just makes them easy to consume everywhere: local dev, CI/CD, application runtime. One &lt;code&gt;envilder.json&lt;/code&gt;, one command, SDKs for three platforms. No servers, no middleman, no SaaS fees. MIT licensed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI part (brief version, full post coming)
&lt;/h2&gt;

&lt;p&gt;I built most of this project with AI assistance. Started with GPT-4 a year ago, went through several Claude models (Sonnet, Haiku), and the real leap came with Claude Opus 4.5 for planning and architecture decisions. For implementation with good skill definitions, Sonnet remains excellent and more cost-effective.&lt;/p&gt;

&lt;p&gt;I also use &lt;a href="https://coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt; for automated code review on every PR. It catches things I miss and forces consistency.&lt;/p&gt;

&lt;p&gt;I'm writing a dedicated post about what worked, what didn't, and where AI genuinely accelerated the project versus where it got in the way. Stay tuned.&lt;/p&gt;




&lt;h2&gt;
  
  
  Numbers (honest ones)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;350 commits, 39 releases&lt;/li&gt;
&lt;li&gt;8 contributors: 1 human, 7 bots (Copilot, Gemini, CodeRabbit, Jules, Dependabot, GitHub Advanced Security)&lt;/li&gt;
&lt;li&gt;SDKs published on &lt;a href="https://www.npmjs.com/package/@envilder/sdk" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, &lt;a href="https://pypi.org/project/envilder/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;, and &lt;a href="https://www.nuget.org/packages/Envilder" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; with full CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Used internally across multiple projects at &lt;a href="https://m47labs.com" rel="noopener noreferrer"&gt;M47 AI Company&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Trusted Publishing enabled on PyPI (Sigstore attestations)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/macalbert/envilder/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt; includes drift detection (check mode), auto-discovery for bulk parameter fetching, and more provider backends. But the priority is adoption: making it easy for people to try, and making sure it works perfectly for the simple use cases it targets.&lt;/p&gt;

&lt;p&gt;If you manage secrets with AWS SSM or Azure Key Vault and the big platforms feel like overkill, give Envilder a try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;a href="https://envilder.com" rel="noopener noreferrer"&gt;envilder.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔧 &lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://www.npmjs.com/package/envilder" rel="noopener noreferrer"&gt;npm (CLI)&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@envilder/sdk" rel="noopener noreferrer"&gt;npm (SDK)&lt;/a&gt; · &lt;a href="https://pypi.org/project/envilder/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; · &lt;a href="https://www.nuget.org/packages/Envilder" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love feedback, especially from people who tried the first version.&lt;/p&gt;

&lt;p&gt;A special thanks to &lt;a href="https://www.linkedin.com/in/brianrinaldi/" rel="noopener noreferrer"&gt;Brian Rinaldi&lt;/a&gt; and the &lt;a href="https://localstack.cloud/" rel="noopener noreferrer"&gt;LocalStack&lt;/a&gt; team for accepting Envilder into the LocalStack for Open Source program. Their fully subsidized Ultimate tier license is what powers our acceptance test suite against a local AWS emulation.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>ai</category>
      <category>aws</category>
    </item>
    <item>
      <title>From chaos to consistency: how we centralized secrets with AWS SSM and a simple CLI</title>
      <dc:creator>Marçal Albert</dc:creator>
      <pubDate>Sat, 10 May 2025 16:18:29 +0000</pubDate>
      <link>https://dev.to/macalbert/stop-hardcoding-secrets-generate-env-files-from-aws-ssm-with-a-simple-cli-f77</link>
      <guid>https://dev.to/macalbert/stop-hardcoding-secrets-generate-env-files-from-aws-ssm-with-a-simple-cli-f77</guid>
      <description>&lt;p&gt;At &lt;a href="https://m47labs.com" rel="noopener noreferrer"&gt;M47&lt;/a&gt;, security is one of our concerns across all our AI and cloud-native projects. That’s why we store all sensitive configurations in a secure and centralized place like the AWS SSM Parameter Store. While our repositories are private, that’s not enough. Secrets don’t belong in code.&lt;/p&gt;

&lt;p&gt;We deliberately use &lt;strong&gt;Parameter Store over Secrets Manager&lt;/strong&gt; because our needs don’t require secret rotation or tight lifecycle policies, and SSM gives us all the flexibility and control we need for storing tokens, API keys, and service credentials.&lt;/p&gt;

&lt;p&gt;But then comes the friction:&lt;br&gt;&lt;br&gt;
Every time we onboard someone, clone a project, create/update a new CD pipeline, or adjust a Dockerfile, &lt;strong&gt;we have to manually fetch and sync secrets&lt;/strong&gt;. This leads to overhead, mistakes, and inconsistencies between local and CD pipelines.&lt;/p&gt;

&lt;p&gt;That’s why I built &lt;strong&gt;Envilder&lt;/strong&gt;. A CLI tool to automate the generation of &lt;code&gt;.env&lt;/code&gt; files from a single source of truth: AWS SSM.&lt;br&gt;&lt;br&gt;
It helps us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep secrets in one place (SSM)&lt;/li&gt;
&lt;li&gt;Stay consistent across teams and environments&lt;/li&gt;
&lt;li&gt;Avoid copy-pasting or writing fragile scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since we often work with &lt;strong&gt;multiple AWS CLI profiles&lt;/strong&gt;, Envilder also supports switching profiles easily to handle multi-account setups.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 [Full guide continues below ⬇️]&lt;/p&gt;


&lt;h2&gt;
  
  
  💡 What does it do?
&lt;/h2&gt;

&lt;p&gt;Envilder reads a mapping file that links environment variable names to AWS SSM parameter paths. Then it fetches the values securely and writes a clean &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧩 Example
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;param-map.json&lt;/code&gt; might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DB_HOST"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/my-app/dev/DB_HOST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DB_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/my-app/dev/DB_PASSWORD"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envilder &lt;span class="nt"&gt;--map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;param-map.json &lt;span class="nt"&gt;--envfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_HOST=mydb.cluster-xyz.rds.amazonaws.com
DB_PASSWORD=supersecret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use different AWS CLI profiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging envilder &lt;span class="nt"&gt;--map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;param-map.json &lt;span class="nt"&gt;--envfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  👥 Why it helps teams
&lt;/h2&gt;

&lt;p&gt;This small tool makes a big difference when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Onboarding new team members&lt;/strong&gt;: no more “what’s the DB password?”&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Keeping environments in sync&lt;/strong&gt;: any change in SSM is reflected across the team&lt;/li&gt;
&lt;li&gt;⚙️ &lt;strong&gt;CI/CD pipelines always up-to-date&lt;/strong&gt;: e.g. GitHub Actions, CodeBuild, GitLab&lt;/li&gt;
&lt;li&gt;🧼 &lt;strong&gt;Centralized configuration&lt;/strong&gt;: avoid duplication and keep secrets in one secure place&lt;/li&gt;
&lt;li&gt;🧭 &lt;strong&gt;Supports multiple AWS profiles&lt;/strong&gt;: ideal for multi-account or multi-env setups&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Works with SecureString and plain parameters&lt;/li&gt;
&lt;li&gt;CLI-first, fast, and script-friendly&lt;/li&gt;
&lt;li&gt;Compatible with any CI/CD system&lt;/li&gt;
&lt;li&gt;Supports static values and fallbacks&lt;/li&gt;
&lt;li&gt;AWS profile support (&lt;code&gt;AWS_PROFILE&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📦 Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; envilder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envilder &lt;span class="nt"&gt;--map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;param-map.json &lt;span class="nt"&gt;--envfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws-account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🙌 I’d love your feedback
&lt;/h2&gt;

&lt;p&gt;It’s still an early-stage project, but already helpful in several real-world teams.&lt;/p&gt;

&lt;p&gt;If this sounds familiar, or you’ve solved this differently, I’d love to hear from you.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;https://github.com/macalbert/envilder&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update (2026):&lt;/strong&gt; &lt;em&gt;That was stage 1: a CLI that writes &lt;code&gt;.env&lt;/code&gt; files. Envilder has since leveled up with SDKs for three languages, push mode, and Azure support. → Next level: &lt;a href="https://dev.to/macalbert/one-year-of-envilder-from-a-cli-script-to-sdks-push-mode-and-a-website-h49"&gt;One year of Envilder&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
