How to integrate Next JS, Gitlab, and Kubernetes

Julian Parra

Julian Parra

  Blog

Software engineer

  Colombia

What is this post about?

Gitlab allows us to carry out the software development life cycle through the automation of continuous integration, and continuous delivery (Devops). Therefore, the goal of this post is to explain how to automate the deployment of a NextJS project to a Kubernetes cluster by using Gitlab.

Pre-requisites

Node, and NPM installed.
A Gitlab account.
A kubernetes cluster in any cloud provider (Azure, AWS, IBM Cloud, GCP, etc).

Create a Next JS project in Gitlab.

Next JS is a fullstack framework to create web applications. To create a NextJS project we need:
Run a npx command to Create a Next JS project.
npx create-next-app@latest --ts
Define a project name <project_name>.
After the installation, we run the Next JS project.
cd <project_name>
npm run dev
Verify the web application is running in the browser http://localhost:3000.
Push changes to the Gitlab repository.
git add .
git commit -m ‘<project_name> is created’
git push

Create a Gitlab agent

The Gitlab agents allow us to integrate our Gitlab repository with our Kubernetes cluster.
To create a Gitlab Agent we need to follow these steps:
Navigate to the Gitlab agent creation.
393b7079-5c71-41be-b45e-02ac087a8b9f
Go to “Infraestructure” > "Kubernetes clusters".
Click on “Connect a cluster” to start the Gitlab agent creation.
Define the new agent name <agent_name>.
d997b353-16df-481c-a8f4-4f9f1c155be6
Input the agent name. Must be in lowercase, and separated by hyphens.
Click on “Create agent”.
Click on "Register".
Once the agent is created, some HELM commands are generated to install the new Gitlab agent in our Kubernetes cluster.
c4d8fae7-4b5e-47f1-806a-2336d1c5ca95
Copy the command, and run it in the Kubernetes cluster terminal.
After the Gitlab agent installation, a new namespace called gitlab-agent is created with its running pods.

To verify the new namespace we should run:
kubectl get pods --all-namespaces
f8f4afed-70e3-4703-819f-c5fcdade6071
We can also check if the Kubernetes cluster and gitlab repository are connected by navigating to “Infraestructure” > “Kubernetes clusters”.
147e0209-2314-474b-9e8c-8d96c3172b3c
With the installed Gitlab agent we can perform any operation to our Kubernetes cluster.

Create a namespace in the Kubernetes cluster

Once we have connected the Gitlab agent to the Kubernetes cluster, let's create a new namespace to deploy instances of our Next JS Docker images.
We should run these commands in the Kubernetes cluster terminal:
Create a new namespace.
kubectl create namespace <namespace>
Verify if the new namespace was created.
kubectl get namespace

Create credentials for the Container Registry.

The Gitlab Container Registry is a repository which stores the Docker images that will be created during the execution of the pipeline. Therefore, it is required to create credentials for the Kubernetes cluster to access it and download the Docker images for deployment.
To create the credentials we should follow these steps:
Create a Deploy Token: This token is used by the kubernetes cluster in order to access to the Gitlab Container Registry.
9ea4fdb0-ccfb-4a0d-9115-3b512207730e
Navigate to "Settings" > "Repository".
Expand "Deploy Tokens".
Add a new name for the deploy token <deploy_token_name>.
Set a username <username>.
Grant read permissions.
Click on “Create deploy token".
Once the deploy token is created, Gitlab generates a password called <token_password>. It is recommended to copy this password because it is only displayed once.
539a1255-4806-4c78-8c47-e04f311fa8ab
Next, we will create a secret in the kubernetes cluster that stores the deploy token, and will make it available to download the Docker images from the Container Registry when deploying our web application.
To create the secret we should follow these steps in the kubernetes cluster terminal:
Verify if we can log in to the Gitlab Container Registry.
docker login -u <username> -p <token_password> registry.gitlab.com
The message "Login Succeeded" should appear.
Create a secret called registrycred of type docker-registry to store credentials of the Container Registry . We should run this command that requires the <token_password>, <username>, and <namespace>:
kubectl create secret docker-registry registrycred --docker-server=registry.gitlab.com --docker-username=<username> --docker-password=<token_password> --namespace=<namespace>
Verify if the new secret called registrycred was created.
kubectl get secrets -n <namespace>

