
Auf der RIT 2019 hat unser Kollege Alexander Korotkov über die Automatisierung der Entwicklung bei CIAN gesprochen: Um das Leben und die Arbeit zu erleichtern, nutzen wir unsere eigene Plattform Integro. Sie verfolgt den Lebenszyklus von Aufgaben, befreit Entwickler von Routineoperationen und reduziert signifikant die Anzahl der Bugs in der Produktion. In diesem Beitrag werden wir Alexanders Vortrag ergänzen und erzählen, wie wir den Weg von einfachen Skripten zur Integration von Open-Source-Produkten über unsere eigene Plattform gegangen sind und womit unser separates Automatisierungsteam beschäftigt ist.
Nullniveau
«Es gibt kein Nullniveau, ich kenne so etwas nicht»
Meister Shifu aus dem Film «Kung-Fu Panda»
Die Automatisierung bei CIAN begann 14 Jahre nach der Gründung des Unternehmens. Damals bestand das Entwicklungsteam aus 35 Personen. Es ist schwer zu glauben, oder? Natürlich gab es in gewisser Weise schon Automation, aber ein separates Konzept für kontinuierliche Integration und Code-Auslieferung begann erst 2015 Gestalt anzunehmen.
Zu diesem Zeitpunkt hatten wir ein riesiges Monolith aus Python, C# und PHP, das auf Linux-/Windows-Servern bereitgestellt war. Für die Bereitstellung dieses Monsters hatten wir eine Sammlung von Skripten, die wir manuell ausführten. Es gab auch die Zusammenstellung des Monolithen, die Schmerz und Leiden durch Konflikte beim Zusammentragen von Branches, Fehlerkorrekturen und das erneute Zusammenstellen „mit einem anderen Aufgabenpaket im Build“ bereitete. Vereinfacht sah der Prozess so aus:

Das genügte uns nicht, und wir wollten einen wiederholbaren, automatisierten und steuerbaren Prozess für das Bauen und Bereitstellen aufbauen. Dafür benötigten wir ein CI/CD-System und wählten zwischen der kostenlosen Version von Teamcity und Jenkins, da wir mit beiden gearbeitet hatten und beide uns hinsichtlich der Funktionalitäten zusagten. Wir entschieden uns für Teamcity als das neuere Produkt. Zu diesem Zeitpunkt verwendeten wir noch keine Microservices-Architektur und rechneten nicht mit vielen Aufgaben und Projekten.
Wir kamen zu der Idee eines eigenen Systems
Die Einführung von Teamcity hat nur einen Teil der manuellen Arbeit beseitigt: Es blieb noch die Erstellung von Pull Requests, die Fortschreibung von Aufgaben in Jira und die Auswahl von Aufgaben für die Veröffentlichung. Mit dieser Aufgabe konnte das Teamcity-System nicht mehr umgehen. Es musste ein Weg zur weiteren Automatisierung gefunden werden. Wir haben die Optionen für die Arbeit mit Scripts in Teamcity oder den Umstieg auf externe Automatisierungssysteme geprüft. Letztendlich haben wir entschieden, dass wir die maximale Flexibilität benötigen, die nur eine eigene Lösung bieten kann. So entstand die erste Version des internen Automatisierungssystems namens Integro.
Teamcity kümmert sich um die Automatisierung auf der Ebene des Starts von Build- und Deploy-Prozessen, während Integro sich auf die hochgradige Automatisierung von Entwicklungsprozessen konzentriert hat. Es war nötig, die Arbeit mit den Aufgaben in Jira mit der Verarbeitung des zugehörigen Quellcodes in Bitbucket zu verbinden. In dieser Phase begannen innerhalb von Integro eigene Workflows für die Bearbeitung von Aufgaben verschiedener Typen zu entstehen.
Durch die zunehmende Automatisierung der Geschäftsprozesse erhöhte sich die Anzahl der Projekte und Runs in Teamcity. So trat ein neues Problem auf: Ein kostenloser Teamcity-Instanz reichte nicht mehr aus (3 Agenten und 100 Projekte), wir haben eine weitere Instanz hinzugefügt (noch 3 Agenten und 100 Projekte), und dann noch eine. Letztendlich hatten wir ein System aus mehreren Clustern, das schwer zu verwalten war:

