はじめに
このドキュメントは、AWS Fargateを使用して実行されるWebサイトのエラーログを監視し、問題が発生した際にメールで通知するためのシステムを構築する手順を説明します。このシステムは、CloudWatch Logsを使用してログを収集し、Lambdaを使用してエラーログを検出し、SNSを使用して通知を送信します。
作成したリポジトリはこちらです。
目的
Webサイトの可用性と信頼性を確保するために、エラーログをリアルタイムで監視し、問題が発生した際に即座に対応することが重要です。このシステムは、以下の目的を達成するために設計されています。
- リアルタイム監視: エラーログをリアルタイムで監視し、迅速な対応を可能にする。
- 自動通知: エラーログが検出された際に、自動的に通知を送信し、担当者に即座に知らせる。
- インフラの可視化: AWSサービスを活用して、ログ監視と通知のインフラを効率的に構築する。
インフラ構成
- AWS Fargate: アプリケーションコンテナを実行し、ログをCloudWatch Logsに送信します。
- CloudWatch Logs: Fargateコンテナから送信されたログを収集・保存します。
- Lambda: CloudWatch Logsからエラーログを検出し、SNSトピックに通知を送信します。
- SNS (Simple Notification Service): Lambdaからの通知を受け取り、指定されたメールアドレスにエラーメールを送信します。
実装方法
以下のコードは、Express.jsを使用したシンプルなNode.jsアプリケーションです。このアプリケーションには複数のルートがあり、特定のルートにアクセスするとエラーメッセージがログに記録されます。
webserver
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を設定します。
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に必要なポリシーを定義します。
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ポリシーを各ロールにアタッチします。
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関数に送信するサブスクリプションフィルタを作成します。
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を作成します。
resource "aws_cloudwatch_log_group" "lambda" {
name = "/aws/lambda/${var.lambda_function_name}"
}
Lambda関数を作成し、CloudWatch Logsからのエラーログを処理してSNS通知を送信するための設定を行います。
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からトリガーされるように、必要な実行許可を設定します。
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コードです。
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トピックを作成します。このトピックにメールアドレスをサブスクライブし、エラーログが発生した際に通知を受け取るようにします。
resource "aws_sns_topic" "sns_topic" {
name = "email_topic"
tags = {
"Name" = "${var.app_name}-email_topic"
}
}
次に、作成したSNSトピックにメールアドレスをサブスクライブします。ここでは、メールアドレスをemail.txtファイルから読み込み、SNSトピックにサブスクライブします。
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"))
}
結果
以下のようなメールが届きました。
まとめ
以上の手順を通じて、AWS Fargate、CloudWatch Logs、Lambda、およびSNSを使用したエラーログ監視およびメール通知システムを構築することができます。このシステムにより、Webサイトで発生したエラーをリアルタイムで検出し、指定されたメールアドレスに通知を送信することが可能になります。
これにより、システムの可用性と信頼性が向上し、問題が発生した際に迅速に対応することができます。各サービスの詳細な設定やパラメータは、AWS公式ドキュメントを参照してください。