ELK ke AWS: Mengelola Log dengan Lebih Mudah

Diterbitkan: 2022-03-11

Elasticsearch adalah solusi perangkat lunak yang kuat yang dirancang untuk mencari informasi dengan cepat dalam berbagai data. Dikombinasikan dengan Logstash dan Kibana, ini membentuk "tumpukan ELK" secara informal, dan sering digunakan untuk mengumpulkan, menyimpan sementara, menganalisis, dan memvisualisasikan data log. Beberapa perangkat lunak lain biasanya diperlukan, seperti Filebeat untuk mengirim log dari server ke Logstash, dan Elastalert untuk menghasilkan peringatan berdasarkan hasil beberapa analisis yang dijalankan pada data yang disimpan di Elasticsearch.

Tumpukan ELK Itu Kuat, Tapi…

Pengalaman saya menggunakan ELK untuk mengelola log cukup beragam. Di satu sisi, ini sangat kuat dan jangkauan kemampuannya cukup mengesankan. Di sisi lain, itu sulit untuk diatur dan bisa menjadi sakit kepala untuk dipertahankan.

Faktanya adalah bahwa Elasticsearch secara umum sangat bagus dan dapat digunakan dalam berbagai skenario; bahkan dapat digunakan sebagai mesin pencari! Karena tidak dikhususkan untuk mengelola data log, ini memerlukan lebih banyak pekerjaan konfigurasi untuk menyesuaikan perilakunya untuk kebutuhan spesifik pengelolaan data tersebut.

Menyiapkan klaster ELK cukup rumit dan mengharuskan saya untuk bermain-main dengan sejumlah parameter untuk akhirnya bisa menjalankannya. Kemudian datang pekerjaan mengonfigurasinya. Dalam kasus saya, saya memiliki lima perangkat lunak yang berbeda untuk dikonfigurasi (Filebeat, Logstash, Elasticsearch, Kibana, dan Elastalert). Ini bisa menjadi pekerjaan yang cukup membosankan, karena saya harus membaca dokumentasi dan men-debug satu elemen rantai yang tidak berbicara dengan yang berikutnya. Bahkan setelah Anda akhirnya mendapatkan dan menjalankan cluster Anda, Anda masih perlu melakukan operasi pemeliharaan rutin di atasnya: menambal, memutakhirkan paket OS, memeriksa penggunaan CPU, RAM, dan disk, membuat penyesuaian kecil yang diperlukan, dll.

Seluruh tumpukan ELK saya berhenti berfungsi setelah pembaruan Logstash. Setelah pemeriksaan lebih dekat, ternyata, untuk beberapa alasan, pengembang ELK memutuskan untuk mengubah kata kunci dalam file konfigurasi mereka dan menjadikannya jamak. Itu adalah tantangan terakhir dan memutuskan untuk mencari solusi yang lebih baik (setidaknya solusi yang lebih baik untuk kebutuhan khusus saya).

Saya ingin menyimpan log yang dihasilkan oleh Apache dan berbagai aplikasi PHP dan node, dan menguraikannya untuk menemukan pola yang menunjukkan bug dalam perangkat lunak. Solusi yang saya temukan adalah sebagai berikut:

  • Instal CloudWatch Agent pada target.
  • Konfigurasikan Agen CloudWatch untuk mengirimkan log ke log CloudWatch.
  • Pemicu pemanggilan fungsi Lambda untuk memproses log.
  • Fungsi Lambda akan memposting pesan ke saluran Slack jika pola ditemukan.
  • Jika memungkinkan, terapkan filter ke grup log CloudWatch untuk menghindari pemanggilan fungsi Lambda untuk setiap log tunggal (yang dapat meningkatkan biaya dengan sangat cepat).

Dan, pada level tinggi, hanya itu! Solusi tanpa server 100% yang akan berfungsi dengan baik tanpa perlu pemeliharaan dan akan diskalakan dengan baik tanpa upaya tambahan apa pun. Keuntungan dari solusi tanpa server seperti itu dibandingkan sekelompok server sangat banyak:

  • Intinya, semua operasi pemeliharaan rutin yang akan Anda lakukan secara berkala di server cluster Anda sekarang menjadi tanggung jawab penyedia cloud. Server apa pun yang mendasarinya akan ditambal, ditingkatkan, dan dipelihara untuk Anda tanpa Anda sadari.
  • Anda tidak perlu lagi memantau klaster dan mendelegasikan semua masalah penskalaan ke penyedia cloud. Memang, pengaturan tanpa server seperti yang dijelaskan di atas akan diskalakan secara otomatis tanpa Anda harus melakukan apa pun!
  • Solusi yang dijelaskan di atas memerlukan lebih sedikit konfigurasi, dan sangat kecil kemungkinannya bahwa perubahan yang melanggar akan dibawa ke dalam format konfigurasi oleh penyedia cloud.
  • Akhirnya, cukup mudah untuk menulis beberapa template CloudFormation untuk menempatkan semua itu sebagai infrastruktur-sebagai-kode. Melakukan hal yang sama untuk menyiapkan seluruh klaster ELK akan membutuhkan banyak pekerjaan.

Mengonfigurasi Peringatan Slack

Jadi sekarang mari kita masuk ke detailnya! Mari kita jelajahi seperti apa template CloudFormation untuk pengaturan seperti itu, lengkap dengan webhook Slack untuk memberi tahu para insinyur. Kita perlu mengonfigurasi semua pengaturan Slack terlebih dahulu, jadi mari selami.

 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

Anda perlu mengatur ruang kerja Slack Anda untuk ini, lihat panduan WebHooks untuk Slack ini untuk info tambahan.

Setelah Anda membuat aplikasi Slack dan mengonfigurasi hook yang masuk, URL hook akan menjadi parameter tumpukan CloudFormation Anda.

 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

Di sini kami membuat dua grup log: satu untuk log akses Apache , yang lain untuk log kesalahan Apache .

Saya tidak mengonfigurasi mekanisme siklus hidup apa pun untuk data log karena di luar cakupan artikel ini. Dalam praktiknya, Anda mungkin ingin mempersingkat jendela retensi dan merancang kebijakan siklus hidup S3 untuk memindahkannya ke Glacier setelah jangka waktu tertentu.

Fungsi Lambda untuk Memproses Log Akses

Sekarang mari kita implementasikan fungsi Lambda yang akan memproses log akses 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

Di sini kami membuat peran IAM yang akan dilampirkan ke fungsi Lambda, untuk memungkinkan mereka melakukan tugasnya. Akibatnya, AWSLambdaBasicExecutionRole adalah (terlepas dari namanya) kebijakan IAM yang disediakan oleh AWS. Itu hanya memungkinkan fungsi Lambda untuk membuat grup log dan aliran log di dalam grup itu, dan kemudian mengirim lognya sendiri ke 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

Jadi di sini kita mendefinisikan fungsi Lambda untuk memproses log akses Apache. Harap dicatat bahwa saya tidak menggunakan format log umum yang merupakan default di Apache. Saya mengonfigurasi format log akses seperti itu (dan Anda akan melihat bahwa itu pada dasarnya menghasilkan log yang diformat sebagai JSON, yang membuat pemrosesan lebih lanjut jauh lebih mudah):

 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

Fungsi Lambda ini ditulis dengan Python 3. Dibutuhkan baris log yang dikirim dari CloudWatch dan dapat mencari pola. Dalam contoh di atas, itu hanya mendeteksi permintaan HTTP yang menghasilkan kode status 5XX dan memposting pesan ke saluran Slack.

Anda dapat melakukan apa pun yang Anda suka dalam hal deteksi pola, dan fakta bahwa itu adalah bahasa pemrograman yang sebenarnya (Python), bukan hanya pola regex dalam file konfigurasi Logstash atau Elastalert, memberi Anda banyak peluang untuk menerapkan pengenalan pola yang kompleks .

Kontrol Revisi

Sebuah kata singkat tentang kontrol revisi: Saya menemukan bahwa memiliki kode inline dalam template CloudFormation untuk utilitas kecil fungsi Lambda seperti ini cukup dapat diterima dan nyaman. Tentu saja, untuk proyek besar yang melibatkan banyak fungsi dan lapisan Lambda, ini kemungkinan besar akan merepotkan dan Anda perlu menggunakan 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:*

Di atas memberikan izin ke CloudWatch Logs untuk memanggil fungsi Lambda Anda. Satu kata peringatan: Saya menemukan bahwa menggunakan properti SourceAccount dapat menyebabkan konflik dengan SourceArn .

