About This Blog

2019/12/11

This blog is generated by Hugo and is stored on AWS as a static site on S3. All of the code mentioned here can be found on GitHub for those who want to follow along.

Background

I had used Google Blogs quite some time ago but never really liked the interface or image management. Then for an internal documentation site at work we have been using Jekyll and I really liked the dead simple vi > git commit > see content workflow so I set out to do something similar for my personal site.

The Jekyll site at work runs on ECS behind an ALB and involves a few containers–mostly an OIDC auth container and a few content containers to share load. This model would be too expensive to run for a personal blog so I sought some cheaper options. I struggled for about 10 minutes with Jekyll static sites and went looking to see if there was something more interesting to work on and that's how I stumbled on Hugo. I've always been a huge fan of Golang so I liked that it was go based and it seemed to have a fairly rich feature set.

From there I just embarked on a simple set of goals to get a Github Webhook to trigger a deployment of a static blog so I could post things when I felt obliged and keep it cheap during the long periods where I have no inspiration.

Getting Started

The first step was to get Hugo generating content in a container so that it could later be used as the dev/deploy environment. I was disappointed that all Hugo offered for Linux environments was linuxbrew brew install hugo setup which I felt was gross to run in a container so I just set it up to compile from source. Here's a cleaned up version of the Dockerfile for my local dev and deploy environment:

FROM ubuntu:18.04
RUN apt-get update

# Necessary to make build-essential not ask stupid questions
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get install wget git build-essential -y
RUN mkdir -p /opt/source/working

# setup Golang
WORKDIR /opt/source
RUN wget https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz
RUN tar -C /usr/local -xzf go1.13.4.linux-amd64.tar.gz
ENV PATH=$PATH:/usr/local/go/bin
ENV GOBIN=/usr/local/bin/

# Setup Hugo compile
RUN git clone https://github.com/gohugoio/hugo.git
WORKDIR /opt/source/hugo
RUN go install --tags extended
RUN mkdir -p /tmp/working
RUN apt-get install vim curl -y
WORKDIR /tmp/working/bitester-blog

# install fixuid to fix file permissions on files created
# in volume mounts from within the container
RUN USER=hugo && \
    GROUP=hugo && \
    curl -SsL https://github.com/boxboat/fixuid/releases/download/v0.1/fixuid-0.1-linux-amd64.tar.gz | tar -C /usr/local/bin -xzf - && \
    chown root:root /usr/local/bin/fixuid && \
    chmod 4755 /usr/local/bin/fixuid && \
    mkdir -p /etc/fixuid && \
    printf "user: $USER\ngroup: $GROUP\n" > /etc/fixuid/config.yml
ENTRYPOINT ["fixuid"]
RUN useradd -ms /bin/bash hugo
USER hugo:hugo

The gist is that we need a container that has Hugo installed in it and to compile Hugo from source we need Golang set up. After that we want to be able to mount a volume from the local filesystem into the container so that the files that Hugo generates have nice clean file permissions on the local system. I've struggled with the file permissions thing in the past and never came up with a solution I liked so I figured I'd try the fixuid thing and I've been happy so far.

This is built with a command such as docker build . -t hug.

From here we have a Makefile that lets us do some things locally when we're debugging or testing some content builds. This Makefile is totally custom to my dev environment so use with caution:

run:
        docker run -p 8080:1313 -v $(shell pwd)/../source:/tmp/working -u 500:500 -it hug hugo serve -D --bind 0.0.0.0
build:
        docker run -p 8080:1313 -v $(shell pwd)/../source:/tmp/working -u 500:500 -it hug hugo -D
        aws s3 sync --delete /home/ec2-user/environment/source/bitester-blog/public s3://blog.bytester.net
run2:
        docker run -p 8080:1313 -v $(shell pwd)/../source:/tmp/working -u 500:500 -it hug /bin/bash

This way when I want to run a local serve of the content I can just do make run. When I want to jump inside the container and poke around with hugo commands I can make run2. If I want to do a manual build/synch to s3 I can run make build.

Keep in mind that the -u 500:500 is the UID of the user I'm running the make commands with and that's why I can get proper file ownership of the files created on the volume mount from within the container. Obviously this and many other portions of the Makefile are highly custom to my environment and you would have to play around with them in yours.

Theming

