Ein besserer Ansatz für die kontinuierliche Bereitstellung von Google Cloud
Veröffentlicht: 2022-03-11Continuous Deployment (CD) ist die Praxis, neuen Code automatisch für die Produktion bereitzustellen. Die meisten Continuous-Deployment-Systeme validieren, dass der bereitzustellende Code realisierbar ist, indem Einheiten- und Funktionstests ausgeführt werden, und wenn alles gut aussieht, wird das Deployment durchgeführt. Der Rollout selbst erfolgt in der Regel in Stufen, um ein Rollback durchführen zu können, wenn sich der Code nicht wie erwartet verhält.
Es gibt keinen Mangel an Blogbeiträgen darüber, wie Sie Ihre eigene CD-Pipeline mit verschiedenen Tools wie dem AWS-Stack, dem Google Cloud-Stack, der Bitbucket-Pipeline usw. implementieren. Aber ich finde, dass die meisten davon nicht zu meiner Vorstellung von einer guten CD-Pipeline passen sollte folgendermaßen aussehen: eine, die zuerst erstellt und nur diese einzelne erstellte Datei testet und bereitstellt.
In diesem Artikel werde ich eine ereignisgesteuerte kontinuierliche Bereitstellungspipeline erstellen, die zuerst erstellt und dann Tests für unser endgültiges Bereitstellungsartefakt durchführt. Dies macht nicht nur unsere Testergebnisse zuverlässiger, sondern macht auch die CD-Pipeline leicht erweiterbar. Es würde in etwa so aussehen:
- Es wird ein Commit in unser Quell-Repository vorgenommen.
- Dies löst einen Build des zugehörigen Bildes aus.
- Tests werden auf dem gebauten Artefakt ausgeführt.
- Wenn alles gut aussieht, wird das Image für die Produktion bereitgestellt.
Dieser Artikel setzt zumindest eine vorübergehende Vertrautheit mit Kubernetes und der Containertechnologie voraus, aber wenn Sie nicht vertraut sind oder eine Auffrischung gebrauchen könnten, lesen Sie Was ist Kubernetes? Ein Leitfaden für Containerisierung und Bereitstellung.
Das Problem mit den meisten CD-Setups
Hier ist mein Problem mit den meisten CD-Pipelines: Sie machen normalerweise alles in der Build-Datei. Die meisten Blogposts, die ich darüber gelesen habe, enthalten eine Variation der folgenden Sequenz in der Build-Datei, die sie haben ( cloudbuild.yaml für Google Cloud Build, bitbucket-pipeline.yaml für Bitbucket).
- Führen Sie Tests durch
- Bild aufbauen
- Push-Image an Container-Repository
- Umgebung mit neuem Image aktualisieren
Sie führen Ihre Tests nicht an Ihrem endgültigen Artefakt durch.
Indem Sie die Dinge in dieser Reihenfolge tun, führen Sie Ihre Tests durch. Wenn sie erfolgreich sind, erstellen Sie das Image und fahren mit dem Rest der Pipeline fort. Was passiert, wenn der Build-Prozess Ihr Image so verändert hat, dass die Tests nicht mehr bestehen würden? Meiner Meinung nach sollten Sie damit beginnen, ein Artefakt (das endgültige Container-Image) zu erstellen, und dieses Artefakt sollte sich zwischen dem Build und dem Zeitpunkt, zu dem es in der Produktion bereitgestellt wird, nicht ändern. Dadurch wird sichergestellt, dass die Daten, die Sie über dieses Artefakt haben (Testergebnisse, Größe usw.), immer gültig sind.
Ihre Build-Umgebung hat die „Schlüssel zum Königreich“.
Indem Sie Ihre Build-Umgebung verwenden, um Ihr Image in Ihrem Produktions-Stack bereitzustellen, erlauben Sie ihm effektiv, Ihre Produktionsumgebung zu ändern. Ich betrachte dies als eine sehr schlechte Sache, da jeder mit Schreibzugriff auf Ihr Quell-Repository jetzt mit Ihrer Produktionsumgebung machen kann, was er will.
Sie müssen die gesamte Pipeline erneut ausführen, wenn der letzte Schritt fehlgeschlagen ist.
Wenn der letzte Schritt fehlschlägt (z. B. aufgrund eines Anmeldeinformationsproblems), müssen Sie Ihre gesamte Pipeline erneut ausführen, was Zeit und andere Ressourcen in Anspruch nimmt, die besser für etwas anderes verwendet werden könnten.
Das führt mich zu meinem letzten Punkt:
Deine Schritte sind nicht unabhängig.
Allgemeiner gesagt, haben Sie durch unabhängige Schritte mehr Flexibilität in Ihrer Pipeline. Angenommen, Sie möchten Ihrer Pipeline Funktionstests hinzufügen. Indem Sie Ihre Schritte in einer Build-Datei haben, muss Ihre Build-Umgebung eine funktionale Testumgebung einrichten und die Tests darin ausführen (höchstwahrscheinlich nacheinander). Wenn Ihre Schritte unabhängig wären, könnten Sie sowohl Ihre Komponententests als auch Ihre Funktionstests durch das Ereignis „Bild erstellt“ starten lassen. Sie würden dann in ihrer eigenen Umgebung parallel laufen.
Mein ideales CD-Setup
Meiner Meinung nach wäre ein besserer Weg, dieses Problem anzugehen, eine Reihe unabhängiger Schritte, die alle durch einen Ereignismechanismus miteinander verbunden sind.
Dies hat mehrere Vorteile gegenüber der bisherigen Methode:
Sie können bei verschiedenen Ereignissen mehrere unabhängige Aktionen ausführen.
Wie oben erwähnt, würde das erfolgreiche Erstellen eines neuen Bildes nur ein „erfolgreiches Erstellen“-Ereignis veröffentlichen. Wir können wiederum mehrere Dinge ausführen lassen, wenn dieses Ereignis ausgelöst wird. In unserem Fall würden wir die Unit- und Funktionstests starten. Sie können sich auch Dinge wie die Benachrichtigung des Entwicklers vorstellen, wenn ein Build-Failed-Ereignis ausgelöst wird oder wenn Tests nicht bestanden werden.
Jede Umgebung hat ihre eigenen Rechte.
Indem jeder Schritt in seiner eigenen Umgebung ausgeführt wird, entfällt die Notwendigkeit, dass eine einzelne Umgebung alle Rechte hat. Jetzt kann die Build-Umgebung nur bauen, die Testumgebung nur testen und die Bereitstellungsumgebung nur bereitstellen. So können Sie sicher sein, dass sich Ihr einmal erstelltes Image nicht mehr ändert. Das produzierte Artefakt ist dasjenige, das in Ihrem Produktionsstapel landet. Es ermöglicht auch eine einfachere Prüfung, welcher Schritt Ihrer Pipeline was tut, da Sie einen Satz von Anmeldeinformationen mit einem Schritt verknüpfen können.
Es gibt mehr Flexibilität.
Möchten Sie bei jedem erfolgreichen Build eine E-Mail an jemanden senden? Fügen Sie einfach etwas hinzu, das auf dieses Ereignis reagiert und eine E-Mail sendet. Es ist ganz einfach – Sie müssen Ihren Build-Code nicht ändern und Sie müssen die E-Mail-Adresse einer anderen Person nicht in Ihrem Quell-Repository fest codieren.
Wiederholungen sind einfacher.
Unabhängige Schritte bedeuten auch, dass Sie nicht die gesamte Pipeline neu starten müssen, wenn ein Schritt fehlschlägt. Wenn die Fehlerbedingung vorübergehend ist oder manuell behoben wurde, können Sie den fehlgeschlagenen Schritt einfach wiederholen. Dies ermöglicht eine effizientere Pipeline. Wenn ein Build-Schritt mehrere Minuten dauert, ist es gut, das Image nicht neu erstellen zu müssen, nur weil Sie vergessen haben, Ihrer Bereitstellungsumgebung Schreibzugriff auf Ihren Cluster zu gewähren.
Implementierung von Google Cloud Continuous Deployment
Die Google Cloud Platform verfügt über alle erforderlichen Tools, um ein solches System in kurzer Zeit und mit sehr wenig Code zu erstellen.
Unsere Testanwendung ist eine einfache Flask-Anwendung, die nur einen statischen Text bereitstellt. Diese Anwendung wird in einem Kubernetes-Cluster bereitgestellt, der sie dem breiteren Internet zur Verfügung stellt.
Ich werde eine vereinfachte Version der Pipeline implementieren, die ich zuvor eingeführt habe. Ich habe die Testschritte im Grunde genommen entfernt, sodass es jetzt so aussieht:
- Ein neuer Commit wird an das Quell-Repository vorgenommen
- Dies löst einen Bildaufbau aus. Bei Erfolg wird es in das Container-Repository übertragen und ein Ereignis in einem Pub/Sub-Thema veröffentlicht
- Ein kleines Skript wird für dieses Thema abonniert und überprüft die Parameter des Bildes – wenn sie mit unseren Anforderungen übereinstimmen, wird es im Kubernetes-Cluster bereitgestellt.
Hier ist eine grafische Darstellung unserer Pipeline.
Der Ablauf ist wie folgt:
- Jemand verpflichtet sich zu unserem Repository.
- Dies löst einen Cloud-Build aus, der ein Docker-Image basierend auf dem Quell-Repository erstellt.
- Der Cloud-Build pusht das Image in das Container-Repository und veröffentlicht eine Nachricht an Cloud Pub/Sub.
- Dies löst eine Cloud-Funktion aus, die die Parameter der veröffentlichten Nachricht überprüft (Status des Builds, Name des erstellten Images usw.).
- Wenn die Parameter gut sind, aktualisiert die Cloud-Funktion eine Kubernetes-Bereitstellung mit dem neuen Image.
- Kubernetes stellt neue Container mit dem neuen Image bereit.
Quellcode
Unser Quellcode ist eine sehr einfache Flask-App, die nur statischen Text bereitstellt. Hier ist die Struktur unseres Projekts:

├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.pyDas Docker-Verzeichnis enthält alles, was zum Erstellen des Docker-Images benötigt wird. Das Image basiert auf dem uWSGI- und Nginx-Image und installiert nur die Abhängigkeiten und kopiert die App in den richtigen Pfad.
Das k8s-Verzeichnis enthält die Kubernetes-Konfiguration. Es besteht aus einem Dienst und einer Bereitstellung. Die Bereitstellung startet einen Container basierend auf dem Image, das aus Dockerfile erstellt wurde. Der Dienst startet dann einen Load Balancer, der über eine öffentliche IP-Adresse verfügt und zu den App-Containern umleitet.
Cloud-Build
Die Cloud-Build-Konfiguration selbst kann über die Cloud-Konsole oder die Google Cloud-Befehlszeile erfolgen. Ich habe mich für die Cloud-Konsole entschieden.
Hier erstellen wir ein Image für jeden Commit in jedem Zweig, aber Sie könnten beispielsweise unterschiedliche Images für Entwicklung und Produktion haben.
Wenn der Build erfolgreich ist, veröffentlicht der Cloud-Build das Image selbst in der Containerregistrierung. Anschließend wird eine Nachricht an das Cloud-Builds-Pub/Sub-Thema veröffentlicht.
Der Cloud-Build veröffentlicht auch Nachrichten, wenn ein Build ausgeführt wird und wenn einer fehlschlägt, sodass Sie auch Dinge auf diese Nachrichten reagieren lassen können.
Die Dokumentation für die Pub/Sub-Benachrichtigungen des Cloud-Builds finden Sie hier und das Format der Nachricht finden Sie hier
Cloud Pub/Sub
Wenn Sie in der Cloud-Konsole auf der Registerkarte „Cloud Pub/Sub“ nachsehen, sehen Sie, dass der Cloud-Build ein Thema namens „Cloud-Builds“ erstellt hat. Hier veröffentlicht der Cloud-Build seine Status-Updates.
Cloud-Funktion
Wir erstellen jetzt eine Cloud-Funktion, die bei jeder Nachricht ausgelöst wird, die im Thema „Cloud-Builds“ veröffentlicht wird. Auch hier können Sie entweder die Cloud-Konsole oder das Google Cloud-Befehlszeilendienstprogramm verwenden. In meinem Fall habe ich den Cloud-Build verwendet, um die Cloud-Funktion bei jeder Änderung bereitzustellen.
Der Quellcode für die Cloud-Funktion ist hier.
Schauen wir uns zunächst den Code an, der diese Cloud-Funktion bereitstellt:
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']Hier verwenden wir das Google Cloud Docker-Image. Dies ermöglicht die einfache Ausführung von GCcloud-Befehlen. Was wir ausführen, entspricht dem Ausführen des folgenden Befehls direkt von einem Terminal aus:
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_NAMEWir bitten Google Cloud, eine neue Cloud-Funktion bereitzustellen (oder zu ersetzen, falls eine Funktion mit diesem Namen in dieser Region bereits vorhanden ist), die die Python 3.7-Laufzeit verwendet und durch neue Nachrichten im Thema „Cloud-Builds“ ausgelöst wird. Wir teilen Google auch mit, wo der Quellcode für diese Funktion zu finden ist (hier sind PROJECT_ID und REPO_NAME Umgebungsvariablen, die vom Build-Prozess festgelegt werden). Wir teilen ihm auch mit, welche Funktion als Einstiegspunkt aufgerufen werden soll.
Nebenbei bemerkt, damit dies funktioniert, müssen Sie Ihrem Cloudbuild-Dienstkonto sowohl den „Cloud Functions Developer“ als auch den „Service Account User“ geben, damit es die Cloud-Funktion bereitstellen kann.
Hier sind einige kommentierte Snippets des Cloud-Funktionscodes
Die Einstiegspunktdaten enthalten die zum Pub/Sub-Thema empfangene Nachricht.
def onNewImage(data, context):Der erste Schritt besteht darin, die Variablen für diese spezifische Bereitstellung aus der Umgebung abzurufen (wir haben diese definiert, indem wir die Cloud-Funktion in der Cloud-Konsole geändert haben.
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')Wir überspringen den Teil, in dem wir überprüfen, ob die Struktur der Nachricht unseren Erwartungen entspricht, und wir überprüfen, ob der Build erfolgreich war und ein Bildartefakt erzeugt hat.
Der nächste Schritt besteht darin, sicherzustellen, dass das erstellte Image dasjenige ist, das wir bereitstellen möchten.
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}') returnJetzt erhalten wir einen Kubernetes-Client und rufen die Bereitstellung ab, die wir ändern möchten
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}') returnSchließlich patchen wir die Bereitstellung mit dem neuen Image; Kubernetes kümmert sich um die Einführung.
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)Fazit
Dies ist ein sehr einfaches Beispiel dafür, wie ich die Architektur einer CD-Pipeline mag. Sie könnten mehr Schritte haben, indem Sie einfach ändern, welches Pub/Sub-Ereignis was auslöst.
Beispielsweise könnten Sie einen Container ausführen, der die Tests innerhalb des Images ausführt und bei Erfolg ein Ereignis und bei einem Fehler ein Ereignis veröffentlicht und darauf reagieren, indem Sie je nach Ergebnis entweder eine Bereitstellung aktualisieren oder eine Warnung ausgeben.
Die von uns erstellte Pipeline ist ziemlich einfach, aber Sie könnten andere Cloud-Funktionen für andere Teile schreiben (z. B. eine Cloud-Funktion, die eine E-Mail an den Entwickler sendet, der den Code festgeschrieben hat, der Ihre Komponententests beschädigt hat).
Wie Sie sehen können, kann unsere Build-Umgebung nichts in unserem Kubernetes-Cluster ändern, und unser Bereitstellungscode (die Cloud-Funktion) kann das erstellte Image nicht ändern. Unsere Privilegientrennung sieht gut aus, und wir können ruhig schlafen, wenn wir wissen, dass ein unseriöser Entwickler unseren Produktionscluster nicht zum Absturz bringen wird. Außerdem können wir unseren ops-orientierten Entwicklern Zugriff auf den Cloud-Funktionscode geben, damit sie ihn reparieren oder verbessern können.
Wenn Sie Fragen, Anmerkungen oder Verbesserungen haben, können Sie sich gerne in den Kommentaren unten an uns wenden.
