Serverless web app in AWS
Your typical Lambda+DynamoDB solution, and 25 best practices for it

Guille Ojeda
March 01, 2023
Welcome to Simple AWS! Today's issue is not late. I'm moving Simple AWS to Wednesdays, because it's hard to find time to write on a weekend. Before we start, there's something I'd like to know about you:
What type of company do you work on? |
Use case: Serverless web app (Lambda + DynamoDB)
Scenario
You're building a web application, and you're not sure whether you'll get 1 or 1000 users on your first week. Traffic is not going to be consistent, because your app depends on trends you can't predict. You think serverless is a good choice (and you're right!), you know the details about Lambda, S3, API Gateway and DynamoDB (probably from reading this newsletter), but you're not sure how everything fits together in this case.
Services
Lambda: Our serverless compute layer. You put your code there, and it automagically runs a new instance for every request, scaling really fast and really well.
S3: Very cheap and durable storage. We'll use it to store our frontend code in this case.
API Gateway: Exposes endpoints to the world, with security and lots of good engineering features. We'll use it to expose our Lambda functions.
DynamoDB: Managed No-SQL database. We'll use it to store our data.
Solution
Design your app: Before you start building, it's essential to have a clear understanding of how your application's different components will fit together. In a serverless architecture, your application can be split into multiple smaller functions, each with its own specific task, which can be executed independently.
So, the first step is to identify the things your app needs to do, group them into functions, and understand how they talk to each other. This process is called (micro)service design, and I could write a whole book on it (maybe some day I will!). But for now, let's leave it at that and focus on the AWS side.
In our example, we'll just have a single Lambda function that talks to DynamoDB.Design your DynamoDB table(s): No-SQL doesn't mean no structure (it doesn't even mean non-relational, or non-ACID-compliant). Understand what data you'll need to store and how you're going to read it. Decide on a structure, a partition key and sort key, and Global and Local Secondary Indexes wherever they're needed. I've never seen a whole book on this topic, but I have seen several 1-hour talks about it (I'll add a few in the Resources section).
In our example, we'll just have a single DynamoDB table with partition key id and no sort key, to keep things simple.Host your static website on S3: The gist of it is that you're hosting your static website's files on an S3 bucket, and using it as a web server. S3 doesn't support HTTPS, so you'll use CloudFront for that and as a CDN. I won't bore you with the details, there's a tutorial for that.
In our example, the details don't matter much, but it'll look just like in the tutorial: S3 bucket with the files, domain in Route 53, CloudFront used to expose the site with an SSL certificate from ACM.Create your DynamoDB table: You designed the data part. Now create the table, and configure the details such as capacity.
In our example, we'll leave it as On Demand. It's basically the full serverless mode, it costs like 7x more per request but scales instantaneously. We're picking on demand because we don't know our traffic patterns and because it's simpler, we can optimize later.
Create your Lambda function: Give it a name, give it an IAM Role with permissions to access your DynamoDB table, put the table name in an environment variable, and put your code in.
Create your API Gateway API: First, create an HTTP API. Then create the routes. Then create an integration with your Lambda function. Finally, attach that integration to your routes.
In our example, you should create the routes GET /items/{id}, GET /items, PUT /items and DELETE /items/{id}.
Discussion
We're building a standard serverless app, so there's not much to discuss other than serverless vs serverful. Here are the key points on that:
Operations: In serverless you don't manage the servers, so there's way less ops work.
Scalability: It's pretty easy to make serverful scale with an Auto Scaling Group. But there's always that delay in scaling, even when using ECS or EKS on Fargate. In serverless there's also a delay (called cold start), but it's significantly smaller.
Cost: It's more expensive per request, period. The final bill can come out cheaper because you have 0 unused capacity (unused capacity is what you waste when your EC2 instance is using 5% of the CPU and you're paying for 100%). Unused capacity tends to decrease a lot as apps grow, because we understand traffic patterns better and because traffic variations are not as proportionally big (it's easier to go from 10.000 to 11.000 than from 0 to 1.000, even though the increase is 1.000 in both cases).
Development speed: There's still servers, but AWS takes care of them for us. That removes a lot of work on our side, which means we can develop faster.
Developer experience: The infrastructure work that remains is typically pushed to developers. Serverless developers usually like this. Non-serverless developers either hate it and only want to write application code, or want to become serverless developers, there's no middle ground. Keep this in mind when hiring.
Optimization: There's usually a lot to optimize, based on the fine implementation details. I try to give readers the tools to do this, but you should consider hiring a consultant for a couple of hours a week.
Best Practices
I'll try to keep the best practices focused on this solution. But for the specifics about each service, you can check past issues on Lambda, DynamoDB and API Gateway.
Operational Excellence
Something something Infrastructure as Code: You knew it was coming. I'll never stop saying it until the AWS web Console is deprecated (ok, that might be a bit extreme). For serverless solutions you can check out AWS SAM, which builds on top of CloudFormation, or the Serverless Framework, a great declarative option. Or use your regular favorite tool.
Use asynchronous processing: To improve scalability and reduce costs, consider using asynchronous processing for things that don't need to happen in real-time. For example, you can use Lambda functions triggered by S3 events to process uploaded files in the background, or you can use SQS to queue up messages for later processing by Lambda functions.
Monitor the app: Use CloudWatch to monitor your app's metrics, logs, and alarms. Set up X-Ray to trace requests through your application and identify bottlenecks and errors.
Automate deployment and testing: Basically, use a CI/CD pipeline. Since there's going to be multiple functions, you'll want a pipeline for each. Use the same CI/CD practices as for microservices, even if your Lambdas are not microservices.
Security
Set up Authentication: Use API Gateway to set up authentication, so your API endpoints are not public. You can use Cognito, or your own custom authorizer.
Use WAF: Set up Web Application Firewall in your API Gateway APIs.
Encrypt data at rest: Both S3 and DynamoDB encrypt data by default, so you're probably good with this. AWS manages the encryption key though, you can change the configuration to use a key you manage.
Encrypt data in transit: DynamoDB already encrypts data in transit. For your static website in S3, set up CloudFront with an SSL certificate.
Implement least privilege access: Give your Lambda functions an IAM Role that lets them access the resources they need, such as DynamoDB. Only give them the permissions they actually need, for example give the role read permissions on table1, instead of giving it * permissions on all DynamoDB.
Control access to your AWS account: There's a lot to say here, but I've already said it in a previous issue.
Reliability
Implement retries and circuit breakers: There's a lot that can (and will) go wrong, such as network errors or a service throttling. To recover from these failures, implementing retries in your code (use randomized exponential backoff). To prevent these failures from cascading, implement circuit breakers.
Use Lambda versions: You can create versions of your Lambda functions, and point API Gateway routes to a specific version. That way, you can deploy and test a new version without disrupting your prod environment, and make the switch once you're confident it works. You can also roll back.
Use canary releases: Versions also allow you to have API Gateway send a small part of the traffic to the new version, so you can test it with real data without impacting all of your users (it's better if it fails for 1% of the users than for 100%).
Back up DynamoDB: Stuff happens, and data can get lost. To protect from that, use DynamoDB backups. If it does happen, use point-in-time recovery to restore the data.
Performance Efficiency
Use caching: To reduce latency and improve performance, consider adding caches. For example, you can use CloudFront to cache your static website, and even for API Gateway responses. DynamoDB usually has a really fast response time, but if it's not fast enough for you, you can use DAX.
Optimize database access: Like I said above, there's a lot to talk about here. Use indexes whenever needed, always use query instead of scan, and don't fetch all attributes if you don't need them. Check our past issue on DynamoDB to understand how these things work.
Rightsize your Lambdas: You get to pick the amount of memory a Lambda function has, and the CPU power (and the price!) is tied to that. Too small, and your Lambda runs slow. Too big, and it gets super expensive. You can use a profiling tool on your code running on local to determine how much memory it needs, but often it's simpler to just try different values until you find the sweet spot. Do this semi-regularly though, since these requirements can change as your code evolves.
Minimize response payload: You can improve response times by sending less data in the response. If you have multiple use cases for the data, and some need a lot of it and some just a summary, it's a good idea to create a new endpoint for the summary, even if you could solve it by querying the other endpoint and summarizing the data in the front end.
Optimize cold starts: Lambda functions actually have a few instances running, and launch more when needed. The time for a new Lambda instance to launch is called cold start. It's the code that sits outside the handler function (plus some things AWS needs to do). Anything you put there will be run only once per instance, and the values are cached for all future invocations that use that instance. So, put all initialization code there, but try to optimize it so your new Lambda instances don't take too long to start.
Cost Optimization
Rightsize your Lambdas (again): Finding the right size can save you a significant amount of money, on top of saving you a significant amount of headaches from Lambdas not performing well.
Monitor usage: Use CloudWatch to monitor and analyze usage patterns. Use Cost Explorer to monitor costs and figure out where your optimization efforts can have the most impact.
Use DynamoDB Provisioned mode: It's much cheaper per operation! You just need to figure out how much capacity you actually need, and deal with throttling due to insufficient capacity (while you're waiting for it to scale). More often than not, you should start with On Demand because it's easier, then consider moving to Provisioned because once you get some traffic it makes a significant difference. Not all workloads are suited for Provisioned mode, but most are.
Go serverful: Wait, what? We were talking about serverless! Yeah, but not every workload is well suited for serverless. I intentionally proposed a scenario that is, but that might not be your case. Here's the trick though: you don't have to go all in on serverless or on serverful. You can split your workload, and use whatever makes more sense for each part. This will mean increased operational efforts though, because you're effectively maintaining two different architectures. And if they mingle, there are scenarios to consider, such as a Lambda function scaling to 1000 concurrent invocations in 10 seconds, which all try to hit a service in a poor EC2 instance. Just keep it in mind, you're not married to serverless just because you've been using it for a few years.
Use Savings Plans: Commit to a certain usage, do a zero, partial or total upfront payment, and enjoy great discounts on your compute resources. Yes, it does work for Lambda.
Use Provisioned Concurrency: This is like serverful serverless. You pay to keep a certain number of Lambda instances always running, but you pay a lower price. Furthermore, it's great for ensuring a minimum capacity.
Resources
Still working on my Security Specialty cert, using this course. There's courses for other certs as well. <-- Affiliate links.
Here's the DynamoDB talks that I mentioned:
For design patterns for DynamoDB: DynamoDB deep dive: Advanced design patterns (from re:Invent 2021).
For a discussion about data models: Deploy modern and effective data models with Amazon DynamoDB (from re:Invent 2022)
For a serverless, event-driven application: The basics here: Get started building your first serverless, event-driven application (from re:Invent 2022); and the Serverlespresso Workshop to get practical.
Help Wild Rydes build a unicorn ride sharing platform using serverless architectures, or build and deploy a completely serverless web application with The Innovator Island (they're awesome workshops).
I've been reading this newsletter about interesting things happening around the world. <-- Affiliate links.
Some of the above resources are paid promotions or contain affiliate links. I only recommend resources I've tried for myself and found actually useful, regardless of whether I get paid for it or not.
Misc.
We've dealt with different aspects of serverless in previous issues. On the serverless compute side, we talked about AWS Lambda, how to do observability with X-Ray, and exposing endpoints with API Gateway. We also had issues about DynamoDB and SNS, and we even discussed a data processing pipeline and event-driven architectures with either just Lambda and S3 and with Step functions. But we had never actually done the web side of serverless in its entirety. So, here it is. I hope you liked it!
Like I said at the start, Simple AWS will now run on Wednesdays. We're 3 months in, this is issue #18, and I'm still on the continuous improvement journey. March will bring a few new things to the newsletter, so I can make it better for you all, but I'll take your feedback on it. Stay tuned.
Did you like this issue? |
Thank you for reading! See ya on the next issue.