Deploy the project in the Kubernetes cluster

At this point, we have already the Gitlab agent, the namespace for our deployments, and the credentials of the Container registry stored in a secret in our Kubernetes cluster. Now, let's create some scripts to automate the deployment process every time we push changes to our Gitlab repostiory.

Therefore, we need scripts to:
Create our Docker image of our Next JS project:
Create a file called Dockerfile in the root folder.
In this file we configure the deployment of our Next JS project in a Docker container. In this case, we will expose the port 3000.

./Dockerfile
FROM node:17-alpine
RUN mkdir -p /usr/src/app
ENV PORT 3000

WORKDIR /usr/src/app

COPY package.json /usr/src/app
COPY package-lock.json /usr/src/app
RUN npm ci

COPY . /usr/src/app

RUN npm run build

EXPOSE 3000
CMD [ "npm", "run", "start" ]
Integrate the Gitlab agent to our Next JS project.
Create a file called config.yaml in the folder ./.gitlab/agents/<agent_name>/
In this file we define the access of the Gitlab agent through the project path <project_path>.

./gitlab/agents/<agent_name>/config.yaml
ci_access:
     projects:
           - id: <project_path>
Define the deployment process in the Kubernetes cluster.
Create a file called deployment.yaml in the folder ./manifest/deploy/.
This file configure how to deploy the Docker image in the Kubernetes cluster.

./manifest/deploy/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: <deployment_name>
  namespace: <namespace>
spec:
  replicas: 1
  selector:
    matchLabels:
      name: <project_name>
  template:
    metadata:
      labels:
        name: <project_name>
    spec:     
      containers:
        - name: <project_name>
          image: registry.gitlab.com/<project_path>:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
      imagePullSecrets:
        - name: registrycred
In this case, we will use the same port that was defined in the Dockerfile, port 3000. In addition, we take into consideration the registrycred secret which contains the credentials for the Container Registry.
Define the service that will be exposed in the Kubernetes cluster.
Create a file called service.yaml in the folder ./manifest/deploy/ to expose our service in the port 80 instead of 3000.

./manifest/deploy/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: <service_name>
  namespace: <namespace>
spec:
  type: NodePort
  ports:
    - name: https
      port: 80
      protocol: TCP
      targetPort: 3000
  selector:
    name: <project_name>
Configure the Ingress which will manage the external access to the service.
Create a file called ingress.yaml in the folder ./manifest/deploy/ to configure the external access of our service.

./manifest/deploy/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: <ingress_name>
  namespace: <namespace>
spec:
  ingressClassName: nginx
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: <service_name>
                port:
                  number: 80
Define the pipeline that will run every time we push changes to the Gitlab repository.
Create a file called ./.gitlab-ci.yml in the root folder to configure the steps to deploy our application in the Kubernetes cluster.

./.gitlab-ci.yml
stages:
  - build
  - deploy

# build a new image, and push it to the cotnainer registry
build:
  image: docker:stable
  stage: build
  services:
    - name: docker:dind
      alias: thedockerhost
  variables:
    DOCKER_HOST: tcp://thedockerhost:2375/
    DOCKER_DRIVER: overlay2
    DOCKER_TLS_CERTDIR: ""
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY
    - docker build . -t $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest
  only:
    - main

# deploy the new image in the k8s cluster
deploy:
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  dependencies:
    - build
  stage: deploy
  variables:
    KUBE_CONTEXT: "<project_path>:<agent_name>"
  before_script:
    - if [ -n "$KUBE_CONTEXT" ]; then kubectl config use-context
"$KUBE_CONTEXT"; fi
  script:
    - kubectl get pods -n <namespace>
    - kubectl apply -f $CI_PROJECT_DIR/manifest/deploy/deployment.yaml
    - kubectl apply -f $CI_PROJECT_DIR/manifest/deploy/service.yaml
    - kubectl apply -f $CI_PROJECT_DIR/manifest/deploy/ingress.yaml
    - kubectl rollout restart deployment <deployment_name> -n <namespace>
    - kubectl get pods -n <namespace>
  only:
    - main
