Kapitel 4. Bauen und Liefern
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Wir erfreuen uns an der Schönheit des Schmetterlings, geben aber selten die Veränderungen zu, die er durchgemacht hat, um diese Schönheit zu erreichen.
Maya Angelou
Die Entwicklung und Bereitstellung von Softwaresystemen ist kompliziert und kostspielig: Code schreiben, kompilieren, testen, in ein Repository oder eine Staging-Umgebung einbringen und dann in die Produktion überführen, damit die Endbenutzer ihn nutzen können. Würde die Förderung von Resilienz während dieser Aktivitäten nicht nur die Komplexität und die Kosten erhöhen? Mit einem Wort: Nein. Für die Entwicklung und Bereitstellung von Systemen, die gegen Angriffe resilient sind, sind keine besonderen Sicherheitskenntnisse erforderlich, und das meiste, was eine "sichere" Software ausmacht, überschneidet sich auch mit dem, was eine "hochwertige" Software ausmacht.
Wie wir in diesem Kapitel feststellen werden, können wir, wenn wir uns schnell bewegen, leicht austauschen und die Wiederholbarkeit unterstützen können, einen großen Beitrag dazu leisten, dass die Angreifer flink sind und wir die Auswirkungen von Stressoren und Überraschungen - ob von Angreifern oder anderen verschwörerischen Einflüssen - in unseren Systemen reduzieren können. Während dieses Kapitel als Leitfaden für Sicherheitsteams dienen kann, um ihre Strategie in dieser Phase zu modernisieren, ist unser Ziel in diesem Kapitel, dass Software- oder Plattformentwicklungsteams verstehen, wie sie die Resilienz durch ihre eigenen Anstrengungen fördern können. Wir brauchen Konsistenz und Wiederholbarkeit. Wir müssen es vermeiden, an allen Ecken und Enden zu sparen und trotzdem schnell zu sein. Wir müssen Innovationen durchziehen, um mehr Spielraum im System zu schaffen. Wir müssen uns ändern, um gleich zu bleiben.
In diesem Kapitel werden wir viel behandeln - es ist vollgepackt mit praktischen Weisheiten! Nachdem wir über mentale Modelle und Eigentumsverhältnisse gesprochen haben, werden wir den magischen Inhalt unseres Resilienztranks untersuchen, um herauszufinden, wie wir widerstandsfähige Softwaresysteme entwickeln und bereitstellen können. Wir werden überlegen, welche Praktiken uns helfen, die kritischen Funktionen des Systems herauszukristallisieren und in ihre Widerstandsfähigkeit gegen Angriffe zu investieren. Wir werden untersuchen, wie wir die Grenzen des sicheren Betriebs einhalten und diese Grenzen für mehr Spielraum erweitern können. Wir werden über Taktiken sprechen, mit denen wir die Interaktionen des Systems über Raum und Zeit hinweg beobachten können - und wie wir sie linearer gestalten können. Wir werden über Entwicklungspraktiken sprechen, die Feedbackschleifen und eine Lernkultur fördern, damit unsere mentalen Modelle nicht versteinern. Abschließend werden wir Praktiken und Muster entdecken, die uns flexibel halten - bereit und fähig, uns zu verändern, um den Erfolg unserer Organisation zu unterstützen, wenn sich die Welt weiterentwickelt.
Mentale Modelle bei der Entwicklung von Software
Im letzten Kapitel haben wir über gutes Design und gute Architektur aus einer Resilienzperspektive gesprochen. Es gibt viele Möglichkeiten, resilientes Design und eine resiliente Architektur zu untergraben, sobald wir mit dem Bau und der Bereitstellung dieser Designs beginnen. Dies ist die Phase, in der die Entwurfsabsichten zum ersten Mal verifiziert werden, denn die Programmierer müssen entscheiden, wie sie den Entwurf verifizieren, und diese Entscheidungen beeinflussen auch den Grad der Kopplung und der interaktiven Komplexität im System. Diese Entscheidungen beeinflussen auch den Grad der Kopplung und der interaktiven Komplexität des Systems. In diesem Kapitel geht es um die zahlreichen Kompromisse und Möglichkeiten, mit denen wir bei der Entwicklung und Bereitstellung von Systemen konfrontiert werden.
Diese Phase - die Entwicklung und Bereitstellung von Software - ist eine unserer wichtigsten Anpassungsmechanismen. In dieser Phase können wir uns anpassen, wenn sich unsere Organisation, unser Geschäftsmodell oder der Markt verändern. In dieser Phase passen wir uns an, wenn unsere Organisation skaliert. Der Weg zur Anpassung an solche chronischen Stressfaktoren führt oft über die Entwicklung neuer Software, daher müssen wir in der Lage sein, die Absicht unserer Anpassung genau in das neue System zu übertragen. Das Schöne an Chaos-Experimenten ist, dass sie aufdecken, wenn unsere mentalen Modelle von der Realität abweichen. In dieser Phase bedeutet das, dass wir eine ungenaue Vorstellung von dem haben, was das System jetzt tut, aber eine Vorstellung davon haben, wie es sich in Zukunft verhalten soll - repräsentiert durch unser Design. Wir wollen sicher vom aktuellen Zustand in den beabsichtigten zukünftigen Zustand gelangen.
In einer SCE-Welt müssen wir in Systemen denken. Das ist einer der Gründe, warum diese Phase als "Bauen und Liefern" und nicht nur als "Entwickeln" oder "Codieren" bezeichnet wird. Zusammenhänge sind wichtig. Die Software ist nur dann von Bedeutung, wenn sie in der Produktionsumgebung und im breiteren Software-Ökosystem "lebendig" wird. Nur weil sie auf deinem lokalen Rechner überleben kann, heißt das nicht, dass sie auch in der freien Wildbahn überleben kann. Erst wenn sie den Nutzern zur Verfügung gestellt wird - ähnlich wie wir die Geburt eines Menschen als Geburt bezeichnen -, wird die Software nützlich, denn jetzt ist sie Teil eines Systems. Während wir uns im nächsten Kapitel mit den Betriebsabläufen befassen, wollen wir den Wert dieser Systemperspektive für Softwareentwickler/innen hervorheben, die sich normalerweise mehr auf die Funktionalität als auf die Umgebung konzentrieren. Unabhängig davon, ob es sich bei deinen Endnutzern um externe Kunden oder um andere interne Teams handelt (die immer noch Kunden sind), musst du bei der Entwicklung und Bereitstellung eines stabilen Systems an den letztendlichen Kontext denken.
Sicherheitschaos-Experimente helfen Programmierern das Verhalten der Systeme zu verstehen, die sie auf mehreren Abstraktionsebenen aufbauen. Das Chaos-Experimentier-Tool kube-monkey löscht zum Beispiel zufällig Kubernetes ("k8s") Pods in einem Cluster und zeigt so, wie Ausfälle zwischen Anwendungen in einem k8s-orchestrierten System kaskadieren können (wobei k8s als Abstraktionsschicht dient). Das ist wichtig, weil Angreifer über Abstraktionsschichten hinweg denken und ausnutzen, wie sich das System tatsächlich verhält und nicht , wie es beabsichtigt oder dokumentiert ist, um sich zu verhalten. Das ist auch nützlich für die Fehlersuche und das Testen bestimmter Hypothesen über das System, um dein mentales Modell zu verfeinern - und so genug zu lernen, um das System mit jeder Iteration besser zu bauen.
Wer ist für die Anwendungssicherheit (und Ausfallsicherheit) verantwortlich?
SCE befürwortet die Softwareentwicklung Teams, die die Verantwortung für die Entwicklung und Bereitstellung von Software übernehmen, die auf belastbaren Mustern basiert, wie sie in diesem Buch beschrieben sind. Das kann in Unternehmen verschiedene Formen annehmen. Softwareentwicklungsteams können sich komplett selbst versorgen - ein vollständig dezentralisiertes Modell - bei dem jedes Team auf der Grundlage seiner Erfahrungen Richtlinien erarbeitet und sich darauf einigt, welche davon zum Standard werden sollen (ein Modell, das sich wahrscheinlich am besten für kleinere oder neuere Organisationen eignet). Ein Beratungsmodell ist eine weitere Option: Softwareentwicklungsteams könnten Verteidiger als Berater einsetzen, die ihnen dabei helfen, sich aus der Sackgasse zu befreien oder die Herausforderungen der Resilienz besser zu meistern. Die Verteidiger, die dies tun, können das Sicherheitsteam sein, aber sie könnten genauso gut das SRE- oder Plattform-Engineering-Team sein, das bereits ähnliche Aktivitäten durchführt - nur vielleicht nicht mit der Angriffsperspektive im Moment. Oder, wie wir in Kapitel 7 ausführlich erörtern werden, können Organisationen ein Resilienzprogramm entwickeln, das von einem Platform Resilience Engineering Team geleitet wird, das Richtlinien und Muster definieren und Werkzeuge entwickeln kann, die den resilienten Weg für interne Nutzer/innen zum zweckmäßigen Weg machen.
Warnung
Wenn deine Organisation ein typisches Verteidigungsmodell hat - wie ein separates Cybersicherheitsteam - gibt es wichtige Überlegungen, die beim Übergang zu einem Beratungsmodell beachtet werden müssen. Die Verteidiger können den Rest der Organisation nicht sich selbst überlassen und erklären, dass die Schulung des Sicherheitsbewusstseins ausreicht; wir werden in Kapitel 7 erörtern, warum das alles andere als ausreichend ist. Verteidiger/innen müssen Belastbarkeits- und Sicherheitsrichtlinien festlegen, dokumentieren und kommunizieren und als Berater/innen zur Verfügung stehen, um bei Bedarf bei der Umsetzung zu helfen. Das ist eine Abkehr vom traditionellen Modell der Cybersecurity-Teams, die Richtlinien und Verfahren durchsetzen, und erfordert einen Sinneswandel vom Autokraten zum Diplomaten.
Das Problem ist, dass die traditionelle Sicherheit - auch in ihrer modernen kosmetischen Aufmachung als "DevSecOps" - danach strebt, Softwareentwicklungsteams zu mikromanagen. In der Praxis drängen sich Cybersecurity-Teams oft in die Prozesse der Softwareentwicklung, um mehr davon zu kontrollieren und sicherzustellen, dass alles "richtig" gemacht wird, wobei "richtig" ausschließlich durch die Linse der Optimierung der Sicherheit gesehen wird. Wie wir aus der Welt des Organisationsmanagements wissen, ist Mikromanagement in der Regel ein Zeichen für schlechte Führungskräfte, unklare Ziele und eine Kultur des Misstrauens. Das Endergebnis ist eine immer engere Kopplung, eine Organisation als Ouroboros.
Das Ziel guter Design- und Plattform-Tools ist es, Resilienz und Sicherheit in den Hintergrund und nicht in den Vordergrund zu stellen. In einer idealen Welt ist die Sicherheit unsichtbar - derEntwickler merkt nicht einmal, dass im Hintergrund Sicherheitsmaßnahmen durchgeführt werden. Ihre Arbeitsabläufe fühlen sich nicht mühsamer an. Das hat mit der Wartbarkeit zu tun: Egal, wie eifrig du bist oder wie edel deine Absichten sind, Sicherheitsmaßnahmen, die die Arbeit in diesem Stadium behindern, sind nicht wartbar. Wie wir im letzten Kapitel beschrieben haben, besteht unser höheres Ziel darin, dem Produktionsdruck zu widerstehen, der dich in die Gefahrenzone treibt. Die Unternehmen wollen, dass du mehr Software billiger und schneller entwickelst. Unsere Aufgabe ist es, einen nachhaltigen Weg dorthin zu finden. Im Gegensatz zur traditionellen Informationssicherheit suchen SCE-basierte Sicherheitsprogramme nach Möglichkeiten, die Softwareentwicklung zu beschleunigen und gleichzeitig die Widerstandsfähigkeit zu erhalten - denn der schnelle Weg ist der Weg, der in der Praxis genutzt wird. Wir werden dies in Kapitel 7 gründlich untersuchen.
Es ist unmöglich, dass alle Teams einen vollständigen Kontext über alle Teile der Systeme deiner Organisation haben. Aber die Entwicklung von Resilienz hängt von diesem Kontext ab, denn die optimale Art und Weise, ein System so aufzubauen, dass es resilient bleibt - denk daran, Resilienz ist ein Verb - hängt von seinem Kontext ab. Wenn wir widerstandsfähige Systeme wollen, müssen wir die lokale Eigenverantwortung fördern. Versuche, die Kontrolle zu zentralisieren - wie die herkömmliche Cybersicherheit - werden unsere Systeme nur brüchig machen, weil sie den lokalen Kontext nicht kennen.
Die Ermittlung des Kontextes beginnt mit einer klaren Mission: "Das System funktioniert trotz der Anwesenheit von Angreifern mit der von uns beabsichtigten Verfügbarkeit, Geschwindigkeit und Funktionalität." Das ist ein wirklich offenes Ziel, so wie es sein sollte. Für das eine Unternehmen ist der effizienteste Weg, diese Mission zu erfüllen, die Entwicklung einer unveränderlichen und kurzlebigen App. Für ein anderes Unternehmen könnte es bedeuten, das System in Rust zu schreiben1 zu schreiben (und das Schlüsselwort unsafe
nicht als Schlupfloch zu benutzen...).2). Und für ein anderes Unternehmen ist es am besten, überhaupt keine sensiblen Daten zu sammeln und sie stattdessen Dritten zu überlassen - und damit auch die Sicherheit.
Lektionen, die wir von der Datenbankadministration auf dem Weg zu DevOps lernen können
Die Vorstellung, dass die Sicherheit erfolgreich sein könnte, während sie in den Händen der technischen Teams liegt, wird oft als Anathema für die Informationssicherheit angesehen. Aber das ist auch in anderen schwierigen Problembereichen wie der Datenbankverwaltung (DBA) der Fall.
DBA hat sich auf das "DevOps"-Modell verlagert (und nein, es heißt nicht DevDBOps). Ohne die Einführung von DevOps-Prinzipien leiden sowohl die Geschwindigkeit als auch die Qualität:
Unausgewogene Verantwortung und Autorität
Überlastetes Personal für den Datenbankbetrieb
Unterbrochene Rückkopplungsschleifen aus der Produktion
Geringere Produktivität der Entwickler
Kommt dir das bekannt vor? Wie DBA sind auch Sicherheitsprogramme traditionell in einem speziellen, zentralen Team angesiedelt, das von den technischen Teams getrennt ist, und stehen oft im Widerspruch zur Entwicklungsarbeit. Was können wir noch über die Anwendung von DevOps auf DBA lernen?
Die Entwickler sind für das Datenbankschema, die Arbeitslast und die Leistung verantwortlich.
Entwickler/innen beheben ihre eigenen Ausfälle und beheben sie.
Schema und Datenmodell als Code.
Es gibt eine einzige vollautomatische Verteilungspipeline.
Die App-Bereitstellung umfasst automatische Schema-Migrationen.
Automatisierte Aktualisierungen der Vorproduktion aus der Produktion.
Es gibt eine Automatisierung von Datenbankoperationen.
Diese Eigenschaften sind ein Beispiel für ein dezentrales Paradigma für die Datenbankarbeit. Es gibt kein einzelnes Team, dem die Datenbankarbeit oder das Fachwissen "gehört". Wenn in einem bestimmten Teil des Systems etwas schief läuft, ist das für diesen Teil des Systems zuständige Entwicklungsteam auch dafür verantwortlich, herauszufinden, was schief läuft und es zu beheben. Die Teams nutzen die Automatisierung der Datenbankarbeit, um die Einstiegshürde zu senken und die kognitive Belastung für die Entwickler/innen zu verringern, so dass nicht mehr so viel Datenbankwissen benötigt wird. Es stellt sich heraus, dass ein Großteil des erforderlichen Fachwissens in mühsamer Arbeit steckt; wenn manuelle, mühsame Aufgaben wegfallen, wird es für alle einfacher.
Es ist erwähnenswert, dass die Mühen und die Komplexität bei dieser Transformation nicht wirklich verschwunden sind (zumindest größtenteils); sie wurden nur hochgradig automatisiert und hinter Abstraktionsbarrieren versteckt, die von Cloud- und SaaS-Providern angeboten werden. Und der größte Einwand gegen diese Umstellung - dass sie entweder die Leistung ruinieren oder den Betrieb behindern würde - hat sich (meistens) als falsch erwiesen. Die meisten Unternehmen stoßen einfach nie auf Probleme, die die Grenzen dieses Ansatzes aufzeigen.
Wie der Daten- und Software-Ingenieur Alex Rasmussen anmerkt, ist dies derselbe Grund, warum SQL auf Cloud-Warehouses benutzerdefinierte Spark-Aufträge weitgehend ersetzt hat. Einige Unternehmen brauchen die Leistung und Flexibilität von Spark und sind bereit, den Aufwand zu investieren, um es erfolgreich zu machen. Die große Mehrheit der Unternehmen möchte jedoch nur einige strukturierte Daten aggregieren und ein paar Joins durchführen. Inzwischen haben wir diesen "gewöhnlichen" Modus hinreichend verstanden, so dass unsere Lösungen, die auf diesen gewöhnlichen Modus abzielen, ziemlich robust sind. Es wird immer Ausreißer geben, aber dein Unternehmen gehört wahrscheinlich nicht dazu.
Auch bei der Sicherheit gibt es Parallelen zu dieser Dynamik. Wie viele Leute machen ihre eigene Zahlungsabwicklung in einer Welt, in der es viele Zahlungsabwicklungsplattformen gibt? Wie viele Leute machen ihre eigene Authentifizierung, wenn es Anbieter von Identitätsmanagement-Plattformen gibt? Dies spiegelt auch das Prinzip "Wähle das Langweilige" wider, das wir im letzten Kapitel besprochen haben und später in diesem Kapitel im Zusammenhang mit dem Aufbau und der Bereitstellung von Lösungen diskutieren werden. Wir sollten davon ausgehen, dass unser Problem langweilig ist, bis das Gegenteil bewiesen ist.
Wenn wir die Attribute des DBA an die DevOps-Transformation für die Sicherheit anpassen, könnten sie etwa so aussehen:
Entwickler haben eigene Sicherheitsmuster, Arbeitsbelastung und Leistung.
Entwickler debuggen, beheben und reparieren ihre eigenen Vorfälle.
Sicherheitsrichtlinien und -regeln als Code.
Es gibt eine einzige, vollständig automatisierte Verteilungspipeline.
Die App-Bereitstellung beinhaltet automatische Änderungen der Sicherheitskonfiguration.
Automatisierte Aktualisierungen der Vorproduktion aus der Produktion.
Automatisierung von Sicherheitsmaßnahmen.
Du kannst diese Eigenschaften nicht durch ein Sicherheitsteam erreichen, das über alle herrscht. Die einzige Möglichkeit, diese Angleichung von Verantwortung und Rechenschaftspflicht zu erreichen, ist die Dezentralisierung der Sicherheitsarbeit. Security-Champions-Programme sind eine Möglichkeit, mit der Dezentralisierung von Sicherheitsprogrammen zu beginnen. Unternehmen, die mit diesem Modell experimentiert haben (wie Twilio, dessen Fallstudie über ihr Programm im früheren SCE-Bericht zu finden ist), berichten von erfolgreichen Ergebnissen und einer engeren Zusammenarbeit zwischen Sicherheit und Softwareentwicklung. Aber Security Champions Programme sind nur eine Brücke. Wir brauchen ein Team, das sich der Dezentralisierung widmet. Deshalb widmen wir das gesamte Kapitel 7 dem Platform Resilience Engineering.
Welche Praktiken fördern die Resilienz bei der Entwicklung und Bereitstellung von Software? Wir werden uns nun damit beschäftigen, welche Praktiken die einzelnen Bestandteile unseres Resilienztranks fördern.
Entscheidungen über kritische Funktionalitäten vor dem Bau
Wie ernten wir die erste Zutat unseres Resilienztrank-Rezepts - das Verständnis der kritischen Funktionen des Systems - wenn wir Systeme bauen und bereitstellen? Nun, wir sollten wahrscheinlich schon etwas früher damit anfangen, wenn wir entscheiden, wie wir unsere Entwürfe aus der vorherigen Phase umsetzen wollen. In diesem Abschnitt geht es um Entscheidungen, die du gemeinsam treffen solltest, bevor du einen Teil des Systems baust und wenn du ihn neu bewertest, wenn sich der Kontext ändert. Wenn wir kritische Funktionen durch die Entwicklung von Code implementieren, ist unser Ziel die Einfachheit und Verständlichkeit der kritischen Funktionen; der Dämon der Komplexität kann jeden Moment auftauchen und uns verschlingen!
Ein Aspekt der kritischen Funktionalität in dieser Phase ist, dass Softwareentwickler/innen in der Regel nur einen Teil des Systems entwickeln und ausliefern, nicht das Ganze. Neville Holmes, Autor der Kolumne "The Profession" in der Zeitschrift Computer des IEEE, sagt: "Im wirklichen Leben sollten Ingenieure das System entwerfen und validieren, nicht die Software. Wenn du das System, das du baust, vergisst, ist die Software oft unbrauchbar." Wenn wir die kritische Funktionalität aus den Augen verlieren - auf der Ebene der Komponenten, aber vor allem auf der Ebene des Systems -, werden wir unsere Investitionen falsch verteilen und unser Portfolio verderben.
Wie können wir den Aufwand in dieser Phase am besten verteilen, um sicherzustellen, dass kritische Funktionen gut definiert sind, bevor sie in der Produktion eingesetzt werden? In diesem Abschnitt schlagen wir einige fruchtbare Möglichkeiten vor, die es uns ermöglichen, schnell voranzukommen und gleichzeitig die Saat der Resilienz zu säen (und die unser Ziel von RAVE unterstützen, das wir in Kapitel 2 besprochen haben).
Tipp
Wenn du zu einem Sicherheitsteam gehörst oder es leitest, solltest du die Möglichkeiten in diesem Kapitel als Praktiken betrachten, die du in deinem Unternehmen verbreiten solltest, und dich darum bemühen, dass sie leicht zu übernehmen sind. Wahrscheinlich wirst du dich mit denjenigen zusammenschließen wollen, die innerhalb der technischen Organisation die Standards festlegen, um dies zu tun. Und wenn du Anbieter auswählst, die diese Praktiken und Muster unterstützen, beziehe die technischen Teams in den Evaluierungsprozess mit ein.
Softwareentwicklungsteams können diese Praktiken selbst übernehmen. Wenn es ein Plattform-Engineering-Team gibt, kann es sich darum bemühen, diese Praktiken so nahtlos wie möglich in die Arbeitsabläufe der Ingenieure zu integrieren. Wir werden den Ansatz des Platform Engineering in Kapitel 7 näher erläutern.
Erstens können wir mit dem "Schleusenansatz" Systemziele und Richtlinien definieren. Zweitens können wir durchdachte Codeüberprüfungen durchführen, um die kritischen Funktionen des Systems durch die Kraft konkurrierender mentaler Modelle zu definieren und zu überprüfen; wenn jemand in seinem Code etwas Seltsames tut - was bei der Codeüberprüfung auf die eine oder andere Weise auffallen sollte -, wird sich das wahrscheinlich in den Belastungseigenschaften seines Codes widerspiegeln. Drittens können wir die Verwendung von bereits im System etablierten Mustern fördern, indem wir "langweilige" Technologien wählen (eine Wiederholung des Themas, das wir im letzten Kapitel untersucht haben). Und schließlich können wir Rohmaterialien standardisieren, um Aufwandskapital freizusetzen, das an anderer Stelle in die Widerstandsfähigkeit investiert werden kann.
Lass uns jede dieser Praktiken der Reihe nach behandeln.
Festlegung von Systemzielen und Richtlinien zum Thema "Was aus der Luftschleuse geworfen werden soll "
Eine Praxis zur Unterstützung kritischer Funktionen in dieser Phase ist der sogenannte "Schleusenansatz": Wann immer wir Software entwickeln und ausliefern, müssen wir festlegen, was wir "aus der Schleuse werfen" können. Welche Funktionen und Komponenten kannst du vorübergehend vernachlässigen, ohne dass das System seine kritischen Funktionen vernachlässigen muss? Was würdest du gerne während eines Zwischenfalls vernachlässigen können? Wie auch immer deine Antwort ausfällt, stelle sicher, dass du die Software so entwickelst, dass du diese Dinge bei Bedarf tatsächlich vernachlässigen kannst. Das gilt sowohl für Sicherheitsvorfälle als auch für Leistungsvorfälle. Wenn eine Komponente gefährdet ist, kannst du sie abschalten, wenn sie unkritisch ist.
Wenn zum Beispiel die Verarbeitung von Transaktionen die kritische Funktion deines Systems ist und das Berichtswesen nicht, solltest du das System so aufbauen, dass du das Berichtswesen "aus der Luftschleuse werfen" kannst, um Ressourcen für den Rest des Systems zu sparen. Es ist möglich, dass das Berichtswesen extrem lukrativ ist - deine größte Gelddruckmaschine - und dennoch kann es geopfert werden, weil die Aktualität des Berichtswesens weniger wichtig ist. Das heißt, um die Sicherheit des Systems und die Genauigkeit der Meldungen zu gewährleisten, opferst du den Meldedienst in einem ungünstigen Szenario - auch wenn er der wertvollste Dienst ist -, weil seine kritische Funktionalität auch mit einer Verzögerung aufrechterhalten werden kann.
Ein weiterer Vorteil ist, dass wir die kritischen Funktionen so genau wie möglich definieren können, um die Größe der Batches einzuschränken - eine wichtige Dimension für unsere Fähigkeit, zu verstehen, was wir bauen und liefern wollen. Wenn wir sicherstellen, dass die Teams den Datenfluss in einem Programm verfolgen können, für das sie zuständig sind, können wir verhindern, dass sich die mentalen Modelle zu weit von der Realität entfernen.
Dieser rücksichtslose Fokus auf kritische Funktionen kann auch auf lokalere Ebenen angewendet werden. Wie wir im letzten Kapitel besprochen haben, führt der Trend zu Einzweckkomponenten zu mehr Linearität im System und hilft uns, die Funktion jeder Komponente besser zu verstehen. Wenn die kritische Funktion unseres Codes unklar bleibt, warum schreiben wir ihn dann?
Codeüberprüfungen und mentale Modelle
Codeüberprüfungen helfen uns zu überprüfen, ob die Implementierung unserer kritischen (und auch unkritischen) Funktionen mit unseren mentalen Modellen übereinstimmt. Codeüberprüfungen sind im besten Fall eine Rückmeldung eines mentalen Modells an ein anderes mentales Modell. Wenn wir ein Design durch Code verifizieren, setzen wir unser mentales Modell um. Wenn wir den Code einer anderen Person überprüfen, erstellen wir ein mentales Modell des Codes und vergleichen es mit unserem mentalen Modell der Absicht.
In modernen Softwareentwicklungs-Workflows werden Codeüberprüfungen in der Regel durchgeführt, nachdem ein Pull Request ("PR") eingereicht wurde. Wenn ein Entwickler den Code lokal ändert und ihn in die Hauptcodebasis (den sogenannten "Hauptzweig") einbringen will, eröffnet er einen PR, der einen anderen Mitarbeiter darüber informiert, dass diese Änderungen - die sogenannten "Commits" - zur Überprüfung bereit sind. In einem Modell mit kontinuierlicher Integration und kontinuierlicher Bereitstellung (CI/CD) sind alle Schritte, die mit Pull Requests verbunden sind, einschließlich des Zusammenführens der Änderungen in den Hauptzweig, automatisiert - mit Ausnahme der Codeüberprüfung.
Im Zusammenhang mit dem iterativen Änderungsmodell, das wir später im Kapitel besprechen werden, wollen wir auch, dass unsere Codeüberprüfungen klein und schnell sind. Wenn der Code eingereicht wird, sollte der Entwickler früh und schnell eine Rückmeldung erhalten. Um sicherzustellen, dass der Prüfer seine Prüfung schnell durchführen kann, sollten die Änderungen klein sein. Wenn ein Prüfer einen PR mit vielen Änderungen auf einmal zugewiesen bekommt, kann es einen Anreiz geben, zu sparen. Sie könnten den Code nur überfliegen, "lgtm" (sieht gut aus) kommentieren und sich dann einer Arbeit zuwenden, die sie für wertvoller halten (z. B. ihren eigenen Code schreiben). Schließlich bekommen sie keinen Bonus oder werden befördert, weil sie Codeüberprüfungen sorgfältig durchgeführt haben; sie werden viel eher dafür belohnt, dass sie Code schreiben, der wertvolle Änderungen in die Produktion einbringt.
Manchmal werden kritische Funktionen bei der Codeüberprüfung übersehen, weil unsere mentalen Modelle, wie wir im letzten Kapitel besprochen haben, unvollständig sind. In einer Studie wurde festgestellt, dass "die Fehlerbehandlungslogik oft einfach falsch ist", und ein einfaches Testen dieser Logik würde viele kritische Produktionsausfälle in verteilten Systemen verhindern.3 Auch für Tests brauchen wir Codeüberprüfungen, bei denen andere Leute die von uns geschriebenen Tests validieren.
Warnung
Formale Codeüberprüfungen werden oft nach einem bemerkenswerten Vorfall vorgeschlagen, in der Hoffnung, dass eine engere Kopplung die Sicherheit verbessert (das tut sie nicht). Wenn der zu überprüfende Code bereits geschrieben wurde und einen erheblichen Umfang hat, viele Änderungen enthält oder sehr komplex ist, ist es bereits zu spät. Wenn sich der Codeautor und der Reviewer zusammensetzen, um die Änderungen zu besprechen (im Gegensatz zum asynchronen, informellen Modell, das weitaus häufiger vorkommt), könnte das zwar hilfreich sein, ist aber nur "Review-Theater". Bei größeren Features sollten wir das "Feature Branch"-Modell anwenden oder, noch besser, eine Designprüfung durchführen, die Aufschluss darüber gibt, wie der Code geschrieben werden soll.
Wie können wir Anreize für durchdachte Codeüberprüfungen schaffen? Es gibt ein paar Dinge, die wir tun können, um das Schneiden von Ecken und Kanten zu verhindern, angefangen damit, dass wir sicherstellen, dass alle Erbsenzählereien von Tools erledigt werden. Ingenieure sollten nie auf Probleme mit der Formatierung oder Leerzeichen am Ende hinweisen müssen; alle stilistischen Bedenken sollten automatisch geprüft werden. Wenn wir sicherstellen, dass automatisierte Tools diese Art von pingeliger Routinearbeit erledigen, können sich die Ingenieure auf höherwertige Tätigkeiten konzentrieren, die die Widerstandsfähigkeit fördern.
Warnung
Es gibt viele Codeüberprüfungs-Antipatterns die im Status Quo der Cybersicherheit leider weit verbreitet sind, obwohl die Sicherheitsteams wohl am meisten darunter leiden. Eines dieser Muster ist die strikte Vorgabe, dass das Sicherheitsteam jeden PR absegnen muss, um seine "Gefährlichkeit" zu bewerten. Abgesehen von der Unbestimmtheit des Begriffs " Risikofähigkeit" besteht auch das Problem, dass dem Sicherheitsteam der relevante Kontext für die Codeänderungen fehlt.
Wie jeder Softwareentwickler nur zu gut weiß, kann ein Entwicklungsteam die PRs eines anderen Teams nicht effektiv überprüfen. Der Ingenieur für die Speicherung könnte vielleicht eine Woche lang die Designdokumente des Netzwerkentwicklungsteams lesen und dann eine PR überprüfen, aber das macht niemand. Ein Sicherheitsteam kann das ganz sicher nicht tun. Das Sicherheitsteam versteht vielleicht nicht einmal die kritischen Funktionen des Systems und in manchen Fällen weiß es nicht einmal genug über die Programmiersprache, um potenzielle Probleme sinnvoll zu identifizieren.
Infolgedessen wird das Sicherheitsteam oft zu einem Engpass, der das Tempo der Codeänderungen verlangsamt, was wiederum die Anpassungsfähigkeit beeinträchtigt. Auch für das Sicherheitsteam fühlt sich das in der Regel miserabel an - und doch erliegen Führungskräfte oft dem Glauben, dass es eine binäre Lösung zwischen den Extremen "manuelle Überprüfung" und "Sicherheitsprobleme durch die Maschen fallen lassen" gibt. Nur ein Sith handelt in absoluten Zahlen.
"Langweilige" Technologie ist widerstandsfähige Technologie
Eine weitere Praxis, die uns dabei helfen kann, unsere kritischen Funktionen zu verfeinern und ihre Widerstandsfähigkeit gegen Angriffe aufrechtzuerhalten, ist die Wahl "langweiliger" Technologie. Wie in Dan McKinleys berühmtem Beitrag "Choose Boring Technology" (Wähle langweilige Technologie) dargelegt , ist langweilig nicht per se schlecht. Vielmehr sind langweilige Technologien ein Zeichen für gut verstandene Fähigkeiten, die uns helfen, die Komplexität in den Griff zu bekommen und das Übergewicht der "verwirrenden Interaktionen" in unseren Systemen zu reduzieren (sowohl das System als auch unsere mentalen Modelle werden linearer).
Im Gegensatz dazu sind neue, "sexy" Technologien weniger bekannt und sorgen eher für Überraschungen und Verwirrung. Bleeding Edge ist ein passender Name, wenn man bedenkt, wie schmerzhaft die Umsetzung sein kann - vielleicht scheint es zunächst nur eine Fleischwunde zu sein, aber sie kann dir und deinen Teams auf Dauer die kognitive Energie rauben. Im Endeffekt führt dies zu einer stärkeren Kopplung und interaktiven Komplexität (abnehmende Linearität). Wenn du dich an das letzte Kapitel erinnerst, führt die Entscheidung für "langweilig" zu einem umfassenderen Verständnis, das weniger Spezialwissen erfordert - ein Merkmal linearer Systeme - und fördert gleichzeitig eine lockerere Kopplung auf verschiedene Weise.
Wenn du also ein durchdachtes Design erhältst (z. B. eines, das auf den Lehren aus Kapitel 3 beruht!), solltest du überlegen, ob die Entscheidungen, die du bei der Programmierung, Erstellung und Bereitstellung triffst, zusätzliche Komplexität und ein höheres Überraschungspotenzial mit sich bringen - und ob du dich selbst oder dein Unternehmen eng an diese Entscheidungen bindest. Softwareentwicklerinnen und -entwickler sollten sich für Sprachen, Frameworks, Werkzeuge usw. entscheiden, mit denen sie spezifische Geschäftsprobleme am besten lösen können. Den Endnutzer interessiert es nicht, dass du das neueste und beste Tool benutzt, das auf HackerNews angepriesen wird. Er will deinen Dienst nutzen, wann immer er will, so schnell wie er will und mit der Funktionalität, die er will. Manchmal erfordert die Lösung dieser Geschäftsprobleme eine neue, ausgefallene Technologie, wenn sie dir einen Vorteil gegenüber deinen Mitbewerbern verschafft (oder auf andere Weise den Auftrag deines Unternehmens erfüllt). Sei jedoch vorsichtig, wie oft du "langweilige" Technologien einsetzt, um dich von deinen Mitbewerbern abzuheben, denn die "blutigen" Kanten erfordern viele Blutopfer, um sie zu erhalten.
Warnung
Eine rote Fahne, die darauf hinweist, dass deine Sicherheitsarchitektur vom Grundsatz "Wähle langweilig" abweicht, ist, wenn sich deine Bedrohungsmodelle radikal von denen deiner Konkurrenten unterscheiden werden. Während die meisten Bedrohungsmodelle unterschiedlich sind - weil nur wenige Systeme exakt gleich sind -, ist es selten, dass zwei Dienste, die dieselbe Funktion in Unternehmen mit ähnlichen Zielen erfüllen, wie Fremde aussehen. Eine Ausnahme könnte sein, wenn deine Konkurrenten noch im finsteren Sicherheitszeitalter feststecken, du aber auf "Security-by-Design" setzt.
Während der Build- und Delivery-Phase müssen wir darauf achten, wie wir unsere kognitiven Anstrengungen priorisieren - und wie wir unsere Ressourcen im Allgemeinen einsetzen. Du kannst deine begrenzten Ressourcen für ein superschickes neues Tool ausgeben, das mithilfe von KI Unit-Tests für dich schreibt. Oder du kannst sie in die Entwicklung komplexer Funktionen stecken, die ein Problem für deine Zielgruppen besser lösen. Ersteres dient deinem Geschäft nicht direkt und hebt dich nicht von anderen ab; es fügt einen erheblichen kognitiven Overhead hinzu, der deinen kollektiven Zielen nicht dient, und das für einen ungewissen Nutzen (der erst nach einem mühsamen Abstimmungsprozess und dem Ziehen von Haaren aus minimalen Fehlerbehebungsdokumenten eintreten würde).
"Okay", sagst du, "aber was ist, wenn das neue, glänzende Ding wirklich richtig, richtig cool ist?" Weißt du, wer auch wirklich coole, neue, glänzende Software mag? Angreifer. Sie lieben es, wenn Entwickler neue Tools und Technologien einsetzen, die sie noch nicht richtig verstanden haben, denn das schafft viele Möglichkeiten für Angreifer, Fehler oder sogar beabsichtigte Funktionen auszunutzen, die noch nicht ausreichend gegen Missbrauch geprüft wurden. Auch Schwachstellenforscher haben einen Lebenslauf, und es sieht beeindruckend aus, wenn sie demonstrieren können, wie sie das neue, glänzende Ding ausnutzen können (was normalerweise als "Besitz" des Dings bezeichnet wird). Sobald sie die Details veröffentlichen, wie sie das neue, glänzende Ding ausgenutzt haben, können kriminelle Angreifer herausfinden, wie sie es in einen wiederholbaren, skalierbaren Angriff verwandeln können (und so die Fun-to-Profit-Pipeline der offensiven Informationssicherheit vervollständigen).
Sicherheits- und Beobachtungstools sind auch nicht von diesem "langweiligen" Prinzip ausgenommen. Unabhängig von deinem "offiziellen" Titel - und unabhängig davon, ob du eine Führungskraft, ein Manager oder ein einzelner Mitarbeiter bist - solltest du einfache, gut verständliche Sicherheits- und Beobachtungstools auswählen und fördern, die in deinen Systemen einheitlich eingesetzt werden. Angreifer lieben es, "besondere" Implementierungen von Sicherheits- oder Beobachtungstools zu finden und sind stolz darauf, neue, glänzende Abhilfemaßnahmen zu besiegen, die damit prahlen, Angreifer auf die eine oder andere Weise zu besiegen.
Viele Sicherheits- und Überwachungs-Tools benötigen spezielle Berechtigungen (z. B. als Root, Administrator oder Domänenadministrator) und umfassenden Zugriff auf andere Systeme, um ihre Funktion erfüllen zu können. Das macht sie zu fantastischen Werkzeugen für Angreifer, um sich tiefgreifenden, mächtigen Zugriff auf deine kritischen Systeme zu verschaffen (denn das sind diejenigen, die du besonders schützen und überwachen willst). Ein neues, glänzendes Sicherheitstool verspricht vielleicht, dass ausgefallene Mathematik all deine Angriffsprobleme lösen wird, aber diese Ausgefallenheit ist das Gegenteil von langweilig und kann zu einer Vielzahl von Kopfschmerzen führen, z. B. zu einem hohen Zeitaufwand für die kontinuierliche Anpassung, zu Netzwerkengpässen aufgrund von Datenabschöpfung, zu Kernel-Paniken oder natürlich zu einer Schwachstelle im Tool (oder seinen ausgefallenen Sammlungs- und KI-gesteuerten, regelbetriebenen Kanälen), die Angreifern einen wunderbaren Zugang zu allen Systemen bietet, die für dich wichtig sind.
Du könntest zum Beispiel versucht sein, die Authentifizierung oder den Schutz vor Cross-Site Request Forgery (XSRF) selbst zu entwickeln. Abgesehen von Randfällen, in denen die Authentifizierung oder der XSRF-Schutz Teil des Wertes ist, den dein Dienst deinen Kunden bietet, ist es viel sinnvoller, "langweilig" zu sein und Middleware für die Authentifizierung oder den XSRF-Schutz zu implementieren. Auf diese Weise nutzt du die Expertise des Anbieters in diesem "exotischen" Bereich.
Warnung
Bastle keine Middleware.
Der Punkt ist, dass es einfacher ist, das System zu warten und zu betreiben und es somit sicher zu halten, wenn du dich für die "am wenigsten schlechten" Werkzeuge für so viele deiner nicht differenzierenden Probleme wie möglich entscheidest. Wenn du für jedes einzelne Problem das beste Werkzeug oder die beste Regel auswählst, werden Angreifer die daraus resultierende kognitive Überlastung und die unzureichende Zuweisung von Komplexitätsmünzen in Dinge, die das System widerstandsfähiger gegen Angriffe machen, gerne ausnutzen. Natürlich ist es auch nicht sinnvoll, an etwas Langweiligem festzuhalten, das ineffektiv ist und die Widerstandsfähigkeit mit der Zeit untergräbt. Wir wollen den goldenen Mittelweg zwischen langweilig und effektiv finden.
Standardisierung von Rohstoffen
Die letzte Praxis, die wir im Bereich kritischer Funktionen behandeln werden, ist die Standardisierung der "Rohmaterialien", die wir bei der Entwicklung und Bereitstellung von Software verwenden - oder wenn wir Softwareentwicklungsteams Praktiken empfehlen. Wie wir im letzten Kapitel besprochen haben, können wir uns unter "Rohstoffen" in Softwaresystemen Sprachen, Bibliotheken und Werkzeuge vorstellen (dies gilt auch für Firmware und andere Rohstoffe, die in Computerhardware wie CPUs und GPUs eingesetzt werden). Diese Rohstoffe sind Elemente, die in die Software eingewebt werden und die für den Betrieb des Systems belastbar und sicher sein müssen.
Bei der Entwicklung von Softwarediensten müssen wir gezielt Sprachen, Bibliotheken, Frameworks, Dienste und Datenquellen auswählen, da der Dienst einige der Eigenschaften dieser Rohstoffe erbt. Viele dieser Materialien können gefährliche Eigenschaften haben, die für den Aufbau eines Systems, das deinen Anforderungen entspricht, ungeeignet sind. Oder die Gefahr ist zu erwarten, und da es keine bessere Alternative für deine Problemdomäne gibt, musst du lernen, damit zu leben, oder dir andere Wege überlegen, um die Gefahren durch Design zu reduzieren (mehr dazu in Kapitel 7). Wenn du dich für mehr als einen Rohstoff in einer Kategorie entscheidest, hast du in der Regel die Nachteile von beiden.
Die National Security Agency (NSA) empfiehlt offiziell speichersichere Sprachen wie C#, Go, Java, Ruby, Rust und Swift zu verwenden, wo immer dies möglich ist. Der CTO von Microsoft Azure, Mark Russovovich, twitterte noch deutlicher: "Apropos Sprachen, es ist an der Zeit, keine neuen Projekte mehr in C/C++ zu starten und Rust für die Szenarien zu verwenden, in denen eine Nicht-GC-Sprache erforderlich ist. Um der Sicherheit und Zuverlässigkeit willen sollte die Industrie diese Sprachen für veraltet erklären." Speichersicherheitsprobleme schaden sowohl dem Benutzer als auch dem Hersteller eines Produkts oder einer Dienstleistung, weil Daten, die sich nicht ändern sollten, auf magische Weise einen anderen Wert annehmen können. Wie Matt Miller, Partner Security Software Engineer bei Microsoft, 2019 vorstellte, sind ~70% der behobenen Sicherheitslücken mit einem CVE zugewiesenen Speicher-Sicherheitslücken, weil Softwareentwickler versehentlich Speicher-Sicherheitsfehler in ihren C- und C++-Code einbauen.
Wenn du Software entwickelst oder überarbeitest, solltest du eine der Dutzenden beliebten Sprachen wählen, die standardmäßig speichersicher sind. Das ist gut für uns, denn wir haben eine Fülle von speichersicheren Optionen, aus denen wir schöpfen können. Wir können uns C-Code wie Blei vorstellen: Er war für viele Anwendungsfälle recht praktisch, aber er vergiftet uns mit der Zeit, vor allem, wenn sich mehr davon ansammelt.
Im Idealfall wollen wir weniger gefährliche Rohstoffe so schnell wie möglich übernehmen, aber diese Aufgabe ist oft nicht trivial (wie die Migration von einer Sprache zur anderen). Bei kleineren Systemen, die über relativ vollständige Integrations-, End-to-End- (E2E) und Funktionstests verfügen, kann ein kompletter Rewrite funktionieren - aber diese Bedingungen sind nicht immer gegeben. Das Strangler-Fig-Muster, das wir am Ende des Kapitels besprechen werden, ist der naheliegendste Ansatz, um unsere Codebasis iterativ zu verändern.
Eine andere Möglichkeit ist, eine Sprache zu wählen, die sich gut mit C integrieren lässt, und deine Anwendung zu einer polyglotten Anwendung zu machen, indem du sorgfältig auswählst, welche Teile du in jeder Sprache schreibst. Dieser Ansatz ist granularer als das Strangler-Fig-Muster und ähnelt dem Oxidation-Projekt, Mozillas Ansatz zur Integration von Rust-Code in und um Firefox (hier findest du eine Anleitung für die Migration von C zu Rust, falls du sie brauchst). Manche Systeme können sogar auf unbestimmte Zeit in diesem Zustand bleiben, wenn es Vorteile hat, Hoch- und Niedrigsprachen gleichzeitig im selben Programm zu haben. Spiele sind ein typisches Beispiel für diese Dynamik: Der Code der Spiel-Engine muss schnell sein, um das Speicherlayout zu kontrollieren, aber der Code des Spiels muss schnell zu iterieren sein, und die Leistung ist viel weniger wichtig. Aber im Allgemeinen sind polyglotte Dienste und Programme selten, was die Standardisierung von einigen Materialien etwas einfacher macht.
Sicherheitsteams, die die Einführung von Speichersicherheit vorantreiben wollen, sollten sich mit den Menschen in deinem Unternehmen zusammentun, die versuchen, technische Standards - seien es Praktiken, Tools oder Frameworks - zu entwickeln, und sich an diesem Prozess beteiligen. Alles in allem ist die Aufrechterhaltung der Konsistenz wesentlich besser für die Ausfallsicherheit. Die Menschen, die du suchst, sind in der technischen Organisation, stellen die Verbindungen her und setzen sich für die Einführung dieser Standards ein.
Auf der anderen Seite haben diese Menschen ihre eigenen Ziele: Sie wollen produktiv mehr Software und die Systeme bauen, die das Unternehmen braucht. Wenn deine Fragen unsensibel sind, werden sie dich ignorieren. Bitte also nicht darum, dass die Laptops der Entwickler aus Sicherheitsgründen vom Internet getrennt werden. Wenn du die Sicherheitsvorteile der Umstrukturierung von C-Code in eine speichersichere Sprache hervorhebst, ist das konstruktiver, da es wahrscheinlich auch ihren Zielen entspricht - denn Produktivität und betriebliche Gefahren schleichen sich bekanntermaßen in C ein. Die Sicherheitsbehörden können mit dieser Gruppe von Menschen in Bezug auf C eine große Gemeinsamkeit haben, da auch sie C loswerden wollen (abgesehen von den Menschen, die gelegentlich darauf bestehen, dass wir alle in Assembler schreiben und die Intel-Bedienungsanleitung lesen sollten).
Warnung
Wie Mozilla betont, "kann das Überschreiten der C++/Rust-Grenze schwierig sein". Das sollte als Nachteil dieses Musters nicht unterschätzt werden. Da C die APIs der UNIX-Plattform definiert, verfügen die meisten Sprachen über eine solide Unterstützung für die Fremdsprachenschnittstelle (Foreign Function Interface, FFI) von C. Bei C++ fehlt diese Unterstützung jedoch, da es viel mehr sprachliche Eigenheiten gibt, mit denen FFI umgehen muss und die zu Fehlern führen können.
Code, der eine Sprachgrenze überschreitet, braucht in allen Entwicklungsphasen besondere Aufmerksamkeit. Ein neuer Ansatz besteht darin, den gesamten C-Code in einer WebAssembly-Sandbox mit automatisch generierten FFI-Wrappern einzuschließen. Dies könnte sogar für Anwendungen nützlich sein, die vollständig in C geschrieben sind, um die unzuverlässigen, gefährlichen Teile in einer Sandbox zu fangen (wie z. B. das Parsen von Formaten ).
Caches sind ein Beispiel für ein gefährliches Rohmaterial, das oft als notwendig erachtet wird. Wenn wir Daten für einen Dienst zwischenspeichern, ist es unser Ziel, das Verkehrsaufkommen für den Dienst zu reduzieren. Eine hohe Cache Hit Ratio (CHR) gilt als erfolgreich, und es ist oft kostengünstiger, Caches zu skalieren als den dahinter stehenden Dienst. Caches können die einzige Möglichkeit sein, um deine Leistungs- und Kostenziele zu erreichen, aber einige ihrer Eigenschaften gefährden die Fähigkeit des Systems, die Ausfallsicherheit zu gewährleisten.
Es gibt zwei Gefahren in Bezug auf die Ausfallsicherheit. Die erste ist banal: Immer wenn sich Daten ändern, müssen die Caches ungültig gemacht werden, sonst erscheinen die Daten veraltet. Wenn das System auf konsistente Daten angewiesen ist, kann die Invalidierung zu einem merkwürdigen oder falschen Verhalten des Gesamtsystems führen - diese "verwirrenden" Interaktionen in der Gefahrenzone. Wenn die sorgfältige Koordination nicht stimmt, können veraltete Daten auf unbestimmte Zeit im Cache verrotten.
Die zweite Gefahr ist ein systemischer Effekt: Wenn die Caches jemals fehlschlagen oder sich verschlechtern, üben sie Druck auf den Dienst aus. Bei hohen CHRs kann selbst ein teilweiser Cache-Ausfall einen Backend-Dienst überfordern. Wenn der Backend-Dienst ausfällt, können die Cache-Einträge nicht aufgefüllt werden, und das führt dazu, dass der Backend-Dienst mit noch mehr Datenverkehr bombardiert wird. Dienste ohne Cache werden langsamer, erholen sich aber schnell wieder, wenn mehr Kapazität hinzugefügt wird oder der Datenverkehr nachlässt. Dienste mit einem Cache brechen zusammen, wenn sie sich ihrer Kapazität nähern, und die Wiederherstellung erfordert oft erhebliche zusätzliche Kapazitäten über den Steady-State hinaus.
Doch trotz dieser Gefahren sind Caches aus Sicht der Ausfallsicherheit nicht uninteressant. Sie erhöhen die Ausfallsicherheit, weil sie die Anfragen vom Ursprung (d. h. dem Backend-Server) entkoppeln können; der Dienst ist besser gegen Überraschungen gewappnet, aber nicht unbedingt gegen dauerhafte Ausfälle. Die Kunden sind nun weniger eng an das Verhalten des Ursprungs gekoppelt, sondern stattdessen eng mit dem Cache verbunden. Diese enge Kopplung sorgt für mehr Effizienz und geringere Kosten, weshalb das Caching weit verbreitet ist. Aber aus den eben genannten Gründen der Ausfallsicherheit betreiben nur wenige Unternehmen ihre eigenen Caches. So lagern sie das Caching des Webverkehrs häufig an einen speziellen Anbieter aus, z. B. an ein Content Delivery Network (CDN).
Tipp
Jede Wahl, die du triffst, widersetzt sich entweder der engen Kopplung oder kapituliert vor ihr. Das Ende der losen Kopplung ist die vollständige Austauschbarkeit von Komponenten und Sprachen in deinen Systemen, aber die Anbieter bevorzugen die enge Kopplung (Lock-in). Wenn du Entscheidungen über dein Rohmaterial triffst, solltest du immer abwägen, ob du dich der Gefahrenzone, die in Kapitel 3 vorgestellt wurde, näherst oder von ihr wegkommst.
In dieser Phase können wir vier Praktiken anwenden, um die kritische Funktionalität, die erste Zutat unseres Resilienztranks, zu unterstützen: den Schleusenansatz, durchdachte Codeüberprüfungen, die Auswahl "langweiliger" Technologien und die Standardisierung von Rohstoffen. Kommen wir nun zur zweiten Zutat: dem Verständnis der Sicherheit des Systems Grenzen (Schwellenwerte).
Entwickeln und Liefern, um die Grenzen der Sicherheit zu erweitern
Die zweite Zutat unseres Zaubertranks besteht darin, die Sicherheitsgrenzen des Systems zu verstehen - die Schwellenwerte, jenseits derer es in ein Versagen rutscht. Aber wir können in dieser Phase auch dazu beitragen, diese Grenzen zu erweitern, indem wir das Toleranzfenster unseres Systems gegenüber widrigen Umständen vergrößern. In diesem Abschnitt wird die Bandbreite des Verhaltens beschrieben, die von einem soziotechnischen System erwartet werden sollte, wobei der Mensch das System kuratiert, wenn es vom Ideal abweicht (die mentalen Modelle, die während der Design- und Architekturphase erstellt wurden). Es gibt vier wichtige Praktiken, die wir behandeln werden, um die Sicherheitsgrenzen zu unterstützen: Vorhersage des Umfangs, Automatisierung von Sicherheitsprüfungen, Standardisierung von Mustern und Werkzeugen und Verständnis von Abhängigkeiten (einschließlich der Priorisierung von Schwachstellen).
Die gute Nachricht ist, dass ein Großteil der "richtigen" Sicherheit eigentlich nur solide Technik ist - Dinge, die du für die Zuverlässigkeit und Widerstandsfähigkeit gegenüber anderen Störungen als Angriffen tun willst. In der Welt der SCE wird die Anwendungssicherheit als eine weitere Facette der Softwarequalität betrachtet: Wie kannst du angesichts deiner Beschränkungen eine qualitativ hochwertige Software schreiben, die deine Ziele erreicht? Die Praktiken, die wir in diesem Abschnitt erkunden, führen zu qualitativ hochwertigerer und widerstandsfähigerer Software.
Im letzten Kapitel haben wir erwähnt, dass wir in unseren Systemen eine dauerhafte Anpassungsfähigkeit anstreben. In dieser Phase können wir die Nachhaltigkeit fördern, indem wir unsere Grenzen für einen sicheren Betrieb erweitern. Nachhaltigkeit und Resilienz sind in vielen komplexen Bereichen miteinander verwoben. In der Umweltwissenschaft geht es sowohl bei der Resilienz als auch bei der Nachhaltigkeit um die Erhaltung der Gesundheit und des Wohlbefindens der Gesellschaft angesichts von Umweltveränderungen.4 In der Softwareentwicklung bezeichnen wir Nachhaltigkeit normalerweise als "Wartbarkeit". In unserer Lebenswelt ist es nicht weniger wahr, dass es sowohl bei der Wartbarkeit als auch bei der Resilienz um die Gesundheit und das Wohlergehen von Softwarediensten in Gegenwart von destabilisierenden Kräften wie Angreifern geht. Wie wir im Laufe dieses Abschnitts herausfinden werden, ist die Unterstützung wartbarer Praktiken der Softwareentwicklung - einschließlich wiederholbarer Arbeitsabläufe - entscheidend für die Entwicklung und Bereitstellung von Systemen, die gegen Angriffe resilient sind.
Die Prozesse, nach denen du erstellst und bereitstellst, müssen klar, wiederholbar und wartbar sein - so wie wir es in Kapitel 2 beschrieben haben, als wir RAVE vorgestellt haben. Das Ziel ist es, den Aufbau und die Bereitstellung so weit wie möglich zu standardisieren, um unerwartete Interaktionen zu vermeiden. Das bedeutet auch, dass du dich nicht darauf verlassen musst, dass alles vor dem Einsatz perfekt ist, sondern dass du mit Fehlern gut umgehen kannst, weil die Behebung von Fehlern ein schneller, unkomplizierter und wiederholbarer Prozess ist. Wenn wir diese Nachhaltigkeit in unsere Build- und Delivery-Praktiken einbeziehen, können wir unsere Sicherheitsgrenzen erweitern und uns besser auf widrige Umstände einstellen.
Vorausschauende Skala und SLOs
Die erste Praxis in dieser Phase die uns helfen kann, unsere Sicherheitsgrenzen zu erweitern, ist, einfach ausgedrückt, die Vorwegnahme des Umfangs. Bei der Entwicklung von robusten Softwaresystemen müssen wir berücksichtigen, wie sich die Betriebsbedingungen entwickeln könnten und wo die Grenzen des sicheren Betriebs liegen. Trotz bester Absichten treffen Softwareentwickler manchmal Architektur- oder Implementierungsentscheidungen, die zu Engpässen bei der Zuverlässigkeit oder Skalierbarkeit führen.
Die Antizipation von Skaleneffekten ist eine weitere Möglichkeit, die im letzten Kapitel beschriebenen "Das wird immer so sein"-Annahmen in Frage zu stellen, die Angreifer für ihre Operationen ausnutzen. Nehmen wir einen eCommerce-Dienst. Wir denken vielleicht: "Bei jeder eingehenden Anfrage müssen wir diese zuerst mit dem vorherigen Warenkorb des Nutzers abgleichen, was bedeutet, dass wir eine Anfrage an diese andere Sache stellen müssen." In diesem mentalen Modell steckt die Annahme "das wird immer so sein": dass das "andere Ding" immer da sein wird. Wenn wir nachdenklich sind, müssen wir hinterfragen: "Was ist, wenn dieses andere Ding nicht da ist? Was passiert dann?" So können wir unsere Konstruktion verfeinern (und wir sollten das Warum - dieAnnahme, die wir in Frage gestellt haben - dokumentieren, wie wir später in diesem Kapitel besprechen werden). Was ist, wenn der Warenkorb des Nutzers nur langsam geladen wird oder nicht verfügbar ist?
Das Infragestellen unserer "Das wird immer so sein"-Annahmen kann auch auf niedrigeren Ebenen potenzielle Skalierbarkeitsprobleme aufdecken. Wenn wir sagen: "Wir beginnen immer mit einem Kontrollflussdiagramm, das das Ergebnis einer früheren Analyse ist", können wir diese Annahme mit einer Frage wie "Was ist, wenn diese Analyse entweder super langsam ist oder fehlschlägt?" in Frage stellen. Indem wir uns die Mühe machen, das Ausmaß zu antizipieren, können wir sicherstellen, dass wir die Sicherheitsgrenzen unseres Systems nicht künstlich einschränken - und dass potenzielle Schwellenwerte in unsere mentalen Modelle des Systems einfließen.
Wenn wir Komponenten entwickeln, die als Teil großer, verteilter Systeme laufen sollen, müssen wir bei der Skalierung auch voraussehen, was die Betreiber bei Störungen brauchen (d.h. welche Anstrengungen sie unternehmen müssen). Wenn ein Techniker auf Abruf stundenlang braucht, um herauszufinden, dass der Grund für die plötzliche Verlangsamung des Dienstes eine SQLite-Datenbank ist, von der niemand etwas wusste, wird das deinen Leistungszielen schaden. Wir müssen auch vorhersehen, wie das Geschäft wachsen wird, z. B. indem wir das Verkehrswachstum anhand von Roadmaps und Geschäftsplänen abschätzen, um uns darauf vorzubereiten. Wenn wir abschätzen, welche Teile des Systems wir in Zukunft erweitern müssen und welche wahrscheinlich nicht, können wir mit unseren Investitionen sparsam umgehen und gleichzeitig sicherstellen, dass das Unternehmen ungehindert durch Softwarebeschränkungen wachsen kann.
Wir sollten uns Gedanken über die Unterstützung der im letzten Kapitel besprochenen Muster machen. Wenn wir für Unveränderlichkeit und Kurzlebigkeit entwerfen, bedeutet das, dass Ingenieure nicht per SSH in das System eindringen können, um etwas zu debuggen oder zu ändern, und dass die Arbeitslast nach Belieben beendet und neu gestartet werden kann. Wie verändert das die Art und Weise, wie wir unsere Software entwickeln? Auch hier sollten wir die Gründe festhalten - dass wir sie so gebaut haben, um Unveränderlichkeit und Vergänglichkeit zu unterstützen -, um Wissen zu sammeln (das wir gleich noch besprechen werden). Auf diese Weise können wir unser Toleranzfenster erweitern und unser Verständnis für die Schwellenwerte des Systems, ab denen ein Fehler auftritt, festigen.
Sicherheitsprüfungen über CI/CD automatisieren
Eine der wertvollsten Praktiken zur Unterstützung der Erweiterung der Sicherheitsgrenzen ist die Automatisierung von Sicherheitsprüfungen durch die Nutzung bestehender Technologien für Resilienz-Anwendungsfälle. Die Praxis der kontinuierlichen Integration und kontinuierlichen Auslieferung5 (CI/CD) beschleunigt die Entwicklung und Bereitstellung von Softwarefunktionen, ohne die Zuverlässigkeit oder Qualität zu beeinträchtigen.6 Eine CI/CD-Pipeline besteht aus einer Reihe von (idealerweise automatisierten) Aufgaben, die eine neue Softwareversion liefern. Sie umfasst in der Regel die Kompilierung der Anwendung (auch "Build" genannt), das Testen des Codes, die Bereitstellung der Anwendung in einem Repository oder einer Staging-Umgebung und die Auslieferung der Anwendung an die Produktion (auch "Delivery" genannt). Mithilfe von Automatisierung sorgen CI/CD-Pipelines dafür, dass diese Aktivitäten in regelmäßigen Abständen und mit minimalen Eingriffen von Menschen durchgeführt werden. Dadurch unterstützt CI/CD die Eigenschaften Schnelligkeit, Zuverlässigkeit und Wiederholbarkeit, die wir in unseren Systemen brauchen, um sie sicher und widerstandsfähig zu machen.
Tipp
- Kontinuierliche Integration (CI)
Menschen integrieren und führen Entwicklungsarbeiten (wie Code) häufig zusammen (z. B. mehrmals am Tag). Es geht um die automatisierte Erstellung und Prüfung von Software, um kürzere und häufigere Release-Zyklen, eine bessere Softwarequalität und eine höhere Produktivität der Entwickler zu erreichen.
- Kontinuierliche Lieferung (CD)
Menschen bringen Softwareänderungen (wie neue Funktionen, Patches, Konfigurationsänderungen und mehr) in die Produktion oder zu den Endnutzern. Es geht um die automatisierte Veröffentlichung und Bereitstellung von Software, um schnellere, sicherere, wiederholbare und nachhaltige Software-Updates zu erreichen.7
Wir sollten CI/CD nicht nur als einen Mechanismus schätzen, mit dem wir die mühsamen manuellen Deployments vermeiden können, sondern auch als ein Werkzeug, das die Softwarebereitstellung wiederholbar, vorhersehbar und konsistent macht. Wir können Invarianten erzwingen, die es uns ermöglichen, jedes Mal, wenn wir Software erstellen, bereitstellen und ausliefern, die von uns gewünschten Eigenschaften zu erreichen. Unternehmen, die Software schneller erstellen und ausliefern können, können auch Schwachstellen und Sicherheitsprobleme schneller beheben. Wenn du deine Software ausliefern kannst, wann du willst, dann kannst du auch sicher sein, dass du Sicherheitslücken beheben kannst, wenn du es brauchst. Für manche Unternehmen kann das stündlich sein, für andere täglich. Der Punkt ist, dass dein Unternehmen nach Bedarf liefern und somit auf Sicherheitsereignisse nach Bedarf reagieren kann.
Aus der Perspektive der Belastbarkeit verschlingen manuelle Bereitstellungen (und andere Teile des Bereitstellungsworkflows) nicht nur kostbare Zeit und Mühe, die besser an anderer Stelle eingesetzt werden sollten, sondern koppeln auch den Menschen eng an den Prozess, ohne Hoffnung auf Linearität. Menschen sind fabelhaft in der Lage, sich anzupassen und mit Abwechslung zu reagieren, und absolut hoffnungslos darin, immer wieder das Gleiche zu tun. Der Sicherheits- und Systemadministrator-Status quo von "ClickOps" ist unter diesem Gesichtspunkt ehrlich gesagt gefährlich. Er erhöht die enge Kopplung und die Komplexität, ohne die Effizienzgewinne zu bringen, die wir uns von diesem faustischen Handel erhoffen - was einem Tausch unserer Seele gegen ein Leben voller Langeweile gleichkommt. Die Alternative der automatisierten CI/CD-Pipelines lockert nicht nur die Kopplung und führt zu mehr Linearität, sondern beschleunigt auch die Softwarebereitstellung - eine der Win-Win-Situationen, die wir im letzten Kapitel beschrieben haben. Das Gleiche gilt für viele Formen der Workflow-Automatisierung, wenn das Ergebnis standardisierte, wiederholbare Muster sind.
Ein Beispiel, das weitaus beunruhigender ist als manuelle Einsätze, ist das Beispiel der einheimischen Bevölkerung auf Noepe (Martha's Vineyard), die mit der Gefahr einer engen Kopplung konfrontiert war, als der einzige Fährdienst, der Lebensmittel lieferte, durch die COVID-19-Pandemie unterbrochen wurde.8 Wenn wir unsere Pipeline als Lebensmittelpipeline (als Teil der breiteren Lebensmittelversorgungskette) betrachten, erkennen wir die dringende Notwendigkeit von Zuverlässigkeit und Widerstandsfähigkeit. Das gilt auch für unsere Baupipelines (die zum Glück keine Menschenleben gefährden).
Tipp
Wenn du Chaos-Experimente an deinen Systemen durchführst, sorgen wiederholbare Build-and-Deploy-Workflows dafür, dass du die Erkenntnisse aus diesen Experimenten reibungslos einfließen lassen und dein System kontinuierlich weiterentwickeln kannst. Mit versionierten und überprüfbaren Build-and-Deploy-Trails kannst du leichter verstehen, warum sich das System nach einer Änderung anders verhält. Das Ziel ist, dass Softwareentwickler/innen so schnell wie möglich Feedback erhalten, solange der Kontext noch frisch ist. Sie wollen, dass ihr Code erfolgreich und zuverlässig in der Produktion läuft, also nutze dieses emotionale Momentum und hilf ihnen dabei.
Schnelleres Patchen und Aktualisieren von Abhängigkeiten
Ein Teilbereich der Automatisierung von Sicherheitsprüfungen zur Erweiterung der Sicherheitsgrenzen ist die Praxis des schnelleren Patchens und der Aktualisierung von Abhängigkeiten. CI/CD kann uns beim Patchen helfen und generell dabei, Abhängigkeiten auf dem neuesten Stand zu halten - was dazu beiträgt, dass wir nicht an diese Sicherheitsgrenzen stoßen. Patching ist ein Problem, das die Cybersicherheit plagt. Das berühmteste Beispiel dafür ist die Sicherheitslücke bei Equifax aus dem Jahr 2017, bei der eine Apache Struts-Schwachstelle vier Monate nach ihrer Aufdeckung nicht gepatcht wurde. Dies verstieß gegen die interne Vorgabe, Schwachstellen innerhalb von 48 Stunden zu flicken, und zeigt einmal mehr, warum strenge Richtlinien nicht ausreichen, um die Widerstandsfähigkeit von Systemen in der realen Welt zu fördern. In jüngerer Zeit löste die in Kapitel 3 beschriebene Log4Shell-Schwachstelle in Log4j im Jahr 2021 ein wahres Feuerwerk an Aktivitäten aus, um verwundbare Systeme im gesamten Unternehmen zu finden und zu patchen, ohne etwas kaputt zu machen.
Theoretisch wollen Entwickler immer die neueste Version ihrer Abhängigkeiten nutzen. Die neuesten Versionen haben mehr Funktionen, enthalten Fehlerkorrekturen und bieten oft Verbesserungen bei Leistung, Skalierbarkeit und Bedienbarkeit.9 Aber wenn Ingenieure an einer älteren Version festhalten, gibt es dafür meist einen Grund. In der Praxis gibt es viele Gründe, warum das nicht der Fall ist; einige sind sehr vernünftig, andere weniger.
Der Produktionsdruck ist wahrscheinlich der wichtigste Grund, denn die Aktualisierung ist eine Aufgabe, die keinen unmittelbaren geschäftlichen Nutzen bringt. Ein weiterer Grund ist, dass die semantische Versionierung (SemVer) zwar ein erstrebenswertes Ideal ist, in der Praxis aber nicht funktioniert. Es ist unklar, ob sich das System beim Upgrade auf eine neue Version der Abhängigkeit korrekt verhält, es sei denn, du hast erstaunliche Tests, die das Verhalten vollständig abdecken, was niemand hat.
Am weniger vernünftigen Ende des Spektrums steht das erzwungene Refactoring, wenn eine Abhängigkeit geschrieben wird oder wesentliche API-Änderungen vorgenommen werden. Dies ist ein Symptom für die Vorliebe von Ingenieuren, glänzende und neue Technologien gegenüber stabilen und "langweiligen" Technologien zu wählen, d.h. Dinge, die für die eigentliche Arbeit nicht geeignet sind. Ein letzter Grund sind aufgegebene Abhängigkeiten. Der Ersteller der Abhängigkeit pflegt sie nicht mehr und es wurde kein direkter Ersatz geschaffen - oder der direkte Ersatz unterscheidet sich erheblich.
Genau aus diesem Grund kann Automatisierung - einschließlich CI/CD-Pipelines - helfen, indem sie den menschlichen Aufwand für die Aktualisierung von Abhängigkeiten verringert und diesen Aufwand für wertvollere Tätigkeiten wie Anpassungsfähigkeit freisetzt. Wir wollen nicht, dass sie sich mit Langeweile abrackern. Automatisierte CI/CD-Pipelines bedeuten, dass Aktualisierungen und Patches innerhalb von Stunden (oder früher!) getestet und in die Produktion eingespielt werden können, anstatt wie bisher Tage, Wochen oder sogar Monate zu dauern. So können die Update- und Patch-Zyklen automatisch und täglich durchgeführt werden, so dass andere Prioritäten in den Vordergrund rücken können.
Automatisierte Integrationstests bedeuten, dass Aktualisierungen und Patches auf mögliche Leistungs- oder Fehlerprobleme geprüft werden, bevor sie in der Produktion eingesetzt werden, genau wie anderer Code. Bedenken, dass Aktualisierungen oder Patches die Produktionsdienste stören könnten - was zu Verzögerungen oder langwierigen Bewertungen führen kann, die Tage oder Wochen dauern - können durch Investitionen in Tests zumindest teilweise ausgeräumt werden. Wir müssen uns zwar die Mühe machen, Tests zu schreiben, die wir automatisieren können, aber wir ersparen uns im Laufe der Zeit viel Arbeit, wenn wir auf manuelle Tests verzichten.
Die Automatisierung der Release-Phase der Softwarebereitstellung bietet auch Vorteile für die Sicherheit. Das automatische Verpacken und Bereitstellen einer Softwarekomponente verkürzt die Zeit bis zur Auslieferung und beschleunigt, wie bereits erwähnt, Patches und Sicherheitsänderungen. Auch die Versionskontrolle ist ein Sicherheitsvorteil, da sie das Rollback und die Wiederherstellung beschleunigt, falls etwas schief geht. Im nächsten Abschnitt werden wir die Vorteile der automatisierten Infrastrukturbereitstellung erörtern.
Vorteile von Continuous Delivery für die Ausfallsicherheit
Continuous Delivery ist eine Praxis, die du erst dann einführen solltest, wenn du die anderen in diesem Abschnitt - und sogar im gesamten Kapitel - beschriebenen Praktiken bereits eingeführt hast. Wenn du nicht über KI und automatisierte Tests verfügst, die die meisten Fehler abfangen, die durch Änderungen verursacht werden, wird CD gefährlich sein und an deiner Fähigkeit nagen, die Widerstandsfähigkeit zu erhalten. CD erfordert mehr Strenge als CI; es fühlt sich ganz anders an. Mit CI kannst du deine bestehenden Prozesse automatisieren und Vorteile im Arbeitsablauf erzielen, aber die Art und Weise, wie du Software einführst und betreibst, muss nicht wirklich geändert werden. CD hingegen erfordert, dass du dein Haus in Ordnung bringst. Jeder mögliche Fehler, der bei der Entwicklung gemacht werden kann, wird nach einer gewissen Zeit auch von den Entwicklern gemacht werden. (Alle Aspekte der Prüfung und Validierung der Software müssen automatisiert werden, um diese Fehler aufzuspüren, bevor sie zu Fehlern werden, und es erfordert mehr Planung in Bezug auf Abwärts- und Vorwärtskompatibilität, Protokolle und Datenformate.
Wie kann uns CD angesichts dieser Vorbehalte helfen, die Widerstandsfähigkeit zu erhalten? Es ist unmöglich, manuelle Einsätze wiederholbar zu machen. Es ist unfair, von einem menschlichen Ingenieur zu erwarten, dass er manuelle Einsätze jedes Mal fehlerfrei durchführt - vor allem unter unklaren Bedingungen. Selbst bei automatisierten Einsätzen können viele Dinge schief gehen, ganz zu schweigen davon, dass ein Mensch die einzelnen Schritte ausführt. Widerstandsfähigkeit - in Form von Wiederholbarkeit, Sicherheit und Flexibilität - ist Teil des Ziels von CD: Änderungen - seien es neue Funktionen, aktualisierte Konfigurationen, Versions-Upgrades, Fehlerbehebungen oder Experimente - sollen den Endnutzern schnell und sicher zur Verfügung gestellt werden.10
Eine häufigere Veröffentlichung erhöht die Stabilität und Zuverlässigkeit. Zu den gängigen Einwänden gegen CD gehört die Vorstellung, dass CD in stark regulierten Umgebungen nicht funktioniert, dass es nicht auf Altsysteme angewendet werden kann und dass es enorme technische Leistungen erfordert, um es zu erreichen. Vieles davon basiert auf dem inzwischen gründlich widerlegten Mythos, dass ein schneller Wechsel das "Risiko" erhöht (wobei "Risiko" nach wie vor ein undurchsichtiges Konzept ist).11
Auch wenn wir es ablehnen, Hyperscale-Unternehmen als Vorbild zu nehmen, lohnt es sich, Amazon als Fallstudie für CD in regulierten Umgebungen zu betrachten. Amazon wickelt Tausende von Transaktionen pro Minute ab (bis zu Hunderttausende während des Prime Day) und unterliegt damit dem PCI DSS (einem Compliance-Standard für Kreditkartendaten). Da es sich um ein börsennotiertes Unternehmen handelt, gilt auch der Sarbanes-Oxley Act, der die Rechnungslegungspraktiken regelt. Aber selbst im Jahr 2011 gab Amazon im Durchschnitt alle 11,6 Sekunden Änderungen für die Produktion frei, was in Spitzenzeiten 1.079 Implementierungen pro Stunde bedeutete.12 SRE und Autor Jez Humble schreibt: "Das ist möglich, weil die Praktiken, die der kontinuierlichen Bereitstellung zugrunde liegen - umfassendes Konfigurationsmanagement, kontinuierliches Testen und kontinuierliche Integration - die schnelle Entdeckung von Fehlern im Code, Konfigurationsproblemen in der Umgebung und Problemen mit dem Bereitstellungsprozess ermöglichen."13 Wenn du Continuous Delivery mit Chaos-Experimenten kombinierst, erhältst du schnelles Feedback, das umsetzbar ist.
Das mag entmutigend klingen. Deine Sicherheitskultur fühlt sich vielleicht wie ein Theaterstück von Shakespeare an. Dein technischer Stapel fühlt sich eher wie ein Stapel LEGOs an, auf den du schmerzhaft trittst. Aber du kannst klein anfangen. Der perfekte erste Schritt, um auf CD hinzuarbeiten, ist "PasteOps". Dokumentiere die manuelle Arbeit, die bei der Implementierung von Sicherheitsänderungen oder der Durchführung sicherheitsrelevanter Aufgaben im Rahmen der Erstellung, Prüfung und Implementierung anfällt. Eine Aufzählung in einer gemeinsamen Ressource kann als MVP für die Automatisierung ausreichen und ermöglicht eine iterative Verbesserung, die schließlich in echte Skripte oder Tools umgewandelt werden kann. Bei SCE dreht sich alles um solche iterativen Verbesserungen. Denk an die Evolution in natürlichen Systemen: Fische haben nicht plötzlich Beine, Daumen und Haare entwickelt, um zu Menschen zu werden. Jede Generation bietet bessere Anpassungen an die Umwelt, genauso wie jede Iteration eines Prozesses eine Gelegenheit zur Verfeinerung ist. Widerstehe der Versuchung, eine große, weitreichende Veränderung, Umstrukturierung oder Migration durchzuführen. Alles, was du brauchst, ist gerade genug, um das Schwungrad in Gang zu setzen.
Standardisierung von Mustern und Werkzeugen
Ähnlich wie bei der Standardisierung von Rohstoffen, um kritische Funktionen zu unterstützen, ist die Standardisierung von Werkzeugen und Mustern eine Praxis, die dazu beiträgt, die Sicherheitsgrenzen zu erweitern und die Betriebsbedingungen innerhalb dieser Grenzen zu halten. Standardisierung bedeutet, dass die produzierte Arbeit mit den vorgegebenen Richtlinien übereinstimmt. Die Standardisierung trägt dazu bei, dass Menschen weniger Fehler machen können, weil sie sicherstellen, dass eine Aufgabe jedes Mal auf die gleiche Weise ausgeführt wird (wofür Menschen nicht geschaffen sind). Im Zusammenhang mit standardisierten Mustern und Werkzeugen meinen wir damit die Konsistenz dessen, was Entwickler für eine effektive Interaktion mit der laufenden Entwicklung der Software verwenden.
Dies ist ein Bereich, in dem Sicherheitsteams und Plattformentwicklungsteams zusammenarbeiten können, um das gemeinsame Ziel der Standardisierung zu erreichen. In der Tat könnten Plattform-Engineering-Teams diese Arbeit sogar alleine durchführen, wenn es in ihren organisatorischen Kontext passt. Wie wir immer wieder betonen, passt der Mantel des "Verteidigers" zu jedem, unabhängig von seinem üblichen Titel, wenn er die Widerstandsfähigkeit von Systemen unterstützt (wir werden das in Kapitel 7 noch viel ausführlicher diskutieren).
Wenn du kein Plattform-Engineering-Team hast und nur ein paar eifrige Verteidiger und ein schmales Budget zur Verfügung stehen, kannst du trotzdem dazu beitragen, die Muster für die Teams zu standardisieren und die Versuchung zu verringern, ihr eigenes Ding auf eine Art und Weise auszurollen, die die Sicherheit beeinträchtigt. Die einfachste Taktik besteht darin, die Muster für die Teile des Systems mit den größten Sicherheitsauswirkungen, wie Authentifizierung oder Verschlüsselung, zu priorisieren. Wenn es für dein Team schwierig ist, standardisierte Muster, Tools oder Frameworks zu entwickeln, kannst du auch Standardbibliotheken empfehlen und sicherstellen, dass diese Liste als zugängliche Dokumentation verfügbar ist. Auf diese Weise wissen die Teams, dass es eine Liste gut geprüfter Bibliotheken gibt, die sie konsultieren und aus denen sie auswählen können, wenn sie eine bestimmte Funktion implementieren müssen. Alles andere, was sie außerhalb dieser Bibliotheken verwenden möchten, ist vielleicht diskussionswürdig, aber ansonsten können sie ihre Arbeit fortsetzen, ohne die Arbeit des Sicherheits- oder Plattformteams zu stören.
Wie auch immer du es erreichst, der Aufbau einer "gepflasterten Straße" für andere Teams ist eine der wertvollsten Aktivitäten in einem Sicherheitsprogramm. Gepflasterte Straßen sind gut integrierte, unterstützte Lösungen für allgemeine Probleme, die es den Menschen ermöglichen, sich auf ihre eigene Wertschöpfung zu konzentrieren (z. B. die Entwicklung einer differenzierten Geschäftslogik für eine Anwendung).14 Auch wenn wir im Zusammenhang mit der Produktentwicklung meist an gepflasterte Straßen denken, können diese auch an anderen Stellen im Unternehmen genutzt werden, z. B. im Sicherheitsbereich. Stell dir ein Sicherheitsprogramm vor, das Wege findet, die Arbeit zu beschleunigen! Einem Vertriebsmitarbeiter die Einführung einer neuen SaaS-Anwendung zu erleichtern, die ihm hilft, mehr Geschäfte abzuschließen, ist eine gepflasterte Straße. Wenn du es den Nutzern leicht machst, die Sicherheit ihrer Konten zu überprüfen, anstatt sie in verschachtelten Menüs zu vergraben, ist das auch ein guter Weg. In Kapitel 7 werden wir mehr darüber sprechen, wie man gepflasterte Straßen als Teil eines Resilienzprogramms ermöglicht.
Gepflasterte Straßen in Aktion: Beispiele aus der freien Wildbahn
Ein überzeugendes Beispiel für einen gepflasterten Weg - die Standardisierung einiger Muster für Teams in einem unschätzbaren Framework - istdas Wall-E Framework von Netflix. Jeder, der schon einmal mit Authentifizierung, Protokollierung, Beobachtbarkeit und anderen Mustern jonglieren musste, während er versuchte, eine App mit kleinem Budget zu entwickeln, wird erkennen, dass es sich wie ein Paradies anhört, wenn man ein solches Framework erhält. Wenn wir einen Schritt zurücktreten, ist es ein perfektes Beispiel dafür, wie wir Wege finden können, um Resilienz- (und Sicherheits-) Lösungen unter Produktionsdruck zu realisieren - der "heilige Gral" in der SCE. Wie viele, die in der Technologiebranche tätig sind, schrecken wir vor dem Wort Synergien zurück, aber in diesem Fall sind sie real - wie bei vielen gepflasterten Wegen - und es kann dich bei deinem Finanzvorstand einschmeicheln, wenn du dich für die Umgestaltung der SCE einsetzt .
Ausgehend von einem merkwürdigen Sicherheitsprogramm begann Netflix mit der Beobachtung, dass die Teams der Softwareentwicklung bei der Entwicklung und Bereitstellung von Software zu viele Sicherheitsaspekte berücksichtigen mussten: Authentifizierung, Protokollierung, TLS-Zertifikate und mehr. Es gab umfangreiche Sicherheitschecklisten für die Entwickler, die viel manuellen Aufwand verursachten und verwirrend waren (wie Netflix feststellte: "Es gab Flussdiagramme innerhalb von Checklisten. Autsch."). Der Status quo bedeutete auch mehr Arbeit für das Sicherheitsteam, das die Entwickler durch die Checkliste führen und ihre Entscheidungen manuell überprüfen musste.
Das Netflix-Team für Anwendungssicherheit (appsec) hat sich daher die Frage gestellt, wie man einen gepflasterten Weg für den Prozess bauen kann, indem man ihn produktiv macht. Das Team sieht den gepflasterten Weg als eine Möglichkeit, Fragen in boolesche Aussagen zu verwandeln. Anstatt zu sagen: "Sag mir, wie deine App diese wichtige Sicherheitsfunktion ausführt", vergewissern sie sich, dass das Team den entsprechenden gepflasterten Weg benutzt, um die Sicherheitsfunktion auszuführen.
Der von Netflix gebaute Weg, Wall-E genannt, führte dazu, dass Sicherheitsanforderungen in Form von Filtern hinzugefügt wurden, die bestehende Checklisten mit Anforderungen an Web Application Firewalls (WAFs), DDoS-Prävention, Validierung von Sicherheits-Headern und dauerhafte Protokollierung ersetzten. In ihren eigenen Worten: "Wir waren schließlich in der Lage, Wall-E mit so vielen Sicherheitsfunktionen auszustatten, dass sich der Großteil der Checkliste für Studio-Anwendungen, die ins Internet gehen, auf einen einzigen Punkt beschränkte: Willst du Wall-E benutzen?"
Sie haben auch intensiv über nachgedacht, um die Reibungsverluste bei der Einführung von Wall-E zu verringern (vor allem, weil die Akzeptanz für sie eine wichtige Erfolgskennzahl war - andere Sicherheitsteams sollten das beachten). Da sie die bestehenden Arbeitsabläufe kannten, baten sie die Produktentwicklungsteams, Wall-E durch die Erstellung einer versionskontrollierten YAML-Datei zu integrieren, was nicht nur die Paketierung von Konfigurationsdaten erleichterte, sondern auch die Absicht der Entwickler erfasste. Da sie über eine "präzise, standardisierte Definition der App verfügten, die sie bereitstellen wollten", konnte Wall-E einen Großteil der mühsamen Arbeit, die die Entwickler nicht machen wollten, bereits nach wenigen Minuten der Einrichtung automatisieren. Die Ergebnisse kommen sowohl der Effizienz als auch der Ausfallsicherheit zugute - genau das, was wir suchen, um den Drang unserer Organisationen nach schnellerer Erledigung von Aufgaben und unser Streben nach Ausfallsicherheit zu befriedigen: "Für eine typische Straßenanwendung ohne ungewöhnliche Sicherheitskomplikationen konnte ein Team in weniger als 10 Minuten von git init
zu einer produktionsreifen, vollständig authentifizierten und über das Internet zugänglichen Anwendung übergehen." Den Produktentwicklern war die Sicherheit nicht unbedingt wichtig, aber sie nahmen sie gerne an, als sie merkten, dass dieses standardisierte Muster ihnen half, den Code schneller an die Benutzer zu liefern und schneller zu iterieren - und Iteration ist ein wichtiger Weg, um die Flexibilität während der Erstellung und Bereitstellung zu fördern, wie wir am Ende des Kapitels besprechen werden.
Abhängigkeitsanalyse und Priorisierung von Schwachstellen
Die letzte Praxis, die wir anwenden können, um zu erweitern und unsere Sicherheitsgrenzen zu wahren, ist die Analyse von Abhängigkeiten und insbesondere die umsichtige Priorisierung von Schwachstellen. Die Abhängigkeitsanalyse hilft uns, Fehler in unseren Tools zu erkennen, damit wir sie beheben oder abmildern können - oder sogar bessere Tools in Betracht ziehen. Wir können diese Praxis als eine Absicherung gegen potenzielle Stressfaktoren und Überraschungen betrachten, die es uns ermöglicht, unser Aufwandskapital anderweitig zu investieren. Die Sicherheitsbranche hat es uns jedoch nicht leicht gemacht, zu erkennen, wann eine Schwachstelle wichtig ist. Daher werden wir zunächst eine Heuristik aufzeigen, die uns zeigt, wann wir in die Behebung von Schwachstellen investieren sollten.
Priorisierung von Schwachstellen
Wann solltest du dich um eine Sicherheitslücke kümmern? Nehmen wir an, eine neue Sicherheitslücke wird in den sozialen Medien gehypt. Bedeutet das, dass du alles stehen und liegen lassen solltest, um einen Fix oder Patch zu installieren? Oder wird die Alarmmüdigkeit deine Motivation schmälern? Ob du dich um eine Sicherheitslücke kümmern solltest, hängt vor allem von zwei Faktoren ab:
Wie einfach ist der Angriff zu automatisieren und zu skalieren?
Wie viele Schritte ist der Angriff vom Ziel des Angreifers entfernt?
Der erste Faktor - die leichte Automatisierbarkeit und Skalierbarkeit des Angriffs (d.h. die Ausnutzung der Schwachstelle) - wird historisch mit dem Begriff " wurmbar" beschrieben.15 Kann ein Angreifer diese Schwachstelle in großem Umfang ausnutzen? Ein Angriff, der keinerlei Interaktion des Angreifers erfordert, wäre leicht zu automatisieren und zu skalieren. Das Krypto-Mining fällt oft in diese Kategorie. Der Angreifer kann einen automatisierten Dienst einrichten, der ein Tool wie Shodan nach verwundbaren Instanzen von rechenintensiven Anwendungen wie Kibana oder einem CI-Tool durchsucht. Der Angreifer führt dann ein automatisiertes Angriffsskript gegen die Instanz aus, lädt automatisch den Krypto-Mining-Payload herunter und führt ihn aus, wenn er erfolgreich ist. Der Angreifer kann benachrichtigt werden, wenn etwas schief läuft (genau wie dein typisches Ops-Team), aber er kann diese Art von Tool oft völlig selbstständig laufen lassen, während er sich auf andere kriminelle Aktivitäten konzentriert. Ihre Strategie ist es, so viele Spuren wie möglich zu finden, um die Anzahl der in einem bestimmten Zeitraum geschürften Münzen zu maximieren.
Der zweite Faktor hängt im Wesentlichen damit zusammen, wie einfach die Schwachstelle für Angreifer zu nutzen ist. Er ist zwar auch ein Element der Automatisierbarkeit und Skalierbarkeit des Angriffs, sollte aber dennoch erwähnt werden, da Schwachstellen, die als "verheerend" beschrieben werden, diesen Ansprüchen oft nicht genügen. Wenn Angreifer eine Schwachstelle ausnutzen, verschafft ihnen das Zugang zu etwas. Die Frage ist, wie nah dieses Etwas an ihren Zielen ist. Manchmal behaupten Schwachstellenforscher - und auch Bug-Bounty-Jäger -, dass die Ausnutzung einer Schwachstelle "trivial" ist, obwohl der Benutzer zahlreiche Schritte ausführen muss. Ein anonymer Angreifer witzelte: "Ich hatte schon Operationen, die fast fehlgeschlagen wären, weil ein freiwilliges Opfer nicht in der Lage war, die Anweisungen zu befolgen, um sich selbst zu kompromittieren."
Erläutern wir diesen Faktor anhand eines Beispiels. Im Jahr 2021 wurde ein Proof of Concept für Log4Shell veröffentlicht, eine Schwachstelle in der Apache Log4j-Bibliothek, die wirbereits in früheren Kapiteln besprochen haben. Die Schwachstelle bot Angreifern eine fantastische Benutzerfreundlichkeit und ermöglichte es ihnen, Code auf einem verwundbaren Host auszuführen, indem sie einen speziellen "jni:"-Text - der sich auf das Java Naming and Directory Interface (JNDI) bezieht - in ein von der Anwendung protokolliertes Feld einfügten. Wenn das relativ trivial klingt, ist es das auch. Der Angriff besteht wohl nur aus einem einzigen Schritt: Der Angreifer stellt den String bereit (eine jndi: Einfügung in einen protokollierbaren HTTP-Header, der eine bösartige URI enthält), wodurch die Log4j-Instanz gezwungen wird, eine LDAP-Anfrage an die vom Angreifer kontrollierte URI zu stellen, was dann zu einer Kette automatisierter Ereignisse führt, die dazu führen, dass eine vom Angreifer bereitgestellte Java-Klasse in den Speicher geladen und von der verwundbaren Log4j-Instanz ausgeführt wird. Nur ein Schritt (plus etwas Vorarbeit) für die Remotecodeausführung? Was für ein Wertbeitrag! Genau deshalb war Log4j für Angreifer so automatisierbar und skalierbar, was ihnen innerhalb von 24 Stunden nach Veröffentlichung des Proof of Concept gelang.
Ein weiteres Beispiel: Heartbleed befindet sich auf an der Grenze der akzeptablen Benutzerfreundlichkeit für Angreifer. Heartbleed ermöglicht es Angreifern, auf beliebigen Speicher zuzugreifen, der Geheimnisse enthalten könnte, die Angreifer dann vielleicht für etwas anderes nutzen könnten und dann... Du siehst, dass die Benutzerfreundlichkeit sehr bedingt ist. Hier kommt der Footprint-Faktor ins Spiel: Wenn nur wenige öffentlich zugängliche Systeme OpenSSL verwenden würden, würde sich die Durchführung dieser Schritte für Angreifer vielleicht nicht lohnen. Aber weil die Bibliothek so beliebt ist, könnten einige Angreifer die Mühe auf sich nehmen, einen Angriff zu entwickeln, der sich skalieren lässt. Wir sagen "einige", weil im Fall von Heartbleed der Zugriff auf beliebigen Speicher den Angreifern im Wesentlichen die Möglichkeit gibt, jeglichen Müll im wiederverwendeten OpenSSL-Speicher zu lesen, z. B. Verschlüsselungsschlüssel oder andere Daten, die verschlüsselt oder entschlüsselt wurden. Und wir meinen wirklich "Schrott". Es ist schwierig und mühsam für Angreifer, an die Daten zu kommen, die sie suchen. Und auch wenn die gleiche Schwachstelle überall und aus der Ferne zugänglich war, braucht es viel zielgerichtete Aufmerksamkeit, um sie in etwas Brauchbares zu verwandeln. Der einzige allgemeine Angriff, der mit dieser Schwachstelle möglich ist, besteht darin, die privaten Schlüssel der verwundbaren Server zu stehlen, und das ist nur als Teil eines ausgeklügelten und komplizierten Meddler-in-the-Middle-Angriffs sinnvoll.
Als extremes Beispiel für eine Schwachstelle, die viele Schritte erfordert, nennt eine Schwachstelle wie Rowhammer - ein Fehler in vielen DRAM-Modulen, bei dem wiederholte Aktivierungen von Speicherzeilen Bitflips in benachbarten Zeilen auslösen können. Theoretisch hat diese Schwachstelle eine enorme Angriffsfläche, weil sie eine ganze Generation von Maschinen betrifft. In der Praxis gibt es eine ganze Reihe von Voraussetzungen, um Rowhammer für die Privilegienerweiterung auszunutzen, und zwar abgesehen von der anfänglichen Einschränkung, dass lokaler Code ausgeführt werden muss: Umgehen des Cache und Zuweisen eines großen Speicherbereichs; Suchen nach schlechten Zeilen (Speicherplätze, die dazu neigen, Bits umzudrehen); Prüfen, ob diese Speicherplätze den Angriff zulassen; Zurückgeben des Speicherbereichs an das Betriebssystem; Zwingen des Betriebssystems, den Speicher wiederzuverwenden; Auswählen von zwei oder mehr "Zeilen-Konflikt-Adresspaaren" und Hämmern der Adressen (d. h, (d.h. Aktivierung der gewählten Adressen), um den Bitflip zu erzwingen, was zu Lese-/Schreibzugriff auf z.B. eine Seitentabelle führt, die der Angreifer dann missbrauchen kann, um auszuführen, was er wirklich tun will. Und das, bevor wir uns mit den Komplikationen befassen, die entstehen, wenn man die Bits zum Umkippen bringt. Du siehst also, warum wir diesen Angriff noch nicht in freier Wildbahn gesehen haben und warum es unwahrscheinlich ist, dass er in einem solchen Ausmaß stattfindet wie die Ausnutzung von Log4Shell.
Wenn du also abwägst, ob du eine Schwachstelle sofort beheben sollst - vor allem, wenn die Behebung zu Leistungseinbußen oder Funktionseinbußen führt - oder ob du warten sollst, bis eine praktikablere Lösung zur Verfügung steht, kannst du diese Heuristik anwenden: Lässt sich der Angriff skalieren und wie viele Schritte müssen die Angreifer dafür ausführen? Ein Autor hat schon einmal gesagt: "Wenn eine Schwachstelle lokalen Zugriff, spezielle Konfigurationseinstellungen und das Springen von Delphinen durch den Ring 0 erfordert, dann ist es übertrieben, die betroffene Software als "kaputt" zu bezeichnen. Aber wenn ein Angreifer nur eine Zeichenkette an einen verwundbaren Server senden muss, um Remotecode ausführen zu können, ist es wahrscheinlich eine Frage, wie schnell dein Unternehmen betroffen sein wird, nicht ob. Mit dieser Heuristik kannst du die Schwachstellen in "technische Schulden" und "drohende Vorfälle" einteilen. Erst wenn du alle Chancen auf zufällige Angriffe eliminiert hast - und das sind die meisten -, solltest du dir Gedanken über raffinierte, gezielte Angriffe machen, bei denen die Angreifer eine Taktik auf Spionagefilm-Niveau anwenden müssen, um erfolgreich zu sein.
Tipp
Dies ist ein weiterer Fall, in dem Isolierung uns helfen kann, die Widerstandsfähigkeit zu unterstützen. Wenn sich die verwundbare Komponente in einer Sandbox befindet, muss der Angreifer eine weitere Herausforderung überwinden, bevor er sein Ziel erreichen kann.
Erinnere dich daran, dass Schwachstellenforscher keine Angreifer sind. Nur weil sie ihre Forschung anpreisen, heißt das nicht, dass der Angriff skalierbar oder für Angreifer ausreichend effizient ist. Dein lokaler Sysadmin oder SRE ist dem typischen Angreifer näher als ein Schwachstellenforscher.
Konfigurationsfehler und Fehlermeldungen
Wir müssen auch Konfigurationsfehler und Fehlermeldungen berücksichtigen, um eine durchdachte Abhängigkeitsanalyse zu fördern. Konfigurationsfehler - oft auch als "Fehlkonfigurationen" bezeichnet - entstehen, weil die Menschen, die das System entworfen und gebaut haben, andere mentale Modelle haben als die Menschen, die das System nutzen. Wenn wir Systeme bauen, müssen wir offen sein für das Feedback der Nutzer/innen. Das mentale Modell der Nutzer/innen ist wichtiger als unser eigenes, denn sie werden die Auswirkungen von Fehlkonfigurationen zu spüren bekommen. Wie wir in Kapitel 6 näher erläutern werden, sollten wir uns nicht auf "Benutzerfehler" oder "menschliches Versagen" als oberflächliche Erklärung verlassen. Wenn wir etwas bauen, müssen wir es auf der Grundlage einer realistischen Nutzung aufbauen, nicht auf dem platonischen Ideal eines Nutzers.
Wir müssen Konfigurationsfehler und Irrtümer verfolgen und sie wie andere Fehler behandeln.17 Wir sollten nicht davon ausgehen, dass die Benutzer oder Bediener die Dokumentation oder das Handbuch lesen, um sie vollständig zu verstehen, und wir sollten uns auch nicht darauf verlassen, dass die Benutzer oder Bediener den Quellcode lesen. Wir sollten auch nicht davon ausgehen, dass die Menschen, die die Software konfigurieren, unfehlbar sind oder über denselben umfassenden Kontext verfügen wie wir als Entwickler/innen. Was sich für uns einfach anfühlt, kann für die Benutzer/innen esoterisch wirken. Eine ikonische Antwort, die dieses Prinzip veranschaulicht, stammt aus dem Jahr 2004, als ein Benutzer eine E-Mail an die OpenLDAP-Mailingliste schickte, um auf die Bemerkung des Entwicklers zu antworten, dass "das Referenzhandbuch bereits in der Nähe von top...." steht. Die Antwort lautete: "Du gehst davon aus, dass diejenigen, die das gelesen haben, verstanden haben, was der Kontext von 'Benutzer' ist. Das habe ich bis jetzt ganz sicher nicht. Leider kommen viele von uns nicht aus dem UNIX-Umfeld und obwohl wir viele Dinge aufschnappen, entgehen uns einige Dinge, die euch grundlegend erscheinen, für einige Zeit."
Wie wir in Kapitel 6 näher erläutern werden, sollten wir dem menschlichen Verhalten nicht die Schuld geben, wenn etwas schief läuft, sondern uns stattdessen bemühen, dem Menschen zum Erfolg zu verhelfen, selbst wenn etwas schief läuft. Wir wollen, dass sich unsere Software an die Konfigurationsfehler der Benutzer/innen anpassen kann. Wie eine Studie rät: "Wenn die Fehlkonfiguration eines Benutzers dazu führt, dass das System abstürzt, sich aufhängt oder stillschweigend fehlschlägt, hat der Benutzer keine andere Wahl, als dies dem technischen Support zu melden. Nicht nur die Benutzer/innen leiden unter der Ausfallzeit des Systems, sondern auch die Entwickler/innen, die Zeit und Mühe aufwenden müssen, um die Fehler zu beheben und vielleicht die Verluste der Benutzer/innen auszugleichen."18
Wie können wir das soziotechnische System dabei unterstützen, sich angesichts von Konfigurationsfehlern anzupassen? Wir können explizite Fehlermeldungen fördern, die eine Rückkopplungsschleife erzeugen (wir werden später in diesem Kapitel mehr über Rückkopplungsschleifen sprechen). Wie Yin et al. in einer empirischen Studie zu Konfigurationsfehlern in kommerziellen und Open-Source-Systemen herausfanden, enthielten nur 7,2 % bis 15,5 % der Fehlkonfigurationsfehler explizite Meldungen, die den Nutzern bei der Fehlersuche helfen.19 Bei expliziten Fehlermeldungen verkürzt sich die Diagnosezeit um das 3- bis 13-fache im Vergleich zu mehrdeutigen Meldungen und um das 1,2- bis 14,5-fache im Vergleich zu gar keinen Meldungen .
Trotz dieser empirischen Beweise besagt die Volksweisheit von, dass beschreibende Fehlermeldungen schädlich sind, weil Angreifer aus ihnen Dinge lernen können, die ihnen bei ihrer Arbeit helfen. Klar, und die Nutzung des Internets erleichtert Angriffe - sollten wir das auch vermeiden? Unsere Philosophie ist, dass wir legitime Nutzer nicht bestrafen sollten, nur weil Angreifer gelegentlich einen Vorteil erlangen können. Das bedeutet nicht, dass wir in allen Fällen ausführliche Fehlermeldungen ausgeben. Das richtige Maß an Ausführlichkeit hängt von dem betreffenden System oder der Komponente und der Art des Fehlers ab. Wenn sich unser Teil des Systems in der Nähe einer Sicherheitsgrenze befindet, sollten wir wahrscheinlich vorsichtiger sein, was wir preisgeben. Das Ad absurdum der aussagekräftigen Fehlermeldungen an einer Sicherheitsgrenze wäre zum Beispiel eine Anmeldeseite, die den Fehler zurückgibt: "Das war wirklich knapp am richtigen Passwort vorbei!"
Als allgemeine Heuristik sollten wir dazu tendieren, in Fehlermeldungen mehr Informationen zu geben, solange nicht klar ist, wie diese Informationen missbraucht werden könnten (z. B. könnte die Bekanntgabe, dass ein erratenes Passwort dem echten sehr nahe kommt, Angreifern leicht helfen). Wenn es sich um einen vorhersehbaren Fehler handelt, gegen den der/die Nutzer/in etwas unternehmen kann, sollten wir ihm/ihr das in einem für Menschen lesbaren Text mitteilen. Das System ist dazu da, dass die Benutzer und das Unternehmen ein bestimmtes Ziel erreichen können, und beschreibende Fehlermeldungen helfen den Benutzern zu verstehen, was sie falsch gemacht haben, und es zu beheben.
Wenn der Nutzer nichts gegen den Fehler tun kann, selbst wenn er Details erfährt, hat es keinen Sinn, ihn anzuzeigen. Für die letztgenannte Fehlerkategorie können wir eine Art Trace Identifier zurückgeben, mit dem ein Support-Mitarbeiter die Logs abfragen kann, um die Details des Fehlers zu erfahren (oder sogar, was sonst in der Sitzung des Nutzers passiert ist).20 Wenn ein Angreifer bei diesem Muster pikante Fehlerdetails aus den Protokollen herausfinden will, muss er den Support-Mitarbeiter sozial manipulieren (d.h. er muss einen Weg finden, ihn zur Preisgabe seiner Anmeldedaten zu überreden). Wenn es keine Möglichkeit gibt, mit dem Support-Mitarbeiter zu sprechen, hat es keinen Sinn, die Fehler-ID anzuzeigen, da der Benutzer nichts damit anfangen kann.
Tipp
Ein System sollte einem Benutzer niemals einen Stack-Trace vor die Nase halten, es sei denn, der Benutzer kann eine neue Version der Software erstellen (oder eine andere konkrete Maßnahme ergreifen). Es ist unhöflich, das zu tun.
Zusammenfassend lässt sich sagen, dass wir in der Build- und Delivery-Phase vier Praktiken anwenden können, um die Sicherheitsgrenzen, die zweite Zutat unseres Resilienztranks, zu unterstützen: Vorausschauende Skalierung, automatisierte Sicherheitsprüfungen über CI/CD, Standardisierung von Mustern und Tools und eine sorgfältige Abhängigkeitsanalyse. Kommen wir nun zur dritten Komponente: die Beobachtung der Systeminteraktionen über die Raum-Zeit hinweg.
Beobachte Systeminteraktionen über die Raum-Zeit hinweg (oder mache sie linearer)
Die dritte Zutat in unserem Resilienztrank ist die Beobachtung der Systeminteraktionen über die Raum-Zeit hinweg. Bei der Entwicklung und Bereitstellung von Systemen können wir diese Beobachtung unterstützen und genauere mentale Modelle entwickeln, wenn sich das Verhalten unserer Systeme im Laufe der Zeit und über ihre Topologie hinweg entfaltet (denn die Betrachtung einer einzelnen Komponente zu einem bestimmten Zeitpunkt sagt uns aus Sicht der Resilienz wenig). Wir können aber auch dazu beitragen, die Interaktionen linearer zu gestalten, und damit unsere Diskussion über die Gestaltung von Linearität im letzten Kapitel ergänzen. Es gibt Praktiken und Muster, die wir übernehmen (oder vermeiden) können, um mehr Linearität in die Entwicklung und Bereitstellung von Systemen zu bringen.
In diesem Abschnitt werden wir vier Praktiken untersuchen, die uns dabei helfen, die Interaktionen des Systems über die Raum-Zeit hinweg zu beobachten oder die Linearität zu fördern: Configuration as Code, Fault Injection, durchdachte Testverfahren und sorgfältige Navigation von Abstraktionen. Jede dieser Praktiken unterstützt unser übergeordnetes Ziel in dieser Phase, nämlich die Geschwindigkeit zu nutzen, um die Eigenschaften und Verhaltensweisen zu fördern, die wir brauchen, um die Widerstandsfähigkeit unserer Systeme gegen Angriffe zu erhalten.
Konfiguration als Code
Die erste Praxis, die uns das Geschenk macht, Interaktionen über die Raum-Zeit hinweg linearer zu gestalten (und sie auch zu beobachten), ist Configuration as Code (CaC). Die Automatisierung von Bereitstellungsaktivitäten reduziert den menschlichen Aufwand (der an anderer Stelle eingesetzt werden kann) und unterstützt eine wiederholbare, konsistente Softwarebereitstellung. Zur Softwarebereitstellung gehört auch die Bereitstellung der Infrastruktur, die deinen Anwendungen und Diensten zugrunde liegt. Wie können wir sicherstellen, dass auch die Infrastruktur auf wiederholbare Weise bereitgestellt wird? Und ganz allgemein: Wie können wir sicherstellen, dass unsere Konfigurationen mit unseren mentalen Modellen übereinstimmen?
Die Antwort darauf sind CaC-Praktiken: die Deklaration von Konfigurationen durch Markup anstelle manueller Prozesse. Während die SCE-Bewegung eine Zukunft anstrebt, in der alle Arten von Konfigurationen deklarativ sind, besteht die Praxis heute meist aus Infrastructure as Code (IaC). IaC ist die Möglichkeit, Infrastrukturen über deklarative Spezifikationen zu erstellen und zu verwalten, statt über manuelle Konfigurationsprozesse. Dabei wird derselbe Prozess wie beim Quellcode verwendet, aber anstatt jedes Mal dieselbe Anwendungsbinärdatei zu erzeugen, wird jedes Mal dieselbe Umgebung erzeugt. So entstehen zuverlässigere und berechenbarere Dienste. CaC ist die Idee, diesen Ansatz auf alle wichtigen Konfigurationen wie Ausfallsicherheit, Compliance und Sicherheit auszuweiten. CaC liegt im Grenzbereich zwischen Bereitstellung und Betrieb, sollte aber als Teil dessen betrachtet werden, was Entwicklungsteams liefern.
Wenn du bereits mit IaC vertraut bist, überrascht es dich vielleicht, dass es als Sicherheitstool angepriesen wird. Unternehmen setzen es bereits ein, weil es einen Prüfpfad erzeugt, der die Sicherheit unterstützt, indem er die Wiederholbarkeit von Praktiken verbessert. Werfen wir einen Blick auf die anderen Vorteile von IaC für Sicherheitsprogramme.
- Schnellere Reaktion auf Vorfälle
IaC unterstützt die automatische Umstellung der Infrastruktur, wenn es zu Zwischenfällen kommt. Noch besser ist, dass es auch automatisch auf Frühindikatoren für Vorfälle reagieren kann, indem es Signale wie Schwellenwerte verwendet, um Problemen zuvorzukommen (wir werden dies im nächsten Kapitel näher erläutern). Mit der automatischen Wiederherstellung der Infrastruktur können wir kompromittierte Workloads abschalten und neu bereitstellen, sobald ein Angriff entdeckt wird, ohne dass die Endnutzer davon betroffen sind.
- Minimierte Umweltabweichung
Die Umgebungsabweichung bezieht sich auf Konfigurationen oder andere Umgebungsattribute, die in einen inkonsistenten Zustand "abdriften", z. B. wenn die Produktion nicht mit dem Staging übereinstimmt. IaC unterstützt die automatische Versionierung der Infrastruktur, um die Umgebungsabweichung zu minimieren, und erleichtert die Rückgängigmachung von Implementierungen, wenn etwas schief läuft. Du kannst auf Flotten von Maschinen so fehlerfrei deployen, wie es für Menschen nur schwer möglich wäre. IaC ermöglicht es dir, Änderungen nahezu atomar vorzunehmen. Es kodiert deine Bereitstellungsprozesse in einer Notation, die von Mensch zu Mensch weitergegeben werden kann, vor allem, wenn sich die Teamzusammensetzung ändert - so wird die Kopplung auf Schicht 8 (d. h. der menschlichen Ebene) gelockert.
- Schnelleres Patching und Sicherheitsfixes
IaC unterstützt ein schnelleres Patchen und Verteilen von Sicherheitsänderungen. Wie wir im Abschnitt über CI/CD erörtert haben ( ), ist die eigentliche Lehre aus dem berüchtigten Equifax-Vorfall, dass Patching-Prozesse benutzbar sein müssen, sonst ist Aufschieben die logische Konsequenz. IaC reduziert die Reibungsverluste bei der Veröffentlichung von Patches, Updates oder Korrekturen und dezentralisiert den Prozess, was eine lockerere organisatorische Kopplung fördert. Ganz allgemein gilt: Wenn ein organisatorischer Prozess umständlich oder unbrauchbar ist, wird er umgangen. Das liegt nicht daran, dass Menschen schlecht sind, ganz im Gegenteil: Menschen sind ziemlich gut darin, effiziente Wege zu finden, um ihre Ziele zu erreichen.
- Minimierte Fehlkonfigurationen
Zum Zeitpunkt der Erstellung dieses Artikels sind Fehlkonfigurationen laut der National Security Agency (NSA) die häufigste Sicherheitslücke in der Cloud; sie sind für Angreifer leicht auszunutzen und weit verbreitet. IaC hilft dabei, Fehlkonfigurationen von Nutzern und automatisierten Systemen gleichermaßen zu korrigieren. Menschen und Computer sind beide in der Lage, Fehler zu machen - und diese Fehler sind unvermeidlich. IaC kann zum Beispiel die Konfiguration der Zugriffskontrolle automatisieren, die bekanntermaßen verwirrend und leicht zu verwechseln ist.
- Abfangen anfälliger Konfigurationen
Um verwundbare Konfigurationen aufzuspüren, ist der Status quo oft ein authentifiziertes Scannen in Produktionsumgebungen, was neue Angriffspfade und Gefahren mit sich bringt. Mit IaC können wir diese Gefahr ausschalten und stattdessen die Codedateien scannen, um verwundbare Konfigurationen zu finden. Mit IaC ist es auch einfacher, Regeln für eine Reihe von Konfigurationsdateien zu schreiben und durchzusetzen, als Regeln für alle APIs deines Cloud-Providers (CSP) zu schreiben und durchzusetzen.
- Autonome Durchsetzung von Richtlinien
IaC hilft bei der Automatisierung der Bereitstellung und der Durchsetzung von IAM-Richtlinien wie dem Principle of Least Privilege (PoLP). IaC-Muster vereinfachen die Einhaltung von Industriestandards, wie z.B. Compliance, mit dem Ziel einer "kontinuierlichen Compliance"(Abbildung 4-1).
- Stärkere Änderungskontrolle
IaC führt die Änderungskontrolle über die Quellcodeverwaltung (SCM) ein, die Peer-Reviews zu Konfigurationen und ein aussagekräftiges Änderungsprotokoll ermöglicht. Dies bringt auch erhebliche Vorteile bei der Einhaltung von Vorschriften mit sich.
Aufgrund all dieser Vorteile und der Tatsache, dass alle Entwicklungsteams sie nutzen können, um gemeinsame Ziele zu erreichen, unterstützt IaC ein flexibleres Sicherheitsprogramm und setzt Aufwandskapital für andere Aktivitäten frei. Der Prüfpfad, den es erzeugt, und die Experimentierumgebungen, die es ermöglicht, fördern auch die Neugierde, die wir für unseren Resilienztrank brauchen. IaC stärkt unseren Resilienztrank, indem es die Interaktionen über die Raum-Zeit hinweg linearer macht, aber auch Flexibilität und die Bereitschaft zu Veränderungen fördert - wie ein "Kaufe ein Reagenz und bekomme eins umsonst"-Angebot in der Hexenapotheke.
Fault Injection während der Entwicklung
Eine weitere Methode, die wir in dieser Phase nutzen können, um Systeminteraktionen über die Raum-Zeit hinweg zu untersuchen und zu beobachten, ist die Fault Injection.21 Für Sicherheitsteams ergeben sich daraus zwei Möglichkeiten: Sie können sich über Fault Injection informieren, um ihren Wert für das Unternehmen zu begründen (und den Weg für ihre Einführung zu ebnen), und sie können mit Entwicklungsteams zusammenarbeiten, um Fault Injection in bestehende Arbeitsabläufe zu integrieren. Wenn wir nur den "glücklichen Weg" in unserer Software testen, wird unser mentales Modell des Systems eine Illusion sein und wir werden verblüfft sein, wenn unsere Software in der Produktion nicht mehr funktioniert. Um widerstandsfähige Softwaresysteme zu entwickeln, müssen wir auch die "unglücklichen Pfade" berücksichtigen und erforschen.
Wenn du eine neue Komponente in das System einfügst, überlege dir, welche Störungsereignisse möglich sind und schreibe Tests, um sie zu erfassen. Diese Tests werden als Fault-Injection-Tests bezeichnet: Sie belasten das System, indem sie absichtlich einen Fehler einführen, z. B. eine Spannungsspitze oder einen übergroßen Eingang. Da die meisten Softwaresysteme, die wir bauen, in etwa wie eine Webanwendung aussehen, die mit einer Datenbank verbunden ist, kann ein früher Fault-Injection-Test oft die Form haben: "Trenne die Verbindung zur Datenbank und verbinde sie wieder, um sicherzustellen, dass deine Datenbankabstraktionsschicht die Verbindung wiederherstellt." Im Gegensatz zu Chaos-Experimenten, bei denen ungünstige Szenarien simuliert werden, führen wir bei der Fault Injection eine absichtlich fehlerhafte Eingabe ein, um zu sehen, was in einer bestimmten Komponente passiert.
Viele Teams räumen der Fault Injection (oder Fehlertoleranz) erst dann Priorität ein, wenn es einen Zwischenfall oder einen Beinaheunfall gibt. Angesichts begrenzter Ressourcen kann man sich fragen, ob sich Fault Injection lohnt - aber das setzt voraus, dass du sie überall durchführen musst, um sie einzuführen. Wenn du mit der Fault Injection für deine kritischen Funktionen beginnst (wie die, die du in der Tier-1-Bewertung in Kapitel 2 definiert hast), kannst du deine Anstrengungen dort investieren, wo sie wirklich wichtig sind. Nehmen wir an, dein Unternehmen bietet eine Auktionsplattform für physische Geräte an, bei der der Datenverkehr während einer Auktion kontinuierlich und ohne Ausfallzeiten abgewickelt werden muss, aber es ist in Ordnung, wenn die Benutzeranalyse verzögert wird. Vielleicht investierst du mehr in die Fehlersuche und andere Tests, um das Design des Auktionssystems zu verbessern, verlässt dich aber auf die Überwachung und Beobachtbarkeit der übrigen Systeme, um die Wiederherstellung nach Fehlern zu vereinfachen.
Tests zur Fehlerinjektion sollten geschrieben werden, solange das Problem noch aktuell ist - also während der Entwicklung. Wenn man die Fehlerinjektion für kritische Funktionen zur Standardpraxis macht, hilft das den Entwicklern, ihre Abhängigkeiten besser zu verstehen, bevor sie sie ausliefern, und kann sie davon abhalten, Komponenten einzuführen, die die Inbetriebnahme des Systems erschweren. Dieses Prinzip gilt übrigens für die meisten Tests, auf die wir als Nächstes eingehen werden.
Integrationstests, Lasttests und Testtheater
Kommen wir nun zu einer wichtigen und umstrittenen Praxis die die Beobachtung von Systeminteraktionen über die Raum-Zeit hinweg unterstützen kann: das Testen. Die Softwareentwicklung als Disziplin muss sich mit dem Thema Testen auseinandersetzen (ganz zu schweigen vom miserablen Status quo der Sicherheitstests). Testen wir auf Belastbarkeit oder Korrektheit im Laufe der Zeit, oder nur, um zu sagen, dass wir getestet haben? Einige Formen des Testens können eher als Sicherheitsvorkehrungen dienen, wenn sie vollständig automatisiert sind und Zusammenführungen oder Deployments blockieren, ohne dass ein menschliches Eingreifen erforderlich ist. Oder wir können einen Haufen Unit-Tests schreiben, die Codeabdeckung als fadenscheinigen Beweis für eine gut gemachte Arbeit verwenden und behaupten, "wir haben es getestet", wenn etwas schief geht. Alternativ können wir unser Kapital in konstruktivere Wege investieren, um die Resilienz-Eigenschaften des Systems durch Integrations- und Lasttests zu beobachten - oder sogar Resilienz-Stresstests (Chaos-Experimente) als Teil der Experimentier-Ebene durchzuführen, die wir in Kapitel 2 besprochen haben.
Die traditionelle dreieckige Testhierarchie eignet sich nicht für die Widerstandsfähigkeit. Das Dreieck (und seine geometrischen Brüder) sehen zwar nett aus und sind intuitiv, aber sie sind eher ästhetisch als realistisch. Welche Tests du für sinnvoll hältst, hängt davon ab, was in deinem lokalen Kontext am wichtigsten ist - deine kritischen Funktionen und deine Ziele und Einschränkungen, die deine Betriebsgrenzen definieren.
Wir müssen über das Testen im Sinne des Aufwandsportfolios nachdenken. Die ideale Mischung aus Testarten und -abdeckung, in die wir investieren, kann je nach Projekt und Teil des Systems unterschiedlich sein. Einem Softwareentwickler ist es vielleicht egal, ob sein Code zum Parsen der Konfiguration langsam oder fehlerhaft ist, solange die Anwendung zuverlässig mit der richtigen Konfiguration startet. Wenn es jedoch wichtig ist, die eingehenden Benutzerdaten zu validieren, könnte Fuzz-Testing ein Kandidat für diese Codepfade sein.
Von Ingenieuren geschriebene Tests sind ein Artefakt ihrer mentalen Modelle zu einem bestimmten Zeitpunkt in der Raum-Zeit. Da sich die Realität weiterentwickelt - einschließlich der Systeme und Arbeitslasten in ihr - werden Tests veraltet. Die Erkenntnisse, die wir aus Chaos-Experimenten, realen Vorfällen und sogar aus der Beobachtung gesunder Systeme gewinnen, müssen wir in unsere Testsuiten einfließen lassen, um sicherzustellen, dass sie die Produktionsrealität widerspiegeln. Wir müssen Tests priorisieren, die uns helfen, unsere mentalen Modelle zu verfeinern, und die sich anpassen können, wenn sich der Systemkontext weiterentwickelt. Im Google SRE-Handbuch heißt es: "Testen ist der Mechanismus, mit dem du die Gleichwertigkeit bestimmter Bereiche nachweisen kannst, wenn Änderungen auftreten. Jeder Test, der sowohl vor als auch nach einer Änderung bestanden wird, verringert die Unsicherheit, die bei der Analyse berücksichtigt werden muss. Gründliches Testen hilft uns, die zukünftige Zuverlässigkeit eines bestimmten Standorts so detailliert vorherzusagen, dass sie praktisch nutzbar ist."
Die Geschichte hat dem Testen in der Softwareentwicklung einen Strich durch die Rechnung gemacht: Früher gab es in den Unternehmen spezielle Testteams, aber heute sind sie nur noch selten anzutreffen. Kulturell bedingt haben Softwareentwickler oft das Gefühl, dass Tests "das Problem von jemand anderem" sind. Das Problem, das hinter dieser Ausrede steckt, ist, dass Tests als zu kompliziert empfunden werden, insbesondere Integrationstests. Aus diesem Grund sind befestigte Straßen für Tests aller Art, nicht nur für Sicherheitstests, eine der wertvollsten Lösungen, die Sicherheits- und Plattformentwicklungsteams entwickeln können. Um den Einwänden gegen die Leistung entgegenzuwirken, könnten wir den Entwicklern sogar erlauben, den Grad des Overheads anzugeben, mit dem sie einverstanden sind.22
In diesem Abschnitt erfahren wir, warum die wichtigste Testkategorie entgegen der landläufigen Meinung wohl der Integrationstest (oder "breiter Integrationstest") ist. Er prüft, ob das System tatsächlich das tut, was es im Grunde genommen tun soll. Wir werden über Lasttests sprechen und darüber, wie wir den Datenverkehr aufzeichnen können, um zu beobachten, wie sich das System über die Raum-Zeit hinweg verhält. Entgegen einer weit verbreiteten Volksweisheit werden wir erörtern, warum Unit-Tests nicht als notwendige Grundlage angesehen werden sollten, bevor ein Unternehmen andere Formen von Tests durchführt. Manche Unternehmen verzichten aus den in diesem Abschnitt erläuterten Gründen ganz auf Unit-Tests. Abschließend werden wir uns mit Fuzz-Tests beschäftigen, die sich erst dann lohnen, wenn wir die "Grundlagen" geschaffen haben.
Integrationstests
Integrationstests werden in der Regel als Teil der "guten Technik" betrachtet, aber ihr Nutzen für die Resilienz wird weniger diskutiert. Bei Integrationstests wird beobachtet, wie die verschiedenen Komponenten eines Systems zusammenarbeiten. Dabei wird in der Regel überprüft, ob sie wie erwartet interagieren, was ein wertvoller erster Schritt ist, um "verwirrende Interaktionen" aufzudecken. Was wir beobachten, ist das idealisierte System, z. B. wenn wir eine neue Iteration des Systems vorschlagen und testen, um sicherzustellen, dass sich alles wie vorgesehen integriert. Die einzigen Änderungen, über die Integrationstests informieren, sind in etwa: "Ihr habt einen Fehler gemacht und wollt verhindern, dass dieser Fehler live geht." Für ein umfassenderes Feedback, wie wir das Systemdesign verbessern können, brauchen wir Chaos-Experimente - die Resilienz-Stresstests, die wir in Kapitel 2 behandelt haben.
Wie sieht ein Integrationstest in der Praxis aus? Kehren wir zu unserem früheren Beispiel einer Webanwendung zurück, die mit einer Datenbank verbunden ist. Ein Integrationstest könnte und sollte diesen Fall abdecken - "Trenne die Verbindung zur Datenbank und verbinde sie wieder, um sicherzustellen, dass deine Datenbankabstraktionsschicht die Verbindung wiederherstellt" - in den meisten Datenbank-Client-Bibliotheken.
Die AttachMe-Schwachstelle - eineCloud Isolation Schwachstelle in Oracle Cloud Infrastructure (OCI) - ist ein Beispiel dafür, was wir mit einem Integrationstest aufdecken wollen, und ein weiteres Beispiel dafür, wie gefährlich es ist, sich beim Testen und Entwickeln generell nur auf "glückliche Wege" zu konzentrieren. Der Fehler ermöglichte es Nutzern, Festplatten-Volumes, für die sie keine Berechtigung hatten - vorausgesetzt, sie konnten das Volume nach der Volume-ID benennen - an virtuelle Maschinen anzuhängen, die sie kontrollieren, um auf die Daten eines anderen Tenants zuzugreifen. Wenn ein Angreifer dies versuchte, konnte er eine Compute Instance starten, das Zielvolume an die Compute Instance unter seiner Kontrolle anhängen und Lese-/Schreibrechte für das Volume erlangen (was es ihm ermöglichen könnte, Geheimnisse zu stehlen, den Zugriff zu erweitern oder möglicherweise sogar die Kontrolle über die Zielumgebung zu erlangen). Abgesehen von dem Angriffsszenario ist diese Art der Interaktion in mandantenfähigen Umgebungen auch aus Gründen der Zuverlässigkeit unerwünscht. Wir könnten mehrere Integrationstests entwickeln, die eine Vielzahl von Aktivitäten in einer mandantenfähigen Umgebung beschreiben, z. B. das Anhängen einer Festplatte an eine VM in einem anderen Konto, mehrere Mandanten, die gleichzeitig dieselbe Aktion in einer gemeinsamen Datenbank ausführen, oder Spitzen im Ressourcenverbrauch in einem Mandanten.
Generell sollten wir Integrationstests durchführen, die es uns ermöglichen, die Interaktionen des Systems über die Raum-Zeit hinweg zu beobachten. Dies ist für die Förderung der Widerstandsfähigkeit viel nützlicher als das Testen einzelner Eigenschaften einzelner Komponenten (wie bei Unit-Tests). Ein Input in einer Komponente reicht nicht aus, um katastrophale Ausfälle in Tests zu reproduzieren. Es werden mehrere Eingaben benötigt, aber das muss uns nicht beunruhigen. Eine Studie aus dem Jahr 2014 ergab, dass drei oder weniger Knotenpunkte ausreichen, um die meisten Ausfälle zu reproduzieren - aber es sind mehrere Eingaben erforderlich und Ausfälle treten nur bei lang laufenden Systemen auf, was sowohl die Unzulänglichkeit von Unit-Tests als auch die Notwendigkeit von Chaos-Experimenten bestätigt.25
Die Studie hat auch gezeigt, dass Fehlerbehandlungscode ein sehr einflussreicher Faktor für die meisten katastrophalen Ausfälle ist. "Fast alle" (92 %) katastrophalen Systemausfälle sind auf die "falsche Behandlung von nicht tödlichen Fehlern, die explizit in der Software signalisiert werden" zurückzuführen. Es ist überflüssig zu erwähnen, dass wir bei der Verteilung unserer Investitionen dem Testen von fehlerverarbeitendem Code Priorität einräumen sollten. Die Autoren der Studie aus dem Jahr 2014 schrieben: "Bei weiteren 23 % der katastrophalen Ausfälle war die Fehlerbehandlungslogik eines nicht-tödlichen Fehlers so falsch, dass jeder Test der Anweisungsabdeckung oder sorgfältigere Codeüberprüfungen durch die Entwickler die Fehler entdeckt hätten." Caitie McCaffrey, Partnerarchitektin bei Microsoft, rät bei der Überprüfung verteilter Systeme: "Das absolute Minimum sollten Unit- und Integrationstests sein, die sich auf Fehlerbehandlung, nicht erreichbare Knoten, Konfigurationsänderungen und Änderungen der Cluster-Mitgliedschaft konzentrieren." Diese Tests müssen nicht kostspielig sein, sondern bieten einen hohen ROI für Ausfallsicherheit und Zuverlässigkeit.
McCaffrey merkte an, dass Integrationstests oft übersprungen werden, weil "man allgemein davon ausgeht, dass es schwierig ist, Fehler offline zu produzieren, und dass die Schaffung einer produktionsähnlichen Umgebung für Tests kompliziert und teuer ist".26 Die gute Nachricht ist, dass die Schaffung einer produktionsähnlichen Testumgebung von Jahr zu Jahr einfacher und billiger wird. Wir werden in Kapitel 5 über einige der modernen Infrastrukturinnovationen sprechen, die kostengünstige Experimentierumgebungen ermöglichen - eine strengere Form der Testumgebung und mit einem größeren Umfang. Jetzt, wo die Rechenleistung billiger ist, sollten traditionelle "Pre-Prod"-Umgebungen, in denen sich eine Vielzahl von Anwendungsfällen aus Kostengründen dieselbe Infrastruktur teilen muss, nicht mehr eingesetzt werden. Wir wollen Integrationstests (und funktionale Tests) vor der Veröffentlichung durchführen, oder bei jedem Zusammenführen mit dem Stammzweig, wenn wir eine CD verwenden. Wenn wir beim Schreiben des Codes Deployment-Metadaten in einem deklarativen Format einbeziehen, können wir auch Integrationstests leichter automatisieren, da unsere Testinfrastruktur einen Service-Abhängigkeitsgraph nutzen kann.
Tipp
Zu den häufigsten Einwänden gegen Integrationstests gehören das Vorhandensein vieler externer Abhängigkeiten, die Notwendigkeit der Reproduzierbarkeit und die Wartbarkeit. Wenn du Integrationstests mit Chaos-Experimenten kombinierst, besteht weniger Druck auf die Integrationstests, das gesamte Spektrum möglicher Wechselwirkungen zu testen. Du kannst dich auf einige wenige konzentrieren, von denen du annimmst, dass sie am wichtigsten sind, und diese Annahme im Laufe der Zeit durch Chaos-Experimente verfeinern.
Die Abneigung gegen Integrationstests geht jedoch noch tiefer. Integrationstests decken proaktiv unvorhergesehene Fehler auf, aber Ingenieure verachten sie manchmal trotzdem. Warum eigentlich? Zum Teil liegt es daran, dass es bei fehlgeschlagenen Integrationstests eine ziemliche Tortur ist, die Ursache herauszufinden und den Fehler zu beheben. Der subjektivere Teil kommt in dem häufigen Refrain eines Ingenieurs zum Ausdruck: "Meine Integrationstests sind langsam und fehlerhaft." "Flackernde Tests sind Tests, bei denen du den Test einmal ausführst und er erfolgreich ist, aber bei der nächsten Ausführung fehlschlägt. Wenn Integrationstests langsam und flackernd sind, ist das System langsam und flackernd. Das kann an deinem Code oder an deinen Abhängigkeiten liegen - aber als Systemdenker bist du für deine Abhängigkeiten verantwortlich.
Ingenieure zögern oft, ihr mentales Modell zu aktualisieren, obwohl es Hinweise darauf gibt, dass die Implementierung unzuverlässig ist. Das liegt in der Regel daran, dass ihre Unit-Tests ihnen sagen, dass der Code genau das tut, was sie wollen (auf die Nachteile von Unit-Tests gehen wir gleich noch ein). Hätten sie ein zuverlässigeres System implementiert und bessere Integrationstests geschrieben, wäre es nicht so notwendig, "unzuverlässigen" Integrationstests hinterherzulaufen. Das eigentliche Problem - die Zuverlässigkeit der Software - verleitet Ingenieure zu der Annahme, dass Integrationstests Zeitverschwendung sind, weil sie unzuverlässig sind. Das ist ein unerwünschter Zustand, wenn wir bei der Entwicklung von Software die Ausfallsicherheit unterstützen wollen.
Warnung
Die Codecov-Kompromittierung aus dem Jahr 2021, bei der sich Angreifer unbefugten Zugriff auf das Bash-Uploader-Skript von Codecov verschafften und es modifizierten, ist ein gutes Beispiel für das Prinzip "Du besitzt deine Abhängigkeiten".
Das Design von Codecov schien nicht auf Resilienz ausgelegt zu sein. Um Codecov zu nutzen, mussten die Nutzer bash <(curl -s https://codecov.io/bash)
in ihre Build-Pipelines einfügen (der Befehl ist inzwischen veraltet). Codecov hätte dieses Skript so gestalten können, dass es Codesignaturen überprüft oder eine Vertrauenskette einrichtet, aber das wurde nicht getan. Auf der Serverseite hätten sie Maßnahmen ergreifen können, um die Verteilung auf diesen Server/Pfad zu beschränken, haben es aber nicht getan. Sie hätten Warnungen und Protokolle für Verteilungen auf diesen Server einfügen können, haben es aber nicht getan. Es gab zahlreiche Stellen, an denen das Design nicht das Vertrauen widerspiegelte, das die Nutzer in sie setzten.
Die Entwickler, die die Software schreiben und den Codecov-Agenten darin implementieren, haben sich also für Codecov entschieden, ohne das Design vollständig zu überprüfen oder die Auswirkungen auf die n-Ordnung zu durchdenken. Erinnere dich daran, dass Angreifer diese Entwürfe gerne für dich "prüfen" und dich mit ihren Erkenntnissen überraschen werden, aber es ist besser, wenn du dir die Einstellung zu eigen machst, dass du deine Abhängigkeiten selbst in der Hand hast und alles, was du in deine Systeme einfügst, genau unter die Lupe nimmst.
Um diesen Vorurteilen entgegenzuwirken, müssen wir eine Kultur der Neugierde pflegen und immer wieder betonen, dass wir unsere mentalen Modelle verfeinern müssen, anstatt uns an trügerische, aber bequeme Erzählungen zu klammern. Peer-Reviews zu Tests, wie sie weiter oben in diesem Kapitel beschrieben wurden, können auch dazu beitragen, dass ein Ingenieur sich über den Integrationstest ärgert und nicht über seinen Code.
Belastungstests
Wenn wir die Interaktionen über die Raum-Zeit hinweg als Teil unseres Resilienztranks beobachten wollen, müssen wir beim Testen einer neuen Softwareversion beobachten, wie sich das System unter Last verhält. Das Testen mit Spielzeuglasten ist wie das Testen eines neuen Rezepts in einem Easy-Bake-Oven und nicht in einem echten Backofen. Nur wenn wir realistische Arbeitslasten entwerfen, die simulieren, wie die Benutzer mit dem System interagieren, können wir die potenziellen "verblüffenden" funktionalen und nichtfunktionalen Probleme aufdecken, die bei der Auslieferung in der Produktion auftreten würden. Natürlich ist es aus Sicht der Ausfallsicherheit nicht ideal, wenn wir von einem Deadlock geschockt werden, nachdem die neue Version eine Weile in der Produktion läuft.
Ein automatisierter Ansatz stellt sicher, dass Softwareentwickler nicht gezwungen sind, Tests ständig neu zu schreiben, was dem Geist der Flexibilität und der Bereitschaft zur Veränderung zuwiderläuft. Wenn wir Lasttests bei Bedarf (oder täglich) durchführen können, können wir mit der Entwicklung des Systems Schritt halten. Wir müssen auch sicherstellen, dass die Ergebnisse umsetzbar sind. Wenn ein Test zu viel Aufwand für die Entwicklung, Durchführung oder Analyse erfordert, wird er nicht genutzt. Können wir hervorheben, ob ein Ergebnis Teil früherer Testergebnisse war und auf ein wiederkehrendes Problem hinweist? Können wir Interaktionen visualisieren, um leichter zu verstehen, welche Designverbesserungen die Widerstandsfähigkeit verbessern könnten? In Kapitel 7 werden wir mehr über Nutzererfahrungen und die Berücksichtigung von Aufmerksamkeit erfahren.
Doch die Entwicklung realistischer Arbeitsbelastungen ist nicht trivial. Die Art und Weise, wie Nutzer - egal ob Mensch oder Maschine - mit der Software (der Last) interagieren, ändert sich ständig, und das Sammeln aller Daten über diese Interaktionen erfordert einen erheblichen Aufwand.27 Unter dem Gesichtspunkt der Ausfallsicherheit geht es uns weniger um die Erfassung des Gesamtverhaltens als um die Erfassung der Vielfalt des Verhaltens. Wenn wir nur mit dem Durchschnittsverhalten testen würden, würden wir wahrscheinlich unsere mentalen Modelle bestätigen, aber nicht in Frage stellen.
Eine Taktik besteht darin, persona-basierte Lasttests durchzuführen, die modellieren, wie ein bestimmter Nutzertyp mit dem System interagiert. Die Forscher, die hinter dieser Taktik stehen , geben ein Beispiel: "Zu den Personas für ein E-Commerce-System könnten 'Shopaholics' (Nutzer, die viele Einkäufe tätigen) und 'Window Shopper' (Nutzer, die viele Artikel ansehen, ohne etwas zu kaufen) gehören." Wir könnten auch Personas für maschinelle Nutzer (APIs) und menschliche Nutzer erstellen. Um unsere mentalen Modelle über raum-zeitliche Interaktionen zu verfeinern, ist es wichtig, unbekannte Personas zu entdecken, die das Systemverhalten (und die Widerstandsfähigkeit) beeinflussen.
Warnung
Eine Gefahr ist das Schreiben von Lasttests, von denen du glaubst, dass sie sind, die aber in Wirklichkeit Benchmarks sind. Das Ziel eines Lasttests ist es, eine realistische Belastung zu simulieren, der das System in der realen Welt ausgesetzt sein könnte. Benchmarks werden in der Regel zu einem festen Zeitpunkt durchgeführt und für alle zukünftigen Versionen der Software verwendet - und das sind die besseren Benchmarks, die auf einem realen Korpus basieren. In der Praxis handelt es sich bei den meisten Tests um synthetische Benchmarks, die eine bestimmte Arbeitslast messen, die für den Test entwickelt wurde.
Die Forscher für personabasierte Lasttests fanden sogar heraus, dass "Lasttests mit Workloads, die nur zur Erfüllung von Durchsatzzielen entwickelt wurden, nicht ausreichen, um mit Sicherheit zu behaupten, dass die Systeme in der Produktion gut funktionieren werden. Mikrobenchmarks entfernen sich sogar noch weiter von der Realität des Systems, indem sie nur einen kleinen Teil des Systems testen, um den Ingenieuren zu helfen, herauszufinden, ob eine Änderung dazu führt, dass dieser Teil des Systems schneller oder langsamer ausgeführt wird.
Einen Ad-hoc-Benchmark zu schreiben, um eine Entscheidung zu treffen, und ihn dann wegzuwerfen, kann unter bestimmten Umständen sinnvoll sein, aber als langfristige Bewertung sind sie miserabel. Trotzdem sind Benchmarking-Tests sehr schwierig. Es ist schwierig zu wissen, ob du das Richtige misst, ob dein Test repräsentativ ist, wie die Ergebnisse zu interpretieren sind und was aufgrund der Ergebnisse zu tun ist.28 Selbst wenn die Ergebnisse einer Veränderung super signifikant sind, müssen sie immer gegen die Vielfalt der Faktoren abgewogen werden, die eine Rolle spielen. Viele kommerzielle Datenbanken verbieten die Veröffentlichung von Benchmark-Ergebnissen aus diesem und anderen Gründen (bekannt als " DeWitt-Klausel ").
Traffic Replay hilft uns ein besseres Gefühl dafür zu bekommen, wie sich das System bei realistischen Eingaben verhält. Wenn wir die Systeminteraktionen über die Raum-Zeit hinweg beobachten und in unsere mentalen Modelle einbeziehen wollen, müssen wir simulieren, wie sich unsere Software in Zukunft verhalten könnte, wenn sie in der Produktion läuft. Ein Mangel an realistischen Abläufen beim Testen neuer Umgebungen führt zu überflüssiger Verwirrung, wenn wir unsere Software in der Produktion einsetzen und ausführen. Wenn wir geskriptete Anfragen schreiben, beschränken wir uns beim Testen auf unsere mentalen Modelle, während die Aufnahme von echtem Datenverkehr in der Produktion unsere mentalen Modelle mit gesunden Überraschungen versorgt .
Bei der Verkehrsspiegelung (oder dem "Shadowing") wird auf echter Produktionsverkehr aufgezeichnet, den wir wiedergeben können, um eine neue Version eines Workloads zu testen. Die bestehende Version des Dienstes ist davon nicht betroffen; sie bearbeitet die Anfragen wie gewohnt. Der einzige Unterschied ist, dass der Datenverkehr auf die neue Version kopiert wird, wo wir beobachten können, wie sie sich bei der Bearbeitung realistischer Anfragen verhält.
Die Nutzung einer Cloud-Infrastruktur kann die Wiederholung von Datenverkehr - und High-Fidelity-Tests im Allgemeinen - kostengünstiger machen. Wir können für die Tests eine komplette Umgebung in der Cloud bereitstellen und sie nach Abschluss der Tests wieder abbauen (mit denselben Prozessen, die wir ohnehin für die Wiederherstellung im Notfall einrichten sollten). Traffic Replay funktioniert auch mit monolithischen, veralteten Diensten. In jedem Fall erhalten wir beim Testen einen empirischeren Überblick über das zukünftige Verhalten, als wenn wir versuchen, realistische Abläufe selbst zu definieren und zu erahnen. Bei den Werkzeugen kann es sich um Open-Source-Tools wie GoReplay, Service-Meshes oder native Tools von Cloud-Providern handeln. Viele etablierte Sicherheitslösungen - wie Intrusion Detection Systeme (IDS), Data Loss Prevention (DLP) und Extended Detection and Response (XDR) - nutzen die Verkehrsspiegelung zur Analyse des Netzwerkverkehrs.
Warnung
Abhängig von deinen Compliance-Vorschriften kann die Wiedergabe von legitimem Benutzerverkehr in einer Test- oder Experimentierumgebung eine zusätzliche Belastung darstellen. Dieses Problem kann entschärft werden, indem der Datenverkehr anonymisiert oder verschlüsselt wird, bevor er in der Umgebung wiedergegeben wird.
Unternehmen in stark regulierten Branchen verwenden bereits den Ansatz, synthetische Datensätze zu erstellen - also solche, die Produktionsdaten imitieren, aber keine echten Nutzerdaten enthalten -, um Vorproduktions-, Staging- und andere Testumgebungen zu bestücken und dabei die Datenschutzbestimmungen (wie HIPAA) einzuhalten. Unternehmen in weniger datenschutzbewussten Branchen müssen möglicherweise einen ähnlichen Ansatz wählen, um ungewollte Haftungen zu vermeiden.
Einheitstests und Testtheater
Du kannst dir Unit-Tests als vorstellen, die die lokale Geschäftslogik (innerhalb einer bestimmten Komponente) überprüfen, und Integrationstests als Überprüfung der Interaktionen zwischen der Komponente und einer ausgewählten Gruppe von anderen Komponenten. Der Vorteil von Unit-Tests ist, dass sie detailliert genug sind, um präzise Ergebnisse zu überprüfen, die als Ergebnis sehr spezifischer Szenarien auftreten. So weit, so gut. Das Problem ist, dass sie auch überprüfen, wie dieses Ergebnis erreicht wird, indem sie mit der internen Struktur eines Programms kommunizieren. Wenn jemand diese interne Struktur ändert, funktionieren die Tests nicht mehr - entweder schlagen die Tests fehl, oder sie lassen sich nicht mehr kompilieren oder prüfen die Typen.
Unit-Tests sind oft eine schlechte Investition unseres Aufwandskapitals; wir sollten unsere Bemühungen lieber anderweitig einsetzen. Manche mögen den Einsatz ohne Unit-Tests als leichtsinnig bezeichnen, aber je nach deinen Zielen und Anforderungen kann er durchaus sinnvoll sein. Unit-Tests bewahren den Status quo, indem sie die Softwareentwicklung um ein gewisses Maß an Reibung erweitern - ein Beispiel für die Einführung einer engen Kopplung beim Testen. Tatsächlich ist ein Unit-Test die am stärksten gekoppelte Art von Test, die du schreiben kannst, und bewegt sich im Bereich der Komponentenebene. Wir können eine ähnliche Kritik an formalen Methoden heranziehen, um zu verstehen, warum wir eine Bewertung auf Systemebene und nicht auf Komponentenebene brauchen: "Formale Methoden können verwendet werden, um zu überprüfen, ob eine einzelne Komponente nachweislich korrekt ist, aber die Komposition von korrekten Komponenten ergibt nicht notwendigerweise ein korrektes System; es ist eine zusätzliche Überprüfung erforderlich, um zu beweisen, dass die Komposition korrekt ist."29
Das soll nicht heißen, dass Unit-Tests nutzlos sind. Es ist sinnvoll, Tests hinzuzufügen, wenn du nicht erwartest, dass sich der Zustand des Systems ändert. Wenn du erwartest, dass die Implementierung stabil ist, ist es wahrscheinlich eine gute Idee, das Verhalten zu bestätigen. Wenn du das beabsichtigte Verhalten einer Komponente bestätigst, kann dies zu einer engen Kopplung führen - wenn Änderungen im Rest des Systems diesen Teil des Systems zerstören. Die unerwartete, verwirrende Interaktion wird in deinem Entwicklungszyklus aufgedeckt und nicht erst in deinem Deployment- und Rollback-Zyklus.
Manche Unit-Tests greifen sogar in die interne Struktur eines Moduls ein, um es als Puppenspieler zu verwenden - fast wie ein wörtliches Testtheater. Stell dir vor, du hast einen mehrstufigen Prozess, den ein Modul implementiert. Andere Module rufen ihn über seine offene Schnittstelle auf und lösen jeden Schritt zum richtigen Zeitpunkt und mit den richtigen Daten aus. Unit-Tests sollten diese Schnittstelle aufrufen und bestätigen, dass die Ergebnisse den Erwartungen entsprechen. Das bedeutet, dass du zum Testen von Schritten, die Vorbedingungen haben, die vorherigen Schritte ausführen musst, was langsam und repetitiv sein kann. Eine Möglichkeit, diese Mühsal zu umgehen, besteht darin, das Modul in einem "funktionalen Stil" umzugestalten, bei dem jeder Schritt seine Voraussetzungen explizit und nicht implizit über den internen Zustand des Moduls erhält. Der Aufrufer muss dann die Voraussetzungen von der Ausgabe eines Schritts an die Eingabe der nachfolgenden Schritte weitergeben. Tests können stattdessen die Voraussetzungen explizit mit bekannten Werten erstellen und jeden Schritt entsprechend validieren. Anstatt sie zu überarbeiten, versuchen viele Ingenieure, den internen Zustand zu "extrahieren" und ihn dann über den Startcode zu "injizieren", der als Teil des Testaufbaus läuft. Die Schnittstelle zum Modul muss sich nicht ändern, nur die Tests müssen sich anpassen - was für uns wenig Erkenntnisgewinn bedeutet.
Was passiert, wenn du neue Verhaltensweisen zu deinem System hinzufügen möchtest? Wenn du sie hinzufügen kannst, ohne die Struktur des Codes zu ändern, können deine Unit-Tests so laufen, wie sie sind, und melden möglicherweise unbeabsichtigte Änderungen im Verhalten des aktualisierten Programms. Wenn du die neuen Verhaltensweisen nicht hinzufügen kannst, ohne die Struktur des Codes zu ändern, musst du neben der Struktur des Codes auch die Unit-Tests ändern. Kannst du den Tests vertrauen, die du zusammen mit dem Code änderst, den sie testen? Vielleicht, wenn der Ingenieur, der sie aktualisiert, gewissenhaft ist. Hoffentlich prüft der Prüfer bei der Codeüberprüfung die Teständerungen, um sicherzustellen, dass die neuen Aussagen mit den alten übereinstimmen... aber frag mal einen Ingenieur (oder dich selbst), wann du das das letzte Mal gesehen hast. Wir brauchen einen Test, der das Verhalten prüft, aber nicht von der Struktur des Codes abhängt. Deshalb bietet die gängige Testpyramide eher einen ästhetischen Anreiz als eine echte Anleitung oder einen echten Wert.
Um die Vorteile von Unit-Tests zu nutzen, darfst du die Struktur deines Codes nie ändern. Dieser Stillstand ist ein Gräuel für die Resilienz. Wenn du Werkzeuge entwickelst, die den Status quo beibehalten und das System in seinem aktuellen Design festhalten, entsteht keine hochwertige Software. Nur wer mutig ist und sowohl den Code als auch die dazugehörigen Tests umschreibt, kann das Design vorantreiben. Vielleicht ist das perfekt für Systeme, die sich im Wartungsmodus befinden, wo wesentliche Änderungen unwahrscheinlich sind, oder für Teams, in denen so viel los ist, dass niemand das Design des Systems versteht - was eine Umgestaltung ohnehin unmöglich macht. Diese Systeme ersticken die Kreativität und die Begeisterung der Ingenieure für Software - abgesehen von ihrer Sprödigkeit - so dass wir sie auf jeden Fall meiden sollten, damit unsere Neugierde nicht untergeht.
Warnung
Die Korrelation zwischen der Codeabdeckung und dem Auffinden von mehr Fehlern ist möglicherweise nur schwach.30 Die Forscher der Softwareentwicklung Laura Inozemtseva und Reid Holmes kamen zu dem Schluss, dass "die Codeabdeckung zwar nützlich ist, um unzureichend getestete Teile eines Programms zu identifizieren, aber nicht als Qualitätsziel verwendet werden sollte, da sie kein guter Indikator für die Effektivität von Testsuiten ist ."
Wir sollten eine hohe Codeabdeckung nicht mit guten Tests verwechseln. Diese Forscher raten: "Die Codeabdeckung misst lediglich, dass eine Anweisung, ein Block oder eine Verzweigung ausgeführt wurde. Sie sagt nichts darüber aus, ob sich der geübte Code korrekt verhalten hat."
Fuzz-Testing (Fuzzing)
Ein Fuzz-Tester "erzeugt iterativ und zufällig Eingaben, mit denen er ein Zielprogramm testet" und sucht dabei normalerweise nach Ausnahmen im Programmverhalten (wie Abstürze oder Speicherlecks).31 Ein Fuzz-Tester (auch "Fuzzer" genannt) läuft auf einem Zielprogramm, wie dem, das wir entwickeln (Angreifer benutzen Fuzzer auch, um Schwachstellen in Programmen zu finden, die potenziell ausgenutzt werden können). Jedes Mal, wenn wir den Fuzz-Tester laufen lassen - ein "Fuzzing-Durchlauf" - kann er "aufgrund der Verwendung von Zufälligkeiten andere Ergebnisse als beim letzten Mal liefern".32 Das Gleiche gilt für Chaos-Experimente (auf die wir in Kapitel 8 näher eingehen werden), die den Unwägbarkeiten der Realität unterliegen.
Um die Erwartungen zu erfüllen, müssen Fuzzers mit erheblichem Aufwand geschrieben und in jeden Teil des Systems integriert werden, der Daten akzeptieren könnte (und welche Teile des Systems akzeptieren keine Daten?). Bevor du das versuchst, solltest du sicherstellen, dass deine anderen Tests zuverlässig sind. Wenn Ingenieure die Integrationstests immer noch umgehen, solltest du herausfinden, warum das so ist und den Prozess verfeinern, bevor du etwas Ausgefalleneres wie Fuzzing ausprobierst. Wenn diese "Grundlagen" aber erst einmal vorhanden sind, kann Fuzz-Testing eine sehr nützliche Testkategorie für die Ausfallsicherheit sein.
Vorsicht vor verfrühten und unsachgemäßen Abstraktionen
Die letzte Praxis, die wir in Betracht ziehen können im Zusammenhang mit raum-zeitlichen Systeminteraktionen ist die Kunst der Abstraktionen. Abstraktionen sind ein grausames Beispiel für das Aufwand-Investitions-Portfolio, denn Abstraktionen sind nur dann praktisch, wenn du sie nicht pflegen musst. Betrachte eine Abstraktion, die kein Computer ist: der Lebensmittelladen. Der Lebensmittelladen stellt sicher, dass das ganze Jahr über Kochbananen verfügbar sind, obwohl das Angebot je nach Jahreszeit, Regenmenge usw. schwankt. Die komplexen Interaktionen zwischen dem Laden und den Lieferanten, zwischen den Lieferanten und den Landwirten, zwischen den Landwirten und dem Kochbananenbaum, zwischen dem Kochbananenbaum und seiner Umwelt - all das wird für den Verbraucher abstrahiert. Für den Verbraucher ist es ganz einfach, in den Laden zu gehen, sich Kochbananen mit dem gewünschten Reifegrad auszusuchen und sie an der Kasse zu kaufen. Für den Laden ist es ein mühsamer Prozess, mit mehreren Bananenlieferanten in Kontakt zu bleiben - denn eine enge Bindung an nur einen Lieferanten würde zu Brüchigkeit führen (was ist, wenn der Lieferant ein schlechtes Jahr hat und es jetzt keine Bananen für deine Kunden gibt?) - sowie Bananen zu puffern und in die Warteschlange zu stellen, um Unwägbarkeiten im Angebot auszugleichen (und dabei so effizient zu sein, dass ein Gewinn erzielt wird).
Tipp
Wir können uns Teams sogar als Abstraktionen über ein bestimmtes Problem oder einen Bereich vorstellen, die der Rest der Organisation nutzen kann. Die meisten Unternehmen, die Geld verdienen, haben eine Art Abrechnungsdienst. In diesen Unternehmen nutzen nicht nur die Softwaresysteme den Abrechnungsdienst, um den Kunden die Produkte in Rechnung zu stellen, sondern auch die Menschen im System nutzen das Abrechnungsteam für ihre Abrechnungsbedürfnisse und erwarten, dass das Abrechnungsteam den Bereich viel besser kennt als alle anderen.
Wenn wir Abstraktionen schaffen, müssen wir uns daran erinnern, dass jemand sie aufrechterhalten muss. Der Aufwand, den wir für den Verbraucher betreiben, ist mit hohen Kosten verbunden, und jemand muss diese Illusionen entwickeln und aufrechterhalten. Für alles, was keinen differenzierten organisatorischen Wert darstellt, lohnt es sich, die Illusionsbildung an Menschen auszulagern, deren Wert auf der Abstraktion dieser Komplexität beruht. Ein Lebensmittelladen formt nicht sein eigenes Plastik, um Einkaufskörbe herzustellen. Ein Transportunternehmen mit einer E-Commerce-Website hat auch nicht die Aufgabe, abstrakte Infrastrukturen zu erstellen und zu warten, wie z. B. den Umgang mit gegenseitigem Ausschluss und Deadlocks.
Jedes Mal, wenn wir eine Abstraktion schaffen, müssen wir uns daran erinnern, dass wir eine Illusion schaffen. Das ist die Gefahr bei der Schaffung von Abstraktionen für unsere eigene Arbeit: Sie kann zu einer Selbstverblödung führen. Eine Abstraktion ist letztlich eine enge Kopplung, um den Overhead zu minimieren. Sie verbirgt die zugrundeliegende Komplexität - aber sie beseitigt sie nicht, es sei denn, wir konsumieren nur, statt sie zu pflegen. Wenn wir also die Schöpfer und Verwalter der Abstraktion sind, können wir uns an der Idee der losen Kopplung stoßen, weil sie von uns verlangt, die Wahrheit zu berücksichtigen. Sie verbirgt nicht die komplexen Interaktionen über die Raum-Zeit hinweg, was sich beängstigend anfühlen kann. Wir dachten, wir hätten das System verstanden, und jetzt sehen wir uns all diese "verwirrenden" Wechselwirkungen an! Die Abstraktion gab uns die Illusion, das System zu verstehen, aber nicht die Wahrheit.
Eine Abstraktion verbirgt zwangsläufig einige Details, und diese Details können für die Widerstandsfähigkeit deines Systems wichtig sein. Wenn deine Softwaresysteme ein verworrenes Nest von Abstraktionen sind und etwas schief läuft, wie kannst du es dann debuggen? Du opferst einen RAM-Stick auf dem Altar der Götter, suchst dir einen anderen Beruf oder weinst eine Weile still vor deinem Computerbildschirm, bevor du Koffein schluckst und das virtuelle Äquivalent dazu machst, deinen Kopf gegen eine Mauer zu schlagen, während du die Abstraktionen auseinander nimmst. Die Abstraktionen versuchen, verwirrende Wechselwirkungen und Auswirkungen von Ereignissen der Ordnung n zu verbergen, aber eine enge Kopplung kann die interaktive Komplexität nicht auslöschen. Kleinere Fehler in den Komponenten führen zu Ausfällen auf Systemebene, und die von der engen Kopplung geforderte unmittelbare Reaktion auf Zwischenfälle ist unmöglich, weil die benötigten Informationen unter dem dicken, glänzenden Glanz der Abstraktion undurchsichtig sind.
Opportunitätskosten-Rahmen den Kompromiss der Abstraktion gut. Welche Vorteile geben wir auf, wenn wir eine Abstraktion wählen? Wie steht das im Vergleich zu der Vereinfachung und Lesbarkeit, die wir von der Implementierung bis zum Scheitern des Systems erhalten? Was wir anstreben, ist, dass der sozioökonomische Teil des Systems dem Verständnis so nahe wie möglich kommt. Wir werden bald über die entscheidende Bedeutung des Wissensaustauschs sprechen, wenn wir untersuchen, wie wir die vierte Zutat des Zaubertranks unterstützen können: Feedbackschleifen und Lernkultur. Genauso wenig wie wir einen einzelnen Dienst wollen, von dem unser gesamtes System abhängt, wollen wir einen einzelnen Menschen, von dem unser gesamtes System abhängt. Unsere Stärke liegt in der Zusammenarbeit und der Kommunikation. Unterschiedliche Perspektiven sind nicht nur hilfreich, sondern ausdrücklich notwendig, um das System so zu verstehen, wie es sich in der Realität verhält, und nicht nur in den statischen Modellen in unseren Köpfen oder in einem Architekturdiagramm oder in Codezeilen.
Förderung von Feedbackschleifen und Lernen während des Build and Deliver
Unsere vierte Zutat des Resilienztranks sind Rückkopplungsschleifen und Lernkultur, also die Fähigkeit, sich an Misserfolge zu erinnern und daraus zu lernen. Wenn wir uns an das Verhalten des Systems als Reaktion auf Stressoren und Überraschungen erinnern, können wir daraus lernen und es nutzen, um Änderungen vorzunehmen, die die Widerstandsfähigkeit des Systems gegenüber solchen Ereignissen in Zukunft verbessern. Was können wir tun, um diese Erinnerungen abzurufen, zu bewahren und aus ihnen zu lernen, um eine Rückkopplungsschleife beim Bauen und Liefern zu schaffen?
In diesem Abschnitt geht es darum, wie wir neugierig und kollaborativ auf das soziotechnische System sein können, um es effektiver zu entwickeln. Wir untersuchen vier Möglichkeiten, um Feedbackschleifen und Lernen in dieser Phase zu fördern: Testautomatisierung, Dokumentation des Warum und Wann, verteiltes Tracing und Logging und die Verfeinerung der menschlichen Interaktion mit unseren Entwicklungspraktiken.
Test Automatisierung
Unsere erste Gelegenheit, Feedbackschleifen und Lernen in dieser Phase zu fördern, ist die Praxis der Testautomatisierung. Bei Tests müssen wir das Warum genauso deutlich machen wie bei anderen Dingen. Wenn wir einen Test schreiben, können wir dann sagen, warum wir die einzelnen Punkte überprüfen? Wenn wir nicht wissen, warum wir etwas überprüfen, werden wir verwirrt sein, wenn unsere Tests fehlschlagen, nachdem wir etwas geändert oder neuen Code hinzugefügt haben. Haben wir etwas kaputt gemacht? Oder sind die Tests einfach veraltet und nicht mehr in der Lage, den aktuellen Stand der Dinge zu berücksichtigen?
Wenn die Gründe für Tests - oder für irgendetwas anderes - nicht dokumentiert und nachvollziehbar sind, gehen die Menschen eher davon aus, dass sie unnötig sind, dass man sie einfach herausreißen und ersetzen kann. Diese voreingenommene Argumentation ist als Chestertons Zaunfehlschluss bekannt, der erstmals in G.K. Chestertons Buch The Thing beschrieben wurde:
Wenn es darum geht, Dinge zu reformieren, anstatt sie zu verformen, gibt es ein klares und einfaches Prinzip, das wahrscheinlich als Paradox bezeichnet werden kann. In einem solchen Fall gibt es eine bestimmte Einrichtung oder ein Gesetz, sagen wir der Einfachheit halber einen Zaun oder ein Tor, das über eine Straße errichtet wurde. Der modernere Reformer geht fröhlich darauf zu und sagt: "Ich weiß nicht, wozu das gut sein soll; wir sollten es abschaffen. Der intelligentere Reformer tut gut daran, darauf zu antworten: "Wenn du keinen Nutzen darin siehst, werde ich dich es bestimmt nicht wegmachen lassen. Geh weg und denk nach. Wenn du dann zurückkommst und mir sagst, dass du einen Nutzen darin siehst, erlaube ich dir vielleicht, es zu zerstören."
Wie können wir schnellere Feedbackschleifen aus unseren Tests fördern? Durch Testautomatisierung. Die Automatisierung hilft uns, Wiederholbarkeit zu erreichen und standardisiert eine Abfolge von Aufgaben (wodurch wir sowohl die Kopplung als auch die Linearität verbessern). Wir wollen die Aufgaben automatisieren, die nicht von menschlicher Kreativität und Anpassung profitieren, wie z. B. das Testen, was auch dazu führt, dass der Code leicht zu ändern ist. Wenn du deine Tests nicht automatisierst, wird jede Iteration des Lernzyklus ins Stocken geraten und viel teurer werden. Testautomatisierung beschleunigt unsere Feedbackschleifen und verringert die Reibung beim Lernen aus unseren Tests.
Leider schreckt die Testautomatisierung manchmal Cybersecurity-Leute ab, die schwerfällige Änderungsprozesse um der "Risikoabdeckung" willen loben (was auch immer das wirklich bedeutet). Das traditionelle Cybersicherheitsteam sollte keine Software testen, da es bereits in den Designprozess eingebunden sein sollte (wie im letzten Kapitel beschrieben) und den Softwareentwicklern bei der Umsetzung dieser Designs vertrauen sollte (wir wollen kein Panoptikum). Wenn wir uns in die Testautomatisierung einmischen und das Testen eng an eine externe Instanz wie ein separates, abgeschottetes Cybersecurity-Team koppeln, gefährden wir die Widerstandsfähigkeit, indem wir den sozialen Teil der Lernfähigkeit des Systems reduzieren.
Aber genug davon, was die Cybersicherheitsbranche derzeit alles falsch macht. Wie machen wir die Testautomatisierung richtig? Sicherheitstestsuiten können automatisch ausgelöst werden, sobald ein PR eingereicht wird, und arbeiten mit Codeüberprüfungen durch andere Entwickler zusammen , um die Effizienz zu steigern (und die Götter des Produktionsdrucks zu befriedigen). Natürlich sind einige herkömmliche Sicherheitstools nicht für automatisierte Workflows geeignet. Wenn ein statisches Analysetool 10 Minuten - oder, wie leider immer noch üblich, ein paar Stunden - für seinen Scan braucht, verstopft es unweigerlich die Pipeline. Im Accelerate State of DevOps Report 2019 wird festgestellt, dass nur 31% der DevOps-Elite automatisierte Sicherheitstests einsetzen, während es bei den Low-Performern nur 15% sind. Sicherheitstestsuiten werden traditionell vom Sicherheitsteam kontrolliert, aber wie wir im Laufe des Buches immer wieder betonen werden, schadet diese Zentralisierung nur unserer Fähigkeit, die Resilienz aufrechtzuerhalten - wie Abbildung 4-2 zeigt.
Wir können die statische Analyse als Beispiel dafür nehmen, wie die Testautomatisierung die Qualität verbessern kann. Abgesehen von den Kosten für die Erstellung des Tests kann die statische Analyse als kostengünstig angesehen werden, wenn sie sich über die Zeit amortisiert, da sie automatisch Fehler in deinem Code aufdeckt. Allerdings gibt es in den Teams der Softwareentwicklung oft eine große Diskrepanz zwischen den Metadaten. Wenn du dein Backlog im Griff hast und dein Design regelmäßig verbesserst, kannst du dir den Luxus leisten, dich um mögliche Sicherheitsmängel zu kümmern und etwas dagegen zu unternehmen. Wenn du unter einem Haufen C-Code erstickst und davon abgehalten wirst, deinen Code zu verbessern, kommen noch mehr Sicherheitsprobleme hinzu, die dich noch mehr verletzen. Das Sicherheitsnetz für Softwareentwicklungsteams, die mit schwer zu wartendem Legacy-Code zu kämpfen haben, besteht aus Tools, die ihnen bei der iterativen Modernisierung und Automatisierung von Prozessen helfen können. Egal, ob es sich um eine Selbstbedienung handelt, ob es von einem Plattform-Engineering-Team bereitgestellt wird oder ob es von einem Sicherheitsteam bereitgestellt wird, das einem Plattform-Engineering-Modell folgt, die Testautomatisierung - neben anderen Automatisierungen, die wir in diesem Kapitel besprochen haben - kann den Teams helfen, sich Stück für Stück aus dem Sumpf zu ziehen.
Aus diesem Grund müssen Sicherheitsprogramme bei der Auswahl von Werkzeugen auch die Perspektive der Softwareentwicklung einbeziehen. Möglicherweise gibt es bereits statische Analysewerkzeuge - oder allgemeinere "Code-Qualitäts"-Werkzeuge -, die CI/CD-freundlich sind, implementiert wurden oder in Erwägung gezogen werden und ausreichen könnten, um auch Fehler mit Sicherheitsauswirkungen zu finden. Die Integration von statischen Analysewerkzeugen in IDEs kann beispielsweise die Zeit reduzieren, die Entwickler/innen mit der Behebung von Schwachstellen verbringen, und die Häufigkeit erhöhen, mit der sie die Sicherheitsanalyse ihres Codes durchführen. Entwickler/innen sind mit solchen Tools bereits vertraut und verlassen sich sogar auf sie, um ihre Arbeitsabläufe zu verbessern. Du hörst vielleicht, wie ein Entwickler von TypeScript schwärmt, einer Sprache, die nur dazu da ist, eine weniger sichere Sprache um eine Typüberprüfung zu erweitern, weil sie ihn produktiver macht. Wenn wir Softwareentwicklungsteams dabei helfen können, produktiver zu sein und gleichzeitig durch schnellere Feedbackschleifen mehr zu lernen, sind wir es wert, dass wir uns selbst ein Lob aussprechen.
Das Warum und Wann dokumentieren
Eine weitere Möglichkeit zur Förderung von Feedbackschleifen und Lernen bei der Entwicklung und Bereitstellung von Systemen ist die Praxis der Dokumentation - insbesondere die Dokumentation des Warum und Wann. Wie wir in Kapitel 1 besprochen haben, ist Resilienz auf das Gedächtnis angewiesen. Wir werden beim Lernen scheitern, wenn wir das relevante Wissen nicht abrufen können. Dieses Wissen muss zugänglich bleiben, damit so viele Menschen wie möglich im sozialen Teil des Systems es in ihren Feedbackschleifen nutzen können. Daher müssen wir die Dokumentation zu einer zentralen Praxis erheben und ihr eine höhere Priorität einräumen als anderen "sexy" Aktivitäten. In diesem Abschnitt wird beschrieben, wie man Dokumentationen entwickelt, um das Lernen zu erleichtern.
Wenn wir Wissen teilen, können wir nicht in statischen Komponenten denken. Erinnere dich: Resilienz ist ein Verb. Wir müssen unser Wissen über das System teilen - nicht nur, wie die Komponenten zusammenwirken, sondern auch warum und wann sie zusammenwirken und warum sie überhaupt existieren. Wir müssen es so behandeln, als würden wir ein Ökosystem beschreiben. Wenn du einen Strand dokumentieren würdest, könntest du beschreiben, was alles dazugehört und wie es funktioniert. Es gibt Sand. Es gibt Wellen, die sich rein und raus bewegen. Es gibt Muscheln. Aber das sagt uns nicht viel. Viel aussagekräftiger ist, wie und wann diese Komponenten zusammenwirken. Wenn um 13:37 Uhr Ebbe ist, ziehen sich Krabben und Seesterne in die Gezeitentümpel zurück; die Gezeitentümpel werden freigelegt, ebenso wie die Austern; Krabben wuseln am Strand entlang, um nach Nahrung zu suchen - wie die leckeren Austern und die Tiere in den Gezeitentümpeln; auch Küstenvögel picken an der Küste nach Leckerbissen. Wenn sechs Stunden später die Flut kommt, öffnen Austern und Jakobsmuscheln ihre Schalen, um zu fressen; Krabben und Seesterne werden ins Meer gespült; Krabben graben sich ein; Küstenvögel schlafen; weibliche Meeresschildkröten kriechen an die Küste, um ihre Eier zu legen - und die Flut wird die Babyschildkröten schließlich ins Meer ziehen.
Unsere Softwaresysteme sind komplex und bestehen aus interagierenden Komponenten. Es sind diese Wechselwirkungen, die Systeme "verwirrend" machen, und deshalb ist es unerlässlich, sie als Konstrukteure zu erfassen. Wir müssen unsere Systeme als einen Lebensraum betrachten, nicht als eine unzusammenhängende Sammlung von Konzepten. Wenn wir einem System zum ersten Mal begegnen, fragen wir uns in der Regel: "Warum wurde es so gebaut?" oder vielleicht noch grundsätzlicher: "Warum gibt es das?" Doch das ist es, was wir bei der Entwicklung und Bereitstellung von Software am wenigsten zu dokumentieren pflegen.
Wir haben das Projekt Oxidation von Mozilla bereits im Zusammenhang mit der Migration zu einer speichersicheren Sprache erwähnt, aber es ist auch ein lobenswertes Beispiel für die Dokumentation des Warum. Für die meisten Komponenten, die sie in Rust ausgeliefert haben, beantworten sie die Frage "Warum Rust?". Bei der Integration des Lokalisierungssystems fluent-rs
haben sie zum Beispiel ausdrücklich dokumentiert, dass sie es in Rust kompiliert haben, weil: "Der Leistungs- und Speichergewinn gegenüber der vorherigen JS-Implementierung ist erheblich. Es ermöglicht ein Zero-Copy-Parsing und ein speicherschonendes Auflösen von Lokalisierungsstrings. Außerdem ebnet es den Weg für die Migration der restlichen Fluent-APIs weg von JS, das für Fission benötigt wird."
Eine solch detaillierte Antwort, die den Zweck und sogar das mentale Modell hinter der Entscheidung angibt, vermeidet in Zukunft geschickt Chestertons Zaunproblem. Aber auch weniger detaillierte Antworten können eine Lernkultur, Feedbackschleifen und vor allem eine Prioritätensetzung unterstützen. Eine der vorgeschlagenen Komponenten, die in Rust "oxidiert" werden sollen - das Ersetzen von DOM-Serialisierern (XML, HTML für Speichern unter..., reiner Text) - besagt zum Beispiel ganz einfach: "Warum Rust? Wir brauchen sowieso eine Neufassung. Geringe Sicherheitslücken in der Vergangenheit." Die Umstellung auf eine speichersichere Sprache kann eine Gelegenheit sein, langjährige Probleme anzugehen, die die Zuverlässigkeit, Widerstandsfähigkeit oder auch nur die Wartbarkeit behindern. Wir sollten immer nach Möglichkeiten suchen, unsere Investitionen zu maximieren, wo wir können.
Sicherheitsanforderungen dokumentieren
Die Dokumentation des Warum und Wann ist ein wichtiger Bestandteil der Optimierung der Aufwandsverteilung in unserem Aufwandsportfolio. Anforderungen definieren die Erwartungen an Qualitäten und Verhaltensweisen, so dass die Teams entscheiden können, wie sie ihr Aufwandskapital investieren, um diese Anforderungen zu erfüllen. Dokumentierte Sicherheitsanforderungen unterstützen die Wiederholbarkeit und Wartbarkeit - wichtige Eigenschaften für Feedback-Schleifen - und verringern gleichzeitig den Aufwand aller Beteiligten für die Ausarbeitung spezifischer Anforderungen für jedes einzelne Projekt.
So investieren Sicherheitsteams oft einen erheblichen Aufwand in die Beantwortung von Ad-hoc-Fragen der Softwareentwicklung, wie ein Produkt, eine Funktion oder ein System so entwickelt werden kann, dass das Sicherheitsprogramm kein Veto einlegen kann. In der Praxis führt dieser manuelle Aufwand zu Rückständen und Engpässen, so dass die Entwicklungsteams "feststecken" und die Sicherheitsteams nur begrenzte Mittel zur Verfügung haben, die sie anderweitig investieren können (z. B. in Aktivitäten, die die Ziele des Sicherheitsprogramms besser erfüllen könnten).
Tipp
Der Accelerate State of DevOps Report 2021 hat ergeben, dass Teams mit einer qualitativ hochwertigen Dokumentation mit 3,8-mal höherer Wahrscheinlichkeit Sicherheitspraktiken umsetzen (und mit 2,4-mal höherer Wahrscheinlichkeit ihre Zuverlässigkeitsziele erreichen oder übertreffen).
Wenn wir explizite Anforderungen definieren und den Entwicklungsteams die Flexibilität geben, ihre Projekte unter Einhaltung dieser Anforderungen zu entwickeln, gewinnen beide Seiten Zeit und Mühe: Die Entwicklungsteams können sich selbst bedienen und starten, ohne ihre Arbeit unterbrechen zu müssen, um mit dem Sicherheitsteam zu diskutieren und zu verhandeln, und das Sicherheitsteam wird nicht mehr so sehr mit Anfragen und Fragen überschwemmt, was Zeit und Mühe für Arbeiten mit größerem Wert freisetzt. Wenn wir eine Dokumentation schreiben, z. B. "So implementieren wir eine Passwortrichtlinie in einem Dienst", investieren wir einen Teil unseres Aufwandskapitals, damit andere Teams mehr Freiheit bei der Verteilung ihres eigenen Aufwandskapitals haben. Sie können auf die Dokumentation zugreifen, verstehen die Anforderungen und müssen nicht ad hoc Fragen zu einmaligen Anforderungen stellen.
Greg Poirier, Principal Software Architect von , hat diesen Ansatz auf CI/CD-Pipelines angewandt und damit die Notwendigkeit eines zentralisierten CI/CD-Systems beseitigt, während er gleichzeitig in der Lage ist, Softwareänderungen zu bestätigen und die Herkunft der Software zu bestimmen. Anstatt strenge Leitplanken aufzustellen, die für alle Entwicklungsteams gleichermaßen gelten, können wir stattdessen die gewünschten Anforderungen in CI/CD-Pipelines definieren (und sie an einem einzigen zugänglichen Ort verfügbar machen). Auf diese Weise können die Entwicklungsteams ihre CI/CD-Pipelines so aufbauen und weiterentwickeln, wie es ihren lokalen Bedürfnissen entspricht, solange sie die Anforderungen erfüllen .
Wir können den Umgang mit Schwachstellen auch durch Wissensaustausch verbessern. Wenn Schwachstellen in standardisierten, gemeinsam genutzten Frameworks und Mustern entdeckt werden, sind sie leichter zu beheben. Wenn Teams von den ausgetretenen Pfaden abweichen und Dinge auf eine seltsame Art und Weise entwickeln, sollten wir mit mehr Schwachstellen rechnen. Um SQL-Injection (SQLi) als Beispiel zu nehmen: Es sollte nicht erst ein Team darunter leiden, dass ein Angreifer eine SQLi-Schwachstelle in seinem Dienst ausnutzt, damit die Organisation parametrisierte Abfragen und ORMs(Object Relational Mappers) entdeckt, die das Schreiben von SQLi-Schwachstellen erschweren. Die Organisation sollte stattdessen ihre Datenbankzugriffsmuster standardisieren und den sicheren Weg zum Standard machen. Wir werden in Kapitel 7 näher auf die Standardeinstellungen eingehen. Wenn ein Entwicklungsteam eine Schwachstelle in seinem Code entdeckt, sollte es die anderen Teams darüber informieren, anstatt nur die eine Schwachstelle zu beheben und weiterzumachen. Das kann zu einem Brainstorming darüber führen, wie das Vorhandensein der Schwachstelle an anderen Stellen überprüft werden kann und welche Strategien es gibt, um die Schwachstelle systemübergreifend zu beheben.
Lernorientierte Dokumente schreiben
Niemand bekommt einen Bonus für das Schreiben guter Dokumente. Deshalb müssen wir es den Menschen leicht machen, Dokumente zu erstellen und zu pflegen, auch wenn das Schreiben von Dokumenten nicht zu ihren Kernkompetenzen oder zu ihren spannendsten Aufgaben gehört. Die beste Ressource, die wir schaffen können, ist eine Vorlage mit einem überprüften Format, dem alle zustimmen und das mit wenig Aufwand ausgefüllt werden kann. Die Vorlage sollte das Minimum widerspiegeln, das für die Weitergabe von Wissen an andere Menschen erforderlich ist. So wird klar, was das Minimum ist, wenn die Dokumentation erstellt wird, und gleichzeitig bleibt sie flexibel, wenn der Mensch mehr hinzufügen möchte.
Manchmal denken Ingenieure, dass gute Dokumentationen für ihr Feature oder ihren Service nur relevant sind, wenn die Nutzer Entwickler sind. Das ist nicht die richtige Denkweise, wenn wir die Wiederholbarkeit unterstützen oder Möglichkeiten bewahren wollen. Was ist, wenn unser Dienst auch von anderen Teams genutzt wird? Was ist, wenn es für unsere Organisation nützlich ist, wenn unser Dienst irgendwann als API verkauft wird? In einer Welt, die zunehmend API-gesteuert ist, gibt uns die Dokumentation diese Flexibilität und stellt sicher, dass unsere Software (einschließlich Firmware oder sogar Hardware) konsumierbar ist. Aus diesem Blickwinkel betrachtet, verbessert die Dokumentation direkt unsere Kennzahlen.
Wenn du ein System baust, interagierst du mit Komponenten, die verschiedene Dinge tun und in unterschiedlichen Beziehungen zueinander stehen. Dokumentiere deine Annahmen über diese Wechselwirkungen. Wenn du ein Sicherheitschaos-Experiment durchführst, lernst du noch mehr über diese Komponenten und Beziehungen. Dokumentiere diese Beobachtungen. Stell dir als Gedankenexperiment vor, ein freundlicher Fremder schenkt dir ein Lotterielos, wenn du dir in einer Arbeitspause deinen Lieblingskaffee holst; du gewinnst im Lotto und beschließt, dir ein Jahr lang eine Auszeit zu nehmen, um alle wunderschönen Inseln der Welt zu bereisen, und kommst dann am Tag 366 an deinen Schreibtisch zurück, um dich wieder in deine Arbeit zu stürzen (vorausgesetzt, es wird dir nie langweilig, an unberührten Stränden zu entspannen und mit exotischer Flora und Fauna zu spielen). Dein Geist ist völlig erfrischt und du hast so gut wie alles vergessen, was du gerade tust. Würden die Unterlagen, die du dir selbst hinterlassen hast, ausreichen, um das System wieder zu verstehen? Würdest du die Vergangenheit verfluchen, weil du keine Annahmen darüber aufgeschrieben hast, wie diese Komponente, die eine Sache tut, mit einer anderen Komponente zusammenhängt, die eine andere Sache tut?
Erkläre Future You, wie du das System aufgebaut hast, wie du glaubst, dass es funktioniert und warum du glaubst, dass es so funktioniert. Vielleicht führst du in Zukunft ein Sicherheitschaos-Experiment durch, das einige dieser Annahmen widerlegt, aber es ist zumindest eine Grundlage, auf der du in Zukunft Hypothesen für Experimente aufstellen kannst. Wie du wahrscheinlich schon vermutet hast, schreiben wir diese Unterlagen nicht nur für deine Zukunft, wenn du Software entwickelst, sondern auch für neue Teammitglieder und solche, die mit dem Teil des Systems, den du geschrieben hast, nicht so vertraut sind. Die Dokumentation kann auch für die Reaktion auf Vorfälle von unschätzbarem Wert sein, worauf wir in Kapitel 6 näher eingehen werden.
Die Dokumente kommen Future You aber auch in anderer Hinsicht zugute: Sie fassen dein Wissen in einem verständlichen Format zusammen, so dass du dich nicht direkt an uns wenden musst und deine Arbeit nicht unterbrochen wird. Das bedeutet, dass wir nicht nur die grundlegenden Fragen in den Dokumenten beantworten, sondern sie auch zugänglich und verdaulich machen müssen (und wenn uns immer wieder dieselbe Frage gestellt wird, ist das eine Aufforderung zum Handeln, die Antwort in das Dokument aufzunehmen). Wer von uns hat sich nicht schon einmal eine novellenartige Doku mit verwirrender Struktur und schlechtem Text angesehen und wollte schon aufgeben? Oder die Doku erklärt bis ins kleinste Detail, wie die Komponente als statische Einheit aufgebaut ist, erklärt aber nicht, warum sie so aufgebaut ist oder wie sie in der Raum-Zeit funktioniert.
Wenn wir nicht beschreiben, wie sich die Komponente zur Laufzeit verhält und wie sie mit Maschinen und Menschen interagiert, erhalten wir nur eine schriftliche Version eines "Stilllebens" der Komponente. Eine visuelle Erklärung ihrer Interaktionen in Raum und Zeit - ein Film statt eines Porträts - kann die Dokumentation für menschliche Augen noch verdaulicher machen. Warum und wann interagieren die Komponenten? Wann und wo fließen die Daten? Warum gibt es eine bestimmte zeitliche Abfolge? Wenn du sicherstellst, dass diese visuelle Erklärung - ob als Diagramm, Gif, Entscheidungsbaum oder in einem anderen Format - leicht zu ändern (und zu versionieren) ist, bleibt das Wissen aktuell, wenn sich die Bedingungen weiterentwickeln und Feedback gesammelt wird. So kann zum Beispiel eine README-Datei versioniert und von einem einzelnen Ingenieur entkoppelt werden. So kannst du einen CI/CD-Prozess mit einer visuellen und schriftlichen Erklärung festhalten, warum jeder Schritt abläuft und warum es bei jedem Schritt Wechselwirkungen gibt.
Wie wir immer wieder betonen werden, ist es viel wichtiger zu erklären , warum sich das System auf eine bestimmte Art und Weise verhält und warum wir uns entschieden haben, es so zu bauen, als wie es sich verhält. Wenn es unser Ziel ist, dass ein brandneues Team schnell mit einem System arbeiten kann und versteht, wie man es pflegt und erweitert, dann wird die Erklärung, warum die Dinge so sind, wie sie sind, Wissenslücken viel schneller schließen als das Wie. Das Warum ist der Motor unseres Lernens, und das ständige Hinterfragen dieser Annahmen belebt unsere Feedbackschleifen.
Wir wollen Softwarekomponenten, die gut verstanden und gut dokumentiert sind, denn wenn wir unser eigenes Wissen über das System teilen, können wir leichter erklären, warum wir etwas mit diesen Komponenten gebaut haben und warum es so funktioniert, wie es funktioniert. Wenn wir unsere eigene Softwarekomponente bauen, ist es zwar einfacher, sie mental zu modellieren, aber schwieriger, dieses mentale Modell mit anderen zu teilen und durch Feedback zu pflegen. Es macht es auch schwieriger, Komponenten auszutauschen; der Endowment-Effekt33 (eine Unterart der Verlustaversion)34 bedeutet, dass wir unsere "Lieblinge" nie wegwerfen wollen. Wir wollen die Dinge nicht zu einem verworrenen, eng gekoppelten Durcheinander zusammenkleben, bei dem sowohl das Warum als auch das Wie schwer zu durchschauen sind. Wenn wir den Bankrott eines mentalen Modells erklären und blind vertrauen, verderben wir unseren Resilienztrank; wir verstehen die kritischen Funktionen der Systeme nicht, kennen die Sicherheitsgrenzen nicht (und stoßen an sie heran), sind verwirrt von raumzeitlichen Interaktionen und können weder lernen noch uns anpassen.
Verteiltes Tracing und Logging
Die dritte Praxis, die wir besprechen und die in dieser Phase Feedbackschleifen und Lernprozesse fördern kann, ist das verteilte Tracing und Logging. Es ist schwierig, nur die kleinen Brotkrümel zu betrachten, die das System verteilt und die nicht zu einer Geschichte zusammengefügt werden (und Menschen denken sehr stark in Geschichten). Egal, ob du einen Vorfall bearbeitest oder dein mentales Modell verfeinerst, um Verbesserungen vorzunehmen, die Beobachtung der Interaktionen über einen längeren Zeitraum ist unerlässlich. Du kannst keine Feedbackschleife bilden, ohne zu sehen, was vor sich geht; das Feedback ist ein zentraler Bestandteil der Schleife.
Wir sollten dieses Feedback einplanen und durch Rückverfolgung und Protokollierung in unsere Dienste integrieren. Beides ist nichts, was man nachträglich einbauen oder automatisch auf alle Dienste anwenden kann, die man betreibt. Du investierst in der Erstellungs- und Lieferphase und erhältst dann in der Beobachtungs- und Betriebsphase eine Rendite auf diese Investition. Du kannst dich aber auch dafür entscheiden, in dieser Phase keinen Aufwand zu betreiben, und dir die Haare raufen, wenn du versuchst, dein kompliziertes Microservice-System zu debuggen, wenn es fehlschlägt, indem du rätst, welche Log-Meldungen mit welchen übereinstimmen (was bei Diensten mit einem vernünftigen Volumen unglaublich mühsam ist). Wir können Tracing und Logging als eine Absicherung gegen einen schwerwiegenden Abschwung betrachten, wenn unsere Software in der Produktion läuft - ein Feedback, das uns hilft, eine produktive Schleife aufrechtzuerhalten, anstatt eine Abwärtsspirale in Gang zu setzen. In diesem Abschnitt werden wir untersuchen, wie wir in dieser Phase über beide Aspekte nachdenken können.
Verteiltes Tracing zur Verfolgung von Datenflüssen
Verteiltes Tracing ist ein Mechanismus zur Beobachtung des Datenflusses in einem verteilten System.35 Verteiltes Tracing gibt uns eine Zeitleiste der Logs und des Datenflusses zwischen den Systemen, eine Möglichkeit, die Interaktionen über die Raum-Zeit hinweg zu verstehen. So kannst du einzelne Vorgänge bis zum ursprünglichen Ereignis zurückverfolgen. Nehmen wir als Analogie eine Partnerschaft mit einem anderen Unternehmen: Für jede Produkt- oder Funktionsanforderung gibt es eine Ticket-ID. Jede Aktivität, die intern mit der Anfrage zusammenhängt, erhält ebenfalls diese Ticket-ID, so dass du weißt, wie du sie in Rechnung stellen kannst (und die damit verbundene Arbeit verfolgen kannst). Verteiltes Tracing funktioniert nach demselben Prinzip: Eine eingehende Anfrage wird mit einer Trace-ID versehen, die in den Protokollen der einzelnen Dienste auftaucht, wenn sie durchläuft.
Nehmen wir einen Fall an, in dem ein Angreifer Daten aus dem Patientenportal eines Krankenhauses ausspäht. Wir können sehen, dass Daten exfiltriert werden - aber wie geschieht das? Es gibt einen Frontend-Dienst, der für die Anzeige des Dashboards verantwortlich ist, das der Patient sieht, wenn er sich anmeldet (der Patientenportal-Dienst). Der Patientenportal-Dienst muss Daten von anderen Diensten abrufen, die von anderen Teams verwaltet werden, z. B. aktuelle Laborberichte vom Labor-Dienst, die Überprüfung des Login-Tokens vom Token-Dienst und die Abfrage der Liste der anstehenden Termine vom Terminplan-Dienst. Das Frontend stellt eine einzige Anfrage an den Patientenportal-Dienst, der wiederum Anfragen an all diese anderen Dienste stellt. Vielleicht sind die Laborberichte gemischt aus internen und ausgelagerten Laborarbeiten. Der interne Dienst kann direkt aus der internen Datenbank lesen und die Benutzer-IDs ordnungsgemäß überprüfen. Um die Laborberichte der Partner aufzunehmen, muss der Labs-Dienst jedoch den Laborbericht-Integrationsdienst des Partners abfragen. Selbst in diesem einfachen Szenario gibt es drei Dienste.
Nehmen wir an, das Team des Partner-Labordienstes stellt fest, dass es einen Fehler gemacht hat (z. B. versehentlich eine Sicherheitslücke eingebaut) und ein Angreifer Daten ausspioniert. Sie könnten vielleicht sagen, welche Daten gesendet werden, aber sie wären nicht in der Lage, die Datenströme zurückzuverfolgen, ohne alle Anfragen zu verstehen, die vom Labordienst kommen - und sie müssten sie bis zu allen Anfragen verfolgen, die vom Patientenportal-Dienst kommen. Das ist ein Albtraum, denn es ist unklar, welche Vorgänge (oder Ereignisse) überhaupt eine Anfrage an den Partnerdienst für Laborergebnisse stellen könnten, ganz zu schweigen davon, welche Anfragen vom Angreifer und welche von einem legitimen Nutzer stammen. Der gesamte Datenverkehr, der in diesen Dienst fließt, kommt aus dem Unternehmen, von den Peer-Teams, aber dieser Datenverkehr steht in Verbindung mit einem Benutzervorgang, der von außerhalb des Unternehmens kommt (z. B. ein Patient, der auf sein Dashboard klickt, um die letzten Laborergebnisse einzusehen).
Verteiltes Tracing beseitigt diesen Albtraum, indem es am Ingress des Datenverkehrs eine Trace-ID zuweist, die das Ereignis auf seinem Weg durch das System verfolgt. Auf diese Weise kann der Partnerdienst für Laborergebnisse nachsehen, wo die Trace-ID in den Protokollen anderer Dienste auftaucht, um den Weg des Ereignisses durch das System zu bestimmen.
Verteiltes Tracing hilft uns nicht nur, Systeminteraktionen über die Raum-Zeit hinweg zu beobachten, sondern auch, das Systemdesign zu verfeinern und neue Versionen zu entwerfen - eine elegante Feedbackschleife. Auf Unternehmensebene hast du keinen vollständigen Überblick darüber, was die Teams, die deine Daten nutzen und auf deinen Dienst zugreifen, damit machen. Deren Vorfälle können leicht zu deinen Vorfällen werden. Wenn du das Design deines Systems verfeinerst, solltest du wissen, wie es sich auf deinen Kundenstamm auswirkt. Je mehr Partner und Verbraucher in die Kette eingeflochten sind, desto schwieriger ist es, die Kette zu verstehen. Du hast ein mentales Modell davon, wie Ereignisse durch das System fließen und wie dein bestimmter Teil des Systems mit anderen Teilen interagiert - aber wie genau ist dein mentales Modell?
Verteiltes Tracing hilft dir, dieses mentale Modell zu verfeinern, indem du mehr über die realen Interaktionen in deinem System und zwischen den Diensten erfährst. Wir können verteiltes Tracing nutzen, um Kapazitäten zu planen, Fehler zu beheben, Kunden über Ausfallzeiten und API-Änderungen zu informieren und vieles mehr. Es ist wichtig zu betonen, dass der Wert der verteilten Rückverfolgung erst dann zum Tragen kommt, wenn die Software in der Produktion läuft; wir müssen jedoch bereits in der Entwicklungsphase viel investieren, um diesen Wert zu erreichen. Verteiltes Tracing bedeutet im Wesentlichen, dass wir in der Lage sein wollen, Daten systemübergreifend zu korrelieren - dass wir diese Trace-ID wollen. In der Entwicklungsphase musst du die Entscheidung treffen, dass du diese Fähigkeit im System haben willst, auch wenn ein Großteil des Wertes erst in der nächsten Phase, dem Betrieb und der Beobachtung, entsteht.
Wenn du den Rat befolgst, deine Systeme lose zu koppeln und sie über logische Grenzen hinweg aufzuteilen, kann es zu Problemen mit der Sichtbarkeit kommen und es kann schwieriger werden, den Fluss zu erkennen - selbst wenn dieser Fluss jetzt belastbarer ist. Genau das soll das verteilte Tracing aufdecken. Es ist nicht schick, aber unbestreitbar nützlich, um eine Feedbackschleife zu betreiben.
Entscheiden, wie und was protokolliert werden soll
Logging hilft uns, etwas über das Systemverhalten zu lernen; Wenn wir Logging-Anweisungen in den Code einfügen, während er geschrieben wird, säen wir Setzlinge, um unsere Feedbackschleifen zu stimulieren. Logging-Anweisungen erzeugen eine Aufzeichnung des Systemverhaltens, die wir als Logs bezeichnen. Wenn wir feststellen, dass wir Informationen über das System (oder einen Teil des Systems) benötigen, um eine neue Funktion hinzuzufügen, ein Problem (z. B. einen Fehler) zu beheben oder die Kapazität zu erweitern, brauchen wir Logging-Anweisungen, um diese Informationen für die Feedbackschleife bereitzustellen. Softwareentwickler bauen manchmal sogar eine neue Version des Systems mit neuen Protokollierungsanweisungen, nur um diese Informationen zu erhalten. Bei einem Zwischenfall kann ein Team der Softwareentwicklung zum Beispiel eine Version mit einer neuen logger.log
erstellen, um einen Blick in das System zu werfen und herauszufinden, was mit der verblüffenden Überraschung passiert ist. Die meisten Softwareentwickler wissen, wie man Logging-Statements hinzufügt, daher werden wir in diesem Abschnitt nicht auf diese Details eingehen. Es lohnt sich jedoch, alle Beteiligten daran zu erinnern, was wir protokollieren sollten und wie wir über die Protokollierung denken sollten.
Tipp
Blöcke sind die Konstrukte, die Entwickler beim Hinzufügen von Logging-Anweisungen verwenden. Blöcke sind die organisatorische Struktur des Codes. In Python zum Beispiel spiegeln die Einrückungsebenen - wie der Inhalt der Funktion - einen Block wider. Wenn du Einrückungen innerhalb der Funktion hast, gibt es einen Unterblock für die true
Bedingung und einen Unterblock für den else
Teil (falls es einen gibt). Grundsätzlich öffnet jeder der Kontrollflussmechanismen einen eigenen Block. Wenn du ein for loop
oder ein while loop
hast, erhältst du einen Block.
Ein Block ist so etwas wie ein Absatz. In der Compiler- und Reverse-Engineering-Welt ist ein Basisblock die Unterstruktur, die immer von oben nach unten ausgeführt wird. Eine Anweisung ist das Äquivalent zu einem Satz - eine Zeile innerhalb eines Blocks. Ein Ausdruck bezieht sich auf einen Teil der Anweisung, der separat ausgewertet wird. Und eine Klausel bezieht sich auf das Prädikat in einer if
oder einer while
Anweisung.
Wir wissen vielleicht nicht, was wir protokollieren müssen, bis wir anfangen, die Daten zu interpretieren, die entstehen, wenn unser Code tatsächlich läuft. Wir müssen ein wenig spekulieren, was nützlich sein könnte, um sich von einem zukünftigen Vorfall zu erholen, um das Wachstum des Datenverkehrs zu informieren, um zu wissen, wie effektiv unsere Caches sind, oder eines der tausend anderen Dinge, die relevant sind. Wenn wir beim Schreiben unseres Codes Logging-Anweisungen hinzufügen, wollen wir die Möglichkeiten erhalten, wenn unser Code in der Produktion läuft und eine Feedback-Schleife antreibt. Die Computerwissenschaftler Li et al. beschreiben den Kompromiss zwischen Sparsamkeit und Ausführlichkeit: "Einerseits kann eine zu spärliche Protokollierung den Wartungsaufwand erhöhen, weil wichtige Informationen zur Systemausführung fehlen. Andererseits kann eine zu ausführliche Protokollierung dazu führen, dass die wirklichen Probleme verschleiert werden und ein erheblicher Leistungsmehraufwand entsteht."36
Warnung
Es sollte selbstverständlich sein, aber wir wollen keine Passwörter, Token, Schlüssel, Geheimnisse oder andere sensible Informationen in unseren Protokollen haben. Wenn du zum Beispiel ein Finanzdienstleistungs- oder Fintech-Unternehmen bist, das mit einer Vielzahl sensibler personenbezogener Daten umgeht, stellen diese sensiblen Daten - ob Namen, E-Mail-Adressen, nationale Identifikatoren (wie Sozialversicherungsnummern) oder Telefonnummern -, die in deinen Protokollen auftauchen, ein Datenleck dar, das zu problematischen Ergebnissen führen kann.
Im Allgemeinen gibt es selten einen Grund, warum PII protokolliert werden müssen, anstatt eine Datenbankkennung zu verwenden. Ein Protokoll mit der Beschreibung "Es gibt ein Problem mit Charles Kinbote, charles@zembia.gov, Datenbankkennung 999" kann ohne Verlust des Nutzens durch "Es gibt ein Problem mit der Benutzerdatenbankkennung 999" ersetzt werden. Der untersuchende Ingenieur kann authentifizierte Systeme nutzen, um mehr Informationen über den betroffenen Benutzer oder Datenbankeintrag zu erhalten - ohne Gefahr zu laufen, sensible Daten preiszugeben.
Der Sinn von Logs ist es, Feedbackschleifen zu informieren - nicht so viel Lärm zu machen, dass es niemandem hilft, oder so sparsam zu sein, dass es auch niemandem hilft. Wir loggen, um zu lernen. Wenn der Erfolg oder Misserfolg von etwas für dein Unternehmen wichtig ist, solltest du es protokollieren. Wir müssen über die Funktionsweise des Systems nachdenken und sicherstellen, dass sie sich in unseren Logging- und Observability-Tools sinnvoll widerspiegelt. Was du protokollieren solltest, hängt vom lokalen Kontext ab. Die am ehesten verallgemeinerbare Logging-Weisheit ist, dass du Fehler in deinem System protokollieren musst, wenn du die Möglichkeit haben willst, sie aufzudecken. Wenn deine Datenbanktransaktion eine Zeitüberschreitung aufweist, kann das bedeuten, dass die Daten nicht gespeichert wurden. Diese Art von Ereignis solltest du nicht in einem leeren Catch-Block ignorieren, sondern zumindest auf der Ebene ERROR kategorisieren.
Entscheidend ist, dass wir sicherstellen, dass Fehler - und ihr Kontext - von einem Menschen bewertet werden. Oft gibt es eine Flut von Logging-Statements, die in einen Eimer (oder ein schwarzes Loch, je nachdem, wen du fragst) fließen, um sie später abzufragen, falls du sie brauchst. Wir gehen vielleicht davon aus, dass Fehler irgendwann im Posteingang oder im Benachrichtigungsstrom landen, aber das ist vielleicht nicht der Fall - daher können Chaos-Experimente dieses erwartete Verhalten überprüfen. Einer der besten Anhaltspunkte für Chaos-Experimente ist die Überprüfung, ob sich deine Logging-Pipelines (oder Alerting-Pipelines) so verhalten, wie du es erwartest. Mehr zu diesem konkreten Anwendungsfall für Experimente erfährst du im "Erfahrungsbericht: Sicherheitsüberwachung (OpenDoor)".
Tipp
Die Log-Levels geben die Wichtigkeit der Meldung an; FATAL ("Kritisch" unter Windows) ist besonders bedrohlich, während INFO ("Informativ" unter Windows) weniger unheilverkündend ist. Wenn Softwareentwickler/innen Log-Levels festlegen, basieren sie auf ihren mentalen Modellen darüber, wie wichtig dieses Verhalten für das Verständnis des Systems ist (ob zur Fehlerbehebung oder zur Verbesserung). Das macht die Entscheidung, welche Stufe angewendet werden soll, subjektiv und daher schwierig.
Wir müssen überlegen, wo wir lokalen Kontext in die Logmeldungen einbinden sollten: z. B. die zugehörigen Benutzer-ID-Anfragen, Trace-IDs, ob der Benutzer eingeloggt ist oder nicht und mehr, je nach lokalem Kontext. Wenn du ein Transaktionsverarbeitungssystem aufbaust, kannst du vielleicht jede Transaktion mit einer ID verknüpfen, so dass du, wenn eine bestimmte Transaktion fehlschlägt, die ID zur Fehlersuche und Untersuchung verwenden kannst.
Ein letzter Hinweis: Die Technikteams unterhalten bereits eine Infrastruktur für die Protokollierung, so dass es für das Sicherheitsteam keinen Grund gibt, eine parallele Infrastruktur aufzubauen. Stattdessen sollten die Sicherheitsteams darauf bestehen, dass ihre Anbieter mit dieser bestehenden Infrastruktur zusammenarbeiten. Es gibt keinen Grund, das Rad neu zu erfinden - denk daran, dass wir uns für "langweilig" entscheiden wollen - und wenn Sicherheitsteams dieses Schattenreich doppelter Infrastruktur schaffen, unterbricht das deine Fähigkeit zu lernen - eine wichtige Zutat in unserem Resilienztrank.
Verfeinerung der menschlichen Interaktion mit den Build- und Delivery-Praktiken
Schließlich können wir die Art und Weise, wie Menschen mit unseren Entwicklungspraktiken interagieren, verfeinern - eine weitere Möglichkeit, Feedbackschleifen zu stärken und eine Lernkultur zu fördern. Um Softwaresysteme zu entwickeln und zu liefern, die widerstandsfähig sind, müssen unsere Praktiken in dieser Phase nachhaltig sein. Wir müssen ständig lernen, wie die Menschen in unseren soziotechnischen Systemen mit den Praktiken, Mustern und Werkzeugen interagieren, die es ihnen ermöglichen, Systeme zu entwickeln und bereitzustellen. Wir müssen offen dafür sein, neue IDEs, Software-Entwurfsmuster, CLI-Tools, Automatisierung, Pairing, Problemmanagementpraktiken und all die anderen Dinge auszuprobieren, die in dieser Phase eine Rolle spielen.
Zu diesem Lernmodus gehört auch, offen dafür zu sein, dass der Status quo nicht funktioniert - und auf das Feedback zu hören, dass die Dinge besser sein könnten. Wir müssen bereit sein, unsere alten Praktiken, Muster und Werkzeuge zu verwerfen, wenn sie uns nicht mehr helfen oder den Aufbau eines widerstandsfähigen oder zuverlässigen Systems erschweren. Das Erinnern an den lokalen Kontext hilft uns auch dabei, die Arbeitsweise in dieser Phase zu verfeinern; manche Projekte erfordern andere Vorgehensweisen und wir müssen uns entscheiden, sie entsprechend anzupassen.
Zusammenfassend lässt sich sagen, dass wir vier Möglichkeiten haben, um Feedbackschleifen zu fördern und das Lernen - die vierte Zutat unseres Resilienzrezepts - zu unterstützen: Testautomatisierung, Dokumentation des Warum und Wann, verteiltes Tracing und Logging sowie die Verfeinerung der menschlichen Interaktion mit den Entwicklungsprozessen. Wie wir diese Interaktionen verändern - und wie wir alles in dieser Phase verändern - bringt uns zur letzten Zutat unseres Resilienz-Tranks: Flexibilität und Bereitschaft zur Veränderung.
Flexibilität und Bereitschaft zur Veränderung
Nachdem wir diese vier Zutaten nun in unser heißes und schokoladiges Gebräu gerührt haben, können wir darüber diskutieren, wie wir die letzte Zutat, den Marshmallow - das Symbol für Flexibilität und Veränderungsbereitschaft - in unseren Resilienztrank geben, den wir beim Aufbau und bei der Umsetzung von Projekten brauen können. In diesem Abschnitt wird beschrieben, wie wir Systeme bauen und bereitstellen können, damit wir angesichts von Fehlschlägen und sich verändernden Bedingungen, die sonst den Erfolg zunichte machen würden, flexibel bleiben können. Der Forscher für verteilte Systeme, Martin Kleppmann, sagte: "Agilität bei Produkten und Prozessen bedeutet auch, dass du die Freiheit brauchst, deine Meinung über die Struktur deines Codes und deiner Daten zu ändern", und das passt perfekt zur letzten Zutat unseres Zaubertranks.
Für einige Unternehmen mit vielen "klassischen" Anwendungen bedeutet Veränderungsbereitschaft, dass sie bereit sind, über viele Quartale, wenn nicht sogar Jahre, Iterationen und Migrationen durchzuführen, um ihre Anwendungen und Dienste in anpassungsfähigere, veränderbare Versionen zu verwandeln. Ein Tech-Startup, das sich in der Gründungsphase befindet, fängt bei Null an und Veränderungen können über Nacht geschehen. Ein jahrhundertealtes Unternehmen mit Großrechnern und älteren Sprachen braucht wohl noch mehr Flexibilität und Veränderungsbereitschaft, da es bereits mit einer brüchigen Grundlage beginnt, aber diese Veränderung kann nicht über Nacht erfolgen. Die Natur ist ein geduldiger Baumeister, der die Evolution über Generationen hinweg vollzieht. Die Migration von einem klassischen, eng gekoppelten Paradigma zu einem modernen, lose gekoppelten Paradigma erfordert ebenfalls Geduld und eine sorgfältig geplante Entwicklung. Auf dem Weg dorthin gibt es schnelle Erfolge, aber die Vorteile der Widerstandsfähigkeit nehmen mit jeder Iteration zu. Nichts von dem, was wir in diesem Buch beschreiben, ist selbst für die mainframe- und COBOL-lastigsten Unternehmen unerreichbar. Was es braucht, ist eine sorgfältige Bewertung deines Aufwandsportfolios und eine Priorisierung der Resilienzkomponenten, die du als Erstes verfolgen willst.
In diesem Abschnitt stellen wir fünf Praktiken und Möglichkeiten vor, mit denen Flexibilität und Veränderungsbereitschaft in dieser Phase gedeihen können: Iteration, Modularität, Feature-Flags, Erhaltung von Refactoring-Möglichkeiten und das Strangler-Fig-Muster. Viele dieser Strategien fördern die Evolution und verflechten die Bereitschaft zur Veränderung mit dem Design - und fördern so die Geschwindigkeit, von der unsere anmutige Anpassungsfähigkeit abhängt.
Iteration zur Nachahmung der Evolution
Die erste Praxis, die wir anwenden können, um die Flexibilität zu fördern und die Bereitschaft zur Veränderung zu erhalten, ist die Iteration. Eine erste Annäherung an das, was "guten Code" ausmacht, ist Code, der leicht zu ersetzen ist. Er hilft uns, die Flexibilität und Veränderungsbereitschaft zu fördern, die für die Widerstandsfähigkeit von Systemen unerlässlich sind, indem er uns ermöglicht, den Code zu ändern und zu überarbeiten, wenn wir Feedback erhalten und sich die Bedingungen ändern. Code, der leicht zu ersetzen ist, lässt sich leicht patchen. Sicherheitsteams raten Softwareentwicklern oft, Sicherheitsprobleme im Code auf einer "grundlegenderen" Ebene zu beheben, anstatt sie mit einem Pflaster zu überdecken.
Ein iterativer Ansatz bei der Entwicklung und Bereitstellung von Systemen ermöglicht die Entwicklungsfähigkeit, die wir brauchen, um die Widerstandsfähigkeit von Systemen zu unterstützen. Minimale lebensfähige Produkte (MVPs) und das Ausprobieren von Funktionen sind in dieser Phase unsere besten Freunde. Sie verkürzen nicht nur die Zeit bis zur Markteinführung des Codes - so erreichen wir die Endnutzer schneller -, sondern ermöglichen es uns auch, schneller festzustellen, was funktioniert und was nicht, um der Falle der Starrheit zu entgehen (die die Widerstandsfähigkeit untergräbt). Auf diese Weise können wir nicht nur eine lockerere Kopplung erreichen, sondern auch die einfachen Substitutionen, die für linearere Systeme charakteristisch sind. Wir müssen das Experimentieren fördern, indem wir es einfach machen, schnell zu innovieren, aber das, was nicht funktioniert, ohne Scham oder Schuldzuweisung zu verwerfen.
Wir müssen unsere MVPs und Experimente auch zu Ende bringen. Wenn du zum Beispiel ein neues Authentifizierungsmuster entwickelst, das besser ist als der Status quo, solltest du es zu Ende bringen - vom MVP zum echten Produkt. Es ist leicht, den Dampf zu verlieren, nachdem ein Prototyp in einem Teil des Systems funktioniert hat, aber wir müssen dranbleiben, wenn wir widerstandsfähig sein wollen. Wenn wir es nicht durchziehen oder in die Pflege investieren, schrumpft das System und wird brüchig. Diese Konsequenz ist auch dann notwendig, wenn unsere Experimente nicht so ausfallen, wie wir gehofft haben. Wenn sich herausstellt, dass das Experiment nicht durchführbar ist, müssen wir hinter uns aufräumen und das Experiment aus der Codebasis entfernen. Die Überreste fehlgeschlagener Experimente werden die Codebasis verstopfen und jeden verwirren, der über sie stolpert. (Das verblüffende Scheitern von Knight Capital im Jahr 2014 ist wohl ein Beispiel dafür).
Leider schlägt der schrittweise Ansatz beim Aufbau und bei der Umsetzung oft aus sozialen Gründen fehl. Wir Menschen lieben das Neue. Wir lieben es oft, einen großen Erfolg auf einmal zu erzielen , anstatt eine Reihe kleinerer Erfolge im Laufe der Zeit. Natürlich bedeutet diese Vorliebe für Neues und auffällige Neuerungen, dass wir den inkrementellen Fortschritt und damit unsere Fähigkeit, widerstandsfähig zu bleiben, opfern. Es ist viel schwieriger, eine Software weiterzuentwickeln, die nur einmal im Quartal ein "Big Bang"-Releases bekommt, als eine Software, die jeden Tag oder jede Woche auf Abruf bereitgestellt wird. Wenn eine neue Sicherheitslücke auftritt, kann mit dem inkrementellen Ansatz schnell ein Patch veröffentlicht werden, während das Modell mit großen, aufsehenerregenden Releases sowohl aus technischen als auch aus sozialen Gründen langsamer sein wird.
Wie können wir die Dinge im iterativen Modell frisch halten? Chaos-Experimente, egal ob im Bereich der Sicherheit oder der Leistung, können das Adrenalin in die Höhe treiben und eine neue Perspektive bieten, die Ingenieure dazu bringt, ihren Code und ihre Software durch eine andere Brille zu sehen. Wir könnten zum Beispiel die Architektur und den Code eines Systems analysieren, um seine Leistung zu verstehen, aber ein effektiverer Ansatz ist das Anbringen eines Profilers, während wir die Last simulieren; das Werkzeug wird uns genau sagen, wo das System seine Zeit verbringt. Wir können auch Anreize schaffen, damit die Leute mitmachen, neugierig sind und sich den Code zu eigen machen, wie zum Beispiel: "Dieses Modul gehört dir persönlich."
Ein iterativer Ansatz steht auch im Einklang mit der Modularität im Design, die wir als Nächstes behandeln werden.
Modularität: Das uralte Werkzeug der Menschheit für Resilienz
Die zweite Möglichkeit, die uns zur Verfügung steht , um Flexibilität zu kultivieren und Anpassungsfähigkeit zu erhalten, ist Modularität. Laut dem U.S. National Park Service (NPS) ermöglicht die Modularität in komplexen Systemen, "dass strukturell oder funktional unterschiedliche Teile während einer Stressphase ihre Autonomie behalten und sich nach einem Verlust leichter erholen können". Es handelt sich um eine Systemeigenschaft, die das Ausmaß widerspiegelt, in dem Systemkomponenten - die normalerweise in einem Netzwerk dicht miteinander verbunden sind37-in getrennte Cluster (manchmal auch als "Gemeinschaften" bezeichnet) aufgeteilt werden können.38
Wir denken bei Modulen vielleicht an Software, aber die Menschen haben schon vor Jahrtausenden intuitiv verstanden, wie Modularität die Widerstandsfähigkeit soziotechnischer Systeme unterstützt. Im alten Palästina wurden auf modularen Steinterrassen Olivenbäume, Weinreben und andere Produkte angebaut.39 Die Angelsachsen setzten Drei-Felder-Systeme ein, bei denen die Feldfrüchte abwechselnd angebaut wurden - eine Strategie, die im ersten Jahrtausend v. Chr. in China eingeführt wurde.40 Aus diesem Grund beschreibt der NPS die Modularität als "eine menschliche Reaktion auf Ressourcenknappheit oder Stressfaktoren, die wirtschaftliche Aktivitäten bedrohen". Modularität ist in der Geschichte der Menschheit verankert, und mit ihr können wir auch eine widerstandsfähige Zukunft gestalten.
Im Kontext von Kulturlandschaften - einer natürlichen Landschaft, die von einer Kulturgruppe geformt wurde - verbessert das Vorhandensein von modularen Einheiten (wie Landnutzungsflächen) oder Merkmalen (wie Obstgärten oder Felder) die Widerstandsfähigkeit gegenüber Stress. Bei einer Störung kann eine modulare Einheit oder ein Merkmal unabhängig vom Rest der Landschaft oder von anderen modularen Merkmalen bestehen bleiben oder funktionieren. Es bietet eine lockerere Kopplung, die den Ansteckungseffekt unterdrückt. Der Einzweckcharakter der Module führt auch zu einer Linearität - eine Möglichkeit, die Landschaft "lesbarer" zu machen, ohne die Homogenität des eng gekoppelten Normalbaums, den wir in Kapitel 3 besprochen haben.
An der John Muir National Historic Site gibt es zum Beispiel mehrere Blöcke mit Bäumen verschiedener Arten und Sorten, die die Widerstandsfähigkeit gegen Frost fördern, wie in Abbildung 4-3 dargestellt. Dieses clevere Design stellt sicher, dass auch dann noch Früchte geerntet werden können, wenn Spätfröste einige der blühenden Bäume beschädigen. Diese Widerstandsfähigkeit ging auch nicht auf Kosten der Effizienz, sondern steigerte sie sogar. Der NPS schreibt: "Das historische System der Obstplantagen an der John Muir National Historic Site wurde als modulare Einheiten von Artenblöcken mit gemischten Sorten gepflanzt, um die Effizienz des Betriebs zu steigern und gleichzeitig die Widerstandsfähigkeit des Systems zu erhöhen."
Ob Kulturlandschaften oder Softwarelandschaften, wenn die Modularität gering ist, kommt es zu Fehlerkaskaden. Eine geringe Modularität führt zu Ansteckungseffekten, bei denen ein Stressfaktor oder eine Überraschung in einer Komponente zum Ausfall des gesamten Systems führen kann. Ein System mit hoher Modularität hingegen kann diese Stressfaktoren und Überraschungen eindämmen oder "puffern", damit sie nicht von einer Komponente auf die anderen übergreifen. Aufgrund dieses Vorteils kann die Modularität als "Maß für die Stärke der Aufteilung eines Systems in Gruppen von Gemeinschaften bezeichnet werden und steht im Zusammenhang mit dem Grad der Konnektivität innerhalb eines Systems."41
Eine größere Modularität kann zum Beispiel die Ausbreitung von Infektionskrankheiten verlangsamen - genau die Theorie, die hinter der sozialen Distanzierung und insbesondere den "COVID-Blasen" steht, bei denen eine Gruppe von weniger als 10 Menschen zusammenbleibt, aber ansonsten die Interaktion mit anderen Gruppen minimiert. Andere Beispiele für Modularität in unserem Alltag sind die Quarantäne an Flughäfen, um invasive Wildtiere oder Epidemien zu verhindern, und Feuerschneisen - Lücken in brennbarem Material -, die die Ausbreitung von Waldbränden verhindern.42
Auch wenn unsere Software-"Arten" - getrennte Dienste oder Anwendungen mit einem einzigartigen Zweck - selten die gleiche Funktion erfüllen43 (wie Bäume, die Obst produzieren), können wir dennoch von der Modularität profitieren. Um die Analogie zum Obstgarten zu erweitern: Die gemeinsame Bewässerungs- und Wartungsarbeit für alle Bäume im Obstgarten ist vergleichbar mit der gemeinsamen Infrastruktur in unseren Software-"Obstgärten" wie Protokollierung, Überwachung und Orchestrierung. Modularität kann sogar unsere kritischen Funktionen verfeinern. Ein Werbetechnologieunternehmen könnte doppelte Dienste entwickeln, die 95% des Verhaltens teilen, aber kleine, kritische Teile haben, um Strategien zur Nutzersegmentierung gegeneinander auszuspielen.
Hinweis
In der soziotechnischen Dimension unserer Softwaresysteme werden in einem Rausch neue Funktionen zu einem System hinzugefügt, dann stabilisiert sich das System, während wir die Auswirkungen unserer Änderungen betrachten. Die Tatsache, dass Funktionen als Alpha, Beta, begrenzte Verfügbarkeit oder GA bezeichnet werden, spiegelt dies wider. Wir können uns das wie einen "Einatmen, Ausatmen"-Zyklus für Softwareprojekte vorstellen (oder einen "Tick-Tock"-Zyklus in der inzwischen veralteten Intel-Architektur-Metapher).
Module sorgen oft für mehr Linearität und ermöglichen eine grundlegende Kapselung und Trennung von Belangen. Außerdem schaffen sie eine lokale Grenze, an der wir später die Isolation einführen können. Auf einer lokaleren Ebene dient die Modularität der Organisation, um die Navigation und Aktualisierung des Systems zu erleichtern und eine gewisse logische Linearität zu schaffen (bei der die Daten in eine Richtung fließen, aber Rückstau und Fehler die vollständige Linearität unterbrechen) - selbst wenn die Module nicht isoliert sind.
Wenn Modularität richtig gemacht wird, unterstützt sie direkt die lose Kopplung: Sie sorgt dafür, dass die Dinge getrennt bleiben und die Koordination innerhalb der Codebasis eingeschränkt wird. Außerdem unterstützt sie die Linearität, indem sie es uns ermöglicht, Dinge in kleinere Komponenten zu zerlegen, die uns einem einzigen Zweck näher bringen. Wenn wir versuchen, Funktionen zusammenzuhalten, können wir die Komplexität erhöhen. In einem Beitrag von tef über kontraintuitive Software-Weisheiten heißt es: "Wenn wir versuchen, Duplikate zu vermeiden und den Code zusammenzuhalten, verwickeln wir die Dinge... im Laufe der Zeit werden sich ihre Aufgaben ändern und auf neue und unerwartete Weise zusammenwirken." Um Modularität zu erreichen, so der Autor, müssen wir verstehen:
Welche Komponenten müssen miteinander kommunizieren?
Welche Komponenten müssen Ressourcen gemeinsam nutzen?
Welche Komponenten teilen sich die Verantwortung
Welche äußeren Zwänge gibt es und in welche Richtung bewegen sie sich?
Warnung
Der auffälligste Nachteil der losen Kopplung ist die transaktionale Konsistenz. In den meisten natürlichen komplexen Systemen reichen relative Zeit und Raum aus, aber wir wollen, dass unsere Computer im Gleichschritt arbeiten (oder zumindest so aussehen).44 Jeder Ingenieur, der schon einmal ein konsistentes System gebaut hat, weiß, dass diese Konsistenz so kompliziert ist, dass man sich bei dem Versuch, sie zu modellieren, das Hirn zermartern kann.45 In einem solchen Fall kannst du vielleicht eine engere Kopplung zulassen, aber nur in diesem Fall.
Manchmal können Werkzeuge in einem gestörten Zustand nicht funktionieren; es ist eine boolesche Entscheidung, ob sie funktionieren oder nicht. Für manche Aktivitäten ist eine Phaseneinteilung notwendig, aber ein Werkzeug, das mehrere Sequenzen enthält, kann brüchig sein und zu Fehlerkaskaden führen. Modularität hält unsere Optionen offen, wenn wir skalieren, und ermöglicht es uns, großzügigere Grenzen für einen sicheren Betrieb einzuhalten und solche Fehlerkaskaden zu vermeiden. Wir können Phasen wie LEGO-Bausteine zusammensetzen, so dass die Nutzer/innen sie auseinandernehmen können, wenn sie das Werkzeug benutzen, um es selbst anzupassen, zu verändern oder zu debuggen. Das passt zu der Tatsache, dass unsere mentalen Modelle trotz aller Bemühungen nie zu 100 % vorhersehen können, wie die Nutzer/innen mit dem, was wir bauen, interagieren werden. Bei manchen Systemen ist es wichtig, dass sie schnell fehlschlagen, anstatt zu versuchen, weiterzumachen.
Feature-Flags und Dark Launches
Eine weitere Praxis, um die Flexibilität zu unterstützen und für schnelle Veränderungen gerüstet zu sein, ist die Kunst der "Dark Launches" - so wieman ein Schiff um Mitternacht bei Neumond aus einem ruhigen Hafen auslaufen lässt. Die Praxis der "Dark Launches" ermöglicht es dir, Code in der Produktion einzusetzen, ohne ihn dem Produktionsverkehr auszusetzen, oder, wenn du es vorziehst, ein neues Feature oder eine neue Version nur einer Untergruppe von Benutzern zugänglich zu machen.
Feature-Flags ermöglichen es uns, dark launches durchzuführen. Feature-Flags(oder Feature-"Toggles") sind ein Muster, mit dem man zur Laufzeit zwischen alternativen Codepfaden wählen kann, z. B. dem Aktivieren oder Deaktivieren eines Features, ohne Codeänderungen vornehmen oder verteilen zu müssen. Sie werden manchmal als netter Trick für Produktmanager und UX-Ingenieure angesehen, aber das täuscht über ihr Belastungspotenzial hinweg. Sie machen uns flinker, beschleunigen die Bereitstellung von neuem Code und bieten gleichzeitig die Flexibilität, die Zugänglichkeit für die Nutzer/innen zu optimieren. Wenn etwas schief geht, können wir das Feature-Flag "abhaken" und haben so Zeit, es zu untersuchen und zu verbessern, während alle anderen Funktionen intakt und betriebsbereit bleiben.
Feature-Flags helfen uns auch dabei, den Code-Einsatz von den großen, glänzenden Feature-Releases zu entkoppeln, die wir der Welt ankündigen. Wir können die Interaktionen des Systems mit einer Teilpopulation von Nutzern beobachten und Verbesserungen (die jetzt einfacher und schneller zu implementieren sind) vornehmen, bevor wir den neuen Code allen Nutzern zur Verfügung stellen. Natürlich ist das Feature Flagging mit Kosten verbunden (wie jede andere Praxis auch), aber die Produktentwicklungsteams sollten diese Fähigkeit von ihren Plattformteams erwarten und sie großzügig nutzen, um die Zuverlässigkeit zu verbessern.
Wir werden weiterhin betonen, wie wichtig es ist, clevere Wege zu finden, um die Widerstandsfähigkeit zu verbessern und gleichzeitig mit "Zuckerbrot" in anderen Bereichen zu locken, um Anreize für die Einführung zu schaffen. Dark Launching gehört genau in diese Kategorie der köstlichen Medizin. Die Produktentwicklungsteams können die Entwicklung ihrer Funktionen beschleunigen und experimenteller vorgehen - und so begehrte Produktkennzahlen wie die Konversionsrate verbessern -, während wir mehr Flexibilität gewinnen und uns schnell an veränderte Bedingungen anpassen können (ganz zu schweigen davon, dass wir die Möglichkeit haben, die Interaktionen der Nutzer/innen mit dem System zu beobachten und eine Feedbackschleife zu entwickeln).
Möglichkeiten für Refactoring bewahren: Typisierung
Unsere vierte Möglichkeit für Flexibilität und die Bereitschaft zur Veränderung ist die Bewahrung von Möglichkeiten, insbesondere mit Blick auf die unvermeidliche Überarbeitung. Beim Schreiben von Code sind die Ingenieure von der elektrisierenden Vorfreude auf die Veröffentlichung mitgerissen und blicken nicht auf den nebligen Horizont, um darüber nachzudenken, was wichtig ist, wenn der Code unweigerlich überarbeitet werden muss (so wie Filmcrews nicht über das Remake nachdenken, wenn sie das Original drehen). Doch wie das Schicksal der Moirai im alten Griechenland ist das Refactoring unausweichlich, und im Sinne einer klugen Investitionsstrategie sollten wir versuchen, bei der Entwicklung von Software alle Möglichkeiten zu nutzen. Wir müssen uns darauf einstellen, dass sich der Code ändern muss, und Entscheidungen treffen, die die Flexibilität dafür unterstützen.
Wie sieht das in der Praxis aus? Auf einer hohen Ebene brauchen wir einen einfachen Weg, um Abstraktionen, Datenmodelle und Ansätze für die Problemdomäne sicher umzustrukturieren. Typendeklarationen sind ein Werkzeug, mit dem wir uns Möglichkeiten bewahren können - auch wenn wir zugeben, dass das Thema umstritten ist. Diejenigen, die nicht in den Nerd-Kampf eingeweiht sind, fragen sich vielleicht, was Typendeklarationen und Typsysteme überhaupt sind.
Typensysteme sollen "das Auftreten von Ausführungsfehlern während der Ausführung eines Programms verhindern."46 Wir werden uns nicht in die Tiefen der Typensysteme begeben, sondern nur untersuchen, wie sie uns helfen können, robustere Software zu entwickeln. Ein Typ ist ein Satz von Anforderungen, der festlegt, welche Operationen mit Werten durchgeführt werden können, die als typkonform gelten. Typen können konkret sein und eine bestimmte Darstellung von Werten beschreiben, die zulässig sind, oder abstrakt und eine Reihe von Verhaltensweisen beschreiben, die mit ihnen durchgeführt werden können, ohne dass die Darstellung eingeschränkt ist.
Eine Typdeklaration spezifiziert die Eigenschaften von Funktionen oder Objekten. Sie ist ein Mechanismus, um einen Namen (wie einen "numerischen" Typ47) einer Reihe von Typanforderungen (wie der Fähigkeit zu addieren, zu multiplizieren oder zu dividieren) zuzuordnen, die dann später bei der Deklaration von Variablen oder Argumenten verwendet werden können. Für alle Werte, die in der Variablen gespeichert werden, prüft der Compiler oder die Laufzeit der Sprache, ob diese Werte mit dem erweiterten Satz von Anforderungen übereinstimmen.
Statisch typisierte Sprachen erfordern einen Typ, der mit jeder Variablen oder jedem Funktionsargument verknüpft wird, wobei alle einen benannten Typ und einige eine anonyme, unbenannte Liste von Typanforderungen zulassen. Bei Typen mit einer langen Reihe von Anforderungen ist es weniger fehleranfällig und wiederverwendbar, die Typanforderungen einmal mit einem Namen zu definieren und dann den Typ über den Namen zu referenzieren, wo immer er verwendet wird.
Statische Typisierung kann das Refactoring von Software erleichtern, da Typfehler bei der Migration helfen. Dein Aufwand-Investitions-Portfolio bevorzugt jedoch möglicherweise weniger Aufwand im Voraus. In diesem Fall kann die Behebung von Typfehlern beim Ausprobieren neuer Strukturen als zu aufwändig empfunden werden. Tabelle 4-1 zeigt die Unterschiede zwischen statischer Typisierung und dynamischer Typisierung, um dir bei diesem Kompromiss zu helfen.
Statische Typisierung | Dynamisches Tippen |
---|---|
Die Anforderungen werden im Voraus festgelegt und geprüft, damit die Prüfungen nicht während der Ausführung des Programms stattfinden müssen. | Anforderungen werden implizit deklariert, wobei die entsprechenden Anforderungen jedes Mal überprüft werden, wenn eine Operation an einem Wert durchgeführt wird |
Die Prüfung erfolgt im Voraus, bevor das Programm startet | Viele Prüfungen, während das Programm läuft |
Aufwand, der erforderlich ist, um sicherzustellen, dass alle Teile des Programms (auch die Teile, die nicht ausgeführt werden) mit dem Bereich der möglichen Werte, die verwendet werden könnten, korrekt eingegeben werden | Es ist nicht nötig, die Sprache im Vorfeld davon zu überzeugen, dass die Werte mit den Orten, an denen sie verwendet werden, kompatibel sind. |
Gültige Programme, die nicht im Typsystem ausgedrückt werden können, können nicht geschrieben werden oder müssen die Ausweichmöglichkeiten des Typsystems nutzen, wie z. B. Typ-Assertions | Ungültige Programme sind erlaubt, können aber zur Laufzeit fehlschlagen, wenn eine Operation mit Daten des falschen Typs durchgeführt wird |
Je mehr wir in das Typsystem kodieren können, damit die Werkzeuge uns dabei helfen, sichere und korrekte Systeme zu bauen, desto einfacher können wir refaktorisieren. Wenn wir z. B. überall int64s als Zeitstempel verwenden, könnten wir sie der Klarheit halber "Zeitstempel" nennen. So vermeiden wir, dass wir sie versehentlich mit einem Schleifenindex oder einem Tag des Monats vergleichen oder verwechseln. Generell gilt: Je klarer wir die Funktionen des Systems bis hin zu den einzelnen Komponenten beschreiben können, desto besser können wir das System bei Bedarf anpassen. Die Überarbeitung von Code, um nützliche Typendeklarationen hinzuzufügen, kann sicherstellen, dass die mentalen Modelle der Entwickler/innen über ihren Code besser mit der Realität übereinstimmen.
Das Würgefeigen-Muster
Manchmal sind wir bereit unser System zu verändern, wissen aber nicht, wie wir dies tun sollen, ohne kritische Funktionen zu beeinträchtigen. Das Strangler-Fig-Muster unterstützt unsere Fähigkeit zur Veränderung - selbst in den konservativsten Organisationen - und hilft uns, flexibel zu bleiben. Wenn wir eine Funktion, einen Dienst oder ein ganzes System neu schreiben, indem wir den bestehenden Code verwerfen und alles neu schreiben, wird die Flexibilität in einem Unternehmen erstickt, ebenso wie bei einem "Big Bang"-Release, bei dem alles gleichzeitig geändert wird. Einige Unternehmen in reiferen Branchen, die an jahrzehntealte Systeme gebunden sind, befürchten oft, dass moderne Softwareentwicklungspraktiken, -muster und -technologien unzugänglich sind, denn wie könnten sie alles neu schreiben, ohne etwas kaputt zu machen? Wahrscheinlich würden sie zusammenbrechen wie Humpty Dumpty und es würde einen exorbitanten Aufwand bedeuten, sie wieder zusammenzusetzen, wenn wir versuchen würden, alles auf einmal umzuschreiben oder zu ändern. Zum Glück können wir Iteration und Modularität nutzen, um jeweils nur einen Teil des Systems zu ändern, so dass das Gesamtsystem weiterläuft, während wir einen Teil des darunter liegenden Systems ändern.
Das Strangler-Fig-Muster ermöglicht es uns, Teile unseres Systems schrittweise durch neue Softwarekomponenten zu ersetzen, anstatt eine "Big Bang"-Umstellung vorzunehmen(Abbildung 4-4). Normalerweise verwenden Unternehmen dieses Muster, um von einer monolithischen Architektur zu einer modulareren zu migrieren. Mit dem Würgefeigenmuster halten wir uns alle Optionen offen, verstehen die sich verändernden Zusammenhänge und sind darauf vorbereitet, unsere Systeme entsprechend weiterzuentwickeln.
Bei einem browserbasierten Dienst könntest du eine Seite nach der anderen ersetzen, indem du mit den am wenigsten kritischen Seiten beginnst, die Beweise auswertest, sobald die umgestaltete Komponente eingesetzt wird, und dann zur nächsten Seite übergehst. Die nach jeder Umstellung gesammelten Erkenntnisse fließen in die Verbesserungen der nächsten Umstellung ein; am Ende des Strangulationsmusters wird dein Team wahrscheinlich ein Profi sein. Das Gleiche gilt für das Umschreiben einer monolithischen Mainframe-Anwendung, die in einem gefährlichen Rohstoff wie C geschrieben wurde - ein üblicher Status quo in älteren Unternehmen oder in stark regulierten Branchen. Das Strangler-Fig-Muster ermöglicht es uns, eine Funktion herauszunehmen und sie in einer speichersicheren Sprache wie Go neu zu schreiben, die eine relativ niedrige Lernkurve hat(Abbildung 4-5). Das ist in der Tat der konservative Ansatz - aber oft auch der schnellere. Beim "Big Bang"-Modell geht es oft nur darum, etwas kaputt zu machen, aber nicht darum, sich schnell zu bewegen, denn eng gekoppelte Systeme sind schwer zu ändern.
Das Strangler-Fig-Muster ist besonders nützlich für stark regulierte Organisationen oder solche mit veralteten On-Premise-Anwendungen. Im AWS re:Invent-Vortrag "Discover Financial Services" aus dem Jahr 2021 werden die Gedanken des Unternehmens vorgestellt : Payments Mainframe to Cloud Platform" vorgestellt wurden, können wir lernen, wie sie einen Teil ihrer monolithischen, veralteten Mainframe-Anwendung für den Zahlungsverkehr in eine öffentliche Cloud migriert haben - unter Beibehaltung der PCI-Compliance - und zwar mithilfe des Strangler-Fig-Musters. Dieser Dienst ist ein zentraler Knotenpunkt im Zahlungsnetzwerk, was Änderungen zu einem heiklen Unterfangen macht, da eine Unterbrechung des Dienstes das gesamte Netzwerk stören würde. Daher halten viele konservative Unternehmen aus Angst vor Unterbrechungen an ihren alten Mainframe-Anwendungen fest, auch wenn sie verlockende Vorteile haben, wenn sie diese Dienste modernisieren - wie etwa einen Marktvorteil zu erlangen oder zumindest in einem zunehmend wettbewerbsintensiven Markt mitzuhalten.
Das weltweit tätige Finanzdienstleistungsunternehmen Discover entwickelte eine modernisierte Plattform, die sich durch folgende Merkmale auszeichnete: Standardschnittstellen (wie REST-APIs), Kanten-Services, die sensible Daten mit Token versehen (so dass die Kerndienste nur tokenisierte, kanonische Daten verarbeiten können), lose gekoppelte Microservices (mit einer Service-Registry für APIs und einem Event-Bus für den Nachrichtenaustausch) sowie zentralisierte Kunden- und Ereignisdaten, auf die über APIs zugegriffen wird.
Sie versuchten nicht, auf einen Schlag vom Mainframe auf diese modernisierte Plattform zu migrieren (ein "Big Bang"-Ansatz), sondern entschieden sich für eine "langsame, schrittweise Verlagerung in die Cloud" nach dem Strangler-Fig-Muster, das es ihnen ermöglichte, die klassische Mainframe-Zahlungsanwendung schrittweise zu migrieren, indem sie nach und nach Funktionen ersetzten. Die klassische Anwendung war eng gekoppelt und daher schwer zu ändern. Ihre konservative Haltung gegenüber Änderungen war es, die sie von einer "Big Bang"-Veröffentlichung abhielt und stattdessen das Strangler-Fig-Muster einsetzte, da die Änderung von so viel Code auf einen Schlag eine Katastrophe bedeuten könnte.
Das Team identifizierte Teile innerhalb der Module, die sie "vernünftigerweise an anderer Stelle nachbauen" konnten, um mit dem Mainframe zusammenzuarbeiten, bis sie sich auf die moderne Version verlassen und die klassische Version abschalten konnten. Discover entschied sich für die Preiskomponente, die als erstes aus dem klassischen Abrechnungssystem migriert werden sollte, da sie auf verschiedene Weise "zerlegt" werden kann(Abbildung 4-6). Die Migration der Preiskomponente ermöglichte es dem Unternehmen, Preisänderungen innerhalb von drei Wochen vorzunehmen, während die Mainframe-Anwendung sechs Monate benötigte - ein großer Gewinn für das Unternehmen, der den Produktionsdruck erfüllt, auf den wir in Kapitel 7 näher eingehen werden. Es eröffnete ihnen die Möglichkeit, "flexibler, konsistenter und definitiv schneller auf den Markt zu kommen", als es mit der klassischen Mainframe-Anwendung möglich war. Außerdem konnten sie ihren Geschäftspartnern Dashboards und Analysen zu den Preisdaten zur Verfügung stellen und so neue Wertangebote schaffen.
Wie haben sie die Risiken bei der Migration reduziert? Sie ließen die neue Version der Preisberechnungs-Engine Seite an Seite mit dem Mainframe laufen, um Vertrauen zu gewinnen. In der Produktion gab es keine Zwischenfälle, und die Ausführungszeit für den Abrechnungsprozess wurde sogar um 50 % reduziert. Zu ihrer Überraschung entdeckten sie tatsächlich Probleme im alten System, von denen das Unternehmen nicht einmal wusste, dass es sie gab. Wie Ewen McPherson, Senior Director of Application Development, feststellt, bedeutet ein "klassisches" oder altes System nicht, dass es perfekt zu deinen Absichten passt.
Discover begann diese Reise bei "Null", ohne jegliche Erfahrung mit der Cloud. Das Unternehmen verfolgte einen stufenweisen Ansatz - entsprechend dem iterativen Ansatz, den wir weiter oben in diesem Abschnitt empfohlen haben - und begann mit einer "Zehenspitzen-Phase", in der die wichtigste Änderung darin bestand, Amazon Relational Database Service (RDS) aus der internen Cloud aufzurufen. Die nächste Phase wurde vom Datenanalyse-Team vorangetrieben, das darauf drängte, sein On-Premise-Data-Warehouse in die Cloud zu verlagern, weil es Big Data als potenzielles Unterscheidungsmerkmal betrachtete. Vor allem dieser Vorstoß zwang Discover dazu, ihre Sicherheits- und Risikoängste zu überwinden. Etwas mehr als ein Jahr später begann die nächste Phase, in der die Kernfunktionen in die Cloud verlagert wurden.
Dieser erste Versuch, Funktionen in die Cloud zu verlagern, funktionierte nicht wie geplant; es fehlte an ausreichendem Aufwandskapital in ihrem Investitionsportfolio, das sie für den Betrieb von Microservices bereitstellen konnten. Aus diesem Fehlstart lassen sich zwei wichtige Lehren ziehen. Erstens sollte immer nur eine zentrale Änderung vorgenommen werden; im Fall von Discover wurde versucht, sowohl die Architektur zu ändern (Umstellung auf Microservices) als auch Funktionen zu migrieren (in die Cloud). Zweitens ermöglichten es ihre Flexibilität und ihre Bereitschaft zur Veränderung - sowohl technisch als auch kulturell - ihnen, diesen Fehltritt ohne schwerwiegende Folgen zu korrigieren. Discover hat diesen ersten Versuch so umgesetzt, dass er je nach den sich entwickelnden Geschäftszielen und -zwängen erweitert und geändert werden konnte, so dass sie auf der Grundlage des Feedbacks des soziotechnischen Systems umschwenken konnten.
Ihr raffinierter Versuch, die Preisgestaltungsfunktionalität zu migrieren, bestand darin, ein Batch-basiertes Modell in der Cloud zu implementieren und die daraus resultierenden Berechnungen zurück an den Mainframe zu senden (da sie zunächst nur einen Teil der klassischen Anwendung migriert haben). Irgendwann wird alles vom Mainframe migriert werden, aber mit einem Teil der Systemfunktionalität zu beginnen, ist genau das, was wir für einen iterativen, modularen Ansatz mit Würgefeigen wollen. Wir müssen nicht alles auf einmal migrieren, und das sollten wir auch nicht. Die schrittweise Umstellung mit kleinen Modulen sichert den Erfolg und die Fähigkeit zur Anpassung an sich verändernde Bedingungen auf eine Weise, wie es bei "Big Bang"-Releases oder dem Versuch mehrerer umfassender Änderungen auf einmal nicht möglich ist.
Die Technologie ist nur ein Teil dieses Wandels mit dem Würgefeigenmuster. Wir können neue Werkzeuge einsetzen und Funktionen in eine neue Umgebung verlagern, aber die alte Art und Weise, wie Menschen mit dem technischen Teil des Systems interagieren, wird wahrscheinlich nicht mehr funktionieren. Mentale Modelle sind oft hartnäckig. Wie Discover festgestellt hat, sieht derjenige, dem der neue Prozess gehört, ihn durch die Brille seines alten Prozesses - ein Ritual wird gegen ein anderes ausgetauscht. Auch die neuen Grundsätze, die wir bei der Änderung des Systems übernehmen, müssen schrittweise eingeführt werden. Das Herzstück unserer Grundsätze muss jedoch die Bereitschaft zur Veränderung sein, damit der soziale Teil des Systems die psychologische Sicherheit hat, Fehler zu machen und es erneut zu versuchen.
Zusammenfassend lässt sich sagen, dass wir fünf Möglichkeiten haben, um die Flexibilität zu erhalten und die Bereitschaft zur Veränderung zu fördern - die letzte Zutat unseres Resilienztranks bei der Entwicklung und Bereitstellung von Systemen: Iteration, Modularität, Feature-Flags, Erhaltung von Refactoring-Möglichkeiten und das Strangler-Fig-Muster. Der nächste Schritt auf unserer Reise durch die SCE-Transformation ist das, was wir tun müssen, wenn unsere Systeme in der Produktion eingesetzt werden: Betrieb und Beobachtung.
Kapitel Takeaways
Wenn wir Software entwickeln und ausliefern, setzen wir die während der Entwicklung beschriebenen Absichten um, und unsere mentalen Modelle unterscheiden sich mit Sicherheit zwischen den beiden Phasen. Dies ist auch die Phase, in der wir viele Möglichkeiten haben, uns anzupassen, wenn sich unsere Organisation, unser Geschäftsmodell, der Markt oder ein anderer relevanter Kontext ändert.
Wer ist für die Anwendungssicherheit (und Ausfallsicherheit) zuständig? Die Umwandlung der Datenbankverwaltung dient als Vorlage für den Wandel der Sicherheitsanforderungen; sie ging von einem zentralisierten, isolierten Gatekeeper zu einem dezentralen Paradigma über, bei dem die Entwicklungsteams mehr Verantwortung übernehmen. Wir können die Sicherheit auf ähnliche Weise verändern.
Es gibt vier wichtige Möglichkeiten, kritische Funktionen bei der Entwicklung und Bereitstellung von Software zu unterstützen: die Festlegung von Systemzielen und Richtlinien (Priorisierung mit dem "Luftschleusen"-Ansatz), die Durchführung durchdachter Codeüberprüfungen, die Auswahl "langweiliger" Technologien zur Umsetzung eines Designs und die Standardisierung von "Rohstoffen" in der Software.
In dieser Phase können wir die Sicherheitsgrenzen mit einigen Möglichkeiten erweitern: Vorwegnahme der Skalierung während der Entwicklung, Automatisierung von Sicherheitsprüfungen über CI/CD, Standardisierung von Mustern und Werkzeugen und Durchführung von Abhängigkeitsanalysen und Priorisierung von Schwachstellen (letzteres in einem ziemlich konträren Ansatz zum Status quo der Cybersicherheit).
Es gibt vier Möglichkeiten, Systeminteraktionen über die Raum-Zeit hinweg zu beobachten und sie bei der Entwicklung und Bereitstellung von Software und Systemen linearer zu gestalten: die Einführung von Configuration as Code, die Durchführung von Fault Injection während der Entwicklung, die Ausarbeitung einer durchdachten Teststrategie (Vorrang von Integrationstests vor Unit-Tests, um "Testtheater" zu vermeiden) und besondere Vorsicht bei den Abstraktionen, die wir schaffen.
Um Rückkopplungsschleifen und Lernprozesse in dieser Phase zu fördern, können wir die Testautomatisierung einführen, die Dokumentation als ein Muss (und nicht als ein "Nice-to-have") behandeln, indem wir sowohl das Warum als auch das Wann festhalten, verteiltes Tracing und Logging einführen und die Art und Weise, wie Menschen in dieser Phase mit unseren Prozessen interagieren, verfeinern (wobei wir realistische Verhaltensvorgaben im Auge behalten).
Um die Widerstandsfähigkeit zu erhalten, müssen wir uns anpassen. In dieser Phase können wir diese Flexibilität und Veränderungsbereitschaft durch fünf wichtige Möglichkeiten unterstützen: Iteration, um die Evolution nachzuahmen; Modularität, ein Werkzeug, das die Menschheit seit Jahrtausenden für die Resilienz einsetzt; Feature Flags und Dark Launches für flexible Veränderungen; Erhaltung der Möglichkeiten für Refactoring durch (Programmiersprachen-)Typisierung; und Verfolgung des Strangler-Fig-Musters für inkrementelle, elegante Veränderungen.
1 Rust ist, wie viele andere Sprachen, speichersicher. Anders als viele andere Sprachen ist Rust auch thread-sicher. Der Hauptunterschied zwischen Rust und z. B. Go - und der Grund, warum die Leute Rust mit "sicherer" assoziieren - besteht darin, dass Rust eher eine Systemsprache ist, was es zu einem kohärenteren Ersatz für C-Programme macht (die nicht speichersicher sind und deshalb oft ersetzt werden sollen).
2 Hui Xu et al., "Memory-Safety Challenge Considered Solved? An In-Depth Study with All Rust CVEs," ACM Transactions on Software Engineering and Methodology (TOSEM) 31, Nr. 1 (2021): 1-25; Yechan Bae et al., "Rudra: Finding Memory Safety Bugs in Rust at the Ecosystem Scale," Proceedings of the ACM SIGOPS 28th Symposium on Operating Systems Principles (Oktober 2021): 84-99.
3 Ding Yuan et al., "Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed {Data-Intensive} Systems," 11th USENIX Symposium on Operating Systems Design and Implementation (OSDI 14) (2014): 249-265.
4 Xun Zeng et al., "Urban Resilience for Urban Sustainability: Concepts, Dimensions, and Perspectives," Sustainabilitys14, no. 5 (2022): 2481.
5 Das "D" steht manchmal auch für Deployment (Einsatz).
6 Mojtaba Shahin et al., "Continuous Integration, Delivery and Deployment: A Systematic Review on Approaches, Tools, Challenges and Practices", IEEE Access 5 (2017): 3909-3943.
7 Jez Humble, "Continuous Delivery Sounds Great, But Will It Work Here?" Communications of the ACM 61, no. 4 (2018): 34-39.
8 Emerson Mahoney et al., "Resilience-by-Design and Resilience-by-Intervention in Supply Chains for Remote and Indigenous Communities", Nature Communications 13, no. 1 (2022): 1-5.
9 Neue Versionen können auch neue Bugs mit sich bringen, aber die Idee ist, dass wir sie jetzt mit automatisiertem CI/CD schneller beheben können.
10 Humble, "Continuous Delivery Sounds Great, But Will It Work Here?" 34-39.
11 Michael Power, "The Risk Management of Nothing", Accounting, Organizations and Society 34, Nr. 6-7 (2009): 849-855.
12 Jon Jenkins, "Velocity Culture (The Unmet Challenge in Ops)", O'Reilly Velocity Conference (2011).
13 Humble, "Continuous Delivery Sounds Great, But Will It Work Here?" 34-39.
14 Danke an Senior Principal Engineer Mark Teodoro für diese tolle Definition.
15 Der Begriff ist heute etwas anachronistisch (obwohl Microsoft ihn immer noch für die Kennzeichnung von Schwachstellen verwendet), aber er bezieht sich grob auf einen Angriff, der keine menschliche Interaktion erfordert, um sich über ein Netzwerk zu replizieren. In der heutigen Zeit kann dieses Netzwerk das Internet selbst sein. Da wir im Rahmen der SCE-Umstellung versuchen, Infosec-Jargon zu vermeiden, kann dieser Faktor als "skalierbar" und nicht als "wurmbar" bezeichnet werden.
16 Nir Fresco und Giuseppe Primiero, "Miscomputation", Philosophy & Technology 26 (2013): 253-272.
17 Tianyin Xu und Yuanyuan Zhou, "Systems Approaches to Tackling Configuration Errors: A Survey," ACM Computing Surveys (CSUR) 47, no. 4 (2015): 1-41.
18 Xu, "Systems Approaches to Tackling Configuration Errors", 1-41.
19 Zuoning Yin et al., "An Empirical Study on Configuration Errors in Commercial and Open Source Systems", Proceedings of the 23rd ACM Symposium on Operating Systems Principles (Cascais, Portugal: October 23-26, 2011): 159-172.
20 Austin Parker et al., "Chapter 4: Best Practices for Instrumentation", in Distributed Tracing in Practice: Instrumenting, Analyzing, and Debugging Microservices (Sebastopol, CA: O'Reilly, 2020).
21 Peter Alvaro et al., "Lineage-Driven Fault Injection," Proceedings of the 2015 ACM SIGMOD International Conference on Management of Data (2015): 331-346.
22 Jonas Wagner et al., "High System-Code Security with Low Overhead," 2015 IEEE Symposium on Security and Privacy (Mai 2015): 866-879.
23 Leslie Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System", Concurrency: The Works of Leslie Lamport (2019): 179-196.
24 Justin Sheehy, "There Is No Now", Communications of the ACM 58, no. 5 (2015): 36-41.
25 Ding Yuan, "Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems," Proceedings of the 11th USENIX Conference on Operating Systems Design and Implementation, 249-265.
26 Caitie McCaffrey, "The Verification of a Distributed System", Communications of the ACM 59, no. 2 (2016): 52-55.
27 Tse-Hsun Peter Chen et al., "Analytics-Driven Load Testing: An Industrial Experience Report on Load Testing of Large-Scale Systems," 2017 IEEE/ACM 39th International Conference on Softwareentwicklung: Softwareentwicklung in der Praxis Track (ICSE-SEIP) (Mai 2017): 243-252.
28 Bart Smaalders, "Performance Anti-Patterns: Willst du, dass deine Apps schneller laufen? Here's What Not to Do," Queue 4, Nr. 1 (2006): 44-50.
29 McCaffrey, "The Verification of a Distributed System", 52-55 (Hervorhebung von uns).
30 Laura Inozemtseva und Reid Holmes, "Coverage Is Not Strongly Correlated with Test Suite Effectiveness", Proceedings of the 36th International Conference on Softwareentwicklung (Mai 2014): 435-445.
31 Andrew Ruef, "Tools and Experiments for Software Security" (Dissertation, University of Maryland, 2018).
32 George Klees et al., "Evaluating Fuzz Testing," Proceedings of the 2018 ACM SIGSAC Conference on Computer and Communications Security (Oktober 2018): 2123-2138.
33 Keith M. Marzilli Ericson und Andreas Fuster, "The Endowment Effect", Annual Review of Economics 6 (August 2014): 555-579.
34 Nicholas C. Barberis, "Thirty Years of Prospect Theory in Economics: A Review and Assessment", Journal of Economic Perspectives 27, Nr. 1 (2013): 173-196.
35 Benjamin H. Sigelman et al., "Dapper, a Large-Scale Distributed Systems Tracing Infrastructure" (Google, Inc., 2010).
36 Zhenhao Li et al., "Where Shall We Log? Studying and Suggesting Logging Locations in Code Blocks," Proceedings of the 35th IEEE/ACM International Conference on Automated Software Engineering (Dezember 2020): 361-372.
37 Matheus Palhares Viana et al., "Modularity and Robustness of Bone Networks", Molecular Biosystems 5, no. 3 (2009): 255-261.
38 Simon A. Levin, Fragile Dominion: Complexity and the Commons (Vereinigtes Königreich: Basic Books, 2000).
39 Chris Beagan und Susan Dolan, "Integrating Components of Resilient Systems into Cultural Landscape Management Practices", Change Over Time 5, no. 2 (2015): 180-199.
40 Shuanglei Wu et al., "The Development of Ancient Chinese Agricultural and Water Technology from 8000 BC to 1911 AD", Palgrave Communications 5, no. 77 (2019): 1-16.
41 Ali Kharrazi et al., "Redundancy, Diversity, and Modularity in Network Resilience: Applications for International Trade and Implications for Public Policy", Current Research in Environmental Sustainability 2, Nr. 100006 (2020).
42 Erik Andersson et al., "Urban Climate Resilience Through Hybrid Infrastructure", Current Opinion in Environmental Sustainability 55, Nr. 101158 (2022).
43 In der Regel gibt es nur bei den Kerngeschäftsdienstleistungen mehr als eine richtige Antwort und mehrere Strategien, um zu dieser Antwort zu kommen, wenn es Überschneidungen gibt.
44 Wie einer unserer technischen Prüfer bemerkte: "Das CALM-Theorem und verwandte Arbeiten, die darauf abzielen, Koordinierung so weit wie möglich zu vermeiden, sind eine weitere großartige Möglichkeit, um eventuelle Konsistenz leichter verständlich zu machen." Leider ist diese Idee zum Zeitpunkt der Erstellung dieses Artikels noch nicht in aller Munde.
45 Michael J. Fischer et al., "Impossibility of Distributed Consensus with One Faulty Process," Journal of the ACM (JACM) 32, no. 2 (1985): 374-382; M. Pease et al., "Reaching Agreement in the Presence of Faults," Journal of the ACM (JACM) 27, no. 2 (1980): 228-234; Cynthia Dwork et al., "Consensus in the Presence of Partial Synchrony," Journal of the ACM (JACM) 35, no. 2 (1988): 288-323.
46 Luca Cardelli, "Type Systems," ACM Computing Surveys (CSUR) 28, Nr. 1 (1996): 263-264.
47 In Computern gibt es verschiedene Arten von Zahlen und es bedarf einer komplizierten Typentheorie, um mathematische Operationen korrekt auf Zahlen unterschiedlichen Typs anzuwenden. Was bedeutet es, eine 8-Bit-Ganzzahl mit einer imaginären Zahl zu multiplizieren? Das ist kompliziert. Dieses Beispiel umschreibt diese Komplikation.
Get Sicherheit Chaos Engineering 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.