DEV Community

Othmane Sabih
Othmane Sabih

Posted on • Edited on

How to deploy an API to a Kubernetes Cluster with a Github Actions CI/CD Workflow

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:

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" ]
Enter fullscreen mode Exit fullscreen mode

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.

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"
Enter fullscreen mode Exit fullscreen mode

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

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:

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
Enter fullscreen mode Exit fullscreen mode

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

on:
  push:
    branches:
      - master
Enter fullscreen mode Exit fullscreen mode

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 , 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.

Top comments (3)

Collapse
 
jpcorry profile image
John Corry

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.

A follow up article that shares step-by-step for these would be nice!

Collapse
 
othpwn profile image
Othmane Sabih

Sure thing, I’ll try to write one soon.

Collapse
 
juliantellez profile image
Julian Tellez

nice article! Cluster access is a security concern and it doesn't follow the least privilege principle. You could setup a service account and patch apply your changes instead. My two cents.