CloudWatchアラーム経由でCPU温度をLINEに通知してみた。

CloudWatchアラーム経由でCPU温度をLINEに通知してみた。

Clock Icon2025.06.07

はじめに

皆様こんにちは、あかいけです。
突然ですが、皆さんのおうちにサーバーはありますか?

おうちでサーバーを 24 時間動かしていると、火災やパーツ劣化の不安がつきまといます。
特に長時間の高温は、ハードウェア寿命を縮めるだけでなく、サーマルスロットリングでパフォーマンス低下を招きかねません。

しかし実際のところ基本的にサーバーは燃えないです、
それこそ電源含めすべてをジャンクパーツで組んだり、ノートPCをサーバーとして24時間付けっぱなしにしなければ、まず燃えることはないでしょう。
とはいえ、ふとした瞬間に不安になるタイミングがある気がします。
(私は外出中の三時間に一回ぐらいは不安になります)

というわけでそんな不安を取り除くために、
今回は CloudWatch アラーム経由で CPU 温度を LINE に通知してみました。

構成図

構成は以下の通りで、CloudWatch Agent で CPU 温度をメトリクスとして転送し、
CloudWatch Alarm でアラーム状態になったら Lambda に連携して、LINE Messaging API を利用して LINE に通知しています。

temp-alarm.drawio

事前準備

Terraformで作成できる部分は後でまとめて作成しますが、
以下は手動で作成するため、事前に準備する必要があります。

  • おうちサーバー側の設定
  • LINE Messaging API の設定

CloudWatch Agent

まずおうちサーバーのメトリクスを CloudWatch に送信するために、
CloudWatch Agent をインストールする必要があります。
具体的な方法は以下の記事にまとめているので、よければこちらをご参照ください。

https://dev.classmethod.jp/articles/aws-at-home-cloudwatch/

次に CPU 温度はデフォルトでメトリクスとして用意されていないため、
StatsD を利用してカスタムメトリクスとして取得します。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-custom-metrics-statsd.html

また StatsD は Cloud Agent のパッケージに含まれています。
以下の re:Post を見た限りだと、バージョン 1.203420.0 から含まれるようになったみたいです。
https://repost.aws/questions/QUQ8baDzRzSvKDJB21PAFKvA/cloudwatch-agent-error-additional-property-statsd-is-not-allowed?utm_source=chatgpt.com

念のため、以下のコマンドでバージョンを確認しておきましょう。

install
$ cat /opt/aws/amazon-cloudwatch-agent/bin/CWAGENT_VERSION

1.300056.0b1123

今回は CPU 温度だけ取得できればいいので、CloudWatch Agent は以下の設定にしておきます。
取得したメトリクスの 60 秒間の平均値を、60 秒ごとに CloudWatch メトリクスに送信しています。

amazon-cloudwatch-agent.json
{
  "agent": {
    "metrics_collection_interval": 60,
    "run_as_user": "root"
  },
  "metrics": {
    "metrics_collected": {
      "statsd": {
        "service_address": ":8125",
        "metrics_collection_interval": 60,
        "metrics_aggregation_interval": 60
      }
    }
  }
}

CPU 温度 取得

CPU の温度を知る方法は色々ありますが、
今回はデフォルトで利用できる温度センサーを使ってみます。

まず /sys/class/thermal/thermal_zone* に各種温度センサーのファイルがあります。
このファイルの数や内容は、ハードウェア側の構成によってまちまちです。

$ ls -la /sys/class/thermal/thermal_zone*

lrwxrwxrwx 1 root root 0 Jun  7 01:23 /sys/class/thermal/thermal_zone0 -> ../../devices/virtual/thermal/thermal_zone0
lrwxrwxrwx 1 root root 0 Jun  7 01:23 /sys/class/thermal/thermal_zone1 -> ../../devices/virtual/thermal/thermal_zone1
lrwxrwxrwx 1 root root 0 Jun  7 01:23 /sys/class/thermal/thermal_zone2 -> ../../devices/virtual/thermal/thermal_zone2
lrwxrwxrwx 1 root root 0 Jun  4 02:00 /sys/class/thermal/thermal_zone3 -> ../../devices/virtual/thermal/thermal_zone3
lrwxrwxrwx 1 root root 0 Jun  7 01:23 /sys/class/thermal/thermal_zone4 -> ../../devices/virtual/thermal/thermal_zone4

