Μετάβαση στο περιεχόμενο
Περισσότερα έργα

ErgoHub Ομαδικό ακαδημαϊκό έργο 2025

Από τον κώδικα της εφαρμογής σε ένα Kubernetes cluster

Το ErgoHub, μια πλατφόρμα freelance: μια εφαρμογή Spring Boot και React και η διαδρομή με Docker, Jenkins και Ansible που την έκανε deploy στο Azure Kubernetes Service

Περίοδος
Φεβρουάριος–Ιούλιος 2025
Ομάδα
Ομάδα δύο ατόμων, με την Αναστασία Ζέρβα
Πλαίσιο
Χαροκόπειο Πανεπιστήμιο: Κατανεμημένα Συστήματα και έπειτα Βασικές Έννοιες και Εργαλεία DevOps

Επισκόπηση

Το να γραφτεί η εφαρμογή ήταν το μισό πρόβλημα. Το άλλο μισό ήταν η διαδρομή από ένα commit ως ένα cluster σε λειτουργία, και να γίνεται κάθε φορά με τον ίδιο τρόπο.

Το ErgoHub είναι μια πλατφόρμα freelance με τρεις ρόλους. Οι πελάτες αναρτούν έργα και ένας διαχειριστής εγκρίνει ή απορρίπτει το καθένα πριν το δουν οι freelancers. Οι freelancers κάνουν αίτηση, αν θέλουν με το βιογραφικό τους, και ο πελάτης δέχεται μία από τις αιτήσεις· από εκεί ξεκινά η δουλειά.

Ξεκίνησε ως εργασία για το μάθημα Κατανεμημένα Συστήματα στο Χαροκόπειο Πανεπιστήμιο. Στο μάθημα Βασικές Έννοιες και Εργαλεία DevOps το πήγαμε μέχρι το τέλος: container images, Jenkins pipelines, Ansible playbooks και deployment στο Azure Kubernetes Service.

Ήμασταν ομάδα δύο ατόμων, η Αναστασία Ζέρβα κι εγώ, και δουλέψαμε και οι δύο σε όλα τα κομμάτια: στο API, στο frontend και στο deployment. Γι’ αυτό η σελίδα μιλά σε πρώτο πληθυντικό· ο απολογισμός στο τέλος είναι δικός μου.

Ήταν ακαδημαϊκό έργο: έγινε deploy και παρουσιάστηκε στο πλαίσιο του μαθήματος, δεν λειτούργησε ως υπηρεσία. Γι’ αυτό δεν υπάρχουν αριθμοί για χρήστες, κίνηση ή διαθεσιμότητα.

Με μια ματιά

Ρόλοι χρηστών
Πελάτης, freelancer, διαχειριστής
Εφαρμογή
Java 21, Spring Boot 3, Spring Security, JWT, React, Redux Toolkit, JUnit, Jest
Δεδομένα
PostgreSQL, Spring Data JPA, MinIO
Παράδοση
Docker, Jenkins, Ansible, Azure Container Registry, AKS
Είσοδος
NGINX Ingress, cert-manager, Let’s Encrypt TLS
Δύο διαδρομές μέσα στο έργο

Εφαρμογή

Ένα αίτημα, από τον browser ως τα δεδομένα

  1. Χρήστης

    Πελάτης, freelancer ή διαχειριστής, στον browser

  2. Frontend

    React και Redux Toolkit, που τα σερβίρει το NGINX

  3. API

    Spring Boot 3 σε Java 21, με JWT και έλεγχο ρόλων

  4. Δεδομένα

    PostgreSQL για τις εγγραφές, MinIO για τα βιογραφικά

Παράδοση

Μια αλλαγή, από το repository ως το cluster

  1. Κώδικας

    Ένα Git repository για το API και το frontend

  2. CI

    Το Jenkins τρέχει τα tests και φτιάχνει image

  3. Registry

    Azure Container Registry, με tag τον αριθμό του build

  4. Kubernetes

    AKS, με rollout μέσω kubectl ή Ansible

  5. Ingress

    NGINX Ingress, HTTPS με Let’s Encrypt

Οι δύο διαδρομές συναντιούνται στο cluster: το frontend και το API που απαντούν σε κάθε αίτημα είναι τα images που έφτιαξε και έκανε rollout η διαδρομή της παράδοσης.

Τρεις ρόλοι, μία ροή εργασίας

