Continuous Delivery with Circle CI, Hugo and Kubernetes
Continuous Delivery (CD) is a process by which you continuously release software updates as changes are made to a product. As companies start using CD in their software development process, they will normally see their release cycles go from once or twice a month to multiple times a week or even multiple times a day; normally leading to higher quality releases and features getting in the hands of users more quickly.
For more information on continuous delivery and why you should be using it, check out this article on why continuous delivery is good for business!
Building a Continuous Delivery Pipeline
The CI/CD pipeline is one of the most important aspects when using CD for a project. The CI/CD pipeline is what is ultimately responsible for running unit tests, integration tests, compiling and building the project, and deploying the project to staging and production, or production-like, environments.
This guide will help you setup a Continuous Delivery pipeline that will use Circle CI to build and deploy a Hugo website to a Kubernetes cluster using Helm to manage deployments. While this guide is specifically tailored for Circle CI, the concepts introduced in this guide should translate to almost any CI platform (Jenkins, GitLab CI, Drone CI, Travis CI, etc).
This guide also assumes that you have a Kubernetes cluster up and running with an ingress controller. If you don’t, checkout the managed Kubernetes service by Digital Ocean! They’ve created an awesome product that makes it incredibly easy to get started with Kubernetes.
Building a Docker Image
Before we get started, ensure you have the latest version of Docker installed on your machine.
We will be creating a multi-stage build process that allows us to separate our build process within the Dockerfile and copy only the necessary portions of the resulting images into a final image; resulting in a smaller docker image with a more secure footprint.
The Dockerfile
This Dockerfile
creates a simple two-stage pipeline for generating a Docker image. The first step in the pipeline uses
the scotwells/hugo
image to generate the static Hugo site. The second stage will copy the static site generated in the
previous stage to the default nginx HTML directory.
# start with an image that has the hugo binary installed
FROM scotwells/hugo:latest as site
# set the working directory so we have a consistent place
# the site will be built
WORKDIR /app
# copy the site contents into the image
COPY . /app
# running this command will build the site
RUN hugo-extended
# start a new image based on the nginx container
FROM nginx:alpine
# copy the built site to the site directory
COPY --from=site /app/public /usr/share/nginx/html
Building an image
Now that you have created the Dockerfile
in the root of your project, test it out by running a docker build
command.
The following command will create a new docker image with the tag of hugo-site:latest
.
$ docker build -t hugo-site:latest .
Once the image finishes building, run the following command in your terminal and open http://localhost:8080
in your
web browser to test that the image functions correctly.
$ docker run --rm -p 8080:80 hugo-site:latest
Creating a Helm Chart for your Hugo Site
Before continuing, ensure you have Helm installed in your Kubernetes
cluster and have the helm
CLI available on your command line. In some clusters (dev and testing), this can be as easy
as running helm init
, but with production clusters, you should take extra precautions to
secure your cluster when using Helm.
Creating a new Helm Chart
The easiest and quickest way to create a basic helm chart for your repository is to use the helm create
command.
Execute the following command in the root of your project to create a new helm chart called chart
.
$ helm create chart
Having your chart live inside the repository is a quick and easy way to have the CI/CD pipeline access your helm chart. When you start getting into re-useable charts that are shared across multiple projects and repositories, it can be easier to extract the chart into its own repository, we’ll save that one for another day.
If you open the chart
directory, you will now see a few newly created files. The values.yaml
file provides a set of
variables that helm will use by default when executing your chart. Within the templates
directory, you’re going to see
three important files: deployment.yaml
is what will create your kubernetes
deployment controller, service.yaml
will
create the kubernetes service which will help load
balance traffic across your deployment, and finally the ingress.yaml
which will configure your service for your
ingress controller.
Configuring the Helm Chart
To configure the helm chart, open up the values.yaml
file. Update the image.repository
setting to the full url of
the docker image you will be building. Next, you should update the image.tag
value to be the tag of the branch you
always consider to be stable. Your image configuration should look similar to the following.
image:
repository: toddcoffee/toddcoffee
tag: latest
Next, we will want to configure ingress for the service. Since we will be using an ingress controller to serve this
site, update ingress.enabled
to true, this will have Helm create the Ingress resource in the cluster. Finally,
configure the hosts that should serve this site.
ingress:
enabled: true
hosts:
- todd.coffee
Note: If you don’t want to use an ingress controller and would rather use a HostPort or LoadBalancer, take a look at
the service.type
parameter in the values.yaml
file.
Configuring the Circle CI Pipeline
While we will be using Circle CI for automating our Hugo site build and deployment process, the steps executed by our pipeline could be used in just about any CI/CD platform (Jenkins, Travis CI, GitLab CI, etc).
For the full Circle CI configuration list, checkout the Circle CI configuration documentation
The pipeline we will create will have the following three basic steps:
- Build the Hugo site using a Dockerfile to produce an nginx image
- Configure the
kubectl
command (kubernetes cli) - Deploy our new Docker Image with Helm
Building the Docker Image
In order to build our docker image and push it to the Docker registry, we need to first update our job configuration to add the appropriate environment variables for building our docker image. Create the following variables in your job configuration.
DOCKER_USER
- the username that’s used logging into your docker registryDOCKER_PASS
- the corresponding password for the userDOCKER_IMAGE
- the full name of the docker image that should be built (excluding the tag)
After the job configuration has been updated, create a new job called build
with the following definition. This job
will start by checking out the repository based on the commit that is being built. Then, it will setup docker so we can
successfully use docker
commands within our build. Next, it will build the image using the configuration variables
defined above using the git tag as the version tag. Finally, it will push the built image to the docker registry.
build:
docker:
- image: docker:stable-git
steps:
- checkout
- setup_remote_docker
- run:
name: Login to the Docker Registry
command: docker login --username $DOCKER_USER --password=$DOCKER_PASS
- run:
name: Build the Docker Image
command: docker build -t $DOCKER_IMAGE:$CIRCLE_TAG .
- run:
name: Push the Docker Image
command: docker push $DOCKER_IMAGE:$CIRCLE_TAG
Deploying the Website
The deployment process we will be setting up will use Helm to deploy the docker image generated in the build
job our
to our configured Kubernetes cluster. Helm uses the same credentials configured for kubectl
, so let’s begin by setting
kubectl
first.
Note: Most of the settings we are setting below can be pulled from your local Kubernetes configuration normally located in ~/.kube/config
.
First, go back into the pipeline configuration settings within Circle CI and add the following variables.
KUBERNETES_CA_CERT
- Base64 encoded CA Cert for the cluster (certificate-authority-data
in your cluster configuration)KUBERNETES_SERVER_URL
- API server URL for your Kubernetes cluster (name
in your cluster configuration)KUBERNETES_AUTH_TOKEN
- Auth Token generated for a service account (generating a new service account with Kubernetes)KUBERNETES_DEPLOY_NAME
- Name to use for the deployment. This will affect how the resources associated with your deployment are named.KUBERNETES_NAMESPACE
- Namespace the associated resources for the deployment should be created in
Once the pipeline configuration has been created, you need to add the following to your Circle CI configuration file
as a new job called build
. The deploy
job will first setup the kubectl
configuration followed by deploying the
new docker image with helm using the configuration above.
deploy:
docker:
- image: scotwells/helm-docker
steps:
- checkout
- run:
name: Setup K8s Cluster Config
command: |
echo $KUBERNETES_CA_CERT | base64 -d > ca.crt
kubectl config set-cluster default \
--server=$KUBERNETES_SERVER_URL \
--embed-certs=true \
--certificate-authority=ca.crt
- run:
name: Setup K8s Credentials Config
command: kubectl config set-credentials default --token=$KUBERNETES_AUTH_TOKEN
- run:
name: Setup K8s Context Config
command: kubectl config set-context default --cluster=default --user=default
- run:
name: Set K8s Context
command: kubectl config use-context default
# deploy the application using Helm
- run:
name: Deploy application with Helm
command: |
helm upgrade $KUBERNETES_DEPLOY_NAME-production ./chart \
--namespace=$KUBERNETES_NAMESPACE \
--wait \
--install \
--values chart/values.yaml \
--set image.repository=$DOCKER_IMAGE \
--set image.tag=$CIRCLE_TAG
The last step is to setup the workflow configuration so that our build
job runs before our deploy
job. We will also
only limit this to run on tags created on the repository.
Workflow Configuration
By default, Circle CI would execute these jobs in parallel depending on your Circle CI configuration. Instead, we need
to tell Circle CI that the deploy
job depends on the build
job. We also want to tell Circle CI to run our build
command on every branch, but only run our deploy
command when a new tag is created on the repository.
workflows:
version: 2
build_and_deploy:
jobs:
- build:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
- deploy:
requires:
- build
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
You could also create an additional job for deploying a specific branch (develop
, feature/*
, etc) to a staging or QA
environment.
Want to see the final product? Check out the full example Circle CI configuration
Wrapping Up
Now that you’ve got your Circle CI pipeline configured, you can easily deploy your code to a production environment by creating a new release in GitHub. By integrating a Continuous Delivery pipeline into your workflow, code changes can be deployed to production faster, safer, and easier than before; meaning new features and bug fixes get to your customers faster.
With Continuous Delivery, it becomes much easier for you and your team to get changes out to a production environment, this can also make it too easy to get changes into production. Make sure you have a solid code review process and that you have ACLs in place around who can create a release for your project.
Happy Helming 🎉!
Questions or Feedback? Comment below or reach out on twitter!