Secara umum, saya menyarankan untuk tidak memasukkannya ketika layanan yang memanggil fungsi Lambda berada di akun AWS yang sama. SourceArn akan melarang akun lain untuk memanggil fungsi Lambda.

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

Sumber daya filter langganan adalah tautan antara CloudWatch Logs dan Lambda. Di sini, log yang dikirim ke ApacheAccessLogGroup akan diteruskan ke fungsi Lambda yang kami definisikan di atas, tetapi hanya log yang melewati pola filter. Di sini, pola filter mengharapkan beberapa JSON sebagai input (pola filter dimulai dengan '{' dan diakhiri dengan '}'), dan akan cocok dengan entri log hanya jika memiliki status bidang yang dimulai dengan “5”.

Ini berarti bahwa kita memanggil fungsi Lambda hanya ketika kode status HTTP yang dikembalikan oleh Apache adalah kode 500, yang biasanya berarti sesuatu yang sangat buruk sedang terjadi. Ini memastikan bahwa kami tidak memanggil fungsi Lambda terlalu banyak dan dengan demikian menghindari biaya yang tidak perlu.

Informasi lebih lanjut tentang pola filter dapat ditemukan di dokumentasi Amazon CloudWatch. Pola filter CloudWatch cukup bagus, meskipun jelas tidak sekuat Grok.

Perhatikan bidang DependsOn , yang memastikan CloudWatch Logs benar-benar dapat memanggil fungsi Lambda sebelum langganan dibuat. Ini hanya ceri pada kue, kemungkinan besar tidak perlu karena dalam skenario kasus nyata, Apache mungkin tidak akan menerima permintaan sebelum setidaknya beberapa detik (misalnya: untuk menautkan instance EC2 dengan penyeimbang beban, dan mendapatkan beban penyeimbang untuk mengenali status instans EC2 sebagai sehat).

Fungsi Lambda untuk Memproses Log Kesalahan

Sekarang mari kita lihat fungsi Lambda yang akan memproses log kesalahan 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

Fungsi Lambda kedua ini memproses log kesalahan Apache dan akan memposting pesan ke Slack hanya jika ditemukan kesalahan serius. Dalam hal ini, pemberitahuan PHP dan pesan peringatan tidak dianggap cukup serius untuk memicu peringatan.

Sekali lagi, fungsi ini mengharapkan log kesalahan Apache diformat JSON. Jadi, inilah string format log kesalahan yang saya gunakan:

 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

Sumber daya ini memberikan izin ke CloudWatch Logs untuk memanggil fungsi Lambda Anda.

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

Terakhir, kami menautkan CloudWatch Logs dengan fungsi Lambda menggunakan filter langganan untuk grup log kesalahan Apache. Perhatikan pola filter, yang memastikan bahwa log dengan pesan yang dimulai dengan "Peringatan PHP" atau "Pemberitahuan PHP" tidak memicu panggilan ke fungsi Lambda.

Pemikiran Akhir, Harga, dan Ketersediaan

Satu kata terakhir tentang biaya: solusi ini jauh lebih murah daripada mengoperasikan klaster ELK. Log yang disimpan di CloudWatch dihargai dengan harga yang sama dengan S3, dan Lambda memungkinkan satu juta panggilan per bulan sebagai bagian dari tingkat gratisnya. Ini mungkin cukup untuk situs web dengan lalu lintas sedang hingga padat (asalkan Anda menggunakan filter CloudWatch Logs), terutama jika Anda mengkodekannya dengan baik dan tidak memiliki terlalu banyak kesalahan!

Juga, harap perhatikan bahwa fungsi Lambda mendukung hingga 1.000 panggilan bersamaan. Pada saat penulisan, ini adalah batasan keras di AWS yang tidak dapat diubah. Namun, Anda dapat mengharapkan panggilan untuk fungsi di atas berlangsung selama sekitar 30-40 ms. Ini harus cukup cepat untuk menangani lalu lintas yang agak padat. Jika beban kerja Anda begitu kuat sehingga Anda mencapai batas ini, Anda mungkin memerlukan solusi yang lebih kompleks berdasarkan Kinesis, yang mungkin akan saya bahas di artikel mendatang.


Bacaan Lebih Lanjut di Blog Teknik Toptal:

  • Logging SSH dan Manajemen Sesi Menggunakan AWS SSM