Static Site Generation in Lambda with React Static

Danila Loginov
10 min readApr 29, 2022

Overview

With the raising of such tools as Gatsby and Next.js building of static websites powered by modern front end frameworks became a new trend, however, for production usage, they do require a build process and therefore complex continuous deployment infrastructure or servers to do it in runtime eliminating some of the important benefits of static websites: simplicity and costs.

While the overall architecture definitely deserves a separate article to overview the pros and cons of static websites, I’d like to present a way to keep Static Site Generation with simplicity and costs by leveraging a serverless approach: AWS Lambda.

Unfortunately neither Gatsby nor Next.js do not support building the website into a non-project directory which becomes an issue to use them inside the Lambda since it provides a read-only filesystem and gives permissions to write to the temporary directory only.

So I did quick research over the React-powered tools to do the Static Site Generation and found React Static which is not actively maintained anymore, but will be a good example to demonstrate the approach. And this is a completely hands-on article so you can try this at home!

Reference Architecture

AWS Reference Infrastructure Diagram

Let’s think about a common static website: it has assets like HTML, CSS, and JavaScript placed somewhere (S3) and recommended to be served via CDN (CloudFront). Actually, this does not have to be a full website but can be just a rarely changed part of a bigger one.

The most important part here is how we generate these assets and put them into the bucket: by using Lambda!

With the Static Site Generation approach, the build process executed by Node.js goes along the project components, gets needed information from the Data Sources (either external or internal), and outputs a number of HTML pages together with assets like CSS and JavaScript to operate on the client-side after users’ browsers download pages content.

The existence of Data Sources here is key here since if the build process should not rely on any changing data there is no point to use the approach overall: the website can be just built and deployed only when the code changes (new releases).

Another important thing is that Lambda builds and deploys a website, instead of returning pages on demand like a regular server — and this is the main difference from the default approaches with deploying SSG frameworks into the Lambda (which is basically just creating a server).

So as Lambda is used as a build server, not a runtime one, it should be bundled with all the dependencies including the development ones required by the project. And we all know about the node_modules size, right? Docker to the rescue! With the help of Elastic Container Registry, we can prepare a Docker image and run Lambda on-demand created from the image.

If code changes — we push the Docker image, if data changes — we trigger the Lambda. But how do we trigger the Lambda? This is the time for all the power of the serverless approach: on schedule, via the URL, from another service, or on data change in a database or another S3 bucket — whatever you may need!

Implementation

TLDR: https://github.com/loginov-rocks/Build-React-Static-in-Lambda

React Static

For the purpose of the article the basic React Static scaffolding fits well:

npm i -g react-static
react-static create

Lambda Code

Node.js Lambda handler does three things:

  1. Spawn the regular build process and wait until it ends.
  2. Collect paths of all files that should be deployed.
  3. Put these files into the S3 bucket.

Ideally, it should also:

  1. Remove not used files from the S3 bucket.
  2. Trigger CloudFront invalidation.

Lambda code placed in the lambda directory, it is dead simple and can be checked in the GitHub repository, the only part that needs explanation is environment variables usage…

Environment Variables

The bucket name to deploy files to is configured by the LAMBDA_S3_BUCKET_NAME variable.

For the sake of testability, it should be possible to check the code out of the AWS cloud as well, so to achieve this we can configure a user in the AWS allowed to put objects into the S3 bucket, and use its LAMBDA_ACCESS_KEY_ID and corresponding LAMBDA_SECRET_ACCESS_KEY.

When code runs in Lambda it will use the corresponding IAM policy, so this can be controlled by settings the LAMBDA_USE_POLICY to true.

Last but not the least the LAMBDA_USE_TMPDIR variable controls whether the Lambda code expects files to be deployed from the temporary directory or not, this variable is also accessed from the React Static configuration, but this will be covered in a minute.

Lambda Dependencies

As Node.js Lambda code runs from the same package as React Static, its dependencies can be installed along:

npm install --save-dev aws-sdk dotenv mime
  • aws-sdk used to access S3;
  • dotenv helps to load the environment variables from the .env file for local testing;
  • mime provides Content-Type headers for the files put to the S3 bucket to correctly serve files to browsers.

Local Testing

With these things done we can already test the Lambda code and trigger build and deployment locally with a simple command:

node lambda/without-docker

Basically, it just triggers the Lambda code and that’s it!

React Static Configuration

The only change from the code perspective left is to control React Static build process to output files into the temporary directory, and this is easily achieved with the help of paths configuration in static.config.js:

import { tmpdir } from 'os'
// ...
const pathsBase = process.env.LAMBDA_USE_TMPDIR === 'true' ? tmpdir() + '/' : '';
// ...
export default {
// ...
paths: {
buildArtifacts: pathsBase + 'artifacts',
dist: pathsBase + 'dist',
temp: pathsBase + 'tmp',
},
// ...
}

So in case the environment variable LAMBDA_USE_TMPDIR=true React Static will use the temporary directory, or project root directory otherwise.

