ELK إلى AWS: إدارة السجلات بأقل قدر من المتاعب

نشرت: 2022-03-11

Elasticsearch هو حل برمجي قوي مصمم للبحث عن المعلومات بسرعة في نطاق واسع من البيانات. بالاقتران مع Logstash و Kibana ، يشكل هذا الاسم غير الرسمي "ELK stack" ، وغالبًا ما يستخدم لجمع بيانات السجل وتخزينها مؤقتًا وتحليلها وتصورها. عادةً ما تكون هناك حاجة إلى بعض البرامج الأخرى ، مثل Filebeat لإرسال السجلات من الخادم إلى Logstash و Elastalert لإنشاء تنبيهات بناءً على نتيجة بعض التحليلات التي تم إجراؤها على البيانات المخزنة في Elasticsearch.

ELK Stack قوي ، لكن ...

تجربتي مع ELK لإدارة السجلات مختلطة تمامًا. من ناحية أخرى ، إنها قوية جدًا ونطاق قدراتها مثير للإعجاب. من ناحية أخرى ، من الصعب إعدادها ويمكن أن تكون صداعًا للمحافظة عليه.

الحقيقة هي أن Elasticsearch جيدة جدًا بشكل عام ويمكن استخدامها في مجموعة متنوعة من السيناريوهات ؛ يمكن استخدامه حتى كمحرك بحث! نظرًا لأنه غير متخصص في إدارة بيانات السجل ، فإن هذا يتطلب المزيد من أعمال التكوين لتخصيص سلوكه للاحتياجات المحددة لإدارة مثل هذه البيانات.

كان إنشاء مجموعة ELK أمرًا صعبًا للغاية وتطلب مني اللعب مع عدد من المعلمات من أجل الحصول عليها وتشغيلها في النهاية. ثم جاء عمل تكوينه. في حالتي ، كان لدي خمسة برامج مختلفة لتكوينها (Filebeat و Logstash و Elasticsearch و Kibana و Elastalert). قد تكون هذه مهمة شاقة للغاية ، حيث كان عليّ قراءة الوثائق وتصحيح عنصر واحد من السلسلة لا يتحدث إلى العنصر التالي. حتى بعد حصولك على نظام المجموعة الخاص بك وتشغيله أخيرًا ، ما زلت بحاجة إلى إجراء عمليات صيانة روتينية عليه: التصحيح ، وترقية حزم نظام التشغيل ، والتحقق من وحدة المعالجة المركزية ، وذاكرة الوصول العشوائي ، واستخدام القرص ، وإجراء تعديلات طفيفة كما هو مطلوب ، وما إلى ذلك.

توقفت مكدس ELK بالكامل عن العمل بعد تحديث Logstash. عند الفحص الدقيق ، اتضح أنه ، لسبب ما ، قرر مطورو ELK تغيير كلمة رئيسية في ملف التكوين الخاص بهم وجمعها. كانت هذه هي القشة الأخيرة وقررت البحث عن حل أفضل (على الأقل حل أفضل لاحتياجاتي الخاصة).

أردت تخزين السجلات التي تم إنشاؤها بواسطة Apache والعديد من تطبيقات PHP والعقدة ، وتحليلها للعثور على أنماط تشير إلى الأخطاء في البرنامج. كان الحل الذي وجدته كما يلي:

  • قم بتثبيت CloudWatch Agent على الهدف.
  • قم بتكوين وكيل CloudWatch لشحن السجلات إلى سجلات CloudWatch.
  • بدء استدعاء وظائف Lambda لمعالجة السجلات.
  • ستنشر وظيفة Lambda الرسائل إلى قناة Slack إذا تم العثور على نمط.
  • حيثما أمكن ، قم بتطبيق مرشح على مجموعات سجل CloudWatch لتجنب استدعاء وظيفة Lambda لكل سجل واحد (مما قد يؤدي إلى زيادة التكاليف بسرعة كبيرة).

