DEV Community

Ryan Dsouza
Ryan Dsouza

Posted on • Edited on

Deploying a SPA using aws-cdk (TypeScript)

I recently read a post on getting started with aws-cdk and it has intrigued me since then.

aws-cdk is Infrastructure as Code. It's like Cloudformation or Terraform but it has an added advantage. You can create your infrastructure in the programming language of your choice. This also means that you are not bound to the constructs of .yaml or have to learn the unknown syntax of Terraform (.tf). It's all in a language that you understand.

Note: Under the hood, the cdk uses Cloudformation itself.

Currently aws-cdk supports 4 languages, namely TypeScript, Python, C# and Java. In this post we shall explore how to create a Codebuild deployment of a SPA on S3 and served with Cloudfront.

TLDR; all the code is in this repo if you want to start hacking right away!

Note: This post assumes that you have aws-cli installed and configured an AWS profile with an access and secret key via aws configure.

We will be using the TypeScript CDK and creating 3 services.

  1. An S3 bucket for hosting our static website.

  2. A Cloudfront Distribution that will act as a CDN.

  3. A Codebuild project that will trigger whenever your code is pushed or a PR is merged.

Let's start with the S3 Bucket. The code will be as follows:

import * as S3 from '@aws-cdk/aws-s3';

const bucket = new S3.Bucket(this, cfg.BUCKET_NAME, {
  websiteIndexDocument: 'index.html',
  websiteErrorDocument: 'index.html',
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});
Enter fullscreen mode Exit fullscreen mode

First we import the aws-s3 package and tell the cdk to create an S3 bucket with a bucket name specified in our configuration.

Note: This configuration comes from the .env file that you will have to setup in the repository. Instructions are provided in the README.md.

We specifically add the index and error documents to be index.html stating that we are deploying a SPA and all redirects are to be done via the index.html file.

Our next step is setting up the Cloudfront distribution that will act as a CDN for our bucket. Here's the code for that.

import {
  CloudFrontWebDistribution,
  CloudFrontWebDistributionProps,
  OriginAccessIdentity,
} from '@aws-cdk/aws-cloudfront';

const cloudFrontOAI = new OriginAccessIdentity(this, 'OAI', {
  comment: `OAI for ${cfg.WEBSITE_NAME} website.`,
});

const cloudFrontDistProps: CloudFrontWebDistributionProps = {
  originConfigs: [
    {
      s3OriginSource: {
        s3BucketSource: bucket,
        originAccessIdentity: cloudFrontOAI,
      },
      behaviors: [{ isDefaultBehavior: true }],
    },
  ],
};

const cloudfrontDist = new CloudFrontWebDistribution(
  this,
  `${cfg.WEBSITE_NAME}-cfd`,
  cloudFrontDistProps
);
Enter fullscreen mode Exit fullscreen mode

First we import our necessary modules from the aws-cloudfront package.

Then we create an Origin Access Identity (OAI), for our Cloudfront distribution. This will allow only Cloudfront to access our static website from S3 and if someone tries to access it otherwise, it will be denied. So everyone would be able to view the website only via the Cloudfront URL.

Lastly, we create the Cloudfront distribution, providing it the props that it requires, the main one being the S3 bucket.

See how we pass the bucket variable initialized while creating the S3 Bucket. This ensures that the order of resources will matter; S3 bucket first, then the cloudformation distribution.

We also pass our OAI created above in the originAccessIdentity prop.

The next part would be adding a policy for our S3 bucket to only accept requests from Cloudfront. And to add policies, the one service that comes to mind is IAM!

So let's create a policy to restrict the S3 bucket access to Cloudfront only.

const cloudfrontS3Access = new IAM.PolicyStatement();
cloudfrontS3Access.addActions('s3:GetBucket*');
cloudfrontS3Access.addActions('s3:GetObject*');
cloudfrontS3Access.addActions('s3:List*');
cloudfrontS3Access.addResources(bucket.bucketArn);
cloudfrontS3Access.addResources(`${bucket.bucketArn}/*`);
cloudfrontS3Access.addCanonicalUserPrincipal(
  cloudFrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId
);
Enter fullscreen mode Exit fullscreen mode