Το API είναι εφαρμογή Spring Boot 3 σε Java 21. Το frontend είναι εφαρμογή React με Redux Toolkit και ξεχωριστό dashboard για κάθε ρόλο, και μιλά με το API μέσω REST.

Ένα έργο, από την ανάρτηση ως την ολοκλήρωση
  1. PENDING

    Ο πελάτης αναρτά ένα έργο, που περιμένει τον διαχειριστή.

  2. APPROVED ή DENIED

    Ο διαχειριστής το εγκρίνει ή το απορρίπτει. Οι freelancers βλέπουν μόνο τα εγκεκριμένα έργα.

  3. IN_PROGRESS

    Οι freelancers κάνουν αίτηση, με βιογραφικό αν θέλουν. Ο πελάτης δέχεται μία αίτηση και οι υπόλοιπες απορρίπτονται.

  4. COMPLETED

    Ο freelancer ή ο διαχειριστής σημειώνει τη δουλειά ως ολοκληρωμένη.

  • Πρόσβαση ανά ρόλο

    Με τη σύνδεση ο χρήστης παίρνει ένα JSON Web Token, υπογεγραμμένο με κοινό κλειδί HMAC και έγκυρο για 24 ώρες, που περιέχει τον ρόλο του και αν ο λογαριασμός έχει επαληθευτεί. Το API δεν κρατά session: το Spring Security ελέγχει το token σε κάθε αίτημα και εφαρμόζει κανόνες ρόλων ανά διαδρομή. Οι κωδικοί αποθηκεύονται ως hashes BCrypt.

  • Εγγραφές και αρχεία

    Οι χρήστες, τα έργα και οι αιτήσεις αποθηκεύονται στην PostgreSQL μέσω Spring Data JPA. Το βιογραφικό που συνοδεύει μια αίτηση ανεβαίνει σε bucket του MinIO· η αίτηση κρατά μόνο τη διαδρομή του και το API το επιστρέφει μέσω ενός endpoint λήψης.

  • Email

    Οι διαχειριστές επαληθεύουν τους λογαριασμούς των freelancers και το API τούς ενημερώνει με email. Στο τοπικό περιβάλλον του Docker Compose τα μηνύματα κατέληγαν στο MailHog, έναν δοκιμαστικό mail server.

  • Tests

    Το API έχει JUnit tests για τα services του, που τρέχουν σε βάση H2 στη μνήμη. Το frontend έχει tests με Jest και Testing Library. Και τα δύο τρέχουν στα pipelines πριν φτιαχτεί το image.

Από τον κώδικα στο container

Το API και το frontend έχουν το καθένα δικό του image, χτισμένο σε δύο στάδια: το πρώτο περιέχει τα εργαλεία του build, το δεύτερο μόνο ό,τι χρειάζεται για να τρέξει.

  • API

    Το πρώτο στάδιο εγκαθιστά το Maven σε image Java 21 και πακετάρει την εφαρμογή. Το δεύτερο αντιγράφει μόνο το jar σε ένα καθαρό slim image Java 21, που ακούει στη θύρα 8080. Το build του image παραλείπει τα tests, γιατί το pipeline τα έχει ήδη τρέξει.

  • Frontend

    Το Node 21 κάνει build την εφαρμογή React· έπειτα το NGINX σε Alpine σερβίρει τα αρχεία του build, με δική του διαμόρφωση.

Το Docker Compose έτρεχε όλο το σύστημα σε ένα μηχάνημα: PostgreSQL 15, το API, το frontend, MinIO, MailHog και το ίδιο το Jenkins.

Και οι δύο διαδρομές προς το AKS ανεβάζουν κάθε image με δύο tags: τον αριθμό του build, που το συνδέει με την εκτέλεση του pipeline που το έφτιαξε, και το latest, στο οποίο παραπέμπουν τα manifests του Kubernetes.

Έξι ορισμοί pipeline, τρεις τρόποι για deployment

Το repository έχει έξι Jenkinsfiles: ένα για το API και ένα για το frontend, για καθέναν από τρεις τρόπους deployment.

