Skip to content

Environment Variables & Secrets

Environment variables allow you to configure your Astro application with different values for different environments (development, staging, production) without changing your code. When using Astro AWS with SSR enabled, you can configure environment variables that are available to your Lambda function and accessible in your Astro pages, API routes, and middleware.

The simplest way to configure environment variables is through the lambdaFunction.environment property in your CDK stack configuration.

lib/astro-site-stack.ts
import { Stack } from "aws-cdk-lib"
import type { StackProps } from "aws-cdk-lib"
import { Construct } from "constructs"
import { AstroAWS } from "@astro-aws/constructs"
export class AstroSiteStack extends Stack {
public constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props)
new AstroAWS(this, "AstroAWS", {
websiteDir: "../my-astro-project",
cdk: {
lambdaFunction: {
environment: {
API_URL: "https://api.example.com",
NODE_ENV: "production",
APP_VERSION: "1.0.0",
},
},
},
})
}
}

Once configured, you can access these environment variables in your Astro code using import.meta.env. Environment variables are available in:

  • Pages (.astro files)
  • API Routes (src/pages/api/*)
  • Middleware (src/middleware.ts)
  • Server Components
src/pages/index.astro
---
const apiUrl = import.meta.env.API_URL
const nodeEnv = import.meta.env.NODE_ENV
// Use the environment variables
const response = await fetch(`${apiUrl}/data`)
const data = await response.json()
---
<html>
<head>
<title>My App</title>
</head>
<body>
<h1>Environment: {nodeEnv}</h1>
<!-- Use your data here -->
</body>
</html>

For better TypeScript support, you can declare your environment variables in src/env.d.ts:

src/env.d.ts
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly API_URL: string
readonly NODE_ENV: string
readonly APP_VERSION: string
readonly DOMAIN?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

This provides autocomplete and type checking when accessing environment variables:

---
// TypeScript will now validate these environment variables
const apiUrl = import.meta.env.API_URL // ✅ Type-safe
const invalid = import.meta.env.INVALID_VAR // ❌ TypeScript error
---

You can configure different environment variables for different deployment environments (development, staging, production) by conditionally setting values based on your stack context.

lib/astro-site-stack.ts
import { Stack } from "aws-cdk-lib"
import type { StackProps } from "aws-cdk-lib"
import { Construct } from "constructs"
import { AstroAWS } from "@astro-aws/constructs"
interface AstroSiteStackProps extends StackProps {
environment: "dev" | "staging" | "prod"
}
export class AstroSiteStack extends Stack {
public constructor(scope: Construct, id: string, props: AstroSiteStackProps) {
super(scope, id, props)
// Define environment-specific values
const envConfig = {
dev: {
API_URL: "https://api-dev.example.com",
NODE_ENV: "development",
},
staging: {
API_URL: "https://api-staging.example.com",
NODE_ENV: "staging",
},
prod: {
API_URL: "https://api.example.com",
NODE_ENV: "production",
},
}
const config = envConfig[props.environment]
new AstroAWS(this, "AstroAWS", {
websiteDir: "../my-astro-project",
cdk: {
lambdaFunction: {
environment: {
...config,
APP_VERSION: "1.0.0",
},
},
},
})
}
}

You can also use CDK context values to determine the environment:

export class AstroSiteStack extends Stack {
public constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props)
const environment = this.node.tryGetContext("environment") ?? "dev"
const envConfig: Record<string, Record<string, string>> = {
dev: {
API_URL: "https://api-dev.example.com",
NODE_ENV: "development",
},
staging: {
API_URL: "https://api-staging.example.com",
NODE_ENV: "staging",
},
prod: {
API_URL: "https://api.example.com",
NODE_ENV: "production",
},
}
new AstroAWS(this, "AstroAWS", {
websiteDir: "../my-astro-project",
cdk: {
lambdaFunction: {
environment: envConfig[environment],
},
},
})
}
}

Then deploy with:

Terminal window
cdk deploy --context environment=prod

For sensitive values like API keys, database passwords, or other secrets, use AWS Secrets Manager instead of plain environment variables. This provides better security, automatic rotation support, and audit logging.

First, create a secret in AWS Secrets Manager (via console, CLI, or CDK):

Terminal window
aws secretsmanager create-secret \
--name my-app/api-keys \
--secret-string '{"API_KEY":"your-secret-key","DATABASE_PASSWORD":"your-password"}'

Your Lambda function needs permission to read the secret. Configure this in your CDK stack:

lib/astro-site-stack.ts
import { Stack } from "aws-cdk-lib"
import type { StackProps } from "aws-cdk-lib"
import { Construct } from "constructs"
import { AstroAWS } from "@astro-aws/constructs"
import { Secret } from "aws-cdk-lib/aws-secretsmanager"
export class AstroSiteStack extends Stack {
public constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props)
// Reference the existing secret
const apiSecret = Secret.fromSecretNameV2(
this,
"ApiSecret",
"my-app/api-keys",
)
const astroAWS = new AstroAWS(this, "AstroAWS", {
websiteDir: "../my-astro-project",
cdk: {
lambdaFunction: {
environment: {
SECRET_ARN: apiSecret.secretArn,
},
},
},
})
// Grant the Lambda function permission to read the secret
if (astroAWS.cdk.lambdaFunction) {
apiSecret.grantRead(astroAWS.cdk.lambdaFunction)
}
}
}

In your Astro code, you’ll need to fetch the secret from AWS Secrets Manager at runtime:

src/utils/secrets.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager"
const secretsClient = new SecretsManagerClient({})
let cachedSecret: Record<string, string> | null = null
export async function getSecret(key: string): Promise<string> {
if (!cachedSecret) {
const secretArn = import.meta.env.SECRET_ARN
if (!secretArn) {
throw new Error("SECRET_ARN environment variable is not set")
}
const command = new GetSecretValueCommand({ SecretId: secretArn })
const response = await secretsClient.send(command)
if (!response.SecretString) {
throw new Error("Secret value is not a string")
}
cachedSecret = JSON.parse(response.SecretString)
}
if (!cachedSecret || !(key in cachedSecret)) {
throw new Error(`Secret key "${key}" not found`)
}
return cachedSecret[key]
}

Then use it in your pages or API routes:

src/pages/api/data.astro
---
import { getSecret } from "../../utils/secrets"
const apiKey = await getSecret("API_KEY")
// Use the API key to make authenticated requests
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
const data = await response.json()
---
<ResponseInit>
{ status: 200 }
</ResponseInit>
{JSON.stringify(data)}

Note: Secrets Manager calls add latency to your requests. Consider caching secret values in memory (as shown above) or using a Lambda layer with cached secrets for better performance.

Important: Environment variables are not supported in Lambda@Edge mode due to AWS limitations.

When you use mode: "edge" in your Astro AWS adapter configuration, environment variables configured in your CDK stack will be automatically removed (removeInEdge: true). This is handled automatically by the construct.

Why Edge Mode Doesn’t Support Environment Variables

Section titled “Why Edge Mode Doesn’t Support Environment Variables”

Lambda@Edge functions have strict limitations:

  • No environment variables: Lambda@Edge doesn’t support environment variables
  • Limited execution time: 5 seconds for viewer request/response, 30 seconds for origin request/response
  • Smaller package size: Deployment packages must be smaller
  • No VPC access: Limited access to AWS services
  • No file system: Except for /tmp directory

If you need configuration values in edge mode, consider:

  1. Hardcode values: For non-sensitive configuration (not recommended for secrets)
  2. Use CloudFront headers: Pass configuration via CloudFront custom headers
  3. Use query parameters: Pass configuration via URL parameters (not recommended for sensitive data)
  4. Switch to SSR mode: Use mode: "ssr" or mode: "ssr-stream" if you need environment variables
astro.config.ts
import { defineConfig } from "astro/config"
import astroAws from "@astro-aws/adapter"
export default defineConfig({
output: "server",
adapter: astroAws({
mode: "ssr", // Use SSR mode instead of "edge" to support environment variables
}),
})

Never commit sensitive values like API keys, passwords, or tokens to your repository. Always use AWS Secrets Manager or environment variables set at deployment time.

// ❌ Bad: Hardcoded secrets
environment: {
API_KEY: "sk_live_1234567890", // Never do this!
}
// ✅ Good: Use Secrets Manager or environment variables
environment: {
SECRET_ARN: apiSecret.secretArn,
}

2. Use Different Values for Different Environments

Section titled “2. Use Different Values for Different Environments”

Always use different API endpoints, keys, and configuration for development, staging, and production environments.

Validate that required environment variables are present:

src/pages/index.astro
---
const apiUrl = import.meta.env.API_URL
if (!apiUrl) {
throw new Error("API_URL environment variable is required")
}
---

When using AWS Secrets Manager, cache secret values in memory to avoid repeated API calls. However, be aware that cached values won’t update if the secret is rotated.

Define your environment variables in src/env.d.ts for type safety and better developer experience.

Document which environment variables are required in your project’s README or documentation:

## Required Environment Variables
- `API_URL`: The base URL for the API
- `NODE_ENV`: The environment (development, staging, production)

Use clear, descriptive names for your environment variables:

// ❌ Bad: Unclear names
environment: {
URL: "https://api.example.com",
KEY: "secret-key",
}
// ✅ Good: Descriptive names
environment: {
API_BASE_URL: "https://api.example.com",
STRIPE_API_KEY: "sk_live_...",
}

Here’s a complete example showing environment variables configuration with different environments and Secrets Manager integration:

lib/astro-site-stack.ts
import { Stack } from "aws-cdk-lib"
import type { StackProps } from "aws-cdk-lib"
import { Construct } from "constructs"
import { AstroAWS } from "@astro-aws/constructs"
import { Secret } from "aws-cdk-lib/aws-secretsmanager"
import { Runtime, Architecture } from "aws-cdk-lib/aws-lambda"
interface AstroSiteStackProps extends StackProps {
environment: "dev" | "staging" | "prod"
}
export class AstroSiteStack extends Stack {
public constructor(scope: Construct, id: string, props: AstroSiteStackProps) {
super(scope, id, props)
// Environment-specific configuration
const envConfig = {
dev: {
API_URL: "https://api-dev.example.com",
NODE_ENV: "development",
},
staging: {
API_URL: "https://api-staging.example.com",
NODE_ENV: "staging",
},
prod: {
API_URL: "https://api.example.com",
NODE_ENV: "production",
},
}
// Reference secret from Secrets Manager
const apiSecret = Secret.fromSecretNameV2(
this,
"ApiSecret",
`my-app/api-keys-${props.environment}`,
)
const astroAWS = new AstroAWS(this, "AstroAWS", {
websiteDir: "../my-astro-project",
cdk: {
lambdaFunction: {
architecture: Architecture.ARM_64,
runtime: Runtime.NODEJS_24_X,
environment: {
...envConfig[props.environment],
SECRET_ARN: apiSecret.secretArn,
APP_VERSION: "1.0.0",
},
},
},
})
// Grant Lambda permission to read the secret
if (astroAWS.cdk.lambdaFunction) {
apiSecret.grantRead(astroAWS.cdk.lambdaFunction)
}
}
}

And in your Astro code:

src/pages/api/data.astro
---
import { getSecret } from "../../utils/secrets"
const apiUrl = import.meta.env.API_URL
const apiKey = await getSecret("API_KEY")
const response = await fetch(`${apiUrl}/data`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
const data = await response.json()
---
<ResponseInit>
{ status: 200 }
</ResponseInit>
{JSON.stringify(data)}