Opmerking vertaler.: Na aanleiding van een recente publicatie over de pull- en push-methoden in GitOps hebben we een grotere interesse in dit model gezien, maar er waren zeer weinig Nederlandstalige publicaties over dit onderwerp (op Habr waren ze gewoon niet aanwezig). Daarom zijn we blij u een vertaling van een ander artikel aan te bieden ā hoewel het inmiddels bijna een jaar oud is! ā van Weaveworks, waarvan de oprichter de term 'GitOps' bedacht. De tekst legt de essentie van de aanpak uit en de belangrijkste verschillen met wat al bestaat.
Een jaar geleden publiceerden we . Toen vertelden we hoe het Weaveworks-team een SaaS lanceerde die volledig op Kubernetes is gebaseerd en een set voorschriften ontwikkelde voor het implementeren, beheren en monitoren in een cloud-native omgeving.
Het artikel was populair. Andere mensen begonnen over GitOps te praten, gingen nieuwe tools publiceren voor , , , , enzovoorts. Op onze website verscheen publicaties en gebruiksvoorbeelden van GitOps. Maar bij sommige mensen bleven er vragen. Wat is het verschil tussen het model en traditionele en continue levering ()? Is het verplicht om Kubernetes te gebruiken?
Al snel realiseerden we ons dat er een nieuwe beschrijving nodig was die het volgende biedt:
- Een groot aantal voorbeelden en verhalen;
- Een concrete definitie van GitOps;
- Een vergelijking met traditionele continue levering.
In dit artikel hebben we geprobeerd al deze onderwerpen te behandelen. U vindt hierin een bijgewerkte inleiding tot GitOps en een blik op dit onderwerp vanuit het perspectief van ontwikkelaars en CI/CD. We richten ons voornamelijk op Kubernetes, hoewel het model gemakkelijk kan worden gegeneraliseerd.
Maak kennis met: GitOps
Stel je Alice voor. Ze leidt het bedrijf Family Insurance, dat gezondheids-, auto-, woningverzekeringen en reisverzekeringen aanbiedt aan mensen die te druk zijn om de fijne kneepjes van contracten zelf te begrijpen. Haar bedrijf begon als een zijproject toen Alice als data scientist bij een bank werkte. Op een dag realiseerde ze zich dat ze geavanceerde computeralgoritmen kon gebruiken voor een efficiƫntere data-analyse en het samenstellen van verzekeringspakketten. Investeerders financierden het project en nu genereert haar bedrijf meer dan 20 miljoen dollar per jaar en groeit het snel. Momenteel werken er 180 mensen in verschillende functies. Onder hen is er een technologie team dat verantwoordelijk is voor de ontwikkeling, het onderhoud van de website, de database en de analyse van de klantenbase. Het team van 60 personen staat onder leiding van Bob - de technische directeur van het bedrijf.
Bobs team implementeert productie systemen in de cloud. Hun belangrijkste applicaties draaien op GKE, waarbij ze profiteren van de voordelen van Kubernetes in Google Cloud. Daarnaast gebruiken ze diverse tools voor data-analyse en rapportage.
Family Insurance was niet van plan om containers te gebruiken, maar raakte enthousiast over Docker. Al snel ontdekte het bedrijf dat GKE het eenvoudig maakte om clusters te implementeren voor het testen van nieuwe functies. Jenkins werd toegevoegd voor CI en Quay voor het organiseren van de container registry, en er werden scripts geschreven voor Jenkins die nieuwe containers en configuraties naar GKE pushte.
Er is enige tijd verstreken. Alice en Bob waren teleurgesteld over de prestaties van de gekozen aanpak en de impact daarvan op het bedrijf. De implementatie van containers had de prestaties niet verhoogd zoals het team had gehoopt. Soms faalden deployments en was het onduidelijk of wijzigingen in de code de schuldige waren. Het bleek ook moeilijk om wijzigingen in de configuraties bij te houden. Vaak moesten ze een nieuwe cluster aanmaken en de applicaties daarheen verplaatsen, omdat dat de gemakkelijkste manier was om de rommel op te ruimen waarin het systeem was veranderd. Alice vreesde dat de situatie zou verslechteren naarmate de applicatie zich ontwikkelde (bovendien kwam er een nieuw project op basis van machine learning aan). Bob had een groot deel van het werk geautomatiseerd en begreep niet waarom de pipeline nog steeds onbetrouwbaar was, slecht schaalde en af en toe handmatige tussenkomsten vereiste.
Toen leerden ze over GitOps. Deze oplossing bleek precies te zijn wat ze nodig hadden om vol vertrouwen vooruit te gaan.
Alice en Bob hadden al jaren gehoord van workflows op basis van Git, DevOps en infrastructure as code. De uniciteit van GitOps is dat het een reeks best practices ā categorisch en normatief ā inbrengt voor de implementatie van deze ideeĆ«n in de context van Kubernetes. Dit onderwerp , onder andere in .
Family Insurance besluit GitOps te implementeren. Nu heeft het bedrijf een geautomatiseerd exploitatiemodel dat compatibel is met Kubernetes en dat snelheid voor stabiliteit, aangezien ze:
- ontdekten dat de productiviteit van het team verdubbelde en niemand gek werd;
- stopten met het onderhouden van scripts. In plaats daarvan kunnen ze zich nu concentreren op nieuwe functies en hun engineeringmethoden verbeteren ā bijvoorbeeld door kanarie-uitrol en verbeterde tests te implementeren;
- het deploymentproces hebben geoptimaliseerd ā het faalt nu zelden;
- de mogelijkheid hebben gekregen om deployments te herstellen na gedeeltelijke storingen zonder handmatige tussenkomst;
- meer vertrouwen hebben in hun leveringssystemen. Alice en Bob ontdekten dat ze het team konden opdelen in groepen die zich bezighielden met microservices en parallel werkten;groter publiek.kunnen elke dag 30-50 wijzigingen in het project aanbrengen dankzij de inspanningen van elke groep en nieuwe technieken uitproberen;
- kunnen dagelijks 30-50 wijzigingen in het project aanbrengen door de inspanningen van elke groep en nieuwe technieken uitproberen;
- trekt gemakkelijk nieuwe ontwikkelaars aan voor het project, die updates naar productie kunnen doorvoeren met behulp van pull requests binnen enkele uren;
- doorstaat gemakkelijk audits in het kader van SOC2 (voor de naleving van de vereisten voor veilige gegevensbeheer door serviceproviders; lees bijvoorbeeld verder ā opmerking vert.).
Wat is er gebeurd?
GitOps ā dit zijn twee zaken:
- Een operationeel model voor Kubernetes en cloud-native. Het biedt een set beste praktijken voor het implementeren, beheren en monitoren van in containers verpakte clusters en applicaties. Een elegante definitie in de vorm van van :
- De weg naar het creƫren van een ontwikkelaarsgerichte omgeving voor applicatiebeheer. Wij passen een Git-workflow zowel voor exploitatie als ontwikkeling toe. Let op dat het niet alleen gaat om Git push, maar om de organisatie van de gehele set CI/CD en UI/UX-tools.
Een paar woorden over Git
Als je niet bekend bent met versiesystemen en een Git-gebaseerde workflow, raden we sterk aan om deze te bestuderen. In het begin kan werken met branches en pull requests als zwarte magie aanvoelen, maar de voordelen zijn de moeite waard. Hier is om te beginnen.
Hoe Kubernetes werkt
In ons verhaal hebben Alice en Bob overgestapt naar GitOps, nadat ze enige tijd met Kubernetes hebben gewerkt. Inderdaad, GitOps is nauw verbonden met Kubernetes ā het is een operationeel model voor infrastructuur en applicaties gebaseerd op Kubernetes.
Wat biedt Kubernetes aan gebruikers?
Dit zijn enkele van de belangrijkste mogelijkheden:
- In het model van Kubernetes kan alles declaratief worden beschreven.
- De Kubernetes API-server accepteert zo'n declaratie als invoer, en probeert continu de cluster in de staat te krijgen zoals beschreven in de declaratie.
- Declaraties zijn voldoende om een breed scala aan workloads ā 'applicaties' ā te beschrijven en te beheren.
- Als gevolg daarvan vinden wijzigingen in de applicatie en de cluster plaats door:
- wijzigingen in containerimages;
- wijzigingen in de declaratieve specificatie;
- fouten in de omgeving ā bijvoorbeeld, het neervallen van containers.
Geweldige mogelijkheden voor convergentie in Kubernetes
Wanneer een beheerder wijzigingen aanbrengt in de configuratie, zal de Kubernetes-orchestrator deze toepassen op de cluster totdat de staat dichtbij de nieuwe configuratie komt. Dit model werkt voor elke Kubernetes-resource en kan worden uitgebreid met Custom Resource Definitions (CRD's). Daarom hebben Kubernetes-deployments de volgende geweldige eigenschappen:
- Automatisering: Kubernetes-updates bieden een mechanisme voor het automatiseren van het aanbrengen van wijzigingen op een correcte en tijdige manier.
- Convergentie: Kubernetes blijft updates proberen totdat ze succesvol zijn.
- Idempotentie: herhaalde toepassingen van convergentie leiden tot hetzelfde resultaat.
- Determinisme: bij voldoende middelen hangt de staat van de bijgewerkte cluster alleen af van de gewenste staat.
Hoe GitOps werkt
We hebben genoeg geleerd over Kubernetes om de principes van GitOps uit te leggen.
Laten we teruggaan naar de teams van Family Insurance die met microservices werken. Wat moeten zij vaak doen? Bekijk de lijst hieronder (als er vreemde of onbekende punten in staan, houd dan alstublieft de kritiek even achterwege en blijf bij ons). Dit zijn gewoon voorbeelden van werkstromen gebaseerd op Jenkins. Er zijn ook veel andere processen met andere tools.
Het belangrijkste is dat we zien dat elke update eindigt met het aanbrengen van wijzigingen in configuratiebestanden en Git-repositories. Deze wijzigingen in Git zorgen ervoor dat de 'GitOps-operator' de cluster bijwerkt:
1. Werkstroom: 'Jenkins-build - master branchĀ».
Takenlijst:
- Jenkins duwt getagde beelden naar Quay;
- Jenkins duwt configuratie en Helm-charts naar de master opslag bucket;
- Een cloudfunctie kopieert configuratie en charts van de master opslag bucket naar de master Git-repository;
- De GitOps-operator werkt de cluster bij.
2. Jenkins-build - release of hotfix branch:
- Jenkins duwt ongeƫtiketteerde beelden naar Quay;
- Jenkins duwt configuratie en Helm-charts naar de staging opslag bucket;
- Een cloudfunctie kopieert configuratie en charts van de staging opslag bucket naar de staging Git-repository;
- De GitOps-operator werkt de cluster bij.
3. Jenkins-build - develop of feature branch:
- Jenkins duwt ongeƫtiketteerde beelden naar Quay;
- Jenkins duwt configuratie en Helm-charts naar de develop opslag bucket;
- Een cloudfunctie kopieert configuratie en charts van de develop opslag bucket naar de develop Git-repository;
- De GitOps-operator werkt de cluster bij.
4. Nieuwe client toevoegen:
- De manager of administrator (LCM/ops) roept Gradle aan voor de initiƫle implementatie en configuratie van de netwerklastbalancers (NLB);
- LCM/ops commit een nieuwe configuratie ter voorbereiding op de updates van de deployment;
- De GitOps-operator werkt de cluster bij.
Korte beschrijving van GitOps
- Beschrijf de gewenste toestand van het hele systeem met declaratieve specificaties voor elke omgeving (in ons voorbeeld definieert Bob's team de volledige systeemconfiguratie in Git).
- De Git-repository is de enige waarheidsbron met betrekking tot de gewenste toestand van het hele systeem.
- Alle wijzigingen naar de gewenste toestand worden uitgevoerd door commits in Git.
- Alle gewenste parameters van het cluster zijn ook waarneembaar in het cluster zelf. Zo kunnen we vaststellen of de gewenste en waarneembare toestanden samenvallen (convergeren, converge) of verschillen (divergeren, diverge) in de gewenste en waarneembare toestanden.
- Als de gewenste en waarneembare toestanden verschillen, dan:
- Er is een convergentiemechanisme dat vroeg of laat automatisch de doel- en waarneembare toestanden synchroniseert. Binnen het cluster zorgt Kubernetes hiervoor.
- Het proces wordt onmiddellijk gestart met de melding "wijziging bevestigd".
- Na een bepaalde instelbare periode kan een melding "verschil" worden verzonden, als de toestanden verschillen.
- Op deze manier veroorzaken alle commits in Git controleerbare en idempotente updates in het cluster.
- Rollback is convergentie naar een eerder gewenste toestand.
- Convergentie is definitief. Het wordt bevestigd door:
- Ontbreken van "verschil" meldingen gedurende een bepaalde tijd.
- Een melding "geconvergeerd" (bijvoorbeeld, webhook, Git writeback gebeurtenis).
Wat is divergeren?
Laten we het nogmaals herhalen: alle gewenste eigenschappen van het cluster moeten waarneembaar zijn in het cluster zelf..
Enkele voorbeelden van divergeren:
- Wijziging in het configuratiebestand door samenvoegen van branches in Git.
- Wijziging in het configuratiebestand door een commit in Git, gedaan door een GUI-client.
- Meerdere wijzigingen in de gewenste toestand door een PR in Git met een daaropvolgende container-image build en wijzigingen in de configuratie.
- Wijziging van de toestand van het cluster door een fout, resourceconflict dat leidt tot "ongewenst gedrag", of gewoon een toevallige afwijking van de originele toestand.
Wat is het convergentiemechanisme?
Enkele voorbeelden:
- Voor containers en clusters biedt het convergentiemechanisme Kubernetes.
- Ditzelfde mechanisme kan worden gebruikt voor het beheren van applicaties en constructies op basis van Kubernetes (zoals Istio en Kubeflow).
- Het systeem voor het beheren van de samenwerking tussen Kubernetes, image repositories en Git biedt , dat deel uitmaakt van .
- Voor basis machines moet het convergentiemechanisme declaratief en autonoom zijn. Uit ervaring kunnen we zeggen dat het het dichtst bij deze definitie komt, maar nog steeds menselijke controle vereist. In die zin breidt GitOps de tradities van Infrastructure as Code uit.
GitOps verenigt Git met het uitstekende convergentiemechanisme van Kubernetes, en biedt een model voor gebruik.
GitOps stelt ons in staat om te zeggen: automatisering en controle zijn alleen mogelijk voor systemen die beschrijft en observeerbaar zijn.
GitOps is bedoeld voor de gehele cloud-native stack (bijvoorbeeld Terraform enz.)
GitOps is niet alleen Kubernetes. We willen dat het gehele systeem declaratief wordt beheerd en gebruik maakt van convergentie. Met het gehele systeem bedoelen we de verzameling omgevingen die met Kubernetes werken ā bijvoorbeeld "dev cluster 1", "productie" enz. Elke omgeving omvat machines, clusters, toepassingen en interfaces voor externe diensten die gegevens, monitoring enz. leveren.
Let op hoe belangrijk Terraform is voor het probleem van bootstrapping in dit geval. Kubernetes moet ergens worden uitgerold, en het gebruik van Terraform betekent dat we dezelfde workflows van GitOps kunnen toepassen voor het creƫren van de beheerslaag die ten grondslag ligt aan Kubernetes en toepassingen. Dit is een nuttige best practice.
Er is grote aandacht voor het toepassen van GitOps-concepten op lagen boven Kubernetes. Tot nu toe zijn er GitOps-type oplossingen voor Istio, Helm, Ksonnet, OpenFaaS en Kubeflow, en bijvoorbeeld voor Pulumi, die een laag creƫren voor de ontwikkeling van cloud-native toepassingen.
Kubernetes CI/CD: een vergelijking van GitOps met andere aanpakken
Zoals besproken, is GitOps twee dingen:
- Een exploitatiemodel voor Kubernetes en cloud native, zoals hierboven beschreven.
- De weg naar het creƫren van een ontwikkelaarsgerichte omgeving voor het beheren van toepassingen.
Voor velen is GitOps vooral een workflow op basis van Git pushes. Ook wij houden daarvan. Maar dat is niet alles: laten we nu eens kijken naar de CI/CD-pijplijnen.
GitOps zorgt voor continue levering (CD) onder Kubernetes
GitOps biedt een mechanisme voor continue levering, dat de noodzaak voor aparte "deployment management systemen" elimineert. Kubernetes doet al het werk voor je.
- De applicatie-updates vereisen een update in Git. Dit is een transactionele update naar de gewenste toestand. "Deployment" wordt vervolgens uitgevoerd binnen het cluster door Kubernetes op basis van de bijgewerkte beschrijving.
- Door de specifieke werking van Kubernetes zijn deze updates convergent. Dit biedt een mechanisme voor continue deployment, waarbij alle updates atomair zijn.
- Opmerking: biedt een GitOps-operator die Git en Kubernetes integreert en Continuous Delivery (CD) mogelijk maakt door de gewenste en huidige toestand van het cluster op elkaar af te stemmen.
Zonder kubectl en scripts
Het gebruik van kubectl voor het bijwerken van het cluster moet worden vermeden, vooral scripts voor het groeperen van kubectl-commando's. In plaats daarvan kan de gebruiker zijn Kubernetes-cluster bijwerken via Git met behulp van een GitOps-pijplijn.
De voordelen zijn onder meer:
- Nauwkeurigheid. Een groep updates kan worden toegepast, geconvergeerd en uiteindelijk gevalideerd, waardoor we dichter bij het doel van atomair deployment komen. Aan de andere kant biedt het gebruik van scripts geen enkele garantie voor convergentie (meer hierover hieronder).
- Beveiliging. Kelsey Hightower: "Beperk de toegang tot het Kubernetes-cluster tot automatiseringstools en beheerders die verantwoordelijk zijn voor het debuggen of onderhouden van de werking ervan." Zie ook over veiligheid en naleving van technische specificaties, evenals door het stelen van inloggegevens uit een slordig samengesteld Jenkins-script.
- Gebruikerservaring. Kubectl onthult de mechanica van het objectmodel van Kubernetes, dat behoorlijk complex is. Idealiter zouden gebruikers op een hoger abstractieniveau met het systeem moeten interageren. Hier verwijs ik opnieuw naar Kelsey en raad aan om .
het verschil tussen CI en CD
GitOps verbetert de bestaande CI/CD-modellen.
Een moderne CI-server fungeert als een orkestratietool. In het bijzonder is het een tool voor het orkestreren van CI-pijplijnen. Deze omvatten build, test, merge naar trunk, enzovoort. CI-servers automatiseren het beheer van complexe meer-staps pijplijnen. Een veelvoorkomende verleiding is om een script te maken voor een set Kubernetes-updates en dit als een onderdeel van de pijplijn uit te voeren voor het pushen van wijzigingen naar het cluster. Veel specialisten doen dit inderdaad. Echter, dit is niet optimaal, en hier is waarom.
CI moet worden gebruikt voor het aanbrengen van updates in de trunk, terwijl het Kubernetes-cluster zichzelf moet aanpassen op basis van deze updates om CD "intern" te beheren. We noemen dit , in tegenstelling tot het CI push-model. CD maakt deel uit van runtime-orchestratie.
Waarom CI-servers geen CD moeten uitvoeren via directe updates in Kubernetes
Gebruik de CI-server niet voor het orkestreren van directe updates in Kubernetes als een reeks CI-taken. Dit is een antipatroon waar we in onze blog.
Laten we teruggaan naar Alice en Bob.
Met welke problemen worden zij geconfronteerd? Bob's CI-server past wijzigingen toe op het cluster, maar als deze crasht tijdens het proces, weet Bob niet in welke staat het cluster verkeert (of zou moeten verkeren) en hoe hij het kan herstellen. Hetzelfde geldt voor een succesvolle uitvoering.
Laten we aannemen dat Bob's team een nieuwe afbeelding heeft opgebouwd en vervolgens zijn deployments heeft gepatcht om de afbeelding uit te rollen (alles vanuit de CI-pijplijn).
Als de afbeelding goed wordt opgebouwd, maar de pijplijn crasht, moet het team uitzoeken:
- Is de update uitgerold?
- Draaien we een nieuwe build? Zal dit leiden tot ongewenste bijeffecten - met de mogelijkheid om twee builds van dezelfde ongewijzigde afbeelding te krijgen?
- Moeten we wachten op de volgende update voordat we de build uitvoeren?
- Wat is er precies misgegaan? Welke stappen moeten worden herhaald (en welke kunnen veilig worden herhaald)?
Een Git-gebaseerde workflow garandeert niet dat Bob's team met deze problemen geconfronteerd wordt. Ze kunnen nog steeds een fout maken met het pushen van een commit, met een tag of een andere parameter; deze benadering is echter veel dichter bij een duidelijke alles-of-niets.
Samenvattend, hier is waarom CI-servers zich niet met CD moeten bezighouden:
- Update-scripts zijn niet altijd deterministisch; ze maken het gemakkelijk om fouten te maken.
- CI-servers convergeren niet naar een declaratief cluster model.
- Het is moeilijk om idempotentie te waarborgen. Gebruikers moeten zich verdiepen in de diepe semantiek van het systeem.
- Het is moeilijker om herstel uit gedeeltelijke storing te realiseren.
Opmerking over Helm: als je Helm wilt gebruiken, raden we aan om het te combineren met een GitOps-operator, zoals . Dit helpt om convergentie te waarborgen. Helm op zichzelf is noch deterministisch, noch atomair.
GitOps als de beste manier om Continuous Delivery voor Kubernetes uit te voeren
Het team van Alice en Bob implementeert GitOps en ontdekt dat het veel eenvoudiger is om met softwareproducten te werken en hoge prestaties en stabiliteit te behouden. Laten we dit artikel afsluiten met illustraties die laten zien hoe hun nieuwe benadering eruitziet. Houd er rekening mee dat we voornamelijk over applicaties en services praten, maar GitOps kan worden gebruikt voor het beheer van het gehele platform.
Exploitatiewijze voor Kubernetes
Kijk naar het volgende diagram. Het stelt Git en de container image-opslag voor als gemeenschappelijke bronnen voor twee georkestreerde levenscycli:
- De pipeline voor continue integratie die bestanden leest en schrijft naar Git en de container image-repository kan bijwerken.
- De Runtime GitOps-pipeline, die implementatie combineert met beheer en observatie. Het leest en schrijft bestanden naar Git en kan container images ophalen.
Wat zijn de belangrijkste conclusies?
- Problemen scheiden: Let op dat beide pipelines gegevens kunnen uitwisselen door alleen Git of de image-repository bij te werken. Met andere woorden, er is een firewall tussen de CI en de runtime-omgeving. We noemen het de "immutability firewall" (immutable firewall), omdat alle updates van repositories nieuwe versies creƫren. Voor meer informatie over dit onderwerp, zie slides 72-87. .
- Elke CI- en Git-server kan worden gebruikt: GitOps werkt met alle componenten. U kunt uw favoriete CI- en Git-servers, image-repositories en testsets blijven gebruiken. Bijna alle andere tools voor Continuous Delivery op de markt vereisen hun eigen CI-/Git-server of image-opslag. Dit kan een beperkende factor zijn in de ontwikkeling van cloud native. In het geval van GitOps kunt u de vertrouwde tools gebruiken.
- Events als integratietool: Zodra de gegevens in Git zijn bijgewerkt, stelt Weave Flux (of de Weave Cloud-operator) de runtime hiervan op de hoogte. Elke keer wanneer Kubernetes een reeks wijzigingen accepteert, wordt Git bijgewerkt. Dit zorgt voor een eenvoudig integratiemodel voor het organiseren van workflows voor GitOps, zoals hieronder weergegeven.
Conclusie
GitOps biedt belangrijke garanties voor updates die nodig zijn voor elke moderne CI/CD-tool:
- automatisering;
- convergentie;
- idempotentie;
- determinisme.
Dit is belangrijk, omdat het een exploitatiemodel biedt voor ontwikkelaars in de cloud native sector.
- Traditionele tools voor systeembeheer en monitoring zijn verbonden met exploitatieteams die werken volgens een runbook. (set van routinematige procedures en operaties - red.), gekoppeld aan een specifieke deployment.
- Bij het beheren van cloud native-systemen is observatiemiddelen de beste manier om de resultaten van implementaties te evalueren, zodat het ontwikkelteam snel op deze kan reageren.
Stel je een groot aantal clusters voor, verspreid over verschillende clouds en talloze services met hun eigen teams en implementatieplannen. GitOps biedt een schaal-onafhankelijk model voor het beheren van deze overvloed.
P.S. van de vertaler
Lees ook op onze blog:
- «»;
- «»;
- «».
Alleen geregistreerde gebruikers kunnen deelnemen aan de enquĆŖte. , alstublieft.
Wist je iets over GitOps vóór de komst van deze twee vertalingen op Habré?
Ja, ik wist het al.
Slechts oppervlakkig.
No
35 gebruikers hebben gestemd. 10 gebruikers onthielden zich.
Bron: habr.com
