Ansible 完全指南
Ansible 通过 SSH 连接被管理节点,无需在目标机器上安装任何代理程序。控制节点(安装 Ansible 的机器)直接通过 SSH 执行任务,任务完成后不留任何持久进程。 核心特性: 动态 Inventory 通过可执行脚本或插件从外部系统(AWS、GCP、CMDB)动态获取主机列表。 aws_ec2.yml 示例: Task 字段说明: 基于 Jinja2 模板引擎渲染文件,支持变量、条件、循环。 模板文件示例(templates/nginx.conf.j2): apt 模块 state 字段: 优先级越高,值越能覆盖低优先级的同名变量: gro
官方文档:https://docs.ansible.com/ansible/latest/
适用版本:Ansible 2.15+(2026-05-07 核实)
核心概念
无 Agent 架构
Ansible 通过 SSH 连接被管理节点,无需在目标机器上安装任何代理程序。控制节点(安装 Ansible 的机器)直接通过 SSH 执行任务,任务完成后不留任何持久进程。
核心特性:
- 无 Agent,目标机器只需 Python(2.7+ 或 3.5+)和 SSH
- 幂等性:多次执行同一 Playbook 结果相同
- 声明式配置:描述目标状态,而非具体步骤
- 基于 YAML,可读性强
核心组件关系
Inventory(目标主机清单)
|
v
Playbook(剧本)
|
+-- Play(针对一组主机的任务集合)
|
+-- Task(调用 Module 的单个步骤)
| |
| +-- Module(执行具体操作的功能单元,如 apt、copy、service)
|
+-- Handler(由 notify 触发的特殊 Task,如重启服务)
|
+-- Role(可复用的 Task/变量/文件/模板集合)
| 组件 | 说明 |
|---|---|
| Inventory | 定义被管理的主机及分组,可以是文件或动态脚本 |
| Playbook | YAML 文件,包含一个或多个 Play |
| Play | 将一组 Task 映射到一组主机上执行 |
| Task | 调用单个 Module 完成一个操作步骤 |
| Module | Ansible 内置或第三方的功能单元,执行实际操作 |
| Handler | 特殊 Task,只在被 notify 时触发,且同一 Play 中只执行一次 |
| Role | 按约定目录结构组织的 Task、变量、文件、模板的可复用单元 |
Inventory
INI 格式
# 独立主机(无分组)
192.168.1.5
[webservers]
web-01.example.com
web-02.example.com ansible_host=192.168.1.11
[dbservers]
db-01.example.com ansible_host=192.168.1.20 ansible_port=2222
# 范围语法(web-01 到 web-05)
[cache]
redis-[01:05].example.com
# 嵌套组:[父组名:children]
[production:children]
webservers
dbservers
# 组变量:[组名:vars]
[webservers:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/prod_key
http_port=80
YAML 格式
# inventory.yml
all:
children:
webservers:
hosts:
web-01.example.com:
ansible_host: 192.168.1.10
http_port: 80
web-02.example.com:
ansible_host: 192.168.1.11
vars:
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/prod_key
dbservers:
hosts:
db-01.example.com:
ansible_host: 192.168.1.20
ansible_port: 2222
production:
children:
webservers:
dbservers:
主机变量说明
| 变量 | 说明 |
|---|---|
ansible_host |
实际连接的 IP 或域名(区别于 inventory 中的别名) |
ansible_port |
SSH 端口,默认 22 |
ansible_user |
SSH 用户名 |
ansible_ssh_private_key_file |
SSH 私钥路径 |
ansible_password |
SSH 密码(不推荐,建议用密钥) |
ansible_become |
是否启用提权,等同于 become: true |
ansible_become_user |
提权目标用户,默认 root |
ansible_python_interpreter |
目标机器 Python 路径,如 /usr/bin/python3 |
ansible_connection |
连接类型,默认 ssh,可设为 local |
动态 Inventory
动态 Inventory 通过可执行脚本或插件从外部系统(AWS、GCP、CMDB)动态获取主机列表。
# 使用动态 inventory 脚本
ansible-playbook -i inventory_script.py site.yml
# 使用 inventory 插件(aws_ec2)
ansible-inventory -i aws_ec2.yml --list
aws_ec2.yml 示例:
plugin: amazon.aws.aws_ec2
regions:
- cn-north-1
filters:
instance-state-name: running
tag:Environment: production
keyed_groups:
- key: tags.Role
prefix: role
Playbook 语法
完整 Playbook 结构
---
# site.yml
- name: 配置 Web 服务器
hosts: webservers
become: true
gather_facts: true
vars:
app_port: 8000
app_user: www-data
vars_files:
- vars/secrets.yml
pre_tasks:
- name: 更新 apt 缓存
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
roles:
- common
- nginx
tasks:
- name: 确保应用目录存在
ansible.builtin.file:
path: /opt/app
state: directory
owner: "{{ app_user }}"
mode: "0755"
- name: 部署应用配置
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: restart app
post_tasks:
- name: 验证服务启动
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
handlers:
- name: restart app
ansible.builtin.service:
name: myapp
state: restarted
Play 字段
| 字段 | 类型 | 说明 |
|---|---|---|
name |
str | Play 名称,显示在执行输出中 |
hosts |
str/list | 目标主机或主机组,支持通配符(webservers:dbservers) |
become |
bool | 是否提权(sudo),默认 false |
become_user |
str | 提权目标用户,默认 root |
gather_facts |
bool | 是否收集主机信息(facts),默认 true,不需要时关闭可加速 |
vars |
dict | Play 级别变量 |
vars_files |
list | 加载外部变量文件 |
tasks |
list | Task 列表 |
pre_tasks |
list | 在 roles 之前执行的 Task |
post_tasks |
list | 在 roles 之后执行的 Task |
roles |
list | 引用的 Role 列表 |
handlers |
list | Handler 列表 |
serial |
int/str | 滚动更新批次大小(如 2 或 "20%") |
any_errors_fatal |
bool | 任意主机失败时立即中止整个 Play |
max_fail_percentage |
int | 允许失败的主机比例上限 |
tags |
list | 为整个 Play 打标签 |
Task 字段
tasks:
- name: 安装 Python 依赖
ansible.builtin.pip:
requirements: /opt/app/requirements.txt
virtualenv: /opt/app/venv
# 条件执行
when: ansible_os_family == "Debian"
# 注册返回值
register: pip_result
# 触发 Handler
notify: restart app
# 循环
loop:
- package1
- package2
# 标签(ansible-playbook --tags deploy 时执行)
tags:
- deploy
- python
# 忽略错误
ignore_errors: true
# 超时(秒)
timeout: 60
# 失败重试
retries: 3
delay: 5
until: pip_result.rc == 0
Task 字段说明:
| 字段 | 说明 |
|---|---|
name |
Task 描述,显示在执行输出中,建议清晰描述操作目的 |
module |
要调用的模块(即 Task 中除元字段外的键) |
when |
条件表达式,为 true 时执行,支持 Jinja2 |
register |
将模块返回值存储到变量中 |
notify |
任务有变更(changed)时通知 Handler |
loop |
循环执行,每次迭代 item 变量为当前元素 |
loop_control |
控制循环行为(loop_var、label、pause) |
tags |
标签,支持 --tags / --skip-tags 选择性执行 |
ignore_errors |
忽略错误继续执行 |
become |
此 Task 级别的提权设置(覆盖 Play 级别) |
delegate_to |
委托给指定主机执行 |
run_once |
整个 Play 中只执行一次(用于需要唯一操作的场景) |
retries |
失败重试次数,配合 until 和 delay 使用 |
when 条件语法
tasks:
# 比较操作系统
- name: 安装 apt 包(仅 Debian/Ubuntu)
ansible.builtin.apt:
name: nginx
when: ansible_os_family == "Debian"
# 检查变量
- name: 仅在生产环境执行
ansible.builtin.command: /opt/scripts/deploy.sh
when: env == "production"
# 多条件(and)
- name: 满足多个条件
ansible.builtin.debug:
msg: "条件满足"
when:
- ansible_memtotal_mb > 2048
- ansible_processor_vcpus >= 2
# or 条件
- name: 满足任一条件
ansible.builtin.debug:
msg: "OS 匹配"
when: ansible_distribution == "Ubuntu" or ansible_distribution == "Debian"
# 检查变量是否定义
- name: 仅在变量已定义时执行
ansible.builtin.debug:
msg: "{{ my_var }}"
when: my_var is defined
# 检查命令返回值
- name: 检查文件是否存在
ansible.builtin.stat:
path: /opt/app/.installed
register: installed_flag
- name: 仅在未安装时执行安装
ansible.builtin.command: /opt/scripts/install.sh
when: not installed_flag.stat.exists
loop 和 with_items
# 现代写法(loop,Ansible 2.5+)
- name: 安装多个软件包
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- git
- python3
- python3-pip
# 遍历字典列表
- name: 创建多个用户
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop:
- { name: alice, groups: sudo }
- { name: bob, groups: www-data }
# loop_control:自定义循环变量名和标签
- name: 部署多个应用
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop: "{{ app_configs }}"
loop_control:
loop_var: item # 默认就是 item,可以改名避免嵌套循环冲突
label: "{{ item.dest }}" # 简化输出中每次迭代的显示
# 传统写法(with_items,仍然有效但不推荐)
- name: 安装软件包(旧写法)
ansible.builtin.apt:
name: "{{ item }}"
with_items:
- nginx
- git
register 保存结果
- name: 检查 Nginx 运行状态
ansible.builtin.command: systemctl is-active nginx
register: nginx_status
ignore_errors: true
- name: 打印 Nginx 状态
ansible.builtin.debug:
msg: "Nginx 状态:{{ nginx_status.stdout }}"
- name: 仅在 Nginx 未运行时启动
ansible.builtin.service:
name: nginx
state: started
when: nginx_status.rc != 0
# 常用 register 返回字段
# result.stdout - 标准输出(字符串)
# result.stderr - 标准错误(字符串)
# result.rc - 返回码(int)
# result.stdout_lines - 标准输出按行分割的列表
# result.changed - 是否有变更(bool)
# result.failed - 是否失败(bool)
常用模块
ansible.builtin.command / shell
# command:不经过 shell,更安全,不支持管道、重定向、通配符
- name: 执行脚本
ansible.builtin.command:
cmd: /opt/scripts/migrate.sh
chdir: /opt/app # 执行前切换目录
creates: /opt/app/.migrated # 文件存在时跳过(幂等性保障)
# shell:经过 /bin/sh,支持管道、重定向、环境变量展开
- name: 统计日志行数
ansible.builtin.shell:
cmd: "grep ERROR /var/log/app.log | wc -l"
register: error_count
| 参数 | 说明 |
|---|---|
cmd |
要执行的命令 |
chdir |
执行前切换到指定目录 |
creates |
指定文件存在时跳过此 Task(幂等性) |
removes |
指定文件不存在时跳过此 Task |
stdin |
向命令的标准输入传递内容 |
executable |
shell 模块可指定解释器,如 /bin/bash |
ansible.builtin.copy
- name: 复制配置文件
ansible.builtin.copy:
src: files/nginx.conf # 控制节点上的路径(相对于 playbook 或 role)
dest: /etc/nginx/nginx.conf # 目标路径
owner: root
group: root
mode: "0644"
backup: true # 覆盖前备份原文件
notify: reload nginx
- name: 直接写入内容
ansible.builtin.copy:
content: |
[Unit]
Description=My App
dest: /etc/systemd/system/myapp.service
mode: "0644"
| 参数 | 说明 |
|---|---|
src |
控制节点上的源文件路径 |
dest |
目标机器上的目标路径 |
content |
直接指定文件内容(与 src 互斥) |
owner |
文件所有者 |
group |
文件所属组 |
mode |
文件权限,建议用字符串格式如 "0644" |
backup |
覆盖前是否备份 |
force |
目标文件已存在时是否强制覆盖,默认 true |
ansible.builtin.template
基于 Jinja2 模板引擎渲染文件,支持变量、条件、循环。
- name: 渲染 Nginx 配置
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/myapp.conf
owner: root
mode: "0644"
notify: reload nginx
模板文件示例(templates/nginx.conf.j2):
# 由 Ansible 自动生成,请勿手动修改
server {
listen {{ http_port }};
server_name {{ ansible_hostname }};
location / {
proxy_pass http://127.0.0.1:{{ app_port }};
proxy_set_header Host $host;
}
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
}
| 参数 | 说明 |
|---|---|
src |
控制节点上的 Jinja2 模板文件路径 |
dest |
目标机器上的目标路径 |
owner |
文件所有者 |
mode |
文件权限 |
trim_blocks |
移除块标签后的换行,默认 true |
variable_start_string |
变量标签起始符,默认 {{ |
ansible.builtin.file
# 创建目录
- name: 创建应用数据目录
ansible.builtin.file:
path: /opt/app/data
state: directory
owner: app_user
group: app_user
mode: "0755"
recurse: true # 递归设置权限
# 创建符号链接
- name: 创建当前版本链接
ansible.builtin.file:
src: /opt/app/releases/v1.2.3
dest: /opt/app/current
state: link
# 删除文件或目录
- name: 清理旧版本
ansible.builtin.file:
path: /opt/app/releases/v1.1.0
state: absent
state 值 |
说明 |
|---|---|
file |
确保是普通文件(不创建,只修改属性) |
directory |
确保目录存在,不存在则创建 |
link |
确保符号链接存在 |
hard |
确保硬链接存在 |
absent |
确保路径不存在(删除文件或目录) |
touch |
创建空文件或更新时间戳 |
ansible.builtin.apt / yum / dnf
# apt(Debian/Ubuntu)
- name: 安装 Nginx
ansible.builtin.apt:
name:
- nginx
- python3-pip
state: present
update_cache: true
cache_valid_time: 3600
# 安装指定版本
- name: 安装指定版本的 Python
ansible.builtin.apt:
name: python3.11=3.11.0-1
state: present
# yum(CentOS/RHEL 7)
- name: 安装依赖
ansible.builtin.yum:
name: "@Development Tools"
state: present
# dnf(Fedora/CentOS 8+)
- name: 安装软件包
ansible.builtin.dnf:
name: python3
state: latest
apt 模块 state 字段:
| 值 | 说明 |
|---|---|
present |
确保安装,版本不限 |
latest |
确保安装最新版本 |
absent |
确保卸载 |
build-dep |
安装包的构建依赖 |
ansible.builtin.service
- name: 启动并设置 Nginx 开机自启
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: 重启应用(用于 Handler)
ansible.builtin.service:
name: myapp
state: restarted
| 参数 | 说明 |
|---|---|
name |
服务名称 |
state |
started / stopped / restarted / reloaded |
enabled |
是否开机自启(true/false) |
ansible.builtin.user / group
- name: 创建应用运行组
ansible.builtin.group:
name: appgroup
gid: 1500
system: true
- name: 创建应用运行用户
ansible.builtin.user:
name: appuser
uid: 1500
group: appgroup
groups:
- sudo
shell: /bin/bash
home: /opt/app
create_home: true
system: false
password: "{{ vault_app_password | password_hash('sha512') }}"
| 参数 | 说明 |
|---|---|
name |
用户/组名称 |
state |
present(创建)或 absent(删除) |
uid / gid |
指定 UID/GID |
group |
主组 |
groups |
附加组列表 |
shell |
登录 Shell |
home |
家目录路径 |
system |
是否为系统用户 |
password |
加密后的密码哈希 |
ansible.builtin.git
- name: 克隆应用代码
ansible.builtin.git:
repo: "https://github.com/org/myapp.git"
dest: /opt/app/releases/{{ app_version }}
version: "{{ app_version }}" # tag、branch 或 commit hash
depth: 1 # 浅克隆,加速
force: false # 是否丢弃本地修改
register: git_result
notify: restart app
| 参数 | 说明 |
|---|---|
repo |
仓库 URL |
dest |
本地目标路径 |
version |
分支、tag 或 commit hash,默认 HEAD |
depth |
浅克隆深度 |
key_file |
SSH 私钥路径(私有仓库) |
accept_hostkey |
自动接受未知 SSH 主机密钥 |
update |
目标目录已存在时是否更新,默认 true |
community.docker.docker_container
- name: 启动 Redis 容器
community.docker.docker_container:
name: redis
image: redis:7-alpine
state: started
restart_policy: unless-stopped
ports:
- "127.0.0.1:6379:6379"
volumes:
- /opt/redis/data:/data
env:
REDIS_PASSWORD: "{{ vault_redis_password }}"
command: "redis-server --requirepass {{ vault_redis_password }}"
networks:
- name: app-network
| 参数 | 说明 |
|---|---|
name |
容器名称 |
image |
镜像名称和标签 |
state |
started / stopped / absent / present |
restart_policy |
重启策略 |
ports |
端口映射列表 |
volumes |
卷挂载列表 |
env |
环境变量字典 |
networks |
网络列表 |
pull |
每次运行时是否拉取最新镜像,默认 missing |
变量系统
变量优先级(从低到高)
优先级越高,值越能覆盖低优先级的同名变量:
| 优先级 | 来源 |
|---|---|
| 1 | role 默认值(roles/role_name/defaults/main.yml) |
| 2 | inventory 文件变量 |
| 3 | inventory group_vars/all |
| 4 | inventory group_vars/<group_name> |
| 5 | inventory host_vars/<host_name> |
| 6 | playbook group_vars/all |
| 7 | playbook group_vars/<group_name> |
| 8 | playbook host_vars/<host_name> |
| 9 | 主机 facts(由 gather_facts 收集) |
| 10 | Play 级别 vars |
| 11 | Play 级别 vars_files |
| 12 | Play 级别 vars_prompt |
| 13 | Task 级别 vars |
| 14 | role 变量(roles/role_name/vars/main.yml) |
| 15 | block 级别 vars |
| 16 | include_vars 加载的变量 |
| 17 | set_fact / register |
| 18 | role 参数 / include_role 参数 |
| 19 | include_tasks 参数 |
| 20 | 命令行 -e / --extra-vars(最高) |
group_vars 和 host_vars 目录
inventory/
hosts.yml
group_vars/
all.yml # 所有主机共享变量
all/
main.yml
vault.yml # ansible-vault 加密的敏感变量
webservers.yml # 仅 webservers 组的变量
dbservers/
main.yml
vault.yml
host_vars/
web-01.example.com.yml # 仅 web-01 的变量
group_vars/all.yml 示例:
# 所有主机的公共变量
timezone: Asia/Shanghai
ntp_server: ntp.aliyun.com
log_level: info
vars_files 加载
- name: 部署应用
hosts: webservers
vars_files:
- vars/common.yml
- vars/{{ env }}.yml # 动态加载(根据 env 变量选择文件)
- "vars/secrets.yml"
ansible-vault 加密敏感变量
# 加密整个文件
ansible-vault encrypt vars/secrets.yml
# 解密文件
ansible-vault decrypt vars/secrets.yml
# 查看加密文件内容
ansible-vault view vars/secrets.yml
# 编辑加密文件
ansible-vault edit vars/secrets.yml
# 加密单个字符串(嵌入普通 YAML 文件)
ansible-vault encrypt_string 'my_secret_password' --name 'db_password'
# 执行 Playbook 时提供密码
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
加密文件内容示例(vars/secrets.yml):
$ANSIBLE_VAULT;1.1;AES256
63303831303566336465396561396531303466306365326665313565353065323...
Roles
Role 目录结构
roles/
myapp/
tasks/
main.yml # 主 Task 文件,其他文件可通过 include 引入
install.yml
configure.yml
handlers/
main.yml # Handler 定义
templates/
nginx.conf.j2 # Jinja2 模板文件
app.conf.j2
files/
app.service # 静态文件(copy 模块使用)
vars/
main.yml # Role 变量(高优先级,一般不被覆盖)
defaults/
main.yml # Role 默认变量(最低优先级,允许使用者覆盖)
meta/
main.yml # Role 元数据(依赖声明)
README.md
defaults/main.yml 示例:
# 使用者可以在 group_vars 或 playbook 中覆盖这些值
app_port: 8000
app_workers: 4
app_log_level: info
meta/main.yml 示例:
galaxy_info:
author: yourname
description: Deploy FastAPI app with Supervisor
min_ansible_version: "2.10"
dependencies:
- role: common
- role: nginx
vars:
nginx_listen_port: 80
创建 Role
# 使用 ansible-galaxy 初始化 Role 目录结构
ansible-galaxy init roles/myapp
# 初始化后生成完整目录结构
requirements.yml 依赖管理
# requirements.yml
---
roles:
# 从 Ansible Galaxy 安装
- name: geerlingguy.nginx
version: "3.2.0"
# 从 Git 仓库安装
- name: internal-common
src: git+https://github.com/org/ansible-common.git
version: main
scm: git
collections:
- name: community.docker
version: ">=3.0.0"
- name: amazon.aws
version: "6.0.0"
# 安装 requirements.yml 中的依赖
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml
实战场景
批量部署 Python 应用(FastAPI + Supervisor)
# playbooks/deploy_fastapi.yml
---
- name: 部署 FastAPI 应用
hosts: webservers
become: true
vars:
app_name: myapi
app_version: "{{ version | default('main') }}"
app_dir: "/opt/{{ app_name }}"
app_user: appuser
app_port: 8000
app_workers: 4
tasks:
- name: 确保应用用户存在
ansible.builtin.user:
name: "{{ app_user }}"
system: true
shell: /bin/false
home: "{{ app_dir }}"
create_home: false
- name: 创建应用目录结构
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
mode: "0755"
loop:
- "{{ app_dir }}"
- "{{ app_dir }}/releases"
- "{{ app_dir }}/shared/logs"
- "{{ app_dir }}/shared/config"
- name: 拉取最新代码
ansible.builtin.git:
repo: "https://github.com/org/{{ app_name }}.git"
dest: "{{ app_dir }}/releases/{{ app_version }}"
version: "{{ app_version }}"
depth: 1
register: git_result
- name: 创建虚拟环境并安装依赖
ansible.builtin.pip:
requirements: "{{ app_dir }}/releases/{{ app_version }}/requirements.txt"
virtualenv: "{{ app_dir }}/venv"
virtualenv_python: python3
- name: 更新 current 符号链接
ansible.builtin.file:
src: "{{ app_dir }}/releases/{{ app_version }}"
dest: "{{ app_dir }}/current"
state: link
notify: restart app
- name: 部署 Supervisor 配置
ansible.builtin.template:
src: templates/supervisor.conf.j2
dest: "/etc/supervisor/conf.d/{{ app_name }}.conf"
mode: "0644"
notify: reload supervisor
- name: 确保 Supervisor 运行
ansible.builtin.service:
name: supervisor
state: started
enabled: true
handlers:
- name: reload supervisor
ansible.builtin.command: supervisorctl reread
notify: update supervisor
- name: update supervisor
ansible.builtin.command: supervisorctl update
- name: restart app
ansible.builtin.supervisorctl:
name: "{{ app_name }}"
state: restarted
Supervisor 配置模板(templates/supervisor.conf.j2):
[program:{{ app_name }}]
command={{ app_dir }}/venv/bin/uvicorn main:app --host 0.0.0.0 --port {{ app_port }} --workers {{ app_workers }}
directory={{ app_dir }}/current
user={{ app_user }}
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile={{ app_dir }}/shared/logs/app.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
environment=APP_ENV="{{ env }}"
配置 Nginx + SSL
# tasks/nginx_ssl.yml
- name: 安装 Certbot
ansible.builtin.apt:
name:
- certbot
- python3-certbot-nginx
state: present
- name: 申请 Let's Encrypt 证书
ansible.builtin.command:
cmd: >
certbot certonly --nginx
-d {{ domain }}
--non-interactive
--agree-tos
--email {{ admin_email }}
creates: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
notify: reload nginx
- name: 部署 Nginx 站点配置
ansible.builtin.template:
src: templates/nginx_ssl.conf.j2
dest: "/etc/nginx/sites-available/{{ domain }}"
mode: "0644"
notify: reload nginx
- name: 启用站点
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ domain }}"
dest: "/etc/nginx/sites-enabled/{{ domain }}"
state: link
notify: reload nginx
- name: 配置证书自动续期 cron
ansible.builtin.cron:
name: "certbot renew"
hour: "3"
minute: "30"
weekday: "1"
job: "certbot renew --quiet && systemctl reload nginx"
踩坑与注意事项
command vs shell 的区别
command 模块不通过 Shell 解释器执行,因此:
| 特性 | command | shell |
|---|---|---|
| 管道符 | | 不支持 | 支持 |
| 重定向 > >> | 不支持 | 支持 |
| 变量展开 $ | 仅 Ansible 变量 | 支持 Shell 环境变量 |
| 通配符 * ? | 不支持 | 支持 |
| 安全性 | 更高 | 较低(注意注入) |
原则:优先用 command,仅在需要管道或重定向时才用 shell。
幂等性设计
Ansible 的核心价值是幂等性,即多次运行结果相同。设计 Task 时注意:
# 错误:每次都会执行,无法判断是否已完成
- name: 初始化数据库
ansible.builtin.command: python manage.py migrate
# 正确:通过 creates 标志或 register + when 实现幂等
- name: 检查迁移标志
ansible.builtin.stat:
path: /opt/app/.migrated
register: migrated
- name: 初始化数据库(仅首次)
ansible.builtin.command:
cmd: python manage.py migrate
chdir: /opt/app
when: not migrated.stat.exists
- name: 创建迁移完成标志
ansible.builtin.file:
path: /opt/app/.migrated
state: touch
when: not migrated.stat.exists
避免以下反幂等模式:
- 不加
creates的command/shell - 用
lineinfile但没有正确设置regexp(会重复追加) template中使用每次都变化的值(如时间戳)
become 提权失败排查
# 测试 sudo 权限
ansible webservers -m command -a "whoami" --become -v
# 常见原因 1:目标用户没有 sudo 权限
# 解决:在 /etc/sudoers 中添加
echo "ansible_user ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/ansible
# 常见原因 2:sudo 需要密码但未提供
# 解决:执行时添加 --ask-become-pass 或设置 NOPASSWD
ansible-playbook site.yml --ask-become-pass
# 常见原因 3:become_method 不对(某些系统用 su 而非 sudo)
- name: 提权执行
ansible.builtin.command: whoami
become: true
become_method: sudo # 或 su、doas、pbrun 等
become_user: root
调试常用命令
# 检查 Inventory 解析结果
ansible-inventory -i inventory/ --list
# 测试主机连通性
ansible all -m ping -i inventory/
# 列出所有 hosts 和 facts
ansible webservers -m setup | grep ansible_distribution
# 空运行(不真正执行,--check)
ansible-playbook site.yml --check
# 显示差异(--diff,配合 template/copy 使用)
ansible-playbook site.yml --check --diff
# 仅执行特定 Tag
ansible-playbook site.yml --tags "deploy,nginx"
# 跳过特定 Tag
ansible-playbook site.yml --skip-tags "slow_tasks"
# 从特定 Task 开始执行
ansible-playbook site.yml --start-at-task="部署应用配置"
# 逐步确认执行
ansible-playbook site.yml --step
# 增加详细输出
ansible-playbook site.yml -v # -vv -vvv 逐级增加
最佳实践
目录结构:遵循 Ansible 推荐布局
project/
├── ansible.cfg # 项目级配置(inventory 路径、roles 路径等)
├── inventory/
│ ├── production/
│ │ ├── hosts # 生产环境主机
│ │ └── group_vars/
│ │ ├── all.yml # 所有主机共用变量
│ │ └── webservers.yml
│ └── staging/
│ └── hosts
├── roles/
│ ├── common/ # 基础配置(NTP、用户、防火墙)
│ ├── nginx/ # Nginx 角色
│ └── app/ # 应用部署角色
├── site.yml # 主 Playbook(入口)
├── webservers.yml # 仅针对 webservers 的 Playbook
└── requirements.yml # Ansible Galaxy 依赖声明
Ansible Vault:加密敏感变量
不要在 Git 中存储明文密码、API Key 等敏感信息:
# 加密整个文件
ansible-vault encrypt vars/secrets.yml
# 编辑加密文件
ansible-vault edit vars/secrets.yml
# 加密单个字符串(嵌入到普通 YAML 中)
ansible-vault encrypt_string 'mysecretpassword' --name 'db_password'
# 执行时提供密码文件
ansible-playbook site.yml --vault-password-file ~/.vault_pass
在 vars/secrets.yml 中:
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439...(加密后的内容)
幂等性设计原则
- 优先使用声明式模块(
file、template、apt、service)而非command/shell - 使用
command/shell时配合creates、removes或changed_when实现幂等 - 避免
command: echo "something"这类永远 changed 的写法
# 错误:每次都 changed,破坏幂等性
- name: 初始化数据库
ansible.builtin.command: python manage.py migrate
# 正确:通过返回码判断是否真正有变更
- name: 初始化数据库
ansible.builtin.command: python manage.py migrate
register: migrate_result
changed_when: "'No migrations to apply' not in migrate_result.stdout"
滚动更新:serial + 健康检查
- name: 滚动更新 Web 服务器(每次 20%)
hosts: webservers
serial: "20%" # 或 serial: 2(绝对数量)
max_fail_percentage: 0 # 任意主机失败立即终止
tasks:
- name: 从负载均衡摘除节点
community.general.haproxy:
state: disabled
host: "{{ inventory_hostname }}"
socket: /var/run/haproxy/admin.sock
- name: 部署新版本
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: "{{ app_version }}"
- name: 健康检查
ansible.builtin.uri:
url: "http://localhost:8000/health"
status_code: 200
retries: 5
delay: 10
- name: 重新加入负载均衡
community.general.haproxy:
state: enabled
host: "{{ inventory_hostname }}"
socket: /var/run/haproxy/admin.sock
性能优化
| 优化项 | 配置 | 效果 |
|---|---|---|
| 关闭 gather_facts | gather_facts: false(不需要 facts 时) |
每台主机节省 1–3 秒 |
| 增加并发数 | ansible.cfg: forks = 20(默认 5) |
并发执行更多主机 |
| SSH 连接复用 | ssh_args = -o ControlMaster=auto -o ControlPersist=60s |
减少 SSH 握手 |
| 使用 Mitogen | strategy_plugins = /path/mitogen/ansible_mitogen/plugins/strategy |
整体提速 3–7 倍 |
| Pipelining | pipelining = True(需目标 sudoers 配置 requiretty 已禁用) |
减少 SSH 往返 |
常见陷阱
陷阱:任务幂等性破坏——每次都触发 changed
现象: Playbook 每次运行相同任务都报 changed,重复执行会触发不必要的 handler(如重启服务)。
原因: 使用 shell/command 模块执行的命令无状态感知,Ansible 无法判断是否需要执行,默认标记为 changed。
解决: 优先用内置模块(copy、template、lineinfile、service),它们原生支持幂等性。必须用 shell 时,配合 creates 或 changed_when: false:
- name: 初始化数据库(仅首次)
shell: init-db.sh
args:
creates: /var/lib/db/.initialized # 文件存在则跳过
陷阱:become: true 与 SSH 转发冲突
现象: 使用 become: true 提权后,任务中的 git 或 ssh 操作报权限错误,无法访问私钥。
原因: become 切换用户后,SSH agent forwarding 的 socket 路径属于原用户,新用户无读取权限。
解决: 避免在 become 后执行需要 SSH 认证的操作;改用 deploy key 或在目标机器上配置 HTTPS token 认证;或用 become_flags: -E 保留环境变量(需 sudo 配置允许)。
陷阱:变量优先级混乱导致意外覆盖
现象: 在 group_vars 中定义的变量被某个任务中的 set_fact 覆盖,后续任务使用了错误值。
原因: Ansible 变量有 22 个优先级层级,set_fact 的优先级高于 group_vars,容易在调试时留下副作用。
解决: 了解优先级顺序(extra vars > set_fact > role vars > group_vars),生产 Playbook 避免用 set_fact 修改全局变量,改用 register 保存局部结果。
参见
- Kubernetes基础 — Ansible 与 Kubernetes 结合部署应用
- Docker Compose完全指南 — 用 Ansible 管理 Docker Compose 应用
- Prometheus与Grafana监控 — 用 Ansible 自动化部署监控栈
- Git进阶指南 — Ansible Playbook 的版本控制工作流