Wir haben DevOps so gut wir konnten entwickelt. Wir waren 8 Personen, und Vasja war der Beste bei Windows. Plötzlich ging Vasja, und ich bekam die Aufgabe, ein neues Projekt zu starten, das Windows-Entwicklung liefert. Als ich den gesamten Windows-Entwicklungsstack auf den Tisch ausbreitete, wurde mir klar, dass die Situation schmerzhaft war…
So beginnt die Geschichte Alexander Sinchinov auf . Als der führende Windows-Spezialist die Firma verließ, fragte sich Alexander, was er nun tun sollte. Natürlich auf Linux umsteigen! Alexander wird erzählen, wie es ihm gelang, einen Präzedenzfall zu schaffen und einen Teil der Windows-Entwicklung auf Linux am Beispiel eines Projekts mit 100.000 Endbenutzern zu migrieren.

Wie kann man ein Projekt einfach und entspannt in RPM ausliefern, indem man TFS, Puppet und Linux .NET Core verwendet? Wie kann man die Versionierung der Projekt-Datenbank unterstützen, wenn die Entwickler zum ersten Mal von Postgres und Flyway hören und die Deadline übermorgen ist? Wie integriert man mit Docker? Wie motiviert man .NET-Entwickler, auf Windows und Smoothies zu verzichten zugunsten von Puppet und Linux? Wie löst man ideologische Konflikte, wenn man Windows im Produktionsbetrieb nicht mehr unterstützen möchte? Darüber, sowie über Web Deploy, Testing, CI, über die Praktiken der Nutzung von TFS in bestehenden Projekten und natürlich über kaputte Krücken und funktionierende Lösungen wird Alexander in seiner Präsentation sprechen.

Also, Vasja ist gegangen, die Aufgabe liegt nun bei mir, die Entwickler warten ungeduldig mit Forken. Als ich endgültig realisierte, dass ich Vasja nicht zurückbringen kann, machte ich mich an die Arbeit. Zuerst bewertete ich den Anteil der Windows-VMs in unserem Portfolio. Die Bilanz war nicht zu Gunsten von Windows.

Da wir DevOps aktiv entwickeln, wurde mir klar, dass sich etwas in der Herangehensweise an die Einführung einer neuen Anwendung ändern muss. Es gab nur eine Lösung – wenn möglich, alles auf Linux zu portieren. Google half mir – zu diesem Zeitpunkt war .NET bereits für Linux portiert, und ich begriff, dass dies die Lösung ist!
Warum .NET Core in Verbindung mit Linux?
Es gab mehrere Gründe dafür. Zwischen „Geld ausgeben“ und „kein Geld ausgeben“ würden die meisten die zweite Option wählen – so wie ich. Eine Lizenz für MSDB kostet etwa 1.000 $. Die Wartung eines Parks von Windows-VMs beläuft sich auf Hunderte Dollar. Für ein großes Unternehmen sind das erhebliche Kosten. Daher Einsparungen — der erste Grund. Nicht der wichtigste, aber einer der gewichtigen.
Windows-VMs benötigen mehr Ressourcen als ihre Linux-Pendants – sie sind schwerfällig. Angesichts des Umfangs eines großen Unternehmens haben wir Linux ausgewählt.
Das System lässt sich einfach in die bestehende CI integrieren. Wir betrachten uns als fortschrittliche DevOps-Profis, nutzen Bamboo, Jenkins und GitLab CI, daher läuft der Großteil bei uns auf Linux.
Der letzte Grund ist die bequeme Wartung. Wir mussten die Einstiegshürde für die „Wartenden“ senken – die Jungs, die die technische Seite verstehen, für die Betriebssicherheit sorgen und die Dienste der zweiten Linie betreuen. Sie waren bereits mit dem Linux-Stack vertraut, daher fiel es ihnen viel einfacher, das neue Produkt zu verstehen, zu unterstützen und zu warten, als zusätzliche Ressourcen aufzuwenden, um ähnliche Funktionen von Software für die Windows-Plattform zu verstehen.
Anforderungen
Das erste und wichtigste ist der Komfort der neuen Lösung für die Entwickler. Nicht alle von ihnen waren bereit für Veränderungen, besonders nach dem Wort Linux. Die Entwickler wollen ihr geliebtes Visual Studio, TFS mit automatisierten Tests für die Builds und Smoothies. Wie die Lieferung in die Produktion erfolgt – es interessiert sie nicht. Deshalb haben wir beschlossen, den gewohnten Prozess nicht zu ändern und alles für die Windows-Entwicklung unverändert zu lassen.
Das neue Projekt muss in die bestehende CI integriert werden. Die Rails waren bereits vorhanden, und die gesamte Arbeit musste unter Berücksichtigung der Parameter des Configuration Management Systems, der angenommenen Bereitstellungsstandards und der Überwachungssysteme durchgeführt werden.
Die Einfachheit in Wartung und Betrieb, als Bedingung für die minimale Einstiegshürde für alle neuen Teilnehmer aus verschiedenen Abteilungen und dem Wartungsservice.
Die Frist war gestern.
Die Windows-Entwicklungsgruppe
Woran hat das Windows-Team dann gearbeitet?

