Theorie und Praxis der Standardisierung von Docker-Diensten

Informationen zum Thema Microservice-Anwendungsarchitektur, die bereits erfolgreich waren, reichen heute völlig aus, um zu entscheiden, ob sie zu Ihrem Produkt passen oder nicht. Und es ist ĂŒberhaupt kein Geheimnis, dass Unternehmen, die sich fĂŒr diesen Weg entschieden haben, sich vielen technischen und kulturellen Herausforderungen stellen mĂŒssen. Eine der Problemquellen ist der Overhead, der sich ĂŒberall vervielfacht, und dies gilt auch fĂŒr die Routine, die mit Produktionsprozessen verbunden ist.



Bildquelle:


Wie Sie sich vorstellen können, ist Anti-Plagiat ein solches Unternehmen, in dem allmĂ€hlich das VerstĂ€ndnis entstand, dass wir mit Microservices unterwegs waren. Aber bevor wir anfingen, den Kaktus zu essen, beschlossen wir, ihn zu reinigen und zu kochen. Und da alle einzig wahren und korrekten Lösungen einzigartig sind, haben wir uns entschlossen, anstelle von universellen DevOps-Folien mit schönen Pfeilen nur unsere eigenen Erfahrungen zu teilen und zu erzĂ€hlen, wie wir bereits einen betrĂ€chtlichen Teil unseres besonderen Weges zum Erfolg zurĂŒckgelegt haben, hoffe ich.


Wenn Sie ein wirklich einzigartiges Produkt herstellen, das weit und breit aus Know-how besteht, besteht fast keine Chance, diesem speziellen Weg auszuweichen, da er aus vielen privaten besteht: ausgehend von den im Unternehmen entwickelten kulturellen und historischen Daten, die mit seiner eigenen SpezifitÀt und dem verwendeten technologischen Stapel enden .


Eine der Aufgaben fĂŒr jedes Unternehmen und Team besteht darin, das optimale Gleichgewicht zwischen Freiheiten und Regeln zu finden, und Microservices bringen dieses Problem auf eine neue Ebene. Dies scheint der Idee von Microservices zu widersprechen, die eine große Freiheit bei der Auswahl der Technologien impliziert. Wenn Sie sich jedoch nicht direkt auf architektonische und technologische Fragen konzentrieren, sondern die Probleme der Produktion als Ganzes betrachten, ist das Risiko, irgendwo in der Handlung des „Gartens der irdischen Freuden“ zu sein, durchaus greifbar .


In dem Buch "Erstellen von Microservices" bietet Sam Newman jedoch eine Lösung fĂŒr dieses Problem, bei der er buchstĂ€blich auf den ersten Seiten von der Notwendigkeit spricht, die KreativitĂ€t von Teams im Rahmen technischer Vereinbarungen einzuschrĂ€nken. Einer der SchlĂŒssel zum Erfolg, insbesondere im Zusammenhang mit einer begrenzten Freihandressource, ist die Standardisierung von allem, was nur verhandelt werden kann und was niemand wirklich die ganze Zeit tun möchte. Durch die Ausarbeitung von Vereinbarungen schaffen wir klare Spielregeln fĂŒr alle Teilnehmer an der Produktion und dem Betrieb von Systemkomponenten. Und wenn Sie die Spielregeln kennen, mĂŒssen Sie zustimmen, dass das Spielen einfacher und unterhaltsamer sein sollte. Das Befolgen dieser Regeln selbst kann jedoch zur Routine werden und den Teilnehmern Unbehagen bereiten, was direkt zu allen Arten von Abweichungen von ihnen und in der Folge zum Scheitern der gesamten Idee fĂŒhrt. Und der naheliegendste Ausweg besteht darin, alle Vereinbarungen in den Code aufzunehmen, da keine einzige Vorschrift das tun kann, was Automatisierung und praktische Tools verwenden können, deren Verwendung intuitiv und natĂŒrlich ist.


In diese Richtung konnten wir immer mehr automatisieren, und je stĂ€rker unser Prozess wurde, desto mehr wurde er zu einem End-to-End-Förderer fĂŒr die Produktion von Bibliotheken und Mikrodiensten (oder nicht so).


Haftungsausschluss
Dieser Text ist kein Versuch, "wie es sollte" anzugeben, es gibt keine universellen Lösungen, nur eine Beschreibung unserer Position auf dem Evolutionspfad und der gewĂ€hlten Richtung. All dies mag nicht fĂŒr jeden geeignet sein, aber in unserem Fall ist es vor allem deshalb sinnvoll, weil:

