Intro
Are you considering creating a blazing-fast, scalable, and cost-efficient website powered by Jekyll, and deploying it using the robust infrastructure as code (IaC) capabilities of Terraform? Look no further! In this guide, I’ll walk you through the steps I took to build and deploy my website, guydevops.com, leveraging the power of Terraform, Amazon S3, and CloudFront.
The Technology Stack
Jekyll : is a static site generator that allows you to build simple to complex websites without the need for a traditional server. Its simplicity and flexibility make it an excellent choice for blogs, portfolios, and personal websites.
Terraform: Terraform, an IaC tool by HashiCorp, enables you to define and provision infrastructure using a declarative configuration language. This means you can define your entire infrastructure in code, making it reproducible, version-controlled, and easily managed.
Amazon S3:: Amazon Simple Storage Service (S3) is a scalable object storage service. In this setup, S3 is used to host and serve the static content of the Jekyll website.
CloudFront: Amazon CloudFront is a content delivery network (CDN) that securely delivers data, videos, applications, and APIs to customers globally with low-latency and high transfer speeds. CloudFront is employed to cache and distribute the website content, ensuring rapid and reliable access for users around the world.
prerequisites
Create Jekyll blog
To start out we need to generate a static site to upload to S3. We are going to use the Jekyll chripty theme. This theme has a modern professional style and is easy to set up. The best part about this theme is you do not need to know almost anything on frontend web design because all you have to do is edit Markdown files to write your articles . I will go over beefily how I set up and generate the static objects and set up the development environment. I used this YouTube video by Techno Tim as inspiration for using Chripty. We aren’t going to be using the github pages feature so you can skip or ignore any part of that in the documentation.
- Login to your GitHub account and fork the Jekyll chripty theme or use the Chripy starter listed in their documentation.
-
Open a terminal of your choice
-
Run
git clone https://github.com/USERNAME/REPONAME.git
(you might need to sign into github locally) -
Run
gem install bundler jekyll
to get bundle in the directory of the repo -
Run
bundle
to install all of the dependencies -
To run the project locally run
bundle exec jekyll s
pull up your local host in a browser to see http://127.0.0.1:4000 - Now that we have the project set up you can edit the values in the
_config.yml
and the.mb
files in the_post
directory to create articles.
Setting up the AWS environment
This guide assumes you already have a domain purchased and a route53 zone set up. This can be done by following this AWS documentation.
- Login to the AWS console
- Navigate to the S3 service and create an S3 bucket for the Terraform state
- We will be using a remote state backend for terraform which requires a bucket to be created outside of terraform (Terraform documentation)
- The bucket should be private and requires no additional set up
- The bucket can be named what ever you like
-
Navigate to the IAM service and create an admin IAM user for your AWS CLI to have permissions
-
Run
aws configure
and input your access key, secret key and the AWS region you’re working in. (AWS documentation) - Run
aws sts get-caller-identity
to confirm your CLI user is properly set up
Setting up Terraform
All of the terraform listed can be found on my GitHub
- First we need to set up the
main.tf
with therequired_providers
and the S3 back end that we set up in step 3 of Setting up the AWS environment. This is just a base terraform file that configures the provider and the s3 remote backend.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Configure the Terraform block
terraform {
# Specify required providers and their versions
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
# Configure the backend for remote state storage in an S3 bucket
backend "s3" {
bucket = "guy-terraform-state" # Specify the name of the S3 bucket for storing Terraform state
key = "terraform" # Specify the key (path) within the bucket where the state file will be stored
region = "us-east-1" # Specify the AWS region for the S3 bucket
}
# Specify the minimum required Terraform version
required_version = ">= 1.2.0"
}
# Configure the AWS provider
provider "aws" {
region = "us-east-1" # Specify the default AWS region for resource provisioning
}
- Now lets create the S3 bucket with s3 website configured.
- The
aws_s3_bucket_website_configuration
resource is what enables the index.html to be auto loaded when you navigate through the site. - The
aws_s3_bucket
has to be set to public for the S3 website configuration to work however there is a way to lock the s3 bucket down via injecting a header in the Cloud Front distribution. Here is a StackOverflow thread about this situation. - The
secret_header
variable can be set via either a tfvar file or other ways of setting environment variables in terraform. Do not commit this secret header to source control if you want it to be actually secret. - The site deployment to s3 is done via a
null_resource
block. This automates the process of copying/synching the files genreated by
- The
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# Define a variable for a secret header (do not commit to source control)
variable "secret_header" {
type = string
default = "secret-header"
}
variable "site_path" {
type = string
default = "~/repos/guydevops.com"
}
# Create an S3 bucket for the website
resource "aws_s3_bucket" "guydevops-com_s3_bucket" {
bucket = "guydevops.com"
tags = {
Name = "guydevops"
Environment = "prod"
}
}
# Configure the S3 bucket as a website with an index document
resource "aws_s3_bucket_website_configuration" "guydevops-com_s3_bucket_website" {
bucket = aws_s3_bucket.guydevops-com_s3_bucket.id
index_document {
suffix = "index.html"
}
}
# Create an S3 bucket policy to allow public access to the site with a secret header
resource "aws_s3_bucket_policy" "allow_public_access_to_site" {
bucket = aws_s3_bucket.guydevops-com_s3_bucket.id
policy = data.aws_iam_policy_document.allow_public_access_to_site_policy.json
}
# Define an IAM policy document to allow public access with a specific referer header
data "aws_iam_policy_document" "allow_public_access_to_site_policy" {
statement {
principals {
type = "*"
identifiers = ["*"]
}
actions = ["s3:GetObject"]
resources = [
aws_s3_bucket.guydevops-com_s3_bucket.arn,
"${aws_s3_bucket.guydevops-com_s3_bucket.arn}/*",
]
condition {
test = "StringLike"
variable = "aws:Referer"
values = ["${var.secret_header}"]
}
}
}
resource "null_resource" "remove_and_upload_to_s3" {
provisioner "local-exec" {
command = "aws s3 sync ${var.site_path}/_site s3://${aws_s3_bucket.guydevops-com_s3_bucket.id}"
}
}
- Now lets create the ACM SSL certificate and use DNS validation. The
aws_route53_zone
can be imported via a terraform data lookup.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Define a data source to get information about an existing Route 53 hosted zone
data "aws_route53_zone" "guydevops_zone" {
name = "guydevops.com"
}
# Define an ACM (AWS Certificate Manager) certificate for guydevops.com
resource "aws_acm_certificate" "guydevops_cert" {
domain_name = "guydevops.com"
validation_method = "DNS" # Use DNS validation for the certificate
tags = {
Name = "guydevops.com"
}
}
# Define Route 53 records for certificate validation
resource "aws_route53_record" "guydevops_cert_record" {
for_each = {
for dvo in aws_acm_certificate.guydevops_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.guydevops_zone.zone_id
}
# Define ACM certificate validation
resource "aws_acm_certificate_validation" "guydevops_cert_validation" {
certificate_arn = aws_acm_certificate.guydevops_cert.arn
validation_record_fqdns = [for record in aws_route53_record.guydevops_cert_record : record.fqdn]
}
- Finally lets create the CloudFront Distrubution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# Define an AWS CloudFront distribution resource
resource "aws_cloudfront_distribution" "guydevops_cf_dis" {
# Define the origin settings for the CloudFront distribution
origin {
domain_name = aws_s3_bucket_website_configuration.guydevops-com_s3_bucket_website.website_endpoint
origin_id = "S3Origin"
# Configure custom origin settings for an S3 bucket
custom_origin_config {
http_port = 80
https_port = 443
origin_keepalive_timeout = 5
origin_protocol_policy = "http-only"
origin_read_timeout = 30
origin_ssl_protocols = ["TLSv1.2"]
}
# Add a custom header for the origin
custom_header {
name = "Referer"
value = var.secret_header
}
}
# Enable the CloudFront distribution
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
# Set aliases (alternate domain names) for the distribution
aliases = ["guydevops.com"]
# Configure the default cache behavior for the distribution
default_cache_behavior {
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
compress = true
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
# Configure an ordered cache behavior for the distribution
ordered_cache_behavior {
path_pattern = "/*"
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
compress = true
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
# Configure the SSL certificate and protocol settings for the distribution
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.guydevops_cert.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2018"
}
# Configure restrictions for the distribution based on geo-location
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["US", "CA", "GB", "DE"]
}
}
}
# Define an AWS Route 53 record resource for the domain
resource "aws_route53_record" "guydevops_record" {
name = "guydevops.com"
type = "A"
zone_id = data.aws_route53_zone.guydevops_zone.zone_id
# Configure an alias for the Route 53 record pointing to the CloudFront distribution
alias {
name = aws_cloudfront_distribution.guydevops_cf_dis.domain_name
zone_id = aws_cloudfront_distribution.guydevops_cf_dis.hosted_zone_id
evaluate_target_health = false
}
}
TEST TEST