Wir haben DevOps so gut wie möglich entwickelt. Wir waren 8 Personen, und Vasja war der Beste im Bereich Windows. Plötzlich ging Vasja, und ich hatte die Aufgabe, ein neues Projekt zu leiten, das Windows-Entwicklung bereitstellt. Als ich den gesamten Stack der Windows-Entwicklung auf den Tisch legte, wurde mir klar, dass die Situation schmerzhaft war…
So beginnt die Geschichte Alexander Sinchinov findet man . Als der führende Windows-Spezialist das Unternehmen verließ, stellte Alexander sich die Frage, was nun zu tun sei. Natürlich auf Linux umsteigen! Alexander wird erzählen, wie es ihm gelungen ist, einen Präzedenzfall zu schaffen und einen Teil der Windows-Entwicklung auf Linux zu übertragen, anhand eines Projekts mit 100.000 Endbenutzern.

Wie gelingt es, Projekte mühelos in RPM zu liefern, indem man TFS, Puppet und Linux .NET Core nutzt? Wie hält man die Versionskontrolle der Datenbank aufrecht, wenn das Entwicklungsteam zum ersten Mal von Postgres und Flyway hört und die Frist übermorgen abläuft? Wie integriert man Docker? Wie motiviert man .NET-Entwickler, Windows und Smoothies zugunsten von Puppet und Linux abzulehnen? Wie löst man ideologische Konflikte, wenn man Windows in der Produktion weder will noch kann? Darüber und über Web Deploy, Tests, CI, die Anwendung von TFS in bestehenden Projekten sowie über kaputte Krücken und funktionierende Lösungen wird in der Auswertung des Vortrags von Alexander gesprochen.

Also, Vasya ist weg, die Aufgabe liegt bei mir, die Entwickler warten mit Forken ungeduldig. Als ich endlich realisierte, dass man Vasya nicht zurückholen kann, machte ich mich an die Arbeit. Zunächst schätzte ich den Anteil an Windows-VMs in unserem Pool. Die Rechnung fiel nicht zugunsten von Windows aus.

Da wir aktiv DevOps entwickeln, wurde mir klar, dass ich etwas an unserem Ansatz zur Einführung neuer Anwendungen ändern musste. Die Lösung war einfach – wenn möglich, alles auf Linux umzustellen. Google half mir dabei – zu diesem Zeitpunkt war .NET bereits für Linux portiert, und ich wusste, dass dies die Lösung ist!
Warum .NET Core in Verbindung mit Linux?
Es gab mehrere Gründe dafür. Zwischen „Geld zahlen“ und „nicht zahlen“ wird die Mehrheit die zweite Option wählen – so wie ich. Die Lizenz für MSDB kostet etwa 1.000 $, und die Wartung einer Flotte von Windows-VMs summiert sich auf Hunderten von Dollar. Für ein großes Unternehmen sind das erhebliche Kosten. Deshalb Einsparungen — der erste Grund. Nicht der wichtigste, aber einer der bedeutenden.
Windows-VMs benötigen mehr Ressourcen als ihre Linux-Geschwister – sie sind schwerfällig. Angesichts der Größe eines großen Unternehmens haben wir uns für Linux entschieden.
Das System lässt sich einfach in die bestehende CI integrieren. Wir betrachten uns als fortschrittliche DevOps, verwenden Bamboo, Jenkins und GitLab CI, daher läuft der Großteil bei uns unter Linux.
Der letzte Grund ist die einfache Wartung. Wir mussten die Einstiegshürden für die „Wartungsteams“ senken – Leute, die die technische Seite verstehen, für den reibungslosen Betrieb sorgen und die Services der zweiten Linie unterstützen. Sie waren bereits mit dem Linux-Stack vertraut, daher fällt es ihnen viel leichter, das neue Produkt zu verstehen, zu unterstützen und zu warten, als zusätzliche Ressourcen aufzuwenden, um sich mit ähnlichen Funktionen von Windows-Software auseinanderzusetzen.
Anforderungen
Zunächst und vor allem – die Bequemlichkeit der neuen Lösung für Entwickler. Nicht alle waren bereit für Veränderungen, besonders nach dem Wort Linux. Entwickler möchten ihr geliebtes Visual Studio, TFS mit automatisierten Tests für Builds und Smoothies. Wie die Auslieferung in die Produktion erfolgt, ist ihnen egal. Deshalb haben wir uns entschieden, den gewohnten Prozess nicht zu ändern und alles für die Windows-Entwicklung unverändert zu lassen.
Ein neues Projekt muss in die bestehende CI integriert werden. Die Rails waren bereits vorhanden, und die gesamte Arbeit musste unter Berücksichtigung der Konfigurationsmanagementparameter, der festgelegten Lieferstandards und der Überwachungssysteme geleistet werden.
Einfache Wartung und Betrieb, als Voraussetzung für die minimale Einstiegshürde für alle neuen Teilnehmer aus verschiedenen Abteilungen und dem Supportteam.
Die Frist war gestern.
Die Win-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 Funktionen ist, oder dass Entity Framework Core — ein Paradies für Entwickler, in dem man sich nicht mit der Erstellung von SQL-Skripten herumschlagen muss, sondern Abfragen in der Datenbank in objektorientierten Begriffen beschreiben kann. Doch als wir den Aktionsplan besprachen, betrachtete ich diesen Stack wie eine sumerische Keilschrift, indem ich nur PostgreSQL und Git verstand.
Zu diesem Zeitpunkt verwendeten wir aktiv Puppet als Konfigurationsmanagementsystem. In den meisten unserer Projekte setzten wir GitLab CI, Elastic, um stark ausgelastete Dienste mit HAProxy überwachen zu können, alles mit Hilfe von Zabbix, einem Ensemble Grafana und Prometheus, Jaeger, und all das lief auf Hardware HP c ESXi findet man VMware. Jeder kennt es – ein Klassiker.