In the above code, we create a new IAM Policy named cloudfrontS3Access and we add some specific actions to it. The actions being getting all the objects and the bucket as well.

In the addResources method, we specify the resources. Now we not only want the bucket itself but also the entire contents inside the bucket. Which is why we have provided two resources.

cloudfrontS3Access.addResources(bucket.bucketArn);
cloudfrontS3Access.addResources(`${bucket.bucketArn}/*`);
Enter fullscreen mode Exit fullscreen mode

The first one is for the bucket and the second for the contents inside the bucket.

Lastly we add as the Principal, the origin access identity that we created above. Only what's in the Principal can access the specified resource. So here, only our Cloudfront distribution can access this resource.

Now, we have created this policy but not added it anywhere. Where could it likely be added? You guessed it right! In our S3 Bucket policy. So we have to tell the bucket in some way that this policy should be attached and the way we do it is below

bucket.addToResourcePolicy(cloudfrontS3Access);
Enter fullscreen mode Exit fullscreen mode

We are telling our S3 Bucket to add the policy cloudfrontS3Access.

Now we are moving on to the last step of our development, i.e. creating a Codebuild project.

First, we create a GitHub repository source that Codebuild can use.

Note: You can create a Bitbucket repo in the same manner as well.

const repo = Codebuild.Source.gitHub({
  owner: cfg.REPO_OWNER,
  repo: cfg.REPO_NAME,
  webhook: true,
  webhookFilters: webhooks,
  reportBuildStatus: true,
});
Enter fullscreen mode Exit fullscreen mode

The above code will create our repository that will act as a source to our Codebuid Project. We have passed the owner of the repo and the repository name as well.

You must have noticed that we have passed something called webhooks set to true and also webhookFilters. What are those?

Webhook filters allow you to run the build on any branch based on the conditions and action on the branch.

We have added a webhook in the following manner

const webhooks: Codebuild.FilterGroup[] = [
  Codebuild.FilterGroup.inEventOf(
    Codebuild.EventAction.PUSH,
    Codebuild.EventAction.PULL_REQUEST_MERGED
  ).andHeadRefIs(cfg.BUILD_BRANCH),
];
Enter fullscreen mode Exit fullscreen mode

This webhook states that on PUSH and PULL REQUEST MERGED on the branch specified in our config, initiate the build runner in Codebuild. As an example, we will be using the master branch. So any push or any PR merge will trigger the build.

Lastly, we shall combine all this in creating our Codebuild pipeline as shown below.

import * as cdk from '@aws-cdk/core';
import * as Codebuild from '@aws-cdk/aws-codebuild';

const project = new Codebuild.Project(this, `${cfg.WEBSITE_NAME}-build`, {
  buildSpec: Codebuild.BuildSpec.fromSourceFilename('buildspec.yml'),
  projectName: `${cfg.WEBSITE_NAME}-build`,
  environment: {
    buildImage: Codebuild.LinuxBuildImage.STANDARD_3_0,
    computeType: Codebuild.ComputeType.SMALL,
  },
  source: repo,
  timeout: cdk.Duration.minutes(20),
});
Enter fullscreen mode Exit fullscreen mode

Here we are telling Codebuild to create a project for the source that we have added above (via GitHub) and we specify parameters related to the build images and timeout.

One last thing left right now. Our Codebuild setup needs access to S3 and Cloudfront to push the build and to create an invalidation respectively.

So let's add a couple of policies to our codebuild project.

// iam policy to push your build to S3
project.addToRolePolicy(
  new IAM.PolicyStatement({
    effect: IAM.Effect.ALLOW,
    resources: [bucket.bucketArn, `${bucket.bucketArn}/*`],
    actions: [
      's3:GetBucket*',
      's3:List*',
      's3:GetObject*',
      's3:DeleteObject',
      's3:PutObject',
    ],
  })
);