Τρεις τρόποι για deployment

  • AKS με kubectl

    Jenkinsfile-backendJenkinsfile-frontend

    Registry
    Azure Container Registry
    Deployment
    kubectl set image, έπειτα αναμονή για το rollout και έλεγχος των pods
  • AKS με Ansible

    Jenkinsfile-backend-ansibleJenkinsfile-frontend-ansible

    Registry
    Azure Container Registry
    Deployment
    Ένα Ansible playbook στήνει όλο το σύστημα με τη σειρά και έπειτα το ελέγχει
  • Minikube με Ansible

    Jenkinsfile-backend-minikubeJenkinsfile-frontend-minikube

    Registry
    Docker Hub
    Deployment
    Ένα Ansible playbook κάνει deploy σε τοπικό cluster Minikube
Τα στάδια των δύο pipelines με kubectl, όπως ονομάζονται στα Jenkinsfiles

API

Build και tests

  1. Compile
  2. Unit Tests
  3. Integration Tests
  4. Package

Δημοσίευση

  1. Azure Login & Setup
  2. Build & Push Docker Image

Deployment

  1. Deploy to Azure AKS
  2. Health Check

Frontend

Build και tests

  1. Checkout
  2. Install Dependencies
  3. Lint
  4. Unit Tests
  5. Integration Tests
  6. Build
  7. Security Audit

Δημοσίευση

  1. Azure Login & Setup
  2. Build & Push Docker Image

Deployment

  1. Deploy to Azure AKS
  2. Health Check
  3. Verify Application

Και τα δύο pipelines με kubectl συνδέονται στο Azure με service principal και στο registry με token, ανεβάζουν το image, στρέφουν σε αυτό το Deployment με kubectl set image και περιμένουν έως πέντε λεπτά για το rollout, πριν εμφανίσουν τα pods. Το pipeline του frontend τρέχει επίσης ESLint και npm audit, που αναφέρουν προβλήματα χωρίς να σταματούν το build.

Τα pipelines με Ansible αναθέτουν το deployment σε ένα playbook, που στήνει το σύστημα με τη σειρά: το namespace, τη διαμόρφωση και τα secrets· την PostgreSQL, το MailHog και το MinIO, με αναμονή ώσπου να είναι έτοιμα· το API και το frontend με το νέο image, μαζί με τα Services τους· και στο τέλος το Ingress, με ελέγχους για ό,τι τρέχει.

Τι έτρεχε μέσα στο cluster

Όλα έτρεχαν σε ένα namespace, το freelance, από απλά manifests που εφαρμόζονταν με kubectl, είτε από ένα script εγκατάστασης είτε από ένα pipeline είτε από το Ansible. Δεν υπάρχει Helm chart.

Το namespace freelance στο Azure Kubernetes Service

Ingress

  • NGINX ingress controller
  • TLS από το Let’s Encrypt, μέσω cert-manager
  • Ανακατεύθυνση από HTTP σε HTTPS

Διαδρομές

  • /frontend-service:80
  • /apibackend-service:8080

Ξεχωριστά hostnames για το API, την κονσόλα του MinIO και το MailHog

Deployments, με ένα replica το καθένα

  • frontend

    Το NGINX με το build του React

    Service
    frontend-service:80
    Αποθήκευση
    Κανένα
    Probes
    liveness, readiness
  • backend

    Το API σε Spring Boot

    Service
    backend-service:8080
    Αποθήκευση
    Κανένα
    Probes
    startup, liveness, readiness
  • postgres

    PostgreSQL 15

    Service
    postgres-service:5432
    Αποθήκευση
    Volume claim 5 Gi
    Probes
    liveness, readiness
  • minio

    Object storage για τα βιογραφικά

    Service
    minio-service:9000 :9001NodePort
    Αποθήκευση
    Volume claim 10 Gi
    Probes
    Κανένα
  • mailhog

    Δοκιμαστικός mail server

    Service
    mailhog:8025 :1025
    Αποθήκευση
    Κανένα
    Probes
    Κανένα

Διαμόρφωση

Ένα ConfigMap, το freelance-config, και ένα Secret, που τα διαβάζουν τα workloads ως μεταβλητές περιβάλλοντος

Η PostgreSQL και το MinIO κρατούσαν τα δεδομένα τους σε persistent volume claims των 5 Gi και 10 Gi. Το καθένα έτρεχε ως Deployment με ένα replica και όχι ως StatefulSet: αρκετό για ένα deployment μαθήματος, όχι σχεδιασμός για failover.

Κάθε container είχε requests και limits για CPU και μνήμη. Τα probes του API καλούσαν το /api/home, μια διαδρομή που δεν θέλει σύνδεση, ώστε το Kubernetes να ξεχωρίζει μια εφαρμογή που ακόμη ξεκινά από μια που έχει αποτύχει.

