Przekazywanie zmiennych w playbookach Ansible

Czasami problemem w playbookach Ansible staje się przekazywanie zmiennych między częściami tego playbooka wykonywanymi na różnych hostach. Rozważmy taki przypadek, w którym wykonamy następujące kroki:

  • Na lokalnej maszynie wykonamy przykładowe polecenie i zarejestrujemy jego wynik jako zmienną testvar
  • Połączymy się z innym hostem i spróbujemy odwołać się na nim do zmiennej testvar za pomocą modułu debug
  • Spróbujemy zmodyfikować przykładowy playbook tak, żeby zawartość zmiennej prawidłowo uzyskać.

Prosty playbook, który pozwoli nam na przetestowanie dostępności zmiennych wygląda np. tak:

---
- hosts: localhost
  become: no
  connection: local

  tasks:
  - shell: echo "Foo bar"
    register: testvar

  - debug: var=testvar

- hosts: vagrant.local
  become: no

  tasks:
  - debug: var=testvar

W efekcie wywołania takiego playbooka przekonamy się, że zmienna testvar, prawidłowo zarejestrowana na maszynie lokalnej, na hoście vagrant.local nie jest dostępna:

$ ansible-playbook -i 'vagrant.local,' test.local

PLAY [localhost] ***************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
    "testvar": {
        "changed": true,
        "cmd": "echo \"Foo bar\"",
        "delta": "0:00:00.001634",
        "end": "2017-03-17 16:58:20.821949",
        "rc": 0,
        "start": "2017-03-17 16:58:20.820315",
        "stderr": "",
        "stdout": "Foo bar",
        "stdout_lines": [
            "Foo bar"
        ],
        "warnings": []
    }
}

PLAY [vagrant.local] ***********************************************************

TASK [setup] *******************************************************************
ok: [vagrant.local]

TASK [debug] *******************************************************************
ok: [vagrant.local] => {
    "testvar": "VARIABLE IS NOT DEFINED!"
}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0
vagrant.local              : ok=2    changed=0    unreachable=0    failed=0

Okazuje się jednak, że wystarczy drobna zmiana, żeby “dobrać się” do naszej zmiennej. Dla czytelności prezentuję tylko drugą część playbooka:

- hosts: vagrant.local
  become: no

  tasks:
  - debug: var=hostvars['localhost']['testvar']

Po wywołaniu zmodyfikowanego playbooka otrzymujemy w tym zadaniu wartość naszej zmiennej:

TASK [debug] *******************************************************************
ok: [vagrant.local] => {
    "hostvars['localhost']['testvar']": {
        "changed": true,
        "cmd": "echo \"Foo bar\"",
        "delta": "0:00:00.001697",
        "end": "2017-03-17 17:03:01.279967",
        "rc": 0,
        "start": "2017-03-17 17:03:01.278270",
        "stderr": "",
        "stdout": "Foo bar",
        "stdout_lines": [
            "Foo bar"
        ],
        "warnings": []
    }
}

Migracja z rvm do rbenv na mac OS

Na początek usuwamy wszystko co dotyczy RVM:

rvm implode

# ręcznie usuwamy (jeśli istnieje) plik:
rm ~/.rvmrc 

# oraz wpisy dotyczące RVM z:
.profile
.bash_profile
.bashrc
.zshrc

Następnie instalujemy rbenv przy pomocy brew:

brew install rbenv ruby-build

Lub ściągając z git hub’a

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Dodajemy rbenv do PATH:

# do .bash_profile lub .zshrc dodajemy:
export PATH="$HOME/.rbenv/bin:$PATH"

Oraz uruchamiamy:

rbenv init 

I uzupełniamy wskazany plik odpowiednim wpisem.

Aby zainstalować odpowiednią wersję ruby:

rbenv install 2.4.0

Możemy nadal używać plików .ruby-version wpisując w nie wersję ruby której chcemy używać w danym projekcie.

Synchroniczne uruchamianie workerów Sidekiq