Als die Frage nach einer vierten Instanz aufkam, wurde uns klar, dass es so nicht weitergehen konnte, da die Gesamtkosten für die Unterstützung von vier Instanzen schon nicht mehr tragbar waren. Es stellte sich die Frage, ob wir Teamcity kaufen oder uns für die kostenlose Jenkins entscheiden sollten. Wir haben die Berechnungen für die Instanzen und die Automatisierungspläne durchgeführt und entschieden, dass wir mit Jenkins leben werden. Nach ein paar Wochen sind wir auf Jenkins umgestiegen und haben einen Teil der Kopfschmerzen, die mit der Unterstützung mehrerer Teamcity-Instanzen verbunden waren, losgeworden. Daher konnten wir uns auf die Entwicklung von Integro und die Anpassung von Jenkins konzentrieren.
Mit dem Anstieg der grundlegenden Automatisierung (in Form der automatischen Erstellung von Pull Requests, der Erfassung und Veröffentlichung der Codeabdeckung und anderer Überprüfungen) gab es den nachhaltigen Wunsch, manuelle Releases möglichst abzulehnen und diese Arbeit den Robotern zu überlassen. Darüber hinaus begann innerhalb des Unternehmens der Umstieg auf Mikrodienste, die häufige Releases erforderten, und zwar getrennt voneinander. So kamen wir schrittweise zu automatisierten Releases unserer Mikrodienste (das Monolith wird aufgrund der Komplexität des Prozesses bisher manuell veröffentlicht). Doch wie das häufig der Fall ist, trat eine neue Komplikation auf.
Automatisieren wir das Testen

Durch die Automatisierung der Releases haben sich die Entwicklungsprozesse beschleunigt, teilweise auch auf Kosten des Überspringens bestimmter Testphasen. Das führte zu einem vorübergehenden Qualitätsverlust. Es klingt banal, aber zusammen mit der Beschleunigung der Releases musste auch die Methodik der Produktentwicklung geändert werden. Wir mussten über die Automatisierung von Tests nachdenken, die persönliche Verantwortung (hier geht es um das „Verstehen der Idee im Kopf“, nicht um Geldstrafen) der Entwickler für den Code und die Fehler darin, sowie um die Entscheidung über das Release/nicht Release der Aufgaben durch automatisiertes Deployment.
Um die Qualitätsprobleme zu beheben, haben wir zwei wichtige Entscheidungen getroffen: Wir haben Canaries-Tests eingeführt und die automatische Überwachung von Fehlern mit automatisierter Reaktion auf deren Überschreitung implementiert. Die erste Entscheidung ermöglichte es, offensichtliche Fehler zu erkennen, bevor der Code vollständig in die Produktion gelangte, die zweite reduzierte die Reaktionszeit auf Probleme in der Produktion. Fehler gibt es natürlich, aber wir verbringen den Großteil unserer Zeit und Energie nicht mit der Behebung, sondern mit der Minimierung.
Das Automatisierungsteam
Aktuell haben wir ein Team von 130 Entwicklern, und wir wachsen weiterhin . Das Team für kontinuierliche Integration und Bereitstellung von Code (im Folgenden als Deploy and Integration Team oder DI bezeichnet) besteht aus 7 Personen und arbeitet in 2 Richtungen: Entwicklung der Automatisierungsplattform Integro und DevOps.
DevOps ist verantwortlich für die Dev/Beta-Umgebungen der CIAN-Website, die Integro-Umgebungen und unterstützt die Entwickler bei der Lösung von Problemen und der Entwicklung neuer Ansätze zur Skalierung der Umgebungen. Der Entwicklungsbereich für Integro kümmert sich sowohl um Integro selbst als auch um verwandte Dienste, wie Plugins für Jenkins, Jira, Confluence, und entwickelt außerdem unterstützende Tools und Anwendungen für Entwicklerteams.
Das DI-Team arbeitet eng mit dem Plattformteam zusammen, das sich mit der Entwicklung der Architektur, Bibliotheken und Entwicklungsansätzen innerhalb des Unternehmens beschäftigt. Darüber hinaus kann jeder Entwickler innerhalb von CIAN zu Automatisierungen beitragen, zum Beispiel Mikroautomatisierungen für die Bedürfnisse des Teams erstellen oder eine großartige Idee teilen, wie man die Automatisierung noch besser gestalten kann.
Schichtkuchen der Automatisierung bei CIAN

