GitHub actions to deliver on kubernetes

22 Jan 2019 · Six minute read · on Gianluca's blog

News! Tech related notes are NOW published to ShippingBytes. See you there! I always felt this was not the right place for me to write consistently about tech and tools. So if you want to read more about that see you at the other side

Recently GitHub released a new feature called Actions. To me, it looks like the best implementation I can think of for serverless. I used AWS Lambda and API Gateway for some basic API, and I wrote a prototype of an application capable of running functions using containers called gourmet I don’t buy the fact that it will make my code easy to manage. At least not to write API or web applications.

That’s why I like what GitHub did because they used serverless for what I think it is designed for, extensibility.

GitHub Actions just like Lambda functions on AWS are a powerful and managed way to extend their product straightforwardly.

With AWS Lambda you can hook your code to almost whatever event happens: EC2 creations, termination, route53 DNS record change and a lot more. You don’t need to run a server, you load your code, and it just works.

Jess Frazelle wrote a blog post about “The Life of a GitHub Action, and I decided to try something I had my mind since a couple of weeks but it required a CI server, and it was already too much for me.

Time to time I like the idea to have a kubernetes cluster that I can use for the testing purpose, so I created a private repository that it is not ready to be open source because it is a mess with secrets inside and so on.

In any case, to give you an idea, this is the project’s folder:

├── .github
│   ├── actions
│   │   ├── deploy
│   │   │   ├── deploy
│   │   │   └── Dockerfile
│   │   └── dryrun
│   │       ├── Dockerfile
│   │       └── dryrun
│   └── main.workflow
└── kubernetes
    ├── digitalocean.yaml
    ├── external-dns.yaml
    ├── micro.yaml
    ├── namespaces.yaml
    ├── nginx.yaml
    └── openvpn.yaml

The kubernetes directory contains all the things I would like to install in my cluster. For every new push on this repository, I would like to check if it can be applied to the kubernetes cluster with the command kubectl apply -f ./kubernetes --dryrun and when the PR is merged the changes should get applied.

So I created my workflow in .github/main.workflow: ( I left some comment to make it understandable)

## Workflow defines what we want to call a set of actions.

## For every new push check if the changes can be applied to kubernetes ## using the action called: kubectl dryrun
workflow "after a push check if they apply to kubernetes" {
  on = "push"
  resolves = ["kubectl dryrun"]
}

## When a PR is merged trigger the action: kubectl deploy. To apply the new code to master.
workflow "on merge to master deploy on kubernetes" {
  on = "pull_request"
  resolves = ["kubectl deploy"]
}

## This is the action that checks if the push can be applied to kubernetes
action "kubectl dryrun" {
  uses = "./.github/actions/dryrun"
  secrets = ["KUBECONFIG"]
}

## This is the action that applies the change to kubernetes
action "kubectl deploy" {
  uses = "./.github/actions/deploy"
  secrets = ["KUBECONFIG"]
}

The secrets are an array of environment variables that you can use to set values from the outside. If your account has GitHub Action enabled there is a new Tag inside the Settings in every repository called “Secrets.”

You can set key-value pairs usable as you see in my workflow. For this example, I set the KUBECONFIG as the base64 of a kubeconfig file that allows the GitHub Action to authorize itself to my Kubernetes cluster.

Both actions are similar the first one is in the directory .github/actions/dryrun

├── .github
    ├── actions
        └── dryrun
            ├── Dockerfile
            └── dryrun

It contains a Dockerfile

FROM alpine:latest

## The action name displayed by GitHub
LABEL "com.github.actions.name"="kubectl dryrun"
## The description for the action
LABEL "com.github.actions.description"="Check the kubernetes change to apply."
## https://developer.github.com/actions/creating-github-actions/creating-a-docker-container/#supported-feather-icons
LABEL "com.github.actions.icon"="check"
## The color of the action icon
LABEL "com.github.actions.color"="blue"

RUN     apk add --no-cache \
        bash \
        ca-certificates \
        curl \
        git \
        jq

RUN curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.13.0/bin/linux/amd64/kubectl && \
  chmod +x /usr/bin/kubectl && \
  kubectl version --client

COPY dryrun /usr/bin/dryrun
CMD ["dryrun"]

As you can see to describe an action, you need just a Dockerfile, and it works the same as in docker. The CMD dryrun is the bash script I copied here:

#!/bin/bash

main(){
    echo ">>>> Action started"
    # Decode the secret passed by the action and paste the config in a file.
    echo $KUBECONFIG | base64 -d > ./kubeconfig.yaml
    echo ">>>> kubeconfig created"
    # Check if the kubernetes directory has change
    diff=$(git diff --exit-code HEAD~1 HEAD ./kubernetes)
    if [ $? -eq 1 ]; then
        echo ">>>> Detected a change inside the kubernetes directory"
        # Apply the changes with --dryrun just to validate them
        kubectl apply --kubeconfig ./kubeconfig.yaml --dry-run -f ./kubernetes
    else
        echo ">>>> No changed detected inside the ./kubernetes folder. Nothing to do."
    fi
}

main "$@"

The second action is almost the same as this one, the Dockerfile is THE same, so I am not posting it here, but the CMD looks like this:

#!/bin/bash

main(){
    # Decode the secret passed by the action and paste the config in a file.
    echo $KUBECONFIG | base64 -d > ./kubeconfig.yaml
     # Check if it is an event generated by the PR is a merge
    merged=$(jq --raw-output .pull_request.merged "$GITHUB_EVENT_PATH")
    # Retrieve the base branch for the PR because I would like to apply only PR merged to master
    baseRef=$(jq --raw-output .pull_request.base.ref "$GITHUB_EVENT_PATH")

    if [[ "$merged" == "true" ]] && [[ "$baseRef" == "master" ]]; then
        echo ">>>> PR merged into master. Shipping to k8s!"
        kubectl apply --kubeconfig ./kubeconfig.yaml -f ./kubernetes
    else
        echo ">>>> Nothing to do here!"
    fi
}

main "$@"

That’s everything, and I am thrilled!

There is nothing more to say other than “GitHub actions are amazing!”. They look well designed since day! The workflow file has a generator that even if I didn’t use it because I don’t like colors, it seems amazing. The secrets allow us to do integration with third-party services out of the box and you can use bash to do whatever you like! Let me know what you use them for on Twitter.

Something weird with this website? Let me know.