0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Terraform】AWS LambdaにCloudWatchログ管理を導入した記録

0
Last updated at Posted at 2026-10-05

はじめに

サーフィンの波情報を通知する Wave アプリ(WavePilot)に、CloudWatch のログ管理と pino による構造化ログを導入した記録です。
Terraform の書き方やハマったエラーも含めて残しておきます。


WavePilot とは

サーファー向けの波情報通知アプリです。

  • お気に入りのサーフスポットを登録すると、毎日指定時刻に LINE で波情報を通知
  • Stormglass API から波高・風速・風向きを取得
  • フロントエンドは React、バックエンドは Node.js (Express) を Lambda で動作

アプリの全体像や作った経緯は、こちらの記事にまとめています。
https://qiita.com/mugicha0141/items/84b9a45daf68d5bdaa27

技術スタック

項目 内容
フロントエンド React → S3 + CloudFront
バックエンド Node.js (Express) → Lambda
DB DynamoDB
通知 LINE Messaging API + EventBridge Scheduler
IaC Terraform

なぜ CloudWatch 管理を導入したか

Lambda は実行するだけで自動的に CloudWatch Logs にログを送ってくれます。
ただし、何も設定しないと以下の問題があります。

  • ロググループの保持期間が無期限になる
    → ログが溜まり続けてコストが増える
  • ロググループが Terraform の管理外で作られる
    → terraform destroy してもログが残り続ける
  • console.log の文字列羅列でログが検索しにくい

console.log とは
JavaScript でログにメッセージを出力する命令です。
Lambda では console.log の出力内容がそのまま CloudWatch Logs に送られます。
実行時の状況を後から追うための、ほぼ唯一の手がかりになります。

今回は「ロググループの保持期間設定と Terraform 管理化」に加えて、console.log を JSON 形式に変えて検索しやすくする構造化ログ(pino の導入)に取り組みました。


追加した Terraform コード

1. CloudWatch Log Groups の定義

terraform/main.tf に追記しました。

# ── CloudWatch Log Groups ─────────────────────────────────────────
resource "aws_cloudwatch_log_group" "wave_app_backend" {
  count             = var.environment == "prod" ? 1 : 0
  name              = "/aws/lambda/wave-app-backend"
  retention_in_days = 14

  tags = {
    Environment = var.environment
  }
}

resource "aws_cloudwatch_log_group" "wave_notify" {
  count             = var.environment == "prod" ? 1 : 0
  name              = "/aws/lambda/wave-notify"
  retention_in_days = 14

  tags = {
    Environment = var.environment
  }
}

ポイント解説

項目 内容
count = var.environment == "prod" ? 1 : 0 本番のみ作成。ローカル(LocalStack)では不要なので作らない
name = "/aws/lambda/wave-app-backend" Lambda のロググループ名は /aws/lambda/関数名 が AWS のデフォルトルール
retention_in_days = 14 14日より古いログは自動削除。コスト削減のため必ず設定する

2. Lambda に logging_config を追加

各 Lambda 関数のリソースブロック内に追記しました。

resource "aws_lambda_function" "wave_app_backend" {
  # ... 既存の設定 ...

  dynamic "logging_config" {
    for_each = var.environment == "prod" ? [1] : []
    content {
      log_format = "Text"
      log_group  = aws_cloudwatch_log_group.wave_app_backend[0].name
    }
  }

  depends_on = [aws_cloudwatch_log_group.wave_app_backend]
}

wave_notify Lambda も同様に追記しました。

dynamic ブロックとは

Terraform でブロックを条件付きで生成する構文です。
通常の if 文のような書き方はブロックに対してできないため、dynamic を使います。

dynamic "logging_config" {
  for_each = var.environment == "prod" ? [1] : []
  # for_each が [1](要素1つ)→ ブロックが1回生成される
  # for_each が [](空)      → ブロックが生成されない
  content {
    # 実際に生成されるブロックの中身
  }
}

