Den 27. maj, i hovedhallen på DevOpsConf 2019-konferencen, som afholdes som en del af festivalen Som en del af afsnittet om kontinuerlig levering blev der udarbejdet en rapport med titlen "werf — vores værktøj til CI/CD i Kubernetes". Den omtaler disse de problemer og udfordringer, som alle står over for, når de implementerer i Kubernetes, samt nuancer, der måske ikke er umiddelbart synlige. Ved at undersøge mulige løsninger viser vi, hvordan dette implementeres i et open source-værktøj .
Siden præsentationen har vores forsyningsselskab (tidligere kendt som dapp) krydset en historisk milepæl i 1000 stjerner på GitHub — Vi håber, at det voksende brugerfællesskab vil gøre livet lettere for mange DevOps-ingeniører.

Så lad os introducere (~47 minutter, meget mere informativt end artiklen) og hovedresuméet af den i tekstform. Lad os komme i gang!
Levering af kode til Kubernetes
Rapporten vil i højere grad handle om CI/CD i Kubernetes end Werf, hvilket antyder, at vores software er pakket i Docker-containere. (Jeg talte om dette i ), og K8'er vil blive brugt til at køre den i produktion. (mere om dette i ).
Hvordan ser levering ud i Kubernetes?
- Der findes et Git-repository med koden og instruktioner til at bygge den. Applikationen er indbygget i et Docker-billede og publiceret i Docker-registret.
- Det samme repository indeholder også instruktioner om, hvordan applikationen implementeres og køres. I implementeringsfasen sendes disse instruktioner til Kubernetes, som henter det nødvendige image fra registreringsdatabasen og kører det.
- Derudover er der normalt tests. Nogle af dem kan køres, når du udgiver billedet. Du kan også (ved at bruge de samme instruktioner) implementere en kopi af applikationen (i et separat K8s-navnerum eller en separat klynge) og køre tests der.
- Endelig har du brug for et CI-system, der modtager hændelser fra Git (eller knaptryk) og kalder alle de angivne faser: build, publish, deploy, test.

