ELKからAWSへ:手間をかけずにログを管理

公開: 2022-03-11

Elasticsearchは、広範囲のデータ内の情報をすばやく検索するように設計された強力なソフトウェアソリューションです。 LogstashおよびKibanaと組み合わせると、これは非公式に名前が付けられた「ELKスタック」を形成し、ログデータの収集、一時的な保存、分析、および視覚化によく使用されます。 通常、サーバーからLogstashにログを送信するFilebeatや、Elasticsearchに保存されているデータに対して実行された分析の結果に基づいてアラートを生成するElastalertなど、他のいくつかのソフトウェアが必要です。

ELKスタックは強力ですが…

ログの管理にELKを使用した私の経験はかなり複雑です。 一方では、それは非常に強力であり、その機能の範囲は非常に印象的です。 一方、設定するのは難しいし、維持するのは頭痛の種になる可能性があります。

実際、Elasticsearchは一般的に非常に優れており、さまざまなシナリオで使用できます。 検索エンジンとしても使えます! ログデータの管理に特化していないため、このようなデータを管理する特定のニーズに合わせて動作をカスタマイズするには、より多くの構成作業が必要になります。

ELKクラスターのセットアップは非常にトリッキーで、最終的に稼働させるためにいくつかのパラメーターを試してみる必要がありました。 次に、それを構成する作業が行われました。 私の場合、構成するソフトウェアは5つありました(Filebeat、Logstash、Elasticsearch、Kibana、Elastalert)。 ドキュメントを読み、次の要素と通信しないチェーンの1つの要素をデバッグする必要があったため、これは非常に面倒な作業になる可能性があります。 最終的にクラスターを起動して実行した後でも、パッチの適用、OSパッケージのアップグレード、CPU、RAM、ディスクの使用状況の確認、必要に応じた微調整など、クラスターの定期的なメンテナンス操作を実行する必要があります。

Logstashの更新後、ELKスタック全体が機能しなくなりました。 よく調べてみると、何らかの理由で、ELK開発者は設定ファイルのキーワードを変更して複数形にすることにしました。 それが最後の藁であり、より良い解決策を探すことにしました(少なくとも私の特定のニーズのためのより良い解決策)。

ApacheとさまざまなPHPおよびノー​​ドアプリによって生成されたログを保存し、それらを解析してソフトウェアのバグを示すパターンを見つけたいと思いました。 私が見つけた解決策は次のとおりです。

  • CloudWatchAgentをターゲットにインストールします。
  • ログをCloudWatchログに送信するようにCloudWatchAgentを設定します。
  • Lambda関数の呼び出しをトリガーして、ログを処理します。
  • パターンが見つかった場合、Lambda関数はSlackチャネルにメッセージを投稿します。
  • 可能な場合は、CloudWatchロググループにフィルターを適用して、すべてのログに対してLambda関数を呼び出さないようにします(これにより、コストが非常に急速に上昇する可能性があります)。

そして、大まかに言えば、それだけです! メンテナンスを必要とせずに正常に動作し、追加の作業なしで適切に拡張できる100%サーバーレスソリューション。 サーバーのクラスターに対するこのようなサーバーレスソリューションの利点は数多くあります。

  • 本質的に、クラスターサーバーで定期的に実行するすべての定期的なメンテナンス操作は、クラウドプロバイダーの責任になります。 基盤となるサーバーには、知らないうちにパッチが適用され、アップグレードされ、保守されます。
  • クラスタを監視する必要がなくなり、すべてのスケーリングの問題をクラウドプロバイダーに委任します。 実際、上記のようなサーバーレスセットアップは、何もしなくても自動的にスケーリングされます。
  • 上記のソリューションでは必要な構成が少なくて済み、クラウドプロバイダーによって構成形式に重大な変更が加えられる可能性はほとんどありません。
  • 最後に、CloudFormationテンプレートを作成して、それらすべてをInfrastructure-as-Codeとして配置するのは非常に簡単です。 ELKクラスター全体をセットアップするために同じことを行うと、多くの作業が必要になります。

Slackアラートの設定

それでは、詳細を見ていきましょう。 エンジニアに警告するためのSlackWebhookを備えた、このようなセットアップでCloudFormationテンプレートがどのように見えるかを見てみましょう。 最初にすべての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ワークスペースを設定する必要があります。追加情報については、このWebHooksforSlackガイドを確認してください。

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

ここでは、2つのロググループを作成しました。1つはApacheアクセスログ用で、もう1つはApacheエラーログ用です。

