DAY 01
▾
Foundations — CI/CD Concepts & App Overview
Goal: Understand CI/CD theory, meet the 2-tier app you'll be deploying, and set up your local environment.
What you'll learn
- CI vs CD vs CD (Continuous Integration / Delivery / Deployment)
- The role of Jenkins in a modern pipeline
- Our app: Golang REST API backed by MongoDB
- Tool versions: Jenkins LTS, Kubernetes 1.29+, Docker 24+, Go 1.22+
The 2-Tier Application
┌─────────────────────────────────────────────┐
│ 2-Tier Architecture │
├─────────────────────────────────────────────┤
│ Backend (Golang) :8080 │
│ ├── GET /api/users │
│ ├── POST /api/users │
│ └── GET /health │
│ │ │
│ Database (MongoDB) :27017 │
│ └── db: appdb col: users │
└─────────────────────────────────────────────┘
Golang Backend — main.go
main.go — Golang REST API
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type User struct {
Name string `json:"name" bson:"name"`
Email string `json:"email" bson:"email"`
}
var client *mongo.Client
func main() {
mongoURI := os.Getenv("MONGO_URI")
if mongoURI == "" {
mongoURI = "mongodb://localhost:27017"
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var err error
client, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
if err != nil { log.Fatal(err) }
defer client.Disconnect(ctx)
http.HandleFunc("/health", healthCheck)
http.HandleFunc("/api/users", usersHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
col := client.Database("appdb").Collection("users")
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
cursor, _ := col.Find(context.Background(), bson.D{})
var users []User
cursor.All(context.Background(), &users)
json.NewEncoder(w).Encode(users)
case http.MethodPost:
var u User
json.NewDecoder(r.Body).Decode(&u)
col.InsertOne(context.Background(), u)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
}
}
Official Documentation
DAY 02
▾
Install & Configure Jenkins
Goal: Install Jenkins via Docker, install essential plugins, and create your first freestyle job.
Run Jenkins with Docker
docker-compose.jenkins.yml
version: "3.8"
services:
jenkins:
image: jenkins/jenkins:lts-jdk17
container_name: jenkins
privileged: true
user: root
ports:
- "8080:8080"
- "50000:50000"
volumes:
- jenkins_home:/var/jenkins_home
# Mount Docker socket so Jenkins can build images
- /var/run/docker.sock:/var/run/docker.sock
environment:
- JAVA_OPTS=-Djenkins.install.runSetupWizard=false
volumes:
jenkins_home:
Auto-install Plugins (JCasC)
plugins.txt — essential plugins list
# Paste into Jenkins plugin manager or use install-plugins.sh
git
workflow-aggregator # Pipeline plugin suite
pipeline-stage-view
docker-workflow
kubernetes
credentials-binding
blueocean
github
slack
junit
cobertura
First Shell Job — Test Go Build
Shell script (Execute Shell step in Freestyle job)
#!/bin/bash
set -e
# Install Go if not present
export GOPATH=/tmp/go
export PATH=$PATH:/usr/local/go/bin
cd $WORKSPACE
go mod tidy
go build ./...
go test ./... -v
echo "Build successful!"
Official Documentation
DAY 03
▾
Dockerize the 2-Tier App
Goal: Write production-grade Dockerfiles for Golang backend and wire it with MongoDB via docker-compose.
Golang Multi-Stage Dockerfile
Dockerfile — Golang (multi-stage, minimal image)
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Cache deps separately for faster rebuilds
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
# Stage 2: Runtime — distroless (no shell, minimal attack surface)
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
docker-compose.yml — Full Stack
docker-compose.yml
version: "3.8"
services:
backend:
build: .
container_name: go-backend
ports:
- "8080:8080"
environment:
- MONGO_URI=mongodb://mongo:27017
depends_on:
mongo:
condition: service_healthy
restart: unless-stopped
mongo:
image: mongo:7.0
container_name: mongodb
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mongo_data:
Official Documentation
DAY 04
▾
Your First Declarative Jenkinsfile Pipeline
Goal: Write a full Declarative Jenkinsfile that checks out code, runs Go tests, and builds a Docker image.
Jenkinsfile — Declarative Pipeline
Jenkinsfile
pipeline {
agent { docker { image 'golang:1.22-alpine' } }
environment {
APP_NAME = 'go-backend'
REGISTRY = 'docker.io/yourdockerhubuser'
IMAGE_TAG = "${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'echo "Branch: ${GIT_BRANCH}, Commit: ${GIT_COMMIT}"'
}
}
stage('Dependencies') {
steps {
sh 'go mod download'
sh 'go mod verify'
}
}
stage('Lint') {
steps {
sh '''
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run ./...
'''
}
}
stage('Test') {
steps {
sh 'go test ./... -v -coverprofile=coverage.out'
sh 'go tool cover -func=coverage.out'
}
post {
always {
junit '**/test-results/*.xml'
}
}
}
stage('Build Binary') {
steps {
sh 'CGO_ENABLED=0 GOOS=linux go build -o server .'
}
}
stage('Build Docker Image') {
agent any // switch back to host for Docker
steps {
script {
def img = docker.build("${REGISTRY}/${APP_NAME}:${IMAGE_TAG}")
img.tag('latest')
}
}
}
}
post {
failure {
echo 'Pipeline failed! Sending notification...'
}
success {
echo "Build ${IMAGE_TAG} succeeded!"
}
always {
cleanWs()
}
}
}
Official Documentation
DAY 05
▾
Kubernetes Fundamentals for the App
Goal: Write all Kubernetes manifests needed to run the Golang + MongoDB stack manually before automating with Jenkins.
MongoDB StatefulSet + PVC
k8s/mongo-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb
namespace: app
spec:
serviceName: mongodb
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: mongo:7.0
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-storage
mountPath: /data/db
readinessProbe:
exec:
command: ["mongosh", "--eval", "db.adminCommand('ping')"]
initialDelaySeconds: 15
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: mongo-storage
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: mongodb
namespace: app
spec:
clusterIP: None # Headless — StatefulSet needs this
selector:
app: mongodb
ports:
- port: 27017
Golang Backend Deployment + Service
k8s/backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-backend
namespace: app
labels:
app: go-backend
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: go-backend
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: go-backend
spec:
containers:
- name: backend
image: docker.io/yourdockerhubuser/go-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: MONGO_URI
value: "mongodb://mongodb.app.svc.cluster.local:27017"
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "500m"
memory: "128Mi"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: go-backend-svc
namespace: app
spec:
selector:
app: go-backend
ports:
- port: 80
targetPort: 8080
type: ClusterIP
Official Documentation
DAY 06
▾
Jenkins Deploys to Kubernetes
Goal: Extend the Jenkinsfile to push images to DockerHub and trigger a rolling update on Kubernetes after every merge to main.
Full Build → Push → Deploy Jenkinsfile
Jenkinsfile — Build, Push, Deploy
pipeline {
agent any
environment {
REGISTRY = "yourdockerhubuser/go-backend"
IMAGE_TAG = "${env.GIT_COMMIT[0..7]}"
KUBECONFIG = credentials('kubeconfig-prod')
DOCKER_CREDS = credentials('dockerhub-creds')
}
stages {
stage('Test') {
agent { docker { image 'golang:1.22-alpine' } }
steps {
sh 'go test ./... -coverprofile=coverage.out'
}
}
stage('Build & Push Image') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-creds') {
def img = docker.build("${REGISTRY}:${IMAGE_TAG}")
img.push()
img.push('latest')
}
}
}
}
stage('Deploy to Kubernetes') {
steps {
sh """
kubectl --kubeconfig=$KUBECONFIG set image \
deployment/go-backend \
backend=${REGISTRY}:${IMAGE_TAG} \
-n app
kubectl --kubeconfig=$KUBECONFIG rollout status \
deployment/go-backend \
-n app \
--timeout=120s
"""
}
}
}
post {
failure {
// Auto-rollback on deploy failure
sh "kubectl --kubeconfig=$KUBECONFIG rollout undo deployment/go-backend -n app"
}
}
}
Official Documentation
DAY 07
▾
Multibranch Pipelines & Webhooks
Goal: Set up Multibranch Pipeline so feature branches get isolated builds, PRs get tested, and only main deploys to production.
Multibranch Jenkinsfile with Branch Logic
Jenkinsfile — multibranch with branch conditions
pipeline {
agent any
triggers {
// Triggered by GitHub webhook automatically
githubPush()
}
stages {
stage('Test') {
agent { docker { image 'golang:1.22-alpine' } }
steps {
sh 'go test ./... -race -coverprofile=coverage.out'
}
}
stage('Build Image') {
when {
anyOf {
branch 'main'
branch 'release/*'
}
}
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-creds') {
def tag = env.BRANCH_NAME == 'main' ? 'latest' : env.BRANCH_NAME.replace('/', '-')
docker.build("yourdockerhubuser/go-backend:${tag}").push()
}
}
}
}
stage('Deploy Staging') {
when { branch 'release/*' }
steps {
sh "kubectl set image deployment/go-backend backend=yourdockerhubuser/go-backend:${BRANCH_NAME.replace('/','-')} -n staging"
}
}
stage('Deploy Production') {
when { branch 'main' }
steps {
input message: 'Deploy to production?', ok: 'Deploy'
sh "kubectl set image deployment/go-backend backend=yourdockerhubuser/go-backend:latest -n app"
sh "kubectl rollout status deployment/go-backend -n app --timeout=120s"
}
}
}
}
Official Documentation
DAY 08
▾
Helm Charts — Package & Version Releases
Goal: Package the 2-tier app as a Helm chart so deployments are parameterised, versioned and easily rolled back.
Helm Chart Structure
helm-chart/
├── Chart.yaml ← chart metadata
├── values.yaml ← default values
└── templates/
├── deployment.yaml ← go-backend Deployment
├── service.yaml ← ClusterIP Service
├── mongo-ss.yaml ← MongoDB StatefulSet
├── secret.yaml ← Mongo credentials
├── configmap.yaml ← App config
└── _helpers.tpl ← named templates
Chart.yaml
helm-chart/Chart.yaml
apiVersion: v2
name: go-mongo-app
description: Golang REST API + MongoDB 2-tier app
type: application
version: 0.1.0
appVersion: "1.0.0"
values.yaml
helm-chart/values.yaml
backend:
image:
repository: yourdockerhubuser/go-backend
tag: latest
pullPolicy: Always
replicas: 2
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
mongodb:
image: mongo:7.0
storage: 5Gi
namespace: app
Helm Deployment Template
helm-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "go-mongo-app.fullname" . }}
namespace: {{ .Values.namespace }}
labels:
{{- include "go-mongo-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.backend.replicas }}
selector:
matchLabels:
app: go-backend
template:
metadata:
labels:
app: go-backend
spec:
containers:
- name: backend
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
env:
- name: MONGO_URI
valueFrom:
secretKeyRef:
name: mongo-secret
key: mongo-uri
Jenkins Helm Deploy Stage
Jenkinsfile — Helm upgrade stage
stage('Helm Deploy') {
steps {
sh """
helm upgrade --install go-mongo-app ./helm-chart \
--namespace app \
--create-namespace \
--set backend.image.tag=${IMAGE_TAG} \
--set backend.replicas=2 \
--atomic \
--timeout 3m \
--wait
"""
}
}
Official Documentation
DAY 09
▾
Secrets, Environment Config & Security Hardening
Goal: Properly manage MongoDB credentials, Docker registry auth, and Kubernetes RBAC so Jenkins can deploy with least privilege.
Kubernetes Secret for MongoDB URI
k8s/secrets.yaml — Never commit plaintext secrets
apiVersion: v1
kind: Secret
metadata:
name: mongo-secret
namespace: app
type: Opaque
stringData:
# In prod: use Sealed Secrets or External Secrets Operator
mongo-uri: "mongodb://admin:CHANGEME@mongodb.app.svc.cluster.local:27017/appdb"
---
# Docker registry pull secret
apiVersion: v1
kind: Secret
metadata:
name: dockerhub-pull-secret
namespace: app
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64-encoded-docker-config>
RBAC — Jenkins Service Account
k8s/jenkins-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins-deployer
namespace: app
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jenkins-deploy-role
namespace: app
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jenkins-deploy-binding
namespace: app
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins-deploy-role
subjects:
- kind: ServiceAccount
name: jenkins-deployer
namespace: app
Jenkins Credentials Binding in Pipeline
Jenkinsfile — using credentials() binding
environment {
DOCKER_HUB = credentials('dockerhub-creds') // binds USR + PSW
KUBECONFIG = credentials('kubeconfig-prod') // file credential
}
steps {
sh '''
echo $DOCKER_HUB_PSW | docker login -u $DOCKER_HUB_USR --password-stdin
kubectl --kubeconfig=$KUBECONFIG get pods -n app
'''
}
Official Documentation
DAY 10
▾
Production Pipeline — HPA, Monitoring & GitOps Patterns
Goal: Graduate to a fully production-ready CI/CD system with autoscaling, observability hooks, and the complete Jenkinsfile for the 2-tier app.
Horizontal Pod Autoscaler
k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: go-backend-hpa
namespace: app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: go-backend
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Final Production Jenkinsfile
Jenkinsfile — FINAL production pipeline (all stages)
pipeline {
agent any
options {
timeout(time: 20, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
}
environment {
REGISTRY = "yourdockerhubuser/go-backend"
IMAGE_TAG = "${env.GIT_COMMIT[0..7]}"
DOCKER_CREDS = credentials('dockerhub-creds')
KUBECONFIG = credentials('kubeconfig-prod')
SLACK_CHANNEL = '#deployments'
NAMESPACE = 'app'
}
stages {
stage('Checkout & Info') {
steps {
checkout scm
sh 'git log --oneline -5'
}
}
stage('Unit Tests') {
agent { docker { image 'golang:1.22-alpine'; reuseNode true } }
steps {
sh 'go mod download'
sh 'go test ./... -race -coverprofile=coverage.out -json | tee test-report.json'
sh 'go tool cover -html=coverage.out -o coverage.html'
}
post {
always {
archiveArtifacts 'coverage.html'
}
}
}
stage('Build & Push') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-creds') {
def img = docker.build("${REGISTRY}:${IMAGE_TAG}",
"--label git-commit=${env.GIT_COMMIT} .")
img.push()
if (env.BRANCH_NAME == 'main') { img.push('latest') }
}
}
}
}
stage('Security Scan') {
steps {
sh """
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image --exit-code 1 --severity HIGH,CRITICAL \
${REGISTRY}:${IMAGE_TAG}
"""
}
}
stage('Deploy to K8s') {
when { branch 'main' }
steps {
sh """
helm upgrade --install go-mongo-app ./helm-chart \
--namespace ${NAMESPACE} \
--create-namespace \
--set backend.image.tag=${IMAGE_TAG} \
--atomic \
--timeout 3m \
--wait \
--kubeconfig=$KUBECONFIG
"""
}
}
stage('Smoke Test') {
when { branch 'main' }
steps {
sh """
sleep 5
kubectl --kubeconfig=$KUBECONFIG get pods -n ${NAMESPACE}
POD=$(kubectl --kubeconfig=$KUBECONFIG get pod -l app=go-backend -n ${NAMESPACE} -o name | head -1)
kubectl --kubeconfig=$KUBECONFIG exec $POD -n ${NAMESPACE} -- \
wget -qO- http://localhost:8080/health | grep 'ok'
"""
}
}
}
post {
success {
slackSend(channel: env.SLACK_CHANNEL,
color: 'good',
message: "✅ Deploy OK | ${env.JOB_NAME} #${env.BUILD_NUMBER} | ${REGISTRY}:${IMAGE_TAG}")
}
failure {
sh "helm --kubeconfig=$KUBECONFIG rollback go-mongo-app -n ${NAMESPACE} || true"
slackSend(channel: env.SLACK_CHANNEL,
color: 'danger',
message: "❌ Deploy FAILED | ${env.JOB_NAME} #${env.BUILD_NUMBER} | Auto-rolled back")
}
always {
cleanWs()
}
}
}
Official Documentation