学习进度
Lv.1
0/4
Lv.2
0/4
Lv.3
0/4
Lv.4
0/3
Lv.5
0/3

Shell & Ansible 从零实战练习

18个脚本,从打印Hello World到自动化部署,覆盖提纲全部知识点

Lv.1 基础入门 Lv.2 流程控制 Lv.3 文本处理 Lv.4 Ansible基础 Lv.5 综合实战
LV.1 基础入门

变量 · 输入输出 · 基本结构

目标:能看懂脚本、能自己写变量和条件判断
01
Hello World & 变量基础
第一个脚本:打印文字、定义变量、获取用户输入
变量赋值 echo read $0 $1 $#
hello.sh
#!/bin/bash
# 第一个Shell脚本:变量和输入输出

# 1. 定义变量(等号两边绝对不能有空格!)
name="张三"
age=20

# 2. 输出变量(用$引用)
echo "姓名: $name"
echo "年龄: $age"

# 3. 读取用户输入
read -p "请输入你的城市: " city
echo "你在 $city"

# 4. 特殊变量
echo "脚本名: $0"
echo "第一个参数: $1"
echo "参数个数: $#"

# 运行方式:
# chmod +x hello.sh
# ./hello.sh 参数1
📖 知识点说明
name="张三"变量赋值,等号两边不能有空格,有空格就报错
$name引用变量加$符号,双引号里的$会被解析
read -p "提示" var-p显示提示文字,然后等用户输入,存入变量var
$0 $1 $#特殊变量:脚本名、第1个参数、参数总个数
chmod +x给脚本添加执行权限,只需做一次
🎯 练习挑战

修改脚本:让用户输入姓名和年龄,然后打印 "你好,XXX,你今年XX岁"

02
条件判断:if 语句
判断文件是否存在、数字大小比较、字符串比较
if/fi -f -d -e -eq -lt -gt $?
check.sh
#!/bin/bash
# if 条件判断练习

# 1. 判断文件是否存在
file="/etc/passwd"
if [ -f "$file" ]; then
    echo "$file 存在"
else
    echo "$file 不存在"
fi

# 2. 数字比较
score=75
if [ $score -ge 90 ]; then
    echo "优秀"
elif [ $score -ge 60 ]; then
    echo "及格"
else
    echo "不及格"
fi

# 3. 判断目录是否存在,不存在就创建
dir="/tmp/mytest"
if [ ! -d "$dir" ]; then
    mkdir -p "$dir"
    echo "目录 $dir 创建成功"
fi

# 4. 检查上一条命令是否成功
ls /tmp > /dev/null 2&1
if [ $? -eq 0 ]; then
    echo "命令执行成功"
fi
📖 知识点说明
[ -f "$file" ]-f判断普通文件;[ ]两边必须有空格,这是语法!
! -d! 表示取反,"不是目录"
-ge -eq数字比较:-ge大于等于,-eq等于,-lt小于,-gt大于
2>&1把错误输出合并到标准输出,>/dev/null丢弃所有输出
$?上一条命令退出码,0=成功,非0=失败
🎯 练习挑战

让用户输入一个数字,判断是正数、负数还是零,并输出结果

03
重定向 & 管道实战
把命令结果写入文件、追加、管道链式处理
> >> 管道 | 2>&1 wc grep
redirect.sh
#!/bin/bash
# 重定向和管道练习

logfile="/tmp/mylog.txt"

# 1. > 覆盖写入
echo "=== 系统信息 ===" > $logfile

# 2. >> 追加写入
date >> $logfile
hostname >> $logfile
echo "写入完成,查看文件:"
cat $logfile

# 3. 管道:统计/etc/passwd有多少用户
user_count=$(cat /etc/passwd | wc -l)
echo "系统用户数: $user_count"

# 4. 管道:找出包含root的行
cat /etc/passwd | grep "root"

# 5. 2>&1 把错误也写入日志
ls /不存在的目录 >> $logfile 2&1
echo "(错误信息也被写入了日志)"

