Introduction
If you need to access some secrets from AWS Secrets Manager, it's a good idea to cache the values. This way you will fetch them less frequently and save on costs. Unfortunately, it's not built-in in aws-sdk
for NodeJS. Fortunately, it's pretty straightforward to implement.
In this post we will take a look at why it's a good idea and one way to do it when using AWS Lambda functions. The provided implementation is in TypeScript.
Why you should cache the values
Every external call is a risk and there are many things that can go wrong. The network is not reliable. I once hit the rate quota for fetching values and the service was just waiting for an answer, eventually timing out.
Retrieving a cached value is faster and you can save money with just a few lines of code. Not only do you save on calls to AWS Secrets Manager but you will also have shorter duration.
Strategy
The first time an AWS Lambda function is run it creates an execution environment if there isn't one already. When the execution is done that environment will remain available for some time for subsequent executions.
We can use this as a simple caching mechanism by creating an object in the environment. When we put values in that object, they can be accessed the next time the function is invoked.
Implementation
Let's break it down into two components. First a component for caching:
class SimpleCache {
private cache: Record<string, string> = {};
constructor(private readonly loadValue: (key: string) => Promise<string | undefined>) {}
async get(key: string) {
// if we find the value in the cache, return immediately
if (this.cache[key]) {
return this.cache[key];
}
// load it with the provided function
const res = await this.loadValue(key);
if (res == null) {
return res;
}
// put the value in the cache and return.
// The next time we need the value, we don't have to fetch it again.
this.cache[key] = res;
return res;
}
}
Then a component for fetching the value of a secret with a given key:
import SecretsManager from 'aws-sdk/clients/secretsmanager';
const secretsClient = new SecretsManager();
const client = new SimpleCache((key) =>
secretsClient
.getSecretValue({ SecretId: key })
.promise()
.then((x) => x.SecretString),
);
Putting it all together:
import SecretsManager from 'aws-sdk/clients/secretsmanager';
class SimpleCache {
private cache: Record<string, string> = {};
constructor(private readonly loadValue: (key: string) => Promise<string | undefined>) {}
async get(key: string) {
if (this.cache[key]) {
return this.cache[key];
}
const res = await this.loadValue(key);
if (res == null) {
return res;
}
this.cache[key] = res;
return res;
}
}
// When we create these two instances outside of the handler
// function, they are only created the first time a new
// execution environment is created. This allows us to use it as a cache.
const secretsClient = new SecretsManager();
const client = new SimpleCache((key) =>
secretsClient
.getSecretValue({ SecretId: key })
.promise()
.then((x) => x.SecretString),
);
export const handler = async () => {
// the client instance will be reused across execution environments
const secretValue = await client.get('MySecret');
return {
statusCode: 200,
body: JSON.stringify({
message: secretValue,
}),
};
};
Additional usage
We can use the SimpleCache
implementation above for other integrations. If we fetch some other static values over the network, we can use the same caching mechanism.
For example, if we decide to use aws-sdk v3
instead we can use the same SimpleCache
but change the component related to SecretsManager. V3 has a nicer syntax where we don't have to mess around with .promise()
.
Don't put secrets in environment variables
You can resolve the values from AWS Secrets Manager during deployment and put them in the environment variables.
Unfortunately this means your secrets are available in plain text for attackers. It's one of the first place they would look. It has happened before and will probably happen again. Here's an example of an attack like that..
Conclusion
In this post we have covered why you should cache values from AWS Secrets Manager. It will save you money and make your code more reliable and performant. There's also an implementation of how to achieve this.
If you found this post helpful, please consider following me on here as well as on Twitter.
Thanks for reading!
Top comments (2)
Awesome!
Do you have a version to expire the cache every X minutes/hours? Otherwise that would eternally lookup for the same keys, even if they got changed.
So far I have not needed that. Because the lambda function resets after some time, you get this behaviour for free.
Another way to expire the cache is to deploy a new version of the function. Chances are, if you are updating a secret, updating the function is not too much extra work.
If you still need it, I would mark a timestamp in the constructor of SimpleCache. Then in the get method, if there is a cached value, check if too much time has elapsed.