Der er et par vigtige noter her:
- Fordi vi har en uforanderlig infrastruktur (uforanderlig infrastruktur), et applikationsbillede, der bruges i alle faser (opsætning, produktion osv.), der må være én. Jeg har talt mere detaljeret om dette og med eksempler. .
- Da vi følger infrastruktur som kode-tilgangen (IaC), applikationskoden, instruktioner til samling og lancering skal findes præcis i ét arkiv. For yderligere oplysninger, se .
- Leveringskæde (levering) Vi ser det normalt sådan her: applikationen samles, testes og udgives (udgivelsesfase) og det er det - leveringen har fundet sted. Men i virkeligheden får brugeren det, du har rullet ud, nej da du leverede det til produktionen, og da han kunne tage derhen, og produktionen fungerede. Så jeg tror, at leveringskæden slutter kun i den operationelle fase (løb), eller for at være mere præcis, selv i det øjeblik, hvor koden blev fjernet fra produktionen (erstattet med en ny).
Lad os vende tilbage til den ovennævnte Kubernetes-leveringsordning: den blev ikke kun opfundet af os, men også af bogstaveligt talt alle, der beskæftigede sig med dette problem. Faktisk kaldes dette mønster nu GitOps. (du kan læse mere om udtrykket og ideerne bag det) )Lad os se på ordningens faser.
Byggefase
Det ser ud til, at der ikke er noget at sige om at bygge Docker-billeder i 2019, når alle ved, hvordan man skriver Dockerfiles og kører dem. docker buildHer er de nuancer, jeg gerne vil henlede opmærksomheden på:
- Billedets vægt det er vigtigt, så brug det , for kun at efterlade det i billedet, der virkelig er nødvendigt for at applikationen kan virke.
- Antal lag skal minimeres ved at kombinere kæder af
RUN-kommanderer efter betydning. - Dette tilføjer dog problemer. fejlfinding, fordi når samlingen går ned, skal du finde den nødvendige kommando fra kæden, der forårsagede problemet.
- Byg hastighed er vigtigt, fordi vi hurtigt vil implementere ændringer og se resultatet. For eksempel ønsker vi ikke at genopbygge afhængigheder i sprogbiblioteker, hver gang vi bygger applikationen.
- Ofte har du brug for fra ét Git-repository mange billeder, som kan løses med et sæt Dockerfiler (eller navngivne stadier i én fil) og et Bash-script med deres sekventielle assembly.
Dette var kun toppen af isbjerget, som alle står over for. Men der er andre problemer, herunder:
- Ofte har vi brug for noget i monteringsfasen montere (for eksempel cache resultatet af en kommando som apt i en tredjepartsmappe).
- Vi ønsker Ansible i stedet for at skrive i shell.
- Vi ønsker Byg uden Docker (Hvorfor har vi brug for en ekstra virtuel maskine, hvor vi skal konfigurere alt til dette, når vi allerede har en Kubernetes-klynge, hvor vi kan køre containere?).
- Parallel samling, som kan forstås på forskellige måder: forskellige kommandoer fra en Dockerfile (hvis der anvendes flertrinskommandoer), flere commits fra ét repository, flere Dockerfiles.
- Distribueret samlingVi ønsker at samle noget i pods, der er "flygtige", fordi deres cache forsvinder, hvilket betyder, at det skal opbevares et separat sted.
- Endelig nævnte jeg toppen af begær automagiDet ville være ideelt at gå til repository'et, skrive en kommando og få et færdigt image samlet med en forståelse af hvordan og hvad man skal gøre korrekt. Personligt er jeg dog ikke sikker på, at alle nuancerne kan forudses på denne måde.
Og her er projekterne:
- — en samler fra Docker Inc (allerede integreret i nuværende versioner af Docker), som forsøger at løse alle disse problemer;
- — en builder fra Google, der giver dig mulighed for at bygge uden Docker;
- — CNCF's forsøg på at udføre automagi og især en interessant løsning med rebase for lag;
- og en masse andre værktøjer, såsom , ...
... og se på hvor mange stjerner de har på GitHub. Så på den ene side, docker build der er og kan gøre noget, men i virkeligheden problemet er ikke blevet fuldt løst — beviset på dette er den parallelle udvikling af alternative assemblere, som hver især løser en del af problemerne.
Montering i skibsværftet
Så vi er nødt til at (tidligere ligesom dapp) — Open Source-værktøjet fra virksomheden "Flant", som vi har lavet i mange år. Det hele startede for omkring 5 år siden med Bash-scripts, der optimerede assemblyen af Dockerfiles, og i de sidste 3 år er der udført fuldgyldig udvikling inden for rammerne af ét projekt med sit eget Git-repository. (først i Ruby, og derefter på Go, og samtidig omdøbt)Hvilke samlingsproblemer løses i werf?

De problemer, der er fremhævet med blåt, er allerede implementeret, parallel samling er udført inden for én vært, og de problemer, der er fremhævet med gult, er planlagt til at være afsluttet inden sommerens udgang.
Offentliggørelsesstadium i registret (publicering)
Vi har rekrutteret docker push... — hvad kunne være svært ved at indlæse et billede i registreringsdatabasen? Og her opstår spørgsmålet: "Hvilket tag skal jeg sætte på billedet?" Det opstår, fordi vi har Gitflow (eller en anden Git-strategi) og Kubernetes, og branchen ønsker, at det, der sker i Kubernetes, skal følge det, der sker i Git. Fordi Git er vores eneste kilde til sandhed.
Hvad er så svært ved det? Sikre reproducerbarhedfra en Git-commit, som er uforanderlig af natur (uforanderlig), til Docker-billedet, som burde forblive det samme.
Det er også vigtigt for os bestemme oprindelsen, fordi vi gerne vil forstå, fra hvilken commit applikationen, der kører i Kubernetes, blev bygget (så kan vi lave diffs og lignende).
Mærkningsstrategier
Den første er simpel git-tagVi har et register med et billede tagget som 1.0I Kubernetes er der en fase og en produktion, hvor dette image implementeres. I Git laver vi commits og sætter på et tidspunkt et tag ind. 2.0Vi kompilerer det i henhold til instruktionerne fra repository'et og placerer det i registreringsdatabasen med tagget 2.0Vi ruller det ud på scenen, og hvis alt er godt, så i produktion.

