GuidesChangelogDiscussions
Log In

Continuous delivery with Ketch, GitHub Actions, and k3d

Can we combine the simplicity of deploying applications with Ketch with GitHub Actions and accomplish a fully automated continuous delivery pipeline?

Here's what we'll do.

We'll create GitHub Actions that will fully automate all the tasks starting from creating a pull request all the way until a release is deployed to production.

We'll create two pipelines. One will be executed whenever we create a pull request or make any change to existing PRs. The other will run as a result of pushing changes to the mainline.

The PR pipeline will perform the following steps.

  1. Build container images and push them to a registry
  2. Create a temporary (and free) Kubernetes cluster
  3. Deploy a release candidate to the cluster
  4. Run tests to validate whether the PR meets the quality standards

The mainline pipeline will perform the following steps.

  1. Build container images and push them to a registry
  2. Connect to the production cluster
  3. Deploy the release to the cluster

The creation of a temporary cluster will be done through k3d, while Ketch will handle deployments.

Let's go.

Prerequisites

Before we begin, we'll need a Kubernetes cluster with Istio. I'll leave it to you to create a cluster any way you usually do unless you already have one you'd like to use for the exercises that follow. Similarly, I will assume that you know how to install Istio. If you don't, a simple istioctl install should do.

Finally, we'll need an environment variable with the IP through which Istio Gateway can be accessed. The variable should be called ISTIO_HOST and can be exported with the command that follows.

export ISTIO_HOST=$(kubectl --namespace istio-system get svc istio-ingressgateway --output jsonpath="{.status.loadBalancer.ingress[0].ip}")

NOTE: Some Kubernetes clusters (e.g., EKS) do not create an externally accessible IP but a hostname. In that case, the commands should be as follow.

export ISTIO_HOSTNAME=$(kubectl --namespace istio-system get service istio-ingressgateway --output jsonpath="{.status.loadBalancer.ingress[0].hostname}")

export ISTIO_HOST=$(dig +short $INGRESS_HOSTNAME)

Finally, let's confirm that the ISTIO_HOST variable has the correct value.

echo $ISTIO_HOST

If the output is empty, the external load balancer was probably not yet created. If that's the case, please wait for a while and repeat the commands.

NOTE: If the output continues having more than one IP, choose one of them and execute export ISTIO_HOST=[...] with [...] being the selected IP.

Now we are ready to set up Ketch.

Setup

We'll be deploying our applications to temporary clusters while working with pull requests and the production environment after merging PRs to the mainline. GitHub Actions will perform all the necessary steps for the former case (temp clusters). However, for deploying to the production cluster, we'll need to install Ketch manually.

We'll be pushing container images of our releases to Docker Hub and, for that, we'll have to create a few GitHub secrets so that Actions can authenticate.

The sample application is available from the GitHub repository vfarcic/github-actions-ketch-demo, so let's start the setup by forking it.

NOTE: we'll use GitHub CLI to fork the repo, as well as for a few other operations. Please watch GitHub CLI - How to manage repositories more efficiently if you are not already familiar with it.

gh repo fork vfarcic/github-actions-ketch-demo --clone

cd github-actions-ketch-demo

Next, we'll create the secrets that will allow GitHub Actions to authenticate with Docker Hub and our production cluster.

Please visit Managing access tokens to create a token if you do not already have one at hand.

NOTE: It is not mandatory to use Docker Hub to push container images. We are using it mainly for simplicity reasons since most people have an account in Docker Hub.

# Replace `[...]` with the Docker Hub token
export DOCKERHUB_TOKEN=[...]

# Replace `[...]` with the Docker Hub user
export DOCKERHUB_USERNAME=[...]

gh secret set DOCKERHUB_TOKEN -b$DOCKERHUB_TOKEN

gh secret set DOCKERHUB_USERNAME -b$DOCKERHUB_USERNAME

There is one more GitHub secret we should create.

# Replace the value with the path to Kube config if not using the default path
export KUBECONFIG_PATH=~/.kube/config

gh secret set KUBE_CONFIG < $KUBECONFIG_PATH

Now that we have the secrets set up, we can install Ketch and its dependencies into our (simulation of the) production cluster.

kubectl apply --filename https://github.com/jetstack/cert-manager/releases/download/v1.0.3/cert-manager.yaml --validate=false

kubectl apply --filename cluster-issuer.yaml

kubectl apply --filename https://github.com/shipa-corp/ketch/releases/download/v0.2.1/ketch-controller.yaml

There is only one more set of setup actions left to perform. We should create a Ketch Pool and an application in the cluster where we just installed Ketch.

ketch pool add production --namespace production --ingress-service-endpoint $ISTIO_HOST --ingress-type istio

ketch app create github-actions-demo --pool production

There's one more thing we need to do. We need to enable Actions for the forked repo.

gh repo view --web

Select the Actions tab and click the I understand my workflows, go ahead and enable them.

That's it. Now we are ready to use GitHub Actions for automating our continuous delivery processes.

Exploring Pull Requests Actions

Let's say that we want to work on a new feature. Typically, we would create a new feature branch, write some code, and, once we're ready, push the changes to the branch. We can accomplish all that through the commands that follow.

git checkout -b my-feature

echo "Is this a feature?" \
    | tee something.txt

git add .

git commit -m "My feature"

git push --set-upstream origin my-feature

Before we make a pull request, we might want to explore the actions that will be

