27 - 自动化与脚本编写(Shell + Ansible)
前提条件
- 已完成云 CLI 工具安装与基本配置
- 以 root 身份登录控制节点,或 ops 用户执行
sudo -i切换至 root 环境 - Ansible 控制节点:
linuxdc(192.168.1.100),系统运行在 Debian 12 Bookworm - 被管节点:至少准备 2 台 Debian 虚拟机(
192.168.1.101、192.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
实践任务
- 编写
sys-mgmt.sh脚本,实现update/backup/status三个子命令,测试各功能正常运行 - 在控制节点安装 Ansible,创建 inventory 文件,对至少两台被管节点执行
ping测试 - 编写并执行
install-nginx.ymlplaybook,在目标机器安装 Nginx 并部署自定义首页 - 用
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 的体系,是从手工运维走向基础设施即代码的关键一步。