私有化仓库的 GO 模块使用实践
本文以又拍云团队私有化模块处理的实践案例为基础,介绍如何使用私有化模块,以及 go get 工具背后的细节,其中包括如何让 go 正确的源获私有化 gitlab 上源代码以及认证等问题。文章根据又拍云资深开发工程师刘云鹏在 Open Talk 公开课直播分享进行整理,回放视频请下拉文末点击“阅读原文”。
关于 Open Talk:由又拍云发起的综合性技术沙龙,秉承又拍云“让创业更简单”的初衷,以全干货的形式为技术开发者提供包括技术、运维、产品、创业等多维度的知识分享,帮助企业成员提升专业技能,推动企业更好更快地发展。
研发背景

GO 在 1.11 版本开始引入 Module 的特性;1.13 版本引入 Module 校验和检查,加强了 Module 的安全性;现在的 1.16 版本已经默认使用 Module 模式。日前 GO 团队在博客上表明,将在 1.17 版本时删除对 GOPAHT 的支持,如果现在还没有使用 GO MODULE,赶紧抓紧时间试试 GOMDULE 吧。
GOMODULE 和 GOPATH 的主要区别在于私有化模块的使用。公有化模块使用是相同的,都是通过 go get 直接获取模块。对于私有化模块 GOPAHT 可以直接将模块代码丢在 GOPAHT 目录下,而 GO Module 不行,它有自己的代码管理方式,下面我们简单介绍下。
GO 如何获取 Module

GO 获取模块通常是使用 go get 工具获取模块,当前 go get 支持两种方式:
第一种是通过传统的 VCS 去代码托管平台上拉取代码,以 git 为主,还支持 svn、hg、等其他平台。
第二种是通过 1.12 版本开始支持的 GOPROXY 协议,go 在 GOPROXY 服务器上获取代码归档文件。
从 1.13 起 GO 还使用校验和检查—— GO SUM ,所有模块下载后都会检查其校验和。它会将下载模块的哈希值与 Google 线上数据库中的哈希值进行比对,防止模块被篡改,只有验证通过后的模块才能正常安装使用。
VCS 获取模块的方式
GO 支持很多的版本管理工具。首先需要判断使用什么版本管理工具去获取模块。判断方式大致分成三类,不依赖其他的两种静态匹配方式和一种动态匹配方式。
静态匹配方式

前缀匹配:比如 github 、谷歌的 bitbuket 和 apache、openstack 等代码托管平台,会内制在 go get 的工具链中,会去判断模块的前缀当前缀匹配上则使用对应的版本管理工具。图中左方的一例子,github.com/eamaple/pkg 模块会匹配前缀,并与 github 相匹配,同时能知道 github 使用 git 工具。
正则匹配:正则的方式是给模块加上后缀,后缀名可以是前文介绍的五种版本管理工具( git,svn ,hg ,bzr,fossil )之一的后缀。后缀的匹配是通过正则表达式实现的。上图中两个例子都是以 .git 作为后缀,通过正则表达式的匹配会得到里面的子分组,即 VCS 子分组会匹配到模块是使用 git 进行管理的。
动态匹配方式

当前缀和正则表达式都匹配不上,则会采用动态判断的方式。go get 会发送一个 HTTP 请求,URL为模块带上协议头和参数( go-get=1 )。go get 期待服务器返回模块相应信息来帮助go get 进一步的操作。GO 默认会发送 HTTPS 请求,如果服务器想用 HTTP 协议,可以通过环境变量 GOINSECURE 来处理,当 GOINSECURE 为 1 时,GO 就会使用 HTTP 协议。

Go get 预期的返回体是一个 HTML 文档,其中对 GO 有意义的是要带 name="go-import" 属性的 meta 标签。该 meta 标签会通过 content 属性告诉 GO 怎么去获取模块。
content 的内容有三部分:第一部分 root-path,指模块的名字;第二部分 vcs 代表需要使用的管理工具,比如说 git、svn。;第三部分 repo-url 指的是模块原代码存放在哪个仓库下面,该仓库就需要是协议加仓库地址的形式。

上图以 GO 的子包为例,通过 curl 模拟发送 go get 请求,golang.org/x/net 服务器返回了一个 html 文档,文档有用的是红圈框起来的部分,里面是 meta 标记,content 第一部分是 GO 模块名称 golang.org/x/net ;第二部分是 git,代表需要使用 git 来获取原码;第三部分是模块托管的地址,表示托管在模块包的地址 googlesource.com/net 上。需要注意 meta 标记只能放在 head 里面,go get 解析会从头开始,当遇到 head 的结束标签或者 body 的开始标签时停止解析。
GIT 在 GO GET 中的应用
git 支持 HTTP 协议和 SSH 协议,GO调用 git 时默认只使用 HTTP 协议,调用过程中会禁用 git 的交互过程。例如 git 使用 HTTP 协议去克隆私有仓库需要输入用户名和密码,但是 GO 调用 git 时不能通过交互输入用户名和密码会导致获取模块失败。交互是通过环境变量 GIT_TERMINAL_PROMPT 控制,如果手动将变量强行更改为 1,就可以启用交互从而手动输入用户名和密码。

