- Published on
Export Serverless Framework Environment Variables to a Local .env File
- Authors

- Name
- Duncan Leung
- @leungd
When you declare a DynamoDB table in serverless.yml, the actual table name does not exist until CloudFormation deploys the stack. The deployed name is something like my-service-dev-RestaurantsTable-1Y097GF7QLUIX - a unique value per deployment that you cannot know ahead of time.
Your deployed Lambda gets that value through the environment: block in serverless.yml. Your local code doesn't - it's running outside the deployed environment with no visibility into what CloudFormation generated.
This post walks through bridging that gap. The deployed Lambda already has the value at runtime via process.env.restaurants_table; we want the same thing locally so the same code can talk to the same deployed table without hardcoding ARNs.
The Problem: Runtime-Generated Resource Names
Consider a DynamoDB table declared in serverless.yml:
resources:
Resources:
RestaurantsTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: name
AttributeType: S
KeySchema:
- AttributeName: name
KeyType: HASH
When CloudFormation deploys this stack, it generates a unique table name. You can't refer to RestaurantsTable by a static string in your Lambda code - the real name only exists after the deploy.
You also can't hardcode the resolved name, because:
- It's different in every stage (
dev,staging,prod) - It changes if you ever rename or recreate the resource
- New developers cloning the repo wouldn't have it
The same applies to API Gateway URLs, SQS queue ARNs, SNS topic ARNs, and any other CloudFormation-generated resource.
Step 1: Declare Environment Variables in serverless.yml
In the environment: block of a function, you can reference CloudFormation resources using the standard intrinsic functions !Ref and !GetAtt. (For the mental model on when to use which, see CloudFormation !Ref vs !GetAtt in serverless.yml.)
functions:
get-restaurants:
handler: functions/get-restaurants.handler
events:
- http:
path: /restaurants
method: get
environment:
restaurants_table: !Ref RestaurantsTable
At deploy time, CloudFormation resolves !Ref RestaurantsTable to the generated table name and bakes it into the Lambda's runtime environment. The Lambda code reads it through process.env.restaurants_table.
For more complex composed strings - for example, building the deployed API Gateway URL out of multiple CloudFormation pseudo-parameters - the serverless-pseudo-parameters plugin lets you use a #{...} substitution syntax inline:
functions:
get-index:
handler: functions/get-index.handler
events:
- http:
path: /
method: get
environment:
restaurants_api: https://#{ApiGatewayRestApi}.execute-api.#{AWS::Region}.amazonaws.com/${self:provider.stage}/restaurants
plugins:
- serverless-pseudo-parameters
$ yarn add -D serverless-pseudo-parameters
#{ApiGatewayRestApi} resolves to the deployed REST API's id; #{AWS::Region} to the deploy region. Without this plugin you'd have to use verbose Fn::Sub or Fn::Join blocks to compose the same string.
That covers the deployed Lambda. Now the local case.
Step 2: Install serverless-export-env
The serverless-export-env plugin reads the deployed stack's CloudFormation outputs, resolves all the references in your environment: block to their real values, and writes them to a local .env file.
$ yarn add -D serverless-export-env
Add it to your plugins:
plugins:
- serverless-pseudo-parameters
- serverless-export-env
Step 3: Export the .env File
Run the plugin's command:
$ serverless export-env
It generates a .env file in the project root containing the resolved values:
restaurants_table=my-service-dev-RestaurantsTable-1Y097GF7QLUIX
restaurants_api=https://abc123def4.execute-api.us-east-1.amazonaws.com/dev/restaurants
Each entry is a Lambda environment variable name from serverless.yml, paired with the value that CloudFormation actually resolved at deploy time. The !Ref and #{...} references that were unresolved in the YAML are now real strings.
Important caveat: the stack must be deployed first. The plugin reads CloudFormation outputs from the live stack - it cannot resolve references against an undeployed template. Re-run serverless export-env after every deploy that changes resource references.
Step 4: Load the .env in Your Local Code
First, add .env to .gitignore. The file contains real production-account ARNs and resource names - those should never be committed:
# .gitignore
.env
.env.*
Then install dotenv and load it at the top of your local entry point:
$ yarn add -D dotenv
require('dotenv').config()
// Now process.env has the same values your deployed Lambda has
const RESTAURANTS_TABLE = process.env.restaurants_table
const RESTAURANTS_API = process.env.restaurants_api
Your deployed Lambda needs no dotenv call - AWS injects the variables directly into process.env. The require('dotenv').config() line is a local-only concern, and a no-op in the Lambda environment because no .env file is bundled into the deployment artifact (assuming .env is in .gitignore and your bundler isn't packaging it).
The Full serverless.yml
Putting it all together:
service: restaurants-api
provider:
name: aws
runtime: nodejs12.x
iamRoleStatements:
- Effect: Allow
Action: dynamodb:Scan
Resource: !GetAtt RestaurantsTable.Arn
functions:
get-index:
handler: functions/get-index.handler
events:
- http:
path: /
method: get
environment:
restaurants_api: https://#{ApiGatewayRestApi}.execute-api.#{AWS::Region}.amazonaws.com/${self:provider.stage}/restaurants
get-restaurants:
handler: functions/get-restaurants.handler
events:
- http:
path: /restaurants
method: get
environment:
restaurants_table: !Ref RestaurantsTable
resources:
Resources:
RestaurantsTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: name
AttributeType: S
KeySchema:
- AttributeName: name
KeyType: HASH
Outputs:
RestaurantsTableName:
Value: !Ref RestaurantsTable
plugins:
- serverless-pseudo-parameters
- serverless-export-env
Common Gotchas
- Stack must be deployed first.
serverless-export-envreads CloudFormation outputs from the live stack. If the stack isn't deployed, references can't be resolved. - Re-export after each deploy that changes resources. Renaming or recreating a CloudFormation resource generates a new physical name. The old
.envwill silently point at a resource that no longer exists. - Never commit
.env. The values are real ARNs and resource identifiers. Treat them as production credentials even if the resources themselves are dev-only. - The deployed Lambda does not need
dotenv. It already has the variables inprocess.envfrom theenvironment:block. Thedotenvcall only does work locally. - IAM permissions on the deployer. The plugin needs
cloudformation:DescribeStacksto read outputs. Most deploy users have this through the broadercloudformation:*permission - if you've followed a least-privilege scoping (see A Least-Privilege IAM Strategy for Serverless Framework Deployments), make sure it's included.
Note: Stage-Specific Exports
For multi-stage projects, pass --stage to target a specific deployed stack and --filename to write to a stage-specific file:
$ serverless export-env --stage dev --filename .env.development
$ serverless export-env --stage staging --filename .env.staging
$ serverless export-env --stage prod --filename .env.production
Pair this with environment-aware loading on the local side:
require('dotenv').config({
path: `.env.${process.env.NODE_ENV || 'development'}`,
})
Add .env.* to .gitignore (the pattern above already covers this) so no stage's resolved values end up in version control.
Takeaways
- CloudFormation-generated resource names (DynamoDB tables, API Gateway URLs, etc.) only exist after deploy. You can't hardcode them locally.
- Declare them in
serverless.ymlunderprovider.environmentor per-functionenvironment:using!Refand!GetAtt. The deployed Lambda gets them automatically. serverless-export-envresolves those references against the deployed CloudFormation outputs and writes them to a local.envfile. Your local code reads them viadotenv.serverless-pseudo-parameterslets you compose CloudFormation values into arbitrary strings (like API Gateway URLs) using#{...}syntax - useful when!Refalone isn't enough.- The stack must be deployed before
serverless export-envworks, and you should re-export after every deploy that changes resource references. .envmust be in.gitignore- the resolved values are real ARNs.- For multi-stage projects, use
--stageand--filenameto keep per-environment.envfiles separate.