Post

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.

This post is licensed under CC BY 4.0 by the author.