Aquesta pàgina que estàs llegint es va publicar sola. Ningú no va pujar fitxers per FTP: algú va fer un commit, un pipeline va validar el codi, va construir una imatge i va actualitzar el servei al clúster. La primera vegada que ho vam muntar així, el pipeline va sortir verd sencer, el webhook de desplegament va tornar un HTTP 200 impecable… i la web va continuar mostrant exactament el d'abans. El desplegament «va funcionar» sense desplegar res. D'aquella tarda i d'un parell més van sortir les tres regles que avui apliquem a qualsevol aplicació que publiquem, nostra o d'un client.
Un pipeline no hi és per desplegar ràpid. Hi és perquè el que està trencat no arribi a producció i perquè puguis tornar enrere sense pensar. La velocitat és un efecte secundari agradable; el que compres de veritat és control.
Com es publica això (tres passos i cap màgia)
El lloc corre sobre la nostra plataforma de contenidors: Docker Swarm amb Traefik al davant, un registry propi i GitLab CI/CD fent de cola. Quan es fa push a la branca principal passen tres coses, en aquest ordre. Primer lint: php -l sobre tots els .php del repositori, de quatre en quatre. Segon build: es construeix la imatge amb Kaniko — sense dimoni Docker, perquè el runner que la construeix no en aixeca cap — amb el lloc enfornat a dins: el codi es copia a la imatge, no es munta des del disc del servidor. Tercer deploy: s'actualitza el servei del clúster, gestionat amb Portainer, perquè faci servir aquella imatge.
Aquest és tot el secret del que la gent anomena GitOps: el repositori és l'única font de veritat i el desplegament és una conseqüència d'un commit, no una tasca manual d'algú. Si un canvi no és a git, no existeix. Ningú no entra a tocar un fitxer al servidor, entre altres coses perquè no serviria de res. I no cal una plataforma interna amb nom propi ni un clúster de Kubernetes per tenir-ho.
Lliçó 1: «latest» és un nom, no una versió
Tornem a la tarda del desplegament fantasma. La imatge nova estava construïda i pujada al registry amb l'etiqueta :latest. El servei del clúster també apuntava a :latest. Es va rellançar el servei i va arrencar… la imatge de sempre. La documentació de Docker ho explica sense embuts: quan crees un servei, queda fixat a un digest concret de la imatge fins que l'actualitzes amb service update --image; només llavors el manager pregunta al registry a quin digest apunta ara aquella etiqueta.
Dit d'una altra manera: l'orquestrador no persegueix la teva etiqueta. La va resoldre un cop, es va quedar amb l'empremta i a partir d'aquí és fidel a aquella empremta. Reiniciar el servei, forçar-lo o demanar-li que es refresqui no canvia res, perquè des del seu punt de vista no li has demanat cap imatge diferent. I és un comportament raonable —ningú no vol que les rèpliques d'un servei corrin versions diferents perquè una va arrencar cinc minuts després—, però trenca la intuïció de tothom.
El que fem des de llavors: cada build etiqueta la imatge dues vegades, amb :latest i amb el SHA curt del commit, perquè cada versió existeixi amb nom propi al registry. I el desplegament que donem per bo és el que apunta el servei a l'etiqueta del SHA amb service update --image — una ordre explícita que sí que obliga el manager a resoldre de nou el digest —, no el simple reinici contra :latest. Amb la veritat al davant: el fitxer del stack d'aquest lloc encara declara :latest, i clavar-hi el SHA de cada commit és el següent de la llista. El deute existeix i sabem on és.
Lliçó 2: el linter va dir que sí i la web va dir que no
Un linter comprova que el codi està ben escrit. No l'executa. Ens ho va recordar un fatal de runtime a la paginació del llistat del blog — una operació entre un enter i una cadena — que va passar el php -l sense despentinar-se i va tombar la portada. Detall cruel: les pàgines dels articles estaven perfectes; el que es va trencar va ser l'índex, que és justament per on entra la gent.
La resposta no va ser una suite de tests que ningú no mantindria. Va ser un smoke test de vint línies que corre dins del mateix pipeline: aixeca el lloc amb el servidor incrustat de PHP, demana quatre rutes reals —el llistat en els tres idiomes i la segona pàgina de la paginació— i falla si a la resposta apareix Fatal error, Uncaught, Parse error o Unsupported operand —aquest últim és just el que caça l'enter contra la cadena—, o si l'HTML no arriba a tancar </html>. Res més. Aquesta pregunta mínima —«la pàgina que rep més trànsit es dibuixa sencera?»— és exactament el que ens hauria avisat del fatal de la portada abans que arribés a producció.
La regla que en vam treure val per a qualsevol aplicació: el control va abans del desplegament, no després. Un avís que arriba quan la pàgina ja està trencada no és un control, és una notificació. I si has de triar un sol test automàtic per a una aplicació web, que sigui el que demana les seves dues o tres URL més importants i comprova que responen senceres.
Això connecta amb una cosa que vam explicar fa uns dies a propòsit de qui manté realment la teva web: tenir un procés escrit no és tenir-lo funcionant. La diferència entre les dues coses és si alguna cosa automàtica s'atreveix a dir «això no surt» encara que hi hagi pressa.
Lliçó 3: el «zero downtime» que deia el nostre propi fitxer
Aquesta l'expliquem perquè ens deixa en mal lloc i per això mateix és útil. Ara mateix, mentre llegeixes això, el fitxer de desplegament d'aquest lloc encara diu en un comentari, tan tranquil, «rolling update (zero downtime)». Amb una sola rèplica —que és el que et dona Swarm si no en declares cap—, això no és veritat. L'especificació de Compose ho diu: l'ordre d'actualització per defecte és stop-first, la tasca vella s'atura abans d'arrencar la nova. Això són uns segons d'error mentre el contenidor nou s'aixeca.
Perquè sigui cert calen quatre coses juntes: com a mínim dues rèpliques, order: start-first (la nova arrenca abans que caigui la vella), un healthcheck que digui quan la nova serveix de veritat —si no, l'orquestrador creu que està llesta tan bon punt el procés existeix— i failure_action: rollback, perquè el valor per defecte és pause: l'actualització es queda a mitges esperant que algú se la miri.
Per a una web corporativa, uns segons de parpelleig a mig matí no arruïnen res, i muntar dues rèpliques per esport tampoc no és gratis. Per a l'ERP, el portal de comandes o l'aplicació amb què treballa la teva gent vuit hores al dia, sí que importa. El rellevant no és exhibir un «zero downtime» en una diapositiva: és saber quant et costa cada desplegament i haver-ho decidit. Un comentari en un YAML no és cap SLA.
El millor del sistema no és desplegar: és desfer
Amb imatges etiquetades per commit, tornar enrere és desplegar l'etiqueta anterior. No cal revertir codi en calent, ni reconstruir res, ni recordar quins fitxers es van tocar divendres. La versió d'ahir existeix, sencera, amb el seu número, i continua al registry. Això és el que converteix un desplegament en una decisió reversible en comptes d'un salt.
D'aquí surt la pregunta que fem a les reunions d'IT i que gairebé ningú no té contestada. No és «quant trigueu a publicar un canvi?». És: quant trigueu a tornar a la versió d'ahir, i qui sap fer-ho un divendres a les vuit del vespre? Si la resposta inclou la paraula «depèn» o el nom d'una sola persona, el problema no és el desplegament.
Quatre dreceres que no prenem
- ✗Muntar Kubernetes perquè toca. Per a una aplicació amb un grapat de contenidors, Swarm amb Traefik i un registry s'entén sencer en una tarda i el pot operar l'equip que ja tens. Kubernetes resol problemes reals d'escala i d'organització; si no tens aquests problemes, el que has comprat és una segona infraestructura per mantenir. La pregunta no és quin és millor, és quin saps arreglar tu a les tres de la matinada.
- ✗Editar en producció. El contenidor és immutable: l'arranjament heroic que algú fa per SSH sobreviu exactament fins al següent desplegament, que se l'endú per davant sense avisar. Pitjor encara: durant aquella estona, el que corre a producció no és a cap lloc. Si cal arreglar alguna cosa amb urgència, s'arregla al repositori i es desplega — el camí ràpid és el mateix que el camí correcte, i aquesta és precisament la gràcia.
- ✗Donar per bo un webhook que torna 200. Un HTTP 200 vol dir «he rebut la teva petició», no «he desplegat el teu codi». El vam donar per bo una vegada i d'aquí ve aquesta història. Ara la comprovació és la de sempre: que el servei hagi convergit i que la URL pública torni el que ha de tornar. Si no ho has mirat, no està desplegat.
- ✗Ficar secrets a la imatge. Les credencials de correu, les claus de les API i els tokens s'injecten com a variables de l'entorn del stack, mai no es copien dins de la imatge. Una imatge es copia sense pensar i es queda en un registry molt més temps del que et penses; una contrasenya dins d'una capa no s'esborra editant el fitxer.
Això no va de la nostra web
Expliquem el nostre cas perquè el tenim al davant cada dia, però el mateix esquema és el que muntem per a les aplicacions de negoci dels nostres clients: la web corporativa, el portal intern, l'aplicació a mida que fa quinze anys que funciona i que ningú no s'atreveix a tocar. La tecnologia concreta importa menys del que sembla — el que canvia el dia a dia és que hi hagi una font de veritat única, un control automàtic que sàpiga dir que no, i un camí de tornada que no depengui de la memòria de ningú.
Quatre preguntes per saber en un minut com estàs: podries reconstruir avui la teva aplicació des del repositori, sense fitxers solts en un servidor? Saps, sense mirar, quina versió exacta està servint ara mateix? Hi ha alguna cosa automàtica que impedeixi publicar una pàgina trencada? Quant trigues a tornar enrere? Amb aquestes quatre contestades, la conversa sobre orquestradors, contenidors i sigles esdevé secundària. Sense contestar-les, no hi ha eina que et salvi — igual que la monitorització que muntem en contenidors només serveix si algú mira què diu.
Nosaltres operem aquesta plataforma de contenidors en producció des de fa temps, amb la mateixa filosofia que apliquem a la resta de la infraestructura que gestionem: preferim avorrit i reproduïble abans que brillant i fràgil. Aquest post, per cert, s'ha publicat pel mateix camí que descriu. Si el smoke test hagués fallat, no l'estaries llegint.
Fonts (verificades): comportament de les etiquetes i els digests en serveis de Swarm, a la documentació oficial de Docker — «Deploy services to a swarm» («when you create a service, it is constrained to create tasks using a specific digest of an image until you update the service using service update with the --image flag»). Valors per defecte d'update_config (order: stop-first, failure_action: pause) a l'especificació de Compose (deploy). La resta — el pipeline, el fatal de paginació, el smoke test i el comentari optimista del YAML — és nostre, amb les seves cicatrius.
Quant trigues a tornar a la versió d'ahir?
A everyWAN dissenyem i operem el cicle de vida de dades i aplicacions d'empresa: repositori com a font de veritat, desplegaments reproduïbles i un camí de tornada que funciona un divendres a la tarda. Si la teva aplicació es publica a mà, o ningú no sap quina versió corre ara mateix, parlem-ne.
Parlar amb everyWAN