サブスクリプションフィルターでFargateコンテナログのエラーを抽出してメールで通知する

目次

CloudWatch Logsサブスクリプションフィルターを使って、Fargateコンテナのログからエラーを抽出して、メールで通知する

CloudWatch Logsの機能の1つにサブスクリプションフィルターがあります。
サブスクリプションフィルターを使用することによって、CloudWatch Logsに配信されたログの中から、特定の文字列を含むログを抽出し、アクションを実行することができます。

今回はFargateタイプのECSコンテナのログをCloudWatch Logsに配信し、その中からエラーログを抽出して、その内容をメールで通知することを目指します。

構築する環境

Diagram of Subscription filter to extract errors in Fargate container logs and notify by email.

プライベートサブネットに、FargateタイプのECSを作成します。

CloudWatch Logsにログを配信するために、コンテナ用サブネットに、Logs用VPCエンドポイントを作成します。

CloudWatch Logsでサブスクリプションフィルターを有効化します。
「error」という文字列を検出するフィルターを作成します。
サブスクリプションフィルターに関連づけるリソースとして、Lambda関数を作成します。

Lambda関数の働きですが、SNSトピックにメッセージをパブリッシュすることです。
サブスクリプションフィルターで抽出したログの内容をパブリッシュします。

DockerHubからNginxの公式イメージを取得するために、パブリックサブネットにNATゲートウェイを配置します。

EC2インスタンスを作成します。
コンテナにアクセスするためのクライアントして使用します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/066

テンプレートファイルのポイント解説

ECS(Fargate)に関する基本的な事項は、以下のページをご確認ください。

あわせて読みたい
CloudFormationでFargate入門 【CloudFormationでFargateに入門するための構成】 AWS FargateはサーバーレスでDockerコンテナを実行することができるサービスです。今回はFargate入門ということで、C...

プライベートサブネットにFargateを配置する方法については、以下のページをご確認ください。

あわせて読みたい
プライベートサブネットにECS(Fargate)を作成する 【プライベートサブネットにECS(Fargate)を作成する】 以下のページでFargateタイプのECSコンテナを作成する方法をご紹介しました。 https://awstut.com/2022/01/25/int...

SNSトピック

Resources:
  Topic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
        - Endpoint: !Ref MailAddress
          Protocol: email
      TopicName: !Ref Prefix
Code language: YAML (yaml)

Subscriptionプロパティがポイントです。
サブスクライバーとしてメールアドレスを指定する場合は、Protocolプロパティに「email」を、Endpointプロパティにメールアドレスを指定します。

メールアドレスをSNSサブスクライバーに指定する方法については、以下のページをご確認ください。

あわせて読みたい
CFNでSNS入門 – email版 【CFNでSNS入門 - email版】 AWS SNSはメッセージングサービスです。 今回は入門編ということで、通知先にEmailを指定する方法をご紹介します。 【構築する環境】 2種類...

CloudWatch Logsサブスクリプションフィルター

サブスクリプションフィルターで検出した内容をSNSに送るためには、4種類のリソースを作成する必要があります。

  • ロググループ
  • サブスクリプションフィルター
  • パーミッション
  • Lambda関数

ロググループ

Resources:
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "${Prefix}-LogGroup"
Code language: YAML (yaml)

特別な設定は不要です。
シンプルにロググループを作成します。

サブスクリプションフィルター

Resources:
  SubscriptionFilter:
    Type: AWS::Logs::SubscriptionFilter
    Properties:
      DestinationArn: !GetAtt Function.Arn
      FilterPattern: error
      LogGroupName: !Ref LogGroup
Code language: YAML (yaml)

DestinationArnプロパティでログを検出した際に連携するAWSリソースを指定します。
今回は後述のLambda関数を指定します。

FIlterPatternプロパティで抽出するログを指定します。
今回は「error」を検出するように設定します。

LogGroupNameプロパティでサブスクリプションフィルタを有効化するロググループを指定します。
先述のロググループを指定します。

パーミッション

Resources:
  SubscriptionFilterPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref Function
      Principal: !Sub "logs.${AWS::Region}.amazonaws.com"
      SourceArn: !GetAtt LogGroup.Arn
Code language: YAML (yaml)

サブスクリプションフィルタが条件を満たすログを検出した際に、ロググループが後述のLambda関数を実行するための権限を付与します。

Lambda関数

Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          REGION: !Ref AWS::Region
          TOPIC: !Ref TopicArn
      Code:
        ZipFile: |
          import base64
          import boto3
          import gzip
          import json
          import os

          topic = os.environ['TOPIC']
          region = os.environ['REGION']

          client = boto3.client('sns', region_name=region)
          subject = 'Error Detection.'

          def lambda_handler(event, context):
            subscription_data = event['awslogs']['data']

            subscription_data_decoded = base64.b64decode(subscription_data)

            subscription_data_decompressed = gzip.decompress(subscription_data_decoded)

            subscription_data_loaded = json.loads(subscription_data_decompressed)

            response = client.publish(
              TopicArn=topic,
              Subject=subject,
              Message=subscription_data_loaded['logEvents'][0]['message']
              )
      FunctionName: !Sub "${Prefix}-function"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)

