ELK to AWS: управление журналами с меньшими трудностями

Опубликовано: 2022-03-11

Elasticsearch — это мощное программное решение, предназначенное для быстрого поиска информации в широком диапазоне данных. В сочетании с Logstash и Kibana это формирует неофициально называемый «стек ELK» и часто используется для сбора, временного хранения, анализа и визуализации данных журнала. Обычно требуется несколько других программ, таких как Filebeat для отправки журналов с сервера в Logstash и Elastalert для создания предупреждений на основе результатов некоторого анализа данных, хранящихся в Elasticsearch.

Стек ELK мощный, но…

Мой опыт использования ELK для управления журналами весьма неоднозначен. С одной стороны, он очень мощный и диапазон его возможностей впечатляет. С другой стороны, это сложно настроить и может быть головной болью в обслуживании.

Дело в том, что Elasticsearch в целом очень хорош и может использоваться в самых разных сценариях; его можно даже использовать в качестве поисковой системы! Поскольку он не предназначен для управления данными журналов, требуется дополнительная работа по настройке, чтобы настроить его поведение в соответствии с конкретными потребностями управления такими данными.

Настройка кластера ELK была довольно сложной и потребовала от меня поиграть с рядом параметров, чтобы наконец запустить его. Затем началась работа по его настройке. В моем случае мне нужно было настроить пять разных программ (Filebeat, Logstash, Elasticsearch, Kibana и Elastalert). Это может быть довольно утомительной работой, так как мне приходилось читать документацию и отлаживать один элемент цепочки, который не взаимодействует со следующим. Даже после того, как вы, наконец, настроите и запустите свой кластер, вам все равно придется выполнять на нем рутинные операции по обслуживанию: устанавливать исправления, обновлять пакеты ОС, проверять использование ЦП, ОЗУ и диска, вносить небольшие корректировки по мере необходимости и т. д.

Весь мой стек ELK перестал работать после обновления Logstash. При ближайшем рассмотрении оказалось, что разработчики ELK по какой-то причине решили изменить ключевое слово в своем файле конфигурации и поставить его во множественное число. Это было последней каплей, и я решил искать лучшее решение (по крайней мере, лучшее решение для моих конкретных нужд).

Я хотел хранить журналы, созданные Apache и различными приложениями PHP и Node, и анализировать их, чтобы найти шаблоны, указывающие на ошибки в программном обеспечении. Решение, которое я нашел, было следующим:

  • Установите агент CloudWatch на цель.
  • Настройте агент CloudWatch для отправки журналов в журналы CloudWatch.
  • Инициировать вызов функций Lambda для обработки журналов.
  • Функция Lambda будет отправлять сообщения в канал Slack, если будет найден шаблон.
  • По возможности применяйте фильтр к группам журналов CloudWatch, чтобы избежать вызова функции Lambda для каждого отдельного журнала (что может очень быстро увеличить затраты).

И все на высоком уровне! 100% бессерверное решение, которое будет нормально работать без необходимости обслуживания и будет хорошо масштабироваться без каких-либо дополнительных усилий. Преимущества таких бессерверных решений перед кластером серверов многочисленны:

  • По сути, все рутинные операции по обслуживанию, которые вы периодически выполняете на своих кластерных серверах, теперь являются обязанностью облачного провайдера. Любой базовый сервер будет исправлен, обновлен и поддержан для вас, даже если вы об этом не знаете.
  • Вам больше не нужно следить за своим кластером, и вы делегируете все вопросы масштабирования облачному провайдеру. Действительно, бессерверная установка, подобная описанной выше, будет масштабироваться автоматически, и вам не нужно будет ничего делать!
  • Описанное выше решение требует меньшей настройки, и крайне маловероятно, что облачный провайдер внесет критические изменения в форматы конфигурации.
  • Наконец, довольно легко написать несколько шаблонов CloudFormation, чтобы представить все это как инфраструктуру как код. Если сделать то же самое для настройки целого кластера ELK, потребуется много работы.

Настройка оповещений Slack

Итак, теперь давайте углубимся в детали! Давайте рассмотрим, как будет выглядеть шаблон CloudFormation для такой настройки, дополненный веб-перехватчиками Slack для оповещения инженеров. Сначала нам нужно настроить все настройки Slack, поэтому давайте углубимся в это.

 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

Для этого вам нужно будет настроить рабочее пространство Slack, ознакомьтесь с этим руководством по WebHooks для Slack для получения дополнительной информации.

После того, как вы создали приложение Slack и настроили входящий хук, URL-адрес хука станет параметром вашего стека CloudFormation.

 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

Здесь мы создали две группы журналов: одну для журналов доступа Apache , другую для журналов ошибок Apache .

Я не настраивал какой-либо механизм жизненного цикла для данных журнала, потому что это выходит за рамки этой статьи. На практике вы, вероятно, захотите сократить период хранения и разработать политики жизненного цикла S3, чтобы перенести их в Glacier по истечении определенного периода времени.

Лямбда-функция для обработки журналов доступа

Теперь давайте реализуем функцию Lambda, которая будет обрабатывать журналы доступа Apache.

 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

Здесь мы создали роль IAM, которая будет прикреплена к функциям Lambda, чтобы позволить им выполнять свои обязанности. По сути, AWSLambdaBasicExecutionRole — это (несмотря на название) IAM-политика, предоставляемая AWS. Это просто позволяет функции Lambda создать свою группу журналов и потоки журналов в этой группе, а затем отправлять свои собственные журналы в журналы CloudWatch.

 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

Итак, здесь мы определяем функцию Lambda для обработки журналов доступа Apache. Обратите внимание, что я не использую общий формат журнала, который используется по умолчанию в Apache. Я настроил формат журнала доступа следующим образом (и вы заметите, что он по существу генерирует журналы в формате JSON, что значительно упрощает дальнейшую обработку):

 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