- Die Entwicklung im Unternehmen erfolgt in 90% der FĂ€lle in C #.
- Es war nicht nötig, bei Null anzufangen, was Teil der akzeptierten Standards, AnsÀtze und Technologien war - dies ist das Ergebnis von Erfahrung oder einfach eines historischen Erbes;
- Repositories mit .NET-Projekten, im Gegensatz zu Teams, Dutzende (und es wird mehr geben);
- Wir verwenden gerne eine sehr einfache CI-Pipeline, um eine Lieferantenbindung so weit wie möglich zu vermeiden.
- FĂŒr einen gewöhnlichen .NET-Entwickler können die Wörter "Container", "Docker" und "Linux" immer noch AnfĂ€lle von leichtem existenziellem Horror verursachen, aber ich möchte niemanden durch das Knie brechen.

Ein kleiner Hintergrund


Im FrĂŒhjahr 2017 stellte Microsoft der Welt eine Vorschau auf .NET Core 2.0 vor, und dieses Jahr beeilten sich C # -Astrologen sofort, das Linux-Jahr zu erklĂ€ren, also ...



Bildquelle:


Seit einiger Zeit haben wir, ohne der Magie zu vertrauen, alles unter Windows und Linux gesammelt und getestet, Artefakte mit einigen SSH-Skripten veröffentlicht und versucht, alte CI / CD-Pipelines im Schweizer Messermodus zu konfigurieren. Aber nach einiger Zeit stellten sie fest, dass wir etwas falsch machten. DarĂŒber hinaus klangen Verweise auf Microservices und Container immer hĂ€ufiger. Also haben wir uns auch entschlossen, die Hype-Welle zu reiten und diese Richtungen zu erkunden.


Bereits in der Phase der Reflexion ĂŒber unsere mögliche Zukunft im Bereich Mikroservice stellten sich eine Reihe von Fragen, die wir ignorierten und die wir in dieser Zukunft mit neuen Problemen riskierten, die wir uns im Austausch gegen gelöste Probleme geschaffen hĂ€tten.


Erstens hatten wir bei der Betrachtung der Betriebsseite der theoretischen Mikroservice-Welt ohne Regeln Angst vor der Aussicht auf Chaos mit allen daraus resultierenden Konsequenzen, einschließlich nicht nur der unvorhersehbaren QualitĂ€t des Ergebnisses, sondern auch der Konflikte zwischen Teams oder Entwicklern und Ingenieuren. Der Versuch, einige Empfehlungen abzugeben, die nicht in der Lage waren, deren Einhaltung sicherzustellen, schien sofort ein leeres Unterfangen zu sein.


Zweitens wusste niemand wirklich, wie man Container richtig erstellt und Docker-Dateien schreibt, die jedoch bereits begonnen haben, in unseren Repositories lebhaft zu schwirren. Außerdem „lesen viele irgendwo“, dass dort nicht alles so einfach ist. Also musste jemand tiefer tauchen und es herausfinden und dann mit den besten Methoden fĂŒr die Montage von Containern zurĂŒckkehren. Aber die Aussicht, die Rolle eines Vollzeit-Docker-Packers zu ĂŒbernehmen, der mit Stapeln von Docker-Dateien allein gelassen wurde, hat aus irgendeinem Grund niemanden im Unternehmen inspiriert. Wie sich herausstellte, ist einmaliges Tauchen eindeutig nicht genug, und selbst wenn es auf den ersten Blick gut und richtig aussieht, kann es sich als falsch oder einfach nicht sehr gut herausstellen.


Und drittens wollte ich sicherstellen, dass die mit den Diensten erhaltenen Bilder nicht nur unter dem Gesichtspunkt der Containerpraktiken korrekt sind, sondern auch in ihrem Verhalten vorhersehbar sind und alle erforderlichen Eigenschaften und Attribute aufweisen, um die Steuerung der gestarteten Container zu vereinfachen. Mit anderen Worten, ich wollte Bilder mit Anwendungen erhalten, die gleichermaßen konfiguriert sind und Protokolle schreiben, eine einzige Schnittstelle zum Abrufen von Metriken bereitstellen, einen konsistenten Satz von Beschriftungen haben und dergleichen. Es war auch wichtig, dass die Baugruppe auf dem Computer des Entwicklers das gleiche Ergebnis wie die Baugruppe in jedem CI-System liefert, einschließlich Bestehen von Tests und Generieren von Artefakten.


So entstand das VerstĂ€ndnis, dass ein Prozess erforderlich sein wĂŒrde, um neues Wissen, neue Praktiken und Standards zu verwalten und zu zentralisieren, und dass der Weg vom ersten Commit zu einem Docker-Image, das vollstĂ€ndig fĂŒr die Produktinfrastruktur bereit ist, einheitlich und so automatisiert wie möglich sein sollte und nicht ĂŒber die beginnenden Begriffe hinausgehen sollte mit dem Wort kontinuierlich.


CLI vs. GUI