Jetzt kann ich mit Zuversicht sagen, dass IdentityServer4 eine großartige kostenlose Alternative zu ADFS mit ähnlichen Möglichkeiten ist, oder dass Entity Framework Core ein Paradies für Entwickler ist, wo man sich nicht um das Schreiben von SQL-Skripten kümmern muss, sondern Abfragen in der Datenbank in objektorientierten Begriffen beschreibt. Aber damals, bei der Diskussion des Aktionsplans, sah ich diesen Stack wie sumerische Keilschrift an, wobei ich nur PostgreSQL und Git kannte.
Zu diesem Zeitpunkt verwendeten wir aktiv Puppet als Konfigurationsmanagementsystem. In den meisten unserer Projekte setzten wir GitLab CI, Elastic, balancierten stark ausgelastete Dienste mit Hilfe von HAProxy, überwachten alles mit Hilfe von Zabbix, einer Kombination mit Grafana und Prometheus, Jaeger, und das Ganze lief auf Hardware von HP c ESXi auf VMware. Allen bekannt – Klassiker der Branche.

Lass uns sehen und versuchen zu verstehen, was passiert ist, bevor wir all diese Eingriffe vorgenommen haben.
Was war
TFS ist ein ziemlich mächtiges System, das nicht nur den Code vom Entwickler zur Produktionsmaschine bringt, sondern auch ein Set für eine sehr flexible Integration mit verschiedenen Dienstleistungen bietet – um CI auf plattformübergreifendem Niveau zu gewährleisten.

Früher war es eine ganze Reihe von Fenstern. TFS verwendete mehrere Build-Agenten, auf denen viele Projekte gebaut wurden. Jeder Agent hatte 3-4 Worker, um Aufgaben parallel zu bearbeiten und den Prozess zu optimieren. Danach lieferte TFS gemäß den Release-Plänen den frisch gebackenen Build auf dem Windows-Anwendungsserver aus.
Wohin wollten wir gelangen
Für die Bereitstellung und Entwicklung verwenden wir TFS, und die Anwendung läuft auf einem Linux-Anwendungsserver, dazwischen liegt eine Art Magie. Diese Magic Box ist die Essenz der bevorstehenden Arbeit. Bevor ich es in seine Teile zerlege, mache ich einen Schritt zur Seite und sage zwei Worte über die Anwendung.
Projekt
Die Anwendung bietet Funktionalität für die Verwaltung von prepaid Karten.

