Infrastructure as Code (IaC) has become a critical practice for managing and provisioning cloud resources efficiently. For full stack applications, which involve both frontend and backend components, managing the underlying infrastructure can be complex. Enter SST (Serverless Stack Toolkit)—a powerful framework that simplifies the process by allowing you to define and manage your infrastructure entirely in code.
In this blog post, we’ll explore how to use SST for IaC in full stack applications, walking through the setup, development, and deployment processes. By the end, you’ll have a solid understanding of how SST can streamline your workflow and enhance your application’s scalability and maintainability.
Table of Contents
- What is SST?
- Why Use SST for Full Stack Applications?
- Getting Started with SST
- Defining Infrastructure with SST
- Building a Full Stack Application
- Deploying Your Application
- Best Practices
- Conclusion
What is SST?
Key Features:
- TypeScript-Based IaC: Use TypeScript to define AWS resources, benefiting from type safety and IntelliSense.
- Live Lambda Development: Test and debug Lambda functions locally with real-time feedback.
- High-Level Constructs: Simplify resource definitions with SST’s higher-level abstractions.
- Multi-Environment Support: Manage different deployment stages seamlessly.
Why Use SST for Full Stack Applications?
Benefits of Using SST:
- Unified Codebase: Manage both frontend and backend infrastructure in a single codebase.
- Simplified Resource Management: High-level constructs reduce boilerplate and complexity.
- Faster Development Cycle: Live Lambda development accelerates testing and debugging.
- Scalability: Easily scale your application by adjusting code, not configurations.
- Consistency Across Environments: Ensure that development, staging, and production environments are consistent.
Getting Started with SST
Prerequisites
- Node.js: Install the latest LTS version from the official website.
- AWS Account: Create an AWS account with appropriate permissions.
- AWS CLI: Install and configure the AWS Command Line Interface.
Installation
npm install --location=global sst
Initializing a New Project
mkdir my-fullstack-app
cd my-fullstack-app
npx sst init --template=typescript-starter
Defining Infrastructure with SST
Project Structure
stacks/
: Contains your infrastructure code.packages/
: Contains your application code (Lambda functions, frontend app).sst.config.ts
: The main configuration file for SST.
Defining a Backend API
stacks/MyStack.ts
import * as sst from "@serverless-stack/resources";
export default class MyStack extends sst.Stack {
constructor(scope: sst.App, id: string, props?: sst.StackProps) {
super(scope, id, props);
// Create a HTTP API
const api = new sst.Api(this, "Api", {
routes: {
"GET /": "packages/functions/src/lambda.handler",
},
});
// Show the API endpoint in the output
this.addOutputs({
ApiEndpoint: api.url,
});
}
}
Creating a Lambda Function
packages/functions/src/lambda.ts
export async function handler(event: any) {
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello from Lambda!" }),
};
}
Setting Up a Database
stacks/MyStack.ts
// ...
// Create a DynamoDB table
const table = new sst.Table(this, "Table", {
fields: {
pk: sst.TableFieldType.STRING,
},
primaryIndex: { partitionKey: "pk" },
});
// Grant the Lambda function read/write permissions to the table
api.attachPermissions([table]);
Adding Authentication
stacks/MyStack.ts
// ...
// Create a Cognito User Pool
const auth = new sst.Auth(this, "Auth", {
cognito: {
userPool: {
signInAliases: { email: true },
},
},
});
// Allow authenticated users to invoke the API
auth.attachPermissionsForAuthUsers([api]);
// Export the auth resources
this.addOutputs({
UserPoolId: auth.cognitoUserPool?.userPoolId || "",
UserPoolClientId: auth.cognitoUserPoolClient?.userPoolClientId || "",
});
Hosting a Frontend Application
packages/frontend
, you can deploy it using SST’s StaticSite
construct.stacks/MyStack.ts
// ...
// Deploy the React app
const site = new sst.StaticSite(this, "ReactSite", {
path: "packages/frontend",
buildCommand: "npm run build",
buildOutput: "build",
});
// Output the URL of the deployed site
this.addOutputs({
SiteUrl: site.url,
});
Building a Full Stack Application
Setting Up the Backend
- Create API routes: Define RESTful endpoints for CRUD operations.
- Implement Lambda functions: Write functions to handle requests and interact with DynamoDB.
stacks/MyStack.ts
// Define API routes
const api = new sst.Api(this, "Api", {
routes: {
"GET /notes": "packages/functions/src/listNotes.main",
"POST /notes": "packages/functions/src/createNote.main",
"GET /notes/{id}": "packages/functions/src/getNote.main",
"PUT /notes/{id}": "packages/functions/src/updateNote.main",
"DELETE /notes/{id}": "packages/functions/src/deleteNote.main",
},
});
// Attach permissions
api.attachPermissions([table]);
Example Lambda Function
packages/functions/src/createNote.ts
import { DynamoDB } from "aws-sdk";
const dynamoDb = new DynamoDB.DocumentClient();
export async function main(event: any) {
const data = JSON.parse(event.body);
const params = {
TableName: process.env.TABLE_NAME!,
Item: {
pk: data.userId,
noteId: data.noteId,
content: data.content,
createdAt: Date.now(),
},
};
try {
await dynamoDb.put(params).promise();
return {
statusCode: 200,
body: JSON.stringify(params.Item),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: "Could not create note" }),
};
}
}
Setting Up the Frontend
- Implement UI Components: Build forms and lists to interact with your API.
- Configure Authentication: Integrate Cognito for user sign-up and sign-in.
- Connect to API: Use the API endpoint and authentication tokens to make secure requests.
// src/config.js
export default {
apiGateway: {
REGION: "your-region",
URL: "your-api-endpoint",
},
cognito: {
REGION: "your-region",
USER_POOL_ID: "your-user-pool-id",
APP_CLIENT_ID: "your-app-client-id",
},
};
Updating the StaticSite
Construct
stacks/MyStack.ts
const site = new sst.StaticSite(this, "ReactSite", {
path: "packages/frontend",
buildCommand: "npm run build",
buildOutput: "build",
environment: {
REACT_APP_API_URL: api.url,
REACT_APP_REGION: scope.region,
REACT_APP_USER_POOL_ID: auth.cognitoUserPool?.userPoolId || "",
REACT_APP_USER_POOL_CLIENT_ID:
auth.cognitoUserPoolClient?.userPoolClientId || "",
},
});
Deploying Your Application
Local Development
sst dev
- Deploys your stack to a local AWS environment.
- Watches for code changes and updates resources in real-time.
- Provides a local endpoint for testing.
Production Deployment
sst deploy --stage prod
prod
with the desired stage name.Best Practices
Organize Your Code
- Modularize: Split your stacks and functions into logical units.
- Reuse Components: Create reusable constructs for common patterns.
Use Environment Variables
- Configuration Management: Use environment variables to manage configurations across different environments.
Implement Testing
- Unit Tests: Write tests for your Lambda functions.
- Integration Tests: Test interactions between services.
Leverage SST Features
- Permissions Management: Use
attachPermissions
to manage IAM permissions effectively. - Monitoring: Utilize SST’s integration with AWS CloudWatch for logging and monitoring.
Conclusion
Key Takeaways:
- Simplified Infrastructure Management: High-level constructs make defining AWS resources straightforward.
- Accelerated Development: Live Lambda development reduces the feedback loop.
- Scalability and Maintainability: Code-first approach ensures your application can grow and adapt over time.
- Consistent Environments: Manage multiple stages with ease, ensuring consistency across deployments.