Sidekiq pozwala na wykonywnie określonych zadań asynchronicznie, “w tle”. Najczęściej realizowane jest to za pomocą specjalnie stworzonych klas (workerów), które dzięki użyciu modułu Sidekiq::Worker zyskują metody z których możemy skorzystać aby zlecić wykonanie kodu w tle.

class FooWorker
  include Sidekiq::Worker

  def perform(foo_id)
    "Wartość foo_id wynosi: #{foo_id}"
  end
end

# wykonanie w tle
FooWorker.perform_async(1)
=> "d26cca4c2786f52e48da9520"

# wykonanie po wskazanym czasie
FooWorker.perform_in(10.seconds, 1)
=> "369aee857301ef89d7669821"

Czasem przydatne może być wykonanie kodu workera w ramach aktualnego wątku, bez planowania zadania w tle. Wbrew pozorom jest to bardzo łatwe - wystarczy pamiętać, że worker jest przecież regularną klasą Ruby, w związku z tym możemy po prostu powołać jego instancję i wykonać metodę perform:

FooWorker.new.perform(1)
=> "Wartość foo_id wynosi: 1"

Oczywiście, przy takim wywołaniu całkowicie pomijamy strukturę Sidekiq, tracąc tym samym kolejkowanie, ponawianie zadań itd. Może to być jednak bardzo przydatne w momencie, kiedy chcemy łatwo sprawdzić działanie kodu zamkniętego w workerze, np. w trakcie debugowania aplikacji.

Sortowanie NULL w PostgreSQL

W przypadku kiedy kolumna według której sortujemy dane pobierane z bazy PostgreSQL przybiera wartość NULL, kolejność wyników jest następująca:

  • dla sortowania w porządku rosnącym (ASC), wartości NULL zostaną umieszczone jako ostatnie,
  • dla sortowania w porządku malejącym (DESC), wartości NULL zostaną umieszczone jako pierwsze.

Nie zawsze jednak jest to preferowane zachowanie - łatwo wyobrazić sobie sytuację, kiedy puste dane chcemy zawsze prezentować jako ostatnie (lub jako pierwsze) - niezależnie od przyjętego porządku sortowania.

Można to łatwo wymusić jawnie wskazując w momencie sortowania w jakim porządku mają być zwracane wartości NULL. Aby to osiągnąć, należy do warunku sortowania - ORDER BY column_name ASC | DESC - dodać:

  • NULLS LAST - jeżeli wartość NULL ma być umieszczona na końcu wyników,
  • NULLS FIRST - jeżeli wartość NULL ma być umieszczona na początku wyników.

Przykładowo, dla danych:

-------------------
| id | name       |
-------------------
| 1  | Zofia      |
| 2  | NULL       |
| 3  | Alicja     |
-------------------

możemy sortować w następujący sposób:

# NULLe zawsze ostatnie
SELECT id, name FROM table_name ORDER BY name ASC NULLS LAST;
=> [3, Alicja], [1, Zofia], [2, NULL]

SELECT id, name FROM table_name ORDER BY name DESC NULLS LAST;
=> [1, Zofia], [3, Alicja], [2, NULL]

# NULLe zawsze pierwsze
SELECT id, name FROM table_name ORDER BY name ASC NULLS LAST;
=> [2, NULL], [3, Alicja], [1, Zofia]

SELECT id, name FROM table_name ORDER BY name DESC NULLS LAST;
=> [2, NULL], [1, Zofia], [3, Alicja]

Więcej informacji dostępne jest w dokumentacji PostgreSQL.

Docker - przydatne polecenia

Kilka użytecznych poleceń przy pracy z docker’em.

Lista kontenerów z informacją o zajmowanym miejscu na dysku:

  docker ps -s

Podgląd używanych zasobów przez poszczególne kontenery:

  docker stats

Usuwanie nieużywanych kontenerów:

  docker rmi -f $(docker images | grep "<none>" | awk "{print \$3}")

watch unicorn - sprawdzenie kiedy się unicorn przeładuje

