跳转至

27 - 自动化与脚本编写(Shell + Ansible)

前提条件

  • 已完成云 CLI 工具安装与基本配置
  • 以 root 身份登录控制节点,或 ops 用户执行 sudo -i 切换至 root 环境
  • Ansible 控制节点linuxdc192.168.1.100),系统运行在 Debian 12 Bookworm
  • 被管节点:至少准备 2 台 Debian 虚拟机(192.168.1.101192.168.1.102),已完成 SSH 密钥免密配置
  • Git 操作部分以 ops 普通用户执行,不使用 root

全局参数

主机名 linuxdc,IP 192.168.1.100/24,SSH 端口 2222,运维用户 ops

Shell 脚本与 Ansible 定位

维度 Shell 脚本 Ansible
适用范围 单机、快速任务、系统级操作 批量多机、配置管理、可重复部署
幂等性 需手动实现 模块原生支持幂等
可读性 中等 高(YAML 声明式)
依赖条件 仅 bash 控制节点需 Python;被管节点需 Python 3
错误处理 手动 set -euo pipefail 内置失败检测与回滚
推荐场景 本机初始化、定时任务、简单脚本 多机批量操作、标准化部署

详细步骤

1. Shell 脚本生产级写法

推荐脚本模板(带错误处理、日志、参数检查):

vim /opt/scripts/sys-mgmt.sh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

LOGFILE="/var/log/sys-mgmt.log"

log()   { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOGFILE}"; }
error() { log "ERROR: $*"; exit 1; }

usage() {
    cat << EOF
Usage: $0 <command>

Commands:
  update   更新系统软件包
  backup   备份 /data 到 /backup
  status   检查关键服务状态
EOF
    exit 1
}

[[ $# -eq 0 ]] && usage

case "$1" in
    update)
        log "开始系统更新..."
        apt update && apt full-upgrade -y && apt autoremove -y
        log "系统更新完成"
        ;;
    backup)
        log "开始备份 /data..."
        DEST="/backup/data-$(date +%Y%m%d-%H%M%S)"
        mkdir -p "${DEST}"
        rsync -aq --delete /data/ "${DEST}/"
        log "备份完成:${DEST}"
        ;;
    status)
        for svc in ssh nginx mariadb fail2ban; do
            if systemctl is-active --quiet "${svc}"; then
                log "${svc}: running"
            else
                log "${svc}: NOT running"
            fi
        done
        ;;
    *)
        usage
        ;;
esac
chmod +x /opt/scripts/sys-mgmt.sh

# 测试各子命令
/opt/scripts/sys-mgmt.sh status
/opt/scripts/sys-mgmt.sh backup

set -euo pipefail 说明

选项 作用
-e 任意命令返回非零退出码时立即终止脚本
-u 引用未定义变量时报错退出(防止静默使用空值)
-o pipefail 管道中任意命令失败则整个管道返回失败
IFS=$'\n\t' 防止带空格的文件名在 for 循环中被错误分割

2. 安装 Ansible

apt update
apt install -y ansible
ansible --version

检查点

ansible --version 显示版本号(Debian 12 仓库提供 Ansible Core 2.14+),python version 行显示 Python 3.x。

3. 创建项目目录结构

ops 用户身份操作(Git 管理的内容不以 root 执行):

su - ops
mkdir -p ~/ansible/{inventory,playbooks,roles,group_vars,host_vars}
cd ~/ansible

4. 配置 Inventory

vim ~/ansible/inventory/hosts
[webservers]
192.168.1.101 ansible_user=ops ansible_port=2222 ansible_ssh_private_key_file=~/.ssh/id_ed25519

[dbservers]
192.168.1.102 ansible_user=ops ansible_port=2222 ansible_ssh_private_key_file=~/.ssh/id_ed25519

[all:vars]
ansible_python_interpreter=/usr/bin/python3

测试连通性:

ansible all -i inventory/hosts -m ping

检查点

所有节点返回 "ping": "pong",无 UNREACHABLE 或认证错误。

5. 第一个 Playbook:安装并配置 Nginx

vim ~/ansible/playbooks/install-nginx.yml
---
- name: Install and configure Nginx
  hosts: webservers
  become: true

  vars:
    site_root: /var/www/html

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present

    - name: Ensure Nginx is enabled and started
      ansible.builtin.systemd:
        name: nginx
        enabled: true
        state: started

    - name: Deploy custom index page
      ansible.builtin.copy:
        content: "<h1>Managed by Ansible  {{ inventory_hostname }}</h1>\n"
        dest: "{{ site_root }}/index.html"
        mode: "0644"
        owner: www-data
        group: www-data

    - name: Allow HTTP through ufw
      community.general.ufw:
        rule: allow
        port: "80"
        proto: tcp
      notify: Reload ufw

  handlers:
    - name: Reload ufw
      ansible.builtin.command: ufw reload
      changed_when: true

