<?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/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/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 now needs an auth token: getting it into Testcontainers without a .env file</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;Since March 2026, &lt;a href="https://blog.localstack.cloud/localstack-for-aws-release-2026-03-0/" rel="noopener noreferrer"&gt;the LocalStack image won't start without &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;&lt;/a&gt;. Here's how I feed it to Testcontainers (TypeScript, Python, .NET) and Aspire straight from AWS SSM, with no &lt;code&gt;.env&lt;/code&gt; file in between.&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;/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%2Fhtgzuxoxgh8b80wsa6li.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhtgzuxoxgh8b80wsa6li.png" alt="Generated .env vs resolving the token in the test fixture" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&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;
  
  
  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="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;"/demo/db/url"&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;&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%2Fnrd09621u3dzsbpi8tck.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnrd09621u3dzsbpi8tck.png" alt="One envilder.json map resolved by .NET, Python, TypeScript and CI" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&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;
  
  
  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
STRIPE_API_KEY        -&amp;gt; /demo/stripe/api-key    # 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 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;Previous post (Docker Compose + generated &lt;code&gt;.env&lt;/code&gt;): &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;/li&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>testing</category>
      <category>testcontainers</category>
      <category>aws</category>
      <category>localstack</category>
    </item>
    <item>
      <title>LocalStack now requires an auth token: keeping it out of Git with Docker Compose</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;
  
  
  How I run LocalStack without committing LOCALSTACK_AUTH_TOKEN
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Updated October 2026:&lt;/strong&gt; This article now includes &lt;code&gt;envilder exec&lt;/code&gt;, Azure Key Vault, and LocalStack for Azure. The original &lt;code&gt;.env&lt;/code&gt; workflow is still here, just no longer the default approach I recommend.&lt;/p&gt;
&lt;/blockquote&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;, starting the AWS emulator requires an auth token. With Docker Compose, that means supplying &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fair enough. What I didn't want was another secret pasted into a Compose file, copied between scripts, or remembered as an undocumented &lt;code&gt;export&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;I wanted the repository to explain what the project needs, without containing the actual credential.&lt;/p&gt;

&lt;p&gt;I ran into this while building &lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;Envilder&lt;/a&gt;. The solution turned out to be slightly recursive. We'll get to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;The part I want in Git is the answer to this question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where does this project get &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Envilder reads that answer from a JSON mapping and retrieves the value from AWS SSM Parameter Store or Azure Key Vault. It can then either start a command with the secret in its environment (&lt;strong&gt;exec mode&lt;/strong&gt;) or generate a local &lt;code&gt;.env&lt;/code&gt; file (&lt;strong&gt;pull mode&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Same tool, same mapping, two ways to deliver the value.&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%2Femn4fxtp8as1d65gcy9h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Femn4fxtp8as1d65gcy9h.png" alt="Envilder resolves secrets from AWS SSM or Azure Key Vault and supplies them to Docker Compose through exec or pull mode" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For starting LocalStack, I now prefer exec mode. No need to create a file just so another process can read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose where the token lives
&lt;/h2&gt;

&lt;p&gt;Pick one of these configurations for your &lt;code&gt;envilder.json&lt;/code&gt;. You don't need both clouds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With AWS SSM Parameter Store:&lt;/strong&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;"$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;Store your token at that parameter path as a &lt;code&gt;SecureString&lt;/code&gt;, and give your AWS identity permission to read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Azure Key Vault:&lt;/strong&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;"$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;"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-development-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;"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;"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;Store the token as a secret named &lt;code&gt;localstack-auth-token&lt;/code&gt; in that vault. For local development, Envilder can use your &lt;code&gt;az login&lt;/code&gt; session, provided your identity has permission to read the secret.&lt;/p&gt;

&lt;p&gt;In both cases, the mapping contains a reference, not the credential. That's what goes into Git. You'll also need Docker Compose and Node.js 22.12+; the &lt;a href="https://github.com/macalbert/envilder/blob/main/docs/requirements-installation.md" rel="noopener noreferrer"&gt;setup guide&lt;/a&gt; covers authentication.&lt;/p&gt;

&lt;p&gt;One important detail: &lt;a href="https://docs.localstack.cloud/azure/getting-started/auth-token/" rel="noopener noreferrer"&gt;Developer Auth Tokens are personal&lt;/a&gt;. Each developer must resolve their own token, for example through separate accounts or vaults, or a per-user mapping. CI needs a separate CI Auth Token. A shared setup must not become a shared personal credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let Compose do the rest
&lt;/h2&gt;

&lt;p&gt;Here's a minimal &lt;code&gt;compose.yaml&lt;/code&gt; for LocalStack for AWS:&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;127.0.0.1:4566:4566"&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:?Start&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Compose&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;through&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;exec}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&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="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command works with either mapping above.&lt;/p&gt;

&lt;p&gt;Envilder resolves the token and starts Docker Compose with it in the process environment. &lt;a href="https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/" rel="noopener noreferrer"&gt;Compose interpolates the variable&lt;/a&gt; and passes it to LocalStack.&lt;/p&gt;

&lt;p&gt;No manual export. No &lt;code&gt;.env&lt;/code&gt; generated by Envilder. And if the secret is missing, empty, or unreadable, &lt;a href="https://github.com/macalbert/envilder/blob/main/docs/exec-command.md" rel="noopener noreferrer"&gt;exec mode&lt;/a&gt; refuses to start Compose rather than hoping for the best.&lt;/p&gt;