Client
Es gab zwei Arten von Benutzern. Erster erhielt Zugang, indem er sich mit einem SSL-Zertifikat SHA-2 authentifizierte. Der zweiten hatte Zugang über Benutzername und Passwort.
HAProxy
Dann gelangte die Kundenanfrage zu HAProxy, das folgende Aufgaben löste:
- Primärautorisierung;
- SSL-Terminierung;
- Tuning von HTTP-Anfragen;
- Anfragenweiterleitung.
Die Überprüfung des Kunden-Zertifikats erfolgte über die Kette. Wir sind Behörde und können uns das erlauben, da wir selbst die Zertifikate an die Kunden des Dienstes ausstellen.
Bitte beachten Sie den dritten Punkt, darauf kommen wir später zurück.
Backend
Das Backend sollte auf Linux entwickelt werden. Das Backend interagiert mit der Datenbank, lädt die erforderliche Liste von Berechtigungen und stellt dann, abhängig von den Berechtigungen des authentifizierten Benutzers, den Zugang zum Unterzeichnen von Finanzdokumenten und deren Einreichung zur Ausführung oder zur Generierung eines Berichts zur Verfügung.
Einsparungen mit HAProxy
Neben den zwei Kontexten, durch die jeder der Clients agierte, gab es auch den Kontext Identity. IdentityServer4 der genau die Authentifizierung ermöglicht, ist ein kostenloses und leistungsstarkes Pendant zu ADFS — Active Directory Federation Services.
Die Anfrage an Identity wurde in mehreren Schritten bearbeitet. Der erste Schritt – Client gelangte ins Backend, der mit diesem Server Daten austauschte und überprüfte, ob ein Token für den Client vorhanden war. Wenn er es nicht fand, wurde die Anfrage zurück an den Kontext geschickt, von dem sie gekommen war, jedoch bereits mit Redirect, und der Redirect ging auf identity.
Der zweite Schritt – die Anfrage landete auf der Anmeldeseite im IdentityServer, wo der Client sich registrierte, und in der Datenbank des IdentityServers erschien das lang ersehnte Token.
Der dritte Schritt – der Client wurde zurückgeleitet zum Kontext, von dem er gekommen war.

IdentityServer4 hat eine Besonderheit: die Antwort auf die Rückanfrage wird über HTTP zurückgegeben. So sehr wir auch mit der Serverkonfiguration kämpften, wie sehr wir uns auch mit der Dokumentation beschäftigten, jedes Mal erhielten wir die ursprüngliche Anfrage des Clients mit der URL, die über HTTPS kam, während der IdentityServer denselben Kontext zurückgab, jedoch mit HTTP. Wir waren schockiert! Und wir haben all das über den Identity-Kontext auf HAProxy weitergeleitet, und in den Headern mussten wir das Protokoll von HTTP auf HTTPS ändern.
Was ist die Verbesserung und wo haben wir gespart?
Wir haben Geld gespart, indem wir eine kostenlose Lösung für die Autorisierung einer Benutzergruppe verwendet haben, Ressourcen, da wir den IdentityServer4 nicht als separaten Knoten in ein separates Segment ausgegliedert haben, sondern ihn gemeinsam mit dem Backend auf demselben Server genutzt haben, auf dem das Backend der Anwendung läuft.
So sollte es funktionieren
Also, wie ich versprochen habe – der Magic Box. Wir verstehen bereits, dass wir garantiert in Richtung Linux gehen. Lassen Sie uns konkrete Aufgaben formulieren, die gelöst werden mussten.

Manifest von Puppet. Um die Konfiguration des Dienstes und der Anwendung bereitzustellen und zu verwalten, mussten großartige Rezepte geschrieben werden. Der kleine Zettel mit dem Bleistift zeigt eindrucksvoll, wie schnell und qualitativ das umgesetzt wurde.
Liefermethode. Der Standard ist RPM. Alle verstehen, dass man in Linux nicht ohne ihn auskommt, aber das Projekt bestand nach der Erstellung aus einem Satz ausführbarer DLL-Dateien. Es waren etwa 150, das Projekt ist recht schwer. Die einzige harmonische Lösung bestand darin, dieses Binärzeug in RPM zu verpacken und die Anwendung daraus zu entpacken.
Versionierung. Wir mussten sehr häufig veröffentlichen und es war notwendig, eine Methode zur Bildung des Paketnamens zu finden. Dies ist eine Frage der Integration mit TFS. Unser Build-Agent lief unter Linux. Wenn TFS eine Aufgabe an den Bearbeiter – Worker – auf dem Build-Agent sendet, übergibt er auch eine Sammlung von Variablen, die in die Umgebung des Prozesses des Bearbeiters gelangen. In diesen Umgebungsvariablen wird der Name des Builds, die Version und andere Variablen übergeben. Nähere Informationen dazu im Abschnitt „RPM-Paketbau“.
TFS-Einstellungen beschränkten sich auf die Konfiguration der Pipeline. Früher haben wir alle Windows-Projekte auf Windows-Agenten gebaut, jetzt haben wir einen Linux-Agenten – Build-Agent, den wir in die Build-Gruppe einfügen, mit Artefakten anreichern, angeben müssen, welche Art von Projekten auf diesem Build-Agent gebaut werden sollen, und die Pipeline irgendwie modifizieren müssen.
IdentityServer. ADFS ist nicht unser Weg, wir setzen auf Open Source.
Lass uns die Komponenten durchgehen.
Magic Box
Besteht aus vier Teilen.