وعلى مستوى عال ، هذا كل شيء! حل 100٪ بدون خادم وسيعمل بشكل جيد دون الحاجة إلى صيانة ويمكن توسيع نطاقه بشكل جيد دون أي جهد إضافي. مزايا مثل هذه الحلول بدون خادم على مجموعة من الخوادم عديدة:

  • من حيث الجوهر ، فإن جميع عمليات الصيانة الروتينية التي تقوم بها بشكل دوري على خوادم المجموعة الخاصة بك هي الآن مسؤولية موفر السحابة. سيتم تصحيح أي خادم أساسي وترقيته وصيانته لك دون علمك بذلك.
  • لا تحتاج إلى مراقبة مجموعتك بعد الآن وأنت تفوض جميع مشكلات القياس إلى موفر السحابة. في الواقع ، سيتم توسيع نطاق إعداد بدون خادم مثل الذي تم وصفه أعلاه تلقائيًا دون الحاجة إلى القيام بأي شيء!
  • يتطلب الحل الموصوف أعلاه تكوينًا أقل ، ومن غير المرجح أن يتم إدخال تغيير فاصل في تنسيقات التكوين بواسطة موفر السحابة.
  • أخيرًا ، من السهل جدًا كتابة بعض قوالب CloudFormation لوضع كل ذلك كبنية تحتية كرمز. سيتطلب القيام بالشيء نفسه لإنشاء مجموعة ELK كاملة الكثير من العمل.

تكوين تنبيهات Slack

الآن دعنا ندخل في التفاصيل! دعنا نستكشف كيف سيبدو نموذج CloudFormation لمثل هذا الإعداد ، مع استكماله مع Slack webhooks لتنبيه المهندسين. نحتاج إلى تهيئة كل إعدادات 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 for 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 لمعالجة سجلات الوصول

الآن دعنا ننفذ وظيفة 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) ، على عكس أنماط regex فقط في ملف تكوين 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 ، وهو ما يعني عادةً حدوث شيء سيء للغاية. هذا يضمن أننا لا نسمي وظيفة Lambda كثيرًا وبالتالي نتجنب التكاليف غير الضرورية.

يمكن العثور على مزيد من المعلومات حول أنماط المرشح في وثائق Amazon CloudWatch. تعد أنماط مرشح CloudWatch جيدة جدًا ، على الرغم من أنه من الواضح أنها ليست بنفس قوة Grok.

لاحظ حقل DependsOn ، والذي يضمن أن CloudWatch Logs يمكنها بالفعل استدعاء وظيفة Lambda قبل إنشاء الاشتراك. هذا مجرد الكرز على الكعكة ، وغالبًا ما يكون غير ضروري كما هو الحال في سيناريو الحالة الواقعية ، ربما لن يتلقى Apache الطلبات قبل بضع ثوانٍ على الأقل (على سبيل المثال: لربط مثيل EC2 مع موازن التحميل ، والحصول على الحمل الموازن للتعرف على حالة مثيل EC2 على أنها سليمة).

وظيفة Lambda لمعالجة سجلات الأخطاء

الآن دعنا نلقي نظرة على وظيفة 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 لاستدعاء وظيفة 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 Warning" أو "PHP Notice" لا تؤدي إلى استدعاء وظيفة Lambda.

الأفكار النهائية ، والتسعير ، والتوافر

كلمة أخيرة عن التكاليف: هذا الحل أرخص بكثير من تشغيل مجموعة ELK. يتم تسعير السجلات المخزنة في CloudWatch بنفس مستوى S3 ، وتتيح Lambda مليون مكالمة شهريًا كجزء من مستواها المجاني. قد يكون هذا كافيًا لموقع ويب به حركة مرور معتدلة إلى كثيفة (بشرط أن تستخدم مرشحات CloudWatch Logs) ، خاصةً إذا قمت بترميزها جيدًا ولم يكن بها الكثير من الأخطاء!

يرجى أيضًا ملاحظة أن وظائف Lambda تدعم ما يصل إلى 1000 مكالمة متزامنة. في وقت كتابة هذا التقرير ، كان هذا حدًا صعبًا في AWS ولا يمكن تغييره. ومع ذلك ، يمكنك أن تتوقع أن تستمر المكالمة الخاصة بالوظائف المذكورة أعلاه لحوالي 30-40 مللي ثانية. يجب أن يكون هذا سريعًا بما يكفي للتعامل مع حركة المرور الكثيفة إلى حد ما. إذا كان عبء العمل لديك شديدًا لدرجة أنك وصلت إلى هذا الحد ، فربما تحتاج إلى حل أكثر تعقيدًا يعتمد على Kinesis ، والذي قد أغطيه في مقال مستقبلي.


مزيد من القراءة على مدونة Toptal Engineering:

  • تسجيل SSH وإدارة الجلسة باستخدام AWS SSM