那么该怎样将用户名和密码无感知的传递给 git 呢?事实上在 git 里,如果是使用 HTTP 协议都可以通过 netrc 文件来传递用户名和密码,该文件在 HOME 目录下,有两种文件格式:
-
第一种:通过服务器名和用户名密码的方式去定义服务器的用户名和密码;
-
第二种:不指定服务器,把所有的服务器都指定相同的用户名和密码。

如上图所示,第一条中配置了 gitlab.com ,用户名为 root,密码是 admin。通过 git 去克隆 gitlab 的私有仓库时,可以把用户名 root 和 admin 传递给 git,让 git 无感知获取到用户名和密码,从而就不会再要求输入密码了。第二条中通过 default 给所有的服务器都设置默认的用户名和密码,设置的用户名为 guest,密码是 123456,表示除了 gitlab.com 之外的所有服务器需要认证时,都会把将 guest 和 123456 作为用户名和密码传递给需要的程序。
go 调用 git 时也支持 SSH 协议,但默认不会使用。只有在动态获取的时候显示指定,才可以使用 SSH 协议。如果通过静态匹配方式(前缀匹配或正则匹配),能匹配上使用的模块信息,都只能使用 HTTPS 协议。

上图中的模块是 example.com/pkg,仓库地址是 gitlab.example.com/example/pkg。meta 标记的content 里包含了完整的模块信息,首先 第一部分是模块的名字,这和前面 module 的名字定义是相同的;紧接着是 git,代表使用 git 去获取代码,最后一部分是仓库地址这里就显示指定了 SSH 协议,同时还有 git 的用户名和服务器 SSH 服务端口号。

Git ssh 认证是基于密钥对实现的,如果没有密钥对,可以通过 SSH 工具套件 ssh-keygen 生成密钥。上图中列举了常用的参数 -t,该参数可以指定密钥的类型。其中 RSA 密钥可能是最常用的,而本人比较喜欢使用 ED25519,它有个明显的优点就是密钥长度非常短,公钥和私钥都只有 32 字节,安全性也可以和 RSA 密钥 3000 位左右的相媲美,能够保证安全性,密钥长度又短,因此会经常使用 ED25519 作为密钥。
当生成秘钥对之后,会在 HOME 下的 .ssh 文件夹中生成密钥队的文件码,包含私钥和公钥。".pub" j结尾的文件是公钥文件,需要把公钥文件配置在 gitlab 或 github 等代码托管平台上。右边是 gitlab 的截图,图中使用的密钥就是 ED25519 格式的密钥,可以看到长度真的非常短。
GOPROXY 获取模块
GO 支持通过 GOPROXY 协议获取 GO 模块。模块是基于 HTTP 协议的,只会使用 HTTP 的 get 请求,并且使用标准的 HTTP 状态码进行调用。当使用的公共 GOPROXY 协议,其 GOPROXY 代理服务器默认都是没有用户名和密码的。但实际上如果需要搭建私有的,是可以支持 HTTP 基础授权,方式与前面一样,通过 .netrc 文件去配置用户名和密码。另外 GOPROXY 还有两点特性:
-
第一:比起使用 VCS 方式直接去克隆,GOPROXY 获取模块的速度会更快,原因后面会详细说明。
-
第二:可以解决模块不能访问的问题,比如 Golang 域名访问不了等问题,通过第三方搭建好的代理服务器即可访问下载到这些模块。
GOPROXY 使用
GOPROXY 的配置是通过 GOPROXY 环境变量来控制,配置的是代理服务器URL。代理服务器 URL 可以配置多个,通过逗号和管道符来进行分割,管道符和逗号的区别后面会举例讲解。
通过固定的字符串 off 和 direct 可以代替 URL。off 禁止从任何来源去下载模块,把 GOPROXY 设置为 off 会禁止下载模块,只能使用本地模块,无论从 gitlab、github或其他地方的模块都不能下载。direct 代表直接从VCS上拉取,一般会作为备选方案。

