第309篇:Ansible Tower/AWX——自动化调度

关键词:Ansible Tower、AWX、Web界面、任务调度、RBAC、执行审计、Webhook、凭证管理


一、引言

Ansible Playbook在命令行下运行十分高效,但当自动化规模扩大后,多个运维团队共享同一套Playbook时,纯命令行方式暴露出明显短板:无权限管控、无执行审计、无可视化调度、无集中式日志。

Ansible Tower是Red Hat提供的企业级Ansible管理平台(商业版),AWX是其开源社区替代版,两者功能高度一致。它们为Playbook提供了Web界面、RBAC权限模型、定时调度、Webhook触发、执行仪表盘和审计日志,是Ansible从"工程师自用工具"进化为"团队级平台"的关键一步。


二、AWX 安装与架构

2.1 安装方式对比

方式 说明 适用场景
Docker Compose 本地快速部署,一键启动 开发测试、小规模生产
Kubernetes (Helm) 容器编排部署 大规模生产、HA需求
Operator (awx-operator) K8s Operator管理 云原生环境、自动化运维
Ansible Tower RPM Red Hat商业版安装 企业级生产、有技术支持需求

2.2 Docker Compose 快速安装

# 克隆AWX安装仓库
git clone https://github.com/ansible/awx.git
cd awx/installer

# 编辑inventory,配置安装参数
# 关键配置项:
# admin_password          - Web界面管理员密码
# admin_user              - Web界面管理员用户名
# postgres_password       - 数据库密码
# secret_key              - 加密密钥(必须随机生成)
# host_port               - Web访问端口(默认80)

# 生成随机密钥
python3 -c "import secrets; print(secrets.token_urlsafe(30))"

# 执行安装
ansible-playbook -i inventory install.yml

2.3 架构组件

架构组件 - 图示

组件 职责
Web UI 浏览器管理界面,配置Project/Template/Credential
API Server REST API,供外部系统集成
Task Manager 任务调度器,管理Job Queue
Task Nodes Ansible执行引擎,实际运行Playbook
PostgreSQL 存储元数据(Project、Template、Job记录等)
Redis 任务队列、缓存、分布式锁

三、核心概念与配置

3.1 核心概念映射

AWX概念 说明 对应CLI概念
Inventory 设备清单 ansible-inventory
Project 源码仓库(Git/SVN) Playbook所在目录
Credential 凭据(SSH/密码/Cloud API) --private-key / --user
Job Template Playbook执行模板 ansible-playbook 命令参数
Survey 运行时参数输入表单 --extra-vars
Workflow 多Job模板编排 顺序执行多个Playbook
Schedule 定时任务 cron
Notification 执行结果通知 邮件/Webhook回调

3.2 配置 Inventory

在AWX Web界面或通过API创建Inventory:

# 通过API导入Inventory
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/inventories/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Production-Network",
    "organization": 1,
    "kind": "tower"
  }'

# 导入主机
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/inventories/1/hosts/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "core-sw-01",
    "variables": {
      "ansible_host": "192.168.1.1",
      "ansible_network_os": "ansible.netcommon.vyos",
      "ansible_connection": "ansible.netcommon.network_cli",
      "ansible_user": "admin",
      "device_role": "core"
    }
  }'

3.3 配置 Project

# 创建Git项目源
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/projects/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Network-Automation",
    "organization": 1,
    "scm_type": "git",
    "scm_url": "https://github.com/company/network-automation.git",
    "scm_branch": "main",
    "scm_clean": true,
    "scm_track_submodules": false
  }'

# 更新项目(拉取最新代码)
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/projects/1/update/

3.4 配置 Credential

# SSH凭据
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/credentials/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Network-SSH-Creds",
    "organization": 1,
    "credential_type": 5,
    "inputs": {
      "username": "admin",
      "ssh_key_data": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
    }
  }'

# 网络凭据(Netmiko密码)
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/credentials/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Network-Device-Password",
    "organization": 1,
    "credential_type": 19,
    "inputs": {
      "username": "admin",
      "password": "H@wa123!",
      "authorize": true,
      "authorize_password": "En@ble123!"
    }
  }'

