Blog

You build it, you run it

Cover Image for You build it, you run it
Vicens Fayos

There isn't much to say about the title — you should already have a clear idea of what this is about. Developers should be self sufficient and set all the required data for deploying successfully the app they develop.

In this post, we’ll walk through a workflow that a typical developer might follow.

As an example, we’ll use a Next.js project I created as a "hello world" demo. You can find it here: hello-world-nextjs-k8s-deployment. We’ll be using GitHub Actions to automate the process. 😊

**Note:** We assume you have access to a Kubernetes cluster capable of handling and deploying your project. A few months ago, I published some guides on how to set this up. While they may be slightly outdated, they’re still worth checking out. If that doesn't help, you can always turn to LLMs for assistance. Good luck!

To make this process work, you’ll need to complete the following steps:

1. Dockerize your project

Prepare a Dockerfile for your project to ensure it can be built and run inside a container.

2. Create a build workflow with GitHub Actions

Set up a GitHub Actions workflow that automatically builds the Docker image for every commit to the `main` branch and pushes it to Docker Hub.

3. Create a deployment workflow

Set up another GitHub Actions workflow that runs on demand. This workflow will instruct the Kubernetes cluster to pull the latest Docker image and deploy it.

4. Coordinate with DevOps

Work with your DevOps team to ensure that the deployment is accessible from the internet.

Dockerize your Project

In this case, it’s pretty straightforward, and we can go with the "good enough" LLM approach. Let’s use ChatGPT for this https://chatgpt.com/share/67601353-a42c-800f-aee9-805ef2d7b33d

Create a Build Workflow

This is part of the CI, so you don't need to do much here. I’ve pasted the one I use. You can either use an LLM to generate it (although this might result in an updated version of some plugins) or use Docker plugins, which you can find here or here.

The file will live in the same repository as the code. Since it’s part of the project, there’s no reason for it to be stored elsewhere.

name: Build Docker Image

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    env:
      DOCKER_HUB_REPOSITORY: ${{ secrets.DOCKER_HUB_REPOSITORY }}
      IMAGE_NAME: "nextjs-app"
      IMAGE_TAG: ${{ github.sha }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}

      - name: Logout from Docker Hub
        run: docker logout

Add the secrets

Since we use Docker Hub as our artifact repository, we need to have a repository (which you will need to create), a user, and a token. You can find these at [hub.docker.com](https://hub.docker.com).

DOCKER_HUB_REPOSITORY: xxx
DOCKER_HUB_USERNAME: xxx
DOCKER_HUB_ACCESS_TOKEN: xxx

Create Deploy Workflow

Once the image is built and pushed to Docker Hub, we create another workflow to instruct the Kubernetes cluster to download this image and deploy it.

As before, the file will reside in the same repository as the code.

name: Deploy to Kubernetes on EC2

on:
  workflow_dispatch:
    inputs:
      IMAGE_TAG:
        description: "Tag for the Docker image"
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DOCKER_HUB_REPOSITORY: ${{ secrets.DOCKER_HUB_REPOSITORY }}
      IMAGE_NAME: "nextjs-app"
      IMAGE_TAG: ${{ github.event.inputs.IMAGE_TAG }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install kubectl
        run: |
          curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
          chmod +x ./kubectl
          sudo mv ./kubectl /usr/local/bin/kubectl

      - name: Configure kubeconfig for the cluster
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBECONFIG_CONTENT }}" | base64 -d > ~/.kube/config
          chmod 600 ~/.kube/config

      - name: Verify kubectl connection
        run: |
          kubectl config view
          kubectl cluster-info
          kubectl get nodes

      - name: Prepare deployment file
        run: |
          # Print environment variables
          echo "DOCKER_HUB_REPOSITORY: ${{ env.DOCKER_HUB_REPOSITORY }}"
          echo "IMAGE_TAG: ${{ env.IMAGE_TAG }}"
          
          # Print the deployment file before replacement
          echo "Before replacement:"
          cat deployment.yml
          
          # Do the replacement
          sed -i "s|image: PLACEHOLDER_IMAGE|image: ${{ env.DOCKER_HUB_REPOSITORY }}:${{ env.IMAGE_TAG }}|" deployment.yml
          
          # Print the deployment file after replacement
          echo "After replacement:"
          cat deployment.yml
          
          # Verify the exact image string that will be used
          echo "Expected image string: ${{ env.DOCKER_HUB_REPOSITORY }}:${{ env.IMAGE_TAG }}"
          grep "image:" deployment.yml

      # Add this new step to actually apply the deployment
      - name: Apply deployment
        run: |
          kubectl apply -f deployment.yml
          kubectl get deployments
          kubectl get pods

You need an additional secret containing the configuration required to use `kubectl` remotely.

secrets.KUBECONFIG_CONTENT

You can obtain the content using this LLM chat: [chatgpt.com/share/6760237b-8838-800f-8900-ef2d42e4eb86](https://chatgpt.com/share/6760237b-8838-800f-8900-ef2d42e4eb86) or, alternatively, you may need to ask your DevOps team or manager for it.

Additionally, you’ll need a Kubernetes `deployment.yml` descriptor file in place.

Here’s mine:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-app-deployment
  labels:
    app: nextjs-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nextjs-app
  template:
    metadata:
      labels:
        app: nextjs-app
    spec:
      containers:
      - name: nextjs-app-container
        image: PLACEHOLDER_IMAGE # PLACEHOLDER_IMAGE
        resources:
          requests:
            cpu: "0.1"           
            memory: "512Mi"
          limits:
            cpu: "0.1"
            memory: "512Mi"
        ports:
        - containerPort: 3000 

You see this PLACEHOLDER_IMAGE, right? This is because, during the CD process, the content of this file will be modified "on the fly" to instruct Kubernetes to pull the correct image. This modification happens in the job mentioned above.

With this in place, you can run this job directly from the **Actions** tab in GitHub, passing the image tag you want Kubernetes to deploy.

And your project will be successfully deployed in the Kubernetes cluster after align with your DevOps.


Más historias

Cover Image for D4D - Concise Technical Overview of Kubernetes for Managers

D4D - Concise Technical Overview of Kubernetes for Managers

Kubernetes is an open-source platform designed to automate the deployment, scaling, and management of containerized applications.

Vicens Fayos
Cover Image for D4D - You Build it You Run it - Add Database Support

D4D - You Build it You Run it - Add Database Support

Integrating database support into your Next.js app with Kubernetes is simpler than you think!

Vicens Fayos