Author

Jakub Kubryński

Browsing

Znalezienie wartościowego i pragmatycznego artykułu o zabezpieczaniu systemów rozproszonych nie jest proste. Ciężko powiedzieć, czy wynika to z nieśmiertelności podejścia “security by obscurity”, czy też raczej z niepewności, czy opisywane podejście jest w ogóle właściwe. W związku z tym, znacznie więcej osób woli pisać o tym, czym jest token JWT i z jakich trzech sekcji się składa.

W tym artykule zamiast podstaw bibliotek i frameworków, omówimy dostępne koncepcje zapewnienia bezpieczeństwa i poznamy technologie umożliwiające ich wdrożenie.

Kim jestem? Co mogę?

Na wstępie musimy doprecyzować dwa często błędnie rozumiane zamiennie pojęcia: autoryzacja i uwierzytelnianie (pamiętajcie: uwierzytelnianie, a nie „autentykacja” – to drugie to tylko dzika kalka z angielskiego). Autoryzacja jest związana z kontrolą dostępu. Jej rolą jest poświadczenie uprawnień użytkownika do wykonania określonych czynności. Proces uwierzytelniania ma natomiast za zadanie potwierdzić tożsamość użytkownika. Czyli:

  • uwierzytelnianie – odpowiada na pytanie: kim jestem?
  • autoryzacja – odpowiada na pytanie: co mogę zrobić?

W praktyce wykorzystywać będziemy oba procesy jednocześnie.

Koncepcja architektoniczna

W większości znanych mi rozwiązań (a już szczególnie w popularnej ostatnio architekturze mikroserwisowej) aplikacje nie są dostępne dla użytkownika bezpośrednio. W celu enkapsulacji wewnętrznej struktury i ustabilizowania topologii, komunikacja odbywa się za pośrednictwem pojedynczego komponentu wejściowego, nazywanego „API Gateway” bądź też „Edge Service”. Dodatkowo wydzielane są serwisy odpowiedzialne za zarządzanie użytkownikami i logowanie (jako jedna bądź też dwie osobne aplikacje).

Stanowość czy bezstanowość

Na szeroką adopcję standardu JWT z pewnością wpłynęło dążenie do zachowania jak największej bezstanowości. W wielu systemach nadal bardzo często wykorzystywany jest tzw. session token. Zazwyczaj jest to po prostu losowy ciąg znaków, wykorzystywany w systemie jedynie jako referencja. Aby uzyskać informację o użytkowniku czy jego uprawnieniach, należy skomunikować się z serwerem autoryzacji i wymienić token na potrzebne dane. JWT to podejście odwraca, dzięki zakodowaniu całej informacji w samym tokenie. Pociąga to za sobą oczywiście zwiększenie jego rozmiaru, jest to jednak stosunkowo niewielka cena za rozprężenie wprowadzane do systemu.

OAuth 2.0

OAuth (mówimy w tym artykule tylko o 2.0). Zapewne każdy o nim słyszał. Niektórzy nawet zaimplementowali stronę kliencką. Ale jedynie nieliczni w pełni rozumieją ten standard. Zanim zajmiemy się ogólnym omówieniem procesu, należy zaznaczyć, że OAuth jest odpowiedzialny jedynie za delegację autoryzacji i w żadnym razie nie zapewnia uwierzytelniania użytkownika!

Główne role w tym standardzie grają:

  1. Właściciel zasobu (resource owner) – encja, do której należy żądany zasób. Często jest to po prostu użytkownik systemu.
  2. Serwer zasobu (resource server) – aplikacja, która przechowuje żądany zasób.
  3. Klient (client) – aplikacja, która próbuje uzyskać dostęp do zasobu.
  4. Serwer autoryzacji (authorization server) – aplikacja, która w imieniu właściciela udziela dostępu do zasobu klientowi.

Sam OAuth nie narzuca jednego słusznego przepływu komunikacji pomiędzy powyższymi elementami. Co więcej, definiuje kilka, z których wybieramy ten właściwy dla nas. Natomiast abstrakcja głównej idei wygląda następująco:

  1. Klient potrzebuje w imieniu właściciela uzyskać dostęp do zasobu.
  2. W tym celu przekierowuje użytkownika do serwera autoryzacji, który poświadcza (np. udostępniając klientowi token dostępowy) uprawnienie do zasobu.
  3. Klient, wykonując żądanie dostępu do zasobu, do serwera zasobu dołącza poświadczenie uzyskane w punkcie 2.
  4. Serwer zasobu weryfikuje poprawność poświadczenia i udostępnia zasób.

