Fredag eftermiddag. Releasen har gått igenom staging, alla tester är gröna och någon trycker på knappen för produktion. Pipelinen börjar om från början: checkar ut koden, hämtar beroenden, bygger, paketerar. Tjugo minuter senare ligger något i produktion som aldrig har funnits förut. Det ni testade i staging var ett annat bygge. Oftast är de identiska. Ordet som gör ont är ”oftast”.
Det här är ett av de vanligaste mönstren vi ser i CI/CD-pipelines som vuxit fram steg för steg under några år: en jobbkedja per miljö, som alla gör ”samma sak”. Principen som löser det är gammal och tråkig, och just därför värd ett inlägg: bygg en gång, och befordra samma artefakt hela vägen.
Två byggen är två artefakter
Ett bygge är inte en ren funktion av koden. Det är en funktion av koden och allt annat som råkar vara sant i det ögonblicket:
- Basimagen som pekar på ”latest”. FROM node:20 är en rörlig referens. Samma Dockerfile ger olika images olika dagar, och ingen bad om skillnaden.
- Verktygskedjan på runnern. Stagingbygget kördes på en agent med en version av kompilatorn eller pakethanteraren, produktionsbygget på en annan. Lockfilen låser era paket — inte det som bygger dem.
- Det som hämtas från nätet under bygget. Ett postinstall-skript, en binär som laddas ner av ett byggsteg, en modell eller en font. Mellan bygget klockan 14.02 och bygget klockan 14.47 kan något ha släppts.
- Miljövariabler vid byggtid. Allt som läses under bygget och bakas in i resultatet gör artefakten miljöspecifik — och varje miljö får sin egen, otestade variant.
Ingen av de här skillnaderna syns i en diff. De syns först i produktion, som ett fel ingen kan reproducera lokalt, i staging eller i CI. Produktionsbygget var ett nytt experiment — det kördes bara utan tester.
Principen: bygg en gång, befordra artefakten
Alternativet är lika enkelt att beskriva som det är att slarva bort. Pipelinen bygger en gång, tidigt, och resultatet är en oföränderlig artefakt: en containerimage med en digest, ett paket med en checksumma, en zip med ett versionsnummer som aldrig återanvänds. Sedan är det den artefakten — exakt de byten — som går till test, staging och produktion. Miljöerna skiljer sig i konfiguration som injiceras när tjänsten startar, inte i vad som byggdes.
Det låter som en detalj i pipelinedesignen. Det är det inte. Det förändrar vad ett grönt test betyder:
- Testresultatet gäller det som driftsätts. Inte en släkting till det. Grönt i staging är ett påstående om produktionsartefakten.
- ”Vad kör i produktion?” har ett svar. En digest. Inte ”main från i tisdags, tror jag”.
- Rollback blir att peka om. Den förra artefakten finns kvar i registret. Att rulla tillbaka är att välja en äldre digest, inte att bygga om gammal kod och hoppas att det blir likadant.
- Produktionssteget blir snabbt och tråkigt. Det kopierar och startar. Inget kompileras, inget hämtas, inget kan gå snett på det sätt det gick snett i förra veckan.
- Konfigurationen tvingas ut ur koden. Allt miljöspecifikt måste läsas vid körning — vilket är där det hörde hemma från början.
Där det brukar gå sönder
- Frontend-bygget som bakar in API-adressen. Det vanligaste undantaget. Byggverktyget läser VITE_API_URL eller motsvarande vid byggtid och adressen hamnar i bundlen. Plötsligt behövs ett bygge per miljö. Lösningen är runtime-konfiguration: en liten config-fil som servern skriver vid uppstart, eller en config-endpoint som appen hämtar innan den renderar. Samma bundle, olika svar.
- Taggen som skrivs över. ”latest”, ”main”, ”staging” — en tagg som flyttas är en pekare, inte en identitet. Att deploya ”latest” till produktion säger ingenting om vad som faktiskt startade. Befordra med digest, eller med en tagg som aldrig återanvänds: git-SHA eller ett versionsnummer.
- Den separata produktionspipelinen. Någon skapade en gång en egen jobbkedja för produktion, ”för att den ska vara säker”. Den gör nästan samma sak som stagingpipelinen, med några andra flaggor. Det är två artefakter — och den som går live är den utan tester.
- Tron att lockfilen räcker. Den låser paketen. Den låser inte basimagen, inte runnern och inte verktygskedjan. Lockfilen är nödvändig, men den är inte samma sak som en artefakt.
Så börjar du
Det här kräver ingen ny plattform. Det kräver en gräns i pipelinen och lite disciplin:
- Ta ert senaste produktionsdeploy och svara på frågan: exakt vilken artefakt kör just nu, med digest eller checksumma? Kan ni inte svara inom en minut är det där ni börjar.
- Dela pipelinen i två steg med en tydlig gräns: bygg producerar en artefakt och skriver ut dess identitet, deploy tar en identitet och en miljö. Deploysteget ska inte ha tillgång till en kompilator.
- Flytta allt miljöspecifikt till körtid: miljövariabler, en config-fil, en secrets-manager. Testet är enkelt — samma image ska starta i staging och produktion med bara olika konfiguration.
- Tagga oföränderligt. Låt ”latest” finnas kvar som bekvämlighet för utvecklare, men deploya aldrig från den.
- Skriv digesten i deploy-loggen och på en sida där alla kan se vad som kör var. Det är punkten som gör att ingen längre behöver gissa.
När artefakten är densamma i alla miljöer följer nästa steg nästan av sig självt: att styra beteende per miljö med feature flags i stället för med byggen, och att rulla ut progressivt till en andel av användarna i taget. Båda förutsätter att det ni rullar ut är samma sak som det ni testade.
Det är precis den här sortens gränser som Delivery Engineering handlar om: inte fler verktyg i pipelinen, utan färre sätt att få olika resultat av samma kod.
Vårt korta svar
Bygg en gång. Ge artefakten en identitet som inte kan ändras. Låt miljöerna skilja sig i konfiguration, inte i bygge. Då betyder ett grönt test i staging faktiskt något i produktion.
Bygger ni om inför produktion har ni inte testat releasen. Ni har testat en släkting till den.