# 6. 命令替换 $() 把命令输出变成变量
today=$(date +%Y-%m-%d)
echo "今天是: $today"
📖 知识点说明
> 和 >>>覆盖(原内容没了);>>追加(保留原内容)
cat file | wc -l管道:cat输出文件内容,交给wc -l统计行数
$(命令)命令替换,把括号里命令的输出赋值给变量
2>&1stderr(错误)合并到stdout(正常输出),一起被重定向
🎯 练习挑战

写一个脚本,把当前目录的文件列表保存到 filelist.txt,并统计有多少个文件打印出来

04
算术运算 & 字符串操作
四则运算、字符串长度、截取、替换
$(()) ${#str} ${str:n:m} tr
string_math.sh
#!/bin/bash
# 算术运算和字符串操作

# 1. 算术运算(必须用$(()),不能直接写3+5)
a=10
b=3
echo "加法: $(($a + $b))"
echo "减法: $(($a - $b))"
echo "乘法: $(($a * $b))"
echo "除法: $(($a / $b))"   # 整数除法
echo "取余: $(($a % $b))"

# 2. 字符串长度
str="Hello World"
echo "字符串: $str"
echo "长度: ${#str}"

# 3. 字符串截取 ${str:起始位置:长度}
echo "截取前5个: ${str:0:5}"    # Hello
echo "从第6位截: ${str:6}"      # World

# 4. 字符串替换
echo "替换一个: ${str/l/L}"    # 替换第一个l
echo "替换全部: ${str//l/L}"   # 替换所有l

# 5. tr命令:大小写转换
echo "$str" | tr a-z A-Z       # 转大写
echo "$str" | tr A-Z a-z       # 转小写

# 6. 默认值:变量为空时用默认值
echo "${未定义变量:-这是默认值}"
📖 知识点说明
$((a + b))Shell只有整数运算,双括号是算术求值
${#str}井号+变量名=字符串长度
${str:0:5}从第0位开始,截取5个字符
${str/old/new}一个斜杠替换第一个;两个斜杠//替换全部
tr a-z A-Z字符映射替换,a→A, b→B 依此类推
🎯 练习挑战

让用户输入一个句子,输出:字符总数、全大写版本、把所有空格替换成下划线

LV.2 流程控制

循环 · 函数 · case选择

目标:能写带循环和函数的实用脚本
05
for 循环:批量操作
遍历列表、遍历文件、C风格循环、break/continue
for..in..do..done break continue seq
for_loop.sh
#!/bin/bash
# for循环各种用法

# 1. 遍历固定列表
echo "=== 水果列表 ==="
for fruit in apple banana orange; do
    echo "  水果: $fruit"
done

# 2. 数字范围(seq生成序列)
echo "=== 1到5 ==="
for i in $(seq 1 5); do
    echo "  数字: $i"
done

# 3. 批量创建用户(模拟)
echo "=== 批量操作 ==="
for user in user1 user2 user3; do
    echo "  创建用户: $user"
    # useradd $user  # 真实环境取消注释
done

# 4. break:跳出循环
echo "=== break示例 ==="
for i in $(seq 1 10); do
    if [ $i -eq 4 ]; then
        echo "  到4了,跳出!"
        break
    fi
    echo "  $i"
done

# 5. continue:跳过本次,继续下次
echo "=== 跳过偶数 ==="
for i in $(seq 1 6); do
    if [ $(( i % 2 )) -eq 0 ]; then
        continue   # 偶数跳过
    fi
    echo "  奇数: $i"
done
📖 知识点说明
for i in 列表i依次取列表中每个值,do和done之间的命令对每个值执行一次
seq 1 5生成1 2 3 4 5的序列,$(seq 1 5)把序列作为列表
break立即跳出整个for循环,执行done后面的代码
continue跳过本次循环剩下的代码,直接进入下一个i的值
i % 2取余运算,结果为0说明是偶数
🎯 练习挑战

用for循环计算 1+2+3+...+100 的结果(答案是5050),打印出来

06
while 循环:逐行读文件
while read逐行处理、倒计时、无限循环
while read line < file.txt until
while_loop.sh
#!/bin/bash
# while循环实战

# 先创建一个测试文件
cat > /tmp/servers.txt << EOF
192.168.1.10 webserver
192.168.1.20 database
192.168.1.30 cache
EOF

# 1. while read 逐行读取文件
echo "=== 读取服务器列表 ==="
while read line; do
    echo "  服务器: $line"
done < /tmp/servers.txt

# 2. 读取多列(按空格分隔)
echo "=== 分别读取IP和名称 ==="
while read ip name; do
    echo "  IP: $ip  角色: $name"
done < /tmp/servers.txt

# 3. while 数字倒计时
echo "=== 倒计时 ==="
count=3
while [ $count -gt 0 ]; do
    echo "  $count..."
    count=$(( count - 1 ))
done
echo "  开始!"
📖 知识点说明
while read line每循环一次读取一行,存入line变量,读完文件自动停止
done < file.txt把文件作为while循环的"输入来源",关键在<符号
read ip name可以一次读多个变量,按空格分隔赋值
<Here Document,多行内容直接写在脚本里
🎯 练习挑战

创建一个包含5个同学姓名的文件,用while read逐行读取,给每个人打印 "同学XXX,加油!"

07
函数:封装可复用代码
定义函数、局部变量、返回值、实用日志函数
function local return echo $()
functions.sh
#!/bin/bash
# 函数定义和使用

# 1. 基本函数(注意:定义要在调用之前)
say_hello() {
    echo "你好,世界!"
}
say_hello   # 调用函数,直接写函数名

# 2. 带参数的函数(用$1 $2接收参数)
greet() {
    local name="$1"    # local声明局部变量
    local age="$2"
    echo "你好,$name,你今年$age岁"
}
greet "李四" 22

# 3. 有返回值的函数(用echo输出,$()捕获)
add() {
    local result=$(( $1 + $2 ))
    echo $result   # 用echo"返回"值
}
sum=$(add 15 27)   # 用$()捕获函数输出
echo "15 + 27 = $sum"

# 4. 实用:带颜色的日志函数
log_info()  { echo "[INFO]  $1"; }
log_ok()    { echo "[  OK] $1"; }
log_error() { echo "[FAIL] $1" >&2; }

log_info  "开始部署..."
log_ok    "nginx 安装成功"
log_error "数据库连接失败"
📖 知识点说明
local var="值"局部变量,只在函数内有效,不影响函数外同名变量
return只能返回0-255的数字(状态码),不能返回字符串
echo + $()真正"返回"任意值的方式:函数echo输出,外面用$()接住
>&2输出到标准错误,不会混在正常输出里
🎯 练习挑战

写一个函数 is_root(),检查当前用户是否为root($UID==0),是则打印"有权限",否则打印"权限不足"

08
case 语句:菜单选择器
case写菜单、匹配多种情况、trap信号捕获
case..esac ;; trap
case_menu.sh
#!/bin/bash
# case语句和trap

# 1. trap:Ctrl+C时打印提示再退出
trap 'echo ""; echo "用户中断,退出!"; exit 0' SIGINT

# 2. case 做菜单
echo "=== 服务管理 ==="
echo "1) 启动服务"
echo "2) 停止服务"
echo "3) 查看状态"
echo "q) 退出"
read -p "请选择: " choice

case $choice in
    1)
        echo "正在启动服务..."
        # systemctl start nginx
        ;;
    2)
        echo "正在停止服务..."
        # systemctl stop nginx
        ;;
    3)
        echo "查看服务状态..."
        # systemctl status nginx
        ;;
    q|Q)          # | 表示"或",q和Q都能匹配
        echo "退出"
        exit 0
        ;;
    *)             # * 是默认情况,匹配所有其他输入
        echo "无效输入!"
        ;;
