<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[PitschersWelt]]></title><description><![CDATA[Komplexes einfach erklärt.]]></description><link>https://pitscher.com/</link><image><url>https://pitscher.com/favicon.png</url><title>PitschersWelt</title><link>https://pitscher.com/</link></image><generator>Ghost 3.37</generator><lastBuildDate>Thu, 07 Dec 2023 02:35:15 GMT</lastBuildDate><atom:link href="https://pitscher.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[B4ckdoor-owned-you]]></title><description><![CDATA[Das Grundrauschen des Internets ist normalerweise kein Grund zur Sorge - sofern man sich mit den Hintergründen befasst. Betreibt man über das Internet erreichbare Systeme erfordert das ständige Wachsamkeit. Ein treffendes Beispiel hierfür ist eines meiner Erlebnisse aus der Vergangenheit.]]></description><link>https://pitscher.com/b4ckdoor-owned-you/</link><guid isPermaLink="false">62815e256e82e80a27c54a4c</guid><category><![CDATA[Security]]></category><dc:creator><![CDATA[Hannes]]></dc:creator><pubDate>Sat, 27 Jun 2020 15:15:00 GMT</pubDate><media:content url="https://cdn.gdelivr.net/pw/images/2020/06/b4ckdoor-owned-you-cover.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://cdn.gdelivr.net/pw/images/2020/06/b4ckdoor-owned-you-cover.jpg" alt="B4ckdoor-owned-you"><p>Das Grundrauschen des Internets ist normalerweise kein Grund zur Sorge - sofern man sich mit den Hintergründen befasst. Betreibt man über das Internet erreichbare Systeme, erfordert das ständige Wachsamkeit. Ein treffendes Beispiel hierfür ist eines meiner Erlebnisse aus der Vergangenheit.</p><h2 id="das-daily-business">Das Daily Business</h2><p>Wirf man einen flüchtigen Blick auf die bereits existierenden Beiträge auf diesem Blog, kann man erahnen, dass ich mich neben einigen anderen Themen auch mit Servern beschäftige. Das Planen und anschließende Verwirklichen einer IT-Infrastruktur bereitet mir große Freude.</p><p>Unabhängig davon, ob es sich um ein privates Projekt oder eines für meinen Arbeitgeber handelt - meist spielen Server eine zentrale Rolle. Natürlich bin ich kein ausgebildeter IT-Systemadministrator, würde allerdings von mir behaupten, die ein oder andere wertvolle Erfahrung gemacht zu haben. Eine dieser Erfahrungen schreibe ich in Form dieses Beitrags nieder und hoffe damit, einen kleinen Denkanstoß geben zu können.</p><h2 id="die-ausgangslage">Die Ausgangslage</h2><p>Aktuell arbeite ich mit einer ganzen Reihe geschätzter Kollegen und Kommilitonen an einigen Projekten. Da der Vorfall über den ich hier berichte zwei unterschiedliche aber dennoch verwandte Systeme betraf, folgt hier eine kurze Auflistung der Fakten:</p><h3 id="projekt-a">Projekt A</h3><ul><li>Infrastruktur betrieben auf der Open Telekom Cloud (OTC)</li><li>Komponente: 1 Elastic Cloud Server (ECS-Instanz) mit einer Linux-Distribution</li><li>Software-Stack: Docker, Nginx, NodeJS, MongoDB</li><li>Eine Subdomain einer Root-Domain zeigt auf IPv4 der ECS-Instanz, Bsp.: projekt-a.example.com</li></ul><h3 id="projekt-b">Projekt B</h3><ul><li>Infrastruktur betrieben auf der Open Telekom Cloud (OTC)</li><li>Komponente: 1 Elastic Cloud Server (ECS-Instanz) mit einer Linux-Distribution</li><li>Software-Stack: Docker, Nginx, NodeJS, MongoDB</li><li>Eine Subdomain einer Root-Domain zeigt auf IPv4 der ECS-Instanz, Bsp.: projekt-b.example.com</li></ul><h2 id="was-ist-passiert">Was ist passiert?</h2><p>An einem Montagnachmittag vor drei Wochen hatte ich auf Bitten des Teams von Projekt B damit begonnen, die Infrastruktur für die zu entwickelnde Software einzurichten. Keine Rocket Science. Eine neue ECS-Instanz war rasch erstellt und eingerichtet. Um den Verwaltungsaufwand zu minimieren noch schnell (eine maßlose Untertreibung) die CI/CD-Pipeline basierend auf GitLab.com eingerichtet und fertig. Sobald die Entwickler neue Funktionen erstellt hatten, konnten Sie durch einen Merge den Deployment-Prozess auslösen. Der Code wurde - vereinfacht gesagt -  automatisch auf den Server der OTC ausgerollt und die Änderungen konnten live eingesehen werden.</p><p>Wie aus der obigen Auflistung schon ersichtlich wurde, nutzen wir als Webserver einen Nginx der gleichzeitig als Reverse Proxy agiert und die Anfragen einer bestimmten Domain an die hinter ihm liegende NodeJS-App weiterleitet. Diese greift bei Bedarf auf die MongoDB-Datenbank zu. Diese drei Dienste/Programme laufen in eigenen Docker-Containern. Gibt es also ein Problem, kann ich mir die Logs jedes Containers anzeigen lassen. Gesagt getan. Natürlich trat ein Problem auf und ich durchstöberte die Logs der einzelnen Services. Erfahrene Systemadministratoren und IT-Professionals werden bei den nun folgenden Schilderungen sicher schmunzeln.</p><p>Mein "Nervositätsgrad" beginn aus heiterem Himmel anzusteigen, als ich in den Logs des Nginx-Webservers diesen Eintrag sah:</p><pre><code class="language-nginx">webapp    | 77.81.xxx.xxx - - [08/Jun/2020:18:57:31 +0000] "POST /cgi-bin/ViewLog.asp HTTP/1.1" 301 169 "-" "B4ckdoor-owned-you" "-"
webapp    | 77.81.xxx.xxx - - [08/Jun/2020:18:57:31 +0000] "7%3b%23&amp;remoteSubmit=Save" 400 157 "-" "-" "-"</code></pre><p>Analysieren wir kurz, was passiert ist: Ein Gerät mit der IP-Adresse 77.81.xxx.xxx hat versucht, über einen HTTP POST-Request auf die Datei "ViewLog.asp" die Inhalte selbiger zu verändern. Aus irgendeinem Grund wurde der <a href="https://developer.mozilla.org/de/docs/Web/HTTP/Headers/User-Agent">User Agent</a> auf den Wert "B4ckdoor-owned-you" geändert. Normalerweise gibt diese Angabe einige Informationen über das anfragende Gerät und den verwendeten Browser weiter. So sieht der User Agent meines Chrome-Browsers der auf meinem Windows 10 Rechner läuft wie folgt aus:<br><code>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36</code></p><p>Wer auch immer diese Anfrage geschickt hat, wollte also eine ganz bestimmte Datei auf dem Server verändern und diese Dreistigkeit noch nicht einmal verbergen.</p><p>Dieser Logeintrag war zu allem Überfluss immer und immer wieder in den Nginx-Logs zu sehen. Der einzige Unterschied war der Ursprung der Anfrage - die IP-Adresse. Also nur Spam? Nicht ganz. Zugegeben war ich etwas nervös und habe schnell die Logs des Webservers von Projekt A (also der anderen ECS-Instanz) überprüft. Auch hier waren die selben Einträge zu finden. Okay. Anscheinend wird fieberhaft versucht, den Inhalt einer auf beiden Servern nicht existierenden Datei zu verändern. Merkwürdig.<br>Da beide Webserver diese Anfragen mit dem HTTP-Fehlercode 400 (Bad Request) abgelehnt haben, bestand also kein Grund zur Sorge. Dachte ich.</p><p>Beide Server laufen in der Open Telekom Cloud und hatten jeweils eine IPv4-Adresse die dicht beieinander lagen. Um mich abzusichern, habe ich direkt nach dem Sichten der Logs einen meiner Kollegen angeschrieben, von dem ich wusste, dass er ebenfalls einige Server in der Open Telekom Cloud betreibt. Allerdings wurde keine seiner Maschinen derart "angegriffen". Also doch ein zielgerichteter Angriff auf mich?</p><p>Die Rückmeldung meines Kollegen hat meinen Nervositätsgrad auf einen unangenehmen Stand angehoben. Da Vorsicht ja bekanntlich besser als Nachsicht ist, exportierten der angesprochene Kollege und ich also auf beiden Servern der Projekte A und B die Logs aller wichtigen Dienste (wie Nginx, NodeJS-App und MongoDB). Unmittelbar danach haben wir beide Maschinen durch die Firewall der OTC abgeschottet und gänzlich vom Netz genommen. Mittlerweile war es schon spät und ich informierte beide Teams über den Vorfall.</p><h2 id="rat-einholen">Rat einholen</h2><p>Es war der nächste Morgen und mein Handy zeigte viele neuen Nachrichten in Abwesenheit an. Nachdem ich dann endlich im Büro angekommen war und alle offenen Fragen beantwortet hatte, schaute ich mir die Logs erneut an. Ein Zugriff auf die NodeJS-Anwendung (geschweige denn die MongoDB) fand nicht statt. Nur auf den Webserver wurde in der oben beschriebenen Art zugegriffen.</p><p>Mein Arbeitgeber hat - wie wohl jede größere Firma - eine ganze Reihe von fachkundigen Kollegen, die sich tagein tagaus mit IT-Security beschäftigen. Dort konnte sich jeder im Notfall professionelle Unterstützung einholen. Eine E-Mail mit den Logs im Anhang war rasch erstellt und abgeschickt. Nach kurzer Zeit rief mich einer der Kollegen an und klärte mich etwas auf.</p><p>Laut ihm ist dieser Angriff bereits seit längerem bekannt. Genauer handelte es sich bei den anfragenden/angreifenden Geräten um Router der Firma ZyXEL. Hier in Deutschland sind die Router der Firma nicht wirklich weit verbreitet. Ganz im Gegensatz zu den Niederlanden. Dort gibt es einige Internet Service Provider (wie bei uns die Telekom, Vodafone, und Co.), die ihren Kunden ZyXEL-Router für den Heimgebrauch zur Verfügung stellen.</p><p>Ein Wurm hatte anscheinend einige dieser Router infiziert und versucht, sich weiter zu verbreiten. Die Intention eines Wurm ist es hierbei, sich vorerst (im Gegensatz zu beispielsweise Ransomeware) eher unbemerkt zu verbreiten. Es sollen also möglichst viele Systeme infiziert werden, bevor fortgefahren wird. Bereits infizierte Systeme dienen dabei als Verbreiter. Es entsteht eine Art Kettenreaktion.</p><p>Ein Wurm der explizit nach einer Schwachstelle in ZyXEL-Routern gesucht hat, sorgte also für diese unerwünschten Zugriffe. Noch vor diesem Tag fand ich diese Rahmeninformationen mit einer raschen Websuche heraus - war mir aber nicht ganz sicher, ob ich richtig lag.<br>Eine genauere Beschreibung der ausgenutzten Schwachstelle gibt es <a href="https://nvd.nist.gov/vuln/detail/CVE-2017-18368">hier</a>.</p><h3 id="warum-diese-beiden-server">Warum diese beiden Server?</h3><p>Der Wurm hatte bereits eine ganze Reihe von Routern infiziert. Diese suchten ihrerseits nun weiter nach verwundbaren Geräten im Netz. In­te­r­es­san­ter­wei­se wurden lediglich die beiden Server der Projekte A und B angegriffen - nicht aber die Infrastruktur hinter der eingangs erwähnten Root-Domain. Daraus lässt sich schlussfolgern, dass die Domains in diesem Fall keine Rolle gespielt haben.</p><p>Nachdem der Wurm also einen Router erfolgreich infizieren konnte und nun dieser versucht weitere Systeme zu infizieren, kommt es zu einem Abprüfen des IP-Adressraums der jeweiligen Router. Jeder Infizierte prüft folglich die Systeme in "seiner Nähe" auf diese Schwachstelle. Ist eine solche vorhanden, kann sie ausgenutzt werden. Laut dem Kollegen der IT-Security liegt der IPv4-Adressraum eines niederländischen Internet Service Providers (80.126.0.0 - 80.126.255.255) in unmittelbarer Nähe zu dem Adressraum der Open Telekom Cloud. Wie es der Zufall wollte, wurde ich damit ungewollt zum Ziel dieses automatisierten Angriffs.</p><p>Da ich keine ZyXEL-Router betrieben habe, ist dieser Angriff also im Endeffekt relativ unbedenklich und wurde auch von besagtem Kollegen als "Grundrauschen" betitelt.</p><h2 id="lessons-learned">Lessons Learned</h2><p>Eine Gefahr zu unterschätzen ist meist deutlich gefährlicher, als sie zu überschätzen. Ein eigener Server ist schnell und einfach aufgesetzt und eingerichtet. Nur sollte man am Ende des Tages nicht nur die Wünsche der "netten" Nutzer sondern auch die Vorhaben der Nutzer mit weniger guten Absichten beachten.</p><p>Einen Server kann man sich wie das eigene Haus oder die eigene Wohnung vorstellen:<br>Wenn ich gehe, schließe ich nicht nur die Tür als offensichtlichen Eingang, sondern auch die Fenster. Da meine Adresse meist kein Geheimnis ist, muss ich damit rechnen, hin und wieder Besuch (habe er nun gute oder böse Absichten) zu erhalten. Trotz alledem fühle ich mich wohl, weil ich der Besitzer bin und die Hoheit über mein Haus bzw. meine Wohnung habe. Damit dieser Umstand nicht gefährdet wird, halte ich einfache Regeln und Verhaltensweisen ein und sorge damit für eine höhere Sicherheit.</p><p>Meinen Server schotte ich deshalb mit einer Firewall vor der Außenwelt ab. Sofern ich Dienste wie einen Webserver von außerhalb erreichbar mache, muss ich auch für seine Sicherheit Sorge tragen.</p><p>Bevor man nun ganz wild mit der Suche nach Server-Security-Anleitungen beginnt, hilft ein regelmäßiger Blick mit einem wachsamen Auge in die Logdateien der einzelnen Services weiter. Ich spreche aus Erfahrung.</p>]]></content:encoded></item><item><title><![CDATA[SSH-Dienst mit Knockd verstecken]]></title><description><![CDATA[SSH ist das Eintrittstor zu so gut wie jedem Linux-basierten Server. Umso wichtiger ist es, diesen Dienst so gut es geht abzusichern. Mit Knockd können wir ihn vor unberechtigten Zugriffen schützen, indem wir ihn verstecken.]]></description><link>https://pitscher.com/knockd/</link><guid isPermaLink="false">62815e256e82e80a27c54a4b</guid><category><![CDATA[Server]]></category><dc:creator><![CDATA[Hannes]]></dc:creator><pubDate>Tue, 03 Dec 2019 17:53:32 GMT</pubDate><media:content url="https://cdn.gdelivr.net/pw/images/2019/12/knockd-thumbnail.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://cdn.gdelivr.net/pw/images/2019/12/knockd-thumbnail.jpg" alt="SSH-Dienst mit Knockd verstecken"><p>SSH ist das Eintrittstor zu so gut wie jedem Linux-basierten Server. Umso wichtiger ist es, diesen Dienst so gut es geht abzusichern. Mit Knockd können wir ihn vor unberechtigten Zugriffen schützen, indem wir ihn verstecken.</p><p>Mit einem eigenen Server ist vieles machbar. Schnell sind alle gewünschten Anwendungen bzw. Services installiert und es kann losgehen.<br>Auf dem überwiegenden Teil aller Server, die über das Internet erreichbar sind, läuft eine Linux-Distribution. Für Administratoren ist insbesondere der Schutz solcher Systeme von höchster Priorität. Deshalb empfiehlt sich u. a. der Einsatz einer Firewall. Damit können wir einen Server nach außen abschotten. Natürlich müssen wir dennoch ein paar "Löcher" bzw. Ports offen lassen. Wollen wir eine Webseite betreiben, ist der Einsatz eines Webservers erforderlich. Damit von außen auf die Seite zugegriffen werden kann, müssen Anfragen an den Port 80 (für HTTPS auch 443) in der Firewall erlaubt werden.</p><p>Jeder fängt einmal klein an. So vergessen gerade Einsteiger, überhaupt eine Firewall einzusetzen. Viel gefährlicher ist es allerdings, sich nach der Aktivierung einer Solchen entspannt zurückzulehnen. Einfach schnell einer Anleitung aus dem Internet folgen und ein paar Ports freischalten - fertig. Wäre es doch nur so einfach.<br>Um sich nicht selber auszusperren, muss bei Linux-Servern der Port 22 freigeschaltet werden. Unter diesem ist der SSH-Daemon erreichbar. Über ihn kann einfach und per Kommandozeile auf den Server zugegriffen werden.</p><p>Vor Kurzem habe ich im Rahmen einer Präsentation einen Server aufgesetzt. Geschockt hat mich, dass es keine fünf Minuten gedauert hat, bis in den Logs erste fehlgeschlagene Anmeldeversuche auf den Port 22 (also den SSH-Service) zu sehen waren. Diese waren natürlich nicht von mir. Jedenfalls war ich zu diesem Zeitpunkt nicht in China unterwegs. Tatsächlich ist es allseits bekannt, dass große Botnetze ganze IP-Adressräume prüfen. Hin und wieder versuchen sie, sich auf Maschinen über interessante Ports, wie 22 für SSH, aufzuschalten. Leider haben sie damit auch das ein oder andere Mal Erfolg.</p><h2 id="unter-dem-radar-fliegen">Unter dem Radar fliegen</h2><p>Eine Möglichkeit, sich vor solchen Angriffen zu schützen, ist es, die Standardeinstellungen von wichtigen Diensten wie SSH etwas anzupassen. So könnte beispielsweise der Port 22 geändert werden. Ein angepasster Port könnte zwischen <a href="https://www.sciencedirect.com/topics/computer-science/registered-port">1024 und 49151</a> liegen. Die Ports 0 bis 1023 sind "Well-Known Ports" und sollten für derartige Anpassungen nicht genutzt werden.</p><p>Diese Maßnahme stellt ein zusätzliches Hindernis dar, welches potenzielle Angreifer überwinden müssten, um unseren Server zu übernehmen. Allerdings haben sie diverse Möglichkeiten, unseren Täuschungsversuch zu umgehen. Mit einem Port-Scan könnten sie herausfinden, unter welchem Port der SSH-Dienst tatsächlich erreichbar ist. Ein solcher Scan ist z.B. mit dem Tool <a href="https://www.informatik-aktuell.de/betrieb/sicherheit/nmap-portscanner-hacking-tool-alleskoenner-teil-i.html">Nmap</a> rasch durchgeführt. Einen fremden Server ohne das "Okay" des Administrators auf offene Ports zu untersuchen, ist eine rechtliche Grauzone. Spätestens mit dem Ausnutzen einer Sicherheitslücke oder der Überlastung des Zielsystems handelt es sich um eine Straftat. Administratoren haben viele Möglichkeiten, einen Port-Scan zu enttarnen, während er noch läuft - z.B. indem die Logs der Firewall laufend auf verdächtige Anfragen überprüft werden.</p><h2 id="port-knocking-mit-knockd">Port Knocking mit Knockd</h2><p>Eine Firewall kann uns vor Angriffen gut abschirmen. Allerdings müssen neben den Ports der öffentlich zugänglichen Dienste (wie 80 und 443 für einen Webserver) auch einige andere wichtige Ports offen sein (Bsp.: 22 für SSH).</p><h3 id="grundlegende-erkl-rung">Grundlegende Erklärung</h3><p>Knockd erlaubt es uns, Dienste wie SSH gegenüber der Außenwelt zu verstecken. Es ist also nicht erkennbar, ob auf unserem Server ein SSH-Dienst läuft, geschweige denn, unter welchem Port er erreichbar ist. Knockd besteht aus dem Client-Programm "Knock" (läuft auf unserem Rechner) und dem zugehörigen Daemon "Knockd" (läuft auf unserem Server).</p><p>Standardmäßig lassen wir den Port des SSH-Dienstes von der Firewall blockieren. Von außen kann nun nicht über SSH auf unseren Server zugegriffen werden. Wollen wir uns verbinden, starten wir Knock auf unserem Computer. Das Programm versucht, auf im Vorhinein definierte Ports auf dem Zielsystem zuzugreifen.</p><p>Der Daemon Knockd auf unserem Server überprüft im Hintergrund durchgehend die Firewall-Logs. Wenn eine IP-Adresse in den konfigurierten Zeitabständen auf die definierten Ports zugreift (also anklopft = knock), wird der Port des SSH-Dienstes in der Firewall für den Anklopfenden geöffnet. Wir können uns anschließend mit dem Server über SSH verbinden. Haben wir unsere Aufgaben erledigt, wird der Port wieder geschlossen.</p><h2 id="einrichtung">Einrichtung</h2><p>Um Knockd verwenden zu können, benötigen wir eine Firewall. Hier nutzen wir "iptables". Anschließend installieren wir Knockd auf dem Server und Knock auf unserem Computer.</p><p><strong>ACHTUNG</strong><br>Änderungen der Firewall-Konfiguration und des SSH-Dienstes sollten mit Ruhe und Bedacht erfolgen. Es besteht die Gefahr, sich aus dem eigenen System auszusperren.</p><h2 id="iptables-konfiguration">Iptables Konfiguration</h2><p>Der Linux-Kernel bringt von Haus aus die Firewall "Netfilter" mit. Sie kann mit Tools wie "iptables" oder "ufw" konfiguriert werden. Iptables ist mächtiger als ufw - aber gleichzeitig komplexer. Unsere erstellten Regeln werden beispielsweise nicht über Neustarts hinweg gespeichert (Ein Programm bietet hier Abhilfe).</p><p>Die folgenden Befehle beziehen sich auf die Verwaltung von Firewall-Regeln für IPv4. Ist der eigene Server auch unter IPv6 erreichbar, müssen alle folgenden Befehle analog mit <code>ip6tables</code> ausgeführt werden!</p><p><strong>Iptables &amp; weitere Tools installieren</strong> (falls nicht bereits vorhanden)<br><code>sudo apt-get install iptables</code><br>Das Paket <code>ip6tables</code> ist in <code>iptables</code> enthalten. Es muss also nicht gesondert installiert werden.</p><p><strong>Default ACCEPT Policy hinzufügen </strong>(solange Konfiguration nicht endgültig)<br>Die bereits vorhandene Konfiguration kann mit <code>sudo iptables -S</code> ausgegeben werden.</p><p><code>sudo iptables -P INPUT ACCEPT</code><br><code>sudo iptables -P OUTPUT ACCEPT</code><br><code>sudo iptables -P FORWARD ACCEPT</code><br><code>sudo iptables -F</code> (bereits vorhandene Regeln löschen - außer die Default Policy)<br>Falls keine von uns im Folgenden festgelegte Regel zutrifft, werden Datenpakete angenommen. Das minimiert die Gefahr, sich aus dem eigenen System auszusperren.<br>Wenn die Konfiguration endgültig ist, sollte als Default Policy "DROP" gesetzt werden.</p><p><strong>Localhost freischalten</strong><br><code>sudo iptables -I INPUT 1 -i lo -j ACCEPT</code><br>Programme auf unserem Server müssen hin und wieder untereinander Daten austauschen. Wir müssen unserer Firewall also mitteilen, dass sie den Datenverkehr des Loopback Devices (lokalen Datenverkehr) nicht blockieren soll. Das ist sehr wichtig. Ohne diese Regel funktioniert unser Betriebssystem nicht korrekt und es kann zum Absturz der Maschine kommen.</p><p><strong>ICMP freischalten</strong><br><code>sudo iptables -A INPUT -p icmp -j ACCEPT</code><br>Falls IPv6 verwendet wird:<br><code>sudo ip6tables -A INPUT -p icmpv6 -j ACCEPT</code><br>Das Internet Control Message Protocol (ICMP) bietet wichtige Funktionalitäten wie Ping. Insbesondere der neue Standard IPv6 nutzt dieses Protokoll intensiv. Würden wir Anfragen für IPv6 nicht freischalten, kann es zu Störungen beim Routen von Datenpaketen kommen.</p><p><strong>Aktuelle SSH-Verbindung zulassen</strong><br><code>sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT</code><br>Alle bereits offenen Verbindungen und neue Verbindungen, die zu den bereits Offenen gehören, werden erlaubt.</p><p><strong>SSH-Port freischalten</strong><br><code>sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT</code><br>Anfragen von außen auf den Port 22 werden erlaubt.<br>Mit diesem Befehl können natürlich auch weitere Dienste freigeschaltet werden. Einfach die Port-Nummer entsprechend anpassen und die Regel hinzufügen.</p><p><strong>Alle konfigurierten Regeln anzeigen</strong><br><code>sudo iptables -S</code><br>Es werden alle konfigurierten Regeln angezeigt. An dieser Stelle unbedingt auf eventuelle Tippfehler kontrollieren!</p><h3 id="funktion-pr-fen">Funktion prüfen</h3><p>Die aktuell geöffnete SSH-Session lassen wir offen. Um zu überprüfen, ob - wie gewünscht - neue Verbindungen aufgebaut werden können, versuchen wir, uns erneut mit unserem Server zu verbinden. Wenn das funktioniert, ist die Konfiguration korrekt. Sollte das nicht der Fall sein, sind wir über die noch offene Session mit dem Server verbunden und können vorhandene Fehler beheben.</p><p>Arbeitet die Firewall korrekt, setzen wir eine "Default DROP Policy". Eingehender und/oder ausgehender Datenverkehr wird, sofern keine von uns erstellte Regel zutrifft, abgewiesen. Der Einfachheit halber konfigurieren wir diese strikte Policy so, dass sie für den eintreffenden Datenverkehr gilt. Programme auf unserem Server können dadurch weiterhin nach außen kommunizieren. Um aber von außen auf sie zuzugreifen, ist eine entsprechende Regel für die Firewall (siehe SSH-Port freischalten) erforderlich.</p><p><strong>Default DROP Policy hinzufügen</strong><br><code>sudo iptables -P INPUT DROP</code><br>Ab dem Setzen dieser Policy ist Vorsicht geboten, wenn die Regeln der Firewall geändert werden. Bevor die Firewall-Einstellungen geändert werden, sollte die DROP zu einer ACCEPT Policy geändert werden:<br><code>sudo iptables -P INPUT ACCEPT</code><br>Danach unbedingt wieder die DROP Policy setzen! <code>sudo iptables -P INPUT DROP</code></p><h3 id="ist-der-server-auch-via-ipv6-erreichbar">Ist der Server auch via IPv6 erreichbar?</h3><p>Wenn das der Fall ist, müssen nun alle bisher aufgeführten Schritte mit dem Tool <code>ip6tables</code> ausgeführt werden. Wie oben erwähnt, ändert sich die Syntax der eigentlichen iptables-Befehle nicht. Die Regeln werden diesmal nicht mit <code>iptables</code>, sondern mit <code>ip6tables</code> erstellt.</p><p>Wenn die Befehle ausgeführt wurden, sollte die Bash-History wie folgt aussehen:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://cdn.gdelivr.net/pw/images/2019/12/iptables_und_ip6tables_bash_history.png" class="kg-image" alt="SSH-Dienst mit Knockd verstecken"><figcaption>Bash History der iptables &amp; ip6tables Konfiguration</figcaption></figure><h3 id="regeln-nach-neustart-automatisch-laden">Regeln nach Neustart automatisch laden</h3><p><code>sudo apt-get install iptables-persistent</code><br>Nach jedem Neustart des Systems erstellt der Dienst automatisch die konfigurierten Regeln. Das ist erforderlich, da iptables die erstellten Regeln nicht persistent abspeichert.</p><p>Während der Installation fragt das Programm, ob alle aktuell vorhandenen Regeln automatisch geladen werden sollen. Dem muss zugestimmt werden. Zusätzlich wird gefragt, ob auch IPv6-Regeln gespeichert werden sollen. Auch hier muss zugestimmt werden, sofern der Server unter IPv6 erreichbar ist und entsprechende Regeln im Vorhinein erstellt wurden. Der Dienst "iptables-persistent" wird automatisch nach einem Neustart gestartet und erstellt alle aktuell definierten Firewall-Regeln.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://cdn.gdelivr.net/pw/images/2019/12/iptables-persistent-ipv4.png" class="kg-image" alt="SSH-Dienst mit Knockd verstecken"><figcaption>iptables-persistent IPv4 Konfiguration</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://cdn.gdelivr.net/pw/images/2019/12/iptables-persistent-ipv6.png" class="kg-image" alt="SSH-Dienst mit Knockd verstecken"><figcaption>iptables-persistent IPv6 Konfiguration</figcaption></figure><p>Sollten die iptables-Regeln einmal verändert werden, muss (nach den Änderungen der Firewall) das Tool iptables-persistent darüber informiert werden:<br><code>sudo dpkg-reconfigure iptables-persistent</code>.<br>Es erscheinen die gleichen Abfragen wie bei der Installation. Beide müssen mit <code>&lt;Yes&gt;</code> beantwortet werden. Nun wurden die Änderungen gespeichert.</p><p>Bitte nach der Einrichtung bzw. Änderung unbedingt prüfen, ob alle Regeln wie gewünscht nach einem Neustart <code>sudo reboot</code> übernommen wurden!</p><p></p><h2 id="knockd-konfiguration">Knockd Konfiguration</h2><p>Installation mit: <code>sudo apt-get install knockd</code>.</p><p>Die Konfiguration des Daemons kann anschließend in der Datei <code>/etc/knockd.conf</code> angepasst werden.<br>Knockd wird nach einem Neustart des Systems automatisch gestartet. Sollten jedoch Änderungen an der Konfiguration des Daemons vorgenommen werden, muss er unbedingt neugestartet werden: <code>sudo service knockd restart</code>.</p><h3 id="beispiel-1">Beispiel 1</h3><p>Die folgende Konfiguration weist Knockd an, den SSH-Port für die anklopfende IP zu öffnen und ihn nach Ausführen der zweiten Sequenz wieder zu schließen. Da die Firewall lediglich unsere IP-Adresse für den Port 22 freischaltet, kann sich kein anderer Rechner mit unserem Server verbinden. Sollte sich unsere IP-Adresse ändern (i.d.R. passiert das alle 24 Std.) könnte theoretisch jemand der unsere alte Adresse erhält, auf den Server zugreifen. Dieses Szenario ist sehr unwahrscheinlich, dennoch sorgt die zweite Konfiguration von Knockd (siehe Beispiel 2) hier für Abhilfe.</p><pre><code class="language-bash">[options]
    UseSyslog
    Interface = eth0
    logfile = /var/log/knockd.log

