Host Static website using AWS CDK for Terraform: Part 1
Tags: aws, terraform, s3, devops
Published: 2020-08-02 07:49:14

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.

```shell
$ npm i -g cdktf-cli
```

### Initialize a New Project

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

```shell
$ mkdir -p project/deployment
$ cd project/deployment
$ cdktf init --template=typescript
```

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.

```shell
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.')
```

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.

```shell
$ npm i -g create-react-app
$ create-react-app web
```

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.

```shell
$ cd web
$ npm run build
```

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

```shell
$ 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
```

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.

```typescript
// 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'
});
```

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.

```typescript
// 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}/*"
        ]
      }
    ]
  }`,
});
```

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.

```typescript
// 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
  });
}
```

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.

```typescript
// 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}`
});
```

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](https://www.terraform.io/docs/configuration/syntax-json.html) files in the `cdktf.out` directory.

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

$ tree cdktf.out
cdktf.out
└── cdk.tf.json
```

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.

```json
"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"
            ]
          }
        }
      }
    }
  }
```

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:

```shell
$ 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.
```

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 thakkaryash94/terraform-cdk-react-example %}

Links:

- [CDK for Terraform: Enabling Python & TypeScript Support](https://www.hashicorp.com/blog/cdk-for-terraform-enabling-python-and-typescript-support/)
- [Terraform CDK React Example GitHub](https://github.com/thakkaryash94/terraform-cdk-react-example)