ELK zu AWS: Verwalten von Protokollen mit weniger Aufwand
Veröffentlicht: 2022-03-11Elasticsearch ist eine leistungsstarke Softwarelösung zur schnellen Suche nach Informationen in einer Vielzahl von Daten. Zusammen mit Logstash und Kibana bildet dies den umgangssprachlich „ELK-Stack“ und wird häufig zum Sammeln, Zwischenspeichern, Analysieren und Visualisieren von Protokolldaten verwendet. In der Regel werden einige andere Softwarekomponenten benötigt, z. B. Filebeat, um die Protokolle vom Server an Logstash zu senden, und Elastalert, um Warnungen basierend auf den Ergebnissen einiger Analysen zu generieren, die auf den in Elasticsearch gespeicherten Daten ausgeführt wurden.
Der ELK Stack ist mächtig, aber…
Meine Erfahrung mit der Verwendung von ELK zum Verwalten von Protokollen ist ziemlich gemischt. Einerseits ist es sehr leistungsfähig und die Bandbreite seiner Fähigkeiten ist ziemlich beeindruckend. Auf der anderen Seite ist es schwierig einzurichten und kann Kopfschmerzen bei der Wartung bereiten.
Fakt ist, dass Elasticsearch im Allgemeinen sehr gut ist und in den unterschiedlichsten Szenarien eingesetzt werden kann; es kann sogar als Suchmaschine verwendet werden! Da es nicht auf die Verwaltung von Protokolldaten spezialisiert ist, erfordert dies mehr Konfigurationsarbeit, um sein Verhalten an die spezifischen Anforderungen der Verwaltung solcher Daten anzupassen.
Das Einrichten des ELK-Clusters war ziemlich knifflig und erforderte, dass ich mit einer Reihe von Parametern herumspielte, um es schließlich zum Laufen zu bringen. Dann kam die Arbeit der Konfiguration. In meinem Fall musste ich fünf verschiedene Softwarekomponenten konfigurieren (Filebeat, Logstash, Elasticsearch, Kibana und Elastalert). Dies kann eine ziemlich mühsame Aufgabe sein, da ich die Dokumentation durchlesen und ein Element der Kette debuggen musste, das nicht mit dem nächsten kommuniziert. Selbst nachdem Sie Ihren Cluster endlich zum Laufen gebracht haben, müssen Sie noch routinemäßige Wartungsarbeiten daran durchführen: Patchen, Aktualisieren der Betriebssystempakete, Überprüfen der CPU-, RAM- und Festplattennutzung, Vornehmen kleinerer Anpassungen nach Bedarf usw.
Mein gesamter ELK-Stack funktionierte nach einem Logstash-Update nicht mehr. Bei näherer Betrachtung stellte sich heraus, dass die ELK-Entwickler aus irgendeinem Grund beschlossen, ein Schlüsselwort in ihrer Konfigurationsdatei zu ändern und es zu pluralisieren. Das war der letzte Strohhalm und ich beschloss, nach einer besseren Lösung zu suchen (zumindest eine bessere Lösung für meine speziellen Bedürfnisse).
Ich wollte Protokolle speichern, die von Apache und verschiedenen PHP- und Node-Apps generiert wurden, und sie parsen, um Muster zu finden, die auf Fehler in der Software hinweisen. Die Lösung, die ich gefunden habe, war die folgende:
- Installieren Sie CloudWatch Agent auf dem Ziel.
- Konfigurieren Sie CloudWatch Agent, um die Protokolle an CloudWatch-Protokolle zu senden.
- Auslösen des Aufrufs von Lambda-Funktionen zum Verarbeiten der Protokolle.
- Die Lambda-Funktion würde Nachrichten an einen Slack-Kanal senden, wenn ein Muster gefunden wird.
- Wenden Sie nach Möglichkeit einen Filter auf die CloudWatch-Protokollgruppen an, um zu vermeiden, dass die Lambda-Funktion für jedes einzelne Protokoll aufgerufen wird (was die Kosten sehr schnell in die Höhe treiben könnte).
Und das war's auf hohem Niveau! Eine 100 % serverlose Lösung, die wartungsfrei funktioniert und sich ohne zusätzlichen Aufwand gut skalieren lässt. Die Vorteile solcher serverlosen Lösungen gegenüber einem Cluster von Servern sind zahlreich:
- Im Wesentlichen liegen alle routinemäßigen Wartungsarbeiten, die Sie regelmäßig auf Ihren Cluster-Servern durchführen würden, jetzt in der Verantwortung des Cloud-Anbieters. Jeder zugrunde liegende Server wird für Sie gepatcht, aktualisiert und gewartet, ohne dass Sie es überhaupt wissen.
- Sie müssen Ihren Cluster nicht mehr überwachen und delegieren alle Skalierungsprobleme an den Cloud-Anbieter. Tatsächlich wird ein serverloses Setup wie das oben beschriebene automatisch skaliert, ohne dass Sie etwas tun müssen!
- Die oben beschriebene Lösung erfordert weniger Konfiguration, und es ist sehr unwahrscheinlich, dass der Cloud-Anbieter eine bahnbrechende Änderung in die Konfigurationsformate einbringt.
- Schließlich ist es ganz einfach, einige CloudFormation-Vorlagen zu schreiben, um all dies als Infrastructure-as-Code bereitzustellen. Dasselbe zu tun, um einen ganzen ELK-Cluster einzurichten, würde viel Arbeit erfordern.
Slack-Warnungen konfigurieren
Kommen wir also jetzt zu den Details! Lassen Sie uns untersuchen, wie eine CloudFormation-Vorlage für ein solches Setup aussehen würde, komplett mit Slack-Webhooks für die Benachrichtigung von Ingenieuren. Wir müssen zuerst das gesamte Slack-Setup konfigurieren, also lasst uns darauf eingehen.
AWSTemplateFormatVersion: 2010-09-09 Description: Setup log processing Parameters: SlackWebhookHost: Type: String Description: Host name for Slack web hooks Default: hooks.slack.com SlackWebhookPath: Type: String Description: Path part of the Slack webhook URL Default: /services/YOUR/SLACK/WEBHOOK
Dazu müssten Sie Ihren Slack-Arbeitsbereich einrichten. Weitere Informationen finden Sie in diesem WebHooks-Leitfaden für Slack.
Sobald Sie Ihre Slack-App erstellt und einen eingehenden Hook konfiguriert haben, wird die Hook-URL zu einem Parameter Ihres CloudFormation-Stacks.
Resources: ApacheAccessLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 100 # Or whatever is good for you ApacheErrorLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 100 # Or whatever is good for you
Hier haben wir zwei Protokollgruppen erstellt: eine für die Apache-Zugriffsprotokolle , die andere für die Apache-Fehlerprotokolle .
Ich habe keinen Lebenszyklusmechanismus für die Protokolldaten konfiguriert, da dies den Rahmen dieses Artikels sprengen würde. In der Praxis möchten Sie wahrscheinlich ein verkürztes Aufbewahrungsfenster haben und S3-Lebenszyklusrichtlinien entwerfen, um sie nach einer bestimmten Zeit zu Glacier zu verschieben.
Lambda-Funktion zum Verarbeiten von Zugriffsprotokollen
Lassen Sie uns nun die Lambda-Funktion implementieren, die die Apache-Zugriffsprotokolle verarbeitet.
BasicLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Hier haben wir eine IAM-Rolle erstellt, die an die Lambda-Funktionen angehängt wird, damit sie ihre Aufgaben erfüllen können. Tatsächlich ist die AWSLambdaBasicExecutionRole
(trotz ihres Namens) eine von AWS bereitgestellte IAM-Richtlinie. Es ermöglicht der Lambda-Funktion lediglich, eine Protokollgruppe und Protokollstreams innerhalb dieser Gruppe zu erstellen und dann ihre eigenen Protokolle an CloudWatch Logs zu senden.
ProcessApacheAccessLogFunction: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !GetAtt BasicLambdaExecutionRole.Arn Runtime: python3.7 Timeout: 10 Environment: Variables: SLACK_WEBHOOK_HOST: !Ref SlackWebHookHost SLACK_WEBHOOK_PATH: !Ref SlackWebHookPath Code: ZipFile: | import base64 import gzip import json import os from http.client import HTTPSConnection def handler(event, context): tmp = event['awslogs']['data'] # `awslogs.data` is base64-encoded gzip'ed JSON tmp = base64.b64decode(tmp) tmp = gzip.decompress(tmp) tmp = json.loads(tmp) events = tmp['logEvents'] for event in events: raw_log = event['message'] log = json.loads(raw_log) if log['status'][0] == "5": # This is a 5XX status code print(f"Received an Apache access log with a 5XX status code: {raw_log}") slack_host = os.getenv('SLACK_WEBHOOK_HOST') slack_path = os.getenv('SLACK_WEBHOOK_PATH') print(f"Sending Slack post to: host={slack_host}, path={slack_path}, url={url}, content={raw_log}") cnx = HTTPSConnection(slack_host, timeout=5) cnx.request("POST", slack_path, json.dumps({'text': raw_log})) # It's important to read the response; if the cnx is closed too quickly, Slack might not post the msg resp = cnx.getresponse() resp_content = resp.read() resp_code = resp.status assert resp_code == 200
Hier definieren wir also eine Lambda-Funktion, um Apache-Zugriffsprotokolle zu verarbeiten. Bitte beachten Sie, dass ich nicht das allgemeine Protokollformat verwende, das bei Apache standardmäßig verwendet wird. Ich habe das Zugriffsprotokollformat so konfiguriert (und Sie werden feststellen, dass es im Wesentlichen Protokolle im JSON-Format generiert, was die Verarbeitung im weiteren Verlauf erheblich vereinfacht):
LogFormat "{\"vhost\": \"%v:%p\", \"client\": \"%a\", \"user\": \"%u\", \"timestamp\": \"%{%Y-%m-%dT%H:%M:%S}t\", \"request\": \"%r\", \"status\": \"%>s\", \"size\": \"%O\", \"referer\": \"%{Referer}i\", \"useragent\": \"%{User-Agent}i\"}" json
Diese Lambda-Funktion ist in Python 3 geschrieben. Sie nimmt die von CloudWatch gesendete Protokollzeile und kann nach Mustern suchen. Im obigen Beispiel erkennt es nur HTTP-Anforderungen, die zu einem 5XX-Statuscode geführt haben, und sendet eine Nachricht an einen Slack-Kanal.