[openSSH]
    sequence = 4000,5000,6000
    seq_timeout = 10
    tcpflags = syn
    command = /sbin/iptables -A INPUT -s %IP% -p tcp --dport 22 -j ACCEPT

[closeSSH]
    sequence = 7000,8000,9000
    seq_timeout = 10
    tcpflags = syn
    command = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT</code></pre><p></p><h3 id="beispiel-2">Beispiel 2</h3><p>Wie im obigen Beispiel, lässt Knockd nach erfolgreichem Anklopfen den Port 22 für die anfragende IP-Adresse öffnen. Allerdings wird das wenige Sekunden danach (wir sind mittlerweile mit dem Server verbunden) wieder rückgängig gemacht. Unsere Firewall haben wir während der Einrichtung angewiesen, bereits bestehende Verbindungen nicht zu unterbrechen. Folglich können wir nun in aller Ruhe via SSH mit der Maschine arbeiten. Wenn wir uns abmelden/die Session schließen, ist wieder Funkstille. Solange bis wir wieder mit der richtigen Kombination anklopfen.<br>Im Folgenden der Inhalt der <code>knockd.conf</code>:</p><pre><code class="language-bash">[options]
    UseSyslog
    Interface = eth0
    logfile = /var/log/knockd.log

[opencloseSSH]
    sequence = 4000,5000,6000
    seq_timeout = 15
    start_command = /sbin/iptables -A INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
    cmd_timeout = 30
    stop_command = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
    tcpflags = syn
