3778 字
19 分钟
阅读量
AWS Lightsail 流量监控与超标自动开关机实战(基于 Serverless 架构)

AWS Lightsail 提供了极具性价比的轻量云服务器套餐,并且每个套餐都自带了丰厚的月度网络传输配额(通常为 1TB ~ 5TB 出方向流量)。

然而,一旦实例遭受网络攻击、异常跑流量或业务突发导致流量超出月度配额,AWS 将按照每 GB 额外计费(通常为 0.09 0.09 ~ 0.12 / GB),极易产生高昂的账单风险。此外,Lightsail 原生监控仅提供简单的邮件提醒,无法在流量超标时自动关机止损,也无法在每月 1 号重置配额时自动拉起实例。

本文将分享一套经过生产验证的 AWS Serverless 全自动监控方案:无需在每台 VPS 安装任何探针或 Agent,完全利用 AWS 官方的原生服务(EventBridge + Lambda + SSM Parameter Store),实现全区域 Lightsail 实例流量巡检、超标自动关机、多渠道 Webhook/企业微信推送以及每月 1 号自动重置开机。


架构设计与拓扑#

整个系统采用 100% 无服务器(Serverless)架构,运行时仅按调用次数计费(每月在 AWS 免费额度内,完全 0 成本运行):

+------------------------+
| Amazon EventBridge | (建议每 5 分钟高频定时触发)
| (Cron / Rate Rule) |
+-----------+------------+
|
v
+-----------+------------+ +----------------------------+
| AWS Lambda 函数 | ----> | AWS Lightsail API |
| (Python 3.12 核心计算) | | (获取配额/统计指标/启停机) |
+-----------+------------+ +----------------------------+
|
+------------------> +----------------------------+
| | AWS SSM Parameter Store |
| | (持久化已停机/告警状态记录) |
| +----------------------------+
|
+------------------> +----------------------------+
| Webhook 消息网关 / 企微/SNS |
| (即时推送告警与汇总日报) |
+----------------------------+

核心设计亮点#

  1. 零受控端侵入(Agentless):完全基于 AWS IAM 鉴权通过云端 API 交互,不在目标 Lightsail 机器内部安装任何软件,机器重装、改系统亦不受任何影响;
  2. 当月新建机器配额智能折算:对于月中新建的机器,脚本会自动计算当月剩余有效天数比例并折算实际可用配额,彻底杜绝月中新建机器误触发超标;
  3. 出入双向流量精准聚合:自动按天聚合查询 NetworkInNetworkOut 数据点求和,精准比对当月总配额;
  4. 防重复打扰与幂等关机:关机后将实例写入 AWS SSM Parameter Store,对于已经处于停止状态的实例保持静默,不反复推送骚扰通知;
  5. 每月 1 号自动重置与唤醒:北京时间每月 1 号 00:00,自动清空历史告警记录;对于上月因超标被系统关机、且当月新流量 ≤ 1MB 的实例,自动执行开机恢复服务;
  6. 每月首尾日全量用量报表:在月初与月末自动汇总账号下所有 Lightsail 实例的流量消耗详情并推送全局报表;
  7. WAF 与 Cloudflare 防火墙兼容:内置标准浏览器 User-Agent,彻底避免 Python 默认 urllib 请求头被 Cloudflare WAF / 边缘防火墙拦截(如 Error 1010 / 403 Forbidden)。

接口与环境变量参数规范#

Lambda 函数所有配置均采用环境变量驱动,便于在 AWS 控制台灵活调整:

