CloudWatch Logs のエラーを自動で GitHub Issues に課題化する
ECS で稼働するWebアプリケーションのエラーログを自動的に GitHub Issues に報告する仕組みを構築しました。手動でログを監視する必要がなくなり、エラー発生時に即座にチームが認識・対応できるようになります。
背景
マルチテナントの業務システムを ECS Fargate 上で運用しています。アプリケーションは2つあり、それぞれ異なるフレームワークで構築されています。
| アプリ | フレームワーク | 用途 |
|---|---|---|
| web | Laravel (PHP) | 業務管理システム |
| api | Django (Python) | API サーバー |
これまで CloudWatch Logs にログは収集していたものの、エラーの検知は手動確認に頼っていました。500エラーや例外発生を見逃すリスクがあり、自動検知の仕組みが必要でした。
アーキテクチャ
Subscription Filter + Lambda + GitHub Issues API の構成を採用しました。
CloudWatch Logs (/ecs/{prefix}-ecs-{app})
└── Subscription Filter (エラーパターンマッチ)
└── Lambda Function (Docker/arm64, Python 3.12)
├── エラー解析 (HTTP 5xx, 例外, スタックトレース)
├── ±5秒のログコンテキスト取得
├── 既存 Open Issue 検索
└── 新規 Issue 作成 or 既存 Issue にコメント追加
この構成を選んだ理由
| 方式 | リアルタイム性 | 柔軟性 | コスト |
|---|---|---|---|
| Subscription Filter + Lambda (採用) | 高 | 高 | 中 |
| Metric Filter + Alarm + SNS | 中 (1分以上遅延) | 低 | 低 |
| CloudWatch Logs Insights (定期実行) | 低 | 高 | 低 |
Subscription Filter はログ出力時にほぼリアルタイムで Lambda を起動するため、エラー発生から数秒で Issue が作成されます。
実装のポイント
1. アプリごとにリポジトリを分離
エラーの報告先を Secrets Manager で管理し、アプリ名に応じてリポジトリを切り替えます。
{
"github": {
"token": "ghp_xxx",
"repos": {
"web": "myorg/web-app",
"api": "myorg/api-server"
}
}
}
Lambda ハンドラーでは、ログストリーム名からアプリ名を抽出してリポジトリを決定します。
def extract_app_name(log_group):
"""/ecs/myapp-stage-ecs-api -> api"""
parts = log_group.rsplit("-", 1)
return parts[-1] if parts else log_group
# Secrets Manager から repos マッピングを取得
repos = github_conf.get("repos", {})
github_repo = repos.get(app_name, github_conf.get("repo", ""))
2. PHP と Python の両方のエラーパターンに対応
Laravel (PHP) と Django (Python) でエラーの出力形式が異なるため、両方のパターンを検出します。
Subscription Filter のフィルタパターン (Laravel):
?"HTTP/1.1\" 5" ?"ERROR" ?"Traceback" ?"CRITICAL" ?"PHP Fatal" ?"production.ERROR"
Python コードでのパターンマッチ:
# HTTP 5xx
re.compile(r'HTTP/\d\.\d"\s+(5\d{2})')
# Python/Django
re.compile(r"\b(ERROR|CRITICAL|FATAL)\b")
re.compile(r"Traceback \(most recent call last\)")
# PHP/Laravel
re.compile(r"PHP Fatal error:", re.IGNORECASE)
re.compile(r"\]\s+\w+\.ERROR:") # Laravel ログ形式
3. スタックトレースのコンテキスト取得
Subscription Filter はフィルタに一致した行のみを Lambda に送信します。しかし、Python の Traceback や PHP のスタックトレースは複数行にまたがるため、エラーの全容がわかりません。
そこで、CloudWatch Logs API で エラー発生時刻の前後±5秒のログを取得し、連続するエラー行を1つのブロックにグルーピングします。
CONTEXT_BUFFER_MS = 5000 # 前後5秒
def fetch_log_context(log_group, log_stream, error_timestamps):
min_ts = min(error_timestamps) - CONTEXT_BUFFER_MS
max_ts = max(error_timestamps) + CONTEXT_BUFFER_MS
response = client.get_log_events(
logGroupName=log_group, logStreamName=log_stream,
startTime=min_ts, endTime=max_ts, limit=200,
)
return response.get("events", [])
4. 連続投稿の抑制 (クールダウン)
エラーが大量発生すると Issue へのコメントが洪水のように投稿されてしまいます。これを防ぐため、10分間のクールダウンを実装しています。
COOLDOWN_MINUTES = 10
def _is_within_cooldown(self, issue_number):
# 直近コメントの created_at を確認
last_time = datetime.fromisoformat(last_created.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
return (now - last_time) < timedelta(minutes=COOLDOWN_MINUTES)
5. Docker イメージベースの Lambda
エラー解析ロジックが複雑化することを見越して、Docker イメージベースの Lambda を採用しました。
- ローカルで
pytestによるユニットテスト実行が可能 - 依存パッケージの管理が容易
- ビルドスクリプトがテスト → ビルド → プッシュを自動化
FROM public.ecr.aws/lambda/python:3.12
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ${LAMBDA_TASK_ROOT}/app/
CMD ["app.handler.lambda_handler"]
Terraform モジュールの構成
modules/log_monitor/ として再利用可能なモジュールにまとめています。
modules/log_monitor/
├── main.tf # Subscription Filter
├── lambda.tf # Lambda 関数 (Docker Image/arm64)
├── iam.tf # IAM ロール・ポリシー
├── secrets.tf # Secrets Manager
├── repository.tf # ECR リポジトリ
├── logs.tf # Lambda 用 Log Group
├── locals.tf # フィルタ定義
├── variables.tf / outputs.tf
├── bin/build.bash # ビルドスクリプト
└── docker/ # Lambda ソースコード + テスト
環境ごとの設定は JSON ファイルで外出しします。
{
"apps": {
"web": {
"filter_pattern": "?\"HTTP/1.1\\\" 5\" ?\"ERROR\" ?\"PHP Fatal\" ?\"production.ERROR\"",
"labels": ["web"]
},
"api": {
"filter_pattern": "?\"HTTP/1.1\\\" 5\" ?\"ERROR\" ?\"Traceback\" ?\"CRITICAL\"",
"labels": ["api"]
}
}
}
GitHub Issue の出力例
Lambda が自動作成する Issue は以下のような形式です。
## Error Report - 2026-02-24 07:38:51 UTC
- **Environment**: stage
- **Log Group**: `/ecs/myapp-stage-ecs-api`
- **Log Stream**: `ecs/api/abc123`
- **Error Blocks**: 3
### [1] 2026-02-24 07:01:27 UTC - `application`
\```
2026-02-24 16:01:27.289 | ERROR | myapp.reports.utils - External API client error
\```
### [2] 2026-02-24 07:01:27 UTC - `5xx`
\```
10.0.12.81:1208 - "POST /api/v1/reports/generate/ HTTP/1.1" 500
\```
ラベル CloudWatchLog + アプリ名が付与され、同一ラベルの Open Issue が既にあればコメントとして追記されます。
デプロイ手順
3ステップでデプロイできます。
# 1. ECR リポジトリ作成 (初回のみ)
terraform apply -target=module.log_monitor.aws_ecr_repository.this
# 2. Docker イメージビルド・プッシュ (テスト実行含む)
modules/log_monitor/bin/build.bash stage --profile my-profile
# 3. Lambda・Subscription Filter 等の作成
terraform apply -target=module.log_monitor
# 4. Secrets Manager に認証情報設定 (初回のみ)
aws secretsmanager put-secret-value \
--secret-id myapp-stage-log-monitor \
--secret-string '{"github":{"token":"ghp_xxx","repos":{...}}}'
Sentry との比較
エラー監視の SaaS として広く使われている Sentry と、本構成(CloudWatch + Lambda + GitHub Issues)を比較します。
本構成の強み
| 強み | 詳細 |
|---|---|
| GitHub Issues との直接統合 | エラーがそのまま開発チームのワークフロー(Issue → PR → Close)に乗る |
| AWS に閉じている | 外部 SaaS への依存がなく、エラー情報が AWS 外に出ない |
| コスト透明性 | Lambda 実行回数ベースで、低〜中トラフィック環境ならほぼ無料 |
| カスタマイズ自由 | フィルタパターン、クールダウン時間、コンテキスト取得範囲を自由に調整可能 |
Sentry が優れている領域
一方で、Sentry には本構成で再現が難しい、または自作すると大きなコストがかかる機能があります。
1. エラーのグルーピングとデデュプリケーション
本構成では「同一ラベルの Open Issue があればコメント追記 + 10分クールダウン」で重複を抑制していますが、異なる原因のエラーが1つの Issue に混在する可能性があります。Sentry はスタックトレースの構造を解析し、同じ原因のエラーを自動的に1つの Issue にまとめます。発生回数・影響ユーザー数のカウントも自動です。
2. ソースコードレベルのコンテキスト
本構成では ±5秒のログからエラーブロックを構成しますが、Sentry はスタックトレースの各フレームに対して該当するソースコードの前後数行を表示します。リリースバージョンと紐づけて「どのデプロイで混入したか」も追跡可能です。
3. パフォーマンスモニタリング(トレーシング)
本構成はエラー(5xx、例外)のみを検知しますが、Sentry Performance はリクエスト全体のトレースを記録し、遅いクエリ、遅い外部 API 呼び出し、N+1 クエリを自動検出します。エラーにならないが遅い処理(P95 レイテンシの劣化等)も可視化できます。これを自前で実装するには OpenTelemetry + Jaeger/Tempo 等の分散トレーシング基盤が必要になり、構築コストが大きくなります。
4. リリースとの紐づけ
「このエラーはリリース v1.2.3 で初めて発生した」「このリリースで regression が起きている」をダッシュボードで確認でき、コミットとの紐づけにより「誰のどのコミットが原因か」まで追跡可能です。本構成ではログの timestamp から推測するしかありません。
5. ユーザーコンテキスト
Sentry SDK はエラー発生時に「どのユーザーが」「どのブラウザ/OS で」「どのページで」操作していたかを自動記録します。マルチテナント環境では「どのテナントに影響しているか」の把握が重要ですが、ログのパースだけでは限界があります。
6. アラートの柔軟性
「過去1時間で同じエラーが50回以上発生したら Slack に通知」「新規エラーのみ通知」「特定のエラーは無視」といったルールを GUI で設定可能です。本構成では通知の粒度を変えるには Lambda コードの修正が必要です。
Sentry のデメリット
| 観点 | 詳細 |
|---|---|
| コスト | Team プランは月 $26〜。エラー量が増えるとイベント数課金が嵩む |
| データの外部送出 | エラー情報(スタックトレース、リクエストデータ)が Sentry のサーバーに送信される。セキュリティポリシーによっては不可(セルフホスト版もあるが運用負荷が大きい) |
| 監視ツールの分散 | CloudWatch と Sentry の両方を見る必要があり、チームが確認する場所が増える |
使い分けの指針
両者を置き換える関係ではなく、役割を分けるのが現実的です。
| 役割 | ツール |
|---|---|
| インフラ・ログレベルの異常検知、Issue 化 | 本構成(CloudWatch + Lambda + GitHub Issues) |
| アプリケーションレベルのエラー分析・パフォーマンス | Sentry(導入する場合) |
Sentry の導入が特に効果的になるのは以下のような状況です。
- エラーの種類が多すぎて GitHub Issues では追いきれなくなった
- 「遅いがエラーにはならない」パフォーマンス問題を追いたい
- リリース頻度が高く、どのデプロイで regression が起きたかを素早く特定したい
- 影響ユーザー数をテナント単位で把握したい
MVP フェーズやチーム規模が小さいうちは、本構成だけで十分にカバーできます。
まとめ
- Subscription Filter + Lambda でセミリアルタイムのエラー検知を実現
- アプリごとにリポジトリを分離し、開発チームが自然に Issue を確認できる
- PHP/Laravel と Python/Django の両方のエラーパターンに対応
- スタックトレースのコンテキスト取得でエラーの全容を報告
- 10分クールダウンで Issue の洪水を防止
- Docker Lambda + pytest でテスト容易性を確保
- Terraform モジュール化で複数環境への展開が容易