</code></pre><p><strong>Nicht vergessen:</strong> Die Ports sollten unbedingt geändert werden, sofern der Einsatz auf einem Server beabsichtigt ist!<br><strong>Wichtig:</strong> <code>seq_timeout</code> muss größer sein als <code>cmd_timeout</code> (Angabe in Sekunden).</p><h3 id="knockd-aktivieren">Knockd aktivieren</h3><p>Wenn wir die Konfiguration abgeschlossen haben, müssen wir Knockd einmalig aktivieren. Hierfür einfach in der Datei <code>/etc/default/knockd</code> den Wert von <code>START_KNOCKD=0</code> ändern zu <code>START_KNOCKD=1</code>.</p><p>Nach erfolgter Konfiguration verwalten wir den Service mit:<br><code>sudo service knockd start/stop/restart/reload/status</code></p><h2 id="mit-dem-server-verbinden-anklopfen">Mit dem Server verbinden / anklopfen</h2><p>Wenn wir uns nun mit dem Server per SSH verbinden wollen, müssen wir korrekt anklopfen. Mit dem Programm Knock auf unserem Computer können wir das sehr einfach handhaben. Der Befehl kann natürlich einzeln ausgeführt oder in ein Bash-Skript eingebunden werden. Hier ein funktionierendes Beispiel:</p><pre><code class="language-bash">#!/bin/bash

