Jeg anbefaler å se nærmere på transkripsjonen av rapporten fra Alexander Sigachev fra Inventos 'Utviklings- og testprosess med Docker + Gitlab CI'
De som nettopp begynner å implementere utviklings- og testprosessen basert på Docker + Gitlab CI har ofte grunnleggende spørsmål. Hvor skal man begynne? Hvordan organisere? Hvordan teste?
Denne rapporten er god fordi den systematisk forklarer utviklings- og testprosessen ved bruk av Docker og Gitlab CI. Selve rapporten er fra 2017. Jeg tror man kan hente ut grunnleggende prinsipper, metodologi, ideer og erfaring fra denne rapporten.

De som er interessert, vennligst klikk her.
Jeg heter Alexander Sigachev. Jeg jobber i selskapet Inventos. Jeg vil fortelle om min erfaring med å bruke Docker og hvordan vi gradvis implementerer det i prosjektene våre.
Temaet for rapporten: Utviklingsprosess med bruk av Docker og Gitlab CI.

Dette er min andre rapport om Docker. På tidspunktet for den første rapporten brukte vi Docker kun i utviklingsmiljøene til utviklerne. Antallet ansatte som brukte Docker var rundt 2-3 personer. Erfaring har blitt opparbeidet gradvis, og vi har gjort noen fremskritt. Lenke til vår .
Hva vil vi dekke i denne rapporten? Vi vil dele erfaringer om hvilke problemer vi har støtt på og hvordan vi løste dem. Det var ikke alltid pent, men det tillot oss å komme videre.
Vårt motto: dockeriser alt vi kommer i kontakt med.

Hvilke problemer løser vi?
Når det er flere team i selskapet, er programmereren en delt ressurs. Det kan være faser der programmereren blir hentet fra ett prosjekt og midlertidig flyttet til et annet prosjekt.
For at programmereren raskt skal sette seg inn i prosjektet, må han laste ned koden og få opp et miljø så raskt som mulig som lar ham fortsette med å løse oppgavene i dette prosjektet.
Vanligvis, hvis man starter fra bunnen av, er dokumentasjonen i prosjektet sparsom. Informasjon om hvordan man konfigurerer, finnes kun hos de erfarne. Ansatte setter vanligvis opp arbeidsplassen sin på en til to dager. For å akselerere dette har vi brukt Docker.
Den neste grunnen er standardisering av innstillinger i Development. Etter min erfaring viser utviklere alltid initiativ. I hvert femte tilfelle blir det introdusert et egendefinert domene, for eksempel vasya.dev. Ved siden av sitter naboen Petja, som har domenet petya.dev. De utvikler et nettsted eller en del av systemet ved å bruke dette domenet.
Når systemet vokser og disse domenene begynner å komme inn i konfigurasjonene, oppstår det konflikt i Development-miljøene, og nettstedets bane blir overskrevet.
Det samme skjer med databasinnstillingene. Noen tenker ikke på sikkerhet og jobber med et tomt root-passord. Noen ganger krever MySQL-passordet under installasjonen et passord, og det blir et enkelt 123. Det skjer ofte at databasekonfigurasjonen stadig blir endret avhengig av utviklerens commit. Noen korrigerer, noen glemmer å rette opp konfigurasjonen. Det var underlige løsninger der vi flyttet en testkonfigurasjon til .gitignore og hver utvikler måtte installere databasen. Dette kompliserte oppstartsprosessen. I tillegg til alt annet, må man huske på databasen. Databasen må initialiseres, man må oppgi passord, oppgi bruker, opprette en tabell og så videre.
Et annet problem er forskjellige versjoner av biblioteker. Det skjer ofte at utviklerne jobber med forskjellige prosjekter. Det er et Legacy-prosjekt som ble startet for fem år siden (fra 2017 — red. anm.). Ved starten ble det startet med MySQL 5.5. Det finnes også moderne prosjekter, der vi prøver å implementere mer moderne versjoner av MySQL, for eksempel 5.7 eller eldre (i 2017 — red. anm.)
De som jobber med MySQL vet at disse bibliotekene drar med seg avhengigheter. Det er ganske problematisk å kjøre 2 databaser sammen. I hvert fall er det problematisk å koble gamle kunder til en ny database. Dette fører i sin tur til flere problemer.
Det neste problemet er når utvikleren jobber på en lokal maskin, bruker lokale ressurser, lokale filer, lokal RAM. All interaksjon under utviklingen av løsningen skjer innenfor rammen av at det fungerer på én maskin. Et eksempel kan være når vi har 3 backend-servere i produksjon, og utvikleren lagrer filer i rotkatalogen, og derfra henter nginx filer for å svare på anrop. Når slik kode går i produksjon, viser det seg at filen finnes på en av de 3 serverne.
Nå utvikles feltet mikroservices. Når vi deler våre store applikasjoner inn i mindre komponenter som interagerer med hverandre. Dette lar oss velge teknologier i henhold til spesifikke oppgaver. Det gjør det også mulig å dele arbeidet og ansvarsområdene mellom utviklerne.
Frontend-utvikleren, ved å utvikle på JS, påvirker praktisk talt ikke backend. Backend-utvikleren, i vårt tilfelle, utvikler Ruby on Rails og forstyrrer ikke frontend. Interaksjonen skjer ved hjelp av API.
Som en bonus har vi med Docker klart å utnytte ressursene på Staging. Hvert prosjekt krever sine spesifikke innstillinger på grunn av sin natur. Fysisk måtte vi tildele enten én virtuell server og konfigurere dem separat, eller dele et variabelt miljø, og prosjektene kunne, avhengig av versjonen av bibliotekene, påvirke hverandre.

