跳到主要内容

生产规范与 Web 节点实战

本章把前面所有能力收束成一套可交付的 Ansible 工程:目录规范、命名约定、变更流程,以及「标准 Web 节点初始化」综合实战。完成后,你应能在 staging 上完整跑通,并具备迁到生产的检查清单。


1. 生产级目录规范

ansible-for-ops/
├── README.md
├── ansible.cfg
├── inventory/
│ ├── staging/
│ │ ├── hosts.ini
│ │ └── group_vars/ # 可选:环境局部变量
│ └── prod/
│ ├── hosts.ini
│ └── group_vars/
├── group_vars/ # 跨环境共享(谨慎)
│ └── all.yml
├── host_vars/
├── playbooks/
│ ├── site.yml # 总入口
│ ├── web.yml
│ └── adhoc/ # 临时排查 play,定期清理
├── roles/
│ ├── common/
│ └── nginx/
├── files/ # 跨角色静态资源(可选)
└── scripts/ # 辅助脚本:加密、包装 CI 调用等

ansible.cfg(生产向)

[defaults]
inventory = ./inventory/staging/hosts.ini
roles_path = ./roles
remote_user = ops
private_key_file = ~/.ssh/ansible_ed25519
host_key_checking = True
retry_files_enabled = True
interpreter_python = auto_silent
forks = 20
timeout = 30
ansible_managed = Managed by Ansible. Source in git: ansible-for-ops
stdout_callback = default

[privilege_escalation]
become = False
become_method = sudo
become_ask_pass = False

[ssh_connection]
pipelining = True

生产把 host_key_checking 打开,并只在通过云控制台或主机管理员核验指纹后,才写入 known_hosts;不要把未经核验的 ssh-keyscan 输出直接当作可信身份。默认不提权,每个需要修改系统状态的 play 明确声明 become: true


2. 命名与协作约定

对象约定
逻辑主机名web1web-a-01,稳定不变
组名小写,角色导向:web/db/lb
变量角色前缀:nginx_listen_port
Vault 键vault_ 前缀
Play name英文或中文短句,可检索
Git 分支main 保护;变更走 MR

README 必写:

  1. 如何安装依赖集合
  2. 如何拿到 Vault 密码(链到密码库,不写明文)
  3. staging / prod 的标准执行命令
  4. 回滚方式

3. 综合实战目标

web 组主机收敛到如下状态:

  1. 基础包就绪(curl/jq/ca-certificates 等)
  2. 时区 Asia/Shanghai(若集合可用)
  3. Nginx 安装、开机自启
  4. 主配置由模板管理,含 /healthz
  5. 首页展示主机名
  6. 二次 apply 基本无 changed
  7. 敏感示例变量来自 Vault(可演示假密码)

4. Inventory 示例

inventory/staging/hosts.ini

[web]
web1 ansible_host=10.0.1.11
web2 ansible_host=10.0.1.12

[staging:children]
web

[all:vars]
ansible_user=ops

group_vars 可放在仓库根或 inventory 目录下。示例使用仓库根:

group_vars/web/vars.yml

---
nginx_listen_port: 80
nginx_worker_processes: 2
nginx_server_name: "_"
nginx_root: /var/www/html
nginx_healthz_enable: true

common_packages:
- curl
- jq
- ca-certificates

common_timezone: Asia/Shanghai

# 由 vault 映射
demo_secret: "{{ vault_demo_secret }}"

group_vars/web/vault.yml(加密前内容):

---
vault_demo_secret: "staging-only-not-real"
ansible-vault encrypt group_vars/web/vault.yml

5. Role:common

roles/common/defaults/main.yml

---
common_timezone: Asia/Shanghai
common_packages:
- curl
- jq
- ca-certificates
common_manage_timezone: true

roles/common/tasks/main.yml

---
- name: 安装基线包
ansible.builtin.package:
name: "{{ common_packages }}"
state: present
tags: [common, packages]

- name: 设置时区
community.general.timezone:
name: "{{ common_timezone }}"
when: common_manage_timezone | bool
tags: [common]

