Python自动化部署Keepalived双VIP:一键搞定高可用集群

在运维工程师的日常工作中,高可用集群的搭建与维护是一项既基础又至关重要的任务。想象一下,当你需要在几十甚至上百台服务器上快速部署一套高可用环境,手动操作不仅耗时费力,还极易因配置差异导致生产事故。传统的部署方式,从源码下载、依赖安装、配置文件修改到服务启动,每一步都充满了重复劳动和潜在的人为错误。这正是自动化脚本大显身手的地方。本文将带你深入探讨如何利用Python脚本,将Keepalived双VIP的部署过程彻底自动化,实现从零到一的一键式部署,将原本可能需要数小时的工作压缩到几分钟内完成,并且确保每一次部署都精准无误。无论你是希望提升个人效率的DevOps工程师,还是负责大规模基础设施管理的团队负责人,这套自动化方案都将为你带来全新的工作体验。

1. 理解Keepalived与双VIP架构的核心价值

在深入自动化脚本之前,我们有必要先厘清Keepalived和双VIP架构究竟解决了什么问题。简单来说,Keepalived是一个基于VRRP(虚拟路由器冗余协议)的开源软件,它的核心目标是实现服务的高可用性。VRRP协议允许多台服务器共享一个或多个虚拟IP地址(VIP),当主服务器发生故障时,备用服务器能够自动接管这些VIP,从而保证服务对外部用户的连续性,实现故障的平滑转移。

那么,什么是“双VIP”呢?这通常指在一个高可用集群中,配置两个独立的虚拟IP地址。这种设计模式带来了几个显著的优点:

  • 服务隔离与负载分担:两个VIP可以分别承载不同的服务或流量。例如,一个VIP指向Web服务器集群,另一个VIP指向数据库访问层。这样,即使其中一个服务出现故障或需要维护,也不会影响另一个服务的正常运行。
  • 提升资源利用率:在传统的“主-备”模式下,备用服务器在大部分时间是闲置的。而通过双VIP,可以设计为“互为主备”或更复杂的负载均衡模式,让所有服务器都参与到业务处理中,提高了硬件资源的整体利用率。
  • 增强系统容错能力:单一VIP的故障域相对集中。双VIP架构在物理或逻辑上进行了进一步划分,降低了单点故障对整个系统的影响范围。

然而,手动配置这样的环境是繁琐的。你需要分别在多台服务器上执行几乎相同的操作序列,任何一处细微的配置差异(如路由ID、优先级、网卡名称)都可能导致脑裂(Split-Brain)或VIP漂移失败。这正是自动化部署脚本的价值所在:将最佳实践固化到代码中,用一致性消灭人为错误,用速度提升响应能力

2. 自动化部署脚本的设计蓝图与准备工作

一个健壮的自动化部署脚本,其设计思路应该像建造房屋一样,先打好地基,再搭建结构。我们的Python脚本目标明确:从远程获取Keepalived源码,在目标服务器上完成编译安装,并根据预设参数生成正确的双VIP配置文件,最后启动服务并验证。整个过程无需人工干预。

在动笔编写脚本之前,我们需要为“施工”准备好“场地”和“工具”。这主要涉及目标服务器的环境预检。

目标服务器环境要求

检查项 要求说明 检查命令示例
操作系统 推荐CentOS/RHEL 7.x 或 8.x,其他基于RPM的发行版可能需调整包管理器命令。 cat /etc/redhat-release
Python版本 Python 2.7 或 Python 3.x。脚本应具备良好的版本兼容性。 python --versionpython3 --version
权限 执行脚本的用户需要拥有sudo权限或直接为root用户,以便安装软件包和修改系统目录。 sudo whoami
网络连通性 服务器必须能够访问互联网(用于下载源码包)或指定的内部软件源。 ping -c 3 www.keepalived.org
基础开发工具 需确保gcc, make, wgetcurl等工具可用,脚本通常会包含安装逻辑。 which gcc make wget

注意:在生产环境中运行自动化脚本前,务必在测试环境中进行充分验证。特别是网络防火墙策略,需要允许VRRP协议报文(协议号112)在集群节点间通信。

除了环境,我们还需要明确部署的核心参数。这些参数将在脚本开头以变量的形式定义,使得脚本具有高度的可配置性。一个清晰的参数列表是脚本可维护性的关键。

