Es ist kein Geheimnis, dass Entwickler ohne klare und organisierte Prozesse Schwierigkeiten haben können, effektiv zusammenzuarbeiten, was zu Verzögerungen bei der Bereitstellung von Software-Updates führt. Vor einigen Jahren stand das Team der Social Discovery Group vor der Herausforderung eines suboptimalen CI/CD-Prozesses. Damals verwendete das Team TeamCity und Octopus, die jeweils ihre Stärken hatten. Beispielsweise ist Octopus praktisch für Bereitstellungen, während TeamCity gut für automatisierte Tests und ausreichend praktisch für Projekt-Builds ist. Um eine umfassende und optisch ansprechende CI/CD-Pipeline zu erstellen, die maximal praktisch und flexibel in der Konfiguration ist, ist es notwendig, einen Mix aus Tools zu verwenden. Der Code wurde für mehrere Projekte in einem lokalen Repository auf Bitbucket gespeichert. Das SDG-Team untersuchte das Problem und beschloss, den Prozess mithilfe der vorhandenen Tools zu optimieren.
Wichtige Optimierungsziele:
Das SDG-Team entschied sich für TeamCity für Builds und automatisierte Tests und Octopus für Bereitstellungen.
Was in TeamCity implementiert wurde:
TeamCity erlaubt in der kostenlosen Version die Verwendung von drei Agenten, was für das SDG-Team ausreichend war. Sie installierten einen neuen Agenten, fügten ihn dem Pool hinzu und wendeten ihn auf ihre Vorlagen an.
Als die neueste Version von TeamCity zum Einsatz kam, arbeitete das Team mit Ubuntu Server. Der Screenshot zeigt die zusätzlichen Plugins, die das Team verwendete:
Das Deployment für NuGet sah wie folgt aus:
Es ist erwähnenswert, dass je nachdem, ob der Zweig der Master ist oder nicht, der Version „-release“ hinzugefügt wurde (Schritte 3, 4).
Die Bereitstellung für Dienste ist unten zu sehen:
Für jeden Dienst wurden entsprechende Variablen basierend auf Systemvariablen (Dienstname, %build.number% und andere) ersetzt.
Ein Beispiel für den Docker Build-Schritt wird im Screenshot dargestellt:
Jedes Projekt-Repository enthielt die entsprechende Docker-Datei.
Die Unterschiede zwischen den Schritten 4 und 5 waren, wie bereits erwähnt, folgende:
Die Variable %deploymentTarget%
diente als Umgebungsparameter, an den während der Bereitstellung die entsprechenden Phasen in Octopus übergeben wurden (z. B. Test, Dev). Wenn Änderungen an die entsprechenden Zweige (konfiguriert) der Entwicklungsteams übertragen wurden, wurden automatisch Builds und Softwarebereitstellungen in den entsprechenden Testumgebungen durchgeführt. Die Einstellungen sind im folgenden Screenshot sichtbar. Um eine Verbindung mit Octopus herzustellen, mussten zwei globale Parameter hinzugefügt werden: octopus.apiKey und octopus.url
Darüber hinaus hat das SDG-Team im Abschnitt „Verbindungen“ ein gemeinsames NuGet-Repository und Container-Register für alle Projekte verbunden.
Darüber hinaus empfiehlt SDG, E-Mail-Benachrichtigungen im Abschnitt „E-Mail-Benachrichtigung“ zu konfigurieren, Backups im Abschnitt „Backup“ einzurichten, erforderliche Gruppen zu erstellen, entsprechende Rollen zuzuweisen und Benutzer zu den erforderlichen Gruppen hinzuzufügen. Die Haupteinrichtung ist abgeschlossen und abschließend empfiehlt das Team, regelmäßig nach Updates zu suchen und TeamCity einmal im Monat zu aktualisieren.
Als nächstes machte sich das Team der Social Discovery Group an die Konfiguration von Octopus. In diesem Artikel werden die Installationsdetails, die grundlegenden Benutzerrechteeinstellungen und andere Aspekte nicht beschrieben, da Sie diese problemlos selbst vornehmen können. Das Team widmete sich sofort dem Lebenszyklus, der im Abschnitt „Bibliothek“ konfiguriert wird. Im folgenden Screenshot sehen Sie einen Beispielablauf des SDG-Teams:
Anschließend erstellte das Team alle notwendigen Variablengruppen nach Themen in Variablensätzen. Für jede Variable wurden Werte festgelegt und Abhängigkeiten von der Umgebung, den Zielen und den Zielrollen (Tags) hergestellt. Ein Beispiel ist im folgenden Screenshot dargestellt:
Als Ziele dienten die Cluster in Kubernetes, und die Zielrollen waren Tags, die den entsprechenden Clustern oder Computerumgebungen zugeordnet waren. All dies kann im Abschnitt Infrastruktur konfiguriert werden.
Projekte könnten auch gruppiert und ein praktisches Dashboard eingerichtet werden, um die darauf bereitgestellten Dienste, Phasen und Versionen anzuzeigen.
Der Bereitstellungsprozess für SDG sah folgendermaßen aus: Alle Testphasen wurden in einem Schritt zusammengefasst und für sie wurde eine gemeinsame Vorlage erstellt, ebenso für die Bühnen- und Livephasen.
Der folgende Screenshot zeigt, wie dies für das SDG-Team aussah:
Auf der rechten Seite wurde der zuvor beschriebene Lebenszyklus ausgewählt. Die Phase „Paket bereitstellen“ enthielt recht einfache Standardeinstellungen.
Für die Phase „Deploy Raw Kubernetes Yaml“ verwendete das SDG-Team universelle, selbstgeschriebene Yaml-Vorlagen. In diesem Beispiel wird das Kubernetes-Skript weiter unten ausführlicher erläutert. Entsprechende rot markierte Parameter wurden ebenfalls ersetzt. Es ist erwähnenswert, dass die erforderlichen globalen Variablengruppen im Menü Variablen->Variablensätze verbunden wurden und projektspezifische Variablen im Menü Variablen->Projekt festgelegt wurden, das eine höhere Priorität hatte.
In diesem Artikel hat das SDG-Team beschlossen, auf Details wie das Hinzufügen eines Logos zum Projekt, das Einrichten von Triggern oder andere kleinere Details zu verzichten. Konzentrieren wir uns auf zwei wichtige Menüpunkte: 1 - Releases, wo Sie immer die Version und das Erstellungsdatum eines bestimmten Releases sehen können; diese Informationen werden auch auf dem Projekt-Dashboard angezeigt, 2 - Variablen->Vorschau, wo Sie sehen können, welche Variablen für die entsprechende Phase ersetzt werden.
Kommen wir zum wichtigsten Teil – der Bereitstellung von Yaml-Vorlagen in Kubernetes-Clustern. Sie wurden im Abschnitt Bibliothek->Schrittvorlagen erstellt. Unten präsentierte das SDG-Team einen Screenshot mit seinen Parametern. Für jeden Parameter können Sie ein Tag, einen Typ und einen Standardwert auswählen sowie eine Beschreibung hinzufügen, was dringend empfohlen wird.
Der Code sah in diesem Fall wie folgt aus:
apiVersion: apps/v1 kind: Deployment metadata: name: '#{Octopus.Project.Name | ToLower}' namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: replicas: #{Replicas} strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25% revisionHistoryLimit: 10 progressDeadlineSeconds: 600 selector: matchLabels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' template: metadata: labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: volumes: #{if usesidecar} - name: dump-storage persistentVolumeClaim: claimName: dumps-#{Octopus.Environment.Name | ToLower} #{/if} #{if MountFolders} #{each folder in MountFolders} - name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower} hostPath: path: #{folder} type: DirectoryOrCreate #{/each} #{/if} - name: logs-volume hostPath: path: #{LogsDir} type: DirectoryOrCreate - name: appsettings secret: secretName: #{Octopus.Project.Name | ToLower} #{if Secrets} #{each secret in Secrets} - name: #{secret.name} secret: secretName: #{secret.name} #{/each} #{/if} #{if usesidecar} - name: diagnostics emptyDir: {} - name: dumps configMap: name: dumps defaultMode: 511 #{/if} containers: - name: #{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}-container image: #{DockerRegistry}/projectname.#{Octopus.Project.Name | ToLower}:#{Octopus.Release.Notes} #{if resources} resources: #{each resource in resources} #{resource.Key}: #{each entry in resource.Value} #{entry.Key}: #{entry.Value} #{/each} #{/each} #{/if} ports: - name: http containerPort: 80 protocol: TCP env: - value: "Development" name: "ASPNETCORE_ENVIRONMENT" - name: DD_ENV value: "#{Octopus.Environment.Name | ToLower}" - name: DD_SERVICE value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}" - name: DD_VERSION value: "1.0.0" - name: DD_AGENT_HOST value: "#{DatadogAgentHost}" - name: DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED value: "true" - name: DD_RUNTIME_METRICS_ENABLED value: "true" volumeMounts: #{if usesidecar} - name: dump-storage mountPath: /tmp/dumps #{/if} #{if MountFolders} #{each folder in MountFolders} - mountPath: #{folder} name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower} #{/each} #{/if} - mountPath: #{LogsDir} name: logs-volume #{if usesidecar} - name: diagnostics mountPath: /tmp #{/if} - name: appsettings readOnly: true mountPath: /app/appsettings.json subPath: appsettings.json #{if Secrets} #{each secret in Secrets} - name: #{secret.name} readOnly: true mountPath: #{secret.mountPath} subPath: #{secret.subPath} #{/each} #{/if} readinessProbe: httpGet: path: hc port: http scheme: HTTP initialDelaySeconds: #{InitialDelaySeconds} imagePullPolicy: IfNotPresent securityContext: {} #{if usesidecar} - name: sidecar image: '#{DockerRegistry}/monitor:3' command: - /bin/sh args: - '-c' - while true; do . /app/init.sh; sleep 1m;done env: - name: USE_MEMORY value: '2048' - name: PROJECT value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}" resources: {} volumeMounts: - name: diagnostics mountPath: /tmp - name: dump-storage mountPath: /tmp/dumps - name: dumps mountPath: /app/init.sh subPath: init.sh shareProcessNamespace: true #{/if} affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: environment operator: In values: - "#{Node}" --- apiVersion: v1 kind: Service metadata: name: #{Octopus.Project.Name | ToLower} namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: type: ClusterIP selector: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' ports: - name: http port: 80 targetPort: http protocol: TCP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: ingress.kubernetes.io/ssl-redirect: 'false' nginx.ingress.kubernetes.io/ssl-redirect: 'false' cert-manager.io/cluster-issuer: "letsencrypt-cluster-issuer" cert-manager.io/renew-before: '#{LetsencryptRenewBefore}' kubernetes.io/ingress.class: nginx #{if IngressAnnotations} #{each annotation in IngressAnnotations} #{annotation.Key}: #{annotation.Value} #{/each} #{/if} name: #{Octopus.Project.Name | ToLower} namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: tls: #{if ExternalHost} #{each host in ExternalHost} - hosts: - #{host} secretName: #{Octopus.Project.Name | ToLower}-#{host | ToBase64 | Replace "\W" X | ToLower}-tls #{/each} #{/if} rules: #{if ExternalHost} #{each host in ExternalHost} - host: '#{host}' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: #{Octopus.Project.Name | ToLower} port: name: http #{/each} #{/if} #{if usesidecar} --- apiVersion: v1 kind: ConfigMap metadata: name: dumps namespace: #{Octopus.Environment.Name | ToLower} data: init.sh: |- #!/usr/bin/env bash mem=$(ps aux | awk '{print $6}' | sort -rn | head -1) mb=$(($mem/1024)) archiveDumpPath="/tmp/dumps/$PROJECT-$(date +"%Y%m%d%H%M%S").zip" fullPathGc="/tmp/$PROJECT-$(date +"%Y%m%d%H%M%S").dump" echo "mem:" $mb" project:" $PROJECT "use:" $USE_MEMORY if [ "$mb" -gt "$USE_MEMORY" ]; then export USE_MEMORY=$(($USE_MEMORY*2)) pid=$(dotnet-dump ps | awk '{print $1}') dotnet-dump collect -p $pid -o $fullPathGc zip $fullPathGc.zip $fullPathGc mv $fullPathGc.zip $archiveDumpPath rm $fullPathGc fi #{/if}
Alle Variablen in Octopus wurden im Code im folgenden Format angegeben: '#{Octopus.Project.Name | ToLower}'
, wobei der letzte Teil die Konvertierung in Kleinbuchstaben anzeigt.
Die letzte Konfigurationsdatei wurde erstellt, um den Status von .NET-Diensten automatisch zu speichern, wenn sie eine bestimmte Speichernutzungsgrenze erreichten. Dies half erheblich dabei, Speicherlecks während der Entwicklung zu identifizieren und die Dienste umgehend zu reparieren.
Schließlich sah das Service-Dashboard wie folgt aus:
Entwicklungs- und Testteams fanden die Arbeit mit diesem Dashboard sehr praktisch.
Optimierungsergebnisse:
Anschließend implementierte SDG viele weitere Funktionen in Octopus. Beispielsweise das automatische Herunterfahren von Clustern nachts nach einem Zeitplan.
Das Streben nach Perfektion kennt jedoch keine Grenzen. Das Team der Social Discovery Group hat seine Entwicklung durch die Beherrschung von Azure DevOps vorangetrieben. Sie haben einen Prozess in Azure DevOps innerhalb eines Ökosystems auf Helm eingerichtet, der noch umfassender und effizienter ist. Das wird im nächsten Artikel behandelt.
Wir würden gerne von Ihren Erfahrungen beim Einrichten und Optimieren von CI/CD mit Octopus und TeamCity hören. Teilen Sie Ihre Erkenntnisse und Tipps!