community.general 时可临时:

- name: 设置时区(Debian 回退)
ansible.builtin.copy:
dest: /etc/timezone
content: "{{ common_timezone }}\n"
when:
- common_manage_timezone | bool
- ansible_os_family == "Debian"
notify: Reconfigure tzdata

6. Role:nginx(完整可跑)

roles/nginx/defaults/main.yml

---
nginx_package: nginx
nginx_service: nginx
nginx_listen_port: 80
nginx_worker_processes: 2
nginx_server_name: "_"
nginx_root: /var/www/html
nginx_healthz_enable: true
nginx_manage_main_config: true
nginx_start_service: true
nginx_index_content: |
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8"/>
<title>{{ inventory_hostname }}</title>
<body>
<h1>{{ inventory_hostname }}</h1>
<p>managed by ansible role nginx</p>
</body>
</html>

roles/nginx/templates/nginx.conf.j2

# {{ ansible_managed }}
worker_processes {{ nginx_worker_processes }};
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
server_tokens off;

server {
listen {{ nginx_listen_port }} default_server;
server_name {{ nginx_server_name }};
root {{ nginx_root }};

location / {
try_files $uri $uri/ =404;
}

{% if nginx_healthz_enable | bool %}
location /healthz {
access_log off;
return 200 'ok';
add_header Content-Type text/plain;
}
{% endif %}
}
}

roles/nginx/tasks/main.yml

---
- name: 安装 nginx
ansible.builtin.package:
name: "{{ nginx_package }}"
state: present
tags: [nginx, packages]

- name: 创建站点目录
ansible.builtin.file:
path: "{{ nginx_root }}"
state: directory
mode: "0755"
tags: [nginx, config]

- name: 部署首页
ansible.builtin.copy:
content: "{{ nginx_index_content }}"
dest: "{{ nginx_root }}/index.html"
mode: "0644"
tags: [nginx, content]

- name: 部署主配置
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
backup: true
validate: "/usr/sbin/nginx -t -c %s"
register: nginx_config
when: nginx_manage_main_config | bool
notify: Reload nginx
tags: [nginx, config]

- name: 确保服务运行
ansible.builtin.service:
name: "{{ nginx_service }}"
state: started
enabled: true
when: nginx_start_service | bool
tags: [nginx, services]

roles/nginx/handlers/main.yml

---
- name: Reload nginx
ansible.builtin.service:
name: "{{ nginx_service }}"
state: reloaded

7. Playbook 入口

playbooks/site.yml

---
- name: Baseline
hosts: all
become: true
serial: "50%"
roles:
- common

- name: Web nodes
hosts: web
become: true
serial: [1, "100%"]
max_fail_percentage: 50

pre_tasks:
- name: 确认 web 组非空
ansible.builtin.assert:
that:
- groups['web'] | length > 0

roles:
- nginx

tasks:
- name: 立即 flush handlers
ansible.builtin.meta: flush_handlers

- name: 健康检查
ansible.builtin.uri:
url: "http://127.0.0.1:{{ nginx_listen_port }}/healthz"
status_code: 200
retries: 8
delay: 2
register: health
until: health.status == 200

playbooks/web.yml(只动 web):

---
- name: Web only
hosts: web
become: true
serial: 1
roles:
- nginx

8. 锁定依赖、静态检查与 CI

生产项目不要让每次执行都安装“当时最新”的 collection。把已经在 staging 验证过的版本写入仓库,并在控制节点与 CI 使用同一份清单。

collections/requirements.yml

---
collections:
- name: community.general
version: "9.5.0"
- name: ansible.posix
version: "1.6.2"

版本号应替换为团队实际验证过的版本;升级 collection 时,先在独立分支和 staging 验证,再提交这个文件。安装与基础静态检查:

ansible-galaxy collection install -r collections/requirements.yml
pipx install ansible-lint
ansible-lint
ansible-playbook -i inventory/staging/hosts.ini playbooks/site.yml --syntax-check