# Vault密码
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/credentials/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Ansible-Vault",
    "organization": 1,
    "credential_type": 3,
    "inputs": {
      "vault_password": "MyVaultPass2025!"
    }
  }'

四、Job Template 与执行

4.1 创建 Job Template

# 创建Job Template
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/job_templates/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Deploy-VLANs-to-Core",
    "description": "批量部署VLAN配置到核心交换机",
    "job_type": "run",
    "inventory": 1,
    "project": 1,
    "playbook": "playbooks/deploy_vlans.yaml",
    "credentials": [2, 3],
    "extra_vars": "{\"target_group\": \"core\"}",
    "verbosity": 1,
    "limit": "",
    "forks": 5
  }'

4.2 执行 Job Template

# 立即执行
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/job_templates/1/launch/
# 返回: {"id": 42}

# 查看执行状态
curl -k -u admin:password \
  https://awx-server/api/v2/jobs/42/

# 查看执行日志
curl -k -u admin:password \
  https://awx-server/api/v2/jobs/42/stdout/

4.3 Survey:运行时参数输入

# 在Job Template中配置Survey
survey_spec:
  name: "VLAN Deployment Survey"
  description: "选择要部署的VLAN和目标设备组"
  spec:
    - question_name: "Target Group"
      question_description: "选择目标设备分组"
      required: true
      type: "multiplechoice"
      variable: "target_group"
      choices: |
        core
        aggregation
        access
        all
      default: "core"

    - question_name: "VLAN IDs"
      question_description: "要创建的VLAN ID(逗号分隔)"
      required: true
      type: "text"
      variable: "vlan_ids"
      default: "10,20,30"

    - question_name: "Dry Run Mode"
      question_description: "是否仅预览不实际下发"
      required: false
      type: "boolean"
      variable: "dry_run"
      default: false
# Playbook中使用Survey变量
---
- name: 基于Survey参数部署VLAN
  hosts: "{{ target_group }}"
  gather_facts: no

  vars:
    vlan_list: "{{ vlan_ids | split(',') }}"

  tasks:
    - name: 部署VLAN配置
      ansible.netcommon.config:
        lines:
          - "vlan {{ item }}"
          - "description AUTO_DEPLOYED_{{ item }}"
      loop: "{{ vlan_list }}"
      when: dry_run | bool | default(false) | not

    - name: 预览模式(Dry Run)
      ansible.builtin.debug:
        msg: "DRY RUN: 将在 {{ inventory_hostname }} 上创建 VLAN {{ item }}"
      loop: "{{ vlan_list }}"
      when: dry_run | bool | default(false)

五、调度与通知

5.1 定时任务(Schedule)

# 创建定时任务:每天凌晨2点执行配置备份
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/job_templates/1/schedules/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Daily-Backup-at-02:00",
    "rrule": "DTSTART:20250101T020000Z RRULE:FREQ=DAILY",
    "description": "每天凌晨2点执行全网配置备份"
  }'

# 创建周期性巡检:每6小时执行
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/job_templates/2/schedules/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Health-Check-Every-6H",
    "rrule": "DTSTART:20250101T000000Z RRULE:FREQ=HOURLY;INTERVAL=6"
  }'

常见rrule格式: | 频率 | rrule | |------|-------| | 每天 | FREQ=DAILY | | 每周 | FREQ=WEEKLY | | 每月 | FREQ=MONTHLY | | 每小时 | FREQ=HOURLY | | 每N小时 | FREQ=HOURLY;INTERVAL=N |

5.2 通知(Notification)

# 创建邮件通知模板
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/notification_templates/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Email-On-Failure",
    "organization": 1,
    "notification_type": "email",
    "avatars": {
      "email": {
        "email_host": "smtp.company.com",
        "email_host_user": "ansible@company.com",
        "email_host_password": "smtp_password",
        "email_port": 587,
        "email_use_tls": true,
        "email_send_to": [
          "netops-team@company.com"
        ]
      }
    }
  }'

# 将通知绑定到Job Template(失败时发送)
curl -k -u admin:password \
  -X PATCH https://awx-server/api/v2/job_templates/1/ \
  -H 'Content-Type: application/json' \
  -d '{
    "notifications": {
      "error": [1]
    }
  }'

5.3 Webhook 触发