また末尾の番号ごとに温度の対象が異なっており、/sys/class/thermal/thermal_zone*/type に何を表しているか書いてあります。

一般的に x86_pkg_temp が Intel CPU の温度を表しているため、
以下であれば /sys/class/thermal/thermal_zone4/temp が確認対象となります。

$ cat /sys/class/thermal/thermal_zone*/type

acpitz
INT3400 Thermal
SEN4
TCPU
x86_pkg_temp

実際に確認すると以下の通りで、
温度はミリ度(1/1000℃)単位で出力されているため、 43℃ という事になります。

$ cat /sys/class/thermal/thermal_zone4/temp

43000

このファイルを 10 秒ごとに参照して、StatD に送信するスクリプトを作成して、実行権限を付けておきます。

/opt/scripts/emit_cpu_temp_statsd.sh
#!/bin/bash

ZONE="thermal_zone4"
THERMAL_DIR="/sys/class/thermal"

while true; do
  if [ -r "${THERMAL_DIR}/${ZONE}/temp" ]; then
    RAW=$(cat "${THERMAL_DIR}/${ZONE}/temp")

	# RAW は「ミリ℃」なので、1000 で割って小数を出す
    # 例: RAW=45312 → CPU_C=45.3
    CPU_INT=$(( RAW / 1000 ))
    CPU_DEC=$(( (RAW % 1000) / 100 ))
    CPU_C="${CPU_INT}.${CPU_DEC}"

    # StatsD 形式で送信 (gauge => |g)
    # 例: cpu_temp:45.3|g
    printf "cpu_temp:%s|g" "${CPU_C}" | nc -u -w0 127.0.0.1 8125
  fi
  sleep 10
done
sudo chmod +x /opt/scripts/emit_cpu_temp_statsd.sh

このスクリプトは定期実行したいので、サービス化して常駐するようにしておきます。

/etc/systemd/system/emit_cpu_temp.service
[Unit]
Description=Emit CPU Temperature to StatsD
After=network.target

[Service]
Type=simple
ExecStart=/opt/scripts/emit_cpu_temp_statsd.sh
Restart=always
User=root

[Install]
WantedBy=multi-user.target

あとは作ったサービスを起動します。

sudo systemctl daemon-reload;
sudo systemctl enable emit_cpu_temp.service;
sudo systemctl start emit_cpu_temp.service;

ステータスを見て、問題なく起動されていれば OK です。

$ sudo systemctl status emit_cpu_temp.service

● emit_cpu_temp.service - Emit CPU Temperature to StatsD
     Loaded: loaded (/etc/systemd/system/emit_cpu_temp.service; enabled; preset: enabled)
     Active: active (running) since Sat 2025-06-07 14:57:43 JST; 26s ago
   Main PID: 647445 (emit_cpu_temp_s)
      Tasks: 2 (limit: 9244)
     Memory: 612.0K (peak: 1.2M)
        CPU: 37ms
     CGroup: /system.slice/emit_cpu_temp.service
             ├─647445 /bin/bash /opt/scripts/emit_cpu_temp_statsd.sh
             └─647679 sleep 10

Jun 07 14:57:43 master-01 systemd[1]: Started emit_cpu_temp.service - Emit CPU Temperature to StatsD.

数分後には CloudWatch 上でメトリクスが確認できるようになります。

スクリーンショット 2025-06-07 150453

LINE Messaging API

次に LINE にメッセージを送る方法ですが、
今回は LINE Messaging API を利用します。

https://developers.line.biz/ja/services/messaging-api/

LINE Messaging API 利用料金はプランごとに異なり、今回は無料枠 (コミュニケーションプラン) を利用します。
この無料枠では月に 200 通までメッセージを送れます。

https://developers.line.biz/ja/docs/messaging-api/pricing/

まず LINE Developers に Line Business ID を作成してログインします。
LINE アカウントがあれば誰でも作れます。

https://developers.line.biz/ja/

次にプロバイダーを作成する必要があります。
名前は好きなものを入力してください。

