Ansible 自动化运维实践笔记:Jinja2 模板、LNMP+WordPress 部署与大项目管理

部署文件到受管主机

使用JINJA2模板部署文件

示例1:部署web服务器,主页内容显示为Welcome to HOSTNAME。HOSTNAME为受管主机完全主机名。

playbook内容如下:

复制代码
 [yuxb@controller web 09:39:37]$ cat playbook.yml 
 ---
 - name: Enable intranet services
   hosts: node1
   tasks:
     - name: ensure latest version of httpd 
       yum:
         name: httpd
         state: latest
 ​
     - name: test html page is installed
       template:
         src: index.html.j2
         dest: /var/www/html/index.html
     
     - name: httpd enabled and running
       service:
         name: httpd
         enabled: true
         state: restarted
 ...

index.html.j2 内容如下:

复制代码
 [yuxb@controller web 09:39:40]$ vim index.html.j2
 [yuxb@controller web 09:40:55]$ cat index.html.j2 
 Welcome to {{ ansible_fqdn }}
 ​

执行:

复制代码
 [yuxb@controller web 09:41:22]$ ansible-playbook playbook.yml 
 ​
 PLAY [Enable intranet services] *********************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node1]
 ​
 TASK [ensure latest version of httpd] ***************************************************************
 ok: [node1]
 ​
 TASK [test html page is installed] ******************************************************************
 changed: [node1]
 ​
 TASK [httpd enabled and running] ********************************************************************
 changed: [node1]
 ​
 PLAY RECAP ******************************************************************************************
 node1                      : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 ​

示例2:推送 ssh 服务配置文件。

sshd_config.j2内容:

复制代码
 [yuxb@controller web 09:50:47]$ cat sshd_config.j2 
 # {{ ansible_managed }}
 # DO NOT MAKE LOCAL MODIFICATIONS TO THIS FILE AS THEY WILL BE LOST
 Port {{ ssh_port }}
 ListenAddress {{ ansible_facts['default_ipv4']['address'] }}
 ​
 HostKey /etc/ssh/ssh_host_rsa_key
 HostKey /etc/ssh/ssh_host_ecdsa_key
 HostKey /etc/ssh/ssh_host_ed25519_key
 SyslogFacility AUTHPRIV
 PermitRootLogin  {{ root_allowed }}
 AllowGroups {{ groups_allowed }}
 ​
 AuthorizedKeysFile      /etc/.rht_authorized_keys .ssh/authorized_keys
 PasswordAuthentication {{ passwords_allowed }}
 ​

playbook

复制代码
 [yuxb@controller web 09:41:00]$ vim playbook.yml 
 [yuxb@controller web 09:50:07]$ cat playbook.yml 
 ---
 - name: config sshd service
   hosts: node1
   vars:
     ssh_port: 1022
     root_allowed: "yes"
     groups_allowed: wheel
     passwords_allowed: "yes"
     ansible_managed: "Ansible managed"
   tasks:
     - name: config sshd service
       template:
         src: sshd_config.j2
         dest: /root/sshd_config
 ​
 # 执行
 [yuxb@controller web 09:51:00]$ ansible-playbook playbook.yml 
 ​
 PLAY [config sshd service] **************************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node1]
 ​
 TASK [config sshd service] **************************************************************************
 changed: [node1]
 ​
 PLAY RECAP ******************************************************************************************
 node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 ​
复制代码
 [yuxb@controller web 10:32:15]$ cat playbook.yml 
 ---
 - name: 部署web服务器
   hosts: node1
   tasks:
     - name: 安装软件包 httpd
       yum: 
         name: httpd
         state: latest
     - name: 启动并启用 httpd 服务
       service:
         name: httpd
         state: started
     - name: 准备 web 服务器主页内容
       copy:
         src: index.html
         dest: /var/www/html/index.html
     - name: 启动并启用 firewalld 服务
       service:
         name: firewalld
         state: started
     - name: 配置防火墙 放行 http
       firewalld:
         service: http
         permanent: yes
         state: enabled
 ​

Jinja2 模板语法

for 语句

Jinja2使用for语句来提供循环功能。

示例1:

复制代码
 [yuxb@controller web 10:41:56]$ vim playbook.yml 
 [yuxb@controller web 10:46:24]$ cat playbook.yml 
 ---
 - name: test template
   hosts: node1
   vars:
     users:
       - tom
       - jack
       - snoopy
       - lucy
 ​
   tasks:
     - name: test template
       template:
         src: testfile.j2
         dest: /tmp/testfile
 ​

testfile.j2内容

复制代码
 [yuxb@controller web 10:45:16]$ cat testfile.j2 
 {% for user in users %}
 username is {{ user }}
 {% endfor %}
 ​

执行并验证

复制代码
 # 执行
 [yuxb@controller web 10:45:19]$ ansible-playbook playbook.yml 
 ​
 PLAY [test template] *******************************************************************************
 ​
 TASK [Gathering Facts] *****************************************************************************
 ok: [node1]
 ​
 TASK [test template] *******************************************************************************
 changed: [node1]
 ​
 PLAY RECAP *****************************************************************************************
 node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 ​
 # 验证
 [yuxb@node1 ~ 10:33:13]$ cat /tmp/testfile 
 tom
 jack
 snoopy
 lucy
 ​

LNMP + WordPress 自动化部署实验

一、实验目标

通过 Ansible 自动化工具 ,在目标服务器(lnmp 主机组)上搭建 LNMP 架构(Linux 系统 + Nginx 网页服务器 + MariaDB 数据库 + PHP 脚本语言),并部署 WordPress 博客系统,最终实现通过浏览器访问博客。

二、前置知识说明

术语 / 工具 作用说明
Ansible 自动化运维工具,通过 "剧本(YAML 文件)" 批量执行命令,无需在目标机装客户端
LNMP Web 服务架构,Nginx 负责接收用户请求,PHP 处理动态代码,MariaDB 存储数据
MariaDB 开源数据库(MySQL 分支),用于存储 WordPress 的文章、用户等数据
Nginx 轻量级网页服务器,负责转发请求、展示静态页面、对接 PHP
PHP-FPM PHP 的进程管理器,处理 Nginx 转发的 PHP 动态请求
WordPress 开源博客系统,需依赖 LNMP 架构运行
Ansible Vault Ansible 自带的加密工具,用于保护数据库密码等敏感信息

三、实验环境准备

3.1 环境说明

角色 主机名 系统 核心软件 IP 地址(示例)
控制节点(执行命令) controller CentOS 7 Ansible、LNMP 配置文件 10.1.8.10(示例)
目标节点(部署服务) node1(属于 lnmp 组) CentOS 7 无(Ansible 自动安装软件) 10.1.8.11(示例)
客户端(访问博客) 个人电脑(Windows) Windows 10/11 浏览器 -

3.2 控制节点前置检查

controller 主机上执行以下命令,确认 Ansible 已安装且能连接目标节点:

复制代码
 # 1. 检查Ansible是否安装(出现版本号即正常)
 [yuxb@controller ~]$ ansible --version
 # 2. 检查能否连通目标节点(lnmp是主机组名,从inventory文件读取)
 [yuxb@controller ~]$ ansible lnmp -m ping
 # 若返回“SUCCESS”,说明控制节点与目标节点连通正常;若失败,需先解决SSH登录问题

四、实验步骤

阶段 1:初始化控制节点工作目录(存放配置文件)

4.1.1 复制并进入工作目录(避免干扰原有文件)
复制代码
 [yuxb@controller ~ 11:21:36]$ cp -r web lnmp
  • 这步做什么? :把原有 web 目录复制一份,命名为 lnmp,作为本次实验的 "工作目录"(所有配置文件都放在这里,不影响其他文件)。

  • 为什么这么做? :工作目录独立,后续操作只在 lnmp 目录内进行,方便管理和回溯。

复制代码
 [yuxb@controller ~ 13:49:03]$ cd lnmp/
  • 这步做什么? :进入 lnmp 工作目录,后续所有命令都在这个目录下执行。
4.1.2 清空工作目录(删除原有冗余文件)
复制代码
 [yuxb@controller lnmp 13:49:15]$ rm -rf *
  • 这步做什么? :删除 lnmp 目录里所有原有文件(从 web 复制过来的旧文件)。

  • 为什么这么做?:避免旧文件(如旧配置、旧脚本)干扰本次实验,保证环境 "干净"。

4.1.3 拷贝 Ansible 核心配置文件(让 Ansible 知道怎么工作)
复制代码
 [yuxb@controller lnmp 13:49:44]$ cp ../web/ansible.cfg .
 [yuxb@controller lnmp 13:49:46]$ cp ../web/inventory .
  • 这步做什么?

    :从上级目录web拷贝两个关键文件到当前lnmp目录:

    1. ansible.cfg:Ansible 的主配置文件(告诉 Ansible 用哪个用户登录目标机、敏感文件怎么解密等);

    2. inventory:主机清单文件(告诉 Ansible 目标节点是哪些,比如 lnmp 组包含 node1)。

  • 为什么这么做?:这两个文件是 Ansible 运行的基础,必须放在工作目录,否则 Ansible 不知道 "操作哪个机器""用什么身份操作"。

阶段 2:准备变量文件(存储配置,保护敏感信息)

4.2.1 创建变量目录(Ansible 规定的目录结构)
复制代码
 [yuxb@controller lnmp 14:03:33]$ mkdir host_vars
 [yuxb@controller lnmp 14:03:57]$ mkdir host_vars/lnmp
  • 这步做什么?

    :创建host_vars/lnmp目录:

    • host_vars:是 Ansible 约定的 "主机变量目录",专门存放不同主机 / 主机组的配置;

    • host_vars/lnmp:专门存放 lnmp 主机组(目标节点)的变量(比如数据库密码、用户名)。

  • 为什么这么做?:变量和剧本分离,后续修改配置(如改密码)不用动核心剧本,更灵活。

4.2.2 编写敏感变量文件(存储密码等重要信息)
复制代码
 [yuxb@controller lnmp 14:04:02]$ vim host_vars/lnmp/vaults.yml
  • 这步做什么? :用 vim 编辑器创建 vaults.yml 文件,内容如下(直接复制粘贴):
复制代码
 mysql_root_password: 123
 app_user: wordpress
 app_password: 123
 app_host: '%'
 app_priv: '*.*:ALL'
  • 每个配置项是什么意思?:

    配置项 含义 作用
    mysql_root_password: 123 MariaDB 数据库的 root 密码(最高权限密码) 用于初始化数据库、创建用户和库
    app_user: wordpress 给 WordPress 用的数据库用户名 WordPress 只能用这个用户操作数据库,避免用 root 提权风险
    app_password: 123 WordPress 数据库用户的密码 验证 WordPress 对数据库的访问权限
    app_host: '%' 允许该用户从 "任意主机" 连接数据库 这里是本地部署,% 表示目标机自身可以访问
    app_priv: '*.*:ALL' 该用户拥有 "所有数据库、所有权限" 让 WordPress 能创建表、插入数据(实际生产可缩小权限,实验简化用 ALL)
  • 为什么要单独放这个文件?:这些是敏感信息(密码),后续要加密保护,不能明文暴露。

4.2.3 编写普通变量文件(存储非敏感配置)
复制代码
 [yuxb@controller lnmp 14:10:23]$ vim host_vars/lnmp/vars.yml
  • 这步做什么? :用 vim 创建 vars.yml 文件,内容如下:
复制代码
 db_name: webapp
  • 配置项含义db_name: webapp 表示给 WordPress 专门创建的数据库名是 webapp

  • 为什么和敏感变量分开?:数据库名不是敏感信息,不用加密,单独存放更清晰。

4.2.4 加密敏感变量文件(保护密码不泄露)
复制代码
 [yuxb@controller lnmp 15:03:40]$ ansible-vault encrypt host_vars/lnmp/vaults.yml
 New Vault password: 
 Confirm New Vault password: 
 Encryption successful
  • 这步做什么?:

    用 Ansible Vault 加密1vaults.yml文件:

    1. 输入 "加密密码"(实验用 123,生产环境用复杂密码);

    2. 再次确认密码;

    3. 提示 "Encryption successful" 表示加密成功。

  • 为什么要加密? :如果不加密,vaults.yml 里的密码是明文,别人看到就能登录数据库,不安全。

  • 加密后怎么用?:后续 Ansible 执行剧本时,需要解密密码,所以要存一个 "解密密码文件"。

4.2.5 创建解密密码文件(避免每次输入密码)
复制代码
 [yuxb@controller lnmp 15:04:53]$ echo 123 > secret.txt
  • 这步做什么? :把 "加密密码(123)" 写入 secret.txt 文件。

  • 为什么这么做?:如果不存这个文件,每次执行 Ansible 剧本都要手动输入解密密码,麻烦;存了之后,Ansible 会自动从这个文件读密码解密。

4.2.6 验证加密文件能否正常解密(确保后续能用)
复制代码
 [yuxb@controller lnmp 15:06:06]$ ansible-vault view host_vars/lnmp/vaults.yml
  • 这步做什么? :查看加密后的 vaults.yml 内容(验证解密是否正常)。

  • 正常结果 :会显示 vaults.yml 的原始内容(和 4.2.2 里写的一致),说明解密成功;如果失败,检查 secret.txt 的密码是否和加密时一致。

阶段 3:准备核心配置文件(Ansible 剧本、Nginx/PHP 配置)

4.3.1 编写 Ansible 主配置文件(告诉 Ansible 怎么执行命令)
复制代码
 [yuxb@controller lnmp 16:25:31]$ vim ansible.cfg
  • 这步做什么? :用 vim 编辑 ansible.cfg 文件,内容如下(直接复制,注释已解释每一行):
复制代码
 [defaults]
 inventory = ./inventory          # 告诉Ansible:主机清单文件在当前目录的inventory里
 remote_user = yuxb               # 告诉Ansible:用“yuxb”用户登录目标节点(需提前配置该用户sudo免密)
 vault_password_file=./secret.txt # 告诉Ansible:解密Vault文件的密码在当前目录的secret.txt里
 #vault_password_file = ./password-for-vault  # 注释:备用密码文件路径(不用管)
 #ask_pass = True                  # 注释:是否每次登录目标机都问SSH密码(实验已配置免密,所以注释)
 #module_name = command            # 注释:Ansible默认执行的模块(不用改)
 #private_key_file = /opt/id_rsa   # 注释:如果用SSH密钥登录,填密钥路径(实验用密码免密,注释)
 #host_key_checking = False        # 注释:是否检查目标机的SSH密钥(新手可取消注释,避免首次登录弹确认)
 ​
 [privilege_escalation]
 become=True          # 告诉Ansible:执行命令时需要提升权限(即sudo)
 become_method=sudo   # 告诉Ansible:提升权限的方式是sudo
 become_user=root     # 告诉Ansible:提升权限后用root用户执行命令
 become_ask_pass=False # 告诉Ansible:sudo时不用问密码(需提前在目标机配置yuxb用户sudo免密)
  • 关键注意点 :目标节点必须配置 yuxb 用户的 sudo 免密(否则 Ansible 执行安装命令会失败),配置方法:在目标机 node1 上执行 visudo,添加 yuxb ALL=(ALL) NOPASSWD: ALL
4.3.2 编写 PHP 配置文件(让 Nginx 能处理 PHP 请求)
复制代码
 [yuxb@controller lnmp 14:11:18]$ vim php.conf
  • 这步做什么? :用 vim 创建 php.conf 文件,内容如下(Nginx 解析 PHP 的规则):
