December 13, 2021

Day 13 - Ephemeral PR Environments: Enabling automated testing at a rapid pace

By: Amar Sattaur
Edited by: Jennifer Davis (@sigje)

Recently, I've been thinking a lot about how to implement the concepts of least privilege while also speeding up the feedback cycle in the developer workflow. However, these two things are not very quickly intertwined. Therefore, there needs to be underlying tooling and visibility to show developers the data they need for a successful PR merge.

A developer doesn't care about what those underlying tools are; they just want access to a system where they can:

  • See the logs of the app that they're making a change for and the other relevant apps
  • See the metrics of their app so they can adequately gauge performance impact

One way to achieve this is with ephemeral environments based on PR's. The idea is that the developer opens up a PR and then automatically a new environment is spun up based on provided defaults with the conditions that the environment is:

  • deployed in the same way that dev/stage/prod are deployed, just with a few key elements different
  • labeled correctly so that the NOC/Ops teams know the purpose of these resources
  • Integrated with logging/metrics and useful tags so that the engineer can easily see metrics for this given PR build

That sounds like a daunting task but through the use of Kubernetes, Helm, a CI Platform (GitHub Actions in this tutorial) and ArgoCD, you can make this a reality. Let's look at an example application leveraging all of this technology.

Example app

You can find all the code readily available in this GitHub Repo.

Pre-requisites Used in this Example

Tool Version
kubectl v1.21
Kubernetes Cluster v1.20.9
Helm v3.6.3
ArgoCD v2.0.5
kube-prometheus-stack v0.50.0

The example app that you’re going to deploy today is a Prometheus exporter that exports a custom metric with an overridable label set:

  • The `version` of the deployed app
  • The `branch` of the PR
  • The PR ID


Now that I've defined the goal, let's go a little more in-depth on how you'll get there. First, let's take a look at the PR pipeline in .github/workflows/pull_requests.yml:

name: 'Build image and push PR image to ghcr'
    types: [assigned, opened, synchronize, reopened]
      - main

    name: Build
    runs-on: ubuntu-latest
      - name: Checkout
        uses: actions/checkout@v2
      - name: Build image
        uses: docker/build-push-action@v1
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
          tags: PR-${{ github.event.pull_request.number }}

This pipeline runs on pull requests events to the main branch. So, when you open a PR, push a commit to an existing PR, reopen a closed PR, or assign it to a user, this pipeline will get triggered. It defines two workflows, the first of which is build. It's relatively straightforward: take the Dockerfile that lives in the root of your repo and build a container image out of it and tag it for use with GitHub Container Registry. The tag is the PR ID of the triggering pull request.

The second workflow is the one where we deploy to ArgoCD:

    needs: build
    runs-on: ubuntu-latest
      - name: Log into argocd
        run: |
          argocd login ${{ secrets.ARGOCD_GRPC_SERVER }} --username ${{ secrets.ARGOCD_USER }} --password ${{ secrets.ARGOCD_PASSWORD }}
      - name: Deploy PR Build
        run: |
          argocd app create sysadvent2021-pr-${{ github.event.pull_request.number }} \
            --repo \
            --revision ${{ github.head_ref }} \
            --path . \
            --upsert \
            --dest-namespace argocd \
            --dest-server https://kubernetes.default.svc \
            --sync-policy automated \
            --values values.yaml \
            --helm-set version="PR-${{ github.event.pull_request.number }}" \
            --helm-set name="sysadvent2021-pr-${{ github.event.pull_request.number }}" \
            --helm-set env[0].value="PR-${{ github.event.pull_request.number }}" \
            --helm-set env[1].value="${{ github.head_ref }}" \
            --helm-set env[2].value="sysadvent2021-pr-${{ github.event.pull_request.number }}"

This workflow runs a custom image that I wrote that wraps the argocd cli tool in a container and allows for arbitrary commands to be executed against an authenticated ArgoCD instance.

It then creates a Kubernetes object of kind: Application which is a CRD that ArgoCD deploys into your cluster to define where you want to pull the application from and how to deploy it (helm/kustomize etc..).

Putting it all together

Now, let's see this pipeline in action. First, head to your repo and create a PR against the main branch with some changes; it doesn't matter what the changes are as all PR events will trigger the pipeline.

You can see that my PR has triggered a pipeline which can be viewed here. Furthermore, you can see that this pipeline was executed successfully, so if I go to my ArgoCD instance, I would see an application with this PR ID.

So, if you are following along, now you have two deployments of this example app, one should show labels for the main branch, and one should show labels for the PR branch.

Let's verify by port-forwarding to each and see what you get back.

Main branch

First, let's check out the main branch application:

kubectl port-forward service/sysadvent2021-main 8000:8000 
Forwarding from -> 8000
Forwarding from [::1]:8000 -> 8000

As you can see, the branch is set to main with the correct version.

And if you check out the state of our Application in ArgoCD:

Everything is healthy!


Now let's check the PR deployment:

kubectl port-forward service/sysadvent2021-pr-1 8000:8000 
Forwarding from -> 8000
Forwarding from [::1]:8000 -> 8000

This one's labels are showing the branch and the version from the PR.

This pod returns:

And in ArgoCD:

Final thoughts

It really is that easy to get PR environments running in your company!


* Source Code Repo

1 comment :

Sri Krishna said...

Simple and effective approach. Thank you for sharing.
What would you suggest for a bit more complex env when there are Databases, caches and other applications as dependencies. The complexity and the with it the cost also go up.