### Folgende Variablen anpassen
SERVER_IP="123.123.123.123"
SERVER_USER="nutzername"

clear
echo "Anklopfen &amp; verbinden zur Maschine: $SERVER_IP"
knock $SERVER_IP 4000 5000 6000
ssh SERVER_USER@$SERVER_IP</code></pre><p>Einfach als eine Skript-Datei (Bsp.: knock.sh) abspeichern und anschließend ausführbar machen mit <code>sudo chmod +x knock.sh</code>. Nun einfach starten mit <code>./knock.sh</code>.</p><h2 id="fehlerbehebung">Fehlerbehebung</h2><p>Hat etwas nicht funktioniert? Selber ausgesperrt? Keine Panik! Die meisten Provider bieten für derartige Fälle eine Online-Konsole an. Einfach kurz nachfragen oder auf eigene Faust das Webinterface durchstöbern.<br>Über eine solche Konsole kann als Root-Nutzer auf die Maschine zugegriffen werden. Anschließend können die gerade erstellten Firewall-Regeln außer Kraft gesetzt werden: <code>sudo iptables -P INPUT ACCEPT</code>.</p>]]></content:encoded></item><item><title><![CDATA[Kubernetes Grundlagen]]></title><description><![CDATA[Heutzutage wohl nicht mehr wegzudenken, wird Kubernetes von unzähligen kleinen wie großen Firmen eingesetzt. Dies hier ist ein Einstieg in eine Welt voller Abstraktionsebenen.]]></description><link>https://pitscher.com/kubernetes-grundlagen/</link><guid isPermaLink="false">62815e256e82e80a27c54a4a</guid><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Hannes]]></dc:creator><pubDate>Fri, 01 Nov 2019 22:26:34 GMT</pubDate><media:content url="https://cdn.gdelivr.net/pw/images/2019/11/Cover-Bild.PNG" medium="image"/><content:encoded><![CDATA[<img src="https://cdn.gdelivr.net/pw/images/2019/11/Cover-Bild.PNG" alt="Kubernetes Grundlagen"><p>Willkommen in einer Welt voller Abstraktionsebenen. Dieser Artikel gibt eine Einführung in die Welt von Kubernetes. Neben der Architektur und Hosting-Möglichkeiten werfen wir einen Blick auf einige grundlegende Objekte und ein Fallbeispiel.</p><h2 id="voraussetzungen">Voraussetzungen</h2><p>Um den Inhalt dieses Artikels verstehen zu können, sollte bereits etwas Vorwissen in den Themenbereichen Docker, Cloud Computing sowie dem Arbeiten mit YAML vorhanden sein. Falls das nicht der Fall ist: Keine Panik! Die genannten Themen werden dennoch grundlegend erklärt.</p><p>Die Nutzer eines Services wie einer einfachen Webseite oder einer komplexeren (Web)App, erwarten heutzutage eine durchgehende Verfügbarkeit. Seit einiger Zeit zeichnet sich ein klarer Wandel vom "Bare-Metal-Server" hin zu virtuellen Maschinen in der Cloud ab. Aber damit noch nicht genug. Virtualisierung ist insbesondere für Unternehmen interessant. Damit können Server mit wenigen Klicks oder gar vollautomatisiert gestartet werden - abhängig davon, wie viel Rechenpower benötigt wird.<br>Für jede Anwendung einen eigenen Server zu betreiben, wäre in vielerlei Hinsicht ungünstig. Einen Solchen aufzusetzen bringt viele zusätzliche Aufgaben mit sich (Konfiguration, Backup &amp; Recovery, Security, ...). Unter Anderem deswegen sind neuere Arten der Virtualisierung interessant - wie <a href="https://www.docker.com/">Docker</a>.<br>Damit können (um bei den Grundlagen zu bleiben) mehrere Anwendungen auf ein und derselben Maschine, aber dennoch völlig autark betrieben werden. Unsere Programme werden in sog. Containern ausgeführt. Es handelt sich hier um eine Art schlanke virtuelle (Linux-)Maschine, in der auch tatsächlich nur Programme laufen, die im Vorhinein festgelegt wurden. Von außen sieht ein Container immer gleich aus. Sein Inhalt kann jedoch angepasst werden (z.B. Software installieren).<br>Docker bietet auch die Möglichkeit, mehrere Server (welche Container ausführen) in einer Art "Verbund" zusammenzuschalten - <a href="https://docs.docker.com/engine/swarm/">Docker Swarm</a> genannt. Damit können wir den Bogen zu <a href="https://kubernetes.io/de/">Kubernetes</a> schlagen.</p><h2 id="ursprung">Ursprung</h2><p>Kubernetes (kurz k8s) wurde von Google entwickelt und 2014 als Open Source Projekt an die <a href="https://www.cncf.io/">Cloud Native Computing Foundation (CNCF)</a> übergeben. Seither wächst seine Bekannt- und Beliebtheit rasant. Viele Firmen, kleine wie große, setzen es ein, um ihre Programme/Services/Workloads zu orchestrieren. Aus besagten Gründen ist die Kubernetes-Community recht groß. Es lassen sich viele Dokus, Talks, Blogposts und mehr im Netz finden.</p><h2 id="stateful-stateless-unterscheidung">Stateful &amp; Stateless Unterscheidung</h2><p>Anwendungen können prinzipiell - bezüglich ihrer Datenhandhabung - in zwei Kategorien eingeteilt werden.<br>Diese Unterscheidung ist sehr wichtig. Kubernetes unterstützt mittlerweile Stateful- und Stateless-Anwendungen. Eigentlich wurde es von Google aber konzipiert, um den Betrieb von Stateless-Anwendungen wie APIs bzw. Microservices auf ein ganz neues Level zu heben.</p><h3 id="stateful">Stateful</h3><p>Ein Beispiel für eine klassische Stateful-Anwendung ist die Datenbank. Auch nach einem Neustart müssen alle bisher gespeicherten Daten vorhanden sein. Hierbei geht es nicht um das Thema Backup &amp; Recovery, sondern um die Persistenz/Dauerhaftigkeit/Statefulness der Daten.</p><h3 id="stateless">Stateless</h3><p>Ein Beispiel für eine klassische Stateless-Anwendung ist die API. Sie speichert keine Daten wie z.B. Benutzereingaben. Wenn eine API beispielsweise eine Benutzeranmeldung anbietet, empfängt sie alle Eingaben wie Name und Passwort des Nutzers. Anschließend wird überprüft, ob die eingegebenen Daten mit den in der Datenbank vorhandenen übereinstimmen. Ist das der Fall, wird der Benutzer angemeldet. Falls nicht, wird meist eine Fehlermeldung ausgegeben.<br>Die API ist hier also für die Verarbeitung der Benutzereingaben zuständig. Sie wird häufig als Anwendungslogik (Business Logic) bezeichnet und ist mittig zwischen der Präsentationsschicht und der Datenhaltungsschicht angesiedelt - also zwischen Benutzerschnittstelle und Datenbank. Da sie keinerlei Daten speichert, die später erneut gebraucht werden, wird sie als transient/zustandslos/stateless bezeichnet.</p><h2 id="cluster-architektur">Cluster Architektur</h2><p>Grundlegend besteht Kubernetes nicht nur aus verschiedenen Software-Komponenten. Wir schließen mehrere Server zu einem Verbund zusammen und nennen diesen dann "Cluster".</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://cdn.gdelivr.net/pw/images/2019/11/k8s-architektur.PNG" class="kg-image" alt="Kubernetes Grundlagen"><figcaption>Kubernetes Architektur (vereinfacht)</figcaption></figure><p>Unser Cluster besteht grundlegend aus verschiedenen Servern (im Folgenden "Nodes" genannt). Wir unterscheiden zwei Arten von Nodes. Auf beiden Arten laufen unterschiedliche Programme im Hintergrund, die nötig sind, um Kubernetes zu nutzen.</p><h3 id="master">Master</h3><p>Die Master-Node ist die Schaltzentrale. Hier laufen alle Informationen die im Cluster anfallen zusammen. Dazu zählen zum Beispiel der Gesundheitsstatus der Worker-Nodes und welche Workloads/Anwendungen gerade ausgeführt werden.</p><p>Wir als Anwender nutzen ein Tool namens <a href="https://kubernetes.io/docs/reference/kubectl/overview/">kubectl</a>. Damit können wir allerlei Informationen aus dem Cluster abfragen oder neue Workloads provisionieren. Als Nutzer kommunizieren wir mittels kubectl direkt mit dem Master der für unser Cluster zuständig ist. Grundlegend handelt es sich hier um API-Abfragen. Sollte der Master nicht mehr erreichbar sein, funktioniert das Cluster (resp. die Anwendungen darin) zwar weiterhin, aber es können keinerlei Informationen abgefragt bzw. Aktionen ausgeführt werden. Wir wären also handlungsunfähig.</p><h3 id="worker">Worker</h3><p>Auf den Worker-Nodes laufen unsere Workloads. Also Programme in Docker Containern (manche Anbieter unterstützen auch <a href="https://www.heise.de/developer/meldung/Kubernetes-1-14-Jetzt-auch-fuer-Windows-Container-4348855.html">Windows-Container</a>). Zusätzlich werden eine Reihe von Kubernetes-Deamons ausgeführt. Diese beanspruchen nur wenig Ressourcen und teilen dem Master durchgehend mit, was passiert. Provisionieren wir also neue Workloads, informiert der Master einzelne Worker. Diese führen die aufgetragenen Aufgaben/Arbeitsschritte aus und lassen den Master anschließend u.a. wissen, ob unsere Programme laufen oder ob es zu Fehlern gekommen ist.</p><p><strong>Übrigens:</strong> Um die dauerhafte Verfügbarkeit der Master-Nodes für Managed Kubernetes Services (siehe Hosting) sicherzustellen, setzen viele Anbieter Kubernetes ein. Das wäre also Kubernetes in Kubernetes. <a href="https://www.ovh.com/blog/kubinception-using-kubernetes-to-run-kubernetes/">Hier</a> gibt es ein Beispiel aus der Praxis.</p><h2 id="hosting">Hosting</h2><p>Da es sich bei Kubernetes um ein Open Source Projekt handelt, könnte ein eigenes Cluster von Hand aufgesetzt werden. Dazu bräuchte es mindestens zwei Server. Einer fungiert als Master und der andere als Worker. Diese Vorgehensweise bringt jedoch gleich mehrere Probleme mit sich. Es empfiehlt sich daher sehr, einem Hosting-Provider das Aufsetzen und den Betrieb des eigenen Clusters zu überlassen. Zwar wurde Kubernetes von Google entwickelt, mittlerweile bieten allerdings alle bekannten Cloud-Anbieter ein Managed Cluster / Managed Kubernetes Service an. Beispiele hierfür sind: <a href="https://azure.microsoft.com/de-de/services/kubernetes-service/">Azure</a>, <a href="https://aws.amazon.com/de/eks/">AWS</a>, <a href="https://cloud.google.com/kubernetes-engine/docs/?hl=de">GCP</a>, <a href="https://www.digitalocean.com/products/kubernetes/">DigitalOcean</a>, <a href="https://www.ovh.de/public-cloud/kubernetes/">OVH</a>.</p><p>Die Preisgestaltung unterscheidet sich natürlich je nach Anbieter. Die Kunden zahlen jedoch nur für die Worker-Nodes die sie im Cluster einbinden möchten. Entscheiden sie sich später dafür, alle Vorteile von Kubernetes in der Cloud zu nutzen (wie LoadBalancer und Persistent Volumes), bringt das weitere Kosten mit sich. Das ist einer der Kernunterschiede zu Docker Swarm. Hier müssten alle Maschinen aus welchen ein Docker Swarm besteht, selber betreiben (Konfiguration, Backup &amp; Recovery, Security, ...) werden. Zusätzlich fallen Kosten für den Betrieb der Swarm-Manager-Node an.</p><h2 id="wahrung-des-cluster-zustands">Wahrung des Cluster-Zustands</h2><p>Kubernetes wahrt den Cluster-Zustand. Wenn wir Workloads provisionieren und dadurch Ressourcen im Cluster erstellt werden, sorgt Kubernetes dafür, dass genau diese Beschreibung (Bsp.: 2 Webserver, 1 NodeJS-App, 1 Datenbank) zu jedem Zeitpunk laufen und verfügbar sind. Sollte es Probleme mit einer Anwendung geben, wie einen Absturz, wird sie automatisch neu gestartet. Es ist kein weiteres Eingreifen erforderlich. Aus diesem Grund bringt der Einsatz von Kubernetes zu Beginn eine steile Lernkurve mit sich. Im weiteren Betrieb wird uns jedoch ein Großteil der Arbeit abgenommen. Wie in der Einführung erwähnt wurde, müssen sich die Nutzer eines Clusters nicht um die Instandhaltung der einzelnen Infrastrukturkomponenten (im Fall eines Managed Kubernetes Service) sorgen. Das übernimmt der Hosting-Anbieter.</p><h2 id="beschreibung-von-objekten">Beschreibung von Objekten</h2><p>Kubernetes baut auf Abstraktionsebenen. Es müssen folglich Objekte (wie Services, Pods oder Secrets) erstellt werden, die dafür sorgen, dass Anwendungen innerhalb eines Clusters laufen können. Um eben diese Abstraktionsebenen bzw. Objekte zu beschreiben, wird YAML (Yet Another Markup Language / YAML Ain't Markup Language) verwendet. Um ein Objekt in Kubernetes zu erstellen, muss seine Spezifikation inklusive weiterer Informationen wie der gewünschte Name des Objektes beschrieben werden. Anschließend wird die Beschreibung als JSON an den Master übermittelt. Einfacher ist es jedoch, das Tool kubectl zu nutzen. Damit können wir die Objekte per YAML (lesefreundlicher als JSON) beschreiben und anschließend mit kubectl an den Master übermitteln. So werden die Vorteile von YAML nutzbar, denn die Konvertierung zu JSON wird von kubectl automatisch durchgeführt.<br>Im Folgenden Kapitel "Abstraktionsebenen" werden einige Kubernetes-Objekte inklusive ihrer YAML-Spezifikation vorgestellt.<br></p><h2 id="abstraktionsebenen">Abstraktionsebenen</h2><p>Wenn wir über Kubernetes sprechen, sollte man sich den Begriff Abstraktionsebenen bzw. Objekte einprägen. Der einzelne Server an sich interessiert uns nicht mehr. Lediglich einige physische Merkmale wie Anzahl der CPU-Kerne und Größe des RAM müssen beachtet werden. Bevor wir also beginnen und ein Cluster mieten, sollte in Erfahrung gebracht werden, welche Anwendungen wir betreiben möchten. Entsprechend unserer Anforderungen kann dann eine gewisse Zahl und Art an Worker-Nodes bestellt werden. Über die eigentliche Infrastruktur legt Kubernetes Abstraktionsschichten. Das erspart uns viel Aufwand und sorgt gleichzeitig für eine Verringerung der Abhängigkeit zu ihr.</p><h3 id="pod">Pod</h3><p>Ein Pod ist das kleinste bereitstellbare Objekt in Kubernetes. Schlussendlich ist k8s für die Orchestrierung von Docker-Containern verantwortlich. Ein Pod kann mehrere solcher Container beinhalten in welchen unsere Anwendungen laufen. Alle in einem Pod laufenden Containern haben auf Ressourcen Zugriff, auf die der Pod Zugriff hat (Beispiel: ein Volume zum Speichern von Dateien). Empfohlen wird allerdings, einen Pod pro Container bzw. pro Anwendung zu nutzen. Container in einem Pod laufen immer auf ein und der selben Worker-Node in einem Cluster und können sich untereinander via <code>localhost</code> erreichen. Zusätzlich erhält jeder Pod eine eigene IP-Adresse und einen DNS-Namen unter denen er innerhalb des Clusters erreichbar ist. Pods können von Kubernetes jederzeit beendet und z.B. auf einer anderen Worker-Node wieder gestartet werden.</p><p><strong>Pod-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: mein-webserver
  labels:
    app: mein-webserver