Linux Build-Agent. Linux, weil wir dafür bauen – logisch. Dieser Teil wurde in drei Schritten durchgeführt.
- Worker konfigurieren und nicht nur einen, da eine verteilte Arbeit am Projekt vorgesehen war.
- Installiere .NET Core 1.x. Warum gerade 1.x, wenn bereits 2.0 im Standardrepository verfügbar ist? Weil zur Zeit des Anfangs der Entwicklung die stabile Version 1.09 war und beschlossen wurde, das Projekt darauf aufzubauen.
- Git 2.x.
RPM-Repository. RPM-Pakete mussten irgendwo gespeichert werden. Es wurde angenommen, dass wir dasselbe Unternehmens-RPM-Repository verwenden, das für alle Linux-Hosts zugänglich ist. So haben wir es auch gemacht. Auf dem Repository-Server ist konfiguriert webhook das die benötigten RPM-Pakete aus dem angegebenen Ort herunterlädt. Die Versionsnummer des Pakets wurde dem Webhook vom Build-Agent mitgeteilt.
GitLab betroffen hat. Achtung! GitLab wird hier nicht von Entwicklern, sondern von der Betriebsabteilung zur Versionskontrolle der Anwendung, der Paketversionen, zur Überwachung des Status aller Linux-Maschinen verwendet, und darin wird das Rezept aufbewahrt – alle Puppet-Manifestdateien.
Puppet – klärt alle strittigen Punkte und liefert genau die Konfiguration, die wir aus GitLab möchten.
Lass uns eintauchen. Wie erfolgt die Lieferung der DLL in RPM?
Lieferung von DLL in RPM
Angenommen, wir haben einen Rock-Star in der .NET-Entwicklung. Er verwendet Visual Studio und erstellt einen Release-Zweig. Danach lädt er ihn in Git hoch, und Git ist hier eine TFS-Entität, das heißt, es ist das Repository der Anwendung, mit dem der Entwickler arbeitet.

Nachdem TFS sieht, dass ein neuer Commit eingegangen ist. Welche Anwendung? In den TFS-Einstellungen gibt es ein Tag, welche Ressourcen der jeweilige Build-Agent hat. In diesem Fall sieht er, dass wir ein .NET Core-Projekt erstellen und wählt den Linux-Build-Agent aus dem Pool aus.
Der Build-Agent erhält die Quellcodes, lädt die notwendigen dependencies aus dem .NET-Repository, npm usw. herunter, und nach dem Erstellen der Anwendung und der anschließenden Verpackung sendet er das RPM-Paket an das RPM-Repository.
Auf der anderen Seite passiert Folgendes. Der Ingenieur der Betriebstechnik beschäftigt sich direkt mit dem Rollout des Projekts: Er ändert die Paketversionen in Hiera im Repository, wo die Rezeptur der Anwendung gespeichert ist, woraufhin Puppet Yumauslöst, das neue Paket aus dem Repository abruft und die neue Version der Anwendung zur Verwendung bereit ist.

Auf Worte klingt alles einfach, aber was passiert wirklich im Build-Agent?
Verpackung der DLL RPM
Die Quellcodes des Projekts sind erhalten und die Aufgabe zur Erstellung kommt von TFS. Der Build-Agent startet den Build des Projekts aus den Quellcodes.Das gebaute Projekt steht in Form einer Vielzahl von DLL-Dateien, die in ein ZIP-Archiv verpackt sind, um die Belastung des Dateisystems zu reduzieren.
Das ZIP-Archiv wird in das Verzeichnis des RPM-Paket-Builds verschoben. Danach initialisiert ein Bash-Skript die Umgebungsvariablen, ermittelt die Build-Version, die Projektversion, den Pfad zum Build-Verzeichnis und startet RPM-build. Nach Abschluss des Builds wird das Paket im lokalen Repository, das sich auf dem Build-Agent befindet, veröffentlicht.
Dann wird ein JSON-Request vom Build-Agent an den Server im RPM-Repository gesendet, mit Angabe des Namens der Version und des Builds. Der Webhook, von dem ich zuvor sprach, lädt dieses Paket aus dem lokalen Repository des Build-Agents herunter und stellt den neuen Build zur Installation bereit.

