Nieraz słyszymy, że ktoś ma na smartfonie jakąś nową apkę, która rozwiązuje mu jakieś codzienne problemy lub coś ułatwia. Aplikacja do zamawiania taksówki, zamawiania jedzenia, kupna biletów do kina, nawigacji czy czego tam jeszcze. Ale tak naprawdę aplikacje mobile (czyli te zainstalowane na naszych smartfonach) to dość często programy do zdalnej obsługi aplikacji webowej, która działa gdzieś w Internecie. W takim podejściu aplikacja mobilna jest tworzona głownie dla wygody użytkownika, żeby w przyjemny i prosty sposób mógł za jej pośrednictwem korzystać z aplikacji webowej. Bez aplikacji webowej, aplikacja mobilna traci sens. Dostęp do Internetu mamy praktycznie wszędzie, więc nawet nie zawsze się zastanawiamy, która z naszych aplikacji mobilnych rozmawia z tą webową, a która działa samodzielnie. A to właśnie aplikacje webowe tak naprawdę wykonują największą pracę i rozwiązują problemy, po czym wysyłają tym mobilnym wyniki, które potem można ładnie wyświetlić. Opowiedzmy sobie czym są aplikacje webowe i jak się je rozwija.
Zacznijmy od podstaw. Aplikacja webowa to zbiór aplikacji serwerowych, które współpracują ze sobą w jakimś celu. Aplikacja webowa może składać się z takich aplikacji serwerowych jak baza danych, aplikacja do obsługi logiki biznesowej, aplikacja do obsługi płatności i dajmy na to aplikacja, która umożliwia wyświetlenie interaktywnej strony internetowej. Skoro aplikacja webowa składa się z kilku aplikacji serwerowych, możemy te serwerowe nazwać komponentami aplikacji webowej. Mamy zatem kilka komponentów, które działają na jednym serwerze lub kilku. Część z nich możemy rozwijać osobiście, a część możemy po prostu uruchamiać, korzystając z gotowych rozwiązan. Zadaniem programistów jest zapewnić odpowiednie sposoby komunikacji między tymi komponentami, a zadaniem administratorów i inżynierów chmury jest odpowiednio przygotować infrastrukturę i ją nadzorować.
Co do zasady aplikacje się pisze. Języków programowania jest wiele i zmieniają się z czasem jak prawie wszystko w tej branży. Możemy jednak na tym etapie wyróżnić dwie grupy języków.
Pierwsza grupa to języki interpretowane. Kiedy programista napisze kod takiej aplikacji, aby go uruchomić potrzebuje tak zwanego interpretera. Interpreter to taki program, który czyta kod źródłowy w formie tekstowej linijka po linijce i wykonuje na tej podstawie zaprogramowane instrukcje. Możemy zatem mieć cały kod źródłowy aplikacji, ale jeśli nie dysponujemy interpreterem, nic nam ten kod nie da, bo go nie uruchomimy. Poza interpreterem i kodem źródłowym aplikacji napisanej w takim języku musimy również posiadać biblioteki, których nasz kod używa. Biblioteka to po prostu zbiór funkcji i parametrów, z których korzysta aplikacja. Gdyby napisanie każdego programu na świecie wiązało się z tym, aby trzeba było za każdym razem na nowo określać standardy matematyki, zapisu do plików, odczytu, kalendarza i setek innych rzeczy, które wykonuje nasz program, proces rozwoju aplikacji trwałby długie lata. Dla tego, tworzy się biblioteki i w nich właśnie zamyka się jakiś zestaw funkcjonalności, który później inni programiści będą mogli wykorzystać w swoich projektach. Biblioteki mogą być zatem rozwijane niezależnie od aplikacji i używane w wielu różnych, niezwiązanych ze sobą aplikacjach.
Druga grupa języków to języki kompilowane. W tym przypadku zamiast interpretera potrzebny jest kompilator. Kompilator to również program, który czyta kod źródłowy aplikacji, ale zamienia go w tak zwany kod maszynowy. Ten właśnie proces nazywamy “kompilowaniem” kodu. Czyli innymi słowy z kodu źródłowego tworzymy plik, którego możemy już uruchomić. Po przeprowadzeniu kompilacji, do uruchomienia takiego programu kompilator już nie jest potrzebny. Jednakże i w tym przypadku zazwyczaj potrzebne są biblioteki. Biblioteki takich języków również są kompilowane, ale już nie możemy ich uruchomić bezpośrednio, ale za pośrednictwem napisanego przez nas programu, możemy skorzystać z ich funkcjonalności.
Skoro już wiemy, że mamy dwa rodzaje języków programowania do tworzenia aplikacji webowych, możemy się zastanowić jak się te aplikacje rozwija. Przyjmijmy na potrzeby tego odcinka, że rozwijam bardzo prostą aplikację webową, składającą się z tzw. “frontendu” czyli komponentu wyświetlającego interfejs graficzny i przyjmującego żądania od moich klientów, “backendu” czyli komponentu z zaprogramowaną całą logiką biznesową mojej aplikacji webowej oraz z bazy danych, która przechowuje dane o moich użytkownikach i wszelkie inne dane potrzebne do działania całej aplikacji. Aplikacje webowe oczywiście bywają znakomicie bardziej rozbudowane, ale my skupmy się na prostej aplikacji składającej się z tych trzech komponentów. Dwa z nich ewidentnie muszą być rozbudowywane przeze mnie, frontend i backend. To właśnie te komponenty stanowią o istocie mojej aplikacji webowej. Frontend wyświetla cały interfejs graficzny, składa żądania klientów w spójne struktury i wysyła je do backendu. Backend otrzymuje żądanie, konstruuje zapytanie do bazy danych, wysyła je i dalej wykonuje działania z otrzymanymi wynikami. Jeśli jest taka potrzeba to wysyła kilka zapytan do bazy. Kiedy już otrzyma wszystkie odpowiedzi, ponownie pakuje spójną odpowiedź i wysyła ją do frontendu. Frontend rozumiejąc odpowiedzi otrzymane z backendu wyświetla wyniki. Zobrazujmy to sobie na przykładzie. Niech ta rozwijana przeze mnie aplikacja webowa będzie służyła do rezerwowania i wypożyczania sprzętu narciarskiego. Wchodzimy na stronę internetową, na której można moją aplikację znaleźć. Naszym oczom się ukazuje ładny i kolorowy interfejs użytkownika. Jest to ta część frontendu, która pokazuje nam pola tekstowe lub inne, które możemy wypełniać określając w ten sposób to, co byśmy chcieli wypożyczyć. Kiedy w odpowiednie pola wpiszemy, że potrzebne są nam narty o konkretnych wymiarach oraz kask w rozmiarze S i kolorze zielonym, frontend zbierze te wszystkie informacje i wyśle do backendu. Backend wie jak sobie z takim żądaniem poradzić. Na podstawie podanych preferencji tworzy odpowiednie zapytanie do bazy danych, aby się dowiedzieć czy taki sprzęt jest dostępny w magazynie. Baza danych odpowiada, że narty będą dostępne, ale kasku w rozmiarze S i kolorze zielonym nie znaleziono. Backend zatem widząc, że nie ma kasku o takim kolorze, może wysłać ponowne zapytanie do bazy, ale już bez kryterium dotyczącego koloru. Kiedy otrzyma jakieś wyniki kompletuje odpowiedź do frontendu w którym podaje listę dostępnych nart (ponieważ głownie to nart szukaliśmy) i informację, że kasku w kolorze zielonym nie znaleziono, ale znaleziono kaski w innych kolorach. Dostarczy również listę znalezionych kasków. Frontend otrzymując tę odpowiedź wie jak zgrabnie przeprosić klienta za niedogodności i subtelnie zasugerować mu inne kaski. W opisanym przykładzie to ja jestem odpowiedzialny za frontend i backend. Bazę danych mogę sobie uruchomić jaką chcę. Obecnie są dostępne rozmaite bazy danych, więc nie ma większego sensu ich tworzyć samodzielnie. Jak zatem rozwija się te dwa pozostałe komponenty?
Załóżmy, że frontend rozwija zespół programistów Pythona, jednego z najpopularniejszych języków interpretowanych. Backend niech rozwija zespół programistów języka Go. Oba komponenty są rozwijane oddzielnie. Oczywiście nie jest możliwe w pełni uniezależnić od siebie te zespoły, ale do pewnego stopnia daje się to zrobić. Powiedzmy, że gdzieś w backendzie znaleziono błąd, który właśnie jeden z programistów poprawił. Wysyła zatem swoje zmiany w kodzie do systemu kontroli wersji kodu. Deklaruje, że zmiany, które wprowadził są gotowe do zaakceptowania i wprowadzenia do backendu na stałe. Ktoś z zespołu ogląda jego zmiany i w razie potrzeby nanosi pewne uwagi. Tym razem jednak nie ma uwag, ponieważ zmiana jest niewielka a błąd, jeśli nie zostanie szybko naprawiony, może spowodować wyciek danych osobowych klientów. Kolega z zespołu naszego bohatera zatem akceptuje zmiany w kodzie i wprowadza je na stałe do kodu tego komponentu. Na tym kroku konczą się wszelkie interakcje ludzkie. Wszystkie dalsze losy kodu są już zautomatyzowane.
Kiedy tylko system kontroli wersji kodu źródłowego odnotuje opisaną wyżej zmianę, uruchamia coś co w naszej nomenklaturze nazywane jest “pipeline” co można by dosłownie przetłumaczyć na słowo “rurociąg”. Niestety jednak polskie odpowiedniki nie zawsze się przyjmują w branżowej terminologii. Rzeczony „pipeline” jest procesem, który jest wykonywany, kiedy zajdą odpowiednie okoliczności. Zmiana kodu jak najbardziej może być jedną z takich okoliczności. W trakcie tego procesu, zachodzi bardzo wiele czynności. Między innymi komponent jest automatycznie kompilowany z użyciem nowego kodu oraz testowany. Potem cała aplikacja webowa jest umieszczana w specjalnie przygotowanym środowisku testowym, aby przeprowadzić testy integracji pomiędzy pojedynczymi komponentami. Następnie przeprowadzane są testy systemowe, które mają wykonać pewne działania oddziałujące na całość aplikacji w poszukiwaniu błędów. Potem są wykonywane kolejne testy a potem jeszcze więcej testów. Ogólnie rzecz ujmując im więcej testów zostanie przeprowadzonych, tym większa szansa, że błędy zostaną wykryte na wcześniejszym etapie, a nie wtedy, kiedy już aplikacja będzie dostępna dla klientów. Kolejność i zakres testów oczywiście zależy od konkretnej aplikacji webowej i od decyzji inżynierów testów.
W trakcie tego procesu również poza testami zachodzi coś, co dzisiaj stanowi rdzen nowoczesnego dostarczania aplikacji webowych. Zachodzi mianowicie konteneryzacja. Konteneryzacja to proces mający na celu zamknięcie komponentu aplikacji w specjalnie spreparowanym obiekcie, zwanym “kontenerem”. Kontener imituje system operacyjny i umożliwia uruchomienie w nim tego komponentu. Oczywiście można zadać pytanie po co w ogóle to robić, skoro możemy uruchomić dany komponent aplikacji webowej bez dodatkowych komplikacji. Zalet wbrew pozorom jest bardzo wiele. Przede wszystkim można za pomocą odpowiedniego zarządzania kontenerami uzyskać sporą odporność na awarie. Specyfika kontenera jest taka, że jeśli program w nim działający się wyłączy, system zarządzający kontenerami zauważy to zdarzenie i natychmiast zarządzi ponowne jego uruchomienie. Będzie ten proces powtarzać do skutku. Zatem jeśli komponent działa względnie prawidłowo i rzadko miewa awarie, nie powinno to zaszkodzić znacząco stabilności całej aplikacji webowej. Drugą zaletą stosowania kontenerów jest łatwość w migrowaniu aplikacji. Jeśli każdy komponent aplikacji działa jako kontener, przeniesienie go jest bardzo proste. Nieraz duże firmy posiadające bardzo duże aplikacje działające w chmurze decydowały się na zmianę dostawcy chmury. Przenoszenie kontenerów było w tym przypadku dość proste, czego nie zawsze można było powiedzieć o aplikacjach, które nie zostały skonteneryzowane. Trzecią zaletą jest potencjalna skalowalność. Kontener to co do zasady mały fragment aplikacji webowej, który powinien być w stanie działać zarówno w pojedynkę jak i jako jeden z setek takich samych kontenerów. Dla systemu zarządzającego kontenerami uruchomienie jednego czy setki kontenerów to jedynie kwestia ustawienia jednej liczby w konfiguracji. Możliwe jest również dynamiczne skalowanie liczby kontenerów w zależności od obciążenia aplikacji. Czwarta zaleta to utrzymanie porządku na serwerach. W dawnych czasach aktualizacja aplikacji bywała dość karkołomna. Tym bardziej jeśli programiści zaktualizowali jakieś biblioteki w swoich kodach albo zaktualizowali interpreter. Ogólnie z bardzo wielu powodów wszelkie zmiany na serwerze nieraz stanowiły wyzwanie, ponieważ trzeba było zmierzyć się z całą masą zależności. Kontenery rozwiązują ten problem już w przedbiegach. To właśnie w trakcie procesu konteneryzacji zarówno komponent aplikacji, jego biblioteki i wszelkie zależności, łącznie z interpreterem, jeśli jest potrzebny, są umieszczane wewnątrz kontenera. Innymi słowy prawidłowo zbudowany kontener powinien mieć absolutnie wszystko co jest potrzebne do uruchomienia komponentu, ale absolutnie nic więcej. Zatem aktualizacja aplikacji w systemie zarządzającym kontenerami wiąże się tylko z jednym czy kilkoma wpisami dotyczącymi wersji kontenera w konfiguracji, zamiast godzinami pracy administratora. System po prostu odnotuje, że od tej pory ma uruchamiać nowszy kontener i podmieni starsze kontenery na nowsze.
Wróćmy do naszego “rurociągu”, zwanego znacznie częściej “pajplajnem”. W trakcie tego procesu, kiedy już zmiany wprowadzone przez bohatera naszej opowieści zostały zaakceptowane, zachodzą liczne testy oraz konteneryzacja. Jeśli wszystko pójdzie zgodnie z planem nowa wersja backendu stanie się dostępna dla klientów, kiedy tylko proces dobiegnie koca. Do decyzji twórców takiej automatyzacji pozostaje czy udostępnienie klientom nowej wersji zajdzie od razu czy po jawnym wyrażeniu zgody na podmiankę.
Na dwie rzeczy warto zwrócić tutaj uwagę. Zespół programistów może prawie w pełni skupić się na rozwijaniu kodu. Pensje programistów wynoszą tyle ile wynoszą i dla tego świetnym sposobem na maksymalne wykorzystanie czasu specjalistów jest po prostu odsunięcie ich od problemów związanych z dostarczaniem usługi. Programiści jedynie dostarczają kod a każde ich poszczególne zadanie się kończy, kiedy testy przeprowadzane przez pipeline zakończą się pomyślnie. A skoro nie muszą testów wykonywać manualnie, będą mogli zaczynać kolejne zadania, skoro w tym czasie automatyczne testy będą zachodzić gdzieś na jakimś serwerze daleko stąd. Druga rzecz, o której warto nadmienić to fakt, że cała ta automatyzacja zdaje się wykonywać czynności z natury nudne. Chyba każdy pracownik, szczególnie bardziej ambitny, gdyby musiał kilka razy dziennie skopiować kod, skompilować go, przeprowadzić ręcznie kilkanaście testów, konteneryzować i tak dalej, dość szybko pożegnałby się z taką pracą. Automatyzacja ma przejmować od ludzi zadania nudne, żeby właśnie mogli zająć się tymi bardziej kreatywnymi czynnościami.
W dzisiejszych czasach przy rozwijaniu jakiegokolwiek kodu zmierzamy do pełnej automatyzacji praktycznie wszelkich procesów. Przyświeca temu prosta zasada “jeśli musiałeś coś zrobić więcej niż raz, to znaczy, że warto dla tego procesu rozważyć wdrożenie automatyzacji”. Chmura oraz rozmaite platformy umożliwiły automatyzację bardzo wielu procesów. Od takich prostych czynności jak powiadamianie o awariach, do stawiania i modyfikowania ogromnych infrastruktur w chmurze. Wiele firm zauważyło znaczące przyspieszenie w rozwijaniu swoich aplikacji, dzięki automatyzacji. Bez wątpliwości trend ten będzie zachowany. Pytanie tylko co nas czeka już w niedalekiej przyszłości, ponieważ technologie już dziś rozwijają się tak szybko, że trudno przewidzieć nawet najbliższe lata.