AWS Networking with Terraform: Hosting a static website using S3
Tags: aws, awscommunitybuilder, terraform, upskilling
Published: 2024-02-26 06:22:15

In our previous post, we leveraged AWS VPC Gateway Endpoint to establish secure connections to S3 over the AWS Private Network. 

Continuing our journey into cloud, this installment will focus on harnessing the power of Amazon S3 for static website hosting. We will utilize Terraform to create modular components, streamlining the deployment process and ensuring scalability and maintainability.

You can use Amazon S3 to host a static website. On a static website, individual webpages include static content. They might also contain client-side scripts.

## Architecture Overview:
Before we embark on the implementation, let's take a moment to understand the architecture we'll be working with:

![S3 Static Website Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rf7y3czmyk6pjqdmnrrv.png)

## Step 1: Create an S3 Bucket:
We'll begin by creating an S3 bucket with a unique name and configuring its public access settings and make it as static website.
```terraform
####################################################
# S3 static website bucket
####################################################
resource "aws_s3_bucket" "my-static-website" {
  bucket = var.bucket_name
  tags = merge(local.common_tags, {
    Name = "${local.naming_prefix}-s3-bucket"
  })
}

####################################################
# S3 public access settings
####################################################
resource "aws_s3_bucket_public_access_block" "static_site_bucket_public_access" {
  bucket = aws_s3_bucket.my-static-website.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

####################################################
# S3 bucket static website configuration
####################################################
resource "aws_s3_bucket_website_configuration" "static_site_bucket_website_config" {
  bucket = aws_s3_bucket.my-static-website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "error.html"
  }
}
```

## Step 2: Implement Resource Policies: 
To control access to our bucket and its contents, we'll establish resource policies. This could involve defining ownership controls using bucket and object ACLs or crafting resource policies.

```terraform
####################################################
# S3 bucket policy
####################################################
resource "aws_s3_bucket_policy" "static_site_bucket_policy" {
  bucket = var.bucket_name

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

  depends_on = [aws_s3_bucket_public_access_block.static_site_bucket_public_access]
}

```

Instead of bucket resource policy, we can define bucket/object ACLs to define more granularity (code commented in github repo)
```terraform
####################################################
# S3 bucket ownership controls and ACL (recommended if you want object level control).
####################################################
resource "aws_s3_bucket_ownership_controls" "static_site_bucket_ownership_controls" {
  bucket = aws_s3_bucket.my-static-website.id
  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

resource "aws_s3_bucket_acl" "static_site_bucket_acl" {
  depends_on = [
    # aws_s3_bucket_ownership_controls.static_site_bucket_ownership_controls,
    aws_s3_bucket_public_access_block.static_site_bucket_public_access,
  ]

  bucket = aws_s3_bucket.my-static-website.id
  acl    = "public-read"
}
```


## Step 3: Upload Website Files: 
With our bucket configured, we'll upload the necessary files comprising our static website.
```terraform
####################################################
# Upload files to S3 Bucket 
####################################################
resource "aws_s3_object" "provision_source_files" {
  bucket = aws_s3_bucket.my-static-website.id

  # webfiles/ is the Directory contains files to be uploaded to S3
  for_each = fileset("webfiles/", "**/*.*")

  key          = each.value
  source       = "webfiles/${each.value}"
  content_type = each.value
  #acl          = "public-read" #use this only if you are using Bucket and Object ACLs, defaults to private
}
```

## Steps to Run Terraform
Follow these steps to execute the Terraform configuration:
```terraform
terraform init
terraform plan 
terraform apply -auto-approve
```

Upon successful completion, Terraform will provide relevant outputs.
```terraform
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Outputs:

static_site_endpoint = "http://my-s3-static-bucket-v1.s3-website-us-east-1.amazonaws.com"
```

## Testing the outcome

S3 Bucket with objects:

![S3 bucket with objects](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ektgino3osjjlqrcndqr.png)

Static website hosting enabled:

![Static Website Settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4wbgacm9ui9fycx93h4.png)

S3 Public Access Block:

![S3 public access block](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkfutmtaoldg1gpoo81n.png)

S3 Bucket Policy:

![S3 Bucket Policy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/10i9mh29vvp5i134hp01.png)

A running website:

![Website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e882h7dfionhdr06oapk.png)

## Cleanup
Remember to stop AWS components to avoid large bills.
```terraform
terraform destroy -auto-approve
```

## Notes:
1) In this tutorial, the website was accessed using S3 Bucket Endpoint which will have name of the bucket, we can hide it by creating a CNAME record in Route53 if you have a domain.
2) We can either use bucket policy (used in majority of cases), but we can use bucket ownership controls and bucket/object ACLs to define more granularity. (Code is commented in main.tf, all objects also need to use acl="public-read")

We've successfully set up a static website hosting solution using Amazon S3.

In our next tutorial, we'll explore the integration of Content Delivery Networks (CDNs) to further optimize website performance and enhance user experience.

## Resources:
Github Link: https://github.com/chinmayto/terraform-aws-s3-static-website
S3 Static Website: https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html