log_format = "Text" にした理由

今回は従来どおりの "Text" のままにしました。
今回導入した pino は標準出力に JSON を直接書き出すため、"Text" のままでも CloudWatch Logs Insights が JSON のフィールドを自動認識してくれます。

depends_on を付けた理由

logging_config 内でロググループを参照しているため、Lambda より先にロググループを作る必要があります。
depends_on で作成順序を明示的に指定しています。


ハマったエラー

terraform apply を実行したところ、以下のエラーが出ました。

Error: creating CloudWatch Logs Log Group (/aws/lambda/wave-app-backend):
ResourceAlreadyExistsException: The specified log group already exists

原因

Lambda は初回実行時に自動でロググループを作ります。
このプロジェクトはすでに本番稼働していたため、最初に Lambda が動いた時点で AWS が自動作成していました。

Lambda 初回実行
    ↓
AWS が自動で作成
    /aws/lambda/wave-app-backend  (保持期間:無期限)
    /aws/lambda/wave-notify       (保持期間:無期限)

これらは Terraform の管理外で作られたため、terraform destroy を実行しても削除されずに残り続けていました。

対処法

古いログは不要だったので、AWS CLI で直接削除してから再度 apply しました。

aws logs delete-log-group \
  --log-group-name "/aws/lambda/wave-app-backend" \
  --region ap-northeast-1

aws logs delete-log-group \
  --log-group-name "/aws/lambda/wave-notify" \
  --region ap-northeast-1

削除後に再度デプロイしました。

./deploy.sh --env prod --infra

今度は「新規作成」として処理され、保持期間14日でロググループが作られました。
image.png

注意
削除から apply までの間に Lambda が実行されると、AWS がロググループを再作成して同じエラーになります。
本番で試す場合は、実行が少ない時間帯に素早く行うか、下の import を使ってください。

補足:古いログを残したい場合
削除せず、既存のロググループを Terraform の管理下に取り込む import という方法もあります。

terraform import \
  'aws_cloudwatch_log_group.wave_app_backend[0]' \
  '/aws/lambda/wave-app-backend'

確認方法

AWS コンソール → CloudWatch → ロググループ で、以下が確認できれば成功です。

  • /aws/lambda/wave-app-backend が保持期間「14日」で存在する
  • /aws/lambda/wave-notify が保持期間「14日」で存在する

pino の導入

console.log の文字列ログのままでは CloudWatch Logs Insights で検索しにくいため、
Node.js の代表的なロガーライブラリ pino を導入して JSON 形式(構造化ログ)に変えました。

インストール

npm install pino

使い方

const pino = require("pino");
const logger = pino();

// info ログ
logger.info({ method: req.method, path: req.path }, "Request received");

// エラーログ
logger.error({ error: err.message }, "Stormglass API error");

出力される JSON:

{"level":30,"time":1234567890,"pid":1,"msg":"Request received","method":"GET","path":"/api/wave-data"}

ログレベル

pino のレベルは数値で管理されています。

レベル 数値 用途
trace 10 詳細なデバッグ情報
debug 20 デバッグ情報
info 30 通常の処理(リクエスト受信、処理完了など)
warn 40 警告(処理はできたが注意が必要な状態)
error 50 エラー(処理に失敗した)
fatal 60 致命的なエラー

追加したログ一覧

場所 レベル 内容
全エンドポイント共通 info リクエスト受信(ミドルウェアで一括管理)
LINE webhook info 署名チェック・検証結果・イベント数
LINE アカウント連携 info 連携完了
ログイン info/error 試行・成功・エラー
波情報取得 info/error リクエスト受信・Stormglass エラー
お気に入り error 登録・取得・更新・削除エラー
EventBridge スケジュール info 作成・更新・削除
サーバー起動 info 起動完了(ローカル開発時)

CloudWatch でログを確認

