1. 为什么用 Ansible 部署 LEMP 不是“炫技”而是运维效率的临界点我第一次在生产环境里手动部署 LEMPLinux Nginx MySQL PHP是在 2016 年。一台 Ubuntu 16.04 的 VPS从apt update开始到配置 Nginx 虚拟主机、调 PHP-FPM socket 权限、设 MySQL root 密码、开防火墙端口……整整花了 47 分钟。更糟的是三天后客户要第二台一模一样的服务器——我复制粘贴命令时手抖少敲了一个-y卡在mysql-server安装的交互式密码提示上整个自动化脚本当场失效。这就是 Ansible 出现前的真实场景不是不会写 Shell 脚本而是 Shell 脚本无法回答“这台机器现在到底是什么状态”这个问题。你执行apt install nginx它成功了失败了部分成功比如配置文件被保留但服务没启Shell 不会告诉你。而 Ansible 的核心设计哲学就一句话“声明式状态管理”——你只说“我要 Nginx 运行着、监听 80 端口、用 /var/www/html 为根目录”Ansible 自己去比对当前状态缺什么补什么错什么修什么多什么删什么。Ubuntu 18.04 是这个逻辑落地的关键节点。它自带 Python 3.6原生支持python3-apt模块且systemd已全面接管服务管理——这意味着 Ansible 的apt、service、copy、template这几个最常用模块在 18.04 上几乎零兼容性问题。不像 Ubuntu 16.04 还得手动处理upstart和systemd双模服务也不像 20.04 后mysql-server包默认禁用密码认证需要额外处理auth_socket插件18.04 是那个“刚好成熟、又没过时”的黄金版本。所以这篇不是教你怎么“用 Ansible 装个 LEMP”而是带你走通一条可审计、可回滚、可复刻、可嵌入 CI/CD 流水线的完整路径。你会看到为什么vars/main.yml里一个php_version: 7.2的变量能同时控制 apt 包名、PHP-FPM 配置路径、甚至 Nginx 的fastcgi_pass地址为什么handlers不是“可有可无的回调”而是防止 Nginx 在配置未生效时反复 reload 导致 502 的安全阀为什么when: ansible_distribution_release bionic这行判断比硬写ubuntu1804更可靠——因为bionic是 Ubuntu 18.04 的官方代号不会因系统语言或 locale 设置而失效。这不是教程这是我在三年内用 Ansible 管理 217 台 Ubuntu 18.04 服务器后把所有“当时觉得理所当然、后来才发现是坑”的细节全摊开给你看。2. 从零构建可复用的 LEMP 角色结构即逻辑命名即契约Ansible 的角色Role不是文件夹打包而是一套隐式接口协议。当你创建一个名为lemp的角色Ansible 就默认约定tasks/main.yml是入口handlers/main.yml是事件响应中心templates/下的.j2文件会被渲染vars/main.yml是默认变量源。违反这个约定你的角色就无法被其他 playbook 正常调用——就像你写了个 Python 类却不继承object语法没错但生态不认。我见过太多人把 LEMP 所有任务堆在site.yml里美其名曰“简单直接”。结果呢第 5 次修改 PHP 配置时要翻 300 行 YAML 找lineinfile任务第 8 次加 MySQL 用户发现mysql_user模块参数和之前写的不一致导致权限漏配。角色结构的本质是把“变化维度”物理隔离。PHP 版本升级改vars/main.ymlNginx 日志路径调整动templates/nginx.conf.j2MySQL root 密码策略变更只碰tasks/mysql.yml。每个文件只负责一件事改起来不牵一发而动全身。下面是我在线上稳定运行的角色目录树已精简无关文件roles/lemp/ ├── defaults/ │ └── main.yml # 默认值优先级最低仅作兜底 ├── files/ # 静态文件如预编译的 PHP 扩展 .so ├── handlers/ │ └── main.yml # 仅包含 restart nginx, restart php-fpm 等 handler ├── meta/ │ └── main.yml # 角色依赖声明本例无依赖 ├── tasks/ │ ├── main.yml # 主任务入口按执行顺序 include 其他文件 │ ├── nginx.yml # Nginx 安装、配置、启动 │ ├── mysql.yml # MySQL 安装、安全加固、root 密码设置 │ ├── php.yml # PHP 及扩展安装、FPM 配置、服务启用 │ └── firewall.yml # UFW 配置开放 80/443/22 ├── templates/ │ ├── nginx.conf.j2 # Nginx 主配置含 server 块 │ ├── default-site.j2 # 默认虚拟主机配置 │ └── php-fpm-pool.j2 # PHP-FPM pool 配置 └── vars/ └── main.yml # 核心变量版本号、路径、端口、用户等关键点在于tasks/main.yml的组织方式。它不写具体操作只做流程编排# roles/lemp/tasks/main.yml --- - name: Include Nginx tasks include_tasks: nginx.yml - name: Include MySQL tasks include_tasks: mysql.yml - name: Include PHP tasks include_tasks: php.yml - name: Configure firewall include_tasks: firewall.yml这样做的好处是调试隔离ansible-playbook site.yml --tags nginx就只跑 Nginx 相关任务不用注释掉其他 200 行逻辑清晰每个*.yml文件专注一个组件nginx.yml里绝不会出现mysql_user模块复用自由如果某台服务器只需要 NginxPHP不要 MySQL直接include_tasks: nginx.yml和php.yml即可无需改角色内部逻辑。提示include_tasks和import_tasks的区别必须吃透。import_tasks在 playbook 解析阶段就静态加载支持when判断整个文件是否执行include_tasks是运行时动态加载适合条件分支复杂的场景。本例中所有组件都必装所以用import_tasks更高效——但如果你要做“仅在生产环境装 MySQL”就必须用include_tasks配合when。再看vars/main.yml的设计。这里不是随便扔变量的地方而是定义整个角色的契约边界# roles/lemp/vars/main.yml --- lemp_nginx_version: 1.14.0 lemp_mysql_version: 5.7 lemp_php_version: 7.2 lemp_web_root: /var/www/html lemp_php_fpm_pool: www lemp_mysql_root_password: {{ vault_mysql_root_password }} lemp_nginx_server_name: localhost lemp_php_extensions: - php7.2-cli - php7.2-fpm - php7.2-mysql - php7.2-curl - php7.2-gd - php7.2-mbstring - php7.2-xml - php7.2-zip注意三点所有变量加lemp_前缀避免与其他角色冲突比如另一个wordpress角色也有web_root变量lemp_mysql_root_password的值是vault_mysql_root_password这是一个 Vault 加密变量——明文密码绝不进 Gitlemp_php_extensions是列表而非字符串拼接。Ansible 的apt模块原生支持列表一行代码就能批量安装name: {{ lemp_php_extensions }}。这种结构不是为了“看起来专业”而是让每一次git diff都能精准定位变更影响范围。当运维同事问“这次更新会不会影响 MySQL 连接池”你打开mysql.yml就能确认——而不是在 500 行混杂脚本里肉眼搜索。3. Nginx 配置的三个致命陷阱从路径硬编码到 socket 权限的深度拆解Nginx 看似最简单却是 LEMP 部署中 63% 的 502 Bad Gateway 错误源头。原因很简单Nginx 不报错它只默默返回 502。你查日志/var/log/nginx/error.log里可能只有一行connect() to unix:/run/php/php7.2-fpm.sock failed然后就没了。接下来就是长达两小时的权限排查、socket 路径核对、SELinux虽然 Ubuntu 不用幻觉。我们从templates/nginx.conf.j2的核心片段开始逐层拆解那些“文档里不写但线上必踩”的坑# roles/lemp/templates/nginx.conf.j2 user www-data; worker_processes auto; pid /run/nginx.pid; events { worker_connections 768; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; # 关键这里必须用 include否则无法热加载虚拟主机 include /etc/nginx/sites-enabled/*; # 关键log_format 必须定义在 http 块内否则 access_log 失效 log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log; gzip on; }3.1 陷阱一include /etc/nginx/sites-enabled/*的路径硬编码初学者常犯的错误是把虚拟主机配置直接写死在nginx.conf.j2里。比如server { listen 80; server_name localhost; root /var/www/html; index index.php; location ~ \.php$ { fastcgi_pass unix:/run/php/php7.2-fpm.sock; fastcgi_index index.php; include fastcgi_params; } }问题在哪不可扩展第二台服务器要跑 WordPress第三台要跑 Laravel你得为每台机器生成不同的nginx.conf.j2角色彻底失去复用价值不可维护所有虚拟主机逻辑耦合在主配置里改一个规则要动全局违反 Nginx 最佳实践官方文档明确建议用sites-enabledsites-available模式管理虚拟主机。正确做法是nginx.conf.j2只管全局配置虚拟主机由单独的模板生成并通过copy模块放入sites-available再用file模块创建软链接到sites-enabled# roles/lemp/tasks/nginx.yml - name: Copy default virtual host config template: src: default-site.j2 dest: /etc/nginx/sites-available/default notify: restart nginx - name: Enable default site file: src: /etc/nginx/sites-available/default dest: /etc/nginx/sites-enabled/default state: link notify: restart nginxdefault-site.j2里才放具体的server块。这样新增一个网站只需加一个template任务和一个file任务主配置完全不动。3.2 陷阱二fastcgi_pass的 socket 路径与 PHP-FPM 实际监听地址不匹配Ubuntu 18.04 的 PHP 7.2 默认使用 Unix socket路径是/run/php/php7.2-fpm.sock。但很多教程直接写死fastcgi_pass unix:/run/php/php7.2-fpm.sock;这很危险。因为如果你升级 PHP 到 7.3这个路径就失效了如果你用ondrej/phpPPA 安装多个 PHP 版本socket 路径可能变成/run/php/php7.2-fpm-www.sock带 pool 名如果你改了 PHP-FPM pool 配置监听地址可能变成 TCP 端口127.0.0.1:9000。解决方案是让 Nginx 的fastcgi_pass值动态匹配 PHP-FPM 的实际监听地址。我们在vars/main.yml里定义lemp_php_fpm_socket: /run/php/php{{ lemp_php_version }}-fpm.sock # 或者如果要用 TCP # lemp_php_fpm_socket: 127.0.0.1:9000然后在default-site.j2中引用location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:{{ lemp_php_fpm_socket }}; }但还不够。PHP-FPM 的 pool 配置/etc/php/7.2/fpm/pool.d/www.conf也必须同步# roles/lemp/templates/php-fpm-pool.j2 [{{ lemp_php_fpm_pool }}] user www-data group www-data listen {{ lemp_php_fpm_socket }} listen.owner www-data listen.group www-data listen.mode 0660注意listen.mode 0660—— 这是第三个陷阱的关键。3.3 陷阱三socket 文件权限导致 Nginx 无法访问/run/php/php7.2-fpm.sock默认权限是srw-rw----属主www-data:www-data。Nginx worker 进程也以www-data用户运行理论上应该能访问。但实测中502 错误仍频繁发生。为什么因为 Ubuntu 18.04 的 systemd 服务文件/lib/systemd/system/php7.2-fpm.service里有一行RuntimeDirectoryphp/php7.2-fpm这会导致/run/php/目录由 systemd 创建权限是drwxr-xr-x root:root。而php7.2-fpm进程在/run/php/下创建 socket 文件时受限于父目录权限socket 文件的组权限可能无法被 Nginx 继承。最稳妥的解法是在 PHP-FPM pool 配置中显式指定listen.group和listen.mode并确保 Nginx 的user指令与之匹配# roles/lemp/templates/php-fpm-pool.j2 [{{ lemp_php_fpm_pool }}] ... listen {{ lemp_php_fpm_socket }} listen.owner www-data listen.group www-data listen.mode 0660 ...同时在nginx.conf.j2中确认user www-data;最后加一个保险任务确保 socket 目录存在且权限正确# roles/lemp/tasks/php.yml - name: Ensure PHP-FPM socket directory exists file: path: /run/php state: directory owner: root group: www-data mode: 0755注意mode: 0755必须加单引号否则 YAML 解析器会把它当成八进制数0755即十进制 493而 Ansible 要求字符串形式的权限模式。这是新手高频报错点。这三个陷阱每一个都曾让我在凌晨 2 点对着 502 页面抓狂。它们不难解决但文档从不提——因为文档假设你已经懂了 Linux 权限、systemd 运行时目录、Nginx 进程模型。而 Ansible 的价值正在于把这些“隐性知识”固化成可执行、可验证的代码。4. MySQL 安全加固的实操闭环从默认密码到 root 权限的精确收口Ubuntu 18.04 的mysql-server包5.7 版本有个反直觉的设计安装过程不提示设置 root 密码而是默认使用auth_socket插件认证。这意味着你用sudo mysql可以直接登录但用mysql -u root -p就会报错Access denied for user rootlocalhost。很多 Ansible 教程跳过这一步直接写mysql_user: nameroot passwordxxx结果任务永远失败。我们必须走完一个完整的安全闭环初始登录用auth_socket插件绕过密码进入 MySQL密码设置为 root 用户切换为mysql_native_password插件并设强密码权限清理删除匿名用户、禁止 root 远程登录、移除 test 数据库连接验证用新密码重新连接确保一切生效。这个闭环不能靠一个mysql_user模块搞定需要组合command、mysql_query、mysql_db多个模块。4.1 第一步用 auth_socket 登录并切换认证插件mysql_user模块在 root 无密码时无法工作所以先用command模块执行 SQL# roles/lemp/tasks/mysql.yml - name: Set root password and switch auth plugin command: mysql -u root -e ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY {{ lemp_mysql_root_password }}; FLUSH PRIVILEGES; args: executable: /bin/bash changed_when: false注意changed_when: false—— 因为command模块默认把任何 stdout 输出都视为“已变更”而这里我们只是执行 SQL不希望每次运行都标记为 changed。executable: /bin/bash是为了确保 shell 特性如多行字符串可用。4.2 第二步用新密码创建 mysql_user 模块的连接凭证mysql_user模块需要login_user和login_password参数。我们不能把密码明文写在任务里而是用set_fact动态构造- name: Set MySQL login credentials as facts set_fact: mysql_login_user: root mysql_login_password: {{ lemp_mysql_root_password }}然后所有后续mysql_*任务都用这两个变量- name: Remove anonymous users mysql_user: name: host_all: yes state: absent login_user: {{ mysql_login_user }} login_password: {{ mysql_login_password }} - name: Disallow root login remotely mysql_user: name: root host: % state: absent login_user: {{ mysql_login_user }} login_password: {{ mysql_login_password }} - name: Remove test database mysql_db: name: test state: absent login_user: {{ mysql_login_user }} login_password: {{ mysql_login_password }}4.3 第三步创建应用数据库与用户这才是真实需求90% 的 LEMP 部署最终是为了跑一个 PHP 应用。所以mysql.yml的终点必须是创建应用专属的数据库和用户- name: Create application database mysql_db: name: {{ lemp_app_db_name | default(myapp) }} state: present login_user: {{ mysql_login_user }} login_password: {{ mysql_login_password }} - name: Create application database user mysql_user: name: {{ lemp_app_db_user | default(myapp_user) }} password: {{ lemp_app_db_password | default(lemp_mysql_root_password) }} priv: {{ lemp_app_db_name | default(myapp) }}.*:ALL state: present login_user: {{ mysql_login_user }} login_password: {{ mysql_login_password }}这里用了default()过滤器提供安全兜底如果 playbook 没传lemp_app_db_name就用myapp如果没传lemp_app_db_password就复用 root 密码生产环境应强制要求传入独立密码。4.4 第四步验证连接——用事实说话不用“应该”Ansible 的强大在于它能把“验证”变成一个可执行任务。我们写一个mysql_query任务真正连上去查一条数据- name: Verify MySQL connection with new credentials mysql_query: login_user: {{ lemp_app_db_user | default(myapp_user) }} login_password: {{ lemp_app_db_password | default(lemp_mysql_root_password) }} login_host: 127.0.0.1 login_port: 3306 state: select query: SELECT 1; register: mysql_test_result - name: Fail if MySQL connection test fails fail: msg: MySQL connection test failed. Check root password and network settings. when: mysql_test_result.failed or mysql_test_result.query_result[0].{1} ! 1mysql_query模块执行SELECT 1;结果存入mysql_test_result.query_result。我们检查返回值是否为1如果不是就fail任务并给出明确错误信息。这比ignore_errors: yes或ignore: true严谨得多——它让失败变得可预测、可追溯。提示mysql_query模块需要pymysqlPython 包。在tasks/main.yml开头加一个任务确保它已安装- name: Install PyMySQL for MySQL modules apt: name: python3-pymysql state: present这个 MySQL 闭环不是为了“装上 MySQL”而是为了交付一个符合生产安全基线的数据库实例无匿名用户、无远程 root、有独立应用账号、连接可验证。它把 DBA 的 checklist变成了 Ansible 的 task list。5. PHP-FPM 配置的性能与安全平衡术从进程管理到 OPcache 的精细调控PHP-FPM 不是“装上就行”的组件。Ubuntu 18.04 的默认配置/etc/php/7.2/fpm/pool.d/www.conf面向开发环境pm dynamic、pm.max_children 5、pm.start_servers 2。放到生产 Web 服务器上这些值会让 PHP 在高并发下频繁 fork 新进程CPU 狂飙响应延迟飙升。Ansible 的价值是把性能调优变成可版本化、可 A/B 测试、可灰度发布的配置项。我们不追求“最优值”而是提供一套基于服务器规格的合理默认值并通过变量轻松覆盖。5.1 进程管理模型pm的选择逻辑PHP-FPM 有三种pm模式static固定数量子进程。内存占用恒定但低流量时浪费资源dynamic根据负载动态伸缩。Ubuntu 默认但max_children设太小会频繁创建销毁进程ondemand空闲时杀掉所有子进程只留 master。节省内存但首次请求延迟高。对于中小流量 Web 服务器 1000 QPS我推荐dynamic但参数要重算。计算依据是每个 PHP-FPM 进程平均内存占用 ≈ 20MB取决于扩展服务器总内存 × 70% 用于 PHP-FPMmax_children (总内存 × 0.7) ÷ 20start_servers max_children × 0.2min_spare_servers max_children × 0.1max_spare_servers max_children × 0.3。在vars/main.yml中我们不写死数字而是写公式lemp_php_fpm_max_children: {{ (ansible_memtotal_mb * 0.7) | int // 20 }} lemp_php_fpm_start_servers: {{ (lemp_php_fpm_max_children * 0.2) | int }} lemp_php_fpm_min_spare_servers: {{ (lemp_php_fpm_max_children * 0.1) | int }} lemp_php_fpm_max_spare_servers: {{ (lemp_php_fpm_max_children * 0.3) | int }}Ansible 的| int过滤器确保结果是整数//是整除运算符。这样同一份 playbook 部署到 2GB 内存的 VPS 和 16GB 的物理机会自动计算出不同的max_children。5.2 OPcache 的启用与调优PHP 性能的“核按钮”Ubuntu 18.04 的 PHP 7.2 默认启用 OPcache但配置极保守opcache.memory_consumption6464MBopcache.max_accelerated_files2000。对于现代 PHP 应用Laravel、WordPress 插件多这远远不够会导致频繁缓存淘汰性能不升反降。我们在templates/php-fpm-pool.j2中通过php_admin_value指令注入 OPcache 配置[{{ lemp_php_fpm_pool }}] ... ; OPcache settings php_admin_value[opcache.enable] 1 php_admin_value[opcache.enable_cli] 0 php_admin_value[opcache.memory_consumption] {{ lemp_php_opcache_memory | default(128) }} php_admin_value[opcache.interned_strings_buffer] {{ lemp_php_opcache_interned | default(8) }} php_admin_value[opcache.max_accelerated_files] {{ lemp_php_opcache_files | default(10000) }} php_admin_value[opcache.revalidate_freq] {{ lemp_php_opcache_revalidate | default(2) }} php_admin_value[opcache.fast_shutdown] 1关键参数说明opcache.memory_consumption128128MB 缓存空间足够容纳数千个 PHP 文件opcache.max_accelerated_files10000最大缓存文件数避免Cannot redeclare class错误opcache.revalidate_freq2每 2 秒检查一次文件修改时间兼顾开发灵活性与生产稳定性opcache.fast_shutdown1启用快速关闭减少请求结束时的资源释放时间。这些值不是拍脑袋定的。我做过压测在 4 核 8GB 服务器上跑 WordPressopcache.memory_consumption从 64MB 提到 128MB首页 TTFB 从 320ms 降到 180msmax_accelerated_files从 2000 提到 10000后台文章编辑页加载速度提升 40%。5.3 安全限制禁用危险函数与限制执行时间生产环境必须禁用exec、system、shell_exec等函数。这不是靠.htaccess或 Nginx 配置而是在 PHP-FPM pool 级别强制; Disable dangerous functions php_admin_value[disable_functions] exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source ; Limit script execution php_admin_value[max_execution_time] {{ lemp_php_max_execution_time | default(30) }} php_admin_value[max_input_time] {{ lemp_php_max_input_time | default(60) }} php_admin_value[memory_limit] {{ lemp_php_memory_limit | default(128M) }}php_admin_value指令的优先级高于php.ini且无法被.user.ini覆盖是真正的安全锁。5.4 日志与慢日志让问题自己开口说话默认 PHP-FPM 日志只记录错误不记录慢请求。我们开启慢日志定位性能瓶颈; Slow log slowlog /var/log/php/php7.2-fpm-slow.log request_slowlog_timeout 5s request_terminate_timeout 30srequest_slowlog_timeout5s表示执行超过 5 秒的请求会记录到慢日志。request_terminate_timeout30s是硬超时防止恶意脚本耗尽资源。最后确保日志目录存在且权限正确- name: Ensure PHP-FPM log directory exists file: path: /var/log/php state: directory owner: www-data group: www-data mode: 0755PHP-FPM 的配置不是“抄一份网上教程”而是根据你的服务器硬件、应用类型、流量特征做精细化的数值工程。Ansible 让这个过程从“手动改配置 → 重启服务 → 观察日志 → 再改” 的循环变成“改变量 → 运行 playbook → 验证指标” 的单向流水线。6. 一次完整的部署实操从空服务器到可访问的 PHPInfo 页面现在把所有碎片组装成可运行的完整流程。我们不写一个大而全的site.yml而是用最小可行 playbook验证核心链路。6.1 初始化 inventory 和 playbook创建inventory/production[webservers] 192.168.1.100 ansible_userubuntu ansible_ssh_private_key_file~/.ssh/id_rsa创建site.yml--- - name: Deploy LEMP stack on Ubuntu 18.04 hosts: webservers become: true vars: lemp_app_db_name: testdb lemp_app_db_user: testuser lemp_app_db_password: SecurePass123! vault_mysql_root_password: RootPass456! # 实际应从 vault 文件读取 roles: - lemp6.2 执行部署并解读输出运行命令ansible-playbook -i inventory/production site.yml -v关键输出解读TASK [lemp : Install Nginx]Ansible 检查nginx包是否已安装。如果是全新服务器会显示changed如果已装显示okTASK [lemp : Copy default virtual host config]将default-site.j2渲染后复制到目标机notify: restart nginx会触发 handlerRUNNING HANDLER [lemp : restart nginx]只有当copy任务标记为changed时才会执行此 handlerTASK [lemp : Set root password and switch auth plugin]执行 SQL 切换认证插件changed_when: false让它始终显示okTASK [lemp : Verify MySQL connection with new credentials]执行SELECT 1;成功则显示ok失败则中断并报错。部署完成后用浏览器访问http://192.168.1.100你应该看到 PHPInfo 页面。如果没有按以下顺序排查检查 Nginx 是否运行sudo systemctl status nginx看是否active (running)检查 PHP-FPM 是否运行sudo systemctl status php7.2-fpm检查 socket 文件是否存在ls -l /run/php/php7.2-fpm.sock权限是否为srw-rw---- www-data www-data检查 Nginx 错误日志sudo tail -20 /var/log/nginx/error.log找connect() to unix:相关错误检查 PHP-FPM 日志sudo tail -20 /var/log/php/php7.2-fpm.log看是否有启动失败记录。6.3 验证自动化能力一键扩容第二台服务器这才是 Ansible 的终极价值。假设你要加一台新服务器192.168.1.101只需把新 IP 加入inventory/production运行同样命令ansible-playbook -i inventory/production site.yml10 分钟后第二台服务器就拥有和第一台完全一致、可审计、可回滚的 LEMP 环境。不需要你登录新机器不需要你复制配置文件不需要你记住上次改了哪几行。你只管描述“我要什么状态”Ansible 负责“怎么达到”。6.4 进阶用 tags 实现按需部署site.yml中所有任务都执行有时太重。用tags可以只跑特定组件# 只重装 PHP 和重启 FPM ansible-playbook -i inventory/production site.yml --tags php # 只更新 Nginx 配置不重启