背景
我之前在记一次SSL握手异常的诊断文章末尾提到,有空要研究一下声明式的系统NixOS了。最近正好,为了探索新的个人开发环境方案,研究了一下Nix以及这个生态,有了初步了解。基本解决了我的开发环境构建问题,这里稍作总结,如果你刚好也在寻觅,盼对你有些许启发。
个人开发环境的几次迭代
我曾经尝试过多个方式,总的来说有四个迭代版本,你可以看下你当前落在哪个阶段?:)
- 最传统方式:我们的个人开发环境,从一台机器开始安装各种软件,搞各种配置,在切换机器后,我们又要再来一次。美其名曰纯手工打造,古法造就,其实就是不可自动化、工业化嘛:D
- 基于一些工具半自动化:如之前在这篇文章基于Cloud-init定制化虚拟机尝试定制部分工具或配置,但我觉得不是正解,因为cloud-init只有最开始,后续的变更不便,当然还有其它缺点不一一细数。那么这阶段你如果看过我这个初窥门径之—Ansible快速入门与实践的话,使用Ansible也能达到部分功能,它还是太“过程式”了,何况是否远程操作我们并不看重。至于terraform等,从我的经验来看,也不是特别适合解决这个问题。
- 容器化:用一个Dockerfile描述环境及配置,将其封装在一个容器中,这使用起来确实方便多了。我曾经给团队制作了一个通用镜像,有不少同学在使用,也获得一些反馈,包括我自己也觉得最大弊端可能是:对于开发环境,隔离不是硬需求,特别是容器的隔离性,导致如在内部做的一些fs操作在重建容器后(可能有新版本)都消失,除非主动保存或只在挂载卷上进行操作,这也太限制了。本来我还想有机会分享一下容器化方案,借助Dagger等重构一下构建流程以满足更好的可定制性,但是在找到下一代方案后,我放弃了在这条路上继续前进的可能性。
- 声明式开发环境:基于devbox或nix定义一套所需要的程序及配置,在需要的时候一键构造出来。裸金属,原生的味道。
这篇文章主要讲一下这个声明式开发环境如何构建。
我们的目标是什么?
想要更好的管理个人开发环境,方便复用和迁移。比如团队级共享一套,新人就相当友好的快速获得了自己的开发环境,不会反复趟进一些不必要的坑里。另外当你像我一样有多台开发机器,想获得一致的开发体验,比如我特别喜欢之前推荐的这套nvim的配置,但是依赖特别多,跨不同OS时不时遇上一些失败。所以我想要的是:
- 软件安装能力。能快速方便的安装所需软件,特别是可以指定某个版本(比如操作k8s集群就要求你的kubectl是上下2个版本差)。
- 外部程序构建安装能力。如
go install
一样可以将外部某些程序安装在系统内。 - 程序的配置功能。有一些配置你经常要重复搞也很烦,比如gitconfig,比如kubeconfig,zsh等,希望这块也能被管理好。
- 敏感信息管理。有些敏感信息比如.ssh/config等,在仓库中或许还需要加密保存,在部署时解密。
而这些信息的描述,应该是可被版本工具如git管理起来,方便持续迭代和追踪。简言之,希望这套环境是可重现(reproducible)的,且可跨多种系统。
Devbox是个好的开始,但不是终点
无意间看到devbox,甚是欣喜,它仿佛为我的目标而定。人家的标语是:
Portable, Isolated Dev Environments on any Machine。
Devbox creates isolated, reproducible development environments that run anywhere. No Docker containers or Nix lang required”。
基本使用
它的使用很简单,你可以配置一个json
便定义出一个环境以及所需要的程序了。比如我们可以这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| {
"packages": [
"go@1.20",
"docker",
"kubectl",
"istioctl@1.14",
"vault",
"earthly",
"k9s"
],
"env": {
"WELCOME": "Welcome!"
},
"shell": {
"init_hook": [
"export PS1='📦 devbox> '"
],
"scripts": {
"hello": "echo \"Hello kevinlin\""
}
},
"nixpkgs": {
"commit": "faa1bc7353d7c0cbd92cb6bf25e794ee2e0b8282"
},
"include": [
"path:plugins/vault.json",
"path:plugins/earthly.json",
"path:plugins/k9s.json"
]
}
|
上述我们在packages
中定义好了环境所需要的程序及指定版本的程序,以及它的配置使用include
来声明。比如earthly配置文件定义,我们先在earthly.json
中定义了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| {
"name": "earthly",
"version": "0.0.1",
"match": "^earthly.*$",
"readme": "设置earthly需要的环境变量",
"env": {
"EARTHLY_CONFIG": "{{ .DevboxDir }}/config.yml"
},
"create_files": {
"{{ .DevboxDir }}/config.yml": "earthly/config.yml"
},
"init_hook": [
"echo \"正在设置软件 \\033[33m earthly\\033[0m\""
]
}
|
上面通过create_files指定了在某个目录(plugins/earthly/config.yml
)放置我们的程序配置,此处配置略。
定制软件
前文提过,有些软件我们需要通过它来构建,比如我们项目内部使用的公司内的仓库,这块可以借助devbox支持的flake.nix来实现(关于flake是什么,它是Nix中重要的一个实验特性)。
我们先稍微修改一下packages
:
1
2
3
4
5
6
7
8
9
10
| "packages": [
"go@1.20",
"docker",
"kubectl",
"istioctl@1.14",
"vault",
"earthly",
"path:deps/lucian",
"k9s"
],
|
然后我们定义一个flake.nix
文件,里面指定如何构建这个程序(下面是go程序的示例):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| {
description = "lucian 用于生成相关协议测试文件的工具";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
go-src= pkgs.fetchgit {
url = "https://xxxx/tools.git";
rev = "master";
sha256 = "...";
};
in
{
packages.${system} = {
lucian = pkgs.buildGoModule rec {
pname = "lucian";
version = "0.1.0";
src = go-src;
vendorHash = "...";
subPackages = [ "cmd/lucian" ];
};
};
defaultPackage.${system} = self.packages.${system}.lucian;
};
}
|
这样在我们使用devbox shell
后,便会获得一个具备上述定制的各种软件和配置的一个交互shell了。如果你擅长在终端开发,后面都不用看了。如果你有时也会用vscode等,便会遇上一个难题,它不能兼容vscode远程开发场景(特别是补全等都会失效,因为环境变量等无法传递给vscode远程开发的终端).即便官方提供了插件也是一用就崩(可能是疏于维护?)。且从devbox所提供的能力来看,单个仓库单独进入某个shell还行,而我想,除非我切换,它应该一直就处在我声明的开发环境中?!所以说,这个非终点,但它给了一个很好的示例,也指明了其底层所依赖的Nix或是此问题的终解。
注:以上代码,可以参考我的仓库DevEnvBootstrap。
Nix比较完美解决问题
我这里不想再科普或搬运一些资料,关于什么是Nix,我摘要了几个关键点。
- Declarative, reproducible Development environments
- Declarative, reproducible package builds
- Largest packages repository(binary deployment)
- Declarative Linux system
它的多版本支持以及依赖完全独立是相当吸引人的特性。其它特点挺多,这里不一一言表。
这里我们需要借助Nix的home-manager模块来方便管理个人软件及配置,还需要借助flake更好的组织我们的各个nix文件。
使用flake.nix定义整体结构
我们只要简单定义一个flake.nix来声明一些inputs/outputs即可用于传递依赖和获得构建集(软件包)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| {
description = "kevin's HomeManager Flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# nvimdots使用
nvimdots.url = "github:ayamir/nvimdots";
# 给指定版本的kubectl使用的nixpkgs
nixpkgs-for-kubectl.url = "github:NixOS/nixpkgs/e7e54ace729a1e88177c0121d05f35352b05aed8";
# home-manager,用于管理用户配置
home-manager = {
# url = "github:nix-community/home-manager/release-23.05";
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, nixpkgs-for-kubectl, home-manager, nvimdots, ... } @inputs:
let
# 全局你可能只需要修改这一个变量
user = "kevin";
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
specialArgs = {
inherit user nvimdots nixpkgs-for-kubectl;
pkgs-for-kubectl = import nixpkgs-for-kubectl {
system = system;
config.allowUnfree = true;
};
};
in {
# 使用home-manager来管理我们的各个软件安装及配置
homeConfigurations = {
kevin = home-manager.lib.homeManagerConfiguration {
inherit pkgs ;
modules = [
./home-manager/home.nix
];
extraSpecialArgs = specialArgs;
};
};
defaultPackage.${system} = self.homeConfigurations.kevin.activationPackage;
};
}
|
以上我作了简要注释。关键干活的还是 ./home-manager/home.nix
。
使用home.nix定义软件与配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| { config, pkgs, user, lib, nvimdots, pkgs-for-kubectl, ... }:
let
# 一些测试
# userCheck = pkgs.lib.throwIfNot (user=="kevin") "User must be kevin but ${user}" user;
in
rec {
home.username = (builtins.trace "user is ${user}" user );
home.homeDirectory = "/data/home/${user}";
home.stateVersion = "23.05";
home.packages = with pkgs; [
python3
git
openssh
curl
htop
lsof
neofetch
tmux
byobu
#docker
go
#k9s
( import ./apps/k9s.nix { inherit lib stdenv fetchurl; } )
# go example
( import ./apps/go-example.nix { inherit (pkgs) lib buildGoModule fetchFromGitHub; } ) # 显式指明lib/buildGoModule/fetchFromGitHub属于pkgs包
kubecm
helm
vault
istioctl
earthly
] ++ [
# 这个版本是v1.23.4
pkgs-for-kubectl.kubectl
];
imports = [
nvimdots.nixosModules.nvimdots
./apps/zsh.nix
./apps/nvim.nix
./apps/nvimdots.nix
# 设置tmux的配置
./apps/tmux.nix
# 设置一些环境变量
./envs/vars.nix
# 设置一些配置
./configs/kubeconfig.nix
./configs/gitconfig.nix
./configs/neofetch.nix
./configs/earthly.nix
];
}
|
这里通过home.packages
定义了一些依赖的软件,并且使用import xx.nix
来引用其它nix构建一些我们所需要的自定义软件(可能不会在软件源中的)。同时也通过传递固定来源的kubectl包URL来安装指定版本的工具。
对于程序的配置,比如zsh的配置,我们可以这样定义apps/zsh.nix
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| {
programs.zsh = {
enable = true;
enableCompletion = true;
enableAutosuggestions = true;
syntaxHighlighting.enable = true;
oh-my-zsh = {
enable = true;
plugins = [ "docker-compose" "docker" ];
theme = "dst";
};
initExtra = ''
bindkey '^f' autosuggest-accept
# goproxy
go env -w GOPROXY="https://kevin:......@goproxy.woa.com|direct"
go env -w GOPRIVATE=""
go env -w GOSUMDB="...."
#earthly
export EARTHLY_SECRETS="GOPROXY=$(go env GOPROXY),GOSUMDB=$(go env GOSUMDB)"
neofetch
'';
shellAliases = {
k = "kubectl";
km = "kubecm";
b = "byobu";
};
};
}
|
这里展示了其插件oh-my-zsh的配置,alias设定以及一些其它额外的指令在initExtra
中。
对于私有的一些配置如gitconfig我们可以在configs/gitconfig.nix
这样定义:
1
2
3
4
| {
# 设置git配置。 其指向的../files/gitconfig是你打算声明并同步到~/.gitconfig的文件。
home.file.".gitconfig".source = ../files/gitconfig;
}
|
对于私有的仓库等,上面有import ./apps/go-example.nix
,我们也看下它的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| { lib, buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "go-dnsmasq";
version = "unstable-2022-07-19"; # 添加实际的提交日期
src = fetchFromGitHub {
owner = "kevin1sMe";
repo = pname;
rev = "6505b037c29de3b4cc6e1ddfab570cefd360c395"; # 添加你想要打包的版本的commit hash
sha256 = "sha256-3b4K0kx10SJ9OAv6cEK52PBSRJC/bUuULKdufBFPapA="; # 添加正确的SHA-256哈希
};
vendorSha256 = "sha256-0DqS/scc4MD+KVs9lGC6t0hHNCaYLf56k0W1bk5cE9s="; # 添加正确的SHA-256哈希
subPackages = [ "cmd/dnsmasq" ]; # 打包的子目录路径
meta = with lib; {
description = "A golang dnsmasq application"; # 添加合适的描述
homepage = "https://github.com/kevin1sMe/go-dnsmasq";
license = licenses.mit; # 确保许可证匹配实际项目许可证
platforms = platforms.unix;
maintainers = with maintainers; [ kevinlin ]; # 将其替换为你的名字
};
}
|
有了以上的一些功能后,我们的目标基本实现了。现在如果安装了home-manager只要执行一个命令:
1
| home-manager switch --flake .#kevin
|
再等一会(首次可能要等3-5分钟,视你网速而定),一个如你所愿的环境原地诞生了。如果没有安装home-manager,你只要有nix也可以一行命令搞定:
1
| nix run nixpkgs#home-manager -- switch --flake .#kevin
|
后续你增删改各种设定,切换是秒级(几秒级)。
后记
这个方案我在Ubuntu22.04TLS测试过后,迁移到CentOS,Debain以及公司的TencentOS都不需要修改一行代码,毕竟都是Linux嘛:) 整个过程相当顺畅,一行命令回车,站起来,取杯水,喝完,一个干净清爽的开发环境就归你了。基于MacOS,理论上只要稍作修改即可同时支持,未来我也会继续打磨它。
很遗憾为了不把文章写得又臭又长,本文有不少概念我没有细讲,总结来说,在声明式的加成下,结合Nix及函数式很好的可重现构建的能力,你的环境会很稳定,只要成功过,想坏真难。
再次注:以上实践或代码呈现非最终版本,显然有优化空间,比如对于nix传参就可以通过callPackages等重构。整个过程用了个把月非常零碎时间边学边实践,只是阶段性验证了可行性,请理解,如有错误欢迎指出。
感谢阅读。
参考资料
看到这参考资料你也假装读完了,再次感谢阅读:P
-EOF