Zero Downtime Jenkins Continuous Deployment mit Terraform auf AWS

Veröffentlicht: 2022-03-11

In der heutigen Welt des Internets, in der buchstäblich alles rund um die Uhr verfügbar sein muss, ist Zuverlässigkeit der Schlüssel. Dies führt zu nahezu null Ausfallzeiten für Ihre Websites, wodurch die gefürchtete „Nicht gefunden: 404“-Fehlerseite oder andere Dienstunterbrechungen vermieden werden, während Sie Ihre neueste Version einführen.

Angenommen, Sie haben eine neue Anwendung für Ihren Kunden oder vielleicht für sich selbst erstellt und es geschafft, eine gute Benutzerbasis zu gewinnen, die Ihre Anwendung mag. Sie haben Feedback von Ihren Benutzern gesammelt und gehen zu Ihren Entwicklern und bitten sie, neue Funktionen zu entwickeln und die Anwendung einsatzbereit zu machen. Wenn Sie damit fertig sind, können Sie entweder die gesamte Anwendung anhalten und die neue Version bereitstellen oder eine CI/CD-Bereitstellungspipeline ohne Ausfallzeiten erstellen, die die ganze mühsame Arbeit des Pushens einer neuen Version für Benutzer ohne manuelles Eingreifen erledigt.

In diesem Artikel werden wir genau über letzteres sprechen, wie wir eine kontinuierliche Bereitstellungspipeline einer dreischichtigen Webanwendung haben können, die in Node.js in AWS Cloud mit Terraform als Infrastrukturorchestrator erstellt wurde. Wir verwenden Jenkins für den Continuous Deployment-Teil und Bitbucket zum Hosten unserer Codebasis.

Code-Repository

Wir werden eine dreistufige Demo-Webanwendung verwenden, für die Sie den Code hier finden können.

Das Repo enthält Code sowohl für die Web- als auch für die API-Schicht. Es ist eine einfache Anwendung, bei der das Webmodul einen der Endpunkte in der API-Schicht aufruft, der intern Informationen über die aktuelle Uhrzeit aus der Datenbank abruft und zur Webschicht zurückkehrt.

Die Struktur des Repos ist wie folgt:

  • API: Code für die API-Schicht
  • Web: Code für die Webebene
  • Terraform: Code für die Infrastruktur-Orchestrierung mit Terraform
  • Jenkins: Code für Infrastructure Orchestrator für Jenkins-Server, der für die CI/CD-Pipeline verwendet wird.

Nachdem wir nun verstanden haben, was wir bereitstellen müssen, lassen Sie uns besprechen, was wir tun müssen, um diese Anwendung auf AWS bereitzustellen, und dann würden wir darüber sprechen, wie wir diesen Teil der CI/CD-Pipeline machen können.

Bilder backen

Da wir Terraform für Infrastructure Orchestrator verwenden, ist es am sinnvollsten, vorgefertigte Images für jede Schicht oder Anwendung zu haben, die Sie bereitstellen möchten. Und dafür würden wir ein anderes Produkt von Hashicorp verwenden – nämlich Packer.

Packer ist ein Open-Source-Tool, das beim Erstellen eines Amazon Machine Image oder AMI hilft, das für die Bereitstellung auf AWS verwendet wird. Es kann verwendet werden, um Images für verschiedene Plattformen wie EC2, VirtualBox, VMware und andere zu erstellen.

Hier ist ein Ausschnitt, wie die Packer-Konfigurationsdatei ( terraform/packer-ami-api.json ) verwendet wird, um ein AMI für die API-Schicht zu erstellen.

 { "builders": [{ "type": "amazon-ebs", "region": "eu-west-1", "source_ami": "ami-844e0bf7", "instance_type": "t2.micro", "ssh_username": "ubuntu", "ami_name": "api-instance {{timestamp}}" }], "provisioners": [ { "type": "shell", "inline": ["mkdir api", "sudo apt-get update", "sudo apt-get -y install npm nodejs-legacy"], "pause_before": "10s" }, { "type": "file", "source" : "../api/", "destination" : "api" }, { "type": "shell", "inline": ["cd api", "npm install"], "pause_before": "10s" } ] }

Und Sie müssen den folgenden Befehl ausführen, um das AMI zu erstellen:

 packer build -machine-readable packer-ami-api.json

Wir werden diesen Befehl später in diesem Artikel aus dem Jenkins-Build ausführen. Auf ähnliche Weise verwenden wir die Packer-Konfigurationsdatei ( terraform/packer-ami-web.json ) auch für die Webebene.