# 配置Webhook,供外部系统(如GitLab CI)触发
# AWX自动生成Webhook URL:
# POST https://awx-server/api/v2/job_templates/1/webhook_token/xxxx/launch/

# 示例:GitLab CI Pipeline触发Ansible部署
# .gitlab-ci.yml
deploy_network:
  stage: deploy
  script:
    - |
      curl -X POST \
        https://awx-server/api/v2/job_templates/1/webhook_token/abc123/launch/ \
        -H "Content-Type: application/json" \
        -d '{"extra_vars": {"deploy_version": "'$CI_COMMIT_SHA'"}}'

六、RBAC 权限管理

6.1 角色层级

角色层级 - 图示

6.2 内置角色

角色 权限范围 适用人群
Organization Admin 管理组织内所有资源 运维负责人
Project Admin 管理Git项目、同步代码 DevOps工程师
Inventory Admin 管理设备清单 运维工程师
Notification Admin 管理通知模板 运维负责人
Execute 执行Job Template 普通操作员
Update 执行+更新(非只读) 配置管理员
Read 只读访问 审计员、观察员

6.3 API 分配角色

# 查询用户
curl -k -u admin:password https://awx-server/api/v2/users/

# 为用户分配角色
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/job_templates/1/access_list/ \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "netops-zhangsan",
    "role_field": "execute_role"
  }'

# 为团队(Team)分配角色
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/inventories/1/access_list/ \
  -H 'Content-Type: application/json' \
  -d '{
    "teamname": "Network-Operations",
    "role_field": "read_role"
  }'

七、Workflow:多任务编排

7.1 编排场景

网络变更通常涉及多个步骤:备份 → 部署 → 验证 → 通知。Workflow将多个Job Template串联执行:

# 创建Workflow Job Template
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/workflow_job_templates/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Safe-Network-Change-Workflow",
    "description": "安全变更流程:备份→部署→验证→通知",
    "organization": 1,
    "inventory": 1,
    "survey_enabled": true
  }'

# 添加Workflow节点
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/workflow_job_templates/1/workflow_nodes/ \
  -H 'Content-Type: application/json' \
  -d '{
    "identifier": "step1_backup",
    "unified_job_template": 5,
    "success_nodes": ["step2_deploy"]
  }'

# 第二步:部署
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/workflow_job_templates/1/workflow_nodes/ \
  -H 'Content-Type: application/json' \
  -d '{
    "identifier": "step2_deploy",
    "unified_job_template": 6,
    "success_nodes": ["step3_verify"],
    "failure_nodes": ["step4_rollback"]
  }'

# 第三步:验证
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/workflow_job_templates/1/workflow_nodes/ \
  -H 'Content-Type: application/json' \
  -d '{
    "identifier": "step3_verify",
    "unified_job_template": 7,
    "success_nodes": ["step5_notify_success"]
  }'

# 第四步:回滚(失败时执行)
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/workflow_job_templates/1/workflow_nodes/ \
  -H 'Content-Type: application/json' \
  -d '{
    "identifier": "step4_rollback",
    "unified_job_template": 8
  }'

# 第五步:通知成功
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/workflow_job_templates/1/workflow_nodes/ \
  -H 'Content-Type: application/json' \
  -d '{
    "identifier": "step5_notify_success",
    "unified_job_template": 9
  }'

7.2 Workflow 执行流程

Workflow 执行流程 - 图示


八、故障案例:AWX并发执行导致设备锁死

8.1 故障现象

运维团队在AWX上同时触发了5个Job Template,每个Template操作10台设备(共50台)。结果30台设备的SSH连接被拒绝,AWX返回大量Connection refused错误。

8.2 根因分析

  1. AWX的forks参数默认设为10,每个Job Template同时连接10台设备
  2. 5个Job Template并行运行,瞬时并发SSH连接数达到50
  3. 部分老旧交换机SSH并发连接上限为5,超出限制的连接被拒绝
  4. AWX重试机制在短时间内连续重试,加剧了设备的SSH压力

8.3 解决方案

# 方案1:降低Job Template的forks
# 在Job Template中设置 forks=3(每台设备串行+少量并行)