To, który możliwy przepływ wybierzemy, jest w dużej mierze determinowane przez przyjętą przez nas architekturę delegacji.

Co się dzieje na granicy?

W systemach rozproszonych wyróżnić możemy dwa główne nurty delegacji.

Słaba delegacja (Poor Man’s Delegation). W tym wypadku użytkownik uzyskuje token bezpośrednio z serwera autoryzacji i dołącza go do każdego żądania, które API Gateway po prostu przekazuje dalej bez jakiejkolwiek ingerencji. Rozwiązanie to jest całkowicie bezstanowe, a komunikacja z serwerem autoryzacji ograniczona jest jedynie do nadawania tokenów. Minusem jest natomiast poważny problem z wylogowaniem użytkownika. Ponieważ token nadawany jest na określony czas, po jego kradzieży atakujący będzie mógł bez przeszkód działać w systemie tak długo, jak długo zachowana jest ważność tokenu. Kolejnym minusem tego podejścia jest pełna przejrzystość zawartości tokena dla użytkownika (token JWT można zdekodować zawsze, o ile nie jest on zaszyfrowany). Po zdekodowaniu możemy zobaczyć, jakie uprawnienia w systemie mamy dostępne. Co więcej, jeżeli występują błędy bezpieczeństwa w wykorzystywanych bibliotekach bądź też biblioteki te zostały nieprawidłowo zaimplementowane, zawartość tokenu można nie tylko odczytać, ale także modyfikować. Ponadto podejście to (z uwagi na potencjalnie duży rozmiar samego tokenu) może niekorzystnie wpływać na wydajność systemu.

Wymiana tokenów (token exchange). Tutaj sytuacja wygląda inaczej. Użytkownik, podobnie jak w pierwszym podejściu, otrzymuje od serwera autoryzacji token dostępowy. Nie jest on jednak bezpośrednio przekazywany w głąb systemu, ale wcześniej, na poziomie API gateway wymieniany na token wewnętrzny. Token publiczny może być albo bardzo prostym tokenem JWT, albo też losowym ciągiem znaków (obscure, jak na przykład identyfikator sesji). W wariancie z dwoma tokenami minusem jest związanie API Gateway bezpośrednio z serwerem autoryzacji i, co za tym idzie, nieco bardziej złożona architektura. Plusów mamy za to zdecydowanie więcej. Po pierwsze, znacząco uprościmy problem wylogowania użytkownika. Do API Gateway udostępniane są dwa tokeny: krótko żyjący token dostępowy (Access Token) i długo żyjący token odświeżający (Refresh Token). Ten drugi pozwala na ponowne pozyskanie tokenu dostępowego, bez ingerencji użytkownika (takiej, jak choćby kolejne logowanie) jednak po komunikacji z serwerem autoryzacji. Serwer ten przy okazji może sprawdzić, czy token nie został przypadkiem unieważniony (revoke). Ponadto, publiczny token jest znacznie mniejszy od wewnętrznego i nie niesie ze sobą żadnej użytecznej dla użytkownika informacji.

Uwierzytelnianie

Skoro OAuth nie jest narzędziem uwierzytelniania, to jak możemy sobie z tym zagadnieniem poradzić? Z pomocą przychodzi nam OpenID Connect. Jest to prosta warstwa identyfikacji (swoista nakładka) współdziałająca z OAuth. W dużym uproszczeniu polega ona na utworzeniu obok tokenu dostępowego (Access Token) także drugiego – identyfikacyjnego (ID Token). Zawiera on pełną (zapisaną w formie JWT) informację o użytkowniku, taką jak adres e-mail, imię, nazwisko, strefa czasowa etc.

Mamy token – i co dalej?

Jeżeli już omówiliśmy sposób pozyskiwania i przekazywania tokenów w głąb systemu, zastanówmy się, co dzieje się dalej. Serwis wewnętrzny, po otrzymaniu pełnego kontekstu wywołania (Access i ID Token), weryfikuje jego poprawność i dekoduje informacje. Dzięki temu wiemy, w imieniu jakiego użytkownika wykonywane jest żądanie, a także jakie posiada on uprawnienia. Dalszy przebieg procesu nie różni się niczym od obsługi użytkownika zalogowanego za pomocą klasycznej sesji.

