hugo't to know how this blog is setup!
This blog has been powered by a few engines since its beginning (17 years ago!), it was first powered by a PHP engine called Dotclear.
Nobody likes operating a PHP website but at that time, a French ISP, Free, was offering free PHP hosting so I did not had to care about its security or anything.
Of course, I changed my mind when I had to move to a self-hosted solution, this quickly led me to a static website generated by Octopress (RIP) then Jekyll. I won’t go into all the pros and cons of static websites, Dane already wrote about it on his blog: Deploying a Static Website via Azure, read it!
Paradoxically, I agree with Tobias’ blog post that static website generator do not scale, mainly because maintaining the build and deploy stack is always a pain in the long term and source of discrepancies.
But this time is different, I think I can mitigate this risk by having strong CI/CD processes in place: if my work is limited to ① fill a Markdown file, ② git commit and then ③ git push, I think I can commit to it (time will tell!).
Of course, let’s be honest, it was also an excuse to invest time in terraform and AWS.
The big picture
So now, I am using:
- hugo engine to transform the Markdown into a website
- AWS for the hosting (Cloudfront, S3, HTTPs certificate, DNS)
- A private repository on Github.com, thank you Nat Friedman!
- Github Actions for the CI/CD part
- Terraform to manage the infrastructure
Github Actions
Github Actions is really awesome, I have never seen a CI/CD system that fast! Its configuration is easy, well documented and very well integrated. I love it.
When there is a push on the master branch, it builds the HTML pages and uploads them to AWS S3:
name: Build and deploy justanothergeek.chdir.org | |
on: | |
push: | |
branches: master | |
jobs: | |
build: | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v1 | |
- name: hugo | |
uses: klakegg/actions-hugo@1.0.0 | |
- name: S3 Sync | |
uses: jakejarvis/s3-sync-action@v0.5.0 | |
with: | |
args: --follow-symlinks --delete | |
env: | |
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
AWS_REGION: 'us-west-1' | |
SOURCE_DIR: 'public' |
It takes approximately 50 seconds between the git push
and its rendering on the website.
Terraform
Initially, I was not using terraform, doing click click everywhere in the AWS console. But when a friend reported an issue, I became mad debugging CloudFront, S3 Bucket Policy, IAM, and Lambda@Edge. I toggled on and off each feature while investigating. When I understood that there was no issue in fact (I shared a bad link in the first place), I already had tweaked so much settings that I was no longer confident that I had not break something else. So it was time to use terraform to have something reproducible and clean.
My configuration is va
provider "aws" { | |
region = "eu-central-1" | |
} | |
provider "aws" { | |
alias = "us_east_1" | |
region = "us-east-1" | |
} | |
locals { | |
my_domain_name = "example.com" | |
domain_name = "blog.example.com" | |
blog_dnsnames = ["blog.example.com"] | |
s3_origin_id = "something random" | |
s3_bucket_logs = "private-blog-logs" | |
s3_bucket = "public-blog-htdocs" | |
} | |
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" { | |
} | |
resource "aws_cloudfront_distribution" "s3_distribution" { | |
price_class = "PriceClass_100" | |
origin { | |
domain_name = aws_s3_bucket.blog_htdocs.bucket_regional_domain_name | |
origin_id = local.s3_origin_id | |
s3_origin_config { | |
origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path | |
} | |
} | |
enabled = true | |
is_ipv6_enabled = true | |
default_root_object = "index.html" | |
logging_config { | |
include_cookies = false | |
bucket = aws_s3_bucket.log_bucket.bucket_domain_name | |
prefix = "cloudfront/" | |
} | |
default_cache_behavior { | |
allowed_methods = ["GET", "HEAD"] | |
cached_methods = ["GET", "HEAD"] | |
target_origin_id = local.s3_origin_id | |
forwarded_values { | |
query_string = false | |
cookies { | |
forward = "none" | |
} | |
} | |
viewer_protocol_policy = "redirect-to-https" | |
min_ttl = 0 | |
max_ttl = 0 | |
default_ttl = 0 | |
} | |
ordered_cache_behavior { | |
allowed_methods = ["HEAD", "GET"] | |
cached_methods = ["HEAD", "GET"] | |
path_pattern = "/*" | |
target_origin_id = local.s3_origin_id | |
viewer_protocol_policy = "redirect-to-https" | |
compress = true | |
// Lambda@Edge association | |
lambda_function_association { | |
event_type = "viewer-request" | |
lambda_arn = aws_lambda_function.redirect_lambda.qualified_arn | |
include_body = false | |
} | |
forwarded_values { | |
query_string = false | |
cookies { | |
forward = "none" | |
} | |
} | |
} | |
restrictions { | |
geo_restriction { | |
restriction_type = "none" | |
} | |
} | |
viewer_certificate { | |
acm_certificate_arn = aws_acm_certificate.https_certificate.arn | |
ssl_support_method = "sni-only" | |
minimum_protocol_version = "TLSv1.1_2016" | |
} | |
wait_for_deployment = false | |
} | |
resource "aws_acm_certificate" "https_certificate" { | |
domain_name = local.domain_name | |
validation_method = "DNS" | |
lifecycle { | |
create_before_destroy = true | |
} | |
provider = aws.us_east_1 | |
} | |
resource "aws_s3_bucket" "blog_htdocs" { | |
bucket = local.s3_bucket | |
acl = "private" | |
logging { | |
target_bucket = aws_s3_bucket.log_bucket.id | |
target_prefix = "s3/" | |
} | |
} | |
resource "aws_s3_bucket" "log_bucket" { | |
bucket = local.s3_bucket_logs | |
acl = "log-delivery-write" | |
} | |
data "aws_iam_policy_document" "s3_policy" { | |
statement { | |
actions = ["s3:GetObject"] | |
resources = ["${aws_s3_bucket.blog_htdocs.arn}/*"] | |
principals { | |
type = "AWS" | |
identifiers = [aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn] | |
} | |
} | |
statement { | |
actions = ["s3:ListBucket"] | |
resources = [aws_s3_bucket.blog_htdocs.arn] | |
principals { | |
type = "AWS" | |
identifiers = [aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn] | |
} | |
} | |
} | |
resource "aws_s3_bucket_policy" "blog_htdocs_policy" { | |
bucket = aws_s3_bucket.blog_htdocs.id | |
policy = data.aws_iam_policy_document.s3_policy.json | |
} | |
resource "aws_iam_user" "deploy" { | |
name = "blog-deploy" | |
} | |
resource "aws_iam_user_policy" "deploy_rw" { | |
name = "deploy_policy" | |
user = aws_iam_user.deploy.name | |
policy = <<EOF | |
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Action": [ | |
"s3:*" | |
], | |
"Effect": "Allow", | |
"Resource": [ | |
"${aws_s3_bucket.blog_htdocs.arn}/*", | |
"${aws_s3_bucket.blog_htdocs.arn}" | |
] | |
} | |
] | |
} | |
EOF | |
} | |
resource "aws_iam_role" "redirect_lambda" { | |
name = "redirectLeadingSlash" | |
assume_role_policy = <<EOF | |
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Action": "sts:AssumeRole", | |
"Principal": { | |
"Service": "lambda.amazonaws.com", | |
"Service": "edgelambda.amazonaws.com" | |
}, | |
"Effect": "Allow", | |
"Sid": "" | |
} | |
] | |
} | |
EOF | |
} | |
resource "aws_lambda_function" "redirect_lambda" { | |
function_name = "lambda_function_name" | |
role = aws_iam_role.redirect_lambda.arn | |
handler = "lambda-redirectLeadingSlash.handler" | |
runtime = "nodejs10.x" | |
filename = "lambda-redirectLeadingSlash.zip" | |
source_code_hash = filebase64sha256("lambda-redirectLeadingSlash.zip") | |
publish = true | |
provider = aws.us_east_1 | |
} | |
resource "aws_lambda_permission" "allow_cloudfront" { | |
statement_id = "AllowExecutionFromCloudFront" | |
action = "lambda:GetFunction" | |
function_name = aws_lambda_function.redirect_lambda.function_name | |
principal = "edgelambda.amazonaws.com" | |
provider = aws.us_east_1 | |
} | |
data "aws_route53_zone" "myzone" { | |
name = local.my_domain_name | |
} | |
resource "aws_route53_record" "blog-a" { | |
zone_id = data.aws_route53_zone.myzone.zone_id | |
name = local.domain_name | |
type = "A" | |
alias { | |
name = aws_cloudfront_distribution.s3_distribution.domain_name | |
zone_id = aws_cloudfront_distribution.s3_distribution.hosted_zone_id | |
evaluate_target_health = false | |
} | |
} | |
resource "aws_route53_record" "blog-aaaa" { | |
zone_id = data.aws_route53_zone.myzone.zone_id | |
name = local.domain_name | |
type = "AAAA" | |
alias { | |
name = aws_cloudfront_distribution.s3_distribution.domain_name | |
zone_id = aws_cloudfront_distribution.s3_distribution.hosted_zone_id | |
evaluate_target_health = false | |
} | |
} |
I am very happy with this setup: I don’t feel any “friction” when I want to post something and it gives me confidence in the future: I am not worried about “what if $something happens and I have to do it all over again”.
Try it!