Ένα script εγκατάστασης προετοίμασε το cluster μία φορά: εγκατέστησε τον NGINX ingress controller και το cert-manager, δημιούργησε τον issuer για το Let’s Encrypt και εφάρμοσε τα manifests. Από εκεί και πέρα, μια εκτέλεση των pipelines με kubectl χρειαζόταν μόνο να κάνει rollout ένα νέο image.

Τι χάλασε όταν ενώθηκαν τα κομμάτια

Η τελευταία εβδομάδα του Ιουνίου 2025 πήγε λιγότερο σε λειτουργίες και περισσότερο σε προβλήματα που φαίνονται μόνο όταν τα κομμάτια τρέχουν μαζί. Τέσσερα από αυτά, από το ιστορικό των commits:

  • Το Jenkins δεν έβρισκε το cluster

    Το Jenkins έτρεχε σε container και το τοπικό cluster του Minikube βρισκόταν έξω από αυτό. Η διεύθυνση στο kubeconfig πέρασε από το host.docker.internal, την IP του Minikube, ένα kubectl proxy και το gateway της γέφυρας του Docker. Στα pipelines για το AKS το ζήτημα λύθηκε αλλιώς: παίρνουν kubeconfig από το Azure στην αρχή κάθε εκτέλεσης.

  • Έλεγχοι υγείας σε ένα προστατευμένο API

    Κάθε διαδρομή πίσω από το Spring Security θέλει token, και ένα probe του Kubernetes δεν έχει. Στρέψαμε τα probes στο /api/home, μια διαδρομή χωρίς σύνδεση, και προσθέσαμε startup probe με μεγαλύτερες καθυστερήσεις, ώστε μια αργή εκκίνηση να μη θεωρείται αποτυχία.

  • Το API έμενε χωρίς μνήμη

    Το container του API τερματιζόταν επειδή ξεπερνούσε τη μνήμη του. Αυξήσαμε τη μνήμη του και δώσαμε στο JVM ρητό όριο heap μέσω JAVA_TOOL_OPTIONS, ώστε να χωρά μέσα στο όριο του container.

  • Ονόματα και tags που έπρεπε να ταιριάζουν

    Το ConfigMap ανέφερε ονόματα services που δεν ταίριαζαν με τα Services των manifests, και στα pipelines με Ansible έπρεπε να δηλωθεί ποιο tag θα γίνει deploy: ο αριθμός build του image που μόλις ανέβηκε ή το latest για το κομμάτι που δεν είχε αλλάξει.

Τι κράτησα από το έργο

Το έργο μετακίνησε το σημείο όπου θεωρώ ότι τελειώνει μια εφαρμογή. Δεν τελειώνει όταν περνούν τα tests· τελειώνει όταν κάτι έξω από το πρόγραμμα μπορεί να την ξεκινήσει, να την ελέγξει και να τη φτάσει.

  • Ο έλεγχος υγείας είναι κι αυτός interface

    Το Kubernetes γνωρίζει μια εφαρμογή μόνο μέσα από τα probes της. Η διαδρομή που καλούν είναι μέρος του API όπως όλες οι άλλες, με δικούς της κανόνες πρόσβασης, κι αν είναι λάθος, μια υπηρεσία που δουλεύει κάνει restart.

  • Η διαμόρφωση περνά από αρχείο σε αρχείο

    Ένα όνομα service σε ένα ConfigMap, ένα tag που περνά σε ένα playbook: ό,τι χάλασε ήταν τιμές γραμμένες σε ένα αρχείο και διαβασμένες σε άλλο. Θέλουν την ίδια προσοχή με τον κώδικα και ένα μόνο σημείο όπου ορίζονται.

  • Τα όρια αφορούν και το πρόγραμμα

    Ένα όριο μνήμης στο container είναι μισή ρύθμιση. Το πρόγραμμα μέσα του έχει τη δική του εικόνα για το πόση μνήμη μπορεί να χρησιμοποιήσει, και τα δύο πρέπει να επιλέγονται μαζί.

  • Γραμμένο, όχι από μνήμης

    Στο τέλος, η σύνδεση στο registry, το rollout ενός image και η αναμονή του ήταν βήματα σε ένα pipeline ή ένα playbook, όχι εντολές που έπρεπε κάποιος να θυμάται. Αυτό έκανε το δεύτερο deployment ίδιο με το πρώτο.

Πίσω στα έργα