Tak w zarysie wygląda koncepcja zapewnienia bezpieczeństwa w systemach rozproszonych. Należy jednak zaznaczyć, że same systemy autoryzacji i uwierzyteleniania, nawet poprawnie zaimplementowane nie gwarantują pełnego bezpieczeństwa. Użytkownik zapisujący hasło na kartce przyklejonej do monitora skutecznie niweczy pracę architektów. To jednak temat na zupełnie inny wpis.

W tym miejscu chciałbym też podziękować Błażejowi Bucko za jego recenzję i wkład merytoryczny w artykuł.

A Ty stosujesz jakąś ciekawą architekturę zabezpieczeń w swoim systemie? Zachęcam do podzielenia się doświadczeniami w komentarzach.

O siedmiu podstawowych błędnych założeniach przy projektowaniu systemów rozproszonych L. Peter Deutsch pisał już w roku 1994. Można się zatem zastanawiać, jak to możliwe, że ponad 20 lat później tak często się o nich zapomina. W czasach popularności publicznych chmur i architektury mikrousługowej, taka ignorancja wydaje się niczym innym jak świadomym proszeniem się o problemy. Co zatem powinniśmy zrobić, aby tych problemów uniknąć? Jak zapewnić odporność systemu (ang. resilience)?

Mikroserwisy, czy też mikrousługi, to jeden z największych buzzword’ów ostatnich lat. Okazuje się jednak, że jest z nim trochę jak z seksem nastolatków – wszyscy mówią, że to robią, ale w rzeczywistości doświadczenie mają tylko nieliczni. Co trzeba zrobić, żeby dobrze wykorzystać ten rodzaj architektury? Jakie warunki musimy spełnić, aby wniósł on coś więcej w naszą pracę niż tylko zapis w CV? O tym przeczytacie w tym wpisie.

Często obserwuję przydzielanie ludzi do projektów za pomocą algorytmu round-robin, czyli w zasadzie losowo. Nowo rekrutowanych czy kończących inne zadania pracowników wrzuca się w projekt, w którym aktualnie jest największe zapotrzebowanie. Czy to na prawdę najlepsza metoda? Jak można to zrobić lepiej, dowiesz się z poniższego artykułu.

Model rozwoju kompetencji

Współcześnie funkcjonuje wiele różnych modeli rozwoju kompetencji. Warto wspomnieć choćby o modelu czterech etapów nabywania kompetencji. Jest on bezpośrednio powiązany z efektem Dunninga-Krugera, który w zasadzie stanowi temat na osobny wpis. Tutaj natomiast chciałbym szerzej omówić model braci Dreyfus. Zakładają oni, że możemy wyróżnić pięć poziomów kompetencji:

  1. Novice (nowicjusz)
  2. Competence (kompetentny)
  3. Proficiency (biegły)
  4. Expertise (ekspert)
  5. Mastery (mistrz)

Nowicjusz. Dopiero zaczynamy przygodę z daną dziedziną. Działamy tylko w oparciu o teorię, wymagamy pełnej dekompozycji złożonych zadań, pozbawienia ich kontekstu (do analizy którego potrzebne jest doświadczenie) i ustalenia prostych reguł działania. Wszystkie decyzje podejmujemy analitycznie, ślepo podążając za regułami. Aby się rozwijać, potrzebujemy nadzoru – zewnętrznego i/lub opartego o własne obserwacje. W IT to ktoś, kto właśnie zaczyna pierwszą pracę. Szybko dowiaduje się, że może zapomnieć o tym, co umie, bo „tam były studia, a tu jest życie”. Często nie może zrozumieć, jak baza danych może działać bez trzeciej postaci normalnej.

Kompetentny. Nabraliśmy już trochę doświadczenia. Nadal, podobnie jak nowicjusz, decyzje podejmujemy analitycznie, wymagamy nadzoru i dekompozycji złożonych zagadnień, ale uczymy się już dobierać reguły w zależności od sytuacji oraz aplikować powtarzalne wzorce. W środowisku pilotów mówi się, że gdy zaczynamy latać, mamy dwa worki – pusty z doświadczeniem i pełny ze szczęściem. I cały problem polega na tym, żeby napełnić worek z doświadczeniem, zanim opróżni się ten ze szczęściem. W IT osoby kompetentne często są podatne na wpływ prostych przykładów i konferencyjnych prezentacji. Ponieważ nie znają szerszego kontekstu, nie potrafią przewidzieć długoterminowych konsekwencji podejmowanych decyzji. „Wdrożyłem transakcje rozproszone, bo na prezentacji człowiek mówił, że rozwiążą wszystkie nasze problemy”.