执行:

ansible-playbook -i inventory/hosts playbooks/install-nginx.yml

检查点

Playbook 执行完成,无 FAILED 任务。在被管节点执行 curl http://localhost 返回包含 Managed by Ansible 的页面。

6. Roles 结构化管理(生产推荐)

cd ~/ansible
ansible-galaxy init roles/common

生成的目录结构:

roles/common/
├── tasks/
│   └── main.yml        # 任务列表
├── handlers/
│   └── main.yml        # 处理器
├── templates/          # Jinja2 模板(.j2 文件)
├── files/              # 静态文件
├── vars/
│   └── main.yml        # 变量(优先级高)
├── defaults/
│   └── main.yml        # 默认变量(优先级低)
└── meta/
    └── main.yml        # 依赖声明

roles/common/tasks/main.yml 示例:

---
- name: Install common packages
  ansible.builtin.apt:
    name: "{{ common_packages }}"
    state: present
    update_cache: true

- name: Set timezone
  community.general.timezone:
    name: "{{ timezone }}"

- name: Ensure ops user exists
  ansible.builtin.user:
    name: ops
    groups: sudo
    append: true
    shell: /bin/bash

7. 变量管理与 Vault 加密

vim ~/ansible/group_vars/all.yml
timezone: Asia/Shanghai
common_packages:
  - vim
  - htop
  - curl
  - git
  - tree

加密敏感变量(数据库密码等):

ansible-vault create ~/ansible/group_vars/secrets.yml
# 输入 vault 密码后,在编辑器中写入:
# db_password: "SuperSecret123!"
# api_key: "xxxxx"

执行时解密:

ansible-playbook site.yml --vault-id @prompt
# 或使用密码文件(CI/CD 场景):
# ansible-playbook site.yml --vault-password-file ~/.vault_pass

实践任务

  1. 编写 sys-mgmt.sh 脚本,实现 update / backup / status 三个子命令,测试各功能正常运行
  2. 在控制节点安装 Ansible,创建 inventory 文件,对至少两台被管节点执行 ping 测试
  3. 编写并执行 install-nginx.yml playbook,在目标机器安装 Nginx 并部署自定义首页
  4. ansible-galaxy init 创建 common 角色,实现安装基础包和设置时区,编写使用该角色的 site.yml

自测问题

1. Shell 脚本中 set -euo pipefail 的作用是什么?

见上文"set -euo pipefail 说明"表格。补充:这四个选项是生产级 Shell 脚本的最低安全保障,缺少 -u 可能导致未初始化变量被当作空字符串处理(如 rm -rf "${DIR}/"DIR 为空时变成 rm -rf /);缺少 -o pipefail 可能导致管道前段命令失败而脚本不感知继续执行。

2. Ansible inventory 中 [webservers] 组和 [all:vars] 的区别是什么?

[webservers] 是主机组,将一批主机归为同一逻辑组,playbook 中用 hosts: webservers 指定只对这批主机执行任务。[all:vars] 是变量定义块,all 是 Ansible 内置的特殊组,包含 inventory 中所有主机,因此 [all:vars] 中定义的变量对所有主机生效。类似地,[webservers:vars] 只对 webservers 组内的主机生效。

3. ansible-vault 的主要用途是什么?如何在 playbook 中安全使用加密变量?

ansible-vault 使用 AES-256 对敏感数据(数据库密码、API 密钥、私钥等)进行加密存储,加密后的文件可以安全地提交到 Git 仓库(内容为密文)。使用方式:将加密文件放在 group_vars/host_vars/ 中,Ansible 会自动加载;执行时通过 --vault-id @prompt(交互输入密码)或 --vault-password-file <path>(密码文件,文件权限设为 600)解密。生产 CI/CD 中通常将 vault 密码存储在 CI 平台的 Secret 变量中,通过环境变量 ANSIBLE_VAULT_PASSWORD_FILE 传入。

4. 为什么生产环境中推荐使用 roles 而不是把所有任务写在一个 playbook 里?

单文件 playbook 随功能增加会迅速膨胀为难以维护的"巨型文件",难以复用(同样的 Nginx 安装逻辑需要复制粘贴)、难以测试(无法单独测试某个功能模块)、难以协作(多人同时修改产生冲突)。Roles 将任务、变量、模板、文件按功能拆分为独立模块:一个 nginx role 可被多个 playbook 复用;可通过 ansible-galaxy 发布和共享;每个 role 可独立测试(molecule 框架);meta/main.yml 中可声明 role 依赖关系,自动处理依赖顺序。

总结

Shell 脚本适合本机快速操作和定时任务;Ansible 提供无代理、幂等、声明式的批量自动化能力,是现代运维的核心工具。从简单脚本到结构化 roles + vault 的体系,是从手工运维走向基础设施即代码的关键一步。