Ανοίγεις ένα περίπτερο με έναν μόνο πωλητή και μια ταμπέλα «API». Ο πωλητής φορά μια κονκάρδα με το όνομά του, και σε κάθε πελάτη λέει «γεια, είμαι ο jv2xw». Όταν η ουρά μεγαλώνει, λες στον διευθυντή «θέλω τρεις πωλητές», και δύο νέες κονκάρδες εμφανίζονται. Οι πελάτες παρουσιάζονται στο ίδιο γκισέ, αλλά άλλοτε απαντά ο 4pl9z, άλλοτε ο nvshh: το φορτίο κατανέμεται. Αν ένας πωλητής φύγει, ο διευθυντής έχει ήδη καλέσει έναν άλλο πριν το προσέξεις, γιατί η οδηγία του είναι «τρεις πωλητές», όχι «αυτοί οι τρεις». Αυτό ακριβώς θα δεις: το traefik/whoami είναι ένας πωλητής που εμφανίζει το όνομά του (το όνομα του Pod), το Deployment είναι ο διευθυντής, το Service είναι το γκισέ, και το localhost:8080 είναι η πόρτα του περιπτέρου.
Όταν πληκτρολογείς kubectl create deployment api --image=traefik/whoami:v1.10, δεν δημιουργείται ένα Pod αλλά τρία φωλιασμένα αντικείμενα. Το Deployment api φέρει την πρόθεσή σου («αυτή η εικόνα, αυτός ο αριθμός replicas») και ξέρει να διαχειρίζεται τις ενημερώσεις. Δημιουργεί ένα ReplicaSet api-9bfb55fc6, του οποίου η μόνη δουλειά είναι να διατηρεί ακριβώς N ταυτόσημα Pods· το επίθημα 9bfb55fc6 είναι μια σύνοψη του προτύπου Pod (εικόνα, θύρες, labels), άρα μια νέα εικόνα θα δώσει ένα νέο ReplicaSet. Το ReplicaSet δημιουργεί τελικά τα Pods api-9bfb55fc6-jv2xw, -4pl9z… με ένα τυχαίο επίθημα πέντε χαρακτήρων. Κάθε Pod λαμβάνει μια εσωτερική IP (10.1.0.210) μη προσβάσιμη από τον browser σου.
Για να αποκτήσεις πρόσβαση, δύο διαδρομές. Το kubectl port-forward ανοίγει ένα προσωρινό tunnel από το τερματικό σου προς ένα Pod: τέλειο για επαλήθευση, άχρηστο για κατανομή φορτίου, και πεθαίνει με Ctrl+C. Ένα Service είναι το μόνιμο αντικείμενο: μια σταθερή IP στο cluster (CLUSTER-IP), ένα όνομα DNS (api.premiers-pas.svc), και μια λίστα διευθύνσεων-στόχων που τηρείται ενημερωμένη από έναν controller βάσει των labels (app=api). Τύπου LoadBalancer, ζητά επιπλέον μια εξωτερική διεύθυνση· στο Docker Desktop, το στοιχείο vpnkit-controller απαντά «localhost» και δημοσιεύει τη θύρα του Service στο μηχάνημά σου. Αυτό κάνει δυνατό το http://localhost:8080 χωρίς tunnel.
Αυτό το μάθημα δουλεύει σε προστακτική λειτουργία: κάθε εντολή λέει στο cluster τι πρέπει να κάνει, αμέσως. Είναι ο σωστός τρόπος για μια γρήγορη νίκη και για να δεις τα αντικείμενα να εμφανίζονται. Από την επόμενη ενότητα, θα γράφεις τα ίδια αντικείμενα σε YAML και θα τα εφαρμόζεις με kubectl apply: είναι η δηλωτική λειτουργία, επαναλήψιμη και εκδοσιοποιήσιμη, αυτή της πραγματικής ζωής. Οι δύο παράγουν ακριβώς τα ίδια αντικείμενα· το kubectl get deployment api -o yaml σου δείχνει ήδη τι έγραψε το create deployment στη θέση σου.
| Προστακτική εντολή | Τι δημιουργεί ή αλλάζει | Δηλωτικό ισοδύναμο (επόμενη ενότητα) |
|---|---|---|
kubectl create namespace premiers-pas | ένα Namespace | kind: Namespace |
kubectl create deployment api --image=… --port=80 | ένα Deployment (+ ReplicaSet + Pod) | kind: Deployment, spec.template.spec.containers[0].image |
kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80 | ένα Service που στοχεύει app=api | kind: Service, spec.type: LoadBalancer, ports[0].port: 8080, targetPort: 80 |
kubectl scale deployment api --replicas=3 | το spec.replicas του Deployment | αλλαγή του replicas: 3 έπειτα kubectl apply |
kubectl delete pod <όνομα> | διαγράφει ένα Pod· το ReplicaSet το αντικαθιστά | τίποτα να γράψεις: είναι η συμφιλίωση |
| Στήλη | Στο get deploy | Στο get rs | Στο get pods |
|---|---|---|---|
READY | Pods έτοιμα / επιθυμητά (3/3) | Pods έτοιμα | containers έτοιμα / σύνολο (1/1) |
UP-TO-DATE | Pods στο τελευταίο πρότυπο | — | — |
DESIRED / CURRENT | — | επιθυμητά / υπάρχοντα | — |
STATUS | — | — | Running, ContainerCreating, ImagePullBackOff… |
Όλες οι εντολές φέρουν -n premiers-pas. Αν χρησιμοποιείς το alias k του μαθήματος 03, αντικατάστησε το kubectl με k.
Δημιούργησε το namespace της ενότητας. Ένα άδειο συρτάρι στο οποίο θα τακτοποιηθούν όλα όσα ακολουθούν, και που το τελευταίο βήμα θα διαγράψει με μία κίνηση.
kubectl create namespace premiers-pasΠραγματική έξοδος:
namespace/premiers-pas createdΤι πρέπει να δεις: created. Αν διαβάσεις AlreadyExists, το είχες κρατήσει από το μάθημα 03: δεν είναι σοβαρό, συνέχισε.
Δημιούργησε το Deployment api. Μια δημόσια εικόνα, ένα παγωμένο tag (v1.10, ποτέ latest: χωρίς ακριβές tag, δύο μηχανήματα μπορούν να κατεβάσουν δύο διαφορετικές εκδόσεις και το «σε μένα δουλεύει» επιστρέφει), και τη θύρα που ακούει το container.
kubectl create deployment api --image=traefik/whoami:v1.10 --port=80 -n premiers-pas
kubectl get deploy,rs,pods -n premiers-pasΠραγματική έξοδος (οκτώ δευτερόλεπτα μετά τη δημιουργία):
deployment.apps/api created
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api 1/1 1 1 8s
NAME DESIRED CURRENT READY AGE
replicaset.apps/api-9bfb55fc6 1 1 1 8s
NAME READY STATUS RESTARTS AGE
pod/api-9bfb55fc6-jv2xw 1/1 Running 0 8sΤι πρέπει να δεις: τρία αντικείμενα για μία εντολή. Διάβασε το όνομα του Pod από δεξιά προς τα αριστερά: jv2xw = αυτό το συγκεκριμένο Pod (τυχαίο), 9bfb55fc6 = το ReplicaSet, δηλαδή το πρότυπο Pod, api = το Deployment. Αν το STATUS λέει ContainerCreating, η εικόνα κατεβαίνει (λίγα δευτερόλεπτα για τα 10,6 MB της)· ξαναπληκτρολόγησε την εντολή.
Κοίτα πού ζει το Pod. Το -o wide προσθέτει την IP του Pod και τον κόμβο που το φιλοξενεί.
kubectl get pods -n premiers-pas -o wideΠραγματική έξοδος:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
api-9bfb55fc6-jv2xw 1/1 Running 0 8s 10.1.0.210 docker-desktop <none> <none>Τι πρέπει να δεις: μια IP σε 10.1.0.x, εσωτερικό δίκτυο του cluster, και τον κόμβο docker-desktop. Άνοιξε το http://10.1.0.210 στον browser σου: τίποτα δεν απαντά, αυτή η διεύθυνση υπάρχει μόνο μέσα στη VM. Αυτό είναι όλο το θέμα των δύο επόμενων βημάτων.
Άνοιξε ένα tunnel με port-forward. Το τερματικό μπλοκάρει όσο το tunnel είναι ανοιχτό· είναι ηθελημένο.
kubectl port-forward deployment/api 8080:80 -n premiers-pasΠραγματική έξοδος:
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Handling connection for 8080Άνοιξε το http://localhost:8080 στον browser (η γραμμή Handling connection εμφανίζεται σε κάθε αίτημα). Εμφανιζόμενη σελίδα, όπως την επιστρέφει το curl -s http://localhost:8080:
Hostname: api-9bfb55fc6-jv2xw
IP: 127.0.0.1
IP: ::1
IP: 10.1.0.210
IP: fe80::48b:d4ff:fe47:6adb
RemoteAddr: 127.0.0.1:43450
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.21.0
Accept: */*Τι πρέπει να δεις: Hostname: api-9bfb55fc6-jv2xw, το όνομα του Pod, και η IP του 10.1.0.210. Γύρνα στο τερματικό και πάτα Ctrl+C: το tunnel κλείνει, το http://localhost:8080 δεν απαντά πια. Ένα tunnel δεν είναι έκθεση.
Έκθεσε με ένα Service LoadBalancer. Το --port είναι η θύρα του Service (αυτό που πληκτρολογείς στον browser), το --target-port αυτή του container.
kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80 -n premiers-pas
kubectl get svc -n premiers-pasΠραγματική έξοδος:
service/api exposed
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api LoadBalancer 10.101.121.221 localhost 8080:30153/TCP 3sΤι πρέπει να δεις: το EXTERNAL-IP είναι localhost, είναι η υπογραφή του Docker Desktop (σε ένα cloud θα διάβαζες μια δημόσια διεύθυνση μετά από ένα λεπτό· σε kind, <pending> για πάντα). Το PORT(S) λέει 8080:30153: 8080 είναι η θύρα που δημοσιεύεται στο μηχάνημά σου, 30153 μια θύρα κόμβου που αποδόθηκε αυτόματα και που δεν χρειάζεσαι εδώ. Ανανέωσε το http://localhost:8080 χωρίς κανένα port-forward: η σελίδα whoami επιστρέφει.
Πέρνα σε τρία replicas και δες το φορτίο να κατανέμεται. Το Deployment αλλάζει οδηγία· το ReplicaSet δημιουργεί δύο Pods παραπάνω· το Service τα προσθέτει στη λίστα του μόλις είναι έτοιμα.
kubectl scale deployment api --replicas=3 -n premiers-pas
kubectl get pods -n premiers-pasΠραγματική έξοδος:
deployment.apps/api scaled
NAME READY STATUS RESTARTS AGE
api-9bfb55fc6-4pl9z 1/1 Running 0 7s
api-9bfb55fc6-jv2xw 1/1 Running 0 82s
api-9bfb55fc6-nvshh 1/1 Running 0 7sΑνανέωσε το http://localhost:8080 έξι φορές (ή, σε ένα δεύτερο τερματικό, curl -s http://localhost:8080 | findstr Hostname σε Windows, | grep Hostname σε bash). Έξι πραγματικές διαδοχικές έξοδοι:
Hostname: api-9bfb55fc6-nvshh
Hostname: api-9bfb55fc6-jv2xw
Hostname: api-9bfb55fc6-4pl9z
Hostname: api-9bfb55fc6-4pl9z
Hostname: api-9bfb55fc6-4pl9z
Hostname: api-9bfb55fc6-nvshhΤι πρέπει να δεις: τρία διαφορετικά ονόματα, σε μια σειρά που δεν είναι αυστηρή εκ περιτροπής (η κατανομή είναι τυχαία, όχι κυκλική). Ο browser, από την άλλη, κρατά μερικές φορές την ίδια σύνδεση ανοιχτή και σου δείχνει τρεις φορές το ίδιο όνομα: κλείσε την καρτέλα και ξανάνοιξέ την, ή χρησιμοποίησε curl. Για να δεις τα Pods να γεννιούνται ζωντανά αντί εκ των υστέρων, κράτα ένα δεύτερο τερματικό με kubectl get pods -n premiers-pas -w (-w για watch, Ctrl+C για έξοδο): η πρακτική 05 το χρησιμοποιεί σε κάθε βήμα.
Διάγραψε ένα Pod και κοίτα την αντικατάσταση. Σκοτώνεις έναν πωλητή· ο διευθυντής καλεί άλλον πριν τελειώσεις το διάβασμα.
kubectl delete pod api-9bfb55fc6-jv2xw -n premiers-pas
kubectl get pods -n premiers-pasΠραγματική έξοδος (η δεύτερη εντολή εκτελέστηκε αμέσως μετά την πρώτη):
pod "api-9bfb55fc6-jv2xw" deleted from premiers-pas namespace
NAME READY STATUS RESTARTS AGE
api-9bfb55fc6-4pl9z 1/1 Running 0 66s
api-9bfb55fc6-nvshh 1/1 Running 0 66s
api-9bfb55fc6-xg8qt 1/1 Running 0 3sΤι πρέπει να δεις: το jv2xw εξαφανίστηκε, το xg8qt έχει ηλικία 3s: το ReplicaSet διαπίστωσε «2 Pods, θέλω 3» και δημιούργησε ένα, με νέο όνομα. Το RESTARTS μένει στο 0: δεν είναι επανεκκίνηση του ίδιου Pod, είναι ένα ολοκαίνουργιο Pod. Ο ιστότοπος δεν σταμάτησε ποτέ να απαντά, τα δύο άλλα Pods απορροφούσαν την κίνηση.
Διάβασε τα logs. Το kubectl logs δέχεται ένα Pod ή, πιο πρακτικά, το Deployment (επιλέγει ένα).
kubectl logs deployment/api -n premiers-pasΠραγματική έξοδος:
Found 3 pods, using pod/api-9bfb55fc6-4pl9z
2026/09/11 01:41:49 Starting up on port 80Τι πρέπει να δεις: το whoami είναι σιωπηλό, μία γραμμή στην εκκίνηση και τέλος. Για ένα συγκεκριμένο Pod: kubectl logs api-9bfb55fc6-4pl9z -n premiers-pas· για ζωντανή παρακολούθηση: -f. Εδώ θα διαβάζεις, πιο κάτω στο μάθημα, τα λάθη μιας εφαρμογής που καταρρέει.
Κάνε την απογραφή του συρταριού. Το get all συνοψίζει τους τέσσερις τύπους αντικειμένων που δημιούργησες.
kubectl get all -n premiers-pasΠραγματική έξοδος:
NAME READY STATUS RESTARTS AGE
pod/api-9bfb55fc6-4pl9z 1/1 Running 0 72s
pod/api-9bfb55fc6-nvshh 1/1 Running 0 72s
pod/api-9bfb55fc6-xg8qt 1/1 Running 0 9s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/api LoadBalancer 10.101.121.221 localhost 8080:30153/TCP 75s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api 3/3 3 3 2m27s
NAME DESIRED CURRENT READY AGE
replicaset.apps/api-9bfb55fc6 3 3 3 2m27sΤι πρέπει να δεις: 3/3 παντού, ένα μόνο Service, ένα μόνο ReplicaSet. Δημιούργησες δύο αντικείμενα με το χέρι (Deployment, Service)· το cluster δημιούργησε τα τέσσερα άλλα.
Ρίξε μια ματιά στο YAML που γράφτηκε στη θέση σου. Κάθε αντικείμενο Kubernetes είναι ένα έγγραφο YAML αποθηκευμένο στο etcd· το -o yaml σου το επιστρέφει ως έχει. Είναι η πρόγευση της ενότητας 2.
kubectl get deployment api -n premiers-pas -o yamlΠραγματική έξοδος (τα πεδία παρακολούθησης του metadata και ένα μέρος του status έχουν κοπεί):
apiVersion: apps/v1
kind: Deployment
metadata:
generation: 2
labels:
app: api
name: api
namespace: premiers-pas
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
spec:
containers:
- image: traefik/whoami:v1.10
imagePullPolicy: IfNotPresent
name: whoami
ports:
- containerPort: 80
status:
availableReplicas: 3
readyReplicas: 3
replicas: 3Τι πρέπει να δεις: το spec είναι αυτό που ζήτησες (replicas: 3 από το scale σου, εξ ου και το generation: 2), το status αυτό που παρατηρεί το cluster· ο βρόχος συμφιλίωσης του μαθήματος 01 δεν κάνει τίποτα άλλο από να φέρνει το δεύτερο κοντά στο πρώτο. Οι τιμές που δεν πληκτρολόγησες ποτέ (imagePullPolicy: IfNotPresent, και μια ενότητα strategy: RollingUpdate κομμένη εδώ) είναι προεπιλογές που η ενότητα 3 θα σου μάθει να ρυθμίζεις. Το ίδιο Deployment και το Service του, γραμμένα με το χέρι σε πλήρες YAML, είναι στο kit του μαθήματος: 01-installer-kubernetes-avec-docker-desktop/04-api-declaratif.yaml (το kubectl apply -f σε αυτό το αρχείο ξαναδημιουργεί τα δύο αντικείμενα· επαληθευμένο: deployment.apps/api created, service/api created).
Διάγραψε τα πάντα με μία κίνηση και απόδειξέ το. Η διαγραφή του namespace διαγράφει όλα όσα περιέχει· η εντολή μπλοκάρει μέχρι να ολοκληρωθεί (47 δευτερόλεπτα στο μηχάνημα του μαθήματος).
kubectl delete namespace premiers-pas
kubectl get all -n premiers-pas
kubectl get namespace premiers-pasΠραγματική έξοδος:
namespace "premiers-pas" deleted
No resources found in premiers-pas namespace.
Error from server (NotFound): namespaces "premiers-pas" not foundΤι πρέπει να δεις: τίποτα πια, και το http://localhost:8080 δεν απαντά πλέον (ο browser εμφανίζει «Δεν είναι δυνατή η πρόσβαση σε αυτόν τον ιστότοπο», το curl απαντά Connection refused). Η θύρα 8080 επιστρέφεται στο μηχάνημά σου: το μάθημα 05 και οι επόμενες ενότητες την ξαναχρησιμοποιούν.
Το kubectl get pods -n premiers-pas δείχνει api-… 0/1 ImagePullBackOff (ή ErrImagePull) → το tag της εικόνας δεν υπάρχει ή το Docker Hub αρνείται να σερβίρει την εικόνα. Το kubectl describe pod <όνομα> -n premiers-pas, ενότητα Events, δίνει τον λόγο· στο μηχάνημα του μαθήματος, με το ανύπαρκτο tag traefik/whoami:v9.99: Failed to pull image "traefik/whoami:v9.99": Error response from daemon: failed to resolve reference "docker.io/traefik/whoami:v9.99": docker.io/traefik/whoami:v9.99: not found. Ένα άλλο πιθανό μήνυμα, 429 Too Many Requests, σηματοδοτεί ότι το Docker Hub περιορίζει τα ανώνυμα κατεβάσματα: περίμενε ή συνδέσου με docker login. Διόρθωσε το tag με kubectl set image deployment/api whoami=traefik/whoami:v1.10 -n premiers-pas (η πρακτική 05 το κάνει βήμα προς βήμα).
Το kubectl get svc δείχνει EXTERNAL-IP <pending> αντί για localhost → στο Docker Desktop, αυτό σημαίνει σχεδόν πάντα ότι η ζητούμενη θύρα δημοσιεύεται ήδη από ένα άλλο Service LoadBalancer (μήνυμα που λήφθηκε εκθέτοντας ένα δεύτερο Service στην 8080: μένει <pending>, το πρώτο κρατά τη θύρα). Το kubectl get svc -A δείχνει ποιος καταλαμβάνει τη θύρα· διάλεξε άλλη (--port=8081) ή διάγραψε το διπλότυπο. Αν δεν είσαι σε Docker Desktop (kind, minikube), το <pending> είναι η φυσιολογική κατάσταση: χρησιμοποίησε port-forward.
Το kubectl port-forward απαντά Unable to listen on port 8080 (ακολουθούμενο, σε Windows, από bind: Only one usage of each socket address … is normally permitted) → η τοπική θύρα 8080 είναι ήδη κατειλημμένη από άλλη εφαρμογή. Δεν αναπαράχθηκε στο μηχάνημα του μαθήματος, όπου ένας LoadBalancer στην 8080 και ένα port-forward στην 8080 συνυπήρξαν χωρίς λάθος (τα Windows δέχονται μια ακρόαση σε 0.0.0.0 και μια άλλη σε 127.0.0.1), κάτι που κάνει το αποτέλεσμα αμφίσημο. Σε κάθε περίπτωση, άλλαξε την τοπική θύρα: kubectl port-forward deployment/api 9090:80 -n premiers-pas, έπειτα http://localhost:9090.
Ο browser δείχνει πάντα το ίδιο Hostname μετά το scale --replicas=3 → HTTP keep-alive: ο browser ξαναχρησιμοποιεί τη σύνδεσή του προς το ίδιο Pod. Δεν είναι ελάττωμα κατανομής. Κλείσε την καρτέλα, ξανάνοιξε, ή σύγκρινε με curl, που ανοίγει νέα σύνδεση σε κάθε κλήση.
Error from server (NotFound): deployments.apps "api" not found → ξεχασμένο -n premiers-pas· το kubectl έψαξε στο default. Όλες οι εντολές αυτού του μαθήματος φέρουν -n.
kubectl create deployment api --image=traefik/whoami:v1.10 --port=80 -n premiers-pas δημιουργεί τρία αντικείμενα: Deployment api → ReplicaSet api-9bfb55fc6 → Pod api-9bfb55fc6-jv2xw· το όνομα ενός Pod διαβάζεται από δεξιά προς τα αριστερά.10.1.0.x) είναι προσβάσιμη μόνο μέσα στο cluster· το kubectl port-forward είναι ένα προσωρινό tunnel προς ένα Pod, ένα Service είναι η μόνιμη και κατανεμημένη πρόσβαση.kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80: στο Docker Desktop, το EXTERNAL-IP είναι localhost και το http://localhost:8080 απαντά χωρίς tunnel.kubectl scale deployment api --replicas=3: τρία Pods, και το Hostname αλλάζει από μια κλήση curl στην άλλη, με τυχαία σειρά.RESTARTS 0): είναι η συμφιλίωση, όχι επανεκκίνηση.kubectl delete namespace premiers-pas διαγράφει όλο το περιεχόμενο του συρταριού με μία εντολή· το kubectl get namespace premiers-pas πρέπει έπειτα να απαντά NotFound.kubectl get <αντικείμενο> -o yaml δείχνει το έγγραφο που το create και το expose έγραψαν στη θέση σου: spec = ζητούμενο, status = παρατηρούμενο.labo.ps1 / labo.sh), ξανακάνεις αυτή την ανάπτυξη, τη σπας δύο φορές και την επιδιορθώνεις.Στο Docker Desktop, μόνο η θύρα του LoadBalancer (8080) δημοσιεύεται στο localhost· η θύρα κόμβου που τη συνοδεύει (30153 στο 8080:30153) δεν απαντά από το μηχάνημά σου (επαληθευμένο: σύνδεση απορρίφθηκε), ενώ ένα Service τύπου NodePort δημιουργημένο μόνο του απαντά κανονικά στο localhost:<θύρα>. Σε ένα cluster cloud, το αντίστροφο μετράει: το LoadBalancer προκαλεί τη δημιουργία ενός χρεώσιμου εξισορροπητή στον πάροχο (ELB, Cloud Load Balancing, Azure LB) και το EXTERNAL-IP γίνεται δημόσια διεύθυνση μετά από ένα έως δύο λεπτά· γι' αυτό δεν εκθέτουμε κάθε εφαρμογή έτσι αλλά πίσω από ένα μοναδικό Ingress, που το μάθημα εισάγει πιο κάτω. Οι τρεις τύποι Service και η ακριβής εμβέλειά τους: kubernetes.io/docs/concepts/services-networking/service/.