Deployer — narzędzie do automatyzacji wdrożeń aplikacji
Automatyzacja wdrożeń słusznie kojarzy się z CI/CD pipelines, czyli zestawem zadań, które mają zostać wykonane w celu opublikowania aplikacji na docelowym środowisku. Typowy prosty zestaw może składać się z etapu budowania aplikacji (np. composer install, npm install itp.), testowania (np. PHPUnit) i ostateczne przesłania kodu na środowisko (najczęściej poprzez SSH). W przypadku aplikacji, w których możemy powiedzieć, że cały kod był pisany od podstaw przez zespół deweloperski, skonfigurowanie CI/CD nie powinno być dużym wyzwaniem dla DevOps’a. Można nawet porzucić CI/CD na rzecz zwykłych skryptów napisanych np. w Bashu.
Natomiast jeśli w repozytorium znajduje się projekt oparty na jednym z CMS i/lub wykorzystujący popularny framework, zaczyna robić się trochę trudniej. Często konieczne jest użycie dodatkowych poleceń z odpowiednimi parametrami podczas etapu budowania, pamiętanie o plikach i katalogach, które powinny być nienaruszalne podczas kolejnych wdrożeń. Również zwykłe utrzymanie takiego projektu staje się w pewnym stopniu skomplikowane. O ile przygotowanie własnego skryptu do wdrożeń może być pouczające i satysfakcjonujące, to lepszym rozwiązaniem okazuje się Deployer.
To napisane w PHP łatwe w obsłudze narzędzie do wdrożeń aplikacji webowych. Korzysta z napisanych przez społeczność „recipes”, czyli zestawów tasków dla popularnych systemów zarządzania treścią i framework’ów (m.in. WordPress, Magento, Laravel, Symfony) prowadzących do uzyskania działającej aplikacji w danym środowisku. Możliwe jest również w pełni automatyczne przygotowanie środowiska do uruchamiania projektów. Charakterystyczną cechą Deployer jest dowolność w zakresie konfiguracji. Jeśli nasza aplikacja wymaga dodatkowych kroków, wystarczy dodać task w recipe, czy przygotować własny zestaw całkowicie samodzielnie.
Zapoznanie z Deployer zaczniemy od skonfigurowania serwera pod nasz projekt. Skrypt do provisioningu wymaga Ubuntu 20.04 i dostępu jako root lub możliwości wykonywania poleceń jako sudo. Powinien to być czysty system, aby uniknąć różnych problemów. Podczas tej operacji obowiązkowo zainstalowane zostają:
- serwer WWW Caddy,
- interpreter PHP w wybranej wersji (5.6, 7.4, 8.0 lub 8.1)
- Node.js (domyślnie wersja 16) wraz z npm
- pozostałe wymagane pakiety
Mamy ponadto możliwość ewentualnego wybrania serwera baz danych (brak obsługi baz, MySQL, MariaDB lub PostgreSQL). Zostaniemy też zapytani o hasło użytkownika (konieczne jedynie, gdy operacji nie wykonujemy z konta root), domenę, ścieżkę do katalogu z plikami serwisu (np. public w przypadku Laravel), nazwę użytkownika i dostępy do bazy danych. Skrypt od podstaw konfiguruje środowisko, aktualizując pakiety, dodając nasz klucz SSH, blokując możliwość zdalnego logowania jako root, ustawiając firewall (ufw).
Jeśli mamy przygotowaną maszynę z Ubuntu 20.04, lokalnie Deployer możemy „zainstalować” np. w ten sposób:
git clone https://github.com/deployphp/deployer
cd deployer
composer install
Aby uzyskać możliwość wykonywania polecenia dep (z katalogu deployer/bin/dep
) do pliku .bashrc musimy dodać poniższy wpis (na przykład na końcu pliku):
export PATH="$HOME/deployer/bin:$PATH"
Można to również wykonać bezpośrednio w konsoli, bez modyfikacji .bashrc, natomiast taka „zmiana” będzie aktualna jedynie do zakończenia sesji (wylogowania się, przerwania połączenia SSH). Teraz automatycznie skonfigurujemy maszynę z Ubuntu. W tym celu niezbędny jest nowy plik o nazwie deploy.php. Polecam zachować porządek i utworzyć go w oddzielnym katalogu. Zawartość powinna wyglądać podobnie do tej:
<?php
namespace Deployer;
require 'recipe/common.php';
host('<adres_IP>')
->set('deploy_path', '~/web');
after('deploy:failed', 'deploy:unlock');
Dokładnie taka zawartość wynika z mojego doświadczenia. To minimalna działająca składnia, Deployer nie pyta o wskazanie deploy_path
, co kończy się błędem przy tasku provision:website
.
Deployer najlepiej działa, jeśli używamy kluczy SSH. W innym przypadku będziemy zmuszeni do każdorazowego wpisywania hasła. Dobrym pomysłem jest wygenerowanie i przesłanie na serwer swojego klucza (używanie kluczy zamiast haseł jest zalecaną praktyką nie tylko podczas wdrożeń):
ssh-keygen
ssh-copy-id root@<adres_IP>
Skrypt do provisioningu wykonujemy w ten sposób (przy założeniu, że mamy dostęp SSH jako root):
dep provision -o remote_user=root
i odpowiadamy na „pytania”:
Możliwe jest pominięcie konieczności odpowiedzi, poprzez dodanie do deploy.php zmiennych (i ustawienie ich wartości):
->set('sudo_password', '…')
->set('domain', '…')
->set('public_path', '…')
->set('php_version', '…')
->set('db_type', '…')
->set('db_user', '…')
->set('db_name', '…')
->set('db_password', '…');
Provisioning może chwilę potrwać, natomiast uzyskujemy w ten sposób gotowy do działania serwer, bez żadnych dodatkowych czynności z naszej strony. Widzę tu możliwość ułatwienia zadań działom Ops, tzn. możemy programistom szybko i praktycznie na żądanie stawiać nowe maszyny wirtualne przygotowane do ich wymagań.
Po zakończeniu procesu warto połączyć się za pomocą SSH z naszą maszyną (klucz publiczny został dodany do authorized_keys podczas provisioningu):
ssh deployer@<adres_IP>
Pewnych zmian może wymagać Caddy. W przykładzie zamierzamy wdrożyć WordPress, który plik index.php ma zawarty w głównym katalogu zamiast public. Dodatkowo działamy lokalnie, więc nie możemy wygenerować certyfikatu SSL (Caddy domyślnie zabezpiecza każdy serwis za pomocą SSL od Let’s Encrypt). Edycji wymagają dwa pliki Caddyfile. W pierwszym z nich, w katalogu domowym (czyli /home/deployer/web/Caddyfile
) do nazwy domeny dodajemy :80 oraz zmieniamy ścieżkę root. Plik po modyfikacji powinien wyglądać podobnie do poniższego przykładu:
bulldog.local:80 {
root * /home/deployer/web/current
file_server
php_fastcgi * unix//run/php/php7.4-fpm.sock {
resolve_root_symlink
}
log {
output file /home/deployer/web/log/access.log
}
handle_errors {
@404 {
expression {http.error.status_code} == 404
}
rewrite @404 /404.html
file_server {
root /var/dep/html
}
}
}
Wymuszenie przekierowania na HTTPS (które funkcjonuje nawet bez zainstalowania prawidłowego certyfikatu) musimy jeszcze wyłączyć globalnie w pliku /etc/caddy/Caddyfile dodając na samym początku:
{
auto_https disable_redirects
}
Restartujemy usługę:
sudo systemctl restart caddy
Od tej pory po wejściu na http://bulldog.local
prawdopodobnie zobaczymy stronę błędu 404 z lokalizacji /var/dep/html/404.html
. W pełni poprawna odpowiedź serwera, ponieważ nasz katalog nie zawiera żadnej treści. Weryfikacji poprawności działania możemy dokonać dodając plik o nazwie index.html lub index.php (np. z phpinfo()) do katalogu /home/deployer/web/current
.
To też dobry moment, aby wyjaśnić tworzoną przez Deployer strukturę katalogów.
W katalogu zdefiniowanym jako deploy_path po udanym wdrożeniu bądź w jego trakcie zobaczymy cztery lub maksymalnie pięć katalogów i dowiązań symbolicznych:
- .dep— katalog pomocniczy Deployer, zawiera przede wszystkim historię przeprowadzanych deployment’ów. W pliku latest_release znajduje się numer odpowiadający ostatniemu wydaniu, a w pliku releases_log można zobaczyć historię każdego z nich. Podczas wdrożenia jest w nim zakładany plik deploy.lock, który uniemożliwia rozpoczęcie kilku wdrożeń jednocześnie. Jeśli proces wdrożenia nagle zakończymy (Ctrl+C) lub połączenie zostanie przerwane, ten plik pozostanie i będzie blokował dalszą pracę. Dlatego należy go ręcznie usunąć lub wykonać dep deploy:unlock. To samo polecenie jest wydawane automatycznie po udanym procesie.
- shared— katalog zawierający dane, który powinny być nienaruszalne pomiędzy kolejnymi wydaniami (np. dla WordPress to wp-content/uploads i wp-config.php). Do plików i katalogów w shared są ustawiane symlinki. Warto też zauważyć, że z reguły jeśli dany zasób jest współdzielony, to nie powinien znajdować się wraz z kodem w repozytorium. Jego zawartość definiuje się jako shared_files i shared_dirs.
- releases— katalog zawierający poszczególne wydania naszej aplikacji. Katalogi podrzędne do releases są oznaczone kolejnymi numerami. Domyślnie przechowywanych jest 10 ostatnich wydań, ich liczbę określamy poprzez keep_releases (np. w pliku recipes/common.php).
- release— link symboliczny do możliwie najnowszego katalogu w releases, jest tworzony podczas wdrożenia i usuwany po jego zakończeniu. Pozostaje w przypadku niepowodzenia.
- current— link symboliczny do działającego wydania, czyli zwykle najnowszego katalogu w releases. W idealnym przypadku zawiera najnowszą wersję aplikacji i zawsze zawiera symlinki do danych z katalogu shared.
Dysponując tą wiedzą, możemy zacząć wdrażać nasze aplikacje. W moim katalogu public_html
znajduje się przykładowa prosta witryna zbudowana w WordPress. Jak wiadomo, ten CMS wymaga także bazy danych, więc w dalszym etapie będziemy musieli wykonać jej zrzut i zaimportować na docelowym hoście. Najpierw jednak dodajmy kod do repozytorium. Zaczniemy od przygotowania .gitignore:
wp-config.php
wp-content/uploads
I jak zawsze wysyłamy zmiany do repozytorium.
Polecam teraz wykonywać wdrożenie z poziomu tego katalogu lub dla zachowania porządku sklonować repozytorium. Pracę zaczynamy zawsze od dep init, po zakończeniu uzyskujemy plik deploy.php lub deploy.yml. Polecenie nie wymaga od nas szczególnej uwagi, w zasadzie wybieramy jedynie składnię pliku deploy.* (PHP lub YAML), template projektu (w naszym przypadku to nr 22 wordpress), adres repozytorium (narzędzie rozpozna automatycznie, gdy istnieje katalog .git), nazwę projektu oraz adres hosta. Gotowy plik będzie wyglądał w ten sposób:
<?php
namespace Deployer;
require 'recipe/wordpress.php';
// Config
set('repository', 'http://192.168.1.130:3000/apps/wordpress');
add('shared_files', []);
add('shared_dirs', []);
add('writable_dirs', []);
// Hosts
host('192.168.1.160')
->set('remote_user', 'deployer')
->set('deploy_path', '~/web');
// Hooks
after('deploy:failed', 'deploy:unlock');
Możemy go dowolnie dostosować, tzn. dodać np. shared_files (wtedy wykonujemy to na poziomie danego projektu, a nie całego narzędzia) czy zmienić nazwę zdalnego użytkownika. Jeśli popełniliśmy błąd i przykładowo, zamiast Contao wybraliśmy WordPress wystarczy zmienić nazwę pliku w dyrektywie require. Wszystkie dostępne recipes dostępne są w katalogu recipe, pozostała struktura deploy.php pozostaje taka sama niezależnie od typu aplikacji.
Gdy jesteśmy przygotowani do procesu deploymentu w zasadzie pozostaje nam uruchomić narzędzie za pomocą dep deploy. Nie wygląda to niestety zbyt przejrzyście, nie widzimy, co dokładnie jest wykonywane na zdalnym hoście:
devops@ubuntu:~/wordpress$ dep deploy
task deploy:info
[192.168.1.160] info deploying HEAD
task deploy:setup
task deploy:lock
task deploy:release
task deploy:update_code
task deploy:shared
task deploy:writable
task deploy:symlink
task deploy:unlock
task deploy:cleanup
task deploy:success
[192.168.1.160] info successfully deployed!
Natomiast wystarczy dodać parametr -v
, aby uzyskać wgląd w wykonywane taski. Większy poziom szczegółowości to po prostu dodawanie kolejnych „v” (dep deploy -vvv
), aczkolwiek nadmiar informacji może znacznie zmniejszyć czytelność.
Można też wypisać wszystkie zdefiniowane taski, używając dep tree deploy
:
devops@ubuntu:~/wordpress$ dep tree deploy
The task-tree for deploy:
└── deploy
├── deploy:prepare
│ ├── deploy:info
│ ├── deploy:setup
│ ├── deploy:lock
│ ├── deploy:release
│ ├── deploy:update_code
│ ├── deploy:shared
│ └── deploy:writable
└── deploy:publish
├── deploy:symlink
├── deploy:unlock
├── deploy:cleanup
└── deploy:success
Pomyślnie przeprowadzony proces zawsze kończy się komunikatem „successfully deployed!”. Po zakończeniu wdrożenia możemy połączyć się poprzez SSH. Deployer ułatwia tę czynność, ponieważ wystarczy wykonać polecenie dep ssh. Połączymy się użytkownikiem zdefiniowanym jako remote_user. Jeśli zastosowaliśmy provisioning, będzie to użytkownik deployer (z uprawnieniami sudo bez konieczności podawania hasła) i znajdziemy się w katalogu /home/deployer/web. Listując zawartość, potwierdzamy istnienie struktury katalogów:
deployer@ubuntu:~/web$ ls -la
total 28
drwxrwxr-x 6 deployer deployer 4096 Jul 25 18:42 .
drwxr-xr-x 7 deployer deployer 4096 Jul 25 17:44 ..
drwxrwxr-x 3 deployer deployer 4096 Jul 25 18:42 .dep
-rw-rw-r-- 1 deployer deployer 349 Jul 25 17:35 Caddyfile
lrwxrwxrwx 1 deployer deployer 10 Jul 25 18:42 current -> releases/1
drwxrwxr-x 2 deployer caddy 4096 Jul 25 17:38 log
drwxrwxr-x 3 deployer deployer 4096 Jul 25 18:42 releases
drwxrwxr-x 3 deployer deployer 4096 Jul 25 18:42 shared
W przypadku pierwszego wdrożenia WordPress tą metodą po wejściu zobaczymy błąd 500. Spowodowany jest on istniejącym (jako dowiązanie symboliczne) pustym plikiem wp-config.php. Należy go uzupełnić, jak również przesłać bazę i zawartość pozostałych danych w katalogu shared (ilość tych danych zależy od użytej recipe, np. Prestashop ma ich 11). Zrzut bazy wykonamy dzięki mysqldump, z kolei dane można najpierw spakować (jeśli to duży katalog), a następnie wysłać np. używając polecenia scp. Po zaimportowaniu należy też pamiętać o zmianie nazwy domeny w tabeli wp_options (rekordy siteurl i home)
Kiedy zdecydowaliśmy się na użycie provisioningu, możemy napotkać „problem” z zainstalowaniem nowych motywów czy wtyczek. Powodem jest brak uprawnień. Sprawdźmy, jak to wygląda:
deployer@ubuntu:~/web/current/wp-content$ ls -la
total 28
drwxrwxr-x 6 deployer deployer 4096 Jul 25 18:42 .
drwxrwxr-x 5 deployer deployer 4096 Jul 25 18:59 ..
drwxrwxr-x 2 deployer deployer 4096 Jul 25 18:22 backups-dup-lite
-rw-rw-r-- 1 deployer deployer 28 Jul 25 18:22 index.php
drwxrwxr-x 4 deployer deployer 4096 Jul 25 18:22 languages
drwxrwxr-x 5 deployer deployer 4096 Jul 25 18:22 plugins
drwxrwxr-x 7 deployer deployer 4096 Jul 25 18:22 themes
lrwxrwxrwx 1 deployer deployer 34 Jul 25 18:42 uploads -> ../../../shared/wp-content/uploads
Właścicielem katalogów plugins i themes jest deployer. Natomiast pula PHP działa na użytkowniku www-data:
deployer@ubuntu:~$ ls -la /run/php
total 4
drwxr-xr-x 2 www-data www-data 100 Jul 25 17:23 .
drwxr-xr-x 31 root root 940 Jul 25 18:58 ..
lrwxrwxrwx 1 root root 30 Jul 25 17:22 php-fpm.sock -> /etc/alternatives/php-fpm.sock
-rw-r--r-- 1 root root 5 Jul 25 17:23 php7.4-fpm.pid
srw-rw---- 1 www-data www-data 0 Jul 25 17:23 php7.4-fpm.sock
To celowe działanie skryptu. W serwisie wdrożonym w ten sposób użytkownik poprzez przeglądarkę będzie mógł przesłać jedynie media do wp-content/uploads
(gdzie z użyciem setfacl umożliwiono dostęp dla www-data). Dzięki temu zabiegowi maleje możliwość ewentualnego uszkodzenia witryny przez użytkownika. Instalacja motywów, wtyczek i aktualizacje możliwe są jedynie w ramach zmiany w repozytorium i kolejnego wdrożenia. Możemy zmienić katalogi do zapisu, dodając je jako writable_dirs.
W środowiskach produkcyjnych raczej powinno unikać się sytuacji, że użytkownik, na którym „działa” aplikacja ma uprawnienia sudo. Sugeruję uniemożliwić wykonywanie poleceń z użyciem sudo, usuwając użytkownika deployer z tej grupy: deluser deployer sudo. Wcześniej polecam dodać osobne konto służące do logowania na root i wprowadzania zmian w systemie.
Wspominałem już, że jako current oznacza się wyłącznie katalog w release z udanym wdrożeniem. Natomiast Deployer to jedynie skrypt i nie wykryje błędów w kodzie. Możemy jednak sprawnie powrócić do poprawnej wersji z użyciem dep rollback. Po naprawie błędu po prostu uruchamiany ponownie dep deploy.
Oprócz znajomości zastosowania shared_files, shared_dirs i writable_dirs przydatne szczególnie do tworzenia własnych recipes okazują się wbudowane w Deployer funkcje i „skróty”. To najważniejsze z nich:
run()
— wykonuje polecenie na zdalnym systemierunLocally()
— wykonuje polecenie lokalnie (świetny przykład to shopware.php)writeln()
— wypisuje tekst na ekrandownload()
— pobiera zdalną zawartość do wskazanego lokalnego katalogu{{release_or_current_path}}
— zgodnie z nazwą oznacza katalog, w którym aktualnie trwa proces wdrożenia{{bin/php}}
— oznacza wykonanie polecenia php (w wersji, na jaką wskazujephp -v
){{bin/composer}}
— wykonanie composer
Dodanie przykładowego zadania wprowadzenia zmiany w pliku może wyglądać w ten sposób:
<?php
namespace Deployer;
require_once __DIR__ . '/common.php';
add('recipes', ['wordpress']);
set('shared_files', ['wp-config.php']);
set('shared_dirs', ['wp-content/uploads']);
set('writable_dirs', ['wp-content/uploads']);
desc('Change release date');
task('deploy:date_change', function () {
run("sed -i s/release_date=.*/release_date=$(date '+%d.%m.%Y')/g {{release_or_current_path}}/changelog");
});
desc('Deploys your project');
task('deploy', [
'deploy:prepare',
'deploy:publish',
'deploy:date_change'
]);
Bardziej rozbudowane recipes mogą wymagać do utworzenia przynajmniej podstawowej znajomości PHP.
WordPress do uruchomienia nie wymaga wykonywania żadnych poleceń na swoich plikach. Natomiast taki wymóg istnieje w różnych framework’ach, np. Symfony. Dzięki zastosowaniu w naszym projekcie narzędzia Deployer nie musimy ręcznie dodawać obsługi wykonywania skryptów.
Myślę, że Deployer to skuteczne ułatwienie w pracy programisty czy DevOps’a. Napisany w PHP pozwala na swobodne dostosowanie do własnych potrzeb i łatwość zwykłego użytkowania. W pewnym stopniu „zwalnia” nas też z umiejętności konfiguracji Linuxa (chociaż ta wiedza i tak okaże się kiedyś konieczna). Możliwość integracji z CI/CD umożlowia sprawne wdrożenia aplikacji.