Static websites are a great way to get your organization online quickly and cost-effectively.

With a static website, you can create a simple web presence without needing to invest in a complex content management system or hosting. You can also use it to create portfolio websites and even small blogs. Additionally, static websites are often faster and more secure than traditional dynamic websites.

Terraform is a remarkably versatile infrastructure-as-code tool for provisioning infrastructure. However, you can also use it for creating your static website quickly and efficiently. In this post, I will take you through the step-by-step approach of using Terraform to create a static S3 website.

1 – Setting up the Terraform Project

First, you need to create a directory to keep your Terraform configuration files.

$ mkdir terraform-custom-module-s3-demo
$ cd terraform-custom-module-s3-demo

Within the root directory of the project, create a folder named modules. This will contain the custom module that will contain the code for provisioning the AWS S3 bucket.

$ mkdir modules
$ cd modules
$ mkdir aws-s3-bucket-static-website

When you create a custom Terraform module, it is considered a good practice to follow a standard structure. Here’s a typical file structure for a custom module:

.
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf

None of these files is required or has any special meaning to Terraform. You can create a module with a single .tf file or use any other file structure you like. However, it is a good practice to follow the convention for documentation purposes.

Also, each of these files has a special purpose:

  • LICENSE contains the license under which your module will be distributed. This file lets people using it know the terms under which it has been made available. Terraform itself does not use this file.
  • README.md contains the necessary documentation about how to use your module. Terraform does not use this file, but services like the Terraform registry and GitHub make use of this file. I recommend using it wherever possible.
  • main.tf contains the configuration of the Terraform module.
  • variables.tf contains the variable definitions for your module. When your module is used by others, the variables will be configured as arguments in the module block.
  • outputs.tf contains the output definitions for your module. Module outputs are made available to the configuration using the module and are used to pass information to other parts of your configuration

By the way, if you are interested in Cloud & Infrastructure, you’d love the Progressive Coder Newsletter where I explain technical concepts in a fun and interesting way.

Subscribe now for free.

2 – Creating the Terraform Static S3 Website Module

Let’s now create the main.tf file for the Terraform static S3 website module.

resource "aws_s3_bucket" "s3_demo_bucket" {
    bucket = var.bucket_name

    tags = var.tags
}

resource "aws_s3_bucket_website_configuration" "s3_demo_bucket" {
    bucket = aws_s3_bucket.s3_demo_bucket.id

    index_document {
        suffix = "index.html"
    }

    error_document {
        key = "error.html"
    }
}

resource "aws_s3_bucket_acl" "s3_demo_bucket" {
    bucket = aws_s3_bucket.s3_demo_bucket.id

    acl = "public-read"
}

resource "aws_s3_bucket_policy" "s3_demo_bucket" {
    bucket = aws_s3_bucket.s3_demo_bucket.id

    policy = jsonencode({
        Version = "2012-10-17"
        Statement = [
            {
                Sid = "PublicReadGetObject"
                Effect = "Allow"
                Principal = "*"
                Action = "s3:GetObject"
                Resource = [
                    aws_s3_bucket.s3_demo_bucket.arn,
                    "${aws_s3_bucket.s3_bucket.arn}/*",
                ]
            },
        ]
    })
}

To put things simply, this configuration creates a public AWS S3 bucket hosting a website with an index page and an error page.

In the process, it creates a bunch of AWS resources:

  • The aws_s3_bucket resource block provisions the S3 bucket based on the name and tags provided to the module.
  • Next, the aws_s3_bucket_website_configuration resource configures the bucket and defines the index_document and error_document
  • Next, you define the aws_s3_bucket_acl resource block to set the access control permissions. Since this is a static website, you need to set the acl value to public-read.
  • Lastly, you also declare the aws_s3_bucket_policy to allow incoming requests.

Notice that there is no provider block in this configuration.

When Terraform processes a module block, it inherits the provider from the enclosing configuration. Therefore, it’s not advisable to include provider blocks in modules.

The Variable Block

You also need to declare the variables for your Terraform custom module. Though, you can do it in the main.tf file, it is generally better to create a separate file named variables.tf.

Here are the contents of the file:

variable "bucket_name" {
  description = "Name of the S3 bucket. Must be unique."
  type        = string
  default     = "saurabh-test-bucket-2023-01-07"
}

variable "tags" {
  description = "Tags to set on the bucket."
  type        = map(string)
  default     = {}
}

You have two variables bucket_name and tags with default values.

When creating a module, ask yourself which resource arguments you want to expose to the module’s end users as input variables.

For example, you might decide to expose the index and error documents to end users of the module so that they can configure it. However, you may not choose to declare a variable to set the ACL because you must set your bucket’s ACL to public-read to host a static S3 website.

Ultimately, it is a matter of deciding the level of customization that you want to provide to the consumer of the module.

Variables declared in modules that aren’t given a default value are considered required and therefore, you must set them whenever you use the module.

The Output Block

While creating a custom Terraform module, you should also consider which values to add as outputs.