复制代码
 location ~ \.php$ {
     try_files $uri =404;
     fastcgi_pass 127.0.0.1:9000;
     fastcgi_index index.php;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
     include fastcgi_params;
 }
  • 每一行是什么意思?:

    1. location ~ \.php$:匹配所有以 .php 结尾的请求(比如 index.php);

    2. try_files $uri =404:检查请求的 PHP 文件是否存在,不存在就返回 404 错误(避免恶意请求);

    3. fastcgi_pass 127.0.0.1:9000:把 PHP 请求转发到本地 9000 端口(PHP-FPM 默认端口);

    4. fastcgi_index index.php:如果请求目录(比如 http://blog.yuxb.cloud/),默认找 index.php

    5. fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name:告诉 PHP-FPM 脚本文件的实际路径(比如 /usr/share/nginx/html/index.php);

    6. include fastcgi_params:加载 FastCGI 的通用参数(比如请求方法、用户 IP 等)。

  • 为什么要这个文件?:Nginx 本身不能处理 PHP 动态代码,必须通过这个配置把 PHP 请求转发给 PHP-FPM 处理。

4.3.3 编写 WordPress 虚拟主机模板(让 Nginx 识别博客域名)
复制代码
 [yuxb@controller lnmp 16:25:38]$ vim vhost-wordpress.conf.j2
  • 这步做什么? :用 vim 创建 vhost-wordpress.conf.j2 模板文件(Jinja2 格式,Ansible 会自动替换变量),内容如下:
复制代码
 server {
     listen       80;
     server_name  {{ blog_vhost }};
     root         /usr/share/nginx/html/{{ blog_vhost }}/wordpress;
     index index.php;
 ​
     include /etc/nginx/default.d/*.conf;
     
     access_log  /var/log/nginx/access-{{ blog_vhost }}.log;
     error_log /var/log/nginx/error-{{ blog_vhost }}.log;
 }
  • 变量和配置项解释:

    内容 含义 作用
    {``{ blog_vhost }} 模板变量,后续会被替换为 blog.yuxb.cloud(博客域名) 避免硬编码,方便修改域名
    listen 80 Nginx 监听 80 端口(HTTP 协议默认端口) 用户访问域名时,默认走 80 端口
    server_name {``{ blog_vhost }} 绑定域名,Nginx 收到该域名的请求就用这个配置 区分不同网站(比如一个服务器跑多个博客)
    root /usr/share/nginx/html/{``{ blog_vhost }}/wordpress WordPress 代码存放的根目录 Nginx 从这个目录找博客的文件
    index index.php 默认首页文件,优先加载 index.php 用户访问域名时,自动打开 index.php
    access_log/error_log 博客的访问日志和错误日志路径 后续排查问题(比如访问失败)时看日志
  • 为什么用模板(.j2 后缀)?:如果后续要改域名,只需改变量,不用改配置文件,更灵活。

4.3.4 本地安装 Nginx(仅为编辑模板,非必须但建议做)
复制代码
 [yuxb@controller lnmp 14:34:30]$ sudo yum install -y nginx
  • 这步做什么? :在控制节点 controller 上安装 Nginx。

  • 为什么这么做? :不是必须的,但安装后可以本地测试 vhost-wordpress.conf.j2 模板是否正确(避免目标机部署后才发现配置错误),实验简化可跳过。

阶段 4:编写 Ansible 核心剧本(自动化部署的 "大脑")

4.4.1 创建并编辑部署剧本
复制代码
 [yuxb@controller lnmp 15:50:25]$ vim deploy_lnmp.yml
  • 这步做什么? :用 vim 创建 deploy_lnmp.yml 剧本文件(Ansible 执行的核心,按 "Play" 分阶段部署),内容如下(每个步骤都有详细注释):
复制代码
 ---
 # ==================== Play 1:部署MariaDB数据库(LNMP的“M”)====================
 - name: deploy mariadb  # Play名称(描述这个阶段做什么)
   hosts: lnmp           # 目标主机组(从inventory文件读取,即node1)
   tasks:                # 该阶段要执行的具体任务(按顺序执行)
 ​
     # 任务1:安装MariaDB服务和PHP连接数据库的依赖
     - name: install mariadb-server
       yum:              # Ansible的yum模块(用于在CentOS上安装软件)
         name:           # 要安装的软件列表
           - mariadb-server  # MariaDB数据库服务(核心)
           - python2-PyMySQL # Python2连接MySQL的模块(Ansible操作MariaDB需要)
         state: present  # 确保软件“已安装”(如果没装就装,装了就不操作)
 ​
     # 任务2:启动MariaDB服务,并设置开机自启
     - name: enable and start mariadb
       service:          # Ansible的service模块(管理系统服务)
         name: mariadb   # 服务名(MariaDB的服务名就是mariadb)
         enabled: yes    # 开机自启(yes=启用,no=禁用)
         state: started  # 服务状态(started=启动,stopped=停止,restarted=重启)
 ​
     # 任务3:首次设置MariaDB的root本地密码(忽略首次执行的错误)
     - name: set root@localhost password
       shell: mysqladmin password {{ mysql_root_password }}  # 执行系统命令设置密码
       ignore_errors: yes  # 忽略错误(首次执行时MariaDB默认无密码,执行该命令可能报错,所以忽略)
 ​
     # 任务4:为不同主机地址(本机域名、回环地址)设置root密码
     - name: set root password
       mysql_user:        # Ansible的mysql_user模块(管理数据库用户)
         name: root       # 要操作的数据库用户(root)
         password: "{{ mysql_root_password }}"  # 密码(从vaults.yml读取,即123)
         host: "{{ item }}"                     # 允许登录的主机地址(循环变量)
         state: present                         # 确保用户“存在”
         login_user: root                       # 登录数据库的用户(用root登录)
         login_password: "{{ mysql_root_password }}"  # 登录密码(root密码)
       with_items:        # 循环执行(为3个地址分别设置密码)
         - "{{ ansible_fqdn }}"  # 目标机的完整域名(比如node1.yuxb.cloud)
         - 127.0.0.1             # IPv4回环地址(本机访问)
         - ::1                   # IPv6回环地址(本机访问)
 ​
     # 任务5:删除数据库匿名用户(提升安全性,避免别人空密码登录)
     - name: delete user anonymous
       mysql_user:
         name: ""         # 匿名用户的名称是空字符串(MariaDB默认有匿名用户)
         host_all: yes    # 所有主机上的匿名用户都删除
         state: absent    # 确保用户“不存在”(即删除)
         login_user: root
         login_password: "{{ mysql_root_password }}"
         #login_unix_socket: /var/lib/mysql/mysql.sock  # 注释:可选的socket登录方式(不用管)
 ​
     # 任务6:删除默认的test数据库(提升安全性,test库默认任何人可访问)
     - name: delete database test
       mysql_db:          # Ansible的mysql_db模块(管理数据库)
         name: test       # 要删除的数据库名(test是默认测试库)
         state: absent    # 确保数据库“不存在”(即删除)
         login_user: root
         login_password: "{{ mysql_root_password }}"
 ​
 ​
 # ==================== Play 2:为WordPress准备数据库(创建专用用户和库)====================
 - name: prepare db for webapp  # Play名称(为Web应用准备数据库)
   hosts: lnmp
   tasks:
 ​
     # 任务1:创建WordPress专用的数据库用户(避免用root提权)
     - name: create user {{ user }}
       mysql_user:
         name: "{{ app_user }}"          # 用户名(从vaults.yml读取,即wordpress)
         password: "{{ app_password }}"  # 密码(从vaults.yml读取,即123)
         host: "{{ app_host }}"          # 允许登录的主机(从vaults.yml读取,即%)
         priv: "{{ app_priv }}"          # 权限(从vaults.yml读取,即*.*:ALL)
         state: present
         login_user: root
         login_password: "{{ mysql_root_password }}"
 ​
     # 任务2:创建WordPress专用的数据库(webapp)
     - name: create database db_name
       mysql_db:
         name: "{{ db_name }}"  # 数据库名(从vars.yml读取,即webapp)
         state: present         # 确保数据库“存在”(即创建)
         login_user: root
         login_password: "{{ mysql_root_password }}"
 ​
 ​
 # ==================== Play 3:部署Nginx服务(LNMP的“N”)====================
 - name: deploy web server  # Play名称(部署网页服务器)
   hosts: lnmp
   tasks:
 ​
     # 任务1:安装Nginx服务
     - name: install nginx
       yum:
         name: nginx
         state: present
 ​
     # 任务2:启动Nginx服务,并设置开机自启
     - name: enable and start nginx
       service:
         name: nginx
         enabled: yes
         state: started
 ​
     # 任务3:创建Nginx测试页面(验证Nginx是否正常运行)
     - name: prepare test file for web server
       copy:  # Ansible的copy模块(复制内容到文件)
         content: hello world from nginx  # 文件内容(测试文本)
         dest: /usr/share/nginx/html/index.html  # 目标文件路径(Nginx默认首页路径)
 ​
 ​
 # ==================== Play 4:部署PHP服务(LNMP的“P”)====================
 - name: php  # Play名称(部署PHP)
   hosts: lnmp
   tasks:
 ​
     # 任务1:安装PHP及依赖(PHP-FPM、MySQL连接驱动)
     - name: install php
       yum:
         name: php,php-fpm,php-mysqlnd  # 要安装的软件(用逗号分隔)
         state: present
       # 软件说明:
       # php:PHP核心
       # php-fpm:PHP进程管理器(处理Nginx转发的PHP请求)
       # php-mysqlnd:PHP连接MySQL/MariaDB的驱动(WordPress需要)
 ​
     # 任务2:修改PHP-FPM的运行用户和组为nginx(权限一致)
     - name: modify running user for php
       lineinfile:  # Ansible的lineinfile模块(修改文件中的某一行)
         path: /etc/php-fpm.d/www.conf  # 要修改的文件(PHP-FPM的用户配置文件)
         regexp: "{{ item}} = "         # 匹配行的正则(找“user = ”或“group = ”)
         line: "{{ item }} = nginx"     # 替换后的内容(把用户/组改成nginx)
       loop:  # 循环修改两个配置项(user和group)
         - user
         - group
       # 为什么改?Nginx默认用nginx用户运行,PHP-FPM也用nginx用户,避免文件权限问题(比如PHP写文件时权限不够)
 ​
     # 任务3:启动PHP-FPM服务,并设置开机自启(重启确保配置生效)
     - name: enable and start php-fpm.service
       service:
         name: php-fpm
         enabled: yes
         state: restarted  # 用restarted(重启),因为刚改了配置,需要生效
 ​
     # 任务4:把本地的php.conf复制到目标机(让Nginx能解析PHP)
     - name: config php for nginx
       copy:
         src: php.conf  # 本地的php.conf文件(3.2步创建的)
         dest: /etc/nginx/default.d/php.conf  # 目标机路径(Nginx会自动加载这个目录的配置)
 ​
     # 任务5:重启Nginx(加载PHP配置,让Nginx能处理PHP请求)
     - name: restart nginx
       service:
         name: nginx
         state: restarted
 ​
 ​
 # ==================== Play 5:部署WordPress博客系统====================
 - name: deploy web app  # Play名称(部署Web应用,即WordPress)
   hosts: lnmp
   vars:  # 该Play的局部变量(只在这个Play里生效)
     blog_vhost: blog.yuxb.cloud  # 博客的域名(后续会替换模板里的{{ blog_vhost }})
   tasks:
 ​
     # 任务1:用模板生成WordPress的Nginx虚拟主机配置
     - name: prepare vhost for wordpress
       template:  # Ansible的template模块(处理Jinja2模板,替换变量)
         src: vhost-wordpress.conf.j2  # 本地模板文件(3.3步创建的)
         dest: /etc/nginx/conf.d/vhost-wordpress.conf  # 目标机路径(Nginx虚拟主机配置目录)
 ​
     # 任务2:创建WordPress的网站根目录(按域名命名,方便管理)
     - name: create /usr/share/nginx/html/{{ blog_vhost }}
       file:  # Ansible的file模块(管理文件/目录)
         path: /usr/share/nginx/html/{{ blog_vhost }}  # 目录路径(替换变量后是/blog.yuxb.cloud)
         state: directory  # 确保目录“存在”(即创建)
 ​
     # 任务3:解压WordPress压缩包到网站根目录
     - name: Unarchive a wordpress file 
       unarchive:  # Ansible的unarchive模块(解压压缩包)
         src: wordpress-4.9.4-zh_CN.zip  # 本地WordPress压缩包(需提前下载到lnmp目录)
         dest: /usr/share/nginx/html/{{ blog_vhost }}/  # 解压目标路径(到博客目录下)
         owner: nginx  # 解压后文件的所有者(nginx,和Nginx/PHP用户一致)
         group: nginx  # 解压后文件的所属组(nginx)
       # 注意:必须提前把wordpress-4.9.4-zh_CN.zip下载到控制节点的lnmp目录,否则会失败!
 ​
     # 任务4:重启Nginx(加载虚拟主机配置,让Nginx识别博客域名)
     - name: restart nginx
       service:
         name: nginx
         state: restarted
  • 剧本关键注意点:

    1. 必须提前下载 wordpress-4.9.4-zh_CN.zip 到控制节点的 lnmp 目录(下载地址:https://wordpress.org/wordpress-4.9.4-zh_CN.zip);

    2. 剧本按 "数据库→Web 服务器→PHP→博客" 顺序执行,不能乱序(比如先装 PHP 再装数据库,PHP 连接数据库会失败)。

阶段 5:执行自动化部署(跑剧本,让 Ansible 干活)

5.1.1 执行 Ansible 剧本
复制代码
 [yuxb@controller lnmp 15:18:48]$ ansible-playbook deploy_lnmp.yml
  • 这步做什么? :执行 deploy_lnmp.yml 剧本,Ansible 会自动连接目标节点 node1,按剧本顺序执行所有任务。

  • 执行过程说明:

    • 输出中会显示每个任务的状态(ok= 已执行过,changed= 刚执行完,failed= 失败);

    • 如果某个任务失败(比如软件安装失败),先看错误信息(通常是网络问题或权限问题),解决后重新执行命令即可(Ansible 支持幂等性,重复执行不会有问题)。

5.1.2 关闭目标机冲突服务(避免端口被占)
复制代码
 # 注意:这步要在目标节点node1上执行,不是控制节点!
 [root@node1 ~ 15:32:00]# systemctl disable httpd --now
  • 这步做什么?:

    1. systemctl disable httpd:禁用 httpd 服务(CentOS 默认的网页服务器),避免开机自启;

    2. --now:立即停止 httpd 服务。

  • 为什么这么做?:httpd 和 Nginx 都默认用 80 端口,同时运行会导致 Nginx 无法使用 80 端口(用户访问博客会失败),所以必须关闭。

阶段 6:验证部署结果(确保每个组件都正常)

6.1.1 验证 WordPress 文件是否部署成功(目标节点 node1)
复制代码
 # 在node1上执行,查看WordPress文件是否存在
 [root@node1 ~ 15:57:26]$ ls /usr/share/nginx/html/blog.yuxb.cloud/wordpress/
  • 正常结果 :会显示 WordPress 的核心文件(如 index.phpwp-admin 目录),和下面一致:
复制代码
 index.php        wp-admin              wp-content         wp-load.php      wp-signup.php
 license.txt      wp-blog-header.php    wp-cron.php        wp-login.php     wp-trackback.php
 readme.html      wp-comments-post.php  wp-includes        wp-mail.php      xmlrpc.php
 wp-activate.php  wp-config-sample.php  wp-links-opml.php  wp-settings.php
  • 如果失败 :检查 Ansible 剧本中 "解压 WordPress" 的任务是否成功,或压缩包是否在控制节点的 lnmp 目录。
复制代码
 # 继续在node1上执行,查看文件权限(确保所有者是nginx)
 [root@node1 ~ 15:58:41]$ ll /usr/share/nginx/html/blog.yuxb.cloud/wordpress/
  • 正常结果 :所有文件的所有者(owner)和所属组(group)都是 nginx(如 -rw-r--r-- 1 nginx nginx 418 9月 25 2013 index.php);

  • 如果权限不对 :检查 Ansible 剧本中 "解压 WordPress" 任务的 ownergroup 是否配置为 nginx

6.2.1 验证 MariaDB 数据库是否正常(目标节点 node1)
复制代码
 # 在node1上执行,用WordPress专用用户登录数据库
 [root@node1 ~ 16:04:31]$ mysql -uwordpress -p123
  • 登录成功标志 :显示 MariaDB 命令行提示符(MariaDB [(none)]>),无报错;

  • 如果登录失败 :检查 vaults.yml 中的 app_userapp_password 是否正确,或 Ansible 剧本中 "创建数据库用户" 的任务是否成功。

复制代码
 # 登录后执行,查看数据库列表
 MariaDB [(none)]> show databases;
  • 正常结果 :显示 webapp 数据库(WordPress 专用库),如下:
复制代码
 +--------------------+
 | Database           |
 +--------------------+
 | information_schema |
 | mysql              |
 | performance_schema |
 | webapp             |
 +--------------------+
 4 rows in set (0.00 sec)
  • 如果没有 webapp 库:检查 Ansible 剧本中 "创建数据库" 的任务是否成功。
复制代码
 # 退出数据库(可选)
 MariaDB [(none)]> exit
 Bye

阶段 7:浏览器访问博客(完成 WordPress 安装)

7.1.1 配置 Windows 客户端 Hosts(让浏览器识别博客域名)
  1. 打开 Windows 文件管理器,进入路径:C:\Windows\System32\drivers\etc

  2. 找到 hosts 文件,右键用 "记事本" 打开(需管理员权限,否则无法保存);

  3. 在文件末尾添加一行:10.1.8.11 blog.yuxb.cloud10.1.8.11 是目标节点 node1 的 IP,blog.yuxb.cloud 是博客域名);

  4. 保存并关闭文件。

  • 为什么这么做? :Windows 不知道 blog.yuxb.cloud 对应哪个 IP,添加后浏览器会把该域名指向 10.1.8.11(目标节点)。
7.1.2 访问 WordPress 并完成安装
  1. 打开浏览器(Chrome、Edge 等),在地址栏输入 blog.yuxb.cloud,按回车;

  2. 进入 WordPress 安装向导,选择 "中文",点击 "继续";

  3. 点击 "现在就开始!",填写数据库信息(和vaults.yml、vars.yml一致):

    • 数据库名:webapp

    • 用户名:wordpress

    • 密码:123

    • 数据库主机:localhost(本地数据库,不用改)

    • 表前缀:默认 wp_(不用改)

  4. 点击 "提交",再点击 "运行安装程序";

  5. 填写博客信息(自定义):

    • 站点标题:比如 "我的第一个博客"

    • 用户名:自定义(比如 admin

    • 密码:自定义(比如 123456,生产环境用复杂密码)

    • 电子邮件:自定义(比如 test@123.com

  6. 点击 "安装 WordPress",安装完成后用刚才的用户名密码登录;

  7. 登录后进入博客后台,或点击 "访问站点" 查看博客首页,至此实验完成!

五、常见问题排查

问题现象 可能原因 解决方法
执行剧本时 "yum 安装软件失败" 目标节点网络不通,或 yum 源有问题 1. 在 node1 上 ping 百度(ping baidu.com)确认网络;2. 更换 yum 源为阿里云(yum install -y wget && wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
浏览器访问 blog.yuxb.cloud 显示 "无法访问" 1. 目标节点 Nginx 没启动;2. 客户端 Hosts 配置错误;3. 防火墙拦截 80 端口 1. 在 node1 上执行 systemctl status nginx 确认 Nginx 已启动;2. 检查 Windows Hosts 配置的 IP 和域名是否正确;3. 在 node1 上关闭防火墙(systemctl stop firewalld && systemctl disable firewalld
WordPress 安装时 "无法连接数据库" 1. 数据库密码输错;2. MariaDB 没启动;3. 数据库用户权限不够 1. 确认密码和 vaults.yml 一致;2. 在 node1 上执行 systemctl status mariadb 确认数据库已启动;3. 检查剧本中 app_priv 是否为 *.*:ALL
访问 PHP 文件显示 "下载文件" 而非执行 Nginx 的 PHP 配置没生效,或 PHP-FPM 没启动 1. 在 node1 上执行 systemctl status php-fpm 确认 PHP-FPM 已启动;2. 检查 /etc/nginx/default.d/php.conf 文件是否存在且内容正确

六、实验总结

  1. 本次实验通过 Ansible 实现了 LNMP + WordPress 的自动化部署,核心是 "剧本驱动",避免手动在目标机敲命令;

  2. 敏感信息用 Ansible Vault 加密,普通配置用变量文件,符合 "安全 + 灵活" 的原则;

  3. 每个组件的作用:Nginx 收请求、PHP 处理动态代码、MariaDB 存数据,三者协同支撑 WordPress 运行;

  4. 零基础同学需重点理解 "剧本的执行顺序" 和 "各组件的联动关系",遇到问题先看日志(Nginx 日志、Ansible 执行日志)。

管理大项目

配置并行

概念

并行配置就是把原本串行执行的任务拆分,使得多个任务可以同时进行,从而加快整体完成速度。这在大项目中尤为重要,比如:

  • 软件开发:编译、测试、打包可以并行。

  • 数据处理:大数据分析任务可以按分区或模块并行。

  • 运维/部署:批量服务器操作可以并行执行。

并行策略

  1. 任务级并行

    • 不同任务之间独立执行。

    • 例:在CI/CD流水线中,同时运行单元测试和代码扫描。

  2. 数据级并行

    • 将数据拆分成多个部分,各自独立处理。

    • 例:大文件分块处理、多节点分布式计算。

  3. 混合并行

    • 结合任务级和数据级。

    • 例:多模块服务并行编译 + 各模块单元测试并行执行。

配置 forks

复制代码
 [yuxb@controller web 16:47:05]$ vim inventory 
 [yuxb@controller web 16:47:48]$ cat playbook.yml 
 ---
 - name: connection
   hosts: all
   tasks:
     - name: conneciton 1
       shell: sleep 5
     - name: conneciton 2
       debug: 
         msg: connection 2
 [yuxb@controller web 16:47:52]$ cat inventory 
 [controller]
 ​
 [webs]
 node1
 node3
 ​
 [dbs]
 node2
 node4
 ​

执行

复制代码
 # 两人一组执行
 [yuxb@controller web 16:48:10]$ ansible-playbook playbook.yml -f 2
 ​
 PLAY [connection] ***********************************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node1]
 ok: [node3]
 ok: [node2]
 ok: [node4]
 ​
 TASK [conneciton 1] *********************************************************************************
 changed: [node3]
 changed: [node1]
 changed: [node2]
 changed: [node4]
 ​
 TASK [conneciton 2] *********************************************************************************
 ok: [node1] => {
     "msg": "connection 2"
 }
 ok: [node3] => {
     "msg": "connection 2"
 }
 ok: [node2] => {
     "msg": "connection 2"
 }
 ok: [node4] => {
     "msg": "connection 2"
 }
 ​
 PLAY RECAP ******************************************************************************************
 node1                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 node2                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 node3                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 node4                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 ​

配置 serial

复制代码
 [yuxb@controller web 16:49:34]$ vim playbook.yml 
 [yuxb@controller web 16:49:57]$ cat playbook.yml 
 ---
 - name: Rolling update
   hosts: all
   serial: 2
   tasks:
     - name: latest apache httpd package is installed
       yum:
         name: httpd
         state: latest
       notify: restart apache
   
   handlers:
     - name: restart apache
       service:
         name: httpd
         state: restarted

执行

复制代码
 [yuxb@controller web 16:50:51]$ ansible-playbook playbook.yml
 ​
 PLAY [Rolling update] *******************************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node3]
 ok: [node1]
 ​
 TASK [latest apache httpd package is installed] *****************************************************
 ok: [node3]
 ok: [node1]
 ​
 PLAY [Rolling update] *******************************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node4]
 ok: [node2]
 ​
 TASK [latest apache httpd package is installed] *****************************************************
 ok: [node2]
 ok: [node4]
 ​
 PLAY RECAP ******************************************************************************************
 node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 node2                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 node3                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 node4                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 ​

配置 async

示例1: 任务执行失败,在规定时间内容任务没有执行完成。

复制代码
 [yuxb@controller web 16:50:01]$ vim playbook.yml 
 [yuxb@controller web 16:52:54]$ cat playbook.yml 
 ---
 - name: connection
   hosts: node1
   tasks:
     - name: conneciton 
       shell: sleep 10
       async: 5
       poll: 2
       
       
 # 执行
 # 中间会卡一会
 [yuxb@controller web 16:51:14]$ ansible-playbook playbook.yml
 ​
 PLAY [connection] ***********************************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node1]
 ​
 TASK [conneciton] ***********************************************************************************
 fatal: [node1]: FAILED! => {"changed": false, "msg": "async task did not complete within the requested time - 5s"}
 ​
 PLAY RECAP ******************************************************************************************
 node1                      : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
 ​

示例2: 放入后台下载,立刻执行下一个任务。

复制代码
 [yuxb@controller web 16:52:58]$ vim playbook.yml 
 [yuxb@controller web 16:54:11]$ cat playbook.yml 
 ---
 - name: connection
   hosts: node1
   tasks:
     - name: download
       get_url: 
         url: http://192.168.48.100/ISOS/openEuler-24.03-LTS-x86_64-dvd.iso
         dest: /home/laoma
       async: 100
       poll: 0
 ​
 # 执行
 # 很快的就执行完毕
 [yuxb@controller web 16:53:14]$ ansible-playbook playbook.yml
 ​
 PLAY [connection] ***********************************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node1]
 ​
 TASK [download] *************************************************************************************
 changed: [node1]
 ​
 PLAY RECAP ******************************************************************************************
 node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 ​

示例3: ansible 默认行为,等该任务执行完,再执行下一个任务。

复制代码
 [yuxb@controller web 16:54:13]$ vim playbook.yml 
 [yuxb@controller web 16:55:18]$ cat playbook.yml 
 ---
 - name: connection
   hosts: node1
   tasks:
     - name: conneciton 
       shell: sleep 10
       async: 0
       poll: 2
 ​
 # 执行
 # 中间会卡一会
 [yuxb@controller web 16:54:26]$ ansible-playbook playbook.yml
 ​
 PLAY [connection] ***********************************************************************************
 ​
 TASK [Gathering Facts] ******************************************************************************
 ok: [node1]
 ​
 TASK [conneciton] ***********************************************************************************
 changed: [node1]
 ​
 PLAY RECAP ******************************************************************************************
 node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 ​