O abordare mai bună a implementării continue Google Cloud
Publicat: 2022-03-11Implementarea continuă (CD) este practica de implementare automată a codului nou în producție. Majoritatea sistemelor de implementare continuă validează faptul că codul care urmează să fie implementat este viabil prin rularea testelor unitare și funcționale și, dacă totul arată bine, implementarea este lansată. Lansarea în sine are loc, de obicei, în etape, pentru a putea fi anulată dacă codul nu se comportă conform așteptărilor.
Nu lipsesc postările de blog despre cum să-ți implementezi propriul canal de CD-uri folosind diverse instrumente, cum ar fi stiva AWS, stiva Google Cloud, conducta Bitbucket etc. Dar constat că majoritatea nu se potrivesc cu ideea mea despre ceea ce este un canal bun de CD-uri. ar trebui să arate așa: unul care se construiește primul și testează și implementează doar acel singur fișier construit.
În acest articol, voi construi o conductă de implementare continuă bazată pe evenimente care se construiește mai întâi și apoi rulează teste pe artefactul final de implementare. Acest lucru nu numai că face rezultatele testelor noastre mai fiabile, dar face și conducta CD-ului ușor de extins. Ar arata cam asa:
- Se face un commit către depozitul nostru sursă.
- Acest lucru declanșează o construcție a imaginii asociate.
- Testele sunt efectuate pe artefactul construit.
- Dacă totul arată bine, imaginea este implementată în producție.
Acest articol presupune cel puțin o familiaritate trecătoare cu Kubernetes și tehnologia containerelor, dar dacă nu sunteți familiarizat sau ați putea folosi o reîmprospătare, consultați Ce este Kubernetes? Un ghid pentru containerizare și implementare.
Problema cu cele mai multe setări de CD
Iată problema mea cu majoritatea conductelor de CD: de obicei, fac totul în fișierul de compilare. Majoritatea postărilor de blog pe care le-am citit despre asta vor avea o variație a următoarei secvențe în orice fișier de compilare pe care îl au ( cloudbuild.yaml
pentru Google Cloud Build, bitbucket-pipeline.yaml
pentru Bitbucket).
- Rulați teste
- Construiți imaginea
- Împingeți imaginea în depozitul containerului
- Actualizați mediul cu o nouă imagine
Nu îți faci testele pe artefactul tău final.
Făcând lucrurile în această ordine, rulați testele. Dacă au succes, construiți imaginea și continuați cu restul conductei. Ce se întâmplă dacă procesul de construire ți-a schimbat imaginea în așa fel încât testele să nu mai treacă? În opinia mea, ar trebui să începeți prin a produce un artefact (imaginea finală a containerului) și acest artefact nu ar trebui să se schimbe între construcție și momentul în care este implementat în producție. Acest lucru asigură că datele pe care le aveți despre respectivul artefact (rezultatele testului, dimensiunea etc.) sunt întotdeauna valide.
Mediul tău de construcție are „cheile regatului”.
Folosind mediul de construcție pentru a vă implementa imaginea în stiva de producție, îi permiteți efectiv să vă schimbe mediul de producție. Consider acest lucru ca fiind un lucru foarte rău, deoarece oricine are acces de scriere la depozitul dumneavoastră sursă poate face acum orice dorește în mediul dumneavoastră de producție.
Trebuie să rulați din nou întreaga conductă dacă ultimul pas a eșuat.
Dacă ultimul pas eșuează (de exemplu, din cauza unei probleme de acreditări), trebuie să rulați din nou întreaga conductă, ocupând timp și alte resurse care ar putea fi mai bine cheltuite pentru a face altceva.
Asta mă duce la punctul meu final:
Pașii tăi nu sunt independenți.
Într-un sens mai general, a avea pași independenți vă permite să aveți mai multă flexibilitate în pipeline. Să presupunem că doriți să adăugați teste funcționale la conducta dvs. Având pașii dvs. într-un singur fișier de compilare, trebuie să faceți ca mediul dvs. de construcție să creeze un mediu de testare funcțional și să rulați testele în el (cel mai probabil secvenţial). Dacă pașii dvs. ar fi independenți, ați putea începe atât testele unitare, cât și testele funcționale prin evenimentul „imagine construită”. Apoi ar alerga în paralel în propriul lor mediu.
Configurarea mea ideală pentru CD
După părerea mea, o modalitate mai bună de a aborda această problemă ar fi să existe o serie de pași independenți, toți legați împreună printr-un mecanism de eveniment.
Aceasta are mai multe avantaje în comparație cu metoda anterioară:
Puteți întreprinde mai multe acțiuni independente pe diferite evenimente.
După cum sa menționat mai sus, construirea cu succes a unei noi imagini ar publica doar un eveniment de „construire cu succes”. La rândul său, putem avea mai multe lucruri să ruleze atunci când acest eveniment este declanșat. În cazul nostru, am începe testele unitare și funcționale. De asemenea, vă puteți gândi la lucruri precum alertarea dezvoltatorului atunci când este declanșat un eveniment eșuat de construcție sau dacă testele nu trec.
Fiecare mediu are propriul său set de drepturi.
Făcând ca fiecare pas să se întâmple în propriul său mediu, eliminăm necesitatea ca un singur mediu să aibă toate drepturile. Acum mediul de compilare poate doar să se construiască, mediul de testare poate doar să testeze, iar mediul de implementare poate doar să se implementeze. Acest lucru vă permite să aveți încredere că, odată ce imaginea dvs. a fost construită, aceasta nu se va schimba. Artefactul care a fost produs este cel care va ajunge în stiva dvs. de producție. De asemenea, permite o auditare mai ușoară a pasului din conducta dvs. care face ceea ce, deoarece puteți lega un set de acreditări la un singur pas.
Există mai multă flexibilitate.
Doriți să trimiteți un e-mail cuiva pentru fiecare versiune reușită? Doar adăugați ceva care reacționează la acel eveniment și trimite un e-mail. Este ușor — nu trebuie să vă schimbați codul de compilare și nu trebuie să codificați e-mailul cuiva în arhiva dvs. sursă.
Reîncercările sunt mai ușoare.
Având pași independenți înseamnă, de asemenea, că nu trebuie să reporniți întreaga conductă dacă un pas nu reușește. Dacă condiția de eșec este temporară sau a fost remediată manual, puteți doar să reîncercați pasul care a eșuat. Acest lucru permite o conductă mai eficientă. Când un pas de construire durează câteva minute, este bine să nu trebuiască să reconstruiți imaginea doar pentru că ați uitat să acordați acces de scriere a mediului de implementare la cluster.
Implementarea Google Cloud Continuous Deployment
Google Cloud Platform are toate instrumentele necesare pentru a construi un astfel de sistem într-un timp scurt și cu foarte puțin cod.
Aplicația noastră de testare este o aplicație simplă Flask care servește doar o bucată de text static. Această aplicație este implementată într-un cluster Kubernetes care o deservește pe internetul mai larg.
Voi implementa o versiune simplificată a conductei pe care am introdus-o mai devreme. Practic, am eliminat pașii de testare, așa că acum arată astfel:
- Se face o nouă confirmare în depozitul sursă
- Acest lucru declanșează o construcție a imaginii. Dacă reușește, este trimis în depozitul de container și un eveniment este publicat într-un subiect Pub/Sub
- Un mic script este abonat la acel subiect și verifică parametrii imaginii - dacă se potrivesc cu ceea ce am cerut, este implementat în clusterul Kubernetes.
Iată o reprezentare grafică a conductei noastre.
Fluxul este următorul:
- Cineva se angajează în depozitul nostru.
- Acest lucru declanșează o construcție în cloud care construiește o imagine Docker pe baza depozitului sursă.
- Compilarea cloud împinge imaginea în depozitul containerului și publică un mesaj către cloud pub/sub.
- Aceasta declanșează o funcție cloud care verifică parametrii mesajului publicat (starea build-ului, numele imaginii construite etc.).
- Dacă parametrii sunt buni, funcția cloud actualizează o implementare Kubernetes cu noua imagine.
- Kubernetes implementează containere noi cu noua imagine.
Cod sursa
Codul nostru sursă este o aplicație Flask foarte simplă, care servește doar un text static. Iată structura proiectului nostru:

├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.py
Directorul Docker conține tot ce este necesar pentru a construi imaginea Docker. Imaginea se bazează pe imaginea uWSGI și Nginx și doar instalează dependențele și copiază aplicația pe calea corectă.
Directorul k8s conține configurația Kubernetes. Constă dintr-un serviciu și o implementare. Implementarea pornește un container bazat pe imaginea construită din Dockerfile . Apoi, serviciul pornește un echilibrator de încărcare care are o adresă IP publică și redirecționează către containerele aplicației.
Cloud Build
Configurația cloud build în sine se poate face prin consola cloud sau linia de comandă Google Cloud. Am ales să folosesc consola cloud.
Aici, construim o imagine pentru orice comit din orice ramură, dar puteți avea imagini diferite pentru dezvoltare și producție, de exemplu.
Dacă construirea are succes, versiunea în cloud va publica singur imaginea în registrul containerului. Apoi va publica un mesaj către subiectul pub/sub-builds în cloud.
Compilarea în cloud publică, de asemenea, mesaje atunci când o versiune este în curs de desfășurare și când una nu reușește, astfel încât lucrurile să reacționeze și la aceste mesaje.
Documentația pentru notificările pub/sub din cloud build este aici și formatul mesajului poate fi găsit aici
Cloud Pub/Sub
Dacă vă uitați în fila cloud pub/sub din consola cloud, veți vedea că versiunea în cloud a creat un subiect numit cloud builds. Aici versiunea cloud își publică actualizările de stare.
Funcția Cloud
Ceea ce vom face acum este să creăm o funcție cloud care este declanșată pentru orice mesaj publicat în subiectul cloud-builds. Din nou, puteți utiliza fie consola cloud, fie utilitarul linie de comandă Google Cloud. Ceea ce am făcut în cazul meu este că folosesc construcția cloud pentru a implementa funcția de cloud de fiecare dată când apare o modificare a acesteia.
Codul sursă pentru funcția cloud este aici.
Să ne uităm mai întâi la codul care implementează această funcție cloud:
steps: - name: 'gcr.io/cloud-builders/gcloud' id: 'test' args: ['functions', 'deploy', 'new-image-trigger', '--runtime=python37', '--trigger-topic=cloud-builds', '--entry-point=onNewImage', '--region=us-east1', '--source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME']
Aici, folosim imaginea Google Cloud Docker. Acest lucru vă permite să rulați cu ușurință comenzile GCcloud. Ceea ce executăm este echivalentul cu rularea următoarei comenzi direct de la un terminal:
gcloud functions deploy new-image-trigger --runtime=python37 --trigger-topic=cloud-builds --entry-point=onNewImage --region=us-east1 --source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME
Solicităm Google Cloud să implementeze o nouă funcție cloud (sau să o înlocuiască dacă o funcție cu acest nume în acea regiune există deja) care va folosi timpul de execuție Python 3.7 și va fi declanșată de mesaje noi în subiectul cloud-build-uri. De asemenea, îi spunem Google unde să găsească codul sursă pentru acea funcție (aici PROJECT_ID și REPO_NAME sunt variabile de mediu care sunt setate de procesul de construire). De asemenea, îi spunem ce funcție să apelăm ca punct de intrare.
Ca o notă secundară, pentru ca acest lucru să funcționeze, trebuie să oferiți contului de serviciu cloudbuild atât „dezvoltatorul de funcții cloud”, cât și „utilizatorul contului de serviciu”, astfel încât să poată implementa funcția cloud.
Iată câteva fragmente comentate din codul funcției cloud
Datele de intrare vor conține mesajul primit pe subiectul pub/sub.
def onNewImage(data, context):
Primul pas este să obținem variabilele pentru acea implementare specifică din mediu (le-am definit prin modificarea funcției cloud în consola cloud.
project = os.environ.get('PROJECT') zone = os.environ.get('ZONE') cluster = os.environ.get('CLUSTER') deployment = os.environ.get('DEPLOYMENT') deploy_image = os.environ.get('IMAGE') target_container = os.environ.get('CONTAINER')
Vom sări peste partea în care verificăm dacă structura mesajului este ceea ce ne așteptăm și validăm că construcția a avut succes și a produs un artefact de imagine.
Următorul pas este să ne asigurăm că imaginea care a fost construită este cea pe care vrem să o implementăm.
image = decoded_data['results']['images'][0]['name'] image_basename = image.split('/')[-1].split(':')[0] if image_basename != deploy_image: logging.error(f'{image_basename} is different from {deploy_image}') return
Acum, obținem un client Kubernetes și recuperăm implementarea pe care dorim să o modificăm
v1 = get_kube_client(project, zone, cluster) dep = v1.read_namespaced_deployment(deployment, 'default') if dep is None: logging.error(f'There was no deployment named {deployment}') return
În cele din urmă, corectăm implementarea cu noua imagine; Kubernetes se va ocupa de lansarea acestuia.
for i, container in enumerate(dep.spec.template.spec.containers): if container.name == target_container: dep.spec.template.spec.containers[i].image = image logging.info(f'Updating to {image}') v1.patch_namespaced_deployment(deployment, 'default', dep)
Concluzie
Acesta este un exemplu foarte de bază al modului în care îmi place ca lucrurile să fie proiectate într-o conductă de CD. Ai putea avea mai mulți pași doar schimbând ce eveniment pub/sub declanșează ce.
De exemplu, puteți rula un container care rulează testele în interiorul imaginii și publică un eveniment despre succes și altul despre eșec și să reacționați la acestea fie actualizând o implementare, fie alertând în funcție de rezultat.
Conducta pe care am construit-o este destul de simplă, dar puteți scrie alte funcții cloud pentru alte părți (de exemplu, o funcție cloud care ar trimite un e-mail dezvoltatorului care a comis codul care a spart testele dvs. unitare).
După cum puteți vedea, mediul nostru de compilare nu poate schimba nimic în clusterul nostru Kubernetes, iar codul nostru de implementare (funcția cloud) nu poate modifica imaginea care a fost creată. Separarea noastră de privilegii arată bine și putem dormi bine știind că un dezvoltator nepoliticos nu va distruge clusterul nostru de producție. De asemenea, putem oferi dezvoltatorilor noștri mai orientați spre operațiuni acces la codul funcției cloud, astfel încât să îl poată repara sau îmbunătăți.
Dacă aveți întrebări, observații sau îmbunătățiri, nu ezitați să contactați în comentariile de mai jos.