Cloud Custodian 过滤器完全指南:从 Value Filter 到 Reduce Filter 的通用过滤机制

原创2026-10-11 09:20:581,747 阅读
文章标签:云原生运维安全

Cloud Custodian 过滤器完全指南:从 Value Filter 到 Reduce Filter 的通用过滤机制

本文是 Cloud Custodian(c7n)通用过滤器(Generic Filters)的实战技术指南,聚焦 docs/source/filters.rst 所定义的核心过滤体系:以 JMESPath 为基础的 Value Filter、列表项过滤、事件过滤与资源归约(reduce)过滤。无论你管理 AWS、Azure 还是 GCP 资源,这些过滤器都适用于所有策略与所有资源类型。读完本文,你将掌握如何用 YAML 编写可嵌套的布尔查询、使用十余种比较与列表运算符、借助 value_type 与 value_from 完成时间/数值/外部数据源的复杂匹配,以及用 reduce 过滤器对资源分组、排序、限量后再执行动作。

一、Generic Filters 概览:一套通用的查询语言

Cloud Custodian 的策略(Policy)由三部分组成:resource(目标资源类型)、filters(筛选条件)与 actions(执行动作)。其中 filters 段使用的就是本文要讲的通用过滤器。它们对任意资源类型都成立,因为其底层是对 JSON 对象进行 JMESPath 查询,并可与可嵌套的布尔条件运算符自由组合。各云厂商的资源专属过滤器(如 image-age、offhours)本质上大多也是在这些通用过滤器之上构建的,因此掌握本文内容等于掌握了全部过滤器的地基。