Biegły. Żadna typowa sytuacja nie jest już dla nas zaskoczeniem. Potrafimy odnaleźć się w zmieniających się okolicznościach. W IT oznacza to, że zaczynamy myśleć długofalowo. Wiemy, że pisanie testów i wykorzystanie architektury hexagonalnej szybko przyniesie wymierny efekt.

Ekspert. Nareszcie decyzje zaczynamy podejmować intuicyjnie. Do gry weszła pasja. Każdą wolną chwilę poświęcamy na zgłębianie tajników technologii. Już nie zastanawiamy się nad każdym krokiem, choć dalej potrzebujemy monitorowania postępów. Reguły zaczynają być dla nas transparentne. Nie skupiamy się już nad tym, jakie wzorce projektowe stosujemy – chcemy po prostu rozwiązać zadanie w najlepszy możliwy sposób.

Mistrz. Najwyższy stopień wtajemniczenia. Intuicja i pełne zatracenie się w realizowanych zadaniach. Teraz reguły nas ograniczają. Świadomie zaczynamy je łamać, aby zmaksymalizować efekty. W IT ciężko nam powiedzieć, jaką architekturę ma nasz system. Jest to miks wielu różnych elementów. Łamiemy popularne reguły lub zaczynamy tworzyć własne. Baza danych dawno nie ma postaci normalnej, często ma nawet kilka równoległych modeli. Do efektywnego działania potrzebujemy znajomości całego spektrum działań.

Podział na domeny

Zgodnie z podejściem Domain Driven Design, systemy dowolnego przedsiębiorstwa możemy podzielić na trzy rodzaje domen:

  • domena główna (core) – krytyczna dla działania firmy. To ona decyduje o naszej przewadze względem konkurencji i ma wpływ na podstawowe procesy biznesowe.  W przypadku portalu aukcyjnego to tutaj będzie mechanizm prowadzenia licytacji czy opisu przedmiotów;
  • domena wspierająca (supporting) – to, co pomaga nam prowadzić biznes, ale bez czego bylibyśmy w stanie się (przynajmniej czasowo) obejść. To tutaj trafią oceny sprzedających czy systemy rekomendacji dodatkowych produktów;
  • domena generyczna (generic) – wszystko, co możemy kupić gotowe i nie musimy tego pisać od podstaw. Systemy płatności, aplikacje CRM, live-chat z klientami etc.

Świadomość tego, jakie systemy wchodzą w skład jakich domen, pozwala nam świadomie zarządzać jakością. Wiadomo, że mimo najszczerszych chęci, nie wszystko będziemy w stanie zrobić idealnie. Całą mądrość polega na tym, żeby wiedzieć, w którym miejscu można odpuścić. Jeżeli gdzieś mamy zrezygnować z jakości, róbmy to w domenie wspierającej. Świadome dopuszczanie długu w głównej domenie to proszenie się o kłopoty. Można to porównać do brania kredytu we frankach szwajcarskich. Początkowo efekty są super, ale później okazuje się trudny bądź niemożliwy do spłacenia.

Warto wiedzieć, że wykorzystanie domeny generycznej zawsze jest dobrym pomysłem. W IT trwa permanentna rekrutacja, bo brakuje rąk do pracy. Jaki jest zatem sens wykorzystywania tych rąk do tworzenia czegoś, co już istnieje? Co więcej, istnieje jako główna domena działalności innej firmy, w związku z czym możemy założyć, że ktoś poświęci jej znacznie więcej czasu niż my kiedykolwiek będziemy mogli to zrobić. A do tego, prawie na pewno w dłuższej perspektywie wyjdzie taniej.

Kompetencje a domeny

Mamy już świadomość różnych poziomów kompetencji naszych pracowników, a także różnych potrzeb w zależności od działających w naszej firmie domen. Możemy zatem połączyć obie informacje i dokonać świadomego przydziału ludzi do projektów.

