How to deploy an API to a Kubernetes Cluster with a Github Actions CI/CD Workflow
Tags: kubernetes, github, docker
Published: 2021-02-18 14:00:25

With single page applications becoming the standard of web development, many of us write APIs with which they communicate to display information for the end user.
 
One can either manually push the API code to a server, build it and serve it, or automate these steps using modern tools like Github Actions.
Moreover, it is easier to build a Docker image of the project and deploy it to an orchestration system like Kubernetes, something that will allow the API to scale and be more resilient.

In this article we will see how you can navigate all these tools to automate your deployment to a Kubernetes Cluster using Github Actions, and we will assume that your project has a **Dockerfile** in the root directory that expose the API to Port 8080, here is an example:

```dockerfile
FROM node:12

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./

RUN npm install
# If you are building your code for production
# RUN npm ci --only=production

# Bundle app source
COPY . .

EXPOSE 8080
CMD [ "node", "server.js" ]
```
# Kubernetes Deployment File

For this article, let's imagine we have a nodejs API called **tutorial-api**, this API needs a couple of environment variables to run, like *PORT*, *DB_NAME*, etc.

```yaml
apiVersion: v1
kind: Service
metadata:
  name: tutorial-api
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: tutorial-api
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tutorial-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tutorial-api
  template:
    metadata:
      labels:
        app: tutorial-api
    spec:
      containers:
        - name: tutorial-api
          image: <IMAGE>
          resources:
            requests:
              memory: "256Mi"
              cpu: "300m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          ports:
            - containerPort: 8080
          env:
            - name: NODE_ENV
              value: "production"
            - name: PORT
              value: "$PORT"
            - name: DB_NAME
              value: "$DB_NAME"
            - name: DB_USERNAME
              value: "$DB_USERNAME"
            - name: DB_PASSWORD
              value: "$DB_PASSWORD"
            - name: DB_HOST
              value: "$DB_HOST"
            - name: DB_PORT
              value: "$DB_PORT"
```

This file should be located in the root of the project too.

# Github Actions

Github Actions allows you to execute a list of actions, once a GitHub event is triggered. 
For example, you may want to create a workflow for greeting a new contributor to your repository, or building a Docker image and pushing it to your favorite Docker container registry once you push code to the master branch, you can find a list of triggers here : [Events that trigger workflows
](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows)

CI/CD happens to be one of many types of workflow that you can create on GitHub Actions, and this is what we're going to do in this section.

First, create a new file called *ci.yaml* inside **.github/workflows**

For the purpose of this article, we will deploy the Docker container to a Kubernetes Cluster hosted on DigitalOcean, that's why you will find a reference to DigitalOcean's CLI tool *doctl*, nonetheless, the steps shouldn't change much with other cloud providers.

Here is an example of a Github Actions workflow:

```yaml
on:
  push:
    branches:
      - master
jobs:
  build:
    name: Build, push, and deploy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout master
        uses: actions/checkout@master

      - name: Update SHA
        run: echo $GITHUB_SHA > $GITHUB_WORKSPACE/_meta

      - name: Install doctl
        uses: digitalocean/action-doctl@v2
        with:
          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
      
      - name: Build container image
        run: docker build -t docker_fake_repo/tutorial-api:$(echo $GITHUB_SHA | head -c7) .

      - name: Docker Login
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
        run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD

      - name: Push image to Docker Hub
        run: docker push docker_fake_repo/tutorial-api

      - name: Update deployment file
        run: TAG=$(echo $GITHUB_SHA | head -c7) && sed -i 's|<IMAGE>|docker_fake_repo/tutorial-api:'${TAG}'|' $GITHUB_WORKSPACE/deployment.yml
      
      - name: Replace Environment Variables
        uses: danielr1996/envsubst-action@1.0.0
        env:
          PORT: ${{ secrets.PORT }}
          DB_HOST: ${{ secrets.DB_HOST }}
          DB_USERNAME: ${{ secrets.DB_USERNAME }}
          DB_PORT: ${{ secrets.DB_PORT }}
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
          DB_NAME: ${{ secrets.DB_NAME }}
        with:
          input: deployment.yml
          output: deploy.yml

      - name: Save DigitalOcean kubeconfig
        run: doctl kubernetes cluster kubeconfig save $CLUSTER_NAME
        env: 
          CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }}
     
      - name: Deploy to Kubernetes
        run: kubectl apply -f deploy.yml
        
      - name: Verify deployment
        run: kubectl rollout status deployment/geerd-drive
```

Let's start with the first block of this file :

```
on:
  push:
    branches:
      - master
```
It means that we want to execute the jobs present in this file once code is pushed to the master branch.

In our case, we will run only one job, that we will call **build, push, and deploy**, this job will run on ubuntu, and will execute couple of steps, each step either uses actions provided for by the community on GitHub's Marketplace, or you can run commands yourself.

1. **Checkout master**: the first action checks-out your repository under $GITHUB_WORKSPACE, so our workflow can access it

2. **Update SHA*: We store the value of the SHA of the commit that triggered the workflow in the variable *$GITHUB_SHA*

3. **Install doctl**: Optional step, because in this example we need to connect to the DigitalOcean API to execute commands on the Kubernetes Cluster

4. **Build container image**: Building the Docker image, the tag name of the image is the first 7 characters of $GITHUB_SHA

5. **Docker Login**: In this example, we will log in to Docker Hub to push our image. The secrets are created under the *settings* tab of your repository on GitHub, you can add secrets that GitHub will make accessible to the workflow, you should store environment variables and sensitive values there.

6. **Push image to Docker Hub**

7. **Update deployment file**: Previously, in the *deployment.yml* file, the image key `image: <IMAGE>` had the value <IMAGE>, we will replace this latter with the tag we just pushed to our container registry.

8. **Replace Environment Variables**: If your *deployment.yml* has environment variables that you Docker container needs, you can use this step to inject the values stored in the repository secrets, which will output a new file *deploy.yml*

9. **Save DigitalOcean kubeconfig**: Choose the cluster we will deploy to.

10. **Deploy to Kubernetes**

11. **Verify Deployment**

Now that you deployed your project, you need to add an ingress to Kubernetes, NGINX for example, then set up a let's encrypt ssl certificate using cert-manager, so that your API is securely exposed online.