Lass uns einen Blick darauf werfen und versuchen zu verstehen, was geschah, bevor wir all diese Eingriffe vornahmen.
Was war
TFS ist ein ziemlich mächtiges System, das nicht nur den Code vom Entwickler zur endgültigen Produktionsmaschine liefert, sondern auch ein Set für sehr flexible Integration mit verschiedenen Diensten bietet – um CI auf plattformübergreifender Ebene zu gewährleisten.

Früher waren das lauter Popup-Fenster. TFS nutzte mehrere Build-Agenten, auf denen zahlreiche Projekte erstellt wurden. In jedem Agenten arbeiteten 3-4 Worker, um Aufgaben parallel zu verteilen und den Prozess zu optimieren. Danach lieferte TFS gemäß den Release-Plänen den frisch gebackenen Build auf dem Windows-Anwendungsserver aus.
Welches Ziel wollten wir erreichen?
Wir verwenden TFS für die Lieferung und Entwicklung, während die Anwendung auf einem Linux-Anwendungsserver läuft, und dazwischen geschieht ein gewisses Zaubern. Diese Magic Box ist das Geheimnis der bevorstehenden Arbeit. Bevor ich es in seine Einzelteile zerlege, mache ich einen Schritt zur Seite und sage zwei Worte zur Anwendung.
Projekt
Die Anwendung bietet Funktionen zur Verwaltung von Prepaid-Karten.

Client
Es gab zwei Arten von Benutzern. Erster erhielt Zugriff, indem er sich mit einem SSL-Zertifikat SHA-2 authentifizierte. Der und zweiten hatte Zugriff mit Benutzername und Passwort.
HAProxy
Der nächste Client-Anfrage wurde an HAProxy weitergeleitet, der folgende Aufgaben übernahm:
- primäre Authentifizierung;
- SSL-Terminierung;
- HTTP-Anfragen optimieren;
- Anfragen weiterleiten.
Die Überprüfung des Client-Zertifikats erfolgte über die Zertifikatskette. Wir sind die Behörde und können uns das erlauben, da wir selbst die Zertifikate für die Dienstkunden ausstellen.
Beachten Sie den dritten Punkt, darauf kommen wir später zurück.
Backend
Das Backend wurde auf Linux geplant. Es interagiert mit der Datenbank, lädt die benötigte Liste von Berechtigungen und gewährt anschließend, je nachdem welche Berechtigungen der autorisierte Benutzer hat, den Zugang zum Unterzeichnen von Finanzdokumenten und deren Übermittlung zur Ausführung oder zur Erstellung eines Berichts.
Ersparnisse mit HAProxy
Neben den beiden Kontexten, in denen jeder Kunde navigierte, gab es auch einen weiteren Kontext für die Identität. IdentityServer4 erlaubt die Authentifizierung, ein kostenloses und leistungsstarkes Äquivalent zu ADFS — Active Directory Federation Services.
Die Anfrage in der Identität wurde in mehreren Schritten verarbeitet. Der erste Schritt - einen Client ging an das Backend, das Daten mit diesem Server austauschte und überprüfte, ob ein Token für den Client vorlag. Wenn nicht, wurde die Anfrage zurück an den Kontext geschickt, von dem sie kam, jedoch mit einem Redirect, und mit dem Redirect ging es zur Identität.
Der zweite Schritt - die Anfrage gelangte zur Anmeldeseite im IdentityServer, wo der Client sich registrierte und der so lange erwartete Token in der Datenbank des IdentityServers erschien.
Der dritte Schritt - der Client wurde zurückgeleitet. im Kontext, aus dem er kam.