CI 不应直接对生产 inventory 做真实变更。最小流水线可使用测试 inventory,依次安装依赖、运行 ansible-lint--syntax-check,再执行 --check --diff;需要运行时验证的 Role 可在后续引入 Molecule 和临时虚拟机。

set -euo pipefail
ansible-galaxy collection install -r collections/requirements.yml
ansible-lint
ansible-playbook -i inventory/staging/hosts.ini playbooks/site.yml --syntax-check
ansible-playbook -i inventory/staging/hosts.ini playbooks/site.yml --check --diff

9. 标准执行流程(照着跑)

cd ~/ansible-for-ops

# 0. 依赖(版本由仓库锁定)
ansible-galaxy collection install -r collections/requirements.yml

# 1. 连通
ansible -i inventory/staging/hosts.ini web -m ping

# 2. 预览
ansible-playbook -i inventory/staging/hosts.ini playbooks/site.yml \
--ask-vault-pass --check --diff

# 3. 灰度一台
ansible-playbook -i inventory/staging/hosts.ini playbooks/site.yml \
--ask-vault-pass -l web1

# 4. 验证
ansible -i inventory/staging/hosts.ini web1 -m uri \
-a "url=http://127.0.0.1/healthz return_content=true"

# 5. 全量 staging
ansible-playbook -i inventory/staging/hosts.ini playbooks/site.yml \
--ask-vault-pass

# 6. 幂等确认
ansible-playbook -i inventory/staging/hosts.ini playbooks/site.yml \
--ask-vault-pass

验收标准

  • ping 全绿
  • 第一次 apply 后 /healthz 返回 ok
  • 第二次 apply 无异常 changed(允许 facts 相关的可解释差异)
  • nginx -t 通过;backup 在变更时生成
  • vault 文件为密文且 playbook 能解密引用
  • 故意断掉一台健康检查时,滚动策略符合预期

10. 回滚策略(务实)

Ansible 不提供自动「state 回滚」,靠工程纪律:

手段做法
Git revert还原模板/变量,再 apply
backup: true变更时生成带时间戳的备份;注册 task 结果中的 backup_file 并记录路径
分批serial 限制爆炸半径
蓝绿/双版本应用层保留 previous release 目录
变更记录MR + 执行日志 + 窗口时间

紧急回滚示例:

# 假设 Git revert 已完成
ansible-playbook -i inventory/prod/hosts.ini playbooks/web.yml \
--vault-password-file ~/.ansible/vault_prod \
--tags config -l web

backup: true 是额外保险,不是完整的发布回滚机制。需要固定、可预测的回退文件时,采用 Playbook 章节中“先保存旧文件、再变更”的方式;Nginx task 的 nginx_config.backup_file 只有文件实际变更时才存在。


11. 生产切换检查单

从 staging 到 prod 前:

  1. inventory 主机与业务确认一致
  2. prod Vault 密码与 staging 分离
  3. host_key_checking=True
  4. --check --diff 并由第二人审 diff
  5. serial1 起,观察指标
  6. 准备回滚 MR / 备份文件路径
  7. 通知相关值班
不要

在笔记本电脑上对生产全量 forks 拉满并行;不要用个人账号长期存产线 Vault 密码;不要在未限制 -l 时试验破坏性 tags。


12. 可选下一步

本系列收束于 Web 初始化。后续可按同一模式扩展:

方向做法
监控 Agent新增 roles/node_exporter,site.yml 挂上
应用发布roles/app:拉制品 → 切 symlink → 健康检查
AWX把现有 playbook 导入,补 RBAC/审批
CIMR 跑 --check,main 部署 staging

保持不变的是:Role 承载能力,Playbook 承载范围与节奏,Vault 承载秘密,Git 承载真相。


13. 系列回顾

能力文档
环境与连通01 安装与连通
主机与变量02 清单与变量
常用模块03 常用模块
编排语法04 Playbook
复用05 Role
变更护栏06 安全变更
密钥07 Vault
交付本文

返回 系列导读