// iam policy to invalidate cloudfront dsitribution's cache
project.addToRolePolicy(
  new IAM.PolicyStatement({
    effect: IAM.Effect.ALLOW,
    resources: ['*'],
    actions: [
      'cloudfront:CreateInvalidation',
      'cloudfront:GetDistribution*',
      'cloudfront:GetInvalidation',
      'cloudfront:ListInvalidations',
      'cloudfront:ListDistributions',
    ],
  })
);
Enter fullscreen mode Exit fullscreen mode

This will give you access to perform the necessary operations on the S3 bucket and cloudfront distibution.

So we're done and the only thing required now for us to test is to create a repository with any SPA using either create-react-app or vue-cli.

Then replace all the config variables with the one's related to the repository and then run npm run deploy -- --profile <profileName> where profileName is the one you configured with the aws-cli.

I have added a sample buildspec.yml below that you can tweak and add in your repository.

version: 0.2
phases:
  install:
    runtime-versions:
      nodejs: 10
  pre_build:
    commands:
      - yarn
  build:
    commands:
      - echo Build started on `date`
      - yarn build
      - cd build
      - aws s3 sync . s3://${S3_BUCKET} --exclude "*.js.map" --delete
      - aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_ID} --paths "/index.html"
    finally:
      - echo Build completed on `date`
Enter fullscreen mode Exit fullscreen mode

Here, I have used Codebuild's environment variables to refer to the S3 bucket and Cloudfront distribution. You can add those in the Codebuild build project from the console or directly add it in the file above as configuration (I have done that in the repo!).

Thanks for reading! I shall add more posts on aws-cdk in this series :)

Top comments (4)

Collapse
 
danko56666 profile image
Daniel Ko

I'm surprised this hasn't gotten more attention, but anyways...other than the benefit of being able to use a programming language of choice, can you list other benefits there are to using cdk over terraform? How's quality of docs? How much support is there when one gets stuck? Is cdk widely used and reliable? Would love to hear your thoughts.

Oh and one unrelated question. How much knowledge of cloudformation is needed to use cdk?

Collapse
 
ryands17 profile image
Ryan Dsouza

Hey Daniel πŸ‘‹
Thanks for enjoying the article.

can you list other benefits there are to using cdk over terraform?

Another great advantage is super less code to create something than either Terraform or Cloudformation.
For e.g. this is what it takes to create a VPC with 2 public and 2 private subnets:

const vpc = new ec2.Vpc(this, 'serverless-app', {
      cidr: '10.0.0.0/20',
      natGateways: 0,
      maxAzs: 2,
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          cidrMask: 22,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 22,
          name: 'private',
          subnetType: ec2.SubnetType.ISOLATED,
        },
      ],
})
Enter fullscreen mode Exit fullscreen mode

Compare it to the code you write with Terraform and you'll see the difference. CDK gives you simple and easy constructs to create such resources.

How's quality of docs? How much support is there when one gets stuck? Is cdk widely used and reliable?

Docs are good and the support is great. There's a CDK Slack channel here that you can join which is awesome and people there are very helpful. Also CDK is widely used as an upgrade over Cloudformation in a great way. You can go ahead and use it in production quite easily as it's simple to setup.

How much knowledge of cloudformation is needed to use cdk?

I would say it's recommended to have basic knowledge of Cloudformation because what CDK creates under the hood is a Cloudformation template.

Let me know if this helps :)

Collapse
 
danko56666 profile image
Daniel Ko

Sweet yea!
I do have basic understanding of cloudformation, if you can even call it that, from playing around with the serverless framework and I do plan to learn more about it eventually. It's just as of right now I am not very familiar with it.

As an aside have you played around with Pulumi at all?

Thread Thread
 
ryands17 profile image
Ryan Dsouza

Unfortunately not, just Cloudformation and Terraform apart from CDK