spec:
  containers:
  - name: nginx-container
    image: nginx
    ports:
    - containerPort: 80</code></pre><p></p><h3 id="service">Service</h3><p>Um Anwendungen die in Pods laufen nach außen (innerhalb oder außerhalb des Clusters) erreichbar zu machen, benötigen wir einen Service. Ein solcher bietet von Haus aus eine Lastenverteilung (LoadBalancing) zwischen den ihm zugewiesenen Pods an. Da Pods jederzeit neu gestartet werden können, muss sichergestellt werden, dass sie im Nachhinein erreichbar sind (die IP-Adressen der Pods können sich ändern). Auch dafür ist ein Service zuständig. Er ist also eine Abstraktionsebene, die eine Menge an Pods und die Art und Weise wie sie von außen erreicht werden können, beschreibt. Ein Service findet den Pod an welchen er eintreffende Anfragen weiterleitet über den Selector (Zeile 6-7 in der Service-Definition). In der Beispiel-YAML werden Anfragen an den Pod mit dem Label <code>app: mein-webserver</code> auf Port 80 weitergereicht.</p><p><strong>Service-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: mein-service
spec:
  selector:
    app: mein-webserver
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80</code></pre><p></p><h3 id="ingress">Ingress</h3><p>Damit Anwendungen die in einem Kubernetes Cluster laufen von außerhalb des Clusters erreichbar sind, wird ein Ingress benötigt. Dieses Objekt kann als eine speziellere Art eines Service angesehen werden. Neben Lastenverteilung bietet er auch SSL-Terminierung (sprich HTTPS) und name-based virtual routing an. Ähnlich wie ein "normaler" Webserver können einem Ingress mehrere Domains zugewiesen werden. Je nachdem über welche Domain eine Anfrage eintrifft, kann er sie an den zuständigen Service innerhalb des Clusters weiterleiten. So können Anfragen an <code>example.com</code> an den/die Pod(s) welche die statische Webseite ausliefern weitergeleitet werden. Anfragen an <code>api.example.com</code> werden hingegen an den/die Pod(s) welche die API-Anwendung (wie NodeJS) beinhalten weitergeleitet. Zu beachten ist jedoch, dass ein Ingress-Objekt lediglich einen Regelsatz darstellt. Die eigentliche Software dahinter (meist Nginx, HA-Proxy oder Traefik) wird zusätzlich benötigt. Diese Thematik wird in einem späteren Beitrag betrachtet.</p><p><strong>Ingress-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: mein-ingress
spec:
  tls:
  - hosts:
    - example.com
    - api.example.com
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-webseite
          servicePort: 80
  - host: api.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-api
          servicePort: 8000</code></pre><p></p><h3 id="persistent-volume-claim-">Persistent Volume (Claim)</h3><p>Wenn eine Stateful-Anwendung in einem Kubernetes Cluster betrieben werden soll, müssen ihre Daten gespeichert werden. Unabhängig davon, ob die Worker-Node auf der die Anwendung ausgeführt wird abstürzt oder die Anwendung an sich neu gestartet wird - ihre Daten müssen persistent gespeichert werden. Mieten wir ein K8s-Cluster bei einem Cloud-Anbieter, ist ein Persistent Volume (PV) ein externes Laufwerk. Es ist also unabhängig von unserem Cluster. Ein Persistent Volume Claim (PVC) ist eine Anfrage (an den Anbieter bei dem unser Cluster betrieben wird), ein Persistent Volume für uns bereitzustellen. Wir geben u.a. an, wie groß (z.B. in Gigabyte) es sein soll sowie welche Art wir nutzen möchten (Auswahl meist zw. HDD und SSD). Das Persistent Volume wird nach der Bereitstellung durch den Anbieter an einen Pod gemountet/angehängt.<br>Beispielsweise speichert eine Datenbank wie MySQL/MariaDB ihre persistenten Daten (die Datenbanken und darin enthaltene Tabellen sowie Nutzer und deren Zugangsdaten) in dem Verzeichnis <code>/var/lib/mysql</code>. Das Persistent Volume wird folglich an genau diesem Pfad gemountet. Das führt dazu, dass alle persistenten Daten der Datenbank direkt auf dem PV - also extern und nicht innerhalb des Datenbank-Containers - gespeichert werden. Sollte der Datenbank-Pod neu gestartet werden, wird das Persistent Volume von Kubernetes automatisch erneut an den Datenbank-Pod und unter dem gleichen Pfad angehängt. Sollte Kubernetes den Datenbank-Pod auf einer anderen Worker-Node starten, wird das PV automatisch an diese Maschine gehangen und steht anschließend für den Pod zur Verfügung.</p><p><strong>Persistent Volume Claim-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mein-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: ssd</code></pre><p></p><h3 id="secret">Secret</h3><p>Um vertrauliche Informationen wie Passwörter oder API-Tokens speichern zu können, benötigen wir Secrets. Es ist deutlich sicherer, derartige Informationen nicht direkt im Code unserer Anwendung oder auf einem Persistent Volume abzulegen, sondern dafür ein Secret zu nutzen. Zugleich sind wir flexibler, denn wir können genau festlegen, welche Pods auf ein Secret zugreifen dürfen. Des Weiteren kann darauf verzichtet werden, solch sensible Zugangsdaten in den YAML-Dateien/Kubernetes-Manifesten als Klartext abzuspeichern. Die Daten, die ein Secret enthält, werden base64-encoded und auf der Master-Node (Anbieter abhängig meist verschlüsselt) gespeichert. Ein Secret kann seinen Inhalt entweder per Volume das an einen Pod angehängt wird oder mittels Umgebungsvariablen verfügbar machen.</p><p><strong>Secret-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: mein-secret
