エラーログ監視アラート: AWS Fargate + CloudWatch + Lambda + SNS

# はじめに

このドキュメントは、AWS Fargateを使用して実行されるWebサイトのエラーログを監視し、問題が発生した際にメールで通知するためのシステムを構築する手順を説明します。このシステムは、CloudWatch Logsを使用してログを収集し、Lambdaを使用してエラーログを検出し、SNSを使用して通知を送信します。


作成したリポジトリはこちらです。

https://github.com/sugiyama404/practice_cloudwatch_sns

## 目的

Webサイトの可用性と信頼性を確保するために、エラーログをリアルタイムで監視し、問題が発生した際に即座に対応することが重要です。このシステムは、以下の目的を達成するために設計されています。

+ **リアルタイム監視**: エラーログをリアルタイムで監視し、迅速な対応を可能にする。
+ **自動通知**: エラーログが検出された際に、自動的に通知を送信し、担当者に即座に知らせる。
+ **インフラの可視化**: AWSサービスを活用して、ログ監視と通知のインフラを効率的に構築する。

## インフラ構成

![aws.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/a28b766f-880e-9b7c-6170-a5c7db9e4bb4.png)


+ **AWS Fargate**: アプリケーションコンテナを実行し、ログをCloudWatch Logsに送信します。
+ **CloudWatch Logs**: Fargateコンテナから送信されたログを収集・保存します。
+ **Lambda**: CloudWatch Logsからエラーログを検出し、SNSトピックに通知を送信します。
+ **SNS (Simple Notification Service)**: Lambdaからの通知を受け取り、指定されたメールアドレスにエラーメールを送信します。

# 実装方法

以下のコードは、Express.jsを使用したシンプルなNode.jsアプリケーションです。このアプリケーションには複数のルートがあり、特定のルートにアクセスするとエラーメッセージがログに記録されます。

## webserver
```javascript:webserver/app/main.js
const express = require('express');
const app = express();

// 各ルートの設定
const routes = [
    { path: '/', action: 'index', message: 'ホーム画面です\n' },
    { path: '/about', action: 'about', message: 'これはAboutページです\n' },
    { path: '/faq', action: 'faq', message: 'これはFAQページです\n' },
    { path: '/portfolio', action: 'portfolio', message: 'これはPortfolioページです\n' },
    { path: '/error', action: 'error', message: 'これはerrorページです\n' },
];

routes.forEach(route => {
    app.get(route.path, (req, res) => {
        try {
            console.log(route.action);
            if (route.action === 'error') {
                throw new Error('An error page was accessed.');
            } else {
                res.send(route.message);
            }
        } catch (err) {
            console.error('Fatal error:', err.message);
            res.status(500).send('致命的なエラーが発生しました\n');
        }
    });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});
```

## iam

まず、AWSリソースに必要な権限を付与するためのIAMロールを作成します。ここでは、Fargateタスク用のecs_roleとLambda関数用のlambda_roleを設定します。

```terraform:infra/modules/iam/aws_iam_role.tf
resource "aws_iam_role" "ecs_role" {
  name = "ecs_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid : ""
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      },
    ]
  })

  tags = {
    Name = "${var.app_name}-ecs-iam-role"
  }
}

resource "aws_iam_role" "lambda_role" {
  name = "lambda_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com",
        },
      },
    ],
  })

  tags = {
    Name = "${var.app_name}-lambda-iam-role"
  }
}
```

次に、各ロールに付与するポリシーを作成します。ここでは、ECR、CloudWatch Logs、およびLambdaに必要なポリシーを定義します。

```terraform:infra/modules/iam/aws_iam_policy.tf
resource "aws_iam_policy" "ecr_policy" {
  name = "${var.app_name}-ecr-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ecr:*"
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

resource "aws_iam_policy" "cloudwatch_policy" {
  name = "${var.app_name}-cloudwatch-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:*"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:*:*:*"
      },
    ]
  })
}

resource "aws_iam_policy" "lambda_policy" {
  name = "lambda_policy"

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
          "sns:Publish"
        ],
        Resource = "*",
        Effect   = "Allow"
      }
    ]
  })
}
```

最後に、作成したIAMポリシーを各ロールにアタッチします。

```terraform:infra/modules/iam/aws_iam_policy_attachment.tf
resource "aws_iam_policy_attachment" "ecr_attach" {
  name       = "${var.app_name}-ecr-attach"
  roles      = ["${aws_iam_role.ecs_role.name}"]
  policy_arn = aws_iam_policy.ecr_policy.arn
}

resource "aws_iam_policy_attachment" "cloudwatch_attach" {
  name       = "${var.app_name}-cloudwatch-attach"
  roles      = ["${aws_iam_role.ecs_role.name}"]
  policy_arn = aws_iam_policy.cloudwatch_policy.arn
}

resource "aws_iam_policy_attachment" "lambda_role_attachment" {
  name       = "${var.app_name}-lambda-attach"
  roles      = ["${aws_iam_role.lambda_role.name}"]
  policy_arn = aws_iam_policy.lambda_policy.arn
}
```


## CloudWatch

CloudWatch Logsからエラーログを検出し、Lambda関数をトリガーするためには、ログサブスクリプションフィルタを設定する必要があります。このフィルタは、特定のロググループ内のログイベントをフィルタリングし、条件に一致するログをLambda関数に送信します。

