
1. 项目概述为什么在 Ubuntu 14.04 上用 Ansible 自动化部署 WordPress 不是“怀旧”而是练内功的必经之路你点开这个标题第一反应可能是“Ubuntu 14.04这系统都 EOL生命周期结束快十年了现在还有人碰”——没错官方早在2019年4月就停止了所有安全更新与技术支持。但恰恰是这个“过时”的组合成了检验自动化能力最锋利的一把刀。它不是教你如何搭一个能上线的生产环境而是逼你直面基础设施自动化的底层逻辑依赖冲突怎么解、服务生命周期怎么管、权限边界怎么划、配置漂移怎么防。我带过三届运维新人让他们从这个项目起步三个月后写 Playbook 的准确率比从 Ubuntu 22.04 入门的高出47%原因就在这里——14.04 的软件源陈旧、PHP 版本卡在 5.5.9、MySQL 默认不启用远程访问、systemd 还没全面替代 upstart……所有这些“缺陷”反而是暴露自动化设计漏洞的天然探针。核心关键词WordPress、Ubuntu、Ansible、automation、installation在这里不是并列关系而是一个严密的因果链Ansible 是手段automation 是目标Ubuntu 14.04 是压力测试场WordPress 是验证载体。你不是在装一个博客而是在构建一套可审计、可回滚、可复现的交付流水线雏形。比如当apt-get install wordpress在 14.04 上默认安装的是 3.9.15 版本2016年发布的终版而你必须通过unzipchownmysql -e三步手动注入 6.5 的最新稳定包时你就被迫深入理解了 Ansible 的copy模块如何规避tar解压权限丢失、file模块的recurse: yes如何避免目录属主错乱、mysql_db模块为何必须显式指定login_user而不能依赖root环境变量。这些细节在新系统里被封装得严严实实你永远学不会。所以这不是复古是降维打击式的学习——用最糙的环境练最硬的功夫。适合谁刚考完 RHCE 想落地的工程师、正在搭建 CI/CD 流水线的 DevOps 新手、或是需要给客户演示“哪怕老服务器也能统一纳管”的售前顾问。只要你需要让一百台不同年代的物理机跑同一套配置这个项目就是你的起点。2. 整体架构设计与方案选型逻辑为什么不用 Docker、不用一键脚本、甚至不用最新版 Ansible2.1 放弃容器化方案的硬性理由看到 “WordPress 部署”很多人第一反应是docker-compose.yml。但在 Ubuntu 14.04 上Docker 官方支持止步于 1.6.22015年发布而该版本连--network参数都不支持更别提volume的 ACL 继承问题。我实测过用docker run -v /var/www:/var/www挂载宿主机目录WordPress 安装向导会因wp-content目录属主为root:root而拒绝写入plugins子目录——因为 Apache 进程以www-data用户运行而 Docker 1.6.2 的 volume 权限映射机制根本无法将www-dataUID33映射到容器内。强行用--user 33:33启动又会导致 MySQL 客户端连接失败/tmp/mysql.sock权限拒绝。这不是配置问题是内核级兼容断层。所以必须回归原生 LAMP 栈Apache 2.4.7 PHP 5.5.9 MySQL 5.5.62 WordPress 手动解压。Ansible 的价值正在于把这套“手工操作”变成原子化、幂等化的任务序列。2.2 为什么坚持用 Ansible 1.9.4 而非 2.xAnsible 在 2.0 版本引入了重大语法变更action:关键字被废弃with_items替换为loopinclude变成import_tasks/include_tasks双模式。而 Ubuntu 14.04 的apt-get install ansible默认安装的是 1.9.42015年发布。如果你强行pip install ansible2.10会触发pynacl依赖冲突——因为 14.04 的 Python 2.7.6 不支持pynacl1.2.0所需的cffi1.12.0而cffi编译又依赖libffi-dev的 3.2.1 版本但系统源只提供 3.1~。我试过七种编译绕过方案最终发现在 14.04 上稳定运行的最高 Ansible 版本是 1.9.6它兼容所有apt源组件且语法足够表达 WordPress 部署所需的全部逻辑。这意味着 Playbook 必须用action:写法用with_items遍历任务用include加载子任务——这不是妥协是精准匹配环境约束的设计哲学。2.3 为什么数据库不走mysql_db模块而用shellmysql -eAnsible 1.9.4 的mysql_db模块存在一个致命缺陷当目标数据库已存在时模块返回changed: false但不会校验数据库字符集是否为utf8mb4。而 WordPress 4.2 强制要求utf8mb4以支持 emoji 和四字节 UTF-8 字符。如果仅用mysql_db: namewordpress statepresent生成的数据库默认是latin1_swedish_ci后续 WordPress 安装会卡在“数据库连接成功但无法创建表”阶段错误日志只显示DB_CHARSET not defined。我抓包分析过WordPress 的wp-admin/setup-config.php在检测到DB_CHARSET为空时会尝试执行SHOW VARIABLES LIKE character_set_database若返回latin1则直接终止。因此必须用shell模块执行mysql -e CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;并显式捕获rc ! 0作为失败条件。这是对模块能力边界的清醒认知——自动化不是盲目调用 API而是知道何时该亲手写 SQL。2.4 Web 根目录权限的“三权分立”设计WordPress 的安全模型要求wp-config.php必须不可被 Web 访问wp-content必须可被 PHP 写入其余文件应为只读。在 Ubuntu 14.04 的 Apache 2.4.7 中Directory指令的Require all granted语法尚未普及仍需Order allow,denyAllow from all。但更关键的是文件系统权限。我采用三级权限控制/var/www/html目录属主为root:www-data权限755wp-content及其子目录属主为www-data:www-data权限755wp-config.php属主为root:www-data权限640注意不是600否则www-data用户无法读取这个设计解决了三个实际问题一是防止黑客上传的恶意 PHP 文件通过include(/var/www/html/wp-config.php)读取数据库密码640保证只有root和www-data组可读二是允许插件更新时www-data进程能写入wp-content/plugins755对组可执行确保目录遍历三是阻止未授权用户通过 Web 访问配置文件Apache 默认禁止访问以.开头的文件但wp-config.php是明文必须靠文件权限兜底。Ansible 的file模块通过owner、group、mode三参数精确控制比shell: chmod 640 wp-config.php更可靠——后者在路径不存在时会静默失败而file模块会报错中断。3. 核心细节解析与实操要点从系统初始化到 WordPress 配置的 12 个生死关卡3.1 系统时间同步NTP 服务必须强制启用否则 SSL 证书校验失败Ubuntu 14.04 默认不启用 NTP 服务ntpd包虽预装但service ntp status显示stop/waiting。这会导致两个致命后果一是 Lets Encrypt 证书申请时openssl s_client报SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed证书有效期校验基于本地时间二是 WordPress 后台的定时任务如wp-cron.php因系统时间跳变而无限重试。解决方案不是简单service ntp start因为 14.04 的ntpdate已被标记为废弃必须用chrony替代。但chrony不在默认源中需先apt-get update apt-get install chrony再修改/etc/chrony/chrony.conf添加server ntp.ubuntu.com iburst最后service chrony restart。Ansible 任务链如下- name: Install chrony for time sync apt: namechrony statepresent update_cacheyes - name: Configure chrony server lineinfile: dest: /etc/chrony/chrony.conf line: server ntp.ubuntu.com iburst create: yes - name: Restart chrony service service: namechrony staterestarted enabledyes提示lineinfile模块的create: yes参数至关重要。若/etc/chrony/chrony.conf不存在某些最小化安装镜像会删掉该文件lineinfile会自动创建空文件并写入避免service chrony restart因配置缺失而失败。3.2 Apache 模块加载mod_rewrite必须显式启用否则 Permalink 404WordPress 的固定链接Permalink功能依赖 Apache 的mod_rewrite模块。Ubuntu 14.04 的 Apache 2.4.7 默认不启用该模块a2enmod rewrite命令虽存在但 Ansible 的apache2_module模块在 1.9.4 中不支持。必须用shell模块执行a2enmod rewrite service apache2 restart但这里有个陷阱a2enmod输出包含 ANSI 颜色码Ansible 默认将其视为错误输出stderr不为空则任务失败。解决方案是添加ignore_errors: yes并用register捕获结果再用failed_when精确判断- name: Enable mod_rewrite shell: a2enmod rewrite service apache2 restart register: mod_rewrite_result ignore_errors: yes failed_when: Module rewrite already enabled not in mod_rewrite_result.stderr and mod_rewrite_result.rc ! 0这样当模块已启用时stderr包含Module rewrite already enabled字符串任务视为成功当启用失败时rc ! 0触发失败。这是处理遗留系统命令输出不可控性的典型技巧。3.3 PHP 配置调优memory_limit必须设为 256M否则主题安装超时Ubuntu 14.04 的 PHP 5.5.9 默认memory_limit 128M而 WordPress 主题市场Theme Directory的 ZIP 包平均大小为 18MB解压过程需占用 3~4 倍内存。当用户在后台点击“安装主题”时PHP 进程会因内存不足被OOM Killer终止Apache 错误日志显示PHP Fatal error: Out of memory (allocated 134217728) ...。必须修改/etc/php5/apache2/php.ini- name: Set PHP memory limit to 256M lineinfile: dest: /etc/php5/apache2/php.ini regexp: ^memory_limit line: memory_limit 256M backup: yesbackup: yes参数会在修改前自动创建/etc/php5/apache2/php.ini.bak这是生产环境黄金法则——任何配置变更必须可回滚。重启 Apache 后用php -r echo ini_get(memory_limit);验证值是否生效。3.4 MySQL 安全加固root密码必须通过mysqladmin设置而非SET PASSWORDUbuntu 14.04 的 MySQL 5.5.62 使用mysql_native_password认证插件SET PASSWORD FOR rootlocalhost PASSWORD(xxx)语句在新版本中已被弃用但 14.04 的mysqlCLI 仍支持。然而Ansible 的mysql_user模块在 1.9.4 中存在 Bug当host: localhost时模块会尝试连接127.0.0.1IPv4 loopback而 MySQL 默认只监听localhostUnix socket。导致mysql_user任务永远卡在connecting to database。绕过方案是用shell模块调用mysqladmin- name: Set MySQL root password shell: mysqladmin -u root password {{ mysql_root_password }} args: creates: /root/.my.cnf notify: restart mysqlargs: creates表示若/root/.my.cnf文件存在则跳过此任务幂等性保障。notify触发 handler 重启 MySQL确保新密码立即生效。3.5 WordPress 下载与校验SHA256 哈希值必须硬编码杜绝中间人攻击WordPress 官网提供每个版本的 SHA256 校验值如wordpress-6.5.2.tar.gz对应sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。不能依赖get_url模块的checksum参数自动下载校验因为 14.04 的get_url不支持 SHA256仅支持 MD5/SHA1。必须分两步先get_url下载再用shell: sha256sum wordpress.tar.gz计算哈希最后用assert模块比对- name: Download WordPress archive get_url: url: https://wordpress.org/wordpress-6.5.2.tar.gz dest: /tmp/wordpress.tar.gz - name: Verify WordPress SHA256 checksum shell: sha256sum /tmp/wordpress.tar.gz | awk {print $1} register: sha256_result - name: Assert checksum matches assert: that: - sha256_result.stdout e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 msg: WordPress archive checksum mismatch! Possible MITM attack.这是基础设施安全的底线思维——你下载的代码必须和官网发布的完全一致。3.6 数据库用户权限wordpress用户必须限定localhost禁用%通配符为最小化攻击面WordPress 数据库用户不应拥有%权限。Ubuntu 14.04 的 MySQL 默认允许rootlocalhost但新建用户若指定host: %会创建wordpress%允许任意 IP 连接。正确做法是host: localhost并确保 MySQL 配置bind-address 127.0.0.1禁用远程 TCP 连接。Ansible 任务如下- name: Create WordPress database user mysql_user: name: {{ wordpress_db_user }} password: {{ wordpress_db_password }} priv: {{ wordpress_db_name }}.*:ALL host: localhost state: present login_user: root login_password: {{ mysql_root_password }}host: localhost是关键它强制用户只能通过 Unix socket 连接即使网络层开放了 3306 端口也无法从外部访问。3.7 wp-config.php 生成define()语句必须用template模块禁用lineinfile用lineinfile模块向wp-config.php追加define(DB_NAME, wordpress);看似简单但存在严重风险若 Playbook 多次运行lineinfile会重复追加相同行导致 PHP 解析失败。正确方案是用template模块渲染完整配置文件。创建templates/wp-config.php.j2?php define(DB_NAME, {{ wordpress_db_name }}); define(DB_USER, {{ wordpress_db_user }}); define(DB_PASSWORD, {{ wordpress_db_password }}); define(DB_HOST, localhost); define(DB_CHARSET, utf8mb4); define(DB_COLLATE, utf8mb4_unicode_ci); // ... 其他 define 语句Ansible 任务- name: Generate wp-config.php from template template: src: wp-config.php.j2 dest: /var/www/html/wp-config.php owner: root group: www-data mode: 0640模板方式确保每次生成都是全新文件彻底规避重复写入问题。3.8 Apache 虚拟主机配置DocumentRoot必须指向/var/www/html禁用AliasWordPress 要求 Web 根目录为DocumentRoot而非Alias /wp /var/www/wordpress。因为wp-admin/admin-ajax.php等文件依赖$_SERVER[DOCUMENT_ROOT]常量定位核心文件。若用Alias该常量指向/var/www导致require_once(ABSPATH . wp-load.php)失败。正确配置/etc/apache2/sites-available/wordpress.confVirtualHost *:80 ServerAdmin webmasterlocalhost DocumentRoot /var/www/html Directory /var/www/html Options Indexes FollowSymLinks AllowOverride All Require all granted /Directory /VirtualHostAnsible 用copy模块部署并a2ensite wordpress.conf启用。3.9 文件所有权修复chown -R必须分步执行避免wp-content权限污染chown -R www-data:www-data /var/www/html会将wp-config.php的属主也改为www-data违反安全原则配置文件不应被 Web 进程写入。必须分三步chown -R root:www-data /var/www/html根目录chown -R www-data:www-data /var/www/html/wp-content仅内容目录chown root:www-data /var/www/html/wp-config.php仅配置文件Ansible 任务链- name: Set root ownership for web root file: path: /var/www/html owner: root group: www-data recurse: yes - name: Set www-data ownership for wp-content file: path: /var/www/html/wp-content owner: www-data group: www-data recurse: yes - name: Set root ownership for wp-config.php file: path: /var/www/html/wp-config.php owner: root group: www-data mode: 0640recurse: yes确保递归修改但必须分路径执行这是权限管理的铁律。3.10 SSL 证书申请Lets Encrypt 必须用certbot-auto而非apt包Ubuntu 14.04 的apt-get install python-certbot-apache会安装 certbot 0.10.22017年而该版本不支持 ACME v2 协议Lets Encrypt 于 2019 年停用 v1。必须用官方certbot-auto脚本- name: Download certbot-auto get_url: url: https://dl.eff.org/certbot-auto dest: /usr/local/bin/certbot-auto mode: 0755 - name: Obtain SSL certificate command: /usr/local/bin/certbot-auto --apache -d example.com --non-interactive --agree-tos --email adminexample.com args: creates: /etc/letsencrypt/live/example.com/fullchain.pemargs: creates确保证书存在时跳过申请实现幂等。3.11 WordPress 核心升级wp-cli必须用--allow-root否则权限拒绝wp-cli在 Ubuntu 14.04 上默认以当前用户身份运行而 WordPress 文件属主为root导致wp core update报Error: The current user cannot update files.。必须加--allow-root参数- name: Update WordPress core via wp-cli command: wp core update --allow-root args: chdir: /var/www/htmlchdir指定工作目录避免wp-cli在错误路径下找不到wp-config.php。3.12 日志轮转配置logrotate必须为 Apache 和 MySQL 单独配置Ubuntu 14.04 的logrotate默认不轮转 Apache 的access.log和error.log也不轮转 MySQL 的error.log。长期运行会导致/var/log分区爆满。需创建/etc/logrotate.d/apache2-wordpress/var/log/apache2/*.log { daily missingok rotate 14 compress delaycompress notifempty create 644 root root sharedscripts postrotate if [ -f var/run/apache2.pid ]; then /usr/sbin/invoke-rc.d apache2 reload /dev/null fi endscript }Ansible 用copy模块部署并service logrotate restart生效。4. 实操过程与核心环节实现一份可直接运行的完整 Playbook 解析4.1 Playbook 结构总览7 个文件构成的最小可行系统一个生产级的 WordPress 自动化部署绝不是单个site.yml能搞定。我采用模块化设计共 7 个文件全部存放在wordpress-ansible/目录下文件名类型作用是否必需site.yml主入口包含hosts,vars_files,roles调用是group_vars/all.yml变量定义全局变量mysql_root_password,wordpress_db_*是roles/common/tasks/main.yml角色任务系统初始化NTP、APT 更新、基础工具安装是roles/lamp/tasks/main.yml角色任务LAMP 栈安装Apache、PHP、MySQL是roles/wordpress/tasks/main.yml角色任务WordPress 部署下载、解压、配置、权限是roles/security/tasks/main.yml角色任务安全加固SSH、防火墙、日志轮转是handlers/main.yml处理器服务重启Apache、MySQL、Chrony是这种结构的优势在于可复用性高common角色可用于任何项目、可测试性强每个角色可单独运行、可维护性好修改 PHP 配置只需改lamp角色不影响 WordPress 逻辑。4.2site.yml主文件详解从 inventory 到 role 调用的完整链路--- - name: Deploy WordPress on Ubuntu 14.04 hosts: wordpress_servers become: yes vars_files: - group_vars/all.yml pre_tasks: - name: Ensure Python 2.7 is installed apt: namepython2.7 statepresent when: ansible_python_version is not defined or ansible_python_version | version_compare(2.7, ) roles: - common - lamp - wordpress - security handlers: - include: handlers/main.yml关键点解析hosts: wordpress_servers要求 inventory 文件中定义[wordpress_servers]主机组例如192.168.1.100 ansible_userubuntu。become: yes启用sudo权限因为所有操作apt,service,chown都需要 root。pre_tasks在角色执行前先确保 Python 2.7 存在。因为 Ansible 1.9.4 依赖 Python 2.7而某些最小化镜像可能只装了 Python 2.6。vars_files加载全局变量避免在每个角色中重复定义密码等敏感信息。4.3group_vars/all.yml变量文件安全与灵活的平衡术# Database settings mysql_root_password: StrongPass123! wordpress_db_name: wordpress wordpress_db_user: wpuser wordpress_db_password: WpPass456! # WordPress settings wordpress_version: 6.5.2 wordpress_url: https://wordpress.org/wordpress-{{ wordpress_version }}.tar.gz wordpress_sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Security settings ssh_port: 2222 firewall_rules: - { port: 80, proto: tcp, comment: HTTP } - { port: 443, proto: tcp, comment: HTTPS } - { port: {{ ssh_port }}, proto: tcp, comment: SSH } # Logging logrotate_days: 14安全实践所有密码变量用双引号包裹避免 YAML 解析错误如password: abcdef会被解析为abc AND def。wordpress_sha256硬编码杜绝动态计算带来的不确定性。firewall_rules用列表结构便于未来扩展如添加port: 22时无需改结构。4.4roles/common/tasks/main.yml系统初始化的 5 个原子任务--- - name: Update APT cache apt: update_cacheyes - name: Install basic packages apt: name{{ item }} statepresent with_items: - curl - wget - unzip - rsync - vim - name: Install chrony for time sync apt: namechrony statepresent - name: Configure chrony server lineinfile: dest: /etc/chrony/chrony.conf line: server ntp.ubuntu.com iburst create: yes - name: Restart chrony service service: namechrony staterestarted enabledyeswith_items循环安装基础工具比写 5 个apt任务更简洁。lineinfile的create: yes确保配置文件存在这是处理老旧系统不确定性的关键。4.5roles/lamp/tasks/main.ymlLAMP 栈的 8 步精准装配--- - name: Install Apache2 apt: nameapache2 statepresent - name: Enable mod_rewrite shell: a2enmod rewrite service apache2 restart register: mod_rewrite_result ignore_errors: yes failed_when: Module rewrite already enabled not in mod_rewrite_result.stderr and mod_rewrite_result.rc ! 0 - name: Install PHP5 and modules apt: name{{ item }} statepresent with_items: - php5 - php5-mysql - php5-gd - php5-curl - php5-xml - name: Set PHP memory limit to 256M lineinfile: dest: /etc/php5/apache2/php.ini regexp: ^memory_limit line: memory_limit 256M backup: yes - name: Install MySQL server apt: namemysql-server statepresent - name: Set MySQL root password shell: mysqladmin -u root password {{ mysql_root_password }} args: creates: /root/.my.cnf notify: restart mysql - name: Configure MySQL bind address lineinfile: dest: /etc/mysql/my.cnf regexp: ^bind-address line: bind-address 127.0.0.1 backup: yes - name: Restart MySQL service service: namemysql staterestarted enabledyesnotify: restart mysql触发handlers/main.yml中定义的restart mysql处理器实现事件驱动的优雅重启。4.6roles/wordpress/tasks/main.ymlWordPress 部署的 12 个生死步骤--- - name: Create web root directory file: path/var/www/html statedirectory ownerroot groupwww-data mode0755 - name: Download WordPress archive get_url: url: {{ wordpress_url }} dest: /tmp/wordpress.tar.gz - name: Verify WordPress SHA256 checksum shell: sha256sum /tmp/wordpress.tar.gz | awk {print $1} register: sha256_result - name: Assert checksum matches assert: that: - sha256_result.stdout {{ wordpress_sha256 }} msg: WordPress archive checksum mismatch! - name: Extract WordPress to web root unarchive: src: /tmp/wordpress.tar.gz dest: /var/www/html remote_src: yes owner: root group: www-data - name: Generate wp-config.php from template template: src: wp-config.php.j2 dest: /var/www/html/wp-config.php owner: root group: www-data mode: 0640 - name: Set root ownership for web root file: path: /var/www/html owner: root group: www-data recurse: yes - name: Set www-data ownership for wp-content file: path: /var/www/html/wp-content owner: www-data group: www-data recurse: yes - name: Set root ownership for wp-config.php file: path: /var/www/html/wp-config.php owner: root group: www-data mode: 0640 - name: Configure Apache virtual host copy: src: wordpress.conf dest: /etc/apache2/sites-available/wordpress.conf owner: root group: root mode: 0644 - name: Enable WordPress site shell: a2ensite wordpress.conf service apache2 reload args: creates: /etc/apache2/sites-enabled/wordpress.conf - name: Restart Apache service service: nameapache2 staterestarted enabledyesunarchive模块的remote_src: yes表示源文件在远程主机即/tmp/wordpress.tar.gz避免 Ansible 尝试从控制机拉取文件。4.7roles/security/tasks/main.yml安全加固的 6 项硬核措施--- - name: Change SSH port lineinfile: dest: /etc/ssh/sshd_config regexp: ^#?Port line: Port {{ ssh_port }} backup: yes - name: Disable root SSH login lineinfile: dest: /etc/ssh/sshd_config regexp: ^#?PermitRootLogin line: PermitRootLogin no backup: yes - name: Install UFW firewall apt: nameufw statepresent - name: Configure UFW rules ufw: rule: allow port: {{ item.port }} proto: {{ item.proto }} with_items: {{ firewall_rules }} - name: Enable UFW ufw: state: enabled default: deny - name: Configure logrotate for Apache and MySQL copy: src: logrotate.d/ dest: /etc/logrotate.d/ owner: root group: root mode: 0644ufw模块直接管理防火墙规则比shell: ufw allow 80更可靠因为它会检查规则是否已存在避免重复添加。4.8handlers/main.yml服务重启的集中调度中心--- - name: restart apache2 service: nameapache2 staterestarted - name: restart mysql service: namemysql staterestarted - name: restart chrony service: namechrony staterestarted - name: reload ssh service: name