DEV Community

loading...

Host Static website using AWS CDK for Terraform: Part 1

thakkaryash94 profile image Yash Thakkar Updated on ・7 min read

There are many ways we can write and host a website like writing plain HTML files, using frameworks like Angular, React, Vue, Gatsby, many more, and hosting it on various services like Netlify, S3, Firebase, Azure, Zeit for free. In this blog, we will see, how we can use AWS CDK and Terraform to host our website on S3 without leaving the terminal.

AWS Cloud Development Kit (AWS CDK)

The AWS-CDK was released and open sourced around May 2018. The AWS Cloud Development Kit (AWS CDK) is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation.

Terraform

Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. Terraform can manage existing and popular service providers as well as custom in-house solutions. The infrastructure Terraform can manage includes low-level components such as compute instances, storage, and networking, as well as high-level components such as DNS entries, SaaS features, etc.

CDK for Terraform

Hashicorp published CDK for Terraform with Python and TypeScript support. CDK for Terraform generates Terraform configuration to enable provisioning with Terraform. The adaptor works with any existing provider and modules hosted in the Terraform Registry. The core Terraform workflow remains the same, with the ability to plan changes before applying.

  • The CDK for Terraform project includes two packages

    • “cdktf-cli” - A CLI that allows users to run commands to initialize, import, and synthesize CDK for Terraform applications.
    • “cdktf” - A library for defining Terraform resources using programming constructs.

Host Static Website

Getting Started

We will be using Node.js with Typescript to setup the deployment.

install the CDK for Terraform globally.

$ npm i -g cdktf-cli
Enter fullscreen mode Exit fullscreen mode

Initialize a New Project

Create a directory name deployment under project folder and initialize a set of TypeScript templates using cdktf init.

$ mkdir -p project/deployment
$ cd project/deployment
$ cdktf init --template=typescript
Enter fullscreen mode Exit fullscreen mode

Enter details regarding the project including Terraform Cloud for storing the project state. You can use the --local option to continue without using Terraform Cloud for state management.

We will now setup the project. Please enter the details for your project.
If you want to exit, press ^C.

Project Name: (default: 'deployment')
Project Description: (default: 'A simple getting started project for cdktf.')
Enter fullscreen mode Exit fullscreen mode

We will be using a ReactJS project to write our website code for the example. You can use any framework and any programming language you want. Now, let's run below command to create ReactJS project.

$ npm i -g create-react-app
$ create-react-app web
Enter fullscreen mode Exit fullscreen mode

After setting up the React project, run below command to build the React website. Below command will create a build folder, which contains all the files like HTML, CSS, JS, images etc which are required for the website under web folder.

$ cd web
$ npm run build
Enter fullscreen mode Exit fullscreen mode

Now, if we run tree command on terminal or open the project folder in code editor, we will see our folder structure as below.

$ tree
├── deployment
│   ├── cdktf.json
│   ├── cdktf.out
│   ├── help
│   ├── main.d.ts
│   ├── main.js
│   ├── main.ts
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── terraform.tfstate
│   └── tsconfig.json
└── web
    ├── README.md
    ├── build
    ├── node_modules
    ├── package.json
    ├── public
    ├── src
    └── yarn.lock
Enter fullscreen mode Exit fullscreen mode

Open main.ts file from deployment folder. We will be writing in this file as per our requirements and cdktf will read this file and setup Terraform file based on this.

Let's import AWS Provider class from .gen folder. Next, we need to specify which region we will using for deployment process.

// import required classes from generated folder
import { AwsProvider, S3Bucket, S3BucketObject } from './.gen/providers/aws';

// assign AWS region
new AwsProvider(this, 'aws', {
  region: 'us-west-1'
});
Enter fullscreen mode Exit fullscreen mode

After setting up the region, we will be creating a S3 bucket with policies.

  1. Use public-read for Access Control.
  2. Enable website static website hosting feature. This is same as Use this bucket to host a website from AWS Console.
  3. Update policy to make the objects in your bucket publicly readable.
// Define AWS S3 bucket name
const BUCKET_NAME = '<YOUR-WEBSITE-BUCKET-NAME>';

// Create bucket with public access
const bucket = new S3Bucket(this, 'aws_s3_bucket', {
  acl: 'public-read',
  website: [{
    indexDocument: 'index.html',
    errorDocument: 'index.html',
  }],
  tags: {
    'Terraform': "true",
    "Environment": "dev"
  },
  bucket: BUCKET_NAME,
  policy: `{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "PublicReadGetObject",
        "Effect": "Allow",
        "Principal": "*",
        "Action": [
          "s3:GetObject"
        ],
        "Resource": [
          "arn:aws:s3:::${BUCKET_NAME}/*"
        ]
      }
    ]
  }`,
});
Enter fullscreen mode Exit fullscreen mode

