<?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>Stop passing around .env files: Python secrets from AWS SSM</title>
      <dc:creator>Marçal Albert</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/macalbert/stop-passing-around-env-files-python-secrets-from-aws-ssm-31cn</link>
      <guid>https://dev.to/macalbert/stop-passing-around-env-files-python-secrets-from-aws-ssm-31cn</guid>
      <description>&lt;p&gt;I thought moving secrets to AWS SSM Parameter Store had solved our configuration problem.&lt;/p&gt;

&lt;p&gt;It solved storage.&lt;/p&gt;

&lt;p&gt;It did not solve distribution.&lt;/p&gt;

&lt;p&gt;More precisely, it did not solve how each execution context knew which parameters the application needed.&lt;/p&gt;

&lt;p&gt;Across several AI and cloud projects, the same friction appeared whenever I switched repositories or helped someone run one.&lt;/p&gt;

&lt;p&gt;A developer requested access. After approval, they still had to find the correct parameters, copy every value and build a local &lt;code&gt;.env&lt;/code&gt;. Sometimes someone sent them one, and we had to work out whether it matched the current branch, included the latest variables or predated a secret rotation.&lt;/p&gt;

&lt;p&gt;The same mapping also appeared across delivery systems. In one of my projects, developers run the service locally, CI runs in GitHub Actions and CD runs through AWS CodePipeline. Application values or their paths could end up repeated in local files, GitHub configuration and AWS pipeline configuration. Every execution context created another configuration surface to keep aligned.&lt;/p&gt;

&lt;p&gt;This created two kinds of drift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Value drift:&lt;/strong&gt; AWS contains the current value, but a copied &lt;code&gt;.env&lt;/code&gt; or GitHub Secret contains an older one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract drift:&lt;/strong&gt; the code expects a variable that one or more execution contexts do not provide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was difficult. It was repetitive, manual and easy to get wrong.&lt;/p&gt;

&lt;p&gt;That is why I built &lt;a href="https://envilder.com/" rel="noopener noreferrer"&gt;Envilder&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Envilder is an open-source secret-resolution toolkit for AWS SSM Parameter Store and Azure Key Vault. It provides runtime SDKs, a CLI and a GitHub Action around one versioned map-file format.&lt;/p&gt;

&lt;p&gt;Its core idea is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git defines what the application needs.
AWS stores the current values.
IAM decides who can access them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  boto3 is a good baseline
&lt;/h2&gt;

&lt;p&gt;Python can already load SSM parameters with boto3:&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;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;mappings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/my-app/development/database-url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/my-app/development/api-token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ssm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ssm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameter_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parameter_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;WithDecryption&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Parameter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is small, clear and valid. If one isolated Python service owns a few parameters, I would seriously consider stopping here.&lt;/p&gt;

&lt;p&gt;Envilder uses boto3 internally. Its value lies not in replacing &lt;code&gt;get_parameter()&lt;/code&gt;, but in turning the mapping into a resolution contract that follows the code and can be reused everywhere the project runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map becomes the resolution contract
&lt;/h2&gt;

&lt;p&gt;Envilder moves the mapping into &lt;code&gt;envilder.json&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;"DATABASE_URL"&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/development/database-url"&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_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;"/my-app/development/api-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 left side is the name expected by the application. The right side is its path in AWS SSM.&lt;/p&gt;

&lt;p&gt;The file contains paths, not secret values, so it can live beside the code and be reviewed in pull requests. Parameter paths can still reveal infrastructure information, so review them before publishing the map in a public repository.&lt;/p&gt;

&lt;p&gt;Install the Python SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add envilder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then load the mapping during application startup. Basic validation of the resolved result is opt-in:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validate_secrets&lt;/span&gt;

&lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="o"&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;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="nf"&gt;validate_secrets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Envilder.load()&lt;/code&gt; resolves the available parameters, injects them into &lt;code&gt;os.environ&lt;/code&gt; and returns them as a dictionary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;validate_secrets()&lt;/code&gt; checks the resolved dictionary, not completeness against the map. It raises &lt;code&gt;SecretValidationError&lt;/code&gt; when the dictionary is empty or an included value is empty or whitespace-only.&lt;/p&gt;