Lassen Sie uns die obige Packer-Konfigurationsdatei durchgehen und verstehen, was sie zu tun versucht.

  1. Wie bereits erwähnt, kann Packer zum Erstellen von Images für viele Plattformen verwendet werden, und da wir unsere Anwendung auf AWS bereitstellen, würden wir den Builder „amazon-ebs“ verwenden, da dies der einfachste Builder für den Einstieg ist.
  2. Der zweite Teil der Konfiguration enthält eine Liste von Anbietern, die eher Skripts oder Codeblöcken ähneln, mit denen Sie Ihr Image konfigurieren können.
    • Schritt 1 führt einen Shell-Provisioner aus, um einen API-Ordner zu erstellen und Node.js auf dem Image mithilfe der inline Eigenschaft zu installieren, bei der es sich um eine Reihe von Befehlen handelt, die Sie ausführen möchten.
    • Schritt 2 führt einen File Provisioner aus, um unseren Quellcode aus dem API-Ordner auf die Instanz zu kopieren.
    • Schritt 3 führt erneut einen Shell-Provisioner aus, verwendet diesmal jedoch eine Skripteigenschaft, um eine Datei (terraform/scripts/install_api_software.sh) mit den auszuführenden Befehlen anzugeben.
    • Schritt 4 kopiert eine Konfigurationsdatei in die Instanz, die für Cloudwatch benötigt wird, die im nächsten Schritt installiert wird.
    • Schritt 5 führt einen Shell-Bereitsteller aus, um den AWS Cloudwatch-Agenten zu installieren. Die Eingabe für diesen Befehl wäre die im vorherigen Schritt kopierte Konfigurationsdatei. Wir werden später in diesem Artikel ausführlich über Cloudwatch sprechen.

Im Wesentlichen enthält die Packer-Konfiguration also Informationen darüber, welchen Builder Sie möchten, und dann eine Reihe von Provisionern, die Sie in beliebiger Reihenfolge definieren können, je nachdem, wie Sie Ihr Image konfigurieren möchten.

Einrichten einer kontinuierlichen Jenkins-Bereitstellung

Als Nächstes werden wir uns mit der Einrichtung eines Jenkins-Servers befassen, der für unsere CI/CD-Pipeline verwendet wird. Wir werden Terraform und AWS auch für die Einrichtung verwenden.

Der Terraform-Code zum Einstellen von Jenkins befindet sich im Ordner jenkins/setup . Lassen Sie uns einige der interessanten Dinge über dieses Setup durchgehen.

  1. AWS-Anmeldeinformationen: Sie können dem Terraform-AWS-Anbieter ( instance.tf ) entweder die AWS-Zugriffsschlüssel-ID und den geheimen Zugriffsschlüssel bereitstellen oder Sie können den Speicherort der Datei mit den Anmeldeinformationen an die Eigenschaft shared_credentials_file im AWS-Anbieter weitergeben.
  2. IAM-Rolle: Da wir Packer und Terraform vom Jenkins-Server aus ausführen werden, würden sie auf S3-, EC2-, RDS-, IAM-, Lastausgleichs- und Autoskalierungsdienste auf AWS zugreifen. Also stellen wir entweder unsere Anmeldeinformationen auf Jenkins für Packer & Terraform bereit, um auf diese Dienste zuzugreifen, oder wir können ein IAM-Profil ( iam.tf ) erstellen, mit dem wir eine Jenkins-Instanz erstellen würden.
  3. Terraform-Zustand: Terraform muss den Zustand der Infrastruktur irgendwo in einer Datei pflegen, und mit S3 ( backend.tf ) könnten Sie ihn einfach dort pflegen, sodass Sie mit anderen Kollegen zusammenarbeiten können und jeder den Zustand ändern und bereitstellen kann wird an einem entfernten Ort gehalten.
  4. Öffentliches/privates Schlüsselpaar: Sie müssen den öffentlichen Schlüssel Ihres Schlüsselpaars zusammen mit der Instanz hochladen, damit Sie nach dem Hochfahren per SSH in die Jenkins-Instanz gelangen können. Wir haben eine aws_key_pair -Ressource ( key.tf ) definiert, in der Sie den Speicherort Ihres öffentlichen Schlüssels mithilfe von Terraform-Variablen angeben.

Schritte zum Einrichten von Jenkins:

Schritt 1: Um den Remote-Zustand von Terraform beizubehalten, müssten Sie manuell einen Bucket in S3 erstellen, der von Terraform verwendet werden kann. Dies wäre der einzige Schritt, der außerhalb von Terraform durchgeführt wird. Stellen Sie sicher, dass Sie AWS configure ausführen, bevor Sie den folgenden Befehl ausführen, um Ihre AWS-Anmeldeinformationen anzugeben.

 aws s3api create-bucket --bucket node-aws-jenkins-terraform --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1

