CloudWatchアラーム経由でCPU温度をLINEに通知してみた。
はじめに
皆様こんにちは、あかいけです。
突然ですが、皆さんのおうちにサーバーはありますか?
おうちでサーバーを 24 時間動かしていると、火災やパーツ劣化の不安がつきまといます。
特に長時間の高温は、ハードウェア寿命を縮めるだけでなく、サーマルスロットリングでパフォーマンス低下を招きかねません。
しかし実際のところ基本的にサーバーは燃えないです、
それこそ電源含めすべてをジャンクパーツで組んだり、ノートPCをサーバーとして24時間付けっぱなしにしなければ、まず燃えることはないでしょう。
とはいえ、ふとした瞬間に不安になるタイミングがある気がします。
(私は外出中の三時間に一回ぐらいは不安になります)
というわけでそんな不安を取り除くために、
今回は CloudWatch アラーム経由で CPU 温度を LINE に通知してみました。
構成図
構成は以下の通りで、CloudWatch Agent で CPU 温度をメトリクスとして転送し、
CloudWatch Alarm でアラーム状態になったら Lambda に連携して、LINE Messaging API を利用して LINE に通知しています。
事前準備
Terraformで作成できる部分は後でまとめて作成しますが、
以下は手動で作成するため、事前に準備する必要があります。
- おうちサーバー側の設定
- LINE Messaging API の設定
CloudWatch Agent
まずおうちサーバーのメトリクスを CloudWatch に送信するために、
CloudWatch Agent をインストールする必要があります。
具体的な方法は以下の記事にまとめているので、よければこちらをご参照ください。
次に CPU 温度はデフォルトでメトリクスとして用意されていないため、
StatsD を利用してカスタムメトリクスとして取得します。
また StatsD は Cloud Agent のパッケージに含まれています。
以下の re:Post を見た限りだと、バージョン 1.203420.0 から含まれるようになったみたいです。
念のため、以下のコマンドでバージョンを確認しておきましょう。
$ cat /opt/aws/amazon-cloudwatch-agent/bin/CWAGENT_VERSION
1.300056.0b1123
今回は CPU 温度だけ取得できればいいので、CloudWatch Agent は以下の設定にしておきます。
取得したメトリクスの 60 秒間の平均値を、60 秒ごとに CloudWatch メトリクスに送信しています。
{
"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 に送信するスクリプトを作成して、実行権限を付けておきます。
#!/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
このスクリプトは定期実行したいので、サービス化して常駐するようにしておきます。
[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 上でメトリクスが確認できるようになります。
LINE Messaging API
次に LINE にメッセージを送る方法ですが、
今回は LINE Messaging API を利用します。
LINE Messaging API 利用料金はプランごとに異なり、今回は無料枠 (コミュニケーションプラン) を利用します。
この無料枠では月に 200 通までメッセージを送れます。
まず LINE Developers に Line Business ID を作成してログインします。
LINE アカウントがあれば誰でも作れます。
次にプロバイダーを作成する必要があります。
名前は好きなものを入力してください。
次に Messaging API チャンネルを作りたいですが、
そのためには LINE 公式アカウントを作る必要があるので、作成します。
ここで認証を求められるので、対応します。
色々設定を進めて作成します。
LINE 公式アカウントを利用するにあたり同意を求められるので、適当に同意します。
ここまで終わると LINE 公式アカウントを作成されます。
アイコンは適当に好きなものを使ってください。 (私はいらすとやの CPU のイラストにしました)
右上の設定を押して、
Messaging API の画面から作成します。
プロバイダーは最初に作成したものを選択して、
今回は個人利用なので、プライベートポリシーと利用規約は特に設定しません。
これでようやく Messaging API チャンネルが作成されました。 (長かった…)
Lambda 側で使用する認証情報として、以下が必要となるのでメモしておきます。
- ユーザーID
- LINE Developers のチャンネル基本設定にて確認
- チャンネルアクセストークン
- LINE Developers の Messaging API 設定にて発行
メッセージがちゃんと送信されるかは、以下のコマンドでテストできます。
CHANNEL_ACCESS_TOKEN
と USER_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に置き換えてください。
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"
}
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"
}
# 対象のホスト名を格納
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"
}
}
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();
});
}
{
"name": "cw-alarm-to-line",
"version": "1.0.0",
"type": "module",
"main": "index.mjs"
}
リソース作成が完了すると、
以下のようにメトリクスに対してアラームが作成されます。
テストとして、閾値を 40 に下げてみます。
ちゃんとメッセージが来ています。
これで旅先でおうちサーバーの心配をする必要がなくなりましたね!
(逆に連絡がきたときはとても不安になりそうです)
さいごに
以上、CloudWatch アラーム経由で CPU 温度を LINE に通知する方法でした。
今回は個人的な理由で CPU 温度を通知しましたが、
カスタムメトリクスであれば実質なんでも CloudWatch メトリクスとして配信できるため、使い道は色々ありそうです。
CloudWatchは個人利用であれば、ほぼ料金がかからず済むことが大半だと思うので、
ぜひ皆さんもおうちサーバーの体調を把握するために利用してみてください。