An example pipeline for Hello World for Kubernetes using a Gitflow(ish) model to deploy to multiple environments.
The pipeline for this project is all able to be run directly within Github Workflows. Locally, the project was created on Linux with Docker and Go. All CI tasks can be run without docker using the included Makefile. This includes a task to pull in all external stand-alone binaries that are used for constructing a kind based kubernetes cluster instance and the tools for deploying this project into a running cluster.
## Show all available make tasks
make
## Download and install all stand-alone binaries used for this project
make deps
NOTE All external binaries are purposefully stand-alone and pulled into an ingored
.local/bin
directory with amake deps
command. This step is required for helm deployments, chart updates, and local pipeline testing. It also pulls in the correct version of kubectl that aligns with the version of Kubernetes being targeted for the deployment.
Two additional tools can be used for quality of life enhancements in the shell and would require additional configuration depending on your shell. asdf-vm and direnv. If asdf and direnv are hooked into your shell then the included .envrc will automatically setup the required version of Go and set appropriate paths for the development environment as well.
The github repo was manually updated in the following ways;
- Default branch is set to
develop
to align with gitflow. - Added
DOCKER_USERNAME
andDOCKER_PASSWORD
for DockerHub integration.
The pipeline process for this example has taken some shortcuts for the following reasons;
- We are not using any centralized helm repository. As such we lint the helm chart and simply use it locally to deploy to kubernetes instead of creating a versioned chart artifact.
- There is no centralized configuration management (like Consul) in this example. To work around this, I use local environment variable files along with gomplate templating.
- We are not working with any sort of enterprise docker registry or git repo either, this somewhat limits our ability to optimize things like the build images without needless complications. In an enterprise environment the build images would need to be localized and deployed via their own pipeline/repo and used in the pipelines for this build process.
- The base images for the multi-stage build are sourced from external locations and would also need to be internal images in an enterprise environment.
- There are no target kubernetes clusters to deploy into as this is an example pipeline only. As such, we include additional pipeline tasks to stand up a kind (Kubernetes in docker) cluster and deploy to it right afterwards.
The pipeline is being performed via Github Actions workflows. The table for how/when these workflows are triggered can be found further down in this document in the Gitflow section.
If you wish to run things locally, every step of the pipeline can be done using the local Makefile tasks instead.
## lint, test, build, then run the local binary
make lint test build run
## lint, test first. Then build, tag, then run the local docker image
make lint test docker/image docker/tag docker/run
## push the image to dockerhub
make docker/login docker/push
NOTE There are additional tasks for pushing the image and more.
The app is a simple Go app that runs the gin framework to serve up a template file in web/template on port 8000.
The CI performs the following actions:
- Code Coverage
- Unit Tests
- Helm chart override rendering
- Build/Tag/Push Docker Image
If this is the develop branch then the CI will be immediately followed by the CD for a release. This includes:
- Create a local kind (kubernetes) cluster
- Apply the rendered chart override for dev to the new cluster
The deployment will also be triggered for a new tag release. This will
- Create a binary github releases artifact with GoReleaser
- Create a local kind (kubernetes) cluster
- Apply the rendered chart override for prod to the new cluster
Gitflow wasn't necessarily designed for multiple release environments. So adaptations need to be made to accommodate the requirement for using it as the Git branching strategy in this example.
A project release manager should be assigned to ensure that the schemantic versioning of the code base is performed when new release-* branches are started and a regular cadence for releases should be established if gitflow will be used. Additionally, the ground rules for the model should be laid out so I'll do so in the following sections.
In our case we will use the following environments and triggers for releases into them.
Release Environment | Action | Branch | Trigger |
---|---|---|---|
N/A | Build | develop, release/, hotfix/, feature/ | Any update to branches with this name |
dev | Build/Deploy | develop | Any updates to the develop branch |
prod | Deploy | master | Any new tags to the master branch |
Additionally, there are manual tasks required in a typical gitflow model that are done automatically in a separate Github Workflow. This workflow will do the following;
- Automatically creates and merges pull requests from master to develop branches
- Automatically creates and merges pull requests from release to master.
Because of this we will use additional pipeline logic to prevent build/deploy processes from running when a merge to develop from master occurs.
The following branch naming strategy should be followed for this project as it (mostly*) adheres to the gitflow model.
Name | Description | Time to live | Example |
---|---|---|---|
master | Rreflects a production-ready state. After code has been validated in a release branch and pushed to production, the code is merged to the master branch. However, this doesn't fully align with multiple releases installed concurrently on different environments. | Permanent | master |
develop | Reflects a production-ready state. After code has been validated in a release branch and pushed to production, the code is merged to the master branch. However, this doesn't fully align with multiple releases installed concurrently on different environments. | Permanent | develop |
feature/{sprint}-{short description} | feature branches are used to develop new features for the upcoming or a distant future release. The essence of a task branch is that it exists if the story/task (feature) is in development but will eventually be merged back into develop (to add the new feature to the upcoming release) or discarded (in case the business goals have been changed). | Short-term | feature/12301-cdnimplem |
hotfix/{sprint}-{short description} | Hotfix branches are like bugfix branches, but created from release branches and intended to fix issues in them once the release that is currently deployed in PROD. | Short-term | hotfix/3-uifix |
bugfix/{sprint}-{short description} | Bugfix branches are like feature branches but created not for a development of a new feature but to fix an issue in develop or release/* branch. | Short-term | bugfix/3-myfix |
release/v{MajorVer}.{MinorVer}.{PatchVer} | Release branches are created when the develop branch is at a stable point and release specific changes need to be made, such as bumping version numbers, etc. At that point, develop should be branched and the changes made before ultimately merging it into master and tagging the release. There should only be one active release branch at a time. | Short-term | release/v1.1.0 |
NOTE 1: This project uses the free version of github which does not allow for branch protection rules that would need to be implemented in an enterprise environment!
NOTE 2:* We use branch naming slightly different than gitflow dictates by using a
/
instead of a-
. This allows for some branch rollup visualization options in certain platforms and can be a way to keep repos with a ton of branches cleaner at the branch root.
The current cluster creation is based on kind (kubernetes in docker) and is run inline via the pipeline for lack of an environment to target otherwise. If you will be building and running this deployment locally then the version of the kubernetes cluster can be defined in the project.env and must align with a released tag for the kind project (found here). Otherwise, the most recent version will be deployed within the pipeline code (it would be a nominal task to include this versioning in a production pipeline deployment pipeline)
Most people seem to know of or use Helm so this is what we will be utilized for deployment of this test application to our cluster. Helm 3 will be implemented in this regard and we will be rendering our helm chart override values file using gomplate. Gomplate can use various backends like consul or others so it makes for a fairly pluggable system down the line if centralized config managment were to be implemented instead of simple env var files.
NOTE: Deployment variables for targeted environments are stored within this repo in
./config/deploy.<target>.env
. Currently only develop and prod are defined. The pipeline code will determine which to use based on the current branch being run. Only master branch will be considered for targeting the prod environment. Develop and feature/* branches will target the develop environment.
To run kind locally for deployment testing you can use some included tasks.
# To start a local cluster and set your context to it.
make start/cluster kube/context
# To destroy a local cluster
make stop/cluster
# deploy helm chart to local cluster (use output to setup proxy to cluster to validate the hello world website is running)
make helm/deploy
# Test the deployed chart service automatically with a test pod
make helm/test
WARNING: The chart deployment to a local kind cluster assumes a versioned image has already been deployed to dockerhub via the github workflow. If developing code this is a sub-optimal experience so alternatives for in-cluster development should be further explored/considered. There are ways to force the kind cluster to use a locally depoyed registry found here
Semantic Versioning are being used per best practice. As it aligns with Go module release versioning we will use the git tag as the version for deployment. The pipelines for this project produces the following artifacts with versions.
Artfact | Artifact Location | Created With | Branch(s) | Version | Example |
---|---|---|---|---|---|
Binary Release | Github Releases | GoReleaser | master | Git Tag | v0.0.1 |
Docker Image | DockerHub | Docker | develop | Git Tag+Commit Hash(short) | v0.0.0-f038f38 |
Docker Image | DockerHub | Docker | master | Git Tag | v0.2.0 |
NOTE: The helm chart is currently not versioned. Instead the image tag is passed through to the chart for deployment. This is a common practice but ideally the chart itself would be packaged up as a whole with an updated version in the Chart.yaml manifest instead.
Using the gitflow git extension (in some areas) we will demonstrate a full workflow from feature to release.
## Start a feature from develop (the default branch)
git flow feature start initial-release
git add --all . && git commit -m 'feature: initial release'
git push origin feature/initial-release
This will lead to a workflow that runs the CI process only. No docker image will be published.
Initialize a pull request into develop for the feature branch. In the github console create a pull request for the feature manually or used the included gh
CLI tool to do the same:
./.local/bin/gh pr create -B develop -f
In the github console, approve and merge the request and delete the feature/ branch as gitflow dictates. This will kick off the CI process, create a new tagged image, and run through the (fake) deployment process into a kubernetes cluster then test the deployment with the built in helm chart tests.
To start a release go back to develop branch and begin the release process.
## Pull merged features or other changes
git checkout develop
git branch --set-upstream-to=origin/develop develop
git pull
git flow release start v0.0.1
# Make some small changes, ideally you would bump any version files that may be required. We use tags for versions so we will not do this.
# I keep a .version file in the root repo and update it here just to have something to commit and push
git add --all . && git commit -m 'release: v0.0.1'
git push origin release/v0.0.1
# Either manually create the pr against master or use the following for this release
./.local/bin/gh pr create -B master -f
Review and approve the PR to master and merge it within Github. When this is completed, the release/v0.0.1 branch will have been merged into master. This will kick off the GitFlow workflow to auto-PR and merge master into the develop branch. This will kick off the production deployment pipeline for this release.
To kick off the final release you will need to bump up the git tag on master. To do this, you can go to Github and select the release
tab in the project repo. In here, select to publish a new release and choose the master branch. Use the new release version tag, in this example it would be v0.0.1
and ad some notes on your awesome release. When completed 2 workflows will kick off;
- Gitflow auto-merge from master to develop
- Release of binaries, docker images, and production cluster deployment using the new tag
When these have completed you should pull all updated and start the process again from the beginning
git checkout develop
git branch --set-upstream-to=origin/develop develop
git pull
Release branches, by gitflow design, should be short lived and should be removed after any hotfixes or other last minute updates are completed.
git flow release finish 'v0.0.1'
If you wish to clone this into your own project for testing purposes follow these directions to get started after cloning this repo. All git commands are assumed to be using ssh key based authentication. Before moving forward you will need to update the config/* files to be correct for your github account and create the dockerhub target container registry.
This will get the github repo started when you are ready:
rm -rf ./.git
make github/bootstrap
Then manually add in DOCKER_USERNAME
and DOCKER_PASSWORD
into the created github repo secrets and configure git flow branches.
make .dep/gitflow
git flow init
When this has been done, go back into the repository settings in github and set the develop branch as your default branch and you are now ready to start testing your own project.
Here are quite a few additional tasks in the Makefiles that are broken out by category in the scripts directory. Running make
at the root directory will show all non-hidden tasks (those not prefixed by a .
). Many of these are one off tasks I needed to do at some point and just dropped into a Makefile for future use. These may or may not be valuable to the right devops engineer as items to cut apart for their own custom pipelines so I just left them in.
The only task that would require elevated rights is the hidden .dep/gitflow
task. This will install the gitflow git extension that can be used to make the gitflow experience quite a bit easier for a developer.
A few more interesting things that can be done with this scaffolding are listed below.
# Check if the current git commit hash has a pushed image (perhaps use to skip an entire build)
make docker/list | grep -w $(make .printshort-RELEASE_VERSION)