Verktøy. Hva bruker vi?
- Selve Docker. I Dockerfile beskrives avhengighetene til én applikasjon.
- Docker-compose er koblingen som forener flere av våre Docker-applikasjoner.
- Vi bruker GitLab for å lagre kildekoden.
- Vi bruker GitLab-CI for systemintegrasjon.

Foredraget består av to deler.
Den første delen vil fortelle om hvordan vi kjørte Docker på utviklermaskinene.
Den andre delen vil fortelle om hvordan vi interagerer med GitLab, hvordan vi kjører tester og hvordan vi distribuerer til Staging.

Docker er en teknologi som gjør det mulig (ved hjelp av en deklarativ tilnærming) å beskrive nødvendige komponenter. Dette er et eksempel på Dockerfile. Her erklærer vi at vi arver fra det offisielle Docker-bildet Ruby:2.3.0. Det inneholder installert Ruby versjon 2.3. Vi installerer nødvendige build-biblioteker og NodeJS. Vi beskriver at vi oppretter en katalog. /app. Vi setter mappen app som arbeidskatalog. I denne mappen legger vi den nødvendige minimale Gemfile og Gemfile.lock. Deretter bygger vi prosjektene som installerer dette bildet med avhengigheter. Vi spesifiserer at containeren vil være klar til å lytte på ekstern port 3000. Den siste kommandoen er den som starter applikasjonen vår direkte. Hvis vi kjører prosjektets oppstartskommando, vil applikasjonen prøve å kjøre og starte den angitte kommandoen.

Dette er et minimalt eksempel på en docker-compose-fil. I dette tilfellet viser vi at to containere oppretter forbindelse. Dette er direkte til databasetjenesten og webtjenesten. Våre webapplikasjoner krever i de fleste tilfeller en database som backend for lagring av data. Siden vi bruker MySQL, er eksemplet med MySQL — men ingenting forhindrer oss fra å bruke en annen database (PostgreSQL, Redis).
Vi tar bildet MySQL 5.7.14 fra den offisielle kilden på Docker hub uten endringer. Bildet som håndterer vår webapplikasjon, bygger vi fra nåværende katalog. Ved første oppstart bygger det bildet for oss. Deretter kjører det kommandoen vi utfører her. Hvis vi går tilbake, vil vi se at oppstartskommanden er definert gjennom Puma. Puma er en tjeneste skrevet i Ruby. I det andre tilfellet overstyrer vi. Denne kommandoen kan være vilkårlig avhengig av våre behov eller oppgaver.
Vi beskriver også at vi må videresende porten på utviklerens vertsmaskin fra 3000 til containerens port 3000. Dette utføres automatisk ved hjelp av iptables og sin egen mekanisme som er innebygd i Docker.
Utvikleren kan også, som før, henvende seg til hvilken som helst tilgjengelig IP-adresse, for eksempel den lokale 127.0.0.1 eller den eksterne IP-adressen til maskinen.
Den siste linjen sier at containeren web er avhengig av containeren db. Når vi kaller oppstarten av containeren web, vil docker-compose først starte databasen. Allerede ved oppstarten av databasen (faktisk — etter oppstarten av containeren! Klareringen av databasen er ikke garantert) vil applikasjonen vår, vår backend, starte.
Dette gjør at vi unngår feil når databasen ikke er oppe, og sparer ressurser når vi stopper databasen, som dermed frigjør ressursene til andre prosjekter.