Schritt 2: Führen Sie terraform init . Dadurch wird der Zustand initialisiert und für die Speicherung auf S3 konfiguriert und das AWS-Anbieter-Plugin heruntergeladen.

Schritt 3: Führen Sie terraform apply . Dadurch wird der gesamte Terraform-Code überprüft, ein Plan erstellt und angezeigt, wie viele Ressourcen nach Abschluss dieses Schritts erstellt werden.

Schritt 4: Geben yes ein, und der vorherige Schritt beginnt mit der Erstellung aller Ressourcen. Nachdem der Befehl abgeschlossen ist, erhalten Sie die öffentliche IP-Adresse des Jenkins-Servers.

Schritt 5: Ssh in den Jenkins-Server mit Ihrem privaten Schlüssel. ubuntu ist der Standardbenutzername für AWS EBS-unterstützte Instances. Verwenden Sie die vom terraform apply -Befehl zurückgegebene IP-Adresse.

 ssh -i mykey [email protected]

Schritt 6: Starten Sie die Web-Benutzeroberfläche von Jenkins, indem Sie zu http://34.245.4.73:8080 gehen. Das Passwort finden Sie unter /var/lib/jenkins/secrets/initialAdminPassword .

Schritt 7: Wählen Sie „Vorgeschlagene Plugins installieren“ und erstellen Sie einen Admin-Benutzer für Jenkins.

Einrichten der CI-Pipeline zwischen Jenkins und Bitbucket

  1. Dazu müssen wir das Bitbucket-Plugin in Jenkins installieren. Gehen Sie zu Manage Jenkins → Manage Plugins und installieren Sie unter Verfügbare Plugins das Bitbucket-Plugin.
  2. Gehen Sie auf der Bitbucket-Repository-Seite zu Einstellungen → Webhooks und fügen Sie einen neuen Webhook hinzu. Dieser Hook sendet alle Änderungen im Repository an Jenkins, wodurch die Pipelines ausgelöst werden.
    Hinzufügen eines Webhook zur kontinuierlichen Bereitstellung von Jenkins über Bitbucker

Jenkins-Pipeline zum Backen/Erstellen von Images

  1. Der nächste Schritt besteht darin, Pipelines in Jenkins zu erstellen.
  2. Die erste Pipeline wird ein Freestyle-Projekt sein, das verwendet wird, um das AMI der Anwendung mit Packer zu erstellen.
  3. Sie müssen die Anmeldeinformationen und die URL für Ihr Bitbucket-Repository angeben.
    Anmeldeinformationen zu Bitbucket hinzufügen
  4. Geben Sie den Build-Trigger an.
    Konfigurieren des Build-Triggers
  5. Fügen Sie zwei Build-Schritte hinzu, einen zum Erstellen des AMI für das App-Modul und andere zum Erstellen des AMI für das Web-Modul.
    Hinzufügen von AMI-Build-Schritten
  6. Sobald dies erledigt ist, können Sie das Jenkins-Projekt speichern, und wenn Sie jetzt etwas in Ihr Bitbucket-Repository verschieben, wird ein neuer Build in Jenkins ausgelöst, der das AMI erstellt und eine Terraform-Datei mit der AMI-Nummer dieses Bildes an das S3-Bucket, den Sie in den letzten beiden Zeilen im Build-Schritt sehen können.
 echo 'variable "WEB_INSTANCE_AMI" { default = "'${AMI_ID_WEB}'" }' > amivar_web.tf aws s3 cp amivar_web.tf s3://node-aws-jenkins-terraform/amivar_web.tf

Jenkins-Pipeline zum Auslösen des Terraform-Skripts

