CloudFront 101: API Gateway and S3 SPA origins
Tags: aws, cloud, api, serverless
Published: 2023-07-06 12:10:58

I was recently thrown into the deep end with CloudFront when I had to configure an AWS account for a client. The account had a CloudFront distribution with multiple origins, one of which is a single page application S3 static website, and the others apis in api gateway. Because of the scarce and/or dispersed information I found, I decided to aggregate all the points that I found challenging into a single blog. 

Unlike my other blogs, this one is more of a “what-to-do” and less of a “step by step how-to”. If you find it too long and too condensed to be read whole, I would recommend referring to the specific points you’re stuck on.

The points that will be covered are:

1 - CloudFront VS edge optimized api gateway 
2 - api default endpoint vs custom domain name
3 - CloudFront multi origin default behavior and ordered behavior
4 - Cloudfront with an SPA (paths, 403 redirects and lambda@edge)

## Introduction

Cloudfront is AWS’s offering of a CDN, or content delivery network. In short, its job is to cache your content through a worldwide network of edge locations to improve latency and user experience.

On the other hand, Api Gateway, as the name suggests, is a service that provides an entry point to your apis. 

And finally S3, which is simply a storage solution that AWS offers. What’s special about S3 is that you can use it to host static websites (think read-only sites, or sites that can use apis to interact with the database rather than having a full-on backend framework). 

## CloudFront Vs edge optimized api gateway

#### Edge optimized endpoints

Suppose you need to leverage the power of edge locations to decrease the latency of your apis. In that case, you need to use CloudFront. Luckily, you don’t need to figure out how CloudFront works to use it with your apis, since Api Gateway offers a nifty integration to CF. 

All you need to do is go into your api settings and switch the endpoint type to edge optimized.
This will create a CloudFront distribution for you and link it to your api.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/njcsxuolbyth0ixutdql.png)

So why would you ever use a manually created CF distribution if this option already exists?


#### Linking your existing CloudFront distribution to your api 

While edge optimized endpoints are good enough for the majority of use cases, the CF distribution that is created for you cannot be modified and is in fact managed by AWS. If your use case involved some tinkering with CF, you might have to opt into the second option: creating your own CF distribution.

To add your api to your CF distribution, you just simply add your api as an origin, while specifying your api’s default endpoint as an origin domain.

**If you happen to have a websocket api**, make sure to add the following headers in an origin request policy. (for more information, consult [the official documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.websockets.html))
Sec-WebSocket-Key
Sec-WebSocket-Version
Sec-WebSocket-Protocol
Sec-WebSocket-Accept
Sec-WebSocket-Extensions


**If you happen to have a lambda authorizer in place for your api**, be sure to add the Authorize header in the origin behavior in CF


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pizxar9gz8wjr856tvjv.png)

**But that's not all**. Remember, your api has a stage defined (often, stages will be the names of the environments, so /dev or /prod). If your api has a /dev stage, then you need to add a /dev to the end of your default endpoint url in CF. This can be done by specifying an origin path of “/dev” to your origin, or by adding a behavior to your cloudfront distribution. 

If you decide to go for the latter approach, you specify the path pattern as “/dev”, and associate your api origin to it. Now, when someone accesses the url: your-cloudfront-domain-name/dev, they will be redirected to your-default-endpoint/dev, which will correctly point to your api stage.

Great, now you’re all set! But…what’s a default endpoint anyway? 

## api default endpoint vs custom domain name

A default endpoint is the url that is created along with your api, and it is how you in fact access said api. The default endpoint is an amazon url, and it looks like this : api-id.execute-api.region.amazonaws.com. And while this endpoint is perfectly usable, it might not work for all use cases. If for some reason you need your api endpoint url to not be an amazon url, you need to use custom domains.

Custom domains are quite simply a custom domain name that points to your api.
To create a custom domain you need to:

1. Go to ACM and create a certificate for the domain in question
and validate the certificate in route 53

2. Create the custom domain in the api console. 

3. Under api mapping, link the custom domain to an api, and to a stage in the api

4. Go to route 53 and create an alias to an api gateway. (the api endpoint to specify can be found in the custom domain page in the api console, under “API Gateway domain name”).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0lazbcmlp3zz0nux8fz9.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ie8tylgp0shb42lnyrma.png)

You’re all set, now you can access your api using the custom domain. Don’t forget to disable the default endpoint in the api settings if you’re not using it anymore.



