はじめに
AWS でログ監視を設計するとき、代表的な方式として次の2つがある。
- 方式①: CloudWatch Metric Filter + CloudWatch Alarm
- 方式②: CloudWatch Logs Subscription Filter + 通知 Lambda
どちらを選ぶかは「インシデントチケットシステムとどう連携するか」が関連する。
| 連携方式 | 向いている方式 |
|---|---|
| メール通知 → 人が確認・詳細は手動起票 | 方式① |
| API で自動起票(ログ内容込み) | 方式② |
本記事では、24/365 オペレータが常駐するインシデント対応運用を前提として、両方式について比較する。
また、ログ管理対象はひとまず AWS Lambda として記載。
前提:運用フロー
本記事では以下の運用フローを想定する。
① Lambda でエラーが発生する
↓
② エラーをインシデントチケットシステム(ServiceNow / Jira / Backlog 等)に連携
↓
③ 24/365 オペレータがインシデントチケットを監視・確認
↓
④ インシデントの優先度に応じて開発者に連絡
├── 高優先度(即時コール)→ 開発者に電話
└── 低優先度(翌営業日コール)→ チケットにコメント・翌日連絡
↓
⑤ 開発者がインシデントチケットを確認 → インシデント対応開始
②のチケット連携方法が、方式①と方式②の選択に影響する。
全体アーキテクチャ概要
両方式とも、Lambda のログ出力から通知までの基本フローは共通。
Lambda(エラー発生)
↓ ログ出力
CloudWatch Logs
↓ [方式によって異なる]
通知(メール or チケット API)
↓
インシデントチケットシステム
↓
24/365 オペレータが確認 → 優先度判定
↓
開発者 → インシデント対応開始
方式①:Metric Filter + CloudWatch Alarm(メール連携)
仕組み
CloudWatch Logs のロググループに Metric Filter を設定し、ERROR / WARNING などをカウントするカスタムメトリクスを生成する。閾値(1件以上)を超えると CloudWatch Alarm が発火し、SNS 経由でメールを送信する。
[監視対象 Lambda]
↓ ログ出力
[CloudWatch Logs]
↓ Metric Filter(1分ごとにカウント)
[CloudWatch Alarm(count >= 1 で ALARM)]
↓
[SNS] → メール通知(アラーム発生の旨のみ)
↓
[インシデントチケットシステム](オペレータが手動起票)
↓
[24/365 オペレータ] → CloudWatch Logs でログ詳細確認
メール連携が自然な理由
CloudWatch Alarm が SNS に渡すのはアラームのメタ情報のみ(関数名・状態変化・理由)。ログの中身は含まれない。
API 連携でチケットを自動起票しようとしても、内容が薄くなる。ログを含めるには CloudWatch Logs API でログを取得する別途 Lambda が必要になり、構成が複雑化する。
→ メール通知 → オペレータがコンソールで確認 → 詳細は手動起票 の運用が最もシンプルに成立する。
メリット
| # | 内容 |
|---|---|
| 1 | シンプルな構成 — 通知専用 Lambda が不要 |
| 2 | コストが低い — Metric Filter は無料、Alarm は $0.10/アラーム/月 |
| 3 | 管理コストがゼロ — Lambda のランタイム更新・障害リスクなし |
| 4 | 高頻度エラーでも安定 — 1 分内の複数エラーはアラームを 1 回のみ発火 |
| 5 | CloudFormation がシンプル — リソース数が少なく見通しが良い |
デメリット
| # | 内容 |
|---|---|
| 1 | 通知メールにログ内容が含まれない — 詳細は CloudWatch Logs で確認が必要 |
| 2 | 最大 2 分の遅延 — Metric Filter 集計(1 分)+ Alarm 評価(1 分) |
| 3 | チケット自動起票が困難 — ログ内容なしでは意味のあるチケットを作れない |
CloudFormation 実装
スタック構成
lambda-log-monitoring-base … SNS Topic + メールサブスクリプション
lambda-log-monitoring-filters … 監視対象 Lambda + Metric Filter + CloudWatch Alarm
スタック 1: 基盤スタック(SNS)
# lambda-log-monitoring-base.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda log monitoring - base (SNS)
Resources:
AlertSNSTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: lambda-log-alert-topic
AlertSNSEmailSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref AlertSNSTopic
Protocol: email
Endpoint: your-email@example.com # ← 変更してください
Outputs:
SNSTopicArn:
Value: !Ref AlertSNSTopic
Export:
Name: !Sub "${AWS::StackName}-SNSTopicArn"
スタック 2: Metric Filter + CloudWatch Alarm
# lambda-log-monitoring-filters.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda log monitoring - Metric Filters and CloudWatch Alarms
Parameters:
BaseStackName:
Type: String
Default: lambda-log-monitoring-base
Resources:
MonitoredLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: monitored-lambda-role
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
MonitoredLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: my-monitored-lambda
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt MonitoredLambdaRole.Arn
Timeout: 30
Code:
ZipFile: |
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
pass # アプリケーションロジック
# Log Group を明示作成(Metric Filter より先に存在させるため)
MonitoredLambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${MonitoredLambdaFunction}"
RetentionInDays: 30
ErrorMetricFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: !Ref MonitoredLambdaLogGroup
FilterName: error-filter
FilterPattern: '"ERROR"'
MetricTransformations:
- MetricName: ErrorCount
MetricNamespace: !Sub "LambdaLogMonitoring/${MonitoredLambdaFunction}"
MetricValue: "1"
DefaultValue: 0
DependsOn: MonitoredLambdaLogGroup
WarningMetricFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: !Ref MonitoredLambdaLogGroup
FilterName: warning-filter
FilterPattern: '"WARNING"'
MetricTransformations:
- MetricName: WarningCount
MetricNamespace: !Sub "LambdaLogMonitoring/${MonitoredLambdaFunction}"
MetricValue: "1"
DefaultValue: 0
DependsOn: MonitoredLambdaLogGroup
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${MonitoredLambdaFunction}-ERROR-alarm"
AlarmDescription: !Sub |
${MonitoredLambdaFunction} のログに ERROR が検知されました。
CloudWatch Logs で詳細を確認してください。
Namespace: !Sub "LambdaLogMonitoring/${MonitoredLambdaFunction}"
MetricName: ErrorCount
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- Fn::ImportValue: !Sub "${BaseStackName}-SNSTopicArn"
WarningAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${MonitoredLambdaFunction}-WARNING-alarm"
AlarmDescription: !Sub |
${MonitoredLambdaFunction} のログに WARNING が検知されました。
CloudWatch Logs で詳細を確認してください。
Namespace: !Sub "LambdaLogMonitoring/${MonitoredLambdaFunction}"
MetricName: WarningCount
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- Fn::ImportValue: !Sub "${BaseStackName}-SNSTopicArn"
デプロイ手順
# 1. 基盤スタック(SNS)をデプロイ
aws cloudformation deploy \
--stack-name lambda-log-monitoring-base \
--template-file lambda-log-monitoring-base.yaml \
--region ap-northeast-1
# 2. 通知先メールアドレスに届く「Confirm subscription」メールをクリック
# ※ここをスキップするとメールが届かない
# 3. Metric Filter スタックをデプロイ
aws cloudformation deploy \
--stack-name lambda-log-monitoring-filters \
--template-file lambda-log-monitoring-filters.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides BaseStackName=lambda-log-monitoring-base \
--region ap-northeast-1
# 4. 動作確認(Lambda を実行して ERROR ログを発生させる)
aws lambda invoke \
--function-name my-monitored-lambda \
--payload '{}' \
--cli-binary-format raw-in-base64-out \
response.json
# 1〜2 分後にアラームメールが届く
新規 Lambda を監視対象に追加する場合
既存スタックに以下を追記してデプロイし直すだけでよい(SNS の変更は不要)。
NewLambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/new-lambda-name
RetentionInDays: 30
NewLambdaErrorMetricFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: !Ref NewLambdaLogGroup
FilterName: new-lambda-error-filter
FilterPattern: '"ERROR"'
MetricTransformations:
- MetricName: ErrorCount
MetricNamespace: LambdaLogMonitoring/new-lambda-name
MetricValue: "1"
DefaultValue: 0
NewLambdaErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: new-lambda-ERROR-alarm
Namespace: LambdaLogMonitoring/new-lambda-name
MetricName: ErrorCount
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- Fn::ImportValue: !Sub "${BaseStackName}-SNSTopicArn"
方式②:Subscription Filter + 通知 Lambda(API 連携)
仕組み
CloudWatch Logs の Subscription Filter を使い、特定パターンにマッチしたログイベントをリアルタイムで 通知 Lambda に転送する。通知 Lambda はイベントを gzip デコードして整形し、SNS メールの送信やチケットシステムへの API 起票を行う。
[監視対象 Lambda]
↓ ログ出力
[CloudWatch Logs]
↓ Subscription Filter(パターンマッチ・リアルタイム転送)
[通知 Lambda](ログ内容をデコード・整形)
├── SNS → メール通知(ログ内容込み)
└── Ticket API → インシデントチケット自動起票(ServiceNow / Jira / Backlog)
API 連携が自然な理由
Subscription Filter が通知 Lambda に渡すのはログの中身そのもの(gzip 圧縮された全データ)。Lambda はすでにログ内容を持っているため、そのままチケットシステムの API リクエストに詰めて送れる。
→ ログ内容を含んだリッチなチケットを Lambda 1 本で自動起票できる。
通知メール本文例(SNS 経由の場合)
件名: [ALERT] Lambda ログ監視通知 - my-monitored-lambda
[ALERT] Lambda ログ監視通知
関数名 : my-monitored-lambda
ログストリーム: 2026/06/03/[$LATEST]abc1234567890
--- ログ内容 ---
2026-06-03 10:30:00 JST ERROR Database connection failed: timeout after 30s
2026-06-03 10:30:01 JST ERROR Retry 1/3 failed
メリット
| # | 内容 |
|---|---|
| 1 | ログ内容をそのまま API に渡せる — リッチなチケットを Lambda 1 本で自動起票できる |
| 2 | リアルタイム性が高い — 数秒〜1 分以内に通知 |
| 3 | 通知先を自由に拡張できる — SNS・チケット API・Slack など複数チャネルに同時送信可能 |
| 4 | 複雑なフィルタパターンに対応 — OR / AND / 任意文字列を柔軟に設定可能 |
デメリット
| # | 内容 |
|---|---|
| 1 | 通知 Lambda の管理が必要 — ランタイム更新・障害監視が必要 |
| 2 | 非同期呼び出しのリスク — Lambda 失敗時は再試行 2 回後にイベントが消える(DLQ / Lambda Destinations で対策可) |
| 3 | 高頻度エラー時はコスト増加 — エラーのたびに Lambda 実行コストが発生 |
| 4 | 1 ロググループあたり最大 2 個 — Subscription Filter の AWS 制限 |
CloudFormation 実装
スタック構成
lambda-log-monitoring-base … SNS Topic + 通知 Lambda + IAM ロール
lambda-log-monitoring-subscriptions … 監視対象 Lambda + Subscription Filter
スタック 1: 基盤スタック(SNS + 通知 Lambda)
通知 Lambda は SNS メール送信とチケット API 起票を同時に行う。API 連携先の接続情報は環境変数で渡す。
# lambda-log-monitoring-base.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda log monitoring - base (SNS + Notification Lambda)
Resources:
AlertSNSTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: lambda-log-alert-topic
AlertSNSEmailSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref AlertSNSTopic
Protocol: email
Endpoint: your-email@example.com # ← 変更してください
NotificationLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: lambda-log-notification-role
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
Policies:
- PolicyName: SNSPublishPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sns:Publish
Resource: !Ref AlertSNSTopic
NotificationLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: lambda-log-notification
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt NotificationLambdaRole.Arn
Timeout: 30
Environment:
Variables:
SNS_TOPIC_ARN: !Ref AlertSNSTopic
JIRA_URL: "" # 例: https://your-org.atlassian.net
JIRA_PROJECT_KEY: "" # 例: OPS
JIRA_TOKEN: "" # Basic 認証トークン(本番では Secrets Manager 推奨)
Code:
ZipFile: |
import json, gzip, base64, boto3, os, urllib.request
from datetime import datetime, timezone, timedelta
def lambda_handler(event, context):
payload = json.loads(
gzip.decompress(base64.b64decode(event['awslogs']['data']))
)
log_group = payload['logGroup']
log_stream = payload['logStream']
function_name = (
log_group.split('/')[-1]
if '/aws/lambda/' in log_group
else log_group
)
jst = timezone(timedelta(hours=9))
messages = [
f"{datetime.fromtimestamp(e['timestamp']/1000, tz=jst).strftime('%Y-%m-%d %H:%M:%S JST')} {e['message'].strip()}"
for e in payload['logEvents']
]
log_text = "\n".join(messages)
# ① SNS メール通知
boto3.client('sns').publish(
TopicArn=os.environ['SNS_TOPIC_ARN'],
Subject=f"[ALERT] Lambda ログ監視通知 - {function_name}",
Message=(
"[ALERT] Lambda ログ監視通知\n\n"
f"関数名 : {function_name}\n"
f"ログストリーム: {log_stream}\n\n"
"--- ログ内容 ---\n" + log_text + "\n"
),
)
# ② Jira にインシデントチケット自動起票(API 連携する場合)
jira_url = os.environ.get('JIRA_URL')
if jira_url:
body = json.dumps({
"fields": {
"project": {"key": os.environ['JIRA_PROJECT_KEY']},
"summary": f"[Lambda Alert] {function_name} でエラー検知",
"description": f"関数名: {function_name}\n\n{log_text}",
"issuetype": {"name": "Bug"},
"priority": {"name": "High"},
}
}).encode()
req = urllib.request.Request(
f"{jira_url}/rest/api/2/issue",
data=body,
headers={
"Content-Type": "application/json",
"Authorization": f"Basic {os.environ['JIRA_TOKEN']}",
},
method="POST",
)
urllib.request.urlopen(req)
# CloudWatch Logs → 通知 Lambda の Invoke を許可
NotificationLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt NotificationLambdaFunction.Arn
Action: lambda:InvokeFunction
Principal: logs.amazonaws.com
SourceAccount: !Ref AWS::AccountId
Outputs:
NotificationLambdaArn:
Value: !GetAtt NotificationLambdaFunction.Arn
Export:
Name: !Sub "${AWS::StackName}-NotificationLambdaArn"
SNSTopicArn:
Value: !Ref AlertSNSTopic
Export:
Name: !Sub "${AWS::StackName}-SNSTopicArn"
Note: 本番環境では
JIRA_TOKENなどの認証情報は AWS Secrets Manager や Parameter Store(SecureString)で管理し、Lambda から取得する構成を推奨する。
スタック 2: Subscription Filter
# lambda-log-monitoring-subscriptions.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda log monitoring - Subscription Filters
Parameters:
BaseStackName:
Type: String
Default: lambda-log-monitoring-base
Resources:
MonitoredLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: monitored-lambda-role
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
MonitoredLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: my-monitored-lambda
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt MonitoredLambdaRole.Arn
Timeout: 30
Code:
ZipFile: |
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
pass # アプリケーションロジック
# Log Group を明示作成(Subscription Filter より先に存在させるため)
MonitoredLambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${MonitoredLambdaFunction}"
RetentionInDays: 30
# ERROR または WARNING をリアルタイムに通知 Lambda へ転送
MonitoredLambdaSubscriptionFilter:
Type: AWS::Logs::SubscriptionFilter
Properties:
LogGroupName: !Ref MonitoredLambdaLogGroup
FilterName: error-warning-filter
FilterPattern: '?"ERROR" ?"WARNING"' # OR 条件
DestinationArn: !ImportValue
Fn::Sub: "${BaseStackName}-NotificationLambdaArn"
DependsOn: MonitoredLambdaLogGroup
フィルタパターン例
| 用途 | パターン |
|---|---|
| ERROR のみ | "ERROR" |
| WARNING のみ | "WARNING" |
| ERROR または WARNING(OR) | ?"ERROR" ?"WARNING" |
| ERROR かつ特定文字列(AND) | "ERROR" "database" |
| 任意のカスタム文字列 | "PAYMENT_FAILED" |
デプロイ手順
# 1. 基盤スタック(SNS + 通知 Lambda)をデプロイ
aws cloudformation deploy \
--stack-name lambda-log-monitoring-base \
--template-file lambda-log-monitoring-base.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--region ap-northeast-1
# 2. 通知先メールアドレスに届く「Confirm subscription」メールをクリック
# 3. Subscription Filter スタックをデプロイ
aws cloudformation deploy \
--stack-name lambda-log-monitoring-subscriptions \
--template-file lambda-log-monitoring-subscriptions.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides BaseStackName=lambda-log-monitoring-base \
--region ap-northeast-1
# 4. 動作確認(Lambda を実行して ERROR ログを発生させる)
aws lambda invoke \
--function-name my-monitored-lambda \
--payload '{}' \
--cli-binary-format raw-in-base64-out \
response.json
# 数秒〜1 分でメール(および Jira チケット)が届く
新規 Lambda を監視対象に追加する場合
既存スタックに以下を追記してデプロイし直すだけでよい(通知 Lambda・SNS の変更は不要)。
NewLambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/new-lambda-name
RetentionInDays: 30
NewLambdaSubscriptionFilter:
Type: AWS::Logs::SubscriptionFilter
Properties:
LogGroupName: !Ref NewLambdaLogGroup
FilterName: new-lambda-error-warning-filter
FilterPattern: '?"ERROR" ?"WARNING"'
DestinationArn: !ImportValue
Fn::Sub: "${BaseStackName}-NotificationLambdaArn"
DependsOn: NewLambdaLogGroup
方式比較
| 項目 | 方式① Metric Filter + Alarm | 方式② Subscription Filter + Lambda |
|---|---|---|
| チケット連携 | メール通知 → 人が手動起票 | Lambda から直接 API 起票(ログ内容込み) |
| 通知内容 | アラーム発生のみ。ログ詳細は CloudWatch で確認 | ログの中身ごとメール・チケットに届く |
| 通知の遅延 | 最大 2 分 | 数秒〜1 分以内 |
| 実装の複雑さ | シンプル(通知 Lambda 不要) | やや複雑(通知 Lambda が必要) |
| コスト | 低い(Alarm $0.10/月/アラーム) | Lambda 実行コストが加算 |
| 1 ロググループあたりの上限 | Metric Filter 最大 10 個 | Subscription Filter 最大 2 個 |
| Lambda の管理 | 不要 | 必要(ランタイム更新・障害監視) |
| 通知チャネルの拡張 | 困難(別途 Lambda が必要) | Lambda コードを追記するだけ |
どちらを選ぶべきか
判断の起点は「チケットシステムが API 連携できるか」となる。
チケットシステムが API 連携できる?
├── Yes → 方式②(通知 Lambda から直接自動起票)
└── No → 方式①(メール通知 → オペレータが手動起票)
方式①を選ぶ場合
- チケットシステムが API 連携に対応していない(またはメール連携で十分)
- コストを最小化したい
- 通知 Lambda の管理コストを避けたい
- オペレータが CloudWatch コンソールにアクセスできる体制がある
方式②を選ぶ場合
- チケットシステムが API 連携に対応しており、ログ内容込みで自動起票したい
- Slack / Teams など複数チャネルへの通知を Lambda 1 本で一元管理したい
- 数秒単位のリアルタイム通知が必要な SLA がある
まとめ
| 方式① | 方式② | |
|---|---|---|
| チケット連携 | メール → 手動起票 | API → 自動起票 |
| 一言で言うと | シンプル・安い・フルマネージド | 柔軟・リアルタイム・カスタマイズ自由 |
| 向いている運用 | API 連携不要・ログ内容をコンソールから確認 | API 連携あり・ログ内容を自動でチケットに反映 |

