deploy first app cicd cycle - juancamilocc/virtual_resources GitHub Wiki

Deploying our first application in Kubernetes with a CI/CD cycle using Jenkins and ArgoCD

In this guide, we will deploy a Golang application in a Kubernetes cluster, implementing a basic CI/CD cycle using Jenkins and ArgoCD.

You can view the general workflow here.

General Workflow

As you can see in the workflow above, the developer uploads changes to the GitHub repository. After that, Jenkins starts the process of building and pushing, while ArgoCD is responsible for deploying the new version of the application, including the changes.

NOTE: This guide assummes that the user already has deployed Jenkins and ArgoCD in a Kubernetes cluster. If you have any doubts or issues about this, you can go here.

Setup our Golang application

For the example, we will use the following repository, you can see it here.

First, we must define the Dockerfile to build our application, in this case we will use the following.

FROM golang:1.22.0 AS build-stage

WORKDIR /app

COPY go.mod ./
RUN go mod download

COPY . .
COPY templates/ templates/
COPY static/ static/

RUN CGO_ENABLED=0 GOOS=linux go build -o /rock_paper_scissors

# Test stage
FROM build-stage AS run-test-stage
RUN go test -v ./...

# Deploy stage
FROM gcr.io/distroless/base-debian11 AS build-release-stage

WORKDIR /

COPY --from=build-stage /rock_paper_scissors /rock_paper_scissors
COPY --from=build-stage /app/templates /templates
COPY --from=build-stage /app/static /static

EXPOSE 8085

USER nonroot:nonroot

ENTRYPOINT ["/rock_paper_scissors"]

We also need to create a pipeline like a Jenkinsfile.