スクリーンショット 2025-06-07 150753

スクリーンショット 2025-06-07 150849

次に Messaging API チャンネルを作りたいですが、

スクリーンショット 2025-06-07 150940

そのためには LINE 公式アカウントを作る必要があるので、作成します。

スクリーンショット 2025-06-07 150940

ここで認証を求められるので、対応します。

スクリーンショット 2025-06-07 151112

色々設定を進めて作成します。

entry.line.biz_form_entry_unverified

entry.line.biz_form_entry_unverified (1)

entry.line.biz_form_entry_unverified (2)

LINE 公式アカウントを利用するにあたり同意を求められるので、適当に同意します。

manager.line.biz_account_@730hjplb_notices_2_2017_Agreement_about_secrecy_of_communications_JP

manager.line.biz_account_@730hjplb_notices_3_2022_Agreement_about_ZHD_data_usage_JP

ここまで終わると LINE 公式アカウントを作成されます。
アイコンは適当に好きなものを使ってください。 (私はいらすとやの CPU のイラストにしました)

右上の設定を押して、

スクリーンショット 2025-06-07 151914

Messaging API の画面から作成します。

スクリーンショット 2025-06-07 151947

プロバイダーは最初に作成したものを選択して、

スクリーンショット 2025-06-07 202022

今回は個人利用なので、プライベートポリシーと利用規約は特に設定しません。

スクリーンショット 2025-06-07 152036

これでようやく Messaging API チャンネルが作成されました。 (長かった…)

スクリーンショット 2025-06-07 152154

Lambda 側で使用する認証情報として、以下が必要となるのでメモしておきます。

  • ユーザーID
    • LINE Developers のチャンネル基本設定にて確認
  • チャンネルアクセストークン
    • LINE Developers の Messaging API 設定にて発行

developers.line.biz_console_channel_2007538925

developers.line.biz_console_channel_2007538925 (1)

メッセージがちゃんと送信されるかは、以下のコマンドでテストできます。
CHANNEL_ACCESS_TOKENUSER_ID は先ほど取得したものに置き換えてください。

curl -v -X POST https://api.line.me/v2/bot/message/push \
  -H "Authorization: Bearer CHANNEL_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "USER_ID",
    "messages":[{"type":"text","text":"test message"}]
  }'

CloudWatch Alarm / Lambda 作成

あとは Terraform で以下のリソースを作成します。

  • CloudWatch Alarm
  • Lambda

以下のファイルを作成して、terraform apply します。

LINE_ACCESS_TOKEN は LINE Developers で取得したチャンネルアクセストークン、
LINE_USER_IDはLINE Developers のユーザーIDに置き換えてください。

provider.tf
terraform {
  required_version = ">= 1.10.0"

  required_providers {
    aws = ">= 5.0.0"
  }
}

provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
      env = "terraform"
      app = "cpu-temperature-notify"
    }
  }
}

locals {
  region      = "ap-northeast-1"
  app_name    = "line-bot"
}
lambda.tf
locals {
  handler_name           = "index.handler"
  architectures          = ["x86_64"]
  ephemeral_storage_size = 512
  memory_size            = 128
  runtime                = "nodejs22.x"
  source_path            = "./src/index.mjs"
  timeout                = 10
  environment_variables = {
    LINE_ACCESS_TOKEN = var.LINE_ACCESS_TOKEN
    LINE_USER_ID      = var.LINE_USER_ID
  }
}

# Line credential
variable "LINE_ACCESS_TOKEN" {}
variable "LINE_USER_ID" {}

data "aws_caller_identity" "current" {}

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = local.source_path
  output_path = "${path.module}/lambda_function.zip"
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "iam_for_lambda" {
  name               = "iam_for_lambda"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy" "lambda_policy" {
  name = "${local.app_name}-lambda-policy"
  role = aws_iam_role.iam_for_lambda.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:*"
      }
    ]
  })
}

resource "aws_lambda_function" "main" {

  function_name          = "${local.app_name}-lambda"
  description            = "${local.app_name}-lambda"
  filename               = data.archive_file.lambda_zip.output_path
  handler                = local.handler_name
  architectures          = local.architectures
  memory_size            = local.memory_size
  role                   = aws_iam_role.iam_for_lambda.arn
  environment {
    variables = local.environment_variables
  }

  runtime     = local.runtime
  timeout     = local.timeout
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  tags = {
    Name = "${local.app_name}-lambda"
  }
}