esac
📖 知识点说明
case $var in对变量进行多值匹配,比多个if elif更清晰
;;每个分支结束必须加;;(类似其他语言的break)
q|Q)竖线|表示"或",两个值都能匹配到这个分支
*)通配符,默认分支,前面所有值都不匹配时执行
trap '命令' 信号捕获信号执行命令;SIGINT=Ctrl+C;SIGKILL不能捕获
🎯 练习挑战

把菜单放进while循环,让用户可以反复操作,直到选q才退出

LV.3 文本处理

grep · sed · awk · 综合脚本

目标:能用三剑客处理日志和配置文件
09
grep & sed:搜索和替换
正则搜索、过滤、批量替换配置文件内容
grep -v -i -n sed s/old/new/g sed -i
grep_sed.sh
#!/bin/bash
# grep 和 sed 实战

# 准备测试文件
cat > /tmp/app.conf << EOF
# 应用配置文件
server_port=8080
server_host=127.0.0.1
db_host=192.168.1.100
db_port=3306
debug=true
log_level=INFO
EOF

echo "=== grep 搜索 ==="
# 搜索包含"port"的行
grep "port" /tmp/app.conf

echo "=== 反向过滤(去掉注释行) ==="
# -v 反向:不包含#的行
grep -v "^#" /tmp/app.conf

