Resource Binding
Access the resources in your app in a secure and typesafe way.
Overview
Resource Binding allows you to connect your functions with your infrastructure. This is done in two steps:
- Bind a resource to the functions in your infrastructure code through the
bind
prop. - Use the
sst/node
package to access the resource in your function.
Quick start
To see how Resource Binding works, we are going to create an S3 bucket and bind it to a Lambda function.
To follow along, you can create a new SST app by running npx create-sst@latest
. Alternatively, you can refer to this example repo that's based on the same template.
To create a new bucket, open up
stacks/MyStack.ts
and add aBucket
construct below the API.stacks/MyStack.tsconst bucket = new Bucket(stack, "myFiles");
You'll also need to import
Bucket
at the top.import { Bucket } from "sst/constructs";
Then, bind the
bucket
to theapi
.stacks/MyStack.tsapi.bind([bucket]);
Now we can access the bucket's name in our API using the
Bucket
helper. Changepackages/functions/src/lambda.ts
to:packages/functions/src/lambda.tsimport { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { Bucket } from "sst/node/bucket";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
// Upload a file to the bucket
await s3.send(
new PutObjectCommand({
Bucket: Bucket.myFiles.bucketName,
Key: "greeting.txt",
Body: "Hello world!",
})
);
return {
statusCode: 200,
headers: { "Content-Type": "text/plain" },
body: `File uploaded`,
};
};And install the AWS SDK for this example.
npm install --save @aws-sdk/client-s3
That's it!
Features
Let's take a look at some of the key features of Resource Binding, and how it makes building apps fun and easy again.
Typesafety
In the above example, the Bucket
object that's imported from sst/node/bucket
is typesafe. Your editor should be able to autocomplete the bucket name myFiles
, as well as its property bucketName
.
Behind the scenes
Let's take a look at how this is all wired up.
First, the
sst/node/table
package predefines an interface.export interface BucketResources {}
When SST builds the app, it generates a type file and adds the bucket name to the
BucketResources
interface.node_modules/@types/serverless-stack__node/Bucket-myFiles.d.tsimport "sst/node/bucket";
declare module "sst/node/bucket" {
export interface BucketResources {
myFiles: {
bucketName: string;
};
}
}This type file then gets appended to
index.d.ts
.node_modules/@types/@serverless-stack__node/index.d.tsexport * from "./Bucket-myFiles";
So when the
Bucket
object is imported fromsst/node/bucket
, it has the typeBucketResources
.
Error handling
If you reference a resource that doesn't exist in your SST app, or hasn't been bound to the function, you'll get a runtime error.
For example, if you forget to bind the bucket
to the API, you'll get the following error when the function is invoked.
Cannot use Bucket.myFiles. Please make sure it is bound to this function.
Testing
When testing your code, you can use the sst bind
CLI to bind the resources to your tests.
sst bind "vitest run"
This allows the sst/node
helper library to work as if it was running inside a Lambda function.
Read more about testing and learn about the sst bind
CLI.
Permissions
When a resource is bound to a Lambda function, the permissions to access that resource are automatically granted to the function.
api.bind([bucket]);
Here, by binding the bucket
to the api
, the API routes are able to perform file download, upload, delete, and other actions against the bucket.
Behind the scenes
An IAM policy is added to the Lambda function's role, allowing it to perform s3:*
actions on the S3 bucket's ARN.
The IAM policy statement looks like:
{
"Action": "s3:*",
"Resource": ["arn:aws:s3:::{BUCKET_NAME}", "arn:aws:s3:::{BUCKET_NAME}/*"],
"Effect": "Allow",
}
Construct support
Resource Binding works across all SST constructs. Here are a few more examples.
Getting the Next.js URL
import { NextjsSite } from "sst/node/site";
NextjsSite.myFrontend.url;DynamoDB table name
import { Table } from "sst/node/table";
Table.myTable.tableName;RDS cluster data
import { RDS } from "sst/node/rds";
RDS.myDB.clusterArn;
RDS.myDB.secretArn;
RDS.myDB.defaultDatabaseName;
See the full list of helpers.
Binding other resources
So far we've seen how Resource Binding allows your functions to access values from other SST constructs. But there are 2 other types of values you might want to access in your functions.
- Secrets, because you can't define the value of the secrets in your functions.
- Values from non-SST constructs, for example static values or values from CDK constructs.
For these you can use Config
. Here are a couple of examples.
Binding secrets
To bind a secret to our function, start by creating a Config.Secret
construct.
const STRIPE_KEY = new Config.Secret(stack, "STRIPE_KEY");
And continuing with our example, bind it to the api
.
api.bind([STRIPE_KEY]);
Now set the secret value using the SST CLI.
npx sst secrets set STRIPE_KEY sk_test_abc123
And access the value in the function.
import { Config } from "sst/node/config";
Config.STRIPE_KEY;
You can read more about secrets.
Binding CDK resources
Assuming you have an ECS cluster in your app and you need to pass the cluster name to your function.
Since SST doesn't have a construct for ECS, create a Config.Parameter
construct with the cluster name being the value.
const cluster = new ecs.Cluster(stack, "myCluster");
const MY_CLUSTER_NAME = new Config.Parameter(stack, "MY_CLUSTER_NAME", {
value: cluster.clusterName,
});
Then bind it to the api
from our example.
api.bind([MY_CLUSTER_NAME]);
And you can access the value in your function.
import { Config } from "sst/node/config";
Config.MY_CLUSTER_NAME;
How it works
When a resource is bound to a Lambda function, the resource values are stored as environment variables for the function. In our example, the bucket name is stored as a Lambda environment variable named SST_Bucket_bucketName_myBucket
.
At runtime, the sst/node/bucket
package reads the value process.env.SST_Bucket_bucketName_MyBucket
and makes it accessible via Bucket.myBucket.bucketName
.
SST also stores a copy of the bucket name in AWS SSM. In this case, an SSM parameter of the type String
is created with the name /sst/{appName}/{stageName}/Bucket/MyBucket/bucketName
, where {appName}
is the name of your SST app, and {stageName}
is the stage. The parameter value is the name of the bucket stored in plain text.
Storing the bucket name in SSM might seem redundant. But it provides a convenient way to fetch all the bound resources in your application. This can be extremely useful for testing. This isn't possible when using Lambda environment variables and we are going to see why.
Binding sensitive values
When binding resources that contain sensitive values, placeholders are stored in the Lambda environment variables. The actual values are stored inside SSM. At runtime, the values are fetched from SSM when the Lambda container first boots up. And the values are cached for subsequent invocations. This is similar to how Config.Secret
works.
Cost
Resource Binding values are stored in AWS SSM with the Standard Parameter type and Standard Throughput. This makes AWS SSM free to use in your SST apps. However when storing a Config.Secret
the value is encrypted by AWS KMS. These are retrieved at runtime in your Lambda functions when it starts up. AWS KMS has a free tier of 20,000 API calls per month. And it costs $0.03 for every 10,000 subsequent API calls. This is worth keeping in mind as these secrets are fetched per Lambda function cold start.
FAQ
Here are some frequently asked questions about Resource Binding.
Resource Binding or Lambda environment variables?
Prior to Resource Binding, people used Lambda environment variables to pass information to their functions.
Aside from the lack of typesafety and error handling, Lambda environment variables have a few drawbacks. Imagine you have a Lambda function that looks like this.
export const handler = async () => {
if (process.env.TOPIC_NAME !== "UserUpdated") {
return;
}
// ...
};
Where TOPIC_NAME
is stored as a Lambda environment variable. You'll need to handle the following:
When testing this function, locally or in your CI, you need to figure out the value for
TOPIC_NAME
and set it as an environment variable.In addition, imagine you have another function that also has a
TOPIC_NAME
Lambda environment variable, but with a different value.packages/functions/src/charged.tsexport const handler = async () => {
if (process.env.TOPIC_NAME !== "InvoiceCharged") {
return;
}
// ...
};What should the
TOPIC_NAME
be in your tests?
With Resource Binding, the value for the topic name is also stored in SSM. When running tests, SST can automatically fetch this from SSM using the sst bind
CLI.
Does this make my Lambda functions slower?
No. The resource values are stored as environment variables for the function. At runtime, reading from environment variables is instantaneous.
For sensitive values, the values are stored in AWS SSM. When the Lambda container first boots up, the values are fetched from SSM and are cached for subsequent invocations.
What if I'm not using Node.js runtime?
For non-Node.js runtimes, you can continue to use Lambda environment variables.
If you want to use Resource Binding, you would need to read the bound values from the Lambda environment variable and AWS SSM directly. Refer to the sst/node
package to see how it is done in Node.js.