How to build a single-page application deployment using AWS CDK
Tags: webdev, aws, devops, javascript
Published: 2024-05-18 11:36:00

All modern applications will require some form of static assets to actually power the functionality for their users, whether it’s media content, supporting scripts or the application content itself.

A super common pattern that I always call upon when deploying single-page applications (SPA) is deploying my static assets into S3 and serving them through CloudFront.

This is incredibly useful when working not only with single-page applications, but also for static assets that need to be served to ensure your applications work as expected.

Let’s take a look at how we can define the infrastructure resources to facilitate serving these assets, using my old favourite AWS CDK.

## 🔍 Architecture

To give you an idea of what we’re wishing to achieve with this, here is a high-level architecture diagram of what we want to build:

![CloudFront and S3 Architecture Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4m80z2xqsvz6588nm03.png)

## ✍️ Let's write some CDK

Crafting the CDK for this pattern is extremely straightforward. There are only the two components:
- S3 Bucket
- CloudFront Distribution

To provision the S3 bucket, you can use the L2 construct that already exists within CDK:

```typescript
const s3Bucket = new cdk.aws_s3.Bucket(this, "s3-bucket-content", {
  blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL,
  encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED,
  enforceSSL: true,
  removalPolicy: cdk.RemovalPolicy.RETAIN,
  bucketName: "cdk-spa-assets-pattern",
});
```

With the above, we're just creating a S3 bucket that has public access denied and encryption at rest enabled.

We're then going to create our CloudFront distribution and update the bucket to allow the distribution to read from it:

```typescript
const originAccessIdentity = new cdk.aws_cloudfront.OriginAccessIdentity(
  this,
  "cloudfront-origin-access-identity"
);
s3Bucket.grantRead(originAccessIdentity);

const cloudfrontS3Origin = new cdk.aws_cloudfront_origins.S3Origin(
  s3Bucket, {
    originAccessIdentity: originAccessIdentity,
  }
);

const distribution = new cdk.aws_cloudfront.Distribution(
  this,
  "cloudfront-content-distribution", {
    defaultBehavior: {
      origin: cloudfrontS3Origin,
    },
    defaultRootObject: "index.html",
    priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_100,
  }
);
```

Firstly, we're creating the `OriginAccessIdentity` construct, this is then used to grant read access to the S3 bucket before assigning it to the CloudFront distribution itself.

We're specifically using the `Distribution` construct here as it's the newer implementation (replacing the `CloudFrontWebDistribution` construct). As part of that, the API has changed slightly and we need to define a CloudFront origin itself - we are using S3 as our origin and can pass our origin identity permission into it to allow the distribution to read from the bucket.

We assign that as our default behaviour (as a CloudFront distribution can have multiple behaviours), and finally define our price class.

And that's it! Deploy those and you'll have a S3 bucket that you can push your build assets into. By default, the entry point for the CloudFront distribution is `index.html` - so whenever you build your SPA assets, you simply push them into the bucket and you will be good to go!

## 💰 Cost efficiency

Low cost because all of the assets are stored in S3 and served via CloudFront, so your costs will scale with the requests and the traffic coming inbound. Since CloudFront heavily caches at edge, the requests downstream to your S3 bucket will be fairly low - unless you explicitly invalidate the cache if you have new objects. 

## Conclusion
- Static assets can be pushed into S3 bucket and served via the global CloudFront CDN network.
- Extremely cost-effective for working with a lot of network requests.
- Can work for SPA static websites or other general static content that might be required to power your applications.