DEV Community

Osuke Uesugi
Osuke Uesugi

Posted on

Hosting SPA with Route53, S3, and CloudFront by CDK for Terraform + TypeScript

Motivation

  • For my new frontend project, it will work on CloudFront + S3. I'd like to know how it works.
  • I'd like to know if CDK for Terraform is beneficial or not.
  • I just want to define AWS resources by Terraform + TypeScript.

What is AWS CDK for Terraform?

AWS CDK stands for AWS Cloud Development Kit.
It's an OSS framework to define cloud application resources by using your familiar programming languages including TypeScript, Python, Java, etc.

When we define our cloud resources, CDK will generate a CloudFormation template.
By using it, the actual resources are created.

From 2020, CDK has started supporting Terraform. So we can choose Terraform instead of CloudFormation when we use CDK.
It means we can define other cloud resources like GCP or Azure as well.

How Route53, CloudFront, and S3 work together?

Before writing the code, let me describe how SPA work with Route53, CloudFront, and S3.

Let's say I have registered example.com domain in Route53.
And i'd like to host a SPA with blog.example.com.
To achieve it, the following settings are necessary.

  • Creating an SSL certification by ACM
  • Creating a bucket on S3 that will have the SPA related files like HTML, JavaScript, CSS, etc
  • Creating a CloudFront distribution
  • Associating the distribution with the certification and the bucket
  • Associating blog.example.com with the distribution

Let's configure them with CDK for Terraform + TypeScript.

Creating a project

First of all, cdktf-cli is necessary.
It can be installed via npm.

npm i -g cdktf-cli@latest
Enter fullscreen mode Exit fullscreen mode

After the installation, you can create a project of CDK for Terraform.
This time I'd like to use TypeScript. So the command to create a project is like this

mkdir my-cdktf-project
cd my-cdktf-project
cdktf init --template=typescript
Enter fullscreen mode Exit fullscreen mode

After executing the above command, you are asked to determine the following settings.

  • Whether you use Terraform Cloud as a remote state or not
  • Project name
  • Project description
Do you want to continue with Terraform Cloud remote state management? (Y/n) → n Since I use S3 as a remote state this time
Project Name (my-cdktf-project) → empty
Project Description (A simple getting started project for cdktf.) → empty
Enter fullscreen mode Exit fullscreen mode

After that, the project is ready with the following structure.

my-cdktf-project/
├ .gitignore
├ .npmrc
├ __tests__/
├ cdktf.json
├ help/
├ jest.config.js
├ main.ts
├ node_modules/
├ package-lock.json
├ package.json
├ setup.js
└ tsconfig.json
Enter fullscreen mode Exit fullscreen mode

main.ts is the place to define the resources. (I’ll define all the resources in the place this time)
The initial code in the file is the following.

import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    // define resources here
  }
}

const app = new App();
new MyStack(app, "my-cdktf-project");
app.synth();

Enter fullscreen mode Exit fullscreen mode

Configure Provider and remote state

Provider

The provider is AWS this time.
In order to configure that, @cdktf/provider-aws is necessary to be installed.

npm i @cdktf/provider-aws
Enter fullscreen mode Exit fullscreen mode

Then you can use the provider.

import { AwsProvider } from "@cdktf/provider-aws";

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    // provider
    new AwsProvider(this, "default", {
      region: "ap-northeast-1",
    });
Enter fullscreen mode Exit fullscreen mode

It's the same as using HCL

provider "default" {
  region = "ap-northeast-1"
}
Enter fullscreen mode Exit fullscreen mode

To create SSL certification via ACM that is defined later, it's necessary to specify us-east-1 region.
So let me define another provider for that.

const awsProviderUsEast = new AwsProvider(this, "use_east", {
  region: "us-east-1",
  alias: "us-east-1"
});
Enter fullscreen mode Exit fullscreen mode

Remote state

I'd like to use S3 for the remote state.
It's defined like this

import { App, S3Backend, TerraformStack } from "cdktf";
...
new S3Backend(this, {
  bucket: "hogehoge-tfstate",
  key: "website",
  region: "ap-northeast-1"
});
Enter fullscreen mode Exit fullscreen mode

Creating an SSL certification

With the following code, the certification for blog.example.com is created with DNS validation.

import { AwsProvider, route53, acm } from "@cdktf/provider-aws";
...

const dataAwsRoute53Zone = new route53.DataAwsRoute53Zone(this, "zone", {
  name: "example.com"
});

const acmCertificate = new acm.AcmCertificate(this, "cert", {
  domainName: "blog.example.com",
  validationMethod: "DNS",
  provider: awsProviderUsEast,
  lifecycle: {
    createBeforeDestroy: true
  }
});

const record = new route53.Route53Record(this, "validation_record", {
  name: acmCertificate.domainValidationOptions("0").resourceRecordName,
  type: acmCertificate.domainValidationOptions("0").resourceRecordType,
  records: [acmCertificate.domainValidationOptions("0").resourceRecordValue],
  zoneId: dataAwsRoute53Zone.zoneId,
  ttl: 60
});

new acm.AcmCertificateValidation(this, "certification_validation", {
  certificateArn: acmCertificate.arn,
  validationRecordFqdns: [record.fqdn],
  provider: awsProviderUsEast
});
Enter fullscreen mode Exit fullscreen mode