Aby nie wpisywać co kilka sekund polecenia sprawdzającego czy np. workery unicorna się przeładowały, możemy użyć polecenia:

  watch 'ps aux | grep master | grep unicorn | grep -v grep'

Po słowie kluczowym watch podejemy polecenie jakie ma zostać wykonane. Polecenie będzie powtarzane co 2 sekundy, aby zmienić interwał używamy opcji -n lub --interva.

Sprawdzanie istnienia rekordów

Istnienie rekordów w bazie danych można sprawdzić używając ActiveRecord na kilka sposobów. Poniżej zestawienie możliwych konstrukcji, wraz z zapytaniem SQL jakie generują. Większość zapytań generuje prawidłowe zapytanie, warto jednak zwrócić uwagę na te, które pobierają wszystkie rekordy z bazy i dopiero na pobranej kolekcji wykonują metody z modułu Enumerable.

Zapytania generujące prawidłowy SQL:

User.scoped.any?
SELECT COUNT(*) FROM "users"

User.scoped.empty?
SELECT COUNT(*) FROM "users"

User.scoped.blank?
SELECT COUNT(*) FROM "users"

User.scoped.exists?
SELECT 1 AS one FROM "users" LIMIT 1

User.scoped.present?
SELECT COUNT(*) FROM "users"

User.scoped.many?
SELECT COUNT(*) FROM "users"

Zapytania pobierające wszystkie rekordy:

User.scoped.none?
SELECT "users".* FROM "users"

User.scoped.one?
SELECT "users".* FROM "users"

URI.join - łatwa modyfikacja ścieżki w adresie URL

Często spotykamy się z sytuacją, kiedy do adresu URL chcemy dołączyć odpowiednią ścieżkę, tworząc tym sposobem kompletny URI. Wydaje się to trywialne, jednak bardzo łatwo może rodzić błędy.

Najprostsze rozwiązanie to zwykła konkatenacja stringów:

base_url = 'https://www.infakt.pl/'
path = 'sample_path'

"#{base_url}#{sample_path}"
=> 'https://www.infakt.pl/sample_path'

Na pierwszy rzut oka wszystko jest w porządku. Jeżeli jednak wartość base_url nie będzie oczywista (np. będzie pochodzić z ustawień zdefiniownych w innym miejscu aplikacji) bardzo łatwo o wygenerowanie błednego adresu. Również w przypadku wartości określającej ścieżkę, możemy w prosty sposób doprowadzić do błędu:

# brak backslasha między hostem a ścieżką
base_url = 'https://www.infakt.pl'
path = 'sample_path'

"#{base_url}#{sample_path}"
=> 'https://www.infakt.plsample_path'

# zdublowany backslash między hostem a ścieżką
base_url = 'https://www.infakt.pl/'
path = '/sample_path'

"#{base_url}#{sample_path}"
=> 'https://www.infakt.pl//sample_path'

Kolejny sposób, to parsowanie adresu URL i dodanie ścieżki, co nie wydaje się być zbyt eleganckie, a dodatkowo obarczone jest ryzykiem wygenerowania wyjątku, w przypadku kiedy ścieżka nie jest ścieżką absolutną.

URI.parse('https://www.infakt.pl/').tap { |uri| uri.path = '/sample_path' }.to_s
 => "https://www.infakt.pl/sample_path"

URI.parse('https://www.infakt.pl').tap { |uri| uri.path = '/sample_path' }.to_s
=> 'https://www.infakt.pl/sample_path'

URI.parse('https://www.infakt.pl/').tap { |uri| uri.path = 'sample_path' }.to_s
URI::InvalidComponentError: bad component(expected absolute path component): sample_path

Na szczęście, równie łatwo jak wygenerować błąd, możemy się przed nim zabezpieczyć, używając URI.join, które łączy domenę ze ścieżką, dbając jednocześnie o poprawność wygenerowego adresu:

URI.join('https://www.infakt.pl/', 'sample_path')
=> 'https://www.infakt.pl/sample_path'

