ELK Stack configuration using Ansible

Często w naszej pracy otrzymujemy informację od klienta, że w aplikacji wystąpił błąd z kodem http 500. Tester szybko tworzy scenariusz odtworzenia błędu i pojawia się inny problem. Pod ładnym frontendem stoi kilkanaście aplikacji zainstalowanych na różnych serwerach. Informacja o błędzie niewątpliwie znajduje się w logu – tylko w którym? Można spędzić kilka dni na próbie znalezienia właściwego pliku na właściwym serwerze lub wykorzystać pakiet narzędzi, który pozwala na agregację logów.

14 maja 2021

Często w naszej pracy otrzymujemy informację od klienta, że w aplikacji wystąpił błąd z kodem http 500. Tester szybko tworzy scenariusz odtworzenia błędu i pojawia się inny problem. Pod ładnym frontendem stoi kilkanaście aplikacji zainstalowanych na różnych serwerach. Informacja o błędzie niewątpliwie znajduje się w logu – tylko w którym? Można spędzić kilka dni na próbie znalezienia właściwego pliku na właściwym serwerze lub wykorzystać pakiet narzędzi, który pozwala na agregację logów.

ELK Stack

ELK to akronim od zestawu Elasticsearch, Logstash i Kibana. Każdy z tych elementów pełni określoną rolę.

  1. Elasticsearch (ES) odpowiada za indeksowanie danych. W naszym przypadku będą to logi – ES pełni też tutaj funkcję swego rodzaju bazy danych, gdzie będą one zbierane.
  2. Logstash z kolei służy do zbierania danych, filtrowania ich, łączenia lub wysyłania w różne miejsca. Oczywiście można wysyłać dane bezpośrednio do ES, natomiast Logstash dodaje możliwość odfiltrowania niektórych danych lub rozdzielenia ich na różne strumienie. Co więcej – na podstawie treści przesyłanych komunikatów można dodać metadane, które ostatecznie wylądują w ES.
  3. Kibana natomiast służy za front do wspomnianych wyżej narzędzi. Pozwala na definiowanie różnego rodzaju dashboardów, tworzenie wykresów, generowanie raportów etc.

Środowisko developerskie

Przygotujemy sobie czyste środowisko developerskie używając Vagranta. Instrukcja instalacji tego narzędzia znajduje się tutaj.

W dużym skrócie – Vagrant pozwoli na postawienie maszyny wirtualnej, którą będzie można łatwo i szybko wymienić na nową. Kody źródłowe, które pojawią się w dalszej części tego wpisu, znajdują się w tym repozytorium – https://github.com/greywarden09/elk-stack

Z kolei do samej konfiguracji środowiska zastosujemy Ansible. Instrukcja instalacji znajduje się tutaj. Konieczne będzie doinstalowanie kolekcji ansible.posix. Zachęcam do zapoznania się z podstawami używania tego narzędzia, pozwoli to na lepsze zrozumienie dalszej części. Przygotowałem również wersję do postawienia na lokalnym środowisku, z wykorzystaniem docker-compose. W repozytorium znajduje się podkatalog docker z plikiem docker-compose.yml.

Przygotowanie maszyny wirtualnej

W pierwszej kolejności postawimy sobie maszynę wirtualną, która posłuży nam za środowisko do instalacji. W tym celu należy przygotować plik Vagrantfile. Poniżej znajduje się przykładowa konfiguracja wykorzystująca libvirt jako provider:

Vagrant.configure("2") do |config|
  config.vm.box = 'generic/ubuntu2004'
  config.vm.hostname = 'elk-stack'
  config.vm.provider :libvirt do |libvirt|
    libvirt.memory = 8192
    libvirt.random_hostname = true
    libvirt.cpus = 2
  end
  config.vm.define :network do |network|
    network.vm.network :private_network, :ip => '172.168.0.2'
  end
end

Powyższa konfiguracja definiuje konfigurację maszyny z Ubuntu 20.04, ustawia 2 procesory, 8192 MB RAM oraz losową nazwę hosta. Skonfigurowana jest również sieć prywatna, gdzie maszyna otrzyma adres 172.168.0.2. Oczywiście nic nie stoi na przeszkodzie, aby użyć innego providera, takiego jak VirtualBox czy VMWare. Tutaj z pomocą przychodzi dokumentacja Vagranta, gdzie jest wyszczególnione, jak taki plik konfiguracyjny przygotować.

Znajdując się w tym samym katalogu, co Vagrantfile, możesz już uruchomić maszynę wirtualną, za pomocą polecenia:

vagrant up

Następnie potrzebna będzie konfiguracja SSH – wygenerujemy zatem taką korzystając z polecenia ssh-config:

