Initial commit of the k8-mini-app project with backend, frontend, worker, and RabbitMQ integration. Added Dockerfiles, Kubernetes manifests, and necessary configurations for deployment.

This commit is contained in:
Francesco Albano 2025-05-23 01:05:47 +02:00
parent 4d4868dd06
commit fe9c240033
16 changed files with 556 additions and 0 deletions

5
backend/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]

145
backend/index.js Normal file
View File

@ -0,0 +1,145 @@
const express = require('express');
const amqp = require('amqplib');
const app = express();
app.use(express.json());
// Variabili globali
let channel = null;
let connection = null;
// Funzione per connettersi a RabbitMQ
async function connectRabbitMQ() {
try {
console.log('Tentativo di connessione a RabbitMQ...');
connection = await amqp.connect('amqp://rabbitmq');
console.log('Connessione a RabbitMQ stabilita');
// Gestione della chiusura della connessione
connection.on('close', () => {
console.warn('Connessione a RabbitMQ chiusa, tentativo di riconnessione tra 5 secondi...');
setTimeout(connectRabbitMQ, 5000);
});
// Gestione errori
connection.on('error', (err) => {
console.error('Errore nella connessione a RabbitMQ:', err);
if (!connection.closed) connection.close();
});
// Crea canale
channel = await connection.createChannel();
console.log('Canale creato');
// NUOVO: Tenta di cancellare tutti i consumatori esistenti
try {
await channel.cancel('');
console.log('Tentativo di cancellazione consumatori esistenti');
} catch (err) {
console.log('Nessun consumatore da cancellare o errore:', err.message);
}
// Configura la coda con durabilità (nome modificato)
await channel.assertQueue('messages_v2', {
durable: true, // La coda sopravvive al riavvio del broker
autoDelete: false // La coda non viene eliminata quando non ha più consumatori
});
console.log('Coda "messages_v2" verificata/creata');
return channel;
} catch (error) {
console.error('Errore nella connessione a RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000);
throw error;
}
}
// Inizializza il server dopo la connessione a RabbitMQ
async function startServer() {
try {
await connectRabbitMQ();
// Endpoint per inviare messaggi alla coda
app.post('/api/enqueue', async (req, res) => {
try {
const msg = JSON.stringify(req.body);
console.log('Invio messaggio alla coda:', msg);
// Invia il messaggio con persistenza (nome coda modificato)
channel.sendToQueue('messages_v2', Buffer.from(msg), {
persistent: true // Il messaggio è persistente
});
console.log('Messaggio inviato con successo');
res.json({ status: 'success', message: 'Messaggio inviato alla coda', data: req.body });
} catch (error) {
console.error('Errore nell\'invio del messaggio:', error);
res.status(500).json({ status: 'error', message: 'Errore nell\'invio del messaggio' });
}
});
// Endpoint per recuperare messaggi dalla coda
app.get('/api/messages', async (req, res) => {
try {
console.log('Tentativo di recupero messaggio con GET...');
// Usa il metodo get invece di consume (nome coda modificato)
const msg = await channel.get('messages_v2', { noAck: false });
if (msg) {
console.log('Messaggio trovato:', msg.content.toString());
// Conferma esplicita che il messaggio è stato ricevuto
channel.ack(msg);
const content = JSON.parse(msg.content.toString());
res.json({ status: 'success', message: content });
} else {
console.log('Nessun messaggio trovato nella coda con GET');
res.json({ status: 'empty', message: 'Nessun messaggio nella coda' });
}
} catch (error) {
console.error('Errore nel recupero del messaggio:', error);
res.status(500).json({ status: 'error', message: `Errore nel recupero: ${error.message}` });
}
});
// Endpoint di debug per verificare lo stato della coda
app.get('/api/queue-info', async (req, res) => {
try {
// Nome coda modificato
const queueInfo = await channel.assertQueue('messages_v2', { passive: true });
console.log('Informazioni coda:', queueInfo);
res.json({
status: 'success',
queue: 'messages_v2',
messageCount: queueInfo.messageCount,
consumerCount: queueInfo.consumerCount,
properties: queueInfo
});
} catch (error) {
console.error('Errore nel recupero info coda:', error);
res.status(500).json({ status: 'error', message: 'Errore nel recupero info coda' });
}
});
// Avvia il server
app.listen(3000, () => console.log('Backend in ascolto sulla porta 3000'));
} catch (error) {
console.error('Errore nell\'avvio del server:', error);
process.exit(1);
}
}
// Avvia il server
startServer();
// Gestione della chiusura pulita
process.on('SIGINT', async () => {
try {
if (channel) await channel.close();
if (connection) await connection.close();
} catch (error) {
console.error('Errore nella chiusura della connessione:', error);
} finally {
process.exit(0);
}
});

9
backend/package.json Normal file
View File

@ -0,0 +1,9 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.18.2",
"amqplib": "^0.10.3"
}
}

