DEV Community

Cover image for Jenkins Ci/Cd Pipeline to Build a Go Application into a Docker Image with Multistage build
Audu Ephraim
Audu Ephraim

Posted on

Jenkins Ci/Cd Pipeline to Build a Go Application into a Docker Image with Multistage build

Introduction

In this article, i will be discussing how I implemented a ci/cd pipeline from scratch to build a simple Golang application into a docker image and push said image to Docker hub

The stages of the said pipeline include checking out the source code repository, in this case, git, running analysis on the source code with sonar cube to check for vulnerabilities, building the source code into a docker image using multi-stage build to reduce the size of the image, and finally pushing the image into a docker hub repository

Prerequisites:

Because the installation and configuration of these tools are long and a subject of another topic, I won't be discussing them here today.

The reader should have the following installed and configured and have basic knowledge and understanding of these tools if they wish to follow along.

  • Golang
  • Docker
  • IDE
  • Jenkins
  • Sonarcube
  • Git

The reader should also have the following accounts signed in

  • Docker hub
  • Sonarcloud
  • Jenkins
  • GitHub

Setting Up Project Files

The Application

As mentioned earlier I’ll be using a simple application written in Golang, this application has three different routes that print three different messages to the browser.

func firstEndPointHandler(w http.ResponseWriter, r *http.Request) {
   message := "this is the first endpoint"
   _, err := w.Write([]byte(message))
   if err != nil {
      log.Fatal(err)
   }
}


func secondEndPointHandler(w http.ResponseWriter, r *http.Request) {
   message := "second endpoint"
   _, err := w.Write([]byte(message))
   if err != nil {
      log.Fatal(err)
   }
}


func thirdEndPointHandler(w http.ResponseWriter, r *http.Request) {
   message := "second endpoint"
   _, err := w.Write([]byte(message))
   if err != nil {
      log.Fatal(err)
   }
}


func main() {
   http.HandleFunc("/first", firstEndPointHandler)
   http.HandleFunc("/second", secondEndPointHandler)
   http.HandleFunc("/third", thirdEndPointHandler)
   err := http.ListenAndServe(":8081", nil)
   log.Fatal(err)
}
Enter fullscreen mode Exit fullscreen mode

Dockerfile

As you all know to build an application into a docker image I’ll need to use a docker file, where I'll specify the build for the image

In this docker file, I’ll be using a multistage build. With multistage builds, you can drastically reduce the size of a docker image and optimize the image by using multiple FROM statements, and each of them begins a new stage of a build. Each FROM statement can use a different base image.

The essence of this is to be able to copy only the necessary artefacts from one stage to another and leave the ones you don't need.

FROM golang:1.22.4 AS builder


WORKDIR /app
COPY go.mod ./
RUN go mod download


COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /test-app


FROM scratch
COPY --from=builder /test-app /test-app


EXPOSE 8081


CMD ["/test-app"]
Enter fullscreen mode Exit fullscreen mode

The first stage uses the golang:1.22.4 as the base image and it is named builder
Sets the working directory to /app
Copies go.mod file and download all dependencies
Copies all files that end with the .go extension
Builds the go binary with CGO_ENABLED=0 and GOOS=linux and produces an output image /test-app
The second stage of the build:
Uses the base image: scratch which is an empty image
Copies the built /test-app binary from the builder stage
Exposes port 8081
Specifies the command to run the binary: CMD[“/test-app”]

Jenkinsfile

The Jenkinsfile defines the pipeline stages and the steps executed during the pipeline. It must be placed in the root directory of the project for Jenkins to discover and initiate the pipeline.

pipeline {
   agent any


   tools {
      go 'golang'
   }
   environment {
       DOCKERHUB_CREDENTIALS = credentials('dockerhub')
       DOCKER_IMAGE = 'ephraimaudu/test-app'
       GITHUB_CREDENTIALS = 'git-secret'
       SONAR_TOKEN = credentials('SONAR_TOKEN')
   }


   stages{
       stage('Checkout'){
           steps{
               echo "checking out repo"
               git url: 'https://github.com/audu97/test-project', branch: 'master',
               credentialsId: "${GITHUB_CREDENTIALS}"
           }
       }
       stage('Run SonarQube Analysis') {
           steps {
               script {
                   echo 'starting analysis'
                   sh '/usr/local/sonar/bin/sonar-scanner -X -Dsonar.organization=eph-test-app -Dsonar.projectKey=eph-test-app-test-go-app -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io'
               }
           }
       }
       stage('Run Docker Build'){
           steps{
               script{
                    echo "starting docker build"
                    sh "docker build build -t ${DOCKER_IMAGE}:${env.BUILD_ID} ."
                    echo "docker built successfully"
               }
           }
       }
       stage('push to docker hub'){
           steps{
               echo "pushing to docker hub"
               script{
                   docker.withRegistry('https://index.docker.io/v1/', 'dockerhub'){
                       docker.image("${DOCKER_IMAGE}:${env.BUILD_ID}").push()
                   }
               }
               echo "done"
           }
       }
   }


   post {
       always{
           cleanWs()
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

NOTE: To use these credentials in the environment variables, you should add them in the “Credentials” section within the “Manage Jenkins” option of the Jenkins UI. This way, your Jenkins jobs can securely access the necessary credentials during their execution.
The agent specifies that the pipeline can run on any available executor in the Jenkins environment
Environment variables: defines various environment variables used in the pipeline execution. DOCKERHUB_CREDENTIALS -contains credentials for signing in to docker hub, GITHUB_CREDENTIALS -contains credentials to use to sign in to GitHub to check out the specified repository, SONAR_TOKEN- also serves as credentials for the Sonar cloud, where I can view the code analysis, DOCKER_IMAGE-specifies the name I want for the docker image.
Stages: the pipeline consists of several stages:

  • Checkout: This stage checks out the code from the specified GitHub repository
  • Run sonarqube analysis: executes sonar cube analysis on the code base. Sonar cube is a tool for static analysis on a code base by analyzing it statically. It detects bugs, vulnerabilities and code smells.
  • Run docker build: builds a docker image using the specified dockerfile.
  • Push to docker hub: pushes the built docker image to docker hub

  • Post processing: the post section ensures that the workspace is cleaned up after the pipeline execution, even if the pipeline fails

Challenges

The biggest challenge I faced, which took considerable time to resolve, was that Jenkins could not locate Docker to execute the Docker build stage in my pipeline because I had installed both Jenkins and Docker using snaps. This resulted in repeated pipeline failures.
To overcome this issue, I uninstalled the snap versions of both Jenkins and Docker. Following that, I installed them following the instructions provided in their documentation. This approach solved my problem by allowing Jenkins to interact with Docker successfully.

Conclusion

This project has provided valuable insights into the importance and the need for CI/CD pipelines. Additionally, it emphasizes multistage Docker image builds and includes security considerations during the build stage (shifting security left) by using SonarQube.

The link to the repository containing the entire project can be located HERE

Top comments (0)