Environment Variables & Secrets
Most Lambda functions need configuration: a database hostname, a feature flag, an API endpoint, or a password. AWS Lambda gives you a built-in way to pass this configuration without editing your code, called environment variables. But there is a sharp line between safe config (like a region name) and real secrets (like a database password), and crossing it is one of the most common security mistakes new cloud engineers make. This page shows you how to use environment variables correctly, encrypt them, and fetch true secrets from AWS Secrets Manager (a service that stores and rotates passwords) or AWS Systems Manager Parameter Store (a service that stores configuration values).
What environment variables are
An environment variable is a key-value pair (for example LOG_LEVEL=debug) that AWS injects into your function’s runtime environment before your code runs. Your code reads them through the normal language API: process.env.LOG_LEVEL in Node.js, os.environ["LOG_LEVEL"] in Python, or System.getenv("LOG_LEVEL") in Java.
They are useful because they let you change behavior between environments (dev, staging, production) without rebuilding or redeploying the same code. You set different values per function or per alias instead.
When to use them: non-secret configuration that changes per environment, such as LOG_LEVEL, TABLE_NAME, STAGE, or a public API URL.
When NOT to use them: raw passwords, API keys, database connection strings with embedded passwords, or private keys. We explain why below.
Setting environment variables
Console steps
- Open the Lambda console and choose your function.
- Select the Configuration tab, then Environment variables in the left list.
- Choose Edit, then Add environment variable.
- Enter a Key (for example
LOG_LEVEL) and a Value (for exampleinfo). - Choose Save.
CLI equivalent
aws lambda update-function-configuration \
--function-name order-processor \
--environment "Variables={LOG_LEVEL=info,TABLE_NAME=orders-prod}"
Output:
{
"FunctionName": "order-processor",
"FunctionArn": "arn:aws:lambda:us-east-1:111122223333:function:order-processor",
"Runtime": "nodejs22.x",
"Environment": {
"Variables": {
"LOG_LEVEL": "info",
"TABLE_NAME": "orders-prod"
}
},
"LastUpdateStatus": "InProgress"
}
Warning:
update-function-configurationreplaces the entireVariablesmap. If you only pass one key, the others are deleted. Always send the full set.
Encrypting environment variables with KMS
Lambda always encrypts environment variables at rest. By default it uses an AWS-managed KMS key (KMS stands for Key Management Service, the AWS service for encryption keys). You can instead supply your own customer-managed KMS key, which lets you control who can decrypt the values and gives you an audit trail in CloudTrail of every decryption.
aws lambda update-function-configuration \
--function-name order-processor \
--kms-key-arn arn:aws:kms:us-east-1:111122223333:key/0a1b2c3d-4e5f-6789-abcd-ef0123456789 \
--environment "Variables={LOG_LEVEL=info}"
This protects data at rest. It does not hide the values from people who can read the function configuration, which is the key problem we tackle next.
The gotcha: environment variables are not secret
Anyone with lambda:GetFunctionConfiguration permission can read your environment variables in plain text from the console or CLI. KMS encryption only protects the bytes on disk; the console decrypts and displays them for authorized callers. They also appear in CloudFormation templates, in CI/CD logs, and in GetFunction API responses.
Security pitfall: Never put a raw password, API key, or private key in an environment variable. Treat env vars as visible to everyone who can view the function. Store the actual secret in Secrets Manager or Parameter Store, and put only the secret’s name in the environment variable.
Fetching secrets at runtime
The correct pattern is: store the secret in a dedicated secrets service, give your function an execution role that allows reading just that secret, put the secret’s name or ARN in an environment variable, and fetch the real value when your code starts.
Secrets Manager vs Parameter Store
| Feature | Secrets Manager | Parameter Store (SecureString) | Parameter Store (String) |
|---|---|---|---|
| Built for secrets | Yes | Yes | No (plain config) |
| Automatic rotation | Yes, built in | No | No |
| Encryption | Always (KMS) | Yes (KMS) | No |
| Cost | ~$0.40 per secret/month + API calls | Free (Standard tier) | Free |
| Best for | Passwords, DB credentials, anything rotated | Cheaper secrets, no rotation needed | Non-secret config |
When to use which: use Secrets Manager when you need automatic rotation (for example RDS database passwords). Use Parameter Store SecureString when you want encrypted secrets without rotation and want to avoid the per-secret monthly fee. Use a plain String parameter for non-secret config you want centralized.
Store the secret
aws secretsmanager create-secret \
--name prod/order-db \
--secret-string '{"username":"app","password":"S3cr3t-Pa55!"}'
Output:
{
"ARN": "arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/order-db-AbCdEf",
"Name": "prod/order-db",
"VersionId": "f1e2d3c4-b5a6-7890-1234-567890abcdef"
}
Grant the execution role access
Attach a policy to the function’s execution role allowing only this secret:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/order-db-*"
}
]
}
Fetch and cache at init
Fetch the secret once when the container starts (outside the handler), not on every invocation. Lambda reuses warm containers, so code outside the handler runs once and the cached value is reused across many invocations.
import { SecretsManagerClient, GetSecretValueCommand }
from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({});
let cachedSecret; // lives across warm invocations
async function getSecret() {
if (cachedSecret) return cachedSecret;
const res = await client.send(
new GetSecretValueCommand({ SecretId: process.env.DB_SECRET_NAME })
);
cachedSecret = JSON.parse(res.SecretString);
return cachedSecret;
}
export const handler = async (event) => {
const { username, password } = await getSecret();
// open DB connection using username/password
return { statusCode: 200, body: "ok" };
};
Here DB_SECRET_NAME is an environment variable holding only the value prod/order-db, which is safe to expose.
Why caching matters: latency and cost
If you call GetSecretValue on every invocation, you add a network round trip (tens of milliseconds) to every request and you pay per API call. Secrets Manager bills about $0.05 per 10,000 API calls; at high traffic that adds real cost and latency. Caching the value in a module-level variable removes almost all of those calls.
The Parameters and Secrets extension
AWS provides a Lambda extension (a Lambda layer that runs a helper process alongside your function) that caches secrets and parameters automatically and exposes them over a local HTTP endpoint. You add the layer, then call http://localhost:2773 instead of the AWS SDK. The extension handles caching and a configurable time-to-live, so you do not manage the cache yourself.
Add the layer:
aws lambda update-function-configuration \
--function-name order-processor \
--layers arn:aws:lambda:us-east-1:177933569100:layer:AWSSecretsAndParameters-extension:21
Then fetch over the local endpoint:
const url = "http://localhost:2773/secretsmanager/get?secretId=prod/order-db";
const res = await fetch(url, {
headers: { "X-Aws-Parameters-Secrets-Token": process.env.AWS_SESSION_TOKEN }
});
const secret = JSON.parse((await res.json()).SecretString);
The first call fetches from Secrets Manager; subsequent calls within the cache window return instantly with no API charge.
Best practices
- Put only non-secret config in environment variables; store real secrets in Secrets Manager or Parameter Store and reference them by name.
- Use a customer-managed KMS key for environment variables when you need decryption auditing and tight access control.
- Fetch secrets in init code (outside the handler) and cache them so warm invocations reuse the value.
- Use the Parameters and Secrets extension to get automatic caching without writing your own cache logic.
- Scope the execution role to the exact secret ARN, never
Resource: "*". - Prefer Secrets Manager when you need automatic rotation; choose Parameter Store SecureString to avoid the per-secret monthly fee.
- Never log secret values, and never bake them into deployment templates or CI/CD pipelines.