Shell 自动化进阶:Ansible 运维编排,从手工 SSH 到声明式基础设施的蜕变
Shell 自动化进阶Ansible 运维编排从手工 SSH 到声明式基础设施的蜕变一、SSH 循环的末日为什么 Shell 脚本管不好 100 台服务器你一定写过这种脚本for host in $(cat hosts.txt); do ssh $host apt update apt install -y nginx done当服务器只有 10 台时这个脚本跑得很好。但当服务器变成 100 台、1000 台时问题接踵而至某台机器 SSH 超时脚本卡住某台机器 apt 源不可用安装失败但脚本继续执行你不知道哪些机器成功了、哪些失败了只能逐台检查。更糟糕的是下次升级 Nginx 版本时你又得写一个类似的脚本但这次要加上先停服务再升级的逻辑——脚本越来越长越来越不可维护。Ansible 的核心价值在于用声明式的方式描述服务器应该是什么状态而不是用命令式的方式描述在服务器上执行什么操作。你声明所有 Web 服务器应该安装 Nginx 1.24 且服务处于运行状态Ansible 负责判断当前状态与期望状态的差异只执行必要的变更。就像训练 K8s我的金毛犬你不需要告诉它每一步怎么走只需要告诉它去把飞盘叼回来它会自己规划路线。二、Ansible 运维编排架构从 Inventory 到 Playbook 的声明式管理Ansible 的核心流程是定义资产清单 → 编写 Playbook 描述期望状态 → 执行并收集结果 → 验证幂等性。flowchart TD A[Ansible 运维编排] -- B[Inventory 资产清单] A -- C[Playbook 编排] A -- D[Role 角色复用] B -- B1[静态 Inventory: INI/YAML] B -- B2[动态 Inventory: 脚本/插件] B1 -- B1a[按功能分组: web/db/cache] B1 -- B1b[按环境分组: prod/staging] B2 -- B2a[从 CMDB 拉取] B2 -- B2b[从云 API 查询] C -- C1[核心模块] C -- C2[条件与循环] C -- C3[错误处理] C1 -- C1a[package: 包管理] C1 -- C1b[service: 服务管理] C1 -- C1c[template: 配置渲染] C1 -- C1d[file: 文件管理] C1 -- C1e[shell/command: 兜底] C2 -- C2a[when: 条件执行] C2 -- C2b[loop: 循环操作] C2 -- C2c[register: 结果注册] C3 -- C3a[block/rescue/always] C3 -- C3b[failed_when: 自定义失败] C3 -- C3c[changed_when: 幂等控制] D -- D1[目录结构规范] D -- D2[变量优先级] D -- D3[依赖管理] D1 -- D1a[tasks/: 任务列表] D1 -- D1b[handlers/: 触发器] D1 -- D1c[templates/: 模板文件] D1 -- D1d[defaults/: 默认变量] D1 -- D1e[vars/: 覆盖变量] D1 -- D1f[meta/: 角色依赖] style B fill:#e1f5fe style C fill:#fff3e0 style D fill:#e8f5e92.1 Inventory 资产清单与动态发现# inventory/production.yml — 生产环境资产清单 # 设计意图按功能和环境分组管理服务器 # 支持变量继承和动态发现 all: children: # --- 按功能分组 --- web: hosts: web-01.example.com: web-02.example.com: web-03.example.com: vars: nginx_worker_processes: auto nginx_worker_connections: 65535 db: hosts: db-master.example.com: mysql_role: master db-slave-01.example.com: mysql_role: slave mysql_master_host: db-master.example.com db-slave-02.example.com: mysql_role: slave mysql_master_host: db-master.example.com vars: mysql_version: 8.0 mysql_max_connections: 500 cache: hosts: redis-01.example.com: redis_role: master redis-02.example.com: redis_role: slave redis_master_host: redis-01.example.com vars: redis_maxmemory: 4gb # --- 按环境分组 --- production: children: web: db: cache: vars: env: production deploy_user: deploy app_root: /opt/app # --- 特殊分组监控 --- monitoring: hosts: prometheus.example.com: grafana.example.com: vars: retention_days: 30 # --- 全局变量 --- vars: ansible_user: deploy ansible_ssh_private_key_file: ~/.ssh/deploy_key ansible_python_interpreter: /usr/bin/python3 # 连接超时和并发配置 ansible_timeout: 30 ansible_ssh_common_args: -o StrictHostKeyCheckingno -o UserKnownHostsFile/dev/null# inventory/dynamic_aws.yml — AWS 动态 Inventory # 设计意图从 AWS EC2 API 动态获取服务器列表 # 自动按 Tag 分组无需手动维护 hosts 文件 plugin: aws_ec2 regions: - cn-north-1 - cn-northwest-1 # 按 Tag 分组 keyed_groups: - key: tags.Role prefix: role - key: tags.Environment prefix: env - key: tags.App prefix: app # 过滤条件只获取运行中的实例 filters: instance-state-name: running # 组合变量 compose: ansible_host: private_ip_address # 使用内网 IP 连接2.2 Playbook 编排Nginx 部署与配置管理# playbooks/nginx-deploy.yml — Nginx 部署 Playbook # 设计意图声明式管理 Nginx 的安装、配置、服务状态 # 确保幂等性——多次执行结果一致 --- - name: 部署 Nginx Web 服务器 hosts: web become: true serial: 1 # 滚动部署每次只更新一台降低风险 pre_tasks: # 部署前健康检查 - name: 检查目标主机连通性 ping: register: ping_result - name: 检查磁盘空间 command: df -h /opt register: disk_check changed_when: false failed_when: disk_check.stdout_lines[1].split()[3] | regex_replace([A-Z], ) | int 1024 tasks: # --- 1. 包管理 --- - name: 添加 Nginx 官方 APT 源 apt_repository: repo: deb [archamd64] http://nginx.org/packages/ubuntu {{ ansible_lsb.codename }} nginx state: present update_cache: true when: ansible_os_family Debian - name: 安装 Nginx {{ nginx_version | default(1.24.0) }} apt: name: nginx{{ nginx_version | default(1.24.0) }}-* state: present update_cache: true when: ansible_os_family Debian register: nginx_install # --- 2. 配置管理 --- - name: 创建 Nginx 配置目录 file: path: {{ item }} state: directory owner: root group: root mode: 0755 loop: - /etc/nginx/conf.d - /etc/nginx/ssl - /var/log/nginx - name: 渲染 Nginx 主配置文件 template: src: templates/nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: 0644 validate: /usr/sbin/nginx -t -c %s notify: reload nginx # validate 参数确保配置语法正确后才写入 - name: 渲染站点配置文件 template: src: templates/site.conf.j2 dest: /etc/nginx/conf.d/{{ item.name }}.conf owner: root group: root mode: 0644 validate: /usr/sbin/nginx -t -c %s loop: {{ nginx_sites }} notify: reload nginx # --- 3. SSL 证书管理 --- - name: 部署 SSL 证书 copy: src: ssl/{{ item }}.pem dest: /etc/nginx/ssl/{{ item }}.pem owner: root group: root mode: 0600 loop: {{ nginx_ssl_domains | default([]) }} notify: reload nginx - name: 部署 SSL 私钥 copy: src: ssl/{{ item }}.key dest: /etc/nginx/ssl/{{ item }}.key owner: root group: root mode: 0400 loop: {{ nginx_ssl_domains | default([]) }} notify: reload nginx # --- 4. 服务管理 --- - name: 确保 Nginx 服务已启动并开机自启 service: name: nginx state: started enabled: true # --- 5. 部署后验证 --- - name: 等待 Nginx 端口就绪 wait_for: port: 80 host: {{ ansible_host }} delay: 2 timeout: 30 - name: 健康检查 uri: url: http://{{ ansible_host }}/healthz status_code: 200 timeout: 5 register: health_check retries: 3 delay: 5 until: health_check.status 200 post_tasks: - name: 部署完成通知 debug: msg: Nginx {{ nginx_version | default(1.24.0) }} 部署完成于 {{ inventory_hostname }} # --- 触发器 --- handlers: - name: reload nginx service: name: nginx state: reloaded # 仅当配置文件变更时触发且在所有 task 执行完后才执行 # 多次变更只触发一次2.3 Role 角色复用与目录规范# roles/nginx/defaults/main.yml — Nginx Role 默认变量 # 设计意图提供合理的默认值允许在 Inventory 或 Playbook 中覆盖 nginx_version: 1.24.0 nginx_worker_processes: auto nginx_worker_connections: 1024 nginx_worker_rlimit_nofile: 65535 # 日志配置 nginx_access_log: /var/log/nginx/access.log nginx_error_log: /var/log/nginx/error.log warn # Gzip 配置 nginx_gzip: true nginx_gzip_types: - text/plain - text/css - application/json - application/javascript # 站点配置列表 nginx_sites: [] # SSL 域名列表 nginx_ssl_domains: []# roles/nginx/ 目录结构 roles/nginx/ ├── defaults/ │ └── main.yml # 默认变量最低优先级 ├── vars/ │ └── main.yml # 角色内部变量不可覆盖 ├── tasks/ │ └── main.yml # 任务入口 ├── handlers/ │ └── main.yml # 触发器 ├── templates/ │ ├── nginx.conf.j2 # Nginx 主配置模板 │ └── site.conf.j2 # 站点配置模板 ├── files/ │ └── ssl/ # SSL 证书文件 ├── meta/ │ └── main.yml # 角色依赖 └── tests/ ├── inventory # 测试用 Inventory └── test.yml # 测试 Playbook{# roles/nginx/templates/nginx.conf.j2 — Nginx 主配置模板 #} {# 设计意图基于变量动态渲染 Nginx 配置支持不同环境差异化 #} user nginx; worker_processes {{ nginx_worker_processes }}; worker_rlimit_nofile {{ nginx_worker_rlimit_nofile }}; error_log {{ nginx_error_log }}; pid /var/run/nginx.pid; events { worker_connections {{ nginx_worker_connections }}; multi_accept on; use epoll; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # 日志格式 log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent rt$request_time; access_log {{ nginx_access_log }} main; # 性能优化 sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; server_tokens off; {% if nginx_gzip %} # Gzip 压缩 gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_min_length 256; gzip_types {{ nginx_gzip_types | join( ) }}; {% endif %} # 包含站点配置 include /etc/nginx/conf.d/*.conf; }四、边界分析与架构权衡幂等性的边界Ansible 的核心设计是幂等——多次执行结果一致。但 shell/command 模块天然不具备幂等性。如果必须使用 shell 模块务必用 changed_when 和 creates/removes 参数控制幂等性判断。能用模块就用模块shell 是最后的兜底手段。串行部署 vs 并行速度serial: 1 实现滚动部署但速度慢100 台服务器逐台部署需要很长时间。serial: 30% 实现百分比滚动兼顾速度和安全性。关键服务建议 serial: 1非关键服务可以 serial: 25%。就像带 K8s 过马路必须一步一步确认安全不能让它撒欢跑。变量优先级的陷阱Ansible 变量优先级从低到高有 22 层。最常见的坑是在 defaults/main.yml 中定义了默认值又在 vars/main.yml 中定义了同名变量后者无法被 Inventory 覆盖。原则defaults 放可覆盖的默认值vars 放角色内部不可覆盖的常量。大规模 Inventory 的性能当 Inventory 超过 1000 台主机时Ansible 的 SSH 连接建立和事实采集gather_facts会消耗大量时间。建议关闭不需要的 gather_factsgather_facts: no使用 Mitogen 插件加速 SSH 连接使用 ansible-pull 模式替代 push 模式主机主动拉取 Playbook。五、总结Ansible 通过声明式 Playbook 实现了运维自动化——你描述期望状态Ansible 负责执行变更。落地建议用 Role 组织可复用的运维逻辑defaults 放默认值、vars 放常量关键服务用 serial: 1 滚动部署配置文件用 template validate 确保语法正确后才写入shell 模块必须配合 changed_when 保证幂等大规模场景考虑 Mitogen 加速或 ansible-pull 模式。从 SSH 循环到 Ansible Playbook不是工具的升级而是运维思维的进化——从告诉机器怎么做到告诉机器应该是什么样。