# 方案2:使用Semaphore限制全局并发
# AWX Semaphore 机制(类似信号量)
# 创建 Semaphore
curl -k -u admin:password \
  -X POST https://awx-server/api/v2/semaphores/ \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "network-device-limit",
    "organization": 1,
    "max_slots": 10,      # 全局最多10个并发设备操作
    "description": "限制网络自动化任务的全局并发数"
  }'

# 在Job Template中绑定Semaphore
# 这样即使同时触发5个Job,实际并发不超过10个设备

# 方案3:Schedule错峰执行
# 将5个Job的Schedule错开时间,避免同时触发

八(续)、高级主题

8.1 AWX 项目管理最佳实践

# projects/network-automation/ 目录结构
network-automation/
├── group_vars/
│   ├── core.yaml          # 核心设备通用变量
│   ├── aggregation.yaml   # 汇聚设备通用变量
│   └── access.yaml        # 接入设备通用变量
├── inventories/
│   ├── production/        # 生产环境清单
│   │   ├── hosts.yaml
│   │   └── group_vars/
│   └── staging/           # 测试环境清单
├── roles/
│   ├── vlan_management/   # VLAN 管理角色
│   ├── interface_config/  # 接口配置角色
│   ├── bgp_config/        # BGP 配置角色
│   └── config_backup/     # 配置备份角色
├── playbooks/
│   ├── deploy_vlans.yaml
│   ├── backup_all.yaml
│   ├── verify_bgp.yaml
│   └── emergency_rollback.yaml
├── templates/
│   ├── bgp.j2
│   ├── interface.j2
│   └── vlan.j2
└── ansible.cfg            # 项目级 Ansible 配置

8.2 AWX 与 CI/CD 集成流水线

# .gitlab-ci.yml - 网络自动化 CI/CD 流水线
stages:
  - validate
  - stage-deploy
  - verify
  - production-deploy
  - post-verify

# 阶段1: 验证 Playbook 语法
validate_playbook:
  stage: validate
  script:
    - ansible-playbook --syntax-check playbooks/deploy_vlans.yaml
    - yamllint playbooks/ roles/
  only:
    - merge_requests

# 阶段2: 在测试环境通过 AWX 执行
stage_deploy:
  stage: stage-deploy
  script:
    - |
      curl -k -u "$AWX_USER:$AWX_TOKEN" \
        -X POST $AWX_URL/api/v2/job_templates/staging-vlan-deploy/launch/ \
        -H 'Content-Type: application/json' \
        -d "{\"extra_vars\": {\"deploy_version\": \"$CI_COMMIT_SHA\"}}"
  only:
    - main

# 阶段3: 自动验证部署结果
verify_stage:
  stage: verify
  script:
    - |
      # 等待 AWX 任务完成
      sleep 30
      curl -k -u "$AWX_USER:$AWX_TOKEN" \
        $AWX_URL/api/v2/job_templates/staging-vlan-deploy/last_job/ | jq '.status'
  only:
    - main

# 阶段4: 生产部署(手动触发)
production_deploy:
  stage: production-deploy
  script:
    - |
      curl -k -u "$AWX_USER:$AWX_TOKEN" \
        -X POST $AWX_URL/api/v2/job_templates/prod-vlan-deploy/launch/ \
        -H 'Content-Type: application/json' \
        -d "{\"extra_vars\": {\"deploy_version\": \"$CI_COMMIT_SHA\"}}"
  when: manual
  only:
    - main

# 阶段5: 生产环境后验证
post_verify:
  stage: post-verify
  script:
    - |
      curl -k -u "$AWX_USER:$AWX_TOKEN" \
        -X POST $AWX_URL/api/v2/job_templates/prod-verify/launch/
  only:
    - main

8.3 AWX 执行审计与合规报告

# scripts/awx_audit_report.py
import requests
from datetime import datetime, timedelta
import csv

