YAML 配置语言基础
YAML 配置语言基础
目录
0. 为什么运维要学 YAML
如果你只用命令行敲单条命令,可能觉得 YAML 离自己很远。但稍微往前多走一步就会发现:YAML 几乎无处不在,而且越是"自动化、编排"类的运维工作,越是离不开它。
看几个运维里最常遇到的场景:
- Ansible:Playbook(剧本)和 Inventory(主机清单)都用 YAML 写;
- Docker Compose:一份
compose.yaml定义整套多容器应用; - Kubernetes:
.yaml描述 Pod、Deployment、Service 等几乎所有资源; - CI/CD:GitHub Actions 的 workflow、GitLab CI 的
.gitlab-ci.yml都是 YAML; - 网络配置:Ubuntu 的 netplan 用 YAML 配置静态 IP、DNS;
- 各类服务配置:很多现代工具(如 Prometheus 的告警规则、各种中间件的配置)也选用 YAML。
为什么这么多工具都选它?因为 YAML 的设计目标就是"给人读、也给人写"。它比 JSON 少了很多花括号和引号,靠缩进来表达结构,肉眼扫一眼就能看懂层级关系——这对"需要人频繁编辑的配置文件"来说非常合适。
YAML 本质上是一门数据序列化语言:它不负责"干活",只负责把数据(键值对、列表、嵌套结构)用一种可读的方式表达出来。和它同类的还有 JSON、INI、XML。这篇不绑定任何具体工具,而是把 YAML 这门"配置语言"本身的语法一次讲透——之后你学 Ansible、Docker Compose、Kubernetes 时,就不用再回头补语法,只需关心"这个工具要求 YAML 怎么组织"即可。
一个重要的心态:学 YAML 不是学某个软件的用法,而是学一门通用的配置语言。把语法地基打好,上面所有工具都能直接受益。
一、YAML 入门:是什么、怎么读
1.1 定义与后缀
YAML(读作 "YAML" 或 "Yamal",全称 "YAML Ain't Markup Language","YAML 不是标记语言")是一种数据序列化格式。所谓"序列化",就是把人/程序要处理的结构化数据(有层级、有类型的信息),用一种文本形式表达出来,既能让机器解析,也能让人阅读。
YAML 文件的后缀常见两种:
.yml:短后缀,很常见(如docker-compose.yml、.gitlab-ci.yml);.yaml:更长更完整(如compose.yaml、Kubernetes 的deployment.yaml)。
两者没有功能区别,只是习惯不同。现代规范倾向用 .yaml,但两种都会被各种工具识别。有些工具明确推荐其中一个(比如较新版本的 Docker Compose 推荐 compose.yaml),具体看工具文档。
YAML 文件开头常有一行 ---:
---
name: web-server
port: 8080
--- 是文档起始符,表示"从这里开始是一份 YAML 文档"。单个文档时它是可选的,但写上更规范;第 5 章会讲它在一份文件含多份文档时的作用。
1.2 三条一眼看懂的规律
YAML 之所以易读,是因为它的核心语法只有三条规律。掌握了这三条,任何 YAML 你基本都能看懂大半:
规律 1:缩进表示层级
同一层级的项缩进量相同,子项缩进得更多:
server:
port: 8080
host: 0.0.0.0
server 是顶层,port 和 host 缩进在它下面,所以它们是 server 的子项。缩进越深,层级越低。
规律 2:key: value 表示键值对
冒号 + 空格 前面是键(key),后面是值(value):
port: 8080
name: web
规律 3:- item 表示列表项
行首的减号 + 空格表示这是一个列表里的元素:
- nginx
- mysql
- redis
就这三条。第 2 章把它们组合起来,就能表达任意复杂度的结构。
1.3 用一段真实例子感受"怎么读"
拿一段接近 Docker Compose 的结构举例,看能不能凭上面的规律读懂:
services:
web:
image: nginx:1.25
ports:
- "8080:80"
depends_on:
- db
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
逐层读:services 是顶层键;下面缩进的 web 和 db 是它的两个子键(两个服务);web 下面又有 image、ports、depends_on;其中 ports 的值是一个列表(- "8080:80"),depends_on 的值也是一个列表(- db);db 下面的 environment 又嵌套了一层键值对。缩进 + 键值对 + 列表,三层组合就把"多服务、每服务多属性、属性含列表"的复杂结构清楚地表达出来了。这就是 YAML 的威力——结构越复杂,相对 JSON 的可读性优势越明显。
二、三大基础结构:键值对、列表、嵌套
YAML 的所有结构,都是由键值对(映射)、列表(序列)、以及它们的嵌套组合出来的。这一章逐个讲清楚,再用一个贴合运维的场景把它们串起来。
2.1 键值对(映射 / Mapping)
键值对是最基本的单元,格式是 key: value:
name: web-server
port: 8080
user: app
三个关键点:
- 冒号后面必须有一个空格,再写值。
name:web-server(冒号后无空格)是错的,会被当成一个整体字符串; - 键通常是字符串,一般不用加引号;
- 值可以是各种类型(字符串、数字、布尔、null),也可以是另一个结构(列表、嵌套映射)——第 3 章讲标量类型,这里先看结构。
2.2 列表(序列 / Sequence)
列表用行首的 - + 空格表示,每一项是列表里的一个元素:
- nginx
- mysql
- redis
列表元素可以是字符串,也可以是映射或另一个列表。比如一个列表,每一项是一个键值对(这是最常见的组合):
- name: web
image: nginx
- name: db
image: mysql
这里每一项是一个"映射",映射里有两个键(name、image)。注意 image 是和它所属项的 name 同层级的(缩进对齐),表示"这两个键属于同一个列表项"。
2.3 嵌套:缩进表达层级
嵌套就是"结构里套结构",靠缩进表达归属关系。最常见的四种组合:
映射套映射(值是一个映射):
server:
host: 0.0.0.0
port: 8080
映射套列表(值是列表):
deps:
- nginx
- mysql
列表套映射(列表项是映射):
- name: web
image: nginx
- name: db
image: mysql
列表套列表(列表项还是列表,较少见):
- - a
- b
- - c
- d
大多数真实配置用到前三种(映射套映射、映射套列表、列表套映射)。
2.4 实战:一份"服务器清单"
把三种结构组合起来,写一份运维常见的"服务器清单":
---
# 服务器清单
webservers:
hosts:
- name: web-01
ip: 192.168.1.10
os: Rocky Linux
- name: web-02
ip: 192.168.1.11
os: Rocky Linux
vars:
http_port: 80
dbservers:
hosts:
- name: db-01
ip: 192.168.1.20
os: Ubuntu
vars:
db_port: 3306
逐层拆解这份清单的层级:
- 顶层是两个键:
webservers、dbservers; webservers下面有两个子键:hosts(列表)和vars(映射);hosts是一个列表,每一项是一个"服务器"映射,含name、ip、os三个键;vars是一个映射,含http_port。
这份结构非常接近 Ansible Inventory 的样子——你不需要现在就学 Ansible,只需要读懂它的结构:顶层分组、组下有主机列表和变量。结构组织好之后,用哪个工具、怎么用,是各自文档的事。这就是"学 YAML 语法"和"学某个工具"的边界。
2.5 缩进规则:只用空格,不用 Tab
缩进是 YAML 表达层级的手段,所以缩进本身必须正确,否则结构就乱了。三条铁律:
- 只用空格缩进,绝不用 Tab。Tab 在不同环境下显示宽度不同,会导致层级判断错乱,是 YAML 最常见的报错来源;
- 同一层级的项必须缩进对齐(缩进量相同);
- 缩进量本身不要求固定(缩进 2 格、4 格都可以),只要同级对齐即可。但为了统一,绝大多数人用 2 空格。
错误(用 Tab 或对齐错乱):
webservers:
hosts: ← 这里用了 Tab,解析器可能报错或理解错层级
hosts: ← 和对齐的另一个键缩进不一致
正确:
webservers:
hosts:
- name: web-01
ip: 192.168.1.10
三、标量类型与引号
前面讲的是结构(怎么组织数据),这一章讲标量——也就是"一个具体的值"。YAML 会根据写法的样子自动推断值的类型,这既方便也暗藏陷阱,需要仔细理解。
3.1 常见的标量类型
一个值(标量)可以是下面这些类型,YAML 按"长相"自动识别:
name: web-server # 字符串(最常用,通常不用加引号)
count: 10 # 整数
ratio: 3.14 # 浮点数
enabled: true # 布尔:true / false
nothing: null # 空值:null(也写成 ~ 或留空)
version: "1.0" # 加了双引号,强制当字符串
注意 version: "1.0":如果不加引号写成 version: 1.0,会被当成浮点数,而 1.0 作为版本号时你往往想要的是字符串。加了引号就能强制把它当字符串——这是"加引号"的一个实际作用。
3.2 字符串:要不要加引号
这是 YAML 里最容易困惑的点。记住结论:大多数时候不用加引号,但有几种情况必须或最好加:
不加引号直接写(最常用):
name: nginx
command: run.sh
什么时候要加引号:
- 字符串以特殊字符开头(如
*、&、[、{、!等),或含冒号加空格的,需要加引号避免被当成特殊结构; - 想保留字符串里末尾/中间的空格;
- 想强制某个值当字符串(避免被识别成数字、布尔,如
"1.0"、"yes")。
title: "2024: 年度总结" # 含冒号加空格,加引号更安全
version: "1.0" # 强制当字符串而非浮点数
message: " 保留首尾空格 "
3.3 单引号 vs 双引号
YAML 支持两种引号,区别在于对反斜杠和转义的处理:
- 双引号:支持转义,
\n会被解释成换行、\t被解释成制表符; - 单引号:不处理转义,里面的
\n就是字面"反斜杠加 n";单引号里要用两个单引号''表示一个单引号。
a: "第一行\n第二行" # 双引号:\n 是真正的换行
b: '第一行\n第二行' # 单引号:原样是 \n(反斜杠 n 两个字)
c: 'it''s ok' # 单引号内用两个 '' 表示一个 '
实际建议:绝大多数配置用双引号或干脆不加引号。只有当你明确想保留反斜杠原样时,才用单引号。别过度纠结,记住"双引号会转义、单引号不会"这一条核心区别即可。
3.4 类型陷阱:那些"看起来是字符串"其实不是
YAML 自动类型推断会带来一些反直觉的坑,运维里尤其要小心:
# 这些都会被当成布尔,不是字符串!
a: yes # 布尔 true
b: no # 布尔 false
c: on # 布尔 true
d: off # 布尔 false
# 这些会被当成数字,不是字符串!
e: 0123 # 数字 123(前导 0 被忽略)
f: 1e3 # 数字 1000(科学计数法)
g: 0x1F # 数字 31(十六进制)
这是 YAML 继承自早期规范的"宽松"行为:yes/no/on/off 会被当成布尔值。如果你确实想要字符串 "yes",必须加引号:
answer: "yes" # 加引号,才是字符串 yes
这个坑在写配置时非常常见——比如某个配置项的值想写成 on,结果被 YAML 悄悄转成了布尔 true,行为就和你预期的不一样。判断不了时,就给值加引号。
3.5 null 空值
null 表示"没有值",有几种等价写法:
a: null # 显式写 null
b: ~ # 波浪号也表示 null
c: # 冒号后什么都不写,也是 null
三种写法都解析成 null。第三种(冒号后留空)最简洁,但有时为了可读性会显式写 null。
四、注释与多行字符串
配置文件里经常需要注释(解释这段配置是干什么的),也常需要写多行字符串(一段脚本、一段长文本)。这一章讲这两件事。
4.1 注释:#
YAML 用 # 表示注释。注释只支持整行或行尾,不支持行内注释(即一行里 # 前面有实际内容时,# 之后才是注释):
# 这是整行注释
name: web-server # 这是行尾注释
注意:YAML 没有"行内注释"语法,如果写成 key: value # comment,那 value # comment 会整体被当成字符串值,# 不是注释。所以要在键值对的"值"里加注释,只能放在值后面用空格 + #,但那样 # 还是算进值里——最安全的做法是注释单独占一行。
这一点和很多语言不同:注释尽量写在单独一行,不要试图塞进值里。
4.2 长字符串的两个问题
配置文件里常要写一段较长的文本(一段命令、一段脚本、一段说明)。直接写一行会又长又难读。YAML 提供了**块标量(block scalar)**来解决:用 | 或 > 开头,下面缩进的每一行都属于这个字符串。
两种风格的区别在于换行如何处理:
4.3 保留换行:|
用 | 开头,字符串里保留每一行的换行,适合写多行脚本、多行命令:
start_script: |
echo "开始"
systemctl start nginx
echo "完成"
这段 start_script 的值是:
echo "开始"
systemctl start nginx
echo "完成"
(三行,换行被保留)
4.4 折叠换行:>
用 > 开头,换行会被折叠成空格——多行会拼成一行(段落间空行保留为换行)。适合写"一段连续的文字":
description: >
这是第一句
这也是同一句的一部分,会被拼到一起
这段 description 的值是:
这是第一句 这也是同一句的一部分,会被拼到一起
(两行被折叠成一行,中间一个空格)
怎么选:要"原样保留换行的内容"(脚本、代码、多行命令)用 |;要"一段连续的说明文字,只是排版上想分行写"用 >。
4.5 实战:把一段 shell 命令写成多行块
运维里最常见的场景之一,是把一段要在目标机器上执行的命令写进配置。用 | 最合适:
---
setup_command: |
set -e
dnf install -y nginx
systemctl enable --now nginx
firewall-cmd --add-service=http --permanent
这样写的好处:命令逐行清晰、换行被完整保留,交给工具执行时就是一段完整的多行脚本。注意这段命令针对 Rocky/RHEL(用 dnf 和 firewall-cmd);如果是 Debian/Ubuntu,命令会是 apt install -y nginx、systemctl enable --now nginx、ufw allow http——块标量语法本身一样,只是里面的命令随系统而异。
五、进阶结构:多文档、锚点、流式写法
前面的基础足够应付大多数配置。这一章讲三个进阶结构,它们在复杂的真实配置(Kubernetes、Ansible、Compose)里常能遇到,学懂它们能让你"看得懂、也用得上"。
5.1 多文档:一份文件含多份配置
前面提过 --- 是文档起始符。它的真正用途是:一份 YAML 文件里可以包含多份独立的文档,用 --- 分隔。每份文档是完整独立的 YAML 结构。
---
# 第一份文档:一个映射
name: app-1
port: 8080
---
# 第二份文档:一个列表
- item-a
- item-b
---
# 第三份文档:另一个映射
name: app-2
replicas: 3
这在一个文件要定义多个对象时非常有用。最典型的例子是 Kubernetes:一个 .yaml 文件里可以依次定义多个资源(一个 Service、一个 Deployment),用 --- 分隔,然后一次 kubectl apply 全部提交。
对只含一份文档的文件,--- 可写可不写;但一旦要放多份,--- 就是必需的分隔符。
5.2 锚点 & 与别名 *:复用片段
写配置时经常有重复的内容——多份几乎一样的结构,只是个别字段不同。YAML 用锚点(anchor)& 和别名(alias)* 来复用片段,避免复制粘贴:
&name(锚点):定义时给这个值"起个名字",后面能引用它;*name(别名):引用前面定义过的锚点,把它的值"复制"到这里。
defaults: &defaults
image: nginx:1.25
restart: always
web:
<<: *defaults
ports:
- "80:80"
worker:
<<: *defaults
image: nginx:1.25
command: ["worker"]
这里 &defaults 给 image: nginx:1.25、restart: always 这份映射起了名。web 和 worker 里用 <<: *defaults 把这份公共配置"合并"进来,就不用每处都重复写一遍了。
5.3 merge key:<<:
上一节的 <<: 叫 merge key(合并键),它是锚点最常见的搭配用法。作用是把一个(或多个)锚点指向的映射,合并进当前映射:
base: &base
user: app
group: app
prod:
<<: *base
env: production
prod 合并了 base 的内容,所以 prod 最终等于 {user: app, group: app, env: production}。当前映射自己写的键(如 env)会覆盖锚点里同名的键。
注意:
<<:只能合并映射,不能合并列表。它和*一起用是"配置复用"的经典手法,Ansible、Kubernetes 里都能见到。
5.4 流式写法:[ ] 与 { }
前面所有例子都是"块状"写法(每行一个键/一个列表项)。YAML 也支持 JSON 风格的行内写法,叫流式(flow):
# 流式列表
ports: [8080, 9090]
# 流式映射
env: {USER: app, HOME: /home/app}
# 混合:列表里的映射用流式
apps: [{name: web, port: 80}, {name: db, port: 3306}]
[a, b, c]:内联列表(方括号、逗号分隔);{k: v, k2: v2}:内联映射(花括号、冒号、逗号分隔)。
流式写法语法和 JSON 几乎一样(这也是 YAML 是 JSON 超集的原因)。它在"需要紧凑地表达一小段结构"时方便,但可读性不如块状。推荐:大结构用块状,小片段用流式。
5.5 关于"{{ }} 是 YAML 吗"
你可能会在 Ansible、Kubernetes 里看到 {{ variable }} 这种写法。它不是 YAML 语法,而是模板引擎(Ansible 用 Jinja2,Kubernetes Helm 用 Go template)在 YAML 里嵌入的占位符。YAML 解析器会把 {{ var }} 当作一个普通的字符串值;真正把它替换成变量值,是工具在解析完 YAML 之后、用自己的模板引擎做的。
image: nginx:{{ version }} # 这是字符串,最终由模板引擎替换成 nginx:1.25
所以:写 YAML 时它只是普通字符串;运行时才被模板引擎处理。理解这个边界,就不会把"模板语法"和"YAML 语法"混为一谈。
六、常见坑与校验
语法上一个小小的错误,就能让整个配置文件加载失败。这一章把最常见的坑集中列出,再教你用工具校验,从"肉眼找错"升级到"工具报错"。
6.1 最常见的五个坑
坑 1:用 Tab 缩进
YAML 只认空格,用 Tab 是头号杀手:
server:
port: 8080 # ← 用了 Tab,解析报错或层级错乱
统一用空格缩进(推荐 2 空格)。
坑 2:冒号后少了空格
port:8080 冒号后没有空格,会被当成一个整体字符串,而不是"键: 值"。
坑 3:列表缩进错位
列表项和它所属结构缩进不一致,会改变层级归属:
services:
web:
ports:
- "80:80" # ← 这样写,ports 的列表项缩进和 web 对齐,层级就错了
ports 下面的列表项应该比 ports 多缩进,否则会被当成 web 的另一个子键。
坑 4:yes / no / on / off 被当成布尔
on: true、no: false,想要字符串必须加引号 "on"。
坑 5:把 {{ }} 当 YAML 语法
{{ var }} 是模板占位符,不是 YAML;写 YAML 时它只是字符串,运行时才由工具的模板引擎替换。
6.2 用 yamllint 校验
肉眼找错不可靠,最稳的做法是用工具校验语法。yamllint 是专门检查 YAML 语法和格式规范的工具。
安装(区分系统包管理器):
# RHEL / Rocky / CentOS
sudo dnf install -y yamllint
# Debian / Ubuntu
sudo apt update && sudo apt install -y yamllint
检查一个文件:
yamllint compose.yaml
yamllint 会列出每个问题的行号、列号、错误类型和原因,比肉眼高效得多。如果文件完全没有问题,它不会输出任何内容(退出码为 0)。
6.3 用 Python 校验(通用兜底)
如果没有 yamllint,用 Python 的 YAML 库也能快速校验语法(也能顺便看解析出的结构):
# 需要先有 pyyaml(Python 的 YAML 库)
python3 -c "import yaml; print(yaml.safe_load(open('compose.yaml')))"
- 如果 YAML 语法有问题,会抛异常并告诉你出错的行号和列号;
- 如果语法正确,会打印解析出来的 Python 结构(字典/列表),方便你确认结构是不是自己想要的。
6.4 排查一个语法报错的完整过程
假设 server.yaml 报错,按这个顺序排查:
# 1. 用 yamllint 定位错误行
yamllint server.yaml
# 2. 看到报错行后,检查那行前后:是不是用了 Tab?冒号后有没有空格?缩进是否对齐?
# 3. 修复后再校验,直到无输出
yamllint server.yaml
# 4. 最后用 Python 看解析出的结构是否符合预期
python3 -c "import yaml; print(yaml.safe_load(open('server.yaml')))"
先定位、再对照"五个坑"修复、最后确认结构,三步走能解决绝大多数 YAML 语法问题。
七、总结与练习
7.1 核心速查
| 语法 | 含义 |
|---|---|
key: value |
键值对(冒号后必须有空格) |
- item |
列表项 |
| 缩进 | 表达层级;只用空格,不用 Tab;同级对齐 |
true / false / null |
布尔、空值 |
"..." 双引号 |
会转义(\n 是换行) |
'...' 单引号 |
不转义(\n 是字面) |
# |
注释(尽量单独一行) |
| |
多行字符串,保留换行 |
> |
多行字符串,折叠成一行 |
--- |
文档起始/分隔符(多文档用) |
&name / *name |
锚点定义 / 别名引用(复用片段) |
<<: |
合并键(合并锚点指向的映射) |
[a, b] / {k: v} |
流式写法(内联列表 / 映射) |
一句话记忆:YAML 靠缩进表示层级,核心只有键值对、列表、嵌套三种结构;值会自动推断类型,拿不准就加引号;缩进用空格、冒号后要空格、yes/no/on/off 会变布尔——这四条能避开绝大多数坑。
7.2 动手练习
练习 1:写一份"类 Ansible"的结构
用映射 + 列表 + 嵌套,描述"web 组下有 2 台主机,每台有名字和 IP,组里还有一个变量 http_port"。参考答案:
web:
hosts:
- name: web-01
ip: 192.168.1.10
- name: web-02
ip: 192.168.1.11
vars:
http_port: 80
练习 2:写一份"类 docker compose"的结构
描述两个服务:web(镜像 nginx,依赖 db,端口映射一个列表)、db(镜像 mysql,含一个环境变量)。参考答案:
services:
web:
image: nginx:1.25
ports:
- "8080:80"
depends_on:
- db
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
练习 3:用 | 写一段多行命令
把下面三条命令写成一个键 deploy 的多行字符串(块标量),并注意它们属于 Rocky/RHEL(用 dnf):
deploy: |
dnf update -y
dnf install -y nginx
systemctl enable --now nginx
练习 4(进阶):用锚点复用公共配置
写两份几乎一样的服务配置,用 & / * / <<: 抽取公共部分。参考答案:
base: &base
image: nginx:1.25
restart: always
web:
<<: *base
ports:
- "80:80"
worker:
<<: *base
command: ["worker"]
提示:做完后用
yamllint和python3 -c "import yaml; print(yaml.safe_load(...))"校验,看结构是否符合预期。netplan(Ubuntu 的静态 IP 配置)也是 YAML,学完可以顺手看看/etc/netplan/*.yaml的结构。
参考资料
- YAML 官方规范(YAML 1.2):https://yaml.org/spec/1.2.2/
- yamllint(YAML 语法与规范检查工具):https://yamllint.readthedocs.io/
- PyYAML(Python 的 YAML 解析库,用于校验):https://pyyaml.org/
- Red Hat 官方博客关于 YAML(英文):https://www.redhat.com/en/topics/automation/what-is-yaml
