DEV Community

Cover image for Serverless Server Side Rendering with Angular on AWS Lambda@Edge
eelayoubi
eelayoubi

Posted on

Serverless Server Side Rendering with Angular on AWS Lambda@Edge

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).

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:

Here is the Github repo
And the application is deployed here

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 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).

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 ...)

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

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
            }
          }
        }
Enter fullscreen mode Exit fullscreen mode

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)

I'm using the serverless-http package that is a fork of the original repo. The main repo maps Api Gateway requests, I added the Lambda@Edge support that can be viewed in this PR

  • 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.

  • 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:

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:

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

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

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

Screenshot 2020-12-14 at 9.52.51 AM

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, 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)

  • 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

  • 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

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 ... 🤓

Discussion (11)

Collapse
eugenesergio profile image
Gio • Edited on

Great article!

I am able to execute the deployment in AWS for the serverless-distribution.yml, but not able to set it up correctly. Two things I noticed: S3 didn't upload any dist files and CloudFront didn't have an Origin. What could be wrong?

For the serverless-distribution.yml, what are the other lines that I need to change for the AWS config? A sample file would really help. Thanks a lot!

Collapse
achraflaakissi profile image
LAAKISSI Achraf • Edited on

Thanks for your article,

everything works good but the root route (/) is not rendered ssr, it s just render the file index.html. I check for your app I found the same thing.
dev-to-uploads.s3.amazonaws.com/up...
Thanks.

Collapse
eelayoubi profile image
eelayoubi Author

If you search for something, or directly access the animal route such as 'd2htk1pm9r9gbg.cloudfront.net/anim...' you will see that it is server side rendered.

Collapse
seanvm profile image
Sean • Edited on

Love this post, and nice work on getting this all working with Lambda@Edge. Only issue is I'm also seeing this same problem as the above person in my app with SSR on the root route. Any idea how to get SSR working for the root route? I'd rather not have to redirect to another page like you're doing.

SSR is working on all routes except for the root route. When running on localhost using the default server.ts file it works fine...

Thread Thread
achraflaakissi profile image
LAAKISSI Achraf

I solved the problem,It was with the Default root object CloudFront, it should be empty, the default value was index.html that's why I receive just the content of this file in SSR. also I check for the empty route on my lambda function. you can find more details here : stackoverflow.com/questions/704282...

Collapse
chriswi profile image
Chris

I am new to all this but using this article do I still get to setup the AWS Lambda, Cognito, DynamoDB, ElasticCache, and API Gateway in the AWS console with my anuglar 13 app and then do the the deploy with AWS CLI?

I want to control everything though the AWS Console but new to the serverless framework (just learning today) but my Angular app does need SSR for SEO and so far to be truly servverless AWS doesn't seem to have a way to do that.

I want this approach but a way to do SSR in angular:
docs.aws.amazon.com/whitepapers/la...

Collapse
rajanpanchal profile image
Rajan Panchal

Where the angular app hosted?

Collapse
eelayoubi profile image
eelayoubi Author • Edited on

The application is deployed using the serverless framework. In the 'Deploying to AWS' section of the blog. We deploy the serverless-distribution.yml stack. That creates a cloudfront distribution and an S3 bucket that will host and serve the application.

Collapse
rajanpanchal profile image
Rajan Panchal

Oh so it's hosted on S3 bucket.. gotcha.

Collapse
choroshin profile image
Alex Choroshin

Thanks for the great article,
everything works good but for some reason only the root route (/) is not rendered, do you have any clue?

Thanks.

Collapse
eelayoubi profile image
eelayoubi Author

Hello Alex,

What is not working exactly? I just went here: d2htk1pm9r9gbg.cloudfront.net/ and you will be redirected to d2htk1pm9r9gbg.cloudfront.net/search