echo "=== 显示行号 ==="
grep -n "db" /tmp/app.conf

echo "=== sed 替换(不修改文件) ==="
# 把8080替换成9090
sed 's/8080/9090/' /tmp/app.conf

echo "=== sed 删除注释行 ==="
# 删除以#开头的行
sed '/^#/d' /tmp/app.conf

echo "=== sed -i 直接修改文件 ==="
# 把debug=true改为debug=false(真实修改文件)
sed -i 's/debug=true/debug=false/' /tmp/app.conf
grep "debug" /tmp/app.conf
📖 知识点说明
grep -v "^#"-v反向,^#匹配以#开头的行,合起来=去掉注释行
grep -n显示匹配行的行号,调试很有用
sed 's/old/new/'s是substitute(替换),只替换每行第一个匹配
sed 's/old/new/g'加g=global,替换每行所有匹配项
sed -iin-place,直接修改文件而不是只打印结果,慎用!
🎯 练习挑战

写脚本读取/etc/passwd,用grep找出UID为0的行,用sed把输出中的":"替换成" | " 再显示

10
awk:按列处理数据
提取列、条件过滤、统计计算、格式化输出
awk -F $1 $NF NR BEGIN END
awk_practice.sh
#!/bin/bash
# awk 实战:处理结构化数据

# 准备数据
cat > /tmp/scores.txt << EOF
张三 语文 88
李四 数学 95
王五 英语 72
张三 数学 91
李四 语文 83
EOF

echo "=== 打印第1列(姓名) ==="
awk '{print $1}' /tmp/scores.txt

echo "=== 打印行号和整行 ==="
awk '{print NR, $0}' /tmp/scores.txt

echo "=== 过滤:只显示张三的成绩 ==="
awk '$1=="张三" {print $0}' /tmp/scores.txt

echo "=== 过滤:成绩>85的 ==="
awk '$3 > 85 {print $1, "的", $2, "成绩:", $3}' /tmp/scores.txt

echo "=== 统计:计算总分 ==="
awk 'BEGIN{sum=0} {sum+=$3} END{print "总分:", sum}' /tmp/scores.txt

echo "=== 处理/etc/passwd(冒号分隔) ==="
# -F: 指定分隔符为冒号
awk -F: '{print "用户:", $1, "家目录:", $6}' /etc/passwd | head -5
📖 知识点说明
$1 $2 $3第1、2、3列;$0是整行;$NF是最后一列
NRNumber of Records,当前行号
-F:Field Separator,指定列分隔符,默认是空格
BEGIN{ }在读取任何数据前执行,常用来初始化变量
END{ }读取完所有数据后执行,常用来打印统计结果
'条件 {动作}'满足条件才执行动作,不写条件则每行都执行
🎯 练习挑战

用awk统计scores.txt中每位同学的平均分(需要用数组按姓名累加)

11
综合实战:系统巡检脚本
把前面所有Shell知识综合:检测CPU、内存、磁盘、进程
函数 awk grep if 循环 日志输出
system_check.sh
#!/bin/bash
# 系统巡检脚本(综合练习)

REPORT="/tmp/check_$(date +%Y%m%d).log"

# 日志函数
log() { echo "$1" | tee -a $REPORT; }

log "============================="
log "系统巡检报告: $(date)"
log "主机名: $(hostname)"
log "============================="

