Continuous deployment - GitHub Actions on DigitalOcean's Kubernetes

After the Kubernetes cluster is created and configured go to your GitHub repository

I recommend using other than master branch, since we'd like to publish our changes only when the code is tested and ready to be released into the wild.

In our case we'll be using a GitHub branch called: do

So create a new branch: do and switch to that branch:

git branch do
git checkout do

Here we assume we have a file named Dockerfile in the branch. If you don't have one go ahead and create it. Test it locally and make sure everything works before your continue.

Some helpful commands for building a docker image from Dockerfile:

docker build -t myproject:0.1 . # build the image

docker tag myproject:0.1 somethingelse:0.1 # renaming docker tag

docker exec -ti myproject:0.1 /bin/bash # or /bin/sh to login to running docker container

docker ps -a # list running containers

docker logs abcdef # checking logs from a running docker containerID/Name

docker run -p 80:80 myproject:0.1 # running a docker container with port mapping 80->80

Create API token on DigitalOcean

Go to your DigitalOcean account and select API from the menu on the left. Click on "Generate new Token", name it e.g. "githubactionstoken". Copy the token (if you don't do so now you'll have to generate another token) and put in some temporary text file that you'll delete later.

Enable Container Registry on your DigitalOcean account

Navigate form the menu on the left to "Images" and then select "Container Registry".

Whatever you name your registry it's going to be referred from now on as "myregistry" in this blog post, while "myproject" defines your specific project name (good idea to keep the same name as GitHub repository name).

Enter Security Tokens on GitHub

Now go to your GitHub page, select "Settings" from the first page of your repository you'd like to set up continuous deployment.

Now click on "Secrets" and click on "New Secret". Add the secret named "DIGITALOCEAN_ACCESS_TOKEN":

If your code requires cloning and compiling dependencies from private repositories then add another Secret here with the Developer token value. You can use that later within docker image build. An example of building docker image with an input argument "token":

docker build . --file Dockerfile --tag registry.digitalocean.com/myregistryname/myproject:0.1 --build-arg token=abcdeff

I usually use this technique to build my go in the cloud for projects that have internal (private GitHub dependencies).

Start creating a Workflow on GitHub

Go to your project and select "Actions":

Scroll down until you find Docker Image. You might have to click on "More continuous integration workflows" if it's not near the top.

Click on Set up this workflow

This will open up an editor. Here is where the meat lives.

Copy paste this yaml file in and modify until it fits your needs:

name: Deploy development to DigitalOcean

on:
  push:
    branches: [ do ]

jobs:

  build:

    runs-on: ubuntu-18.04

    steps:
    - name: UPDATE SHA
      run: echo $GITHUB_SHA > $GITHUB_WORKSPACE/_meta
      
    - uses: actions/checkout@v2
    - name: Build the Docker image
      run: docker build . --file Dockerfile --tag registry.digitalocean.com/myregistry/myproject:$(echo $GITHUB_SHA | head -c7) --build-arg token=${{ secrets.GO_BUILD_TOKEN }}
    
    - name: Install Digitalocean doctl
      uses: digitalocean/action-doctl@v2
      with: 
        token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
      
    - name: Log in to DigitalOcean Container Registry with short-lived credentials
      run: doctl registry login --expiry-seconds 600
    
    - name: Push image to DigitalOcean Container Registry
      run: docker push registry.digitalocean.com/myregistry/myproject
    
    - name: Update deployment file
      run: TAG=$(echo $GITHUB_SHA | head -c7) && sed -i 's|<IMAGE>|registry.digitalocean.com/myregistry/myproject:'${TAG}'|' $GITHUB_WORKSPACE/kubernetes-do.yaml
    
    - name: Save DigitalOcean kubeconfig
      run: doctl kubernetes cluster kubeconfig save mydigitaloceanclustername

    - name: Store DO authentication token as a secret in cluster
      run: doctl registry kubernetes-manifest | kubectl apply -f -
    
    - name: Deploy to DigitalOcean Kubernetes
      run: kubectl apply -f $GITHUB_WORKSPACE/kubernetes-do.yaml

Go ahead and commit it. It can be committed to master (if you branched from master then it should be easy to merge to your branch: do).

Before we commit or push any changes to docker repository we need to create our deployment descriptor for kubernetes. So let's create kubernetes-do.yaml. This can be any type of deployment. Here i show an example of DaemonSets. I prefer deploying with DaemonSets over Deployment, because a DaemonSet ensures that all (or some) Nodes run a copy of a Pod. As nodes are added to the cluster, Pods are added to them. As nodes are removed from the cluster, those Pods are garbage collected. Deleting a DaemonSet will clean up the Pods it created.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: myproject
spec:
  selector:
    matchLabels:
      app: myproject
  template:
    metadata:
      labels:
        app: myproject
    spec:
      imagePullSecrets:
      - name: registry-myregistry
      containers:
      - name: myproject
        image: <IMAGE>
        ports:
        - containerPort: 80
        - containerPort: 8090
        resources:
          requests:
            cpu: 50m
        volumeMounts:
        - name: keys
          mountPath: /var/run/keys
          readOnly: true
        workingDir: /app
        command: ["/bin/sh","-c"]
        args: ["nginx && ./main --config /var/run/keys/conf.yaml"]
      volumes:
      - name: keys
        secret:
          secretName: keys
---
apiVersion: v1
kind: Service
metadata:
  name: myproject-service
spec:
  type: LoadBalancer
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 5000
  selector:
    app: myproject

As you can see you need to modify this deployment descriptor to your needs. One important thing to remember (regardless if your using Deployment or DaemonSets) is to ass this to the kubernetes-do.yaml:

 spec:
      imagePullSecrets:
      - name: registry-myregistry

If you don't add it you will get "Authentication required" and ImagePullBackoff error.

Now you can commit the changes. But don't push yet. We still need to merge master with our do branch.

git add .
git commit -am "Added digitalocean deployment descriptor for CD"
git checkout master
git pull origin master
git checkout do
git merge master

We should see in our folder structure new folder called .github. Under that there should be a sub-folder called workflows with containing file: docker-image.yml.

The docker-image.yml is just a simple set of command. They're equivalent as we would make a deployment from our local computer. It's just that syntax is a bit different.

I'd recommend trying to manually follow step by step the commands to verify that everything works as it should:

docker build . --file Dockerfile --tag registry.digitalocean.com/myregistry/myproject:mytest01 --build-arg token=abcdef # --build-arg is specific to private GitHub repos here

doctl registry login --expiry-seconds 600

docker push registry.digitalocean.com/myregistry/myproject:mytest01

In kubernetes.yml replace <IMAGE> with your image: registry.digitalocean.com/myregistry/myproject:mytest01

doctl kubernetes cluster kubeconfig save mydigitaloceanclustername

doctl registry kubernetes-manifest | kubectl apply -f -

kubectl apply -f kubernetes-do.yaml

When this works, revert the changes you've made for manual deployment testing and push the do branch to GitHub. Then go to GitHub actions and watch the magic. Or pull some hair out if it doesn't work for you.

I recommend manually installing LoadBalancer directly from your command line and potential secrets. Those really to be deployed only once. After that they don't have to be messed with.

kubect apply -f do-service.yml

Some additional resources: