ELK'dan AWS'ye: Günlükleri Daha Az Sorunla Yönetme
Yayınlanan: 2022-03-11Elasticsearch, geniş bir veri yelpazesinde hızla bilgi aramak için tasarlanmış güçlü bir yazılım çözümüdür. Logstash ve Kibana ile birlikte bu, gayri resmi olarak adlandırılan “ELK yığınını” oluşturur ve genellikle günlük verilerini toplamak, geçici olarak depolamak, analiz etmek ve görselleştirmek için kullanılır. Günlükleri sunucudan Logstash'e göndermek için Filebeat ve Elasticsearch'te depolanan veriler üzerinde yapılan bazı analizlerin sonucuna göre uyarılar oluşturmak için Elastalert gibi genellikle birkaç başka yazılım parçasına ihtiyaç duyulur.
ELK Yığını Güçlüdür, Ama…
Günlükleri yönetmek için ELK kullanma deneyimim oldukça karışık. Bir yandan, çok güçlüdür ve yeteneklerinin kapsamı oldukça etkileyicidir. Öte yandan, kurulumu zor ve bakımı baş ağrısı olabilir.
Gerçek şu ki, Elasticsearch genel olarak çok iyidir ve çok çeşitli senaryolarda kullanılabilir; hatta bir arama motoru olarak kullanılabilir! Günlük verilerini yönetmek için uzmanlaşmadığından, bu tür verilerin yönetilmesine ilişkin belirli ihtiyaçlar için davranışını özelleştirmek için daha fazla yapılandırma çalışması gerektirir.
ELK kümesini kurmak oldukça zordu ve sonunda onu çalıştırıp çalıştırabilmem için bir dizi parametreyle oynamamı gerektirdi. Sonra onu yapılandırma işi geldi. Benim durumumda, yapılandırmam gereken beş farklı yazılım parçası vardı (Filebeat, Logstash, Elasticsearch, Kibana ve Elastalert). Belgeleri baştan sona okumak ve zincirin bir sonrakiyle konuşmayan bir öğesinde hata ayıklamak zorunda kaldığım için bu oldukça sıkıcı bir iş olabilir. Sonunda kümenizi kurup çalıştırdıktan sonra bile, üzerinde rutin bakım işlemlerini gerçekleştirmeniz gerekir: yamalama, işletim sistemi paketlerini yükseltme, CPU, RAM ve disk kullanımını kontrol etme, gerektiğinde küçük ayarlamalar yapma vb.
Tüm ELK yığınım bir Logstash güncellemesinden sonra çalışmayı durdurdu. Daha yakından incelendiğinde, ELK geliştiricilerinin bir nedenden dolayı yapılandırma dosyalarındaki bir anahtar kelimeyi değiştirmeye ve çoğul hale getirmeye karar verdikleri ortaya çıktı. Bu bardağı taşıran son damlaydı ve daha iyi bir çözüm aramaya karar verdi (en azından benim özel ihtiyaçlarım için daha iyi bir çözüm).
Apache ve çeşitli PHP ve düğüm uygulamaları tarafından oluşturulan günlükleri depolamak ve yazılımdaki hataları gösteren kalıpları bulmak için bunları ayrıştırmak istedim. Bulduğum çözüm şuydu:
- CloudWatch Agent'ı hedefe yükleyin.
- Günlükleri CloudWatch günlüklerine göndermek için CloudWatch Agent'ı yapılandırın.
- Günlükleri işlemek için Lambda işlevlerinin çağrılmasını tetikleyin.
- Lambda işlevi, bir kalıp bulunursa, mesajları Slack kanalına gönderir.
- Mümkün olduğunda, her bir günlük için Lambda işlevini çağırmaktan kaçınmak için CloudWatch günlük gruplarına bir filtre uygulayın (bu, maliyetleri çok hızlı bir şekilde artırabilir).
Ve yüksek düzeyde, işte bu! Herhangi bir bakım gerektirmeden sorunsuz çalışacak ve herhangi bir ek çaba gerektirmeden iyi ölçeklendirilebilecek %100 sunucusuz bir çözüm. Bu tür sunucusuz çözümlerin bir sunucu kümesine göre avantajları çoktur:
- Özünde, küme sunucularınızda periyodik olarak gerçekleştireceğiniz tüm rutin bakım işlemleri artık bulut sağlayıcının sorumluluğundadır. Herhangi bir temel sunucu, siz farkında olmadan sizin için yamalanacak, yükseltilecek ve bakımı yapılacaktır.
- Artık kümenizi izlemenize gerek yok ve tüm ölçeklendirme sorunlarını bulut sağlayıcısına devrediyorsunuz. Aslında, yukarıda açıklanan gibi sunucusuz bir kurulum, sizin hiçbir şey yapmanıza gerek kalmadan otomatik olarak ölçeklenir!
- Yukarıda açıklanan çözüm daha az yapılandırma gerektirir ve bulut sağlayıcısı tarafından yapılandırma biçimlerine bir kırılma değişikliği getirilmesi pek olası değildir.
- Son olarak, hepsini kod olarak altyapı olarak koymak için bazı CloudFormation şablonları yazmak oldukça kolaydır. Bütün bir ELK kümesini kurmak için aynısını yapmak çok fazla çalışma gerektirir.
Slack Uyarılarını Yapılandırma
O halde şimdi ayrıntılara geçelim! Mühendisleri uyarmak için Slack web kancalarıyla tamamlanmış böyle bir kurulum için bir CloudFormation şablonunun nasıl görüneceğini keşfedelim. Önce tüm Slack kurulumunu yapılandırmamız gerekiyor, o yüzden başlayalım.
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
Bunun için Slack çalışma alanınızı ayarlamanız gerekir, ek bilgi için bu Slack için WebHooks kılavuzuna bakın.
Slack uygulamanızı oluşturup gelen kancayı yapılandırdıktan sonra, kanca URL'si CloudFormation yığınınızın bir parametresi olur.
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
Burada iki günlük grubu oluşturduk: biri Apache erişim günlükleri için, diğeri Apache hata günlükleri için.
Bu makalenin kapsamı dışında olduğu için günlük verileri için herhangi bir yaşam döngüsü mekanizması yapılandırmadım. Pratikte, muhtemelen kısaltılmış bir saklama penceresine sahip olmak ve belirli bir süre sonra bunları Glacier'a taşımak için S3 yaşam döngüsü ilkeleri tasarlamak istersiniz.
Erişim Günlüklerini İşleme için Lambda İşlevi
Şimdi Apache erişim günlüklerini işleyecek Lambda işlevini uygulayalım.
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
Burada, Lambda işlevlerine eklenecek ve görevlerini yerine getirmelerine olanak sağlayacak bir IAM rolü oluşturduk. Aslında, AWSLambdaBasicExecutionRole
(adına rağmen) AWS tarafından sağlanan bir IAM politikasıdır. Sadece Lambda işlevinin kendi günlük grubunu ve bu grup içindeki günlük akışlarını oluşturmasına ve ardından kendi günlüklerini CloudWatch Günlüklerine göndermesine izin verir.
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
Yani burada Apache erişim günlüklerini işlemek için bir Lambda işlevi tanımlıyoruz. Lütfen Apache'de varsayılan olan ortak günlük biçimini kullanmadığımı unutmayın. Erişim günlüğü biçimini şu şekilde yapılandırdım (ve bunun esas olarak JSON olarak biçimlendirilmiş günlükler oluşturduğunu fark edeceksiniz, bu da daha sonraki işlemleri çok daha kolay hale getiriyor):
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
Bu Lambda işlevi Python 3'te yazılmıştır. CloudWatch'tan gönderilen günlük satırını alır ve kalıpları arayabilir. Yukarıdaki örnekte, yalnızca 5XX durum koduyla sonuçlanan HTTP isteklerini algılar ve bir Slack kanalına bir mesaj gönderir.

Kalıp algılama açısından istediğiniz her şeyi yapabilirsiniz ve bunun bir Logstash veya Elastalert yapılandırma dosyasındaki normal ifade kalıplarının aksine gerçek bir programlama dili (Python) olması, size karmaşık kalıp tanımayı uygulamak için birçok fırsat sunar. .
Revizyon denetimi
Revizyon kontrolü hakkında kısa bir söz: Bunun gibi küçük yardımcı Lambda işlevleri için CloudFormation şablonlarında kodun satır içi olmasının oldukça kabul edilebilir ve kullanışlı olduğunu gördüm. Elbette, birçok Lambda işlevi ve katmanı içeren büyük bir proje için bu, büyük olasılıkla elverişsiz olacaktır ve SAM kullanmanız gerekecektir.
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:*
Yukarıdakiler, Lambda işlevinizi çağırmak için CloudWatch Günlüklerine izin verir. Bir uyarı: SourceAccount
özelliğini kullanmanın SourceArn
ile çakışmalara yol açabileceğini buldum.
Genel olarak konuşursak, Lambda işlevini çağıran hizmet aynı AWS hesabındayken dahil etmemenizi öneririm. SourceArn
, diğer hesapların Lambda işlevini yine de çağırmasını yasaklayacaktır.
ApacheAccessLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheAccessLogFunctionPermission Properties: LogGroupName: !Ref ApacheAccessLogGroup DestinationArn: !GetAtt ProcessApacheAccessLogFunction.Arn FilterPattern: "{$.status = 5*}"
Abonelik filtresi kaynağı, CloudWatch Günlükleri ile Lambda arasındaki bağlantıdır. Burada ApacheAccessLogGroup
gönderilen günlükler, yukarıda tanımladığımız Lambda işlevine iletilir, ancak yalnızca filtre modelini geçen günlükler. Burada, filtre kalıbı girdi olarak bir miktar JSON bekler (filtre kalıpları '{' ile başlar ve '}' ile biter) ve yalnızca "5" ile başlayan bir alan status
sahipse günlük girişiyle eşleşir.
Bu, Lambda işlevini yalnızca Apache tarafından döndürülen HTTP durum kodu 500 kodu olduğunda çağırdığımız anlamına gelir, bu da genellikle oldukça kötü bir şey olduğu anlamına gelir. Bu, Lambda işlevini çok fazla çağırmamamızı ve böylece gereksiz maliyetlerden kaçınmamızı sağlar.
Filtre kalıpları hakkında daha fazla bilgiyi Amazon CloudWatch belgelerinde bulabilirsiniz. CloudWatch filtre modelleri oldukça iyi, ancak açıkçası Grok kadar güçlü değil.
CloudWatch Logs'un abonelik oluşturulmadan önce Lambda işlevini gerçekten çağırabilmesini sağlayan DependsOn
alanına dikkat edin. Bu sadece pastanın üzerinde bir kiraz, gerçek bir senaryoda olduğu gibi büyük olasılıkla gereksizdir, Apache muhtemelen istekleri en az birkaç saniyeden önce almazdı (örneğin: EC2 örneğini bir yük dengeleyiciye bağlamak ve yükü almak için). EC2 örneğinin durumunu sağlıklı olarak tanımak için dengeleyici).
Hata Günlüklerini İşleyecek Lambda İşlevi
Şimdi Apache hata günlüklerini işleyecek Lambda işlevine bir göz atalım.
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
Bu ikinci Lambda işlevi, Apache hata günlüklerini işler ve yalnızca ciddi bir hatayla karşılaşıldığında Slack'e bir mesaj gönderir. Bu durumda, PHP uyarı ve uyarı mesajları, bir uyarıyı tetikleyecek kadar ciddi kabul edilmez.
Yine bu işlev, Apache hata günlüğünün JSON biçimli olmasını bekler. İşte kullandığım hata günlüğü biçimi dizesi:
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
Bu kaynak, Lambda işlevinizi çağırmak için CloudWatch Günlüklerine izin verir.
ApacheErrorLogSubscriptionFilter: Type: AWS::Logs::SubscriptionFilter DependsOn: ApacheErrorLogFunctionPermission Properties: LogGroupName: !Ref ApacheErrorLogGroup DestinationArn: !GetAtt ProcessApacheErrorLogFunction.Arn FilterPattern: '{$.msg != "PHP Warning*" && $.msg != "PHP Notice*"}'
Son olarak, Apache hata günlüğü grubu için bir abonelik filtresi kullanarak CloudWatch Günlüklerini Lambda işlevine bağlarız. “PHP Warning” veya “PHP Notice” ile başlayan bir mesajla günlüklerin Lambda işlevine bir çağrıyı tetiklememesini sağlayan filtre modeline dikkat edin.
Son Düşünceler, Fiyatlandırma ve Kullanılabilirlik
Maliyetler hakkında son bir söz: Bu çözüm, bir ELK kümesini çalıştırmaktan çok daha ucuzdur. CloudWatch'ta depolanan günlükler, S3 ile aynı düzeyde fiyatlandırılır ve Lambda, ücretsiz katmanının bir parçası olarak ayda bir milyon çağrıya izin verir. Bu, orta ila yoğun trafiği olan bir web sitesi için (CloudWatch Logs filtrelerini kullanmanız şartıyla), özellikle de iyi kodladıysanız ve çok fazla hata içermiyorsa, muhtemelen yeterli olacaktır!
Ayrıca, Lambda işlevlerinin 1.000'e kadar eşzamanlı aramayı desteklediğini lütfen unutmayın. Yazma sırasında, bu, AWS'de değiştirilemeyen katı bir sınırdır. Ancak, yukarıdaki işlevler için yapılan aramanın yaklaşık 30-40 ms sürmesini bekleyebilirsiniz. Bu, oldukça yoğun trafiği kaldıracak kadar hızlı olmalıdır. İş yükünüz bu sınıra ulaşacak kadar yoğunsa, muhtemelen Kinesis'e dayalı daha karmaşık bir çözüme ihtiyacınız vardır.
Toptal Mühendislik Blogunda Daha Fazla Okuma:
- AWS SSM Kullanarak SSH Günlüğü ve Oturum Yönetimi