&lt;p&gt;Missing SSM parameters are omitted before this check, while permission, credential and other provider errors still propagate. The application's normal startup configuration layer therefore remains responsible for required keys, types and semantic validation.&lt;/p&gt;

&lt;p&gt;The map defines how application variable names map to provider paths.&lt;/p&gt;

&lt;p&gt;Do not log &lt;code&gt;resolved&lt;/code&gt;. It contains the secret values.&lt;/p&gt;

&lt;p&gt;I named the project Envilder as a blend of &lt;strong&gt;env&lt;/strong&gt; and &lt;strong&gt;builder&lt;/strong&gt;: it builds the process environment from a versioned map.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj070onj5ag548v4d82ft.gif" 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%2Fj070onj5ag548v4d82ft.gif" alt="Monkey puppet looking awkward after the Envilder name reveal" width="376" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Branches carry their requirements
&lt;/h2&gt;

&lt;p&gt;Suppose a pull request introduces:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The same pull request adds its SSM mapping to &lt;code&gt;envilder.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Anyone checking out that branch receives the matching resolution contract. A CI/CD workflow using the map receives it from the same commit.&lt;/p&gt;

&lt;p&gt;The parameter must still exist in AWS, and each consumer must have permission to read it. Envilder cannot create missing infrastructure, grant access or force someone to update the map.&lt;/p&gt;

&lt;p&gt;What it does is make the requirement visible beside the code that needs it. Reviewers can catch a missing or incorrect mapping before another developer encounters a broken local setup or deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop replicating application secrets
&lt;/h2&gt;

&lt;p&gt;A common solution is to copy application secrets from AWS into GitHub Secrets.&lt;/p&gt;

&lt;p&gt;That works, but the same value now exists in two systems. When the AWS value changes, someone must update the GitHub copy. Adding another repository or environment creates more copies.&lt;/p&gt;

&lt;p&gt;If the application already calls &lt;code&gt;Envilder.load()&lt;/code&gt; at startup, a workflow that runs the application does not need an Envilder-specific resolution step. It only needs an AWS identity and can start the application as usual. The relevant job fragment is:&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="c1"&gt;# ...workflow trigger, job and runner configuration omitted...&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;actions/checkout@v7&lt;/span&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;actions/setup-python@v7&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;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&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;astral-sh/setup-uv@v9.0.0&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv sync --locked&lt;/span&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;aws-actions/configure-aws-credentials@v6&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;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.AWS_ROLE_TO_ASSUME }}&lt;/span&gt;
      &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.AWS_REGION }}&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run python -m my_app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;configure-aws-credentials&lt;/code&gt; exchanges GitHub's OIDC token for short-lived AWS credentials. boto3 finds those credentials through its default chain, and the Python SDK resolves &lt;code&gt;envilder.json&lt;/code&gt; inside the application process.&lt;/p&gt;

&lt;p&gt;No application secret needs to be copied into GitHub Secrets, repeated in the workflow or written to a &lt;code&gt;.env&lt;/code&gt; in this path. The workflow still selects the correct AWS role, region and application command. Those are execution-context decisions, not application secret values.&lt;/p&gt;

&lt;p&gt;CI and CD do not need to live on the same platform. In my case, GitHub Actions runs CI while AWS CodePipeline orchestrates deployment. CodeBuild has its own IAM service role, and the deployed workload can use an ECS task role or Lambda execution role.&lt;/p&gt;

&lt;p&gt;The deployment pipeline may not need to read application secrets at all. It can deploy the code and its map together, then let the application resolve the values at startup. If a CodeBuild command genuinely needs a secret, it can use the same map through its own AWS identity instead of maintaining another copy.&lt;/p&gt;

&lt;p&gt;If a build or deployment tool specifically requires an environment file, the Envilder CLI and GitHub Action remain available as alternative delivery mechanisms.&lt;/p&gt;

