我们最近在 OpenSauced 平台上发布了与 OpenSSF Scorecard 的集成。 OpenSSF Scorecard 是一个功能强大的 Go 命令行界面,任何人都可以使用它来开始了解其项目和依赖项的安全状况。它会对危险的工作流程、CICD 最佳实践、项目是否仍在维护等等进行多项检查。这使得软件开发者和消费者能够了解他们的整体安全状况,推断项目是否可以安全使用,以及需要对安全实践进行改进。
但我们将 OpenSSF 记分卡集成到 OpenSauced 平台的目标之一是使其可供更广泛的开源生态系统使用。如果它是 GitHub 上的存储库,我们希望能够显示它的分数。这意味着将 Scorecard CLI 扩展到 GitHub 上的几乎所有存储库。说起来容易做起来难!
在这篇博文中,让我们深入了解如何使用 Kubernetes 做到这一点,以及我们在实现此集成时做出了哪些技术决策。
我们知道我们需要构建一个 cron 类型的微服务,它可以频繁地更新无数存储库中的分数:真正的问题是我们如何做到这一点。临时运行记分卡 CLI 是没有意义的:该平台很容易被淹没,我们希望能够对整个开源生态系统的分数进行更深入的分析,即使 OpenSauced 存储库页面尚未被访问最近访问过。最初,我们考虑使用 Scorecard Go 库作为直接依赖代码,并在单个整体微服务中运行记分卡检查。我们还考虑使用无服务器作业来运行一次性记分卡容器,该容器将返回各个存储库的结果。
我们最终采用的方法结合了简单性、灵活性和强大功能,是大规模使用 Kubernetes 作业,所有这些都由“调度程序”Kubernetes 控制器微服务管理。运行一个 Kubernetes 作业无需与记分卡构建更深层次的代码集成,它为我们提供了与使用无服务器方法相同的好处,但成本更低,因为我们直接在 Kubernetes 集群上管理这一切。作业的运行方式也提供了很大的灵活性:它们可以有很长的超时时间,可以使用磁盘,并且像任何其他 Kubernetes 范例一样,它们可以让多个 pod 执行不同的任务。
让我们分解这个系统的各个组件,看看它们是如何深入工作的:
这个系统的第一个也是最大的部分是“scorecard-k8s-scheduler”;类似 Kubernetes 控制器的微服务,可在集群上启动新作业。虽然此微服务遵循构建传统 Kubernetes 控制器或操作器时使用的许多原则、模式和方法,但它不会监视或改变集群上的自定义资源。它的功能是简单地启动运行 Scorecard CLI 的 Kubernetes 作业并收集完成的作业结果。
我们首先看一下Go代码中的主控制循环。该微服务使用 Kubernetes Client-Go 库直接与运行微服务的集群进行交互:这通常称为集群上配置和客户端。在代码中,引导集群上的客户端后,我们轮询数据库中需要更新的存储库。一旦找到一些存储库,我们就会在各个工作“线程”上启动 Kubernetes 作业,这些作业将等待每个作业完成。
// buffered channel, sort of like semaphores, for threaded working sem := make(chan bool, numConcurrentJobs) // continuous control loop for { // blocks on getting semaphore off buffered channel sem <- true go func() { // release the hold on the channel for this Go routine when done defer func() { <-sem }() // grab repo needing update, start scorecard Kubernetes Job on-cluster, // wait for results, etc. etc. // sleep the configured amount of time to relieve backpressure time.Sleep(backoff) }() }
这种具有缓冲通道的“无限控制循环”方法是 Go 中连续执行某些操作但仅使用配置数量的线程的常见方法。在任何给定时间运行的并发 Go 函数的数量取决于“numConcurrentJobs”变量的配置值。这将缓冲通道设置为充当工作池或信号量,表示在任何给定时间运行的并发 Go 函数的数量。由于缓冲通道是所有线程都可以使用和检查的共享资源,因此我经常将其视为信号量:一种资源,很像互斥体,多个线程可以尝试锁定和访问。在我们的生产环境中,我们扩展了该调度程序中同时运行的线程数量。由于实际的调度程序的计算量不是很大,只会启动作业并等待结果最终浮出水面,因此我们可以突破该调度程序可以管理的范围。我们还有一个内置的退避系统,可以在需要时尝试缓解压力:如果出现错误或没有找到可以计算分数的存储库,该系统将增加配置的“退避”值。这确保了我们不会不断地用查询猛击我们的数据库,并且记分卡调度程序本身可以保持在“等待”状态,而不占用集群上宝贵的计算资源。
在控制循环中,我们做了一些事情:首先,我们在数据库中查询需要更新记分卡的存储库。这是一个简单的数据库查询,它基于我们监视的一些时间戳元数据并具有索引。自计算存储库的最后一个分数以来,一旦经过配置的时间量,它将冒泡并由运行记分卡 CLI 的 Kubernetes 作业进行处理。
接下来,一旦我们有了可以获取分数的存储库,我们就使用“gcr.io/openssf/scorecard”镜像启动 Kubernetes 作业。使用 Client-Go 在 Go 代码中引导这项工作看起来与使用 yaml 非常相似,只需使用通过“k8s.io”导入提供的各种库和 API 并以编程方式执行:
// defines the Kubernetes Job and its spec job := &batchv1.Job{ // structs and details for the actual Job // including metav1.ObjectMeta and batchv1.JobSpec } // create the actual Job on cluster // using the in-cluster config and client return s.clientset.BatchV1().Jobs(ScorecardNamespace).Create(ctx, job, metav1.CreateOptions{})
作业创建后,我们等待它发出已完成或出错的信号。与 kubectl 非常相似,Client-Go 提供了一种有用的方式来“监视”资源并观察它们发生变化时的状态:
// watch selector for the job name on cluster watch, err := s.clientset.BatchV1().Jobs(ScorecardNamespace).Watch(ctx, metav1.ListOptions{ FieldSelector: "metadata.name=" + jobName, }) // continuously pop off the watch results channel for job status for event := range watch.ResultChan() { // wait for job success, error, or other states }
最后,一旦我们成功完成作业,我们就可以从作业的 pod 日志中获取结果,其中将包含来自记分卡 CLI 的实际 json 结果!获得这些结果后,我们可以将分数更新插入数据库并更改任何必要的元数据,以向我们的其他微服务或 OpenSauced API 发出信号,表明有一个新分数!
如前所述,scorecard-k8s-scheduler 可以同时运行任意数量的并发作业:在我们的生产环境中,我们同时运行大量作业,所有作业均由该微服务管理。目的是能够每两周更新一次 GitHub 上所有存储库的分数。凭借这种规模,我们希望能够为任何开源维护者或消费者提供强大的工具和见解!
“调度程序”微服务最终成为整个系统的一小部分:任何熟悉 Kubernetes 控制器的人都知道,系统运行还需要额外的 Kubernetes 基础设施。在我们的例子中,我们需要一些基于角色的访问控制(RBAC)来使我们的微服务能够在集群上创建作业。
首先,我们需要一个服务帐户:这是调度程序将使用的帐户,并具有与其绑定的访问控制:
apiVersion: v1 kind: ServiceAccount metadata: name: scorecard-sa namespace: scorecard-ns
我们将此服务帐户放置在所有这些运行的“scorecard-ns”命名空间中。
接下来,我们需要为服务帐户进行角色和角色绑定。这包括实际的访问控制(包括能够创建作业、查看 Pod 日志等)
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: scorecard-scheduler-role namespace: scorecard-ns rules: - apiGroups: ["batch"] resources: ["jobs"] verbs: ["create", "delete", "get", "list", "watch", "patch", "update"] - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] — apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: scorecard-scheduler-role-binding namespace: scorecard-ns subjects: - kind: ServiceAccount name: scorecard-sa namespace: scorecard-ns roleRef: kind: Role name: scorecard-scheduler-role apiGroup: rbac.authorization.k8s.io
您可能会问自己“为什么我需要授予此服务帐户访问权限才能获取 pod 和 pod 日志?这不是访问控制的过度延伸吗?”记住!作业有 pod,为了获取包含记分卡 CLI 实际结果的 pod 日志,我们必须能够列出作业中的 pod,然后读取它们的日志!
第二部分“RoleBinding”,是我们实际将角色附加到服务帐户的地方。然后可以在集群上启动新作业时使用此服务帐户。
—
向 Alex Ellis 和他出色的运行作业控制器致敬:这是正确使用 Client-Go 与 Jobs 的巨大启发和参考!
大家保持俏皮!
以上是我们如何使用 Kubernetes 作业来扩展 OpenSSF Scorecard的详细内容。更多信息请关注PHP中文网其他相关文章!