ELK a AWS: administración de registros con menos complicaciones
Publicado: 2022-03-11Elasticsearch es una poderosa solución de software diseñada para buscar rápidamente información en una amplia gama de datos. Combinado con Logstash y Kibana, esto forma la "pila ELK" informalmente llamada, y a menudo se usa para recopilar, almacenar temporalmente, analizar y visualizar datos de registro. Por lo general, se necesitan algunas otras piezas de software, como Filebeat para enviar los registros del servidor a Logstash y Elastalert para generar alertas basadas en el resultado de algún análisis realizado en los datos almacenados en Elasticsearch.
El ELK Stack es poderoso, pero...
Mi experiencia con el uso de ELK para administrar registros es bastante variada. Por un lado, es muy potente y el rango de sus capacidades es bastante impresionante. Por otro lado, es complicado de configurar y puede ser un dolor de cabeza de mantener.
El caso es que Elasticsearch es muy bueno en general y puede usarse en una gran variedad de escenarios; ¡incluso se puede utilizar como motor de búsqueda! Dado que no está especializado para administrar datos de registro, esto requiere más trabajo de configuración para personalizar su comportamiento para las necesidades específicas de administrar dichos datos.
Configurar el clúster ELK fue bastante complicado y requirió que jugara con una serie de parámetros para finalmente ponerlo en funcionamiento. Luego vino el trabajo de configurarlo. En mi caso, tenía que configurar cinco piezas diferentes de software (Filebeat, Logstash, Elasticsearch, Kibana y Elastalert). Este puede ser un trabajo bastante tedioso, ya que tuve que leer la documentación y depurar un elemento de la cadena que no se comunica con el siguiente. Incluso después de que finalmente ponga en funcionamiento su clúster, aún necesita realizar operaciones de mantenimiento de rutina en él: aplicar parches, actualizar los paquetes del sistema operativo, verificar el uso de la CPU, la RAM y el disco, realizar ajustes menores según sea necesario, etc.
Toda mi pila de ELK dejó de funcionar después de una actualización de Logstash. Tras un examen más detenido, resultó que, por alguna razón, los desarrolladores de ELK decidieron cambiar una palabra clave en su archivo de configuración y pluralizarla. Esa fue la gota que colmó el vaso y decidí buscar una mejor solución (al menos una mejor solución para mis necesidades particulares).
Quería almacenar registros generados por Apache y varias aplicaciones PHP y de nodos, y analizarlos para encontrar patrones que indicaran errores en el software. La solución que encontré fue la siguiente:
- Instale CloudWatch Agent en el destino.
- Configure el agente de CloudWatch para enviar los registros a los registros de CloudWatch.
- Activar la invocación de funciones de Lambda para procesar los registros.
- La función Lambda publicaría mensajes en un canal de Slack si se encuentra un patrón.
- Siempre que sea posible, aplique un filtro a los grupos de registros de CloudWatch para evitar llamar a la función Lambda para cada uno de los registros (lo que podría aumentar los costos muy rápidamente).
Y, a un alto nivel, ¡eso es todo! Una solución 100% serverless que funcionará bien sin necesidad de mantenimiento y que escalará bien sin ningún esfuerzo adicional. Las ventajas de estas soluciones sin servidor sobre un clúster de servidores son numerosas:
- En esencia, todas las operaciones de mantenimiento de rutina que realizaría periódicamente en sus servidores de clúster ahora son responsabilidad del proveedor de la nube. Cualquier servidor subyacente será reparado, actualizado y mantenido sin que usted lo sepa.
- Ya no necesita monitorear su clúster y delega todos los problemas de escalamiento al proveedor de la nube. De hecho, una configuración sin servidor como la descrita anteriormente escalará automáticamente sin que usted tenga que hacer nada.
- La solución descrita anteriormente requiere menos configuración, y es muy poco probable que el proveedor de la nube introduzca un cambio importante en los formatos de configuración.
- Finalmente, es bastante fácil escribir algunas plantillas de CloudFormation para poner todo eso como infraestructura como código. Hacer lo mismo para configurar un clúster ELK completo requeriría mucho trabajo.
Configuración de alertas de holgura
¡Así que ahora entremos en detalles! Exploremos cómo se vería una plantilla de CloudFormation para tal configuración, completa con webhooks de Slack para alertar a los ingenieros. Primero debemos configurar toda la configuración de Slack, así que profundicemos en eso.
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
Deberá configurar su espacio de trabajo de Slack para esto, consulte esta guía de WebHooks para Slack para obtener información adicional.
Una vez que creó su aplicación de Slack y configuró un enlace entrante, la URL del enlace se convertirá en un parámetro de su pila de 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
Aquí creamos dos grupos de registros: uno para los registros de acceso de Apache y el otro para los registros de errores de Apache .
No configuré ningún mecanismo de ciclo de vida para los datos de registro porque está fuera del alcance de este artículo. En la práctica, probablemente le gustaría tener una ventana de retención más corta y diseñar políticas de ciclo de vida de S3 para moverlos a Glacier después de un cierto período de tiempo.
Función Lambda para procesar registros de acceso
Ahora implementemos la función Lambda que procesará los registros de acceso de 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
Aquí creamos un rol de IAM que se adjuntará a las funciones de Lambda para permitirles realizar sus funciones. En efecto, AWSLambdaBasicExecutionRole
es (a pesar de su nombre) una política de IAM proporcionada por AWS. Solo permite que la función Lambda cree su grupo de registro y registre transmisiones dentro de ese grupo, y luego envíe sus propios registros a CloudWatch Logs.
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
Así que aquí estamos definiendo una función Lambda para procesar los registros de acceso de Apache. Tenga en cuenta que no estoy usando el formato de registro común que es el predeterminado en Apache. Configuré el formato de registro de acceso así (y notará que esencialmente genera registros formateados como JSON, lo que facilita mucho el procesamiento más adelante):
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
Esta función Lambda está escrita en Python 3. Toma la línea de registro enviada desde CloudWatch y puede buscar patrones. En el ejemplo anterior, solo detecta las solicitudes HTTP que generaron un código de estado 5XX y publica un mensaje en un canal de Slack.

