はじめに
前回の記事では、Lambda のエラーログ監視として以下の2方式を CloudFormation テンプレートで実装・比較しました。
| 方式 | 構成 | 特徴 |
|---|---|---|
| ① Metric Filter + CloudWatch Alarm + SNS | ログを1分ごとに集計 → 閾値超過でメール | シンプル・Lambda 不要・約2分の遅延 |
| ② Subscription Filter + 通知 Lambda | ログをリアルタイム転送 → API 起票 | リッチ・ログ内容あり・数秒で通知 |
今回は 実際に Backlog へ連携し、両方式の動作を検証します。通知のタイミング・Backlog 課題の内容・運用上のハマりポイントを実測値と合わせて紹介します。
前提:運用フロー
前回記事と同じく、24/365 オペレータ常駐を想定した運用フローです。
Lambda でエラーが発生
↓
Backlog に課題が自動登録
↓
24/365 オペレータが Backlog を監視・確認
↓
優先度に応じて開発者に連絡
├── 高優先度 → 即時コール
└── 低優先度 → 翌営業日対応
↓
開発者がインシデント対応開始
検証環境
監視対象 Lambda(共通)
両方式で同じ Lambda 関数をエラー発生源として使います。
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info("Starting process...")
logger.error("Database connection failed: timeout after 30s")
logger.error("NullPointerException at module.process_data line 42")
logger.info("Process ended with errors")
raise Exception("Application error: critical failure")
実行すると CloudWatch Logs に以下のログが記録されます。
[INFO] 2026-06-27T09:54:00Z xxxx Starting process...
[ERROR] 2026-06-27T09:54:00Z xxxx Database connection failed: timeout after 30s
[ERROR] 2026-06-27T09:54:00Z xxxx NullPointerException at module.process_data line 42
[INFO] 2026-06-27T09:54:00Z xxxx Process ended with errors
[ERROR] Exception: Application error: critical failure
方式①:Metric Filter + CloudWatch Alarm → SNS → Backlog メール
構成図
監視対象 Lambda
↓ ログ出力
CloudWatch Logs
↓ Metric Filter("[ERROR]" を含む行をカウント)
カスタムメトリクス(LambdaErrorCount)
↓ CloudWatch Alarm(1分間に1件以上で ALARM)
SNS Topic
├─→ Backlog 課題登録用メールアドレス → 課題自動登録
└─→ 担当者メール(確認用)
設定手順
⚠️ Metric Filter パターンのハマりポイント
Lambda のログ形式は \t[ERROR]\t{timestamp}\t{requestId}\t{message} です。
構造化パターン [timestamp, requestId, level="ERROR", ...] ではマッチしません。
# ❌ マッチしない(今回の検証で実際にハマった)
--filter-pattern '[timestamp, requestId, level="ERROR", ...]'
# ✅ 正しい(キーワード検索を使う)
--filter-pattern '"[ERROR]"'
CloudFormation テンプレート
前回記事のテンプレートから変更点は SNS のサブスクリプション先を Backlog のメールアドレスにする点のみです。
AlertSNSEmailSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref AlertSNSTopic
Protocol: email
Endpoint: issue-{PROJECT_KEY}-{TOKEN}@{SPACE}.backlog.com # ← Backlog の課題登録用メールアドレス
⚠️ SNS サブスクリプション確認メールへの対応
SNS にメールアドレスをサブスクライブすると確認メールが送信されます。
Backlog の課題登録用アドレスはどんなメールでも課題として登録するため、確認メール自体が課題として登録されます。
サブスクリプションを有効にするには、Backlog に登録された確認課題の本文中にある 「Confirm subscription」 リンクをクリックしてください。
動作検証結果
Lambda を3回実行後、約2分で CloudWatch Alarm が ALARM 状態に遷移しました。
Lambda 実行: 2026-06-27 18:54:56 JST
Alarm 発火: 2026-06-27 18:55:54 JST(約60秒後)
Backlog 課題登録: 2026-06-27 18:56:xx JST(さらに数十秒後)
Alarm の根拠:
1 datapoint [9.0] was greater than or equal to the threshold (1.0)
※ 3回の Lambda 実行 × 各3件の ERROR ログ = 9 件カウント
SNS から届くメール文面(= Backlog 課題内容)
方式①では SNS が生成するアラーム通知メールがそのまま Backlog の課題詳細になります。
件名: ALARM: "monitored-app-error-alarm" in Asia Pacific (Tokyo)
You are receiving this email because your Amazon CloudWatch Alarm
"monitored-app-error-alarm" in the Asia Pacific (Tokyo) region has
entered the ALARM state, because "Threshold Crossed: 1 datapoint [9.0
(27/06/26 09:54:00)] was greater than or equal to the threshold (1.0)."
Alarm Details:
- Name: monitored-app-error-alarm
- Description: [方式①] Lambda エラー検知アラーム
- State Change: INSUFFICIENT_DATA -> ALARM
- Reason for State Change: Threshold Crossed: ...
- Timestamp: Saturday 27 June, 2026 09:55:54 UTC
- AWS Account: 471112657080
- Alarm Arn: arn:aws:cloudwatch:...
Monitored Metric:
- MetricNamespace: MonitoredApp
- MetricName: LambdaErrorCount
- Period: 60 seconds
- Statistic: Sum
- TreatMissingData: notBreaching
Backlog 課題の見え方(方式①)
-
課題タイトル:
ALARM: "monitored-app-error-alarm" in Asia Pacific (Tokyo)(固定) - 課題詳細: 上記 AWS アラーム通知テキストがそのまま入る
- エラーログの内容: ❌ 含まれない
- 優先度・担当者: ❌ 自動設定不可
方式②:Subscription Filter → 通知 Lambda → Backlog REST API
構成図
監視対象 Lambda
↓ ログ出力
CloudWatch Logs
↓ Subscription Filter("ERROR" を含む行をリアルタイム転送)
通知 Lambda
↓ ログをデコード・整形
Backlog REST API(POST /api/v2/issues)
↓
Backlog 課題登録(エラーログ内容つき)
通知 Lambda の実装
前回記事の通知 Lambda を Backlog API 向けに実装します。API キーは AWS Secrets Manager で管理し、コードには書きません。
import json, base64, gzip, boto3, os
import urllib.request, urllib.parse, urllib.error
from datetime import datetime, timezone
secrets_client = boto3.client('secretsmanager', region_name='ap-northeast-1')
_backlog_config = None
def get_backlog_config():
global _backlog_config
if _backlog_config is None:
secret = secrets_client.get_secret_value(
SecretId=os.environ['BACKLOG_SECRET_NAME']
)
_backlog_config = json.loads(secret['SecretString'])
return _backlog_config
def lambda_handler(event, context):
log_data = decode_log_data(event['awslogs']['data'])
log_group = log_data['logGroup']
log_stream = log_data['logStream']
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
error_events = [
e for e in log_data['logEvents']
if 'ERROR' in e.get('message', '')
]
if not error_events:
return {'statusCode': 200, 'body': 'No error logs'}
config = get_backlog_config()
issue_key = create_backlog_issue(config, log_group, log_stream, timestamp, error_events)
print(f'Backlog 課題作成: {issue_key}')
return {'statusCode': 200, 'issueKey': issue_key}
def create_backlog_issue(config, log_group, log_stream, timestamp, error_events):
domain = config['domain']
api_key = config['apiKey']
project_id = os.environ['BACKLOG_PROJECT_ID']
issue_type_id = os.environ['BACKLOG_ISSUE_TYPE_ID']
priority_id = os.environ['BACKLOG_PRIORITY_ID']
summary = f'[Lambda] エラー検知: {log_group} ({timestamp})'
desc = '\n'.join([
'## エラー検知通知',
'',
'|項目|値|',
'|---|---|',
f'|検知日時|{timestamp}|',
f'|ロググループ|{log_group}|',
f'|ログストリーム|{log_stream}|',
f'|エラー件数|{len(error_events)} 件|',
'',
'## エラーログ(最大10件)',
'',
*[line for ev in error_events[:10]
for line in ['```', ev.get('message', '').strip(), '```', '']],
])
params = urllib.parse.urlencode({
'projectId': project_id,
'summary': summary,
'issueTypeId': issue_type_id,
'priorityId': priority_id,
'description': desc,
}).encode('utf-8')
req = urllib.request.Request(
f'https://{domain}/api/v2/issues?apiKey={api_key}',
data=params, method='POST',
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
with urllib.request.urlopen(req, timeout=15) as res:
return json.loads(res.read())['issueKey']
def decode_log_data(data):
return json.loads(gzip.decompress(base64.b64decode(data)))
Backlog の ID を事前確認する
projectId と issueTypeId は数値 ID です。以下のコマンドで確認します。
DOMAIN="your-space.backlog.com"
API_KEY="your-api-key"
# プロジェクト ID
curl "https://${DOMAIN}/api/v2/projects?apiKey=${API_KEY}" | jq '.[] | {id, projectKey}'
# 課題種別 ID
curl "https://${DOMAIN}/api/v2/projects/PROJECT_KEY/issueTypes?apiKey=${API_KEY}" | jq '.[] | {id, name}'
# 優先度 ID
curl "https://${DOMAIN}/api/v2/priorities?apiKey=${API_KEY}" | jq '.[] | {id, name}'
動作検証結果
Lambda 実行から 約30秒で Backlog に課題が作成されました。
Lambda 実行: 2026-06-27 18:52:10 JST
Backlog 課題登録: 2026-06-27 18:52:40 JST(約30秒後)
Backlog 課題の見え方(方式②)
-
課題タイトル:
[Lambda] エラー検知: /aws/lambda/monitored-app (2026-06-27 09:52:04 UTC)(自由設定) - 課題詳細: エラーログの実内容が入る(下記)
- 優先度: ✅ API で設定可能
- 担当者・カテゴリ: ✅ API で自由に設定可能
課題詳細に記録されるエラーログ内容:
## エラー検知通知
|項目|値|
|---|---|
|検知日時|2026-06-27 09:52:04 UTC|
|ロググループ|/aws/lambda/monitored-app|
|ログストリーム|2026/06/27/[$LATEST]xxxx|
|エラー件数|3 件|
## エラーログ(最大10件)
[ERROR] 2026-06-27T09:52:04.523Z xxxx Database connection failed: timeout after 30s
[ERROR] 2026-06-27T09:52:04.523Z xxxx NullPointerException at module.process_data line 42
[ERROR] Exception: Application error: critical failure
両方式の比較
| 観点 | 方式① Alarm + SNS | 方式② Subscription Filter + Lambda |
|---|---|---|
| 通知までの時間 | 約2分(集計1分+評価1分) | 約30秒 |
| エラーログの内容 | ❌ なし(アラームメタデータのみ) | ✅ あり(実際のエラーメッセージ) |
| 課題タイトル |
ALARM: "アラーム名" in リージョン(固定) |
自由に設定可能 |
| 優先度・担当者 | ❌ 設定不可 | ✅ 設定可能 |
| Lambda の必要性 | ❌ 不要 | ✅ 必要 |
| 実装コスト | 低 | 中 |
| ランニングコスト | 低(Lambda 実行なし) | 中(Lambda 実行課金) |
選択基準
シンプルに始めたい / Lambda 管理コストを避けたい
→ 方式①
※ コンソールで CloudWatch Logs を別途確認する運用が前提
エラーの詳細を課題に入れたい / 優先度・担当者を自動設定したい
→ 方式②
ハマりポイントまとめ
1. Metric Filter パターンは "[ERROR]" で指定する
Lambda ログの実際のフォーマットは次のとおりです。
\t[ERROR]\t2026-06-27T09:54:00Z\txxxxxx\tDatabase connection failed
構造化パターン [timestamp, requestId, level="ERROR", ...] ではマッチしません。キーワード検索 "[ERROR]" を使ってください。
2. SNS 確認メールが Backlog に課題として登録される
SNS にメールアドレスをサブスクライブすると AWS から確認メールが届きます。
Backlog の課題登録用アドレスはどんなメールも課題として登録するため、確認メールも課題になります。
これは避けられない動作なので、「最初の1件は確認メール由来の課題が作られる」ことを運用ルールとして周知しておくとよいでしょう。
3. Subscription Filter は1ロググループに1つのみ
1ロググループに設定できる Subscription Filter は1つだけです。
複数の通知先(Slack・Backlog など)に同時に送りたい場合は、通知 Lambda の中で複数の送信先を処理する設計にします。
4. 方式②は Lambda 実行のたびに課題が1件作られる
エラーが頻発すると課題が大量に作られるケースがあります。
対策として SQS バッファや重複チェック(同一ロググループの未完了課題を API で検索してスキップ)を実装することを検討してください。
まとめ
実際に Backlog へ連携して動作検証した結果をまとめます。
| 方式① | 方式② | |
|---|---|---|
| Backlog 課題の内容 | アラーム通知のみ(エラーログなし) | エラーログ本文込み |
| 通知までの時間 | 約2分 | 約30秒 |
| 一言で | シンプル・安い・情報が薄い | 柔軟・リッチ・管理コストあり |
Backlog でインシデント管理を完結させるなら方式②が適しています。 課題を見るだけでエラー内容が把握でき、対応スピードが上がります。
方式①は「まずアラートを受け取れればよい」という段階や、コスト・シンプルさを優先する場面で有効です。