URI.join('https://www.infakt.pl', 'sample_path')
=> 'https://www.infakt.pl/sample_path'

URI.join('https://www.infakt.pl/', '/sample_path')
=> 'https://www.infakt.pl/sample_path'

URI.join w dokumentacji Ruby

$LOAD_PATH - łatwiejsze ładowanie kodu

Przyzwyczajenie do znanego z Rails automatycznego ładowania całego kodu aplikacji przy jej starcie powoduje, że przy pracy z mikroserwisami, gdzie często cały kod aplikacji składa się z klikunastu plików może pojawić się problem z ładowaniem potrzebnych plików.

Pierwszym rozwiązaniem jakie przychodzi do głowy jest instrukcja require_relative umożliwiająca wskazanie ścieżki względnej w stosunku do pliku w którym ta instrukcja się znajduje. Fakt - wygodne, ale trzeba pilnować ilości ../ w ścieżce, co sprawia że bardzo łatwo o pomyłkę.

Z pomocą przychodzi globalna zmienna predefiniowana w Ruby - $LOAD_PATH - której wartość to tablica zawierająca ścieżki znane aplikacji, z których możemy ładować pliki. Wystarczy dodać do tej ścieżki główny katalog naszej aplikacji, i potem możemy już ładować pliki zawsze względem katalogu głównego. To znacznie ułatwia ustalenie właściwej ścieżki, i wydaje się też być bardziej naturalne.

boot.rb
app
  services
    user
      finder.rb
    
lib
  mapper.rb

Dla podanej struktury aplikacji, aby załadować plik lib/mapper.rb wewnątrz pliku app/servives/user/finder.rb należałoby użyć instrukcji:

 
# app/servives/user/finder.rb
require_relative '../../../lib/mapper.rb'

Po odpowiednim ustawieniu $LOAD_PATH w pliku startowym aplikacji (boot.rb) instrukcja ta staje się dużo prostsza:

 
# boot.rb
$LOAD_PATH.unshift File.expand_path('../', __FILE__)

# app/servives/user/finder.rb
require 'lib/mapper.rb'

Warto wiedzieć, że $LOAD_PATH jest tylko zdecydowanie czytelniejszym aliasem do innej globalnej zmiennej predefiniowanej - $:.

httplog - łatwe śledzenie komunikacji HTTP w aplikacji

Gem httplog pozwala w bardzo łatwy sposób podglądać zapytania HTTP generowane przez aplikację. Jest to szczególnie przydatne w przypadku komunikacji z zewnętrznymi serwisami i pozwala szybko zorientować się pod jaki adres oraz z jakimi danymi wysyłane są dane oraz jaka jest odpowiedź zdalnego serwera.

Uruchomienie jest banalne: dodajemy gem do Gemfile oraz uruchamiamy bundle install, a następnie dołączamy gem do aplikacji - require 'httplog'.

Gem pozwala na szczegółową konfigurację tego jakie informacje będą pokazywane, dokąd będa wysyłane (stdout, logger) oraz w jakis sposób będą pokazywane (np. kolorowanie).

Przykładowa informacja widoczna w konsoli przy dodawaniu notatki do leada w BaseCRM:

NoteCreator.new(user: u, note: 'notatka testowa').perform
DEBUG -- : [httplog] Connecting: api.getbase.com:443
DEBUG -- : [httplog] Sending: POST http://api.getbase.com:443/v2/notes
DEBUG -- : [httplog] Data: {"data":{"content":"notatka testowa","resource_id":123123123,"resource_type":"lead"}}
DEBUG -- : [httplog] Status: 200
DEBUG -- : [httplog] Benchmark: 0.236076 seconds
DEBUG -- : [httplog] Response:
{"data":{"id":231231231,"content":"notatka testowa","created_at":"2016-08-11T11:48:34Z","updated_at":"2016-08-11T11:48:34Z","creator_id":34534,"resource_type":"lead","resource_id":456456456},"meta":{"type":"note"}}

httplog na GitHub