After setting up the Bucket configuration, it's time to write code, which will upload the files to S3.

  1. Read all the files from the build folder
  2. Create a S3 bucket object for each file
    1. dependsOn: this step will wait till the bucket is not created. This is very import parameter.
    2. key: defines our files and folder structure on S3.
    3. source: is the actual file we want to upload to S3.
    4. etag: This is very important and tricky, reason is Terraform only creates or deletes the files. If there is already a file uploaded on S3 with a name and we are changing the content of the file then it won't replace the file. With this parameter, it will replace existing files as well because their etags are changed.
    5. contentType: This is also very important because this parameter will instruct the browser to open the file as a HTML, image, js, css file. If we don't define this, then when we open our S3 bucket public URL in the browser, the browser will download the index.html file instead of opening it because without content-type, the browser has no idea what to do with this file.
// import necessary packages
import * as path from 'path';
import * as glob from 'glob';
import * as mime from 'mime-types';

// Get all the files from build folder, skip directories
const files = glob.sync('../web/build/**/*', { absolute: false, nodir: true });

// Create bucket object for each file
for (const file of files) {
  new S3BucketObject(this, `aws_s3_bucket_object_${path.basename(file)}`, {
    dependsOn: [bucket],            // Wait untill the bucket is not created
    key: file.replace(`../web/build/`, ''),       // Using relative path for folder structure on S3
    bucket: BUCKET_NAME,
    source: path.resolve(file),          // Using absolute path to upload
    etag: `${Date.now()}`,
    contentType: mime.contentType(path.extname(file)) || undefined       // Set the content-type for each object
  });
}
Enter fullscreen mode Exit fullscreen mode

Last step is to output the bucket public URL. So after successful execution, we will get the public URL on our terminal. We just need to copy and paste it on browser and we will be able to view our website.

// import required class to print the output
import { TerraformOutput } from 'cdktf';

// Output the bucket url to access the website
new TerraformOutput(this, 'website_endpoint', {
  value: `http://${bucket.websiteEndpoint}`
});
Enter fullscreen mode Exit fullscreen mode

So now, out script is ready, it's time to Synthesize TypeScript to Terraform Configuration.

Synthesize TypeScript to Terraform Configuration

Let's synthesize TypeScript to Terraform configuration by running cdktf synth. The command generates Terraform JSON configuration files in the cdktf.out directory.

$ cd deployment
$ cdktf synth
Generated Terraform code in the output directory: cdktf.out

$ tree cdktf.out
cdktf.out
└── cdk.tf.json
Enter fullscreen mode Exit fullscreen mode

Inspect the generated Terraform JSON file by examining cdktf.out/cdk.tf.json. It includes the Terraform configuration for the S3 and S3 bucket objects which looks like below.