cat .github/workflows/pr.yaml

The output is as follows.

name: pr
on:
  pull_request:
    branches:
    - master
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/[email protected]
      with:
        submodules: recursive
    - name: Set up QEMU
      uses: docker/[email protected]
    - name: Set up Docker Buildx
      uses: docker/[email protected]
    - name: Login to DockerHub
      uses: docker/[email protected] 
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    - name: Build and push container image
      uses: docker/[email protected]
      with:
        push: true
        tags: |
          ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest
          ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:pr-${{ github.event.pull_request.number }}
    - name: Create a cluster
      uses: AbsaOSS/[email protected]
      with:
        cluster-name: test
        args: >-
          --config k3d.yaml
    - name: Setup cluster
      run: |
        ./ketch-setup.sh
        ./ketch pool add test
    - name: Deploy the release
      run: |
        ./ketch app create github-actions-demo --pool test
        ./ketch app deploy github-actions-demo --image ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:pr-${{ github.event.pull_request.number }}
        sleep 3
        kubectl --namespace ketch-test rollout status deployment github-actions-demo-web-1
    - name: Test
      run: |
        kubectl --namespace ketch-test run test --image alpine -i --restart Never -- sh -c "apk add --update curl && curl github-actions-demo-web-1"

As I already mentioned, we will not go deep into how GitHub Actions work. If you're not familiar with it, please watch Github Actions review and tutorial. Instead, we'll only go through the steps that the pipeline will execute.

First, we are checking out the code of the application (Checkout). Further on, we are setting up Docker Buildx (Set up QEMU and Set up Docker Buildx), logging into Docker Hub (Login to DockerHub), and building and pushing the image of the release candidate (Build and push container image).

Once we have a release candidate image, we are creating a temporary cluster using k3d (Create a cluster) (more info can be found in K3d - How to run Kubernetes cluster locally using Rancher k3s.

Since we are creating a new temporary cluster for every execution of GitHub actions, the next step is to install Ketch and create a pool (Setup cluster). The commands to install Ketch and its dependencies are in the ketch-setup.sh script.

Once the cluster is up and running and set up with the tools we need, we will create and deploy the release candidate (Deploy the release).

Finally, all that's left is to run tests (Test). We are not running "real" tests in this demo (I was too lazy to write them). Instead, we are "simulating" them by sending a curl request to the newly deployed application.

Let's see what happens if we create a PR.

Running Pull Request Actions

Now that we know what will happen if we create a pull request, we should probably see it in action.

gh pr create --title "My feature" --body "Read the title"

The output should contain the address of the newly created PR. Please open it in your favorite browser.

We should see that the GitHub Action is running. Feel free to click the Details link for more info.

Now we have a pipeline that builds container images, pushes them to a registry, creates a temporary cluster, deploy a Ketch application, and runs tests. Let's explore what happens next.

Running Mainline Actions

Assuming that we did code review and performed whichever other actions we might normally do with PRs, we should be ready to merge it to the mainline. I already prepared an Action for that.

cat .github/workflows/main.yaml
name: main
on:
  push:
    branches:
    - master
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/[email protected]
      with:
        submodules: recursive
    - name: Set up QEMU
      uses: docker/[email protected]
    - name: Set up Docker Buildx
      uses: docker/[email protected]
    - name: Login to DockerHub
      uses: docker/[email protected] 
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    - name: Build and push container image
      uses: docker/[email protected]
      with:
        push: true
        tags: |
          ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest
          ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:${{ github.sha }}
    - name: Setup Kube config
      uses: azure/[email protected]
      with:
        method: kubeconfig
        kubeconfig: ${{ secrets.KUBE_CONFIG }}
    - name: Deploy the app
      run: |
        curl -Lo ketch https://github.com/shipa-corp/ketch/releases/download/v0.2.1/ketch-linux-amd64
        chmod +x ketch
        ./ketch app deploy github-actions-demo --image ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:$GITHUB_SHA

The first part of that Action is the same as the one we are using with pull requests. It sets up Docker BuildX, logs into Docker Hub, and builds and pushes a release.

Further on, we are setting up Kube config (Setup Kube config) using azure/k8s-set-context.

*NOTE: The Setup Kube config action will work for any Kubernetes platform, except for EKS and GKE. Neither of those use "normal" Kube config. GKE needs access to gcloud binary, and EKS requires the authenticator. If you are using one of those, you will have to replace that step with get-gke-credentials or one of the EKS Actions.

Finally, the last step (Deploy the app) is downloading ketch CLI and deploying the application.

Let's merge the PR and see whether the Action works properly.

gh pr merge

Follow the instructions until the PR is merged. A new Action should be triggered. We can find it and monitor progress by going to the repo and selecting the Actions tab.

Once the action is finished, we can confirm that the application was indeed deployed to the "real" cluster.

ketch app info github-actions-demo

The output should be similar to the one that follows.

Application: github-actions-demo
Pool: production
Address: http://github-actions-demo.40.88.207.125.shipa.cloud

No environment variables.

DEPLOYMENT VERSION    IMAGE                                                                   PROCESS NAME    WEIGHT    STATE          CMD
1                     vfarcic/github-actions-demo:c7f47a7fc6d426b9ffa1093ea52c151e9271f8cb    web             100%      1 deploying    /docker-entrypoint.sh nginx -g daemon off;

If you are still not convinced that everything is working correctly, you can open the Address available in the output.