To ma działać automatycznie!

Ansible wrz 15, 2020

Ż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!

Tagi

Bartłomiej Komendarczuk

Newbie DevOps :)

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.