vagrant ssh-config --host elk-stack > elk-stack

To polecenie wygeneruje konfigurację potrzebną w dalszych krokach – pojawi się plik o nazwie elk-stack, z zawartością podobną do tej:

Host elk-stack
  HostName 192.168.121.93
  User vagrant
  Port 22
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /home/mlas/poligon/elk-stack/vagrant/.vagrant/machines/network/libvirt/private_key
  IdentitiesOnly yes
  LogLevel FATAL

Ostatnim etapem jest przygotowanie inventory pod Ansible. Tworzymy zatem plik vagrant.yml o następującej zawartości:

all:
  hosts:
    elk:
      ansible_host: elk-stack
      ansible_user: vagrant
      ansible_ssh_common_args: -F vagrant/elk-stack[1]
    vars:
      ansible_python_interpreter: /usr/bin/python3

Zwróć uwagę na wcięcia! Parametr ansible_ssh_common_args[1] zawiera dodatkowy parametr wskazujący lokalizację pliku z konfiguracją SSH.
Mamy już postawione czyste środowisko, na którym zainstalujemy ELK stack. Możesz zweryfikować działanie maszyny – na przykład logując się na nią poprzez SSH.


ssh elk-stack -F vagrant/elk-stack

Do zatrzymania maszyny wirtualnej służy polecenie vagrant halt, natomiast do jej usunięcia – vagrant destroy.

Instalacja ELK Stack na maszynie wirtualnej

W dalszej części artykułu będę się posługiwał strukturą plików i katalogów z repozytorium, do którego link znajduje się w części Środowisko developerskie.

Do uruchomienia poszczególnych części składowych ELK Stack wykorzystamy Dockera. Skorzystamy zatem z roli, która tego Dockera zainstaluje:

ansible-playbook -i vagrant/vagrant.yml bootstrap.yml

Docker się zainstalował, zatem można przystąpić do instalacji reszty elementów. Do tego służy playbook site.yml, który instaluje Elasticsearcha, Logstasha i Kibanę. Przejdziemy teraz po konfiguracji.

Zacznijmy od Elastica.

- name: create Docker volumes for Elasticsearch
  include_role:
    name: common
    tasks_from: prepare-volumes[1]
  with_items:
    - { name: elasticsearch-data, path: "{{ elasticsearch.data_dir }}" }
    - { name: elasticsearch-conf, path: "{{ elasticsearch.conf_dir }}" }

- name: increase max_map_count in sysctl
  block:
    - ansible.posix.sysctl:
        name: vm.max_map_count
        value: 262144[2]
        state: present
        reload: True

- name: pull Elasticsearch image[3]
  docker_image:
    name: docker.elastic.co/elasticsearch/elasticsearch
    tag: "{{ elasticsearch.version }}"
    source: pull

- name: start Elasticsearch[4]
  docker_container:
    name: elasticsearch
    image: docker.elastic.co/elasticsearch/elasticsearch:{{ elasticsearch.version }}
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
      - elasticsearch-conf:/usr/share/elasticsearch/config
    restart_policy: unless-stopped
    env:
      node.name: elasticsearch
      discovery.type: 'single-node'[5]
      bootstrap.memory_lock: "true"
      ES_JAVA_OPTS: "-Xms512m -Xmx512m"
    ports[6]:
      - 9200:9200
      - 9300:9300
    ulimits:
      - 'memlock:-1:-1'[7]
    networks[8]:
      - name: elk
    purge_networks: True

Najpierw przygotowujemy dwa wolumeny dla kontenera – potrzebujemy zachować konfigurację oraz dane w przypadku usunięcia kontenera. Taski podlinkowane w linii 4 za pomocą dyrektywy tasks_from[1] w pierwszej kolejności tworzą katalog (lub katalogi, jeżeli struktura jest zagnieżdżona), a następnie wolumen dockerowy o określonej nazwie. W tym przypadku utworzą się dwa wolumeny – elasticsearch-data oraz elasticsearch-conf. Następnym punktem, który przyprawia o ból głowy każdego, kto pierwszy raz stawia Elastica – jest zwiększenie wartości flagi vm.max_map_count do minimum 262144[2]. Przyczynę takiego stanu rzeczy można sprawdzić na stronie z dokumentacją. Potem już z górki – pobranie obrazu[3] i jego wystartowanie[4]. Dysponujemy jedną instancją, zatem wyłączamy poszukiwanie innych w ramach jednego klastra – ustawiamy flagę discovery.type na single-node[5]. Wystawiamy porty[6] 9200 i 9300, ustawiamy memlock w obu przypadkach (soft i hard) na -1[7]. Na razie pozostaje wziąć na wiarę to ustawienie. Na koniec wpinamy kontener do sieci elk i wypinamy go z sieci domyślnych[8].