Creating a bucket on S3

HTML, JavaScript, CSS files will be in this bucket.

import { AwsProvider, s3, route53, acm } from "@cdktf/provider-aws";
...

const s3Bucket = new s3.S3Bucket(this, "blog_example_com", {
  bucket: "blog.example.com"
});
Enter fullscreen mode Exit fullscreen mode

Creating a CloudFront distribution

Next, will create OAI first.
OAI(origin access identity) is a special CloudFront user.
By associating it with a Cloudfront distribution, CloudFront can access an S3 bucket that doesn't allow public access.

import { AwsProvider, s3, route53, acm, cloudfront } from "@cdktf/provider-aws";
...

const oai = new cloudfront.CloudfrontOriginAccessIdentity(this, "oai", {
  comment: "blog.example.com"
});
Enter fullscreen mode Exit fullscreen mode

And the S3 bucket is supposed to be accessed by the OAI.
So I defined the following bucket policy.

import { AwsProvider, s3, route53, acm, cloudfront, iam } from "@cdktf/provider-aws";
...

const policyDocument = new iam.DataAwsIamPolicyDocument(this, "policy_document", {
  statement: [{
    actions: ["s3:GetObject"],
    resources: [`${s3Bucket.arn}/*`],
    principals: [{
      type: "AWS",
      identifiers: [oai.iamArn]
    }]
  }]
});

new s3.S3BucketPolicy(this, "connect_bucket_and_policy", {
  bucket: s3Bucket.id,
  policy: policyDocument.json
});
Enter fullscreen mode Exit fullscreen mode

And the following code is the distribution definition.
In the code, the S3 bucket is defined as the origin and the certification is attached.

const distribution = new cloudfront.CloudfrontDistribution(this, "distribution", {
  enabled: true,
  defaultRootObject: "index.html",
  aliases: ["blog.example.com"],
  customErrorResponse: [
    {
      errorCode: 403,
      responseCode: 200,
      responsePagePath: "/"
    }
  ],
  origin: [
    {
      originId: s3Bucket.id,
      domainName: s3Bucket.bucketRegionalDomainName,
      s3OriginConfig: {
        originAccessIdentity: oai.cloudfrontAccessIdentityPath
      }
    }
  ],
  defaultCacheBehavior: {
    allowedMethods: ["GET", "HEAD"],
    cachedMethods: ["GET", "HEAD"],
    targetOriginId: s3Bucket.id,
    forwardedValues: {
      queryString: false,
        cookies: {
          forward: "none"
      }
    },
    viewerProtocolPolicy: "redirect-to-https",
      minTtl: 0,
      defaultTtl: 0,
      maxTtl: 0
    },
    restrictions: {
      geoRestriction: {
        restrictionType: "none"
      }
    },
    viewerCertificate: {
      acmCertificateArn: acmCertificate.arn,
      sslSupportMethod: "sni-only"
    }
  });
Enter fullscreen mode Exit fullscreen mode

You might already know, SPA doesn’t have an actual HTML file for all the paths.
So with the following configuration, the root index.html is returned in that case.

customErrorResponse: [
    {
      errorCode: 403,
      responseCode: 200,
      responsePagePath: "/"
    }
  ],
Enter fullscreen mode Exit fullscreen mode

Associating blog.example.com with the distribution

Alias record can use for some AWS endpoints like CloudFront distribution, S3 bucket, ELB, and the endpoint returns its IP address.
I'd like to associate blog.example.com with the IP address of the distribution.
So the code is the following

new route53.Route53Record(this, "website", {
  zoneId: dataAwsRoute53Zone.id,
  name: "blog.example.com",
  type: "A",
  alias: [{
    name: distribution.domainName,
    zoneId: distribution.hostedZoneId,
    evaluateTargetHealth: false
  }]
});
Enter fullscreen mode Exit fullscreen mode

Create resources by cdktf

The code is ready to be applied, so to confirm the change by above code, you can run the following command

cdktf plan
Enter fullscreen mode Exit fullscreen mode

To apply them

cdktf apply
Enter fullscreen mode Exit fullscreen mode

After a while, all the resources defined with the above code will be created. Then you can see your SPA. Of course you need to upload your HTML, JS, CSS, etc files to the S3 bucket.

Is CDK for Terraform beneficial?

Type checking

Type checking is a benefit.
TypeScript informs us when the type of a value doesn't have the expected type.

Needs to translate the Terraform syntax to CDK's

When you define a data source, you can write it by HCL like

data "aws_iam_policy_document" "example" {
Enter fullscreen mode Exit fullscreen mode

But with CDK, it becomes

new iam.DataAwsIamPolicyDocument(this, "example", {
Enter fullscreen mode Exit fullscreen mode

You need to translate the Terraform syntax to CDK's every time. it's a bother a bit.

Other

  • With CDK, need to instantiate a class every time we define a resource or a data source. HCL is simpler and more concise.
  • CDK for Terraform is still beta. It's not recommended to use it in production.

Conclusion

If you are already familiar with Terraform, you can't find reasone to use CDK for Terraform i think.
But if you are newbie of IaC and want to start with your familiar programing language, it might be comfortable.

Top comments (0)