DEV Community

Cover image for Easily deploy your portfolio website with AWS CDK 🚀
Pierre Chollet for Serverless By Theodo

Posted on • Edited on

Easily deploy your portfolio website with AWS CDK 🚀

Why follow this tutorial?

Coding your own portfolio website is a cool introduction to HTML, CSS and JavaScript frameworks, but there is always a hurdle: once I am done, how do I deploy my website to the world wide web? Other question: how do I accomplish it using state of the art technologies, and following industry standards?

To reach this goal, I will use the AWS CDK (Cloud Development Kit) combined with TypeScript, to provision a 100% "Infrastructure as Code" CloudFront application serving my website all around the world. Here is a quick look of what the architecture of the app will look like at the end:

CloudFront architecture

Quick announcement: I also work on a library called 🛡 sls-mentor 🛡. It is a compilation of 30 serverless best-practices, that are automatically checked on your AWS serverless projects (no matter the framework). It is free and open source, feel free to check it out!

Find sls-mentor on Github ⭐️

Set-up a TypeScript CDK project

> To use the CDK, you first have to configure an AWS profile in your CLI. The official documentation is very easy to follow if you need help.

Run these 2 commands in your CLI to setup your CDK project.

mkdir portfolio && cd portfolio
npx cdk init --language typescript
Enter fullscreen mode Exit fullscreen mode

cdk init --language typescript creates a base TypeScript repository with the following structure.

portfolio
│   .gitignore
│   cdk.json
│   package.json
│   package-lock.json
│   ts-config.json
└───bin
│   └───portfolio.ts
└───lib
│   └───portfolio-stack.ts
└───node-modules
Enter fullscreen mode Exit fullscreen mode
  • bin folder contains the different stacks of your project. This is where I can set environment variables like the AWS accountId or region.
  • lib folder contains the details of my stacks. I only have one, and it's where I will provision all the necessary resources to deploy my static website.
  • cdk.json contains the global CDK configuration. For this simple use-case, there will be no need to modify it.

⚠️ For certificate reasons, my CDK application must be deployed to region us-east-1 (the region is not important even if end users are not part of it, as we will see). To enable it, add the following snippet in bin/portfolio.ts

new PortfolioStack(app, 'PortfolioStack', {
  // To be added
  env: {
    region: 'us-east-1',
  },
  // End
});
Enter fullscreen mode Exit fullscreen mode

To finish the CDK set-up, run the following command in your CLI:

cdk bootstrap
Enter fullscreen mode Exit fullscreen mode

cdk bootstrap deploys on your AWS account the necessary resources to be able to later deploy your full app.

Last step: add the folder containing my website:

portfolio
...
└───front
│   └───src
│       └───index.html
│       └───styles.css
...
Enter fullscreen mode Exit fullscreen mode

Here I use simple HTML+CSS but it can be the built files of a React or Angular project too!

Upload your portfolio on the AWS cloud

First step to host my portfolio on AWS is to create a container hosting the website's files. The easiest solution is to use AWS S3 and to provision a new bucket containing these files.

I need to specify the path of the base html file of my website (index.html). It is also one of the rare cases where I need to set publicReadAccess to true because the content is public (I want everyone to have access to my portfolio).

I have another constraint: I want the content of this bucket to always contain the latest version of the website's source code. CDK offers a construct named BucketDeployment, that replaces the content of the bucket with the files found at a specified location (../front/src) of my repository at each deployment.

phase 1

Added together, the stack definition of PortfolioStack, in the file lib/portfolio-stack.ts should look like this:

export class PortfolioStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Path of the folder containing the built website
    const FRONT_SRC_PATH = path.join(__dirname, '../front/src');

    // Create the bucket hosting the website
    const staticWebsiteHostingBucket = new Bucket(this, 'StaticWebsiteHostingBucket', {
      publicReadAccess: true,
      websiteIndexDocument: 'index.html',
    });

    // Sync the bucket's content with the codebase
    new BucketDeployment(this, 'StaticWebsiteHostingBucketSync', {
      sources: [Source.asset(FRONT_SRC_PATH)],
      destinationBucket: staticWebsiteHostingBucket,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's run a first deployment with the CLI command:

cdk deploy
Enter fullscreen mode Exit fullscreen mode

It will create the bucket and fill it with the index.html file found in my local repository. I can see it in my AWS console!

bucket content

Access the portfolio from a custom domain name

My website is now uploaded on the AWS cloud, but it would be nicer if anyone could access it using my domain name (sls-mentor.dev).

If you do not own a domain name, AWS Route 53 offers the possibility to buy ones for a fair price. In my example, .dev domains was not available on AWS, so I bought it on gandi, but you can buy your domain name anywhere you want. The important thing is to have access to the name servers after it's yours.

How to link my S3 bucket with sls-mentor.dev? It is done in 4 simple steps:

  • Create a Route 53 hosted zone that will contain the custom DNS records needed to create this connection.
  • Create a HTTPS certificate, otherwise, my portfolio will only be accessible via http.
  • Create a CloudFront distribution, allowing to distribute my website all around the world, and to use my new HTTPS certificate.
  • Create DNS records in my hosted zone, to redirect traffic from sls-mentor.dev and www.sls-mentor.dev to the CloudFront distribution.

phase 2

Seems too complicated ? Using CloudFront allows to use the HTTPS protocol, and to deliver my website very quickly around the world thanks to caching. Furthermore, these 4 steps can be coded in a few lines thanks to the CDK, everything left to do is to launch the deployment!

Let's create the hosted zone, by adding this snippet of code under the S3 bucket definition:

const DOMAIN_NAME = 'sls-mentor.dev'; // your domain name

// Create a Route53 hosted zone to later create DNS records
// ⚠️ Manual action required: when the hosted zone was created, copy its NS records into your domain's name servers
const hostedZone = new HostedZone(this, 'DomainHostedZone', {
  zoneName: DOMAIN_NAME,
});
Enter fullscreen mode Exit fullscreen mode

Then deploy this change to create the hosted zone.

cdk deploy
Enter fullscreen mode Exit fullscreen mode

I have to perform the only manual input required in this tutorial (⚠️ very important ⚠️): after deployment, Route 53 will create a NS record containing 4 values in my new hosted zone, I have to copy them into my DNS's name servers (in order to prove my ownership of the domain).

NS records in the new hosted zone NS records

Copied into the domain name servers on gandi Name servers

On the screenshots, I did it using a domain name bought on gandi. But it can also be achieved on providers like AWS or any other one.

Now that I'm done with the domain, I can create the certificate, and set the validation method to fromDns. It will automatically communicate with my freshly created hosted zone to validate the authenticity of the certificate.

⚠️ The certificate MUST be created in the region us-east-1 to be compatible with CloudFront, that's why I deploy all my resources in this region. CloudFront allows for "at edge" delivery, which means that it's not a problem if my visitors are not in the US.

const WWW_DOMAIN_NAME = `www.${DOMAIN_NAME}`;

// Create the HTTPS certificate (⚠️ must be in region us-east-1 ⚠️)
const httpsCertificate = new Certificate(this, 'HttpsCertificate', {
  domainName: DOMAIN_NAME,
  subjectAlternativeNames: [WWW_DOMAIN_NAME],
  validation: CertificateValidation.fromDns(hostedZone),
});
Enter fullscreen mode Exit fullscreen mode

Then, time for the CloudFront distribution. It is the main node of my architecture:

  • It communicates with the certificate I just created to allow HTTPS communication.
  • It is the link between the us-east-1 bucket and users all around the world: thanks to caching, there will be minimal latency for end users.

I specify my bucket as the origin, enable a REDIRECT_TO_HTTPS policy, set the domain names (with the addition of the www subdomain for optimal compatibility with all browsers), and finally reference my new https certificate.

I also specified an optional responseHeadersPolicy, with the ID corresponding to the AWS Managed-SecurityHeadersPolicy. It's a quick win to improve security in my distribution. (This best practice is part of sls-mentor, feel free to check it out!)

// Create the CloudFront distribution linked to the website hosting bucket and the HTTPS certificate
const cloudFrontDistribution = new Distribution(this, 'CloudFrontDistribution', {
  defaultBehavior: {
    origin: new S3Origin(staticWebsiteHostingBucket, {}),
    viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    responseHeadersPolicy: { responseHeadersPolicyId: '67f7725c-6f97-4210-82d7-5512b31e9d03' },
  },
  domainNames: [DOMAIN_NAME, WWW_DOMAIN_NAME],
  certificate: httpsCertificate,
});
Enter fullscreen mode Exit fullscreen mode

Last step, redirect requests heading to sls-mentor.dev and www.sls-mentor.dev to the CloudFront distribution. I create 2 DNS records in my hosted zone, which have the CloudFront distribution as target. These records are "A" records, linking my domain name with the IPv4 address of the CloudFront distribution.

// Add DNS records to the hosted zone to redirect from the domain name to the CloudFront distribution
new ARecord(this, 'CloudFrontRedirect', {
  zone: hostedZone,
  target: RecordTarget.fromAlias(new CloudFrontTarget(cloudFrontDistribution)),
  recordName: DOMAIN_NAME,
});

// Same from www. sub-domain
new ARecord(this, 'CloudFrontWWWRedirect', {
  zone: hostedZone,
  target: RecordTarget.fromAlias(new CloudFrontTarget(cloudFrontDistribution)),
  recordName: WWW_DOMAIN_NAME,
});
Enter fullscreen mode Exit fullscreen mode

And I am done! Last step is to deploy everything one more time.

cdk deploy
Enter fullscreen mode Exit fullscreen mode

Be aware that changes to DNS records can take up to 24 hours to be propagated, do not worry if the deployment is successful but your website doesn't work instantly

Time to test it! These 4 links should all redirect to the same website, always using the https protocol, regardless of what I specify:

Top comments (2)

Collapse
 
__natethegreat8 profile image
Bilbo Baggins

Hi what are import definitions for path, bucket, and bucketdeployment in stack file

Collapse
 
pchol22 profile image
Pierre Chollet

oops sorry I forgot to mention them,

import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import path from 'path';
import { Construct } from 'constructs';
Enter fullscreen mode Exit fullscreen mode

(all imports are then from aws-cdk-lib/something, auto-completion should help you)

if you have typescript troubles with the "path" import, add "esModuleInterop": true in the tsconfig.json compilerOptions