Pora na Logstasha. Z pewnością zauważysz, że rola wygląda bardzo podobnie:

- name: create Docker volumes for Logstash[1]
  include_role:
    name: common
    tasks_from: prepare-volumes
  with_items:
    - { name: logstash-conf, path: "{{ logstash.conf_dir }}" }
    - { name: logstash-pipeline, path: "{{ logstash.pipeline_dir }}" }

- name: pull Logstash image
  docker_image:
    name: docker.elastic.co/logstash/logstash
    tag: "{{ logstash.version }}"
    source: pull

- name: deploy Logstash pipeline configuration[2]
  copy:
    src: logstash.conf
    dest: "{{ logstash.pipeline_dir }}"

- name: start Logstash
  docker_container:
    name: logstash
    image: docker.elastic.co/logstash/logstash:{{ logstash.version }}
    volumes:
      - logstash-pipeline:/usr/share/logstash/pipeline
      - logstash-conf:/usr/share/logstash/config
    restart_policy: unless-stopped
    env:
      monitoring.elasticsearch.hosts: "http://elasticsearch:9200"[3]
      LS_JAVA_OPTS: "-Xms512m -Xmx512m"
    ports:
      - 9600:9600
      - 5044:5044
    networks:
      - name: elk[4]
    purge_networks: True

Analogicznie jak w przypadku Elastica, najpierw przygotowujemy wolumeny, tym razem na konfigurację oraz definicję pipeline (wejścia i wyjścia, filtrowanie, ogółem – konfiguracja działań na strumieniach danych), wystawiamy porty oraz konfigurujemy lokalizację Elasticsearcha za pomocą zmiennej środowiskowej monitoring.elasticsearch.hosts[3]. Zwróć uwagę na fakt, że nie użyłem localhost, lecz nazwy domenowej kontenera. Jest to możliwe tylko wtedy, kiedy te znajdują się w tej samej sieci – tutaj jest to elk[4].
Jest jednak pewien task, który robi co innego – tutaj nazywa się on deploy Logstash pipeline configuration[2]. Wrzuca on plik z konfiguracją pipeline Logstasha do katalogu podlinkowanego w liście wolumenów. Plik ten wygląda tak:

input {
  beats {
    port => 5044
  }
}

output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    index => "%{[@metadata][beat]}-%{[@metadata][version]}"
  }
}

Zatem definiujemy wejście (input) typu beats, które nasłuchuje na porcie 5044. To, co wpadnie, przekazywane jest do wyjścia (output) – tutaj jest to elasticsearch, słuchający na porcie 9200.

Na samym końcu znajduje się Kibana.

- name: create Docker volume for Kibana
  include_role:
    name: common
    tasks_from: prepare-volumes
  with_items:
    - { name: kibana-conf, path: "{{ kibana.conf_dir }}" }

- name: pull Kibana image
  docker_image:
    name: docker.elastic.co/kibana/kibana
    tag: "{{ kibana.version }}"
    source: pull

- name: start Kibana
  docker_container:
    name: kibana
    image: docker.elastic.co/kibana/kibana:{{ kibana.version }}
    volumes:
      - kibana-conf:/usr/share/kibana/config
    restart_policy: unless-stopped
    env:
      elasticsearch.url: "http://elasticsearch:9200"
      elasticsearch.hosts: "http://elasticsearch:9200"
      server.name: kibana
    ports:
      - 5601:5601
    networks:
      - name: elk
    purge_networks: True

Tutaj sytuacja jest najprostsza – przygotowujemy wolumen na konfigurację, ściągamy obraz, a następnie startujemy kontener. Jeżeli wszystko poszło dobrze, to po wdrożeniu całości ELK Stack powinno być możliwe wejście na stronę główną Kibany. Wdrożenie uruchamiamy poniższym poleceniem:

ansible-playbook -i vagrant/vagrant.yml site.yml

Adres w przeglądarce jest tym samym adresem, który znajduje się w pliku z konfiguracją SSH – w moim przypadku jest to 192.168.121.87, ale po każdym odtworzeniu maszyny wirtualnej (usunięciu i postawieniu jej ponownie) może być inny.

Podsumowanie

W następnej części opiszę konfigurację Filebeat oraz Metricbeat. Pierwsze narzędzie służy do zbierania danych z logów, a następnie wysyłania ich do Logstasha lub Elastica. Natomiast Metricbeat pozwala na zbieranie metryk – na przykład zużycia CPU, RAM, liczby operacji zapisu/odczytu i innych.

bannerbanner

Your software development experts

We’re a team of experienced and skilled software developers – and people you’ll enjoy working with.

Start Your Projectadd