Files
xinfra/ansible/mysql-deploy.yml
T
wonder c26790f84b docs(ansible): 为 mysql-deploy 相关文件添加详细中文注释
- mysql-deploy.yml: 为所有变量、任务、handlers 添加中文注释,解释参数含义、设计逻辑和平台契约
- mysql-delivery@.service: 为 Systemd 模板单元添加中文注释,说明 %i 参数机制、Type=notify、OOMScoreAdjust 等配置项
- mysql-instance.cnf.j2: 为 MySQL 配置模板添加中文注释,说明各参数含义、版本差异、复制配置逻辑
- inventory.example.yml: 为 Inventory 示例添加中文注释,说明主机组与拓扑对应关系及使用方式
2026-07-28 11:05:57 +08:00

470 lines
28 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
# =============================================================================
# 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