
1. 项目概述与整体设计思路1.1 核心需求解析这年头做全栈项目开发环境跑通只是第一步真正让人头疼的是部署。Spring Boot 后端、Vue 前端、MySQL、Redis四个组件各自有一堆配置要处理手动部署一套下来少说也要半天而且每台服务器都得重复来一遍中间漏掉一个环节排查起来更是要命。这次我做的这套自动化部署方案就是围绕 Ansible 这个自动化运维工具把整个部署流程固化下来。目标很明确一套 playbook 搞定从裸机到可用服务的全部流程包括系统基础配置、镜像源替换、MySQL 和 Redis 安装初始化、JDK 环境配置、Spring Boot 后端部署、Vue 前端构建以及 Nginx 反向代理。整套方案在 RockyLinux 9.6 上实测通过换一台新机器改下 IP 就能跑省下的时间用来摸鱼不香吗适合谁来参考这套方案第一类是正在做全栈项目但部署总出问题的开发同学第二类是刚接触 Ansible 想找个完整实战案例的运维新人第三类是公司内部需要频繁交付测试环境、每次都要手工配半天的人。看完这篇文章你不仅能复现这套部署方案还能理解每一步的设计意图遇到问题知道从哪里排查。1.2 为什么选择 Ansible 而不是其他自动化工具市面上自动化部署工具不少Shell 脚本、Docker Compose、Kubernetes、Ansible各有各的适用场景。我最终选 Ansible核心原因是它在批量配置管理这个维度上最合适。先说说为什么不继续用 Shell 脚本。Shell 脚本的痛点是幂等性差同样的脚本跑两遍可能就出问题。比如修改配置文件第一次执行生效了第二次执行可能把文件内容搞重复了。Ansible 的 playbook 天生强调幂等性每个模块执行前都会先检查当前状态只有状态不符才会执行变更这一点在长期维护中价值极大。Docker Compose 虽然也能一键拉起多个服务但它解决的问题是容器编排不是主机配置。如果业务要求直接部署在宿主机上比如有些公司有安全合规要求或者需要直接操作裸金属环境Compose 就帮不上忙了。Kubernetes 则更像是另一个维度的东西全栈项目用 K8s 属于大炮打蚊子学习成本和运维成本都太高。Ansible 还有一个杀手级特性——无 Agent 架构。它通过 SSH 直接连接目标机器执行任务不需要在每台服务器上预先安装客户端这对于首次初始化一台全新的 RockyLinux 9.6 机器来说太重要了。因为在新机器上你本来就得通过 SSH 登进去做初始配置Ansible 恰好顺着这条路就完成了所有工作。1.3 部署架构梳理这套方案的目标架构是这样组织的一台控制节点可以是你的开发机也可以是专门的跳板机安装 Ansible 即可一台或多台目标服务器RockyLinux 9.6部署 MySQL、Redis、Spring Boot 后端、Nginx承载 Vue 构建产物。后端用 systemd 托管配好环境变量和启动参数前端 Vue 构建出静态文件放到 Nginx 的站点目录Nginx 统一监听 80/443 端口做反向代理把 /api 路径转发给 Spring Boot其他路径直接返回前端静态资源。MySQL 和 Redis 只监听内网地址不直接暴露到公网。整个部署链路拆成四个角色base 负责系统初始化db 负责 MySQLcache 负责 Redisapp 负责后端和前端。每个角色独立成一个目录里面有 tasks、handlers、templates、vars 四个子目录职责边界非常清晰。这样做的好处是哪一层的服务出问题就去对应的角色目录下排查不需要在一个几百行的 playbook 文件里翻来翻去。2. 环境准备Rockylinux 9.6 基础配置2.1 系统安装与初始化设置RockyLinux 9.6 的安装过程本身不复杂官方 ISO 刻到 U 盘引导安装即可。有几个细节建议在安装时就直接处理好。分区方面如果机器是专门跑这套服务的建议给 / 根分区留足空间Vue 构建产物和后端日志都比较吃磁盘。我的建议是 / 至少 60Gswap 根据物理内存大小来定物理内存 8G 以下就分 4G swap16G 以上可以不单独分 swap 或者分 8G 意思一下。数据盘如果有独立磁盘可以单独挂载到 /dataMySQL 的数据目录后面会指过去。网络配置这里要格外留意。RockyLinux 9 默认用 NetworkManager 管理网络安装时图形界面配置好 IP 就行。但如果你用的是云厂商的镜像或者说虚拟机克隆出来的网卡名可能不是 ens160而是 ens3、eth0 之类。Ansible 的 inventory 里写的是 IP 地址所以网卡名不影响自动化部署但建议在安装阶段就固定好主机名我习惯用hostnamectl set-hostname来设置方便后续日志区分。安全相关的一个点RockyLinux 9.6 默认开启 SELinux处于 enforcing 模式。很多新手在部署服务时遇到权限问题第一反应就是setenforce 0这确实能解决眼前问题但不推荐作为长期方案。后面我会详细说怎么在 SELinux 开启的情况下让服务正常运行。2.2 镜像源替换与基础依赖新装好的 RockyLinux 9.6 默认用的是官方源在国内下载速度非常慢尤其是安装 MySQL 这种动辄几百兆的包等得人想摔键盘。所以第一件事就是换镜像源。我用的是清华的镜像源操作方法如下# 先备份原有的 repo 文件 sudo sed -e s|^mirrorlist|#mirrorlist|g \ -e s|^#baseurlhttp://dl.rockylinux.org/$contentdir|baseurlhttps://mirrors.tuna.tsinghua.edu.cn/rocky|g \ -i.bak \ /etc/yum.repos.d/Rocky-*.repo跑完上面这条命令再用dnf makecache更新缓存源就换好了。这里有两点要说明一是$contentdir这个变量指向/rocky路径不同镜像站的目录结构不一样一定要确认你用的镜像支持这个路径格式二是 dnf 的缓存会和服务器的版本绑定如果后续系统升级了小版本可能需要再执行一次dnf makecache。基础依赖方面我一般会装这些vim、git、tar、unzip、wget、tree还有编译工具链gcc gcc-c make因为有些 Python 包在安装时需要现场编译。虽然 Ansible 本身是 Python 写的我控制节点直接用系统自带的 Python 3.9不需要额外装什么。2.3 Ansible 控制节点准备这里的关键点在于区分控制节点和目标节点。控制节点是发出指令的机器目标节点是真正运行服务的机器。Ansible 只需要安装在控制节点上目标节点只需要能被 SSH 连接即可。控制节点的 Ansible 安装很简单sudo dnf install -y epel-release sudo dnf install -y ansible ansible --version如果系统自带的 ansible 版本太旧也可以用 pip 装新版但 RockyLinux 9.6 自带的版本我测过是 2.14 左右基本够用。我这里用的是 2.14playbook 里的语法全部兼容。安装完后需要配置 SSH 免密登录。Ansible 虽然也支持密码登录通过-k参数输入密码但每次执行都输密码太痛苦而且密钥认证更安全。生成密钥对并分发到目标机器ssh-keygen -t rsa -b 4096 ssh-copy-id root192.168.1.101这条命令会把控制节点的公钥追加到目标机器的~/.ssh/authorized_keys里。我建议所有操作都用 root 用户不是说不可以用普通用户加 sudo只是在自动化场景下 root 的身份模型最简单不容易出权限问题。你要是对安全有要求可以建一个专用账号并配置 sudo 权限但对应的 Ansible playbook 里所有任务都要考虑提权复杂度会明显上升。3. Ansible 项目结构与 Inventory 设计3.1 项目目录规划一个规范的 Ansible 项目目录结构一定要清晰这是后面维护体验的天壤之别。我用的结构如下ansible-fullstack/ ├── ansible.cfg ├── inventory/ │ ├── production.ini │ └── group_vars/ │ ├── all.yml │ ├── db.yml │ ├── cache.yml │ └── app.yml ├── roles/ │ ├── base/ │ ├── db/ │ ├── cache/ │ └── app/ └── playbooks/ ├── deploy.yml └── health.ymlansible.cfg是 Ansible 的配置文件里面主要指定 inventory 路径、角色目录路径、SSH 连接参数等inventory目录放主机清单roles目录是核心每个角色管一块独立职责playbooks目录放编排入口。这种结构是我实践过很多次之后沉淀下来的适合中小型项目不冗杂也不简陋。3.2 主机清单与变量分层inventory 文件是 Ansible 的地图它告诉 Ansible 指挥哪些机器。我这里用一个简单的 production.ini[all:vars] ansible_userroot ansible_ssh_private_key_file~/.ssh/id_rsa ansible_python_interpreter/usr/bin/python3 [db] 192.168.1.101 [cache] 192.168.1.101 [app] 192.168.1.101 [deploy:children] db cache app注意这里有个技巧db、cache、app 三组机器可以是同一台机器。用[deploy:children]这个语法把三个组聚合起来playbook 里就可以针对deploy组执行全部任务也可以单独对某一个组执行特定任务灵活度很高。变量分层的设计也值得讲讲。group_vars/all.yml放全局通用变量比如项目名称、版本号、时区group_vars/db.yml放数据库相关变量比如 MySQL 的 root 密码、业务库名group_vars/app.yml放应用相关变量比如 Spring Boot 的 JVM 参数、Vue 构建时的 API 地址。这样做的目的是避免在 playbook 和角色里写死具体值——改配置只在变量文件里改不动代码逻辑。一个很重要的教训是vars 文件里绝对不能写生产密码至少不能直接写明文。我一般用 Ansible Vault 对敏感变量做加密ansible-vault encrypt inventory/group_vars/db.yml执行 playbook 时需要加--ask-vault-pass参数输入 Vault 密码这样即使仓库代码泄露数据库密码也不至于跟着泄露。3.3 Playbook 的编排思路Playbook 是整个自动化流程的剧本ansible 剧本这个词在网络上的搜索热度一直很高说明大家对这个核心概念确实感兴趣。我这里的 deploy.yml 长这样--- - name: 全栈应用基础环境配置 hosts: deploy gather_facts: yes become: yes roles: - base - name: 数据库部署 hosts: db become: yes roles: - role: db when: install_mysql | default(true) - name: 缓存服务部署 hosts: cache become: yes roles: - role: cache when: install_redis | default(true) - name: 应用部署 hosts: app become: yes roles: - app可以看到我把编排分成四个 play每个 play 对应一个角色。为什么分成四个而不是一个大 play 里串四个角色因为 Ansible 在一个 play 里会执行完角色所有任务后再进入下一个角色中途如果某个角色失败后面的角色不会执行。分成多个 play 后可以单独用--limit参数只运行某一段比如数据库装好了但应用没部署成功修复了应用问题后只需要重跑 app 相关的 play不用把所有服务重新跑一遍。gather_facts: yes这个配置也很重要。Ansible 默认会在 play 开始时收集目标机器的系统信息CPU、内存、磁盘、发行版版本等这些信息存在变量里后面配置模板时可以直接引用。设置时区、判断包管理器类型这些任务都会用到 facts。4. 核心环节数据库角色部署详解4.1 MySQL 8 的自动化安装MySQL 是这套系统里最重的依赖安装步骤多、坑也多所以我把这个角色单独拿出来详细讲。RockyLinux 9.6 自带的软件源里没有 MySQL需要先从官方仓库添加Ansible 里对应的任务是这样写的- name: 添加 MySQL 官方仓库 dnf: name: https://dev.mysql.com/get/mysql80-community-release-el9-1.noarch.rpm state: present - name: 安装 MySQL 服务 dnf: name: - mysql-community-server state: present update_cache: yes这里有个细节Ansible 的 dnf 模块会检测到源里没有要装的包就自动update_cache但显式写出来更保险。安装包前最好能确认一下目标机器的网络能访问到 dev.mysql.com不然会一直卡在下载阶段。装完之后第一件要做的事是设置数据库数据目录。默认情况下 MySQL 数据存放在/var/lib/mysql而我把数据盘挂在了/data所以需要配置自定义路径。这里有个关键步骤MySQL 的 SELinux 上下文必须正确否则服务启动时无法访问新数据目录。正确的处理方式是这样的- name: 创建 MySQL 数据目录 file: path: {{ mysql_datadir }} state: directory owner: mysql group: mysql mode: 0750 - name: 配置 MySQL 数据目录 SELinux 上下文 sefcontext: target: {{ mysql_datadir }}(/.*)? setype: mysqld_db_t state: present - name: 应用 SELinux 上下文 command: restorecon -Rv {{ mysql_datadir }}restorecon这步不能省否则即使设置了 sefcontext已存在的文件上下文也不会自动更新。初始化 MySQL 有--initialize和--initialize-insecure两种模式区别在于前者会生成一个临时 root 密码在日志文件里后者 root 密码默认为空。自动化场景下我更喜欢用后者因为密码处理更可控然后在 playbook 里直接设置新密码- name: 初始化 MySQL 数据目录 command: mysqld --initialize-insecure --usermysql --datadir{{ mysql_datadir }} - name: 启动 MySQL systemd: name: mysqld state: started enabled: yes - name: 修改 root 密码 mysql_user: login_user: root login_password: user: root host: localhost password: {{ mysql_root_password }}这里要注意首次连接 MySQL 时 root 密码为空mysql_user模块不加login_password就能连上。改完密码后后面的任务都要带上login_password参数。4.2 MySQL 性能参数与业务库初始化MySQL 装好只是第一步配置调优才是真正的生产力。很多从开发环境转过来的同学默认配置直接用结果一上生产就出问题慢查询、缓存命中率低、连接数打满。我用模板渲染的方式管理 my.cnf 的核心参数[mysqld] datadir/data/mysql socket/var/lib/mysql/mysql.sock symbolic-links0 max_connections500 innodb_buffer_pool_size2G innodb_log_file_size256M character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci default-storage-engineInnoDB slow_query_logON slow_query_log_file/data/mysql/slow-query.log long_query_time2关于innodb_buffer_pool_size的设置业界通用的建议是物理内存的 70% 左右但也要结合机器实际用途看。如果这台机器还跑了 Spring BootJava 应用默认会吃掉不少内存建议把比例调到物理内存的 40%~50%否则 MySQL 和 Java 可能互相争抢内存导致 OOM。业务库和账号的创建放在角色最后。我习惯通过模板传递 SQL再用 shell 执行- name: 创建业务数据库和用户 shell: | mysql -uroot -p{{ mysql_root_password }} EOF CREATE DATABASE IF NOT EXISTS {{ db_name }} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS {{ db_user }}% IDENTIFIED BY {{ db_password }}; GRANT ALL PRIVILEGES ON {{ db_name }}.* TO {{ db_user }}%; FLUSH PRIVILEGES; EOF这里为什么不直接用 mysql_db 和 mysql_user 模块因为有些版本的 Ansible 模块在处理多条 SQL 和特殊字符时会翻车比如密码里有、#这类字符。用 heredoc 方式最直观也最不容易出问题前提是先把 SQL 脚本放到模板文件里不要直接拼变量防止注入和转义问题。4.3 Redis 6 安装与安全加固Redis 的自动化安装相比 MySQL 要简单一个量级但简单不代表可以忽略安全配置。市面上存在大量因为 Redis 未授权访问被入侵的案例这点必须重视。安装部分用 dnf 一条命令就搞定重点在配置上。我的 Redis 模板里有这么几项bind 127.0.0.1 protected-mode yes port 6379 daemonize no requirepass {{ redis_password }} appendonly yes maxmemory 512mb maxmemory-policy allkeys-lruprotected-mode yes和bind 127.0.0.1是双保险即使配置了密码也限制只能本机访问。这里有个部署上的细节——如果 Spring Boot 和 Redis 在同一台机器上这种配置完全没问题但如果 Redis 在独立的机器上Spring Boot 要通过内网 IP 访问那么 bind 就得改成内网 IP并且要在防火墙里放行 6379 端口。Redis 的 systemd 配置我这里也单独做了。RockyLinux 9 的 Redis 包默认带了 systemd unit 文件有个要注意的点是daemonize no因为 systemd 管理下的进程不能 fork 到后台去否则 systemd 会认为服务启动失败。maxmemory 设置也很关键不设置的话 Redis 可能把机器内存吃光严重影响同机部署的 MySQL 和 Java 应用。我习惯用 maxmemory 限制 Redis 最多用 512MB淘汰策略用 allkeys-lru保证在内存满时优先淘汰最少使用的 key而不是直接拒绝写入。5. 应用角色Spring Boot 与 Vue 部署5.1 JDK 17 与 Maven 构建后端要跑起来JDK 是前提。RockyLinux 9.6 的软件源里默认带了 OpenJDK 17这也是 Spring Boot 3.x 的推荐版本。用 dnf 模块安装即可- name: 安装 OpenJDK 17 dnf: name: java-17-openjdk-devel state: present这里我特意装了java-17-openjdk-devel而不是java-17-openjdk区别在于 devel 版本包含了完整的 JDK 工具链javac、jar 等不只是 JRE。虽然构建在控制节点做还是目标节点做可以商榷但装了 devel 总归能应付更多场景。关于后端构建方式我采用的是在控制节点构建然后拷贝 jar 包到目标机器的模式。具体流程是控制节点上有 Node.js 和 Maven 环境先把 Spring Boot 项目mvn clean package打成 jar再通过 Ansible 的 copy 模块把 jar 分发到目标机器。这么做的好处是目标机器不需要安装 Maven 和 Node.js 构建工具链减少了部署面的复杂度也降低了容器被塞满一堆编译工具的风险。有一个细节容易踩坑spring boot 四层架构controller、service、mapper、entity/domain在代码层面怎么组织跟部署关系不大但 jar 包的名字和位置要统一管理。我习惯在应用的 pom.xml 里显式指定finalName比如fullstack-app这样打包出来的 jar 永远是fullstack-app.jarAnsible 里 copy 的 src 和 dest 路径就不会随版本号变化而要改来改去。5.2 systemd 服务托管 Spring Boot后端进程的管理我用了 systemd 而不是 nohup Shell 脚本。systemd 的好处是开机自启、崩溃自动重启、日志统一管理这三个能力对生产环境太重要了。模板文件如下[Unit] DescriptionFullstack Spring Boot Application Afternetwork.target mysqld.service redis.service [Service] Typesimple Userappuser Groupappuser WorkingDirectory/opt/fullstack/app EnvironmentFile/opt/fullstack/app/application.env ExecStart/usr/bin/java -Xms512m -Xmx1g -jar /opt/fullstack/app/fullstack-app.jar --spring.profiles.activeprod SuccessExitStatus143 Restarton-failure RestartSec10 [Install] WantedBymulti-user.target几个关键参数解释一下Aftermysqld.service redis.service等 MySQL 和 Redis 启动后再启动后端避免因为数据库还没就绪导致启动失败EnvironmentFile环境变量文件存放数据库连接串、密码、Redis 地址这些敏感信息不让它们出现在 systemd unit 文件和代码仓库里SuccessExitStatus143Spring Boot 优雅停机时会收到 SIGTERM 信号退出143 128 15显式声明这个退出码是正常行为防止 systemd 误判为失败Userappuser用专用账号跑应用避免 root 权限过大。写环境变量文件时有个小技巧Spring Boot 的配置文件 application.yml 里可以用${DB_URL}这种占位符引用环境变量而 EnvironmentFile 里直接写DB_URLjdbc:mysql://...。这样应用代码和部署配置彻底解耦换环境只需要换 EnvironmentFile。5.3 Vue 前端构建与 Nginx 部署前端的自动化部署是整个流程里比较讲究顺序的一个环节。Vue 项目需要先安装依赖npm install再执行构建npm run build生成 dist 目录后把静态文件拷贝到 Nginx 站点目录。这个流程用 Ansible 写出来很清晰- name: 安装前端依赖 npm: path: {{ app_src_dir }}/frontend state: present delegate_to: localhost run_once: true - name: 构建前端生产包 command: npm run build -- --modeproduction args: chdir: {{ app_src_dir }}/frontend delegate_to: localhost run_once: true - name: 创建 Nginx 静态资源目录 file: path: {{ nginx_root }} state: directory notify: reload nginx - name: 同步前端构建产物 synchronize: src: {{ app_src_dir }}/frontend/dist/ dest: {{ nginx_root }}/ delete: yesdelegate_to: localhost和run_once: true是这套流程的精髓。前者告诉 Ansible 这个任务不在目标机器上执行而是在控制节点本机执行后者保证即使主机组里有多台机器也只执行一次。前端构建这种耗时任务放控制节点避免每台目标机器都构建一遍浪费时间也浪费资源。Nginx 的安装和配置同样用角色管理核心的 server 配置块如下server { listen 80; server_name _; root /var/www/fullstack; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /m3u8/ { alias /data/videos/; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } } }这里面有几个 Vue 项目特有的坑点。第一Vue Router 如果是 history 模式刷新非首页的 URL 会 404必须配置try_files $uri $uri/ /index.html兜底第二接口请求如果路径带 /api 前缀Nginx 需要把它转发到后端同时要设置正确的请求头第三如果你在开发环境用 vue 播放 m3u8 视频流部署时同样要在 Nginx 里配置对应的 Content-Type否则浏览器不认识 m3u8 文件直接拒绝播放。5.4 Nginx 的 SELinux 与路径权限问题Nginx 在 RockyLinux 上跑最常遇到的问题就是上游连不通。明明后端在 8080 端口监听得好好的Nginx 反代就是 502 Bad Gateway大概率是 SELinux 拦住了 Nginx 的出站网络连接。判断方法很简单setsebool -P httpd_can_network_connect 1Ansible 里对应写法是- name: 允许 Nginx 转发网络请求 seboolean: name: httpd_can_network_connect state: yes persistent: yes还有一个常见问题Nginx 读取自定义静态文件目录时被 SELinux 拒绝。默认的/usr/share/nginx/html上下文是 httpd_sys_content_t如果你把站点目录自定义到/var/www/fullstack需要设置正确的类型标签- name: 设置 Nginx 静态目录上下文 sefcontext: target: {{ nginx_root }}(/.*)? setype: httpd_sys_content_t state: present notify: restorecon这个坑我踩过一次之后就把所有自定义路径的 SELinux 配置在 playbook 里提前写好了不再等到报错才去补。6. Playbook 编排与执行实战6.1 从零到可用的完整执行流程所有的角色和配置都准备好之后部署入口就一行命令cd ansible-fullstack ansible-playbook -i inventory/production.ini playbooks/deploy.yml --ask-vault-pass执行过程 Ansible 会打印每个任务的执行状态我用-v参数可以获得更详细的输出方便观察哪里有异常。整个 playbook 正常跑完的耗时取决于网络状况通常在 5 到 10 分钟大头都在 MySQL 安装和前端构建这两步。有个经验要分享第一次跑 playbook 时不要用--limit或者跳过角色让所有角色完整跑一遍。为什么因为角色的依赖关系是隐藏的比如 app 角色里的 systemd template 引用了 db 角色创建的变量即使你只想部署应用也要保证 db 组的主机可达或者相关变量有默认值。第一次全量执行能尽早发现变量和路径引用的问题。6.2 幂等性验证与重复执行Ansible 的一个核心优势就是可以安全地重复执行。我之前用 Shell 脚本部署的时候最怕的就是用户说帮我再跑一遍因为脚本重复执行经常会把服务搞挂。Ansible 的每个模块都自带幂等逻辑比如dnf模块发现包已经安装了就不会重复装template模块对比文件内容没变化就不触发服务重载。但 playbook 完全幂等吗答案是不一定。比如我的 MySQL 初始化任务用的是command: mysqld --initialize-insecure这个任务第一次执行时数据目录是空的初始化成功第二次执行时数据目录已有文件MySQL 会报错退出。解决办法是加一个条件判断- name: 初始化 MySQL 数据目录 command: mysqld --initialize-insecure --usermysql --datadir{{ mysql_datadir }} args: creates: {{ mysql_datadir }}/mysqlcreates参数的意思是如果这个文件已经存在这个任务就被标记为 ok不会执行。这个写法比我最初用的when: not mysql_initialized.stat.exists要简洁高效得多。验证幂等性最直接的方法就是连续跑两遍 playbook第二次跑完看输出里所有任务都应该是 ok 而不是 changed如果有 changed 的任务要分析一下是正常更新还是配置漂移。6.3 健康检查与一键验证部署完成不等于万事大吉我习惯在 playbook 最后加一个验证段或者单独写一个 health check playbook用来确认所有服务的健康状态。简单有效的检查方式- name: 检查后端健康检查接口 uri: url: http://127.0.0.1:8080/actuator/health status_code: 200 register: backend_health - name: 检查 MySQL 连接 mysql_info: login_user: root login_password: {{ mysql_root_password }} register: mysql_status - name: 检查 Redis 连接 redis: login_password: {{ redis_password }} command: ping register: redis_ping关于后端健康检查有两点提示。第一Spring Boot 自带的 actuator 模块如果暴露了 health 端点务必不要泄露敏感信息Spring Boot actuator 未授权访问这个问题网上讨论热度很高现实中确实有人利用未授权 actuator 端点拿到服务器信息和配置我在生产配置里把 health 之外的其他端点全部关闭或用 Spring Security 保护起来了。第二健康检查频率不宜太频繁自动化脚本里每分钟探测一次足够了不然日志里全是检查记录。7. 常见问题与排查技巧实录7.1 MySQL 类问题MySQL 启动失败日志提示权限错误这类问题十有八九是数据目录的属主不对或者 SELinux 上下文配置错误。排查思路# 查看是否属主正确 ls -ld /data/mysql # 查看 SELinux 上下文 ls -Z /data/mysql # 查看服务日志定位具体报错 journalctl -u mysqld -n 50属主不对用chown -R mysql:mysql /data/mysql修正上下文不对用我前面的 sefcontext restorecon 方案。远程连接 MySQL 报 Access denied先用 root 在本地登录确认密码正确再检查远端用户是否存在且允许对应来源 IP 连接。常见问题是CREATE USER xx%和GRANT都执行了但 MySQL 默认绑定 127.0.0.1远端根本连不上。排查从应用侧用telnet 192.168.1.101 3306测试端口通不通开始逐步缩小范围。7.2 Redis 类问题Spring Boot 连接 Redis 超时最常见的坑是 Redis 只监听本地回环地址而 Spring Boot 配置里指向了内网 IP。检查 Redis 配置文件的 bind 字段应用和 Redis 同机的话直接用 127.0.0.1 访问最省事。Redis 内存占用异常增高排除业务本身的使用量重点检查是不是设置了 maxmemory。我用 allkeys-lru 淘汰策略后内存高的时候会自动淘汰冷数据问题基本可控。7.3 Spring Boot 与前端类问题systemd 服务启动失败但手动执行 jar 正常这个问题很有意思很多新手卡在这里。systemd 环境是最小环境它不会继承你 SSH 登录时的环境变量。比如你手动执行时 JAVA_HOME 已经设在了 /etc/profile 里但 systemd 不读这个文件。解决方法是在 unit 文件里显式配置EnvironmentJAVA_HOME/usr/lib/jvm/java-17-openjdk或者用ExecStart/usr/bin/java的绝对路径。Vue 构建产物刷新页面 404这就是我前面说的history 模式路由需要 Nginx 的 try_files 兜底。如果已经配置了try_files仍然 404检查一下 Nginx 的配置文件是否真正加载生效了用nginx -t验证语法再 reload。Vue 打包后布局异常构建后布局异常多半是静态资源路径问题。Vue 默认的资源引用路径是绝对路径/js/app.js如果部署在子路径下就会 404。解决方式是在 vue.config.js 里把publicPath设为./相对路径但要注意相对路径在 history 路由模式下可能有问题更好的方案是保持根路径部署。另外打包前先 clean dist 目录再 build防止旧文件残留影响判断。7.4 Ansible 自身的小坑报错 /usr/bin/python3 not found 或 no module named yumAnsible 默认用目标机器的 python 执行任务如果系统里 python3 不在默认路径需要改ansible_python_interpreter在 inventory 里显式指定/usr/bin/python3即可。某个任务执行超时可以通过给模块加 timeout 参数解决比如dnf模块支持timeout: 300。还有一个做法是把耗时的任务通过async异步执行后面再轮询结果但复杂度高了不少非必要不建议。8. 最后分享几个部署经验做这套自动化部署方案的过程中最深的体会是自动化不是减少工作量而是把工作量前置。写角色、调模板、处理各个模块的边界情况前期开发成本确实比手动部署一次高很多但当你面对第三台、第五台服务器时优势就会立刻凸显出来。我现在的部署节奏是拿到一台新的 RockyLinux 9.6 机器改一下 inventory 里的 IP10 分钟以内所有服务就绪。还有一点想提醒大家Ansible 的角色设计不要一开始就追求完美。我第一版是把所有任务都放在一个 playbook 文件里的随着需要管理的机器和配置项越来越多才逐步拆成角色。如果你刚接触先跑通一个小项目感受到自动化带来的效率提升再慢慢重构也不迟不要一开始就迷失在最佳实践的细节里。最后一个小技巧部署完成后建议在控制节点写一个简单的备忘脚本我习惯叫它 deploy.sh里面封装 ansible-playbook 命令和常用参数。这样哪怕隔了几个月再回来操作也只需要跑一条命令不需要回忆完整的命令格式。自动化部署这东西做一次很简单坚持做下来才有真正的复利效应。