43
ci-build.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
# Script per buildare e pushare le immagini al registry locale
# Variabili
REGISTRY="localhost:5000"
TAG=$(date +%Y%m%d-%H%M%S) # Usa timestamp come tag
# Verifica che il registry locale sia in esecuzione
if ! curl -s http://localhost:5000/v2/ > /dev/null; then
echo "❌ Registry localhost:5000 non raggiungibile. Avvialo con: docker run -d -p 5000:5000 registry:2"
exit 1
fi
# Build e push delle immagini
echo "🔨 Building backend image..."
docker build -t $REGISTRY/backend:$TAG ./backend
docker push $REGISTRY/backend:$TAG
echo "🔨 Building frontend image..."
docker build -t $REGISTRY/frontend:$TAG ./frontend
docker push $REGISTRY/frontend:$TAG
echo "🔨 Building worker image..."
docker build -t $REGISTRY/worker:$TAG ./worker
docker push $REGISTRY/worker:$TAG
# Aggiorna i file YAML di Kubernetes
echo "📝 Updating Kubernetes manifests..."
sed -i "s|localhost:5000/backend:latest|$REGISTRY/backend:$TAG|g" k8s/backend.yaml
sed -i "s|localhost:5000/frontend:latest|$REGISTRY/frontend:$TAG|g" k8s/frontend.yaml
sed -i "s|localhost:5000/worker:latest|$REGISTRY/worker:$TAG|g" k8s/worker.yaml
# Commit e push a Gitea se necessario
if [ "$1" == "--push" ]; then
echo "🚀 Pushing changes to Gitea..."
git add k8s/
git commit -m "Update images to tag $TAG"
git push
fi
echo "✅ Build completato. Immagini taggate come: $TAG"
echo "📋 Per applicare le modifiche manualmente: kubectl apply -f k8s/"
echo "🔄 Per sincronizzare con Argo CD: argocd app sync k8-mini-app"

26
docker-compose.yml Normal file
View File

@ -0,0 +1,26 @@
version: '3'
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- "15672:15672"
- "5672:5672"
backend:
build: ./backend
ports:
- "3000:3000"
depends_on:
- rabbitmq
frontend:
build: ./frontend
ports:
- "8080:80"
depends_on:
- backend
worker:
build: ./worker
depends_on:
- rabbitmq

5
frontend/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
# Dockerfile - Frontend
FROM nginx:alpine
COPY ./index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