# 脚本核心配置参数示例
KEEPALIVED_VERSION = '2.2.2'        # Keepalived版本
DOWNLOAD_URL = f'https://www.keepalived.org/software/keepalived-{KEEPALIVED_VERSION}.tar.gz'
INSTALL_PREFIX = '/usr/local/keepalived'  # 安装路径
SOURCE_DIR = '/tmp/keepalived_src'        # 源码解压目录

# 节点角色与网络配置
IS_MASTER = True                    # 当前节点是否为主节点
NETWORK_INTERFACE = 'ens192'        # 物理网卡名称
VIRTUAL_ROUTER_ID = 88              # 虚拟路由器ID,集群内必须一致
PRIORITY_MASTER = 100               # 主节点优先级
PRIORITY_BACKUP = 90                # 备节点优先级
ADVERT_INT = 1                      # VRRP通告间隔(秒)

# 双VIP配置
VIP_LIST = [
    {'ip': '192.168.1.100', 'label': f'{NETWORK_INTERFACE}:vip1'},  # 第一个VIP
    {'ip': '192.168.1.101', 'label': f'{NETWORK_INTERFACE}:vip2'},  # 第二个VIP
]

# 健康检查脚本(可选)
CHECK_SCRIPT_PATH = '/usr/local/scripts/check_nginx.sh'

3. 分步拆解:Python脚本的模块化实现

一个复杂的自动化任务最好的实现方式就是将其分解为多个独立的、功能单一的模块。我们的部署脚本可以划分为以下几个核心函数,每个函数负责一个明确的子任务。

3.1 环境检查与依赖安装

这是脚本安全执行的第一道关卡。我们需要检查必要的命令是否存在,并安装编译Keepalived所需的开发库。

import os
import sys
import subprocess
import shutil

