Centralizacja logów z kontenerów Docker w usłudze Amazon CloudWatch Logs

Podczas tworzenia oprogramowania staram się wykorzystywać możliwości jakie niesie ze sobą konteneryzacja aplikacji oraz usługi chmury Amazon Web Services. Praca w środowisku skonteneryzowanym wymaga pewnych dodatkowych narzędzi aby stała się bardziej przyjemna, szczególnie gdy rozwijamy system oparty o wiele współpracujących ze sobą kontenerów.

Chciałbym przedstawić Ci sposób na agregację i centralizację logów w usłudze Amazon CloudWatch Logs z aplikacji uruchomionych w kontenerach Docker.

Docker Logs

Docker zapewnia nam kilka wbudowanych sterowników logów. Podstawowym, a zarazem domyślnym jest JSON File logging driver czyli logowanie wszystkich pojawiających się logów na strumieniu stdout oraz stderr do pliku w formacie JSON.

Aby podglądać na bieżąco logi wystarczy wykorzystać wbudowane polecenie:

$: docker logs [container id] --follow

W przypadku pracy nad pojedynczym kontenerem, uruchamianym lokalnie rozwiązanie bywa te wystarczające. Natomiast jak radzić sobie z wieloma kontenerami? Przeglądanie każdego pliku z osobna, przeszukiwanie staje się bardziej uciążliwe, a tym bardziej gdy pojawia się błąd, którego nie jesteśmy jednoznacznie zlokalizować np. który z uruchomionych kontenerów źle przetwarza wiadomość z kolejki.

Z pomocą przychodzi nam Docker, ponieważ strumień logów może zostać przekierowany między innymi do następujących systemów/protokołów centralnego logowania:

Dzięki temu za pomocą odpowiedniej konfiguracji (dla wszystkich lub wybranych kontenerów) możemy przesyłać logi do odpowiedniego rozwiązania.

Amazon CloudWatch Logs

Amazon CloudWatch to kombajn do monitorowania zasobów. Umożliwia zbieranie metryk oraz logów, monitorowanie danych (pulpity z wykresami, licznikami itp.), analizę danych oraz definiowanie i zarządzanie alarmami.

W moim przypadku wykorzystuję pod usługę Amazon CloudWatch Logs odpowiadającą za przechowywanie zgrupowanych logów, jednocześnie udostępniając CloudWatch Logs Insights do przeszukiwania logów dedykowanym językiem zapytań.

Ważne jest aby zapoznać się z obowiązującą terminologią i hierarchią:

  • Event – elementarna informacja – pojedynczy wpis w logu,
  • Stream – zbiór logów, pochodzących z jednego źródła – każdy kontener to osobny stream,
  • Group – grupa streamów powiązanych ze sobą – można tworzyć grupy względem schematu [PROJEKT-ŚRODOWISKO URUCHOMIENIOWE] czyli np. devenv-prod.

Dla grupy streamów możemy zdefiniować czas życia logów po przekroczeniu, którego zostaną one usunięte. Przydatną opcją jest także możliwość wyeksportowania danych do usługi pamięci masowej Amazon S3.

Ostatnim elementem o którym chcę wspomnieć jest Metric Filter. Zasada jest bardzo prosta – na podstawie wzorca wyliczamy metrykę, a na następnie względem jej występowania w danym okresie możemy zareagować odpowiednim alarmem.

W praktyce zastosowanie może wyglądać następująco: jeśli w przeciągu 5 minut pojawi się więcej niż 10 logów o najwyższym priorytecie (fatal) to wygeneruj alarm z powiadomieniem na maila.

CloudWatch Logs - Alarmy

Oczywiście powiadomienie mailowe możemy zastąpić np. wyzwoleniem AWS Lambda – co zaprezentowałem na diagramie powyżej. Finalnie informacja o wygenerowanym alarmie może trafić w dowolne, docelowe miejsce np. jako wiadomość w komunikatorze Slack.

Aplikacja testowa

Na potrzeby testów związanych z logami w środowisku Docker przygotowałem prostą aplikację Hello Api, której kod źródłowy umieściłem w serwisie GitHub.

Aplikacja udostępnia kilka endpointów (np. /fatal, /info itp.), a następnie loguje informacje o przetworzonym żądaniu HTTP.

Od strony implementacji wykorzystałem framework Fastify, który używa do rejestracji logów bibliotekę pino. Cel aplikacji jest prosty – przekierować wszystkie logi na strumienie stdout i stderr.

Aby usprawnić sobie pracę ale także utrzymać pewien standard w projektach spełniam dwie zasady:

  • każdy log zapisany jest w jednolitym stylu – posługuję się dobrze znanym formatem wymiany danych – JSON;
  • poszczególne logi oddzielone są od siebie tym samym znacznikiem – żadnego rocket science, wystarcza znak nowej linii.

W przypadku Amazon CloudWatch Logs, znak nowej linii pozwala od razu interpretować każdą linię jako osobny log, a format JSON jest nie tylko „ładnie zaprezentowany” ale także dekodowany do struktury, którą można przeszukiwać względem wybranego pola.

Uruchomienie kontenera

Przed uruchomieniem kontenera musimy zapewnić dostęp do usług AWS. Możemy zrealizować to za pomocą:

  • zmiennych środowiskowych – AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY i AWS_SESSION_TOKEN,
  • pliku poświadczeń – ~/.aws/credentials dla użytkownika root,
  • jeśli Docker deamon uruchomiony jest na instancji Amazon EC2 to poprzez przypisanie IAM Role (z dostępem do akcji logs:CreateLogStream i logs:PutLogEvents).

Uruchomienie kontenera tak aby jego logi zostały przekazane do usługi Amazon CloudWatch Logs wymaga podania kilku opcji konfiguracyjnych --log-opt.