图中展示了两个例子:
-
第一个是 Linux 环境变量的语法,通过 export 来设置环境变量。前面配置了proxy.golang.org,这是 google 官方的 goproxy 的服务器,逗号之后指定了备选方案 direct。在 GOPROXY 服务器返回403和410状态码时,表示找不到模块。以逗号为分隔指定备选方案时只有当服务器返回了403或410状态码时,go get 会尝试使用备选方案,这里是从版本管理平台上去下载代码。
-
第二个使用了另一种语法配置,go env -w 语法是 GO 自带的,GO1.13版本开始支持。它是可以跨平台使用的,通过这种语法,没有操作系统的差异,在 windows、Linux、max 上面,都可以通过该方式去配置 GO 相关的环境变量。示例中设置成了国内常用的 proxy 的地址:goproxy.cn。这里使用了管道符指定备选方案,管道符的意义是无论代理服务器返回了什么错误,即便不是 HTTP 的错误,如 GOproxy 服务器挂了返回500的错误,或者网络错误。都会尝试使用备选方案去下载模块。
GOPROXY 实现
GOPROXY 的实现很简单,官方定义只有五个接口。

URL 中的三个变量意义如下:
-
base 代表是 GOPROXY 服务器的 URL 地址;
-
module 表示需要需获取模块的名字;
-
version 是模块的版本。
大小写编码问题
在 HTTP 的 URL 定义上是不区分大小写的,当 module 或 version 出现大写字母时,在某些系统中可能会出现混淆的问题。为了避免此问题,需要进行大小写的编码,把大写字母转换成感叹号加小写字母的编码。
-
第一个接口是获取所有版本列表;
-
第二个接口是获取指定版本的信息;
-
第三个接口是获取指定模块,指定版本的 mod 文件;
-
第四个接口是获取模块的最新版本。这是可选的接口,不提供与实现该接口,GOPROXY 仍然可以正常工作;
-
最后一个接口是下载模块指定版本的 zip 文件。

上图是 list 接口示例, proxy.golang.org 是代理服务器的地址, golang.org/x/text 是要获取的模块名字,@v 为固定的字符串,list 是要调用的 list 接口。可以看到该接口返回了 text 包的所有版本,图中 GO 获取了所有版本后可以通过版本语义推断出模块的最新版本。

如上图所示, INFO 接口和 LATEST 接口返回的内容是一样的。 Version:固定版本字符串的版本号, Time 是 fc3339 时间格式的字符串,为可选项,代表版本的提交时间。

最后是 MOD 和 ZIP 接口。MOD 接口就是返回指定版本的 mod 文件,上图示例中获取了最新版本的 mod 文件, text 包只依赖了 tools 模块。ZIP 文件接口就是获取模块指定版本的 ZIP 文件,当它把版本的所有原文件打包成 ZIP 文件,go get 最终通过接口去下载的就是这个版本的模块。
前面提到通过 GOPROXY 去获取源代码会比通过 VCS 获取要更快,通过 zip 去下载只会下载当前版本的所有文件不会包含历史的版本信息,如果是通过 VCS 比如 git 去克隆仓库,就会获取所有的历史版本信息;因此通过 GOPROXY zip 接口获取文件的体积会更小,下载也会更快,需要注意的是 GOPROXY 定义了模块 zip 文件的大小和其所有文件的未压缩总限制为 500 MiB,go.mod 文件和 LICENSE 文件大小限制为 16 MiB。
module 验证
Go1.13 版本开始加入模块 SUM 验证机制,默认所有 go 模块下载后都会验证其 hash 是否与线上( 默认:sum.golang.org 国内:sum.golang.google.cn)记录的一致。

验证的过程可以通过环境变量 GONOSUMDB 和 GOSUMDB 来控制:首先来看 GOSUMDB 的配置,它指定了需要使用的线上数据库地址。因为默认使用的 sum.golang.org 在国内无法访问,上图中配置使用的是 google 搭建的国内镜像,还可以配置为 off,代表禁用校验,即下载模块不进行哈希值的校验,彻底抛弃这个过程。使用中我不建议这样做,可以使用 GONOSUMDB 的环境变量去配置不需要验证的模块,比如私有模块肯定是不能通过验证的。GONOSUMDE 是通过前缀匹配的方式运行的。图中配置了 gitlab.com,那么所有以 gitlab.com 开头的包都不会进行 GO 的校验和检查。