Nachdem wir nun die AMIs für die API- und Webmodule haben, lösen wir einen Build aus, um den Terraform-Code zum Einrichten der gesamten Anwendung auszuführen, und gehen später die Komponenten im Terraform-Code durch, wodurch diese Pipeline die Änderungen ohne Ausfallzeiten des Dienstes bereitstellt.

  1. Wir erstellen ein weiteres Freestyle-Jenkins-Projekt, nodejs-terraform , das den Terraform-Code zum Bereitstellen der Anwendung ausführen würde.
  2. Zunächst erstellen wir in der globalen Berechtigungsnachweisdomäne einen Berechtigungsnachweis vom Typ „geheimer Text“, der als Eingabe für das Terraform-Skript verwendet wird. Da wir das Passwort für den RDS-Dienst nicht in Terraform und Git fest codieren möchten, übergeben wir diese Eigenschaft mit Jenkins-Anmeldeinformationen.
    Erstellen eines Geheimnisses zur Verwendung mit Terraform ci cd
  3. Sie müssen die Anmeldeinformationen und die URL ähnlich wie beim anderen Projekt definieren.
  4. Im Build-Trigger-Abschnitt werden wir dieses Projekt so mit dem anderen verknüpfen, dass dieses Projekt beginnt, wenn das vorherige fertig ist.
    Projekte miteinander verknüpfen
  5. Dann könnten wir die Anmeldeinformationen konfigurieren, die wir zuvor mithilfe von Bindungen zum Projekt hinzugefügt haben, sodass sie im Build-Schritt verfügbar sind.
    Bindungen konfigurieren
  6. Jetzt können wir einen Build-Schritt hinzufügen, der die Terraform-Skriptdateien ( amivar_api.tf und amivar_web.tf ) herunterlädt, die vom vorherigen Projekt auf S3 hochgeladen wurden, und dann den Terraform-Code ausführt, um die gesamte Anwendung auf AWS zu erstellen.
    Hinzufügen des Build-Skripts

Wenn alles richtig konfiguriert ist, sollte jetzt, wenn Sie Code in Ihr Bitbucket-Repository übertragen, das erste Jenkins-Projekt ausgelöst werden, gefolgt vom zweiten, und Sie sollten Ihre Anwendung in AWS bereitgestellt haben.

Terraform Zero Downtime-Konfiguration für AWS

Lassen Sie uns nun besprechen, was im Terraform-Code dafür sorgt, dass diese Pipeline den Code ohne Ausfallzeit bereitstellt.

Das erste ist, dass Terraform diese Lebenszyklus-Konfigurationsblöcke für Ressourcen bereitstellt, in denen Sie eine Option create_before_destroy als Flag haben, was wörtlich bedeutet, dass Terraform eine neue Ressource desselben Typs erstellen soll, bevor die aktuelle Ressource zerstört wird.

Jetzt nutzen wir diese Funktion in den Ressourcen aws_autoscaling_group und aws_launch_configuration . aws_launch_configuration konfiguriert also, welche Art von EC2-Instance bereitgestellt werden soll und wie wir Software auf dieser Instance installieren, und die Ressource aws_autoscaling_group stellt eine AWS-Autoscaling-Gruppe bereit.

Ein interessanter Haken dabei ist, dass alle Ressourcen in Terraform eine eindeutige Kombination aus Name und Typ haben sollten. Wenn Sie also keinen anderen Namen für die neue aws_autoscaling_group und aws_launch_configuration haben, ist es nicht möglich, die aktuelle zu zerstören.

Terraform handhabt diese Einschränkung, indem es der Ressource aws_launch_configuration eine name_prefix Eigenschaft bereitstellt. Sobald diese Eigenschaft definiert ist, fügt Terraform allen aws_launch_configuration Ressourcen ein eindeutiges Suffix hinzu und Sie können diesen eindeutigen Namen verwenden, um eine aws_autoscaling_group -Ressource zu erstellen.

Sie können den Code für alle oben genannten Punkte in terraform/autoscaling-api.tf überprüfen

 resource "aws_launch_configuration" "api-launchconfig" { name_prefix = "api-launchconfig-" image_ instance_type = "t2.micro" security_groups = ["${aws_security_group.api-instance.id}"] user_data = "${data.template_file.api-shell-script.rendered}" iam_instance_profile = "${aws_iam_instance_profile.CloudWatchAgentServerRole-instanceprofile.name}" connection { user = "${var.INSTANCE_USERNAME}" private_key = "${file("${var.PATH_TO_PRIVATE_KEY}")}" } lifecycle { create_before_destroy = true } } resource "aws_autoscaling_group" "api-autoscaling" { name = "${aws_launch_configuration.api-launchconfig.name}-asg" vpc_zone_identifier = ["${aws_subnet.main-public-1.id}"] launch_configuration = "${aws_launch_configuration.api-launchconfig.name}" min_size = 2 max_size = 2 health_check_grace_period = 300 health_check_type = "ELB" load_balancers = ["${aws_elb.api-elb.name}"] force_delete = true lifecycle { create_before_destroy = true } tag { key = "Name" value = "api ec2 instance" propagate_at_launch = true } }