docker run --log-driver=awslogs \
   --log-opt awslogs-region=eu-west-1b \
   --log-opt awslogs-group=hello-api-service-group \
   --log-opt awslogs-stream=hello-api-service-stream \
   --log-opt awslogs-create-group=true \
   -i \
   -p 3000:3000 \
   devenv/hello-api:latest

Powyższy przykład jest wystarczający aby logi kontenera zostały wysłane do AWS. Objaśniając poszczególne opcje:

  • --log-driver=awslogs – ustawiamy Docker log driver dla AWS,
  • --log-opt awslogs-region=eu-west-1b – AWS Region,
  • --log-opt awslogs-group=hello-api-service-group – nazwa grupy,
  • --log-opt awslogs-stream=hello-api-service-stream – nazwa streamu,
  • --log-opt awslogs-create-group=true – jeśli grupa nie istnieje pozwól na jej stworzenie.

Jeżeli aplikacja nie stosuje praktyki jeden log = jeden wiersz, można zmienić zachowanie parsowania logów i stworzyć własne wyrażenie, które będzie definiowało zakres jednego loga. W tym celu należy ustawić awslogs-multiline-pattern.

Po uruchomieniu kontenera z aplikacją testową możemy wysłać requesty HTTP, a wygenerowane logi zapisywane są w usłudze Amazon CloudWatch Logs.

Jak wyglądają przesłane logi

Pierwszym widokiem jest lista wszystkich utworzonych grup:

CloudWatch Logs - Grupy

Z tego poziomu mamy dostęp do opcji związanych z zarządzaniem grupą, tworzeniem metryk ale także eksportowaniem danych do S3.

Po wejściu w konkretną grupę otrzymujemy listę streamów:

CloudWatch Logs - Lista streamów

Jak wspomniałem wcześniej jeden stream to logi z jednego kontenera Docker. Z tego poziomu możemy także przejść za pomocą przycisku Search Log Group do prostego przeszukiwania wszystkich logów w grupie.

Aby przejść dalej należy wybrać stream, następnym widokiem jest ograniczona do danego streamu prosta wyszukiwarka i filtr zakresu czasu.

CloudWatch Logs - Lista eventów

Szczegóły konkretnego eventu są już parsowane do czytelniejszej formy:

CloudWatch Logs - Szczegóły eventu

W powyższym logu dla endpointa /info w aplikacji dodałem kilka swoich pól aby pokazać, że oprócz samej wiadomości msg możemy przekazać zestaw własnych, dowolnych danych.

CloudWatch Logs Insights – Zaawansowane przeszukiwanie

Nie zawsze jednak proste wyszukiwanie jest dla nas wystarczające. Czasem potrzebujemy wyszukiwać wartości w konkretnym polu, sortować wyniki czy też wybierać tylko niektóre informacje z logu. W tym celu warto zapoznać się z CloudWatch Logs Insights.

Usługa udostępnia dedykowany język zapytań pozwalający przeszukiwać dane w podobny sposób jak język SQL. Interesowało mnie pobieranie logów dla błędów o priorytecie równym 60 (fatal) w chronologii od najnowszego do najstarszego – z zawężeniem przeszukiwania godzinę w wstecz w wybranej grupie.

Zapytanie do pobrania takiego zestawu danych wygląda następująco:

fields @logStream, @message
| filter level = 60
| sort @timestamp desc

Resztę warunków określiłem za pomocą kontrolek w interfejsie graficznym (czas i grupa).

CloudWatch Logs Insights - Filtry

Wyniki prezentowane są na liście logów z możliwością podejrzenia szczegółów. Możemy także wizualizować dane w postaci wykresów.

CloudWatch Logs Insights - Wyniki wyszukiwania

Niezależnie od formy prezentacji wyników, każde z takich zapytań może zostać przypięte na pulpit w Amazon CloudWatch jako widget. Dzięki temu nie musimy ręcznie wykonywać zapytania, a jego wyniki zostaną zaprezentowane wraz z resztą interesujących nas metryk na jednym widoku.

Podsumowanie

Jak widać cały proces uruchomienia centralnego logowania w usłudze Amazon CloudWatch Logs składa się z kilku prostych kroków.

Gdzie warto zastosować?

Ja korzystam z rozwiązania na wszystkich środowiskach testowych. Usługę konfiguruję tak aby przechowywać logi strumienia maksymalnie przez 7 dni oraz w zależności od potrzeby loguję jedynie informacje o odpowiednim priorytecie. Jeśli tylko zaczynasz nie ogarniać logów z swoich kontenerów to jest to odpowiedni moment na zastanowienie się nad usługą ich centralizacji.

Co mi to dało?

Przede wszystkim bezobsługowe środowisko do gromadzenia logów, szybkie ich przeszukiwanie, możliwość tworzenia metryk, które po przekroczeniu ustawionego limitu mogą poinformować mnie odpowiednim kanałem komunikacji. Wszystko out of the box.

Czy musiałem się nad tym wszystkim napracować?

Nie. Wystarczyło skonfigurować uruchamianie kontenera oraz zapewnić odpowiednią formę wyjściową logu z aplikacji. Raz wykonana czynność jest później powtarzalna na całą resztę aplikacji systemu.

A w jaki sposób wygląda u Ciebie organizacja logów z aplikacji uruchomionych w kontenerach?

Na co dzień programujący CTO w Emphie Solutions. Projektuje, tworzy oraz wdraża rozwiązania oparte o ekosystem JavaScript. Rozwija swoje umiejętności z zakresu Cloud / DevOps / SRE. Fascynat programowania, architektury, chmury i dobrych praktyk w szerokim ujęciu. Na temat technologii publikuje materiały w ramach projektu DevEnv, którego jest założycielem.
PODZIEL SIĘ