Problemet med denne tilgang er, at vi først sætter tagget, og først derefter tester og ruller det ud. Hvorfor? For det første er det simpelthen ulogisk: vi udgiver en version af software, som vi ikke engang har testet endnu (vi kan ikke gøre andet, for for at teste det skal vi sætte tagget). For det andet fungerer denne tilgang ikke med Gitflow.
Den anden mulighed - git commit + tagDer er et tag i mastergrenen 1.0; for det i registreringsdatabasen — et billede, der er implementeret i produktion. Derudover har Kubernetes-klyngen preview- og staging-konturer. Dernæst følger vi Gitflow: i hovedgrenen til udvikling (develop) vi laver nye funktioner, hvilket resulterer i en commit med identifikatoren #c1Vi indsamler det og offentliggør det i registret ved hjælp af denne identifikator (#c1). Med den samme identifikator ruller vi ud til forhåndsvisning. Vi gør det samme med commits #c2 и #c3.
Da vi indså, at der var nok funktioner, begyndte vi at stabilisere alt. I Git oprettede vi en branch. release_1.1 (på basen #c3 af develop). Der er ikke behov for at bygge denne udgivelse, da det blev gjort i den forrige fase. Derfor kan vi blot rulle den ud til staging. Vi retter fejl i #c4 og på samme måde ruller vi ud til staging. Parallelt er der samtidig udvikling i gang i develop, hvor ændringer periodisk tages fra release_1.1På et tidspunkt får vi en commit, som vi er tilfredse med, bygget og skubbet til staging (#c25).
Så laver vi en merge (med spol frem) af release-grenen (release_1.1) i master. Vi sætter et tag med den nye version på denne commit (1.1). Men dette billede er allerede bygget i registreringsdatabasen, så for ikke at skulle bygge det igen, tilføjer vi blot et andet tag til det eksisterende billede (nu har det tags i registreringsdatabasen #c25 и 1.1Derefter ruller vi det ud til produktion.
Der er en ulempe, at kun ét billede implementeres til staging (#c25), og i produktion - som om det var anderledes (1.1), men vi ved, at dette "fysisk" er det samme billede fra registreringsdatabasen.

Den virkelige ulempe er, at der ikke er understøttelse af merge commits; du er nødt til at spole fremad.
Vi kan gå videre og lave et trick... Lad os se på et eksempel på en simpel Dockerfile:
FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb
FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/publicLad os bygge en fil ud fra den efter følgende princip: vi tager:
- SHA256 fra ID'erne for de anvendte billeder (
ruby:2.3иnginx:alpine), som er kontrolsummer af deres indhold; - alle hold (
RUN,CMDog så videre.); - SHA256 fra filer, der blev tilføjet.
... og tag kontrolsummen (SHA256 igen) fra en sådan fil. Dette er signatur alt, der definerer indholdet af et Docker-billede.

Lad os gå tilbage til diagrammet og I stedet for commits vil vi bruge sådanne signaturer, dvs. tag billeder med signaturer.

Når vi for eksempel skal flette ændringer fra en udgivelse til en master, kan vi lave en rigtig merge-commit: den vil have en anden identifikator, men den samme signatur. Med den samme identifikator vil vi rulle imaget ud til produktion.
Ulempen er, at det nu er umuligt at bestemme, hvilken commit der blev sendt til produktion – checksums fungerer kun på én måde. Dette problem løses af et ekstra metadatalag – jeg vil fortælle dig mere om det senere.
Tagging i werf
I werf er vi gået endnu længere og forbereder os på at lave et distribueret build med en cache, der ikke er gemt på én maskine ... Så vi har bygget to typer Docker-billeder, vi kalder dem etape и billede.
Werf Git-arkivet indeholder specifikke byggeinstruktioner, der beskriver de forskellige faser af byggeprocessen (før installation, installere, føropsætning, setup). Vi samler det første fasebillede med en signatur defineret som en checksum for de første trin. Derefter tilføjer vi kildekoden, og for det nye fasebillede beregner vi dets checksum... Disse operationer gentages for alle faser, hvilket resulterer i, at vi får et sæt fasebilleder. Derefter laver vi det endelige billede, som også indeholder metadata om dets oprindelse. Og vi tagger dette billede på forskellige måder (detaljer senere).

Lad os sige, at der efter dette vises en ny commit, hvor kun applikationskoden er blevet ændret. Hvad vil der ske? En patch vil blive oprettet til kodeændringerne, og et nyt stage-image vil blive forberedt. Dets signatur vil blive defineret som en checksum af det gamle stage-image og den nye patch. Et nyt endeligt image vil blive dannet ud fra dette image. Lignende adfærd vil forekomme med ændringer på andre stadier.
Stage-billeder er således en cache, der kan lagres distribueret, og de billedbilleder, der oprettes ud fra den, indlæses i Docker-registreringsdatabasen.

Rengøring af registreringsdatabasen
Vi taler ikke om at slette lag, der forbliver hængende efter slettede tags - dette er en standardfunktion i selve Docker Registry. Vi taler om en situation, hvor mange Docker-tags akkumuleres, og vi forstår, at vi ikke længere har brug for nogle af dem, men de optager plads (og/eller vi betaler for det).
Hvad er rengøringsstrategierne?
- Du kan bare ikke gøre noget rengør ikkeNogle gange er det virkelig nemmere at betale lidt for ekstra plads end at udrede et stort virvar af mærker. Men det virker kun op til et vist punkt.
- Fuld nulstillingHvis du sletter alle billeder og kun genopbygger de nuværende i CI-systemet, kan der opstå et problem. Hvis containeren genstartes i produktion, vil et nyt billede blive indlæst til den - et billede, der endnu ikke er testet af nogen. Dette ødelægger ideen om en uforanderlig infrastruktur.
- BlågrønEt register er begyndt at løbe over - vi indlæser billeder i et andet. Samme problem som i den forrige metode: hvornår kan vi rydde det register, der er begyndt at løbe over?
- Efter tidSlet alle billeder, der er ældre end 1 måned? Men der vil helt sikkert være en tjeneste, der ikke er blevet opdateret i en måned...
- manuelt bestemme, hvad der allerede kan slettes.
Der er to virkelig brugbare muligheder: ikke at rense eller en kombination af blågrøn + manuel. I sidstnævnte tilfælde taler vi om følgende: Når du forstår, at det er tid til at rense registreringsdatabasen, skal du oprette en ny og tilføje alle nye images til den i f.eks. en måned. Og efter en måned skal du se på, hvilke pods i Kubernetes der stadig bruger den gamle registreringsdatabase, og flytte dem også til den nye registreringsdatabase.
Hvad er vi kommet til? werfVi indsamler:
- Git head: alle tags, alle branches, forudsat at alt der er tagget i Git, skal være i billederne (og hvis ikke, så skal vi slette det i selve Git);
- alle pods, der i øjeblikket er implementeret i Kubernetes;
- gamle ReplicaSets (det, der for nylig blev rullet ud), og vi planlægger også at scanne Helm-udgivelser og vælge de nyeste billeder der.
... og laver en hvidliste ud fra dette sæt - en liste over billeder, som vi ikke sletter. Vi renser alt andet, finder derefter forældreløse scenebilleder og sletter dem også.
Implementeringsfase
Pålidelig deklarativitet
Det første punkt, jeg gerne vil henlede opmærksomheden på i forbindelse med implementeringen, er udrulningen af den opdaterede ressourcekonfiguration, der er deklarativt deklareret. Det originale YAML-dokument, der beskriver Kubernetes-ressourcer, afviger altid meget fra det resultat, der rent faktisk fungerer i klyngen. Fordi Kubernetes tilføjer følgende til konfigurationen:
- identifikatorer;
- serviceoplysninger;
- sæt af standardværdier;
- afsnit med aktuel status;
- ændringer foretaget som en del af optagelses-webhooken;
- resultatet af forskellige controlleres (og planlæggerens) arbejde.
Derfor, når en ny ressourcekonfiguration vises (ny), kan vi ikke bare tage den og overskrive den nuværende "live"-konfiguration (leve). For at gøre dette bliver vi nødt til at sammenligne ny med den tidligere anvendte konfiguration (sidst anvendt) og rul videre leve modtaget programrettelse.
Denne tilgang kaldes 2-vejs sammenlægningDet bruges for eksempel i Helm.
Der er også 3-vejs sammenlægning, som er kendetegnet ved, at:
- sammenligning sidst anvendt и ny, vi ser på, hvad der blev fjernet;
- sammenligning ny и leve, vi ser på, hvad der er blevet tilføjet eller ændret;
- vi anvender den summerede patch på leve.
Vi implementerer over 1000 applikationer med Helm, så vi lever dybest set med 2-vejs merge. Der er dog en række problemer, som vi har løst med vores patches, der hjælper Helm med at fungere korrekt.
Status for reel udrulning
Når vores CI-system har genereret en ny konfiguration til Kubernetes til den næste begivenhed, sender det den videre til applikationen. (anvende) ind i en klynge - ved hjælp af Helm eller kubectl applyDernæst sker den allerede beskrevne N-way merge, hvorpå Kubernetes API'en reagerer anerkendende på CI-systemet, og CI-systemet reagerer på sin bruger.

Der er dog et stort problem: En vellykket applikation betyder ikke en vellykket udrulningHvis Kubernetes forstår, hvilke ændringer der skal implementeres, og implementerer dem, ved vi endnu ikke, hvad resultatet bliver. For eksempel kan opdatering og genstart af pods i frontend muligvis lykkes, men ikke i backend, og vi vil få forskellige versioner af kørende applikationsbilleder.
For at gøre alt rigtigt kræver denne ordning et ekstra link - en speciel tracker, der modtager statusinformation fra Kubernetes API'en og sender den til yderligere analyse af tingenes reelle tilstand. Vi har oprettet et Open Source-bibliotek i Go - (se hendes annoncering ), - som løser dette problem og er indbygget i werf.
Denne trackers opførsel på werf-niveau konfigureres ved hjælp af annotationer, der placeres på Deployments eller StatefulSets. Hovedannotationen er fail-mode — forstår følgende betydninger:
-
IgnoreAndContinueDeployProcess— vi ignorerer problemerne med udrulningen af denne komponent og fortsætter implementeringen; -
FailWholeDeployProcessImmediately- en fejl i denne komponent stopper implementeringsprocessen; -
HopeUntilEndOfDeployProcess— Vi håber, at denne komponent vil fungere ved udrulningens afslutning.
For eksempel denne kombination af ressourcer og annotationsværdier fail-mode:

Når du implementerer den for første gang, er databasen (MongoDB) muligvis ikke klar endnu - implementeringer vil gå ned. Men du kan vente, indtil den starter, og implementeringen vil stadig gå igennem.
Der er to yderligere annotationer til kubedog i werf:
-
failures-allowed-per-replica— det tilladte antal fald for hver replika; -
show-logs-until— regulerer det tidspunkt, indtil hvornår werf viser (i stdout) logs fra alle implementerede pods. Som standard er dettePodIsReady(for at ignorere beskeder, vi sandsynligvis ikke ønsker, når poden begynder at få trafik), men værdierneControllerIsReadyиEndOfDeploy.
Hvad ønsker vi os mere af udrulningen?
Udover de to punkter, der allerede er beskrevet, ønsker vi:
- at se logfiler - og kun de nødvendige, ikke alle;
- spore fremskridt, fordi hvis et job hænger "lydløst" i flere minutter, er det vigtigt at forstå, hvad der sker der;
- иметь automatisk tilbagerulning i tilfælde af at noget går galt (og derfor er det afgørende at kende den reelle status for implementeringen). Udrulningen skal være atomar: enten går den helt til ende, eller også vender alt tilbage til den tidligere tilstand.
Resultaterne af
For os som virksomhed er et CI-system og et værktøj tilstrækkeligt for at implementere alle de beskrevne nuancer på forskellige leveringsstadier (bygge, publicere, implementere). .
I stedet for en konklusion:

Med Werf har vi gjort gode fremskridt med at løse et stort antal problemer for DevOps-ingeniører, og vi ville være glade, hvis det bredere fællesskab i det mindste afprøvede dette værktøj i praksis. Det vil være lettere at opnå gode resultater sammen.
Videoer og dias
Video fra forestillingen (~47 minutter):

Præsentation af rapporten:
PS
Andre rapporter om Kubernetes på vores blog:
- «» (Dmitry Stolyarov; 27. april 2019 på "Strachka");
- «» (Andrey Polovov; 8. april 2019 på Saint HighLoad++);
- «» (Dmitry Stolyarov; 8. november 2018 på HighLoad++);
- «» (Dmitry Stolyarov; 28. maj 2018 på RootConf);
- «» (Dmitry Stolyarov; 7. november 2017 på HighLoad++);
- «» (Dmitry Stolyarov; 6. juni 2017 på RootConf).
Kilde: www.habr.com
