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.
Dockerfile creates a simple two-stage pipeline for generating a Docker image. The first step in the pipeline uses
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
$ 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
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
$ 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
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
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
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
service.type parameter in the
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
kubectlcommand (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 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
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
Note: Most of the settings we are setting below can be pulled from your local Kubernetes configuration normally located in
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-datain your cluster configuration)
KUBERNETES_SERVER_URL- API server URL for your Kubernetes cluster (
namein 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
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.
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
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 (
feature/*, etc) to a staging or QA
Want to see the final product? Check out the full example Circle CI configuration
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!