105
frontend/index.html Normal file
View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Frontend</title>
<style>
#messageDisplay {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
min-height: 50px;
background-color: #f9f9f9;
}
button {
margin-right: 10px;
padding: 8px 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>Mini Frontend</h1>
<button onclick="sendMessage()">Invia Messaggio</button>
<button onclick="retrieveMessage()">Recupera Messaggio</button>
<button onclick="checkQueueInfo()">Verifica Stato Coda</button>
<div id="messageDisplay">
<p>I messaggi recuperati appariranno qui...</p>
</div>
<script>
async function sendMessage() {
try {
const response = await fetch('/api/enqueue', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ message: 'Hello from frontend!' })
});
const data = await response.json();
alert('Messaggio inviato alla coda!');
} catch (error) {
console.error('Errore nell\'invio del messaggio:', error);
alert('Errore nell\'invio del messaggio!');
}
}
async function retrieveMessage() {
try {
const response = await fetch('/api/messages');
const data = await response.json();
const displayDiv = document.getElementById('messageDisplay');
if (data.status === 'success') {
displayDiv.innerHTML = `
<h3>Messaggio recuperato:</h3>
<pre>${JSON.stringify(data.message, null, 2)}</pre>
`;
} else if (data.status === 'empty') {
displayDiv.innerHTML = `<p>Nessun messaggio disponibile nella coda.</p>`;
} else {
displayDiv.innerHTML = `<p>Errore: ${data.message}</p>`;
}
} catch (error) {
console.error('Errore nel recupero del messaggio:', error);
document.getElementById('messageDisplay').innerHTML =
`<p>Errore nella connessione al backend: ${error.message}</p>`;
}
}
async function checkQueueInfo() {
try {
const response = await fetch('/api/queue-info');
const data = await response.json();
const displayDiv = document.getElementById('messageDisplay');
if (data.status === 'success') {
displayDiv.innerHTML = `
<h3>Informazioni Coda:</h3>
<p>Nome: ${data.queue}</p>
<p>Messaggi in coda: ${data.messageCount}</p>
<p>Consumatori attivi: ${data.consumerCount}</p>
<pre>Proprietà complete: ${JSON.stringify(data.properties, null, 2)}</pre>
`;
} else {
displayDiv.innerHTML = `<p>Errore: ${data.message}</p>`;
}
} catch (error) {
console.error('Errore nel recupero info coda:', error);
document.getElementById('messageDisplay').innerHTML =
`<p>Errore nella connessione al backend: ${error.message}</p>`;
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,21 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: k8-mini-app
namespace: argocd
spec:
project: default
source:
repoURL: https://git.algios.dev/francescoalbano/k8-mini-app.git
targetRevision: HEAD
path: k8s
# Se Gitea richiede autenticazione, aggiungi:
# username: git
# password: <vuoto, verrà usato un segreto>
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true

34
k8s/backend.yaml Normal file
View File

@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: localhost:5000/backend:20250523-010002
ports:
- containerPort: 3000
tolerations:
- key: "node.kubernetes.io/disk-pressure"
operator: "Exists"
effect: "NoSchedule"
---
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: backend
ports:
- port: 3000
targetPort: 3000

38
k8s/frontend.yaml Normal file
View File

@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: localhost:5000/frontend:20250523-010002
ports:
- containerPort: 80
tolerations:
- key: "node.kubernetes.io/disk-pressure"
operator: "Exists"
effect: "NoSchedule"
tolerations:
- key: "node.kubernetes.io/disk-pressure"
operator: "Exists"
effect: "NoSchedule"
---
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80

40
k8s/ingress.yaml Normal file
View File

@ -0,0 +1,40 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
spec:
ingressClassName: traefik
rules:
- host: local.test
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: backend
port:
number: 3000
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
- http: # Regola senza host per accesso via IP
paths:
- path: /api
pathType: Prefix
backend:
service:
name: backend
port:
number: 3000
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80

37
k8s/rabbitmq.yaml Normal file
View File

@ -0,0 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: rabbitmq
spec:
replicas: 1
selector:
matchLabels:
app: rabbitmq
template:
metadata:
labels:
app: rabbitmq
spec:
containers:
- name: rabbitmq
image: rabbitmq:3-management
ports:
- containerPort: 5672
- containerPort: 15672
tolerations:
- key: "node.kubernetes.io/disk-pressure"
operator: "Exists"
effect: "NoSchedule"
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
spec:
ports:
- port: 5672
name: amqp
- port: 15672
name: management
selector:
app: rabbitmq

21
k8s/worker.yaml Normal file
View File

@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker
spec:
replicas: 1
selector:
matchLabels:
app: worker
template:
metadata:
labels:
app: worker
spec:
containers:
- name: worker
image: localhost:5000/worker:20250523-010340
tolerations:
- key: "node.kubernetes.io/disk-pressure"
operator: "Exists"
effect: "NoSchedule"

5
worker/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "worker.js"]

8
worker/package.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "worker",
"version": "1.0.0",
"main": "worker.js",
"dependencies": {
"amqplib": "^0.10.3"
}
}

14
worker/worker.js Normal file
View File

@ -0,0 +1,14 @@
const amqp = require('amqplib');
(async () => {
const conn = await amqp.connect('amqp://rabbitmq');
const ch = await conn.createChannel();
await ch.assertQueue('messages');
console.log('Worker started, waiting for messages...');
ch.consume('messages', (msg) => {
if (msg !== null) {
console.log('Received:', msg.content.toString());
ch.ack(msg);
}
});
})();