Ci najlepsi (poziom 4 i 5) powinni zająć się rozwojem właśnie domeny głównej. Będziemy stosowali takie techniki, jak Domain Driven Design, Test Driven Development, CQRS etc. Nie ukrywajmy, że nie każdy programista jest w stanie biegle wykorzystywać te podejścia. Oczywiście, nie da się obsadzić całego rozwoju (nawet ograniczonego do głównych procesów) ekspertami. Uzupełniamy zespół kompetentnymi pracownikami (poziom 3), ale nie zapominajmy, na kim spoczywa ciężar odpowiedzialności.

Domena wspierająca, poza tym, że obejmuje mniej newralgiczne części naszego biznesu, jest też ciekawym miejscem do eksperymentów. Jeżeli rozważamy wejście w nową, niesprawdzoną technologię, powinno ono mieć miejsce właśnie tutaj. Kto się tym zajmie? Całe grono mało lub średnio doświadczonych (poziomy 1-3) programistów, pilotowanych przez kilku ekspertów poziomu czwartego.

Serce nie sługa

Niewątpliwie, podejmowanie decyzji jedynie na podstawie merytorycznych przesłanek jest inżynierską utopią. Niestety, w prawdziwym życiu, pojawia się wiele zewnętrznych, trudno kontrolowanych czynników.

  • „X nie chce pracować z Y”
  • „Z nie chce się uczyć nowej domeny”
  • „W napalił się na Kafkę i zrobi wszystko, żeby wylądować w projekcie AnonimowyKafkoholik”

Pominięcie powyższych aspektów może, delikatnie mówiąc, doprowadzić do ogólnozakładowej katastrofy. Pamiętajmy zatem, że kompetencje techniczne, są oczywiście bardzo ważne, ale nie najważniejsze.

A może ktoś zna jeszcze lepszy sposób na alokowanie developerów? Zapraszam do podzielenia się nimi w komentarzu.

Rodzice zawsze mi powtarzali – „ucz się dziecko systematycznie”. Ale dzieci zawsze wiedzą lepiej. Z czasem każdy z nas przekonuje się jednak, że systematyczne podejście ma większy sens niż walka za pięć dwunasta. Co to ma wspólnego z wytwarzaniem oprogramowania? To właśnie temat tego wpisu.

Życie bez ciągłej integracji

Klasyczny proces rozwoju oprogramowania polega na tworzeniu osobnej gałęzi dla każdego projektu. Największym plusem takiego podejścia jest możliwość odłożenia w czasie decyzji, które projekty wejdą w skład wdrożenia. Zamiast planować długofalowo, możemy w oparciu o stan zaawansowania developmentu i testów integrować poszczególne projekty. A jakie są minusy? Scalenie (merge) gałęzi kodu, w których wykonano dużo zmian, jest dość czasochłonne. Zawsze pojawiają się konflikty, wynikające z tego, że jeden fragment kodu został w różny sposób zmodyfikowany w ramach kilku projektów. Liczba takich konfliktów i czas ich rozwiązywania są trudne do przewidzenia. W zależności od wielkości projektu i czasu życia scalanych gałęzi, potrafi to zająć kilka, a nawet kilkanaście dni. Co więcej, proces rozwiązywania konfliktów nie jest bezbłędny. Dość często dochodzi do przypadkowej ingerencji w poprawność procesów biznesowych. Pół biedy, jeżeli po połączeniu projektów wykonujemy pełną procedurę testową. Wtedy tracimy „tylko” czas. Niestety z reguły firmy ograniczają się do weryfikacji regresji. A błędy związane z nowymi zmianami odkrywane są dopiero po wdrożeniu na środowisko produkcyjne.

Co integrujemy?

Ciągła integracja to w zasadzie podstawa nowoczesnego prowadzenia projektów. Zakłada ona, że cały rozwój odbywa się w jednej gałęzi (branchu) kodu. Takie podejście bardzo ogranicza liczbę konfliktów i praktycznie do zera eliminuje problematyczne merge. Szansa, że podczas dwu-, trzymiesięcznej fazy developmentu, dwóch programistów zmodyfikuje tę samą metodę jest bardzo duża. Szansa, że zrobią to tego samego dnia, jest już zdecydowanie mniejsza. Właśnie na takiej systematyce opiera się ciągła integracja. Dodatkowo, skoro mamy już wszystkie zmiany w jednej gałęzi, możemy iść o krok dalej. Standardem jest regularne (nawet po każdej integracji) kompilowanie aplikacji i uruchamianie choćby podstawowego zestawu testów. Zbudowaną paczkę można od razu wdrożyć na środowisko stage’ingowe (preprodukcyjne). Tutaj można na bieżąco weryfikować implementowane zmiany.

Co znaczy ciągle?

Tutaj ważny jest zdrowy rozsądek. Integrowanie zmian co miesiąc jest lepsze niż co dwa miesiące. A robienie tego raz w tygodniu jest lepsze niż raz w miesiącu. Ideałem, do którego dążymy, jest wykonywanie tego codziennie. Standardowy proces polega na pracy w branchu utworzonym specjalnie na potrzeby danej zmiany. Następnie tworzymy pull-request (czyli żądanie integracji). Zmieniony kod jest przeglądany (proces code-review), w celu wykrycia potencjalnych błędów, niespójności czy możliwych usprawnień. W zależności od wyniku przeglądu wraca do poprawy lub jest integrowany. Czy koniecznie trzeba to robić na koniec każdego dnia pracy? Oczywiście nie. Jeżeli wykonanie sensownej zmiany zajmie dwa dni, to właśnie po takim czasie rozpoczynamy proces.

Duże zmiany

A jak radzić sobie z dużymi zmianami? Nie wszystkie prace da się przecież podzielić na małe zadania. Czy wtedy rezygnujemy z ciągłej integracji? Oczywiście, że nie. Zastanówmy się, czy wrzucenie nieskończonego lub nawet niedziałającego kodu jest problematyczne? Na pierwszy rzut oka tak. Jednak czy na pewno? Problem stanowi tylko użycie takiego kodu. Tak długo, jak nie jest on wywoływany, wszystko jest w porządku. Najprostszym sposobem byłoby jego zakomentowanie, jednak to wyklucza nawigowanie po nim czy testowanie go. Zdecydowanie lepszym pomysłem jest zastosowanie feature flag. Działają one na zasadzie przełącznika, który aktywuje lub dezaktywuje określone fragmenty kodu. To z kolei daje nam realną możliwość zastosowania ciągłej integracji, nawet w przypadku długo trwających implementacji.

Jeżeli myślimy o pracy w metodykach zwinnych, to właśnie od wdrożenia praktyki ciągłej integracji powinniśmy zacząć. Bez tego uzyskanie szybkiego feedbacku, który jest podstawą agile, nie będzie możliwe.

Świat się zmienia. Ciężko z tym polemizować. Wiek pary, wiek elektryczności, wiek komputerów. Obecnie znajdujemy się w okresie określanym jako „industry 4.0”, w którym dzięki szerokiemu zastosowaniu internetu, automatyzacji i przetwarzania danych, powoli zaciera się granica między człowiekiem a maszyną. Jak wpłynie to na kształt znanego nam IT? Analiza w dalszej części wpisu.

O długach i kredytach słyszymy na każdym kroku. W dobie wszechobecnego konsumpcjonizmu karty kredytowe, leasingi i kredyty hipoteczne to codzienność. O długu technicznym czy technologicznym też słyszał chyba każdy, kto ma styczność z projektami informatycznymi. Niemniej jednak, przemyślane i niebanalne podejście do tematu widuje się zaskakująco rzadko.  Jak zdefiniować dług techniczny, co różni go od zwykłego niedbalstwa oraz jak możemy go mierzyć – to wszytko wyjaśnię w poniższym wpisie.

Czym jest dług?

Dług jest pewnego rodzaju zobowiązaniem, czy może raczej obowiązkiem spłaty tego zobowiązania na rzecz wierzyciela. Zwyczajowo od długu nalicza się odsetki, które podwyższają podstawowe zobowiązanie. Im szybciej taki dług spłacamy, tym mniejsze odsetki oddajemy. Podobnie sprawa wygląda z długiem technicznym. W tym wypadku odsetki przybierają formę dłuższego czasu wymaganego do wdrożenia zmian. Przyczyny występowania długu technicznego są różne. Czasem wprowadza się go świadomie poprzez wdrażanie rozwiązań tymczasowych czy też z uwagi na istotne okoliczności zewnętrzne. Z reguły jednak jest on konsekwencją presji biznesowej, błędnych decyzji projektowych, przekładania w nieskończoność refactoringu czy aktualizacji zależności.

Czym dług techniczny nie jest?

