十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从Shell到Ansible再到CI/CD:零基础自动化运维实战

从Shell到Ansible再到CI/CD:零基础自动化运维实战 还在手工运维阶段时最让我头疼的不是业务逻辑复杂而是同样的命令要在几十台服务器上重复执行改一个配置、发一个脚本、重启一个服务都要登录每台机器操作一遍。后来接触了 Ansible、Shell 和 CI/CD才发现自动化运维并不是“懂几个命令”这么简单它是一整套把重复劳动抽象成代码、再把代码变成流水线的工程方法。这篇文章会把三件事串起来讲用 Shell 处理服务器上的细节操作用 Ansible 把操作批量下发到所有节点再通过 CI/CD 把验证、构建、发布流程固化下来。内容不追求高深每个环节都给出可直接运行的示例适合零基础入行运维、想转自动化运维方向、或者开发人员想了解部署链路的读者。学完之后你应该能独立写一个简单的自动化部署脚本知道 Playbook 怎么写也能看懂一条发布流水线的基本结构。1. 先弄清楚三个概念Shell、Ansible、CI/CD 分别在解决什么问题很多刚入门的读者会把“自动化运维”理解成一个工具其实它是一组工具的配合。先把三个主角的定位理清楚后面实战才不会乱。1.1 Shell 是“单机手工操作”的批量化Shell 本身是 Linux 系统的命令解释器也是我们和操作系统交互的入口。你可以在终端里敲cd、ls、mv、cp系统立刻执行。而 Shell 脚本是把这些命令按照一定逻辑写到文件里通过解释器逐行执行。它可以解决的问题很具体批量重命名目录下的文件。清理过期日志。自动备份目录到另一个位置。按条件判断服务是否存活。封装复杂的命令参数避免每次手敲出错。它适合处理“一台机器内部”的重复操作。缺点是当你有 50 台机器时脚本只能帮你在一台机器上干活你仍然需要想办法把脚本分发到其他机器上。1.2 Ansible 是“多机批量操作”的控制器Ansible 是一个自动化运维工具它把 SSH 作为传输通道把我们要执行的“操作意图”用 YAML 格式写成 Playbook然后批量下发到目标主机上执行。它的核心价值是不需要在目标机器上安装 Agent代理程序依赖 Python 和 SSH。能同时操作几十台、几百台机器。Playbook 是声明式的你描述“最终状态”工具负责执行。支持幂等同一套配置重复执行不会产生重复问题例如文件已经存在再 copy 一次不会做额外动作。在真实场景里Shell 脚本更像是“某个动作的具体实现”而 Ansible 负责把这个动作调度到所有节点。1.3 CI/CD 是“从代码到发布”的流水线CI 是持续集成开发提交代码后自动触发构建、单元测试、静态检查等CD 是持续交付或持续部署通过自动化流水线将构建产物发布到测试环境、预发布环境或生产环境。CI/CD 可以把前面两样东西串起来当代码仓库发生变化时自动拉取代码。自动执行编译、测试、镜像构建。自动调用ansible-playbook或ansible命令完成服务器配置变更和应用发布。这样形成一条闭环开发提交代码 → CI 验证 → CD 发布 → 服务器状态变更。你不需要记住“每次发布要先手动作什么”流水线就是团队的发布标准。我的建议是不要只盯某一个工具而是把“Shell 写操作、Ansible 做分发、CI/CD 做调度”当作一组思维模型来学这也是本文的核心主线。2. 环境准备从零搭一套自动化运维练习环境自动化运维必须实际动手只看不练很难建立感觉。下面是一套最省事的环境准备方案。2.1 准备一台 Linux 环境推荐使用 Ubuntu 22.04 LTS 或 CentOS 7/8 这类常见发行版。如果你手边没有 Linux 服务器两个低成本方案本机安装 VMware 或 VirtualBox建一台最小化安装的虚拟机。在云服务商买一台低配云主机新用户往往有优惠。我通常会在本机准备三台虚拟机配置如下角色IP 示例系统用途控制节点192.168.100.10Ubuntu 22.04安装 Ansible在这台机器上执行命令目标节点 1192.168.100.20Ubuntu 22.04被管理的主机目标节点 2192.168.100.30CentOS 7被管理的主机如果你的机器配置不高准备一台控制节点 一台目标节点就够用了。Ansible 的很多练习两台机器就能验证。2.2 在控制节点上安装 AnsibleAnsible 控制节点需要 Python 3 环境推荐使用包管理器直接安装。Ubuntu 环境sudo apt update sudo apt install -y ansibleCentOS 环境sudo yum install -y epel-release sudo yum install -y ansible安装完成后验证版本ansible --version输出类似这样ansible 2.16.3如果你看到的版本相对较老不用紧张Ansible 的核心语法和 Playbook 结构在 2.x 版本之间保持了很久的兼容性本文演示的 playbook 在主流 2.x 版本上都可以运行。2.3 配置 SSH 免密登录Ansible 通过 SSH 连接目标机器核心配置是免密登录。在控制节点生成密钥ssh-keygen -t rsa一路回车即可。然后把公钥拷贝到目标节点ssh-copy-id root192.168.100.20 ssh-copy-id root192.168.100.30这一步会要求输入目标机器的 root 密码或对应用户密码。完成后控制节点执行ssh root192.168.100.20应该不再需要输入密码。生产环境不应直接用 root也不建议把机器密码随便外传。真实项目中建议使用专用运维账号配合sudo权限管理。本文为了演示方便以 root 为例但你应该理解这种做法的风险。2.4 创建 Ansible Inventory主机清单Ansible 通过 inventory 管理目标主机。默认文件是/etc/ansible/hosts但更推荐在项目目录中维护方便版本管理。修改/etc/ansible/hosts[web_servers] 192.168.100.20 [db_servers] 192.168.100.30 [all_nodes:children] web_servers db_servers然后在控制节点执行ansible all -m ping如果看到每台主机都返回pong说明环境已经打通了。3. Shell 脚本自动化运维的第一根手指Ansible 的很多操作最终都要落到目标机器上执行命令。Shell 脚本写得好不好直接影响自动化质量。下面挑几个高频的 Shell 知识点展开讲。3.1 最基础的 for 循环运维中出现的重复动作用for循环是最容易想到的解决方式。比如批量创建文件for i in 1 2 3 4 5; do echo file$i /tmp/file_$i.txt done更常见的用法是遍历目录下的文件。例如把/var/log/nginx/下所有.log文件压缩备份#!/bin/bash backup_dir/backup/nginx_$(date %Y%m%d) mkdir -p $backup_dir for log in /var/log/nginx/*.log; do gzip $log mv $log.gz $backup_dir/ echo 已备份: $log.gz done这里用$(date %Y%m%d)动态生成日期后缀避免备份目录冲突。3.2 批量重命名文件很多朋友在搜索“shell 重命名文件”最常见的场景是目录下有一批旧文件需要追加日期后缀或者统一定义前缀。简单写法for f in *.log; do mv $f ${f%.log}_$(date %Y%m%d).log done说明${f%.log}是变量替换去掉结尾的.log。mv $f ${f%.log}_$(date %Y%m%d).log将文件重命名。如果不想在脚本内修改只是批量查看文件信息可以配合ls或者findfind /opt/data -type f -name *.tmp -exec echo 检测到临时文件: {} \;这种命令在清理临时文件时非常好用。3.3 shift 命令处理脚本参数shift是 Shell 内置命令作用是让位置参数左移$2变成$1$3变成$2。它适合用来解析命令行参数。写一个简单的部署脚本deploy.sh#!/bin/bash usage() { echo 用法: $0 -e 环境 -v 版本 exit 1 } while [ $# -gt 0 ]; do case $1 in -e) ENV$2 shift 2 ;; -v) VERSION$2 shift 2 ;; *) echo 未知参数: $1 usage ;; esac done if [ -z $ENV ] || [ -z $VERSION ]; then usage fi echo 开始部署到 $ENV 环境版本 $VERSION调用方式bash deploy.sh -e prod -v 1.2.0shift 2的含义是处理完-e prod后把这两段都移除下一次循环直接处理-v。3.4 忽略错误继续执行Shell 脚本默认情况下如果某条命令返回非 0 退出码脚本不会自动停止除非你显式开启了set -e。这个特性可能带来两个方向的坑第一个坑脚本中途出错后面的逻辑继续跑结果出现脏数据。解决方式是set -e这样只要命令失败脚本立即退出避免错误被放大。第二个坑有的命令“失败”是我们预期内的例如grep没匹配到内容返回 1但我们不想让脚本退出。解决方式是grep 关键字 /tmp/app.log || true或者if grep 关键字 /tmp/app.log; then echo 找到关键字 else echo 没有找到但继续执行 fi所以“忽略错误继续执行”不是简单地在所有命令后加|| true而是要有选择地忽略并且建议用if捕获判断。下面是一个综合示例#!/bin/bash set -e echo 开始检查磁盘 df -h # 某个可忽略的检查 if [ -f /tmp/lock ]; then echo 存在锁文件继续执行 fi # 即使这个目录删不掉也不退出 rm -rf /tmp/cache || echo 缓存清理失败但不影响后续流程 echo 脚本执行完成这样既能让非预期错误立即暴露又允许预期内的问题被记录后继续。3.5 冒号命令:看起来无用实际很有用Shell 内置了一个空操作命令:。它不做任何事情且退出码永远是 0。在两种场景下很实用。第一种作为占位符保证语法规整if [ -f /tmp/config.yml ]; then : else echo 配置文件不存在 fi第二种初始化变量让变量有默认值: ${APP_ENV:production}这句的意思是如果APP_ENV变量未定义或为空则将其设置为production。这里用冒号是为了“不执行任何其他命令”只是完成变量替换和默认值设置。实际工作中我看到很多脚本因为临时删除代码导致if分支为空而报语法错误用:就能快速解决。3.6 一个完整的 Shell 实战脚本把上面知识串起来写一个日志目录巡检脚本check_logs.sh#!/bin/bash # 用途: 检查日志目录大小清理超过 7 天的历史日志 # 用法: bash check_logs.sh 日志目录 LOG_DIR${1:-/var/log/myapp} KEEP_DAYS7 CURRENT_DATE$(date %Y%m%d) if [ ! -d $LOG_DIR ]; then echo 目录不存在: $LOG_DIR exit 1 fi # 1. 输出当前占用空间 echo 目录大小: du -sh $LOG_DIR # 2. 查找超过指定天数的文件 echo 将清理超过 $KEEP_DAYS 天的文件: find $LOG_DIR -type f -mtime $KEEP_DAYS -name *.log | while read -r file; do echo 删除: $file rm -f $file || echo 删除失败: $file done echo 巡检完成日期: $CURRENT_DATE这个脚本把目录判断、变量默认值、循环处理、错误容忍都串起来了。实际项目中你可以在脚本里加入logger写系统日志或者把输出追加到一个固定的日志文件。4. Ansible把批量操作变成可复用的配置有了 Shell 基础后Ansible 学起来会非常有感觉因为你已经理解“目标机器上需要执行什么”Ansible 只是把这些操作声明化、批量化的工具。4.1 核心概念速览Inventory主机清单说明哪些机器归你管。Module模块是 Ansible 帮我们封装好的操作单元比如copy复制文件service管理服务yum/apt安装软件。Playbook剧本用 YAML 描述“在哪些主机上执行哪些任务”。TaskPlaybook 里的一个具体任务调用某个模块。Handler在任务改变状态时触发的动作常用于重启服务。理解这些名字之后看一个 Playbook 就不会觉得费解了。4.2 第一个 Playbook批量安装并启动 Nginx在/opt/ansible-demo目录下新建nginx.yml--- - name: 在 web_servers 组安装并启动 nginx hosts: web_servers become: yes tasks: - name: 安装 nginx apt: name: nginx state: present - name: 启动 nginx 服务 service: name: nginx state: started enabled: yes如果你的目标节点是 CentOS把apt换成yum- name: 安装 nginx yum: name: nginx state: present执行cd /opt/ansible-demo ansible-playbook -i /etc/ansible/hosts nginx.yml这就是 Ansible 的基本使用方式。写好 Playbook交给ansible-playbook命令执行即可。4.3 复制文件到所有节点并设置权限很多刚接触 Ansible 的人都会遇到“复制文件到所有节点、并授权 777 权限”这个需求。先看写法--- - name: 复制脚本到所有节点 hosts: all_nodes become: yes tasks: - name: 复制 check_disk.sh 到目标节点 copy: src: files/check_disk.sh dest: /usr/local/bin/check_disk.sh owner: root group: root mode: 0777执行后所有节点都会出现这个文件并且权限是 777。但我必须提醒你777在生产环境通常应该避免原因是它允许所有用户读取、修改和执行该文件安全风险很大。这个需求之所以常见多半是临时调试、或者目录本身没有精细的权限设计。更稳妥的做法是如果脚本需要被其他用户执行用755。如果脚本只能由特定用户操作用750并把属主设置为对应用户。- name: 复制脚本并设置合理权限 copy: src: files/check_disk.sh dest: /usr/local/bin/check_disk.sh owner: root group: root mode: 0755如果某次操作确实需要临时放权建议后续立刻调整权限以免遗留安全漏洞。修改权限可以用file模块统一管理- name: 修正目标文件权限 file: path: /tmp/app.sh mode: 07554.4 使用变量和循环提高 Playbook 复用性如果要把不同文件复制到不同目录或者批量创建用户就需要变量和循环。先看变量。在 Playbook 同级目录建group_vars/all.yml--- app_dir: /opt/myapp app_owner: appuser然后在 Playbook 中使用--- - name: 配置应用 hosts: web_servers become: yes tasks: - name: 创建应用目录 file: path: {{ app_dir }} state: directory owner: {{ app_owner }} mode: 0755再看循环。批量创建多个用户- name: 创建运维账号 user: name: {{ item }} state: present groups: sudo shell: /bin/bash loop: - devops01 - devops02 - devops03loop会遍历列表item是当前循环项。这种写法比复制三遍任务清爽很多。4.5 在 Ansible 中执行 Shell 命令虽然 Ansible 提供了很多模块但有些临时操作没有现成模块或者你想直接复用已有的 Shell 脚本可以用shell模块- name: 执行磁盘检测脚本 shell: bash /usr/local/bin/check_disk.sh register: disk_result - name: 输出检测结果 debug: msg: {{ disk_result.stdout }}register可以捕获命令输出然后用debug打印出来。这是非常常用的排查手段。再强调一点如果某个操作可以用 Ansible 原生模块完成优先用模块因为模块拥有幂等性和更清晰的状态管理shell模块适合脚本改造成本过高、或确实需要和系统命令交互的场景。5. CI/CD把自动化运维放进发布流水线Shell 负责单机操作Ansible 负责批量配置CI/CD 则负责让这一切自动触发。一个典型的自动化发布流程是这样的开发提交代码 - 自动拉取代码 - 编译/测试 - 构建产物 - 执行 Ansible Playbook - 部署到服务器 - 健康检查下面演示一个 GitLab CI 结合 Ansible 的发布流水线。5.1 GitLab CI 基础配置在项目根目录创建.gitlab-ci.ymlstages: - test - build - deploy test_job: stage: test script: - echo 运行单元测试 - make test build_job: stage: build script: - echo 构建项目 - make build artifacts: paths: - dist/ deploy_prod: stage: deploy script: - ansible-playbook -i inventory/prod deploy.yml only: - main environment: name: production说明stages定义了流水线的阶段顺序。test_job、build_job、deploy_prod是三个任务。artifacts会把构建产物传递给后续任务。only: main表示只有 main 分支触发部署。最关键的部署 STep直接调用ansible-playbook。5.2 Jenkins 流水线写法如果你所在团队用的是 Jenkins写法也类似。新建一个Jenkinsfilepipeline { agent any stages { stage(测试) { steps { sh echo 运行单元测试 sh make test } } stage(部署) { steps { sh ansible-playbook -i inventory/prod deploy.yml } } } }Jenkins 的优势是插件生态成熟缺点是环境维护相对费力GitLab CI 的优势是和仓库天然集成。两者在“调用 Ansible 做部署”这一层没有本质区别。5.3 编写一个可供流水线调用的部署 Playbook上面的流水线调用了deploy.yml这个文件需要提前准备好。一个最简的部署 Playbook 如下--- - name: 部署应用 hosts: web_servers become: yes tasks: - name: 拉取最新代码 git: repo: https://github.com/example/app.git dest: /opt/app version: {{ version }} - name: 配置环境变量 copy: src: envs/{{ env }}.env dest: /opt/app/.env mode: 0640 - name: 重启应用服务 service: name: myapp state: restarted实践中version和env通常通过 CI 的变量注入例如ansible-playbook -i inventory/prod deploy.yml -e version1.2.0 -e envprod5.4 发布失败与回滚流水线不能只考虑“顺利发布”还要考虑“万一发布失败怎么办”。常见方案部署前备份当前版本目录。发布失败时通过ansible-playbook rollback.yml回滚到上一个版本。部署完成后做健康检查例如请求健康检查接口返回非 200 则自动触发失败。这里有一个最简的备份和回滚思路。部署前先执行- name: 备份当前版本 shell: cp -r /opt/app /opt/app_backup_{{ ansible_date_time.date }}回滚时把备份目录复制回去- name: 回滚版本 shell: cp -r /opt/app_backup_{{ date }} /opt/app when: backup_exists回滚的关键是提前约定好命名规则否则流水线自动化时无法准确定位备份文件。6. 常见问题与排查清单对于刚入门的读者环境搭建和运行阶段最容易卡住。下面整理几个常见问题。6.1 Shell 脚本执行报错问题现象常见原因解决思路执行提示Permission denied脚本文件没有执行权限chmod x 脚本名或者用bash 脚本名执行提示/bin/bash^M: bad interpreterWindows 下编辑导致的回车符问题用sed -i s/\r$// 脚本名去除回车变量丢失或为空子进程无法继承父进程未导出的变量使用export导出变量set -e后脚本感觉被“无故退出”某个命令返回非 0例如grep没有匹配用 6.2 Ansible 连接失败问题现象常见原因解决思路UNREACHABLE!SSH 免密失败检查ssh userhost是否能直接登录Permission denied (publickey,password)目标主机没有控制节点的公钥重新执行ssh-copy-idERROR! Specified hosts ... not foundInventory 中的组名写错使用ansible all --list-hosts检查使用become: yes报 sudo 权限不足当前用户不在 sudo 组在目标主机给对应用户配置 sudo 权限排查时最常用的命令是ansible -i inventory_file hostname -m ping -vvv-vvv会输出详细连接日志能看到 SSH 命令到底在哪个环节失败。6.3 CI/CD 流水线失败问题现象常见原因解决思路Runner 上找不到ansible-playbook执行机没安装 Ansible在 Runner 环境安装 Ansible或在镜像中预装连接生产服务器无法免密流水线机器的 SSH 公钥未加入服务器将 Runner 的公钥加入目标服务器代码拉取失败仓库地址错误或凭据缺失检查流水线变量中的凭据配置部署后服务还访问不了服务自身启动失败或端口未开放增加健康检查步骤并结合日志定位6.4 通用排查顺序遇到自动化流程失败我建议按下面的顺序查先看错误输出最后 20 行。确认报错出现在哪个环节是代码语法、环境依赖、连接失败还是权限不足。先手动执行一次同样的命令确认在交互式环境中是否正常。再把手动执行的命令固化成脚本验证脚本本身能跑通。最后把脚本接入 CI/CD逐步排查环境差异。7. 最佳实践与工程建议自动化运维做得好不好不是看脚本多炫而是看它是否安全、稳定、易维护。下面是我在项目中沉淀下来的一些建议。7.1 权限和安全隐患不要在 Playbook 中明文写入密码或密钥优先使用 Ansible Vault 或 CI 变量。文件权限遵循最小原则777应该被当作“临时处理”而不是默认方案。生产环境使用独立运维账号配合sudo提权避免直接使用 root。对高危操作删除目录、批量重启服务加when条件避免误执行。例如删除目录加条件校验- name: 仅当确认参数为 yes 时删除旧目录 file: path: /opt/app/old_version state: absent when: force_delete yes7.2 配置管理和版本化Ansible Playbook、Shell 脚本、CI 配置都应该入库管理而不是留在某个人的服务器上。推荐的项目结构ops/ ├── inventory/ │ ├── dev │ ├── test │ └── prod ├── playbooks/ │ ├── deploy.yml │ ├── install_nginx.yml │ └── rollback.yml ├── roles/ │ └── app/ │ ├── tasks/ │ ├── templates/ │ └── files/ ├── group_vars/ │ ├── all.yml │ └── prod.yml └── scripts/ └── check_logs.sh这样的结构可以让新同学快速理解项目布局也方便做 Code Review。7.3 日志和可观测性脚本里要输出关键步骤echo $(date %Y-%m-%d %H:%M:%S) 开始执行Ansible Playbook 中可以用debug打印关键变量- name: 打印部署目标 debug: msg: 目标环境: {{ env }}版本: {{ version }}同时建议把 Ansible 执行日志持久化ansible-playbook -i inventory/prod deploy.yml -v logs/deploy_$(date %Y%m%d_%H%M%S).log 217.4 幂等性和可重放性写 Playbook 时优先使用声明式模块。比如安装软件用apt/yum而不是shell: apt install。创建目录用file而不是shell: mkdir。修改配置文件用template或copy而不是用sed命令。原因很简单模块知道自己管理的资源状态重复执行不会出错而纯命令每次执行都会重新跑一遍容易造成重复操作和不可控的副作用。7.5 生产环境变更纪律在涉及生产环境的自动化操作时务必先在测试环境完整执行一遍。发布前备份可回滚的数据或配置。执行前评估影响范围确认目标主机组。使用流水线审批功能只有指定人才能触发生产发布。保留完整的操作日志便于事后审计。8. 学习路线从入门到真正上手自动化运维的知识点不少但有一条清晰的主线你按顺序推进即可。先熟练 Linux 常用命令cd、ls、cp、mv、grep、find、chmod、vim。这些命令用不熟任何脚本都会写得很痛苦。再学 Shell 脚本语法变量、条件判断、循环、函数、参数处理。掌握 Ansible先会用ansible命令执行临时命令再学 Playbook 结构最后学习 Roles 组织复杂任务。最后学 CI/CD先用 GitLab CI 或 Jenkins 跑通一个最简单的“提交代码后自动执行 Ansible Playbook”的流程。过程中给自己设计一个目标项目比如“搭建一个能自动部署 Nginx 静态网站的小系统”。一个比较现实的目标是用两周时间掌握 Shell 和 Ansible 的常用操作再用一周时间把一条简单的 CI/CD 发布流水线跑起来。期间遇到报错不要急按照前面的排查顺序逐层定位。自动化运维的入门曲线并不陡峭难的是把每个环节都练扎实。如果你现在还在纠结“到底先学 Ansible 还是先学 Shell”我的答案是先学 Shell然后用 Ansible 去调度 Shell最后用 CI/CD 调度 Ansible。三者不是选择题而是同一套自动化体系里的不同层级。动手把环境搭起来写你的第一个脚本建第一条部署流水线。只有真正跑通一条“提交代码 → 自动测试 → 自动部署”的链路你才会突然理解自动化运维的全部价值。
返回列表