# 检查磁盘使用率
check_disk() {
    log "--- 磁盘使用率 ---"
    df -h | awk 'NR>1 {
        used=$5
        gsub(/%/,"",used)
        if(used+0 > 80)
            print "  ⚠ 警告: " $6 " 使用率 " $5
        else
            print "  ✓ 正常: " $6 " 使用率 " $5
    }' | tee -a $REPORT
}

# 检查内存
check_mem() {
    log "--- 内存使用 ---"
    free -h | awk 'NR==2 {print "  总内存:", $2, "  已用:", $3, "  空闲:", $4}' \
        | tee -a $REPORT
}

# 检查指定进程是否运行
check_process() {
    local proc="$1"
    if pgrep -x "$proc" > /dev/null 2&1; then
        log "  ✓ $proc 运行中"
    else
        log "  ✗ $proc 未运行"
    fi
}

# 执行检查
check_disk
check_mem

log "--- 关键进程 ---"
for proc in sshd cron bash; do
    check_process "$proc"
done

log "============================="
echo "报告已保存到: $REPORT"
📖 用到的知识点
tee -a同时输出到屏幕和追加到文件,-a是追加
df -h | awk管道:df输出磁盘信息,awk处理每一行
gsub(/%/,"")awk内置函数,全局替换字符(去掉%符号)
pgrep -x按进程名精确查找,找到返回0,找不到返回非0
🎯 练习挑战

在脚本中增加一个 check_load() 函数,读取系统负载(uptime命令),如果超过CPU核心数就报警

LV.4 Ansible 基础

Inventory · ad-hoc · Playbook · 模块

目标:能写基本的Playbook完成批量部署
12
Inventory & ad-hoc 命令
写inventory文件、常用ad-hoc命令、模块参数
inventory ansible -m -a ping shell copy
inventory(INI格式)
# /etc/ansible/hosts 或自定义 inventory 文件

# 直接写IP(属于默认组ungrouped)
192.168.1.5

# 定义组
[webservers]
192.168.1.10
192.168.1.11

[databases]
192.168.1.20  ansible_port=2222   # 主机变量

# 组变量(对整组生效)
[webservers:vars]
ansible_user=root
ansible_ssh_private_key_file=~/.ssh/id_rsa

# 组的组
[all_servers:children]
webservers
databases
ad-hoc 常用命令
# 格式:ansible 主机/组 -m 模块名 -a "参数"

# 测试连通性
ansible all -m ping

# 执行shell命令(支持管道)
ansible webservers -m shell -a "df -h | grep '/$'"

# 复制文件到远程
ansible all -m copy -a "src=/tmp/test.txt dest=/tmp/test.txt mode=644"

# 安装软件包
ansible webservers -m yum -a "name=nginx state=present" -b

# 启动服务
ansible webservers -m service -a "name=nginx state=started enabled=yes" -b

# -b 表示 become(sudo提权)
# -i 指定inventory文件
# -v/-vv/-vvv 增加输出详细度
📖 知识点说明
-m 模块名指定使用哪个模块
-a "参数"模块的参数,key=value格式
-bbecome,以sudo提权执行
[组名:vars]给整个组定义变量,组内所有主机都生效
13
第一个 Playbook:部署 nginx
完整Playbook结构、变量、when条件、handlers
tasks vars when notify handlers {{ }}
deploy_nginx.yml
---
# Playbook:在webservers组部署nginx
- name: 部署 Nginx Web服务器
  hosts: webservers         # 对应inventory里的组名
  become: true             # 用sudo执行
  gather_facts: true       # 收集主机信息(facts)

  vars:                     # 定义变量
    http_port: 80
    nginx_pkg: nginx

  tasks:
    # 任务1:只在RedHat系列执行
    - name: 安装 nginx(RedHat)
      yum:
        name: "{{ nginx_pkg }}"   # 引用变量
        state: present
      when: ansible_os_family == "RedHat"

    # 任务2:只在Debian系列执行
    - name: 安装 nginx(Debian)
      apt:
        name: "{{ nginx_pkg }}"
        state: present
      when: ansible_os_family == "Debian"

    # 任务3:复制配置文件,有变化就通知handler
    - name: 复制 nginx 配置
      copy:
        src: files/nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: 重启 nginx       # 文件有变化才触发

    # 任务4:启动并开机自启
    - name: 启动 nginx
      service:
        name: nginx
        state: started
        enabled: yes

  handlers:               # 被notify触发,所有task完成后执行
    - name: 重启 nginx
      service:
        name: nginx
        state: restarted
