3778 字
19 分钟
阅读量
AWS Lightsail 流量监控与超标自动开关机实战(基于 Serverless 架构)
AWS Lightsail 提供了极具性价比的轻量云服务器套餐,并且每个套餐都自带了丰厚的月度网络传输配额(通常为 1TB ~ 5TB 出方向流量)。
然而,一旦实例遭受网络攻击、异常跑流量或业务突发导致流量超出月度配额,AWS 将按照每 GB 额外计费(通常为 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 | | (即时推送告警与汇总日报) | +----------------------------+核心设计亮点
- 零受控端侵入(Agentless):完全基于 AWS IAM 鉴权通过云端 API 交互,不在目标 Lightsail 机器内部安装任何软件,机器重装、改系统亦不受任何影响;
- 当月新建机器配额智能折算:对于月中新建的机器,脚本会自动计算当月剩余有效天数比例并折算实际可用配额,彻底杜绝月中新建机器误触发超标;
- 出入双向流量精准聚合:自动按天聚合查询
NetworkIn与NetworkOut数据点求和,精准比对当月总配额; - 防重复打扰与幂等关机:关机后将实例写入 AWS SSM Parameter Store,对于已经处于停止状态的实例保持静默,不反复推送骚扰通知;
- 每月 1 号自动重置与唤醒:北京时间每月 1 号 00:00,自动清空历史告警记录;对于上月因超标被系统关机、且当月新流量 ≤ 1MB 的实例,自动执行开机恢复服务;
- 每月首尾日全量用量报表:在月初与月末自动汇总账号下所有 Lightsail 实例的流量消耗详情并推送全局报表;
- WAF 与 Cloudflare 防火墙兼容:内置标准浏览器 User-Agent,彻底避免 Python 默认
urllib请求头被 Cloudflare WAF / 边缘防火墙拦截(如 Error 1010 / 403 Forbidden)。
接口与环境变量参数规范
Lambda 函数所有配置均采用环境变量驱动,便于在 AWS 控制台灵活调整:
| 环境变量名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
AWS_REGION | String | ap-southeast-1 | Lightsail 实例所在区域(与 Lambda 区域保持一致) |
ALERT_THRESHOLD | Float | 0.95 | 流量超标预警阈值(例如 0.96 代表达到 96% 即触发关机) |
WECHAT_NOTIFY_URL | String | (可选) | Webhook 消息通知地址(含 Token,留空则不发送通知仅执行关机) |
SNS_TOPIC_ARN | String | (可选) | AWS SNS 主题 ARN(留空则不使用 SNS 推送) |
METRIC_PERIOD | Integer | 86400 | CloudWatch 指标聚合周期(默认 86400 秒即按天聚合) |
SSM_PARAM_STOPPED | String | /lightsail/stopped-by-quota | SSM 中记录被停机实例列表的参数路径 |
SSM_PARAM_ALERTED | String | /lightsail/alerted-instances | SSM 中记录已触发告警实例字典的参数路径 |
AWS 资源配置与部署流程
第一步:创建 IAM 执行角色与权限策略
- 进入 AWS IAM 控制台 -> Roles (角色) -> Create role;
- 实体类型选择 AWS service,用例选择 Lambda;
- 创建自定义权限策略(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,在目标区域创建两个初始参数:
- 参数一:
- Name:
/lightsail/stopped-by-quota - Tier: Standard
- Type:
StringList(或String) - Value:
[]
- Name:
- 参数二:
- Name:
/lightsail/alerted-instances - Tier: Standard
- Type:
String - Value:
{}
- Name:
第三步:创建并配置 Lambda 函数
- 打开 AWS Lambda 控制台,点击 Create function;
- 选择 Author from scratch:
- Function name:
lightsail-traffic-monitor - Runtime:
Python 3.12 - Architecture:
arm64(或x86_64) - Execution role: 选择刚刚创建的
LightsailMonitorRole;
- Function name:
- 进入 Configuration -> General configuration:
- Timeout (超时时间) 设置为 1 分钟 (60 秒);
- Memory (内存) 设置为 128 MB;
- 进入 Configuration -> Environment variables,添加环境变量:
ALERT_THRESHOLD:0.96WECHAT_NOTIFY_URL:https://sms.example.com/message?token=YOUR_TOKENAWS_REGION:ap-southeast-1
第四步:部署 Python 完整代码
在 Lambda 代码编辑器中,将 lambda_function.py 的内容替换为以下完整源码并点击 Deploy:
import jsonimport boto3import calendarimport osimport timefrom datetime import datetime, date, time as dt_time, timedeltafrom urllib.request import Request, urlopenfrom 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 通知 ARNMETRIC_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**9MB_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_notifydef 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 分钟高频巡检)
- 进入 Lambda 函数的 Add trigger 页面;
- 触发器源选择 EventBridge (CloudWatch Events);
- 选择 Create a new rule:
- Rule name:
Every5MinLightsailCheck - Rule type: Schedule expression
- Schedule expression:
rate(5 minutes)(或者cron(*/5 * * * ? *),每 5 分钟执行一次高频巡检);
- Rule name:
- 点击 Add 保存触发器。
💡 为什么推荐 5 分钟高频巡检?
- 防止突发流量跑超:遭遇异常大流量或恶意刷量时,若 1 小时检测一次可能已跑超数百 GB 造成巨额账单,5 分钟巡检可在超标瞬间极速关机止损;
- 完全 0 费用运行:全月每 5 分钟执行一次仅约 8,640 次调用,而 AWS Lambda 永久免费额度提供每月 1,000,000 次调用与 320 万秒计算时间,完全处于免费层,零额外成本。
运维避坑与经验总结
- User-Agent 拦截问题:
如果通知 Webhook 部署在 Cloudflare 之后,Python 原生
urllib默认的Python-urllib/3.x请求头会触发 Cloudflare 浏览器完整性检查(Error 1010: 403 Forbidden)。因此在 Request Headers 中必须显式注入标准浏览器User-Agent。 - 状态防死锁与清理:
如果测试期间曾手动在 SSM Parameter Store 中写入过测试数据,可以通过将
/lightsail/alerted-instances重置为{},以及将/lightsail/stopped-by-quota重置为[]即可一键恢复全新检测状态。 - 跨区域监控注意: Lightsail 的网络配额是按地域(Region)独立的。如果有多个区域的 Lightsail 实例,只需在对应区域分别部署该 Lambda 函数,或在代码中循环遍历区域列表即可。