Hva gir oss bruken av dockerisering av databasen i prosjektet? Vi fester MySQL-versjonen for alle utviklere. Dette gjør at vi unngår noen feil som kan oppstå ved konfigurasjonsforskjeller når syntaks, innstillinger og standardinnstillinger endres. Dette lar oss spesifisere felles hostname for databasen, login og passord. Vi tar avstand fra det kaotiske navnesystemet og konfliktene i konfigurasjonsfilene som eksisterte tidligere.
Vi har muligheten til å bruke en mer optimal konfigurasjon for utviklingsmiljøet, som vil være forskjellig fra standardinnstillingen. MySQL er som standard konfigurert for svake maskiner, og ytelsen er svært lav rett ut av boksen.

Docker lar oss bruke den nødvendige versjonen av Python, Ruby, NodeJS og PHP. Vi slipper å bruke en versjonshåndterer. Tidligere brukte vi rpm-pakker for Ruby som tillot oss å endre versjon avhengig av prosjekt. Dette gjør at vi, takket være Docker-containeren, jevnt kan migrere koden og versjonere den sammen med avhengighetene. Vi har ingen problemer med å forstå versjonen av både tolken og koden. For å oppdatere versjonen, må vi senke den gamle containeren og heve den nye containeren. Hvis noe går galt, kan vi senke den nye containeren og heve den gamle.
Etter byggingen av bildet vil containerne både i utviklings- og produksjonsmiljø være identiske. Dette er spesielt aktuelt for store installasjoner.
På frontend bruker vi JavaScript og NodeJS.
Nå har vi vårt siste prosjekt på ReactJS. Utvikleren kjørte alle containerne og utviklet med hot-reload.
Deretter kjøres en oppgave for å bygge JavaScript, og koden som er bygd i statisk form, leveres gjennom nginx for å spare ressurser.

Her har jeg vist diagrammet over vårt siste prosjekt.
Hvilke oppgaver har vi løst? Vi hadde behov for å bygge et system som kommuniserer med mobile enheter. De mottar data. En av mulighetene er å sende push-varsler til disse enhetene.
Hva gjorde vi for dette?
Vi delte opp applikasjonen i komponenter som: admin-delen i JS, backend som kjører via REST-grensesnittet under Ruby on Rails. Backend samhandler med databasen. Resultatet som genereres, leveres til klienten. Adminpanelet samhandler med backend og databasen via REST-grensesnitt.
Vi hadde også behov for å sende Push-varsler. Før dette hadde vi et prosjekt hvor vi implementerte en mekanisme som håndterer levering av varsler til mobile plattformer.
Vi utviklet et slikt skjema: operatøren fra nettleseren interagerer med adminpanelet, adminpanelet interagerer med backend, og det settes et oppdrag for å sende Push-varsler.
Push-varsler interagerer med en annen komponent som er implementert på NodeJS.
Køer bygges, og så går sending av varsler gjennom sin egen mekanisme.
Her er det tegnet to databaser. For øyeblikket bruker vi 2 uavhengige databaser ved hjelp av Docker, som ikke er knyttet til hverandre, annet enn at de har et felles virtuelt nettverk, og at de fysiske dataene lagres i separate kataloger på utviklerens maskin.