以下のTerraformコードは、特定のロググループから"error"というパターンに一致するログイベントを検出し、Lambda関数に送信するサブスクリプションフィルタを作成します。

```terraform:infra/modules/cloudwatch/aws_cloudwatch_log_subscription_filter.tf
resource "aws_cloudwatch_log_subscription_filter" "lambda_trigger" {
  name            = "ErrorLogsToLambda"
  log_group_name  = var.webserver_log_group_name
  filter_pattern  = "error"
  destination_arn = var.lambda_function_arn
}
```


## Lambda

Lambda関数のログを保存するためのCloudWatch Log Groupを作成します。

```terraform:infra/modules/lambda/aws_cloudwatch_log_group.tf
resource "aws_cloudwatch_log_group" "lambda" {
  name = "/aws/lambda/${var.lambda_function_name}"
}
```

Lambda関数を作成し、CloudWatch Logsからのエラーログを処理してSNS通知を送信するための設定を行います。

```terraform:infra/modules/lambda/aws_lambda_function.tf
data "archive_file" "lambda" {
  type        = "zip"
  source_dir  = "./modules/lambda/src/in"
  output_path = "./modules/lambda/src/out/lambda_function_payload.zip"
}

resource "aws_lambda_function" "main" {
  filename         = "./modules/lambda/src/out/lambda_function_payload.zip"
  function_name    = var.lambda_function_name
  description      = "lambda_function"
  role             = var.lambda_iam_role
  architectures    = ["x86_64"]
  handler          = "index.lambda_handler"
  source_code_hash = data.archive_file.lambda.output_base64sha256
  timeout          = 30
  runtime          = "python3.9"
  depends_on       = [aws_cloudwatch_log_group.lambda]

  environment {
    variables = {
      SNS_TOPIC_ARN = var.sns_topic_arn
    }
  }
  tags = {
    Name = "${var.app_name}-lamdba"
  }
}
```

Lambda関数がSNSとCloudWatch Logsからトリガーされるように、必要な実行許可を設定します。

```terraform:infra/modules/lambda/aws_lambda_permission.tf
resource "aws_lambda_permission" "allow_sns" {
  statement_id  = "AllowExecutionFromSNS"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = var.sns_topic_arn
}

resource "aws_lambda_permission" "allow_cloudwatch" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  principal     = "logs.${var.region}.amazonaws.com"
  source_arn    = "arn:aws:logs:${var.region}:${local.account_id}:log-group:${var.webserver_log_group_name}:*"
}
```

CloudWatch Logsからエラーログを受信し、SNSを使用して通知を送信するLambda関数のPythonコードです。

```python:infra/modules/lambda/src/in/index.py
import boto3
import os
import json
import base64
import gzip

sns_client = boto3.client('sns')
sns_topic_arn = os.environ['SNS_TOPIC_ARN']

def lambda_handler(event, context):
    # CloudWatch LogsのデータはBase64でエンコードされています
    log_data_b64 = event['awslogs']['data']
    log_data = base64.b64decode(log_data_b64)

    log_data_decoded = gzip.decompress(log_data)
    log_data_decoded = json.loads(log_data_decoded)
    logEvents = log_data_decoded['logEvents']

    log_messages = []
    for index, event in enumerate(logEvents):
        log_messages.append(f"[error message {index + 1}]")
        log_messages.append(event['message'])

    # 文字列のリストを改行で結合します
    message = f"Error log detected:\n" + "\n".join(log_messages)
    subject = "CloudWatch Error Log Alert"

    # SNSトピックにメッセージをパブリッシュします
    sns_client.publish(
        TopicArn=sns_topic_arn,
        Message=message,
        Subject=subject
    )
    return {
        'statusCode': 200,
        'body': json.dumps('Email sent successfully!')
    }
```

## sns

まず、エラーログ通知用のSNSトピックを作成します。このトピックにメールアドレスをサブスクライブし、エラーログが発生した際に通知を受け取るようにします。

```terraform:infra/modules/sns/aws_sns_topic.tf
resource "aws_sns_topic" "sns_topic" {
  name = "email_topic"

  tags = {
    "Name" = "${var.app_name}-email_topic"
  }
}
```

次に、作成したSNSトピックにメールアドレスをサブスクライブします。ここでは、メールアドレスをemail.txtファイルから読み込み、SNSトピックにサブスクライブします。

```terraform:infra/modules/sns/aws_sns_topic_subscription.tf
resource "aws_sns_topic_subscription" "email_subscription" {
  topic_arn = aws_sns_topic.sns_topic.arn
  protocol  = "email"
  endpoint  = trimspace(file("${path.module}/src/email.txt"))
}
```


# 結果

以下のようなメールが届きました。

![email.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/eba5abbd-da48-0fb5-af92-a440c0d9d919.png)

# まとめ
以上の手順を通じて、AWS Fargate、CloudWatch Logs、Lambda、およびSNSを使用したエラーログ監視およびメール通知システムを構築することができます。このシステムにより、Webサイトで発生したエラーをリアルタイムで検出し、指定されたメールアドレスに通知を送信することが可能になります。

これにより、システムの可用性と信頼性が向上し、問題が発生した際に迅速に対応することができます。各サービスの詳細な設定やパラメータは、AWS公式ドキュメントを参照してください。