Introduction

As a DevOps Engineer, I believe in practicing what I preach. When I decided to start this blog, I wanted to use it as a real-world example of DevOps principles in action. This post documents how I built the very site you’re reading, using Hugo for static site generation and AWS for hosting.

Why This Tech Stack?

Hugo: The Developer’s Choice

  • Speed: Builds sites in milliseconds, not minutes
  • Simplicity: Write in Markdown, deploy as HTML
  • SEO-friendly: Optimized out of the box
  • Themes: Professional themes available
  • No database: Static = secure and fast

AWS: The DevOps Playground

  • Cost-effective: ~$3-5/month for a personal blog
  • Global performance: CloudFront CDN
  • Scalability: Handles traffic spikes automatically
  • Learning opportunity: Hands-on AWS experience
  • Infrastructure as Code: Everything versioned and reproducible

Architecture Overview

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   GitHub Repo   │───▶│  GitHub Actions  │───▶│   S3 Bucket     │
│   (Hugo Site)   │    │     (CI/CD)      │    │   (Static Host) │
└─────────────────┘    └──────────────────┘    └─────────────────┘
                                                         │
                                                         ▼
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  serloaiza.com  │◀───│    Route 53      │◀───│   CloudFront    │
│   (Your Site)   │    │     (DNS)        │    │     (CDN)       │
└─────────────────┘    └──────────────────┘    └─────────────────┘

Step 1: Hugo Setup

Installation

# macOS
brew install hugo

# Verify installation
hugo version

Create the Site

hugo new site serloaiza-blog
cd serloaiza-blog

Add a Theme

I chose PaperMod for its clean design and DevOps blog-friendly features:

git init
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod

Configuration

Here’s my hugo.toml with multilingual support:

baseURL = 'https://serloaiza.com/'
defaultContentLanguage = 'en'
theme = 'PaperMod'

[languages]
  [languages.en]
    languageCode = 'en-US'
    languageName = 'English'
    title = 'SerLoaiza - DevOps Engineer Blog'
    weight = 1
    
    [languages.en.params]
      author = "Sergio Loaiza"
      description = "Personal blog about DevOps, AWS, Terraform, Kubernetes and more"
      
      [languages.en.params.homeInfoParams]
        Title = "Hi! I'm Sergio Loaiza 👋"
        Content = "DevOps Engineer passionate about automation and best practices."

  [languages.es]
    languageCode = 'es-ES'
    languageName = 'Español'
    title = 'SerLoaiza - Blog de Ingeniero DevOps'
    weight = 2

[params]
  ShowReadingTime = true
  ShowShareButtons = true
  ShowPostNavLinks = true
  ShowBreadCrumbs = true
  ShowCodeCopyButtons = true

Step 2: Content Creation

Creating Posts

hugo new posts/my-first-post.md

Content Structure

content/
├── posts/
│   ├── _index.md
│   ├── what-devops-really-means.md
│   └── building-devops-blog-hugo-aws.md
├── about.md
└── es/              # Spanish content
    ├── posts/
    └── about.md

Local Development

hugo server -D

Visit http://localhost:1313 to see your site.

Step 3: AWS Infrastructure with Terraform

Now for the fun part—let’s build the AWS infrastructure using Terraform.

Project Structure

terraform/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

S3 Bucket for Hosting

resource "aws_s3_bucket" "blog" {
  bucket = var.domain_name
}

resource "aws_s3_bucket_website_configuration" "blog" {
  bucket = aws_s3_bucket.blog.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "404.html"
  }
}

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
}

CloudFront Distribution

resource "aws_cloudfront_distribution" "blog" {
  origin {
    domain_name = aws_s3_bucket_website_configuration.blog.website_endpoint
    origin_id   = "S3-${var.domain_name}"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  enabled             = true
  default_root_object = "index.html"
  aliases             = [var.domain_name, "www.${var.domain_name}"]

  default_cache_behavior {
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "S3-${var.domain_name}"
    compress               = true
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate_validation.blog.certificate_arn
    ssl_support_method  = "sni-only"
  }
}

SSL Certificate

resource "aws_acm_certificate" "blog" {
  provider          = aws.us_east_1
  domain_name       = var.domain_name
  subject_alternative_names = ["www.${var.domain_name}"]
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

Step 4: CI/CD with GitHub Actions

Workflow Configuration

.github/workflows/deploy.yml:

name: Deploy Hugo Site to AWS

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
      with:
        submodules: true
        fetch-depth: 0

    - name: Setup Hugo
      uses: peaceiris/actions-hugo@v2
      with:
        hugo-version: 'latest'
        extended: true

    - name: Build
      run: hugo --minify

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v2
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1

    - name: Deploy to S3
      run: |
        aws s3 sync ./public/ s3://${{ secrets.S3_BUCKET }} --delete

    - name: Invalidate CloudFront
      run: |
        aws cloudfront create-invalidation \
          --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
          --paths "/*"

Step 5: Domain Configuration

Route 53 Setup

resource "aws_route53_zone" "blog" {
  name = var.domain_name
}

resource "aws_route53_record" "blog" {
  zone_id = aws_route53_zone.blog.zone_id
  name    = var.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
  }
}

Cost Breakdown

Monthly AWS Costs:

  • S3 Storage: ~$0.50 (for a typical blog)
  • CloudFront: ~$1-3 (depends on traffic)
  • Route 53: $0.50 (hosted zone)
  • Data Transfer: ~$0.50-2

Total: ~$2.50-6/month 💰

Performance Benefits

Before (Traditional Hosting):

  • Load time: 2-4 seconds
  • Global availability: Limited
  • SSL: Extra cost
  • CDN: Not included

After (AWS + Hugo):

  • Load time: 200-500ms ⚡
  • Global availability: 216+ edge locations
  • SSL: Free with ACM
  • CDN: Included with CloudFront

Lessons Learned

What Worked Well:

  1. Hugo’s speed: Site builds in under 1 second
  2. AWS reliability: 99.99% uptime
  3. Cost efficiency: Cheaper than traditional hosting
  4. Developer experience: Git-based workflow

Challenges:

  1. Initial setup complexity: Terraform learning curve
  2. DNS propagation: Takes time for changes
  3. Cache invalidation: Need to clear CloudFront cache

Next Steps

  1. Add monitoring: CloudWatch dashboards
  2. Implement analytics: Google Analytics or AWS analytics
  3. SEO optimization: Structured data and meta tags
  4. Performance monitoring: Core Web Vitals tracking

Conclusion

Building this blog was a perfect example of DevOps principles in action:

  • Infrastructure as Code: Everything is versioned and reproducible
  • Automation: Push to Git = automatic deployment
  • Monitoring: CloudWatch alerts for issues
  • Cost optimization: Pay only for what you use
  • Scalability: Handles traffic spikes automatically

The best part? This entire setup took about 2 hours to implement and costs less than a fancy coffee per month.

Want to build something similar? The code for this blog is available on GitHub. Feel free to fork it and make it your own!


Questions about the setup? Reach out on LinkedIn or GitHub. I’m always happy to help fellow DevOps engineers!