Kapitel 4. Code und Tests verwalten
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Drei Phasen innerhalb des DevSecOps-Lebenszyklus konzentrieren sich auf traditionell entwicklungsbezogene Aufgaben. Dazu gehören die Entwicklung oder Codierung selbst, die Erstellung des resultierenden Codes zu einer ausführbaren Anwendung und das Testen der Anwendung. An den vollständigen Tests sind andere Teams wie die Qualitätssicherung (QA) beteiligt, aber zumindest einige Tests werden auf der Entwicklerebene durchgeführt. Der Build-Schritt ist je nach verwendeter Programmiersprache sehr unterschiedlich. Bei einigen Sprachen müssen die Artefakte kompiliert werden, während andere in einem nicht kompilierten Zustand bereitgestellt werden können. Das Kapitel beginnt mit der Entwicklung und endet mit Konzepten und Werkzeugen zum Testen von Software. Nebenbei werden auch Git für die Quellcodeverwaltung und zwei Muster für die Verwendung von Git bei der Softwareentwicklung vorgestellt.
Die Entwicklung prüfen
Bei der Vielzahl der verfügbaren Programmiersprachen ist es unmöglich, einen einzigen Abschnitt, ein einziges Kapitel auf oder gar ein einziges Buch zu finden, das das gesamte Wissen enthält, das man braucht, um in dieser Sprache erfolgreich zu entwickeln. Es gibt auch zahlreiche Bücher, die sich mit dem Design und der Architektur von High-Level-Programmen beschäftigen. Auch wenn es eigennützig erscheint, ist eine allgemeine Regel, die ich in meiner Karriere befolgt habe, dass ich nach Büchern suche, die von O'Reilly herausgegeben wurden, weil diese Bücher eine umfassende Abdeckung bieten. Auf dem Gebiet des Softwaredesigns und der Softwarearchitektur hat Martin Fowler mehrere Bücher geschrieben, die in ihren jeweiligen Bereichen ebenso kanonisch sind wie die TCP/IP Illustrated-Reihe von W. Richard Stevens, die viele Jahre lang die beste Quelle war. In Bezug auf diese und andere verwandte Werke gibt es einige Ideen, die ich versuche, meinen Schülern und Schülerinnen zu vermitteln, die an der Programmierung im Produktionsstil arbeiten. Bemerkenswert ist auch, dass diese Ideen selbst eine Zusammenfassung der Ideen der oben genannten und anderer Autoren sind, aber ich habe sie als äußerst hilfreich und zugänglich für Schüler/innen empfunden.
Sei absichtsvoll und überlegt
Noch bevor künstliche Intelligenz es Menschen ermöglichte, praktikabel aussehende Antworten auf Probleme zu erhalten, liehen sich Entwickler/innen Code von anderen. Ob der Code genau richtig funktionierte oder zum Design passte, war manchmal zweitrangig, wenn es darum ging, die Aufgabe zu erfüllen. Hier ist es wichtig, absichtlich und mit Bedacht vorzugehen. Ein Entwickler könnte die Aufgabe technisch gesehen mit verschachtelten Schleifen und fest kodierten Werten erledigen, aber das würde zu technischen Schulden führen und könnte über den engen Fokus der aktuellen Aufgabe hinaus und bei begrenzten Tests nicht korrekt funktionieren. In diesem Code wird davon ausgegangen, dass es immer 50 Bundesstaaten in den Vereinigten Staaten gibt und dass ihre alphabetische Reihenfolge immer gleich bleibt:
for (i = 0; i < 50; i++): if (i == 49): state_name = "Wisconsin"
Das Beispiel mag zwar etwas extrem sein, aber diese Art von Hardcoding kommt vor, wenn Zeitdruck oder andere Faktoren einen Entwickler dazu veranlassen, den Code als vollständig zu betrachten, obwohl er vielleicht noch nicht ganz ausgereift ist.
Hinweis
"Technische Schuld" ist ein Begriff, der verwendet wird, um zu beschreiben, wie viel Zeit für die zukünftige Entwicklung oder Weiterentwicklung einer Anwendung oder eines Systems aufgewendet wird. Das Festcodieren von Werten in einem Programm, anstatt die Werte in eine Variable oder Konstante zu abstrahieren, spart vielleicht Zeit für diese eine Aufgabe mit einem einzigen Wert von Testdaten, aber wenn dieser Wert das nächste Mal benötigt wird, muss er erneut festcodiert werden. Wenn sich der Wert in der Zukunft ändert, müssen alle Stellen, an denen der Wert hartkodiert wurde, geändert werden, was zu Fehlern führen kann. Die Zeit wurde zwar für die einzelne Instanz in der einen Datei gespeichert, aber diese Zeit wird später zurückgezahlt, genau wie eine Geldschuld zu einem späteren Zeitpunkt zurückgezahlt wird.
Wiederholen Sie sich nicht
Betrachte diesen Code, der verwendet wird, um die Gesamtsumme für eine Bestellung zu berechnen, indem die Zwischensumme mit dem Steuersatz (5,5%) multipliziert wird:
order_tax = subtotal * 0.055
Wenn sich der Steuersatz nie ändert und auch sonst nirgendwo in der Anwendung verwendet werden muss, erfüllt dieser Code die Kriterien für ein Minimal Viable Product (MVP). Ein anderer Entwickler arbeitet jedoch auch an einem Teil der Anwendung, der den Steuersatz benötigt. Anstatt die Dezimaldarstellung des Steuerprozentsatzes zu verwenden, entschied er sich, den Prozentsatz selbst zu verwenden. Ihr Code sieht wie folgt aus:
order_tax = subtotal * 5.5
Diese beiden Teile des Codes werden völlig unterschiedliche Werte erzeugen. Der Entwickler bemerkt das Problem vielleicht nicht, weil die Mathematik technisch korrekt ist, d.h. der Wert, der durch den Multiplikationsoperator erzeugt wird, ergibt ein korrektes Ergebnis.
Anstatt sich auf fest kodierte Werte zu verlassen, könnte eine Konstante für den Steuersatz verwendet werden:
const TAX_RATE = 0.055 order_tax = subtotal * TAX_RATE
Durch die Verwendung einer Konstante gibt es weniger Verwirrung. Außerdem gibt es jetzt nur noch eine Stelle, an der der Steuersatz geändert werden kann, wenn der Steuersatz in Zukunft steigt.
Quellcode mit Git verwalten
Egal, ob du im Team oder alleine entwickelst, die Nachverfolgung von Änderungen am Quellcode ermöglicht es dir, die Historie der Änderungen am Code nachzuvollziehen. Du kannst dann zu einer alten Version des Codes zurückkehren, falls bei einem neu eingeführten Code etwas kaputt geht. Werkzeuge zur Quellcodeverwaltung (SCM) wie CVS, SVN, Mercurial, Git und andere bieten die Möglichkeit, Änderungen zu verfolgen.
In einer Organisation ist die Wahrscheinlichkeit groß, dass Code aus verschiedenen Teilen eines Projekts von mehreren Entwicklern gleichzeitig bearbeitet wird. Jeder Entwickler nimmt Änderungen vor, die vom SCM verfolgt werden. Wenn der Code auf einen gemeinsamen SCM-Server hochgeladen wird, werden die Änderungen der einzelnen Entwickler zusammengeführt, so dass ein einziger zusammenhängender Satz von Softwaredateien entsteht, der alle Änderungen dieser Entwickler enthält. Linus Torvalds, der Erfinder des Linux-Kernels, hat das SCM-Tool Git entwickelt. Git ist ein beliebtes Open Source SCM, das weit verbreitet ist. In diesem Abschnitt werden zwei Methoden für die Verwaltung von Quellcode mit Git vorgestellt: das Gitflow-Muster und das trunk-basierte Muster. Doch zunächst legen wir eine Basis oder ein minimales Muster fest.
Eine einfache Einrichtung für Git
In diesem Abschnitt wird eine Methode für beschrieben, bei der Git auf einem privaten und unabhängigen Server eingesetzt wird, z. B. auf einem Server in den Räumlichkeiten einer Organisation. Zu den offensichtlichen Vorteilen gehören Datenschutz und Kosten. Das Quellcode-Repository muss nicht bei einem Dritten gehostet werden, und die Kosten für Git sind unabhängig von der Anzahl der Entwickler, die es in einer Organisation nutzen. Der Nachteil ist eine etwas schwierigere Integration, abhängig von der Anzahl der Nutzer, die Zugang benötigen.
In diesem Abschnitt wird davon ausgegangen, dass du einen Linux-Server im Einsatz hast und Git sowie einen SSH-Server installiert hast. Wenn nicht, kannst du eine Linux-Instanz bei AWS oder einem anderen Cloud-Provider einrichten. Sowohl Git als auch ein SSH-Server sind über die Paketverwaltungstools der meisten großen Linux-Distributionen verfügbar.
Hinweis
Der Begriff "Git-Server" ist ein wenig irreführend. Auf einem Git-Server läuft keine spezielle Software außer denselben Git-bezogenen Befehlen, die auch auf einem Client laufen. Der "Server" ist lediglich eine Vereinbarung, dass du einen zentralen Ort nutzt, von dem aus der Quellcode hoch- und heruntergeladen wird. Für viele ist dieser zentrale Ort GitHub, aber für andere ist es ein interner Server.
Eines der Protokolle, die du für die Kommunikation zwischen Client und "Server" mit Git nutzen kannst, ist SSH. Da SSH eine Schlüsseltechnologie hinter vielen anderen DevSecOps-Prozessen ist, macht die Verwendung von SSH für Git auch deshalb Sinn, weil die Software normalerweise aus anderen Gründen installiert wurde.
Die Git-Verwendungsmuster in diesem Abschnitt basieren beide auf einer rollenbasierten Zugriffskontrolle über Gruppen. Mit anderen Worten: Benutzer werden dem Linux-Server hinzugefügt und diese Benutzer werden dann zu Gruppen hinzugefügt. Zum Beispiel wird eine Gruppe namens gitusers erstellt. Die Mitglieder dieser Gruppe haben Zugriff auf die Git-Repositories. Das folgende Beispiel zeigt die gemeinsame Nutzung eines Repositorys durch zwei Benutzer. Dabei wird davon ausgegangen, dass die Benutzer bereits existieren. Danach können beide Benutzer Änderungen an den anderen Benutzer übertragen und von ihm abrufen. Die beiden Benutzernamen im Beispiel sind suehring und rkonkol, und beide werden der Gruppe gitusers hinzugefügt. Das Repository im Beispiel heißt devsecops und ist im Verzeichnis /opt/git/ auf dem Server gespeichert. Es gibt komplexere Szenarien für die gemeinsame Nutzung, sei es mit Git oder mit anderer Software wie GitHub.
Füge einen Benutzer namens gituser hinzu:
adduser gituser
Ändere die Shell des gituser-Kontos. Wenn du dazu aufgefordert wirst, ändere die Shell in /usr/bin/git-shell:
chsh gituser
Erstelle ein .ssh-Verzeichnis innerhalb des Home-Verzeichnisses von gituser:
cd /home/gituser && mkdir .ssh
Ändere die Eigentümerschaft des .ssh-Verzeichnisses sowie seine Berechtigungen:
chown gituser.gituser .ssh && chmod 700 .ssh
Füge eine authorized_keys-Datei im .ssh-Verzeichnis hinzu und ändere ihre Berechtigungen. Technisch gesehen ist dieser Schritt im Moment nicht erforderlich, aber er spart später einen Schritt:
touch .ssh/authorized_keys chmod 600 .ssh/authorized_keys
Füge eine Gruppe namens gitusers hinzu:
groupadd gitusers
Füge die beiden Konten für deine Entwickler hinzu:
adduser suehring adduser rkonkol
Füge jeden Benutzer zur Gruppe gitusers hinzu:
usermod -G gitusers suehring usermod -G gitusers rkonkol
Lass jeden der Entwickler mit dem Befehl ssh-keygen
SSH-Schlüssel erzeugen. Du kannst dies auch für die Entwickler tun, indem du sie wirst oder ihre Identität annimmst, indem du den Befehl su
verwendest, wie z. B:
su - suehring
Der Vollständigkeit halber: Wenn du als Benutzer suehring eingeloggt bist (oder dies angenommen hast):
mkdir .ssh chmod 700 .ssh cd .ssh ssh-keygen
akzeptiere die Standardeinstellungen für den Dateinamen und entscheide, ob du eine Passphrase zum Schlüssel hinzufügen möchtest.
Wenn ein SSH-Schlüssel erzeugt wird, wird ein Dateipaar erstellt. Standardmäßig heißen die Dateien id_rsa und id_rsa.pub. Die Datei id_rsa ist ein privater Schlüssel und die Datei id_rsa.pub ist ein öffentlicher Schlüssel. Der private Schlüssel sollte geheim gehalten und mit niemandem geteilt werden, während der öffentliche Schlüssel geteilt werden kann.
Zu diesem Zweck kopierst du den Inhalt des öffentlichen Schlüssels für jeden Benutzer in eine Datei namens authorized_keys im Home-Verzeichnis von gituser. Dieser Schritt ermöglicht es beiden Entwicklern, sich per SSH als gituser anzumelden. Achte darauf, dass du zwei Größer-als-Zeichen für diesen Befehl verwendest, sonst wird der Inhalt von authorized_keys überschrieben.
Angenommen, dein aktuelles Arbeitsverzeichnis enthält die Datei id_rsa.pub, was der Fall ist, wenn du die vorherigen Befehle befolgt hast, dann führe den folgenden Befehl aus, um den Schlüssel zur authorized_keys-Datei für gituser hinzuzufügen. Dieser Befehl sollte für jeden Entwickler mit dem Inhalt der jeweiligen Public-Key-Datei ausgeführt werden:
cat id_rsa.pub >> ~gituser/.ssh/authorized_keys
Bei den bisher durchgeführten Schritten handelt es sich um einmalige Gründungsschritte, die zur Vorbereitung des Servers durchgeführt werden müssen. In Zukunft werden nur noch die Entwicklerkonten erstellt und ein SSH-Schlüssel generiert und zur authorized_keys-Datei hinzugefügt. Nach der Ersteinrichtung wird es einfacher!
Wenn diese Schritte abgeschlossen sind, ist es an der Zeit, ein Git-Repository zu erstellen. Führe auf dem Git-Server diesen Befehl aus, um das Verzeichnis zu erstellen, in dem das Repository gespeichert wird:
mkdir /opt/git/devsecops.git && cd /opt/git/devsecops.git
Wie bereits erwähnt, verwendet dieser Server /opt/git als Basis für Git-Repositories. Du kannst die Repositories auch an einem anderen Ort speichern, je nach deinem Organisationsstandard.
Erstelle das Repository:
git init --bare --shared=group
Ändere die Eigentumsverhältnisse und Berechtigungen:
chown -R gituser.gitusers /opt/git/devsecops.git chmod 770 /opt/git/devsecops.git
Das war's. Wenn du das nächste Mal ein Repository hinzufügen musst, kannst du einfach die Befehle ausführen, um das Repository zu initialisieren und seine Eigentümerschaft und Berechtigungen zu ändern, da das gituser-Konto und die Entwickler-Konten bereits erstellt wurden.
An diesem Punkt sollte der Entwickler in der Lage sein, das Git-Repository in seine lokale Entwicklungsumgebung zu klonen. Dieser Befehl geht von dem Servernamen source.example.com aus . Ändere diesen entsprechend deiner Server-Namenskonvention:
git clone gituser@source.example.com:/opt/git/devsecops.git
Wenn der Entwickler zum ersten Mal per SSH auf den Server zugreift, wird er aufgefordert, den Host-Schlüssel vom Server zu akzeptieren. Unter der Annahme, dass der Host-Schlüssel gültig und korrekt ist, wird nach der Eingabe von "Ja" ein Klon des Git-Repositorys in das Verzeichnis namens devsecops im aktuellen Verzeichnis heruntergeladen.
Jetzt, da die Einrichtung abgeschlossen ist, ist es an der Zeit, sich mit Git zu beschäftigen.
Git verwenden (in Kürze)
Es gibt eine Handvoll Befehle, die du häufig mit Git verwenden wirst. Die Grundidee ist:
-
Klon-Repository.
-
Schreibe den Code.
-
Commit-Code.
-
Push-Code.
Wenn du mit anderen Entwicklern zusammenarbeitest, musst du einen weiteren Schritt hinzufügen:
-
Code zusammenführen.
Es ist dieser letzte Schritt, das Zusammenführen von Code, bei dem alle Probleme des Lebens auftreten und der ein Hauptgrund dafür ist, warum DevSecOps benötigt wird. Mehr zum Zusammenführen später.
Du hast bereits die beiden Git-Befehle git init
und git clone
kennengelernt. Die Initialisierung eines Repositorys wird einmal pro Repository durchgeführt, daher wird der Befehl git init
nur selten verwendet. Das Klonen eines Projektarchivs wird häufiger vorkommen, nämlich jedes Mal, wenn du eine neue Kopie des Projektarchivs herunterladen musst. Sobald das Projektarchiv geklont ist, wirst du jedoch andere Befehle verwenden, um deinen Code an den Server zu senden und Code von anderen im gleichen Projektarchiv abzurufen.
Es gibt keinen Git-spezifischen Befehl für den Schritt "Code schreiben", den ich bereits erwähnt habe. Nachdem der Code geschrieben wurde, sollten die Änderungen jedoch in das Repository übertragen werden. Es gibt zwei Hauptwege, auf denen der Code in einem Repository verfolgt wird:
-
Hinzufügen einer neuen Datei mit neuem Code
-
Hinzufügen von Code zu einer Datei, die bereits im Repository existiert
Wenn eine neue Datei zu einem Git-Repository hinzugefügt wird, muss diese Datei nachverfolgt werden, damit Änderungen notiert werden und einen Verlauf dieser Änderungen erhält. Der Befehl git add
wird verwendet, um eine neue Datei hinzuzufügen, die von einem Git-Repository verfolgt werden soll. Dieser Befehl fügt eine Datei namens README.md zu einem bestehenden Repository hinzu:
git add README.md
Von diesem Zeitpunkt an werden Änderungen an der Datei README.md von Git in diesem Repository verfolgt.
Wenn eine Datei bereits im Repository vorhanden ist, was so viel bedeutet wie: "Git kennt die Datei", dann werden die Änderungen zwar verfolgt, müssen aber noch in das Repository übertragen werden. Man kann sich einen Commit auch als Checkpoint oder als Momentaufnahme des Inhalts der Datei(en) zu diesem Zeitpunkt vorstellen. Ein wichtiger konzeptioneller Unterschied zwischen Git und anderen SCM-Tools ist, dass der Befehl git add
auch jedes Mal ausgeführt wird, wenn du Änderungen an der Datei festschreiben willst. Dieses Konzept kann verwirrend sein und bedarf einer zusätzlichen Erklärung.
Erinnere dich daran, dass im vorherigen Abschnitt ein Repository namens devsecops erstellt wurde. Dieses Repository enthält nichts; es ist leer bis auf ein .git-Verzeichnis, das von Git selbst verwaltete Metadaten enthält. Wenn eine Datei zum devsecops-Verzeichnis hinzugefügt wird, befindet sich die Datei in einem ungetrackten Zustand, d. h. Git weiß, dass die Datei im devsecops-Verzeichnis existiert, ignoriert sie aber.
Unverfolgt ist einer von zwei Zuständen, in denen Dateien in einem Git-Repository existieren. Ein weiterer Status für eine Datei innerhalb eines Repositorys ist der getrackte Status. Wenn Dateien innerhalb eines Repositorys Git bekannt werden, werden sie als getrackt bezeichnet. Aber diese beiden Zustände sagen nur einen Teil der Geschichte aus. Wenn eine Datei verfolgt wird, beginnt Git, diese Datei auf Änderungen zu überwachen. Hier beginnen die konzeptionellen Probleme rund um die Begriffe "Zustand" und "Status". In der Praxis sind nicht verfolgte Dateien für das Repository irrelevant, deshalb lassen wir sie hier stehen und konzentrieren uns stattdessen auf verfolgte Dateien.
Wenn eine Datei getrackt wird, wird sie für immer von Git getrackt. Die Datei existiert in einem dieser Zustände:
-
Unverändert
-
Geändert
-
Inszeniert
Eine unveränderte Datei ist eine Datei, die seit der letzten Übertragung nicht verändert hat. Bei bestehenden Repositories, die gerade geklont wurden, sind alle Dateien unverändert, da sie gerade vom entfernten Repository heruntergeladen wurden. Wenn jedoch eine getrackte Datei geändert wird, bezeichnet Git die Datei als modifiziert.
Nur weil die Datei geändert wurde, heißt das nicht, dass sie übertragen wird oder von anderen Entwicklern eingesehen werden kann. Damit die Datei übertragen und von anderen Entwicklern eingesehen werden kann, muss sie bereitgestellt werden. Eine Stagedatei ist eine Datei, die in die nächste Übertragung einbezogen wird.
Der Unterschied zwischen geänderten und bereitgestellten Dateien ist genauso grundlegend wie der Unterschied zwischen getrackten und ungetrackten Dateien. Mit geänderten und bereitgestellten Dateien kannst du auswählen, welche Dateien du übertragen möchtest. Du kannst auch mehrere Commits machen, so dass jeder Commit einen eigenen Snapshot mit eigenen Dateien erstellt. Es kann sein, dass du die Commits nie auf diese Weise trennen musst, aber die Flexibilität von modified versus staged steht dir zur Verfügung, falls du sie einmal brauchst.
Das Übertragen von Änderungen in das Projektarchiv erfolgt mit dem Befehl git commit
. Nehmen wir an, dass eine Datei namens index.php bereits im Projektarchiv existiert.
Wenn du Änderungen an der Datei vornimmst, musst du die Datei noch mit git add
zum Staging für diesen Commit hinzufügen. Nachdem git add
ausgeführt wurde, ist der nächste Schritt die Übergabe der bereitgestellten Änderungen:
git commit
Der Code selbst wird als Checkpoint gespeichert und zu den Metadaten der Historie hinzugefügt, die von Git verfolgt werden. Wenn du git commit
ausführst, wirst du aufgefordert, eine Commit-Nachricht hinzuzufügen. Die Commit-Nachricht ist eine kurze Nachricht über den Commit selbst. Wenn du zum Beispiel der index.php einen neuen Titel hinzugefügt hast, kannst du die Nachricht "Neuer Titel hinzugefügt" hinzufügen. Diese Nachricht ist dann in der Commit-Historie des Repositorys zu sehen (mehr dazu später).
Wenn du nicht zur Eingabeaufforderung für eine Commit-Nachricht aufgefordert werden möchtest und auch nicht für jede Änderung an einer verfolgten Datei git add
ausführen willst, kannst du ein paar Befehlszeilenoptionen hinzufügen, die dir beides ersparen. Der folgende Befehl entspricht der Ausführung von git add
und git commit
und dem anschließenden Hinzufügen der vorherigen Nachricht in einem Texteditor:
git commit -a -m "Added new title"
Die Option -a
fügt dem Commit Dateien hinzu, die dem Repository bereits hinzugefügt oder bekannt gemacht wurden. Die Option -m
fügt eine Nachricht hinzu.
Auch wenn die Änderungen an das Repository übertragen wurden, werden sie nur auf deinem lokalen Rechner gespeichert. Das bedeutet, dass die Änderungen nicht von anderen Entwicklern eingesehen werden können und, was noch wichtiger ist, dass sie auch nicht auf andere Weise gesichert werden als durch Backups, die du für deinen lokalen Entwicklungsrechner eingerichtet hast. Der letzte Schritt besteht darin, den Code zurück an den Server zu senden. Dies geschieht ganz einfach mit dem Befehl git push
:
git push
Der Code wird auf den Server hochgeladen, von dem das Repository zuerst geklont wurde.
Tipp
Wenn du dir nicht sicher bist, wohin der Code gehen soll, verwende den folgenden Befehl:
git remote show origin
Auf diese Weise wird das Ziel für den Befehl git push
angezeigt.
Du kannst die Commit-Historie des Repositorys mit dem Befehl git log
einsehen. Wenn du git log
ausführst, werden die Commits angezeigt, die im Repository bekannt sind. Da die Commit-Historie nicht mit dem Server kommuniziert, werden nur die Commits angezeigt, die aus dem Projektarchiv heruntergeladen oder geklont wurden.
Verzweigung und Zusammenführung
In einer idealen Welt ist ein einziger Entwickler für den gesamten Code einer Anwendung verantwortlich. Dieser Entwickler versteht nie eine Anforderung falsch, Anforderungen werden nie verfehlt, sein Code ist perfekt und es gibt nie Bugs oder andere Fehler, die außerhalb seiner Kontrolle liegen.
In der realen Welt arbeiten Entwickler in Teams, Anforderungen werden übersehen und falsch interpretiert, Fehler werden eingeführt und Fehler werden gefunden, die außerhalb der Kontrolle der Entwickler liegen. Wenn Entwickler/innen im Team arbeiten, kann der gemeinsame Code manchmal eine Fehlerquelle sein. Wenn die Entwickler/innen eine Datenstruktur unterschiedlich implementieren oder eine Variable einfach falsch schreiben, treten beim Zusammenführen des Codes Fehler auf. Je später die Zusammenführung erfolgt, desto mehr Auswirkungen hat der Fehler auf die vorherigen Schritte.
Anders ausgedrückt: Die Wahrscheinlichkeit, dass sich ein Fehler auf das Veröffentlichungsdatum auswirkt, ist größer, wenn der Code später zusammengeführt und getestet wird. Die Behebung des Fehlers bedeutet zum Beispiel, dass zuvor getesteter Code erneut getestet werden muss. Wenn ein anderer Teil des Codes auf dem Fehler beruhte oder den Fehler umging, muss dieser andere Code möglicherweise neu geschrieben werden.
Verzweigungen im SCM bieten die Möglichkeit, parallel an einem Code zu arbeiten, ohne andere zu beeinträchtigen. Wenn ein Repository geklont wird, wird der Master- oder Hauptzweig geklont. Von diesem Hauptzweig aus kann ein Entwickler einen neuen Zweig erstellen, um an dem Code für eine neue Funktion zu arbeiten. Gleichzeitig können andere Entwicklerinnen und Entwickler dasselbe tun, indem sie jeweils einen eigenen Zweig des Codes erstellen, der von den anderen getrennt ist.
Das Erstellen eines Zweigs innerhalb eines Git-Repositorys erfolgt mit dem Befehl git branch
. Wenn du zum Beispiel einen Zweig mit dem Namen project5
erstellen möchtest, um an den Änderungen für den neuen Titel der Website zu arbeiten, verwendest du den folgenden Befehl:
git branch project5
Der Zweig wird dann erstellt, wobei der aktuelle Code als Basis dient. Während der Zweig erstellt wurde, bleiben alle Änderungen, die du vornimmst, im aktuellen Zweig, bis du zum neu erstellten Zweig wechselst. Dies wird mit dem Befehl git checkout
erreicht, wie in:
git checkout project5
Genau wie die Optionen, die du dem Befehl git commit
hinzugefügt hast, kannst du auch dem Befehl git checkout
eine Kommandozeilenoption hinzufügen, die den Zweig erstellt und auf einmal zu ihm wechselt:
git checkout -b project5
Änderungen am Code und die damit verbundenen Commits werden jetzt an den project5
Zweig gesendet. Der Befehl git merge
wird verwendet, wenn Code zurück in den Hauptzweig gebracht werden soll. Beim Merging wird jedes Objekt im Repository untersucht, um festzustellen, ob es Änderungen gibt, die zwischen den beiden Zweigen des Codes eingefügt werden müssen. Git tut sein Bestes, um festzustellen, welches Objekt die neueste Version ist und um Konflikte zwischen zwei Dateien, die sich zwischen den Zweigen geändert haben, zu lösen. Weitere Informationen findest du im Abschnitt Grundlagen des Verzweigens und Zusammenführens in der offiziellen Git-Dokumentation, wo du weitere Details zum Zusammenführen findest und was du tun kannst, wenn ein Konflikt beim Zusammenführen auftritt.
Durch die Verzweigung wird der Code von mehreren Entwicklern zwar logisch getrennt, aber sie löst nicht das Problem, dass späte Zusammenführungen zu Fehlern und ungetestetem Verhalten führen. Es gibt mehrere Methoden, um die Entwicklung im Team zu verwalten. Eine davon ist das Gitflow-Muster, das wir uns im Folgenden ansehen werden.
Untersuchung des Gitflow-Musters
Gitflow beschreibt einen Prozess zum Austausch von Code über Git, der getrennte Entwicklungspfade verwendet. Gitflow verwendet Verzweigungen. Abbildung 4-1 zeigt das Gitflow-Muster.
Wie Abbildung 4-1 zeigt, gibt es mehrere Swimlanes, in denen aktive Entwicklung stattfindet, während andere Swimlanes für die Hauptproduktionslinie oder freigegebenen Code reserviert sind. Ein Durchlauf des Codes durch Gitflow hilft zu veranschaulichen, wie Änderungen angewendet und dann wieder in ein Release eingebracht werden, bevor sie an die Produktion gesendet werden. Nehmen wir einen Code für eine Website. Am ersten Tag, an dem der Code veröffentlicht wird, wird ein Fehler gefunden. Der Fehler muss sofort behoben werden. Deshalb erstellt ein Entwickler einen Zweig, um einen Hotfix anzuwenden. In Abbildung 4-1 erscheint der Hotfix in der Hotfix-Swimlane. Der Entwickler stellt jedoch fest, dass der Fehler etwas größer ist als erwartet, und überlegt daher weiter, wie er ihn beheben kann.
In der Zwischenzeit wird mit der Entwicklung von Verbesserungen an der Website begonnen. Dies wird durch eine Swimlane "Entwickeln" und einen entsprechenden Zweig dargestellt. Der Entwicklungszweig wird weiter in Funktionszweige aufgeteilt, damit mehrere Entwickler/innen gemeinsam an diesem Entwicklungsschritt oder Sprint arbeiten können. Wenn die Features fertiggestellt sind, wird der Code wieder in den Entwicklungszweig eingefügt. Schließlich werden die Features und Hotfixes (in Abbildung 4-1 nur ein Hotfix) mit einem Release-Zweig zusammengeführt.
Der Release-Zweig enthält den Code, der bereit ist, eingesetzt zu werden. In dieser letzten Phase werden verschiedene Elemente zusammengeführt, wobei die genauen Komponenten und Prozesse von Unternehmen zu Unternehmen und von Release zu Release variieren. Manche Unternehmen haben zum Beispiel einen formalen Build-Prozess, bei dem der Code kompiliert und abschließende Tests durchgeführt werden. Andere Unternehmen haben vielleicht eine längere Beta-Testphase mit einem Release Candidate.
Während des Zusammenführungsprozesses zwischen den einzelnen Swimlanes in der Gitflow-Architektur kann es eine oder mehrere Genehmigungsstufen geben, bevor die Zusammenführung erlaubt wird. Diese Gatekeeping-Prozesse dienen als Kontrollpunkt, um sicherzustellen, dass unerwünschter Code und seine Nebeneffekte nicht in den Release- oder Hauptzweig oder in die Produktionsumgebung gelangen. Ein größeres Problem ist, dass Entwicklungszweige oft lange Zeit aktiv bleiben. Features und Hotfixes werden in den Entwicklungszweig eingebracht, aber manchmal wird ein Hotfix nicht angewendet oder durch späteren Code überschrieben, der dann das Problem, für das der Hotfix ursprünglich angewendet wurde, erneut aufwirft.
Mit DevOps und dann DevSecOps wurde der Schwerpunkt auf kontinuierliche Integration/kontinuierliche Bereitstellung (CI/CD) und die automatisierten Tests gelegt, die notwendig sind, um den Code mit minimalen Prüfungen in die Produktion zu überführen. Die Prämisse ist, das Testen früher und häufiger durchzuführen und damit die Testphase in einem traditionellen Wasserfall-SDLC-Modell nach links zu verschieben.
Hinweis
Wenn du "shift left" liest oder hörst, bedeutet das, dass das Testen und andere Elemente der Softwareentwicklung früher stattfinden, damit Probleme erkannt und behoben werden, bevor sie sich verschlimmern.
Mit dem Schwerpunkt auf Automatisierung und dem Verzicht auf formale und manuelle Genehmigungsprozesse wurde eine neue Methode für das Branching entwickelt. Diese neue Methode ist eine Vereinfachung des Gitflow-Musters, und ist der Schwerpunkt des nächsten Abschnitts.
Untersuchung des stammbasierten Musters
Die Prämisse hinter dem trunk-basierten Muster ist es, auf langlebige Entwicklungszweige zu verzichten und stattdessen den Code häufig in einen Hauptzweig, den sogenannten Trunk, zu übertragen, damit der Code getestet und eingesetzt werden kann. Abbildung 4-2 zeigt ein Beispiel für das trunk-basierte Muster.
Wenn du das Trunk-basierte Muster mit dem Gitflow-Muster vergleichst, wirst du feststellen, dass es weniger Swimlanes gibt: nur Trunk, Features und Hotfixes. Die Idee ist, vom Trunk aus zu promoten, der notwendigerweise kurzlebig ist, um große Merges zu vermeiden. Es ist erwähnenswert, dass es manchmal sowohl den Release- als auch den Main-Zweig gibt, aber das ist eher eine logische oder prozessbedingte Notwendigkeit als eine Voraussetzung für die trunkbasierte Entwicklung.
"Promote early, promote often" ist der Grundgedanke der trunk-basierten Entwicklung. Das klingt in der Theorie großartig, aber in der Praxis bedeutet das, dass eine ausgereifte und gründliche Testinfrastruktur vorhanden und in der Lage sein muss, die Anwendung zu testen. Dies ist das Konzept der Codeabdeckung. Die Codeabdeckung beschreibt das Verhältnis zwischen dem Code und den Tests dieses Codes. Betrachte zum Beispiel diese einfache bedingte Anweisung:
if ($productType == "grocery") { $taxable = false; }
Ein positiver Test für diesen Code besteht darin, den Inhalt der Variablen $productType
auf grocery zu setzen und dann zu prüfen, ob $taxable
danach wahr, falsch oder nicht gesetzt ist. Ein anderer Test besteht darin, $productType
auf irgendetwas anderes als die Zeichenkette grocery zu setzen und danach den Inhalt der Variablen $taxable
zu überprüfen. Es wäre verlockend zu sagen, dass die Codeabdeckung bei diesem Code 100% beträgt. Was aber, wenn $productType
nicht gesetzt ist? Die Antwort hängt weitgehend von dem Code ab, der über dem hier gezeigten Beispiel stehen würde. In einigen Sprachen ist es auch nicht möglich, $productType
zu deaktivieren, was zu einer Fehlermeldung beim Kompilieren führen würde. Daher hängt die Codeabdeckung von der Sprache und dem Kontext ab.
Die Hinwendung zur Codeabdeckung führt das Kapitel zum Konzept des Testens, das zufälligerweise der nächste Abschnitt ist. Die Wahl einer Verzweigungsstrategie ist keine dauerhafte Entscheidung. Ich empfehle, formalisiertere und bewusstere Muster für die Codeverwaltung auszuprobieren (oder damit fortzufahren), um besser zu verstehen, wo Lücken in der Entwicklung, beim Testen und beim Einsatz bestehen. Wenn du die Entwicklungspraktiken, die Testabdeckung und die Bereitstellung verbesserst, vereinfache die Prozesse der Codeverwaltung , damit sie dem Fortschritt nicht im Weg stehen.
Code prüfen
Die Prüfung einer Anwendung auf Fehler wird in verschiedenen Phasen des SDLC durchgeführt. Auf der einfachsten Ebene testet ein Entwickler seinen eigenen Code. Betrachte die bedingte Anweisung aus dem vorherigen Abschnitt. Ein Entwickler würde seinen Code wahrscheinlich auf die in diesem Abschnitt beschriebenen Fälle testen, und zwar sowohl den positiven Fall, wenn der Produkttyp auf Lebensmittel eingestellt ist, als auch mindestens einen negativen Fall, wenn der Produkttyp auf etwas anderes als Lebensmittel eingestellt ist.
In diesem Abschnitt werden verschiedene Aspekte des Testens untersucht, von grundlegenden Entwicklertests bis hin zu QS-Tests durch Endnutzer. Bei der Diskussion über das Testen geht es sowohl um funktionale als auch um nicht-funktionale Anforderungen. Aus Kapitel 1 wissen wir, dass eine nicht-funktionale Anforderung etwas wie Sicherheit oder Transaktionsgeschwindigkeit ist. Es ist zwar möglich, dass diese als spezifische Anforderungen hervorgehoben werden, aber es ist wahrscheinlicher, dass die Anforderungserhebung keine Fragen wie "Wie schnell soll die Anwendung geladen werden?" enthält. Stattdessen können sich nicht-funktionale Anforderungen auf Service Level Agreements stützen.
Einheitstest
Der bedingte Code, den wir weiter oben in diesem Kapitel besprochen haben, ist Teil eines größeren Codeblocks, der von den Entwicklern beim Schreiben des Codes getestet wird . Wenn er in kleinen Einheiten getestet wird, auf Funktionsebene oder in ähnlich kleinen Codeteilen, nennt man das Unit Testing. Es gibt keine strikte Regel, wie klein oder groß ein Codeblock sein darf, damit er noch als Unit-Test gilt. Je größer die Anzahl der Abhängigkeiten ist, desto unwahrscheinlicher wird es, dass der Test als Einheitstest angesehen wird. Anders ausgedrückt: Wenn der zu prüfende Code von mehreren anderen Dateien und Vorbedingungen abhängt, dann ähnelt der Test eher einem Integrationstest, bei dem mehrere Elemente kombiniert werden.
Ein grundlegendes Ziel von Unit-Tests ist die 100%ige Abdeckung aller Bedingungen, wie z.B. die oben beschriebene Bedingung. Zusätzlich sollte eine statische Analyse des Codes durchgeführt werden. Die statische Analyse beschreibt eine Möglichkeit, den Code zu untersuchen, ohne ihn auszuführen. Die statische Analyse wird häufig eingesetzt, um die Einhaltung von Codierungsstandards zu überprüfen und um die grundlegende Sicherheit der Anwendung zu gewährleisten.
Unit-Tests gibt es unabhängig von DevSecOps-Prozessen. Auf dem Weg zu DevSecOps müssen jedoch so viele Unit-Tests wie möglich automatisiert werden. Im Idealfall sollten alle Unit-Tests automatisiert ausgeführt werden. Dies erleichtert die CI/CD-Prozesse, die notwendig sind, um DevSecOps vollständig zu nutzen.
Integrationstests
Integrationstests bringen Codeeinheiten zusammen, um zu überprüfen, ob diese Einheiten zusammenarbeiten, um die funktionalen Anforderungen der Anwendung zu erfüllen. Das Erreichen der Ebene der Integrationstests setzt voraus, dass die Unit-Tests vollständig und erfolgreich sind. Wenn es Verbindungen zwischen Codeeinheiten gibt, überprüfen Integrationstests, ob diese Verbindungen wie vorgesehen funktionieren.
Systemprüfung
Die dritte Stufe der Prüfung wird gemeinhin als Systemprüfung bezeichnet. Das Ziel der Systemtests ist es, alle Komponenten in einer Umgebung zu kombinieren, die der Produktionsumgebung so nahe wie möglich kommt. Bei den Systemtests sollten sowohl funktionale als auch nicht-funktionale Tests durchgeführt werden, und im Idealfall handelt es sich bei den verwendeten Daten um eine anonymisierte Version der Produktionsdaten oder eine Teilmenge davon. Der Vorbehalt bei der Frage, ob eine Teilmenge von Daten akzeptabel ist, besteht darin, dass die Verwendung nur eines Teils der Daten leistungsbezogene Probleme verbergen kann. Wenn es sich bei dem normalen Produktionsdatensatz beispielsweise um eine Legacy-Datenbank mit mehreren Terabyte handelt und eine der Funktionen der Anwendung die Abfrage dieser Daten erfordert, kann die Verwendung von nur wenigen Gigabyte ein Problem mit der Abfrage verschleiern. In der Testumgebung kann die Abfrage Ergebnisse mit akzeptabler Leistung liefern, aber wenn der gesamte Datenbestand abgefragt wird, dauert es Minuten, bis die Ergebnisse zurückkommen.
Tests automatisieren
Die Automatisierung ist ein Schlüsselfaktor für den Erfolg aller DevSecOps-Bemühungen. Es gibt zahlreiche Tools, die dabei helfen, das Testen von Code zu automatisieren. Ein solches Tool ist Selenium. Selenium bietet eine vollwertige Testsuite, die skaliert werden kann, um Tests von mehreren Standorten aus zu verteilen, sowie eine IDE, die bei der Erstellung von Tests hilft. Für den zugrunde liegenden Selenium-Web-Treiber gibt es auch Python-Bindings.
Abrufen einer Seite mit Selenium und Firefox
Du kannst Selenium-Tests über die Kommandozeile mit Python ausführen. Auf diese Weise kannst du ein einfaches Mittel zum Testen während der Entwicklung, aber auch einen ausgeklügelten Bot erstellen, der die Website beim Erstellen crawlen kann und dabei Screenshots macht, um zu beweisen, dass eine Seite existiert und ohne Fehler gerendert wurde. Dieser Abschnitt zeigt Selenium mit einem Headless Firefox-Browser, der auf Debian Linux läuft. Später im Buch werde ich dir ein komplexeres Beispiel mit Docker zeigen. Das einfache Beispiel in Beispiel 4-1 scheint in vielen Online-Tutorials zu fehlen. Dem Beispiel fehlen zwar einige Debugging-Optionen und andere Annehmlichkeiten, die du dir vielleicht wünschst, wie z. B. ein try/catch
Block, aber die können später hinzugefügt werden.
Beispiel 4-1. Einfacher Python-Code zum Abrufen einer Webseite und Erfassen der Ergebnisse
#!/usr/bin/env python from selenium import webdriver proto_scheme = "https://" url = "www.braingia.org" opts = webdriver.FirefoxOptions() opts.add_argument('--headless') driver = webdriver.Firefox(options=opts) driver.implicitly_wait(10) driver.get(proto_scheme + url) driver.get_screenshot_as_file('screenshot.png') result_file = 'page-source_' + url with open(result_file,'w') as f: f.write(driver.page_source) f.close() driver.close() driver.quit()
In Beispiel 4-1 fragt die erste Zeile die Umgebung nach einer ausführbaren Python-Datei ab und ermöglicht die Ausführung der Datei als normales Kommando, ohne dass dem Dateinamen in der Kommandozeile "python" vorangestellt werden muss. Du wirst im Internet zahlreiche Beispiele finden, in denen in Python geschriebene Programme auf diese Weise ausgeführt werden:
python3 program.py
Wenn du stattdessen den Interpreter wie gezeigt in die erste Zeile einfügst, kann die Datei wie folgt ausgeführt werden:
./program.py
Hinweis
Es wird davon ausgegangen, dass die Datei ausführbar ist; wenn nicht, kannst du unter chmod u+x program.py
das ausführbare Bit hinzufügen.
Python 3 sollte die Standardeinstellung sein. Wenn das nicht der Fall ist oder wenn du Fehler bezüglich der auf deinem System verwendeten Python-Version erhältst, kannst du diese Zeile komplett entfernen und die Datei wie oben gezeigt ausführen, indem du den Python 3-Interpreter voranstellst.
Als Nächstes wird der Webdriver von Selenium importiert, gefolgt von zwei Variablen, um das Protokollschema und den Hostnamen festzulegen, die getestet werden sollen. In den nächsten drei Zeilen wird eine Option gesetzt, mit der Firefox im Headless-Modus ausgeführt wird. Die Headless-Option wird verwendet, damit kein X Window System oder eine Desktop-Umgebung installiert sein muss, damit das Programm funktioniert. Firefox wird einfach hinter den Kulissen ausgeführt, ohne dass die grafische Oberfläche benötigt wird.
In der folgenden Zeile wird eine Wartezeit von 10 Sekunden festgelegt. Dies kann je nach Bedarf für deine Umgebung angepasst werden. Für dieses Beispiel wurden zehn Sekunden willkürlich gewählt. In der nächsten Zeile wird die Webseite abgerufen und ein Screenshot mit dem Namen screenshot.png erstellt. Der letzte Abschnitt des Programms öffnet eine lokale Datei zum Schreiben und legt den Seitenquelltext in dieser Datei ab. Zum Schluss wird die Sitzung geschlossen und die Ausführung des Browsers beendet.
Es ist wichtig zu wissen, dass das Programm eine Kopie von Firefox im Hintergrund ausführt. Wenn der letzte Aufruf von quit()
aufgrund eines früheren Fehlers nicht ausgeführt wird, laufen verwaiste Firefox-Prozesse auf dem System. Ein Neustart des Computers würde das Problem lösen, aber da es sich um Linux handelt, sollte ein Neustart nicht nötig sein. Du kannst die übrig gebliebenen Firefox-Prozesse mit diesem Befehl finden:
ps auwx | grep -i firefox
Die Ausgabe sieht dann ungefähr so aus, auch wenn der Benutzername und die Prozess-IDs unterschiedlich sind:
suehring 1982868 51.9 16.3 2868572 330216 pts/1 Sl 12:12 25:43 firefox-esr --marionette --headless --remote
Wenn du den Befehl kill
auf die Prozess-ID gibst, wird der Prozess angehalten. In diesem Beispiel lautet die Prozess-ID 1982868
. Daher sollte der folgende Befehl eingegeben werden, um diesen Prozess zu stoppen:
kill 1982868
Wie bereits erwähnt, bestünde eine offensichtliche Verbesserung darin, einen Großteil der Verarbeitung in einen try/catch
Block einzubinden, um die Gefahr zu verringern, dass nach einem Fehler verwaiste Prozesse übrig bleiben. Eine weitere Verbesserung wäre die Erfassung der Ausgangs-URL als Befehlszeilenoption sowie die Möglichkeit, die Website zu crawlen, d. h. die auf der Website gefundenen Links zu sammeln und diese zu besuchen. Manche halten das vielleicht nicht für notwendig oder gar für eine Verbesserung. Deshalb ist einfach besser, und dieses Beispiel zeigt die Grundlagen des Abrufs einer Seite.
Abrufen von Text mit Selenium und Python
Das vorherige Beispiel zeigt die Verwendung von Python, Selenium und Firefox, um den Quelltext einer Webseite abzurufen und einen Screenshot dieser Seite zu erstellen. Beim Testen möchtest du vielleicht darauf aufmerksam gemacht werden, dass eine Seite nicht richtig oder mit den richtigen Elementen oder Text innerhalb dieser Elemente dargestellt wird. Wenn du zum Beispiel Tests geschrieben hast, um dich auf einer Seite anzumelden und auf der nächsten Seite eine Begrüßung zu erwarten, die deinen Namen enthalten sollte, kannst du einen Test schreiben, der das angegebene HTML-Element abruft und überprüft, ob der richtige Name in diesem Element angezeigt wird.
Der Text kann mit verschiedenen Methoden abgerufen werden. Beispiel 4-2 zeigt, wie man den Copyright-Hinweis von einer Seite abrufen kann, wenn er in einem <p>
-Element enthalten ist, wie es derzeit auf meiner Website der Fall ist.
Beispiel 4-2. Abrufen einer Webseite und Anzeigen des Urheberrechtshinweises
#!/usr/bin/env python from selenium import webdriver proto_scheme = "https://" url = "www.braingia.org" opts = webdriver.FirefoxOptions() opts.add_argument('--headless') driver = webdriver.Firefox(options=opts) driver.implicitly_wait(10) driver.get(proto_scheme + url) driver.get_screenshot_as_file('screenshot.png') copyright = driver.find_element("xpath", "//p[contains(text(),'Copyright')]") print(copyright.text) result_file = 'page-source_' + url with open(result_file,'w') as f: f.write(driver.page_source) f.close() driver.close() driver.quit()
Die beiden wesentlichen Änderungen sind in der Auflistung von Beispiel 4-2 fett gedruckt. Du könntest den Text auch einfach in einer Zeile drucken, etwa so:
print(driver.find_element("xpath","//p[contains(text(),'Copyright')]").text)
Ich tendiere jedoch zu der in Beispiel 4-2 gezeigten Version, weil diese Version die Manipulation des Elements für andere Zwecke als die Anzeige des Textes ermöglicht.
Zusammenfassung
Dieses Kapitel enthält Informationen zur Entwicklung und zum Testen. Unter den Entwicklungsparadigmen und -mustern wird "absichtlich und bewusst" unterschätzt. Die Formulierung "absichtlich und bewusst" bringt jedoch auf den Punkt, warum du ein Muster oder sogar eine Codezeile an einer bestimmten Stelle verwendest. Wir haben auch das SCM-Tool Git sowie die Gitflow- und Trunk-basierten Architekturen für die Verwaltung des Codes von der Erstellung bis zur Bereitstellung untersucht. Schließlich habe ich drei Testebenen besprochen und Beispiele für die Testautomatisierung mit Selenium und Python vorgestellt. Mein Ziel war es, mit den Beispielen eine einfache Grundlage zu schaffen, zu der du zusätzliche Komplexität hinzufügen kannst.
In Kapitel 5 geht es weiter mit DevSecOps-Praktiken und der Verwaltung von Konfiguration als Code. Die Entwicklung mit Containerisierungstechniken ist häufig Teil von DevSecOps und moderner Entwicklung. In Kapitel 5 wird auch Docker vorgestellt.
Get DevSecOps lernen now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.