&lt;p&gt;This is a minimal startup example. Add service-specific ports or Docker socket access only when your workload needs them, and pin a tested image version for reproducible CI runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  And LocalStack for Azure?
&lt;/h3&gt;

&lt;p&gt;For the same basic setup, replace the image with:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localstack/localstack-azure:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.localstack.cloud/azure/getting-started/" rel="noopener noreferrer"&gt;LocalStack for Azure&lt;/a&gt; also uses &lt;code&gt;LOCALSTACK_AUTH_TOKEN&lt;/code&gt; and port &lt;code&gt;4566&lt;/code&gt; by default. The same token can activate either emulator when your account has the corresponding access. At the time of this update, Azure is in private preview and requires access to be enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where you store the token and which emulator you run are independent choices.&lt;/strong&gt; Azure Key Vault can supply the token for LocalStack for AWS. AWS SSM can supply it for LocalStack for Azure.&lt;/p&gt;

&lt;p&gt;Just use a real cloud secret store for this bootstrap credential. Asking an emulated Key Vault for the token needed to start that same emulator is a chicken-and-egg problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still need a .env file?
&lt;/h2&gt;

&lt;p&gt;The original approach remains available:&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;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. Use this when a tool genuinely needs a file; otherwise, exec mode saves a step and avoids another plaintext copy.&lt;/p&gt;

&lt;p&gt;When switching, remove any old secret-bearing &lt;code&gt;.env&lt;/code&gt; you no longer need. Exec mode doesn't delete it, and Compose can still load it independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not protect
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No generated &lt;code&gt;.env&lt;/code&gt; does not mean the token never touches disk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker retains container environment variables in its configuration metadata. Someone with sufficient Docker access can read them through &lt;code&gt;docker inspect&lt;/code&gt;. Likewise, &lt;code&gt;docker compose config&lt;/code&gt; can print resolved values, so don't paste its output into a public issue without checking it.&lt;/p&gt;

&lt;p&gt;The command you start, and its child processes, can also read the injected secrets. Exec mode is not a sandbox, and it resolves values once at startup rather than continuously refreshing them.&lt;/p&gt;

&lt;p&gt;The improvement is specific: one fewer plaintext secret file in the working directory. Useful, but not magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The slightly meta part
&lt;/h2&gt;

&lt;p&gt;I originally ran into this while working on Envilder's integration tests.&lt;/p&gt;

&lt;p&gt;Envilder uses LocalStack. LocalStack needs a token. So I used Envilder to resolve the token needed to start the LocalStack environment used to test Envilder.&lt;/p&gt;

&lt;p&gt;At that point, the meme was unavoidable.&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 resolve the LocalStack token needed to test Envilder" width="500" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Originally, that meant generating a &lt;code&gt;.env&lt;/code&gt; file. Exec mode now offers another way to deliver the same secret without changing the mapping.&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;
  
  
  Why not lstk, direnv, 1Password, or chamber?
&lt;/h2&gt;

&lt;p&gt;For running LocalStack by itself, I'd start with &lt;a href="https://docs.localstack.cloud/aws/developer-tools/running-localstack/lstk/authentication/" rel="noopener noreferrer"&gt;lstk&lt;/a&gt;. It handles login and normally stores the token in the system keyring. You don't need to add a cloud secret store just to try LocalStack.&lt;/p&gt;

&lt;p&gt;There are good options for larger workflows too. &lt;a href="https://direnv.net/" rel="noopener noreferrer"&gt;direnv&lt;/a&gt; can run your secret-retrieval commands when you enter a directory. &lt;a href="https://www.1password.dev/cli/reference/commands/run" rel="noopener noreferrer"&gt;1Password &lt;code&gt;op run&lt;/code&gt;&lt;/a&gt; injects secrets from 1Password. &lt;a href="https://github.com/segmentio/chamber" rel="noopener noreferrer"&gt;chamber&lt;/a&gt; already handles AWS SSM and command execution.&lt;/p&gt;

&lt;p&gt;Envilder isn't claiming to have invented environment injection.&lt;/p&gt;

&lt;p&gt;What I'm building around is the explicit &lt;code&gt;variable name -&amp;gt; secret path&lt;/code&gt; mapping: one JSON contract format shared by the CLI, GitHub Action, and .NET, Python, and Node.js SDKs, using AWS SSM or Azure Key Vault.&lt;/p&gt;

&lt;p&gt;If a small script already solves your problem, keep it. The contract becomes useful when you would otherwise maintain the same mapping in several different places.&lt;/p&gt;

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

&lt;p&gt;The original goal was to keep the token out of Git. Exec mode goes a little further: I no longer need to generate a local &lt;code&gt;.env&lt;/code&gt; just to hand that token to Docker Compose.&lt;/p&gt;

&lt;p&gt;The secret stays managed in my cloud provider. The repository describes where to find it. LocalStack receives the variable it already expects.&lt;/p&gt;

&lt;p&gt;That's why I'm building &lt;a href="https://github.com/macalbert/envilder" rel="noopener noreferrer"&gt;Envilder&lt;/a&gt;: not to create another place to store secrets, but to make resolving them explicit and repeatable.&lt;/p&gt;

&lt;p&gt;How are you handling this in your own projects? I'd be interested in hearing where your setup differs, especially where this approach wouldn't fit.&lt;/p&gt;

</description>
      <category>localstack</category>
      <category>docker</category>
      <category>aws</category>
      <category>security</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>