Warum gerade ein solches Modell zur Paketlieferung im RPM-Repository? Warum kann das gebaute Paket nicht sofort ins Repository gesendet werden? Das liegt daran, dass dieses Verfahren zur Sicherstellung der Sicherheit dient. Ein solches Szenario beschränkt die Möglichkeit für Unbefugte, RPM-Pakete auf den Server hochzuladen, der für alle Linux-Maschinen zugänglich ist.
Versionierung der Datenbank
In einem Konsil mit der Entwicklung stellte sich heraus, dass die Leute MS SQL bevorzugen, aber in den meisten Non-Windows-Projekten bereits PostgreSQL intensiv genutzt wurde. Da wir uns bereits entschieden hatten, auf alles Kostenpflichtige zu verzichten, begannen wir auch hier, PostgreSQL zu verwenden.

In diesem Teil möchte ich erzählen, wie wir die Versionierung der Datenbank durchgeführt haben und wie wir zwischen Flyway und Entity Framework Core ausgewählt haben. Lassen Sie uns ihre Vor- und Nachteile betrachten.
Nachteile
Flyway funktioniert nur in eine Richtung, wir können nicht zurückrollen — das ist ein wesentlicher Nachteil. Man kann jedoch andere Parameter wie die Benutzerfreundlichkeit des Entwicklers mit Entity Framework Core vergleichen. Sie erinnern sich, dass wir dies in den Vordergrund gestellt haben, und das Hauptkriterium war, nichts für die Windows-Entwicklung zu ändern.
Für Flyway benötigten wir eine Art Wrapper, damit die Leute nicht SQL-Anfragen. Es ist ihnen viel lieber, in objektorientierten Begriffen zu arbeiten. Sie schrieben Anleitungen zur Arbeit mit Datenbankobjekten, die SQL-Abfrage wurde gebildet und ausgeführt. Die neue Version der Datenbank ist bereit, sie hat funktioniert – alles gut, alles läuft.
Entity Framework Core hat einen Nachteil – unter hoher Last generiert es nicht optimale SQL-Abfragen, und die Belastung der Datenbank kann erheblich sein. Da wir jedoch keinen hoch ausgelasteten Dienst haben, messen wir die Belastung nicht in Hunderten von RPS, haben wir diese Risiken akzeptiert und das Problem an zukünftige uns delegiert.
Vorteile
Entity Framework Core arbeitet out-of-the-box und ist für die Entwicklung praktisch, während Flyway einfach in das bestehende CI integriert werden kann. Aber wir machen es den Entwicklern bequem :)
Der Bereitstellungsprozess
Puppet sieht, dass eine Änderung der Paketversionen kommt, unter denen dasjenige ist, das für die Migration verantwortlich ist. Zuerst wird das Paket installiert, das die Migrationsskripte und die mit der Datenbank verknüpfte Funktionalität enthält. Danach wird die Anwendung, die mit der Datenbank arbeitet, neu gestartet. Dann erfolgt die Installation der verbleibenden Komponenten. Die Reihenfolge der Paketinstallation und des Anwendungstarts ist im Puppet-Manifest beschrieben.
Die Anwendungen verwenden sensible Daten wie Tokens, Datenbankpasswörter, all dies wird im Konfigurationsfile mit dem Puppet Master abgerufen, wo sie in verschlüsselter Form gespeichert sind.
Probleme mit TFS
Nachdem wir uns entschieden und verstanden haben, dass alles bei uns wirklich funktioniert, beschloss ich, mir anzusehen, was mit den Builds in TFS im Allgemeinen für die Abteilung für Windows-Entwicklung bei anderen Projekten los ist - ob wir schnell bauen/releasen oder nicht, und ich entdeckte erhebliche Probleme mit der Geschwindigkeit.
Eines der Hauptprojekte benötigt 12-15 Minuten zum Bauen – das ist zu lang, so kann es nicht weitergehen. Eine schnelle Analyse zeigte einen schrecklichen Rückgang bei I/O, und das bei großen Arrays.
Nach einer komponentenweisen Analyse identifizierte ich drei Brennpunkte. Der erste – „Kaspersky Antivirus“, der auf allen Windows-Build-Agenten den Quellcode scannt. Zweiter — Windows Indexer. Er wurde nicht deaktiviert, und auf den Build-Agenten wurde in Echtzeit alles im Deploy-Prozess indexiert.
Dritter — Npm install. Es stellte sich heraus, dass wir in den meisten Pipelines genau dieses Skript verwendet haben. Was ist daran schlecht? Das Npm install Verfahren wird beim Erstellen des Abhängigkeitsbaums in package-lock.json, wo die Versionen der Pakete festgehalten werden, die für den Bau des Projekts verwendet werden. Der Nachteil ist, dass Npm install jedes Mal die aktuellen Versionen der Pakete aus dem Internet holt, und das kostet viel Zeit bei großen Projekten.
Entwickler experimentieren manchmal auf ihrer lokalen Maschine, um den Betrieb eines bestimmten Teils oder des gesamten Projekts zu überprüfen. Manchmal stellte sich heraus, dass lokal alles toll war, aber beim Bauen und Ausrollen funktionierte nichts. Wir fangen an zu klären, wo das Problem liegt — aha, verschiedene Versionen der Abhängigkeits-Pakete.
Lösung
- Quellcodes in den Ausnahmen AV.
- Deaktivierung der Indizierung.
- Überwechseln zu npm ci.
Die Vorteile von npm ci bestehen darin, dass wir den Abhängigkeitsbaum einmalig erstellen, und die Möglichkeit erhalten, dem Entwickler die aktuelle Liste der Pakete, mit der er lokal beliebig experimentieren kann, zur Verfügung zu stellen. Das spart Zeit für die Entwickler, die Code schreiben.
Konfiguration
Jetzt ein wenig zur Konfiguration des Repositories. Historisch gesehen verwenden wir Nexus für das Repository-Management, einschließlich Internal REPO. In dieses interne Repository werden alle Komponenten geliefert, die wir für interne Zwecke verwenden, wie zum Beispiel selbstgeschriebene Monitoring-Tools.