class AWXAuditReporter:
    """AWX 执行审计报表生成器"""

    def __init__(self, awx_url, token):
        self.base_url = awx_url.rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {token}",
            "Accept": "application/json"
        })
        self.session.verify = False

    def get_jobs_since(self, days=30):
        """获取指定时间段内的所有执行记录"""
        since = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
        params = {"order_by": "-created", "created__gte": since}
        jobs = []
        url = f"{self.base_url}/api/v2/jobs/"

        while url:
            resp = self.session.get(url, params=params)
            data = resp.json()
            jobs.extend(data["results"])
            url = data.get("next")
        return jobs

    def generate_audit_csv(self, jobs, output_file="audit_report.csv"):
        """生成审计 CSV 报表"""
        with open(output_file, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow([
                "执行时间", "Job Template", "Playbook", "状态",
                "执行人", "变更设备数", "成功数", "失败数", "跳过数", "耗时(秒)"
            ])
            for job in jobs:
                writer.writerow([
                    job.get("created", ""),
                    job.get("job_template_name", ""),
                    job.get("playbook", ""),
                    job.get("status", ""),
                    job.get("uniform_execution_environment", {}).get("name", ""),
                    job.get("summary_fields", {}).get("hosts", {}).get("total", 0),
                    job.get("summary_fields", {}).get("hosts", {}).get("ok", 0),
                    job.get("summary_fields", {}).get("hosts", {}).get("failures", 0),
                    job.get("summary_fields", {}).get("hosts", {}).get("skipped", 0),
                    round(job.get("ended", "") - job.get("started", ""), 2) if job.get("ended") and job.get("started") else ""
                ])
        return output_file

    def generate_compliance_summary(self, jobs):
        """生成合规摘要"""
        total = len(jobs)
        success = sum(1 for j in jobs if j.get("status") == "successful")
        failed = sum(1 for j in jobs if j.get("status") == "failed")
        cancelled = sum(1 for j in jobs if j.get("status") == "canceled")

        # 按 Job Template 统计
        template_stats = {}
        for job in jobs:
            tpl = job.get("job_template_name", "unknown")
            if tpl not in template_stats:
                template_stats[tpl] = {"total": 0, "success": 0, "failed": 0}
            template_stats[tpl]["total"] += 1
            if job.get("status") == "successful":
                template_stats[tpl]["success"] += 1
            elif job.get("status") == "failed":
                template_stats[tpl]["failed"] += 1

        return {
            "total_jobs": total,
            "success_rate": f"{success/total*100:.1f}%" if total else "N/A",
            "success": success,
            "failed": failed,
            "cancelled": cancelled,
            "template_stats": template_stats
        }

# 使用示例
reporter = AWXAuditReporter(
    "https://awx.company.com",
    "your-awx-token"
)

jobs = reporter.get_jobs_since(days=30)
reporter.generate_audit_csv(jobs)
summary = reporter.generate_compliance_summary(jobs)
print(f"近30天执行: {summary['total_jobs']} 次, "
      f"成功率: {summary['success_rate']}")

8.4 AWX 高可用部署架构

AWX 高可用部署架构 - 图示

组件 HA 方案 备注
AWX Web K8s Deployment (多副本) 无状态,水平扩展
AWX Task K8s Deployment (多副本) 按需水平扩展
PostgreSQL Patroni + Etcd 自动故障切换
Redis Sentinel 模式 主从复制 + 自动故障转移
负载均衡 HAProxy + VIP 健康检查 + 会话保持

九、总结

知识点 要点
AWX vs Tower AWX开源免费,Tower商业支持,核心功能一致
核心概念 Project/Inventory/Credential/Job Template/Schedule
Survey 运行时参数表单,使Job Template可配置化
Schedule rrule格式定义定时执行策略
Notification 失败时邮件/Webhook告警
RBAC 基于组织的细粒度角色权限管控
Workflow 多Job编排,实现备份→部署→验证→回滚流程
Semaphore 控制全局并发数,防止设备过载

思考题

  1. AWX的Workflow和Ansible的import_playbook有什么区别?什么场景下应该优先使用Workflow?
  2. 如果需要在AWX中实现"灰度发布"(先执行10%设备、等待确认后再执行剩余90%),如何利用Workflow或Survey实现?
  3. 对比Ansible CLI与AWX平台:在权限管理、执行审计、任务调度三个维度上,AWX分别解决了什么CLI无法解决的问题?

下篇预告

第310篇:YANG 数据模型基础。从CLI走向模型驱动编程,学习如何用结构化数据模型描述网络配置与状态,为NETCONF/RESTCONF奠定理论基础。