Det samme, men i tall. Her er gjenbruk av kode viktig.
Hvis vi tidligere snakket om gjenbruk av kode i form av biblioteker, så i dette tilfellet brukes vår tjeneste som håndterer Push-varsler, som en fullverdig server. Den tilbyr et API, og vår nye utvikling interagerer med den.
På det tidspunktet brukte vi NodeJS versjon 4. Nå (i 2017 — bemerkning fra redaktøren) i nye utviklinger bruker vi NodeJS versjon 7. Det er ingen problemer med å bruke nye versjoner av bibliotekene i de nye komponentene.
Hvis nødvendig, kan vi gjennomføre refaktorering og oppgradere versjonen av NodeJS for Push-varslingstjenesten.
Og hvis vi kan opprettholde API-kompatibilitet, kan vi erstatte den med andre prosjekter som ble brukt tidligere.

Hva trengs for å legge til Docker? Vi legger til en Dockerfile i vårt repository som beskriver de nødvendige avhengighetene. I dette eksemplet er komponentene delt opp etter logikk. Dette er det minimal settet for backend-utvikleren.
Når vi oppretter et nytt prosjekt, lager vi en Dockerfile, beskriver den nødvendige økosystemet (Python, Ruby, NodeJS). I docker-compose beskriver vi nødvendige avhengigheter — databasen. Vi spesifiserer hvilken versjon av databasen som trengs, og hvor dataene skal lagres.
Vi bruker en egen tredje container med nginx for å håndtere statikk. Det er lagt til rette for opplasting av bilder. Backend plasserer dem i en forhåndsforberedt volum som også er montert i containeren med nginx, som leverer statikken.
For storing the configurations of nginx and mysql, we have added a Docker folder where we keep the necessary configs. When a developer clones the repository to their machine, they already get a project ready for local development. There is no question about which port or settings to apply.

Next, we have several components: admin, info API, push notifications.
To run all of this, we created another repository, which we named dockerized-app. Currently, we use several repositories for each component. They are just logically different — in GitLab, it looks like a folder, while on the developer's machine, it’s a folder for a specific project. Below them lie the components that will be combined.

This is an example of the contents of dockerized-app. We also bring the Docker directory here, where we fill in the configurations required for the interaction of all components. There is a README.md that briefly describes how to run the project.
Here we have applied two docker-compose files. This is done to enable a step-by-step launch. When a developer works with the core and does not need Push notifications, they simply run the docker-compose file and thereby save resources.
If there is a need for integration with Push notifications, then docker-compose.yaml and docker-compose-push.yaml are launched.
Since docker-compose.yaml and docker-compose-push.yaml are in the same folder, a unified virtual network is automatically created.

Description of the components. This is a more detailed file that is responsible for assembling the components. What’s noteworthy here? Here we introduce the load balancer component.
This is a ready Docker image, which runs nginx and an application that listens to the Docker socket. Dynamically, as containers are turned on and off, it regenerates the nginx config. We distribute the interactions with components across third-level domain names.
For the Development environment, we use the .dev domain — api.informer.dev. Applications with the .dev domain are accessible on the developer's local machine.
Next, the configs are passed to each project, and all projects are launched together simultaneously.

If we depict this graphically, the client becomes our browser or some tool through which we make requests to the load balancer.
The load balancer determines which container to contact based on the domain name.
Dette kan være nginx som leverer JS-administrasjonsverktøyet. Det kan være nginx som leverer API eller statiske filer som hentes av nginx i form av bildegalleri.
På diagrammet er det synlig at containerne er koblet sammen i et virtuelt nettverk og skjult bak en proxy.
På utviklerens maskin kan man få tilgang til containeren ved å vite IP-adressen, men vi bruker i prinsippet ikke dette. Det er sjelden behov for direkte tilgang.

