上一篇文章里我们把F5的iControl REST接口怎么通、Python环境怎么搭、第一次调通GET /mgmt/tm/ltm/virtual的细节基本讲完了。这篇直接进正题,把负载均衡日常交付里最常用的四件套——monitor、pool、node、vs——一次建完,并且把我在实际环境中踩过的坑一并写出来。
这个系列适合两类人:一类是运维或交付工程师,日常工作里要反复在F5的Web界面里点鼠标创建后端节点、建负载均衡池、绑健康检查、发布虚拟服务器,嫌慢又容易漏;另一类是刚接触“用代码管设备”的网工,想看看Python调F5 API到底能自动化到什么程度。这篇不会停留在“调通接口”的层面,而是直接给你一套可以抄作业的脚本思路,从连接封装、对象创建、依赖顺序到重复执行的幂等处理,全部串起来。
1. 整体思路:直接requests裸调,不迷信F5 SDK
1.1 为什么选requests而不是F5 SDK
F5官方有Python SDK,叫f5-sdk,也提供配置工具CCCL。但我的建议是:如果只是做配置交付而非平台级的持续管理,直接用requests裸调REST API反而更省心。
原因有三个。第一,F5的REST API本身非常稳定,核心路径就是/mgmt/tm/ltm/下面那几个资源,不需要SDK再做一层封装;第二,SDK的版本跟进不一定跟得上设备固件版本,我遇到过SDK某个方法在特定版本上报错,查源码发现是字段映射问题,裸调反而一清二楚;第三,裸调的调试成本低,抓包或者看返回的JSON直接就能定位问题。
当然,SDK也有它的价值,比如自动处理token过期、自带建模。但对于“创建monitor、pool、node、vs”这种短平快的交付任务,requests足够,还能少装一个依赖。
1.2 工程结构怎么摆
先看整体结构,我习惯用一个目录放所有脚本,核心分三个文件:
f5_auto/ ├── requirements.txt ├── f5_client.py # API连接与基础请求封装 └── create_ltm_objects.py # 创建四件套的主脚本requirements.txt里只有一行:
requests>=2.20.0依赖越少,后面扔到跳板机或者CI流水线里跑的时候越不容易出幺蛾子。
1.3 前置信息准备清单
动手之前,先把这几个信息列出来,脚本里都要用:
| 信息项 | 示例 | 说明 |
|---|---|---|
| F5管理地址 | lb-mgmt.example.com | 注意走管理口还是业务口 |
| 账号 | api_deploy | 建议单独建账号,别用admin |
| 密码 | 环境变量传递 | 不要写死在脚本里 |
| 分区 | Common | 跨分区引用时要写成/Common/xxx |
| 虚拟服务器IP | 10.1.1.10 | 对外的VS地址 |
| 后端节点 | 10.0.0.10/10.0.0.11 | 真实业务服务器 |
| 业务端口 | 80 | 对外和回源都用HTTP 80 |
账号这件事多说一句。生产环境最好给自动化脚本创建独立账号,权限范围缩小到目标分区。否则某天误操作把/Common里的配置全刷了,光恢复就有得受。
2. 连接与会话封装:把认证和请求统一管起来
2.1 F5认证的两种方式
F5的REST API认证方式主要是两种:HTTP Basic认证和Token认证。
Basic认证最简单,直接把用户名密码放到请求头里,适合脚本和短任务。Token认证是先调/mgmt/shared/authn/login换一个token,后续请求用X-F5-Auth-Token头携带,适合频繁调用的场景,避免每次请求都带明文密码。
下面的客户端类用的是Basic认证,代码最直观:
# f5_client.py import os import requests import urllib3 urllib3.disable_warnings() class F5Client: def __init__(self, host=None, user=None, password=None, timeout=15): self.base = "https://{}/mgmt/tm/ltm".format(host or os.getenv("F5_HOST")) self.timeout = timeout self.session = requests.Session() self.session.auth = ( user or os.getenv("F5_USER"), password or os.getenv("F5_PASSWORD"), ) self.session.verify = False def get(self, path, params=None): url = "{}/{}".format(self.base, path.strip("/")) return self.session.get(url, params=params, timeout=self.timeout) def post(self, path, body, params=None): url = "{}/{}".format(self.base, path.strip("/")) return self.session.post(url, json=body, params=params, timeout=self.timeout) def delete(self, path): url = "{}/{}".format(self.base, path.strip("/")) return self.session.delete(url, timeout=self.timeout)2.2 为什么关闭SSL校验
F5默认证书是自签的,用verify=False跳过证书校验,顺手用urllib3.disable_warnings()去掉告警。如果你对内网环境的安全要求比较高,也可以提前把F5的CA证书导入到系统信任链,然后把verify改成证书路径,代码层面不用变。
一个容易被忽略的细节是timeout。F5在配置比较大的时候,某些POST操作会超过默认的几秒钟,比如创建VS时触发了profile关联,返回时间可能变长。我在客户端类里设了15秒超时,批量创建大配置时可以再放宽一些。
2.3 关于F5 API的返回状态码
F5的REST API有个容易让人懵的点:POST创建成功后返回的状态码是200,而不是很多REST服务习惯的201 Created。别看到200就以为“没创建成功,只是通了”,只要返回体里有对象的name和selfLink,就是创建成功了。
另外,F5的API对错误状态的表达也不完全统一。比如创建重复对象时,某些版本返回400,某些版本返回409 Conflict。所以写脚本时别只检查状态码,还要把返回体里的message字段打出来看。
3. 核心创建实战:node、monitor、pool、vs逐个过
3.1 先建node:后端节点的基本面
F5里的node,对应的就是一台真实后端服务器的IP地址。它本身不绑定端口,因为一个node可以配合多个服务端口使用,比如10.0.0.10:80和10.0.0.10:443可以同时存在。
创建node的API路径是:
POST /mgmt/tm/ltm/node请求体最简形式:
body = { "name": "10.0.0.10", "address": "10.0.0.10", "partition": "Common", "description": "backend web server 01" }这里有个关键点:name和address虽然经常一样(都是IP),但含义不同。name是F5对象名,address是真实IP。如果你先在一个环境用IP命名,到了另一个环境可能想用主机名命名,比如web-01,那address照样填IP就行。
创建node时还可以直接挂“节点级监控”,但我一般不在这里做。原因后面讲pool的时候会说明。
3.2 创建monitor:健康检查怎么构造才不踩坑
monitor是F5里最容易配错的一类对象。它负责定时探测后端节点的健康状态,探测失败就自动把节点摘除。
创建HTTP monitor的API路径是:
POST /mgmt/tm/ltm/monitor/http请求体示例:
body = { "name": "http_monitor", "defaultsFrom": "/Common/http", "interval": 10, "timeout": 30, "send": "GET /healthz HTTP/1.0\r\nHost: web.example.com\r\n\r\n", "recv": "200 OK", "partition": "Common" }逐个字段说:
defaultsFrom:必填,表示继承哪个内置模板。这里是继承F5自带的/Common/http,然后基于它修改探测参数。interval:探测间隔,单位秒。10秒一次是相对合理的默认值。timeout:超时时间,单位秒。这里设30秒,意味着F5认为节点不可达的门槛是30秒。send:F5发送的探测请求内容。注意HTTP协议里换行必须是\r\n,字符串里写成\r\n。recv:响应里必须包含的字符串。如果响应内容不包含200 OK,F5就判定节点down。
关于interval和timeout的关系,建议保持timeout至少是interval的3倍。比如10秒探一次,30秒超时,这样F5可以在约3个探测周期内判定节点状态,避免一次慢响应就误判。我见过有人把interval设成5秒、timeout也设成5秒,结果后端稍微慢一点,节点健康状态就来回抖动。
另一个坑是send里的Host头。很多Web服务器对HTTP/1.0的请求要求必须带Host,否则直接返回400 Bad Request。如果F5发出去的探测请求不带Host,目标服务器根本理都不会理,monitor永远显示red。所以生产环境里我习惯把Host头写进send里,跟真实业务域名保持一致。
3.3 创建pool:把node和monitor绑到一起
pool是F5 LTM里的核心容器,它把一组后端节点聚合起来,统一绑定一个健康检查,并决定负载均衡算法。
创建pool的API路径是:
POST /mgmt/tm/ltm/pool请求体示例:
body = { "name": "web_pool", "partition": "Common", "monitor": "/Common/http_monitor", "loadBalancingMode": "round-robin", "members": [ { "name": "10.0.0.10:80", "address": "10.0.0.10", "port": 80 }, { "name": "10.0.0.11:80", "address": "10.0.0.11", "port": 80 } ] }这里有几个细节:
monitor字段可以直接写/Common/http_monitor。如果这个monitor不存在,F5会直接报错。所以顺序上必须先建monitor再建pool,这个依赖关系在后面编排脚本时要守住。
members字段里的每个元素,name格式是“node名:端口”。这个member概念跟node不同:node只代表IP,member才代表“某个IP的某个端口”。创建pool时直接在members里写address和port,F5会自动寻找或创建对应的node对象。我们前面已经手动建了node,所以这里的member会关联到已有的node上。
loadBalancingMode默认是round-robin。如果后端机器配置差异大,可以改成least-connections-node,F5会按当前连接数调度。生产环境建议按业务类型选,默认round-robin不是万金油。
3.4 创建vs:对外流量的入口
vs(virtual server)是真正对外的流量入口,它把虚拟IP:端口映射到pool上。
创建vs的API路径是:
POST /mgmt/tm/ltm/virtual请求体示例:
body = { "name": "vs_web", "partition": "Common", "destination": "10.1.1.10%80", "mask": "255.255.255.255", "pool": "/Common/web_pool", "sourceAddressTranslation": { "type": "automap" }, "profiles": [ { "name": "/Common/tcp", "context": "all" }, { "name": "/Common/http", "context": "all" } ] }里面最有迷惑性的是destination字段。F5用的是%来分隔IP和端口,不是冒号。写成10.1.1.10:80会直接报错,因为F5把这个字符串整体当作地址解析。我第一次写的时候就是这里卡了半小时,报错信息还不直观。
mask字段也值得注意。如果虚拟IP是单个IP,这里必须写255.255.255.255。如果是一个网段,比如10.1.1.0%80,mask就要写成255.255.255.0。同时这个vs会占用整个网段的80端口,使用时要非常小心。
sourceAddressTranslation是源地址转换,automap表示自动映射为F5当前出接口的IP。这里用新式写法,不要用旧字段snat。
profiles这个数组指定VS启用的协议栈。HTTP服务的VS建议显式挂http和tcp两个profile,注意context要写成all。如果你只是做TCP四层转发,不挂http也行,但tcp还是建议显式指定。
3.5 一个完整的创建函数清单
前面这些片段拆开看不直观,把它们放进一个主脚本里更清晰。下面是我常用的写法,每个对象对应一个函数,主流程里依次调用。
# create_ltm_objects.py import sys from f5_client import F5Client def create_node(api, ip, description=""): body = { "name": ip, "address": ip, "partition": "Common", "description": description, } return api.post("node", body) def create_http_monitor(api, name, host, uri="/healthz", interval=10, timeout=30): body = { "name": name, "partition": "Common", "defaultsFrom": "/Common/http", "interval": interval, "timeout": timeout, "send": "GET {} HTTP/1.0\r\nHost: {}\r\n\r\n".format(uri, host), "recv": "200 OK", } return api.post("monitor/http", body) def create_pool(api, name, monitor_name, member_list): members = [ { "name": "{}:{}".format(ip, port), "address": ip, "port": port, } for ip, port in member_list ] body = { "name": name, "partition": "Common", "monitor": "/Common/{}".format(monitor_name), "loadBalancingMode": "round-robin", "members": members, } return api.post("pool", body) def create_vs(api, name, vip, port, pool_name): body = { "name": name, "partition": "Common", "destination": "{}%{}".format(vip, port), "mask": "255.255.255.255", "pool": "/Common/{}".format(pool_name), "sourceAddressTranslation": {"type": "automap"}, "profiles": [ {"name": "/Common/tcp", "context": "all"}, {"name": "/Common/http", "context": "all"}, ], } return api.post("virtual", body) def main(): api = F5Client() BAKCEND_NODES = [ ("10.0.0.10", 80), ("10.0.0.11", 80), ] create_node(api, "10.0.0.10", "web01") create_node(api, "10.0.0.11", "web02") create_http_monitor(api, "http_monitor", "web.example.com") create_pool(api, "web_pool", "http_monitor", BAKCEND_NODES) create_vs(api, "vs_web", "10.1.1.10", 80, "web_pool") if __name__ == "__main__": sys.exit(main())注意,上面代码里把create_node单独调了两次,是刻意为之。如果后续想在node上追加描述、加节点级监控,node对象必须提前存在。直接在pool里写address让F5自动建node也能用,但node对象是隐式创建的,后续不好单独管理。
4. 全流程编排:执行顺序、幂等与配置保存
4.1 为什么执行顺序必须是node到monitor到pool再到vs
创建顺序不是随便定的,背后是F5的对象依赖关系:
- 先建node,因为pool的member要关联node。
- 再建monitor,因为pool的
monitor字段要求引用一个已存在的monitor,否则POST直接失败。 - 然后建pool,因为vs的
pool字段要求引用已存在的pool。 - 最后建vs,它只依赖pool。
依赖关系简单说就是:vs吃pool,pool吃monitor和node。倒过来建,每一步都会报错。
4.2 重复执行不报错:幂等处理
生产环境里你的脚本不会只跑一次。可能是同事重复执行,可能是CI里多次触发。如果不做幂等处理,第二次跑就会创建重复对象报错。
幂等逻辑很朴素:创建前先GET查一下对象是否存在,存在就跳过,不存在才创建。
def ensure_object(api, collection_path, object_name, body_factory): resp = api.get("{}/{}".format(collection_path, object_name)) if resp.status_code == 200: print("[SKIP] {} already exists: {}".format(collection_path, object_name)) return resp.json() if resp.status_code == 404: resp = api.post(collection_path, body_factory()) print("[CREATED] {}: {}".format(collection_path, object_name)) return resp.json() raise RuntimeError( "unexpected status {}: {}".format(resp.status_code, resp.text) )调用时这样用:
ensure_object( api, "node", "10.0.0.10", lambda: { "name": "10.0.0.10", "address": "10.0.0.10", "partition": "Common", }, )这个函数的精髓在于:判断标准是404还是200。F5返回别的状态码时直接抛异常,避免把权限问题、网络问题当成“对象不存在”处理。
4.3 配置保存:不save,重启就丢
这是最容易翻车的一步。通过REST API创建的对象,默认只在内存运行配置里生效,不会自动进入到启动配置。设备重启后,你用脚本建的所有node、pool、vs全部消失。
F5保存配置的接口不是TMSH里的save sys config,而是:
POST /mgmt/tm/sys/config请求体:
body = { "command": "save" }我习惯在创建脚本最后执行一次保存。注意一次全量save在配置量大的设备上可能需要几十秒,超时时间要放宽。
另外,F5某些型号在集群同步场景下还需要额外触发config sync,这里不展开,但生产环境一定要确认你的变更是否需要同步到对端设备。
5. 排错实录:遇到最多的问题与排查方法
5.1 401和403:认证与权限问题
401通常是账号密码错误。排掉账号密码因素后,要检查是不是账号被锁定或启用了双因子认证。
403通常是权限不够。F5的RBAC比较细,如果账号只有某个分区的操作权限,访问其他分区资源就会403。报错信息里一般会提示not authorized之类的关键字。解决方法是给账号加对应分区的权限,或者统一把脚本要操作的对象都建在目标分区内。
5.2 400:看似创建失败,其实请求体有坑
F5的400最常见原因有三种:
第一种是monitor创建时缺了defaultsFrom字段。这个字段不填,F5不知道怎么继承参数模板,直接报错。
第二种是pool里monitor字段指向了不存在的monitor。比如monitor建在/Common,pool引用时却写成http_monitor而不是/Common/http_monitor,在不同的分区场景下会解析不到。
第三种是vs的destination格式错误。写成冒号分隔的IP:端口,F5解析不了。牢记%分隔。
5.3 member一直unknown或者red
配置都创建成功了,但pool里的member状态不对,这时先分三个层面排查:
第一层,看node状态。如果node本身就是red,说明F5连基础连通性都没有,检查后端服务器的防火墙、路由和ARP。
第二层,看monitor是否真的在工作。把send里的URI改成后端服务器实际存在的路径,很多场景里后端健康检查路径是/healthz,但应用实际只响应/,返回404或者301,monitor就会判red。
第三层,看recv匹配是否合理。recv字段是“响应里必须包含的字符串”,如果F5的HTTP响应是HTTP/1.1 200 OK,你写recv: "HTTP/1.0 200"就匹配不上。调试时可以先在F5上用TMSH命令modify monitor http_monitor recv ""临时放开匹配,或者干脆把recv改成一个短字符串200试试。
5.4 VS建好了但访问不通
VS建好不代表能通,我列一个排查顺序:
- 看VS状态是不是
green。如果VS本身是red,说明pool里没有可用member。 - 检查VS的VLAN限制。默认VS对所有VLAN生效,但如果你指定了
vlans字段,来源VLAN不在列表内就会被丢弃。 - 检查目的地址和端口。用F5侧
tcpdump抓包看有没有流量到达VS,有流量但没转出去,多半是SNAT或路由问题。 - 确认pool的member端口是否跟VS端口匹配。VS的80端口转给pool的member,如果member端口写成了8080,后端没监听8080,当然不通。
5.5 常见问题速查表
| 现象 | 直接原因 | 处理办法 |
|---|---|---|
| POST返回401 | 认证失败 | 检查用户名密码、账号是否锁定 |
| POST返回403 | RBAC权限不足 | 给账号分配目标分区权限 |
| POST返回400,monitor报错 | 缺defaultsFrom | 补上defaultsFrom: "/Common/http" |
| POST返回400,pool报错 | monitor不存在 | 先建monitor,引用写成/Common/xxx |
| POST返回400,vs报错 | destination格式错误 | 用%分隔IP和端口,如10.1.1.10%80 |
| member显示red | 健康检查路径或recv不匹配 | 确认send和recv与真实响应一致 |
| VS是red | pool没有可用member | 查member状态和monitor |
| 重启后配置丢失 | 没有save | POST/mgmt/tm/sys/config保存 |
6. 扩展建议:从“脚本”到“平台”
四件套创建脚本跑通之后,最自然的下一步是把它做成配置驱动的工具。把每套环境里的业务参数抽出来放到JSON或者YAML文件里,脚本只负责解析和执行,这样你不需要为每个业务都改一遍代码。
比如建一套代理配置,只需写一个这样的输入文件:
project: demo_web vip: 10.1.1.10 vport: 80 pool: web_pool monitor: http_monitor nodes: - ip: 10.0.0.10 port: 80 - ip: 10.0.0.11 port: 80脚本读取后自动补齐依赖、校验参数、创建对象、最后save。这样再接到几十个业务的交付需求时,你的工作量就从“每个业务写一遍脚本”变成“每个业务写一段YAML”,效率和正确率完全不是一个量级。
我个人在实际操作中还有一个体会:F5的REST API返回的JSON信息量很大,创建完别只打印status_code,把返回对象里的selfLink或generation字段打出来,后面排查配置漂移和做变更记录都很有用。另外,脚本里所有账号密码务必走环境变量或密钥管理服务,不要出现在代码仓库里。这个坑,一旦踩到就是安全事故,不是开玩笑的。