X. Jednolitość środowisk

Utrzymuj środowisko developerskie, stagingowe i produkcyjne tak podobne jak tylko możliwe

Z doświadczenia wiadomo, że od zawsze istniały różnice pomiędzy środowiskiem developerskim (developer pracujący nad swoją lokalną wersją kodu aplikacji) a produkcyjnym (działająca aplikacja dostępna dla użytkowników. Ze względu na ich charakter, możemy wymienić trzy rodzaje różnic:

Aplikacja 12factor jest zaprojektowana tak by można ją było bez przerwy wdrażać na produkcję minimalizując różnice pomiędzy środowiskami. Mając na uwadze powyższe różnice, można sobie z nimi radzić na różne sposoby:

Podsumowując w formie tabeli:

Tradycyjna aplikacja Aplikacja 12factor
Czas pomiędzy wdrożeniami Tygodnie Godziny
Tworzenie kodu vs wdrażanie kodu Różne osoby Te same osoby
Środowisko developerskie vs produkcyjne Mocno różniące się Jak najbardziej zbliżone

Zachowanie podobieństw między wdrożeniami jest ważne w przypadku usług wspierających takich jak baza danych aplikacji, system kolejkowania czy też cache. Wiele języków oferuje biblioteki, które upraszczają korzystanie z usług wspierających w tym adaptery do usług różnego typu. Kilka przykładów w tabeli poniżej:

Typ Język Biblioteka Adaptery
Baza danych Ruby/Rails ActiveRecord MySQL, PostgreSQL, SQLite
Kolejka Python/Django Celery RabbitMQ, Beanstalkd, Redis
Cache Ruby/Rails ActiveSupport::Cache Pamięć, system plików, Memcached

Czasami zdarza się, że developerzy w swoim lokalnym środowisku wolą korzystać z “lżejszych” wersji różnych usług, na produkcji natomiast używając bardziej zaawansowanych narzędzi. Przykładem takiej sytuacji jest używanie lokalnie SQLite, a PostgreSQL na produkcji. Podobnie wygląda też użycie pamięci podręcznej procesu na środowisku developerskim do cachowania pamięci, zamiast Memcached znajdującego się na produkcji.

Developer postępujący zgodnie zasadami 12factor opiera się pokusie używania usług różniących się pomiędzy środowiskami, nawet wtedy, gdy adaptery teoretycznie ukrywają różnice w implementacji pod warstwą abstrakcji. Z powodu różnic pomiędzy usługami wspierającymi mogą pojawić się niezgodności, powodując, że kod, który działał i był testowany lokalnie lub na stagingu, przestanie funkcjonować na produkcji. Pojawianie się tego typu błędów negatywnie wpływa na proces ciągłego wdrażania aplikacji. Czas stracony na wykrywaniu takich błędów i w konsekwencji awarii podczas wdrażania aplikacji może sporo kosztować, zwłaszcza gdy podobne problemy będą się z czasem gromadzić.

Lekkie wersje usług w obecnych czasach nie są już tak atrakcyjne jak kiedyś. Nowoczesne usługi takie jak Memcached, PostgreSQL oraz RabbitMQ nie są trudne do instalacji w lokalnym środowisku, dzięki narzędziom jak Homebrew i apt-get. Innym rozwiązaniem są narzędzia do deklaratywnego provisioningu takie jak Chef czy Puppet połączone z lekkimi środowiskami wirtualnymi jak np. Vagrant. Pozwala ono developerom na uruchamianie lokalnych środowisk, które są bardzo zbliżone do produkcyjnych. Koszt instalacji i używania takich rozwiązań jest stosunkowo niski, biorąc pod uwagę korzyści płynące z utrzymywania jednolitych środowisk i procesu ciągłego wdrażania aplikacji.

Adaptery dla różnych usług wspierających są wciąż użyteczne, gdyż dzięki nim zmiana usługi jest relatywnie łatwa. Należy jednak pamiętać, że wszystkie wdrożenia aplikacji (środowiska developerskie, stagingowe, produkcyjne) powinny używać tych samych typów i wersji usług wspierających.