**Important note when using CloudFront** : 
Since the custom domain name already points to a stage, you don't need to add any paths in CloudFront. This means that, unlike with default endpoints, you do not specify an origin path, or a behavior. 

Alternatively, if you have to specify a behavior because you have multiple origins, you can add a path to the custom domain name. By adding a “/dev” path to the api custom domain name, you can now keep the CF behavior of “/dev” as well.

But how do you use multiple origins in CloudFront?

## CloudFront multi origin default behavior and ordered behavior

CloudFront can handle multiple origins, and they don’t even have to have the same type! As an example, you can have 2 rest api origins, 1 websocket origin and an S3 bucket origin.

They Way CloudFront knows which request to forward to what origin is by using behaviors. In the behaviors you specify a path that, when matched, the request is passed to the origin. 

So for example, you can specify a behavior path of “/api” to forward to your api origin, and a default behavior to forward to your S3 website.

In this example, if you go to the url cloudfront-domain-name/api, you’re forwarded to the api. and if you go to any other path, (including cloudfront-domain-name/ base path), you’re forwarded to the S3 website.

If you happen to be using Terraform, you need to note the following: 

- To specify an ordered behavior, you have to first specify a default behavior

- The ordered behaviors are created in the same order as in the code. This has an effect on the precedence (the order in which the behaviors get evaluated by CloudFront)

Now that we know how to add origins, let's look at the specifics of adding an SPA website hosted on S3.

## Cloudfront with an SPA

#### Reaching your S3 static website

 Before we go into the specifics of an SPA, let's first discuss how to reach your website hosted on S3.

First you need to create an origin, and put the S3 website url as an origin domain. To retrieve your S3 website url, you need to go into your S3 bucket, and under permissions, scroll to the end. (make sure the s3 website hosting option is enabled)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8jywytbdy5djfnn2y70z.png)

Second, create an origin access control for CloudFront to have permissions to reach the S3 bucket. In terraform, this might look like this

```
resource "aws_cloudfront_origin_access_control" "CloudFrontOriginAccessControl" { 
  name                              = "CF-access-control"
  description                       = "reach s3 static website"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}
```

Third, make sure the bucket permissions allow CF to access the bucket. The statement for the bucket policy might look like this:

 `{
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::your-s3-bucket-name/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::account-id:distribution/distribution-id"
                }
            }
        }`

**Important note about paths**: CloudFront always redirects the path as is. So for example, if you have set the behavior path of “/static-website” to redirect to your s3 static website origin, then you have to make sure your website code is in a folder named “static-website“ in S3. Generally, it's best to keep the behavior to “/”, and to keep the website code in the root of your S3 bucket.

#### Redirecting 403 errors in an SPA
Apparently, CloudFront has some weird behavior with single page application websites, which causes it to throw 403 errors. A solution for this is to redirect any 403 errors to the index page.

If your CF distribution only has one origin, then this is fairly straightforward. You just need to add an error page to your distribution that redirects 403 errors to a 200 (which is the http code for an ‘OK’ response), and to display the index page.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3d8e6ullyjvyir9apey.png)

However, when your CF distribution has multiple origins, it gets a little more complicated. This error page that we just created is applied to all origins, which means that in the case where our api origin was to throw a 403, then it would get redirected to the index page of the static website.

If we want the redirects to only apply to one of our origins, then we need to add logic to our Cloudfront. To do That, we need to use lambda@edge. The steps are: 

1. The lambda needs to be created in us-east-1, and here is what the code could look like (Nodejs runtime)

```
'use strict';
exports.handler = (event, context, callback) => {
       const response = event.Records[0].cf.response;
    if (response.status === '403') {
        response.status = 302;
        response.statusDescription = 'page found';
        /* Drop the body*/
        response.body = '';
        response.headers['location'] = [{ key: 'Location', value: '/index.html' }];
    }
    callback(null, response);
};
```

2. in the lambda console, under action, press deploy to lambda@edge. Specify your distribution and and deploy it as an “origin response”

3. in CF, in your origin behavior for the S3 origin, scroll down. Under origin response, add the lambda arn


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iox6v6zf4wrml5hi9lt0.png)

You’re all set!

## Conclusion

This blog was a non-exhaustive list of the problems you might face when working with CloudFront and api gateway, as well as when working with CloudFront and S3 static websites. My attempts to be brief still resulted in an 8 page blog, and I still feel I can add more detail, so feel free to ask any questions you have in the comments and I will be sure to get to them.