type: Opaque
stringData:
  DB_USER: "Nutzername"
  DB_PASSWORT: "Passwort"
  API_KEY: "apiKey"</code></pre><p></p><h3 id="configmap">ConfigMap</h3><p>Um nicht vertrauliche Informationen wie Einstellungsparameter von Anwendungen zu speichern, können ConfigMaps genutzt werden. Sie sind das Pendant zu Secrets. Die Konfigurationsparamenter sind damit unabhängig von der Anwendung. Sollten wir also einige Einstellungen ändern, muss nicht der Code der Anwendung, sondern lediglich der Inhalt der ConfigMap editiert werden.</p><p><strong>ConfigMap-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: meine-configmap
data:
  URL: "https://example.com"
  LOGGING_LEVEL: "development"
  CHECK_UPDATE: "yes"</code></pre><p></p><h3 id="cronjob">CronJob</h3><p>Wie auch auf einem Linux-Server, gibt es in Kubernetes eine Möglichkeit, bestimmte Aufgaben regelmäßig auszuführen. Ein Beispiel für Aufgaben dieser Art sind Backups. Wollen wir regelmäßig (z.B. täglich 2 Uhr morgens) ein Backup unserer Datenbank erstellen, können wir einen CronJob nutzen. Dieser besteht grundlegend aus der Information, wann er ausgeführt werden soll und der Konfiguration des Pods. Schlussendlich wird durch den CronJob zu den von uns festgelegten Zeiten ein Pod gestartet. Wenn er seine Aufgabe erfolgreich erledigt hat, beendet er sich automatisch. Als Pod kann das Image "<a href="https://hub.docker.com/_/busybox">busybox</a>" verwendet werden. Es besteht aus einer sehr leichtgewichtigen Linux-Distribution. Wir teilen dem CronJob bei Bedarf mit, an welches (Persistent) Volume und unter welchem Pfad er sich anhängen soll. Wird der Job gestartet, hängt der Pod das Volume ein, führt die von uns definierten Befehle aus, gibt es nach getaner Arbeit wieder frei und beendet sich anschließend. Die Backups können je nach Bedarf natürlich auch direkt auf einen externen Speicher kopiert werden.</p><p><strong>CronJob-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: mein-cronjob
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: db-backup
            image: busybox
            args:
            - /bin/sh
            - -c
            - &lt;backupBefehle&gt;
          restartPolicy: OnFailure</code></pre><p></p><h3 id="replicaset">ReplicaSet</h3><p>Um die Verfügbarkeit einer festen Anzahl an Pods jederzeit garantieren zu können, wird ein ReplicaSet benötigt. Wie der Name bereits erschließen lässt, handelt es sich immer um die gleiche Art von Pods. Wollen wir beispielsweise wegen eines erwarteten Besucheransturms skalieren, können wir die Replikate unserer Webserver-Pods erhöhen. Kubernetes startet automatisch weitere Instanzen des Webserver-Pods entsprechend der von uns festgelegten Anzahl. Ein ReplicaSet kann von einem Deployment verwaltet werden (siehe Punkt "Deployment"). Dementsprechend ist es nicht erforderlich, ein ReplicaSet händisch zu erstellen - das kann ein Deployment übernehmen. Sofern wir ein Cluster von einem Anbieter betreiben lassen, können wir die Anzahl der Worker-Nodes sowie die Anzahl von Replikaten unserer Anwendungen vollautomatisiert und je nach Bedarf skalieren lassen. Voraussetzung dafür ist aber natürlich die Unterstützung dieser Funktionalität durch den Anbieter.</p><p><strong>ReplicaSet-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: frontend
  labels:
    app: webserver
    tier: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      tier: frontend
  template:
    metadata:
      labels:
        tier: frontend
    spec:
      containers:
      - name: nginx
        image: nginx</code></pre><p></p><h3 id="deployment">Deployment</h3><p>Mit Hilfe eines Deployments können wir Pods und ReplicaSets verwalten. Wir beschreiben hier einen Zustand, der unter allen Umständen gewahrt werden soll. Wollen wir beispielsweise einen Webserver betreiben, beschreiben wir ihn nicht als einzelnen Pod. Stattdessen nutzen wir ein Deployment. Dadurch können z.B. die Anzahl der Replikate des Webservers beliebig erhöht werden, sollten wir mehr Besucher unserer Webseite erwarten. Ist der Ansturm wieder vorüber, verringern wir die Anzahl der Webserver wieder, indem wir in der YAML-Definition unseres Deployments die Zahl der <code>replicas: 3</code> (siehe YAML) verringern. Sollte sich einmal der Fehlerteufel einschleichen, können wir auf einen früheren Stand des Deployments zurückspringen. Zusammenfassend ist ein Deployment also eine Möglichkeit, mehrere Kubernetes-Objekte wie Pods und ReplicaSets zentralisiert zu verwalten. Übergeben wir dem Cluster unser Deployment, werden die darin beschriebenen Ressourcen (wie ein Pod) automatisch erstellt. Löschen wir es, werden auch alle zugehörigen Ressourcen automatisch gelöscht.<br>Neben dem Namen des Deployments geben wir an, wie viele Replikate (hier Pods die den Webserver Nginx ausführen) vorhanden sein sollen. Mit <code>selector: matchLabels: app: nginx</code> stellen wir sicher, dass das Deployment weiß, für welche Pods es zuständig ist. In diesem Fall ist es ausschließlich für Pods mit dem Label <code>app: nginx</code> verantwortlich. Es folgt unter dem Punkt <code>template:</code> die eigentliche Definition der Pods, die zu unserem Deployment gehören. Hier also drei Mal ein Pod mit dem Container Nginx.</p><p><strong>Deployment-Definition in YAML:</strong></p><pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: mein-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80</code></pre><p></p><h2 id="fallbeispiel">Fallbeispiel</h2><p>Es soll ein Online Shop mit folgendem Technologie-Stack betrieben werden:</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Typ</th>
<th style="text-align:center">Frontend</th>
<th style="text-align:center">Backend</th>
<th style="text-align:center">Backend</th>
<th style="text-align:center">Backend</th>
</tr>
</thead>
<tbody>
<tr>
<td>Art</td>
<td style="text-align:center">WebApp</td>
<td style="text-align:center">Shop-API</td>
<td style="text-align:center">Payment-API</td>
<td style="text-align:center">Datenbank</td>
</tr>
<tr>
<td>Beispiel</td>
<td style="text-align:center">React</td>
<td style="text-align:center">NodeJS</td>
<td style="text-align:center">Python</td>
<td style="text-align:center">MongoDB</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p><strong>WebApp</strong> (stateless)<br>Eine React-WebApp besteht nach einem <a href="https://create-react-app.dev/docs/production-build/">Production Build</a> aus einer HTML sowie einigen JavaScript- und CSS-Dateien. Diese müssen von einem Webserver ausgeliefert werden.</p><p><strong>Shop-API</strong> (stateless)<br>Die Shop-API bestehend aus NodeJS und Express nimmt Anfragen der WebApp entgegen. Sie kommuniziert mit der Payment-API und der Datenbank.</p><p><strong>Payment-API</strong> (stateless)<br>Die Payment-API kommuniziert mit der Shop-API und der Datenbank. Für die Besucher des Shops ist sie nicht erreichbar. Hier wird mit Zahlungsdienstleistern wie PayPal und Stripe kommuniziert, um z.B. Käufe von Kunden im Online Shop abzuwickeln.</p><p><strong>Datenbank</strong> (stateful)<br>Unsere Datenbank speichert alle Informationen des Online Shops. Dazu zählen z.B. Kundendaten und Details zu Produkten die angeboten werden.</p><p>Alle Komponenten des Shops sollen in einem Kubernetes Cluster betrieben werden. Damit das möglich ist, müssen wir dafür sorgen, dass unsere Anwendungen "containerisiert" sind. Also in Docker Containern laufen können und passende Images vorliegen. (Mehr dazu in einem späteren Beitrag)</p><h3 id="beispielarchitektur">Beispielarchitektur</h3><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://cdn.gdelivr.net/pw/images/2019/11/k8s-fallbeispiel.PNG" class="kg-image" alt="Kubernetes Grundlagen"><figcaption>Fallbeispiel Architektur (vereinfacht)</figcaption></figure><p>Die Besucher des Shops greifen über den Ingress zuerst auf die WebApp zu. Wie oben erwähnt, werden hier von Nginx die Dateien der React-WebApp an die Nutzer ausgeliefert. Die WebApp stellt Anfragen an die Shop-API. Diese kommuniziert im Hintergrund mit der Datenbank des Shops. Sollten Zahlungen abgewickelt werden müssen (wie bei einem Kauf), greift die Shop-API auch auf die Payment-API zu. Diese steht in Verbindung mit externen Zahlungsdienstleistern wie Stripe und PayPal. Die Shop- und Payment-API nutzen Secrets, um auf sensible Daten wie die Datenbank-Anmeldeinformationen und die API-Tokens der externen Zahlungsdienstleister zuzugreifen. Da es sich bei der Datenbank um eine Stateful-Anwendung handelt, speichert sie ihre Daten auf einem Persistent Volume.<br>Die Anwendungen greifen aufeinander über ihre Cluster-internen Services zu.</p><p>Um zu verstehen, wie die oben dargestellte Architektur in Kubernetes-Manifesten umgesetzt werden könnte, sind die einzelnen Bestandteile inkl. einer kurzen Erläuterung im Folgenden aufgeführt.</p><h3 id="ingress-1">Ingress</h3><pre><code class="language-yaml">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: demo-shop-com-ingress
  annotations:
  # Einfachheitshalber entfernt
  # Hiermit könnte z.B. Weiterleitung von HTTP zu HTTPS konfiguriert werden
spec:
  tls:
  - hosts:
    - demo-shop.com
    - api.demo-shop.com
    secretName: demo-shop-com
  rules:
  - host: demo-shop.com
    http:
      paths:
      - backend:
          serviceName: shop-webapp-svc
          servicePort: 8080
  - host: api.demo-shop.com
    http:
      paths:
      - backend:
          serviceName: shop-api-svc
          servicePort: 8000</code></pre><ul><li><code>secretName:</code> Name des Secrets das genutzt wird, wenn HTTPS angeboten wird (es würde z.B. die Zertifikats-Informationen enthalten).</li><li><code>paths:</code> Regel für Weiterleitung an angegebenen Service kann auch als Pfad definiert werden. Damit könnte auf eine Subdomain für die API verzichtet werden. Sie könnte z.B. stattdessen unter demo-shop.com/api erreichbar gemacht werden.</li><li><code>serviceName:</code> &amp; <code>servicePort:</code> Daten des Cluster-internen Service, an den alle Anfragen für demo-shop.com bzw. api.demo-shop.com weitergeleitet werden.</li></ul><p><strong>Hinweis: </strong>Um einen Ingress nutzen zu können, ist etwas Vorarbeit nötig. Insbesondere, wenn HTTPS genutzt werden soll (wie in dem YAML-Beispiel zu sehen ist). In einem späteren Beitrag auf diesem Blog wird näher darauf eingegangen.</p><hr><h3 id="service-webapp-">Service (WebApp)</h3><pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: shop-webapp-svc
spec:
  type: ClusterIP
  selector:
    app: shop-webapp
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 80
      name: shop-webapp-svc</code></pre><ul><li><code>name:</code> Name unter dem der Service erreichbar ist (siehe bspw. Ingress).</li><li><code>type:</code> Art des Service. ClusterIP=nur innerhalb des Clusters erreichbar.</li><li><code>selector: app: shop-webapp</code> Anfragen werden an Pods (z.B. eines Deployments) mit dem Label <code>app: shop-webapp</code> weitergeleitet.</li><li><code>ports:</code> Service ist erreichbar unter Port 8080 und gibt Anfragen weiter an Port 80. Der Port wird hier folglich 'umgewandelt'.</li></ul><p></p><h3 id="deployment-webapp-">Deployment (WebApp)</h3><pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-webapp-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: shop-webapp
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: shop-webapp
    spec:
      containers:
      - name: shop-webapp
        image: demo-shop-gmbh/webapp:1.0
        imagePullPolicy: Always
        ports:
        - containerPort: 80
      imagePullSecrets:
      - name: private-registry
</code></pre><ul><li><code>replicas: 3</code> Das Deployment besteht aus 3 gleichen Pods die den gleichen Container (die WebApp) ausführen. Kubernetes sorgt i.d.R. für eine gleichmäßige Verteilung auf vorhandene Worker-Nodes.</li><li><code>selector: matchLabels: app: shop-webapp</code> Das Deployment ist für alle Pods mit dem Label <code>app: shop-webapp</code> zuständig. Alle Einstellungen des Deployments wirken sich lediglich auf diese Pods aus.</li><li><code>template:</code> Definition des/der Pods der/die durch das Deployment verwaltet werden.</li><li><code>labels: app: shop-webapp</code> Ein Label dient der Bezeichnung/Beschreibung von Kubernetes-Objekten. Hier wird es gebraucht, damit der WebApp-Service Anfragen an den/die Pods des Deployments weiterleiten kann und damit eine Verwaltung des/der Pods mit diesem Label durch das WebApp-Deployment möglich ist.</li><li><code>containers:</code> Innerhalb eines Pods können mehrere Container ausgeführt werden. Hier wird lediglich einer definiert.</li><li><code>image:</code> Angabe eines Docker-Images. Die in der YAML verwendete Angabe gibt an, dass Kubernetes das Image von der Plattform/Docker-Registry "Docker Hub" herunterladen soll. Syntax: "dockerHubNutzer/repositoryBzwImageName:version". Wenn die Images nicht veröffentlicht werden sollen, kann eine private Container-Registry verwendet werden (wie GitLab Container Registry). In diesem Fall wird als Image folgendes angegeben: "registry.gitlab.com/nutzername/webapp:1.0".</li><li><code>imagePullSecrets:</code> Viele Docker Images sind öffentlich über hub.docker.com verfügbar. Sollte allerdings eine private Container-Registry genutzt werden, muss sich das Kubernetes Cluster authentifizieren, um auf die eigenen privaten Images zugreifen zu können. Unter imagePullSecrets wird der Name des Secrets angegeben, das die Anmeldeinformationen der privaten Registry enthält.</li></ul><hr><h3 id="service-shop-api-">Service (Shop-API)</h3><pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: shop-api-svc
spec:
  type: ClusterIP
  selector:
    app: shop-api
  ports:
    - protocol: TCP
      port: 8000
      targetPort: 8000
      name: shop-api-svc
