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" ]
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"
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
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.
Checkout master: the first action checks-out your repository under $GITHUB_WORKSPACE, so our workflow can access it
*Update SHA: We store the value of the SHA of the commit that triggered the workflow in the variable $GITHUB_SHA
Install doctl: Optional step, because in this example we need to connect to the DigitalOcean API to execute commands on the Kubernetes Cluster
Build container image: Building the Docker image, the tag name of the image is the first 7 characters of $GITHUB_SHA
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.
Push image to Docker Hub
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.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
Save DigitalOcean kubeconfig: Choose the cluster we will deploy to.
Deploy to Kubernetes
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)
A follow up article that shares step-by-step for these would be nice!
Sure thing, I’ll try to write one soon.
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.