&lt;p&gt;Version tags are shown here for readability. In production workflows, pin actions to full commit SHAs according to your supply-chain policy.&lt;/p&gt;

&lt;p&gt;The important difference is that &lt;code&gt;DATABASE_URL&lt;/code&gt; and &lt;code&gt;API_TOKEN&lt;/code&gt; no longer need to be maintained as long-lived GitHub Secrets. AWS remains their source of truth, the map remains part of the code review and the SDK resolves the values only when the process starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  From handoff to checkout
&lt;/h2&gt;

&lt;p&gt;Envilder does not remove the access request. Any consumer that resolves secrets still needs the correct AWS account and region, the required SSM permissions and, when applicable, KMS permissions.&lt;/p&gt;

&lt;p&gt;It removes the manual work that often follows approval.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request access
Find every parameter
Copy every value
Build a local .env
Replicate values into CI/CD
Check whether everything matches the branch
Repeat for each project and environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request access
Authenticate with AWS
Check out the project
Run the application or pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS CLI is not required by the Python SDK, but it is useful for local profiles and AWS SSO. Production applications should normally use runtime IAM roles.&lt;/p&gt;

&lt;p&gt;This also answers the rotation question from the opening. A copied &lt;code&gt;.env&lt;/code&gt; or GitHub Secret remains stale after a value changes, while Envilder resolves the current value the next time the application starts, whether locally, in CI or in its runtime environment.&lt;/p&gt;

&lt;p&gt;It does not refresh an already running process automatically. Live refresh still requires an application-specific strategy.&lt;/p&gt;

&lt;p&gt;💡 &lt;em&gt;A supervised refresh mode that re-resolves secrets and restarts an Envilder-managed child process is &lt;a href="https://github.com/macalbert/envilder/issues/479" rel="noopener noreferrer"&gt;on the roadmap&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One map, several execution contexts
&lt;/h2&gt;

&lt;p&gt;The same map can travel through different systems while each context keeps its own AWS identity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;AWS identity&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local development&lt;/td&gt;
&lt;td&gt;AWS SSO or profile&lt;/td&gt;
&lt;td&gt;The SDK resolves secrets when the application starts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions CI&lt;/td&gt;
&lt;td&gt;IAM role assumed through OIDC&lt;/td&gt;
&lt;td&gt;Tests or the application resolve secrets inside their process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS CodePipeline with CodeBuild&lt;/td&gt;
&lt;td&gt;CodeBuild service role&lt;/td&gt;
&lt;td&gt;Deployment commands resolve only the secrets they genuinely need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECS or Lambda runtime&lt;/td&gt;
&lt;td&gt;ECS task role or Lambda execution role&lt;/td&gt;
&lt;td&gt;The application resolves secrets at startup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The credentials and permissions remain environment-specific. The shared part is the versioned resolution contract between application names and provider paths.&lt;/p&gt;

&lt;p&gt;Python, Node.js and .NET SDKs use that resolution contract inside the application process. The Envilder CLI and GitHub Action can generate a &lt;code&gt;.env&lt;/code&gt; when a tool explicitly requires one.&lt;/p&gt;

&lt;p&gt;Envilder also supports Azure Key Vault, and development, staging and production can use separate map files. Those topics deserve their own posts, so this article stays focused on Python and AWS SSM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the trade-off lands
&lt;/h2&gt;

&lt;p&gt;Envilder is not a secrets manager, proxy or new security boundary. The cloud provider remains the backing store and access boundary.&lt;/p&gt;

&lt;p&gt;It also does not remove every environment-specific decision. Any context that resolves secrets must still select the correct map and cloud identity for development, staging or production.&lt;/p&gt;

&lt;p&gt;Resolving directly from AWS also means local startup depends on network access and valid AWS credentials. Teams that require fully offline development will need a separate local override or emulation strategy.&lt;/p&gt;

&lt;p&gt;The current Python SDK makes one synchronous &lt;code&gt;get_parameter()&lt;/code&gt; call per mapping. Each call still inherits boto3's retry and backoff behaviour, but this SDK does not batch or parallelize resolution. For a large mapping or performance-sensitive startup, direct boto3 code using batched &lt;code&gt;get_parameters()&lt;/code&gt; calls may be better.&lt;/p&gt;

