Immutable Kubernetes Nodes ως OCI Images - Μια ματιά από κοντά στο bootc
// Τι αλλάζει στην πράξη στο πώς κάνεις build, rollout και rollback ένα Kubernetes node όταν το λειτουργικό του έρχεται ως OCI image αντί για ένα σύνολο πακέτων, με το bootc και ένα μικρό πρακτικό παράδειγμα που λέγεται Corium.
Σε κάθε self-managed cluster που έχω κληρονομήσει υπήρχε το ίδιο πράγμα παρατημένο σε μια γωνιά: ένα Packer template που κανείς δεν ήθελε να αγγίξει. Αυτό έβγαζε το AMI, το AMI έβγαζε το node pool, και κάπου ανάμεσα έτρεχε ένα dnf update πάνω σε ό,τι τύχαινε να σερβίρει το mirror εκείνο το πρωί. Δύο nodes χτισμένα με τρεις μήνες διαφορά δεν ήταν το ίδιο μηχάνημα, και ο μόνος τρόπος να μάθεις σε τι διέφεραν ήταν να μπεις με SSH και στα δύο και να αρχίσεις τα diff.
Η συνηθισμένη απάντηση είναι η πειθαρχία: κάνε pin τα πάντα, κάνε rebuild συχνά, μην αγγίζεις ποτέ node που τρέχει. Η πειθαρχία δουλεύει μια χαρά μέχρι το kernel CVE στις 2 τα ξημερώματα, όταν κάποιος θα κάνει patch επί τόπου, γιατί το rebuild του bakery θέλει σαράντα λεπτά και το maintenance window είναι τώρα.
Το bootc πάει από άλλο δρόμο. Δεν σου ζητάει να είσαι πειθαρχημένος με ένα σύνολο πακέτων. Κάνει το λειτουργικό σύστημα container image: χτίζεται με ένα Containerfile, γίνεται push σε ένα registry, αναφέρεται με digest και αλλάζει ατομικά στο reboot. Εδώ θα δούμε τι αλλάζει πραγματικά για ένα Kubernetes node, και τι όχι.
Ποιος πρέπει να το διαβάσει;#
Το post απευθύνεται σε:
- SREs που έχουν στην ευθύνη τους το node lifecycle σε self-managed ή hybrid clusters και έχουν βαρεθεί το drift ανάμεσα σε nodes χτισμένα με μήνες διαφορά
- Platform Engineers που συντηρούν ένα Packer/AMI bakery και θέλουν για τα nodes το build, promote και rollback που έχουν ήδη για τα application images
- Ομάδες infrastructure που αξιολογούν immutable λειτουργικά για nodes (Talos, Flatcar, Bottlerocket) και αναρωτιούνται πού κολλάει η γενιά του bootc
- Όποιον έχει απαντήσει στην ερώτηση «τι είναι τελικά εγκατεστημένο σε αυτό το node;» τρέχοντας
rpm -qaκαι μισοκλείνοντας τα μάτια
Αν τρέχεις managed node groups σε EKS, GKE ή AKS και είσαι ευχαριστημένος, όλα αυτά είναι ενδιαφέροντα, όχι επείγοντα. Το πρόβλημα το έχει ήδη αναλάβει για σένα ο cloud provider.
Τι είναι το bootc;#
Το bootc περιγράφει τον εαυτό του ως «boot and upgrade via container images». Στην πράξη, επιτρέπει transactional, in-place ενημερώσεις του λειτουργικού συστήματος με OCI/Docker container images. Το container image περιέχει έναν Linux kernel (στο /usr/lib/modules/$kver/vmlinuz), και αυτόν κάνει boot το μηχάνημα.
Δεν είναι παιχνίδι. Το project τρέχει από τα τέλη του 2022, έχει 2290 stars, είναι γραμμένο σε Rust και το τελευταίο push έγινε στις 2026-09-25. Από κάτω βρίσκεται το ostree, που εδώ και χρόνια κάνει σταθερές ενημερώσεις λειτουργικών συστημάτων, άρα το δύσκολο κομμάτι (ατομικά deployments, σειρά στον bootloader, rollback) δεν είναι καινούργιος κώδικας.
Η βασική αλλαγή οπτικής: το OS σου δεν είναι ένα μηχάνημα που μαζεύει state. Είναι ένα artifact που το κάνεις build, το υπογράφεις, το σκανάρεις και το κάνεις promote με τα ίδια εργαλεία που ήδη χρησιμοποιείς για τα application images σου.
Τι κάνει ένα image bootable#
Ένα έγκυρο bootc image είναι ένα συνηθισμένο OCI image με μερικές απαιτήσεις:
| Απαίτηση | Λεπτομέρεια |
|---|---|
| Label | LABEL containers.bootc=1, για να μπορούν να το αναγνωρίζουν τα εργαλεία υψηλότερου επιπέδου |
| Kernel | Στο /usr/lib/modules/$kver/vmlinuz, με το initramfs.img δίπλα του |
/boot | Άδειο μέσα στο container image |
/sysroot | Πρέπει να υπάρχει, με mode 0755, ως mount point για το φυσικό root |
/ostree | Ένα symlink /ostree -> /sysroot/ostree, που προς το παρόν το απαιτεί το bootc container lint |
| composefs | Ενεργοποιημένο μέσω του /usr/lib/ostree/prepare-root.conf με [composefs] enabled = true |
Η τελευταία γραμμή μετράει περισσότερο απ’ όσο δείχνει. Με το composefs ενεργό, «/usr is not different from /; they are part of the same immutable image», και ολόκληρο το / είναι read-only filesystem. Το documentation το χαρακτηρίζει «very important for achieving correct semantics», και έχει δίκιο: χωρίς αυτό, το immutability είναι σύμβαση και όχι ιδιότητα.
Γιατί όχι ένα Packer/AMI bakery;#
| Πτυχή | Packer/AMI bakery | bootc image |
|---|---|---|
| Εργαλεία για το build | Packer, provisioners, builders ανά cloud | Ένα Containerfile και το podman |
| Ταυτότητα του artifact | Ένα AMI ID ανά region, ανά account | Ένα image digest, παντού |
| Διανομή | Αντιγραφή των AMIs σε κάθε region, διαμοιρασμός με accounts | Το registry που ήδη τρέχεις |
| Scanning | Ειδικοί AMI scanners, ή το κάνεις boot και σκανάρεις το instance | Ο container scanner που ήδη τρέχεις |
| Promotion | Νέα tags στα AMIs, αλλαγές σε Terraform variables | Μετακινείς ένα tag |
| Upgrade | Αντικαθιστάς το instance | Αντικαθιστάς το instance ή κάνεις bootc upgrade επί τόπου |
| Rollback | Ξανασηκώνεις από το προηγούμενο AMI και χάνεις το instance | bootc rollback και reboot, το μηχάνημα μένει |
| Incremental μεταφορά | Ολόκληρο το image, ή κάποιο ειδικό delta format | Τα κοινά layers είναι ήδη στον δίσκο |
| Drift μετά το boot | Τα dnf, apt και Ansible δουλεύουν όλα κανονικά | Το /usr είναι read-only, οπότε ως επί το πλείστον δεν δουλεύουν |
Με δυο λόγια: ένα bakery σου δίνει αναπαραγώγιμα nodes αν όλοι ακολουθούν τους κανόνες. Το bootc σου δίνει αναπαραγώγιμα nodes επειδή το /usr είναι read-only και ο μόνος τρόπος να το αλλάξεις είναι να χτίσεις καινούργιο image.
Τι ΔΕΝ είναι το bootc#
- Δεν είναι container runtime για το OS σου - το βασικό userspace δεν τρέχει το ίδιο μέσα σε container by default. Το systemd παραμένει pid1, οι διεργασίες σου είναι κανονικές διεργασίες και το
psδείχνει ακριβώς ό,τι έδειχνε πάντα - Δεν είναι Kubernetes distribution - το bootc κάνει boot ένα μηχάνημα. Το τι θα τρέχει αυτό το μηχάνημα είναι δικό σου θέμα
- Δεν είναι fleet manager - δεν υπάρχει controller που να κάνει reconcile τα nodes σου προς ένα επιθυμητό image. Το
bootc upgradeτρέχει πάνω στο node, και το πότε πρέπει να το αποφασίσει κάτι άλλο - Δεν σε γλιτώνει από το να καταλάβεις το filesystem layout σου - δες την επόμενη ενότητα, γιατί εκεί βρίσκεται η πραγματική δουλειά
Το συμβόλαιο του filesystem#
Αυτό το κομμάτι κρίνει αν η μετάβασή σου θα είναι βαρετή ή επώδυνη. Τρεις κατάλογοι, τρεις εντελώς διαφορετικοί κύκλοι ζωής.
flowchart TB
subgraph Image["OCI image (the artefact)"]
USR["/usr<br/>kernel, k0s, systemd units<br/>read-only"]
ETCDEF["/etc defaults<br/>shipped in the image"]
VARSEED["/var seed content<br/>install-time only"]
end
subgraph Machine["The machine (persistent)"]
ETC["/etc<br/>three-way merged on upgrade"]
VAR["/var<br/>never touched by an upgrade"]
end
USR -->|replaced wholesale| Machine
ETCDEF -->|merged with local edits| ETC
VARSEED -->|unpacked at install only| VAR
Το /usr είναι το image. Read-only στο runtime, αντικαθίσταται ολόκληρο σε κάθε upgrade. Ό,τι θέλεις να ακολουθεί το node καθώς προχωράει σε νέες εκδόσεις, μπαίνει εδώ.
Το /etc είναι το μηχάνημα, με ένα merge. By default κρατάει mutable, persistent state, και το bootc κάνει three-way merge στο upgrade: παίρνει ως βάση το νέο default /etc και εφαρμόζει από πάνω το diff ανάμεσα στο τρέχον και στο προηγούμενο /etc. Οι τοπικές σου αλλαγές επιβιώνουν, και τα defaults του image ενημερώνονται γύρω τους. Αν προτιμάς να μην επιβιώνουν, το etc.transient = true στο /usr/lib/ostree/prepare-root.conf κάνει το /etc transient.
Το /var είναι το μηχάνημα, τελεία. Ό,τι υπάρχει στο /var παραμένει by default. Η παγίδα είναι στην αντίθετη κατεύθυνση: οι αλλαγές που γίνονται στο /var μέσα στα container images δεν εφαρμόζονται αυτόματα στα επόμενα deployments. Μόνο το αρχικό περιεχόμενο του image γίνεται unpack, κατά την εγκατάσταση. Το σκεπτικό στέκει (τα δεδομένα των εφαρμογών δεν πρέπει να γυρίζουν πίσω μαζί με ένα OS update), αλλά ο τρόπος που αποτυγχάνει είναι ύπουλος, και θα επανέλθουμε.
Γιατί έχει σημασία: ένα αρχείο που κάνεις
COPYστο/varστο build φτάνει σε κάθε μηχάνημα που εγκαταστάθηκε από το image σου και σε κανένα που έκανε upgrade σε αυτό. Το φρεσκοστημένο test node σου δουλεύει τέλεια. Ο fleet που έκανες upgrade δεν το βλέπει ποτέ, και τίποτα δεν βγάζει σφάλμα. Δήλωσε τους καταλόγους για το state στοtmpfiles.dκαι άσε το systemd να τους δημιουργεί σε κάθε boot.
Οι εντολές#
Όλη η επιφάνεια είναι αρκετά μικρή για να τη θυμάσαι απέξω.
# What is running, and what is staged for next boot.
sudo bootc status
# Pull a newer build of the image this machine already tracks.
# Stages it for next boot. Does not reboot.
sudo bootc upgrade
# Move this machine to a different image or tag entirely.
# Preserves existing state in /etc and /var.
sudo bootc switch ghcr.io/example/node-image:0.3
# Swap the bootloader ordering back to the previous deployment.
sudo bootc rollback
sudo systemctl reboot
Τρεις ιδιότητες που αξίζει να σου γίνουν δεύτερη φύση:
- Τίποτα δεν κάνει reboot by default. Τα
bootc upgradeκαιbootc switchκάνουν stage ένα deployment για το επόμενο boot, άρα το maintenance window παραμένει δικό σου. Βάλε--applyγια άμεσο reboot. - Μπορείς να χωρίσεις το fetch από το apply. Το
bootc upgrade --download-onlyκατεβάζει τώρα, τοbootc upgrade --from-downloaded --applyεφαρμόζει αργότερα. Χρήσιμο όταν το bandwidth και το downtime έχουν διαφορετικά προγράμματα. - Το rollback είναι δουλειά του bootloader. Γυρίζει τη σειρά των boot entries πίσω στο προηγούμενο deployment. Ο οδηγός αναβάθμισης του Corium το λέει ξεκάθαρα: το rollback «is instant and downloads nothing», γιατί απλώς αλλάζει τη σειρά των boot entries ανάμεσα στο deployment που τρέχεις και στο προηγούμενο, και τα δύο είναι ήδη στον δίσκο.
Το upstream δίνει και ένα opinionated bootc-fetch-apply-updates.timer, για distributions που θέλουν περιοδικά unattended updates. Σε Kubernetes node, σκέψου το καλά πριν το ενεργοποιήσεις. Δες την ενότητα για το Day 2.
Τι σημαίνει αυτό για ένα Kubernetes node#
Εδώ το pattern παύει να είναι ένα αξιοπερίεργο των λειτουργικών και αρχίζει να αλλάζει τα runbooks σου.
Το Kubernetes γίνεται μέρος του image. Το binary του k0s ζει στο read-only /usr και έχει ενσωματωμένα το kubelet, το δικό του containerd και το control plane. Στο runtime κάνει unpack αυτά τα supervised binaries κάτω από το /var/lib/k0s/bin, αλλά την έκδοσή τους την καρφώνει το image. Έτσι δύο άξονες εκδόσεων γίνονται ένας: δεν υπάρχει πια «έκδοση OS» και «έκδοση Kubernetes» που κάνουν drift ανεξάρτητα, γιατί το upgrade οποιουδήποτε από τα δύο σημαίνει boot σε διαφορετικό image.
Το state του cluster ζει στο /var και επιβιώνει. Το CA, τα δεδομένα του etcd, το storage των container images και το state του ίδιου του kubelet βρίσκονται όλα στη writable πλευρά της γραμμής. Ένα node που κάνει reboot σε νέο image είναι το ίδιο node: ίδιο όνομα, ίδια ταυτότητα, ίδια πιστοποιητικά.
Το «αντικατέστησε το node» και το «κάνε upgrade το node» γίνονται πραγματικά διαφορετικές ενέργειες.
| Ενέργεια | Τι συμβαίνει | Πότε τη χρησιμοποιείς |
|---|---|---|
Upgrade (bootc upgrade, reboot) | Ίδιο μηχάνημα, ίδιο node object, ίδιο /var. Μεταφέρονται μόνο τα layers που άλλαξαν | Patching ρουτίνας, kernel CVEs, minor αναβαθμίσεις του Kubernetes |
| Αντικατάσταση (νέο μηχάνημα από το ίδιο digest) | Νέο μηχάνημα, νέο node object, άδειο /var, πλήρες pull του image | Autoscaling, βλάβες hardware, οτιδήποτε θέλει καθαρό /var |
Και οι δύο ξεκινούν από το ίδιο digest, και αυτό είναι το ζητούμενο. Δεν διαλέγεις πια ανάμεσα στο «κάν’ το patch» και στο «κάν’ το rebuild» σαν να είναι δύο διαφορετικά pipelines με δύο διαφορετικούς ορισμούς του σωστού. Είναι το ίδιο artifact, εφαρμοσμένο με δύο τρόπους.
Η incremental μεταφορά έρχεται δωρεάν. Επειδή το OS είναι OCI image, τα upgrades είναι incremental για τον ίδιο λόγο που είναι και τα pulls των application images. Ο οδηγός αναβάθμισης του Corium αναφέρει τα εξής για ένα switch ανάμεσα σε δύο builds του node image του:
layers already present: 65; layers needed: 11 (214.0 MB)
Deploying...done (15 seconds)
Μετακινήθηκαν 214 MB για ένα image περίπου 2.3 GB. Ούτε ειδικό delta format, ούτε patch baseline ανά release για να συντηρείς.
Το να εγκαθιστάς οτιδήποτε σε node που τρέχει παύει να δουλεύει. Το dnf install αποτυγχάνει γιατί το /usr είναι read-only. Αν πετάξεις ένα binary στο /var με ένα runcmd, θα δουλεύει ακριβώς μέχρι το επόμενο upgrade, όταν τίποτα δεν θα το ξαναβάλει στη θέση του. Αν σήμερα έχεις στα nodes σου monitoring agent, CA bundle, backup client ή κάποιον driver, όλα γίνονται περιεχόμενο του image. Αυτό είναι το πραγματικό κόστος της μετάβασης, και δεν είναι μικρό.
Το /opt θα σου σπάσει το CNI, και το σύμπτωμα δεν είναι προφανές. Σε ένα OSTree σύστημα το /opt είναι μέρος του read-only image, και το documentation του bootc για το filesystem επιβεβαιώνει ότι μένει read-only με composefs. Το k0s βάζει τα binaries των CNI plugins στο /opt/cni/bin στο runtime, οπότε το CNI DaemonSet αποτυγχάνει με mkdir /opt/cni: read-only file system και το node δεν φεύγει ποτέ από το NotReady. Το Containerfile του Corium το λύνει όπως προτείνει το documentation του bootc, μεταφέροντας στο /var το κομμάτι του /opt που πρέπει να είναι writable:
RUN rm -rf /opt && ln -s var/opt /opt
Το configuration του CNI δεν επηρεάζεται: το /etc/cni/net.d βρίσκεται κάτω από το /etc, που είναι writable και γίνεται merge στο upgrade.
Ένα πρακτικό παράδειγμα: Corium#
Για να γίνει συγκεκριμένο, θέλω ένα node image που κάνει ήδη το κομμάτι του Kubernetes, και το Corium είναι το πιο άμεσο παράδειγμα που βρήκα: «an immutable Kubernetes node, built as an OCI image. Fedora bootc, k0s baked into a read-only /usr, configured with cloud-init.»
Ας το δούμε χωρίς αυταπάτες. Το Corium δημιουργήθηκε στις 2026-09-15, έχει 8 stars, και το ίδιο του το README ανοίγει με μια σημείωση κατάστασης πιο ειλικρινή από τις περισσότερες:
Κατάσταση: 0.x. Η αρχιτεκτονική έχει κατασταλάξει και η διαδρομή δουλεύει από άκρη σε άκρη […] Δύο πράγματα δεν έχουν κατασταλάξει. Η επιφάνεια του configuration μπορεί ακόμα να αλλάξει ανάμεσα σε minor releases, και το testing είναι πιο στενό απ’ όσο φαίνεται: όλα έχουν επαληθευτεί σε virtual machines, τίποτα σε φυσικό hardware. Κάνε pin μια έκδοση, διάβασε το changelog και μην του εμπιστευτείς τίποτα που θα σου έλειπε.
Άρα: ένα μικρό, πολύ πρόσφατο project, με άδεια MIT, χτισμένο πάνω στο Fedora bootc, που έρχεται με k0s v1.36.4+k0s.0 (Kubernetes 1.36). Το χρησιμοποιώ για να δείξω το pattern, όχι για να σου προτείνω να το τρέξεις σε production. Αν θέλεις τις ώριμες επιλογές στον χώρο, αυτές είναι το Talos Linux ή το Kairos, και το λέει ξεκάθαρα και το comparison doc του ίδιου του Corium.
1. Πάρε έναν bootable δίσκο#
Τα bootc images δεν γίνονται boot απευθείας σε hypervisor, άρα κάτι πρέπει να μετατρέψει το image σε δίσκο. Το Corium δημοσιεύει έτοιμα artifacts σε κάθε release:
# A qcow2 for Proxmox, KVM or libvirt.
oras pull ghcr.io/corium-os/corium-qcow2:0.3.6
# Verify what you got before you boot it.
cosign verify ghcr.io/corium-os/corium-qcow2:0.3.6 \
--certificate-identity-regexp 'https://github.com/Corium-OS/Corium/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Το tag 0.3.6 παραπάνω είναι απλώς παράδειγμα. Το quickstart του Corium παραπέμπει στη σελίδα των releases για τις ακριβείς συντεταγμένες των artifacts, οπότε κοίτα εκεί αντί να αντιγράψεις μια έκδοση από ένα blog post.
Αν θες να χτίσεις τον δικό σου, περνάς από το image-builder, που βγάζει δίσκους qcow2 και raw. Ο τύπος anaconda-iso περνάει ακόμα από το παλιότερο entry point bootc-image-builder. Είναι το λιγότερο σταθεροποιημένο κομμάτι όλου του οικοσυστήματος, και αξίζει να δεις πού βρίσκεται σήμερα πριν σχεδιάσεις ένα pipeline γύρω του.
2. Γράψε το configuration του node#
Το configuration layer του Corium είναι το cloud-init, όχι κάποιο δικό του API. Ένα ολόκληρο single-node cluster:
#cloud-config
corium:
role: single
users:
- name: core
groups: [wheel]
ssh_authorized_keys:
- ssh-ed25519 AAAA... you@example.com
Ένας worker που μπαίνει σε υπάρχον cluster και τραβάει το token του από ένα secrets endpoint, αντί να το έχει γραμμένο μέσα στο document:
#cloud-config
corium:
role: worker
cluster:
name: edge
join:
tokenFrom:
url: https://secrets.example.com/corium/worker-token
Πρόσεξε το περιτύλιγμα. Μέσω cloud-init το configuration πρέπει να είναι ένα document #cloud-config με ένα block corium:, όπως παραπάνω. Το σκέτο schema, χωρίς περιτύλιγμα (role: στο top level), είναι αυτό που δέχονται οι άλλες τρεις πηγές: το /etc/corium/config.yaml, το kernel command line και ένα default ψημένο σε ένα derived image.
Οι τέσσερις ρόλοι είναι single, controller, controller+worker και worker. Το configuration προκύπτει από την πρώτη πηγή που απαντάει:
| Πηγή | Για |
|---|---|
/etc/corium/config.yaml | Την απάντηση ενός operator για το συγκεκριμένο μηχάνημα |
| cloud-init | NoCloud, ConfigDrive, EC2, Azure, GCE, OpenStack, Hetzner, VMware |
corium.config= στο kernel command line | PXE και netboot |
/usr/share/corium/config.yaml | Ένα default ψημένο σε ένα derived image |
Μια σχεδιαστική λεπτομέρεια που θέλω να ξεχωρίσω, γιατί είναι από αυτά που τα προσθέτεις μόνο αφού καείς: μια πηγή που αποτυγχάνει για οποιονδήποτε λόγο εκτός από το ότι δεν υπάρχει σταματάει την αναζήτηση. Ένα config URL που δεν απαντάει δεν πέφτει πίσω στο ψημένο default, γιατί έτσι ακριβώς ένα node μπαίνει αθόρυβα σε λάθος cluster.
3. Κάνε boot και έλεγξε#
Δώσε το document στην πλατφόρμα ως cloud-init user-data. Για ένα απλό NoCloud seed:
printf 'instance-id: node-01\nlocal-hostname: node-01\n' > meta-data
cp node.yaml user-data
genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data
Μετά επαλήθευσε:
ssh core@192.168.0.190
# What the bootstrap agent did, and why if it failed.
systemctl status corium-bootstrap
sudo k0s kubectl get nodes
sudo k0s kubectl get pods -A
Αποτέλεσμα: το quickstart του Corium αναφέρει ότι ένα single node φτάνει σε Ready περίπου ένα λεπτό μετά το άναμμα. Όλοι οι χρόνοι σε αυτό το post είναι μετρήσεις που έχει δημοσιεύσει το ίδιο το Corium, παρμένες σε virtual machines, όχι δικές μου. Το API απαντάει αρκετά πριν γίνει register το node, οπότε το ορόσημο που περιμένεις είναι το Ready, όχι ένα επιτυχημένο curl στον API server.
4. Φτιάξε το δικό σου derived image#
Αυτό το βήμα κρίνει αν το pattern θα αντέξει την επαφή με το περιβάλλον σου, γιατί κάθε agent που τρέχεις σήμερα πρέπει να μπει μέσα στο image. Ένα συνηθισμένο Containerfile:
FROM ghcr.io/corium-os/corium:0.3.6
ARG VERSION=1.10.0
ARG SHA256=a1b2c3... # the checksum you looked up, pinned here
# The binary goes in /usr, which is what an upgrade carries forward.
RUN curl --fail --silent --show-error --location --retry 3 \
--output /tmp/node_exporter.tar.gz \
"https://github.com/prometheus/node_exporter/releases/download/v${VERSION}/node_exporter-${VERSION}.linux-amd64.tar.gz" \
&& echo "${SHA256} /tmp/node_exporter.tar.gz" | sha256sum --check \
&& tar -xzf /tmp/node_exporter.tar.gz -C /tmp \
&& install -m 0755 "/tmp/node_exporter-${VERSION}.linux-amd64/node_exporter" \
/usr/bin/node_exporter \
&& rm -rf /tmp/node_exporter*
# Units belong in /usr/lib/systemd/system, never /etc/systemd/system: that is
# the machine's, and an operator who disables your unit there should stay
# disabled across upgrades.
COPY node-exporter.service /usr/lib/systemd/system/node-exporter.service
RUN systemctl enable node-exporter.service
# Catches the /var mistake, among others, and it is cheap.
RUN bootc container lint --fatal-warnings
Αποτέλεσμα: κάθε node που κάνει boot αυτό το digest τρέχει το node_exporter, και κάθε node που κάνει upgrade σε αυτό το κρατάει. Τρέξε το bootc container lint --fatal-warnings στο CI. Είναι το φθηνότερο guardrail όλου του μοντέλου.
Day 2: rollout, rollback, και τι σπάει#
Rollout σε ένα cluster#
Τα nodes είναι cattle, αλλά το control plane έχει ένα quorum που πρέπει να σεβαστείς. Η χειροκίνητη σειρά ανά node είναι αυτή που ήδη ξέρεις:
# 1. Stop scheduling and move the workloads off.
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data
# 2. Stage the new image and reboot into it.
ssh node-1 sudo bootc switch ghcr.io/corium-os/corium:0.3.7
ssh node-1 sudo systemctl reboot
# 3. Wait for Ready, then let it take work again.
kubectl wait --for=condition=Ready node/node-1 --timeout=5m
kubectl uncordon node-1
Ο οδηγός αναβάθμισης του Corium αναφέρει ότι ένα drained node επιστρέφει σε περίπου 45 δευτερόλεπτα και ξαναμπαίνει στο cluster μόνο του. Το Corium κρατάει ένα marker στο /var/lib/corium για να μην τρέξει δεύτερη φορά ο bootstrap agent. Αφού το /var επιβιώνει από την αλλαγή του image, το node δεν προσπαθεί να ξανακάνει bootstrap τον εαυτό του σε καινούργιο cluster.
Με τους controllers θέλει προσοχή, και η αριθμητική είναι η γνωστή αριθμητική του etcd: με τρεις controllers, έναν τη φορά, και ανάμεσα περιμένεις το Ready. Ποτέ δύο από τους τρεις. Με πέντε, δύο τη φορά αντέχεται. Με δύο, δεν υπάρχει quorum να χάσεις με αξιοπρέπεια και το cluster είναι μη διαθέσιμο σε κάθε reboot.
Σημείωση: το rollout tool του ίδιου του Corium σταματάει στο πρώτο node που δεν επιστρέφει στο digest στο οποίο το έστειλαν. Ό,τι κι αν χτίσεις, βάλ’ το κι εσύ αυτό. Ένα rollout που συνεχίζει προσπερνώντας ένα χαλασμένο μηχάνημα μετατρέπει ένα outage σε outage όλου του cluster.
Rollback που το κάνει το node μόνο του#
Το ενδιαφέρον μισό του rollback δεν είναι το bootc rollback. Είναι ένα node που αποφασίζει μόνο του ότι το νέο image είναι χαλασμένο. Το Corium δένει τα health checks του greenboot στο boot path, και το documentation του δημοσιεύει τον πίνακα των boots που μέτρησε με ένα check που αποτυγχάνει πάντα:
| Boot | Check | Τι κάνει το greenboot |
|---|---|---|
| 1 | αποτυγχάνει | Πρώτη αποτυχία, βάζει τον boot counter στο 3 |
| 2 | αποτυγχάνει | Counter στο 2, κάνει reboot για να ξαναδοκιμάσει |
| 3 | αποτυγχάνει | Counter στο 1, κάνει reboot για να ξαναδοκιμάσει |
| 4 | αποτυγχάνει | Ο counter εξαντλήθηκε, ξεκινάει rollback, που πετυχαίνει |
| 5 | υγιές | greenboot health-check passed, boot_success=1 |
Το GREENBOOT_MAX_BOOT_ATTEMPTS στο /etc/greenboot/greenboot.conf ορίζει το πλήθος, και τα δικά σου checks τα προσθέτεις ως εκτελέσιμα στο /etc/greenboot/check/required.d/, όπου ένα μη μηδενικό exit σημαίνει αποτυχημένο boot.
Δύο σχεδιαστικές επιλογές σε αυτό το setup αξίζει να τις κλέψεις, όποιο image κι αν τρέχεις:
Το health check είναι σκόπιμα χαλαρό. Το check που έρχεται με το Corium ρωτάει αν το k0s τρέχει και απαντάει, και ρητά δεν απαιτεί το node να είναι Ready στο Kubernetes. Ένα node μπορεί να είναι NotReady για λόγους που το image δεν μπορεί να διορθώσει: δεν έχει εγκατασταθεί ακόμα CNI, το control plane επανέρχεται ακόμα, υπάρχει πρόβλημα σε όλο το cluster. Αν γυρίσεις πίσω το OS σε αυτές τις περιπτώσεις, βγάζεις εκτός λειτουργίας ένα υγιές μηχάνημα τη χειρότερη δυνατή στιγμή και δεν διορθώνεις τίποτα. Ένα υπερβολικά αυστηρό health check είναι χειρότερο από το κανένα.
Το rollback προϋποθέτει ότι κάποιο upgrade έχει κάνει stage κάτι. Ένα check που αποτυγχάνει σε node που δεν το έκανε κανείς upgrade σου δίνει ένα warning και χειροκίνητη παρέμβαση, όχι ένα απρόσμενο ταξίδι σε παλιότερο image.
Οι τρόποι αποτυχίας#
| Αποτυχία | Συμπτώματα | Επίλυση |
|---|---|---|
Agent εγκατεστημένος στο /var στο build | Δουλεύει στα φρεσκοστημένα nodes, λείπει αθόρυβα από όσα έκαναν upgrade | Βάλ’ τον στο /usr και δήλωσε τους καταλόγους του state στο tmpfiles.d. Το bootc container lint το πιάνει |
dnf install σε node που τρέχει | Σφάλμα read-only filesystem | Φτιάξε derived image. Δρόμος in-place δεν υπάρχει, και είναι σκόπιμο |
| Απόρριψη ανυπόγραφου derived image | Το node αρνείται το image που μόλις έκανε push το CI σου | Υπόγραψέ το και επέκτεινε το signing policy του node. Βγάλε το policy ένα image πριν από τους agents που βασίζονται σε αυτό |
| Επανεμφανίζονται δύο άξονες εκδόσεων | Ένα in-cluster upgrade tool αλλάζει την έκδοση του Kubernetes ανεξάρτητα από το OS | Απενεργοποίησέ το. Το k0s Autopilot αλλάζει το binary του k0s στον δίσκο, πράγμα που δεν γίνεται όταν αυτό ζει σε read-only /usr, και δύο μηχανισμοί που μπορούν να αλλάξουν την έκδοση είναι χειρότεροι από έναν που δεν μπορεί |
| Το kubeconfig δείχνει σε έναν controller | Δουλεύει μέχρι να κάνει reboot αυτός ο controller | Στρέψε τους clients στο virtual IP, όχι σε όποια διεύθυνση έγραψε το k0s στο admin kubeconfig |
| Timer για unattended updates σε Kubernetes node | Το node κάνει reboot χωρίς drain | Μην ενεργοποιείς το bootc-fetch-apply-updates.timer σε μέλη του cluster. Το drain και το reboot ανήκουν στο rollout tooling σου |
Δύο πράγματα που οι πηγές δεν ξεκαθαρίζουν, και δεν θα εφεύρω εγώ απαντήσεις:
- Τι κάνει σε ένα node που τρέχει ένα αποτυχημένο pull κατά το upgrade. Η εύλογη ανάγνωση είναι ότι δεν συμβαίνει τίποτα, γιατί δεν έγινε stage τίποτα καινούργιο και το τρέχον deployment μένει ανέγγιχτο. Κανένα από τα bootc docs που διάβασα δεν το λέει ρητά, οπότε επιβεβαίωσέ το σε ζωντανό node πριν στηριχτεί πάνω του κάποιο runbook.
- Πώς δένει αυτό με το cluster-autoscaler και τα node pools των cloud providers. Το πώς επιλέγεται το «τρέχον» digest στο scale-out, και το τι σου κάνει ένα node που κάνει boot παλιότερο digest από τον υπόλοιπο fleet, δεν το καλύπτει καμία από τις πηγές που συμβουλεύτηκα.
bootc node images: υπέρ και κατά#
Υπέρ#
| Πλεονέκτημα | Περιγραφή |
|---|---|
| Ένα artifact, μία ταυτότητα | Ένα digest, όχι ένα AMI ID ανά region και ανά account |
| Εργαλεία που ήδη έχεις | Το podman για το build, ο scanner σου για το scan, το registry σου για τη διανομή, το signing σου για την υπογραφή |
| Ατομικό upgrade και rollback | Staged deployments και rollback σε επίπεδο bootloader, και τα δύο γρήγορα, κανένα δεν θέλει επανεγκατάσταση |
| Incremental εκ κατασκευής | Τα κοινά layers είναι ήδη στον δίσκο, δεν έχεις ειδικό delta format να συντηρείς |
| Το drift αποκλείεται από τη δομή | Το /usr είναι read-only. Δεν προϋποθέτει πειθαρχημένους μηχανικούς |
| Ώριμα θεμέλια | Το ostree βγάζει OS updates εδώ και χρόνια, το bootc είναι το front end σε σχήμα container |
| Boots που αυτοθεραπεύονται | Με το greenboot, ένα κακό image καταλήγει σε rollback και όχι σε page |
Κατά#
| Περιορισμός | Περιγραφή |
|---|---|
| Κάθε agent γίνεται περιεχόμενο του image | Monitoring, CAs, drivers, backup clients. Εδώ είναι ο κύριος όγκος της δουλειάς στη μετάβαση |
| Δεν υπάρχουν διορθώσεις in-place | Το hotfix στις 2 τα ξημερώματα είναι ένα build, ένα push, ένα stage και ένα reboot. Πιο γρήγορο από ένα bakery, πιο αργό από ένα dnf install |
Η παγίδα του seeding στο /var | Αθόρυβη, φαίνεται μόνο στα nodes που έκαναν upgrade, και βγαίνει εύκολα στην παραγωγή |
| Το build path για τα disk images δεν έχει κατασταλάξει | Τα qcow2 και raw είναι στο path που συντηρείται. Το ISO περνάει από ένα compatibility entry point που είναι υπό απόσυρση |
| Το signing policy είναι πρόβλημα σειράς στο rollout | Το policy πρέπει να φτάσει στα nodes πριν από τα images που εξαρτώνται από αυτό |
| Το layer του fleet το χτίζεις ακόμα εσύ | Το bootc κάνει upgrade ένα μηχάνημα. Το drain, η σειρά, το quorum και το stop-on-failure είναι δικά σου |
Πότε να μην το κάνεις#
Ας είμαστε ειλικρινείς για το σημείο αναφοράς. Τα περισσότερα clusters τρέχουν Kubernetes πάνω σε ένα συνηθισμένο Ubuntu, Debian ή RHEL, και απέναντι σε αυτή την επιλογή πρέπει να δικαιολογήσει την ύπαρξή του το pattern. Εκεί έχεις πλήρη έλεγχο, τα εργαλεία που ήδη έχεις, τον package manager σου και την επιχειρησιακή γνώση που έχει ήδη η ομάδα σου. Αυτά που χτίζεις μόνος σου είναι τα ατομικά updates με rollback, η πρόληψη του drift, ένα release pipeline για images και το provisioning των nodes. Αν δεν χρειάζεσαι αυτά τα τέσσερα, δεν χρειάζεσαι ούτε αυτό.
Προσπέρασε το pattern του bootc node όταν:
- Το node lifecycle το έχει ήδη ο cloud provider σου. Τα managed node groups σε EKS, GKE ή AKS λύνουν το ίδιο πρόβλημα, και δεν το πληρώνεις σε ώρες μηχανικών
- Θέλεις το μικρότερο δυνατό attack surface. Το Talos αφαιρεί εντελώς το shell. Ένα Fedora bootc image κρατάει ένα κανονικό userland με SSH, άρα εκ κατασκευής περισσότερη επιφάνεια
- Χρειάζεσαι εμπορική υποστήριξη ή απάντηση για το compliance σήμερα. Το Talos Enterprise και το RHEL image mode την έχουν. Ένα project 0.x με 8 stars δεν την έχει
- Χρειάζεσαι ουδέτερη διακυβέρνηση για όλο το stack. Το ίδιο το bootc είναι CNCF Sandbox project (έγινε δεκτό τον Ιανουάριο του 2025), αλλά ένα μικρό project χτισμένο πάνω του, όπως το Corium, δεν είναι. Το Kairos είναι επίσης CNCF Sandbox και το Flatcar έχει δωριστεί στο CNCF. Αξιολόγησε το project, όχι μόνο το pattern
- Είσαι εξ ολοκλήρου σε AWS ή στο οικοσύστημα του Rancher. Το Bottlerocket και το SUSE SL Micro δένουν καλύτερα απ’ όσο θα δέσει ποτέ ένα image γενικής χρήσης
- Οι αλλαγές στο configuration είναι συχνές και ad hoc. Αν κάθε αλλαγή περνάει από ένα build και ένα reboot, και αλλάζεις πράγματα συχνά, θα το μισήσεις
Και αν θέλεις το pattern χωρίς το συγκεκριμένο project: πάνω στο μοντέλο του bootc είναι χτισμένα το RHEL image mode και το Fedora bootc. Το Corium είναι ένα μικρό integration από πάνω, όχι ο μοναδικός δρόμος.
Συμπέρασμα#
Αυτό που αξίζει να κρατήσεις από το bootc δεν είναι το immutability, για το οποίο ο κλάδος μιλάει από το πρώτο Packer template. Είναι ότι το OS παύει να χρειάζεται δικό του supply chain. Κάνεις build με ένα Containerfile, αποθηκεύεις στο registry που ήδη τρέχεις, σκανάρεις με τον scanner που ήδη τρέχεις, υπογράφεις με τα κλειδιά που ήδη έχεις, κάνεις promote μετακινώντας ένα tag, και γυρίζεις πίσω με ένα flip στον bootloader. Ένα artifact, μία ταυτότητα, ένα pipeline.
Ειδικά για ένα Kubernetes node, το κέρδος είναι ότι δύο άξονες εκδόσεων γίνονται ένας. Όταν το k0s ή το kubelet ζει σε read-only /usr, δεν υπάρχει «έκδοση OS» και «έκδοση Kubernetes» που μπορούν να απομακρυνθούν η μία από την άλλη, γιατί υπάρχει ένα μόνο πράγμα να μετακινήσεις και ένας μόνο τρόπος να το κάνεις. Αυτό αξίζει περισσότερο από το immutability από μόνο του.
Το κόστος είναι πραγματικό και το πληρώνεις μπροστά. Κάθε agent που έχεις σήμερα στα nodes σου γίνεται περιεχόμενο του image. Το rollout tooling σου πρέπει να μάθει drain, σειρά, quorum και stop-on-first-failure, γιατί το bootc κάνει upgrade ακριβώς ένα μηχάνημα. Και το build path για τα disk images, το βήμα ανάμεσα στο «έχω ένα OCI image» και στο «έχω κάτι που θα το κάνει boot ένας hypervisor», είναι αυτή τη στιγμή το λιγότερο σταθεροποιημένο κομμάτι του οικοσυστήματος.
Αν διαλέγεις σήμερα: πάρε Talos αν θέλεις αυτό το pattern hardened και με υποστήριξη, Kairos αν το θέλεις κάτω από τη διακυβέρνηση ενός foundation, και managed node groups αν μπορεί να το κάνει για σένα ο cloud provider σου. Πάρε τον σκέτο δρόμο του bootc όταν το επιχείρημα που σε πείθει είναι το εργονομικό: ότι ένα λειτουργικό σύστημα που είναι ένα συνηθισμένο container image, και το χειρίζεσαι με τα εργαλεία που ήδη έχεις, κερδίζει έναν καλύτερο μηχανισμό που πρέπει να μάθεις ξεχωριστά.
Το Corium αξίζει να το διαβάσεις ακόμα κι αν δεν το τρέξεις ποτέ. Είναι μικρό, πρόσφατο και ειλικρινές για τα όριά του, και η τεκμηρίωσή του για το συμβόλαιο /usr / /etc / /var είναι η πιο καθαρή διατύπωση του πραγματικού tradeoff που έχω βρει.
Αν σου φάνηκε χρήσιμο, ίσως σου αρέσουν και τα σχετικά posts μου για container images και node lifecycle:
- Αγκαλιάζοντας το Minimalist Containerization
- Χτίζοντας Container Images με Buildpacks
- Βελτιστοποιώντας το Kubernetes Autoscaling με το Karpenter
- Γιατί το ArgoCD μετράει για τους SREs - Μια GitOps προσέγγιση στα Kubernetes Deployments

