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 失败重试次数,配合 untildelay 使用

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

避免以下反幂等模式:

  • 不加 createscommand / 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...(加密后的内容)

幂等性设计原则

  • 优先使用声明式模块(filetemplateaptservice)而非 command/shell
  • 使用 command/shell 时配合 createsremoveschanged_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
解决: 优先用内置模块(copytemplatelineinfileservice),它们原生支持幂等性。必须用 shell 时,配合 createschanged_when: false

- name: 初始化数据库(仅首次)
  shell: init-db.sh
  args:
    creates: /var/lib/db/.initialized  # 文件存在则跳过

陷阱:become: true 与 SSH 转发冲突

现象: 使用 become: true 提权后,任务中的 gitssh 操作报权限错误,无法访问私钥。
原因: 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 保存局部结果。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog