Jak ogarnąć różnice w konfiguracji systemu, przy pomocy Vagranta i Ansible
Pracując w zespole, często zdarza się, że uruchamiając tą samą aplikację na różnych konfiguracjach systemowych, u każdego developera działa ona inaczej. U jednego dostajemy HTTP500, u drugiego występują problemy z połączeniem z bazą danych, a trzeci ma zainstalowaną złą wersję PHP, czy Node.js i aplikacja zwyczajnie nie wstaje. Nierealnym jest zmuszenie developerów do utrzymywania identycznej konfiguracji oprogramowania na swoim komputerze, trzeba więc znaleźć sposób, na wyizolowanie aplikacji do wirtualnego środowiska, które zawsze będzie miało identyczną konfigurację. Z pomocą przychodzi Vagrant.
Mateusz Cholewka. Full-stack Developer w krakowskim software housie Owls Department. Młody dynamicznie rozwijający się programista języków PHP oraz JavaScript. Zafascynowany tematyką DevOps. Autor bloga blog.webmtk.pl, na którym pisze o technologiach webowych.
Vagrant, to tak naprawdę coś w rodzaju nakładki, interfejsu na narzędzie do wirtualizacji. Współpracuje z kilkoma popularnymi providerami, takimi jak VirtualBox, VMware (tylko w płatnej wersji), Hyper-V, a nawet Docker. Ja skupię się na tym pierwszym, chyba najczęściej wykorzystywanym z Vagrantem VirtualBox’ie. Vagrant ułatwia nam zarządzanie maszynami wirtualnymi, przez dodanie CLI, łatwej konfiguracji przez Vagrantfile, oraz prostego zaopatrywania maszyn, udostępniania katalogów itd…
Spis treści
Co daje nam Vagrant?
Użycie Vagranta w projekcie, umożliwia pozbycie się problemu z różną konfiguracją systemową na komputerach developerów, a jednocześnie umożliwia stworzenie konfiguracji identycznej, jak na środowisku produkcyjnym. Vagrant uruchamia maszynę wirtualną z wybranym przez nas systemem operacyjnym, udostępnia wybrany katalog z kodem naszej aplikacji, oraz uruchamia skrypty zaopatrujące naszą maszynę w odpowiednie biblioteki, paczki, narzędzia itp. Dzięki temu mamy pewność, że za każdym razem dostajemy identyczne środowisko. Konfigurując Vagranta, ułatwiamy sobie również deploy aplikacji, ponieważ skrypty zaopatrujące możemy wykorzystać do zaopatrzenia środowiska produkcyjnego, co bardzo przyspiesza prace i zmniejsza ryzyko błędów na tym etapie.
Co z zaopatrzeniem?
Pozostaje jeszcze kwestia samych skryptów zaopatrujących. Vagrant podczas uruchomienia, jest w stanie uruchomić zwyczajne skrypty bashowe, ale z doświadczenia wiem, że ich tworzenie i debugowanie jest upierdliwe. Warto więc skorzystać z któregoś z dostępnych narzędzi do automatyzacji, które pozwalają zapomnieć o niektórych drobiazgach, a do tego dostępne są do nich gotowe skrypty wykonujące konkretne rzeczy za nas. Vagrant obsługuje takie narzędzia jak Ansible, CFEngine, Chef, Puppet, Salt. Jeśli jakiegoś narzędzia domyślnie nie obsługuje, to nic nie stoi na przeszkodzie, aby uruchomić go przy pomocy skryptu basha.
Instalacja
Aby zacząć używać Vagranta, będziemy musieli zainstalować dwa narzędzia. Pierwsze z nich, to VirtualBox, którego możemy pobrać z oficjalnej strony: virtualbox.org, natomiast drugie narzędzie to Vagrant, którego pobierzemy tak samo z oficjalnej strony producenta: vagrantup.com. Do samego Vagranta, warto od razu zainstalować plugin “vagrant-vbguest”, który automatycznie zainstaluje na maszynie gościa dodatki do obsługi VirtualBoxa:
vagrant plugin install vagrant-vbguest
Po zainstalowaniu tych dwóch programów, możemy przystąpić do pracy.
Struktura projektu
Człowiek najlepiej uczy się w praktyce, dlatego też przedstawię przykładową konfigurację Vagranta na przykładzie konfiguracji pod aplikację w języku PHP. Struktura naszego projektu jest tak naprawdę dowolna i w przykładach będzie ona wyglądała tak:
project/ ├── code/ ├── provision/ │ ├── files/ │ │ └── env │ ├── provision.yml │ ├── requirements.yml │ └── vars.yml └── Vagrantfile
W katalogu projektu znajduje się katalog code, w którym umieszczam kod aplikacji, w katalogu provision poszczególne pliki Ansible, pliki konfiguracyjne aplikacji, oraz plik Vagrantfile z konfiguracją.
Vagrantfile
Konfiguracja maszyny odbywa się przy pomocy pliku Vagrantfile. Vagrant automatycznie rozpoznaje taki plik i na jego podstawie konfiguruje maszynę. Przykładowy Vagrantfile:
Vagrant.configure("2") do |config| config.vm.box = "debian/jessie64" config.vm.network "forwarded_port", guest: 80, host: 8080 config.vm.network "forwarded_port", guest: 3306, host: 3333 config.vm.synced_folder "./code", "/var/www", :owner => 'www-data', :group => 'www-data', :mount_options => ["dmode=755","fmode=644"] config.vm.provider "virtualbox" do |vb| vb.memory = "1024" end config.vm.provision "shell", inline: <<-SHELL sudo apt-get update sudo apt-get upgrade -y sudo apt-get install -y python-setuptools sudo easy_install pip sudo pip install ansible SHELL config.vm.provision "ansible_local" do |ansible| ansible.galaxy_role_file = "./provision/requirements.yml" ansible.playbook = "./provision/provision.yml" end end
W pierwszej linii pliku, otwieramy konfigurację Vagranta i podajemy jej wersję. Nowe wersje konfiguracji Vagranta, mogą znacząco się od siebie różnić, dlatego twórcy wymyślili, że w każdym pliku musimy zadeklarować jej wersję, tak aby zachować kompatybilność wsteczną.
W trzeciej linii deklarujemy, jaki obraz systemu chcemy wykorzystać. Vagrant pozwala tworzyć swoje własne tzw. “base box”, lub użyć boxa z oficjalnej chmury app.vagrantup.com, gdzie znajdziemy zarówno oficjalne, jak i modyfikowane przez społeczność boxy. Najpopularniejszym systemem używanym do stworzenia wirtualnej maszyny jest ubuntu. Osobiście preferuję debiana, ale w przypadku tych dwóch systemów nie ma to aż tak dużego znaczenia, ponieważ oba te systemy pochodzą z jednej rodziny, więc ich zaopatrywanie i konfiguracja zazwyczaj przebiega podobnie.
Linie 5, oraz 6 odpowiadają za przekazywanie portów. W systemie, na którym uruchamiany jest Vagrant, mogą działać różne usługi, jak np. serwer WWW na porcie 80, czy też baza danych MySQL na porcie 3306. Gdybyśmy teraz 1:1 przekazali wszystkie porty z naszej maszyny wirtualnej, to pokrywałyby się one z portami na naszym komputerze, przez co nasze usługi nie będą działać poprawnie. Aby uniknąć konfliktów, żaden port z Vagranta nie jest przekazywany, jeśli go nie zadeklarujemy. I tak w naszym przykładzie przekierowujemy port 80 z maszyny gościa na port 8080 na maszynie hosta oraz 3306 z gościa na 3333 hosta. Dzięki temu unikamy konfliktów i nasze usługi mogą działać bez problemu na nowych portach.
Od linii 8 do 11, deklarujemy jaki katalog z naszego projektu ma być synchronizowany z naszą maszyną. Vagrant przy uruchomieniu automatycznie jednorazowo kopiuje główny katalog projektu do katalogu /vagrant na maszynie gościa, ale nie synchronizuje go na bieżąco. Kod naszej aplikacji możemy umieścić gdzie tylko chcemy, ja zazwyczaj umieszczam go w katalogu /var/www. Dobrze jest też ustawić mu odpowiednie uprawnienia, żeby później nie mieć problemów z działaniem aplikacji.
W liniach 13 – 15, znajduje się konfiguracja providera. Vagrant może korzystać z kilku providerów np. VirtualBoxa, VMware, czy nawet Dockera i właśnie tutaj możemy to zadeklarować. Ja korzystam z VirtualBoxa i jedyna opcja jaką ustawiam, to ilość pamięci ram, z jakiej może korzystać maszyna.
W kolejnych liniach znajduje się konfiguracja zaopatrzenia maszyny. Od 17, do 23 linii, uruchamiany jest zwykły skrypt bashowy, który instaluje na naszej maszynie Ansible, który jest wymagany do uruchomienia właściwych skryptów zaopatrujących. Można uruchomić skrypty z maszyny hosta i pominąć ten krok, ale wymagana jest wtedy instalacja Ansible lokalnie, co jest kolejną zależnością, którą musimy zainstalować ręcznie i dlatego ja preferuję instalację tego narzędzia na maszynie gościa.
W następnych liniach mamy skrypt Ansible. Mamy w nim zadeklarowane dwa pliki, pierwszy to requirements.yml, w którym znajduje się lista roli do pobrania z Ansible Galaxy, który jest hubem ze skryptami napisanymi przez społeczność, oraz pliku provision.yml, który jest głównym skryptem zaopatrującym.
Ansible playbook
Piękno Ansible i podobnych narzędzi takich jak Puppet, Chef itp. polega na tym, że piszemy tutaj językiem deklaratywnym. Oznacza to tyle, że piszemy co chcemy osiągnąć, a nie co skrypt ma wykonać. Dzięki temu skrypt jest czytelniejszy, nie wykonuje niepotrzebnych akcji nadpisując poprzednie, a developer jest mniej sfrustrowany ciągłym debugowaniem skryptów basha.
Skrypty Ansible, pisane są przy pomocy składni YAML, nie jest to najlepsze na świecie rozwiązanie, ponieważ bardzo dużą uwagę szczególnie na początku, trzeba zwracać na formatowanie. Wystarczy, że w którymś miejscu zabraknie spacji i skrypt nie uruchomi się i wyrzuci mało zrozumiały błąd. Nie jest to też najgorsze rozwiązanie, ponieważ dzięki temu w składni nie ma niepotrzebnych znaków i przez to cały plik jest dużo czytelniejszy.
W Ansible mamy dostęp do skrótów, pisanych przez społeczność, zwanych rolami, dzięki czemu nie musimy pisać wszystkiego od nowa. Platforma przechowująca i udostępniająca te skrypty, została nazwana Ansible Galaxy i jest dostępna pod tym adresem.
Przejdźmy teraz do samych plików yml. Pierwszy z nich, to requirements.yml, w którym deklarujemy jakie “role”, bo tak nazywają się zewnętrzne skrypty, mają zostać pobrane przed uruchomieniem głównego skryptu:
#requirements.yml - geerlingguy.apache - geerlingguy.mysql - geerlingguy.php
Jest to prosta lista od myślników, gdzie wpisujemy nazwy roli. Można tutaj także zdefiniować własne role, pobierane z repozytorium git, ale nie będę tego tutaj opisywał, więc odsyłam do dokumentacji.
Drugi plik to główny skrypt zaopatrujący:
#playbook.yml - hosts: all become: true become_user: root vars_files: - vars.yml roles: - role: geerlingguy.php - role: geerlingguy.apache - role: geerlingguy.mysql tasks: - name: Copy .env file copy: src: ./provision/files/env dest: ./code/.env owner: root group: root mode: 0644
W pierwszej linii, znajduje się parametr hosts. Skrypty Ansible, mogą być wykonywane na zdalnych serwerach przez SSH i wtedy wpisujemy tam adres serwera. Jeśli skrypt ansible chcemy wykonać lokalnie, wpisujemy localhost, ale jako że będzie on wykonywany przez Vagranta, to wpisujemy tutaj all. Vagrant posiada konfiguracje hostów i automatycznie wrzuca tam nasz serwer, więc po wpisaniu all, Vagrant wykona skrypt na naszej maszynie.
Ansible pozwala wykonywać skrypty jako inny użytkownik, aby włączyć tę opcję, ustawiamy become na yes, a następnie ustawiamy parametr become_user na konkretnego użytkownika. W przypadku Vagranta osobiście zawsze operuje na roocie.
W kolejnej linii, mamy vars_files, lub też zwyczajnie vars, jak sama nazwa wskazuje, są to zmienne. Ansible pozwala na deklarowanie zmiennych zarówno w samym skrypcie, jak i w osobnym pliku z samymi zmiennymi. Ja preferuję to drugie rozwiązanie. Zmienne możemy wykorzystywać w skryptach, oraz są one też wykorzystywane przez role.
Następny parametr, to role, w których deklarujemy zewnętrzne skrypty zwane rolami. Mogą to być skrypty stworzone przez nas, jak i role pobrane z Ansible Galaxy, tak jak w przykładzie.
Ostatni parametr, to tasks, w którym deklarujemy taski naszego skryptu. Każdy task składa się z nazwy oraz samego taska. W przykładzie mamy kopiowanie pliku env, z katalogu provision do naszej aplikacji, natomiast sam Ansible obsługuje dużo więcej komend, takich jak klonowanie repozytoriów, obsługa menadżerów pakietów itd., odsyłam do dokumentacji Ansible. Oczywiście można także wykonywać skrypty sh, przez task o nazwie shell.
#vars.yml ####### PHP - vars ####### php_memory_limit: "512M" php_max_execution_time: "90" php_upload_max_filesize: "256M" php_install_recommends: yes php_webserver_daemon: "nginx" php_packages: - php - php-cli - php-pear - php-curl - php-fpm - php-zip - php-mysql - php-common - php-dev - php-gd - php-mbstring - php-soap - php-xml ####### MySQL - vars ####### mysql_root_password: super-secure-password mysql_databases: - name: database_name encoding: utf8 collation: utf8_general_ci mysql_users: - name: username host: "%" password: similarly-secure-password priv: "*.*:ALL" ######## Apache - vars ######### apache_listen_port: 80 apache_vhosts: - {servername: "example.com", documentroot: "/var/www/"}
W pliku vars.yml, deklarujemy zmienne, których możemy użyć podczas konfiguracji, oraz zmienne konfiguracyjne dla roli. W tym przypadku zmienne konfiguracji PHP, MySQL oraz Apache. Wszystkie zmienne dla danej roli z Ansible Galaxy, możemy znaleźć w pliku readme.md danej roli.
Uruchomienie
Kiedy cała konfiguracja naszej maszyny jest już gotowa, przyszła pora na jej pierwsze uruchomienie. Jak to zrobić? Nic prostszego, wchodzimy do katalogu z plikiem Vagrantfile, wpisujemy komendę “vagrant up” i to wszystko. Pierwsze uruchomienie może trochę potrwać, ponieważ wymaga ono pobrania boxa, zaktualizowania menedżera pakietów, oraz zaopatrzenia maszyny. Kiedy wszystko się uruchomi, możemy sprawdzić, czy nasza maszyna działa. Wpisujemy więc w przeglądarce adres http://localhost:8080/. Jeśli na ekranie ukazała się nasza aplikacja, to mamy sukces.
Żeby zatrzymać maszynę, wystarczy wpisać polecenie “vagrant halt”. Jeśli coś stanie się z konfiguracją, lub coś pójdzie nie tak, możemy skorzystać z komendy “vagrant up —provision”, która uruchomi naszą maszynę, ze wszystkimi skryptami zaopatrującymi. Jeśli nie jesteśmy pewni, czy nasza maszyna jest uruchomiona, można użyć komendy “vagrant status”, która wyświetli nam informacje o stanie maszyny.
A co z Dockerem?
Ostatnio dużą popularność zyskał Docker, więc również poruszę ten temat. Czym tak właściwie różną się te dwa narzędzia?
Sam Vagrant tak jak pisałem wyżej jest narzędziem, które do działania potrzebuje providera. Takim providerem może być oprogramowanie do wirtualizacji, takie jak VirtualBox, ale także oprogramowanie do konteneryzacji takie jak Docker. Podczas użycia Dockera jako providera konfiguracja wygląda odrobinę inaczej, ale nie będę się o tym rozpisywał, wszystkie informacje można znaleźć w oficjalnej dokumentacji Vagranta.
Czym różni się wirtualizacja od konteneryzacji?
Wirtualizacja, polega na emulacji całej maszyny ze wszystkimi podzespołami, natomiast konteneryzacja, polega na stworzeniu lekkiego środowiska zaopatrzonego w potrzebne biblioteki i narzędzia, korzystającego z jądra systemu hosta. Tutaj pojawia się mały kruczek, otóż musi to być jądro systemu linux. Dockera można jednak uruchomić na Windowsie, czy Macu, dzięki maszynie wirtualnej. Uruchamiając kontener Dockera na maszynie wyposażonej w system inny niż linux, Docker tworzy sobie maszynę wirtualną z Linuxem i dopiero na tej maszynie uruchamia kontenery.
Tak więc w skrócie, dzięki wirtualizacji dostajemy w pełni odizolowane od systemu hosta środowisko, natomiast kontener dostarcza nam odpowiednie biblioteki i narzędzia, operując nadal na jądrze systemu hosta.
Co lepsze, z czego korzystać?
Tak jak wszędzie, tutaj też nie jest czarno-biało i sami musimy zdecydować. W tym zadaniu mam nadzieję, że pomocna będzie ta oto lista różnic:
Docker:
+ Lekkie, mało zasobożerne środowisko
+ Krótki czas uruchomienia
— Brak pełnej izolacji
— Nie wspierany natywnie przez systemy różne od Linuxa (w przypadku OSX i Windows, uruchamiany przez maszynę wirtualną)
Vagrant:
+ W pełni odizolowane środowisko
+ Powtarzalność konfiguracji niezależnie od środowiska hosta
+ Uruchomienie pełnego systemu operacyjnego
— Czasochłonna konfiguracja
— Maszyny wirtualne są bardziej zasobożerne
Podsumowując
Użycie Vagranta znacznie ułatwia pracę przy projektach, kiedy pracujemy w zespołach, oraz kiedy konfiguracja środowiska aplikacji jest mocno skomplikowana i zajmuje dużo czasu. Dzięki Vagrantowi i skryptach konfiguracyjnych, pozbywamy się obowiązku ręcznej konfiguracji, oraz uzyskujemy powtarzalność operacji, przez co unikamy ew. pomyłek i błędów. Tą powtarzalność możemy w tym momencie w bardzo prosty sposób przenieść na środowisko produkcyjne, dzięki czemu mamy pewność, że na produkcji wszystko zadziała tak samo jak przy pracy nad projektem.
Wady:
– Konfiguracja może pochłonąć sporo czasu
– Pierwsze uruchomienie jest czasochłonne
Zalety:
– Pozbycie się problemów z różną konfiguracją systemów
– Zautomatyzowana konfiguracja aplikacji
– Skrypty do zaopatrzenia gotowe do uruchomienia na produkcji
– Mniej problemów z instalacją aplikacji przez nowych developerów w zespole