In Bezug auf die Mustererkennung können Sie alles tun, was Sie möchten, und die Tatsache, dass es sich um eine echte Programmiersprache (Python) handelt, im Gegensatz zu nur Regex-Mustern in einer Logstash- oder Elastalert-Konfigurationsdatei, gibt Ihnen viele Möglichkeiten, komplexe Mustererkennung zu implementieren .
Revisionskontrolle
Ein kurzes Wort zur Revisionskontrolle: Ich fand, dass es durchaus akzeptabel und bequem ist, den Code in CloudFormation-Vorlagen für kleine Dienstprogramm-Lambda-Funktionen wie diese zu haben. Für ein großes Projekt mit vielen Lambda-Funktionen und -Ebenen wäre dies natürlich höchstwahrscheinlich unpraktisch und Sie müssten SAM verwenden.
ApacheAccessLogFunctionPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref ProcessApacheAccessLogFunction Action: lambda:InvokeFunction Principal: logs.amazonaws.com SourceArn: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*
Das Obige erteilt CloudWatch Logs die Erlaubnis, Ihre Lambda-Funktion aufzurufen. Ein Wort der Vorsicht: Ich habe festgestellt, dass die Verwendung der Eigenschaft SourceAccount
zu Konflikten mit SourceArn
führen kann.
Im Allgemeinen würde ich vorschlagen, es nicht einzuschließen, wenn sich der Dienst, der die Lambda-Funktion aufruft, im selben AWS-Konto befindet. Der SourceArn
verbietet anderen Konten sowieso, die Lambda-Funktion aufzurufen.
ApacheAccessLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheAccessLogFunctionPermission Properties: LogGroupName: !Ref ApacheAccessLogGroup DestinationArn: !GetAtt ProcessApacheAccessLogFunction.Arn FilterPattern: "{$.status = 5*}"
Die Abonnementfilterressource ist die Verbindung zwischen CloudWatch Logs und Lambda. Hier werden an die ApacheAccessLogGroup
gesendete Protokolle an die oben definierte Lambda-Funktion weitergeleitet, aber nur die Protokolle, die das Filtermuster passieren. Hier erwartet das Filtermuster JSON als Eingabe (das status
beginnt mit '{' und endet mit '}') und stimmt nur dann mit dem Protokolleintrag überein, wenn er einen Feldstatus hat, der mit „5“ beginnt.
Das bedeutet, dass wir die Lambda-Funktion nur aufrufen, wenn der von Apache zurückgegebene HTTP-Statuscode ein 500-Code ist, was normalerweise bedeutet, dass etwas ziemlich Schlimmes vor sich geht. Dadurch wird sichergestellt, dass wir die Lambda-Funktion nicht zu oft aufrufen und dadurch unnötige Kosten vermeiden.
Weitere Informationen zu Filtermustern finden Sie in der Amazon CloudWatch-Dokumentation. Die CloudWatch-Filtermuster sind ziemlich gut, wenn auch offensichtlich nicht so leistungsfähig wie Grok.
Beachten Sie das DependsOn
-Feld, das sicherstellt, dass CloudWatch Logs die Lambda-Funktion tatsächlich aufrufen kann, bevor das Abonnement erstellt wird. Dies ist nur ein Sahnehäubchen, es ist höchstwahrscheinlich unnötig, da Apache in einem realen Szenario wahrscheinlich keine Anfragen vor mindestens ein paar Sekunden erhalten würde (z. B. um die EC2-Instance mit einem Load Balancer zu verbinden und die Last zu erhalten). Balancer, um den Status der EC2-Instance als fehlerfrei zu erkennen).
Lambda-Funktion zum Verarbeiten von Fehlerprotokollen
Sehen wir uns nun die Lambda-Funktion an, die die Apache-Fehlerprotokolle verarbeitet.
ProcessApacheErrorLogFunction: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !GetAtt BasicLambdaExecutionRole.Arn Runtime: python3.7 Timeout: 10 Environment: Variables: SLACK_WEBHOOK_HOST: !Ref SlackWebHookHost SLACK_WEBHOOK_PATH: !Ref SlackWebHookPath Code: ZipFile: | import base64 import gzip import json import os from http.client import HTTPSConnection def handler(event, context): tmp = event['awslogs']['data'] # `awslogs.data` is base64-encoded gzip'ed JSON tmp = base64.b64decode(tmp) tmp = gzip.decompress(tmp) tmp = json.loads(tmp) events = tmp['logEvents'] for event in events: raw_log = event['message'] log = json.loads(raw_log) if log['level'] in ["error", "crit", "alert", "emerg"]: # This is a serious error message msg = log['msg'] if msg.startswith("PHP Notice") or msg.startswith("PHP Warning"): print(f"Ignoring PHP notices and warnings: {raw_log}") else: print(f"Received a serious Apache error log: {raw_log}") slack_host = os.getenv('SLACK_WEBHOOK_HOST') slack_path = os.getenv('SLACK_WEBHOOK_PATH') print(f"Sending Slack post to: host={slack_host}, path={slack_path}, url={url}, content={raw_log}") cnx = HTTPSConnection(slack_host, timeout=5) cnx.request("POST", slack_path, json.dumps({'text': raw_log})) # It's important to read the response; if the cnx is closed too quickly, Slack might not post the msg resp = cnx.getresponse() resp_content = resp.read() resp_code = resp.status assert resp_code == 200
Diese zweite Lambda-Funktion verarbeitet Apache-Fehlerprotokolle und sendet nur dann eine Nachricht an Slack, wenn ein schwerwiegender Fehler auftritt. In diesem Fall werden PHP-Hinweise und -Warnmeldungen nicht als schwerwiegend genug angesehen, um eine Warnung auszulösen.
Auch hier erwartet diese Funktion, dass das Apache-Fehlerprotokoll JSON-formatiert ist. Hier ist also die Formatzeichenfolge für das Fehlerprotokoll, die ich verwendet habe:
ErrorLogFormat "{\"vhost\": \"%v\", \"timestamp\": \"%{cu}t\", \"module\": \"%-m\", \"level\": \"%l\", \"pid\": \"%-P\", \"tid\": \"%-T\", \"oserror\": \"%-E\", \"client\": \"%-a\", \"msg\": \"%M\"}"
ApacheErrorLogFunctionPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref ProcessApacheErrorLogFunction Action: lambda:InvokeFunction Principal: logs.amazonaws.com SourceArn: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* SourceAccount: !Ref AWS::AccountId
Diese Ressource erteilt CloudWatch Logs Berechtigungen zum Aufrufen Ihrer Lambda-Funktion.
ApacheErrorLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheErrorLogFunctionPermission Properties: LogGroupName: !Ref ApacheErrorLogGroup DestinationArn: !GetAtt ProcessApacheErrorLogFunction.Arn FilterPattern: '{$.msg != "PHP Warning*" && $.msg != "PHP Notice*"}'
Schließlich verknüpfen wir CloudWatch Logs mit der Lambda-Funktion unter Verwendung eines Abonnementfilters für die Apache-Fehlerprotokollgruppe. Beachten Sie das Filtermuster, das sicherstellt, dass Protokolle mit einer Meldung, die entweder mit „PHP Warning“ oder „PHP Notice“ beginnt, keinen Aufruf der Lambda-Funktion auslösen.
Abschließende Gedanken, Preise und Verfügbarkeit
Ein letztes Wort zu den Kosten: Diese Lösung ist viel günstiger als der Betrieb eines ELK-Clusters. Die in CloudWatch gespeicherten Protokolle liegen preislich auf dem gleichen Niveau wie S3, und Lambda erlaubt im Rahmen seines kostenlosen Kontingents eine Million Aufrufe pro Monat. Dies würde wahrscheinlich für eine Website mit mäßigem bis starkem Datenverkehr ausreichen (vorausgesetzt, Sie haben CloudWatch Logs-Filter verwendet), insbesondere wenn Sie sie gut codiert haben und nicht zu viele Fehler aufweisen!
Bitte beachten Sie auch, dass Lambda-Funktionen bis zu 1.000 gleichzeitige Aufrufe unterstützen. Zum Zeitpunkt des Verfassens dieses Artikels ist dies eine feste Grenze in AWS, die nicht geändert werden kann. Sie können jedoch davon ausgehen, dass der Aufruf der oben genannten Funktionen etwa 30-40 ms dauert. Dies sollte schnell genug sein, um ziemlich starken Verkehr zu bewältigen. Wenn Ihre Arbeitsbelastung so hoch ist, dass Sie diese Grenze erreichen, benötigen Sie wahrscheinlich eine komplexere Lösung auf Basis von Kinesis, die ich möglicherweise in einem zukünftigen Artikel behandeln werde.
Weiterführende Literatur im Toptal Engineering Blog:
- SSH-Protokollierung und Sitzungsverwaltung mit AWS SSM