Source code used as the base of the work: 🔗 https://github.com/vittorioexp/ywti-j2ee-webapp-project
Docker · Kubernetes · Jenkins · SonarQube · Prometheus · Grafana
- Introduction and Prerequisites
- Step 1: GitHub – Source Code Management
- Step 2: Jenkins – Continuous Integration
- Step 3: SonarQube – Code Quality
- Step 4: Docker – Containerization
- Step 5: Kubernetes – Deployment
- Step 6: Prometheus & Grafana – Monitoring
- Conclusion and Best Practices
This project aims to design and implement a complete CI/CD pipeline for deploying a JEE web application on Kubernetes. The pipeline automates the entire lifecycle:
- Developers push code to GitHub.
- Jenkins pulls the updates, builds the project, and runs tests.
- SonarQube performs automated static analysis and enforces code-quality gates.
- Docker packages the JEE application into container images.
- Kubernetes deploys and manages these images.
- Prometheus and Grafana provide real-time monitoring and visualization of application metrics.
This setup ensures reliability, repeatability, and full automation of the deployment process.
| Tool | Installation Link |
|---|---|
| Git | https://git-scm.com |
| Java JDK | https://www.oracle.com/java/technologies/downloads |
| Maven | https://maven.apache.org |
| Docker Desktop | https://www.docker.com |
| Jenkins | https://www.jenkins.io |
| SonarQube | https://www.sonarqube.org |
| kubectl | https://kubernetes.io |
- Basics of Java & Maven
- Understanding Git commands (commit, branch, merge)
- Docker concepts (images, containers, Dockerfile)
- Basic command-line usage
- Sign in to GitHub
- Click the + icon (top-right) → New Repository
- Enter repository name, optional description
- Set the visibility to Public
- Create the repository
A .gitignore file tells Git which files should not be committed (build artifacts, IDE configs, secrets, etc.).
Place this file in your project root:
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
# IDE
.idea/
*.iml
.vscode/
.settings/
.project
.classpath
# Logs
*.log
# Docker
.dockerignore
# Kubernetes secrets
k8s/*-secret.yaml
# Environment variables
.env
.env.local
# OS
.DS_Store
Thumbs.db# Navigate to your project
cd C:\Path\To\YourProject\
# Initialize Git
git init
# Add all project files
git add .
# First commit
git commit -m "Initial commit"
# Link GitHub repository
git remote add origin https://github.com/UserName/repositoryName.git
# Push to main branch
git branch -M main
git push -u origin mainCheck that you're on main:
git branch
# * maingit pull origin main
git checkout -b develop
git push -u origin developgit pull origin develop
git checkout -b feature/example
git push -u origin feature/examplegit branch -aDownload jenkins.war from:
🔗 https://www.jenkins.io/download/
Run Jenkins locally:
cd C://Path/To/Jenkins/
java -jar jenkins.war --httpPort=8080Access Jenkins at: http://localhost:8080
The initial admin password is located at:
C://Users/<your-user>/.jenkins/secrets/initialAdminPassword
Go to: Manage Jenkins → Tools
- Add Maven
- Name:
Maven-3.9.11 - Install automatically: Yes
- Version: 3.9.11
- Add JDK
- Name:
JDK-17 - Install automatically: Yes
- Name:
git.exe - Path:
C:\Program Files\Git\bin\git.exe
Because Jenkins is running locally, we use ngrok to expose port 8080.
ngrok http 8080Use the generated HTTPS URL for GitHub Webhooks.
GitHub → Repository → Settings → Webhooks → Add Webhook
| Field | Value |
|---|---|
| Payload URL | https://<ngrok-url>/github-webhook/ |
| Content type | application/json |
| Secret | Leave empty |
| Events | Just the push event |
New Item → Pipeline
- GitHub hook trigger for GITScm polling
- Poll SCM:
H/5 * * * *(checks every 5 minutes)
- Definition: Pipeline script from SCM
- SCM: Git
- Repository URL
- Credentials
- Branch:
*/developor any branch you want to build - Script Path:
Jenkinsfile
The pipeline consists of 10 automated stages:
pipeline {
agent any
tools {
maven 'Maven-3.9.11'
jdk 'JDK-17'
}
environment {
MAVEN_OPTS = '-Xmx1024m'
SCANNER_HOME = tool 'SonarScanner'
DOCKER_IMAGE = 'gyro0/ywti'
DOCKER_TAG = "2.0"
GIT_BRANCH = "develop"
}
stages {
stage('1. Cloner le repo') {
when {
anyOf {
branch 'develop'
}
}
steps {
script{
if (isUnix()){
sh 'git config --global core.autocrlf input'
}
}
echo "Clonage du repository depuis GitHub (branche: ${env.GIT_BRANCH})..."
checkout scm
}
}
stage('2. Compiler le projet') {
steps {
echo 'Compilation du projet Maven...'
script{
if (isUnix()){
sh 'mvn clean compile'
}
else{
bat 'mvn clean compile'
}
}
}
}
stage('3. Lancer les tests unitaires') {
steps {
echo 'Execution des tests unitaires...'
script{
if (isUnix()){
sh 'mvn test'
}
else{
bat 'mvn test'
}
}
}
post {
always {
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml'
}
}
}
stage('4. Generer le package WAR/JAR') {
steps {
echo 'Creation du package WAR...'
script{
if (isUnix()){
sh 'mvn package -DskipTests'
}
else{
bat 'mvn package -DskipTests'
}
}
}
post {
success {
archiveArtifacts artifacts: '**/target/*.war', allowEmptyArchive: true, fingerprint: true
}
}
}
stage('5. Analyse SonarQube') {
steps {
echo 'Lancement de l\'analyse SonarQube...'
withSonarQubeEnv('SonarQube') {
script{
if (isUnix()){
sh 'mvn sonar:sonar'
}
else{
bat 'mvn sonar:sonar'
}
}
}
}
}
stage('6. Quality Gate') {
steps {
echo 'Verification du Quality Gate...'
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('7. Build Docker Image') {
steps {
echo 'Construction de l\'image Docker...'
script {
if (isUnix()) {
sh "docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} ."
sh "docker rmi ${DOCKER_IMAGE}:latest || exit 0"
sh "docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest"
} else {
bat "docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} ."
bat "docker rmi ${DOCKER_IMAGE}:latest || exit 0"
bat "docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest"
}
}
}
}
stage('8. Push Docker Image') {
steps {
echo 'Push de l\'image vers Docker Hub...'
script {
withCredentials([usernamePassword(credentialsId: 'dockerhub-credentials', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
if (isUnix()) {
sh 'echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin'
sh "docker push ${DOCKER_IMAGE}:${DOCKER_TAG}"
sh "docker push ${DOCKER_IMAGE}:latest"
} else {
bat 'echo %DOCKER_PASS% | docker login -u %DOCKER_USER% --password-stdin'
bat "docker push ${DOCKER_IMAGE}:${DOCKER_TAG}"
bat "docker push ${DOCKER_IMAGE}:latest"
}
}
}
}
}
stage('9. Deploy to Kubernetes') {
steps {
echo 'Deploiement de l\'application sur Kubernetes...'
script {
if (isUnix()) {
sh 'cd k8s && ./deploy.sh'
} else {
bat 'cd k8s && powershell -ExecutionPolicy Bypass -File deploy.ps1'
}
}
}
}
stage('10. Deploy Monitoring Stack') {
steps {
echo 'Deploiement de Prometheus et Grafana...'
script {
if (isUnix()) {
sh 'cd k8s/monitoring && ./deploy.sh'
} else {
bat 'cd k8s\\monitoring && powershell -ExecutionPolicy Bypass -File deploy.ps1'
}
}
}
}
}
post {
success {
echo '==============Le Pipeline est execute avec succes!=============='
echo "Branche: ${GIT_BRANCH}"
echo "Image Docker publiee: ${DOCKER_IMAGE}:${DOCKER_TAG}"
echo "Application deployee sur: http://ywti.local/html/index.html"
echo "Grafana: http://localhost:30300"
echo "Prometheus: http://localhost:30090"
}
failure {
echo '==============Le pipeline a echoue.=============='
}
always {
echo "workspace: ${WORKSPACE}"
echo 'Cleaning workspace...'
cleanWs()
}
}
}tools {
maven 'Maven-3.9.11'
jdk 'JDK-17'
}Purpose: Specifies which Maven and JDK versions Jenkins should use. These must be pre-configured in Jenkins under Manage Jenkins > Global Tool Configuration.
environment {
MAVEN_OPTS = '-Xmx1024m' // Allocates 1GB RAM to Maven
SCANNER_HOME = tool 'SonarScanner' // SonarQube scanner location
DOCKER_IMAGE = 'gyro0/ywti' // Docker Hub repository
DOCKER_TAG = "2.0" // Image version tag
GIT_BRANCH = "develop" // Target branch for deployment
}Why these variables?
MAVEN_OPTS: Prevents OutOfMemoryError during large buildsSCANNER_HOME: Required by SonarQube Maven pluginDOCKER_IMAGE&DOCKER_TAG: Avoid hardcoding values in multiple stages
when {
anyOf {
branch 'develop'
}
}Purpose: Only runs the pipeline when code is pushed to the develop branch.
Cross-platform handling:
if (isUnix()){
sh 'git config --global core.autocrlf input'
}This prevents Windows line-ending issues (\r\n) in Unix/Linux environments.
script{
if (isUnix()){
sh 'mvn clean compile'
}
else{
bat 'mvn clean compile'
}
}Purpose: Compiles Java source code without running tests.
Cross-platform:
shfor Linux/macOSbatfor Windows
post {
always {
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml'
}
}Purpose: Executes JUnit tests and publishes results to Jenkins dashboard.
Benefits:
- Test trend graphs
- Failure notifications
- Historical test data
bat 'mvn package -DskipTests'Purpose: Creates deployable WAR file (skips tests since they already ran in Stage 3).
post {
success {
archiveArtifacts artifacts: '**/target/*.war', fingerprint: true
}
}Artifact Archiving: Stores the WAR file in Jenkins for download/deployment.
withSonarQubeEnv('SonarQube') {
bat 'mvn sonar:sonar'
}Purpose: Sends code to SonarQube for quality analysis.
What withSonarQubeEnv does:
- Injects SonarQube server URL
- Adds authentication token automatically
- Configures Maven plugin parameters
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}Purpose: Waits for SonarQube to finish analysis and checks if code passes quality standards.
Behavior:
- If Quality Gate PASSES: Pipeline continues
- If Quality Gate FAILS: Pipeline aborts immediately
- If no response in 5 minutes: Pipeline times out
bat "docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} ."
bat "docker rmi ${DOCKER_IMAGE}:latest || exit 0"
bat "docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest"Steps:
- Builds image with version tag (e.g.,
gyro0/ywti:2.0) - Removes old
latesttag (ignores error if doesn't exist) - Tags new image as
latest
Why two tags?
2.0: Specific version for rollback capabilitylatest: Always points to newest version
withCredentials([usernamePassword(credentialsId: 'dockerhub-credentials', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
bat 'echo %DOCKER_PASS% | docker login -u %DOCKER_USER% --password-stdin'
bat "docker push ${DOCKER_IMAGE}:${DOCKER_TAG}"
bat "docker push ${DOCKER_IMAGE}:latest"
}Security: Credentials are stored securely in Jenkins and never exposed in logs.
Pushes:
gyro0/ywti:2.0gyro0/ywti:latest
bat 'cd k8s && powershell -ExecutionPolicy Bypass -File deploy.ps1'Purpose: Runs the Kubernetes deployment script (covered in Step 5).
What it deploys:
- PostgreSQL database (1 pod)
- YWTI application (2 pods)
- Services and Ingress
bat 'cd k8s\\monitoring && powershell -ExecutionPolicy Bypass -File deploy.ps1'Purpose: Deploys Prometheus and Grafana for monitoring (covered in Step 6).
post {
success {
echo "Application deployee sur: http://ywti.local/html/index.html"
echo "Grafana: http://localhost:30300"
echo "Prometheus: http://localhost:30090"
}
failure {
echo '==============Le pipeline a echoue.=============='
}
always {
cleanWs() // Deletes workspace to save disk space
}
}Cleanup: The cleanWs() command prevents disk space issues by removing build artifacts after each run.
| Stage | Description | Command | Duration |
|---|---|---|---|
| 1 | Clone repository from GitHub | checkout scm |
10s |
| 2 | Compile Java source code | mvn clean compile |
30s |
| 3 | Run JUnit tests | mvn test |
45s |
| 4 | Package WAR file | mvn package |
20s |
| 5 | SonarQube code analysis | mvn sonar:sonar |
60s |
| 6 | Wait for Quality Gate result | Webhook callback | 30s |
| 7 | Build Docker image | docker build |
90s |
| 8 | Push image to Docker Hub | docker push |
120s |
| 9 | Deploy to Kubernetes cluster | PowerShell script | 60s |
| 10 | Deploy monitoring stack | PowerShell script | 45s |
SonarQube automatically analyzes source code to detect bugs, vulnerabilities, code smells, duplications, and test coverage using JaCoCo.
Download: 🔗 https://www.sonarsource.com/products/sonarqube/downloads/
Run SonarQube :
sonarqube/bin/windows-x86-64/StartSonar.bat
Access SonarQube: http://localhost:9000/
-
SonarQube → My Account → Security
-
Create token:
- Name:
Jenkins - Type: User Token
- Expiration: 90 days
- Name:
-
Copy the token (starts with
squ_)
Jenkins → Manage Jenkins → Plugins → Install SonarQube Scanner
Jenkins → Manage Jenkins → System → SonarQube Servers
| Field | Value |
|---|---|
| Name | SonarQube |
| URL | http://localhost:9000 |
| Credentials | Token created earlier |
Jenkins → Manage Jenkins → Tools → SonarQube Scanner
- Install automatically: Yes
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<!-- SonarQube -->
<sonar.projectKey>yourwaytoitaly</sonar.projectKey>
<sonar.projectName>YourWayToItaly</sonar.projectName>
<sonar.host.url>http://localhost:9000</sonar.host.url>
<sonar.java.binaries>target/classes</sonar.java.binaries>
<sonar.coverage.jacoco.xmlReportPaths>target/site/jacoco/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>
<build>
<plugins>
<!-- JaCoCo Coverage -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>Docker is a platform that allows us to package an application and all its dependencies into a portable container image.
To do this, we use a Dockerfile, which is a text file containing the instructions needed to build the image (like a recipe for creating the container).
Create a file named Dockerfile in the root directory of your project:
# Build Stage
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# Runtime Stage
FROM tomcat:9-jdk17
ENV CATALINA_HOME=/usr/local/tomcat \
POSTGRES_HOST=postgres \
POSTGRES_PORT=5433 \
POSTGRES_DB=yourwaytoitaly \
POSTGRES_USER=ywti \
POSTGRES_PASSWORD=ywti_password
# Install dependencies and clean up
RUN apt-get update && apt-get install -y unzip && \
rm -rf /var/lib/apt/lists/* && \
rm -rf ${CATALINA_HOME}/webapps/*
# Setup Application
COPY --from=builder /build/target/ywti-wa2021-1.0-SNAPSHOT.war /tmp/ROOT.war
RUN mkdir -p ${CATALINA_HOME}/webapps/ROOT && \
cd ${CATALINA_HOME}/webapps/ROOT && \
unzip -q /tmp/ROOT.war && \
rm /tmp/ROOT.war
# Startup script for env var substitution
RUN echo '#!/bin/bash\nset -e\n\
for var in POSTGRES_HOST POSTGRES_PORT POSTGRES_DB POSTGRES_USER POSTGRES_PASSWORD; do\n\
sed -i "s|\${${var}}|${!var}|g" ${CATALINA_HOME}/webapps/ROOT/META-INF/context.xml\n\
done\n\
exec catalina.sh run' > /startup.sh && chmod +x /startup.sh
EXPOSE 8080
CMD ["/startup.sh"]FROM maven:3.9-eclipse-temurin-17 AS builder- Uses an official image containing Maven and Java 17.
AS buildergives a name to this build stage (used later to copy artifacts).
WORKDIR /build- Sets
/buildas the working directory inside the image. - All subsequent commands run from this directory.
COPY pom.xml .
RUN mvn dependency:go-offline -B- Copies only the
pom.xmlfile first. - Downloads all Maven dependencies in advance (
go-offline), speeding up subsequent builds.
COPY src ./src
RUN mvn clean package -DskipTests -B- Copies the source code into
/build/src. - Compiles and packages the project, generating the
.warfile while skipping tests.
This build stage is only used to compile the application and produce the deployable artifact.
FROM tomcat:9-jdk17- Uses a lightweight image containing Tomcat 9 and Java 17.
- This is the runtime environment where the application will actually run.
ENV CATALINA_HOME=/usr/local/tomcat \
POSTGRES_HOST=postgres \
POSTGRES_PORT=5433 \
POSTGRES_DB=yourwaytoitaly \
POSTGRES_USER=ywti \
POSTGRES_PASSWORD=ywti_password-
Sets environment variables for:
CATALINA_HOME: Tomcat installation directory.- Database connection parameters: host, port, name, user, password.
-
These will be injected later into the
context.xmlfile.
RUN apt-get update && apt-get install -y unzip && \
rm -rf /var/lib/apt/lists/* && \
rm -rf ${CATALINA_HOME}/webapps/*- Installs
unzip(needed to extract the WAR file). - Cleans up APT cache to reduce image size.
- Removes default webapps (like
ROOT,docs, etc.) from Tomcat.
COPY --from=builder /build/target/ywti-wa2021-1.0-SNAPSHOT.war /tmp/ROOT.war- Copies the
.warfile produced in the builder stage into/tmp/ROOT.warinside the runtime image.
RUN mkdir -p ${CATALINA_HOME}/webapps/ROOT && \
cd ${CATALINA_HOME}/webapps/ROOT && \
unzip -q /tmp/ROOT.war && \
rm /tmp/ROOT.war- Creates the
ROOTapplication directory. - Unzips the WAR content into
webapps/ROOT, so the application is accessible as the root context/. - Removes the temporary WAR file.
RUN echo '#!/bin/bash\nset -e\n\
for var in POSTGRES_HOST POSTGRES_PORT POSTGRES_DB POSTGRES_USER POSTGRES_PASSWORD; do\n\
sed -i "s|\${${var}}|${!var}|g" ${CATALINA_HOME}/webapps/ROOT/META-INF/context.xml\n\
done\n\
exec catalina.sh run' > /startup.sh && chmod +x /startup.sh-
Creates a startup script
/startup.shthat:- Replaces placeholders like
${POSTGRES_HOST}inMETA-INF/context.xmlwith actual env values. - Starts Tomcat (
catalina.sh run).
- Replaces placeholders like
-
Marks the script as executable.
EXPOSE 8080- Documents that the container listens on port 8080 (Tomcat default).
CMD ["/startup.sh"]-
Defines the command executed when the container starts:
- Runs the startup script, injects DB values, then launches Tomcat.
From your project root (where the Dockerfile is located):
cd C:\Path\To\YourProject
# Build the Docker image
docker build -t YourUserName/YourProjectName:1.0 .You can update the tag (1.0, 1.1, etc.) for each new version:
docker build -t YourUserName/YourProjectName:1.1 .- Go to https://hub.docker.com
- Create an account and log in.
# Log in to Docker Hub
docker login
# Push a specific version
docker push YourUserName/YourProjectName:1.0
# Tag the image as "latest" (most stable version)
docker tag YourUserName/YourProjectName:1.0 YourUserName/YourProjectName:latest
# Push the "latest" tag
docker push YourUserName/YourProjectName:latestCreate a docker-compose.yml file in your project root:
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: yourwaytoitaly
POSTGRES_USER: ywti
POSTGRES_PASSWORD: ywti_password
PGPORT: 5433
ports:
- "5433:5433"
command: -p 5433
volumes:
- postgres_data:/var/lib/postgresql/data
- ./src/database/CreateTables.sql:/docker-entrypoint-initdb.d/01-schema.sql
- ./src/database/INSERT.sql:/docker-entrypoint-initdb.d/02-data.sql
restart: unless-stopped
app:
build: .
image: gyro0/ywti:latest
depends_on:
postgres:
condition: service_started
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: 5433
POSTGRES_DB: yourwaytoitaly
POSTGRES_USER: ywti
POSTGRES_PASSWORD: ywti_password
ports:
- "8081:8080"
restart: unless-stopped
volumes:
postgres_data:-
postgresservice:- Runs PostgreSQL 15.
- Exposes it on port 5433.
- Initializes DB schema and data from SQL files.
-
appservice:- Uses the image
gyro0/ywti:latest(built previously or built frombuild: .). - Connects to the
postgresservice using the environment variables. - Maps container port
8080(Tomcat) to host port8081, just because we are already running Jenkins on port8080.
- Uses the image
From the folder containing docker-compose.yml:
# Start all services in detached mode
docker-compose up -d
# Follow logs of all services
docker-compose logs -f
# Check the status of services
docker-compose psOpen the application in your browser:
start http://localhost:8081/html/index.htmlNote: We mapped
8081:8080, so the app is available on port 8081 on the host.
To stop and clean up:
# Stop containers
docker-compose down
# Stop containers and remove volumes (wipe all data)
docker-compose down -vKubernetes is a container orchestration platform that automates the deployment, scaling, and management of containerized applications. In this step, we deploy our JEE application and PostgreSQL database on a local Kubernetes cluster provided by Docker Desktop.
Kubernetes manages containers at scale by:
- Automatically restarting failed containers
- Load balancing traffic across multiple instances
- Providing service discovery and DNS
- Managing configuration and secrets
- Enabling zero-downtime deployments
┌──────────────────────── Kubernetes Cluster ────────────────────────┐
│ │
│ ┌─────────────────── Control Plane ──────────────────┐ │
│ │ - API Server (receives kubectl commands) │ │
│ │ - Scheduler (assigns pods to nodes) │ │
│ │ - Controller Manager (maintains desired state) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────── Worker Node ────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Pod │ │ Pod │ │ Pod │ │ │
│ │ │ ywti-app │ │ ywti-app │ │ postgres │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ kubelet (node agent) kube-proxy (networking) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
| Object | Description | Our Usage |
|---|---|---|
| Pod | Smallest deployable unit containing one or more containers | 1 postgres pod + 2 ywti-app pods |
| Deployment | Manages replicas of pods and rolling updates | One for database, one for application |
| Service | Stable network endpoint for accessing pods | ClusterIP for internal communication |
| Ingress | HTTP routing and load balancing | Routes ywti.local to our application |
| Job | One-time task that runs to completion | Database initialization |
Docker Desktop includes a built-in single-node Kubernetes cluster, perfect for local development.
- Open Docker Desktop
- Click the Settings icon (gear icon in top-right)
- Navigate to Kubernetes in the left sidebar
- Check Enable Kubernetes
- Click Apply & Restart
- Wait 2-5 minutes until status shows Kubernetes is running
# Check current context
kubectl config current-context
# Expected output: docker-desktop
# Check cluster nodes
kubectl get nodes
# Expected output:
# NAME STATUS ROLES AGE VERSION
# docker-desktop Ready control-plane 5m v1.29.1
# Verify cluster connectivity
kubectl cluster-info
# Expected: Kubernetes control plane is running at https://kubernetes.docker.internal:6443All Kubernetes manifests are organized in the k8s directory:
project/
├── k8s/
│ ├── deployment.yaml # Defines postgres + ywti-app pods
│ ├── service.yaml # Network access to pods
│ ├── ingress.yaml # HTTP routing rules
│ ├── init-db-job.yaml # Database initialization
│ └── deploy.ps1 # Automation script
├── src/
├── Dockerfile
├── docker-compose.yml
└── pom.xml
Create the directory:
cd C:\Users\Gyro\Desktop\project
mkdir k8s
cd k8sThis file defines two separate deployments: one for PostgreSQL (1 replica) and one for the YWTI application (2 replicas for high availability).
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5433
env:
- name: POSTGRES_DB
value: yourwaytoitaly
- name: POSTGRES_USER
value: ywti
- name: POSTGRES_PASSWORD
value: ywti_password
- name: PGPORT
value: "5433"
command: ["docker-entrypoint.sh"]
args: ["-p", "5433"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ywti-app
spec:
replicas: 2
selector:
matchLabels:
app: ywti-app
template:
metadata:
labels:
app: ywti-app
spec:
containers:
- name: ywti-app
image: gyro0/ywti:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: POSTGRES_HOST
value: postgres
- name: POSTGRES_PORT
value: "5433"
- name: POSTGRES_DB
value: yourwaytoitaly
- name: POSTGRES_USER
value: ywti
- name: POSTGRES_PASSWORD
value: ywti_passwordExplanation:
PostgreSQL Deployment:
replicas: 1- Single database instance (databases are stateful, we avoid multiple writers)image: postgres:15- Official PostgreSQL 15 Docker imagePGPORT: "5433"- Runs on port 5433 instead of default 5432 to avoid conflictscommandandargs- Configures PostgreSQL to listen on port 5433
YWTI Application Deployment:
replicas: 2- Two instances for high availability and load balancingimage: gyro0/ywti:latest- Our Docker image from Docker HubimagePullPolicy: IfNotPresent- Uses local image if available (faster deployment)- Environment variables - Database connection parameters that connect to the
postgresservice
Why 2 replicas for the app?
- If one pod crashes, the other continues serving requests
- Kubernetes automatically load balances traffic between both pods
- Simulates a production environment with redundancy
Services provide stable network addresses to access pods. Even if pods are recreated with new IP addresses, the service DNS name remains constant.
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5433
targetPort: 5433
---
apiVersion: v1
kind: Service
metadata:
name: ywti-app
spec:
type: ClusterIP
selector:
app: ywti-app
ports:
- port: 80
targetPort: 8080Explanation:
Service Types:
| Type | Accessibility | Use Case |
|---|---|---|
| ClusterIP | Internal only (default) | Database, backend services |
| NodePort | External via fixed port | Development/testing |
| LoadBalancer | External via cloud provider | Production (AWS, GCP, Azure) |
PostgreSQL Service:
- No
typespecified, defaults toClusterIP - Only accessible inside the cluster
- Other pods reach it using DNS name
postgres:5433 - This provides service discovery - applications don't need to know pod IPs
YWTI Application Service:
type: ClusterIP- Internal only (external access via Ingress)port: 80- Service listens on port 80targetPort: 8080- Forwards to container port 8080 (Tomcat)- The Ingress controller routes external traffic to this service
Why use DNS names instead of IP addresses?
Instead of hardcoding IP addresses like 10.96.157.174, we use the service name postgres. Kubernetes provides automatic DNS resolution:
POSTGRES_HOST=postgres → Resolves to postgres service IP automatically
This way, even if pod IPs change, the service name remains constant.
The Ingress acts as an HTTP router that directs traffic based on hostnames and paths.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ywti-ingress
spec:
ingressClassName: nginx
rules:
- host: ywti.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ywti-app
port:
number: 80Explanation:
What is an Ingress?
An Ingress is like a reverse proxy (similar to NGINX or Apache) that routes HTTP requests to the correct service based on rules.
Traffic Flow:
Browser (http://ywti.local)
↓
Hosts file (ywti.local → 127.0.0.1)
↓
Ingress Controller (NGINX on port 80)
↓
Ingress rules (host=ywti.local → service ywti-app:80)
↓
Service ywti-app (load balances to 2 pods)
↓
One of the ywti-app pods
Configuration Parameters:
ingressClassName: nginx- Uses NGINX Ingress Controllerhost: ywti.local- Only requests to this hostname are matchedpath: /- All URLs under/are routed to the backendbackend.service.name: ywti-app- Traffic goes to theywti-appservicebackend.service.port.number: 80- Service port to target
Benefits of Ingress vs NodePort:
- Clean URL (
http://ywti.localinstead ofhttp://localhost:30080) - Standard port 80 (no need to specify port in URL)
- Supports SSL/TLS certificates for HTTPS
- Can host multiple applications on different hostnames
A Kubernetes Job is a one-time task that runs until completion. We use it to create database tables and insert sample data.
apiVersion: batch/v1
kind: Job
metadata:
name: init-db
spec:
template:
spec:
restartPolicy: Never
containers:
- name: init-db
image: postgres:15
command:
- /bin/bash
- -c
- |
cat <<'EOF' | psql -h postgres -p 5433 -U ywti -d yourwaytoitaly
CREATE TABLE IF NOT EXISTS City (
ID_city SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS Type_advertisement (
ID_type SERIAL PRIMARY KEY,
type VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS Company (
email_c VARCHAR(100) PRIMARY KEY,
name_c VARCHAR(100) NOT NULL,
phone_number VARCHAR(100) NOT NULL,
address VARCHAR(200) NOT NULL,
password VARCHAR(100) NOT NULL,
ID_city INT NOT NULL,
FOREIGN KEY (ID_city) REFERENCES City (ID_city)
);
CREATE TABLE IF NOT EXISTS Tourist (
email_t VARCHAR(100) PRIMARY KEY,
surname VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
birth_date DATE NOT NULL,
phone_number VARCHAR(100) NOT NULL,
address VARCHAR(200) NOT NULL,
password VARCHAR(100) NOT NULL,
ID_city INT NOT NULL,
FOREIGN KEY (ID_city) REFERENCES City (ID_city)
);
CREATE TABLE IF NOT EXISTS Advertisement (
ID_advertisement SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description VARCHAR(10000) NOT NULL,
score INT NOT NULL,
price INT NOT NULL,
num_tot_item INT NOT NULL,
date_start DATE NOT NULL,
date_end DATE NOT NULL,
time_start TIME NOT NULL,
time_end TIME NOT NULL,
email_c VARCHAR(100) NOT NULL,
ID_type INT NOT NULL,
FOREIGN KEY (email_c) REFERENCES Company (email_c),
FOREIGN KEY (ID_type) REFERENCES Type_advertisement (ID_type)
);
CREATE TABLE IF NOT EXISTS Tourist_Advertisement (
email_t VARCHAR(100) NOT NULL,
ID_advertisement INT NOT NULL,
num_items INT NOT NULL,
rating INT,
PRIMARY KEY (email_t, ID_advertisement),
FOREIGN KEY (email_t) REFERENCES Tourist (email_t),
FOREIGN KEY (ID_advertisement) REFERENCES Advertisement (ID_advertisement)
);
CREATE TABLE IF NOT EXISTS Review (
ID_review SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description VARCHAR(1000) NOT NULL,
email_t VARCHAR(100) NOT NULL,
ID_advertisement INT NOT NULL,
FOREIGN KEY (email_t) REFERENCES Tourist (email_t),
FOREIGN KEY (ID_advertisement) REFERENCES Advertisement (ID_advertisement)
);
CREATE TABLE IF NOT EXISTS Image (
ID_image SERIAL PRIMARY KEY,
image BYTEA NOT NULL,
ID_advertisement INT NOT NULL,
FOREIGN KEY (ID_advertisement) REFERENCES Advertisement (ID_advertisement)
);
EOF
# Insert data only if tables are empty
COUNT=$(psql -h postgres -p 5433 -U ywti -d yourwaytoitaly -t -c "SELECT COUNT(*) FROM City;")
if [ "$COUNT" -eq 0 ]; then
cat <<'EOF' | psql -h postgres -p 5433 -U ywti -d yourwaytoitaly
INSERT INTO City (name) VALUES ('Abano Terme'), ('Affi'), ('Agugliaro'), ('Altavilla Vicentina'), ('Altissimo');
INSERT INTO Type_advertisement (type) VALUES ('Museum'), ('Tour'), ('Restaurant'), ('Beach'), ('Park');
INSERT INTO Company (email_c, name_c, phone_number, address, password, ID_city) VALUES ('company1@example.com', 'Tourism Venice', '+39 041 1234567', 'Via Roma 1, Venice', 'password123', 1);
INSERT INTO Tourist (email_t, surname, name, birth_date, phone_number, address, password, ID_city) VALUES ('tourist1@example.com', 'Rossi', 'Mario', '1990-01-15', '+39 340 1234567', 'Via Dante 10, Padua', 'password123', 1);
EOF
echo "Database initialized successfully!"
else
echo "Database already initialized, skipping data insertion."
fi
env:
- name: PGPASSWORD
value: ywti_password
backoffLimit: 3Explanation:
Why use a Job instead of manual SQL execution?
- Automatic - Runs during deployment without manual intervention
- Idempotent - Can be re-run safely (thanks to
IF NOT EXISTS) - Logged - Kubernetes stores logs for debugging with
kubectl logs - Repeatable - Same initialization every time
How it works:
- Creates a temporary pod using the
postgres:15image - Connects to the
postgresservice using-h postgres -p 5433 - Runs SQL commands to create tables (if they don't exist)
- Checks if
Citytable is empty - If empty, inserts sample data
- Pod completes and stops
Configuration Parameters:
restartPolicy: Never- Don't restart if the job failsbackoffLimit: 3- Retry up to 3 times if it failsPGPASSWORDenvironment variable - Authenticates to PostgreSQL without password promptCREATE TABLE IF NOT EXISTS- Prevents errors if tables already existCOUNTcheck - Only inserts data once to avoid duplicates
To avoid typing multiple kubectl commands, we created a PowerShell script that automates the entire deployment process.
#script de deploiement Kubernetes
Write-Host "=== Deploiement sur Kubernetes (Docker Desktop) ===" -ForegroundColor Cyan
# Verifier le contexte
$context = kubectl config current-context
if ($context -ne "docker-desktop") {
Write-Host "Erreur: Veuillez basculer vers le contexte docker-desktop" -ForegroundColor Red
kubectl config use-context docker-desktop
exit 1
}
Write-Host "Contexte actuel: $context" -ForegroundColor Green
# Appliquer tous les fichiers YAML
Write-Host "1. Deploiement des applications" -ForegroundColor Yellow
kubectl apply -f deployment.yaml
Write-Host "2. Creation des services" -ForegroundColor Yellow
kubectl apply -f service.yaml
Write-Host "3. Configuration de l'Ingress" -ForegroundColor Yellow
kubectl apply -f ingress.yaml
Write-Host "4. Attente du demarrage des pods" -ForegroundColor Yellow
Start-Sleep -Seconds 10
Write-Host "5. Initialisation de la base de donnees" -ForegroundColor Yellow
kubectl delete job init-db 2>$null
kubectl apply -f init-db-job.yaml
kubectl wait --for=condition=complete job/init-db --timeout=10s
Write-Host "6. etat du cluster:" -ForegroundColor Green
kubectl get all
Write-Host "7. Ingress:" -ForegroundColor Green
kubectl get ingress
Write-Host "=== Deploiement termine ===" -ForegroundColor Cyan
Write-Host "Lien vers l'application:" -ForegroundColor Green
Write-Host " http://ywti.local/html/index.html" -ForegroundColor CyanExplanation:
What the script does (step-by-step):
- Checks Kubernetes context - Ensures we're using
docker-desktopcluster - Deploys applications - Creates postgres and ywti-app pods
- Creates services - Makes pods accessible via stable network addresses
- Configures Ingress - Sets up HTTP routing rules
- Waits for pods - Gives containers time to start (10 seconds)
- Initializes database - Runs the init-db Job
- Deletes old job first (if exists) with
2>$nullto suppress error messages - Waits for job completion with a 10-second timeout
- Deletes old job first (if exists) with
- Shows cluster status - Displays all Kubernetes resources
- Shows Ingress configuration - Displays HTTP routing rules
- Prints access URL - Shows how to reach the application
Before using the script, allow PowerShell script execution:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -ForceThe Ingress object is just configuration. We need an Ingress Controller (actual software) to process those rules. We use NGINX.
Installation:
# Install NGINX Ingress Controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.1/deploy/static/provider/cloud/deploy.yaml
# Wait for it to be ready (1-2 minutes)
kubectl wait --namespace ingress-nginx `
--for=condition=ready pod `
--selector=app.kubernetes.io/component=controller `
--timeout=120sVerify Installation:
kubectl get svc -n ingress-nginx
# Expected output:
# NAME TYPE EXTERNAL-IP PORT(S)
# ingress-nginx-controller LoadBalancer localhost 80:xxxxx/TCP,443:xxxxx/TCPThe EXTERNAL-IP: localhost confirms the Ingress Controller is accessible on localhost:80.
cd C:\Users\Gyro\Desktop\project\k8s
.\deploy.ps1Expected Output:
=== Deploiement sur Kubernetes (Docker Desktop) ===
Contexte actuel: docker-desktop
1. Deploiement des applications
deployment.apps/postgres created
deployment.apps/ywti-app created
2. Creation des services
service/postgres created
service/ywti-app created
3. Configuration de l'Ingress
ingress.networking.k8s.io/ywti-ingress created
4. Attente du demarrage des pods
5. Initialisation de la base de donnees
job.batch/init-db created
job.batch/init-db condition met
6. etat du cluster:
NAME READY STATUS RESTARTS AGE
pod/init-db-xxxxx 0/1 Completed 0 15s
pod/postgres-xxxxx 1/1 Running 0 25s
pod/ywti-app-xxxxx 1/1 Running 0 25s
pod/ywti-app-yyyyy 1/1 Running 0 25s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10m
service/postgres ClusterIP 10.105.157.174 <none> 5433/TCP 20s
service/ywti-app ClusterIP 10.109.12.134 <none> 80/TCP 20s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/postgres 1/1 1 1 25s
deployment.apps/ywti-app 2/2 2 2 25s
7. Ingress:
NAME CLASS HOSTS ADDRESS PORTS AGE
ywti-ingress nginx ywti.local localhost 80 15s
=== Deploiement termine ===
Lien vers l'application:
http://ywti.local/html/index.html
cd C:\Users\Gyro\Desktop\project\k8s
# Deploy workloads
kubectl apply -f deployment.yaml
# Create services
kubectl apply -f service.yaml
# Configure Ingress
kubectl apply -f ingress.yaml
# Wait for pods to start
Start-Sleep -Seconds 10
# Initialize database
kubectl apply -f init-db-job.yaml
kubectl wait --for=condition=complete job/init-db --timeout=60s
# Check cluster state
kubectl get all
kubectl get ingresskubectl get pods
# Expected output:
# NAME READY STATUS RESTARTS AGE
# init-db-xxxxx 0/1 Completed 0 2m
# postgres-xxxxx 1/1 Running 0 3m
# ywti-app-xxxxx 1/1 Running 0 3m
# ywti-app-yyyyy 1/1 Running 0 3mPod Status Meanings:
| Status | Meaning |
|---|---|
Running |
Pod is healthy and working |
Completed |
Job finished successfully |
Pending |
Waiting for resources |
CrashLoopBackOff |
Pod keeps crashing |
Error |
Pod failed |
kubectl get services
# Expected output:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10m
# postgres ClusterIP 10.105.157.174 <none> 5433/TCP 5m
# ywti-app ClusterIP 10.109.12.134 <none> 80/TCP 5mkubectl get ingress
# Expected output:
# NAME CLASS HOSTS ADDRESS PORTS AGE
# ywti-ingress nginx ywti.local localhost 80 5m# Connect to PostgreSQL
kubectl exec -it deployment/postgres -- psql -U ywti -d yourwaytoitaly
# Inside PostgreSQL shell:
\dt # List tables (should see 8 tables)
SELECT COUNT(*) FROM City; # Should return 5
SELECT COUNT(*) FROM Type_advertisement; # Should return 5
\q # ExitTo access the application via the custom hostname ywti.local, we need to add an entry to the hosts file.
Step 1: Open Notepad as Administrator
notepad C:\Windows\System32\drivers\etc\hostsStep 2: Add this line at the end:
127.0.0.1 ywti.local
Step 3: Save and close
ping ywti.local
# Expected output:
# Pinging ywti.local [127.0.0.1] with 32 bytes of data:
# Reply from 127.0.0.1: bytes=32 time<1ms TTL=128start http://ywti.local/html/index.htmlTraffic Flow:
Browser (http://ywti.local)
↓
Hosts file (ywti.local → 127.0.0.1)
↓
Ingress Controller NGINX (port 80)
↓
Ingress rule (routes to ywti-app service)
↓
Service ywti-app (load balances between 2 pods)
↓
One of the ywti-app pods
Benefits:
- Clean URL without port number
- Standard HTTP port (80)
- Production-like setup
- Ready for HTTPS with SSL certificates
Viewing Resources:
kubectl get pods # List all pods
kubectl get services # List all services
kubectl get deployments # List all deployments
kubectl get ingress # List ingress rules
kubectl get all # Overview of all resourcesInspecting Resources:
kubectl describe pod <pod-name> # Detailed pod information and events
kubectl logs <pod-name> # View container logs
kubectl logs -f deployment/ywti-app # Follow logs in real-time
kubectl exec -it <pod-name> -- bash # Open shell inside podManaging Deployments:
kubectl scale deployment ywti-app --replicas=3 # Change to 3 instances
kubectl rollout restart deployment/ywti-app # Restart all pods
kubectl rollout status deployment/ywti-app # Check rollout progress
kubectl rollout undo deployment/ywti-app # Rollback to previous versionCleanup:
kubectl delete -f deployment.yaml # Delete deployments
kubectl delete -f service.yaml # Delete services
kubectl delete -f ingress.yaml # Delete ingress
kubectl delete job init-db # Delete jobMonitoring is essential for maintaining application health and performance. In this step, we deploy Prometheus for metrics collection and Grafana for visualization.
┌─────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ YWTI App │ │ PostgreSQL │ │
│ │ (Pods) │ │ (Pod) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ │ Metrics │ │
│ └─────────────┬───────┘ │
│ │ │
│ ┌──────▼──────────┐ │
│ │ Prometheus │ Scrapes Metrics │
│ │ (NodePort) │ │
│ └──────┬──────────┘ │
│ │ │
│ │ Queries │
│ ┌──────▼──────────┐ │
│ │ Grafana │ Visualizes │
│ │ (NodePort) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │
│ http://localhost:30090 │ http://localhost:30300
└───────────────────────────┘
| Metric | Description | Source |
|---|---|---|
| CPU Usage | CPU consumption per pod (ywti-app, postgres) | cAdvisor |
| Memory Usage | RAM consumption per pod | cAdvisor |
| Pod Status | Running, pending, failed pods | Kubelet |
| Node Health | Kubernetes node status | Kubelet |
Prometheus scrapes metrics from the Kubernetes cAdvisor endpoint, which provides container-level statistics.
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
data:
prometheus.yml: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'kubernetes-cadvisor'
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
kubernetes_sd_configs:
- role: node
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- target_label: __address__
replacement: kubernetes.default.svc:443
- source_labels: [__meta_kubernetes_node_name]
regex: (.+)
target_label: __metrics_path__
replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisorExplanation:
scrape_interval: 15s- Prometheus collects metrics every 15 secondsjob_name: 'kubernetes-cadvisor'- Scrapes container metrics from Kubernetes nodesbearer_token_file- Authenticates with Kubernetes API using the service account token/metrics/cadvisor- Endpoint exposing CPU/memory data from containers
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: prometheus
rules:
- apiGroups: [""]
resources: ["nodes", "nodes/proxy", "nodes/metrics", "services", "endpoints", "pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: prometheus
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus
subjects:
- kind: ServiceAccount
name: prometheus
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
serviceAccountName: prometheus
containers:
- name: prometheus
image: prom/prometheus:latest
args:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- containerPort: 9090
volumeMounts:
- name: config-volume
mountPath: /etc/prometheus
volumes:
- name: config-volume
configMap:
name: prometheus-config
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
spec:
type: NodePort
selector:
app: prometheus
ports:
- port: 9090
targetPort: 9090
nodePort: 30090Explanation:
- ServiceAccount - Allows Prometheus to access Kubernetes API
- ClusterRole - Grants permissions to read nodes, pods, and metrics
- ClusterRoleBinding - Binds the role to the service account
- Deployment - Runs Prometheus in a pod with the configuration mounted
- NodePort Service - Exposes Prometheus on
http://localhost:30090
Grafana needs to know where to fetch data from (Prometheus) and which dashboards to display.
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-datasources
data:
datasources.yaml: |
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
uid: prometheus
---
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboards-provider
data:
dashboards.yaml: |
apiVersion: 1
providers:
- name: 'Default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboardsExplanation:
url: http://prometheus:9090- Grafana queries Prometheus using Kubernetes internal DNSuid: prometheus- Used in dashboard JSON to reference this data sourcepath: /var/lib/grafana/dashboards- Where pre-configured dashboards are loaded from
This ConfigMap contains a complete dashboard JSON that automatically appears in Grafana.
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard
data:
ywti-dashboard.json: |
{
"title": "YWTI Application Monitoring",
"panels": [
{
"title": "CPU Usage by Pod",
"targets": [
{
"expr": "sum(rate(container_cpu_usage_seconds_total{pod=~\"ywti-app.*|postgres.*\", container!=\"POD\"}[5m])) by (pod)",
"legendFormat": "{{pod}}"
}
],
"type": "timeseries"
},
{
"title": "Memory Usage by Pod",
"targets": [
{
"expr": "sum(container_memory_usage_bytes{pod=~\"ywti-app.*|postgres.*\", container!=\"POD\"}) by (pod)",
"legendFormat": "{{pod}}"
}
],
"type": "timeseries"
}
]
}Prometheus Queries Explained:
| Query | Description |
|---|---|
sum(rate(container_cpu_usage_seconds_total{pod=~"ywti-app.*|postgres.*", container!="POD"}[5m])) by (pod) |
Calculates CPU usage rate over 5 minutes for ywti-app and postgres pods |
sum(container_memory_usage_bytes{pod=~"ywti-app.*|postgres.*", container!="POD"}) by (pod) |
Shows current memory usage for those pods |
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:latest
ports:
- containerPort: 3000
env:
- name: GF_SECURITY_ADMIN_PASSWORD
value: "admin"
- name: GF_PATHS_PROVISIONING
value: /etc/grafana/provisioning
volumeMounts:
- name: grafana-storage
mountPath: /var/lib/grafana
- name: grafana-datasources
mountPath: /etc/grafana/provisioning/datasources
- name: grafana-dashboards-provider
mountPath: /etc/grafana/provisioning/dashboards
- name: grafana-dashboard
mountPath: /var/lib/grafana/dashboards
volumes:
- name: grafana-storage
persistentVolumeClaim:
claimName: grafana-pvc
- name: grafana-datasources
configMap:
name: grafana-datasources
- name: grafana-dashboards-provider
configMap:
name: grafana-dashboards-provider
- name: grafana-dashboard
configMap:
name: grafana-dashboard
---
apiVersion: v1
kind: Service
metadata:
name: grafana
spec:
type: NodePort
ports:
- port: 3000
targetPort: 3000
nodePort: 30300
selector:
app: grafanaExplanation:
- PersistentVolumeClaim - Stores Grafana data persistently (survives pod restarts)
- Volume Mounts:
grafana-datasources- Prometheus connection configurationgrafana-dashboards-provider- Dashboard auto-loading configurationgrafana-dashboard- Pre-built dashboard JSON
- NodePort 30300 - Access Grafana at
http://localhost:30300
Write-Host "=== Deploiement Des Services De Monitoring ===" -ForegroundColor Cyan
$context = kubectl config current-context
if ($context -ne "docker-desktop") {
Write-Host "ERREUR: Contexte incorrect. Basculement vers docker-desktop..." -ForegroundColor Red
kubectl config use-context docker-desktop
}
Write-Host "Contexte actuel: $context" -ForegroundColor Green
# Deploy Prometheus
Write-Host "1. Deploiement de Prometheus" -ForegroundColor Yellow
kubectl apply -f prometheus-config.yaml
kubectl apply -f prometheus-deployment.yaml
# Deploy Grafana with provisioning
Write-Host "2. Deploiement de Grafana avec provisioning" -ForegroundColor Yellow
kubectl apply -f grafana-provisioning.yaml
kubectl apply -f grafana-dashboard.yaml
kubectl apply -f grafana-deployment.yaml
Write-Host "3. Attente du demarrage..." -ForegroundColor Yellow
kubectl wait --for=condition=ready pod -l app=prometheus --timeout=180s
kubectl wait --for=condition=ready pod -l app=grafana --timeout=180s
Write-Host "=== Deploiement termine ===" -ForegroundColor Green
Write-Host "Prometheus: http://localhost:30090" -ForegroundColor Cyan
Write-Host "Grafana: http://localhost:30300 (admin/admin)" -ForegroundColor Cyan
Write-Host "Dashboard 'YWTI Application Monitoring' disponible!" -ForegroundColor GreenExplanation:
- Verifies Kubernetes context is
docker-desktop - Deploys Prometheus configuration and pods
- Deploys Grafana with auto-provisioned datasources and dashboards
- Waits for pods to be ready (maximum 3 minutes)
- Prints access URLs
cd C:\Users\Gyro\Desktop\project\k8s\monitoring
# Deploy Prometheus
kubectl apply -f prometheus-config.yaml
kubectl apply -f prometheus-deployment.yaml
# Deploy Grafana
kubectl apply -f grafana-provisioning.yaml
kubectl apply -f grafana-dashboard.yaml
kubectl apply -f grafana-deployment.yaml
# Check status
kubectl get pods -l app=prometheus
kubectl get pods -l app=grafanacd C:\Users\Gyro\Desktop\project\k8s\monitoring
powershell -ExecutionPolicy Bypass -File deploy.ps1Verify Targets:
- Navigate to Status → Targets
- You should see
kubernetes-cadvisorwith UP status
Test a Query:
- Go to Graph tab
- Enter:
container_cpu_usage_seconds_total - Click Execute
- You should see CPU metrics from all containers
Login Credentials:
- Username:
admin - Password:
admin(change on first login)
Pre-Loaded Dashboard:
- Navigate to Dashboards in the left sidebar
- You should see "YWTI Application Monitoring"
- Click it to view:
- CPU Usage by Pod (line graph)
- Memory Usage by Pod (line graph)
Manual Query Verification:
- Click Explore (compass icon in left sidebar)
- Select Prometheus as data source
- Run query:
container_memory_usage_bytes{pod=~"ywti-app.*"} - Click Run Query to see memory usage data
┌─────────────────────────────────────────────────────────┐
│ STEP 1: Application Generates Metrics │
│ ├─ CPU usage (from cAdvisor) │
│ ├─ Memory usage (from cAdvisor) │
│ └─ Pod status (from Kubelet) │
└──────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ STEP 2: Prometheus Scrapes Metrics (every 15s) │
│ ├─ Reads from /metrics/cadvisor │
│ ├─ Stores time-series data │
│ └─ Exposes query API (:9090/api/v1/query) │
└──────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ STEP 3: Grafana Queries Prometheus │
│ ├─ Sends PromQL queries │
│ ├─ Receives time-series data │
│ └─ Renders graphs/dashboards │
└──────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ STEP 4: User Views Dashboard (http://localhost:30300) │
│ ├─ Real-time CPU/Memory graphs │
│ ├─ Auto-refreshes every 5 seconds │
│ └─ Historical data for analysis │
└─────────────────────────────────────────────────────────┘
| Component | Purpose | Access URL |
|---|---|---|
| Prometheus | Metrics collection and storage | http://localhost:30090 |
| Grafana | Visualization dashboards | http://localhost:30300 |
| cAdvisor | Container metrics (built-in K8s) | N/A (scraped by Prometheus) |
| ConfigMaps | Store configs and dashboards | Applied via kubectl apply |
What We Achieved:
- Automatic metrics collection from Kubernetes
- Pre-configured Grafana dashboard
- Real-time CPU/Memory monitoring
- Zero manual configuration (fully automated)
- Integrated with CI/CD pipeline (Jenkins deploys it)
This project successfully implements a complete DevOps pipeline covering all aspects of modern software delivery:
- Automated CI/CD with Jenkins (10 stages)
- Code quality enforcement with SonarQube
- Container orchestration with Kubernetes
- Real-time monitoring with Prometheus and Grafana
- Cross-platform compatibility (Windows/Linux/macOS)