I thought moving secrets to AWS SSM Parameter Store had solved our configuration problem.
It solved storage.
It did not solve distribution.
More precisely, it did not solve how each execution context knew which parameters the application needed.
Across several AI and cloud projects, the same friction appeared whenever I switched repositories or helped someone run one.
A developer requested access. After approval, they still had to find the correct parameters, copy every value and build a local .env. 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.
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.
This created two kinds of drift:
-
Value drift: AWS contains the current value, but a copied
.envor GitHub Secret contains an older one. - Contract drift: the code expects a variable that one or more execution contexts do not provide.
None of this was difficult. It was repetitive, manual and easy to get wrong.
That is why I built Envilder.
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.
Its core idea is:
Git defines what the application needs.
AWS stores the current values.
IAM decides who can access them.
boto3 is a good baseline
Python can already load SSM parameters with boto3:
import os
import boto3
mappings = {
"DATABASE_URL": "/my-app/development/database-url",
"API_TOKEN": "/my-app/development/api-token",
}
ssm = boto3.client("ssm")
for variable_name, parameter_path in mappings.items():
response = ssm.get_parameter(
Name=parameter_path,
WithDecryption=True,
)
os.environ[variable_name] = response["Parameter"]["Value"]
This code is small, clear and valid. If one isolated Python service owns a few parameters, I would seriously consider stopping here.
Envilder uses boto3 internally. Its value lies not in replacing get_parameter(), but in turning the mapping into a resolution contract that follows the code and can be reused everywhere the project runs.
The map becomes the resolution contract
Envilder moves the mapping into envilder.json:
{
"DATABASE_URL": "/my-app/development/database-url",
"API_TOKEN": "/my-app/development/api-token"
}
The left side is the name expected by the application. The right side is its path in AWS SSM.
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.
Install the Python SDK:
uv add envilder
Then load the mapping during application startup. Basic validation of the resolved result is opt-in:
from envilder import Envilder, validate_secrets
resolved = Envilder.load("envilder.json")
validate_secrets(resolved)
Envilder.load() resolves the available parameters, injects them into os.environ and returns them as a dictionary.
validate_secrets() checks the resolved dictionary, not completeness against the map. It raises SecretValidationError when the dictionary is empty or an included value is empty or whitespace-only.
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.
The map defines how application variable names map to provider paths.
Do not log resolved. It contains the secret values.
I named the project Envilder as a blend of env and builder: it builds the process environment from a versioned map.
Branches carry their requirements
Suppose a pull request introduces:
PAYMENTS_API_TOKEN
The same pull request adds its SSM mapping to envilder.json.
Anyone checking out that branch receives the matching resolution contract. A CI/CD workflow using the map receives it from the same commit.
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.
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.
Stop replicating application secrets
A common solution is to copy application secrets from AWS into GitHub Secrets.
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.
If the application already calls Envilder.load() 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:
# ...workflow trigger, job and runner configuration omitted...
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v9.0.0
- run: uv sync --locked
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ vars.AWS_REGION }}
- run: uv run python -m my_app
configure-aws-credentials exchanges GitHub's OIDC token for short-lived AWS credentials. boto3 finds those credentials through its default chain, and the Python SDK resolves envilder.json inside the application process.
No application secret needs to be copied into GitHub Secrets, repeated in the workflow or written to a .env in this path. The workflow still selects the correct AWS role, region and application command. Those are execution-context decisions, not application secret values.
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.
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.
If a build or deployment tool specifically requires an environment file, the Envilder CLI and GitHub Action remain available as alternative delivery mechanisms.
Version tags are shown here for readability. In production workflows, pin actions to full commit SHAs according to your supply-chain policy.
The important difference is that DATABASE_URL and API_TOKEN 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.
From handoff to checkout
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.
It removes the manual work that often follows approval.
Before:
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
After:
Request access
Authenticate with AWS
Check out the project
Run the application or pipeline
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.
This also answers the rotation question from the opening. A copied .env 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.
It does not refresh an already running process automatically. Live refresh still requires an application-specific strategy.
💡 A supervised refresh mode that re-resolves secrets and restarts an Envilder-managed child process is on the roadmap.
One map, several execution contexts
The same map can travel through different systems while each context keeps its own AWS identity:
| Context | AWS identity | What happens |
|---|---|---|
| Local development | AWS SSO or profile | The SDK resolves secrets when the application starts |
| GitHub Actions CI | IAM role assumed through OIDC | Tests or the application resolve secrets inside their process |
| AWS CodePipeline with CodeBuild | CodeBuild service role | Deployment commands resolve only the secrets they genuinely need |
| ECS or Lambda runtime | ECS task role or Lambda execution role | The application resolves secrets at startup |
The credentials and permissions remain environment-specific. The shared part is the versioned resolution contract between application names and provider paths.
Python, Node.js and .NET SDKs use that resolution contract inside the application process. The Envilder CLI and GitHub Action can generate a .env when a tool explicitly requires one.
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.
Where the trade-off lands
Envilder is not a secrets manager, proxy or new security boundary. The cloud provider remains the backing store and access boundary.
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.
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.
The current Python SDK makes one synchronous get_parameter() 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 get_parameters() calls may be better.
Use boto3 directly when:
- One application owns a small mapping
- You need complete control over SSM requests
- You need custom batching, caching or live-refresh behaviour
These are current boundaries, not permanent ones. Batching for the Python SDK is already tracked, and caching or refresh should evolve from real use cases rather than guesses.
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.
Use Envilder when:
- Mapping changes should travel with branches and pull requests
- Developers should not exchange or rebuild
.envfiles - Local development, CI/CD and multiple runtimes need the same resolution contract
The choice is not really boto3 versus Envilder.
Envilder uses boto3.
The choice is whether to maintain your own convention around it or use Envilder's map-file convention.
Push is optional
The Python SDK only reads secrets.
Envilder also includes an optional CLI for importing an existing .env into the mapped provider paths:
npx envilder \
--push \
--envfile=.env.bootstrap \
--map=envilder.json
The CLI requires Node.js. Push is an explicit, one-way operation for bootstrapping or migration, not continuous synchronization.
Treat .env.bootstrap as sensitive migration input: do not commit it and remove it after the import.
If you do not need it, do not use it.
Try it on one service
Pick a small, non-critical Python service that already uses SSM:
- Install Envilder.
- Commit its
envilder.json. - Add
Envilder.load()before the application's normal startup configuration validation. - Let the versioned map travel through CI, deployment and the running service.
- Ask a teammate who has AWS access but no list of parameter paths or copied
.envto run it.
If Envilder adds more ceremony than it removes, keep the boto3 version.
If it shortens the handoff and removes duplicated configuration across execution contexts, I would like to know what worked and what did not.
Open an issue for confusing behaviour, missing documentation or anything that made setup harder than expected.
Honest feedback from a real project is more useful to me than a generic feature request.
Learn more
- Python SDK documentation
- AWS SSM documentation
- Map-file documentation
- Envilder on PyPI
- Envilder on GitHub
Envilder is open source under the MIT license.
The storage problem was already solved.
Envilder aims to make configuration handoffs explicit wherever code runs: for developers, coding agents, delivery pipelines and production runtimes.

Top comments (0)