IdentityServer4 hat eine Besonderheit: die Antwort auf die Rückfrage wird über HTTP zurückgegeben.Egal wie sehr wir uns mit der Serverkonfiguration bemühten und die Dokumentation studierten, wir erhielten jedes Mal die ursprüngliche Clientanfrage mit der URL, die über HTTPS kam, während IdentityServer denselben Kontext mit HTTP zurückgab. Wir waren schockiert! Und haben all dies über den Identitätskontext auf HAProxy geleitet, wobei wir in den Headern das Protokoll von HTTP auf HTTPS modifizieren mussten.
Was ist das Verbesserung und wo haben wir gespart?
Wir haben Geld gespart, indem wir eine kostenlose Lösung für die Autorisierung der Benutzergruppe verwendet haben, anstatt IdentityServer4 als separate Node in ein eigenes Segment zu bringen, haben wir ihn gemeinsam mit dem Backend auf demselben Server genutzt, auf dem auch das Backend der Anwendung läuft.
Wie es funktionieren sollte
Also, wie versprochen – Magic Box. Wir verstehen bereits, dass wir garantiert in Richtung Linux gehen. Lassen Sie uns die spezifischen Aufgaben formulieren, die gelöst werden mussten.

Puppet-Manifeste. Um die Konfiguration des Dienstes und der Anwendung bereitzustellen und zu verwalten, mussten wir effektive Rezepte erstellen. Das Notizbuch mit dem Bleistift zeigt anschaulich, wie schnell und trotzdem qualitativ hochstehend das umgesetzt wurde.
Liefermethode. Standardmäßig verwenden wir RPM. Jeder versteht, dass das in Linux unverzichtbar ist, doch das Projekt stellte nach dem Build eine Sammlung ausführbarer DLL-Dateien dar. Es waren etwa 150 Dateien, und das Projekt war ziemlich umfangreich. Die einzige sinnvolle Lösung war, dieses binäre Paket in RPM zu verpacken und die Anwendung daraus zu deployen.
Versionierung. Wir mussten sehr häufig Releases durchführen und mussten entscheiden, wie wir den Paketnamen bilden. Dies war eine Frage der Integration mit TFS. Unser Build-Agent war unter Linux. Wenn TFS eine Aufgabe an den Bearbeiter – Worker – auf dem Build-Agent sendet, übergibt er ihm auch eine Sammlung von Variablen, die in die Umgebung des Bearbeitungsprozesses gelangen. In diesen Umgebungsvariablen befinden sich der Build-Name, die Versionsnummer und weitere Variablen. Nähere Informationen finden Sie im Abschnitt „RPM-Paket erstellen“.
TFS-Konfiguration Wir haben uns auf die Einstellung der Pipeline konzentriert. Früher haben wir alle Windows-Projekte auf Windows-Agents gesammelt, und jetzt gibt es einen Linux-Agenten – den Build-Agent, der in die Build-Gruppe integriert werden muss, mit Artefakten angereichert werden sollte, um den Typ der Projekte festzulegen, die auf diesem Build-Agenten erstellt werden, und um die Pipeline entsprechend anzupassen.
IdentityServer. ADFS ist nicht unser Weg, wir setzen auf Open Source.
Schauen wir uns die Komponenten an.
Magic Box
Es besteht aus vier Teilen.