この記事の範囲外であるため、ログデータのライフサイクルメカニズムを構成しませんでした。 実際には、保持期間を短縮し、S3ライフサイクルポリシーを設計して、一定期間後にそれらをGlacierに移動することをお勧めします。

アクセスログを処理するLambda関数

次に、Apacheアクセスログを処理するLambda関数を実装しましょう。

 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

ここでは、Lambda関数にアタッチされるIAMロールを作成して、Lambda関数が職務を遂行できるようにします。 事実上、 AWSLambdaBasicExecutionRoleは(その名前にもかかわらず)AWSによって提供されるIAMポリシーです。 Lambda関数がロググループとそのグループ内のログストリームを作成し、独自のログをCloudWatchLogsに送信できるようにするだけです。

 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

したがって、ここでは、Apacheアクセスログを処理するためのLambda関数を定義しています。 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関数はPython3で記述されています。CloudWatchから送信されたログ行を取得し、パターンを検索できます。 上記の例では、5XXステータスコードを生成したHTTPリクエストを検出し、Slackチャネルにメッセージを投稿します。

パターン検出に関しては何でもできます。LogstashまたはElastalert構成ファイルの正規表現パターンではなく、真のプログラミング言語(Python)であるという事実により、複雑なパターン認識を実装する多くの機会が得られます。 。

リビジョン管理

リビジョン管理について簡単に説明します。このような小さなユーティリティLambda関数のCloudFormationテンプレートにコードをインライン化すると、非常に受け入れやすく便利であることがわかりました。 もちろん、多くの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:*

上記は、CloudWatchLogsに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*}"

サブスクリプションフィルターリソースは、CloudWatchLogsとLambdaの間のリンクです。 ここで、 ApacheAccessLogGroupに送信されたログは、上記で定義したLambda関数に転送されますが、フィルターパターンを通過したログのみが転送されます。 ここで、フィルターパターンは入力としてJSONを想定しており(フィルターパターンは「{」で始まり「}」で終わります)、「5」で始まるフィールドstatusがある場合にのみログエントリと一致します。

これは、Apacheによって返されるHTTPステータスコードが500コードである場合にのみ、Lambda関数を呼び出すことを意味します。これは通常、非常に悪いことが起こっていることを意味します。 これにより、Lambda関数を過度に呼び出さないようにし、不要なコストを回避できます。

フィルタパターンの詳細については、AmazonCloudWatchのドキュメントをご覧ください。 CloudWatchのフィルターパターンは非常に優れていますが、Grokほど強力ではありません。

DependsOnフィールドに注意してください。これにより、サブスクリプションが作成される前にCloudWatchLogsが実際にLambda関数を呼び出すことができます。 これは単なるサクランボです。実際のシナリオのように、おそらく不要です。Apacheは、少なくとも数秒前にリクエストを受信しない可能性があります(たとえば、EC2インスタンスをロードバランサーにリンクし、負荷を取得するEC2インスタンスのステータスを正常であると認識するバランサー)。

エラーログを処理するLambda関数

次に、Apacheエラーログを処理するLambda関数を見てみましょう。

 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

この2番目の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

このリソースは、CloudWatchLogsにLambda関数を呼び出すためのアクセス許可を付与します。

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

最後に、Apacheエラーロググループのサブスクリプションフィルターを使用して、CloudWatchLogsをLambda関数にリンクします。 「PHPWarning」または「PHPNotice」で始まるメッセージを含むログがLambda関数の呼び出しをトリガーしないようにするフィルターパターンに注意してください。

最終的な考え、価格設定、および可用性

コストについて最後に一言:このソリューションは、ELKクラスターを運用するよりもはるかに安価です。 CloudWatchに保存されているログの価格はS3と同じレベルであり、Lambdaでは無料利用枠の一部として月額100万回の通話が許可されています。 これは、トラフィックが中程度から大量のWebサイト(CloudWatch Logsフィルターを使用している場合)にはおそらく十分です。特に、適切にコーディングし、エラーが多すぎない場合はそうです。

また、Lambda関数は最大1,000の同時呼び出しをサポートすることに注意してください。 これを書いている時点では、これはAWSの厳しい制限であり、変更することはできません。 ただし、上記の関数の呼び出しは約30〜40ミリ秒続くと予想できます。 これは、かなり重いトラフィックを処理するのに十分な速度である必要があります。 ワークロードが非常に激しく、この制限に達する場合は、Kinesisに基づくより複雑なソリューションが必要になる可能性があります。これについては、今後の記事で取り上げます。


Toptal Engineeringブログでさらに読む:

  • AWSSSMを使用したSSHロギングとセッション管理