Blog

How to Deploy to DigitalOcean Kubernetes with GitHub Actions

GitHub Actions were one of the most exciting things launched by our friends at GitHub last year. Now that they're in public beta, people are using them to build awesome stuff, from running tests and linters to more lighthearted use cases. With the DigitalOcean doctl Action, you can interact with all of your DigitalOcean resources.

One of the most powerful aspects of GitHub Actions is the ability to compose workflows using multiple Actions to accomplish complicated tasks. In this post, we’ll show what that looks like in practice.

Using multiple Actions, including ones for DigitalOcean and Docker, we’ll build a simple continuous delivery pipeline that deploys an application to a DigitalOcean Kubernetes cluster on push to the master branch of a GitHub repository. Along the way, we’ll dig into some of the details of working with GitHub Actions.

Creating Your Workflow

The first step in using GitHub Actions is to create a workflow. You can do this from the Actions tab of your GitHub repository. This is where you define what will trigger a run of your workflow.

Nearly any GitHub event can be used from a new PR being opened to a new release being tagged. In our example, we’ll be using the “push” event so that our workflow is executed when a new commit is pushed to the master branch.

This will create a new file in your repository at .github/main.workflow with the following contents:

workflow "New workflow" {  
  on = "push"
}

This highlights an important aspect of GitHub Actions. While workflows can be created and edited using the GitHub GUI, they are configured in code using HCL – the same format used by tools like HashiCorp’s Terraform. Each change made in the GUI is mirrored in the file and will be committed to the repository. This allows you to edit your workflows offline and collaborate on them via pull requests. For the rest of this post, we’ll mostly be showing the examples as code so that it is easier to see the details of how all the pieces fit together.

Defining Your First Action

Our repository contains a Dockerfile in its root directory that defines how to build and run our application. In order to keep our example simple and focused on the workflow rather than the details of the application, our “application” is just a static site served by NGINX. The first Action block that we’ll define will build a container image from this Dockerfile:

action "Build Docker image" {  
  uses = "actions/docker/cli@master"
  args = ["build", "-t", "andrewsomething/static-example:$(echo $GITHUB_SHA | head -c7)", "."]
}

The first line is just a label for the block; the interesting bits are inside. The uses line specifies the Action that will be run. The path used to reference the Action matches its location on GitHub. For instance, here we are using the Docker CLI Action which can be found in the cli/ directory of the github.com/actions/docker repository. This Action is a wrapper around the same Docker CLI tool that you would use on the command line locally.

If you have ever built a Docker image, the next line should look familiar. The args line is just what it sounds like. Here we can pass arguments to the Docker command needed to build the image.

When we build the image, we are tagging it so that we can push it to Docker Hub. If you are following along, make sure to replace "andrewsomething" with your own username. You probably noticed that we are using the $GITHUB_SHA environment variable as part of the tag. Its value is the SHA of the commit that triggered the workflow. It is one of a number of variables made available in the Action’s runtime environment.

Using Secrets

Often you will need to store secrets that your Action will require in order to run. Our next Action block demonstrates this. To push the image we built to Docker Hub, we will first need to log in. Using the secrets line of an Action block, we can securely pass the needed information as environment variables:

action "Docker Login" {  
  uses = "actions/docker/login@master"
  secrets = ["DOCKER_USERNAME", "DOCKER_PASSWORD"]
}

The contents of these secrets can be configured in the GitHub GUI:

While we’re here, we will also specify a DIGITALOCEAN_ACCESS_TOKEN secret using a personal access token generated from the API section of the DigitalOcean Control Panel. We’ll be using this in a later step.

Specifying Dependencies

In the next step of our workflow, we’ll push the Docker image to Docker Hub. This looks similar to the previous Action blocks, but this time we have a new line:

action "Push image to Docker Hub" {  
  needs = ["Docker Login", "Build Docker image"]
  uses = "actions/docker/cli@master"
  args = ["push", "andrewsomething/static-example"]
}

Multiple Action blocks may run in parallel. In this case, we need to ensure that the Docker image has been built and that we have logged into Docker Hub before we can push it there. So we’ve specified a needs line referencing the labels for those two Action blocks so that they will be executed in the correct order.

Accessing Your Workspace