Jeżeli dług ma mieć jakikolwiek sens, musi, tak jak kredyt, nieść ze sobą jakąś korzyść. Tak samo zaciągnięcie długu technicznego musi gwarantować solidny zysk. Nie jest to więc efekt złych praktyk, wynikających zazwyczaj z lenistwa bądź braku umiejętności. Jeżeli nie piszemy testów i świadomie rezygnujemy z techniki Test Driven Development, to stwierdzenie, że robimy to, „żeby było szybciej”, jest po prostu zakłamywaniem rzeczywistości. Nawet jeżeli samo napisanie kodu będzie trwało krócej (z czym zresztą praktycy TDD się nie zgodzą), to już jego wdrożenie na produkcję czy też pierwsza modyfikacja cały ten „bonus” pochłonie. Podobnie jest z ignorowaniem podstawowej struktury kodu. Tworzenie ogromnych metod (mających po kilka tysięcy linii), dlatego, że nie ma czasu na refactoring? A tak szczerze – ile zajmuje wydzielenie fragmentu do osobnej metody? 5 sekund? Nie oszukujmy się zatem, że nie robimy tego z powodu braku czasu.

Metryki długu technicznego

Jak mierzycie w projekcie dług techniczny? „MD to fix” wyświetlane przez SonarQube? To wyjątkowo popularna metoda, ale według mnie wyjątkowo bezwartościowa. Nie jest to liczba, która realnie jest w stanie wpłynąć na czas dostarczania zmian, jakość procesu wytwórczego czy też bezpieczeństwo aplikacji. Jak inaczej możemy ugryźć temat pomiaru długu? Przede wszystkim należy zastanowić się, w czym nam on w ogóle przeszkadza. Według mnie podstawowy problem dotyczy właśnie czasu wykonania zmian, zarówno ze względu na pracochłonność ich implementacji, jak i czas potrzebny na kompilacje i wdrożenie. Jak można przełożyć to na liczby?

„Czas potrzebny na podbicie wersji zależności po publikacji CVE. Przyjmijmy wartość 15 minut. Co gwarantuje ta metryka? Przede wszystkim szybką reakcję na opublikowanie podatności bezpieczeństwa, w wykorzystywanych przez nas bibliotekach czy narzędziach. Powiedzmy sobie szczerze, że w tak krótkim czasie możemy zaktualizować się tylko o wersję patch (zgodnie z wersjonowaniem semantycznym). Zatem, aby utrzymać tak rygorystyczne kryteria, musimy regularnie dbać o aktualizację wersji minor i major, aby cały czas wykorzystywać linię zapewniającą stałe wsparcie dostawcy/autorów. Samo podbicie zależności to jednak dopiero połowa sukcesu. Należy jeszcze wdrożyć zaktualizowaną paczkę na produkcję. I tu pojawia się druga metryka.

„Czas od commitu (wykonania) zmiany do wdrożenia jej na wszystkie instancje produkcyjne”. Znów 15 minut. Tym razem dbamy o to, aby kompilacja aplikacji była możliwie szybka. Następnie wykonujemy wdrożenie na środowisko staging, tam testujemy podstawowe procesy biznesowe. Po prawidłowym zakończeniu testów rozpoczynamy wdrożenie produkcyjne. Oczywiście bez pełnej automatyzacji uzyskanie tak krótkiego czasu byłoby niemożliwe. Zarówno testy, jak i wdrożenia czy restarty, muszą być wykonywane bez ingerencji człowieka. To zapewnia także powtarzalność i dobrą estymację czasu wykonania.

Także strukturę kodu możemy pośrednio kontrolować za pomocą metryk. „Procent klas oznaczonych jako publiczne”. W skrócie – im więcej klas jest wystawionych publicznie, tym łatwiej doprowadzić system do postaci spaghetti. Klasa upubliczniona, czyli dostępna z dowolnego miejsca w systemie. Często właśnie z lenistwa upubliczniamy więcej niż trzeba. W Javie jest to głównie związane z domyślnymi ustawieniami IDE, które każdą klasę oznaczają jako publiczną. Sama zmiana tego jednego szablonu daje ogromną poprawę jakości tworzonego kodu. Jednak nawet wtedy trzeba pilnować się, aby otwierać jedynie te klasy, które faktycznie są interfejsem danego pakietu/modułu, a nie te, których kawałek akurat przyda się w innym miejscu.

A jak w Waszych projektach wygląda mierzenie się z długiem technicznym? Zachęcam do dyskusji w komentarzach.