Skip to main content

Deploying hugo on RHEL9 AWS instance

·736 words·4 mins
IT Linux Study-Note

Recently, I decided to move my tiny blog from Vultr to AWS for the sake of learning; because, really, learning in a vacuum of a lab won’t teach you the painful lessons that a sysadmin needs to etch into their memory. Just look at my last lesson when I lost all 15 years of media data playing with ZFS in production. I’m sure as hell I’ll remember forever not to test in production, and my developer in Bangladesh will hear no end of it whenever he works in the prod environment.

So, there’s nothing better than throwing in some small requirements for such a simple production environment as my own static HTML-generated website.

Another goal is to meticulously document each and every step to deploy the blog from scratch, which can then be automated with Ansible.

Provide an instance with Terraform
#

This is how my simple terraform main file looks like:

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

provider "aws" {
  region = "ap-XXXXX"
}

# Data source for existing Elastic IP
data "aws_eip" "<name>" {
  id = "eipalloc-XXXXXXX"  
  public_ip = "XX.XX.XXX.XXX"
}

# Provide t3.small RHEL instance
resource "aws_instance" "<name>" {
  ami = "ami-0d6857b844e855670" # RHEL9
  instance_type = "t3.small"
  key_name = "XXXXX"
  tags = {
    Name = "luu.is"
  }
}

# Connect elastic IP with instance
resource "aws_eip_association" "eip_assoc" {
  instance_id   = aws_instance.<name>.id
  allocation_id = data.aws_eip.<name>.id
}

It’s interesting to see how AWS break their management and control of instances and resources into such fine components such as elastic IP association, which has its own ID.

Configuration & installation
#

The following part follows “do this then that” format which comes with little explanation for the sake of keeping the flow clean.

  1. Update system

    • sudo dnf update
    • sudo reboot
  2. Install git:

  • sudo dnf install git
  1. Install wget:
  • sudo dnf install wget
  1. Download Go tar file:
  • wget https://go.dev/dl/go1.22.3.linux-amd64.tar.gz
  1. Extract Go tar file into /usr/local:
  • sudo tar -C /usr/local -xzf go1.22.3.linux-amd64.tar.gz
  1. Add /usr/local/go/bin to the PATH environment variable:
  • echo export PATH=$PATH:/usr/local/go/bin >> ~/.bash_profile
  1. Download Dart Sass:

wget https://github.com/sass/dart-sass/releases/download/1.77.0/dart-sass-1.77.0-linux-x64.tar.gz

  1. Extract to /usr/local:

sudo tar -C /usr/local -xzf dart-sass-1.77.0-linux-x64.tar.gz

  1. Add /usr/local/dart-sass to PATH env:

echo export PATH=$PATH:/usr/local/dart-sass >> ~/.bash_profile

  1. Install nginx:

sudo dnf install nginx -y

  1. Start nginx:

systemctl enable nginx --now

  1. Create hugo dir at /var/www/html:

sudo mkdir /var/www/html/hugo -p

  1. Go to that dir:

cd /var/www/html/hugo

  1. Download Hugo:

wget https://github.com/gohugoio/hugo/releases/download/v0.126.1/hugo_extended_0.126.1_linux-amd64.tar.gz

  1. Extract to dir:

sudo tar -xzf hugo_extended_0.126.1_linux-amd64.tar.gz

  1. Move the hugo binary to the /usr/bin:

sudo mv hugo /usr/bin/hugo

  1. Spin up new blog:

hugo new site <blog-name>

  1. Copy entire blog backup to new blog:

sudo rsync -avz -e "ssh -i <path-to-key>" --exclude=".*" <source> <remote-user>@<remote-host>:<path-to-new-blog-dir>

  1. Go to blog directory:

cd <blog-name>

  1. Publish & copy all public content from blog dir to /var/www/html:

hugo && sudo rsync -avz --delete <path-to-blog-dir>/public/* /var/www/html/.

  1. Install Certbot:

sudo dnf install certbot

  1. Install Certbot nginx plugin:

sudo dnf install python3-certbot-nginx

  1. Generate SSL certificate:

sudo certbot certonly --nginx --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m <email> -d <domain>

  1. Generate a Diffie-Hellman group cert:

sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096

  1. Config Nginx:

sudo nano /etc/nginx/conf.d/hugo.conf

server {
  listen 80;
  listen [::]:80;
  server_name <domain>;
  return 301 https://$server_name$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name luu.is;

  root <path-to-blog-dir>;
  index index.html
  
  access_log /var/log/nginx/hugo.access.log;
  error_log /var/log/nginx/hugo.error.log;
  client_max_body_size 20m;
  gzip on;

  http2_push_preload on; # Enable HTTP/2 Server Push

  ssl_certificate     /etc/letsencrypt/live/<domain>/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/<domain>/chain.pem;
  ssl_session_timeout 1d;

  # Enable TLS versions (TLSv1.3 is required upcoming HTTP/3 QUIC).
  ssl_protocols TLSv1.2 TLSv1.3;

  # Enable TLSv1.3's 0-RTT. Use $ssl_early_data when reverse proxying to
  # prevent replay attacks.
  #
  # @see: https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
  ssl_early_data on;

  ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384';
  ssl_prefer_server_ciphers on;
  ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
  ssl_session_cache shared:SSL:50m;
  ssl_session_tickets off;

  # OCSP Stapling ---
  # fetch OCSP records from URL in ssl_certificate and cache them
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;

  add_header X-Early-Data $tls1_3_early_data;
  
  location / {
      try_files $uri $uri/ =404;
  }
  
  # Media
  location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|mp4|ogg|ogv|webm|htc)$ {
      expires 30d;
  }

  # CSS and Js
  location ~* \.(css|js)$ {
      expires 7d;
  }
}
  1. Restart nginx:

sudo systemctl reload nginx

  1. Change SELinux file context:

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html/(/.*)?"

  1. Restore context:

sudo restorecon -Rv /var/www/html/

  1. Allow Nginx to connect to the network:

sudo setsebool -P httpd_can_network_connect 1

Next steps
#

  1. Setup auto-publish every hour with cronjob
  2. Use terraform to provide additional instance & load balancer
  3. Use Ansible to replicate the first instance config to the new one
  4. Install self-hosted comment system such as this, this, or this

Related

Lessons learned deleting 15 years of media content
·816 words·4 mins
IT Linux Study-Note
I was learning more about ZFS while applying what I learned to my OWN data. Of course, as Murphy’s Law states… so here are the lesson from a fool.
How to Install NVIDIA Drivers on AlmaLinux 8
·430 words·3 mins
IT Linux Study-Note
Installing NVIDIA GPU driver on AlmaLinux is a pain in the ass as there are so many sources with different approaches. These steps worked for me
Replacing physical disk in LVM
·806 words·4 mins
IT Linux Study-Note
During installation of Alma Linux, I noticed that it created a logical volume (LV) by default for /home spanning across 2 disks. One of it is failing.