Wat is een herhaalbare build?
Deterministische Build of Reproducerbare Build zijn iets anders, maar kunnen als hetzelfde worden opgevat als hetzelfde als in dit artikel.
Reproducerbare Builds verwijzen naar deMeerdere uitvoeringen van het buildproces met dezelfde invoer en buildomgeving kunnen exact dezelfde resultaten opleveren。 Deze technologie is belangrijk voor softwareontwikkeling, distributie en beveiligingsvalidatie.
Een build is reproduceerbaar als deze exact dezelfde output levert, ongeacht wanneer en waar deze wordt uitgevoerd. Ongeacht op welke computer je draait, op welk tijdstip van de dag en welke externe diensten je via het netwerk benadert, reproduceerbare builds leveren dezelfde byte-voor-byte output. Dit is geweldig voor zowel ontwikkeling (omdat reproduceerbare builds makkelijk te delen zijn tussen verschillende ontwikkelaarsapparaten) als productie (omdat het makkelijk is om te garanderen dat de resultaten van reproduceerbare builds niet zijn aangetast – draai de build gewoon opnieuw uit op je eigen machine en controleer of de resultaten consistent zijn!). zijn erg nuttig.
De drie pijlers van herhaalbare bouwwerken
Pijler 1: Herhaalbare builds
Build-herhaalbaarheid verwijst naar wat er met de buildmachine zelf gebeurt. Aangenomen dat onze build-inputs beschikbaar zijn en er niets verandert in de wereld om ons heen, produceert onze build dan dezelfde output wanneer ze worden herhaald?
Deterministisch installatieplan
De eerste, eenvoudigste en meest voor de hand liggende vereiste bij een herhaalbare bouw is een deterministisch afhankelijkheidsinstallatieplan.
In de meeste talen is het zo simpel als het inchecken van een vergrendeld bestand. Moderne buildtools stellen projecten vaak in staat directe afhankelijkheidsvereisten als constraints uit te drukken, en vervolgens deze constraints op te lossen om een installatieplan te genereren (een lijst met afhankelijkheidsnamen en versieparen om te installeren). Veel van deze tools genereren ook lock-bestanden voor geserialiseerde installatieplannen. Ontwikkelaars kunnen deze lock-bestanden indienen bij versiebeheer zodat toekomstige builds dezelfde afhankelijkheidsnamen en versies gebruiken.
Let op dat we ook deterministisch moeten zijn in de dependency build zelf (niet alleen versieselectie), en een deterministisch installatieplan laat ons dit niet bereiken!
Deterministische constructie
Zodra we weten wat we moeten bouwen, moet onze build zelf (inclusief onze eigen code en de build van dependency code) daadwerkelijk deterministisch zijn.
Dit is misschien eigenlijk geen probleem voor projecten zonder een compileerstap! Bijvoorbeeld, een Node-project met alle afhankelijkheden is pure JavaScript, en er is geen extra werk nodig om effectieve deterministiek te bereiken.
Voor projecten die wel compilatie- of vertaalstappen (bron-tot-bron compilatie) bevatten, is het waarborgen van determinisme verreweg het moeilijkste onderdeel van het bouwen van een reproduceerbare build. Het compilatieproces kan impliciet non-determinisme op verschillende manieren introduceren, waaronder:
- Turing-complete programmabuildscripts kunnen de gecompileerde uitvoer naar wens wijzigen.
- Post-installatiescripts die vertrouwen op uitvoerbare bestandssysteemzoekopdrachten of netwerkaanroepen.
- C-binding aan een systeemgeïnstalleerd pakket waarbij bindings op verschillende systemen met verschillende headers verschillende uitvoer kunnen produceren.
- Stappen om een bestand te bouwen dat buiten versiebeheer kan worden gelezen.
- Bouw stappen om tijdstempels te genereren met behulp van systeemtijd.
- Stappen om afhankelijkheden te bouwen die niet in het netwerkdownload-installatieplan staan (bijvoorbeeld een NPM-afhankelijkheid downloaden van GitHub voor een gecachte binaire build die C-bound is).
- Verander het gedrag op basis van de momenteel ingestelde omgevingsvariabele, maar dien geen build in met de configuratie van de omgevingsvariabele.
Niet al deze gedragingen veroorzaken per se onzekerheid wanneer ze correct worden ingesteld, maar het correct configureren van het bouwproces kan complex en moeilijk zijn. Je kunt bijvoorbeeld deze blogpost lezen over onzekerheid in Chromium-builds. Veel van deze problemen kunnen worden verminderd door de lokale bouwomgeving te beheersen, die we in de volgende sectie zullen bespreken.
Pijler 2: Onveranderlijke omgeving
Zelfs bij herhaalbare builds moeten we ervoor zorgen dat de build-inputs niet veranderen. Vaak betekent dit dat we ervoor willen zorgen dat we bouwen op een onveranderlijke momentopname van onze omgeving.
Onveranderlijke lokale omgeving
Zoals we hierboven bespraken, is een veelvoorkomende bron van bouwonzekerheid het vertrouwen op "afhankelijkheden" die niet door de buildtool worden vastgelegd. C-bound systeembibliotheken zijn de meest voorkomende voorbeelden, maar andere lokale omgevingsfactoren zoals instellingen van omgevingsvariabelen en bestanden buiten het bereik van versiebeheer kunnen ook de build beïnvloeden.
Een eenvoudige manier om dit probleem te beperken is door de build in een bekende, onveranderlijke container te draaien. Bijvoorbeeld, een containerruntime zoals Docker helpt ervoor te zorgen dat iedereen dezelfde systeemafhankelijkheden gebruikt, dezelfde omgevingsvariabelen en op hetzelfde bestandssysteem draait. Daarnaast is het eenvoudig te verifiëren dat de inhoud van de container overeenkomt met een bekende goede buildcontainer, en indien nodig kan de container eenvoudig volledig uit het bekende goede beeld worden verwijderd en opnieuw worden gemaakt.
Let op dat we heel duidelijk zijn over bekende containers of bekende containerafbeeldingen. Het is niet genoeg om zomaar een Dockerfile in te dienen! Waarom? Omdat de Dockerfile zelf geen volledig reproduceerbaar buildproces voor Docker-images beschrijft, omdat ze niet draaien in een onveranderlijke globale omgeving.
Onveranderlijke wereldwijde omgeving
Build-systemen werken vaak samen met externe services om taken uit te voeren zoals versieresolutie en het downloaden van afhankelijkheden. Maar externe diensten veranderen vaak.
Het draaien van apt install nodejs vandaag geeft andere resultaten dan vorig jaar, en waarschijnlijk volgend jaar ook andere resultaten. Daarom kunnen Dockerfiles zelf geen reproduceerbare builds beschrijven – hetzelfde Dockerfile op verschillende momenten draaien levert verschillende build-uitvoer op!
De eenvoudige mitigatie hier is om de build waar mogelijk te configureren, waarbij een exacte versie wordt gespecificeerd (idealiter ook een exacte content-hash) zodat toekomstige builds dezelfde versie gebruiken als de huidige build. Maar externe diensten kunnen hun gedrag ook onverwacht veranderen – een echt pessimistische, reproduceerbare build draait een interne image met zoveel mogelijk netwerkbronnen.
Pilaar 3: Beschikbaarheid van grondstoffen
Stel dat onze bouw herhaalbaar is en de wereld onder onze voeten niet verandert. Het enige wat we nu nog nodig hebben is toegang tot de buildinput. Het lijkt simpel, toch? Goed......
Het register faalt soms
De meeste Node-ontwikkelaars hebben minstens één NPM-storing meegemaakt, waarbij de build-pipeline zonder caching of mirroring van NPM-pakketten wordt verstoord. Veel Node-ontwikkelaars hebben ook te maken gehad met het verwijderen van linker pads en fakers, wat het NPM-ecosysteem ernstig heeft beschadigd en feitelijk neerkwam op een uitval.
De enige betrouwbare manier om zulke buildbreaks te beperken is door je eigen package registry mirror te draaien. Wanneer externe diensten niet beschikbaar zijn, kan de afbeelding online blijven; Wanneer het officiële register het oude pakket verwijdert, kan de mirror diensten blijven leveren. Hetzelfde principe geldt voor andere externe diensten: tenzij je je eigen image draait, is de beschikbaarheid van een build-pipeline alleen vergelijkbaar met die van haar services.
Het kiezen om een service-image te gebruiken is altijd een delicate afweging. Enerzijds hebben registries zoals NPM speciale engineering- en operationele teams die de expertise hebben om deze systemen online te houden. Aan de andere kant is het veel makkelijker om een kleine image te draaien voor een kleine set afhankelijkheden dan om alle NPM-images te draaien. Je moet spiegelingsbeslissingen nemen op basis van de specifieke kenmerken van elke dienst, rekening houdend met de betrouwbaarheid van historische externe diensten en de beschikbaarheid en personeelsbehoefte van je team.
Leveranciers zorgen voor maximale beschikbaarheid
Een eenvoudige manier om maximale beschikbaarheid van de afhankelijkheden van je project te waarborgen, is door ze toe te voegen aan je leverancier. De meeste pakketbeheerders ondersteunen een vorm van "vendoring", wat betekent dat we in plaats van te vertrouwen op downloads van externe diensten, de afhankelijkheidsbroncode opslaan in versiebeheer, waarbij we samenleven met onze broncode. In Node kan dit bijvoorbeeld lijken op het committen van node_modules aan source control.
Hoewel deze oplossing niet perfect is (afhankelijk van hoe je leverancier en project zijn opgezet, wat veel druk kan leggen op je versiebeheer), is het vaak de eenvoudigste en makkelijkste oplossing voor maximale beschikbaarheid.
Referentie:
De hyperlink-login is zichtbaar.
De hyperlink-login is zichtbaar. |