Alle Systeme, die an der Automatisierung beteiligt sind, können in mehrere Schichten unterteilt werden:
- Externe Systeme (Jira, Bitbucket usw.). Diese werden von den Entwicklungsteams verwendet.
- Die Integro-Plattform. Häufig arbeiten die Entwickler nicht direkt damit, aber sie unterstützt die gesamte Automatisierungsarbeit.
- Liefer-, Orchestrierungs- und Entdeckungsdienste (zum Beispiel Jenkins, Consul, Nomad). Mit deren Hilfe setzen wir den Code auf Servern um und sorgen dafür, dass die Dienste miteinander funktionieren.
- Physikalischer Ebene (Server, Betriebssysteme, verwandte Software). Auf dieser Ebene läuft unser Code. Dies kann sowohl ein physischer Server als auch ein virtueller (LXC, KVM, Docker) sein.
Basierend auf diesem Konzept teilen wir die Zuständigkeiten innerhalb des DI-Teams. Die ersten beiden Ebenen liegen im Verantwortungsbereich der Integro-Entwicklung, während die letzten beiden Ebenen im Zuständigkeitsbereich von DevOps liegen. Diese Aufteilung ermöglicht es, sich auf die Aufgaben zu konzentrieren und stört nicht die Interaktion, da wir nah beieinander sind und ständig Wissen und Erfahrungen austauschen.
Integro
Konzentrieren wir uns auf Integro und beginnen wir mit dem Technologiestack:
- CentOs 7
- Docker + Nomad + Consul + Vault
- Java 11 (das alte Monolith Integro bleibt auf Java 8)
- Spring Boot 2.X + Spring Cloud Config
- PostgreSql 11
- RabbitMQ
- Apache Ignite
- Camunda (embedded)
- Grafana + Graphite + Prometheus + Jaeger + ELK
- Web UI: React (CSR) + MobX
- SSO: Keycloak
Wir folgen dem Prinzip der Mikrodienstentwicklung, obwohl wir auch ein Legacy-System in Form des Monolithen der frühen Version von Integro haben. Jeder Mikrodienst läuft in seinem eigenen Docker-Container, die Dienste kommunizieren über HTTP-Anfragen und RabbitMQ-Nachrichten. Mikrodienste finden sich über Consul und stellen eine Anfrage, wobei sie sich über SSO (Keycloak, OAuth 2/OpenID Connect) authentifizieren.

Als konkretes Beispiel betrachten wir die Interaktion mit Jenkins, die aus den folgenden Schritten besteht:
- Der Mikrodienst für die Workflow-Verwaltung (im Folgenden Flow-Mikrodienst) möchte einen Build in Jenkins starten. Dazu findet er über Consul die IP:PORT des Mikrodienstes zur Integration mit Jenkins (im Folgenden Jenkins-Mikrodienst) und sendet eine asynchrone Anfrage zum Start eines Builds in Jenkins.
- Der Jenkins-Mikrodienst formt nach Erhalt der Anfrage eine Job-ID und gibt diese als Antwort zurück, mit der später das Ergebnis identifiziert werden kann. Gleichzeitig startet er den Build in Jenkins über einen Aufruf der REST API.
- Jenkins führt den Build aus und sendet nach Abschluss einen Webhook mit den Ergebnissen der Ausführung an den Jenkins-Mikrodienst.
- Der Jenkins-Mikrodienst, der den Webhook erhält, erstellt eine Nachricht über den Abschluss der Anfrageverarbeitung und fügt die Ergebnisse der Ausführung hinzu. Die erstellte Nachricht wird in die RabbitMQ-Warteschlange gesendet.
- Über RabbitMQ gelangt die veröffentlichte Nachricht zum Flow-Mikrodienst, der so über das Ergebnis der Verarbeitung seiner Aufgabe informiert wird, indem er die Job-ID aus der Anfrage mit der erhaltenen Nachricht vergleicht.
Momentan haben wir etwa 30 Mikrodienste, die in mehrere Gruppen unterteilt werden können:
- Konfigurationsmanagement.
- Benachrichtigung und Interaktion mit Benutzern (Messenger, E-Mail).
- Arbeit mit Quellcode.
- Integration mit Bereitstellungstools (Jenkins, Nomad, Consul usw.).
- Überwachung (von Releases, Fehlern usw.).
- Web-Utilities (UI zur Verwaltung von Testumgebungen, Sammlung von Statistiken usw.).
- Integration mit Aufgabenverfolgungssystemen und ähnlichen Systemen.
- Workflow-Management für verschiedene Aufgaben.
Workflow der Aufgabe
Integro automatisiert Aktionen im Zusammenhang mit dem Lebenszyklus einer Aufgabe. Vereinfacht gesagt, verstehen wir unter dem Lebenszyklus einer Aufgabe den Workflow der Aufgabe in Jira. In unseren Entwicklungsprozessen gibt es mehrere Varianten von Workflows, abhängig vom Projekt, vom Aufgabentyp und von den in der jeweiligen Aufgabe ausgewählten Optionen.
Betrachten wir den Workflow, den wir am häufigsten verwenden:

In der Abbildung deutet das Zahnrad darauf hin, dass der Übergang automatisch von Integro ausgelöst wird, während die Figur eines Menschen bedeutet, dass der Übergang manuell von einer Person initiiert wird. Lassen Sie uns mehrere Pfade betrachten, auf denen die Aufgabe durch diesen Workflow gehen kann.
Vollständige manuelle Tests auf DEV+BETA ohne Canary-Tests (normalerweise so veröffentlichen wir das Monolith):

Es können auch andere Kombinationen des Übergangs existieren. Manchmal kann der Weg, den die Aufgabe nimmt, über Optionen in Jira ausgewählt werden.
Bewegung der Aufgabe
Betrachten wir die Hauptschritte, die bei der Bewegung einer Aufgabe im Workflow ‚Testen auf DEV + Canary-Tests‘ durchgeführt werden:
1. Der Entwickler oder PM erstellt die Aufgabe.
2. Der Entwickler nimmt die Aufgabe an. Nach Abschluss wird sie in den Status IN REVIEW versetzt.
3. Jira sendet ein Webhook an den Jira-Mikroservice (verantwortlich für die Integration mit Jira).
4. Der Jira-Mikroservice sendet eine Anfrage an den Flow-Service (verantwortlich für interne Workflows, in denen die Arbeit ausgeführt wird), um den Workflow zu starten.
5. Im Flow-Service:
- Es werden Reviewer für die Aufgabe zugewiesen (Users-Mikroservice, der alles über die Benutzer weiß + Jira-Mikroservice).
- Über den Source-Mikroservice (weiß über Repositories und Branches Bescheid, arbeitet aber nicht mit dem Code selbst) wird nach Repositories gesucht, die einen Branch mit unserer Aufgabe enthalten (zur Vereinfachung des Suchens hat der Branch den gleichen Namen wie die Aufgabennummer in Jira). Am häufigsten hat eine Aufgabe nur einen Branch in einem Repository, was das Management der Deployment-Warteschlange vereinfacht und die Kopplung zwischen den Repositories verringert.
- Für jeden gefundenen Branch wird folgende Aktionsequenz ausgeführt:
i) Einfügen des Master-Branches (Git-Mikroservice zur Arbeit mit Code).
ii) Der Branch wird vom Entwickler (Bitbucket-Mikroservice) vor Änderungen gesperrt.
iii) Ein Pull Request für diesen Branch wird erstellt (Bitbucket-Mikroservice).
iv) Eine Nachricht über den neuen Pull Request wird in den Entwickler-Chats gesendet (Notify-Mikroservice für Benachrichtigungen).
v) Der Build, die Tests und das Deployment der Aufgabe auf DEV werden gestartet (Jenkins-Mikroservice für die Arbeit mit Jenkins).
vi) Wenn alle vorherigen Punkte erfolgreich abgeschlossen wurden, fügt Integro seine Genehmigung im Pull Request hinzu (Bitbucket-Mikroservice). - Integro wartet auf die Genehmigung im Pull Request von den zugewiesenen Reviewern.
- Sobald alle erforderlichen Genehmigungen erhalten werden (einschließlich positiv bestandener automatisierter Tests), versetzt Integro die Aufgabe in den Status Test auf Dev (Jira-Mikroservice).
6. Tester führen die Testung der Aufgabe durch. Wenn es keine Probleme gibt, wird die Aufgabe in den Status Bereit für Build übertragen.
7. Integro „sieht“, dass die Aufgabe bereit für die Veröffentlichung ist und startet ihr Deployment im Canary-Modus (Jenkins-Microservice). Die Bereitstellung zur Veröffentlichung wird durch eine Reihe von Regeln bestimmt. Zum Beispiel muss die Aufgabe im richtigen Status sein, es dürfen keine Blockierungen auf andere Aufgaben existieren, es gibt derzeit keine aktiven Deployments dieses Microservices usw.
8. Die Aufgabe wird in den Status Canary (Jira-Microservice) überführt.
9. Jenkins startet über Nomad das Deployment der Aufgabe im Canary-Modus (in der Regel 1-3 Instanzen) und benachrichtigt den Release-Monitoring-Service (DeployWatch-Microservice) über das Deployment.
10. Der DeployWatch-Microservice sammelt den Fehlerhintergrund und reagiert darauf, falls nötig. Bei Überschreitung des Fehlerhintergrunds (die Norm wird automatisch berechnet) erfolgt eine Benachrichtigung der Entwickler über den Notify-Microservice. Wenn der Entwickler nach 5 Minuten nicht reagiert hat (Revert oder Stay gedrückt hat), wird ein automatischer Rollback der Canary-Instanzen gestartet. Wenn der Hintergrund nicht überschritten wird, muss der Entwickler manuell das Deployment der Aufgabe in die Produktion starten (durch Knopfdruck im UI). Wenn der Entwickler innerhalb von 60 Minuten das Deployment in die Produktion nicht gestartet hat, werden aus Sicherheitsgründen auch die Canary-Instanzen zurückgefahren.
11. Nach dem Start des Deployments in die Produktion:
- Die Aufgabe wird in den Status Produktion (Jira-Microservice) überführt.
- Der Jenkins-Microservice startet den Deployment-Prozess und benachrichtigt den DeployWatch-Microservice über das Deployment.
- Der DeployWatch-Microservice überprüft, dass alle Container in der Produktion aktualisiert wurden (es gab Fälle, in denen nicht alle aktualisiert wurden).
- Eine Benachrichtigung über die Ergebnisse des Deployments in die Produktion wird über den Notify-Microservice versendet.
12. Die Entwickler haben 30 Minuten Zeit, um das Rollback der Aufgabe aus der Produktion im Falle unerwünschten Verhaltens des Microservices zu starten. Nach Ablauf dieser Zeit wird die Aufgabe automatisch in den Master integriert (Git-Microservice).
13. Nach dem erfolgreichen Merge in den Master wird der Status der Aufgabe auf Geschlossen (Jira-Microservice) geändert.
Das Schema erhebt nicht den Anspruch auf Vollständigkeit (in der Realität gibt es noch mehr Schritte), ermöglicht jedoch eine Bewertung des Integrationsgrads in die Prozesse. Wir betrachten dieses Schema nicht als ideal und verbessern die Prozesse der automatischen Bereitstellung von Releases und Deployments.
Was kommt als nächstes
Wir haben große Pläne zur Entwicklung der Automatisierung, zum Beispiel den Verzicht auf manuelle Operationen bei der Veröffentlichung des Monolithen, die Verbesserung der Überwachung bei automatischen Deployments und die Optimierung der Zusammenarbeit mit den Entwicklern.
Doch an dieser Stelle halten wir zunächst inne. Viele Themen in der Übersicht zur Automatisierung haben wir nur oberflächlich behandelt, einige gar nicht erwähnt, deshalb freuen wir uns darauf, Fragen zu beantworten. Wir warten auf Vorschläge, was wir detailliert beleuchten sollen, schreiben Sie uns in die Kommentare.
Quelle: habr.com