Linux Build-Agent. Linux, da wir dafür entwickeln – das ist logisch. Dieser Teil wurde in drei Schritten durchgeführt.
- Worker einrichten und nicht nur einen, da eine verteilte Arbeit am Projekt vorgesehen war.
- Installieren von .NET Core 1.x. Warum genau 1.x, wenn bereits 2.0 im Standard-Repository verfügbar ist? Weil, als wir mit der Entwicklung begannen, die stabile Version 1.09 war und das Projekt beschlossen wurde, es dafür zu erstellen.
- Git 2.x.
RPM-Repository. RPM-Pakete mussten irgendwo gespeichert werden. Es wurde angenommen, dass wir dasselbe Unternehmens-RPM-Repository verwenden würden, das allen Linux-Hosts zur Verfügung steht. Das haben wir auch getan. Auf dem Repository-Server wurde Webhooks eingerichtet, der das benötigte RPM-Paket von dem angegebenen Ort herunterlud. Die Version des Pakets wurde dem Build-Agent mitgeteilt.
GitLab. Achtung! GitLab wird hier nicht von Entwicklern, sondern von der Betriebsabteilung zur Versionskontrolle der Anwendung, zur Versionsverwaltung von Paketen und zur Überwachung des Zustands aller Linux-Maschinen verwendet. Dort wird auch das Rezept aufbewahrt – alle Puppet-Manifest-Dateien.
Puppet – klärt alle strittigen Punkte und liefert genau die Konfiguration, die wir aus GitLab möchten.
Lassen Sie uns eintauchen. Wie erfolgt die Lieferung von DLL in RPM?
Lieferung von DLL in RPM
Angenommen, wir haben einen Rockstar-Entwickler auf .NET. Er verwendet Visual Studio und erstellt einen Release-Branch. Danach lädt er diesen in Git hoch, und Git ist hier ein TFS-Objekt, d. h. es handelt sich um das Repository der Anwendung, mit dem der Entwickler arbeitet.

Daraufhin sieht TFS, dass ein neuer Commit eingeht. Welche Anwendung? In den TFS-Einstellungen gibt es ein Tag, das angibt, über welche Ressourcen der jeweilige Build-Agent verfügt. In diesem Fall sieht er, dass wir ein .NET Core-Projekt bauen und wählt einen Linux-Build-Agent aus dem Pool aus.
Der Build-Agent erhält die Quellcodes, lädt die notwendigen Abhängigkeiten aus den .NET-, npm- usw. Repositories herunter und sendet nach dem Bauen der Anwendung und dem anschließenden Verpacken das RPM-Paket in das RPM-Repository.
Andersherum geschieht Folgendes. Der Ingenieur der Betriebsabteilung ist direkt mit dem Rollout des Projekts beschäftigt: Er ändert die Paketversionen in Hiera im Repository, wo die Rezeptur der Anwendung gespeichert ist, wonach Puppet Yumtriggert, das neue Paket aus dem Repository abruft, und die neue Version der Anwendung bereit ist zur Nutzung.

Theoretisch ist alles einfach, aber was passiert intern auf dem eigentlichen Build-Server?
Verpackung der DLL RPM
Die Quellcodes des Projekts und die Build-Anforderung von TFS wurden erhalten. Der Build-Agent startet den Build des Projekts aus den Quellcodes.Das gebaute Projekt ist in Form von vielen DLL-Dateienverfügbar, die in einem ZIP-Archiv verpackt sind, um die Belastung des Dateisystems zu reduzieren.
Das ZIP-Archiv wird in das Verzeichnis des RPM-Paket-Builds ausgegeben. Anschließend initialisiert ein Bash-Skript die Umgebungsvariablen, ermittelt die Build-Version, die Projektversion, den Pfad zum Build-Verzeichnis und startet den RPM-Build. Nach Abschluss des Builds wird das Paket im lokalen Repositoryveröffentlicht, das sich auf dem Build-Agent befindet.
Von dort aus wird eine JSON-Anfrage vom Build-Agent an den Server im RPM-Repository gesendet. mit der Angabe des Versionsnamens und des Builds. Der Webhook, von dem ich zuvor gesprochen habe, lädt dieses Paket aus dem lokalen Repository auf dem Build-Agenten herunter und macht eine neue Version zur Installation verfügbar.