Und die zweite Herausforderung bei Bereitstellungen ohne Ausfallzeit besteht darin, sicherzustellen, dass Ihre neue Bereitstellung bereit ist, mit dem Empfang der Anfrage zu beginnen. In manchen Situationen reicht es nicht aus, einfach eine neue EC2-Instanz bereitzustellen und zu starten.

Um dieses Problem zu lösen, verfügt aws_launch_configuration über eine Eigenschaft user_data , die die native AWS-Autoscaling-Eigenschaft user_data unterstützt, mit der Sie jedes Skript übergeben können, das Sie beim Start neuer Instances als Teil der Autoscaling-Gruppe ausführen möchten. In unserem Beispiel verfolgen wir das Protokoll des App-Servers und warten auf die Startmeldung. Sie können auch den HTTP-Server überprüfen und sehen, wann sie aktiv sind.

 until tail /var/log/syslog | grep 'node ./bin/www' > /dev/null; do sleep 5; done

Darüber hinaus können Sie auch eine ELB-Prüfung auf der Ressourcenebene aws_autoscaling_group , wodurch sichergestellt wird, dass die neu hinzugefügte Instanz die ELB-Prüfung besteht, bevor Terraform die alten Instanzen zerstört. So sieht der ELB-Check für die API-Schicht aus; Es prüft, ob der /api/status Endpunkt Erfolg zurückgibt.

 resource "aws_elb" "api-elb" { name = "api-elb" subnets = ["${aws_subnet.main-public-1.id}"] security_groups = ["${aws_security_group.elb-securitygroup.id}"] listener { instance_port = "${var.API_PORT}" instance_protocol = "http" lb_port = 80 lb_protocol = "http" } health_check { healthy_threshold = 2 unhealthy_threshold = 2 timeout = 3 target = "HTTP:${var.API_PORT}/api/status" interval = 30 } cross_zone_load_balancing = true connection_draining = true connection_draining_timeout = 400 tags { Name = "my-elb" } }

Zusammenfassung und nächste Schritte

Das bringt uns zum Ende dieses Artikels; Hoffentlich haben Sie Ihre Anwendung inzwischen entweder bereits bereitgestellt und mit einer CI/CD-Pipeline ohne Ausfallzeiten ausgeführt, die eine Jenkins-Bereitstellung und bewährte Methoden von Terraform verwendet, oder Sie fühlen sich etwas wohler, wenn Sie dieses Gebiet erkunden und dafür sorgen, dass Ihre Bereitstellungen so wenig manuelle Eingriffe erfordern wie möglich.

In diesem Artikel wird die verwendete Bereitstellungsstrategie als Blau-Grün-Bereitstellung bezeichnet, bei der wir eine aktuelle Installation (Blau) haben, die Live-Datenverkehr empfängt, während wir die neue Version (Grün) bereitstellen und testen, und sie dann ersetzen, sobald die neue Version verfügbar ist alles bereit. Abgesehen von dieser Strategie gibt es noch andere Möglichkeiten, Ihre Anwendung bereitzustellen, was in diesem Artikel, Einführung in Bereitstellungsstrategien, gut erklärt wird. Die Anpassung einer anderen Strategie ist jetzt so einfach wie die Konfiguration Ihrer Jenkins-Pipeline.

Außerdem bin ich in diesem Artikel davon ausgegangen, dass alle neuen Änderungen in API, Web und Datenschichten kompatibel sind, sodass Sie sich keine Sorgen machen müssen, dass die neue Version mit einer älteren Version kommuniziert. Aber in Wirklichkeit ist das vielleicht nicht immer der Fall. Um dieses Problem zu lösen, denken Sie beim Entwerfen Ihrer neuen Version/Features immer an die Abwärtskompatibilitätsebene, sonst müssen Sie Ihre Bereitstellungen optimieren, um auch diese Situation zu bewältigen.

Integrationstests fehlen auch in dieser Bereitstellungspipeline. Da Sie nicht möchten, dass etwas ungetestet für den Endbenutzer freigegeben wird, sollten Sie dies definitiv im Hinterkopf behalten, wenn es an der Zeit ist, diese Strategien auf Ihre eigenen Projekte anzuwenden.

Wenn Sie daran interessiert sind, mehr darüber zu erfahren, wie Terraform funktioniert und wie Sie mithilfe der Technologie auf AWS bereitstellen können, empfehle ich Terraform AWS Cloud: Sane Infrastructure Management , wo Toptaler Radoslaw Szalski Terraform erklärt und Ihnen dann die Schritte zeigt, die zum Konfigurieren eines Multi erforderlich sind -umgebungs- und produktionsreifes Terraform-Setup für ein Team

Siehe auch : Terraform vs. CloudFormation: The Definitive Guide