Wir verwenden auch NuGet, da es im Vergleich zu anderen Paketmanagern besser cacht.
Ergebnis
Nachdem wir die Build-Agenten optimiert haben, hat sich die durchschnittliche Build-Zeit von 12 Minuten auf 7 Minuten verkürzt.
Wenn man alle Maschinen zählt, die wir für Windows hätten verwenden können, aber in diesem Projekt auf Linux umgestellt haben, haben wir etwa 10.000 Dollar gespart. Und das nur bei Lizenzen, wenn man die Betriebskosten berücksichtigt, wäre es mehr.
Pläne
Für das nächste Quartal haben wir die Arbeit an der Optimierung der Codeauslieferung eingeplant.
Überwechseln zu einem vorgefertigten Docker-Abbild. TFS ist ein tolles Tool mit vielen Plugins, die eine Integration in die Pipeline ermöglichen, einschließlich dem Bauen per Trigger, beispielsweise für Docker-Images. Diesen Trigger möchten wir für genau diesen package-lock.json. Wenn sich die Zusammensetzung der Komponenten, die für den Aufbau des Projekts verwendet werden, aus irgendeinem Grund ändert – erstellen wir ein neues Docker-Image. Dieses wird anschließend für die Bereitstellung des Containers mit der gebauten Anwendung verwendet. Momentan gibt es das nicht, aber wir planen, auf eine Mikrodienstarchitektur in Kubernetes umzusteigen, die aktiv in unserem Unternehmen entwickelt wird und seit langem Produktionslösungen bereitstellt.
Zusammenfassung
Ich fordere alle auf, Windows wegzuwerfen, aber das liegt nicht daran, dass ich nicht weiß, wie man es vorbereitet. Der Grund ist, dass der größte Teil der Opensource-Lösungen – das ist der Linux-Stack. Sie werden gut Ressourcen sparen. Meiner Meinung nach gehört die Zukunft den Open Source-Lösungen auf Linux mit einer starken Community.
Das Profil des Sprechers Alexander Sinchinov .
ist eine Konferenz zur Integration von Entwicklungs-, Test- und Betriebsprozessen für Professionals von Professionals. Genau deshalb ist das Projekt, über das Alexander berichtet hat, realisiert und funktioniert, und am Tag seines Vortrags wurden zwei erfolgreiche Releases durchgeführt. Auf werden am 27. und 28. Mai noch mehr solcher Fälle von Praktikern präsentiert. Sie können auch im letzten Moment auf den Zug aufspringen und oder in Ruhe Wir sehen uns in Skolkovo!
Quelle: habr.com
