Build S3 Static Website and CloudFront Using Terraform and Gitlab

Theara Seng
6 min readNov 27, 2022

In this article, I’m guiding you through how we could set up an S3 static website and CloudFront using Terraform and Gitlab.

We focus on how to implement IaC (Terraform) and use Gitlab CI as our Continuous Integration tool.

What is Infrastructure as Code (IaC)

Infrastructure as Code (IaC) manages and provides an infrastructure through code instead of through manual processes.

With IaC, configuration files containing your infrastructure specifications are created, making it easier to edit and distribute configurations. Terraform is one of the Infrastructure as Code (IaC) tools developed by HashiCorp.

Terraform is one way to build and manage the infrastructure in the cloud platform such as Azure, AWS, GCP, etc.

If it’s your first time hearing about IaC or Infrastructure as Code, we can refer to this link to get you comfortable.

Terraform file structure

All the Terraform files locate within the terraform folder.

Please start with the first file, backend.tf, which defines where Terraform stores its state data files.

In our case, we will use Gitlab as our Terraform state storage.

The contents of the backend.tf

terraform {
backend "http" {
}
}

variables.tf and version.tf are where input variables and Terraform provider constraint are defined.

The contents of the variables.tf

variable "region" {
type = string
default = "ap-southeast-1"
}

variable "s3_name" {
type = string
}

The contents of the version.tf

terraform {
required_providers {
aws = {
version = "~> 4.34.0"
}
}
}

Note: The region is set to ap-southeast-1 (Singapore) as it’s my closest region which varies from your location. You can find a list of AWS regions here. : https://howto.lintel.in/list-of-aws-regions-and-availability-zones

s3.tf is where S3 and its ACL and bucket policy are defined.

The contents of the s3.tf

resource "aws_s3_bucket" "this" {
bucket = var.s3_name
}

resource "aws_s3_bucket_acl" "this" {
bucket = aws_s3_bucket.this.id
acl = "private"
}

resource "aws_s3_bucket_website_configuration" "this" {
bucket = aws_s3_bucket.this.bucket

index_document {
suffix = "index.html"
}

error_document {
key = "index.html"
}

}

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

policy = jsonencode({
Version = "2012-10-17"
Id = "AllowGetObjects"
Statement = [
{
Sid = "AllowPublic"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.this.arn}/**"
}
]
})
}

cloudfront.tf is where AWS CloudFront and its default caching behaviour are defined. The important thing is that it uses S3, defined earlier as its default origin.

The contents of the cloudfront.tf

locals {
s3_origin_id = "${var.s3_name}-origin"
s3_domain_name = "${var.s3_name}.s3-website-${var.region}.amazonaws.com"
}

resource "aws_cloudfront_distribution" "this" {

enabled = true

origin {
origin_id = local.s3_origin_id
domain_name = local.s3_domain_name
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1"]
}
}

default_cache_behavior {

target_origin_id = local.s3_origin_id
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]

forwarded_values {
query_string = true

cookies {
forward = "all"
}
}

viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 0
max_ttl = 0
}

restrictions {
geo_restriction {
restriction_type = "none"
}
}

viewer_certificate {
cloudfront_default_certificate = true
}

price_class = "PriceClass_200"

}

Gitlab CI/CD

Now we got Terraform files in place. Next, to provision the resources, Terraform will need CI/CD tools to do this particular job.

Gitlab CI is a robust CI/CD system with many built-in templates that we use to integrate with Terraform. The important life cycle of IaC (Infrastructure as Code) are:

  • Initialize (Validate)
  • Plan (Build)
  • Apply (Deploy)
  • Destroy

Gitlab has come with built-in templates for such IaC life cycle events. And the best fit template is Terraform/Base.gitlab-ci.yml that we’re going to use.

A regular Gitlab YAML file would come with the list of templates, stages, and some user-defined variables

include:
- template: Terraform/Base.gitlab-ci.yml

stages:
- validate
- build
- deploy
- cleanup

variables:
TF_STATE_NAME: dev-tf-state
TF_CACHE_KEY: dev-tf-cache
TF_ROOT: terraform # the terraform files are in this subdirectory. set TF_ROOT accordingly if yours are different

The rest of the Gitlab jobs are:

validate job

validate: # validate stage
extends: .terraform:validate

build job

build:
extends: .terraform:build
script:
- cd "${TF_ROOT}"
- gitlab-terraform plan
-var "s3_name=${S3_NAME}"
- gitlab-terraform plan-json

deploy job

deploy:
extends: .terraform:deploy
environment:
name: $TF_STATE_NAME
on_stop: destroy
dependencies:
- build
rules:
- when: always

cleanup job

destroy:
extends: .terraform:destroy
environment:
name: $TF_STATE_NAME
action: stop
script:
- cd "${TF_ROOT}"
- gitlab-terraform destroy
-var "s3_name=${S3_NAME}"

Gitlab CI/CD variables

For CI to authenticate and authorize the provisioning of cloud resources, we need AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

Detail on how to get your AWS access key ID and secret access key https://docs.aws.amazon.com/powershell/latest/userguide/pstools-appendix-sign-up.html

Hence CI variables needed for this particular Terraform are: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and S3_NAME (since Amazon bucket names must be unique across all AWS accounts in all the AWS Regions within a partition)

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • S3_NAME

Input all required variables in Gitlab

Gitlab Pipeline UI

The most exciting part is to see what is accomplished in the Gitlab Pipeline portal itself.

If everything works properly, we see the stages of the pipeline run successfully.

AWS Console

Amazon S3

The Amazon S3 Console will have a newly provisioned bucket listed here.

Create a hello-world index page

Let’s create a simple index.html page that will show a greeting message.

index.html

<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
<style>
h1 {
text-align: center;
}
p {
text-align: center;
}
</style>
</head>
<body>
<h1>Hello, greeting from the Cloud...</h1>
<p>This is your very first static page</p>
</body>
</html>

Create the index.html with the above HTML code, then upload it to the Amazon S3 bucket just created. Go into the bucket and drag the index.html file into the console.

Then click the Upload button.

Amazon CloudFront

The Amazon S3 Console will have an AWS CloudFront just created.

S3 Static Website

Congratulations. You made it. :) By visiting CloudFront URL, we can view our static website made by the sample index.html.

You can find the complete source code on the public Gitlab Repo.

Peace!

--

--