"resource": {
    "aws_s3_bucket": {
      "typescriptaws_awss3bucket_D835B1D8": {
        "acl": "public-read",
        "bucket": "thakkaryash94-cdk-dev",
        "policy": "{\n        \"Version\": \"2012-10-17\",\n        \"Statement\": [\n          {\n            \"Sid\": \"PublicReadGetObject\",\n            \"Effect\": \"Allow\",\n            \"Principal\": \"*\",\n            \"Action\": [\n              \"s3:GetObject\"\n            ],\n            \"Resource\": [\n              \"arn:aws:s3:::thakkaryash94-cdk-dev/*\"\n            ]\n          }\n        ]\n      }",
        "tags": {
          "Terraform": "true",
          "Environment": "dev"
        },
        "website": [
          {
            "index_document": "index.html",
            "error_document": "index.html"
          }
        ],
        "//": {
          "metadata": {
            "path": "typescript-aws/aws_s3_bucket",
            "uniqueId": "typescriptaws_awss3bucket_D835B1D8",
            "stackTrace": [
              "new TerraformElement (/Users/yash/github_workspace/typescript-aws/deployment/node_modules/cdktf/lib/terraform-element.js:10:19)",
              "new TerraformResource (/Users/yash/github_workspace/typescript-aws/deployment/node_modules/cdktf/lib/terraform-resource.js:9:9)",
              "new S3Bucket (/Users/yash/github_workspace/typescript-aws/deployment/.gen/providers/aws/s3-bucket.js:13:9)",
              "new MyStack (/Users/yash/github_workspace/typescript-aws/deployment/main.js:18:24)",
              "Object.<anonymous> (/Users/yash/github_workspace/typescript-aws/deployment/main.js:66:1)",
              "Module._compile (internal/modules/cjs/loader.js:1185:30)",
              "Object.Module._extensions..js (internal/modules/cjs/loader.js:1205:10)",
              "Module.load (internal/modules/cjs/loader.js:1034:32)",
              "Function.Module._load (internal/modules/cjs/loader.js:923:14)",
              "Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)",
              "internal/main/run_main_module.js:17:47"
            ]
          }
        }
      }
    },
    "aws_s3_bucket_object": {
      "typescriptaws_awss3bucketobjectindexhtml_E7345193": {
        "bucket": "thakkaryash94-cdk-dev",
        "content_type": "text/html; charset=utf-8",
        "etag": "1596349930423",
        "key": "index.html",
        "source": "/Users/yash/github_workspace/typescript-aws/web/build/index.html",
        "depends_on": [
          "aws_s3_bucket.typescriptaws_awss3bucket_D835B1D8"
        ],
        "//": {
          "metadata": {
            "path": "typescript-aws/aws_s3_bucket_object_index.html",
            "uniqueId": "typescriptaws_awss3bucketobjectindexhtml_E7345193",
            "stackTrace": [
              "new TerraformElement (/Users/yash/github_workspace/typescript-aws/deployment/node_modules/cdktf/lib/terraform-element.js:10:19)",
              "new TerraformResource (/Users/yash/github_workspace/typescript-aws/deployment/node_modules/cdktf/lib/terraform-resource.js:9:9)",
              "new S3BucketObject (/Users/yash/github_workspace/typescript-aws/deployment/.gen/providers/aws/s3-bucket-object.js:13:9)",
              "new MyStack (/Users/yash/github_workspace/typescript-aws/deployment/main.js:50:13)",
              "Object.<anonymous> (/Users/yash/github_workspace/typescript-aws/deployment/main.js:66:1)",
              "Module._compile (internal/modules/cjs/loader.js:1185:30)",
              "Object.Module._extensions..js (internal/modules/cjs/loader.js:1205:10)",
              "Module.load (internal/modules/cjs/loader.js:1034:32)",
              "Function.Module._load (internal/modules/cjs/loader.js:923:14)",
              "Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)",
              "internal/main/run_main_module.js:17:47"
            ]
          }
        }
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

We can also print Terraform JSON configuration in their terminal using cdktf synth --json command.

After synthesis, we can use the Terraform workflow of initializing, planning, and applying changes within the cdktf.out working directory or use the CDK for Terraform CLI to run cdktf deploy.

Terraform workflow is as follows:

$ cd cdktf.out
$ terraform init
Terraform has been successfully initialized!
$ terraform plan
# omitted for clarity
$ terraform apply
aws_s3_bucket.typescriptaws_awss3bucket_D835B1D8: Creating...
aws_s3_bucket.typescriptaws_awss3bucket_D835B1D8: Creation complete after 25s [id=thakkaryash94-cdk-dev]
aws_s3_bucket_object.typescriptaws_awss3bucketobjectindexhtml_E7345193: Creating...
# omitted for clarity
Apply complete! Resources: 20 added, 0 changed, 0 destroyed.

Outputs:

typescriptaws_websiteendpoint_D74DC454 = http://thakkaryash94-cdk-dev.s3-website-us-west-1.amazonaws.com
# destroy resources
$ terraform destroy
Plan: 0 to add, 0 to change, 20 to destroy.
Destroy complete! Resources: 20 destroyed.
Enter fullscreen mode Exit fullscreen mode

This is how we can deploy our ReactJS website to AWS S3 using AWS CDK for Terraform.Here is the sample repo for the reference.

GitHub logo thakkaryash94 / terraform-cdk-react-example

Host react website using terraform CDK on AWS S3

Host Static website using AWS CDK for Terraform

This repo contains the code for DEV.to blog

Links:

Discussion (2)

pic
Editor guide
Collapse
skorfmann profile image
Sebastian Korfmann

That's pretty cool, thanks for writing this!

In April, AWS open-sourced Cloud Development Kit (CDK), IT is an open-source software development framework. The AWS Cloud Development Kit (AWS CDK) is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation.

To clarify a bit: The AWS-CDK was released and open sourced around May 2018. AWS extracted the underlying programming model - Constructs - around March this year. That's what the Terraform CDK is using as well, plus jsii for the polyglot libraries.

Collapse
thakkaryash94 profile image
Yash Thakkar Author • Edited

Thank you for the correction. I have updated the blog. Do you have any more suggestions?