Warum besteht das Paketlieferungsmodell im RPM-Repository so? Warum kann das gebaute Paket nicht sofort ins Repository geschickt werden? Der Grund ist die Sicherheitsanforderung. Dieses Szenario beschränkt die Möglichkeit einer unbefugten hochladung von RPM-Paketen durch Dritte auf einen Server, der für alle Linux-Maschinen zugänglich ist.
Datenbankversionierung
Beim Konsilium mit der Entwicklung stellte sich heraus, dass die Kollegen näher zu MS SQL tendieren, aber in den meisten Non-Windows-Projekten bereits PostgreSQL verwenden. Da wir uns entschieden hatten, auf alles Bezahlte zu verzichten, begannen wir auch hier PostgreSQL zu verwenden.

In diesem Abschnitt möchte ich erläutern, wie wir die Datenbankversionierung umgesetzt haben und wie wir zwischen Flyway und Entity Framework Core entschieden haben. Lassen Sie uns ihre Vor- und Nachteile untersuchen.
Nachteile
Flyway funktioniert nur in eine Richtung, wir können nicht zurückrollen. — das ist ein wesentlicher Nachteil. Der Vergleich mit Entity Framework Core kann auf anderen Parametern erfolgen – aus der Sicht der Entwicklerfreundlichkeit. Sie erinnern sich, dass wir dies in den Mittelpunkt gestellt haben, und das Hauptkriterium war, nichts für die Windows-Entwicklung zu verändern.
Für Flyway benötigten wir eine Art Wrapper, damit die Leute nicht schreiben mussten SQL-Abfragen. Es ist ihnen viel lieber, in OOP-Begriffen zu arbeiten. Wir haben Anleitungen zur Arbeit mit Datenbankobjekten erstellt, der SQL-Befehl wurde formuliert und ausgeführt. Die neue Version der Datenbank ist bereit, funktionsfähig – alles funktioniert gut.
Entity Framework Core hat einen Nachteil – bei hohen Lasten generiert es nicht optimale SQL-Abfragen, und die Belastung für die Datenbank kann erheblich sein. Da unser Dienst jedoch nicht hochgradig belastet ist und wir die Last nicht in hunderten von RPS messen, haben wir diese Risiken akzeptiert und das Problem an zukünftige Versionen delegiert.
Vorteile
Entity Framework Core funktioniert sofort und ist für die Entwicklung angenehm, während Flyway sich leicht in bestehende CI integrieren lässt. Aber wir machen es ja den Entwicklern bequem :)
Deployment-Prozedur
Puppet erkennt, dass eine Versionsänderung für Pakete kommt, unter denen dasjenige zur Migration verantwortlich ist. Zuerst wird das Paket installiert, das die Migrationsskripte und die mit der Datenbank verbundene Funktionalität enthält. Danach wird die Anwendung neu gestartet, die mit der Datenbank arbeitet. Anschließend erfolgt die Installation der verbleibenden Komponenten. Die Reihenfolge der Paketinstallationen und Anwendungsstarts ist im Puppet-Manifest beschrieben.
Anwendungen verwenden sensible Daten wie Tokens und Datenbankpasswörter, die alle in der Konfiguration vom Puppet Master abgerufen werden, wo sie verschlüsselt gespeichert sind.
TFS-Probleme
Nachdem wir uns einig waren und verstanden hatten, dass bei uns wirklich alles funktioniert, beschloss ich, zu überprüfen, wie es insgesamt um die Builds im TFS für die Win-Entwicklungsabteilung bei anderen Projekten steht – ob wir schnell sammeln/releasen können oder nicht, und stellte erhebliche Probleme mit der Geschwindigkeit fest.
Eines der Hauptprojekte benötigt 12-15 Minuten zum Build – das ist zu lange, so kann es nicht weitergehen. Eine schnelle Analyse zeigte einen katastrophalen Rückgang bei I/O, und das auf Arrays.
Nach einer komponentengenauen Analyse habe ich drei Brennpunkte identifiziert. Der erste – «Kaspersky Antivirus», der auf allen Windows Build-Agenten den Quellcode scannt. Der zweite — Windows Indexer. Er wurde nicht deaktiviert und während des Deployments wurde in Echtzeit alles auf den Build-Agenten indexiert.
Der dritte — Npm install. Es stellte sich heraus, dass wir in den meisten Pipelines genau dieses Skript verwendet haben. Was ist daran schlecht? Der Npm install-Prozess 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 aktuellsten Versionen der Pakete aus dem Internet zieht, und das kann viel Zeit in Anspruch nehmen, besonders bei großen Projekten.
Entwickler experimentieren manchmal auf ihrem lokalen Rechner, um die Funktionalität eines einzelnen Teils oder des gesamten Projekts zu überprüfen. Manchmal kam es vor, dass lokal alles großartig war, aber beim Bauen und Deployen nichts funktionierte. Wir fangen an, das Problem zu analysieren — aha, verschiedene Versionen der Abhängigkeitspakete.
Lösung
- Quellcode in AV-Ausschlüssen.
- Deaktivierung der Indizierung.
- Wechsel zu npm ci.
Die Vorteile von npm ci liegen darin, dass wir den Abhängigkeitsbaum nur einmal erstellen, und wir die Möglichkeit haben, dem Entwickler eine aktuelle Liste der Pakete zur Verfügung zu stellen., mit dem man lokal so viel experimentieren kann, wie man möchte. Dies spart Zeit für Entwickler, die Code schreiben.
Konfiguration
Jetzt kurz zur Konfiguration des Repositories. Historisch gesehen verwenden wir Nexus für die Verwaltung von Repositories, einschließlich Internal REPO. In dieses interne Repository werden alle Komponenten geliefert, die wir für interne Zwecke verwenden, beispielsweise maßgeschneiderte Monitoring-Tools.