</code></pre><p></p><h3 id="deployment-shop-api-">Deployment (Shop-API)</h3><pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-api-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shop-api
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: shop-api
    spec:
      containers:
      - name: shop-api
        image: demo-shop-gmbh/api:1.0
        imagePullPolicy: Always
        ports:
        - containerPort: 8000
          name: shop-api
        envFrom:
        - configMapRef:
            name: shop-api-config
        env:
          - name: DB_USER
            valueFrom:
              secretKeyRef:
                name: shop-api-secret
                key: DB_USER
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: shop-api-secret
                key: DB_PASSWORD
      imagePullSecrets:
      - name: private-registry
</code></pre><ul><li><code>envFrom: - configMapRef: name:</code> Um eine ConfigMap, die Umgebungsvariablen enthält, einbinden zu können, wird ihr Name hier angegeben. Der/Die Pod(s) des Deployments können dadurch auf die Werte der ConfigMap als Umgebungsvariablen zugreifen. Die Shop-API kann also z.B. auf die Umgebungsvariable NODE_ENV mit dem Wert "production" zugreifen.</li><li><code>env:</code> Unter diesem Punkt können die Werte von Secrets dem/den Pod(s) eines Deployments als Umgebungsvariablen zugänglich gemacht werden. Die Shop-API kann also z.B. auf die Umgebungsvariable DB_USER mit dem Wert "Nutzername" zugreifen.</li></ul><h3 id="secret-shop-api-">Secret (Shop-API)</h3><pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: shop-api-secret
type: Opaque
stringData:
  DB_USER: "Nutzername"
  DB_PASSWORD: "Passwort"</code></pre><ul><li><code>name:</code> Name der z.B. in der Deployment-YAML verwendet wird, um Inhalte des Secrets als Umgebungsvariable verfügbar zu machen.</li><li><code>stringData:</code> Vertrauliche Informationen, die von der Shop-API als Umgebungsvariable abgerufen werden, können hier als KeyValue Pair eingefügt werden.</li></ul><p><strong>Hinweis:</strong> Eine YAML-Datei mit sensiblen Daten im Klartext sollte nicht verwendet werden. Ein Secret kann stattdessen mittels kubectl erstellt werden. Entweder mit den Daten direkt im eigentlichen Befehl (1) oder mit Daten aus einer separaten Datei (2). Mit Option 2 wird verhindert, dass die sensiblen Informationen in der Bash-History gespeichert werden.</p><p><strong>(1)</strong> kubectl create secret generic shop-api-secret --from-literal=DB_USER=Nutzername --from-literal=DB_PASSWORD=Passwort</p><p><strong>(2)</strong> kubectl create secret generic shop-api-secret --from-file=pfad/zur/datei.txt</p><p></p><h3 id="configmap-shop-api-">ConfigMap (Shop-API)</h3><pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: shop-api-config
data:
  NODE_ENV: "production"
  DB_HOST: "shop-db-svc"
  DB_DATABASE_NAME: "shop_db"
  DB_PORT: "27017"</code></pre><ul><li><code>name:</code> Name der z.B. in der Deployment-YAML verwendet wird, um Inhalte der ConfigMap als Umgebungsvariable einbinden zu können.</li><li><code>data:</code> Konfigurationsparameter die von der Shop-API als Umgebungsvariable abgerufen werden, können hier als KeyValue Pair eingefügt werden.</li></ul><hr><h3 id="service-payment-api-">Service (Payment-API)</h3><pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: shop-payment-api-svc
spec:
  type: ClusterIP
  selector:
    app: shop-payment-api
  ports:
    - protocol: TCP
      port: 8000
      targetPort: 8000
      name: shop-payment-api-svc
</code></pre><p></p><h3 id="deployment-payment-api-">Deployment (Payment-API)</h3><pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-payment-api-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shop-payment-api
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: shop-payment-api
    spec:
      containers:
      - name: shop-payment-api
        image: demo-shop-gmbh/payment-api:1.0
        imagePullPolicy: Always
        ports:
        - containerPort: 8000
          name: shop-payment-api
        envFrom:
        - configMapRef:
            name: shop-payment-api-config
        env:
          - name: DB_USER
            valueFrom:
              secretKeyRef:
                name: shop-payment-api-secret
                key: DB_USER
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: shop-payment-api-secret
                key: DB_PASSWORD
      imagePullSecrets:
      - name: private-registry</code></pre><p></p><h3 id="secret-payment-api-">Secret (Payment-API)</h3><pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: shop-payment-api-secret
type: Opaque
stringData:
  DB_USER: "Nutzername"
  DB_PASSWORD: "Passwort"
  PAYPAL_API_KEY: "PaypalApiKey"
  STRIPE_API_KEY: "StripeApiKey"</code></pre><p><strong>Hinweis:</strong> Die Payment-API sollte nicht die gleichen Zugriffsrechte auf die Datenbank haben wie die Shop-API. Aus diesem Grund sollten in diesem Secret andere Werte für DB_USER und DB_PASSWORD als bei dem der Shop-API angegeben werden.</p><p></p><h3 id="configmap-payment-api-">ConfigMap (Payment-API)</h3><pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: shop-payment-api-config
data:
  NODE_ENV: "production"
  DB_HOST: "shop-db-svc"
  DB_DATABASE_NAME: "shop_db"
  DB_PORT: "27017"
  STRIPE_CONFIG: "..."
  PAYPAL_COMFIG: "..."</code></pre><hr><h3 id="service-datenbank-">Service (Datenbank)</h3><pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: shop-db-svc
spec:
  type: ClusterIP
  selector:
    app: shop-db
  ports:
    - protocol: TCP
      port: 27017
      targetPort: 27017
      name: shop-db-svc</code></pre><p></p><h3 id="deployment-datenbank-">Deployment (Datenbank)</h3><pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-db-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shop-db
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: shop-db
    spec:
      containers:
      - name: shop-db
        image: mongodb:4.0
        imagePullPolicy: Always
        ports:
        - containerPort: 27017
          name: mongodb
        volumeMounts:
        - mountPath: /data/db
          name: shop-db-storage
      volumes:
      - name: shop-db-storage
        persistentVolumeClaim:
          claimName: shop-db-pvc
</code></pre><ul><li><code>volumeMounts: - mountPath:</code> Alle Dateien in "/data/db" sollen nicht innerhalb des Datenbank-Containers, sondern auf einem Volume gespeichert werden.</li><li><code>volumes:</code> Es kann sich z.B. um ein hostPath-Volume handeln. Diese Art Volume speichert seine Dateien auf dem Dateisystem der Worker-Node, auf der der Datenbank-Pod aktuell läuft. Wird diese Worker-Node gelöscht, werden auch alle Dateien des Volumes gelöscht. Alternativ kann der Name eines Persistent Volume Claim angegeben werden. Dadurch werden alle Dateien im Ordner "/data/db" direkt auf einem Persistent Volume gespeichert und sind somit unabhängig von dem Container und der Worker-Node, auf der er läuft.</li></ul><p></p><h3 id="persistent-volume-claim-datenbank-">Persistent Volume Claim (Datenbank)</h3><pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shop-db-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: ssd #Name unterscheidet sich zw. Providern</code></pre><ul><li><code>name:</code> Name für die Referenzierung auf das Persistent Volume, welches durch den Persistent Volume Claim erstellt wurde.</li><li><code>accessModes: - ReadWriteOnce</code> Auf das Volume können nicht mehrere Worker-Nodes gleichzeitig zugreifen. Je nach Anbieter gibt es weitere Access-Modes.</li><li><code>storage: 10Gi</code> Durch den Persistent Volume Claim wird bei dem Anbieter ein Persistent Volume von der Größe 10GB angefordert.</li><li><code>storageClassName:</code> Bestimmung der Art des Persistent Volume. Meist kann zwischen SSD- und HDD-Volumes gewählt werden. Die genaue Bezeichnung dafür (die an dieser Stelle anzugeben ist) muss bei dem Anbieter der unser Cluster betreibt eingeholt werden. Meist hilft eine rasche Websuche wie "NameDesAnbieters kubernetes storage class" weiter.</li></ul><hr><h2 id="abschlie-ende-worte">Abschließende Worte</h2><p>Kubernetes ist eine fantastische Möglichkeit, 'containerisierte' Anwendungen zu betreiben. Durch zahlreiche Abstraktionsebenen werden die Programme von der eigentlichen Infrastruktur, auf der sie laufen, entkoppelt. Mittlerweile hat sich das Projekt als De-facto-Standard für Container-Orchestrierung etabliert. Dank YAML sind die K8s-Manifeste rasch erstellt und auch nach längerer Zeit noch gut verständlich. Mit einem erfahrenen Anbieter als Partner ist es ein Leichtes, ein Kubernetes Cluster zu erstellen und zu betreiben.</p><p>Dieser Artikel ist ein Einstiegspunkt. In späteren Beiträgen befassen wir uns mit dem Aufsetzen eines Clusters und provisionieren erste Workloads. Bis es allerdings soweit ist, freue ich mich über die ein oder andere <a href="https://pitscher.com/feedback/">Rückmeldung</a>.</p>]]></content:encoded></item></channel></rss>