pipeline {
    agent {
        kubernetes {
        cloud 'kubernetes-staging'    
        defaultContainer 'jnlp'
        yaml """
apiVersion: v1
kind: Pod
metadata:
  name: rocky-pod
  namespace: jenkins
spec:
  containers:
    - name: rocky
      image: ghcr.io/juancamilocc/builders:rocky8-docker
      imagePullPolicy: IfNotPresent
      tty: true
      securityContext:
        runAsUser: 0
        privileged: true
      resources:
        limits:
          memory: "2Gi"
          cpu: "750m"
        requests:
          memory: "1Gi"
          cpu: "500m"
      volumeMounts:
        - name: docker-graph-storage
          mountPath: /var/lib/docker
  volumes:
    - name: docker-graph-storage
      emptyDir: {}
            """
            containerTemplate {
            name 'jnlp'
            image 'jenkins/inbound-agent'
            resourceRequestCpu '256m'
            resourceRequestMemory '500Mi'
            resourceLimitCpu '512m'
            resourceLimitMemory '1000Mi'
            }
        }
    }
    environment {
        REPOSITORY = 'github.com/juancamilocc/rock_paper_scissors.git' 
        BRANCH = 'deployment'
        MANIFEST = 'deployment.yaml' 
        IMAGE_TAG = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
        DATE = sh(script: 'TZ="America/Bogota" date "+%Y-%m-%d-%H-%M-%S"', returnStdout: true).trim()
        RETRY_COUNTS = 2
    }
    stages {
        stage('Build and Push image') {
            steps {
                container('rocky') {
                    script {
                        retry(RETRY_COUNTS) {
                            try {
                                sh 'git config --global --add safe.directory $WORKSPACE'

                                withCredentials([usernamePassword(credentialsId: 'credentials-dockerhub', usernameVariable: 'DOCKERHUB_USERNAME', passwordVariable: 'DOCKERHUB_PASSWORD')]) {
                                    sh '''
                                        echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin                      
                                        docker build -t juancamiloccc/rps-game:$IMAGE_TAG-$DATE-staging .
                                        docker push juancamiloccc/rps-game:$IMAGE_TAG-$DATE-staging
                                    '''
                                }

                            } catch (Exception e) {
                                echo "Error occurred: ${e.message}"
                                echo "Retrying..."
                                error("The stage 'Build and Push image' failed")
                            }
                        }
                    }
                }
            }
        }
        stage('Update deployment') {
            steps {
                container('rocky') {
                    script {
                        retry(RETRY_COUNTS) {
                            try {
                                withCredentials([usernamePassword(credentialsId: 'credentials-github', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
                                    sh '''
                                        git config --global user.email "[email protected]"
                                        git config --global user.name "juancamilocc"
                                        git clone -b $BRANCH --depth 5 https://GIT_USERNAME:$GIT_PASSWORD@$REPOSITORY
                                        cd rock_paper_scissors/deployment/staging
                                        sed -i "s/\\(image:.*:\\).*/\\1$IMAGE_TAG-$DATE-staging/" $MANIFEST
                                        git add $MANIFEST 
                                        git commit -m "Trigger Build"                     
                                        git push origin $BRANCH
                                    '''

                                    // Delete repository
                                    sh 'rm -rf rock_paper_scissors'
                                }
                            } catch (Exception e) {
                                echo "Error occurred: ${e.message}"
                                echo "Retrying..."
                                error("The stage 'Update Deployment' failed")
                            }
                        }    
                    }
                }
            }
        }
    }
    post {
        success {
            echo "SUCCESS"    
        }
        failure {
            echo "FAILURE"
        }
    }
}

As you can see, the pipeline uses the image ghcr.io/juancamilocc/builders:rocky8-docker, if you also want to use it or build it for yourself, here is the Dockerfile.

FROM rockylinux:8-minimal

RUN microdnf install \
  bash \
  curl \
  git \
  sed \
  ca-certificates \
  epel-release \
  nodejs

RUN curl -o /etc/yum.repos.d/docker-ce.repo https://download.docker.com/linux/centos/docker-ce.repo
RUN microdnf install docker-ce docker-ce-cli containerd.io 
CMD ["bash", "-c", "dockerd"]

It is a preconfigured image, you can find it here. The repository should look like this.

Code repository

Now, we will set up a webhook in our repository to ensure communication with Jenkins. Go to the repository, click on settings > Webhooks > Add Webhook, then enter the Jenkins URL, select application/json, click on Let me select individual events and check the options Pull Requests and Pushes. If you have doubts or issues about this, go here.

We can verify if the webhook is activated correctly, go to Webhook option again and checking for the message Last delivery was successful.

github webhook configuration github webhook configuration github webhook configuration github webhook configuration

NOTE: If you don't have a url domain for your Jenkins server, you can use ngrok tool, this will allow you to get a public url to use in the webhook configuration, more info here. This was used in the previous webhook configuration example.

Now, we will create a new branch named deployment, this will allow us to save the Kubernetes manifest of our application and it will be monitored for ArgoCD to deploy the new application versions.

In this branch, we will have two folders, one for staging environment and another for production, each folder will have two manifests, a deployment.yamland a service.yaml. This help us maintain an organized structure.

Deployment branch

These are the .yaml manifests.

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rps-game
  namespace: rps-game
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rps-game
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: rps-game
    spec:
      containers:
      - name: rps-game
        image: juancamiloccc/rps-game:v0.1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8085
          protocol: TCP
        resources:
          limits:
            cpu: 100m
            memory: 100Mi
          requests:
            cpu: 50m
            memory: 50Mi
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: rps-game-service
  namespace: rps-game
spec:
  selector:
    app: rps-game
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8085
  type: ClusterIP

With that, we have finished the repository configuration.

Setting up our pipeline in Jenkins

In our Jenkins server, go to New Item, set a name and select the Pipeline option.

Pipeline configuration

Now, select the Github project option and enter the repository URL without the .git extension. Addirionally, select the Throttle Concurrent Builds option and set the value to 1 in both fields, This will ensure that there are no multiple executions at the same time, allowing only one execution.

Pipeline configuration

NOTE: If the Throttle Concurrent Builds option doesn't appear, go to the plugins, search it as Throttle Concurrent and install it.

In the Build Triggers, select the GitHub hook trigger for GITScm polling option, this will enable the webhook detection. Meanwhile, in the Pipeline section, select the option Pipeline Script from SCM.

Pipeline configuration

Also, set the Repository URL, assign the github credentials, and chooses the repository branch.

Pipeline configuration

Finally, click on save.

Setting up our application in ArgoCD

Let's enter into the dashboard, first we will associate the repository. For that go to Settings > Repositories > Connect Repo, select Choose your connection method: the Via https option, set Repository URL, username and password.

repository connect

It will show something like this.

repository connect

Now, let's create a projects. Go to Settings > Projects > New Project. These will be rps-game-staging and rps-game-production respectively.

rps-game-staging project

In Sources repositories, click on edit and select the previously associated repository. Meanwhile in Destinations select the cluster.

rps-game-staging project configuration

Now, go to Applications option and click on New App, give it a name, select the project that we have created and change the Sync policy for Automatic.

rps-game-staging project configuration

Navigate until Source section. There, select the Repository URL, in Revision option set the branch that we have created previously, in the Path field, set folder route deployment/staging/, and finally click on create.

rps-game-staging project configuration rps-game-staging project configuration

If we click on the app, we can see this.

rps-game-staging project configuration

As we can see, ArgoCD is monitoring the application status.

Testing our CI/CD cycle

Here, we will make an intentional change in the repository to verify the correct operation of our CI/CD cycle.

Go to the repository and change any line in the code. This will activate the build in Jenkins and subsequently will deploy the new version in the cluster through ArgoCD.

Before doing this, we must set up a port-forward to access it via the browser.

kubectl -n rps-game port-forward svc/rps-game-service 8085:80
# Forwarding from 127.0.0.1:8085 -> 8085
# Forwarding from [::1]:8085 -> 8085
# Handling connection for 8085
# Handling connection for 8085
# Handling connection for 8085

The application looks like this.

RPS Game

Let's change the title, to do that, go to the repository in templates > home.html, as follows.

Intentional change

When we accept the commit, it will automaticaly activate the build in Jenkins, as follows.

Activate pipeline in Jenkins

When the execution finishes, you can verify the logs if you want. Click on the build number, in this case 34, and then click on Console Ouput.

Logs execution in Jenkins

After that, let's validate in ArgoCD, this will deploy the new version, as follows.

Deploy in ArgoCD

If we verify in the cluster, we can see the correct operation.

Verify pods

Finally, let's review the web app.

Web app updated

NOTE: For the production environment, yo must follow the same process shown in this guide. I recommend deactivating the Auto Sync option in ArgoCD, it should be a process approved manually.

Conclusions

In this guide, you learned how to deploy a Golang application in a CI/CD cycle using Jenkins and ArgoCD. In the next guide, you will learn how to add notifications Add notifications in CI/CD Cycle depending of the execution pipeline's state. It is important to keep in mind that you can adapt this process for deploying any application developed in any language.