Der Ausgangspunkt fĂŒr eine neue Komponente, sei es ein Dienst oder eine Bibliothek, ist das Erstellen eines Repositorys. Diese Phase kann in zwei Teile unterteilt werden: Erstellen und Konfigurieren des Repositorys auf dem Hosting des Versionskontrollsystems (wir haben Bitbucket) und Initialisieren mit dem Erstellen einer Dateistruktur. GlĂŒcklicherweise gab es fĂŒr beide bereits eine Reihe von Anforderungen. Daher war es eine logische Aufgabe, sie im Code zu formalisieren.


Also, was sollte unser Repository sein:


  • Befindet sich in einem der Projekte, in denen Name, Zugriffsrechte, Richtlinien zum Akzeptieren von Pull-Anforderungen usw.;
  • Enthalten erforderliche Dateien und Verzeichnisse wie:
    • Datei mit der Konfiguration und Informationen zum Repository von SolutionInfo.props (mehr dazu weiter unten);
    • Projektquellcodes im src Verzeichnis;
    • .gitignore , README.md usw.;
  • EnthĂ€lt die erforderlichen Git-Submodule.
  • Das Projekt muss aus einer der Vorlagen abgeleitet werden.

Da die Bitbucket-REST-API die vollstĂ€ndige Kontrolle ĂŒber die Konfiguration von Repositorys bietet, wurde ein spezielles Dienstprogramm fĂŒr die Interaktion mit diesen erstellt - der Repository-Generator. Im Frage-Antwort-Modus erhĂ€lt sie vom Benutzer alle erforderlichen Daten und erstellt ein Repository, das alle unsere Anforderungen vollstĂ€ndig erfĂŒllt, nĂ€mlich:


  • Definiert ein Projekt in Bitbucket zur Auswahl;
  • Validiert den Namen gemĂ€ĂŸ unserer Vereinbarung;
  • Nimmt alle erforderlichen Einstellungen vor, die nicht vom Projekt geerbt werden können.
  • Aktualisiert die Liste der benutzerdefinierten Vorlagen (wir verwenden Dotnet- Vorlagen) fĂŒr das Projekt und schlĂ€gt eine Auswahl vor.
  • FĂŒllt die minimal erforderlichen Informationen zum Repository in der Konfigurationsdatei und in *.md Dokumenten aus.
  • Es verbindet Submodule mit der CI / CD-Pipeline-Konfiguration (in unserem Fall Bamboo Specs ) und Assembly-Skripten.

Mit anderen Worten, der Entwickler, der ein neues Projekt startet, startet das Dienstprogramm, fĂŒllt mehrere Felder aus, wĂ€hlt den Projekttyp aus und erhĂ€lt beispielsweise die vollstĂ€ndig fertige „Hallo Welt!“ Ein Dienst, der bereits mit dem CI-System verbunden ist. Von dort aus kann der Dienst sogar veröffentlicht werden, wenn Sie ein Commit durchfĂŒhren, bei dem die Version auf ungleich Null geĂ€ndert wird.


Der erste Schritt wurde getan. Keine manuelle Arbeit und Fehler, Suche nach Dokumentation, Registrierungen und SMS. Fahren wir nun mit dem fort, was dort generiert wurde.


Struktur


Die Standardisierung der Repository-Struktur hat lange Zeit Wurzeln geschlagen und war erforderlich, um die Montage, die Integration in das CI-System und die Entwicklungsumgebung zu vereinfachen. ZunĂ€chst gingen wir von der Idee aus, dass die Pipeline in CI so einfach und, wie Sie sich vorstellen können, standardisiert sein sollte, um die PortabilitĂ€t und Reproduzierbarkeit der Baugruppe sicherzustellen. Das heißt, dass das gleiche Ergebnis sowohl in jedem CI-System als auch am Arbeitsplatz des Entwicklers leicht erzielt werden kann. Daher wird alles, was sich nicht auf die Funktionen einer bestimmten kontinuierlichen Integrationsumgebung bezieht, einem speziellen Git-Submodul ĂŒbergeben und ist ein autarkes Build-System. Genauer gesagt das Montage-Standardisierungssystem. Die Pipeline selbst sollte in minimaler NĂ€herung nur das Skript build.sh , einen Bericht ĂŒber die Tests build.sh und gegebenenfalls eine Bereitstellung initiieren. Lassen Sie uns der Klarheit halber sehen, was passiert, wenn Sie das SampleService- Repository in einem Projekt mit dem sprechenden Namen Sandbox generieren.


 . ├── [bamboo-specs] ├── [devops.build] │ ├── build.sh │ └── ... ├── [docs] ├── [.scripts] ├── [src] │ ├── [CodeAnalysis] │ ├── [Sandbox.SampleService] │ ├── [Sandbox.SampleService.Bootstrap] │ ├── [Sandbox.SampleService.Client] │ ├── [Sandbox.SampleService.Tests] │ ├── Directory.Build.props │ ├── NLog.config │ ├── NuGet.Config │ └── Sandbox.SampleService.sln ├── .gitattributes ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── README.md └── SolutionInfo.props 