从源码结构看,通用过滤器的实现集中在 c7n/filters/core.py:文件顶部的 OPERATORS 字典(c7n/filters/core.py#L89-L111)注册了全部比较/列表/模式运算符,VALUE_TYPES 列表(c7n/filters/core.py#L114-L117)声明了全部值类型转换,ValueFilter(c7n/filters/core.py#L589)是通用值过滤器的核心类,ReduceFilter(c7n/filters/core.py#L966)与 ListItemFilter(c7n/filters/core.py#L1203)则分别实现了归约与列表项过滤。

二、Value Filter:最基础的值过滤器

Value Filter(type: value)允许你用 JMESPath 表达式从资源的 describe 调用返回数据中提取字段值,再与期望值进行比较:

filters:
     - type: value
       key: "State[0]"    # 从 describe 调用返回数据中提取的值
       value: "running"   # 用于比较的目标值

当省略 op 时,过滤器执行的是等值比较(r == v)。这里有一个容易忽略的细节:match() 方法在 op 为 in/not-in 且资源值为 None 时,会把提取值视为空元组 () 参与比较(见 c7n/filters/core.py#L755-L756),从而保证这类运算符不会因空值抛异常。

2.1 如何获取每个资源可用的 key 列表

官方提供了三种途径:

途径一:通过 Custodian CLI。 新建一个只包含 name 与 resource 字段的 YAML 文件,然后运行 custodian run -s OUTPUT_DIR。运行结束后,合法的 key 字段会写入输出目录下的 resources.json 中:

policies:
  - name: my-first-policy
    resource: aws.ec2

仓库测试数据中就有真实示例:tests/data/output/ec2-running-instances/resources.json。这也是调试过滤器时最常用的手段——先用最小策略把资源字段完整 dump 出来,再挑选需要的 key。

途径二:通过云厂商 CLI。 直接调用相应的 describe API 查看所有可用字段。例如 AWS 执行 aws ec2 describe-instances,Azure 执行 az vm list。

注意:大多数情况下不需要包含最外层的 JSON 字段(如 Reservations、value),因为 Custodian 在返回结果前会剥掉这层壳。

途径三:通过云厂商 SDK 文档。 在对应云厂商 SDK 文档中搜索该资源的 describe API 调用,其返回值下方列出的字段即是可用 key。

2.2 特殊值(Special Values):测试 key 的存在性与空值

以下元值用于判断资源是否包含某 key以及该 key 是否为空:

元值 匹配条件
absent key 不存在
present key 存在
empty 值为 false、空或缺失
not-null 值存在,且非 false、非空

假设某 S3 桶只有如下精简属性:

{
  "Name": "my_bucket",
  "Versioning": {},
  "Tags": [{
    "Environment": "dev",
    "Owner": ""
  }]
}

那么以下过滤器全部会匹配该资源:

filters:
  - "tag:Environment": "dev"
  - "tag:Environment": "not-null"
  - "tag:Environment": "present"
  - "tag:Owner": "empty"
  - "tag:Owner": "present"
  - "tag:Team": "empty"
  - "tag:Team": "absent"
  - "Versioning": "empty"
  - "Versioning": "present"
  - "Versioning.Status": "empty"
  - "Versioning.Status": "absent"

注意 tag: 前缀是 Cloud Custodian 对标签的专有简写——它会被解析为对 Tags 数组的 JMESPath 查询,因此 tag:Environment 等价于从标签列表中取名为 Environment 的标签值。这些特殊值的判定逻辑可以在 c7n/filters/core.py#L764-L782 的 match() 中看到:absent 要求提取值为 None、present 要求非 None、not-null 要求值存在且为真、empty 要求值为假值(not r)。tags.py 中的 ValueRegex 等辅助类也复用了这一套提取逻辑。

2.3 比较运算符(Comparison Operators)

通用值过滤器支持以下比较运算符,支持全称与缩写两种写法:

  • equal 或 eq(等于)
  • not-equal 或 ne(不等于)
  • greater-than 或 gt(大于)
  • gte 或 ge(大于等于)
  • less-than 或 lt(小于)
  • lte 或 le(小于等于)
  • in(属于)
  • not-in 或 ni(不属于)
  • contains(包含)

示例:筛选 CPU 核数大于 36 的实例:

filters:
   - type: value
     key: CpuOptions.CoreCount   # describe 调用返回的值
     value: 36                   # 参与比较的值
     op: greater-than            # 比较运算符

这些运算符在源码中与 Python operator 模块直接映射(c7n/filters/core.py#L89-L111),例如 gt 即 operator.gt、contains 即 operator.contains。在 match() 中,运算符通过 OPERATORS<a href="https://link.gitcode.com/i/1748cc667c3604f3708a9d431a0f3aa8" target="_blank">self.op] 取出后执行 op(r, v),若类型不匹配引发 TypeError 则安全返回 False([c7n/filters/core.py#L773-L778),不会中断策略运行。

2.4 逻辑运算符(Logical Operators):布尔组合

  • or 或 Or(或)
  • and 或 And(且)
  • not(非)

示例:匹配 CPU 核数为 36 或 42 的实例:

filters:
   - or:                             # 逻辑运算符
     - type: value
       key: CpuOptions.CoreCount     # describe 调用返回的值
       value: 36                     # 参与比较的值
     - type: value
       key: CpuOptions.CoreCount     # describe 调用返回的值
       value: 42                     # 参与比较的值

从实现看,And 采用链式短路处理:逐个执行子过滤器,一旦资源集为空立即 break(c7n/filters/core.py#L482-L494)。Not 则基于集合差实现:先让子过滤器处理全部资源,再用 before - after 得到被剔除的集合(c7n/filters/core.py#L514-L537)。特别地,块内过滤器会在资源上写入 c7n:MatchedFilters 等注解,Not 过滤器配合 AnnotationSweeper(c7n/filters/core.py#L540)在取反后清扫未匹配资源的注解,避免下游误读。

2.5 列表运算符(List Operators)

有一组运算符专门用于用户提供的列表。默认求值方向为 value from key(运算符)given value,即"key 中的值"作用于"给定的值"。如果想反转方向(given value 作用于 value from key),可以使用 swap 转换或 contains 运算符。

  • in(key 的值是否出现在给定列表中)
  • not-in 或 ni(key 的值是否不在给定列表中)
  • contains(给定列表/值是否包含 key 的值)
  • intersect —— 两个列表存在任意交集即匹配
  • difference —— 第一个列表存在任何不在第二个列表中的值即匹配

示例:匹配 ImageId 出现在预定义列表中的资源:

filters:
   - type: value
     key: ImageId                   # describe 调用返回的值
     op: in                         # 列表运算符
     value: [ID-123, ID-321]        # 参与比较的值列表

有些资源属性本身就是列表。例如 EC2 实例可以挂多个安全组,以下三个示例假设要评估三个实例:

实例 安全组名称
instance1 default, common, custom
instance2 common, custom, extra
instance3 common

示例一: 匹配安全组列表包含 default 的实例(命中 instance1):

filters:
   - type: value
     key: SecurityGroups[].GroupName
     op: contains
     value: default

示例二: 用 difference 找出带"预期外"安全组的实例。default 和 extra 不在预期列表 [common, custom] 中,因此命中 instance1 与 instance2:

filters:
   - type: value
     key: SecurityGroups[].GroupName
     op: difference
     value:
       - common
       - custom

示例三: value_type: swap 反转判断方向——检查预期列表中是否有值未出现在实例上。instance3 缺少 custom,因此命中:

filters:
   - type: value
     key: SecurityGroups[].GroupName
     op: difference
     value:
       - common
       - custom
     value_type: swap

从源码看,difference 与 intersect 的实现非常直观(c7n/filters/core.py#L77-L82):bool(set(x).difference(y)) 与 bool(set(x).intersection(y)),swap 则在 process_value_type 中交换比较双方(c7n/filters/core.py#L812-L813)。

2.6 模式匹配运算符(Pattern Matching Operators)

  • glob —— Glob 通配匹配
  • regex —— 正则匹配,忽略大小写(1)
  • regex-case —— 正则匹配,区分大小写(1)
filters:
   - type: value
     key: FunctionName                # describe 调用返回的值,或 resources.json 中的字段
     op: regex                        # 特殊运算符
     value: '(custodian|c7n)_\w+'     # 正则:匹配所有以 custodian_ 或 c7n_ 开头的值

   - type: value
     key: name                        # describe 调用返回的值,或 resources.json 中的字段
     op: regex                        # 特殊运算符
     value: '^.*c7n.*$'               # 正则:匹配所有包含 c7n 的值

   - type: value
     key: name                        # describe 调用返回的值,或 resources.json 中的字段
     op: regex                        # 特殊运算符
     value: '^((?!c7n).)*$'           # 正则:匹配所有不包含 c7n 的值

(1)这两个运算符基于 Python re.match 实现。如果过滤结果与预期不符,请查阅 Python re 模块文档中 search 与 match 的差异——re.match 是从字符串开头开始匹配,而不是任意位置搜索。

源码佐证:regex_match 使用 re.match(regex, value, flags=re.IGNORECASE)(c7n/filters/core.py#L53-L58),regex_case_sensitive_match 不带 IGNORECASE 标志(c7n/filters/core.py#L61-L66)。此外 ValueFilter.validate() 会在策略校验阶段就用 re.compile 预检正则合法性,非法正则直接抛出 PolicyValidationError(c7n/filters/core.py#L671-L677),把错误提前到部署期而非运行期。

2.7 值类型转换(Value Type Transformations)

通过 value_type 关键字可以对提取值做转换,支持的类型如下:

  • age —— 转换为 datetime(用于过去日期比较)
  • cidr —— 解析为 ipaddress
  • cidr_size —— 网络前缀长度
  • expiration —— 转换为 datetime(用于未来日期比较)
  • integer —— 转换为整数
  • normalize —— 转换为小写
  • resource_count —— 与匹配到的资源总数比较
  • size —— 元素长度
  • swap —— 交换 value 与被求值的 key
  • date —— 把过滤器的 value 解析为日期

注意:age 与 expiration 转换期望 value 以天数给出;要匹配短于一天的时间段,请使用浮点数(如 0.5 表示半天)。

典型示例:

# 获取一个列表的大小(安全组数量)
- type: value
  key: SecurityGroups[].GroupId
  value_type: size
  value: 2

# 用 swap 做成员判断
- type: value
  key: SecurityGroups[].GroupId
  value_type: swap
  op: in
  value: sg-49b87f44

# 比较前先转成整数
- type: value
  key: tag:Count
  op: greater-than
  value_type: integer
  value: 0

# 只作用于指定日期之后创建的 RDS 实例
- type: value
  key: InstanceCreateTime
  op: greater-than
  value_type: date
  value: "2019/05/01"

# 查找最近 31 天内启动的实例
- type: value
  key: LaunchTime
  op: less-than
  value_type: age
  value: 32

# 查找过去 12 小时内启动的实例
- type: value
  key: LaunchTime
  op: less-than
  value_type: age
  value: 0.5

# 用 resource_count 基于“匹配数量”过滤资源
# 注意:该 value_type 不需要 key,因为它比较的是资源列表的
# 规模而非某个具体字段
- type: value
  value_type: resource_count
  op: lt
  value: 2

age 的比较方向值得一提:源码中做了反转(c7n/filters/core.py#L816-L826),即最终以"资源时间值 > 阈值"的语义执行,因此"32 天前"用 less-than 加 value: 32 表达——直觉上"启动时间早于 32 天前"的实例会命中。expiration 则相反,以"now + 天数"作为阈值用于未来日期(c7n/filters/core.py#L841-L847)。resource_count 走的是独立校验路径:不要求 key,value 必须是整数或列表,且不允许 regex/value_regex(c7n/filters/core.py#L617-L643)。

value_type 与外部数据源结合:intersect 实战

下面的策略用 intersect 运算符,把 RDS 实例的子网组列表与一个外部 S3 txt 文件中的公网子网列表做交集比对:

- name: find-rds-on-public-subnets-using-s3-list
  comment:  |
     The txt file needs to be in utf-8 no BOM format and contain one
     subnet per line in the file no quotes around the subnets either.
  resource: aws.rds
  filters:
      - type: value
        key: "DBSubnetGroup.Subnets[].SubnetIdentifier"
        op: intersect
        value_from:
            url: s3://cloud-custodian-bucket/PublicSubnets.txt
            format: txt

也可以直接内联一个子网列表:

- name: find-rds-on-public-subnets-using-inline-list
  resource: aws.rds
  filters:
      - type: value
        key: "DBSubnetGroup.Subnets[].SubnetIdentifier"
        op: intersect
        value:
            - subnet-2a8374658
            - subnet-1b8474522
            - subnet-2d2736444

2.8 附加 JMESPath 函数(Additional JMESPath Functions)

Cloud Custodian 扩展了标准 JMESPath 函数库,当前支持:

  • split(seperator, input_string) -> list[str]:接收两个参数(分隔符 token 与输入字符串),返回字符串列表。

    示例:从日志组名中切出 Lambda 函数名,配合 copy-related-tag 动作复制标签:

    policies:
      - name: copy-related-tag-with-split
        resource: aws.log-group
        filters:
          - type: value
            key: logGroupName
            value: "/aws/lambda/"
            op: in
            value_type: swap
        actions:
          - type: copy-related-tag
            resource: aws.lambda
            # 切分日志组名以得到 Lambda 函数名
            key: "split(`/`, logGroupName)[-1]"
            tags: "*"
    
  • from_json(json_encoded_string) -> obj:接收一个 JSON 编码字符串,返回解码后的对象。

从源码看,这两个函数注册在 C7NJmespathFunctions 中(c7n/utils.py#L1184-L1198),_func_split 即 string.split(sep),_func_from_json 即 json.loads(string)(解析失败返回 None)。所有 Custodian 的 JMESPath 调用——包括 jmespath_search(c7n/utils.py#L1218-L1223)与 jmespath_compile(c7n/utils.py#L1242-L1244)——都会注入这组自定义函数,因此过滤器、value_path、value_from 的 expr 等所有 JMESPath 场景都能使用。

2.9 Value Regex:先从字段值中提取子串再比较

使用 Value Filter 时可以指定 value_regex。此时用于比较的值,是对 key 提取出的资源值执行正则后的捕获结果。

  • 过滤器要求正则中恰好一个捕获组,但也允许非捕获组,如 (?:newkey|oldkey)。
  • 如果正则没有匹配到,返回 None。

典型场景:expiration 比较需要 datetime,但标签里还混着其他数据。用 value_regex 只捕获日期部分,即可正常求值:

# 从标签内容中提取过期时间
- type: value
  key: "tag:metadata"
  value_type: expiration
  value_regex: ".*delete_after=([0-9]{4}-[0-9]{2}-[0-9]{2}).*"
  op: less-than
  value: 0

源码中 ValueRegex.get_resource_value 用 re.match 求值并返回 capture.group(1)(c7n/filters/core.py#L954-L963)。BaseValueFilter.get_resource_value 会把 value_regex 作为第三个参数透传(c7n/filters/core.py#L704-L705)。对应测试用例覆盖了"匹配首个出现"与"非捕获组"两种场景(见 tests/test_filters.py#L635 与 tests/test_filters.py#L657)。

2.10 Value From:引入外部值

value_from 允许在 Value Filter 中使用外部数据源的值。其底层实现是 c7n.resolver.ValuesFrom(c7n/resolver.py#L129),支持从 URL 拉取 json、csv、行分隔文本等格式,也支持 DynamoDB 查询(url: dynamodb + PartiQL query)。核心参数:

  • url(必填):数据源地址,支持 s3://、http:///https://、azure://(Azure Blob,需 c7n_azure)、dynamodb 等协议;
  • format:json、txt、csv、csv2dict 四选一;未显式指定时按 URL 扩展名推断;
  • expr:对数据做二次提取——json 用 JMESPath 表达式,csv 可用整数列号或 JMESPath 表达式,csv2dict 用 JMESPath;
  • headers:HTTP 请求的自定义头(如 authorization)。

官方示例(出自 ValuesFrom 类 docstring):

value_from:
   url: s3://bucket/xyz/foo.json
   expr: [].AppId

value_from:
   url: http://foobar.com/mydata
   format: json
   expr: Region."us-east-1"[].ImageId
   headers:
      authorization: my-token

value_from:
   url: s3://bucket/abc/foo.csv
   format: csv2dict
   expr: key[1]

# 使用 PartiQL 查询 DynamoDB 作为异常名单
value_from:
   url: dynamodb
   query: |
     select resource_id from exceptions
     where account_id = '{account_id} and policy = '{policy.name}'
   expr: [].resource_id

实现细节值得注意:ValuesFrom 的构造器会用 account_id 与 region 对配置做字符串格式化(c7n/resolver.py#L200-L205),这意味着 {account_id}、{region} 这类占位符可以出现在 url、expr、query 中;解析结果会以 ("value-from", cache_key) 为键缓存在 manager 的缓存中(c7n/resolver.py#L231-L245),多个资源复用同一份外部数据,避免重复拉取。

2.11 Value Path:资源内部两个字段互相比对

value_path 允许用 JMESPath 指定比较值的来源,且该值针对每个正在过滤的资源单独重新计算。注意它只提取当前资源自身的属性,因此非常适合比较同一资源内两个字段/两个列表的关系。

policies:
 - name: find-admins-with-user-roles
   resource: gcp.project
   filters:
     - type: iam-policy
       doc:
         key: bindings[?(role=='roles/admin')].members[]
         op: intersect
         value_path: bindings[?(role=='roles/user_access')].members[]

这里的 iam-policy 过滤器复用通用 Value Filter 实现,通过 key 与 value_path 对同一资源内的两个成员列表求交集。get_path_value() 的实现即对 value_path 执行 JMESPath 搜索(c7n/filters/core.py#L707-L731)。测试用例验证了 value_path 必须逐资源重新求值、不能被首个资源的结果缓存污染,且 value_from 优先级高于 value_path(tests/test_filters.py#L289-L329)。

三、List Item Filter:深入列表内部做多属性判断

list-item 过滤器让"包含列表值的资源属性"的评估变得容易——它可以针对列表中的每一个元素执行一组子过滤器(attrs),并对命中数量做约束。纯 value 过滤器做不到这一点,因为 regex 等运算符无法直接作用于列表。

3.1 示例一:AWS ECS 任务定义

ECS 任务定义包含一组容器定义(containerDefinitions)。下面的策略匹配"任一容器镜像来自指定账号与区域之外"的任务定义:

policies:
 - name: find-task-def-not-using-registry
   resource: aws.ecs-task-definition
   filters:
     - not:
       - type: list-item
         key: containerDefinitions
         attrs:
           - not:
             - type: value
               key: image
               value: "${account_id}.dkr.ecr.us-east-2.amazonaws.com.*"
               op: regex

这里 not 包裹 list-item 得到"不存在镜像来自指定仓库的容器"语义,而 attrs 内部的 not 表示"镜像不符合预期仓库"。

3.2 示例二:S3 生命周期规则

S3 桶的生命周期策略可能包含多条规则(Lifecycle.Rules)。下面的策略匹配"缺少清理未完成分片上传规则"的桶:

policies:
 - name: s3-mpu-cleanup-not-configured
   resource: aws.s3
   filters:
     - not:
       - type: list-item
         key: Lifecycle.Rules[]
         attrs:
           - Status: Enabled
           - AbortIncompleteMultipartUpload.DaysAfterInitiation: not-null

这里 list-item 确保对每一条生命周期规则同时检查多个属性(启用状态 + 分片清理字段存在),这正是 value 过滤器无法完成的组合判断。

3.3 实现原理与扩展参数

从源码看,ListItemFilter(c7n/filters/core.py#L1203)的 schema 支持三个关键字段:key(JMESPath 定位列表)、attrs(对每个元素执行的子过滤器数组)、count/count_op(对命中元素数量做断言,默认 eq)。其执行流程是:对每个资源提取列表 → 为每个元素打上临时索引 c7n:_id → 用 ListItemResourceManager 对列表元素整体跑一遍子过滤器(c7n/filters/core.py#L1275-L1310)。命中元素会以 key<a href="https://link.gitcode.com/i/ada8288942f6136629624b4fd1f629a0" target="_blank">idx] 形式记入资源的 c7n:ListItemMatches 注解。值得注意的是,attrs 中注册了 value、or、and、not、reduce 子过滤器([c7n/filters/core.py#L1185-L1193),因此列表元素内部同样可以嵌套布尔逻辑。

四、Event Filter:按事件过滤

Event Filter(type: event)针对与资源类型关联的 CloudWatch 事件 JSON 做过滤。此时可用的 key 来自 CloudTrail 事件,而不是 Value Filter 所用的 describe 资源调用。

policies:
 - name: no-ec2-public-ips
   resource: aws.ec2
   mode: make
     - type: cloudtrail
       events:
         - RunInstances
   filters:
     - type: event
       # key 是对 CloudWatch 事件 JSON 的 JMESPath 查询
       key: "detail.requestParameters.networkInterfaceSet.items[].associatePublicIpAddress"
       # key 表达式返回列表。将 "op: contains" 与 "value: true" 组合,
       # 使过滤器在任一网卡带公网 IP 时命中
       op: contains
       value: true
   actions:
     - type: terminate
       force: true

实现上 EventFilter 继承自 ValueFilter(c7n/filters/core.py#L909-L927),但有两处差异:其一,validate() 强制要求策略必须带 mode(即 Lambda 模式),否则抛出校验错误;其二,process() 直接对事件对象求值——事件不存在时原样返回资源,事件存在时只有事件命中过滤器才返回资源集,否则返回空列表。因此 Event Filter 只适用于事件驱动(CloudTrail/定时器等)策略,配合上述示例中的 mode: cloudtrail 使用。

五、Reduce Filter:分组、排序、限量

reduce 过滤器用于对资源分组、排序、限量,然后再交给后续过滤器或动作。典型场景:批量删除 AMI 时想按批次操作最老的镜像;或者做混沌工程(Chaos Engineering)时从每个 ASG 里随机挑实例,但要保证每个 ASG 最多只影响一台。

处理流程分四步:

  1. 对资源分组(Group)
  2. 对每组资源排序(Sort)
  3. 从每组中选出一定数量资源(Select)
  4. 合并各组结果(Combine)

5.1 分组资源(Grouping)

依据 group-by 属性提取的值对资源分组。无法提取到值的资源各自独立成组;未指定 group-by 时同样如此(所有资源按无分组处理)。

5.2 排序资源(Sorting)

组内排序由 sort-by 与 order 组合控制:sort-by 决定用哪个值排序,order 决定排序方向。值为 null 的资源默认排到最后,可用 null-order 改为排到最前。

注意:如果 sort-by 与 order 都未指定,则不进行任何排序。

5.3 选择资源(Selecting)

分组排序完成后,先 discard 掉一部分资源,再对剩余资源 limit 最大数量。

  • 指定 discard 或 discard-percent 时,取"discard-percent 百分比数量"与"discard 绝对数量"中的较大者,从每组有序列表的头部丢弃。
  • 丢弃后施加数量上限:先应用 limit-percent(占原始数量的百分比),再应用 limit(绝对数量)。资源从列表头部开始保留。

用一个 50 个资源的组来推演:

discard: 5
discard-percent: 20
limit: 10
limit-percent: 30

先丢弃前 10 个(50 的 20% 是 10,大于 5),剩 40 个;再施加 limit:40 的 30% 是 12,但 limit 是 10 更小,所以保留前 10 个。若资源编号为 #1-50,则丢弃 1-10,保留 11-20,丢弃 21-50。

再看一组设置:

discard-percent: 25
limit-percent: 50

丢弃 50 的 25%(12 个),剩余 38 个中保留 50%(19 个),最终得到资源 13-31。

注意:这些规则可能让某组资源被清空。例如一个组 20 个资源、另一个组 5 个资源,指定 limit-percent = 10 时,第一组得 2 个、第二组得 0 个。

5.4 合并资源组(Combining)

各组处理完后需要合并回一个资源集合。由于组由 JMESPath 表达式决定,先按 order 属性对组本身排序(与组内排序方式一致),然后简单地拼接各组资源。源码中 limit() 正是按组排序后逐组 discard/limit 再 extend 到结果列表(c7n/filters/core.py#L1142-L1170),且每组的 discard 数量按组内资源数即时计算,避免大组的丢弃量泄漏到后续小组。

5.5 属性一览

  • group-by、sort-by

    两者定义方式相同,为简化写法可直接给一个字符串,等价于指定 key:

    • key —— 提取值的 JMESPath 表达式
    • value_regex —— 带单个捕获组的正则,从 key 的结果中再提取一部分
    • value_type —— 解析为以下类型之一:
      • string(默认)
      • number
      • date
  • order —— 控制排序方式:

    • asc(默认)—— 基于 key 升序
    • desc —— 基于 key 降序
    • reverse —— 反转资源顺序(忽略 key)
    • randomize —— 随机化资源顺序(忽略 key)
  • null-order —— 排序时 null 值资源的落位:

    • last(默认)—— 列表末尾
    • first —— 列表开头
  • discard —— 每组丢弃前 N 个资源

  • discard-percent —— 每组丢弃前 N% 资源

  • limit —— 每组(丢弃后)保留前 N 个资源

  • limit-percent —— 每组(丢弃后)保留前 N% 资源

源码中 null_sort_value() 对三种 value_type 分别构造极值哨兵(数字用 ±inf、日期用 datetime.min/max、字符串用空串或 '\uffff'),从而让 null 值稳定沉底或浮顶(c7n/filters/core.py#L1121-L1140)。

5.6 实战示例

示例一:混沌工程。 从每个 ASG 中选出运行最久的实例,再随机取其中 10%(总数不超过 15),最后终止它们:

policies:
 - name: chaos-engineering
   resource: aws.ec2
   filters:
     - "State.Name": "running"
     - "tag:aws:autoscaling:groupName": present
     - type: reduce
       group-by: "tag:aws:autoscaling:groupName"
       sort-by: "LaunchTime"
       order: asc
       limit: 1
     - type: reduce
       order: randomize
       limit: 15
       limit-percent: 10
   actions:
     - terminate

这里第一个 reduce 每个 ASG 只保留最早启动的 1 台;第二个 reduce 在全局随机化后按 10% 且上限 15 台裁剪。

示例二:限量清理旧 AMI。 只处理按年龄排序前 10 个镜像:

- name: limited-ami-expiration
  resource: aws.ami
  filters:
    - type: image-age
      days: 180
      op: ge
    - type: reduce
      sort-by: "CreationDate"
      order: asc
      limit: 10
  actions:
    - deregister

示例三:按过期日期排序。 标签里的日期格式可能不统一、无法按文本排序,因此用 date 类型解析后再排序:

- name: ami-expiration-by-expire-date
  resource: aws.ami
  filters:
    - type: value
      key: "tag:expire-after"
      value_type: age
      op: gt
      value: 0
    - type: reduce
      sort-by:
        key: "tag:expire-after"
        value_type: date
      order: asc
      limit: 10
  actions:
    - deregister

六、小结:从过滤器到可治理的策略

至此,Cloud Custodian 的通用过滤器体系已完整呈现:value 过滤器通过 JMESPath + 运算符 + 值类型转换实现绝大多数字段级匹配;list-item 深入列表元素做组合判断;event 把过滤对象切换到事件 JSON;reduce 则在资源集合层面完成分组、排序与限量。所有运算符与转换均在 c7n/filters/core.py 中集中注册,外部值解析在 c7n/resolver.py 中实现,附加 JMESPath 函数位于 c7n/utils.py,对应行为有 tests/test_filters.py 中的大量单测背书(如 value_path 逐资源重算、value_from 优先级、value_regex 捕获组等)。

编写策略时建议遵循这样的排查路径:先跑一个只有 name + resource 的最小策略,从输出目录的 resources.json(参见 tests/data/output/ec2-running-instances/resources.json)确认字段结构;再按"提取 key → 选择运算符 → 决定是否需要 value_type/value_regex/value_from/value_path"的顺序组装过滤器;最后用 custodian run --dryrun 或直接执行策略验证命中集合,再附加 actions 生效。掌握这套通用过滤器,你就能以一致的 YAML 语法在 AWS、Azure、GCP 等多个云上写出可复用、可审计的治理策略。

登录后查看全文
cloud-custodian