4、YAML高级特性:锚点(&)与别名(*)、合并键(<<)、类型标签、多文档分隔符(---)
YAML 的基础语法,说白了就是键值对和缩进。但真正让 YAML 在 DevOps 领域大放异彩的,是它那套高级特性。我刚开始接触时也觉得这些符号花里胡哨的,直到在项目中遇到几百行的重复配置……嗯,真香。
这一节,我们来啃下 YAML 的四个硬核特性:锚点与别名、合并键、类型标签、多文档分隔符。掌握了它们,你的配置文件会从「能跑」变成「优雅」。
4.1 锚点(&)与别名(*):告别重复劳动
你想想看,一个 Kubernetes 部署文件里,同一个镜像标签要写七八遍。改版本号时,漏改一个就是事故。锚点和别名就是来解决这个痛点的。
锚点用 &名称 定义,别名用 *名称 引用。就这么简单。
# 定义锚点
default_config: &default
timeout: 30
retries: 3
log_level: info
# 引用别名
service_a:
<<: *default
name: service-a
service_b:
<<: *default
name: service-b
timeout: 60 # 覆盖默认值
核心要点:锚点定义的是整个节点,不是单个值。别名引用的是整个结构,然后可以用合并键覆盖部分字段。
我在项目中遇到过这样的场景:一个微服务架构有 12 个服务,每个服务的健康检查配置几乎一模一样。用锚点之前,改一次超时时间要改 12 个文件。用锚点之后,改一个地方就行。你想想看,这省了多少事。
个人习惯:我通常把锚点定义放在文件顶部,用 defaults 或 _base 作为键名。这样一眼就能看到所有可复用的配置块。
4.2 合并键(<<):把配置「拼」起来
合并键 << 是锚点的最佳搭档。它的作用是把一个映射(字典)合并到当前映射中。说白了,就是「继承」。
database_defaults: &db_defaults
host: localhost
port: 5432
pool: 10
development:
<<: *db_defaults
database: dev_db
debug: true
production:
<<: *db_defaults
database: prod_db
host: db.prod.example.com # 覆盖 host
pool: 50 # 覆盖 pool
合并键可以同时合并多个锚点:
base_app: &app
version: 1.0
author: team
base_web: &web
port: 8080
ssl: true
my_service:
<<: [*app, *web]
name: my-service
port: 9090 # 覆盖 web 的 port
我曾经踩过的坑:合并键只能合并映射(字典),不能合并列表。如果你试图 <<: *some_list,解析器会直接报错。另外,合并的顺序很重要——后面的会覆盖前面的。
4.3 类型标签:让 YAML 更「严谨」
YAML 默认会做类型推断。比如 123 会被解析成整数,true 会被解析成布尔值。但有时候,我们需要强制指定类型。这时候就用到了类型标签。
类型标签的格式是 !!类型:
# 强制类型转换
port: !!str 8080 # 强制转为字符串 "8080"
enabled: !!bool "yes" # 强制转为布尔值 true
count: !!int "42" # 强制转为整数 42
price: !!float "3.14" # 强制转为浮点数 3.14
# 时间与日期
start_time: !!timestamp 2024-01-01T10:00:00Z
expire_date: !!timestamp 2024-12-31
# 空值与集合
items: !!set
? apple
? banana
? cherry
# 二进制数据
icon: !!binary |
R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5
OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+
实际场景:在 CI/CD 配置中,我经常用 !!str 来确保版本号不被解析成数字。比如 version: !!str "1.10",如果不加标签,YAML 会把它当成浮点数 1.1,这显然不是我们想要的。
常用的类型标签:
| 标签 | 类型 | 示例 |
|---|---|---|
!!str |
字符串 | !!str 123 |
!!int |
整数 | !!int "42" |
!!float |
浮点数 | !!float "3.14" |
!!bool |
布尔值 | !!bool "yes" |
!!null |
空值 | !!null ~ |
!!timestamp |
时间戳 | !!timestamp 2024-01-01 |
!!set |
集合 | !!set {? a, ? b} |
!!binary |
二进制 | !!binary | |
我的建议:除非有明确的类型转换需求,否则不要滥用类型标签。YAML 的自动类型推断在 95% 的场景下都够用。加太多标签反而让文件变得臃肿。
4.4 多文档分隔符(---):一个文件,多个配置
一个 YAML 文件里可以包含多个文档,用 --- 分隔。每个文档都是独立的 YAML 结构。
# 第一个文档:API 配置
---
api_version: v1
endpoint: /api/v1/users
timeout: 30
# 第二个文档:数据库配置
---
database:
host: localhost
port: 5432
name: myapp
# 第三个文档:缓存配置
---
cache:
type: redis
host: redis.example.com
port: 6379
多文档分隔符在以下场景特别有用:
- Kubernetes 资源清单:一个文件里放多个资源(Deployment、Service、ConfigMap)
- CI/CD 流水线:不同阶段的配置放在同一个文件的不同文档中
- 测试数据:多个测试用例的数据集
注意:解析多文档 YAML 时,解析器会返回一个文档列表。在 Python 中可以用 yaml.load_all() 来读取。每个文档之间完全独立,不能跨文档引用锚点。
我记得有一次在写 GitLab CI 配置时,把 staging 和 production 的部署配置放在同一个 .gitlab-ci.yml 里,用 --- 分隔。结果同事 review 时一脸懵:「这文件怎么有多个 ---?」后来我加了个注释说明,大家才明白。嗯,这里要注意:多文档分隔符虽然强大,但也要考虑团队的习惯。
我曾经踩过的坑:锚点不能跨文档引用。如果你在第一个文档定义了 &default,第二个文档里用 *default 是无效的。每个文档的锚点作用域仅限于当前文档。
4.5 综合实战:一个完整的 DevOps 配置
把今天学的四个特性串起来,写一个真实的 CI/CD 配置:
# 文档 1:全局默认配置
---
global_defaults: &global
image: node:18-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
# 文档 2:构建阶段
---
build:
stage: build
<<: *global
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
# 文档 3:测试阶段
---
test:
stage: test
<<: *global
script:
- npm install
- npm run test
coverage: !!str "/Coverage: \d+\.\d+%/"
# 文档 4:部署阶段
---
deploy_staging:
stage: deploy
<<: *global
script:
- npm run deploy:staging
environment:
name: staging
url: https://staging.example.com
only:
- develop
deploy_production:
stage: deploy
<<: *global
script:
- npm run deploy:production
environment:
name: production
url: https://example.com
only:
- main
这个配置里,我们用 &global 定义了公共配置,用 <<: *global 复用到每个阶段,用 !!str 强制了覆盖率正则表达式的类型,用 --- 把不同阶段分成了独立文档。你看,四个特性全用上了。
最后说一句:这些高级特性用好了,你的 YAML 配置会像代码一样优雅。但别过度设计——如果一个锚点只被引用一次,那还不如直接写。我个人的原则是:重复三次以上才考虑用锚点。