This is because outputs are the only supported way for users to get information about resources configured by the module.

For our Terraform static S3 website module, below is the outputs.tf file.

output "arn" {
  description = "ARN of the bucket"
  value       = aws_s3_bucket.s3_demo_bucket.arn
}

output "name" {
  description = "Name or id of the bucket"
  value       = aws_s3_bucket.s3_demo_bucket.id
}

output "domain" {
  description = "Domain name of the bucket"
  value       = aws_s3_bucket_website_configuration.s3_demo_bucket.website_domain
}

Just like variables, outputs in Terraform custom modules perform the same function as they do in the root module but are accessed in a different way.

You can access a module’s output from the configuration that calls the module through the following syntax: module.<MODULE NAME>.<OUTPUT NAME>.

Also, note that module outputs are read-only attributes.

3 – Using the S3 Custom Module in the Main Module

Now that you have defined the Terraform static S3 website module, you need to use the custom module in the project’s main module.

Below is the main.tf file of the project’s root directory.

provider "aws" {
    region = "us-west-2"
    profile = "terraform-user"
}

module "website_s3_bucket" {
  source = "./modules/aws-s3-bucket-static-website"

  bucket_name = "saurabh-test-bucket-2023-01-07"

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

If you notice, the project-level main.tf file contains the provider block where you need to configure the AWS region and IAM user that you want to use.

In the module block, you need to provide the location of the static S3 website module’s source code. You can also pass the variables such as bucket_name and tags.

Also, here are the contents of the root directory’s output file (outputs.tf).

output "website_bucket_arn" {
  description = "ARN of the bucket"
  value       = module.website_s3_bucket.arn
}

output "website_bucket_name" {
  description = "Name (id) of the bucket"
  value       = module.website_s3_bucket.name
}

output "website_bucket_domain" {
  description = "Domain name of the bucket"
  value       = module.website_s3_bucket.domain
}

Installing the Custom Module

To use the module, you need to install it in the configuration’s root directory.

There are a couple of ways you can do so:

  • Use terraform init which will update the backends and also update any modules
  • Use terraform get to update and install modules

Since our project is new, we can directly use terraform init.

When installing a local module, Terraform will directly refer to the source directory. Due to this, Terraform will automatically notice changes to local modules without having to re-run terraform init or terraform get.

4 – Provisioning the Static S3 Website using Terraform

You can now apply the configuration using the terraform apply command. If everything worked fine, you should see the output as below:

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

website_bucket_arn = "arn:aws:s3:::saurabh-test-bucket-2023-01-07"
website_bucket_domain = "s3-website-us-west-2.amazonaws.com"
website_bucket_name = "saurabh-test-bucket-2023-01-07"

You can also check the details in the AWS Console. There should be a new S3 bucket.

terraform static s3 website bucket
Terraform Static S3 Website Bucket

However, at this point, the bucket is empty.

You need to copy the index.html and error.html to the S3 bucket using the below command:

$ aws s3 cp modules/aws-s3-bucket-static-website/www/ s3://$(terraform output -raw website_bucket_name)/ --recursive --profile terraform-user
upload: modules/aws-s3-bucket-static-website/www/error.html to s3://saurabh-test-bucket-2023-01-07/error.html
upload: modules/aws-s3-bucket-static-website/www/index.html to s3://saurabh-test-bucket-2023-01-07/index.html

Basically, this will copy the template files from the www folder to the S3 bucket. You need to place the HTML files in the www folder.

See the below example for the index.html file.

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>S3 Demo Website</title>
  </head>
  <body>
    <p>Welcome to the S3 Demo Website</p>
  </body>
</html>

The S3 bucket name is fetched from the output of the terraform output command.

With this, you can now access your static S3 website created with Terraform by accessing the URL http://saurabh-test-bucket-2023-01-07.s3-website-us-west-2.amazonaws.com/index.html.

5 – Destroying the Terraform Static S3 Website

In case you want to destroy the static S3 website created using Terraform, you need to first delete the HTML files from the bucket.

Run the below command:

$ aws s3 rm s3://$(terraform output -raw website_bucket_name)/ --recursive --profile terraform-user
delete: s3://saurabh-test-bucket-2023-01-07/index.html
delete: s3://saurabh-test-bucket-2023-01-07/error.html

Once the deletion is successful, run the terraform destroy command to delete the S3 bucket.

Conclusion

In this article, you learnt how to use Terraform modules to provision a static S3 website. To make the process more modular, we created a custom module and used that to provision the S3 bucket.

The code for this entire project is available on GitHub.

In case you are interested in learning more about Terraform, here are a few posts that might interest you:

If you have any comments or queries about this post, please write them in the comments section below.

Anyways, before we end this post, a quick reminder about the Progressive Code Newsletter where I explain Cloud, Infra & Backend concepts in a fun & interesting manner so that you never forget what you’ve learned.

I’m 100% sure you’d love it.

Subscribe now and see you over there.

Categories: BlogTerraform

Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *