In this article, we’ll address a question we often get at Humanitec and that many engineering teams struggle with: how to effectively enable dynamic preview environments on top of Kubernetes. The following is based on a very successful workshop our CTO Chris Stephenson hosted in September this year. Given the great response and engagement it got from the audience, we decided to put in written form for easier consumption. If you’d like to watch the whole workshop session, you can find it here.
Why preview environments
What are preview environments and why would you care about enabling them for your Kubernetes setup? A preview environment is an ephemeral environment created with the code of your pull requests. It provides a realistic environment to see your changes live before merging to master. A link to the preview environment is added to the Pull Request (PR), so everyone in your team can see your changes live with just one click. Preview environments are great to test out code changes in a real life setup, without the code reviewer having to set everything up on their own machine.
For this particular example, we’ll use GitHub Actions and Humanitec to create preview environments on top of our K8s clusters to which we can deploy our sample application to. Before we get started, let’s establish a couple of definitions.
A Pull Request, or PR, is GitHub terminology to describe the process where a developer proposes some code change to be merged from a feature branch to a master branch.
In trunk-based development, the master branch is the main trunk, and PRs are based on feature branches. Once a PR is approved, it gets merged into the main trunk or master branch.
Now, if you overlay the concept of a preview environment on top of this PR flow, this is the target workflow we want to get to:
The lifecycle of the preview environment should be the same as the lifecycle of the PR, so when the PR is created the environment is automatically spun up by the Internal Developer Platform (IDP), in this case, Humanitec. Once the PR is reviewed and merged into the master branch, the environment, and all associated resources are destroyed, so we don’t leave any resources unused in our setup.
At this point, it’s probably worth defining the IDP as well. In general, An IDP is designed to improve the developer experience and make life easier for Ops, allowing them to set clear roles and golden paths for the rest of the engineering organization. But the main thing to focus on for this article is developer self-service. Using an IDP like Humanitec, a developer can self-serve an environment and everything else attached to it to run their services and apps and test their (or their colleagues’) code changes.
An IDP prevents developers from having to deal with complicated, unorganized YAML and config files, so they can independently deploy and test things without having to master their setup end to end or bother Ops (or whoever is in charge of the infrastructure).
To recap, using an IDP engineers can simply create a PR and an associated preview environment will be automatically created for the application to be deployed into and, so any code changes can be easily tested.
Now that we have established a few useful definitions and exactly what we want to achieve, let’s dive right into the code. First though, we need to look at exactly what requirements we want to set for our system. It should:
- Create a preview environment automatically when a new PR is opened
- Should be possible to access the preview environment via PR
- Keep preview environment up-to-date with PR
- Destroy preview environment when PR is closed
We can add more things for more complex use cases, but for this particular example, we can keep to these requirements.
We will use GitHub Actions to define our workflow and call the Humanitec API on PR creation. We will also use Humanitec automation rules to keep the preview environment up to date.
We won’t go into too much detail around the configuration of Humanitec itself. If you’d like to see what that looks like in more depth, our get started video playlist on our docs gives you a great overview on how to get configs set up with Humanitec. What is important to understand is that once called, the Humanitec API will create a namespace on the target Kubernetes cluster for the environment and automatically generate a DNS name associated with it.
The last thing we'll set up on the Humanitec side is the automation rule, so every time the IDP gets notified by the CI pipeline that a new image has been built and is available, we can trigger a deployment to the target environment and namespace we dynamically created.
Once that is set up, we need to make sure our GitHub Actions hooks are properly configured to create a PR and trigger the environment creation from there. Here’s the GitHub Action code to create the preview environment:
<p> CODE:<script src="https://gist.github.com/eskilavelon/413f1fd1076567bd003c3f759bdfb184.js"></script>
The first thing we need to do is setting the context, using HUMANITEC_TOKEN (which can be generated within Humanitec itself) to communicate with the Humanitec API and specifying the org, app, and base environment. The base environment is what Humanitec will use as a baseline to clone from to create the preview environment, in this case, the development environment.
<p> CODE:<script src="https://gist.github.com/eskilavelon/537fb88a933a0dc7d28bd90def9c0926.js"></script>
We then generate a new env ID using the repo and the PR number and clone the BASE_ENV in the context of APP_ID to the new ENV_ID, enforcing a few checks.
<p> CODE:<script src="https://gist.github.com/eskilavelon/a9b91293415e1445b57d0fb371750300.js"></script>
Next, we set the automation rule so the new image gets deployed every time a new version on BRANCH_NAME is available and create a comment that points to the PR (issue.number) inside of GitHub.
<p> CODE:<script src="https://gist.github.com/eskilavelon/6fc5637b2c18413aebffb9c5f165b43d.js"></script>
In addition, we have a similar GitHub Action set up to delete the environment once the PR is merged and closed.
<p> CODE:<script src="https://gist.github.com/eskilavelon/d1b86956ddbd20d3a4b6364be97cfd66.js"></script>
<p> CODE:<script src="https://gist.github.com/eskilavelon/9ce692a75b2f67de7d1ba227b5b4fd6b.js"></script>
And voila, we are ready to make a change, create a new PR and test our setup. Once all steps run, we should get a new environment up and running with our application deployed inside, ready to be tested in a real-life setup with all the required dependencies.
Once all checks are performed, we can merge the PR to the master branch, and our workflows will automatically sunset the preview environment and destroy all associated dynamic resources.