Environmentプロパティで関数に渡すことができる環境変数を定義できます。
先述のSNSトピックのARNや、トピックを作成したリージョン情報を渡します。

Lambda関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationでLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

コードは以下のページを参考に作成しました。

https://dev.classmethod.jp/articles/cwl-lambda-sns-publish/

ちなみに関数用のIAMロールは以下の通りです。

Resources:
  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: SNSPublishPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource:
                  - !Ref TopicArn
Code language: YAML (yaml)

まずAWS管理ポリシーAWSLambdaBasicExecutionRoleを指定して、関数実行のために必要な権限を付与します。
加えて、SNSトピックにメッセージをパブリッシュする権限も付与します。

(参考)タスク定義

Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: !Sub "${Prefix}-container"
          Image: nginx:latest
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Sub "${Prefix}-container"
      Cpu: !Ref TaskCpu
      ExecutionRoleArn: !Ref FargateTaskExecutionRole
      Memory: !Ref TaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      TaskRoleArn: !Ref TaskRole
Code language: YAML (yaml)

タスク定義のLogConfigurationプロパティがポイントです。
配信するロググループ等を設定します。

ECSのログをCloudWatch Logsに配信する方法については、以下のページもご確認ください。

あわせて読みたい
プライベートサブネットのFargateコンテナのログをCloudWatch Logsに配信する 【プライベートサブネットのECS(Fargate)コンテナのログをCloudWatch Logsに配信する】 ECSタスク内で実行されているコンテナのログを収集する方法の1つに、CloudWatch ...

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • SNSトピック:fa-066
  • ECSクラスター:fa-066-cluster
  • ECSサービス:fa-066-service
  • CloudWatch Logsロググループ:fa-066-LogGroup
  • EC2インスタンス:i-0bac8729dfcf0dc9f

メールアドレスの認証

SNSトピックのサブスクライバーとしてメールアドレスを指定した場合、そのメールアドレスを認証する必要があります。

詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CFNでSNS入門 – email版 【CFNでSNS入門 - email版】 AWS SNSはメッセージングサービスです。 今回は入門編ということで、通知先にEmailを指定する方法をご紹介します。 【構築する環境】 2種類...

リソース確認

AWS Management Consoleから各リソースを確認します。

SNSトピックを確認します。

Detail of SNS Topic.

正常にSNSトピックが作成されていることがわかります。

加えて、サブスクライバーとして登録したメールアドレスが登録されていることもわかります。
またこのメールアドレスのStatusの値を見ると「Confirmed」とあり、認証が完了していることもわかります。

続いてECS(Fargate)を確認します。

Detail of ECS(Fargate) 1.
Detail of ECS(Fargate) 2.

正常にECSクラスター・サービス・タスクが作成されています。
タスクに割り当てられているプライベートアドレスが「10.0.3.61」ということもわかります。

Lambda関数を確認します。

Detail of Lambda Function.

正常に作成されています。
サブスクリプションフィルターが「error」を検出すると、この関数が実行されて、SNSトピックにログの中身が連携されます。

CloudWatch Logsロググループを確認します。

Detail of CloudWatch Logs Log Group 1.
Detail of CloudWatch Logs Log Group 2.

ロググループにストリームとサブスクリプションフィルターが作成されていることがわかります。

動作確認

準備が整いましたので、EC2インスタンスにアクセスします。

% aws ssm start-session --target i-0bac8729dfcf0dc9f

Starting session with SessionId: root-079cb8e3e83a3c9e9
sh-4.2$Code language: JavaScript (javascript)

SSM Session Managerの詳細につきましては、以下のページをご確認ください。

あわせて読みたい
LinuxインスタンスにSSM Session Manager経由でアクセスする 【LinuxインスタンスにSSM Session Manager経由でアクセスする】 EC2インスタンスにSSM Session Manager経由でアクセスする構成を確認します。 Session Manager は完全...

curlコマンドを使って、タスク内のコンテナにアクセスします。

sh-4.2$ curl http://10.0.3.61
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
Code language: Bash (bash)

正常にアクセスすることができました。

続いて存在しないページにアクセスし、わざとエラーを発生させます。

sh-4.2$ curl http://10.0.3.61/hogehoge.html
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.23.1</center>
</body>
</html>
Code language: Bash (bash)

エラーが発生しました。

すぐさま登録したアドレスにメールが届きました。

Authentication of email addresses as notification recipients for SNS.

メールの本文は、先ほど発生したエラー内容となっております。
このようにサブスクリプションフィルターを使用することによって、特定の文字列を検出し、SNSと連携してメール通知することができます。

ちなみに以下がCloudWatch Logsロググループのストリーム内容です。

Error log in CloudWatch Logs.

確かにメールの本文の元となったログが確認できます。

まとめ

ECSコンテナのログをCloudWatch Logsに配信し、その中からエラーログを抽出して、その内容をメールで通知する方法を確認しました。

目次