驶离不可维护性漩涡-可复现的 Homelab 运维方法
前言
Homelab 是什么
性质:
- 复杂
- 不追求吞吐量
- 追求低延迟
目标:
- 快速实现新的运维目标
- 极高的完整性
- 较高的可用性
挑战:
- 服务之间产生非预期的影响
- 系统腐化
解决方案:
- 声明性
- 可复现性
在运维体系中
安全
架构
基础设施、硬件、操作系统、扁平局域网、单机应用、虚拟局域网、分布式应用
基础设施
我在同一个城市有 2 个家。每个家里都有电信宽带,具有公网 ipv4, ipv6,且 80 和 443 端口的传入连接被屏蔽。下载速率分别为 1000 / 300 mbps,上传速率则都为 40 mbps。2 个家之间的 ping 延迟为 7ms。
通过桥接后,在自己的路由器上 PPPoE 上网。每次拨号都会分配新的 ip。就算没有重新拨号,每隔几天都会重新分配新的 ip。
在阿里云购买了域名,并进行了备案操作。
硬件
云服务器
路由器 n100/n150, 内存 16/12g,硬盘 512/16G(傲腾),1G/2.5G 双网口
家用服务器
台式主机
笔记本。
个人不习惯 MacBook,多余键位、快捷键、交互逻辑、ARM 架构让我用起来非常不舒服。Linux 桌面也不是合适的选择,因为相关生态的缺失,折腾桌面需要花费大量时间。更关键的是部分外设缺失 Linux 上的驱动,玩游戏也很麻烦。所以还是使用 Windows 作为直接交互的系统。但是 Windows 上搭建开发环境特别麻烦,难以自动化,文件系统的权限设置与 Linux 不兼容,所以开发环境全部放在 Linux 上,Windows 通过浏览器使用各种开发工具,避免在 Windows 上安装。
操作系统
传统使用 OpenWRT + PVE + ArchLinux
OpenWRT 具有最好的生态,有成熟的包管理体系,安装、使用较为简单。
PVE 是一个开源的虚拟化管理系统,基于 Debian,可以进行可视化的运维操作。之所以要在服务器上安装 PVE 而不是直接安装 ArchLinux,是为了防止 ArchLinux 被搞坏的时候,需要重新去宿主机上安装。另外就是 ArchLinux 卡住的时候,会束手无策,而有 PVE 的话就可以在 NoVNC 查看 ArchLinux 的状态。可以把 PVE 理解为 idrac。同时尽可能不对 PVE 进行修改。
个人相当喜爱 ArchLinux,因为 ArchLinux 的使用更为简约。Ubuntu、Debian、CentOS 等系统,号称自己很稳定,但进行更改的时候特别麻烦。比如 Ubuntu 要安装 docker,就需要添加源、密钥,再进行安装。而 ArchLinux 只需要 pacman -S docker
就可以安装最新版本的 docker。这样的例子比比皆是,ArchLinux 通过高质量的软件源,避免了其他发行版里各种奇怪的软件安装方式。ArchLinux 的 wiki 也相当棒,包含了最佳实践的例子,符合由浅至深的认知顺序。其他发行版的文档都是进行罗列,man 文档洋洋洒洒一堆,没有重点不知所云。遇到问题在网上搜索的时候,Ubuntu、Debian、CentOS 存在大量的小白文章,把虽然 work 但不正确的方法、不 work 也不正确的方法
现在统一使用 NixOS
扁平局域网
路由器 mesh
网络拓扑
单机应用
使用 docker-compose
虚拟局域网
tailscale
landns
分布式应用
k8s
安全
我也尊重内外网隔离。但我更相信零信任安全,即使对端和自己在同一个内网,也需要进行安全的认证鉴权操作。所以我看到 有人写服务器的安全指南时,把更改 sshd 端口号放在第一条,我就血压上涨。因为这种方法虽然确实能降低黑客和脚本小子的攻击请求数,但治标不治本,不能实质的防止攻击。我的做法就是关闭密码认证,只启用密钥认证。
我的脑子只能记得很少的密码。有 300 个以上的不同网站。如果都使用相同密码,那么大网站账户的安全性会受到小网站的影响,不论是黑客攻破小网站,还是小网站管理员监守自盗。如果使用不同密码,那么就算使用一定规则简化记忆,我也记不住。所以我是用 Bitwarden 作为密码管理器,只记一个强密码,然后在密码管理器中为每个网站生成不同的强密码。
有好几十个服务,要尽可能降低攻击面。本来有很多种不同协议,很容易出问题。但其实只要暴露 tailscale vpn 的 端口即可。
在实践中,我认为基于密码登录的 SSH 是不安全的。因为第一次连接服务器时,用户几乎不可能去核实服务器公钥是否正确。此时攻击者只要进行中间人攻击,就可以在后续的 SSH 连接中持续监听用户的所有操作。https://pandaychen.github.io/2020/04/09/OPENSSH-CERT-BEST-PRACTISE/ https://blog.cloudflare.com/public-keys-are-not-enough-for-ssh-security/
我认为 Shell 脚本的使用应当非常慎重。
- 语法可读性差,if 要跟着 fi,for 要跟着 do 和 done,还有一堆 magic 语法和变量
- 内置能力差,比如难以处理 JSON
- 强外部依赖,比如使用
curl
进行网络请求,使用jq
处理 JSON。但是curl
和jq
等外部命令的安装、版本都无法保证。此外,虽然脚本文件本身看上去比较小,但是脚本强依赖于命令行解释器/bin/sh
or/bin/bash
,而 distroless 容器中都不存在此类解释器。 - 过度简写,脚本编写者常常使用短命令调用外部程序,如
curl -i -v baidu.com
。但是对于大量命令、大量参数的场景下,脚本阅读者难以得知这些命令的含义。 - 易忽略错误处理,Shell 脚本默认甚至是忽略错误的,脚本编写者如果不写
set -e
,很容易就写出带病运行的脚本,使异常真正出现时反而难以定位。此外,Shell 引以为傲的管道特性虽然一连串写起来很舒服,但是也很容易让编写者忽略某些步骤的意外错误。
相比之下,Pure Go 非常适合替代 Shell 脚本。
- 只依赖系统调用,我认为这个是大杀器。很多语言的编程产物或者依赖一个复杂的运行时(Python, Java, PHP, Node),或者依赖 glibc 等库(Rust without musl)。运行时就不用说了,依赖一大堆东西。我认为依赖 glibc 也是不可以接受的,因为 glibc 没有版本兼容性保证,大部分 Linux 发行版也没有提供为应用单独自动提供指定版本 glibc、与其他应用依赖隔离的机制,因此用户往往因为 glibc 版本不匹配而遇到各种问题。可以说,应用依赖于 glibc,就像把建筑造在了浮沙上。而不启用 CGO 的 Pure Go 就做出了最正确的做法,标准库里用 Go 把 glibc 的内存管理、网络功能全部重写了,从而使编译出来的二进制只依赖于系统调用,而 Linux 系统调用是稳定的,所以 Pure Go 程序放在哪都能跑。
- 生态强大,有一堆第三方库,而且导库方便,只要
go get
就可以了,不像 C++ 导个包要搞得七上八下。通过go.sum
能锁住依赖版本,比较放心。 - 语法简单,和 C 类似,且各种库函数的命名都比较合理。对于简单的代码片段,只要了解基本编程知识和英文就能看懂。
- 开发环境搭建方便,只用装个最新版的 Go,然后不管是超级大项目还是几年前的老项目,基本都能用
go run .
就能跑起来
只有需要执行一连串命令,不涉及 if for 时,我才会使用 shell 脚本。除此以外,我都倾向于使用 Go。