📖 知识点说明
--- 开头YAML文件标准开头,Playbook必须是YAML格式,缩进用空格不用Tab
{{ nginx_pkg }}双花括号引用变量,这是Jinja2语法
when: ansible_os_familyansible_os_family是Facts变量,自动收集的系统信息
notify: 名称任务状态changed时触发对应名称的handler
handlers在所有tasks执行完后才运行,且同名handler只运行一次
🎯 练习挑战

在Playbook中增加一个任务:用shell模块获取nginx版本号,用register保存结果,再用debug模块打印出来

14
变量 & 循环 & register
多种定义变量方式、loop循环批量、register捕获结果
vars loop register debug set_fact
vars_loop.yml
---
- name: 变量和循环演示
  hosts: all
  become: true

  vars:
    packages:              # 变量可以是列表
      - vim
      - wget
      - curl
    web_user: webadmin

  tasks:
    # loop循环:批量安装软件包
    - name: 批量安装软件包
      yum:
        name: "{{ item }}"      # item = 当前循环元素
        state: present
      loop: "{{ packages }}"    # 遍历packages列表

    # register:捕获命令结果
    - name: 获取系统时间
      shell: date +%Y-%m-%d
      register: current_date   # 结果存入变量

    # debug:打印变量内容
    - name: 打印时间
      debug:
        msg: "当前日期: {{ current_date.stdout }}"

    # set_fact:运行时创建新变量
    - name: 设置备份目录变量
      set_fact:
        backup_dir: "/backup/{{ current_date.stdout }}"

    - name: 创建备份目录
      file:
        path: "{{ backup_dir }}"
        state: directory
        mode: '0755'
📖 知识点说明
loop: "{{ list }}"遍历列表,每次迭代{{ item }}是当前元素
register: varname把模块执行结果存入变量,包含stdout/stderr/rc等字段
current_date.stdoutregister变量的.stdout字段是命令的标准输出
set_fact动态创建变量,可以基于前面任务的结果计算新值
debug模块打印调试信息,开发测试时很常用
LV.5 综合实战

Roles · block/rescue · Shell+Ansible联合

目标:能设计完整的自动化运维方案
15
Ansible Roles 结构
创建Role、目录结构、在Playbook中使用Role
ansible-galaxy init roles目录 defaults vars
创建和使用Role
# 1. 创建Role目录结构
ansible-galaxy init nginx_role

# 生成的目录结构:
nginx_role/
├── tasks/main.yml        # 主任务(必须有)
├── handlers/main.yml     # 处理器
├── vars/main.yml         # 变量(高优先级)
├── defaults/main.yml     # 默认变量(低优先级,可被覆盖)
├── files/                # 静态文件(copy模块用)
├── templates/            # Jinja2模板(template模块用)
└── meta/main.yml         # 元数据(依赖其他role)
nginx_role/tasks/main.yml
---
# roles/nginx_role/tasks/main.yml
- name: 安装 nginx
  yum:
    name: nginx
    state: present

- name: 部署配置文件
  template:                  # 用template渲染变量
    src: nginx.conf.j2        # 在templates/目录下
    dest: /etc/nginx/nginx.conf
  notify: restart nginx

- name: 启动 nginx
  service:
    name: nginx
    state: started
    enabled: yes
site.yml(使用Role的Playbook)
---
- name: 部署Web服务器
  hosts: webservers
  become: true
  roles:
    - nginx_role            # 直接写role名称
    - role: nginx_role      # 或这种写法,可以传变量
      vars:
        http_port: 8080