Эта функция Lambda написана на Python 3. Она принимает строку журнала, отправленную из CloudWatch, и может искать закономерности. В приведенном выше примере он просто обнаруживает HTTP-запросы, которые привели к коду состояния 5XX, и отправляет сообщение в канал Slack.

Вы можете делать все, что угодно с точки зрения обнаружения шаблонов, и тот факт, что это настоящий язык программирования (Python), а не просто шаблоны регулярных выражений в конфигурационном файле Logstash или Elastalert, дает вам много возможностей для реализации сложного распознавания шаблонов. .

Контроль версий

Несколько слов о контроле версий: я обнаружил, что наличие кода, встроенного в шаблоны CloudFormation для небольших служебных функций Lambda, таких как эта, вполне приемлемо и удобно. Конечно, для большого проекта, включающего множество функций и слоев Lambda, это, скорее всего, будет неудобно, и вам придется использовать SAM.

 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:*

Приведенное выше дает разрешение CloudWatch Logs на вызов вашей функции Lambda. Одно предостережение: я обнаружил, что использование свойства SourceAccount может привести к конфликтам с SourceArn .

Вообще говоря, я бы посоветовал не включать его, если служба, вызывающая функцию Lambda, находится в той же учетной записи AWS. SourceArn в любом случае запретит другим учетным записям вызывать функцию Lambda.

 ApacheAccessLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheAccessLogFunctionPermission Properties: LogGroupName: !Ref ApacheAccessLogGroup DestinationArn: !GetAtt ProcessApacheAccessLogFunction.Arn FilterPattern: "{$.status = 5*}"

Ресурс фильтра подписки является связующим звеном между журналами CloudWatch и Lambda. Здесь журналы, отправленные в ApacheAccessLogGroup , будут перенаправлены в функцию Lambda, которую мы определили выше, но только те журналы, которые проходят шаблон фильтра. Здесь шаблон фильтра ожидает некоторый JSON в качестве входных данных (шаблоны фильтра начинаются с «{» и заканчиваются на «}») и будут соответствовать записи журнала, только если она имеет status поля, начинающийся с «5».

Это означает, что мы вызываем функцию Lambda только тогда, когда код состояния HTTP, возвращаемый Apache, представляет собой код 500, что обычно означает, что происходит что-то очень плохое. Это гарантирует, что мы не будем слишком часто вызывать лямбда-функцию и тем самым избежим ненужных затрат.

Дополнительную информацию о шаблонах фильтров можно найти в документации Amazon CloudWatch. Шаблоны фильтров CloudWatch довольно хороши, хотя явно не такие мощные, как Grok.

Обратите внимание на поле DependsOn , которое гарантирует, что CloudWatch Logs действительно может вызывать функцию Lambda до создания подписки. Это просто вишенка на торте, скорее всего, в этом нет необходимости, так как в реальном сценарии Apache, вероятно, не будет получать запросы, по крайней мере, через несколько секунд (например: связать экземпляр EC2 с балансировщиком нагрузки и получить нагрузку). балансировщик распознал состояние экземпляра EC2 как исправное).

Лямбда-функция для обработки журналов ошибок

Теперь давайте посмотрим на функцию Lambda, которая будет обрабатывать журналы ошибок Apache.

 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

Эта вторая функция Lambda обрабатывает журналы ошибок Apache и отправляет сообщение в Slack только при обнаружении серьезной ошибки. В этом случае уведомления и предупреждения PHP не считаются достаточно серьезными, чтобы вызвать предупреждение.

Опять же, эта функция ожидает, что журнал ошибок Apache будет в формате JSON. Итак, вот строка формата журнала ошибок, которую я использовал:

 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

Этот ресурс предоставляет журналам CloudWatch Logs разрешения на вызов вашей функции Lambda.

 ApacheErrorLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheErrorLogFunctionPermission Properties: LogGroupName: !Ref ApacheErrorLogGroup DestinationArn: !GetAtt ProcessApacheErrorLogFunction.Arn FilterPattern: '{$.msg != "PHP Warning*" && $.msg != "PHP Notice*"}'

Наконец, мы связываем журналы CloudWatch с функцией Lambda, используя фильтр подписки для группы журналов ошибок Apache. Обратите внимание на шаблон фильтра, который гарантирует, что журналы с сообщением, начинающимся с «Предупреждение PHP» или «Уведомление PHP», не вызовут вызов функции Lambda.

Заключительные мысли, цены и доступность

И последнее слово о затратах: это решение намного дешевле, чем работа с кластером ELK. Журналы, хранящиеся в CloudWatch, стоят на том же уровне, что и S3, а Lambda разрешает один миллион вызовов в месяц в рамках бесплатного уровня. Этого, вероятно, будет достаточно для веб-сайта с умеренным или интенсивным трафиком (при условии, что вы использовали фильтры журналов CloudWatch), особенно если вы хорошо закодировали его и не имеете слишком много ошибок!

Также обратите внимание, что функции Lambda поддерживают до 1000 одновременных вызовов. На момент написания статьи это было жестким ограничением в AWS, которое нельзя изменить. Однако вы можете ожидать, что вызов вышеуказанных функций будет длиться около 30-40 мс. Это должно быть достаточно быстро, чтобы справиться с довольно интенсивным трафиком. Если ваша рабочая нагрузка настолько интенсивна, что вы достигли этого предела, вам, вероятно, потребуется более сложное решение на основе Kinesis, о котором я могу рассказать в следующей статье.


Дальнейшее чтение в блоге Toptal Engineering:

  • Ведение журнала SSH и управление сеансами с помощью AWS SSM