Hosting a Jekyll Site in S3 Part 1
I am a huge fan of terraform. I use it quite a lot at my day job as an Infrastructure Engineer
. I’ve also wanted to learn more about using Gitlab’s CI/CD processes. Hosting a blog in S3 and controlling deployment via Gitlab CI seems like the perfect combination.
This tutorial utilizes a locally hosted gitlab server and gitlab runners in my homelab.
Building AWS infrastructure to host a static site in S3
Delegating a subdomain
I host my primary domain, schenk.tech
, in Cloudflare. This required a record in my root domain for the blog to be delegated to AWS Route53.
I used Terraform to first create the hosted_zone:
1
2
3
resource "aws_route53_zone" "blog" {
name = "blog.schenk.tech"
}
Once the hosted zone was created via terraform apply
, I can grab thename server values and add the delegated NS records to my DNS records in Cloudflare for blog.schenk.tech
.
Setup blog resource in AWS
Setup the S3 bucket for static website hosting
The blog will be hosted directly out of S3. Cloudfront is used to facilitate HTTPS connections with the ACM cert created later.
S3 Bucket
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
data "aws_canonical_user_id" "current" {}
resource "aws_s3_bucket" "blog" {
bucket = var.s3_bucket
tags = {
Name = "Homelab blog"
deployed_by = "terraform"
project = "aws-terraform"
}
}
resource "aws_s3_bucket_website_configuration" "blog_website" {
bucket = aws_s3_bucket.blog.bucket
index_document {
suffix = "index.html"
}
error_document {
key = "index.html"
}
}
resource "aws_s3_bucket_ownership_controls" "blog" {
bucket = aws_s3_bucket.blog.id
rule {
object_ownership = "BucketOwnerPreferred"
}
}
resource "aws_s3_bucket_acl" "blog" {
bucket = aws_s3_bucket.blog.id
depends_on = [aws_s3_bucket_ownership_controls.blog]
acl = "private"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "blog_encryption" {
bucket = aws_s3_bucket.blog.bucket
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_versioning" "blog_versioning" {
bucket = aws_s3_bucket.blog.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "blog_lifecycle" {
bucket = aws_s3_bucket.blog.id
rule {
id = "non-current"
noncurrent_version_expiration {
noncurrent_days = 30
}
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "blog" {
bucket = aws_s3_bucket.blog.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "blog_policy" {
bucket = aws_s3_bucket.blog.id
policy = jsonencode({
Version = "2012-10-17"
Id = "AllowGetObjects"
Statement = [
{
Sid = "AllowPublic"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.blog.arn}/**"
}
]
})
}
Setup Route 53 DNS configuration to point blog.schenk.tech
to the newly created S3 bucket
Route 53 Setup
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
resource "aws_route53_zone" "blog" {
name = "blog.schenk.tech"
tags = {
Name = "blog hosted zone"
deployed_by = "terraform"
project = "aws-terraform"
}
}
resource "aws_route53_record" "cert" {
for_each = {
for dvo in aws_acm_certificate.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 = aws_route53_zone.blog.zone_id
}
resource "aws_route53_record" "blog" {
zone_id = "${aws_route53_zone.blog.zone_id}"
name = var.blog_domain_name
type = "A"
alias {
name = "${aws_cloudfront_distribution.blog.domain_name}"
zone_id = "${aws_cloudfront_distribution.blog.hosted_zone_id}"
evaluate_target_health = false
}
}
Setup Cloudfront
Cloudfront
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
locals {
s3_origin_id = "${var.s3_bucket}-origin"
}
resource "aws_cloudfront_distribution" "blog" {
enabled = true
origin {
origin_id = local.s3_origin_id
domain_name = aws_s3_bucket_website_configuration.blog_website.website_endpoint
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
aliases = ["${var.blog_domain_name}"]
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 {
acm_certificate_arn = aws_acm_certificate.cert.arn
minimum_protocol_version = "TLSv1.2_2021"
ssl_support_method = "sni-only"
}
price_class = "PriceClass_100"
}
ACM Certificate
ACM Certificate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
resource "aws_acm_certificate" "cert" {
domain_name = "blog.schenk.tech"
validation_method = "DNS"
tags = {
deployed_by = "terraform"
project = "aws-terraform"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_acm_certificate_validation" "blog-cert" {
certificate_arn = aws_acm_certificate.cert.arn
validation_record_fqdns = [for record in aws_route53_record.cert : record.fqdn]
}
Terraform Variables
TF Vars
1
2
3
4
5
6
7
8
9
variable s3_bucket {
type = string
default = "blog.schenk.tech"
}
variable blog_domain_name {
type = string
default = "blog.schenk.tech"
}
After all of the terraform code has been applied, the AWS infrastructure will be available to host the Jekyll site. Automating deployment of the Jekyll site via gitlab CI is coming in Part 2.