def check_and_install_prerequisites():
    """检查并安装必要的依赖包"""
    print("[INFO] 开始检查系统环境与安装依赖...")
    
    # 定义必需的包列表(以CentOS为例)
    packages = [
        'gcc', 'make', 'autoconf', 'libtool',
        'openssl-devel', 'libnfnetlink-devel',
        'pkgconfig', 'wget'
    ]
    
    missing_packages = []
    for pkg in packages:
        # 通过检查命令是否存在或尝试查询rpm包来判断
        if subprocess.call(['which', pkg.split()[0]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
            # 更精确的方法:使用包管理器查询 `rpm -q`
            check_cmd = ['rpm', '-q', pkg]
            if subprocess.call(check_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
                missing_packages.append(pkg)
    
    if missing_packages:
        print(f"[INFO] 发现缺失的依赖包: {', '.join(missing_packages)}")
        try:
            # 使用yum安装缺失的包,假设是root或sudo权限
            install_cmd = ['yum', 'install', '-y'] + missing_packages
            result = subprocess.run(install_cmd, check=True, capture_output=True, text=True)
            print(f"[SUCCESS] 依赖包安装成功。")
        except subprocess.CalledProcessError as e:
            print(f"[ERROR] 依赖包安装失败: {e.stderr}")
            sys.exit(1)
    else:
        print("[INFO] 所有系统依赖已满足。")

这个函数首先通过whichrpm -q命令组合判断软件包是否已安装,然后使用yum install -y批量安装缺失的项。使用subprocess.run并设置check=True可以在命令执行失败时自动抛出异常,便于错误处理。

3.2 源码下载与编译安装

接下来,脚本需要从官方源下载指定版本的Keepalived源码,并完成经典的configure, make, make install三部曲。

def download_and_compile_keepalived(version, download_url, source_dir, install_prefix):
    """下载指定版本的Keepalived源码并编译安装"""
    print(f"[INFO] 准备安装Keepalived版本 {version}...")
    
    # 创建源码目录
    os.makedirs(source_dir, exist_ok=True)
    tar_file = os.path.join(source_dir, f'keepalived-{version}.tar.gz')
    extracted_dir = os.path.join(source_dir, f'keepalived-{version}')
    
    # 步骤1: 下载源码包
    if not os.path.exists(tar_file):
        print(f"[INFO] 正在从 {download_url} 下载源码包...")
        try:
            subprocess.run(['wget', '-q', '-O', tar_file, download_url], check=True)
        except subprocess.CalledProcessError:
            # 如果wget失败,尝试使用curl
            print("[INFO] wget失败,尝试使用curl...")
            subprocess.run(['curl', '-sL', download_url, '-o', tar_file], check=True)
        print("[SUCCESS] 源码包下载完成。")
    else:
        print("[INFO] 发现已存在的源码包,跳过下载。")
    
    # 步骤2: 解压源码包
    if not os.path.exists(extracted_dir):
        print("[INFO] 正在解压源码包...")
        subprocess.run(['tar', '-xzf', tar_file, '-C', source_dir], check=True)
    
    os.chdir(extracted_dir)
    
    # 步骤3: 配置、编译、安装
    print("[INFO] 开始配置编译选项...")
    configure_cmd = ['./configure', f'--prefix={install_prefix}']
    # 可以添加更多配置选项,例如指定内核目录
    # configure_cmd.extend(['--with-kernel-dir=/usr/src/kernels/xxx'])
    
    try:
        subprocess.run(configure_cmd, check=True)
        print("[INFO] 配置完成,开始编译...")
        subprocess.run(['make', '-j$(nproc)'], check=True) # -j 参数利用多核加速编译
        print("[INFO] 编译完成,开始安装...")
        subprocess.run(['make', 'install'], check=True)
        print(f"[SUCCESS] Keepalived 已成功安装到 {install_prefix}")
    except subprocess.CalledProcessError as e:
        print(f"[ERROR] 编译安装过程出错: {e}")
        sys.exit(1)
    
    return install_prefix

这个函数清晰地展示了从下载到安装的完整流程。其中,-j$(nproc)参数能自动根据CPU核心数启动并行编译任务,显著加快编译速度。函数最后返回了安装路径,供后续步骤使用。

3.3 动态生成双VIP配置文件

这是脚本最核心的部分之一。我们需要根据传入的角色参数(主/备)和VIP列表,动态生成正确的keepalived.conf配置文件。手动编写配置文件字符串容易出错,使用模板或结构化的方式更佳。

def generate_keepalived_conf(config_params):
    """根据参数动态生成Keepalived配置文件内容"""
    is_master = config_params['is_master']
    priority = config_params['priority_master'] if is_master else config_params['priority_backup']
    state = 'MASTER' if is_master else 'BACKUP'
    interface = config_params['network_interface']
    router_id = config_params['virtual_router_id']
    advert_int = config_params['advert_int']
    vip_list = config_params['vip_list']
    check_script = config_params.get('check_script_path')
    
    config_lines = []
    config_lines.append('! Configuration File for keepalived - Auto generated by Python script')
    config_lines.append('')
    config_lines.append('global_defs {')
    config_lines.append('   router_id LVS_{}'.format('MASTER' if is_master else 'BACKUP'))
    config_lines.append('   vrrp_skip_check_adv_addr')
    config_lines.append('   vrrp_strict')
    config_lines.append('}')
    config_lines.append('')
    
    # 如果有健康检查脚本,则定义vrrp_script
    if check_script and os.path.exists(check_script):
        config_lines.append(f'vrrp_script chk_service {{')
        config_lines.append(f'    script "{check_script}"')
        config_lines.append('    interval 2   # 检查间隔')
        config_lines.append('    timeout 2    # 超时时间')
        config_lines.append('    fall 3       # 连续失败次数触发失败')
        config_lines.append('    rise 2       # 连续成功次数恢复成功')
        config_lines.append('    weight -5    # 检查失败时优先级降低的值')
        config_lines.append('}')
        config_lines.append('')
    
    # 为每一个VIP生成一个vrrp_instance
    for index, vip_info in enumerate(vip_list, start=1):
        vip_ip = vip_info['ip']
        vip_label = vip_info.get('label', f'{interface}:vip{index}')
        
        config_lines.append(f'vrrp_instance VI_{index} {{')
        config_lines.append(f'    state {state}')
        config_lines.append(f'    interface {interface}')
        config_lines.append(f'    virtual_router_id {router_id + index - 1}') # 为每个实例分配不同的VRID
        config_lines.append(f'    priority {priority}')
        config_lines.append(f'    advert_int {advert_int}')
        config_lines.append('    authentication {')
        config_lines.append('        auth_type PASS')
        config_lines.append('        auth_pass 12345678  # 建议在生产环境中使用更复杂的密码')
        config_lines.append('    }')
        config_lines.append(f'    virtual_ipaddress {{')
        config_lines.append(f'        {vip_ip}/24 dev {interface} label {vip_label}')
        config_lines.append('    }')
        if check_script:
            config_lines.append('    track_script {')
            config_lines.append('        chk_service')
            config_lines.append('    }')
        # 可以添加track_interface来监控网卡状态
        config_lines.append('    track_interface {')
        config_lines.append(f'        {interface}')
        config_lines.append('    }')
        config_lines.append('}')
        config_lines.append('')
    
    return '\n'.join(config_lines)

# 使用函数生成配置
config_params = {
    'is_master': IS_MASTER,
    'priority_master': PRIORITY_MASTER,
    'priority_backup': PRIORITY_BACKUP,
    'network_interface': NETWORK_INTERFACE,
    'virtual_router_id': VIRTUAL_ROUTER_ID,
    'advert_int': ADVERT_INT,
    'vip_list': VIP_LIST,
    'check_script_path': CHECK_SCRIPT_PATH
}
config_content = generate_keepalived_conf(config_params)
print("生成的配置文件内容预览:")
print(config_content[:500]) # 打印前500字符预览

这个函数通过一个字典接收所有配置参数,然后使用字符串拼接的方式生成标准的Keepalived配置。它为每个VIP创建独立的vrrp_instance,并确保虚拟路由器ID(virtual_router_id)不同,这是配置双VIP的关键。同时,它优雅地处理了可选的健康检查脚本集成。

3.4 系统集成与服务管理

安装和配置完成后,需要将Keepalived集成到系统服务中,并设置开机自启。

def setup_system_integration(install_prefix, config_content):
    """配置系统服务、环境变量并启动Keepalived"""
    print("[INFO] 开始进行系统集成配置...")
    
    # 1. 创建必要的系统目录并复制配置文件
    etc_keepalived_dir = '/etc/keepalived'
    os.makedirs(etc_keepalived_dir, exist_ok=True)
    
    config_file_path = os.path.join(etc_keepalived_dir, 'keepalived.conf')
    with open(config_file_path, 'w') as f:
        f.write(config_content)
    print(f"[SUCCESS] 配置文件已写入: {config_file_path}")
    
    # 2. 创建systemd服务单元文件(如果不存在)
    systemd_service_path = '/usr/lib/systemd/system/keepalived.service'
    if not os.path.exists(systemd_service_path):
        # 从编译目录复制或手动创建服务文件
        source_service_file = os.path.join(install_prefix, 'etc', 'sysconfig', 'keepalived')
        if os.path.exists(source_service_file):
            shutil.copy(source_service_file, '/etc/sysconfig/')
        
        # 创建基本的systemd服务文件
        service_content = """[Unit]
Description=LVS and VRRP High Availability Monitor
After=network-online.target syslog.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/var/run/keepalived.pid
KillMode=process
EnvironmentFile=-/etc/sysconfig/keepalived
ExecStart={}/sbin/keepalived $KEEPALIVED_OPTIONS
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target
""".format(install_prefix)
        with open(systemd_service_path, 'w') as f:
            f.write(service_content)
        print(f"[SUCCESS] systemd服务文件已创建: {systemd_service_path}")
    
    # 3. 创建软链接,方便命令行直接调用
    keepalived_bin = os.path.join(install_prefix, 'sbin', 'keepalived')
    link_path = '/usr/sbin/keepalived'
    if not os.path.exists(link_path):
        os.symlink(keepalived_bin, link_path)
        print(f"[SUCCESS] 已创建软链接: {link_path} -> {keepalived_bin}")
    
    # 4. 重载systemd并启动服务
    subprocess.run(['systemctl', 'daemon-reload'], check=True)
    print("[INFO] 正在启动Keepalived服务...")
    start_result = subprocess.run(['systemctl', 'start', 'keepalived'], capture_output=True, text=True)
    if start_result.returncode == 0:
        print("[SUCCESS] Keepalived服务启动成功。")
        # 设置开机自启
        subprocess.run(['systemctl', 'enable', 'keepalived'], check=False)
        print("[INFO] 已设置Keepalived开机自启。")
    else:
        print(f"[ERROR] Keepalived服务启动失败: {start_result.stderr}")
        # 尝试查看日志以排查问题
        subprocess.run(['journalctl', '-u', 'keepalived', '-n', '20', '--no-pager'])
        sys.exit(1)
    
    # 5. 验证服务状态和VIP绑定
    print("[INFO] 验证服务状态...")
    subprocess.run(['systemctl', 'status', 'keepalived', '--no-pager', '-l'])
    print("[INFO] 检查虚拟IP绑定情况...")
    subprocess.run(['ip', 'addr', 'show', 'dev', NETWORK_INTERFACE])

这个函数完成了从配置文件落地到服务运行的全套集成工作。它特别注意了错误处理,在服务启动失败时会自动调用journalctl查看最新的日志,这对于调试非常有用。最后,通过ip addr命令直观地展示VIP是否已正确绑定到网卡上。

4. 从脚本到工具:进阶优化与实践建议

一个能工作的脚本只是一个开始。要让它在生产环境中真正可靠、易用,我们还需要从多个维度进行打磨和优化。

1. 增强脚本的健壮性与可维护性

  • 参数外部化:不要将配置硬编码在脚本里。可以使用argparse库处理命令行参数,或者从YAMLJSON配置文件中读取。这样,同一份脚本可以轻松应对不同的部署环境(如测试、预生产、生产)。
    import argparse
    import yaml
    
    def load_config(config_file):
        with open(config_file, 'r') as f:
            return yaml.safe_load(f)
    
    if __name__ == '__main__':
        parser = argparse.ArgumentParser(description='自动化部署Keepalived')
        parser.add_argument('-c', '--config', default='deploy_config.yaml', help='配置文件路径')
        parser.add_argument('-n', '--node-role', choices=['master', 'backup'], required=True, help='节点角色')
        args = parser.parse_args()
        
        config = load_config(args.config)
        # 根据args.node_role和config执行部署...
    
  • 完善的日志记录:使用Python的logging模块替代print,可以方便地设置日志级别(DEBUG, INFO, WARNING, ERROR),并将日志输出到文件,便于事后审计和排查问题。
  • 幂等性设计:脚本应该可以安全地多次运行。在关键步骤(如创建目录、安装软件包)前先检查是否已存在,避免重复操作或产生冲突。

2. 与配置管理工具结合 对于超大规模集群,单纯的Python脚本可能仍显吃力。此时,可以将其封装为Ansible模块SaltStack Execution Module。这样,你就能利用这些成熟的配置管理工具提供的并发执行、状态管理、变量系统等强大功能。

例如,一个简化的Ansible角色目录结构可能如下:

roles/keepalived_dual_vip/
├── tasks/
│   └── main.yml        # 调用我们的Python脚本,或分解为多个Ansible任务
├── templates/
│   └── keepalived.conf.j2  # 使用Jinja2模板生成动态配置
├── files/
│   └── check_service.sh    # 健康检查脚本
└── vars/
    └── main.yml        # 定义主备节点参数、VIP列表等变量

3. 部署后的验证与监控 脚本执行成功并不代表万事大吉。必须建立验证机制。

  • 基础连通性测试:在部署完成后,脚本可以自动执行简单的测试,例如从集群外部ping两个VIP,或者使用curl测试VIP背后的服务端口是否开放。
  • 故障转移演练:这是验证高可用是否生效的“金标准”。脚本可以提供一个“测试模式”,用于安全地模拟主节点故障(如systemctl stop keepalived),然后自动验证VIP是否按预期漂移到备节点,并在测试后恢复原状。
  • 集成监控系统:将Keepalived的状态(进程、VIP绑定情况)上报到Prometheus、Zabbix等监控系统。可以编写一个简单的指标暴露脚本,或者直接利用Keepalived的SNMP或JSON输出功能。

4. 安全加固考量

  • 认证密码:示例中的auth_pass是弱密码。生产环境中必须使用强随机密码,并且可以通过脚本从安全的密码管理服务中动态获取。
  • 配置文件的权限keepalived.conf文件包含密码,应确保其权限为600(仅root可读可写)。
  • 网络隔离:确保VRRP通信所在的网络是受信任的,可以通过防火墙限制只允许集群节点间的VRRP协议通信,防止恶意欺骗。

把一次性的部署脚本,进化成一个可重复、可验证、可监控的自动化部署体系,这才是DevOps自动化的精髓所在。这个Python脚本只是一个起点,它为你打开了用代码定义和运维基础设施的大门。当你下次需要扩展集群、升级版本或迁移到新机房时,只需修改几个参数再次运行脚本,一切都会在可控、可预见的过程中完成。这种效率与确定性的提升,正是自动化带给运维工程师的最大礼物。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