AWS Secrets Manager

Need to load secrets in your Node.js app without exposing them? Here’s how.

If you’re still storing API keys or database credentials in .env files or hardcoding them into your codebase, it’s time for a better approach. Secrets should stay secret especially in production.

The Problem

Managing Sensitive Values

Managing sensitive values across environments can get messy fast. Hardcoded secrets are risky, and .env files aren’t ideal when you’re working with teams or deploying to the cloud. It’s easy to lose control over where that information ends up.

The Solution

AWS Secrets Manager Overview

AWS Secrets Manager gives you a safe, centralized place to store secrets. You can fetch them at runtime using the AWS SDK, so you don’t need to keep secrets on disk or in code. This snippet shows how to pull them securely in a Node.js app using SDK v3.

TL;DR

  • Use AWS Secrets Manager to securely access secrets in Node.js.
  • Avoid hardcoding secrets or using local .env files in production.
  • This snippet helps fetch secrets using the AWS SDK v3.

Code Snippet (TypeScript):

fetchSecret.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
ResourceNotFoundException,
SecretsManagerServiceException,
} from '@aws-sdk/client-secrets-manager';
import {config} from 'dotenv';
config();
interface CachedSecret {
value: string;
expiry: number;
}
const secretCache = new Map<string, CachedSecret>();
const DEFAULT_CACHE_TTL = 5 * 60 * 1000;
const defaultRegion = process.env.AWS_REGION;
/**
* Custom error for when a secret is not found in AWS Secrets Manager.
* This is thrown when the secret does not exist or cannot be accessed.
*/
class SecretNotFoundError extends Error {
constructor(secretName: string) {
super(`Secret "${secretName}" not found in AWS Secrets Manager.`);
this.name = 'SecretNotFoundError';
}
}
/**
* Custom error for when a secret's value is invalid.
* This can happen if the secret is binary, empty, or not a string.
*/
class InvalidSecretValueError extends Error {
constructor(secretName: string) {
super(
`Secret "${secretName}" is binary or empty, or does not contain a string value.`,
);
this.name = 'InvalidSecretValueError';
}
}
let secretsManagerClient: SecretsManagerClient | null = null;
/**
* Initializes and returns a singleton SecretsManagerClient.
* This prevents recreating the client on every `fetchSecret` call.
* @param region - The AWS region to use for the client.
* @returns An initialized SecretsManagerClient instance.
*/
function getSecretsManagerClient(region: string): SecretsManagerClient {
if (!region) {
throw new Error(
'AWS_REGION is not defined. Please set it in your .env file or pass it as an argument.',
);
}
if (!secretsManagerClient) {
secretsManagerClient = new SecretsManagerClient({region});
}
return secretsManagerClient;
}
/**
* Fetches a secret's string value from AWS Secrets Manager with optional caching.
* @param secretName - The name or ARN of the secret.
* @param options - Optional configuration for fetching the secret.
* @param options.region - Overrides the default AWS region for this fetch operation.
* @param options.cacheTTL - Time-to-live for the cached secret in milliseconds. Set to 0 to disable caching for this call.
* @returns A promise that resolves to the secret string.
* @throws {SecretNotFoundError} If the secret does not exist.
* @throws {InvalidSecretValueError} If the secret's value is binary or empty.
* @throws {Error} For other AWS SDK or network-related errors.
*/
export async function fetchSecret(
secretName: string,
options?: {region?: string; cacheTTL?: number},
): Promise<string> {
const region = options?.region || defaultRegion;
const cacheTTL =
options?.cacheTTL !== undefined ? options.cacheTTL : DEFAULT_CACHE_TTL;
if (!region) {
throw new Error(
"AWS_REGION is not defined. Ensure it's in your .env file or passed in options.",
);
}
if (cacheTTL > 0) {
const cached = secretCache.get(secretName);
if (cached && Date.now() < cached.expiry) {
return cached.value;
}
}
const client = getSecretsManagerClient(region);
try {
const command = new GetSecretValueCommand({SecretId: secretName});
const response = await client.send(command);
if (response.SecretString) {
const secretValue = response.SecretString;
if (cacheTTL > 0) {
secretCache.set(secretName, {
value: secretValue,
expiry: Date.now() + cacheTTL,
});
}
return secretValue;
} else if (response.SecretBinary) {
throw new InvalidSecretValueError(secretName);
} else {
throw new InvalidSecretValueError(secretName);
}
} catch (error: any) {
console.error(`[ERROR] Failed to fetch secret "${secretName}":`, error);
if (error instanceof ResourceNotFoundException) {
throw new SecretNotFoundError(secretName);
} else if (error instanceof SecretsManagerServiceException) {
throw new Error(
`AWS Secrets Manager error for "${secretName}": ${error.message} (Code: ${error.name})`,
);
} else {
throw new Error(
`An unexpected error occurred while fetching secret "${secretName}": ${error.message}`,
);
}
} finally {
// Add any cleanup or finalization logic here.
// For example, if you had an active connection or resource to close.
// In this specific code, there isn't an obvious resource to clean up
// within the fetchSecret function itself, as the client is a singleton
// and caching is handled by a Map.
// However, for demonstration, you could log something:
console.log(`[INFO] Finished attempting to fetch secret "${secretName}".`);
}
}

Why This Matters

This setup helps keep your apps more secure and your secrets off disk. It fits perfectly into cloud-native workflows, especially when you’re using IAM roles or CI/CD pipelines. It’s simple, flexible, and secure.

Over to you

How do you handle secrets in your projects? Tried this approach before?

Related Posts

Check out some of our other posts

Check S3 Bucket Existence

'Cloud' 'Automation' isCodeSnippet: true draft: falseQuick Tip Don’t let your deployment blow up because of a missing S3 bucket. This Bash script lets you check if a bucket exists

List S3 Buckets

Overview Multi-Profile S3 Management Multi-Profile S3 Safari! Ever juggled multiple AWS accounts and needed a quick S3 bucket inventory across all of them? This Python script is your guid

Essential Bash Variables for Every Script

Overview Quick Tip You know what's worse than writing scripts? Writing scripts that break every time you move them to a different machine. Let's fix that with some built-in Bash variables tha

Why printf Beats echo in Linux Scripts

Scripting Tip You know that feeling when a script works perfectly on your machine but fails miserably somewhere else? That's probably because you're using echo for output. Let me show you why pri

Optimizing your python code with __slots__?

Memory Optimization with slots Understanding the Problem Dev Tip: Optimizing Data Models in Big Data Workflows with slots In big data and MLOps workflows, you often work with