resource "aws_lambda_permission" "allow_cloudwatch" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  principal     = "lambda.alarms.cloudwatch.amazonaws.com"
}
alarm.tf
# 対象のホスト名を格納
variable "hosts" {
  default = ["server-01", "server-02", "server-03", "server-04", "server-05", "server-06"]
}

resource "aws_cloudwatch_metric_alarm" "mem_used_alarm" {
  for_each = toset(var.hosts)

  alarm_name          = "cpu_temp_${each.key}"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = "cpu_temp"
  namespace           = "CWAgent"
  period              = 60
  statistic           = "Average"
  threshold           = 80
  alarm_description   = "CPU temperature alarm for ${each.key}"
  alarm_actions       = [aws_lambda_function.main.arn]
  dimensions = {
    host = each.key
    metric_type = "gauge"
  }
}
src/index.mjs
import https from 'https';

const LINE_ACCESS_TOKEN = process.env.LINE_ACCESS_TOKEN;
const LINE_USER_ID      = process.env.LINE_USER_ID;

export async function handler(event) {
  console.log('Received event:', JSON.stringify(event, null, 2));

  const d = event.alarmData;
  const AlarmName      = d.alarmName;
  const NewStateValue  = d.state.value;
  const NewStateReason = d.state.reason;
  const StateChangeTime= d.state.timestamp;

  let temperatureText = '';
  try {
    const rd = JSON.parse(d.state.reasonData);
    const temp = rd.recentDatapoints?.[0];
    if (typeof temp === 'number') {
      temperatureText = `現在の温度: ${temp}℃`;
    }
  } catch (e) {
    console.warn('reasonData parsing failed:', e);
  }

  const text = [
    `🌡️ CPUがアチアチです!`,
    temperatureText,
    `▶ アラーム名: ${AlarmName}`,
    `▶ 状態    : ${NewStateValue}`,
    `▶ 理由    : ${NewStateReason}`,
    `▶ 発生時刻: ${StateChangeTime}`
  ]
  .filter(line => line)
  .join('\n');

  const body = JSON.stringify({
    to: LINE_USER_ID,
    messages: [{ type: 'text', text }]
  });

  const options = {
    hostname: 'api.line.me',
    path:     '/v2/bot/message/push',
    method:   'POST',
    headers: {
      'Content-Type':  'application/json',
      'Authorization': `Bearer ${LINE_ACCESS_TOKEN}`
    }
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, res => {
      let resp = '';
      res.on('data', chunk => resp += chunk);
      res.on('end', () => {
        console.log('LINE API response:', res.statusCode, resp);
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve({ statusCode: res.statusCode, body: resp });
        } else {
          reject(new Error(`LINE API error ${res.statusCode}: ${resp}`));
        }
      });
    });

    req.on('error', err => {
      console.error('Request error:', err);
      reject(err);
    });

    req.write(body);
    req.end();
  });
}
src/package.json
{
    "name": "cw-alarm-to-line",
    "version": "1.0.0",
    "type": "module",
    "main": "index.mjs"
}

リソース作成が完了すると、
以下のようにメトリクスに対してアラームが作成されます。

スクリーンショット 2025-06-07 161030

テストとして、閾値を 40 に下げてみます。

スクリーンショット 2025-06-07 161319

ちゃんとメッセージが来ています。
これで旅先でおうちサーバーの心配をする必要がなくなりましたね!
(逆に連絡がきたときはとても不安になりそうです)

line_demo

さいごに

以上、CloudWatch アラーム経由で CPU 温度を LINE に通知する方法でした。

今回は個人的な理由で CPU 温度を通知しましたが、
カスタムメトリクスであれば実質なんでも CloudWatch メトリクスとして配信できるため、使い道は色々ありそうです。

CloudWatchは個人利用であれば、ほぼ料金がかからず済むことが大半だと思うので、
ぜひ皆さんもおうちサーバーの体調を把握するために利用してみてください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.