Hvilket eksempel kan jeg se for å dockerisere applikasjonen min? Etter mitt syn er det offisielle docker-image for MySQL et godt eksempel.
Det er ganske komplisert. Det finnes mange versjoner. Men funksjonaliteten dekker mange behov som kan oppstå i løpet av videre utvikling. Hvis du bruker tid på å forstå hvordan alt dette samhandler, tror jeg du ikke vil ha problemer med selvstendig implementering.
På hub.docker.com er det vanligvis lenker til github.com, hvor de rå dataene er angitt, og hvor du kan bygge bildet selv.
I dette depotet er det også et skript som heter docker-endpoint.sh, som er ansvarlig for initialisering og videre prosessering av applikasjonsoppstart.
I dette eksemplet er det også mulighet for konfigurasjon ved hjelp av miljøvariabler. Ved å definere miljøvariabler ved oppstart av en enkelt container eller via docker-compose kan vi angi at vi trenger å sette et tomt passord for docker for root i MySQL eller et annet passord vi ønsker.
Det er en mulighet for å lage et tilfeldig passord. Vi sier at vi trenger en bruker, må sette et passord for brukeren og opprette en database.
I prosjektene våre har vi noe standardisert Dockerfile som håndterer initialiseringen. Der har vi tilpasset det etter våre behov for å enkelt gi brukeren utvidede rettigheter som applikasjonen bruker. Dette gjorde det senere enkelt å opprette en database fra applikasjonens konsoll. I Ruby-applikasjoner finnes det kommandoer for oppretting, endring og sletting av databaser.

Dette er et eksempel på hvordan en spesifikk versjon av MySQL ser ut på github.com. Dockerfile kan åpnes for å se hvordan installasjonen der skjer.
docker-endpoint.sh er skriptet som svarer til inngangspunktet. Ved første initiering kreves det noen forberedende handlinger, og alle disse handlingene er samlet i initieringsskriptet.

Vi går over til del to.
For lagring av kildekoder har vi gått over til gitlab. Dette er et kraftig system med et visuelt grensesnitt.
En av komponentene i Gitlab er Gitlab CI. Det lar oss beskrive sekvenser av kommandoer som deretter brukes til å organisere kodeleveransen eller kjøre automatisert testing.
Presentasjon om Gitlab CI 2 — presentasjonen fra Ruby Russia club — den er ganske detaljert og kan interessere deg.

Nå skal vi se på hva som kreves for å aktivere Gitlab CI. For å starte Gitlab CI er det nok å legge filen .gitlab-ci.yml i rotmappen av prosjektet.
Her beskriver vi hvilke sekvenser av tester og distribusjoner vi ønsker å utføre.
Vi kjører skriptene som direkte kaller docker-compose for å bygge applikasjonen vår. Dette er et eksempel på backend.
Deretter sier vi at det er nødvendig å kjøre migrasjoner for å endre databasen og utføre tester.
Hvis skriptene utføres korrekt og ikke returnerer en feilkode, går systemet videre til den andre distribusjonsstadiet.
Distribusjonsstadiet er i øyeblikket implementert for staging. Vi har ikke organisert en sømløs omstart.
Vi slukker alle containere, og deretter starter vi opp alle containerne igjen, bygget i det første testingssteget.
Vi kjører migrasjoner av databasene som ble skrevet av utviklerne for det nåværende miljøet.
Det er notert at dette kun skal brukes for master-grenen.
Ingen handlinger utføres ved endring i andre grener.
Det er mulighet for å organisere distribusjoner basert på grener.

For å organisere dette videre, må vi installere Gitlab Runner.
Dette verktøyet er skrevet i Golang. Det er en enkelt fil som det er vanlig i Golang-verdenen, som ikke har noen avhengigheter.
Når vi starter, registrerer vi Gitlab Runner.
Vi får en nøkkel i webgrensesnittet til Gitlab.
Deretter kaller vi initialiseringskommandoen i kommandolinjen.
Vi setter opp Gitlab Runner i dialogmodus (Shell, Docker, VirtualBox, SSH)
Koden på Gitlab Runner vil kjøre ved hver commit, avhengig av innstillingen av .gitlab-ci.yml.

Slik ser det ut visuelt i GitLab i nettgrensesnittet. Etter at vi har koblet til GitLab CI, får vi et flagg som viser i hvilken tilstand bygget er på nåværende tidspunkt.
Vi ser at det for 4 minutter siden ble gjort en commit som bestod alle tester uten problemer.