Wir verwenden auch NuGet, da es im Vergleich zu anderen Paketmanagern besser cached.
Ergebnis
Nachdem wir die Build-Agenten optimiert haben, reduzierte sich die durchschnittliche Build-Zeit von 12 Minuten auf 7.
Wenn wir alle Maschinen zählen, die wir für Windows hätten nutzen können, aber in diesem Projekt auf Linux umgestellt haben, haben wir etwa 10.000 $ gespart. Und das ist nur für Lizenzen, wenn wir die Betriebskosten mit einbeziehen, dann noch mehr.
Pläne
Für das nächste Quartal ist die Arbeit an der Optimierung der Bereitstellung von Code eingeplant.
Der Übergang zu einem vorgebauten Docker-Image. TFS ist eine großartige Sache mit vielen Plugins, die eine Integration in die Pipeline ermöglichen, einschließlich der Auslösung des Builds eines Docker-Images. Diese Auslösung möchten wir für dasjenige package-lock.json. Wenn sich die Komponenten, die zur Erstellung des Projekts verwendet werden, irgendwie ändern – stellen wir ein neues Docker-Image zusammen. Dieses wird dann für die Bereitstellung des Containers mit der erstellten Anwendung verwendet. Das gibt es derzeit nicht, aber wir planen, auf eine Microservices-Architektur in Kubernetes umzusteigen, die in unserem Unternehmen aktiv entwickelt wird und seit langem Produktionslösungen unterstützt.
Zusammenfassung
Ich fordere alle auf, Windows abzulehnen, aber nicht, weil ich es nicht beherrschen kann. Der Grund liegt darin, dass der Großteil der Open-Source-Lösungen – das sind Linux-Stacks. Damit können Sie ordentlich Ressourcen sparen. Meiner Meinung nach liegt die Zukunft in 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 Betriebprozessen für Profis von Profis. Genau deshalb wurde das Projekt, über das Alexander gesprochen hat, umgesetzt und funktioniert, und am Tag seiner Präsentation wurden zwei erfolgreiche Releases durchgeführt. Auf wird es am 27. und 28. Mai noch mehr solcher Fälle von Praktikern geben. Es gibt noch die Möglichkeit, einzusteigen und es sich gemütlich zu machen. Ticket. Wir sehen uns in Skolkovo!
Quelle: habr.com