Unfortunately, this is the exact thing neither Gatsby nor Next.js can do at the moment: https://github.com/gatsbyjs/gatsby/discussions/1878 and https://nextjs.org/docs/api-reference/next.config.js/setting-a-custom-build-directory

Docker

Let’s prepare Dockerfile according to the AWS documentation:

FROM public.ecr.aws/lambda/nodejs:14
COPY . ${LAMBDA_TASK_ROOT}
RUN npm install
CMD ["lambda/index.handler"]

The only change I made is to point the container to the exportedhandler() of the lambda/index.js module.

Also, don’t forget to describe .dockerignore to avoid putting irrelevant files into the Docker image:

artifacts
dist
node_modules
tmp

.env

.git
.gitignore

With that we can build the Docker image:

docker build -t build-react-static-in-lambda .

Run a container locally:

docker run --env-file .env -p 9000:8080 build-react-static-in-lambda

And test it by triggering the Lambda from the console:

curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

This is how it will work in the AWS cloud with the difference that Lambda in the local container will have write access to the file system in contrast to the actual Lambda.

More on that can be found in the AWS documentation: Creating Lambda container images.

AWS

Now, to the AWS part! Despite this section being rich with screenshots and therefore long, it actually takes no more than 15 minutes to complete. A description of every step is below the screenshot.

Step 1: Create Elastic Container Repository and push Docker image

1. Go to ECR and create a private repository.

2. Go to the newly created repository.

3. Click the “View push commands” button.

4. Check the instructions…

5. And follow them one by one until the image is pushed to the repository.

6. Refresh the repository view to verify the image was uploaded.

Step 2: Create and configure Lambda

7. Go to the Lambda dashboard and create a new function from the container image.

8. Select the image just uploaded.

9. And create the function, important to use the x86_64 architecture.

10. Wait until the function is created…

11. And switch to the “Configuration” tab.

12. Such kind of Lambda will require more memory and timeout, let’s raise it to 1024 MB and 15 minutes which is the maximum execution time.

13. Next switch to the “Permissions” section and click on the role assigned to the Lambda.

14. Here we will need to configure a policy, “inline” will work.

15. Policy should provide Lambda access to PutObject into the S3 bucket.

16. Review and name the policy, then click on “Create policy”…

17. And make sure it’s there!

Step 3: Create an S3 bucket

18. Go to the S3 dashboard and create a bucket with default settings.

19. Get the bucket created!

Step 4: Test Lambda

20. Go to the Lambda and switch to the “Environment variables” section under the “Configuration” tab.

21. Set environment variables values. Access key ID and secret access key are not needed since Lambda will use the IAM policy configured, just make sure LAMBDA_USE_POLICY is set to true. Lambda also has no write access to the file system it’s running on, so should use a temporary directory.

22. Then, switch to the “Test” tab.

23. Create any test event, payload is not important, since the Lambda does not access it, and click on the “Test” button.

Test A
Test B
Test C

24. Wait until the test finishes, I did a few to check the duration of subsequent calls.

25. And check out the bucket!

Next, you can configure the S3 bucket to “host the public website”, or as recommended plug CloudFront in to serve static assets via CDN. That’s it!

Optional Step 5: Clean up resources

If you don’t need AWS resources created, make sure you remove:

  1. S3 bucket
  2. Lambda itself
  3. Lambda's role in IAM
  4. ECR repository

Lambda Costs

As this is not the regular task Lambda can carry out it’s important to mention the costs of running such Lambda, and it can be easily calculated: as seen in the tests it takes about 1 minute and 870 MB of memory to build and deploy static resources.

On-demand price for Lambda running in the US East (North Virginia) region with 1 GB of memory per 1 ms is $0.0000000167 in April 2022.

Assuming you are going to use the default 512 MB of ephemeral storage (/tmp directory) which comes at no additional cost, the daily build and deployment of such a project will cost you just about $0.02 per month!

Check the actual AWS Lambda Pricing. And don’t forget these are not the only costs incurred since ECR, S3, and other infrastructure components you’ll leverage to host your static website should also be covered.

Conclusion

So in the end we have a static React-powered website completely built and deployed in Lambda on-demand, so you can configure this Lambda to be triggered by schedule, other Lambdas, or services, directly or via events, including events emitted by other AWS services like S3, DynamoDB and so on.

This approach keeps benefits from the Static Site Generation as well as from the simplicity and almost zero costs. Of course, it is not a very good fit for static websites in which data often changes, but the decision on using it always comes from comparing different approaches given certain business requirements and costs of having servers running 24/7 or updating the website on-demand.

Here is a GitHub template repository you can easily start with your own project: https://github.com/loginov-rocks/Build-React-Static-in-Lambda

That’s all, Folks!

--

--

Danila Loginov

🛠️ Solution Architect ⛽️ Petrolhead 🛰️ IoT hobbyist https://loginov.rocks