尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用Shell脚本实现轻量级基础设施即代码(IaC)实践
做了这么多年运维我一直觉得“基础设施即代码”这件事不应该只有大厂那套玩法。很多小团队、轻量项目根本不需要立刻上Terraform、Ansible这些重型工具直接用Shell脚本也能把IaC做得明明白白。这次分享的这套实践是我在一个内部模拟项目X里反复打磨过的方案用Shell脚本完成基础设施的资源定义、状态管理、一键部署和销毁整个过程能被Git追踪也能被CI/CD调用效果不比专业工具差多少。如果你的服务器数量不多基础设施以配置文件、服务、定时任务为主那这篇文章就是写给你的。我会从为什么用Shell做IaC开始讲一直讲到目录结构、幂等实现、核心脚本编写再到我踩过的那些坑和排查经验。看完之后你完全可以照着搭一套属于自己的轻量IaC管理方案。1. 思路拆解为什么用Shell写IaC而不是直接上Terraform1.1 重型IaC工具的适用边界与Shell的现实价值先讲一个真实场景。我所在的团队维护着一套内部演示环境的服务器上面跑着Nginx、几个定时任务、一些配置文件。之前也尝试过用Terraform管理云资源后来发现大部分资源都是本地的、非云端的Terraform反而不顺手。Ansible呢语法确实优雅但对一个只有三五台机器、一百多行配置的环境来说引入一套Python依赖和完整控制节点本身就带来了额外的维护负担。这时候Shell脚本的优势就出来了。它不需要安装任何运行时几乎每台Linux服务器上都自带它写起来直白部署逻辑完全可以按照自然的步骤来组织它还能天然地和cron、systemd、SSH这些基础设施协作。很多人的第一反应是“Shell脚本能做IaC吗”我的回答是如果你的基础设施范围可控、规模不大、变化频率偏低Shell脚本是完全能承担这个职责的。为了说得更清楚我把几种常见方案放在一起对比方案适合规模依赖环境学习成本状态管理Terraform云资源、跨云多环境需要安装terraform二进制较高需要学HCL自带stateAnsible配置管理、应用部署控制节点需要Python中等需要学YAML和模块无状态靠剧本幂等Shell脚本小规模内网、单一主机仅需bash和常用命令低熟悉Linux命令即可自己写标记文件表格里的差异很直观。但这里并不是说Shell要替代前两者而是在特定的“轻量IaC”场景里它能比前两者更省心。如果你的基础设施里有大量网络配置、DNS记录、云安全组这类资源那还是老老实实用Terraform。如果只是管理几台服务器上的配置文件和服务状态Shell脚本的精简特性反而能帮你把复杂度控制在极低的范围内。1.2 Shell IaC的核心设计思路资源定义、执行器、状态记录我理解的IaC并不是“用代码写命令”而是把“基础设施的期望状态”用代码表达出来并用统一的入口去执行和记录变化。因此Shell版的IaC也需要有清晰的抽象至少分成三层第一层是资源定义也就是把每一台服务器上应该有什么东西用文件描述清楚。比如“Nginx应该运行在80端口”、“每天凌晨2点执行清理脚本”、“某个配置文件的内容应该是什么”。这些定义不能散落在零散的Shell命令里而要集中放在一个可以被Git管理的目录中。第二层是执行器也就是一个统一的入口脚本比如apply.sh和destroy.sh。它会读取资源定义检查当前系统的真实状态再执行相应操作把系统调整到期望状态。执行器还要返回标准化的退出码和日志方便接入CI/CD流水线。第三层是状态记录。专业IaC工具都有state机制Shell可以自己实现一个轻量的版本用目录下的标记文件记录哪些资源已经部署过、哪些资源被修改过。这个状态下一次执行时可以用于比对和幂等判断。用生活类比来解释这就像装修房子。资源定义是设计图纸执行器是施工队状态记录是施工日志。没有设计图施工队会乱来没有施工日志下一次施工就只能全部重来。Shell IaC虽然简陋但只要这三层结构齐全它就能做到专业工具中最关键的能力——可重复、可追踪、可回滚。2. 开局设计目录结构、配置分离与幂等性基础2.1 一套可复用的目录规划在动手写脚本之前先把目录结构定下来。没有结构脚本写久了就是一团乱麻。我这套方案的目录是这样的iac/ ├── env/ │ ├── dev.env │ └── prod.env ├── resources/ │ ├── nginx.conf │ ├── cron.daily_clean.sh │ └── app.service ├── scripts/ │ ├── lib/ │ │ ├── logger.sh │ │ └── check_state.sh │ ├── apply.sh │ ├── destroy.sh │ └── status.sh └── state/ └── .gitkeep其中env/目录存放不同环境的变量配置resources/目录存放具体的资源文件模板scripts/目录放执行脚本和公共函数库state/目录用来记录状态标记。这种做法最大的好处是环境之间只通过env文件区分资源定义是同一份执行器是同一套。也就是说开发环境、生产环境跑的是完全相同的代码唯一差异来自env文件里的参数。一开始我也试过把变量直接写在脚本里后来发现一旦带了环境脚本就会变得没法维护。所以现在强制要求所有环境差异必须进env文件脚本里不允许出现硬编码的IP、端口、用户名。这样做的核心目的不是“看起来很规范”而是为了后续做CI/CD时可以用同一个脚本替换不同env文件把环境切换变成一次参数注入。2.2 通过env文件实现配置与代码分离env文件本质上就是一组KEYvalue格式的变量声明。比如dev.env内容可能是APP_ENVdev NGINX_PORT8080 DATA_DIR/opt/data/dev CRON_SCHEDULE0 2 * * *prod.env里则是另一组值。在apply.sh开头我们会通过source命令载入当前选定的env文件然后后续所有函数都使用这些变量。这样做既避免了敏感信息被写死在脚本里也使得代码评审时一眼就能看出环境差异。有一点必须注意env文件里如果包含密码、Token那就不能直接提交到Git仓库需要配合部署时从外部注入或者在CI/CD里用密钥管理服务注入。很多初级脚本把数据库密码直接写在env文件里推到了仓库这是一个必须回避的坏习惯。我通常在.gitignore里加上env/*.local.env把带敏感信息的文件排除掉仓库里只保留不含秘密的模板文件。2.3 幂等性让同一套脚本可以反复执行Shell脚本做IaC最关键的技术点就是“幂等性”。简单说同一套代码在同一套环境上执行一遍和执行十遍结果应该完全一致。如果不做幂等脚本就成了“一次性脚本”第二次跑就会报错、重复创建、或覆盖已有配置。在Shell里我常用两种方式实现幂等。第一种是“存在性检查”。部署某个资源之前先判断这个资源是否已经存在。比如要创建一个用户先看id username是否返回0如果用户已存在就跳过创建要写一份Nginx配置先对比/etc/nginx/conf.d/default.conf和resources/nginx.conf的内容是否一致一致就跳过不一致才执行覆盖。第二种是“状态标记文件”。对于无法用简单命令判断状态的操作比如一次性迁移、初始化数据库、生成密钥我们需要在state/目录里留一个标记。执行成功就写一个.app_initialized文件下次执行时先检查这个文件是否存在存在就直接跳过整个初始化步骤。这个思路和Terraform的state文件本质相同只是实现更轻。3. 实操过程从零编写一套Shell IaC管理脚本3.1 全局执行入口解析参数与载入环境日常使用时我并不想记住每个资源的管理命令只希望有一个统一的入口。所以我在scripts/里放了一个apply.sh职责是负责整个环境的部署和更新。它的开头是这样的#!/usr/bin/env bash set -euo pipefail ACTION${1:-apply} ENV_FILE${2:-env/dev.env} if [ ! -f $ENV_FILE ]; then echo env file not found: $ENV_FILE exit 1 fi # shellcheck disableSC1090 source $ENV_FILE这里set -euo pipefail几乎是现代Bash脚本的标配。-e让脚本在遇到任何错误时立即退出-u防止变量未定义-o pipefail让管道中任何一个命令失败都能被捕获。这一行能避免很多莫名其妙的后续问题。然后是载入env文件。这里我习惯用相对路径加命令行参数的方式而不是在脚本里写死source ./env/dev.env。因为脚本的调用位置会直接影响路径解析一旦从其他目录调用脚本相对路径就失效了。更稳妥的做法是让脚本自己找到自己的位置SCRIPT_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) ROOT_DIR$(dirname $SCRIPT_DIR) ENV_FILE${ROOT_DIR}/env/dev.env3.2 核心函数库logger和state封装在scripts/lib/logger.sh里我封装了一个简单的日志输出函数。为什么要单独封装因为后续需要给日志加时间戳、颜色和退出码如果不封装这些逻辑会重复出现在每个脚本片段里。log_info() { echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $* } log_error() { echo [$(date %Y-%m-%d %H:%M:%S)] [ERROR] $* 2 }check_state.sh里则封装了“状态判断”用的两个函数state_exists() { local name$1 [ -f $ROOT_DIR/state/${name}.done ] } mark_state() { local name$1 touch $ROOT_DIR/state/${name}.done } remove_state() { local name$1 rm -f $ROOT_DIR/state/${name}.done }这三个函数配合起来就能实现对大段部署逻辑的幂等控制。比如初始化数据库在apply.sh中写成if state_exists database_init; then log_info database already initialized, skip else # 执行初始化操作 mysql -h $DB_HOST -u $DB_USER -p$DB_PASS init.sql mark_state database_init fi很多第一次接触Shell IaC的人会问为什么用.done文件而不是用一个变量标记原因很简单脚本每次都是独立进程进程内变量是无法跨次执行的。而文件是持久化的能够记录“上一次执行完成到哪一步”这个信息。一个空文件大小只有几字节存储成本完全可以忽略。3.3 场景实例管理Nginx配置和定时任务下面用一个具体场景把上面这些模块串起来。假设我们要管理Nginx的站点配置。首先在resources/nginx.conf里放一份模板文件里面使用env文件里定义的变量server { listen ${NGINX_PORT}; server_name ${APP_DOMAIN}; root ${DATA_DIR}/html; index index.html; }注意Nginx并不原生支持环境变量替换所以我们需要在脚本里完成替换动作。这里我用了envsubst命令它是gettext-base包提供的工具作用是把标准输入中的$VARIABLE替换为环境变量的值deploy_nginx_config() { local target/etc/nginx/conf.d/${APP_DOMAIN}.conf envsubst ${NGINX_PORT} ${APP_DOMAIN} ${DATA_DIR} \ $ROOT_DIR/resources/nginx.conf \ /tmp/nginx.conf.$$ if diff -q $target /tmp/nginx.conf.$$ /dev/null; then log_info nginx config is up to date rm -f /tmp/nginx.conf.$$ return 0 fi cp /tmp/nginx.conf.$$ $target rm -f /tmp/nginx.conf.$$ log_info nginx config updated, reloading... nginx -s reload }这里有几个细节值得说明。第一diff判断完全必要它保证了配置没变化时不会触发Nginx reload。频繁reload虽然看起来影响小但生产环境里每个SIGHUP都会引起连接处理变化能避免就避免。第二临时文件使用了$$后缀表示当前进程ID避免多个实例同时运行时互相覆盖文件。第三nginx -s reload前应该用nginx -t检查配置语法我通常会在生产脚本里加上这个步骤语法有错时直接退出避免把自己锁在门外。定时任务也是类似逻辑。把任务的脚本放在resources/cron.daily_clean.sh然后用一个函数在/etc/cron.d/下生成对应的调度描述文件deploy_cron() { local cron_file/etc/cron.d/app_clean local content$CRON_SCHEDULE root $ROOT_DIR/scripts/lib/cron_wrapper.sh $APP_ENV echo $content $cron_file chmod 644 $cron_file }这只是一个示例实际项目中我们应该保留cron脚本的源码副本并且在使用cron的脚本里加上set -euo pipefail和锁机制防止任务重叠执行。3.4 apply、destroy、status三个入口的实现一套完整的Shell IaC至少要有三个入口apply、destroy、status。我把它们都放到了同一个入口脚本中用第一个参数来区分。这样用户只需要记住一个命令./scripts/apply.sh status ./scripts/apply.sh apply ./scripts/apply.sh destroy入口脚本的主要部分是这样的case $ACTION in apply) deploy_nginx_config deploy_cron initialize_app_if_needed log_info apply completed ;; destroy) remove_nginx_config remove_cron remove_state log_info destroy completed ;; status) show_status ;; *) echo unknown action: $ACTION exit 1 ;; esac这里的destroy和apply是对称的apply负责创建和更新destroy负责移除和清理。关于destroy用的不是删掉整个目录那么粗暴而是只移除我们之前管理的资源。可以看到我的destroy实现会调用remove_state把状态标记文件也清理掉这样再次apply时会认为环境是全新的避免出现“destroy后状态仍然存在”的矛盾。status函数则遍历所有资源定义输出当前环境与期望状态的差异。它不修改任何东西只做只读检查。我用的是把所有资源名称列出来然后逐项调用对应的检查函数。比如Nginx配置是否一致、定时任务文件是否存在、服务是否运行。每项都输出一条状态信息最后汇总退出码。这样在CI里调用status就能自动判断部署是否成功。4. 避坑指南、安全细节与问题排查实录4.1 高频故障环境变量丢失、路径错误和命令缺失在实际使用这套脚本的过程中我踩过不少坑也帮其他同事排查过不少问题。这里整理几个高频故障场景列成速查表格现象可能原因解决方式apply报“command not found: envsubst”镜像或系统缺少gettext-base安装依赖或在脚本里先检查命令是否可用Nginx配置始终没生效envsubst只替换了部分变量显式传入需要替换的变量白名单不要用envsubst无参数形式多次执行后产生重复cron任务sed或echo反复追加到同一文件先完整覆盖写文件而不是追加写入前检查目标内容destroy后重新apply状态混乱状态文件没有清理destroy时统一调用remove_state定时任务里脚本执行失败但没报错cron默认环境变量极少在cron脚本里重新source env文件并使用绝对路径排查这些问题的通用思路是先看日志再跑单步调试。我在每个函数里都写了log_info所以当脚本失败时从日志中定位到最后一步执行到哪个函数。如果是环境变量问题就临时在脚本中加一句env | grep APP来确认变量是否载入。4.2 避免并发冲突脚本锁和临时文件策略Shell脚本在单个服务器上执行时最容易被忽略的就是并发问题。尤其是定时任务和CI任务同时触发apply时两个进程可能同时修改同一个文件产生互相覆盖或半个文件的情况。解决办法是为关键操作加一个简单的互斥锁。在脚本开头执行LOCK_DIR/tmp/iac_${APP_ENV}.lock if ! mkdir $LOCK_DIR 2/dev/null; then log_error another instance is already running exit 1 fi trap rm -rf $LOCK_DIR EXIT用mkdir做锁的原因在于mkdir是一个原子操作当目录不存在时会创建成功如果目录已经存在则会返回非零退出码。这比使用flock更简单直观适合脚本内部分段锁。而trap确保脚本无论正常结束还是异常退出都会清理锁目录避免死锁。临时文件命名上也要注意。前面提到Nginx配置时用了/tmp/nginx.conf.$$这个$$是进程PID但如果两个PID不同它们不会互相影响。不过更稳妥的做法是使用mktemp命令自动生成唯一的临时文件路径并在退出时统一清理。尤其是在脚本运行时间较长时手工指定的临时文件很容易残留最后堆满/tmp目录。4.3 让脚本更健壮参数校验与错误处理Shell脚本最大的短板在于没有类型系统和完整的错误处理。通过一些习惯可以有效弥补。参数校验在入口脚本执行前检查必要的变量是否存在。比如DB_HOST、NGINX_PORT是否为空如果为空就直接报错退出而不是等到执行到某一步再报一个难以理解的错误。: ${DB_HOST:?DB_HOST is required} : ${NGINX_PORT:?NGINX_PORT is required}这种写法利用了Shell参数展开中的:?操作符如果变量未定义或为空会直接输出错误信息并终止脚本。配合set -u使用相当于给脚本加了一道类型检查。命令失败处理不要忽略任何关键命令的失败状态。比如执行nginx -t后一定要检查它的退出码if ! nginx -t; then log_error nginx config syntax error exit 1 fi还有一个很实用的习惯在脚本每段执行完毕后用log_info打一行当前执行到的地方。这样问题定位从“大海捞针”变成“按图索骥”。虽然日志会变多但运维排查时那份安心是值得的。4.4 在CI/CD中调用这套管理脚本的经验最后分享一个实践把Shell IaC接入到CI流程中。我是这样用的在部署阶段CI先拉取代码然后执行./scripts/apply.sh status # 先检查当前状态 ./scripts/apply.sh apply # 执行部署由于脚本本身是幂等的重复执行不会造成副作用这正好符合CI中“一次部署可重复运行”的原则。在发布后还可以再执行一次status如果退出码是0就认为部署成功否则触发告警。有个细节容易被忽略CI环境里的bash版本可能与服务器不一样。比如set -o pipefail在旧版bash中存在但某些精简镜像里的/bin/sh并不支持。所以我会在脚本首行明确写上#!/usr/bin/env bash并在CI脚本中先检查bash --version确保版本满足要求。到这里一套基于Shell的基础设施即代码管理实践就完整搭建起来了。从目录设计、资源定义、状态管理到apply、destroy、status三个入口再到底层幂等性、并发安全和CI/CD集成每一步都是我在真实环境里反复打磨过的。我个人最深刻的体会是做IaC工具不一定要高级关键在于能不能把“期望状态”和“执行逻辑”清晰地分离。Shell脚本恰恰能用最简单的方式做到这一点。如果你们团队正好也是一个小规模环境不妨动手搭一套相信它会比想象中更可靠。
RELATED

相关推荐

本地AI项目部署实战:环境准备、API接口与批量任务全流程解析

本地AI项目部署实战:环境准备、API接口与批量任务全流程解析

高效启动本地 AI 项目:从环境准备到接口联调的一次完整实测打开这篇文章的读者,大概率不是来看概念介绍的,而是想知道三件事:这个项目怎么跑起来、跑起来之后能干什么、遇到问题怎么排查。这次我们就围绕一个本地 AI 工具类项目的…

📅 2026/10/10 3:14:20
OpenHarmony真机Flutter应用错误处理与异常管理实战指南

OpenHarmony真机Flutter应用错误处理与异常管理实战指南

在OpenHarmony真机上跑Flutter,最磨人的不是写页面,而是排错。原因很简单:你在模拟器里跑得好好的逻辑,一旦上了真机,摄像头权限、传感器驱动、系统省电策略、通知开关,任何一环出问题,整个App就…

📅 2026/10/10 3:14:20
人类阅读与大语言模型如何应对概念中断和指称中断?

人类阅读与大语言模型如何应对概念中断和指称中断?

这次我们来看一个研究性项目:Distinct dynamics of conceptual and referential disruptions in human reading and large language model processing,翻译过来是“人类阅读与大语言模型处理中概念与指称中断的不同动态”。它不是一个可以一键部署的模型…

📅 2026/10/10 3:09:20
MORE NEWS

更多资讯

📰

可儿瑞慈童装加盟 曲靖市门店童装品牌代理 提供整店输出与运营培训支持

童装加盟市场前景与可儿瑞慈品牌业务认知近年来,随着家庭消费结构升级与育儿观念转变,童装行业持续保持稳健增长态势。家长对孩子穿着的安全性、舒适性与品质感的要求不断提升,童装消费正从满足基本需求向品质化、场景化、品牌化方向演进。与…

📰

口碑好的真皮沙发换皮翻新服务商筛选名录

北京真皮沙发换皮翻新市场观察与优质服务商筛选指南 一、真皮沙发换皮翻新成为北京家庭与商户的务实之选近年来,随着北京本地家庭消费理念趋于理性,以及酒店、民宿、写字楼等商用场所对成本控制的要求不断提升,真皮沙发换皮翻新服务迎来快速增…

📰

Java面试原理拆解:从集合到微服务的底层逻辑与高频考点

准备Java面试这件事,我很早就发现一个扎心的规律:背八股的人永远打不过懂原理的人。经常有人拿着一堆题库刷了半个月,自我感觉良好,结果面试官换个问法就懵了——不是他不会,是他只记住了“答案”,没理解“…

📰

学生公寓管理系统毕设开发全流程:从需求分析到答辩交付指南

从大二开始陆续帮人参谋过不少毕业设计,说实话,每次听到“管理系统”四个字,第一反应都是“又一个CRUD”。但真正做完、陪着别人答辩完几轮之后,我的看法变了:管理系统这类题目能不能出彩,完全不在于题目新…

📰

用NetFlow Analyzer透视网络流量:从带宽拥塞到安全监测

前阵子办公网连续出现视频会议卡顿,出口链路利用率确实打满了,但原来的监控平台只能告诉我“满了”,至于谁在填满它,完全是个黑盒。我后来把 NetFlow Analyzer 接进核心交换机的上联口,不到半小时就看清了拥堵背后的流…

📰

Agent Reach:一句话接通16个平台,AI Agent联网能力实战指南

1. 从"信息孤岛"说起:AI Agent 为什么需要联网能力如果你最近在折腾 AI Agent,大概率遇到过这样一个尴尬场景:你花了大半天时间把 Agent 的推理链路、工具调用、记忆模块都调通了,结果让它去查一条实时信息,…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