Site CI/CD
Whenever I write a new article for my site, I would like to have it published automatically. As mentioned earlier, I’m using Hugo to generate it and I have seen several solutions of other Hugo users for automated deployment.
Update August 2021:
I recently discovered OneDev.io, which is a self-acclaimed all-in-one devops platform. See https://code.onedev.io to check the code and see what OneDev looks like. It is packed with features, such as:
- git version control
- CI/CD using docker containers
- issue tracking
- and much more
While I enjoyed using Gitea, the integrated CI/CD functionality really appealed to me, so I decided to try it out.
Deployment process change
With Gitea, I generated the static site in the hosts file system and had my main webserver host it. With OneDev I decided to create Docker image and use that to host the site. Obviously, there is some overhead, but idle there is no CPU usage and memory usage is with ~9MB not too bad.
Docker builder
To keep my image small and clean, I’m using a builder to generate the site and then copy the result in container running Caddy for hosting:
FROM klakegg/hugo:latest-ext AS builder
COPY . /src
RUN hugo --gc --minify
FROM caddy:2
COPY --from=builder /src/public/ /srv/
COPY Caddyfile /etc/caddy/Caddyfile
Deploy the image
The OneDev buildspec UI looks nice and clean and gives a good overview:
But, you get more control using the underlying yaml file:
version: 9
jobs:
- name: Build, Push & Deploy
steps:
- !CheckoutStep
name: Checkout
cloneCredential: !DefaultCredential {}
condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL
- !CommandStep
name: Build image
image: docker
commands:
- docker build -t @property:image_tag@ . --label "vcs-ref=@commit_hash@" --label
"version=@build_number@" --label "build-date=$(date +"%Y-%m-%d %H:%M:%S")"
useTTY: false
condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL
- !CommandStep
name: Push to registry
image: docker
commands:
- docker push @property:image_tag@:latest
useTTY: false
condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL
- !CommandStep
name: Deploy
image: docker
commands:
- docker pull @property:image_tag@:latest
- docker stop @property:container_name@ || echo "container @property:container_name@
does not exist"
- docker rm @property:container_name@ || echo "container @property:container_name@
does not exist"
- docker run --name @property:container_name@ -p 8082:80 --restart always -d @property:image_tag@:latest
useTTY: false
condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL
triggers:
- !BranchUpdateTrigger {}
retryCondition: never
maxRetries: 3
retryDelay: 30
cpuRequirement: 250m
memoryRequirement: 128m
timeout: 3600
properties:
- name: image_tag
value: <image_tag>
- name: container_name
value: <container_name>
Conclusion
I’m sticking with OneDev!
Previously
The Hugo documentation refers to several online services like Wercker (link to gohugo.io/hosting-and-deployment/deployment-with-wercker/ is no longer valid), Netlify and GitLab.
The documentation also mentions GitHub, but those instructions have not been updated to use GitHub Actions. GitHub has numerous Actions to support Hugo site generation and deployment.
Other users have set up other creative solutions to automate their site deployment. Ryan Himmelwright decided to use Jenkins, while Sean Gransee used CircleCI and Chris Ferdinandi set up a webhook on a Digital Ocean server. I’m self-hosting my site and my Git server. Hosting my own Jenkins server as well, feels overdone.
Instead, I considered using Git Hooks to update my site after pushing new content to the repository. That would have worked nicely, if I wasn’t running Gitea in a container. I didn’t want to modify the image to include the required Hugo executable, so I had to find another way. Gitea also supports using webhooks which allows accessing resources outside Gitea’s container. This post describes how I used webhooks to deploy my Hugo based sites.
Toolchain
The workflow is implemented using the following chain of tools:
After writing my post, I commit it and push it to my Gitea Git server. Gitea is then calling an end-point which is implemented using webhook. webhook makes it very easy to define the end-point and have an application executed. It is calling a bash-script that is checking out the latest changes on Gitea, is then building the site using Hugo and is finally copying the result to the webroot of the site.
Gitea webhook configuration
Gitea has many configuration options to set up a webhook:
In my case, I want to post some information about the push, when it was pushed. Using the secret, the end-point can be secured.
webhook configuration
webhook allows to define many end-points using a JSON formatted config file:
/etc/webhook.conf
[
{
"id": "gitea",
"execute-command": "/home/bas/dockerfiles/caddy2/deploy_site.sh",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "repository.ssh_url"
},
{
"source": "payload",
"name": "repository.name"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "value",
"value": "<secret text>",
"parameter":
{
"source": "payload",
"name": "secret"
}
}
}
]
}
}
]
End-point fields:
- id: the name of the end-point; is the last part of the path on the server:
http://<server>:3000/hooks/gitea
- execute-command: absolute path to script or executable on the filesystem to be executed when the end-point is hit and the trigger rules are met
- pass-arguments-to-command: specifies the arguments to pass to the command, where the source
payload
refers to the JSON posted to the end-point and thename
to the field in it to be used.repository.ssh_url
would refer to the URL on which to access the Git repository to which an update was pushed to.
See the webhook project for more detailed information.
Deploy script
#!/usr/bin/env bash
REPOSITORY_SSH_URL=$1
REPOSITORY_NAME=$2
WORKDIR=/var/tmp
SITEDIR=/home/bas/sites
pushd $WORKDIR
rm -rf $REPOSITORY_NAME
git clone $REPOSITORY_SSH_URL $REPOSITORY_NAME
docker pull klakegg/hugo:latest-ext
docker run --rm klakegg/hugo:latest-ext version
docker run --rm -v $WORKDIR/$REPOSITORY_NAME:/src klakegg/hugo:latest-ext --gc --minify
rm -rf $SITEDIR/$REPOSITORY_NAME
mkdir -p $SITEDIR/$REPOSITORY_NAME
cp -r $WORKDIR/$REPOSITORY_NAME/public/* $SITEDIR/$REPOSITORY_NAME
popd
First a temporary working directory is deleted. Then I clone the repository to build from. The latest klakegg/hugo image is pulled and then used to generate the site. Finally, the generated site directory is replaced by the newly generated site files and the work is done.
This script is generic and can deploy any Hugo site as long as the repository.ssh_url
and repository.name
are passed as
arguments. This makes it easy to use a single webhook end-point and deploy script to refresh multiple Hugo sites.
Conclusion
This relatively simple set of tools and scripts really work well for me. Push and forget!
October 19, 2020