We define two steps:
- Build - it will create the Docker image, and push it to the Container registry.
- Deploy - it will deploy the Docker image in our Kubernetes namespace.
Once the scripts are created, we push the changes to the Gitlab repository.
git add .
git commit -m 'Integration with the k8s cluster is done'
git push
We can verify in the Gitlab user interface if the pipeline was successful by navigating to "CI/CD" > "Pipelines".
2a9aaf91-b0aa-45ae-81db-116c32dc5f2a
We can also verify if the new pods were created in the Kubernetes cluster terminal.
kubectl get pods -n <namespace>
Finally, the new ingress can be verified if it has been created correclty.
kubectl describe ingress <ingress_name> -n <namespace>

Generate a Ingress Controller

After the web service of our Next JS application has been deployed on our kubernetes cluster, we proceed to create the Ingress Controller which is basically an Nginx proxy and load balancer whose function is to accept outside traffic and route it to our ingress resources.
To install an ingress controller, we must execute the following command in the kubernetes cluster console.
helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace
After installing the ingress controller, we can verify if it was installed correctly by running:
kubectl get po -n ingress-nginx  -o wide
We can obtain the external IP of the ingress controller by executing the following command and checking the EXTERNAL-IP column.
kubectl get svc -n ingress-nginx
With that external IP address we can go to a browser and see our application running.

Bonus: Running the pipelines in our own runners

So far, we have managed to connect Gitlab with our Kubernetes cluster and deploy our NextJS application. Now, if we visualize the execution of the pipelines, we can realize that Gitlab provides us with shared runners to execute the tasks of each job. However, this may involve a cost depending on the number of executions for private projects. For this reason, we can provide our own runners using the Kubernetes cluster.

To create our runners we must follow the steps below:
A namespace is created exclusively for runners, in this case we are going to call it gitlab-runners
kubectl create namespace gitlab-runners
Obtain the registration token which will give us access to the gitlab project.
52fa6fa0-feb0-43fd-bd8d-182b955c2191
Navigate to Settings > "CI/CD" > "Runners".
Deactivate the shared runners option.
Copy the registration token <runner_token>.
Now, in the kubernetes cluster console we create a file called values.yaml which will be used to define the configuration of our runners using the <runner_token>.

values.yaml
image: gitlab/gitlab-runner:alpine-v12.9.0
imagePullPolicy: IfNotPresent
init:
  image: busybox
  tag: latest
gitlabUrl: "https://gitlab.com/"
runnerRegistrationToken: "<runner_token>"
unregisterRunners: true
concurrent: 5
checkInterval: 30
rbac:
  create: true
  clusterWideAccess: false
metrics:
  enabled: true
runners:
  image: ubuntu:16.04
  tags: "k8s-runner"  
  privileged: true
  namespace: gitlab-runners
  cachePath: "/opt/cache"
  cache: {}  
  builds: {}
  services: {}    
  helpers: {}    
resources: {}
Important: In this configuration there is an attribute called tags whose value is k8s-runner, this value will be used later in our pipelines configuration.
After the creation of the values.yaml file, we proceed to execute the following commands in the file folder:
helm repo add gitlab https://charts.gitlab.io
helm install --namespace gitlab-runners gitlab-runner -f values.yaml gitlab/gitlab-runner
Once the installation is finished, we verify that the pod has been created in the gitlab-runners namespace by running.
kubectl get pods -n gitlab-runners
We can also check if gitlab recognizes the new runner by navigating to "Settings" > "CI/CD" > "Runners".
6171afd9-4ee3-432a-8f15-1ce659d04878
A new runner should appear in the specific runners section with the name of the pod you are connected to.
To use the runner in our pipeline we must modify the .gitlab-ci.yml file located in the root of the project. There we add for each stage the tag with value k8s-runner that we had previously configured in the values.yaml file.
stages:
  - build
  - deploy

build:
  image: docker:stable
  stage: build
  # adicionamos este nuevo tag para el stage build
  tags:
    - k8s-runner
….

deploy:
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  dependencies:
    - build
  stage: deploy
 # adicionamos este nuevo tag para el stage build
  tags:
    - k8s-runner
Push the changes to Gitlab.
Finally, we can visualize that the pipeline is running in our runner.
Julian Parra © 2023