Puede hacer lo que quiera en términos de detección de patrones, y el hecho de que sea un verdadero lenguaje de programación (Python), a diferencia de los patrones de expresiones regulares en un archivo de configuración de Logstash o Elastalert, le brinda muchas oportunidades para implementar el reconocimiento de patrones complejos. .
Control de revisión
Una palabra rápida sobre el control de revisión: descubrí que tener el código en línea en las plantillas de CloudFormation para funciones de Lambda de pequeña utilidad como esta es bastante aceptable y conveniente. Por supuesto, para un proyecto grande que involucre muchas funciones y capas de Lambda, esto probablemente sería un inconveniente y necesitaría usar 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:*
Lo anterior otorga permiso a CloudWatch Logs para llamar a su función Lambda. Una advertencia: descubrí que usar la propiedad SourceAccount
puede generar conflictos con SourceArn
.
En términos generales, sugeriría no incluirlo cuando el servicio que llama a la función Lambda está en la misma cuenta de AWS. SourceArn
prohibirá que otras cuentas llamen a la función Lambda de todos modos.
ApacheAccessLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheAccessLogFunctionPermission Properties: LogGroupName: !Ref ApacheAccessLogGroup DestinationArn: !GetAtt ProcessApacheAccessLogFunction.Arn FilterPattern: "{$.status = 5*}"
El recurso de filtro de suscripción es el vínculo entre CloudWatch Logs y Lambda. Aquí, los registros enviados a ApacheAccessLogGroup
se reenviarán a la función Lambda que definimos anteriormente, pero solo aquellos registros que pasen el patrón de filtro. Aquí, el patrón de filtro espera algo de JSON como entrada (los patrones de filtro comienzan con '{' y terminan con '}') y coincidirán con la entrada de registro solo si tiene un status
de campo que comienza con "5".
Esto significa que llamamos a la función Lambda solo cuando el código de estado HTTP devuelto por Apache es un código 500, lo que generalmente significa que algo bastante malo está sucediendo. Esto asegura que no llamamos demasiado a la función Lambda y, por lo tanto, evitamos costos innecesarios.
Puede encontrar más información sobre los patrones de filtro en la documentación de Amazon CloudWatch. Los patrones de filtrado de CloudWatch son bastante buenos, aunque evidentemente no tan potentes como los de Grok.
Tenga en cuenta el campo DependsOn
, que garantiza que CloudWatch Logs pueda llamar a la función Lambda antes de que se cree la suscripción. Esto es solo una guinda del pastel, lo más probable es que sea innecesario, ya que en un escenario real, Apache probablemente no recibiría solicitudes antes de al menos unos segundos (por ejemplo, para vincular la instancia EC2 con un balanceador de carga y obtener la carga). balancer para reconocer el estado de la instancia EC2 como saludable).
Función Lambda para procesar registros de errores
Ahora echemos un vistazo a la función Lambda que procesará los registros de errores de 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
Esta segunda función de Lambda procesa los registros de errores de Apache y publicará un mensaje en Slack solo cuando se encuentre un error grave. En este caso, los mensajes de aviso y advertencia de PHP no se consideran lo suficientemente graves como para activar una alerta.
Nuevamente, esta función espera que el registro de errores de Apache tenga formato JSON. Así que aquí está la cadena de formato de registro de errores que he estado usando:
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
Este recurso otorga permisos a CloudWatch Logs para llamar a su función de Lambda.
ApacheErrorLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheErrorLogFunctionPermission Properties: LogGroupName: !Ref ApacheErrorLogGroup DestinationArn: !GetAtt ProcessApacheErrorLogFunction.Arn FilterPattern: '{$.msg != "PHP Warning*" && $.msg != "PHP Notice*"}'
Finalmente, vinculamos CloudWatch Logs con la función Lambda mediante un filtro de suscripción para el grupo de registro de errores de Apache. Tenga en cuenta el patrón de filtro, que garantiza que los registros con un mensaje que comience con "Advertencia de PHP" o "Aviso de PHP" no activen una llamada a la función Lambda.
Pensamientos finales, precios y disponibilidad
Una última palabra sobre los costos: esta solución es mucho más económica que operar un clúster ELK. Los registros almacenados en CloudWatch tienen el mismo precio que S3, y Lambda permite un millón de llamadas por mes como parte de su nivel gratuito. Esto probablemente sería suficiente para un sitio web con tráfico de moderado a pesado (siempre que haya usado filtros de CloudWatch Logs), especialmente si lo codificó bien y no tiene demasiados errores.
Además, tenga en cuenta que las funciones de Lambda admiten hasta 1000 llamadas simultáneas. En el momento de escribir este artículo, este es un límite estricto en AWS que no se puede cambiar. Sin embargo, puede esperar que la llamada a las funciones anteriores dure entre 30 y 40 ms. Esto debería ser lo suficientemente rápido para manejar un tráfico bastante pesado. Si su carga de trabajo es tan intensa que alcanza este límite, probablemente necesite una solución más compleja basada en Kinesis, que podría cubrir en un artículo futuro.
Lecturas adicionales en el blog de ingeniería de Toptal:
- Registro de SSH y administración de sesiones mediante AWS SSM