The config directory of our repository contains a Kubernetes YAML file specifying our deployment. As committed in git, there is only a placeholder for the Docker image that we want to deploy. It will need to be updated to point to the image we’ve tagged and pushed to Docker Hub. To do this, we’ll use the Shell Action provided by GitHub. Based on Debian, it includes all the standard UNIX tools you’d expect. Here we’re using sed to update the contents of our deployment file:

action "Update deployment file" {  
  needs = ["Push image to Docker Hub"]
  uses = "actions/bin/sh@master"
  args = ["TAG=$(echo $GITHUB_SHA | head -c7) && sed -i 's|<IMAGE>|andrewsomething/static-example:'${TAG}'|' $GITHUB_WORKSPACE/config/deployment.yml"]
}

This demonstrates another important environment variable available to you, $GITHUB_WORKSPACE. This directory contains a copy of the repository that triggered the workflow. Changes made here will persist from one step to the next.

Deploying to DigitalOcean Kubernetes

In our next step, we’ll retrieve the credentials needed to access our Kuberenetes cluster using the DigitalOcean doctl Action. This Action enables you to use any doctl sub-command just like from the command line giving you access to all of your DigitalOcean resources. Using the DIGITALOCEAN_ACCESS_TOKEN secret we configured earlier, we will save the kubeconfig file for our cluster:

action "Save DigitalOcean kubeconfig" {  
  uses = "digitalocean/action-doctl@master"
  secrets = ["DIGITALOCEAN_ACCESS_TOKEN"]
  args = ["kubernetes cluster kubeconfig show actions-example > $HOME/.kubeconfig"]
}

Next, we’ll configure an Action block using kubectl to apply the actual deployment:

action "Deploy to DigitalOcean Kubernetes" {  
  needs = ["Save DigitalOcean kubeconfig", "Update deployment file"]
  uses = "docker://lachlanevenson/k8s-kubectl"
  runs = "sh -l -c"
  args = ["kubectl --kubeconfig=$HOME/.kubeconfig apply -f $GITHUB_WORKSPACE/config/deployment.yml"]
}

You’ll notice something new in this block demonstrating just how flexible GitHub Actions can be. In this case, the uses line is not specifying an Action on GitHub like our previous steps. Instead, it is referencing a container image hosted on DockerHub. This opens up a whole world of tools not packaged as Actions for use in your workflow.

Verifying the Deployment

In the final step of our workflow, using the same kubectl Docker image, we will check on the status of our deployment. The kubectl rollout status command returns a zero exit code when a deployment was successful:

action "Verify deployment" {  
  needs = ["Deploy to DigitalOcean Kubernetes"]
  uses = "docker://lachlanevenson/k8s-kubectl"
  runs = "sh -l -c"
  args = ["kubectl --kubeconfig=$HOME/.kubeconfig rollout status deployment/static-example"]
}

If the deployment fails, it returns a non-zero exit code. So that the status of our workflow will correctly reflect whether or not our application was successfully deployed, we will return to our workflow block from the first step and add a new resolves line:

workflow "New workflow" {  
  on = "push"
  resolves = ["Verify deployment"]
}

Since our "Verify deployment" Action depends on all of our other Actions, we can specify it here alone. If our workflow contained completely independent Actions, we’d want to include each of them here.

Bringing It All Together

Now that we’ve successfully configured our workflow, each time a commit is pushed to the master branch of our repository it will be triggered. Each step will run in the order that we specified. The GitHub GUI will display the progress:

With everything green, our site is now live: https://doctl-action.do-api.dev/

You can find the complete workflow file with the full end-to-end example on GitHub.

Next Steps

GitHub Actions allow you to craft powerful workflows integrating multiple Actions to accomplish complicated tasks. In this post, we’ve only scratched the surface of what they can do. With the doctl Action, you can incorporate your DigitalOcean resources into your workflows. Here are a few resources to help you get started building your own:

In this post we mostly focused on the GitHub Actions side of the equation. If you’re looking for more info on working with Kubernetes, the DigitalOcean Kubernetes Resource Center is a great place to start.

We’d love to know how you are using GitHub Actions. So let us know in the comments below! Are there other Actions for DigitalOcean that you’d like to see? Share your feedback and requests by opening an issue on GitHub.