Die ersten beiden Verzeichnisse sind Git-Submodule. bamboo-specs ist "Pipeline as Code" fĂŒr das Atlassian Bamboo CI-System (es könnte an seiner Stelle ein devops.build ). devops.build ist unser Build-System, auf das ich weiter unten nĂ€her eingehen werde. Das Verzeichnis .scripts . Das .NET-Projekt selbst befindet sich in src : NuGet.Config enthĂ€lt die Konfiguration des privaten NuGet- Repositorys, NLog.config Dev-Time-Konfiguration von NLog . Wie Sie vielleicht erraten haben, ist die Verwendung von NLog in einem Unternehmen ebenfalls einer der Standards. Von den interessanten Dingen hier ist die fast magische Directory.Build.props Datei. Aus irgendeinem Grund kennen nur wenige Menschen eine solche Möglichkeit in .NET-Projekten, z. B. die Anpassung der Assembly . Kurz gesagt, Dateien mit den Namen Directory.Build.props und Directory.Build.targets automatisch in Ihre Projekte importiert und ermöglichen es Ihnen, gemeinsame Eigenschaften fĂŒr alle Projekte an einem Ort zu konfigurieren. Auf diese Weise verbinden wir beispielsweise den StyleCop.Analyzers- Analysator und seine Konfiguration aus dem CodeAnalysis Verzeichnis mit allen Projekten im CodeAnalysis , legen Versionsregeln und einige allgemeine Attribute fĂŒr Bibliotheken und Pakete fest ( Firma , Copyright usw.) und stellen auch eine Verbindung ĂŒber das <Import> Datei SolutionInfo.props , genau dieselbe Repository-Konfigurationsdatei, die oben beschrieben wurde. Es enthĂ€lt bereits die aktuelle Version, Informationen zu den Autoren, die URL des Repositorys und dessen Beschreibung sowie verschiedene Eigenschaften, die sich auf das Verhalten des Assemblysystems und die daraus resultierenden Artefakte auswirken.


Beispiel `SolutionInfo.props`
 <?xml version="1.0"?> <Project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="devops.build/SolutionInfo.xsd"> <PropertyGroup> <!-- Product name --> <Product>Sandbox.SampleService</Product> <!-- Product version. Version 0.0.0 wouldn't be published! --> <BaseVersion>0.0.0</BaseVersion> <!-- Project name which contains Main() --> <EntryProject>Sandbox.SampleService.Bootstrap</EntryProject> <!-- Exposed port --> <ExposedPort>4000/tcp</ExposedPort> <!-- DOTNET_SYSTEM_GLOBALIZATION_INVARIANT value. See https://github.com/dotnet/corefx/blob/master/Documentation/architecture/globalization-invariant-mode.md --> <GlobalizationInvariant>false</GlobalizationInvariant> <!-- Project URL --> <RepositoryUrl>https://bitbucket.contoso.com/projects/SND/repos/sandbox.sampleservice/</RepositoryUrl> <!-- Documentation URL --> <DocumentationUrl>https://bitbucket.contoso.com/projects/SND/repos/sandbox.sampleservice/browse/README.md</DocumentationUrl> <!-- Your name here --> <Authors>User Name &lt;username@contoso.com&gt;</Authors> <!-- Project description --> <Description>The sample service for demo purposes.</Description> <!-- Bamboo plan key (required for Bamboo Specs --> <BambooBlanKey>SMPL</BambooBlanKey> </PropertyGroup> </Project> 

Beispiel `Directory.Build.props`
 <Project> <Import Condition="Exists('..\SolutionInfo.props')" Project="..\SolutionInfo.props" /> <ItemGroup> <None Include="$(MSBuildThisFileDirectory)/CodeAnalysis/stylecop.json" Link="stylecop.json" CopyToOutputDirectory="Never"/> <PackageReference Include="StyleCop.Analyzers" Version="1.*" PrivateAssets="all" /> </ItemGroup> <PropertyGroup> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/CodeAnalysis/stylecop.ruleset</CodeAnalysisRuleSet> <!-- Enable XML docs generating--> <GenerateDocumentationFile>true</GenerateDocumentationFile> <!-- Enable C# 7.x features --> <LangVersion>latest</LangVersion> <!-- default base version --> <BaseVersion Condition="'$(BaseVersion)' == ''">0.0.0</BaseVersion> <!-- default build number and format --> <BuildNumber Condition="'$(BuildNumber)' == ''">0</BuildNumber> <BuildNumber>$([System.String]::Format('{0:0000}',$(BuildNumber)))</BuildNumber> <!-- default version suffix --> <VersionSuffix Condition="'$(VersionSuffix)' == ''">local</VersionSuffix> <!-- empty version suffix instead of 'prod' --> <VersionSuffix Condition="'$(VersionSuffix)' == 'prod'"></VersionSuffix> <!-- format version prefix --> <VersionPrefix>$(BaseVersion).$(BuildNumber)</VersionPrefix> <!-- disable IsPackable by default --> <IsPackable>false</IsPackable> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> <Company>Contoso</Company> <Copyright>Copyright $([System.DateTime]::Now.Date.Year) Contoso Ltd</Copyright> </PropertyGroup> </Project> 