下面来梳理下常见的变量:
-
GONOPROXY,基于前缀的匹配方式运行,上图中指定了gitlab.com,也就是所有 gitlab.com 上的代码,不从 GOPROXY 服务器上去获取,全部通过传统 VCS 方式,直接去原代码服务器上拉取;
-
GONOSUMDB,可以让前缀匹配上的模块跳过安全性检查;
-
GOPRIVATE,相当于前面两个环境变量的集合,配置了 GOPRIVATE 就相当于把前面的两个环境变量一起配置了;
-
GOVCS ,这是 GO1.16 版本才添加的,其主要作用是指定哪些模块使用哪些 VCS。
又拍云的业务实践
私有包的使用
下面介绍一下如何使用私有模块。一般公司内使用较多的是私有化搭建的 gitlab 服务,gitlab 本身是支持响应 go get 的 HTTP 请求。通过 go get 获取包时,客户端会发送 HTTP 请求到 gitlab 服务器上,服务器收到请求后会返回响应中包含 meta 标记。该标记会告诉客户端,模块使用 git 通过 HTTP 协议获取原代码。gitlab 默认使用 HTTPS 协议。客户端收到 gitlab 服务器响应结果后,能正确的使用 git 去拉取模块的源代码。模块下载通过后,同样会有校验和检查的过程,可以在 GOPRIVATE 变量加上 gitlab.com,告知 go gitlabc.com 相关的模块都是私有模块跳过校验和检查。

在又拍云内部实践中,情况有些不同,又拍云内部所有使用的 HTTP 服务都需要经过 google 的二次验证。所有发往内部 gitlab 服务器的请求都会预先检查是否有 google 授权的 head,如果没有会被直接拦截掉并返回403错误。这样会导致所有的简单 HTTP 请求都不能到达 gitlab 服务器直接被拦截。go 发送的 HTTP 请求同样也会被拦截掉,将导致 go 不能正确的获取模块信息。这时虽然可以直接通 ssh 协议 clone 服务器上原代码,但由于 go get 没有这些信息,导致请求失败。因此下图中灰线表示的请求实际上是发不出来的。

那么该如何解决呢?方法是采用额外的 http 服务来处理 go get 的 HTTP 请求。额外 HTTP 服务没有验证过程,请求通过后会 go get能正确的获取到需要的 meta 信息。meta 中必须指定使用 ssh 协议,因为 gitlab http 服务有二次认证,没有认证的请求都不能通过,因此只能使用 ssh 协议。权限认证可以由 SSH 密钥对完成,进行无感知进行授权。go get 引导 http 服务不会管理授权相关问题,所有的授权处理都交给 gitlab。作为私有模块,如果没有对应的响应程序,授权认证都交给 gitlab 处理。

go get 请求指引
采用额外的服务去引导 go get 是怎么做的呢?这需要对模块包的命名进行修改,需要基于 gitlab 命名的规则修改。
gitlab.com/lyp256/pkg
域名 仓库名
一个完整的模块有几部分组成,首先是域名 gitlab.com,lyp256 是所有者,pkg 是模块的项目名字。对于单个 gitlab平台重要的是后面两段,也就是指定这个模块所有者和项目名,域名肯定是固定的可以忽略。
基于这样的规则我实现了一个简单的小服务,来解决 go get http 请求的处理。代码如下:

Gitlab CI 实践

Gitlab CI 时会起一个空的容器,图中示例使用的是 golang alpine 的镜像。这个镜像里除了 golang 没有其他的东西。我们需要安装相关依赖和注入 SSH 认证相关内容。script 中定义如下:
第一步: 使用 mikdir -p,在 cache 下创建目录,这个目录是我们 CI 机器上的缓存挂载的是物理盘上的一块空间可以保留数据,用来缓存 go mod 减少模块下载。
第二步:安装基础环境、工具软件包等。图中示例安装了 git 和 g++,g++ 是 go 编译所需要的依赖,openssh 是 ssh 的工具链 git 需要用到。
第三步:处理 SSH 秘钥。这里有两步,信任 gitlab 服务器秘钥和导入认证私钥。私钥是通过环境变量 $DEPLOY_SSH_KEY 导入,只需要保留该环境变量中的内容到对应的秘钥文件就可以了。gitlab 服务器秘钥使用 ssh-keyscan 来获取并保存到 known_hosts 文件。通过 gitlab SI 的配置把能访问 git 项目的私钥放在环境变量 $DEPLOY_SSH_KEY 里面,把私钥放在相应的 ssh 私钥文件并且授予正确的权限。
最后还需要配置 GOPRIVATE 变量定义所有 go.holdcloud.com 相关的模块为 PRIVATE 模块不要使用代理和检验和检查。
到此已基本完成所有准备工作,后面的 go test 是正常的 ci 测验逻辑,可以根据实际情况来写。
总结
-
GO 在 1.17 版本会删除对 GOPATH 的支持,建议尽快迁移至 GOMDULE;
-
GO 的校验和检查可以感知到代码的变化提高安全与可用性,建议不要关闭;
-
建议保留 vendor ,防止依赖模块被删除。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步