To ma działać automatycznie!
Żyjemy w czasach, gdzie lenistwo jest bardzo popularne - księgowa chce, aby przelewy wysyłały się automatycznie, kadrowa aby wypłaty naliczały się bez jej udziału. Po prostu, wszystko i wszędzie ma działać automatycznie!
Halo, czy ten program jest zepsuty?
Czasami mam wrażenie, że ludzie sami się proszą, aby o nich zapomniano i uznano, że są niepotrzebni. Któregoś razu znajomy opowiadał mi, że zadzwoniła jakaś księgowa w starszym wieku, która korzysta z programu księgowego.
Była oburzona od początku rozmowy, ponieważ program miał wszystko sam automatycznie robić, a tak nie jest. Znajomy zapytał, w czym dokładnie problem, bo program po zaznaczeniu zapisów kasowych lub wskazaniu dat i wciśnięciu przycisku z ikoną "pieniążka" wysyła przelewy do banku.
Po zaznaczeniu dokumentów i wciśnięciu przycisku z ikoną dokumentu księguje zapisy, a przycisk z kopertą wysyła przypomnienie o płatnościach do wszystkich dłużników. I tutaj jest pies pogrzebany, bo drogiej Pani przeszkadza właśnie to, że ona MUSI cokolwiek wciskać... Cóż, szkoda, że program nie parzy jej automatycznie kawy, to w sumie spełniałby jej wszystkie obowiązki :)
Deploy aplikacji na Heroku
Wystarczy tych uszczypliwości, przejdźmy do meritum tego posta. Jakiś czas temu dostałem za zadanie stworzenie automatycznego deploy aplikacji na Heroku. Jedynym warunkiem było to, że to ma być deploy półautomatyczny - otóż cała operacja ma działać automatycznie i umieszczać aplikację na Heroku, natomiast nie po każdym commicie.
Czasami zmiany są krytyczne i muszą być wprowadzone od razu, a czasami chodzi o jakieś drobne poprawki, które mogą poczekać. W pierwszej kolejności pomyślałem o CI/CD, ale skąd ma to mieć wiedzę, że akurat ten konkretny commit jest krytyczny? W pierwszej kolejności pomyślałem o treści danego commita - jakieś słowo klucz na końcu, ale to zmieniło by procedurę nazewnictwa commitów.
Poza tym co gdy programista zapomni dodać tą adnotację? Uznałem za zbędne, rozwiązywanie tych problemów, dlatego postawiłem na playbook napisany w Ansible, który będzie sprawdzał, czy aktualnie pobrany projekt, różni się od tego, który trzymamy w repozytorium kodu.
Jeżeli tak to ma zaciągnąć najnowszy commit i wykonać deploy na Heroku. Sam playbook będzie wywoływany jedną komendą przez programistę (tu jest ten pół-automatyzm :D), a dodatkowo można go wrzucić w CRON, aby wywoływał się np. raz na tydzień.
Plan jest? Jest! No to do roboty. W pierwszej kolejności potrzebujemy na maszynie trzech rzeczy - snap, expect oraz Heroku-CLI, które zainstalujemy za pomocą snap'a. Jeżeli znasz chociaż podstawy Ansible, to pewnie wiesz jak zainstalować te zależności. Natomiast dla tych, którzy nigdy nie mieli z nim styczności:
- name: Ensure snap package is installed
apt:
name: snap
- name: Ensure expect package is installed
apt:
name: expect
- name: Ensure heroku-cli is installed
snap:
name: heroku
classic: true
Kiedy mam już wszystko czego potrzebuje, mogę zacząć budować playbooka. Gdy pisałem rolę, wszystko było wrzucone do jednego pliku, natomiast podczas refactoru podzieliłem rolę na trzy, dla własnej wygody. Pierwszą częścią jest heroku_prepare_app.yml - jak sama nazwa wskazuje, na tym etapie przygotuję aplikację.
- name: Add host to ssh config
blockinfile:
dest: /home/{{ssh_key_user}}/.ssh/config.j2
create: true
block: |
Host gitlab.com
IdentityFile /home/{{ssh_key_user}}/.ssh/id_rsa
IdentitiesOnly yes
tags:
- heroku_prepare_app
- name: Create a directory for app if it does not exist
file:
path: /home/{{ssh_key_user}}/{{app_dir}}
state: directory
mode: '0755'
tags:
- heroku_prepare_app
- name: Clone git repository
git:
repo: '{{repo_name}}'
dest: /home/{{ssh_key_user}}/{{app_dir}}
key_file: '{{ ssh_key }}'
update: true
register: cloned
tags:
- heroku_prepare_app
W pierwszym etapie dodaje host mojego repozytorium do configu ssh. W kolejnym kroku upewniam się, że istnieje folder dla projektu, który pobiorę z repozytorium kodu i który będę chciał udostępnić na Heroku. Następnie pobieram projekt w wybrane miejsce.
Najprostszy etap mam za sobą, teraz przyszedł czas na drugą część roli czyli heroku_setup.yml - ten etap jest o tyle bardziej interesujący, że nie poszło tak łatwo, jak być powinno. Nie spodziewałem się tylko, że zatrzymam się na całe 2 dni...
- name: Login to heroku using expect script
shell:
cmd: sudo expect roles/harlina_heroku/login.exp
tags:
- heroku_setup
- name: Check if heroku app exists
command: bash -c "heroku apps | grep {{ app_name }}"
register: app_exists
ignore_errors: true
tags:
- heroku_setup
- name: Create heroku App
when: app_exists is failed
command: bash -c "heroku create --region eu {{ app_name }}"
tags:
- heroku_setup
- name: Set remote for repository
command: chdir=/home/{{ssh_key_user}}/{{app_dir}} heroku git:remote -a {{ app_name }}
tags:
- heroku_setup
Otóż playbook nie był w stanie zalogować się poprzez Heroku-CLI na konto, na którym mieliśmy wykonywać deploy naszej aplikacji - w czym problem? Otóż logowanie bezpośrednio przez terminal, za pomocą Heroku-CLI przebiegało poprawnie, natomiast gdy chciałem to samo zrobić za pomocą ansiblowego command czy shell, otrzymywałem dziki timeout.
Gdy debugowałem tą sytuację, widziałem, że dane do logowania są przesyłane, natomiast Heroku zachowywało się tak, jakby nigdy ich nie dostało... Dwa dni spędziłem na czytaniu pobocznych wątków i pisaniu rozwiązania na setki sposobów.
Nagle natknąłem się na coś z czym nigdy wcześniej nie miałem do czynienia, czyli Expect. Napisałem w nim bardzo prosty skrypt, który został wywołany w playbook'u - praktycznie robił to samo, co ansible za pomocą command lub shell, ale skoro po dwóch dniach działa, to nie mam zamiaru się czepiać :)
Okazało się, że przyczyną było to, że ansible szybciej podawał dane logowania, niż by Heroku się tego spodziewał - dorzucenie polecenia sleep, dzięki któremu skrypt wstrzymywał się tą jedną sekundę przez kolejnymi operacjami było złotym strzałem! Sam skrypt wygląda dokładnie w ten sposób:
#!/usr/bin/expect
spawn heroku login -i
sleep 1
send "HEROKU_EMAIL";
send "\r"
sleep 1
send "HEROKU_PASSWORD"
send "\r"
sleep 2
interact
Wywołuję w nim polecenie logowania do Heroku, wstrzymuję się poleceniem sleep, podaje email konta w Heroku oraz przechodzę do następnej linii, znowu "śpię", podaję hasło, przechodzę do kolejnej linii i przysypiam na wszelki wypadek jeszcze dwie sekundy - ot zaawansowany skrypt :P
Po zalogowaniu warto sprawdzić czy aplikacja znajduje się już na Heroku - jeżeli jej nie ma, to tworzę ją i ustawiam do niej remote address. Jeżeli jest - to mamy to z głowy.
Najważniejsze na koniec
Przyszedł czas na ostatni, najważniejszy etap, dzięki któremu zmiany zastosowane w projekcie będą deployowane na Heroku czyli Heroku_push.yml.
- name: Check if there are new changes
command: bash -c "cd /home/{{ssh_key_user}}/{{app_dir}} && git status | grep 'nothing to commit'"
register: change_exists
ignore_errors: true
tags:
- heroku_push
- name: Heroku add
command: chdir=/home/{{ssh_key_user}}/{{app_dir}} git add .
when: change_exists is failed
tags:
- heroku_push
- name: Heroku commit
command: chdir=/home/{{ssh_key_user}}/{{app_dir}} git commit -m '{{ commit_message }}'
when: change_exists is failed
tags:
- heroku_push
- name: Heroku push
command: chdir=/home/{{ssh_key_user}}/{{app_dir}} git push heroku master
when: (change_exists is failed) or (cloned is changed)
tags:
- heroku_push
Pierwszy zapis sprawdza, czy projekt z repozytorium kodu różni się od tego, który umieściliśmy ostatnio na Heroku i/lub od momentu ostatniego deploy aplikacji na Heroku. Jeżeli takich zmian nie było, to playbook po prostu kończy swoje działanie, gdyż rejestruje, czy napotkał po zastosowaniu polecenia git status zwrotkę "nothing to commit". Oznacza to, że aplikacja z repozytorium kodu nie różni się od tej na Heroku.
Kolejne kroki wywołują się tylko i wyłącznie wtedy, gdy powyższej odpowiedzi nie ma, a odpowiada za to change_exists is failed. Ostatnie trzy kroki, to klasyczne polecenia git, czyli dodanie i commitowanie zmian oraz push aplikacji na Heroku.
Zapewne zauważyłeś, że w stworzonej przeze mnie roli, są kroki które zawierają w swojej składni np. {{app_dir}} czy {{app_name}}. Są to domyślne zmienne, którymi może posługiwać się rola. Więcej na temat zmiennych w Ansible możesz poczytać tutaj.
Ja do projektu wprowadziłem je z trzech powodów - pierwszym jest to, że nie chciałem hardcodować danych - to nigdy nie jest dobre. Drugi powód jest taki, że gdyby np. folder w którym przechowuje aplikację się zmienił, wystarczy, że w zmiennych domyślnych zmienię wartość dla app_dir. Trzeci powód jest taki, że playbook może być uniwersalny - jeżeli chce w identyczny sposób deployować inną aplikację, wystarczy, że uzupełnię zmienne dla innej aplikacji i w kilka sekund mogę wykonać deploy innej aplikacji na Heroku. Przykładowy plik ze zmiennymi wygląda w ten sposób:
app_dir: BestAppEver_Heroku
repo_name: git@github.com:BElluu/BestAppEver.git
app_name: BestAppEver
commit_message: commited new things
ssh_key_user: BElluu
ssh_key: /home/belluu/.ssh/id_rsa
Powyższy projekt jest zamieszczony na moim koncie GitHub - jeżeli chcesz możesz go użyć do własnych celów lub po prostu przeanalizować w jaki sposób działa. Link do projektu wraz z drobną uruchomienia playbooka jest tutaj :)
Jestem pewny, że w tym projekcie wiele rzeczy można byłoby zrobić lepiej, dlatego jeżeli masz jakieś spostrzeżenia lub sugestie, to podziel się nimi w komentarzu poniżej :) Do następnego!