Montage


Es ist sofort erwĂ€hnenswert, dass sowohl ich als auch meine Kollegen bereits recht erfolgreiche Erfahrungen mit der Verwendung verschiedener Build-Systeme gesammelt haben . Und anstatt ein vorhandenes Tool mit völlig untypischen Funktionen zu gewichten, wurde beschlossen, ein anderes Tool zu erstellen, das auf unseren neuen Prozess spezialisiert ist, und das alte Tool in Ruhe zu lassen, um seine Aufgaben im Rahmen von Legacy-Projekten auszufĂŒhren. Die Fixidee war der Wunsch, ein Tool zu erhalten, das den Code mithilfe eines einzigen Standardprozesses in ein Docker-Image verwandelt, das alle unsere Anforderungen erfĂŒllt, wĂ€hrend Entwickler nicht mehr in die Feinheiten der Assembly eintauchen mĂŒssen, aber die Möglichkeit einer Anpassung beibehalten mĂŒssen.


Die Auswahl eines geeigneten Rahmens hat begonnen. Basierend auf den Anforderungen an die Reproduzierbarkeit des Ergebnisses sowohl auf Build-Maschinen mit Linux als auch auf Windows-Maschinen eines Entwicklers wurden die echte plattformĂŒbergreifende und ein Minimum an vordefinierten AbhĂ€ngigkeiten zu einer SchlĂŒsselbedingung. Zu verschiedenen Zeiten konnte ich einige der Assembly-Frameworks fĂŒr .NET-Entwickler recht gut kennenlernen: von MSBuild und seinen monströsen XML-Konfigurationen, die spĂ€ter in Psake (Powershell) ĂŒbersetzt wurden, bis hin zu exotischem FAKE (F #). Aber diesmal wollte ich etwas Frisches und Leichtes. DarĂŒber hinaus wurde bereits entschieden, dass die Montage und das Testen vollstĂ€ndig in einer isolierten Containerumgebung durchgefĂŒhrt werden sollten, sodass ich nicht vorhatte, etwas anderes als die Docker CLI- und Git-Befehle auszufĂŒhren, dh der grĂ¶ĂŸte Teil des Prozesses sollte in der Docker-Datei beschrieben worden sein.
Zu diesem Zeitpunkt waren sowohl FAKE 5 als auch Cake for .NET Core noch nicht fertig. Bei einer plattformĂŒbergreifenden Entwicklung waren diese Projekte also mittelmĂ€ĂŸig. Aber mein geliebter PowerShell 6 Core wurde bereits veröffentlicht und ich habe ihn in vollem Umfang genutzt. Deshalb habe ich mich entschlossen, mich wieder Psake zuzuwenden, und wĂ€hrend ich mich umdrehte, bin ich auf ein interessantes Invoke-Build- Projekt gestoßen, das ein Umdenken von Psake darstellt und, wie der Autor selbst betont, dasselbe ist, nur besser und einfacher. So ist es. Ich werde im Rahmen dieses Artikels nicht nĂ€her darauf eingehen, ich werde nur bemerken, dass mich die Kompaktheit darin besticht, wenn alle Grundfunktionen fĂŒr diese Produktklasse verfĂŒgbar sind:


  • Die Abfolge der Aktionen wird durch eine Reihe miteinander verbundener Aufgaben (Tasks) beschrieben, die anhand ihrer gegenseitigen AbhĂ€ngigkeiten und zusĂ€tzlichen Bedingungen gesteuert werden können.
  • Es gibt mehrere nĂŒtzliche Hilfsprogramme, z. B. exec {} fĂŒr die korrekte Behandlung von Exit-Codes fĂŒr Konsolenanwendungen.
  • Jede Ausnahme oder Beendigung der Verwendung von Strg + C wird in einem speziellen integrierten Exit-Build-Block korrekt verarbeitet. Dort können Sie beispielsweise alle temporĂ€ren Dateien oder eine Testumgebung löschen oder einen Bericht zeichnen, der fĂŒr das Auge angenehm ist.


Generisches Dockerfile


Die Docker-Datei selbst und die Baugruppe mit docker build bieten relativ schwache Parametrierungsfunktionen, und die FlexibilitĂ€t dieser Tools ist kaum höher als die eines Schaufelgriffs. DarĂŒber hinaus gibt es eine Vielzahl von Möglichkeiten, um das „falsche“ Bild zu groß, zu unsicher, zu unintuitiv oder einfach unvorhersehbar zu machen. GlĂŒcklicherweise bietet die Microsoft-Dokumentation bereits einige Beispiele fĂŒr Dockerfile , mit denen Sie die grundlegenden Konzepte schnell verstehen und Ihr erstes Dockerfile erstellen und spĂ€ter schrittweise verbessern können. Er verwendet bereits ein mehrstufiges Muster und erstellt ein spezielles „ Test Runner “ -Image, um Tests auszufĂŒhren.


Mehrstufiges Muster und Argumente


Der erste Schritt besteht darin, die Montagestufen in kleinere zu unterteilen und neue hinzuzufĂŒgen. Es lohnt sich daher, den Start von dotnet build als separate Phase hervorzuheben, da es fĂŒr Projekte, die nur Bibliotheken enthalten, keinen Sinn macht, dotnet publish . Jetzt können wir nach unserem Ermessen nur die erforderlichen Montageschritte mit ausfĂŒhren
dotnet build --target <name>
Zum Beispiel sammeln wir hier ein Projekt, das nur Bibliotheken enthÀlt. Die Artefakte hier sind nur NuGet-Pakete, was bedeutet, dass es keinen Sinn macht, ein Laufzeit-Image zu erfassen.



Oder wir bauen bereits einen Service auf, aber aus dem Feature-Zweig. Wir brauchen ĂŒberhaupt keine Artefakte einer solchen Baugruppe, es ist nur wichtig, die Tests und den Gesundheitscheck zu bestehen.



Als nĂ€chstes mĂŒssen Sie die Verwendung von Basisbildern parametrisieren. In der Docker-Datei kann die ARG Direktive seit einiger Zeit außerhalb der Erstellungsphasen platziert werden , und die ĂŒbertragenen Werte können im Namen des Basisimages verwendet werden .


 ARG DOTNETCORE_VERSION=2.2 ARG ALPINE_VERSION= ARG BUILD_BASE=mcr.microsoft.com/dotnet/core/sdk:${DOTNETCORE_VERSION}-alpine${ALPINE_VERSION} ARG RUNTIME_BASE=mcr.microsoft.com/dotnet/core/runtime:${DOTNETCORE_VERSION}-alpine${ALPINE_VERSION} FROM ${BUILD_BASE} AS restore ... FROM ${RUNTIME_BASE} AS runtime ... 

So bekamen wir neue und auf den ersten Blick nicht offensichtliche Möglichkeiten. Wenn Sie ein Image mit einer ASP.NET Core-Anwendung erstellen möchten, benötigt das Laufzeit-Image zunĂ€chst ein anderes: mcr.microsoft.com/dotnet/core/aspnet . Der Parameter mit einem nicht standardmĂ€ĂŸigen Basisabbild muss in der Konfiguration des SolutionInfo.props Repositorys gespeichert und wĂ€hrend der Assemblierung als Argument ĂŒbergeben werden. Wir haben es dem Entwickler auch einfacher gemacht, andere Versionen der .NET Core-Images zu verwenden: beispielsweise Vorschauen oder sogar benutzerdefinierte (Sie wissen es nie!).


Zweitens ist die Möglichkeit, die Docker-Datei zu "erweitern", noch interessanter, da sie Teil der VorgĂ€nge in einer anderen Assembly ist, deren Ergebnis bei der Erstellung des Laufzeitabbilds zugrunde gelegt wird. Einige unserer Dienste verwenden beispielsweise JavaScript und Vue.js, deren Code wir in einem separaten Image vorbereiten, indem wir dem Repository einfach eine solche „expandierende“ Docker-Datei hinzufĂŒgen:


 ARG DOTNETCORE_VERSION=2.2 ARG ALPINE_VERSION= ARG RUNTIME_BASE=mcr.microsoft.com/dotnet/core/aspnet:${DOTNETCORE_VERSION}-alpine${ALPINE_VERSION} FROM node:alpine AS install WORKDIR /build COPY package.json . RUN npm install FROM install AS src COPY [".babelrc", ".eslintrc.js", ".stylelintrc", "./"] COPY ClientApp ./ClientApp FROM src AS publish RUN npm run build-prod FROM ${RUNTIME_BASE} AS appbase COPY --from=publish /build/wwwroot/ /app/wwwroot/ 

Sammeln wir dieses Image mit dem Tag, das wir an die Phase des Zusammenstellens des Laufzeitimages des ASP.NET-Dienstes als Argument fĂŒr RUNTIME_BASE ĂŒbergeben. So können Sie die Assembly beliebig erweitern, einschließlich der Parametrisierung dessen, was Sie nicht nur im docker build tun können. Möchten Sie das HinzufĂŒgen von Volume parametrisieren? Einfach:


 ARG DOTNETCORE_VERSION=2.2 ARG ALPINE_VERSION= ARG RUNTIME_BASE=mcr.microsoft.com/dotnet/core/aspnet:${DOTNETCORE_VERSION}-alpine${ALPINE_VERSION} FROM ${RUNTIME_BASE} AS runtime ARG VOLUME VOLUME ${VOLUME} 

Wir starten die Assemblierung dieser Docker-Datei so oft, wie wir VOLUME-Direktiven hinzufĂŒgen möchten. Wir verwenden das resultierende Bild als Basis fĂŒr den Service.


AusfĂŒhren von Tests


Anstatt Tests direkt in der Montagephase auszufĂŒhren, ist es korrekter und bequemer, dies in einem speziellen „Test Runner“ -Container durchzufĂŒhren. Ich möchte kurz auf das Wesentliche dieses Ansatzes eingehen und stelle fest, dass Sie Folgendes tun können:


  • FĂŒhren Sie alle geplanten Starts durch, auch wenn einer von ihnen abstĂŒrzt.
  • HĂ€ngen Sie das Host-Dateisystemverzeichnis in den Container ein, um einen Testbericht zu erhalten, der beim Erstellen des CI-Systems von entscheidender Bedeutung ist.
  • FĂŒhren Sie das Testen in einer temporĂ€ren Umgebung durch, indem Sie den Namen des Netzwerks an den docker run --network <test_network_name> .

Der letzte Absatz bedeutet, dass wir jetzt nicht nur Komponententests, sondern auch Integrationstests ausfĂŒhren können. Wir beschreiben die Umgebung beispielsweise in docker-compose.yaml und fĂŒhren sie fĂŒr den gesamten Build aus. Jetzt können Sie die Interaktion mit der Datenbank oder unserem anderen Dienst ĂŒberprĂŒfen und die Protokolle von ihnen speichern, falls Sie sie fĂŒr die Analyse benötigen.


Wir ĂŒberprĂŒfen immer das resultierende Laufzeitbild, um den Healthcheck zu bestehen, was auch eine Art Test ist. Eine temporĂ€re Testumgebung kann hier nĂŒtzlich sein, wenn der zu testende Dienst AbhĂ€ngigkeiten von seiner Umgebung aufweist.


Ich dotnet build auch fest, dass der Ansatz mit den Runner-Containern, die in der Phase mit dotnet build zusammengebaut wurden, dann gut zum Starten von dotnet publish , dotnet pack und dotnet nuget push . Auf diese Weise können wir Baugruppenartefakte lokal speichern.


Healthcheck und BetriebssystemabhÀngigkeiten


Ziemlich schnell wurde klar, dass unsere standardisierten Dienstleistungen auf ihre Weise immer noch einzigartig sein wĂŒrden. Sie haben möglicherweise unterschiedliche Anforderungen an vorinstallierte Pakete des Betriebssystems im Image und unterschiedliche Methoden zur ÜberprĂŒfung des IntegritĂ€tsprĂŒfens. Und wenn Curl zum ÜberprĂŒfen des Status einer Webanwendung geeignet ist, ist es fĂŒr ein gRPC-Backend oder darĂŒber hinaus fĂŒr einen kopflosen Dienst nutzlos und ein zusĂ€tzliches Paket im Container.


Um Entwicklern die Möglichkeit zu geben, das Image anzupassen und seine Konfiguration zu erweitern, verwenden wir die Vereinbarung fĂŒr mehrere spezielle Skripte, die im Repository neu definiert werden können:


 .scripts ├── healthcheck.sh ├── run.sh └── runtime-deps.sh 

Das Skript healthcheck.sh enthĂ€lt die Befehle, die zum ÜberprĂŒfen des Status erforderlich sind:


  • FĂŒr das Web mit Curl:


     #!/bin/ash set –e curl -sIf -o /dev/null -w "%{http_code}\n" 127.0.0.1/health || exit 1 

  • Andere Dienste, die unser eigenes Dienstprogramm cli verwenden:


     #!/bin/ash set –e healthcheck || exit 1 


Mit runtime-deps.sh werden AbhĂ€ngigkeiten installiert und bei Bedarf alle anderen Aktionen auf dem Basisbetriebssystem ausgefĂŒhrt, die fĂŒr das normale Funktionieren der Anwendung im Container erforderlich sind. Typische Beispiele:


  • FĂŒr eine Webanwendung:


     #!/bin/ash apk add --no-cache curl icu-libs 

  • FĂŒr den gRPC-Service:


     #!/bin/ash apk add --no-cache libc6-compat 


Daher ist die Art und Weise, wie AbhĂ€ngigkeiten verwaltet und der Status ĂŒberprĂŒft werden, standardisiert, es besteht jedoch Raum fĂŒr eine gewisse FlexibilitĂ€t. Was run.sh , so ist es weiter.


Einstiegspunkt-Skript


Ich bin sicher, dass sich jeder, der mindestens einmal seine Docker-Datei geschrieben hat, gefragt hat, welche Direktive er verwenden soll - CMD oder ENTRYPOINT . DarĂŒber hinaus verfĂŒgen diese Teams ĂŒber zwei Syntaxoptionen, die sich am dramatischsten auf das Ergebnis auswirken. Ich werde den Unterschied nicht im Detail erklĂ€ren und nach denen wiederholen, die bereits alles geklĂ€rt haben . Ich empfehle nur daran zu denken, dass es in 99% der Situationen richtig ist, ENTRYPOINT und die Exec-Syntax zu verwenden:


ENTRYPOINT ["/ path / to / ausfĂŒhrbare Datei"]


Andernfalls kann die gestartete Anwendung Betriebssystembefehle wie SIGTERM usw. nicht korrekt verarbeiten, und Sie können auch in Form von Zombie-Prozessen und allem, was mit dem PID 1-Problem zusammenhĂ€ngt , in Schwierigkeiten geraten. Was aber, wenn Sie den Container starten möchten, ohne die Anwendung zu starten? Ja, Sie können den Einstiegspunkt ĂŒberschreiben:
docker run --rm -it --entrypoint ash <image_name> <params>
Es sieht nicht allzu bequem und intuitiv aus, oder? Aber es gibt gute Neuigkeiten: Sie können es besser machen! Verwenden Sie nĂ€mlich ein Einstiegspunktskript . Mit einem solchen Skript können Sie beliebig komplexe ( Beispiel- ) Initialisierungen, die Verarbeitung von Parametern und alles, was Sie möchten, durchfĂŒhren.


In unserem Fall wird standardmĂ€ĂŸig das einfachste, aber gleichzeitig funktionale Szenario verwendet:


 #!/bin/sh set -e if [ ! -z "$1" ] && $(command -v $1 >/dev/null 2>&1) then exec $@ else exec /usr/bin/dotnet /app/${ENTRY_PROJECT}.dll $@ fi 

Damit können Sie den Start des Containers sehr intuitiv steuern:
docker run <image> env - fĂŒhrt nur env im Bild aus und zeigt Umgebungsvariablen an.
docker run <image> -param1 value1 - Starten Sie den Dienst mit den angegebenen Argumenten.


UnabhĂ€ngig davon mĂŒssen Sie auf den Befehl exec achten: Wenn er vor dem Aufruf der ausfĂŒhrbaren Anwendung vorhanden ist, erhĂ€lt er die begehrte PID 1 in Ihrem Container.


Was sonst


NatĂŒrlich hat das Build-System in mehr als anderthalb Jahren viele verschiedene Funktionen gesammelt. Neben der Verwaltung der Startbedingungen in verschiedenen Phasen, der Speicherung von Artefakten, der Versionierung und anderen Funktionen wurde auch unser „Standard“ fĂŒr den Container entwickelt. Es wurde mit wichtigen Attributen gefĂŒllt, die es vorhersehbarer und administrativ bequemer machen:


  • Alle erforderlichen Bildbezeichnungen sind installiert: Versionen, Versionsnummern, Links zur Dokumentation, Autor und andere.
  • Im Laufzeitcontainer wird die NLog-Konfiguration neu definiert, sodass nach der Veröffentlichung alle Protokolle sofort in strukturierter Form mit json dargestellt werden, dessen Version versioniert ist.
  • Statische Analyseregeln und andere Standards werden automatisch auf dem neuesten Stand gehalten.

Ein solches Tool kann natĂŒrlich immer verbessert und weiterentwickelt werden. Es hĂ€ngt alles von den BedĂŒrfnissen und der Vorstellungskraft ab. ZusĂ€tzlich zu allem war es beispielsweise möglich, zusĂ€tzliche CLI-Dienstprogramme in ein Image zu packen. Der Entwickler kann sie einfach in das Image einfĂŒgen und in der Konfigurationsdatei nur den erforderlichen Dienstprogrammnamen und den Namen des .NET-Projekts healthcheck aus dem es zusammengestellt werden soll (z. B. unser healthcheck ).


Fazit


Hier wird nur ein Teil eines integrierten Standardisierungsansatzes beschrieben. Die Dienste selbst, die aus unseren Vorlagen erstellt werden, blieben hinter den Kulissen und sind daher durch viele Kriterien, wie einen einzigen Konfigurationsansatz, allgemeine Methoden fĂŒr den Zugriff auf Metriken, Codegenerierung usw., ziemlich einheitlich. . , .


, Linux , - . , , . , , , Code Style, , .


, ! , « », , . , Docker .

Source: https://habr.com/ru/post/de468683/


All Articles