Serverless Server Side Rendering with Angular on AWS Lambda@Edge
Tags: angular, serverless, aws, javascript
Published: 2020-12-14 12:47:59

In this article we will look at how we can enable server side rendering on a Angular application and make it run serverless on 'AWS Lambda@Edge'. 
How do we go from running a  non server side rendered static Angular application on AWS S3 to enabling SSR and deploying it to Lambda@Edge, S3 whilst utilising CloudFront in front of it?

### Lambda@Edge to the rescue

I was recently interested in seeing how to server side render an Angular app with no server. As using Lambda@Edge.

Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute functions that customize the content that CloudFront delivers ([more info](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html)).

Lambd@Edge can be executed in 4 ways:
- Viewer Request
- Origin Request (we will be using this for SSR 🤓)
- Origin Response
- Viewer Response

In this example, I am using: 
- Angular 11
- Express js for SSR
- AWS S3 for storing the application build
- AWS Cloudfront as the CDN
- and of course the famous Lambda@Edge

This post already assumes the following:
- having an aws account
- having [aws cli configure](https://aws.amazon.com/cli/)
- having [serverless framework installed](https://www.serverless.com/framework/docs/getting-started/)
- Already familiar with Angular SSR

Here is the [Github repo](https://github.com/eelayoubi/angular-lambda-ssr)
And the application is deployed [here](d2htk1pm9r9gbg.cloudfront.net)

## Introducing the sample application 
The application is pretty simple, as we have 2 modules:
- SearchModule
- AnimalModule (lazy loaded)

When you navigate to the application, you are presented with an input field, where you can type a name (ex: Oliver, leo ...), or an animal (ex: dog, cat). You will be presented with a list of the findings. You can click on an anima to go see the details in the animal component.

As simple as that. Just to demonstrate the SSR on Lambda@Edge.

You can clone the [repo](https://github.com/eelayoubi/angular-lambda-ssr) to check it out

## Enabling SSR on the application
Okay ... Off to the SSR part. The first thing to do is to run the following command:

`ng add @nguniversal/express-engine`

Which will generate couple of files ([more on this here](https://angular.io/guide/universal#universal-tutorial)).

To run the default ssr application, just type:

`yarn build:ssr && yarn serve:ssr` and navigate to http://localhost:4000

You will notice that angular generated a file called 'server.ts'. This is the express web server. If you are familiar with lambda, you would know that there are no servers. As you don't think about it as a server ... You just give a code, and Lambda runs it ...

To keep the Angular SSR generated files intact, I made a copy of the following files:
- server.ts -> serverless.ts
- tsconfig.server.json -> tsconfig.serverless.json

In the serverless.ts I got rid of the 'listen' part (no server ... no listener 🤷🏻‍♂️).

The server.ts file uses ngExpressEngine to bootstrap the application. However, I replaced that in serverless.ts with 'renderModule' that comes from '@angular/platform-server' (more flexibility ...)

{% gist https://gist.github.com/eelayoubi/0da507405b12ad96c26e313ad5be9a79 %}

In tsconfig.serverless.json, on line 12, instead of including server.ts in the 'files' property, we are including our own serverless.ts.

{% gist https://gist.github.com/eelayoubi/1642fc7537a38b7aeefad8505d424b58 %}

In the angular.json file I added the following part:
```
"serverless": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/angular-lambda-ssr/serverless",
            "main": "serverless.ts",
            "tsConfig": "tsconfig.serverless.json"
          },
          "configurations": {
            "production": {
              "outputHashing": "media",
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "sourceMap": false,
              "optimization": true
            }
          }
        }
```
Then in the package.json I added the following property:

`"build:sls": "ng build --prod && ng run angular-lambda-ssr:serverless:production"`

As you can see in the 'options' property we are pointing to our customized main and tsconfig. So when running the `yarn build:sls`, these config will be used to generate the `dist/angular-lambda-ssr/serverless`

## Creating the Lambda function to execute SSR
I added a new file called 'lambda.js. This is the file that contains the Lambda function, that will be executed on every request from CloudFront To the Origin ([Origin Request](https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html))

{% gist https://gist.github.com/eelayoubi/3ca96547bb9a04b2ad535984f64ad5f6 %}

I'm using the [serverless-http](https://github.com/eelayoubi/serverless-http/) package that is a fork of the original [repo](https://github.com/dougmoscrop/serverless-http). The main repo maps Api Gateway requests, I added the Lambda@Edge support that can be viewed in this [PR](https://github.com/dougmoscrop/serverless-http/pull/192)

- Anyway, as you can see on line 8, we are passing the app (which is express app) to the serverless function, and it returns a function that accepts the Incoming event and a context. 

- On line 18 some magic will happen, basically mapping the request and passing it to the app instance which will return the response (the ssr response).

- Then on line 19 we are just minifying the body, since there is a 1MB limit regarding the Lambda@Edge [origin-request](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-see-limits).

- Finally on line 27 we are returning the response to the user. 

Keep in mind that we are only doing SSR to requests to the index.html or to any request that doesn't have an extension.

If the request contains an extension, it means you are requesting a file... so we pass the request to S3 to serve it.

### Deploying to AWS
You will notice in the repo 2 files: 
- serverless-distribution.yml
- serverless.yml

We will first deploy the serverless-distribution.yml:
{% gist https://gist.github.com/eelayoubi/ecb031fae56ec15c45b5ecd7218059b9 %}

This will deploy the following resources:
- Cloudfront Identity (used by S3 and Cloudfront to ensure that objects in 3 are only accessible via Cloudfront)
- Cloudfront Distribution
- S3 bucket that will store the application build
- A Bucket Policy that Allows the CloudFront Identity to Get the S3 objects.

To deploy this stack, on line 58 change the bucket name to something unique for you, since S3 names are global ... Then just run the following command: 

`serverless deploy --config serverless-distribution.yml`

This may take a few minutes. When the deployment is done, we need to get the cloudfront endpoint. You can do that by going to the console or by running:
`aws cloudformation describe-stacks --stack-name angular-lambda-ssr-distribution-dev`
The endpoint will have the following format:
**d1234244112324.cloudfront.net**

Now we need to add the cloudfront endpoint to the search.service.ts:
{% gist https://gist.github.com/eelayoubi/d443d9e9b542054b3bf4b082c80624c3 %}

On line 15, replace "/assets/data/animals.json" with "https://cloudfrontendpointhere/assets/data/animals.json"

Now that we have that done, we need to build the app with our serverless.ts (in case already done, we need to build it again since we changed the endpoint to fetch the data), so run: 

`yarn build:sls`

That will generate the dist folder that contains the angular app that we need to sync to S3 (since S3 will serve the static content, as the js, css ..)

After the dist is generated, go to the browser folder in the dist:

`cd dist/angular-lambda-ssr/browser`

Then run the following command to copy the files to S3:

`aws s3 sync . s3://replacewithyourbucketname`

Be sure to replace the placeholder with your S3 bucket Name.

Once this is done, we need to deploy the lambda function, which is in serverless.yml, simply run:

`serverless deploy`

{% gist  https://gist.github.com/eelayoubi/447c7d680ad29c3c28e9ca03587a539e %}

This will deploy the following resources:
- The Lambda Function
- The Lambda execution role

Once the stack is created, we need to deploy Lambda@Edge to the Cloudfront behaviour we just created, so copy and paste this link in a browser tab (make sure you are logged in to aws console)
https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions/angular-lambda-ssr-dev-ssr-origin-req/versions/$LATEST?tab=configuration

⚠️ Make sure the $LATEST version is selected

1- Click on 'Actions'
2- Click on 'Deploy to lambda@Edge'
3- Choose the distribution we created
3- Choose the Default behaviour (there is only one for the our distribution)
4- For Cloudfront Event, choose 'Origin Request'
5- Leave the include Body unticked
6- Tick the Acknowledge box
7- Click Deploy

It will take a couple of minutes to deploy this function to all the cloudfront edge locations.

### Testing

You can navigate to the cloudfront endpoint again, and access the application, you should see that the SSR is working as expected.

You can see that the animal/3 request was served from express server

![Screenshot 2020-12-14 at 9.47.07 AM](https://dev-to-uploads.s3.amazonaws.com/i/6tj24vgvx6eh730ckrw9.png)

And the main js is served from S3 (it is cached on Cloudfront this time)

![Screenshot 2020-12-14 at 9.52.51 AM](https://dev-to-uploads.s3.amazonaws.com/i/j4puo800gvebuqwukvi9.png) 
 

### Cleanup

To return the AWS account to its previous state, it would be a good idea to delete our created resources.

Note that in term of spending, this will not be expensive, if you have an AWS Free Tier, you won't be charged, unless you go above the limits ([lambda pricing](https://aws.amazon.com/lambda/pricing/), [cloudfront pricing](https://aws.amazon.com/cloudfront/pricing/)) 

First we need to empty the S3 bucket, since if we delete the Cloudformation stack with a non empty bucket, the stack will fail.
So run the following command:

`aws s3 rm s3://replacewithyourbucketname --recursive`

Now we are ready to delete the serverless-distribution stack, run the following command:

`serverless remove --config serverless-distribution.yml`

We must wait for a while to be able to delete the serverless.yml stack, if you try to delete it now you will run into an error, as the lambda function is deployed on Cloudfront.

After a while, run the following:

`serverless remove`


### Some Gotchas

* We could have combined the two stacks (serverless-distribution & serverless) in one file. However, deleting the stack will fail, as it will delete all resource except the lambda function, since as explained we need to wait until the Replicas are deleted, which might take some time ([more info](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html)) 

* We could have more complicated logic in the Lambda function to render specific pages, for specific browsers ... I tried to keep it simple in this example

* Be aware that Lambda@Edge origin-request has some limits:
Size of a response that is generated by a Lambda function, including headers and body : 1MB
Function timeout: 30 seconds
[more info](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-see-limits)

* We can test the Lambda function locally, thanks to serverless framework, we can invoke our lambda. To do so, run the following command:
`serverless invoke local --function ssr-origin-req --path event.json`
You will see the result returned contains the app ssr rendered.
The event.json file contains an origin-request cloudfront request, in other words, the event the Lambda function expects in the parameter. [more info](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request)

## Conclusion

In this post we saw how we can leverage Lambda@Edge to server side render our angular application.

- We have a simple angular app
- We enabled SSR with some customisation
- We Created the Lambda Function that will be executed on every request to Origin (to S3 in our case)
- We deployed the serverless-distribution stack
- we deployed the Lambda stack and associated the Lambda to the Cloudfront Behaviour
- We tested that everything is working as expected

I hope you found this article beneficial. Thank you for reading ... 🤓