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.
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.
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.
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.yaml
and a service.yaml
. This help us maintain an organized structure.
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.
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.
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
.
Also, set the Repository URL
, assign the github credentials
, and chooses the repository branch.
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.
It will show something like this.
Now, let's create a projects. Go to Settings > Projects > New Project
. These will be rps-game-staging
and rps-game-production
respectively.
In Sources repositories
, click on edit and select the previously associated repository. Meanwhile in Destinations
select the cluster.
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
.
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.
If we click on the app, we can see this.
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.
Let's change the title, to do that, go to the repository in templates > home.html
, as follows.
When we accept the commit, it will automaticaly activate the build in Jenkins, as follows.
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
.
After that, let's validate in ArgoCD, this will deploy the new version, as follows.
If we verify in the cluster, we can see the correct operation.
Finally, let's review the web app.
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.