📖 知识点说明
defaults vs varsdefaults优先级最低(可被覆盖,适合写默认配置);vars优先级高(不轻易被覆盖)
template模块与copy的区别:template会把.j2文件里的{{ var }}替换成真实值再传输
文件查找规则copy的src自动在role的files/目录找;template的src自动在templates/目录找
16
block/rescue/always:错误处理
类比try/except/finally,优雅处理部署失败
block rescue always ignore_errors failed_when
error_handling.yml
---
- name: 错误处理演示
  hosts: all
  become: true

  tasks:
    # block/rescue/always = try/except/finally
    - block:                         # try: 正常任务
        - name: 备份配置文件
          copy:
            src: /etc/nginx/nginx.conf
            dest: /tmp/nginx.conf.bak
            remote_src: yes

        - name: 更新配置(可能失败)
          template:
            src: nginx.conf.j2
            dest: /etc/nginx/nginx.conf
          notify: restart nginx

      rescue:                         # except: block失败时执行
        - name: 恢复备份
          copy:
            src: /tmp/nginx.conf.bak
            dest: /etc/nginx/nginx.conf
            remote_src: yes
        - name: 发送告警
          debug:
            msg: "⚠ 部署失败,已回滚配置!"

      always:                         # finally: 无论成败都执行
        - name: 清理临时文件
          file:
            path: /tmp/nginx.conf.bak
            state: absent

    # ignore_errors: 失败也继续
    - name: 检查可选服务
      shell: systemctl status optional-service
      ignore_errors: true   # 找不到也没关系
📖 知识点说明
block包裹正常执行的任务,任何任务失败都会跳到rescue
rescue只在block中有任务失败时才执行,相当于catch
always不管block成功还是失败,always里的任务必定执行
remote_src: yescopy的src是远程主机上的文件,而不是控制节点的文件
ignore_errors只忽略这一个任务的失败,不影响其他任务
17
终极实战:Shell脚本触发Ansible部署
Shell脚本做前置检查 → 调用Ansible Playbook → 验证结果
Shell + Ansible 完整流程 set -e trap
deploy.sh(一键部署脚本)
#!/bin/bash
# 一键部署脚本:Shell做检查,Ansible做部署

set -e   # 任何命令失败立即退出
trap 'log_error "部署异常中断!"; exit 1' ERR

PLAYBOOK="site.yml"
INVENTORY="inventory/hosts"
LOG="/var/log/deploy_$(date +%Y%m%d_%H%M%S).log"

log_info()  { echo "[INFO] $1" | tee -a $LOG; }
log_ok()    { echo "[ OK] $1"  | tee -a $LOG; }
log_error() { echo "[ERR] $1"  | tee -a $LOG >&2; }

# 步骤1:环境检查
log_info "检查 Ansible 是否安装..."
if ! command -v ansible-playbook >/dev/null 2&1; then
    log_error "Ansible 未安装!"
    exit 1
fi
log_ok "Ansible 已安装"

# 步骤2:检查文件存在
for f in $PLAYBOOK $INVENTORY; do
    if [ ! -f "$f" ]; then
        log_error "文件不存在: $f"
        exit 1
    fi
done
log_ok "文件检查通过"

# 步骤3:语法检查
log_info "检查 Playbook 语法..."
ansible-playbook --syntax-check -i $INVENTORY $PLAYBOOK
log_ok "语法检查通过"

# 步骤4:执行部署
log_info "开始部署..."
ansible-playbook -i $INVENTORY $PLAYBOOK \
    -e "deploy_time=$(date +%Y%m%d%H%M%S)" \
    --diff 2>&1 | tee -a $LOG

# 步骤5:验证结果
if [ $? -eq 0 ]; then
    log_ok "部署成功!日志: $LOG"
else
    log_error "部署失败!查看日志: $LOG"
    exit 1
fi
📖 综合知识点
set -e任意命令非零退出时立即终止脚本,防止错误累积
trap '...' ERR捕获错误事件,发生任何错误时执行清理/告警
command -v检查命令是否存在(比which更通用)
--syntax-check只检查YAML语法,不连接任何主机执行
--diff显示文件变更的差异(类似git diff),便于审查
-e "k=v"命令行传变量,优先级最高,可覆盖playbook中的vars

✅ 完成全部18个脚本练习,提纲知识点基本掌握

建议在Linux虚拟机上实际运行每个脚本,遇到报错是最好的学习机会 💪