Hugo theming was a bit annoying because you have to pick a theme or you won't see any content. I finally settled on a very simple theme which I had to essentially copy into the themes/ folder in the root of the source and reference the theme by name in the config.toml file. I tried using git submodules like in the example on the site but it was kind of gross and I just wanted to get something running quickly so I settled on copying the theme in its entirety.

Another annoying thing is that each theme has custom parameters/settings that need to be set before it can work. Once you figure out the magic combination you might see content when you hugo serve -D. All of the problems I had with Hugo were theme related in some way. I was either missing theme content files or launching from the wrong directory or something like that. The error messages Hugo gives you are quite vague but Google fu will carry you through.

Pipeline

Once some test content was flowing it was time to get auto build/deploy using something cheaper than running a Jenkins EC2 instance. For this I settled on AWS CodePipeline since it only uses infrastructure on demand and accepts webhooks from Github.

Build Environment

The first step is to get the Docker build environment up to ECR so it's available for CodeBuild to spin up as a step so there's an environment in which to run the buildspec.yml content. You could get really crazy with this and put your whole stack in Cloudformation or something but I tend to start with baby steps and I figured this env wouldn't change too terribly often. For that reason I did the old manual upload flow with the docker login nonsense.

PreReqs

#!/bin/bash
REGION=$(aws configure get region)
ACCOUNT=$(aws --output text sts get-caller-identity | cut -f1)
docker build . -t hug
docker tag hug:latest $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/hug
COMMAND=$(aws ecr get-login --region $REGION --no-include-email)
eval #COMAND
docker push $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/hug

So whenever there's a change needed to the Docker environment I just need to run this script to upload the new environment to ECR. This isn't perfect but good enough for a personal blog.

CodePipeline

The next step is to set up the CodePipeline in AWS so that we can start running automated build/deploys. For this I just went through the AWS console and followed the CodePipeline wizard. I'll just cover some of the major options that were selected:

This part was pretty straighforward and I didn't have to mess much with the CodePipeline console. The real work happens in the CodeBuild section.

CodeBuild

This part took a while to get right as with any new CI/CD environment you have to do a lot of discovery to see where you are in the filesystem when you enter your container, whether or not you need full paths, what binaries and environment variables are available, etc. A lot of trial and error was required to get this very simple buildspec.yml created:

version: 0.2

phases:
  build:
    base-directory: /tmp/working/bitester
    commands:
      - hugo -D 
  post_build:
    commands:
      - aws s3 sync --delete ./public s3://$OUTPUT_BUCKET
# create an archive artifact
artifacts:
  name: bitester-blog_$(date +%Y-%m-%d) 
  files:
    - '**/*'

What this will do is launch the container from ECR, run the hugo -D command and then run an s3 sync command to upload the new fies–that's it. It will also archive a tarball of the content to the artifacts s3 bucket that codebuild created in the wizard. The actual destination is a bucket I created ahead of time and it's name is stored in the environment variable associated with CodePipeline.

ECR Notes

The ECR repo had to have permissions added to it so that CodeBuild could access the repository. Apparently CodeBuild does not use its own service role when attempting to access ECR. Below is the permissions on the ECR repo:

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "codebuild-statement",
      "Effect": "Allow",
      "Principal": {
        "Service": "codebuild.amazonaws.com"
      },
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer"
      ]
    }
  ]
}

S3

This part was manually set up as a public bucket configured for website hosting. The name of the bucket has to match the URL that you want the site to run on. So for this blog hosted at blog.bytester.net the bucket must be named blog.bytester.net. The root document on the static website settings can be index.html. Obviously the box has to be unchecked to allow public hosting.

Here is a copy of the bucket policy on the bucket:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::blog.bytester.net/*"
        }
    ]
}

Route53

Luckily I already owned the domain in Route53 so all I had to do was add the A-record Alias for the S3 static website. From there as long as the bucket is correctly named AWS handles the rest. There are about a thousand guides on the web on how to do this part.

IAM Notes

In addition to the bucket policy, ECR policy, etc already mentioned in this article I wanted to note that I had to “doctor” the service role for CodeBuild and add permissions to sync to the s3 bucket. Essentially all I had to do was add the following SID block to the policy on the service role

        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::blog.bytester.net*"
            ],
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetObjectVersion",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation"
            ]
        },

Conclusion

So now when I want to make posts all I do is write a .md file and then push to github and CodePipeline takes care of the rest.

I hope this was interesting or helpful to anyone trying to do something similar. However, we all know that this post is really directed at future me so I know how to manage my own blog.

Thanks, Russell