&lt;p&gt;Use boto3 directly when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One application owns a small mapping&lt;/li&gt;
&lt;li&gt;You need complete control over SSM requests&lt;/li&gt;
&lt;li&gt;You need custom batching, caching or live-refresh behaviour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are current boundaries, not permanent ones. &lt;a href="https://github.com/macalbert/envilder/issues/480" rel="noopener noreferrer"&gt;Batching for the Python SDK is already tracked&lt;/a&gt;, and caching or refresh should evolve from real use cases rather than guesses.&lt;/p&gt;

&lt;p&gt;If one of these limits affects your project, open an issue or contribute. With shared experience, we can grow Envilder together and solve common problems once instead of rebuilding the same conventions in every codebase.&lt;/p&gt;

&lt;p&gt;Use Envilder when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mapping changes should travel with branches and pull requests&lt;/li&gt;
&lt;li&gt;Developers should not exchange or rebuild &lt;code&gt;.env&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Local development, CI/CD and multiple runtimes need the same resolution contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The choice is not really boto3 versus Envilder.&lt;/p&gt;

&lt;p&gt;Envilder uses boto3.&lt;/p&gt;

&lt;p&gt;The choice is whether to maintain your own convention around it or use Envilder's map-file convention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push is optional
&lt;/h2&gt;

&lt;p&gt;The Python SDK only reads secrets.&lt;/p&gt;

&lt;p&gt;Envilder also includes an optional CLI for importing an existing &lt;code&gt;.env&lt;/code&gt; into the mapped provider paths:&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="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--envfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.bootstrap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--map&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;The CLI requires Node.js. Push is an explicit, one-way operation for bootstrapping or migration, not continuous synchronization.&lt;/p&gt;

&lt;p&gt;Treat &lt;code&gt;.env.bootstrap&lt;/code&gt; as sensitive migration input: do not commit it and remove it after the import.&lt;/p&gt;

&lt;p&gt;If you do not need it, do not use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on one service
&lt;/h2&gt;

&lt;p&gt;Pick a small, non-critical Python service that already uses SSM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Envilder.&lt;/li&gt;
&lt;li&gt;Commit its &lt;code&gt;envilder.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;Envilder.load()&lt;/code&gt; before the application's normal startup configuration validation.&lt;/li&gt;
&lt;li&gt;Let the versioned map travel through CI, deployment and the running service.&lt;/li&gt;
&lt;li&gt;Ask a teammate who has AWS access but no list of parameter paths or copied &lt;code&gt;.env&lt;/code&gt; to run it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Envilder adds more ceremony than it removes, keep the boto3 version.&lt;/p&gt;

&lt;p&gt;If it shortens the handoff and removes duplicated configuration across execution contexts, I would like to know what worked and what did not.&lt;/p&gt;

&lt;p&gt;Open an issue for confusing behaviour, missing documentation or anything that made setup harder than expected.&lt;/p&gt;

&lt;p&gt;Honest feedback from a real project is more useful to me than a generic feature request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://envilder.com/docs/sdks/python/" rel="noopener noreferrer"&gt;Python SDK documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://envilder.com/docs/providers/aws-ssm/" rel="noopener noreferrer"&gt;AWS SSM documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://envilder.com/docs/map-file/" rel="noopener noreferrer"&gt;Map-file documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/envilder/" rel="noopener noreferrer"&gt;Envilder on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;Envilder on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Envilder is open source under the MIT license.&lt;/p&gt;

&lt;p&gt;The storage problem was already solved.&lt;/p&gt;

&lt;p&gt;Envilder aims to make configuration handoffs explicit wherever code runs: for developers, coding agents, delivery pipelines and production runtimes.&lt;/p&gt;

</description>
      <category>python</category>
      <category>aws</category>
      <category>devops</category>
      <category>showdev</category>
    </item>
    <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>