Vi kan se nærmere på bygdene. Her ser vi at det allerede har vært to tilstander. Tilstand for testing og tilstand for distribusjon til staging.
Hvis vi klikker på et spesifikt bygg, vil der være konsollutdata fra kommandoene som ble kjørt i prosessen i henhold til .gitlab-ci.yml.

Slik ser historikken til produktet vårt ut. Vi ser at det har vært vellykkede forsøk. Når testene feiler, går den ikke videre til neste trinn, og koden på staging oppdateres ikke.

Hvilke oppgaver løste vi på staging når vi implementerte Docker? Systemet vårt består av komponenter, og vi hadde behov for å restarte bare de oppdaterte komponentene i depotet, ikke hele systemet.
For dette måtte vi dele opp alt i separate mapper.
Etter at vi gjorde dette, oppsto det et problem med at Docker-compose oppretter sitt eget nettverksområde for hver mappe og ikke ser komponentene til nabomappen.
For å omgå dette, opprettet vi et nettverk i Docker manuelt. I Docker-compose spesifiserte vi at vi bruker dette nettverket til prosjektet.
Dermed ser hver komponent som starter med dette nettverket komponentene i andre deler av systemet.
Det neste problemet er deling av staging mellom flere prosjekter.
For at alt skal se bra ut og være så nært produksjon som mulig, er det best å bruke port 80 eller 443, som brukes overalt på nettet.

Hvordan løste vi dette? Vi tildelte én GitLab Runner til alle store prosjekter.
GitLab tillater å kjøre flere distribuerte GitLab Runners, som vil ta alle oppgaver sekvensielt i en tilfeldig rekkefølge.
For å unngå kaos, begrenset vi gruppen av prosjektene våre til én GitLab Runner, som håndterer volumene våre problemfritt.
Vi flyttet nginx-proxy til et eget oppstartsskript og spesifiserte nettverkene til alle prosjektene der.
Prosjektet vårt har ett nettverk, mens balancereren har flere nettverk etter prosjektnavn. Den kan proxy videre basert på domenenavn.
Vi får forespørslene via domenet på port 80, og de dirigeres til en gruppe containere som betjener dette domenet.

Hvilke andre problemer oppstod? Det som er standard, er at alle containere kjøres av brukeren root. Denne root er ikke den samme som root på vertsoperativsystemet.
Men hvis du logger inn i containeren, vil du være root, og filene som vi oppretter i denne containeren, får root-rettigheter.
Hvis utvikleren har logget inn i containeren og kjørt kommandoer som oppretter filer, og deretter logger ut av containeren, vil han i sitt arbeidskatalog ha en fil han ikke har tilgang til.
Hvordan kan dette løses? Vi kan legge til brukere som skal være i containeren.
Hvilke problemer oppstod da vi la til en bruker?
Når vi oppretter en bruker, er det ofte at gruppering ID-ene (UID) og bruker ID-ene (GID) ikke samsvarer.
For å løse dette problemet i containeren bruker vi brukere med ID 1000.
I vårt tilfelle stemte dette overens med at de fleste utviklerne bruker Ubuntu-operativsystemet. Og på Ubuntu er den første brukeren ID 1000.

Hva er planene våre?
Les gjennom dokumentasjonen for Docker. Prosjektet utvikler seg aktivt, og dokumentasjonen endres. Dataene som ble samlet inn for to-tre måneder siden, er allerede i ferd med å bli utdaterte.
Noen av problemene vi har løst, er sannsynligvis allerede løst med standardmidler.
Vi ønsker så mye å gå videre og begynne med orkestrering.
Et eksempel er den innebygde mekanismen i Docker kalt Docker Swarm, som følger med standardinstallasjonen. Vi vil kjøre noe i produksjon basert på Docker Swarm-teknologi.
Generering av containere gjør det vanskelig å jobbe med logger. Nå er loggene isolerte. De er spredt over containerne. En av oppgavene er å gjøre logger lett tilgjengelig via webgrensesnittet.

Kilde: habr.com
