Continuous Delivery with Circle CI, Hugo and Kubernetes

Author Profile Image
Scot Wells

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:

  1. Build the Hugo site using a Dockerfile to produce an nginx image
  2. Configure the kubectl command (kubernetes cli)
  3. 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 registry
  • DOCKER_PASS - the corresponding password for the user
  • DOCKER_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!


Comments powered by Disqus