Deploy Pull Requests for Review into Kubernetes with GitHub Actions
To review a Pull Request, it can be helpful to deploy it temporarily. The infrastructure and the application must be prepared for this.
To review a Pull Request, it can be helpful to deploy it temporarily. Both, the infrastructure and the application itself must be prepared for this kind of deployment. This article shows, how to deploy Pull Requests into a Kubernetes Cluster.
Note: This guide assumes, that you want to deploy a containerized application build from a Pull Request into a Kubernetes cluster for review. We only focus on the application. Existing infrastructure will be re-used and there won't be a dedicated infrastructure deployment for the Pull Request in this guide.
Prerequisites
- Container Registry
- Kubernetes Cluster
- Ingress Controller deployed (e.g. NGINX)
- IP address attached to the Ingress Controller
- Access to the Container Registry
- Helm Chart for the application
- Access to DNS provider and rights to add records
Preparations
Environment
DNS
Find a domain pattern for your Pull Requests. If your application is usually available at example.com
and the Pull Request with the ID 4711 should be available at 4711.pr.example.com
, make sure to allow both rules in your DNS by adding the following records.
Type | Name | Value |
---|---|---|
A | example.com |
10.20.30.40 |
A | *.pr.example.com |
10.20.30.40 |
In the example above, both DNS records point to the same IP address, which indicates that both, the production environment and the deployed Pull Requests are hosted in the same cluster. In a real-world scenario, you might want to host them in separate clusters and point the two DNS records to different IP addresses.
Helm Chart
The Helm Chart should also be prepared for multiple types of deployment. It's recommended to use the same Chart for all kind of deployments, including Pull Request and Production deployments.
Values
In most cases, there are two properties of your deployment, that you want to make configurable: The container image that gets deployed and the host name (domain) where the application will be available at. For the host name, it is common to split it up into the core domain, an optional prefix (like *.pr
in our example) and an optional suffix.
An according values file in your Helm Chart could look like this:
image:
repository: "repo.io/example"
tag: "latest"
ingress:
domain:
base: "example.com"
prefix: ""
suffix: ""
In case you need to use compound values at multiple places in your Helm Chart, it is recommended to define them in a helpers file. Check out our Helm Best Practices, to learn more.
{{- define "ingress.hostName" -}}
{{- .Values.ingress.domain.prefix }}{{ if ne .Values.ingress.domain.prefix "" }}.{{ end }}{{- .Values.ingress.domain.base }}{{- .Values.ingress.domain.suffix }}
{{- end }}
Ingress
The Ingress resource configures the Ingress controller to listen on specific host names and then route traffic to a Kubernetes Service. For our deployed Pull Request, we want to configure the Ingress to listen on the Pull Request specific host name (e.g. 1.pr.example.com
). The Helm Chart should be configured in a way, that it uses the compound host name.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example
spec:
rules:
- host: {{ include "ingress.hostName" . }}
http :
paths:
- path: /
pathType: Prefix
backend:
service:
name: example
port:
name: http
If you use TLS, make sure to also add it to the tls
section of your Ingress resource.
spec:
tls:
- secretName: example-ingress-tls
hosts:
- {{ include "ingress.hostName" . }}
Pods
To allow pods to pull the very specific container image that gets specified for this release (like the one that has been built for the Pull Request), we should make sure to use the Helm variables in the Pod definition.
spec:
containers:
- name: example
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
Pipeline
Once the Helm Chart is prepared for flexible deployments, we can start to build a pipeline that we can run whenever a Pull Request should be reviewed by deploying it into a cluster.
Github Actions supports both, triggering a workflow when a Pull Request has been labeled and triggering another one when a Pull Request has been closed.
We will create two workflows:
- The
deploy_pull_request.yaml
workflow deploys a Pull Request when thedeploy
label has been added - The
cleanup_pull_request.yaml
workglow deletes the deployed resources when a Pull Request gets closed
Trigger
Triggering the pipeline is an important step. It is very likely, that you don't want to deploy every single Pull Request but rather want to be able to run the pipeline manually when needed.
For this, you need to come up with an event, that triggers the pipeline. In GitHub, this trigger can be a label that gets attached to the Pull Request.
GitHub Actions support a lot of events that can trigger workflows. One of these is the pull_request
event, than can be filtered by the type of Pull Request event that occurred. The ones we are interested in are labeled
to trigger the deployment workflow and closed
to trigger the cleanup workflow.
By default, GitHub repositories don't come with a label that we can use for triggering the deployment. It is worth adding one (either for the repository or the whole organization) called deploy
.
To create a workflow that only gets triggered when a Pull Request gets labeled with the deploy
label, use the code below.
on:
pull_request:
types: [labeled]
jobs:
deploy-pr:
name: 'Deploy Pull Request'
runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'deploy' }}
Steps
In the pipeline, we need to make sure that we Build and push the container images(s) to a Container Registry that is available to the cluster and then install the Helm Chart into that cluster with the updated values for the Pull Request to deploy. Afterwards, it would be nice to post the review URL as a comment to the Pull Request.
Build and Push the container image(s)
Make sure to build a container image with the version of your code of the Pull Request you want to review and tag it accordingly. In this example, we use the pr-4711
tag for a Pull Request with the ID 4711. Once the container is built, push it to a registry, that the target cluster has access to. This can be the same Container Registry that all your other containers use or a dedicated one just for test builds.
- name: Build Docker image
run: docker build --tag repo.io/example:pr-${{ github.event.pull_request.number }} ...
- name: Push Docker image
run: docker push --all repo.io/example
Install the Helm Chart
Now that the Pull Request's version of the application is containerized and the container is pushed to a Container Registry, we can kick-off the deployment by installing the Helm Chart and overwriting the variables with values that match the Pull Request.
- name: Install Helm Chart
working-directory: env/helm
run: |
helm upgrade pr-${{ github.event.pull_request.number }} <YOUR_CHART> \
--install \
--namespace pr-${{ github.event.pull_request.number }} \
--create-namespace \
--wait \
--set image.tag=pr-${{ github.event.pull_request.number }} \
--set ingress.domain.prefix=${{ github.event.pull_request.number }}.pr
Post the review URL as a Pull Request comments
It's a nice touch to post the URL to the freshly deployed Pull Request to the comments, so that Developers and Testers can access it easily.
- name: Post comment to Pull Request
uses: unsplash/comment-on-pr@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: "Your Pull Request Deployment is ready for review at https://..."
check_for_duplicate_msg: true
Cleanup
To avoid that the deployed Pull Request lived beyond the lifetime of the actual pull request, we need to clean up the resources, once the Pull Request gets closed.
The cleanup proccess also needs a trigger. GitHub Actions supports triggering a pipeline on closing a Pull Request, you should use this as a trigger for these steps.
name: 'Cleanup Pull Request'
on:
pull_request:
types: [closed]
jobs:
cleanup-pr:
name: 'Cleanup Pull Request'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- run: ...
Uninstall the Helm Chart and delete the Kubernetes Namespace.
- name: Delete Helm Chart
run: helm delete pr-${{ github.event.pull_request.number }} --namespace pr-${{ github.event.pull_request.number }}
- name: Delete Namespace
if: always()
run: kubectl namespace delete pr-${{ github.event.pull_request.number }}
Full Example
Deployment pipeline
name: 'Deploy Pull Request'
on:
pull_request:
types: [labeled]
jobs:
deploy-pr:
name: 'Deploy Pull Request'
runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'deploy' }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker Login
run: docker login ...
- name: Build Docker image
run: docker build --tag repo.io/example:pr-${{ github.event.pull_request.number }} ...
- name: Push Docker image
run: docker push --all repo.io/example
- name: Kubernetes Login
run: ...
- name: Install Helm Chart
working-directory: env/helm
run: |
helm upgrade pr-${{ github.event.pull_request.number }} <YOUR_CHART> \
--install \
--namespace pr-${{ github.event.pull_request.number }} \
--create-namespace \
--wait \
--set image.tag=pr-${{ github.event.pull_request.number }} \
--set ingress.domain.prefix=${{ github.event.pull_request.number }}.pr
- name: Post comment to Pull Request
uses: unsplash/comment-on-pr@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: "Your Pull Request Deployment is ready for review at https://..."
check_for_duplicate_msg: true
Cleanup pipeline
name: 'Cleanup Pull Request'
on:
pull_request:
types: [closed]
jobs:
cleanup-pr:
name: 'Cleanup Pull Request'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Kubernetes Login
run: ...
- name: Delete Helm Chart
run: helm delete pr-${{ github.event.pull_request.number }} --namespace pr-${{ github.event.pull_request.number }}
- name: Delete Namespace
if: always()
run: kubectl namespace delete pr-${{ github.event.pull_request.number }}
☝️ Advertisement Block: I will buy myself a pizza every time I make enough money with these ads to do so. So please feed a hungry developer and consider disabling your Ad Blocker.