デプロイ後にアプリを操作(ログイン → 地図から波情報取得 → お気に入り登録)したところ、
CloudWatch Logs にログが出力されていることを確認しました。

確認場所:AWS コンソール → CloudWatch → ロググループ → /aws/lambda/wave-app-backend

ログの種類

ロググループを開くとログストリームが並んでいます。中には2種類のログが混在しています。

Lambda が自動で出力するログ(Text 形式では消せません):

START RequestId: abc123 Version: $LATEST
END RequestId: abc123
REPORT RequestId: abc123 Duration: 123ms Billed Duration: 200ms Memory Size: 128MB

pino が出力する構造化ログ:

{"level":30,"time":1234567890,"msg":"Request received","method":"POST","path":"/login"}
{"level":30,"time":1234567890,"msg":"Login successful","userId":"1"}
{"level":30,"time":1234567890,"msg":"Request received","method":"GET","path":"/api/wave-data"}
{"level":30,"time":1234567890,"msg":"Wave data request","lat":"35.6","lng":"139.7"}

ログストリームが2つ表示される理由

/aws/lambda/wave-app-backend の中に、2つのログストリームが作られていました。

image.png

ログストリームは Lambda のインスタンスごとに1つ作られます。

アプリを開いた際にブラウザが複数のリクエストを同時に送ると(波情報取得・お気に入り一覧取得など)、Lambda が複数インスタンスを起動して並列処理します。これがログストリームが2つになった理由です。

リクエスト① → Lambda インスタンス A が起動 → ログストリーム1
リクエスト② → Lambda インスタンス B が起動 → ログストリーム2

使い続けるとストリームはどんどん増えますが、正常な動作です。

Lambda は常時起動していない
Lambda はリクエストが来たときに実行環境が用意され、使われない時間が続くと環境は破棄されます。
使った分だけ課金される仕組みなので、軽量アプリには非常に向いています。


Logs Insights でまとめて分析

ロググループを直接見ると「どのストリームを見ればいいかわからない」問題が起きます。
Logs Insights を使えば全ストリームをまとめて検索・分析できます。

確認場所:CloudWatch → ログ分析

ロググループで /aws/lambda/wave-app-backend を選択してクエリを実行します。

クエリの基本構文

コマンド 役割
fields 表示するフィールドを指定
filter 条件で絞り込む
stats 集計する
sort 並び替え
limit 表示件数を制限

よく使うクエリ例

pino のログだけを、新しい順に50件表示する

fields @timestamp, msg, method, path, error
| filter ispresent(msg)
| sort @timestamp desc
| limit 50

filter ispresent(msg) で、Lambda が自動で出力する START・END・REPORT の行を除外しています。

pino のログだけが新しい順に表示され、リクエストの内容を確認できました。
image.png

エラーだけ絞り込む

fields @timestamp, msg, error
| filter level = 50
| sort @timestamp desc

pino のレベルは数値
Logs Insights で filter level = "ERROR" と書いても動きません。
pino は info = 30、warn = 40、error = 50 の数値で管理しているため、filter level = 50 と書きます。

エンドポイント別のアクセス数を集計する

fields path
| filter msg = "Request received"
| stats count() by path

まとめ

やったこと 効果
CloudWatch Log Groups を Terraform で定義 terraform destroy でログも一緒に消えるようになった
retention_in_days = 14 を設定 古いログが自動削除されてコスト増を防げる
logging_config で Lambda と紐付け Lambda が使うロググループを Terraform 管理のものに固定できた
pino で構造化ログを導入 CloudWatch Logs Insights でクエリ分析できるようになった

Lambda を使っている場合はロググループの保持期間設定と構造化ログを最初から入れておくことを強くおすすめします。


次回の課題

現在はログを確認・分析できる状態になりましたが、エラーが起きても気づけない状態です。

次回は CloudWatch Alarms を設定して、エラーが一定数を超えたときに通知が来るようにする予定です。通知先は、普段使っている LINE を予定しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?