--- # ============================================================================= # XINFRA MySQL 原生交付 Playbook # ============================================================================= # 功能:在目标主机上从零开始安装、配置并启动一个 MySQL 实例 # 支持拓扑:standalone(单机)、primary_replica(一主一从)、mgr_3(三节点组复制) # 支持版本:8.0(Ubuntu 自带源锁版本)、8.4(MySQL 官方 APT 源 LTS 组件) # 设计特点: # - 同一主机可部署多个实例(systemd 模板单元 + 独立配置/数据目录) # - 挂载点无关:数据目录统一在 {data_disk}/mysql-delivery/{instance_id}/ 下 # - 幂等性:数据目录初始化有 creates 守护,配置变更触发 notify: Restart # ============================================================================= - name: Native MySQL delivery hosts: "{{ target_hosts }}" become: true # 提权执行 gather_facts: true # 收集目标主机信息(用于后续 ansible_mounts、ansible_distribution_release 等) any_errors_fatal: true # 任一主机失败则整个 play 中止(避免部分主机成功、部分失败的不一致状态) vars: # ==================== 基础身份信息 ==================== mysql_instance: "{{ instance_name }}" # 实例唯一标识,用于目录/配置/systemd 服务名 mysql_version_value: "{{ mysql_version | default('8.0') }}" # MySQL 版本,默认 8.0 # ==================== 版本包映射 ==================== # 8.0 使用 Ubuntu 24.04 自带源,精确锁版本(确保可复现) # 8.4 使用 MySQL 官方 APT 源的 mysql-8.4-lts 组件(noble 自带源无 8.4) # 5.6/5.7 已官方 EOL 且 Ubuntu 24.04 无可用 apt 包,不纳入白名单 mysql_package_map: "8.0": package: "mysql-server=8.0.46-0ubuntu0.24.04.3" # 精确锁版本 repo_component: "" # 无需额外 APT 源 "8.4": package: "mysql-community-server" # 官方源组件内版本号 repo_component: "mysql-8.4-lts" # MySQL 官方源组件名 mysql_package_name: "{{ (mysql_package_map[mysql_version_value] | default({})).package | default('') }}" mysql_repo_component: "{{ (mysql_package_map[mysql_version_value] | default({})).repo_component | default('') }}" # ==================== 平台分配的输入参数(带安全兜底) ==================== # 端口池 13306–13999:平台从池分配并传入;未传时兜底池首端口 mysql_port_value: "{{ mysql_port | default(13306) | int }}" # GR 组通信端口:仅 mgr_3 拓扑使用,平台成对分配;兜底为 SQL 端口 +10000 mysql_gr_port_value: "{{ gr_port | default((mysql_port | default(13306) | int) + 10000) | int }}" # 数据盘挂载点:平台按白名单选定并传入;兜底 /data mysql_data_disk: "{{ data_disk | default('/data') }}" # ==================== 资源配额 ==================== # 单位保持 MB/GB(与平台侧 Go→AWX 契约一致,不擅自修改) mysql_memory_mb_value: "{{ memory_mb | default(4096) | int }}" # 内存配额,默认 4G mysql_storage_gb_value: "{{ storage_gb | default(50) | int }}" # 存储配额,默认 50G # ==================== 挂载点无关的目录布局 ==================== # 所有实例的数据/日志/临时文件统一放在 {data_disk}/mysql-delivery/{instance_id}/ 下 # 这样无论数据盘挂载在 /data、/mnt/ssd 还是其他路径,结构都一致 mysql_base_dir: "{{ mysql_data_disk }}/mysql-delivery/{{ mysql_instance }}" # 数据根目录 mysql_install_dir: "/opt/mysql-delivery/{{ mysql_instance }}" # 实例安装目录(二进制符号链接) mysql_data_dir: "{{ mysql_base_dir }}/data" # MySQL 数据目录 mysql_log_dir: "{{ mysql_base_dir }}/logs" # 日志根目录 mysql_binlog_dir: "{{ mysql_base_dir }}/logs/binlog" # binlog 和 relay log 目录 mysql_redo_dir: "{{ mysql_base_dir }}/logs/redo" # InnoDB redo log 目录 mysql_tmp_dir: "{{ mysql_base_dir }}/tmp" # 临时文件目录 # socket/pid 放在 /run tmpfs(重启自动清理),由 systemd RuntimeDirectory 自动创建 mysql_run_dir: "/run/mysql-delivery-{{ mysql_instance }}" mysql_config_file: "/etc/mysql/mysql-delivery/{{ mysql_instance }}.cnf" # 配置文件路径 # ==================== 数据库基础配置 ==================== mysql_timezone: "{{ timezone | default('+08:00') }}" # 时区,默认东八区 mysql_lower_case_table_names: "{{ lower_case_table_names | default(1) | int }}" # 表名大小写敏感,1=不敏感 mysql_character_set: "{{ character_set | default('utf8mb4') }}" # 字符集,默认 utf8mb4 mysql_collation: "{{ collation | default('utf8mb4_general_ci') }}" # 排序规则 # ==================== 高级参数(可覆盖,默认假设 SSD) ==================== mysql_flush_log_at_trx_commit: "{{ innodb_flush_log_at_trx_commit | default(1) | int }}" # 事务提交时刷日志,1=最安全 mysql_sync_binlog: "{{ sync_binlog | default(1) | int }}" # 每次提交同步 binlog,1=最安全 mysql_io_capacity: "{{ innodb_io_capacity | default(2000) | int }}" # InnoDB I/O 能力,默认 2000(SSD 适合) mysql_long_query_time: "{{ long_query_time | default(1) }}" # 慢查询阈值,单位秒 mysql_binlog_expire_logs_seconds: "{{ binlog_expire_logs_seconds | default(604800) | int }}" # binlog 过期时间,默认 7 天 mysql_max_binlog_size: "{{ max_binlog_size | default('256M') }}" # 单个 binlog 文件最大大小 mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}" # GR 组名(UUID 格式) # ==================== 拓扑节点数 ==================== # standalone=1、primary_replica=2、mgr_3=3,用于校验 inventory 中的主机数 mysql_expected_hosts: "{{ {'standalone': 1, 'primary_replica': 2, 'mgr_3': 3}[topology | default('standalone')] }}" # ==================== 敏感信息(通过环境变量注入) ==================== # 密码通过环境变量 XINFRA_MYSQL_ROOT_PASSWORD / XINFRA_MYSQL_ADMIN_PASSWORD 传入 # 不写入 playbook 或 inventory,避免泄露 mysql_root_password_value: "{{ lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD') }}" mysql_admin_password_value: "{{ lookup('ansible.builtin.env', 'XINFRA_MYSQL_ADMIN_PASSWORD') }}" # ==================== 前置校验任务 ==================== # 在正式安装前执行参数校验和资源检查,尽早失败(fail-fast) pre_tasks: # --- 校验所有交付参数是否在白名单范围内 --- - name: Validate delivery parameters ansible.builtin.assert: that: - topology in ['standalone', 'primary_replica', 'mgr_3'] # 拓扑必须是三种之一 - mysql_instance is match('^[a-z0-9][a-z0-9-]{0,62}$') # 实例名:小写字母/数字开头,仅含小写字母/数字/短横线,最长63 - mysql_version_value in mysql_package_map # 版本必须在包映射表中 - mysql_data_disk is match('^/') # 数据盘必须是绝对路径 - (mysql_port_value | int) >= 13306 # 端口下限 - (mysql_port_value | int) <= 13999 # 端口上限(端口池范围) - (mysql_memory_mb_value | int) >= 2048 # 最小 2G 内存 - (mysql_memory_mb_value | int) <= 65536 # 最大 64G 内存 - (mysql_storage_gb_value | int) >= 20 # 最小 20G 存储 - (mysql_storage_gb_value | int) <= 2000 # 最大 2T 存储 - (mysql_lower_case_table_names | int) in [0, 1] # 只能是 0 或 1 - mysql_root_password_value | length >= 16 # root 密码长度 ≥ 16 - mysql_admin_password_value | length >= 16 # admin 密码长度 ≥ 16 fail_msg: >- 交付参数超出目标状态白名单(拓扑/版本包映射/端口池13306-13999/内存2-64G/存储20-2000G) quiet: true # 静默模式,不打印整个 that 列表 no_log: true # 避免密码等敏感信息出现在日志中 # --- 校验拓扑要求的主机数与实际 inventory 主机数一致 --- - name: Validate topology host count ansible.builtin.assert: that: - (ansible_play_hosts_all | length | int) == (mysql_expected_hosts | int) fail_msg: "拓扑={{ topology }} 期望 {{ mysql_expected_hosts }} 台主机,实际 {{ ansible_play_hosts_all | length }} 台" run_once: true # 只在第一台主机执行(全局校验) # --- 探测目标主机的端口占用情况(SQL 端口 + GR 端口) --- # 使用 ss 命令检查端口是否已被监听;如果已被占用,检查是否属于当前实例(允许幂等重跑) - name: Probe target-host port occupancy (SQL + GR) # 注意:使用 cmd 字典形式而非自由形式,避免 heredoc 中的引号/Jinja 块被 split_args 误解析 ansible.builtin.shell: cmd: | set -o pipefail for p in {{ mysql_probe_ports | join(' ') }}; do if ss -lntH "sport = :${p}" | grep -q .; then # 端口已被占用,但如果是当前实例的服务在用则允许(幂等重跑场景) if ! systemctl is-active --quiet "mysql-delivery@{{ mysql_instance }}.service"; then echo "端口 ${p} 在目标主机已被其他服务占用" >&2 exit 3 fi fi done executable: /bin/bash vars: # mgr_3 拓扑需要探测两个端口(SQL + GR),其他拓扑只探测 SQL 端口 mysql_probe_ports: "{{ [mysql_port_value, mysql_gr_port_value] if topology == 'mgr_3' else [mysql_port_value] }}" changed_when: false # 只读操作,不产生变更 # --- 检查目标主机当前可用内存 --- - name: Read currently available memory (point-in-time guard) ansible.builtin.shell: awk '/^MemAvailable:/ { print int($2 / 1024) }' /proc/meminfo args: executable: /bin/bash register: mysql_available_memory changed_when: false # 只读操作 # --- 获取数据盘挂载点的可用空间 --- # 优先使用 mysql_data_disk 挂载点的空间,如果找不到则回退到根分区 / - name: Resolve data-disk mountpoint available space ansible.builtin.set_fact: mysql_mount_avail: >- {{ (ansible_mounts | selectattr('mount', 'equalto', mysql_data_disk) | map(attribute='size_available') | first) | default(ansible_mounts | selectattr('mount', 'equalto', '/') | map(attribute='size_available') | first) }} # --- 最终检查:可用内存和磁盘空间是否满足配额 --- - name: Check available memory and data-disk space ansible.builtin.assert: that: - (mysql_available_memory.stdout | int) >= (mysql_memory_mb_value | int) # 可用内存 ≥ 配额 - (mysql_mount_avail | int) >= (mysql_storage_gb_value | int) * 1073741824 # 可用空间 ≥ 配额(GB→字节) fail_msg: >- 目标主机内存或 {{ mysql_data_disk }} 磁盘空间不足; 主机级配额预算是平台侧职责,这里是最后的安全兜底。 # ==================== 主任务 ==================== tasks: # --- 检测主机上是否已安装 MySQL --- - name: Detect an existing MySQL server package ansible.builtin.command: dpkg-query -W -f '${Package}=${Version}\n' mysql-server mysql-community-server register: mysql_package_before failed_when: false # 未安装时不报错 changed_when: false # 只读操作 # --- 提取已安装的 MySQL 版本号 --- - name: Resolve the installed MySQL server version ansible.builtin.set_fact: mysql_installed_version: >- {{ (mysql_package_before.stdout_lines | select('search', '=') | list | first | default('')).split('=') | last }} # --- 强制执行主机级 MySQL 版本系列一致性 --- # /usr/sbin/mysqld 等二进制是全机共享的,一台主机只能运行一个 MySQL 版本系列 # 如果已安装 8.0.x,不能再安装 8.4.y - name: Enforce host-level MySQL series consistency ansible.builtin.assert: that: - mysql_installed_version == '' or mysql_installed_version.startswith(mysql_version_value ~ '.') fail_msg: >- 主机已安装 MySQL {{ mysql_installed_version }},但请求的是 {{ mysql_version_value }}; /usr 下的二进制是全机共享的,一台主机只能承载一个 MySQL 版本系列。 # ==================== 安装 MySQL 软件包 ==================== # 仅 8.4 需要添加官方 APT 源(8.0 使用 Ubuntu 自带源,无需额外配置) - name: Install the MySQL APT repository signing key ansible.builtin.get_url: url: https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 # MySQL 官方 GPG 签名密钥 dest: /etc/apt/keyrings/mysql.asc owner: root group: root mode: '0644' when: mysql_repo_component != '' # 仅 8.4 需要 - name: Configure the MySQL APT repository component ansible.builtin.apt_repository: repo: >- deb [signed-by=/etc/apt/keyrings/mysql.asc] http://repo.mysql.com/apt/ubuntu {{ ansible_distribution_release }} {{ mysql_repo_component }} filename: xinfra-mysql-delivery # 自定义源文件名,避免与已有源冲突 state: present when: mysql_repo_component != '' # 仅 8.4 需要 # 安装 MySQL 服务端包(apt 会自动安装依赖如 mysql-client 等) - name: Install the MySQL server package ansible.builtin.apt: name: "{{ mysql_package_name }}" state: present update_cache: true # 安装前更新 apt 缓存 cache_valid_time: 3600 # 缓存有效期 1 小时 # apt 安装 MySQL 后会自动创建并启动一个默认的 mysql.service # 我们使用自定义的 systemd 模板单元管理实例,所以需要停止并禁用默认实例 - name: Stop the automatically created default instance on a clean host ansible.builtin.systemd_service: name: mysql.service state: stopped enabled: false when: mysql_installed_version == '' # 仅在全新安装时执行 # ==================== AppArmor 配置 ==================== # MySQL 的 AppArmor 配置文件限制了 mysqld 只能访问特定目录 # 我们使用自定义路径,需要授权 delivery 目录的访问权限 - name: Check for the bundled MySQL AppArmor profile ansible.builtin.stat: path: /etc/apparmor.d/usr.sbin.mysqld # 检查 AppArmor 配置文件是否存在 register: mysql_apparmor_profile - name: Authorize the delivery paths in the MySQL AppArmor profile ansible.builtin.copy: dest: /etc/apparmor.d/local/usr.sbin.mysqld # local/ 目录下的规则会追加到主配置 owner: root group: root mode: '0644' content: | # XINFRA MySQL delivery 管理 - 授权实例原生路径访问 {{ mysql_data_disk }}/mysql-delivery/ r, # 允许读取 delivery 根目录 {{ mysql_data_disk }}/mysql-delivery/** rwk, # 允许读写创建 delivery 子目录下所有内容 /run/mysql-delivery-*/ rw, # 允许读取运行时目录 /run/mysql-delivery-*/** rwk, # 允许读写运行时目录下所有内容(socket/pid) when: mysql_apparmor_profile.stat.exists register: mysql_apparmor_local - name: Reload the MySQL AppArmor profile ansible.builtin.command: apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld # 重新加载 AppArmor 配置 when: mysql_apparmor_profile.stat.exists and mysql_apparmor_local.changed # 仅配置变更时重载 changed_when: true # ==================== 创建实例目录结构 ==================== # 所有实例的数据/日志/配置文件等放在统一的目录布局中 - name: Create instance directories (mount-point-agnostic layout) ansible.builtin.file: path: "{{ item.path }}" state: directory owner: "{{ item.owner }}" group: "{{ item.group }}" mode: "{{ item.mode }}" loop: - { path: /etc/mysql/mysql-delivery, owner: root, group: mysql, mode: '0750' } # 配置文件父目录 - { path: "{{ mysql_install_dir }}", owner: root, group: root, mode: '0755' } # 实例安装目录(二进制链接) - { path: "{{ mysql_base_dir }}", owner: mysql, group: mysql, mode: '0750' } # 数据根目录 - { path: "{{ mysql_data_dir }}", owner: mysql, group: mysql, mode: '0750' } # MySQL 数据目录 - { path: "{{ mysql_log_dir }}", owner: mysql, group: mysql, mode: '0750' } # 日志目录 - { path: "{{ mysql_binlog_dir }}", owner: mysql, group: mysql, mode: '0750' } # binlog 目录 - { path: "{{ mysql_redo_dir }}", owner: mysql, group: mysql, mode: '0750' } # redo log 目录 - { path: "{{ mysql_tmp_dir }}", owner: mysql, group: mysql, mode: '0750' } # 临时文件目录 # --- 在实例目录中创建指向系统二进制的符号链接 --- # 虽然二进制在 /usr 下,但在实例目录创建链接方便管理,也便于未来版本隔离 - name: Link native binaries into the instance directory ansible.builtin.file: src: "{{ item.src }}" dest: "{{ mysql_install_dir }}/{{ item.dest }}" state: link loop: - { src: /usr/sbin/mysqld, dest: mysqld } # mysqld 守护进程 - { src: /usr/bin/mysql, dest: mysql } # MySQL 客户端 - { src: /usr/bin/mysqladmin, dest: mysqladmin } # MySQL 管理工具 # ==================== 计算节点角色和 server-id ==================== - name: Calculate host role and server id ansible.builtin.set_fact: # 节点索引:在 inventory 中的位置(从 0 开始) mysql_node_index: "{{ ansible_play_hosts_all.index(inventory_hostname) }}" # 节点角色: # standalone 拓扑 → standalone # primary_replica 拓扑 → 第一个节点为 primary,其余为 replica # mgr_3 拓扑 → 第一个节点为 primary(用于初始引导),其余为 mgr mysql_node_role: >- {{ 'standalone' if topology == 'standalone' else ('primary' if ansible_play_hosts_all.index(inventory_hostname) == 0 else ('replica' if topology == 'primary_replica' else 'mgr')) }} # server-id 必须全机唯一,用于复制标识 # 平台可显式传入 mysql_server_id;否则用 port + node_index 派生(端口池模型保证端口唯一) mysql_server_id_value: >- {{ mysql_server_id | default((mysql_port_value | int) + (ansible_play_hosts_all.index(inventory_hostname))) | int }} # --- 将内存从 MB 转换为 GB,用于后续梯度计算 --- - name: Resolve memory tier (GB) ansible.builtin.set_fact: mysql_memory_gb: "{{ ((mysql_memory_mb_value | int) // 1024) | int }}" # --- 根据内存梯度计算 max_connections 和 redo log 容量 --- # 这些是合理的默认值,可根据实际硬件校准调整 - name: Resolve linkage-derived defaults (illustrative tiers, pending hardware calibration) ansible.builtin.set_fact: # 最大连接数:按内存梯度递增 # 2G→200、4G→500、8G→1000、16G→2000、32G→4000、64G→8000、>64G→16000 mysql_max_connections: >- {{ (max_connections | int) if (max_connections is defined and (max_connections | string) != 'auto') else (200 if (mysql_memory_gb | int) <= 2 else 500 if (mysql_memory_gb | int) <= 4 else 1000 if (mysql_memory_gb | int) <= 8 else 2000 if (mysql_memory_gb | int) <= 16 else 4000 if (mysql_memory_gb | int) <= 32 else 8000 if (mysql_memory_gb | int) <= 64 else 16000) }} # InnoDB redo log 容量:按内存梯度递增 # ≤4G→128M、≤16G→256M、≤32G→512M、>32G→1G mysql_redo_capacity: >- {{ innodb_redo_log_capacity if (innodb_redo_log_capacity is defined) else ('128M' if (mysql_memory_gb | int) <= 4 else '256M' if (mysql_memory_gb | int) <= 16 else '512M' if (mysql_memory_gb | int) <= 32 else '1G') }} # ==================== 生成 MySQL 配置文件 ==================== # 使用 Jinja2 模板渲染实例配置,写入后通知 handler 重启实例 - name: Write instance configuration ansible.builtin.template: src: templates/mysql-instance.cnf.j2 dest: "{{ mysql_config_file }}" owner: root group: mysql mode: '0640' # 仅 root 可读,mysql 组可读 notify: Restart MySQL delivery instance # 配置变更时触发重启 # ==================== 初始化数据目录 ==================== # 使用 mysqld --initialize-insecure 初始化(无密码),后续通过 SQL 设置密码 # 幂等:如果 data 目录下已有 auto.cnf,说明已初始化过,跳过 - name: Initialize the data directory once ansible.builtin.command: argv: - /usr/sbin/mysqld - "--defaults-file={{ mysql_config_file }}" - --initialize-insecure # 初始无密码,后续设置 - --user=mysql args: creates: "{{ mysql_data_dir }}/auto.cnf" # 幂等守卫:文件存在则跳过 no_log: true # 避免初始化日志泄露敏感信息 # ==================== 安装 Systemd 模板单元 ==================== # mysql-delivery@.service 是模板单元,%i 被替换为实例名 # 支持同一主机运行多个 MySQL 实例(如 mysql-delivery@db01、mysql-delivery@db02) - name: Install the delivery systemd template ansible.builtin.copy: src: files/mysql-delivery@.service dest: /etc/systemd/system/mysql-delivery@.service owner: root group: root mode: '0644' register: mysql_systemd_unit # 重载 systemd 以识别新安装的模板单元 - name: Reload systemd units ansible.builtin.systemd_service: daemon_reload: true when: mysql_systemd_unit.changed # ==================== 启动 MySQL 实例 ==================== - name: Start the MySQL delivery instance ansible.builtin.systemd_service: name: "mysql-delivery@{{ mysql_instance }}.service" state: started enabled: true # 设置开机自启 # 等待 MySQL socket 文件出现(表示实例已启动完成) - name: Wait for the local MySQL socket ansible.builtin.wait_for: path: "{{ mysql_run_dir }}/mysql.sock" timeout: 60 # 最长等待 60 秒 # ==================== 配置管理账号 ==================== # 通过 shell 脚本完成: # 1. 如果 root 未设密码(initialize-insecure 初始状态),通过 socket 本地连接设置密码 # 2. 创建 xinfra_admin 管理账号,授予所有权限 - name: Configure local administrative accounts # 注意:使用 cmd 字典形式而非自由形式,heredoc SQL 中的奇数单引号不会被 split_args 误解析 ansible.builtin.shell: cmd: | set -euo pipefail # 创建临时客户端配置文件和 SQL 文件,退出时自动清理 client_file="$(mktemp)" sql_file="$(mktemp)" trap 'rm -f "$client_file" "$sql_file"' EXIT chmod 600 "$client_file" "$sql_file" # 写入客户端连接配置(使用 socket 连接,不走 TCP) cat >"$client_file" <<'EOF' [client] user=root password={{ mysql_root_password_value }} socket={{ mysql_run_dir }}/mysql.sock EOF # 尝试用配置的密码连接,如果失败说明 root 还没设密码,需要设置 if ! /usr/bin/mysql --defaults-extra-file="$client_file" -e 'SELECT 1' >/dev/null 2>&1; then cat >"$sql_file" <<'EOF' ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ mysql_root_password_value | replace("'", "''") }}'; EOF /usr/bin/mysql --protocol=socket --socket={{ mysql_run_dir }}/mysql.sock -uroot <"$sql_file" fi # 创建/更新 xinfra_admin 管理账号(允许从任意主机连接,拥有所有权限) cat >"$sql_file" <<'EOF' CREATE USER IF NOT EXISTS 'xinfra_admin'@'%' IDENTIFIED BY '{{ mysql_admin_password_value | replace("'", "''") }}'; ALTER USER 'xinfra_admin'@'%' IDENTIFIED BY '{{ mysql_admin_password_value | replace("'", "''") }}'; GRANT ALL PRIVILEGES ON *.* TO 'xinfra_admin'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; EOF /usr/bin/mysql --defaults-extra-file="$client_file" <"$sql_file" executable: /bin/bash changed_when: false # 已有账号时不产生变更 no_log: true # 避免密码出现在日志中 # ==================== 健康检查 ==================== # 验证 MySQL TCP 端口可连通,确保实例正常对外服务 - name: Verify MySQL TCP health ansible.builtin.wait_for: host: "{{ ansible_host | default(inventory_hostname) }}" port: "{{ mysql_port_value }}" timeout: 30 # 最长等待 30 秒 # --- HA 拓扑提示 --- # 非 standalone 拓扑:配置已就绪(GTID/binlog/relay/GR 参数已写入), # 但复制接线(CHANGE REPLICATION SOURCE)和组复制引导(START GROUP_REPLICATION) # 尚未自动化,需要平台侧另行处理 - name: Note pending HA runtime orchestration ansible.builtin.debug: msg: >- 拓扑={{ topology }} 已部署 HA 就绪配置(GTID/binlog/relay/GR 参数就位), 但复制接线(CHANGE REPLICATION SOURCE)和组复制引导(START GROUP_REPLICATION) 尚未自动化 - 节点仅以配置就绪状态启动。 when: topology != 'standalone' run_once: true # 只打印一次 # ==================== Handlers ==================== # 配置变更时触发的处理动作 handlers: - name: Restart MySQL delivery instance ansible.builtin.systemd_service: name: "mysql-delivery@{{ mysql_instance }}.service" state: restarted