环境变量名类型默认值说明
AWS_REGIONStringap-southeast-1Lightsail 实例所在区域(与 Lambda 区域保持一致)
ALERT_THRESHOLDFloat0.95流量超标预警阈值(例如 0.96 代表达到 96% 即触发关机)
WECHAT_NOTIFY_URLString(可选)Webhook 消息通知地址(含 Token,留空则不发送通知仅执行关机)
SNS_TOPIC_ARNString(可选)AWS SNS 主题 ARN(留空则不使用 SNS 推送)
METRIC_PERIODInteger86400CloudWatch 指标聚合周期(默认 86400 秒即按天聚合)
SSM_PARAM_STOPPEDString/lightsail/stopped-by-quotaSSM 中记录被停机实例列表的参数路径
SSM_PARAM_ALERTEDString/lightsail/alerted-instancesSSM 中记录已触发告警实例字典的参数路径

AWS 资源配置与部署流程#

第一步:创建 IAM 执行角色与权限策略#

  1. 进入 AWS IAM 控制台 -> Roles (角色) -> Create role
  2. 实体类型选择 AWS service,用例选择 Lambda
  3. 创建自定义权限策略(Policy),策略 JSON 如下(仅赋予最小必需权限):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LightsailOperations",
"Effect": "Allow",
"Action": [
"lightsail:GetInstances",
"lightsail:GetInstance",
"lightsail:GetInstanceMetricData",
"lightsail:StartInstance",
"lightsail:StopInstance"
],
"Resource": "*"
},
{
"Sid": "SSMParameterAccess",
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:PutParameter",
"ssm:DeleteParameter"
],
"Resource": "arn:aws:ssm:*:*:parameter/lightsail/*"
},
{
"Sid": "SNSPublish",
"Effect": "Allow",
"Action": [
"sns:Publish"
],
"Resource": "*"
},
{
"Sid": "CloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}

将该策略命名为 LightsailTrafficMonitorPolicy 并绑定到新建的 Role(例如 LightsailMonitorRole)。


第二步:初始化 SSM Parameter Store 参数#

进入 AWS Systems Manager -> Parameter Store,在目标区域创建两个初始参数:

  1. 参数一
    • Name: /lightsail/stopped-by-quota
    • Tier: Standard
    • Type: StringList(或 String
    • Value: []
  2. 参数二
    • Name: /lightsail/alerted-instances
    • Tier: Standard
    • Type: String
    • Value: {}

第三步:创建并配置 Lambda 函数#

  1. 打开 AWS Lambda 控制台,点击 Create function
  2. 选择 Author from scratch
    • Function name: lightsail-traffic-monitor
    • Runtime: Python 3.12
    • Architecture: arm64(或 x86_64
    • Execution role: 选择刚刚创建的 LightsailMonitorRole
  3. 进入 Configuration -> General configuration
    • Timeout (超时时间) 设置为 1 分钟 (60 秒)
    • Memory (内存) 设置为 128 MB
  4. 进入 Configuration -> Environment variables,添加环境变量:
    • ALERT_THRESHOLD: 0.96
    • WECHAT_NOTIFY_URL: https://sms.example.com/message?token=YOUR_TOKEN
    • AWS_REGION: ap-southeast-1

第四步:部署 Python 完整代码#

在 Lambda 代码编辑器中,将 lambda_function.py 的内容替换为以下完整源码并点击 Deploy

import json
import boto3
import calendar
import os
import time
from datetime import datetime, date, time as dt_time, timedelta
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
# ------------------------------ 全局常量定义(纯环境变量驱动,无硬编码)------------------------------
# AWS 区域(与 Lightsail / SSM / Lambda 同区域)
AWS_REGION = os.getenv('AWS_REGION', os.getenv('AWS_DEFAULT_REGION', 'ap-southeast-1'))
# 环境变量读取配置
ALERT_THRESHOLD = float(os.getenv('ALERT_THRESHOLD', '0.95')) # 超标阈值(默认 95%)
SNS_TOPIC_ARN = os.getenv('SNS_TOPIC_ARN', '') # 可选 SNS 通知 ARN
METRIC_PERIOD = int(os.getenv('METRIC_PERIOD', '86400')) # 指标聚合周期(秒,默认 1 天)
WECHAT_NOTIFY_URL = os.getenv('WECHAT_NOTIFY_URL', '') # 消息通知 Webhook URL
# SSM 参数名称(持久化存储被停 / 已告警实例)
SSM_PARAM_STOPPED = os.getenv('SSM_PARAM_STOPPED', '/lightsail/stopped-by-quota')
SSM_PARAM_ALERTED = os.getenv('SSM_PARAM_ALERTED', '/lightsail/alerted-instances')
# 单位转换常量
GB_TO_BYTE = 10**9
MB_TO_BYTE = 1024 * 1024
# 实例状态常量
INSTANCE_STATE_STOPPED = 'stopped'
INSTANCE_STATE_RUNNING = 'running'
INSTANCE_STATE_STARTING = 'starting'
INSTANCE_STATE_STOPPING = 'stopping'
# 告警状态常量
ALERT_STATUS_TRIGGERED = 'triggered'
NOTIFY_RETRY_TIMES = 2
# ------------------------------ 全局客户端复用 ------------------------------
lightsail_client = boto3.client('lightsail', region_name=AWS_REGION)
ssm_client = boto3.client('ssm', region_name=AWS_REGION)
# ------------------------------ 通用辅助函数 ------------------------------
def json_serialize(obj):
"""通用 JSON 序列化,处理 boto3 响应对象"""
return json.dumps(obj, default=str)
def update_ssm_instance_list(instance_name, operation='add', ssm_param=SSM_PARAM_STOPPED):
"""通用 SSM 列表更新函数,支持并发重试与状态清空"""
try:
# 每月 1 号清空整个 SSM 参数
if instance_name == '' and operation == 'remove':
ssm_client.put_parameter(
Name=ssm_param,
Value='{}' if ssm_param == SSM_PARAM_ALERTED else '[]',
Type='StringList' if ssm_param == SSM_PARAM_STOPPED else 'String',
Overwrite=True
)
print(f"SSM 参数 {ssm_param} 已全部清空(配额重置)")
return True
for _ in range(3):
try:
response = ssm_client.get_parameter(Name=ssm_param, WithDecryption=False)
ssm_data = json.loads(response['Parameter']['Value'])
except ssm_client.exceptions.ParameterNotFound:
ssm_data = {} if ssm_param == SSM_PARAM_ALERTED else []
except json.JSONDecodeError:
ssm_data = {} if ssm_param == SSM_PARAM_ALERTED else []
origin_data = ssm_data.copy()
if ssm_param == SSM_PARAM_STOPPED:
if operation == 'add' and instance_name not in ssm_data:
ssm_data.append(instance_name)
print(f"实例 {instance_name} 已添加到 SSM 被停列表")
elif operation == 'remove' and instance_name in ssm_data:
ssm_data.remove(instance_name)
print(f"实例 {instance_name} 已从 SSM 被停列表移除")
else:
return True
elif ssm_param == SSM_PARAM_ALERTED:
if operation == 'add':
ssm_data[instance_name] = ALERT_STATUS_TRIGGERED
print(f"实例 {instance_name} 已标记为 SSM 已告警")
elif operation == 'remove' and instance_name in ssm_data:
del ssm_data[instance_name]
print(f"实例 {instance_name} 已从 SSM 告警列表移除")
else:
return True
if origin_data == ssm_data:
return True
ssm_client.put_parameter(
Name=ssm_param,
Value=json_serialize(ssm_data),
Type='StringList' if ssm_param == SSM_PARAM_STOPPED else 'String',
Overwrite=True
)
return True
return False
except Exception as e:
print(f"更新 SSM 列表失败:{str(e)}")
return False
def get_general_instance_usage(instance_name, start_time, end_time, data_types=None):
"""通用流量查询函数,支持出入双向指标聚合"""
if data_types is None:
data_types = ['NetworkIn', 'NetworkOut']
start_time_str = start_time.strftime('%Y-%m-%dT%H:%M:%SZ')
end_time_str = end_time.strftime('%Y-%m-%dT%H:%M:%SZ')
total_usage = 0.0
for data_type in data_types:
try:
response = lightsail_client.get_instance_metric_data(
instanceName=instance_name,
metricName=data_type,
period=METRIC_PERIOD,
unit='Bytes',
statistics=['Sum'],
startTime=start_time_str,
endTime=end_time_str
)
metric_data = response.get('metricData', [])
if not metric_data:
continue
metric_sum = sum(dp.get('sum', 0.0) for dp in metric_data)
total_usage += metric_sum
except Exception as e:
print(f"获取实例 {instance_name} {data_type} 指标失败:{str(e)}")
print(f"实例 {instance_name} 流量使用量:{total_usage:.2f} Byte({total_usage/GB_TO_BYTE:.2f} GB)")
return total_usage
def get_alerted_instances():
"""获取已告警实例列表,用于防止重复打扰"""
try:
response = ssm_client.get_parameter(Name=SSM_PARAM_ALERTED, WithDecryption=False)
alerted_instances = json.loads(response['Parameter']['Value'])
return alerted_instances if isinstance(alerted_instances, dict) else {}
except (ssm_client.exceptions.ParameterNotFound, json.JSONDecodeError):
return {}
def retry_notify(func):
"""通知重试装饰器,网络抖动时自动重试 2 次"""
def wrapper(title, message):
for retry in range(NOTIFY_RETRY_TIMES + 1):
try:
result = func(title, message)
if result:
return True
time.sleep(1)
except Exception as e:
print(f"通知重试第 {retry+1} 次异常:{str(e)}")
time.sleep(1)
return False
return wrapper
# ------------------------------ Lightsail 实例操作 ------------------------------
def stop_instance(instance_name):
"""停止实例,带幂等性校验"""
instance_state = get_instance_state(instance_name)
if instance_state not in [INSTANCE_STATE_RUNNING, INSTANCE_STATE_STARTING]:
print(f"实例 {instance_name} 当前状态 {instance_state},无需停止")
return True
try:
response = lightsail_client.stop_instance(instanceName=instance_name, force=True)
print(f"实例 {instance_name} 停止成功:{json_serialize(response)}")
update_ssm_instance_list(instance_name, operation='add', ssm_param=SSM_PARAM_STOPPED)
return True
except Exception as e:
print(f"实例 {instance_name} 停止失败:{str(e)}")
return False
def start_instance(instance_name):
"""开启实例,带幂等性校验"""
instance_state = get_instance_state(instance_name)
if instance_state != INSTANCE_STATE_STOPPED:
print(f"实例 {instance_name} 当前状态 {instance_state},无需开启")
return True
try:
response = lightsail_client.start_instance(instanceName=instance_name)
print(f"实例 {instance_name} 开启成功:{json_serialize(response)}")
return True
except Exception as e:
print(f"实例 {instance_name} 开启失败:{str(e)}")
return False
def list_instances():
"""获取所有 Lightsail 实例名称"""
instances_list = []
try:
paginator = lightsail_client.get_paginator('get_instances')
for page in paginator.paginate(PaginationConfig={'MaxItems': 50}):
instances_list.extend([instance.get('name') for instance in page.get('instances', []) if instance.get('name')])
return instances_list
except Exception as e:
print(f"获取实例列表失败:{str(e)}")
return []
def get_instance_state(instance_name):
"""获取实例当前状态"""
try:
response = lightsail_client.get_instance(instanceName=instance_name)
return response.get('instance', {}).get('state', {}).get('name', 'unknown').lower()
except Exception as e:
print(f"获取实例 {instance_name} 状态失败:{str(e)}")
return 'unknown'
def get_month_dto_quota(instance_name):
"""计算实例当月网络配额(月中创建自动按比例折算)"""
try:
response = lightsail_client.get_instance(instanceName=instance_name)
instance = response.get('instance', {})
base_quota_gb = instance.get('networking', {}).get('monthlyTransfer', {}).get('gbPerMonthAllocated', 0.0)
if base_quota_gb <= 0:
return 0.0
current_datetime = datetime.utcnow()
instance_created_datetime = instance.get('createdAt')
# 非当月创建实例,返回全额配额
if not (instance_created_datetime and instance_created_datetime.year == current_datetime.year and instance_created_datetime.month == current_datetime.month):
return base_quota_gb
# 当月创建实例,按天折算可用配额
first_day_zero = get_current_month_first_day_zero_time()
last_day_last = get_current_month_last_day_last_time()
month_ts = last_day_last.timestamp() - first_day_zero.timestamp()
instance_valide_ts = last_day_last.timestamp() - instance_created_datetime.timestamp()
if month_ts <= 0 or instance_valide_ts <= 0:
return base_quota_gb
return (instance_valide_ts / month_ts) * base_quota_gb
except Exception as e:
print(f"计算实例 {instance_name} 配额失败:{str(e)}")
return 0.0
def get_instance_current_month_usage(instance_name):
"""获取实例当月累计流量使用量"""
start_time = get_current_month_first_day_zero_time()
end_time = datetime.utcnow()
return get_general_instance_usage(instance_name, start_time, end_time)
# ------------------------------ 通知推送函数 ------------------------------
def push_notification(msg):
"""SNS 告警通知"""
if not SNS_TOPIC_ARN:
return False
try:
sns_client = boto3.client('sns', region_name=AWS_REGION)
sns_client.publish(TopicArn=SNS_TOPIC_ARN, Message=msg, Subject='Lightsail 流量配额告警')
return True
except Exception as e:
print(f"SNS 通知发送失败:{str(e)}")
return False
@retry_notify
def send_wechat_notification(title, message):
"""消息推送通知(带重试,URL 从环境变量读取,带 User-Agent 防 WAF 拦截)"""
if not WECHAT_NOTIFY_URL:
print("未配置 WECHAT_NOTIFY_URL 环境变量,跳过消息通知")
return False
try:
payload_bytes = json.dumps({
"title": title,
"content": message,
"message": message
}, ensure_ascii=False).encode('utf-8')
request = Request(
url=WECHAT_NOTIFY_URL,
data=payload_bytes,
headers={
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
},
method='POST'
)
with urlopen(request, timeout=10) as response:
res_text = response.read().decode('utf-8')
print(f"消息通知发送成功,响应:{res_text}")
return True
except (HTTPError, URLError) as e:
print(f"消息通知发送失败:{str(e)}")
return False
except Exception as e:
print(f"消息通知发送失败(未知错误):{str(e)}")
return False
# ------------------------------ 时间工具函数 ------------------------------
def get_current_month_first_day_zero_time():
today_utc = date.today()
return datetime.combine(today_utc.replace(day=1), dt_time.min, tzinfo=None)
def get_current_month_last_day_last_time():
today_utc = date.today()
last_day = today_utc.replace(day=calendar.monthrange(today_utc.year, today_utc.month)[1])
return datetime.combine(last_day, dt_time(23, 59, 59), tzinfo=None)
def get_beijing_current_date():
return (datetime.utcnow() + timedelta(hours=8)).date()
def is_beijing_month_first_day():
return get_beijing_current_date().day == 1
def is_beijing_month_first_or_last_day():
beijing_today = get_beijing_current_date()
last_day_of_month = calendar.monthrange(beijing_today.year, beijing_today.month)[1]
return beijing_today.day in (1, last_day_of_month)
def get_all_instances_summary(instance_names):
"""生成所有实例当月流量汇总报表"""
summary = [
"=== Lightsail 实例当月网络流量汇总 ===",
f"统计日期:{get_beijing_current_date().strftime('%Y-%m-%d')}(北京时间)",
""
]
for instance_name in instance_names:
quota_gb = get_month_dto_quota(instance_name)
quota_byte = quota_gb * GB_TO_BYTE
if quota_byte <= 0:
summary.append(f"实例名称:{instance_name} - 无有效配额,跳过统计\n")
continue
total_usage_byte = get_instance_current_month_usage(instance_name)
usage_percent = (total_usage_byte / quota_byte) * 100 if quota_byte > 0 else 0.0
instance_info = (
f"实例名称:{instance_name}\n"
f" 当月用量:{total_usage_byte/GB_TO_BYTE:.2f} GB\n"
f" 配额总量:{quota_gb:.2f} GB\n"
f" 当前占比:{usage_percent:.2f} %\n"
f" 告警阈值:{ALERT_THRESHOLD * 100:.1f} %\n"
)
summary.append(instance_info)
return "\n".join(summary)
# ------------------------------ 每月 1 号自动开机逻辑 ------------------------------
def start_stopped_instances_on_month_first_day():
"""每月 1 号自动唤醒上月被停止且当月流量已重置(≤1MB)的实例"""
if not is_beijing_month_first_day():
return
print("=== 开始执行每月 1 号实例开启逻辑 ===")
update_ssm_instance_list('', operation='remove', ssm_param=SSM_PARAM_ALERTED)
try:
response = ssm_client.get_parameter(Name=SSM_PARAM_STOPPED, WithDecryption=False)
stopped_instances = json.loads(response['Parameter']['Value'])
except (ssm_client.exceptions.ParameterNotFound, json.JSONDecodeError):
stopped_instances = []
if not stopped_instances:
send_wechat_notification("每月 1 号 Lightsail 实例开启通知", "SSM 中无被限额停止的实例,无需开启")
return
start_results = []
for instance_name in stopped_instances:
instance_state = get_instance_state(instance_name)
if instance_state != INSTANCE_STATE_STOPPED:
start_results.append(f"实例 {instance_name}:当前状态 {instance_state},跳过")
update_ssm_instance_list(instance_name, operation='remove', ssm_param=SSM_PARAM_STOPPED)
continue
total_usage = get_instance_current_month_usage(instance_name)
if total_usage > MB_TO_BYTE:
start_results.append(f"实例 {instance_name}:当月用量 {total_usage/MB_TO_BYTE:.2f}MB > 1MB,跳过开启")
continue
start_success = start_instance(instance_name)
if start_success:
update_ssm_instance_list(instance_name, operation='remove', ssm_param=SSM_PARAM_STOPPED)
start_results.append(f"实例 {instance_name}:开启成功")
else:
start_results.append(f"实例 {instance_name}:开启失败")
summary_msg = "\n".join([
"=== 每月 1 号 Lightsail 实例开启执行结果 ===",
f"执行时间:{get_beijing_current_date().strftime('%Y-%m-%d')}(北京时间)",
f"待处理实例数:{len(stopped_instances)}",
"执行结果:",
] + start_results)
send_wechat_notification("每月 1 号 Lightsail 实例开启结果通知", summary_msg)
# ------------------------------ Lambda 入口主函数 ------------------------------
def lambda_handler(event, context):
try:
# 1. 每月 1 号开机与配额重置检查
start_stopped_instances_on_month_first_day()
# 2. 获取所有实例
instance_names = list_instances()
if not instance_names:
return {'statusCode': 200, 'body': json_serialize('No Lightsail instances found!')}
# 3. 获取已告警实例列表
alerted_instances = get_alerted_instances()
# 4. 逐个检查实例流量
for instance_name in instance_names:
quota_gb = get_month_dto_quota(instance_name)
quota_byte = quota_gb * GB_TO_BYTE
if quota_byte <= 0:
continue
total_usage_byte = get_instance_current_month_usage(instance_name)
usage_percent = (total_usage_byte / quota_byte) * 100 if quota_byte > 0 else 0.0
is_over_quota = total_usage_byte > (quota_byte * ALERT_THRESHOLD)
is_alerted = instance_name in alerted_instances
current_state = get_instance_state(instance_name)
is_running = current_state in [INSTANCE_STATE_RUNNING, INSTANCE_STATE_STARTING]
print(f"实例: {instance_name}, 超标: {is_over_quota}, 已标记告警: {is_alerted}, 运行中: {is_running}")
# 超标停机与通知逻辑
if is_over_quota:
if is_running:
stop_result = stop_instance(instance_name)
action_type = "首次超标自动停机" if not is_alerted else "超标保护(检测到运行中,再次停机)"
alert_msg = (
f"实例名称:{instance_name}\n"
f"触发类型:{action_type}\n"
f"总使用量:{total_usage_byte/GB_TO_BYTE:.2f} GB\n"
f"配额总量:{quota_gb:.2f} GB\n"
f"使用占比:{usage_percent:.2f} %\n"
f"告警阈值:{ALERT_THRESHOLD * 100:.1f} %\n"
f"处理结果:{'实例已成功停止' if stop_result else '实例停止失败'}"
)
push_notification(alert_msg)
send_wechat_notification(f"Lightsail 流量超标停机通知 [{instance_name}]", alert_msg)
if not is_alerted:
update_ssm_instance_list(instance_name, operation='add', ssm_param=SSM_PARAM_ALERTED)
else:
print(f"实例 {instance_name} 已超标且处于停止状态,静默跳过")
else:
if is_alerted:
print(f"实例 {instance_name} 流量在阈值内,恢复正常状态")
update_ssm_instance_list(instance_name, operation='remove', ssm_param=SSM_PARAM_ALERTED)
# 5. 每月首尾日汇总通知
if is_beijing_month_first_or_last_day():
send_wechat_notification(
"Lightsail 实例当月网络使用汇总",
get_all_instances_summary(instance_names)
)
return {
'statusCode': 200,
'body': json_serialize('Lightsail quota check and auto operation completed successfully!')
}
except Exception as e:
error_msg = f"Lambda 执行异常:{str(e)}"
print(error_msg)
send_wechat_notification("Lightsail 监控 Lambda 执行失败告警", error_msg)
push_notification(error_msg)
return {
'statusCode': 500,
'body': json_serialize(error_msg)
}

第五步:设置 EventBridge 定时触发器(建议 5 分钟高频巡检)#

  1. 进入 Lambda 函数的 Add trigger 页面;
  2. 触发器源选择 EventBridge (CloudWatch Events)
  3. 选择 Create a new rule
    • Rule name: Every5MinLightsailCheck
    • Rule type: Schedule expression
    • Schedule expression: rate(5 minutes)(或者 cron(*/5 * * * ? *),每 5 分钟执行一次高频巡检);
  4. 点击 Add 保存触发器。

💡 为什么推荐 5 分钟高频巡检?

  1. 防止突发流量跑超:遭遇异常大流量或恶意刷量时,若 1 小时检测一次可能已跑超数百 GB 造成巨额账单,5 分钟巡检可在超标瞬间极速关机止损;
  2. 完全 0 费用运行:全月每 5 分钟执行一次仅约 8,640 次调用,而 AWS Lambda 永久免费额度提供每月 1,000,000 次调用与 320 万秒计算时间,完全处于免费层,零额外成本。

运维避坑与经验总结#

  1. User-Agent 拦截问题: 如果通知 Webhook 部署在 Cloudflare 之后,Python 原生 urllib 默认的 Python-urllib/3.x 请求头会触发 Cloudflare 浏览器完整性检查(Error 1010: 403 Forbidden)。因此在 Request Headers 中必须显式注入标准浏览器 User-Agent
  2. 状态防死锁与清理: 如果测试期间曾手动在 SSM Parameter Store 中写入过测试数据,可以通过将 /lightsail/alerted-instances 重置为 {},以及将 /lightsail/stopped-by-quota 重置为 [] 即可一键恢复全新检测状态。
  3. 跨区域监控注意: Lightsail 的网络配额是按地域(Region)独立的。如果有多个区域的 Lightsail 实例,只需在对应区域分别部署该 Lambda 函数,或在代码中循环遍历区域列表即可。