翻译:RangWu
原文:《Tutorial: Automate releases and release notes with GitLab — Ben Ridley》
在开发软件时,对每次发布的变更进行有效沟通至关重要。将新功能以及任何修改或删除及时通知用户,确保他们能正确使用软件,并在升级过程中避免一些意外。
以往创建发布说明和维护变更日志是一项繁重的任务,需要开发人员密切关注外部变更或由专门负责发布的经理筛选合并历史。而使用GitLab的Changelog API,您可以通过git仓库中提供的丰富的历史信息轻松创建发布说明和维护变更日志。
在本文中,我们将深入探讨如何使用GitLab自动化发布程序,包括生成发布制品、发布说明和详细的面向用户的软件变更日志。
首先,让我们了解GitLab中的发布(Release)是如何工作的。
在GitLab中,发布是代码的一个特定版本,由git标签(Tag)来标识,其中包括自上次发布以来的变更详情(与发布说明),以及从该代码版本构建的相关制品,例如Docker镜像、安装包和文档。
您可以使用UI在GitLab中创建和跟踪发布,也可以通过调用GitLab的Release API或在CI流水线中定义一个特殊的release
任务来创建和跟踪发布。在本教程中,我们将在CI/CD流水线中创建release
任务,这使得我们可以扩展流水线的自动化能力,在实现自动编译、测试、代码扫描的基础上,实现自动发布。
要实现自动化发布,我们首先需要回答一个问题:我们将从何处获取发布说明和变更日志的信息?答案是:git仓库,它为我们提供了通过提交消息(Commit Message)和合并提交(Merge Commit Message)的开发活动历史记录。让我们看看是否可以利用这些丰富的历史记录来自动创建发布说明和变更日志。
Commit trailers 提交追踪器是git提交信息中的结构化条目,是通过将简单的<HEADER>:<BODY>
格式消息添加到提交信息的末尾来创建的。git
CLI工具可以解析并提取这些信息供其他系统使用。您可能已经使用过的一个示例是git commit --sign-off
,它用于对提交进行签名。这是通过将Signed-off-by: <Your Name>
追踪器添加到提交中来实现的。我们可以在这里添加任意的结构化数据,这使得它成为存储变更日志信息的好地方。
实际上,如果我们在提交中使用一个Changelog: <added/changed/removed>
追踪器,GitLab Changelog API将解析这些信息并自动使用它们来为我们创建变更日志!
让我们通过对真实代码库进行一些修改,实现自动发布以及生成发布说明和变更日志条目,来看看它的效果。
为了演示效果,这里使用了一个简单的Python web应用程序代码库。让我们模拟应用程序的1.0.0版本刚刚发布,并且是基于当前代码的版本。我还手动创建了一个1.0.0版本的GitLab发布,因为我们还没有创建自动发布流水线:
假设我们正处于快速开发模式,今天我们将发布应用程序的2.0.0版本。作为2.0.0版本的一部分,我们将向应用程序中添加新功能:一个聊天机器人!并且我们将删除“量子区块链Quantum Blockchain功能”,因为我们只需要在首次风险投资中使用它。此外,我们将为2.0.0版本的CI/CD流水线添加一个自动发布任务。
首先,让我们删除不需要的功能。我创建了一个包含必要删除的合并请求。重要的是,我们需要确保提交消息包括Changelog: removed
追踪器。有几种方法可以做到这一点,比如直接在提交信息(Commit Message)中包括它,或者使用git CLI通过交互式变基(rebase)来添加它。但在这个例子中,我使用了最简单的方法就是等到功能开发完,在合并请求中使用编辑提交消息
按钮将追踪器添加到合并提交中,如下所示:
如果使用此方法,您还可以更改合并提交标题,使其更为简洁。我已将我的合并提交的标题更改为“Remove unused features”,因为这将出现在变更日志条目中。
接下来,让我们为2.0.0版本添加一些新功能。同样,我们只需创建另一个包含新功能的合并请求,然后编辑合并提交以包括Changelog: added
追踪器,并编辑提交标题“Add ChatBot”:
现在我们准备好可以发布2.0.0版本了。但这次我们不想手动创建发布。在发布之前,我们将向.gitlab-ci.yml
文件添加一些任务,实现在代码上标记新版本,例如2.0.0
时,自动执行发布,并生成相应的发布说明和变更日志条目。
注意:如果要强制设置变更追踪器,请考虑使用类似于dangerbot的工具来执行有关MR规范的自动检查。
为了使流水线正常工作,我们需要创建一个项目访问令牌,以允许我们调用GitLab的API生成变更日志条目。 创建具有API范围的项目访问令牌,然后将令牌存储为名为CI_API_TOKEN
的CI/CD变量。我们将引用此变量来进行API身份验证。
接下来,我们将在gitlab-ci.yml
文件中添加两个新任务:
1 | prepare_job: |
在上面的配置中,prepare_job
使用curl
和jq
来调用GitLab Changelog API,然后将其传递给release_job
来实际创建发布。
更详细地解释:
release_notes.md
。$CI_COMMIT_TAG
变量用作版本。为使其工作,我们需要在标签(Tag)中使用语义版本(例如2.0.0
这样的格式),因此您会注意到我还使用rules
部分限制了发布任务,以检查是否有语义版本标签(Tag)。release-cli
镜像。使用release-cli
需要在任务中使用release
关键字。release
关键字创建GitLab发布。这是专用的作业关键字,用于创建发布并填写所需字段。description
参数。在这个例子中,我们将prepare_job
中生成的文件,作为制品传递给此作业。按照上文进行设置后,要执行发布,我们只需要将一个遵循我们版本规范的标签(Tag)推送到代码库。您可以使用CLI简单地推送一个标签,也可以使用GitLab的UI在主分支上创建标签。通过在侧边栏选择“代码 -> 标签 -> 新建标签”来创建标签:
创建后,流水线将开始自动执行。GitLab Changelog API将自动生成用于发布说明的markdown,其中包含与上一次发布之间的所有更改。以下是在我们示例中生成的markdown的结果:
1 | ## 2.0.0 (2023-08-25) |
正如您所看到的,GitLab使用我们的git提交追踪器自动提取了发布说明的条目。此外,它还提供了有关变更的更多详细信息和讨论链接,以便读者可以查看。
最终发布如下所示:
接下来,我们要更新变更日志(所有发布说明的历史记录)。您可以使用POST
请求我们之前使用的Changelog API来完成这个操作。
如果您愿意,可以将这个操作作为发布流水线的一部分,例如,将以下内容添加到release
任务的script
部分:
1 | 'curl -H "PRIVATE-TOKEN: $CI_API_TOKEN" -X POST "$CI_API_V4_URL/projects/$CI_PROJECT_ID/repository/changelog?version=$CI_COMMIT_TAG" |
请注意,这会修改代码库。它将创建一个提交,将最新的说明添加到CHANGELOG.md
文件中:
至此,我们就实现了基于git
提供的丰富的历史信息和提交追踪器,利用GitLab强大的API和CI/CD流水线来自动化我们的发布流程,并为我们生成发布说明。
翻译:RangWu
原文:《How to export vulnerability reports to HTML/PDF and Jira — Siddharth Mathur》
GitLab的漏洞报告功能可以让开发人员在统一的平台上面管理代码,对其进行安全扫描,管理漏洞报告并修复漏洞。但有些团队更喜欢使用类似Jira的单独工具来管理他们的安全漏洞。他们也可能需要以易于理解的格式向领导层展示漏洞报告。
GitLab的漏洞报告可以通过一键点击的方式导出为CSV,方便在其他工具中进行分析。但在某些情况下,一个简单的PDF报告就足够了。
使用GitLab的API,可以轻松查询漏洞信息并将报告详细信息发送到其他地方,比如PDF文件或Jira项目。在本博客中,我们将向您展示如何将GitLab的漏洞报告导出HTML/PDF格式,或直接导出到Jira。
请注意,本教程中使用的脚本仅供参考,并不受GitLab支持。
要将漏洞报告导出为HTML或PDF,可参考custom-vulnerability-reporting项目。该项目包含一个脚本,用于查询项目的漏洞报告,然后基于漏洞报告的数据生成HTML文件。项目中配置的流水线会运行此脚本,并将HTML文件转换为PDF格式。
要使用导出工具,首先需要对该项目进行Fork(派生)或导入到新项目(选择“通过URL导入存储库”,并粘贴原始项目的git URL)。按照自述文件README.MD
的说明设置CI/CD变量。您需要以下GitLab信息:
在设置必需的CI/CD变量之后,从项目的流水线页面手动运行一个流水线。完成流水线后,可以访问build_report
(用于HTML)或pdf_conversion
作业,在侧边栏的“任务制品”下选择“下载”或“浏览”来查看您的文件导出。至此,您就获得了一个可共享且易于阅读的项目漏洞的pdf报告。
GitLab允许您通过UI使用我们的Jira集成功能从漏洞中创建Jira工单。尽管您可以为需要处理的漏洞单独执行此操作,但有时团队需要批量创建Jira工单来处理所有的漏洞。我们可以利用GitLab和Jira的API来实现这一点。
开始操作之前,请参考external-vulnerability-tracking项目。此脚本与上面的脚本以相同的方式获取漏洞,但它使用Jira API为每个漏洞创建一个工单。每个工单的描述也包含了来自GitLab漏洞报告的详细信息。
要使用导出工具,只需Fork(派生)该项目或导入到新项目(选择“通过URL导入存储库”,并粘贴原始项目的git URL),并按照自述文件的说明设置CI/CD变量。
您需要准备以下GitLab信息:
您还将需要以下Jira信息:
在根据项目自述文件中的说明设置CI/CD变量后,只需从该项目的流水线页面运行一个流水线,然后就可以看到所有的漏洞信息将会在Jira中自动创建工单。如果将来再次运行流水线,脚本将对Jira项目运行搜索查询,防止创建重复的工单。它只会为没有记录在Jira中的新漏洞创建工单。
翻译:RangWu
原文:《How GitLab can support your ISO 27001 compliance journey — Joseph Longo》
作为一体化平台,通过GitLab可以很容易实现DevSecOps全生命周期管理。GitLab使开发人员能够更快地构建更好的软件应用。但是,它的能力还不仅限于DevSecOps。
2022年10月,ISO组织发布了ISO 27001标准的最新版本。ISO/IEC 27001:2022与其之前的版本相比,包含了一些变化,其中在附件A中新增了对安全编码和配置管理的要求。
利用GitLab产品的功能特性来支持GitLab企业内部的安全合规计划,这是我们内部称为DogFooding的企业文化。GitLab维护的合规和保证凭证概述可以在GitLab的信任中心页面查看。
接下来我们可以一起回顾,如何使用GitLab以支持您的ISO 27001合规之旅。
控制ID | 控制描述 |
---|---|
5.3 职责分离 | 应分离冲突职责和冲突责任领域。 |
5.15 访问控制 | 应根据业务和信息安全要求建立和实施控制物理和逻辑访问信息和其他相关资产的规则。 |
5.16 身份管理 | 应管理身份的全部生命周期。 |
8.2 特权访问权 | 应限制和管理特权访问权的分配和使用。 |
8.4 对源代码的访问 | 应适当管理对源代码、开发工具和软件库的读写访问。 |
通过GitLab,您可以在将用户添加到项目或群组时为他们分配角色。用户的角色确定他们在GitLab实例内可以执行的操作。可分配的角色如下:
GitLab的角色使您能够根据最小特权原则和您的业务和信息安全要求来限制用户的权限。
通过GITLAB SAML SSO集成,GitLab使您能够集中进行身份验证和责任授权,从而支持GitLab实例的身份验证和授权。GitLab可以与多种身份提供者集成(如Auth0、ADFS、Okta、Oauth2.0、LDAP),以支持客户多样的技术栈。GitLab还支持跨域身份管理系统(SCIM)。通过GitLab的SSO和SCIM集成,您可以以安全和高效的方式自动化用户身份的生命周期管理。
注意: ISO/IEC 27001:2022附件A中关于技术控制的8.2和8.4也包含在上面的图表中,因为它们与组织控制的5.3、5.15和5.16密切相关。GitLab的功能同样可用于支持这些控制要求。
控制ID | 控制描述 |
---|---|
5.8 项目管理中的信息安全 | 应将信息安全集成到项目管理中。 |
使用GitLab,您可以使用我们的计划工具来支持项目管理工作,并确保在项目生命周期的所有阶段都适当考虑了信息安全。
GitLab的团队计划功能允许用户从构思到组织、计划、协调和跟踪项目工作。
史诗、议题和任务可用于构思协作、解决问题以及和信息安全团队的工作协同。描述模板和检查项使用户能够将一致的信息描述和工作流程应用到议题或合并请求上。这些模板可以很好的将信息安全一致地整合到项目管理生命周期中。
标签允许用户根据自己的要求自定义议题的类型。为支持信息安全,标签可用于标识与项目相关的风险级别、项目所处的阶段,或项目对应的信息安全团队。范围标签是一种类似K-V键值对的标签,具有排他性,可防止议题同时具备逻辑冲突的标签(如议题具备devops::configure标签,它就不能同时具备devops::create标签)。在GitLab中,可以利用范围标签来标识分配给不同团队的工作、工作所在的项目阶段以及与工作相关的产品或功能集。
控制ID | 控制描述 |
---|---|
8.8 技术漏洞的管理 | 应获取信息系统中技术漏洞的信息,评估组织对这些漏洞的曝露,并采取适当的措施。 |
8.9 配置管理 | 应建立、记录、实施、监控和审查硬件、软件、服务和网络的配置,包括安全配置。 |
8.25 安全开发生命周期 | 应建立和应用软件和系统安全开发的规则。 |
8.26 应用安全要求 | 在开发或采购应用程序时,应确定、规定和批准信息安全要求。 |
8.27 安全系统架构和工程原则 | 应建立、记录、维护和应用到任何信息系统开发活动中的安全系统工程原则。 |
使用GitLab,您可以存储您的硬件和软件配置,保持版本控制,通过合并请求更新您的配置,并利用GitLab的CI/CD流水线将这些配置推送到您的应用程序和基础设施。GitLab使组织能够通过单一平台实施GitOps。
GitLab的基础设施即代码扫描功能使您能够扫描您的IaC配置文件以查找已知漏洞。GitLab的IaC扫描支持多种IaC配置文件和语言,使其适应不同的技术栈。
对于合规专业人员,GitLab使您能够通过合规框架和合规流水线实施统一的、强制的自动化流程,从而支持您的安全规范,并促进遵守组织内部和外部的合规要求。
对于Ultimate(旗舰版)客户,GitLab的合规中心提供了对组合规中项目中应用的不同合规框架的集中视图。您可以看到您的项目是否符合GitLab标准。
控制ID | 控制描述 |
---|---|
8.15 记录 | 应生成、存储、保护和分析记录活动、异常、故障和其他相关事件的记录。 |
8.16 监控活动控制 | 应监控网络、系统和应用程序以寻找异常行为,并采取适当措施评估潜在的信息安全事件。 |
使用GitLab,您可以使用审计事件来跟踪重要事件,包括谁执行了相关操作以及何时执行的。审计事件涵盖了广泛的类别,包括:
对于Ultimate(旗舰版)客户,可以启用审计事件流。审计事件流使用户能够为顶级组或实例设置流目的地,以接收有关组、子组和项目的所有审计事件的结构化JSON。
控制ID | 控制描述 |
---|---|
8.28 安全编码 | 应将安全编码原则应用于软件开发。 |
8.29 开发和验收中的安全测试 | 应在开发生命周期中定义和实施安全测试流程。 |
您可以使用GitLab的安全阶段中的功能来增强您的软件开发生命周期并提高产品的安全性。GitLab的Secure阶段功能包括:
以及更多!
敏感信息泄露是安全漏洞的主要问题之一。GitLab的秘钥检测功能可以扫描您的代码库,防止您的敏感信息被泄露。
GitLab的安全策略功能使用户能够自定义扫描执行策略和扫描结果策略。这些策略将安全阶段的扫描结果与合并请求批准功能结合,形成安全门禁,可以进一步满足合规要求。
综合来看,GitLab的安全功能为安全的软件开发生命周期程序打下了基础,并使您能够根据组织的要求实践安全编码原则。
控制ID | 控制描述 |
---|---|
8.32 变更管理 | 应按照变更管理程序对信息处理设施和信息系统的更改进行管理。 |
GitLab提供了许多功能,以支持全面的变更管理。
GitLab的源代码管理功能使用户能够使用受保护分支。受保护分支允许GitLab用户对重要分支施加限制,实现:
代码库中的默认分支(如master、main分支)会自动指定为受保护分支。
合并请求(MR)是软件开发生命周期的核心组成部分。GitLab用户可以配置他们的合并请求,以便变更必须获得批准后才能合并。合并请求批准允许用户自定义审批流程,包括:
正如之前提到的,议题和任务可用于记录和协作变更请求。描述模板使用户能够将一致的信息描述应用于议题或合并请求,实现对变更的统一管理。
作为一体化DevSecOps平台,GitLab支持更广泛的需求。ISO在2022年的ISO标准中增加了围绕安全编码和配置管理的附加控制。这表明认证机构对软件整体的安全性有了进一步关注。作为战略合作伙伴,GitLab可以帮助您更好的支持ISO 27001标准,并帮助您更快的开发更好的软件。
要了解更多信息,请查看我们的tutorials库。
]]>使用ChatGPT翻译,RangWu校审。
原文:《The ultimate guide to securing your code on GitLab.com — Steve Grossman》
DevSecOps方法论的一个关键方面是在开发环境中应用最佳实践来保护您的软件免受恶意和意外的暴露或修改。本文介绍了如何控制和管理JihuLab.com和GitLab.com的访问以及与之相关的源代码管理、流水线构建、依赖和软件包仓库以及部署密钥,这些都涉及到软件供应链安全。以下最佳实践专门针对多租户JihuLab.com和GitLab.com上的最终用户,并针对旗舰版Ultimate许可证编写。并非所有这些功能都适用于专业版Premium许可证。
许多与安全相关的设置可以在顶层群组上设置,并会向所有子群组和项目进行级联。它们是保护您的JihuLab.com和GitLab.com实例的最简单和最重要的设置。
在顶层群组中,应应用以下设置,以提供对该群组内代码的最佳安全性:
这可能是通用设置中最重要的设置。通过群组“设置——通用——可见性级别”将群组可见性设置为“私有”,除非用户为该群组成员,否则任何人都无法访问该群组。此外,通过将顶层群组设置为私有,所有子群组和项目也将变为私有,并且不可公开。
在群组“设置——通用——权限和群组功能——权限”中:
通过合并请求批准来防止恶意代码被注入仓库中。在群组中为所有项目启用合并请求批准:
需要注意,在群组设置完合并请求批准后,还需在项目中设置具体的合并请求批准规则。
为了更加严格地控制可以访问JihuLab.com和GitLab.com上代码的人员,可以设置SAML单点登录。这将确保每个访问系统的人员都得到授权。
配置SAML单点登录,在群组“设置——SAML SSO”中:
定期检查和审查合规报告,以验证谁批准了合并请求以及被批准的合并请求是哪些。设置流式审计事件到企业安全信息和事件管理(SIEM)系统,并监控其中是否存在异常活动。这需要对层级结构中的每个群组和项目进行重复操作,以获取最大数量的审计事件。
在群组级别“设置——仓库——预定义推送规则”设置严格的推送规则有助于确保恶意代码不会注入到仓库中:
以下设置可以确保CI/CD流水线的完整性,并减少滥用和恶意行为的机会:
.gitlab-ci.yml
流水线定义文件的更改权限进行严格控制,通过CODEOWNERS文件防止CI/CD系统的恶意使用。一些设置不能从群组级别进行继承,或者在群组级别不可用,必须在单个项目上进行设置。这些设置包括一些特定于仓库的设置。
设置受保护的分支和受保护的标签,与上述受保护的Runner和受保护的变量相配合。
在项目“设置——CI/CD——流水线通用设置”中
使用受保护的环境并严格限制可以部署和要求批准的人员。
在项目“设置——CI/CD——令牌访问”勾选“允许使用 CI_JOB_TOKEN 访问此项目”,默认仅允许当前项目使用该令牌,可添加其他指定的项目使用该令牌。不要关闭该功能,来确保恶意项目无法检索并使用它来访问API。
将密钥、配置文件和签名证书存储在安全文件中存储,而不是存储在代码仓库中。
作为上述安全扫描的补充方案,您可以选择启用扫描执行策略以防止合并具有严重漏洞的代码。
遵循这些最佳实践将有助于确保您托管在JihuLab.com和GitLab.com上的代码不受篡改和公开暴露,确保您的软件供应链安全,只有授权的用户可以访问您的软件资产。
作为一体化DevOps平台,极狐GitLab内置了开箱即用的CI/CD引擎,并可以与K8S集成,便于实现更快、更可靠和更高效的云原生应用程序开发、测试和部署。
网络上有很多关于使用极狐GitLab在K8S中进行CI的方案,本身也比较简单。而关于使用极狐GitLab在K8S中进行CD的内容却比较少,总结的也不是很全面。所以我将这部分内容单独抽离出来,汇总成一篇文章,供大家参考。
在开始阅读文章或进行实操前,你需要掌握以下知识:
其原理是将KubeConfig
文件作为GitLab CI/CD 环境变量进行存储,在流水线脚本中使用kubectl
通过KubeConfig
文件连接到K8S集群并执行命令。
该方案使用简单,但在安全性较差,已被GitLab遗弃,详见:《Kubernetes clusters | GitLab》
但你依然可以根据实际情况选择使用这种方式,比如在测试环境、小型团队或者在K8S CD的起步阶段使用。
获取K8S集群的KubeConfig
文件,示例内容如下:
1 | apiVersion: v1 |
在实例级、群组级或项目级设置CI/CD环境变量,如创建名为UAT_KUBE_CONFIG
的变量,类型为文件,内容为KubeConfig
文件中的内容:
在GitLab项目中添加K8S Manifest,如deploy.yaml
文件:
1 | apiVersion: v1 |
在GitLab项目中添加流水线脚本,示例内容如下:
1 | deploy-to-sit: |
该方案操作简单,容易实现,但在流水线脚本中可以通过cat $KUBECONFIG
命令读取KubeConfig
文件内容,存在安全风险,对此可参考《如何安全使用GitLab CICD SSH部署》文章中的内容,对KubeConfig
的部分内容进行隐藏,可在一定程度上提高安全性。
为解决基于认证的K8S集成所带来的安全性问题,提高效率和性能,以及实现更多的功能,GitLab设计了GitLab Agent for K8S来作为K8S和GitLab沟通的桥梁,详见:《GitLab Agent for Kubernetes | GitLab》。
GitLab Agent for K8S在GitLab 14.5版本之后从专业版下放到标准版(社区版)。GitLab Agent for K8S同时支持Push模型和Pull模型,关于这两者的介绍和区别可参考文章《云原生时代,你还不懂 GitOps》。
在GitLab中,基于传统的Push模型的CD方式称之为”GitLab CI/CD Workflow”,而基于Pull模型的CD方式称之为”GitOps Workflow”,接下来将分别说明这两种方式如何实现。
不论是GitLab CI/CD Workflow还是GitOps Workflow,都需要安装GitLab Agent for K8S,目前GitLab Agent for K8S支持的K8S版本如下:
此外需要在本地电脑安装helm
和kubectl
用于链接K8S集群并安装Agent。
安装方式详见文档:《Installing the agent for Kubernetes | GitLab》
以下是安装Agent的主要步骤:
创建一个根群组,如ci
。在根群组ci
下创建子群组,如agents
。在子群组agents
下创建一个项目,如agent1
。在该项目下创建文件,路径为.gitlab/agents/<agent-name,如my-agent>/config.yaml
,内容留空,用于作为Agent的配置文件。
在agent1
项目左侧的菜单栏中,选择“基础设置—Kubernetes集群”,新建一个集群,如my-agent
,需注意集群名称需与上一步文件路径中的<agent-name>
一致。
使用kubectl
连接到K8S集群,根据指引使用helm
命令安装Agent.
安装完成后检查Agent的连接状态。
需注意GitLab Agent for K8S只能安装在指定的项目中,不能安装在实例或群组中。如果有很多项目都需要用到Agent,虽然可以给每个项目创建Agent,但管理比较复杂,而且一点也不优雅,所以我们希望尽可能的去复用同一个Agent。
Agent支持给其他项目或者群组复用,但这些项目或群组需要与Agent这个项目本身处于同一个根群组下,不能跨根群组复用Agent,详见:《Using GitLab CI/CD with a Kubernetes cluster | GitLab》
所以基于GitLab CI/CD Workflow的群组划分方式一般建议如下:
1 | ├── Group: ci # 该群组中的项目都可以复用agents中的GitLab Agent项目 |
基于2.1中配置的Agent,实现GitLab CI/CD Workflow的主要步骤如下:
在agent1
项目中,修改.gitlab/agents/<agent-name>/config.yaml
文件,增加以下内容:
1 | ci_access: |
在根群组ci
下创建子群组,如gitlab-cicd-workflow
。在子群组gitlab-cicd-workflow
中创建项目,如push-model-demo
,该项目的相对路径是ci/gitlab-cicd-workflow/push-model-demo
。
在push-model-demo
项目中添加K8S Manifest,如一个deploy.yaml
文件。
在push-model-demo
项目中添加流水线脚本,示例内容如下:
1 | deploy: |
运行push-model-demo
项目的流水线,验证结果。
使用GitLab CI/CD Workflow基于Agent的K8S集成比基于认证的K8S集成略显复杂,但它不会泄露KubeConfig
文件,也不直接操作K8S API
,此外在agent
项目中可修改配置文件实现对指定项目的CD授权,也从多方面增加了系统的安全性。
但是由于Push模型本身在设计上就会出现“配置漂移”和安全合规问题,所以GitLab CI/CD Workflow依然被认为是一种“不安全”的CD模式。
基于GitLab Agent for K8S的GitOps Workflow如下图所示:
开发人员使用GitLab CI对代码进行自动构建,将打包的镜像存放到制品库,将配置清单存放到配置库。部署在K8S集群上的GitLab Agent监听配置库,当发现配置库有变化时,基于配置库中配置清单自动执行部署任务。
需要注意的是目前基于GitLab Agent for K8S的GitOps Workflow存在一些缺陷:
如果要复用Agent,则配置清单项目需设置可见性为公开(Public)。而一个项目的可见性要设置为公开(Public),则它的群组、父群组的可见性也需要设置为公开(Public)。这增加了管理上的风险。
或者在每个配置清单项目里设置单独的Agent,这样这些项目的可见性就可设置为私有(Private)。但这又增加了管理的复杂度。
GitLab官方目前正在解决这个问题,详见:https://gitlab.com/groups/gitlab-org/-/epics/7704
基于2.1中配置的Agent,实现GitLab CI/CD Workflow的主要步骤如下:
在根群组ci
下创建子群组,如gitops-workflow
。在子群组gitops-workflow
中创建公开(Public)项目,如pull-model-demo
,该项目的相对路径是ci/gitops-workflow/pull-model-demo
。
在pull-model-demo
项目中添加K8S Manifest,如一个deploy.yaml
文件。
在agent1
项目中,修改.gitlab/agents/<agent-name>/config.yaml
文件,增加以下内容:
1 | gitops: |
等待片刻后使用kubectl get pod -A
查看部署情况。
可使用以下命令查看Agent日志或进行调试:
1 | # 查询所有GitLab Agent的命名空间 |
正如上文所述,基于GitLab Agent for K8S的GitOps Workflow实现了GitOps,但它目前还存在一些问题,在这些问题得到解决之前,建议你充分考虑使用这种方式的利弊,或者考虑使用第三方的GitOps工具,如Flux、ArgoCD等。
关于GitOps Workflow的更多内容可以参考:《Using GitOps with a Kubernetes cluster | GitLab》
Flux 是一个 GitOps 工具,用于自动化地管理 Kubernetes 应用程序的部署和更新。它的主要思想是将 Kubernetes 集群配置文件存储在 Git 存储库中,并使用 Git 的工作流来管理应用程序的生命周期,包括部署、升级和回滚。
Flux 可以通过轮询 Git 存储库或使用 Webhooks 自动同步 Kubernetes 应用程序的部署状态。当 Git 存储库中的配置文件发生更改时,Flux 会自动检测并将更改推送到 Kubernetes 集群中,从而实现自动部署和更新应用程序的能力。
2023年2月,GitLab官方也宣布了未来将会与Flux深度集成,计划将Flux作为GitLab GitOps解决方案的一部分来替代GitLab Agent for K8S。详见:《GitOps with GitLab: What you need to know about the Flux CD integration | GitLab》
目前GitLab与Flux的集成还是依靠Flux原生的能力,后续会在GitLab上开发相关的UI界面以增强用户体验,预计2024年会发布GA版本。
使用Flux与GitLab集成实现GitOps可参考官方文档:《Tutorial: Set up Flux for GitOps | GitLab》
主要步骤如下:
创建空项目,如flux-config
,作为Flux的配置数据源。并为该项目创建访问令牌,角色为Maintainer
,范围是api
。
在本地电脑安装kubectl
,配置上下文以访问K8S集群,用于安装Flux。
在本地电脑安装Flux CLI
,Flux CLI
的安装方式可参考《Install the Flux CLI》,以Mac和Linux为例,可执行以下命令安装:
1 | curl -s https://fluxcd.io/install.sh | sudo bash |
在本地电脑通过Flux CLI
在K8S集群中安装Flux:
1 | # 第1步中获取的访问令牌 |
安装成功后显示内容如下:
执行kubectl get pod -n flux-system
查看Flux的部署情况。
创建项目,如web-app-manifests
,用于托管某项目的K8S Manifest,如nginx-deployment.yaml
:
1 | apiVersion: apps/v1 |
创建该项目的部署令牌,设置名称,如flux_deploy_token
,范围是read_repository
。
为了避免每个项目都要创建部署令牌,也可使用群组访问令牌或者个人访问令牌,范围同样也是read_repository
。
在本地电脑通过Flux CLI
在K8S集群中生成该部署令牌的Secret:
1 | # flux-deploy-authentication 是Secret名称 |
使用命令kubectl -n default get secrets flux-deploy-authentication -o yaml
验证Secret是否生成成功。
在Flux配置项目flux-config
中添加文件clusters/my-cluster/web-app/web-app-manifests-source.yaml
,内容如下:
1 |
|
该文件用于将web-app-manifests
项目以GitRepository类型同步到K8S集群中。
在Flux配置项目flux-config
中添加文件clusters/my-cluster/web-app/clusters/my-cluster/web-app/web-app-manifests-kustomization.yaml
,内容如下:
1 |
|
该文件用于监听K8S中的GitRepository资源,当资源发生变化时,使用kustomize
来运行这些Manifest。
使用kubectl get pods -n default
命令,可以看到Flux根据web-app-manifests
项目中的nginx-deployment.yaml
部署了3个nginx-deployment
。
修改web-app-manifests
项目中的nginx-deployment.yaml
,如将replicas
修改为2
,等待片刻,再次使用kubectl
命令查看,Flux自动将nginx-deployment
的副本数量调整为2
。
如果想划分部署环境,可参考以下方式:
相同集群,不同命名空间:无需修改Flux配置库,只需用不同名称的配置清单或配置清单库的不同分支来区分Manifest和Namespace即可。
1 | ├── Group: A |
不同集群:需修改Flux配置库,用Flux配置库中的不同目录区分不同的K8S环境,用不同名称的配置清单或配置清单库的不同分支来区分Manifest和Namespace。
1 | export GITLAB_TOKEN=xxxxx |
1 | ├── Group: A |
使用Flux与GitLab集成,开发人员只需通过CI将应用打包成镜像存放在镜像库,开发人员或运维人员维护该应用对应的配置清单库如web-app-manifests
,运维人员维护Flux配置库如flux-config
,即可实现GitOps。
ArgoCD 是一款开源且主要针对 Kubernetes 来做 GitOps 的持续交付工具,是 CNCF 的孵化项目。
相较于Flux,ArgoCD提供了更完整的GitOps解决方案,包括多集群支持、应用程序版本控制、可视化部署状态等功能。
ArgoCD作为目前使用最为广泛的GitOps工具,亦提供与极狐GitLab集成,将极狐GitLab作为单一可信源,从而实现GitOps。网络上关于ArgoCD+GitLab的相关文章和介绍很多,也可直接参考极狐GitLab官方的技术博客:《极狐GitLab 和 ArgoCD 的集成实践-极狐GitLab》
作为诞生于社区的开源产品,GitLab在CI/CD方面大部分的功能是免费的,基于极狐GitLab和这篇文章,你可以实现在K8S中进行CD的基础功能。如果要做的更深、更好,肯定需要花费更多的时间来实践、打磨。当然如果你的团队和企业需要一些技术支持和一些最佳实践的经验指导,少踩坑、快上线、有兜底,也可以使用极狐GitLab的企业版,在拥有更多企业级功能、性能的基础上,获得更全面、更可靠的服务。
特别鸣谢:感谢ChatGPT和New Bing,真是写作之利器。
版本控制,也称为源码控制、代码管理,是跟踪和管理软件代码的工作实践。随着信息化、数字化技术的发展,源代码逐渐成为企业的核心数据资产,在企业中有着非常重要的地位。所以如何管理好代码这个数据资产,是每一个企业都需要考虑和解决的问题。而作为数字化时代的先行者,大部分互联网和科技型企业已经完成或者部分完成了这项工作,并借助敏捷开发、DevOps等方法论和工具实现了代码的规范、可靠管理,以及高效高质量的产品交付。
在产业数字化转型,企业DevOps转型的大趋势下,近些年有不少传统嵌入式开发的企业开始参与进来。嵌入式与互联网、科技公司有着不同的技术栈、开发模式和交付方式,对他们来说,并不能发扬拿来主义精神,将“前辈”的经验直接复用。需要结合实际情况走出一条不一样的路,这里首当其冲要解决的就是代码的可靠管理、高效协同以及高质量交付问题。
根据我过往工作经历以及接触到的部分客户来看,嵌入式开发场景的主要特点如下:
团队规模:团队规模不大,多数在100人左右。
技术路线:主要以C/C++为主。
开发模式:主要以瀑布流为主。
需求管理:需求相对简单,相较于应用系统的复杂业务逻辑,嵌入式更多是在跟芯片、元器件打交道。
代码大小:较小,多数在KB或者MB级别。
协同方式:主要以单人开发为主,无需协同。
交付频率:较慢,大部分按照月度进行交付。
交付方式:固件为主(单片机、下位机等),少部分是可执行程序(上位机)。
测试方式:真机烧录手动测试为主,极少团队使用仿真模拟测试。
系统工具:主要围绕需求管理和代码管理。
在这样的背景下,从事嵌入式开发的企业其实也是不紧不慢,安稳度过了一段时期。但在数字化转型的浪潮下,很多企业管理者开始居安思危,希望借助一些先进的开发和管理模式,帮助企业和研发团队转型升级。
如上文所述,嵌入式开发场景的特点决定了他们的工作模式,也带来了一些弊端。简而言之就是缺少管理、缺乏规范、不成体系。根据我接触到的客户情况来看,从事嵌入式开发的管理者和研发人员对于管理企业源代码的主要诉求如下:
统一管理:源代码是企业核心资产,有必要进行集中、统一管理。
权限管控:需要实现最基本的无权限、只读、读写、管理等权限分配和管控。
版本控制:需对代码进行版本控制,实现最基本的查看历史记录、查看提交人、进行版本回退等功能。
代码规范:需要对开发人员的代码规范进行约束,避免开发人员上传一些无关要紧的大文件以及不规范的代码提交信息,从而导致性能问题并影响协同效率。
代码复用:需要有代码复用的能力,避免企业内重复造轮子,避免相同功能的代码在多个项目中多次实现。
分支策略:需要一套分支策略和工作流,实现单人、多人对于一个项目的持续开发、修复、发布,并且能较好的区分和管理不同的环境和版本,如开发环境、生产环境以及为某客户定制版本。
审核机制:需提供一套审核机制,实现代码的评审和确认,未经审核的代码不允许进入代码管理系统,从而提高代码质量,提升开发团队的综合能力。
安全审计:需记录系统用户在代码管理系统上的行为和操作,便于对一些风险行为进行分析、追溯和告警,降低代码泄露的风险。
所谓工欲善其事必先利其器,嵌入式开发团队或企业要解决的问题远不止如此,但很多团队选择先从工具侧入手,基于一个好的工具再慢慢探索和完善其实践方式和管理流程。
纵观版本控制系统,即代码管理系统的发展史,大致分为4个时期,它们对应的主流工具如下:
毫无疑问,Git是当下用于源代码管理的主流工具,它的发起人正是Linux之父Linus Torvalds。早在2002年以前,世界各地的Linux开发者通过邮件的方式把代码diff发送给Linus本人,再由他人工评审和合并,效率非常之低下,社区开发者也表达了不满。而Linus非常反对CVS、SVN等集中式的版本控制系统,它们性能较差且必须联网才能使用。在2002年至2005年这段时间,BitMover公司授权Linux社区免费使用它们的商业化版本控制系统BitKeeper,然而在2005年,Linux社区的开发人员却试图破解BitKeeper,导致BitMover公司收回使用权。故事的最后,Linus本人花了两周时间用C写了一个分布式的版本控制系统,就是Git。所以Git自诞生之日起,就是为了更好的管理Linux的核心代码。它跟Linux一样也是开源的,并且在社区开发人员的持续贡献下发展了快二十年。
而SVN作为上一代的版本控制系统,开始逐渐退出历史舞台,今年1月,GitHub也宣布自2024年1月8日起,停止对 Subversion (SVN)的支持。
对于没有使用任何代码管理工具的企业,没有历史负担,会直接选择Git。而大多数的从事嵌入式开发的团队,都在使用上一代的源代码管理工具SVN,这时候他们要考虑的就是Why和How的问题了。
要考虑为什么或者要不要从SVN切换到Git,最直接的方式就是对它们进行一个对比。
总结一下:
架构:SVN是集中式架构,Git是分布式架构,下图是对它们架构的一个描述。但从架构模式上来看,也不太容易能理解两者的区别,那么可以参考灵活性这项的内容,并且举一个形象的例子,更有助于理解它们显著的差异。
所以Git能满足远程办公、异地办公等临时脱离代码服务器的开发场景,某种意义上更符合未来协同办公的发展趋势。同时为开发人员在本地进行版本控制提供了可能性,这使得开发人员在有后悔药可以吃的前提下能够在本地进行非稳定功能的开发,等功能相对稳定后再同步到远端,这为多人协同开发打下了基础,避免不稳定的代码影响到他人。
如果你在使用SVN,为了解决上面的问题,就不得不多在本地建几个文件夹存放临时的代码副本,这一点也不优雅。
安全:由于架构模式的区别,两者在安全可靠方面也有不同的表现。
当然,Git的副本数越多,可靠性也就越高,同时存在数据泄露的风险就越高,数据的安全性又面临挑战,这是另外一个话题。好在国内企业对于数据隐私、数据保护非常重视,大部分传统企业已经通过DLP(Data loss prevention 数据泄露保护)技术实现了对企业内部系统数据的加密和保护,如IP-guard等工具,这个技术对于SVN和Git同样有效。
权限:SVN和Git在权限模型上也有较大的差异。
这是两个工具设计理念引起的差异,这也可能是SVN用户唯一不愿意迁移到Git的理由了。
由于SVN支持按目录授权,早期也没有太多模块化设计的实践,所以使用SVN用户习惯性的把整个项目的所有组件和相关依赖塞进一个仓库,走的是大仓模式,再根据需要对目录或整个仓库授权。这又导致SVN的仓库普遍臃肿、容量大,加剧了性能问题。
而Git的理念是模块化的,倡导解耦,走的是分仓模式。如果代码彼此之间业务和功能逻辑相对独立,并且能够独立编译,那么就应该放到不同的代码库进行管理和授权。如果代码彼此之间有强依赖,无法独立编译,那么可以放到一个仓库里,但就没必要进行按目录授权了,因为不具备某些目录权限的开发人员无法通过编译来验证自己开发的功能是否正确,这样就失去了协同开发的意义。当然,有些以Git为底层的代码管理系统支持对代码库的目录进行写入控制,在一定程度上弥补了这个权限控制的缺失。
性能:同样也是由于架构模式的区别,两者在性能上也相差甚远,用C++开发人员的话来讲。
正因如此,SVN的用户基本上不会有多个分支,因为分支越多,性能越差。分支少决定了SVN的用户无法实现多人协同开发,一旦多个开发人员在同一个分支下进行开发,那么产生冲突的概率就会增加,并且相互影响,后提交的人需要解决冲突,强制覆盖后又会影响先提交的人,最后变成竞赛游戏。而在嵌入式开发场景中,一个固件对应多个版本,或者针对不同芯片、不同用户又有定制版本的情况时有发生,又不得不利用分支来进行管理,分支数量一上去,分支间的代码同步和性能问题又开始显现,可谓是让SVN的用户骑虎难下。
Git与之相反,依靠灵活快捷的分支管理方式,配合多种分支策略,可以实现多人协同开发、按环境和版本区分代码、并尽可能确保通用的代码在不同环境、版本间保持同步。分支策略本身也决定了研发流程的规范建设,这也是企业关心的核心内容。
体验:
从学习和使用层面来看,Git的成本相对较高,但也有一些降低门槛的方法和工具。相较于架构、模式所带来的变化,操作体验上的变化显得微不足道了。从另一方面来说,Git也算是开发人员的基本技能,毕竟全球94%的开发人员都在使用Git或者会使用Git。
此外,因为SVN支持按文件进行下载,再加上操作简单,非技术人员比如产品经理、项目经理也容易上手使用,所以非常多的SVN用户使用SVN进行文档管理,比如Word、Excel等。Git恰恰相反,不支持按文件下载(可以通过部分克隆实现,但对于非技术人员成本略高),必须把整个仓库下载下来,操作也比较复杂,所以当这些用户迁移到Git后,旧文档的管理方式又变成了一个问题。对于这个问题,文档就应该放在文件服务器(FTP、SMB、NFS、NAS等)或者文档协同系统(Wiki、Confluence、腾讯文档、飞书等)上进行管理。放在SVN上,在以前的时代虽然可行,但集中式的架构需要在线才能访问,且大文件使得SVN本就不足的性能雪上加霜,此外现在人们对于文档管理的需求除了管理、存储、版本控制之外,还看重协同,所以如果决定从SVN向Git进行迁移,文档的管理模式也需要进行升级。
综上,从SVN迁移到Git,优势自然是顺应技术发展的趋势,相较于SVN这个已经没有任何官方支持的上一代产品,有非常多的以Git为底层的成熟工具和商业化产品,比如GitLab、GitHub、Gerrit等,可以为企业DevOps转型提供更好的支持。此外Git可以从根本上解决SVN的性能问题,实现更好的研发协同,便于企业建立规范的研发流程。但问题是除了工具本身和使用体验上的差异,Git在分仓模式、分支策略、授权模式上还有不同,甚至直接影响研发流程的改变,这里面有哪些潜在的风险,又该如何去解决,为此我会在后续章节来展开,详细讨论这些问题。
在上文中我已经简单介绍了SVN和Git在分仓模式和权限管理上的一些区别,简单来说:
这本是一个简单的选择题。要么留在SVN,保留现有的目录授权模式;要么使用Git分仓,权限也落在不同的仓库上;要么使用Git大仓,权限就控制在整个仓库上。直到另一个需求打破了这个平衡:代码复用。
做过C/C++且又做过Java、Python、Node等语言开发的技术人员常常会感叹C/C++缺少好的包管理工具,羡慕Java有Maven、Python有Pip、Node有Npm,而C/C++ 一直深陷DLL地狱、代码版本与交付版本不一致、重复造轮子等各式各样依赖问题的泥潭不可自拔。
首先C/C++由于其语言特性,构建时常常是系统底层相关,所以打包时需要考虑操作系统、体系架构、编译器版本、构建类型等一系列因素。且需要关注一些包自身的属性:是纯头文件库、静态库还是动态库,以及包的构建参数(比如优化级别、是否开启exception和rtti的编译选项等)、还有指定裁剪性(特性宏)等配置。另外,由于C/C++的标准库(glibc和libstdc++)存在版本兼容性问题,以及C++存在ABI兼容性问题,这会让包的版本管理超越语义化版本(SemVer)所能解决的问题范围,这导致包的创建者需要在发包的时候为包的兼容性做更多的考虑。最后,由于C/C++语言在语法上缺乏包级别的模块化机制,会让包的符号冲突以及依赖解决变得困难。如果还不够,再加上交叉编译的场景,绝对会让一个通用C/C++包管理器的复杂度超过其它任何语言。
包管理器的目的就是对包进行版本控制,可以解决项目间的依赖问题,也能从根本上解决代码复用的问题。但由于C/C++包管理器的复杂度,导致C/C++开发人员在过去很长一段时间内没有包管理工具可用。在这样的背景下,大家对于C/C++的代码复用发展出了很多模式。
基于代码文件目录划分
项目划分好模块,定义好自己的目录,协商出一个Common目录作为公共的头文件目录,然后对不同的开发人员分配不同的目录权限就可以分工协作了。看起来是不是非常眼熟,这就是SVN的使用方式。然而这个Common目录可能又需要被另一个项目B所引用,那么项目B整个代码也加进这个代码库。最后所有的模块都耦合到一起,代码库膨胀的很快,导致严重的性能问题。
基于代码复用工具划分
由于SVN性能问题,很多开发者开始尝试迁移到Git,把代码分不到不同的代码库中。但由于没有包管理工具,嵌入式的项目需要先将项目关联的代码库拉到一起,然后再构建。而Git本身不提供仓库之间的关联关系,这就对分库之后的聚合管理提出了需求。后来出现了git submodule
、git subtree
、git repo
等工具,就是为了解决分仓后,如何把项目再聚合出来,从而实现项目管理和代码复用。但这些都是在代码文件级别的复用,增加了管理的复杂度。
基于CMake
一些构建工具的发展,为C/C++的代码复用引入了更好的方式。例如CMake从3.0版本开始被称之为“Modern CMake”,是因为它引入了target的概念,以及基于target建立起了构建的依赖可见性和传播控制机制。这些都更好的支持了代码在构建上的模块化,借助CMake的ExternalProject和find_package特性,使得我们可以从指定的http或者git分支下载、构建、安装和引用代码库。但是这种复用方式,对于间接依赖的管理仍旧是不足的,没有办法做到全链条的依赖解析、依赖追溯、冲突判决,以及基于变更进行最小范围的重构建和发布管理。
基于包管理工具
解铃还须系铃人,问题发展到最后还是回到问题本身,C/C++没有好的包管理工具,那就做一个。Conan是一款出色的开源C/C++包管理器。它吸收了很多现代化包管理器的设计思想,探索解决通用C/C++包管理器的各种挑战。它需要使用Python进行配置,目前在国内的普及度还不算高,相关的文档教程也不是很齐全,相对来说有一定的门槛,但Conan可以说是目前C/C++唯一可用的包管理工具,也可能是真正的破局者。
这就是为什么一些软件企业从SVN迁移到Git没有那么大阻力而从事嵌入式开发的企业则不同的原因。归根到底是C/C++缺乏好的依赖管理手段,而企业、管理者、开发人员一直都面临代码复用这个问题,并希望通过从SVN迁移到Git来解决这个问题。但显然这个问题仅依靠SVN或者Git自身是无法解决的。
既然Git现在是代码管理的主流方案,并且依靠Git自身无法解决分仓后的多仓管理问题和代码复用问题,那就需要借助一些其他的工具和方法,其实都是上文中提到过的。虽然这些工具和方法本身不够完善,但对于处于不同阶段不同场景的企业和用户,可以有个参考,毕竟软件世界没有银弹。
Git submodule可以让一个Git仓库作为另一个仓库的子目录,从而实现在一个代码库中引用其他的代码库进行复用。
Git submodule的特点如下:
在主库中通过 git submodule add <子库git地址>
命令实现引用子库代码。
在主库中通过.gitmodule
文件来记录主库和子库的引用关系。
主库只是引用了子库的SHA,并没有直接拷贝子库代码,所以子库的代码变更内容在主库上不可见,只是在本地拉取时将对应的子库拉取到本地。所以在Git服务器上看不到完整的项目代码,这也意味着无法实现对于整个项目的代码评审。
Clone主库不会自动Clone子库,除非在Clone主库时:
git submodule update
。git clone --recursive
。git config --global submodule.recurse true
。子库commit/push
也需要在主库 pull/commit/push
,容易遗忘导致代码未同步或者主库关联了旧的子库。
git submodule foreach 'git pull origin master'
更新所有子库。个人不建议在实际项目中使用Git submodule,除非能有效解决以上问题,或者可在少量的项目中进行试用再进行决策。
Git subtree与Git submodule功能类似,但目前Git subtree在开发人员中的呼声高于Git submodule。
Git subtree的特点如下:
在主库中通过 git subtree add --prefix=<主库子目录> <子库git地址> <子库分支>
命令实现引用子库代码。
主库拷贝了子库的代码,所以在Git服务器上可以看到完整的项目代码,也可以实现整个项目的代码评审。所以Git submodule is Link, Git subtree is Copy.
也意味着Git subtree的性能略差,会增加主库的大小。
无法通过默认的Git命令将主库的代码变更同步到子库,需要适应新的工作流。
1 | 添加子库 |
可通过一些工具和方法简化工作流。
设置Git别名以简化操作。
1 | 在 .gitconfig 文件中添加 |
设置子库为主库的远端别名以简化操作。
1 | 添加子库为主库的remote别名 |
使用第三方工具git-subrepo.
个人建议可以在依赖场景不复杂的中小型项目中使用Git subtree,以避免性能问题,它的体验和工作方式相对比较友好。
最简单的办法往往最有效,通过脚本来拉取或者相关子库的代码,将脚本放在主库中,按需执行,比如拉取相关子库代码:
1 | git clone -b <子库A分支> <子库A git地址> <本地目录A> |
或者通过CMake的FetchContent来实现,可以参考《C++ 工程依赖管理新方向:CMake & Git | KC的废墟堆》,基于FetchContent可以再封装一套脚本。
个人建议也是可以在依赖场景不复杂的中小型项目中使用,比较轻量和灵活,但有一定的技术门槛,甚至需要专人来做,这对做传统嵌入式开发的团队是个挑战。
Git-Repo是Google开源的一款Git客户端工具,是为了搭配Google开源的代码管理工具Gerrit进行使用,而Gerrit是为了管理Android的开源项目AOSP而设计的。Google是为数不多的坚持大仓模式(Monorepo)的巨头公司,AOSP也是一个大型项目,一个项目包含了数百个代码库,彼此之间存在依赖关系。所以为了更好的管理这些仓库,Google形成了自己独特的AOSP工作流,Gerrit通过一个项目清单Manifest.xml
来组织仓库关系,Git-Repo就可以通过Manifest.xml
来实现代码的批量拉取和推送。
Git-Repo和Gerrit主要是解决了多仓管理的问题,除了对仓库进行批量操作,Gerrit还支持跨代码库进行评审,所以AOSP从流程到系统到工具都是相辅相成的。Git-Repo可以在一定程度上解决代码复用问题,不过Gerrit本身不是商业化产品,没有厂商技术支持,且Gerrit的用户体验较差,复杂度较高,所以当嵌入式项目使用AOSP专用的工具,又显得有点水土不服。
借鉴Git-Repo和Manifest.xml
的思想,阿里使用Golang重写了一个Git-Repo-Go工具,可以在GitLab、GitHub、阿里云效CodeUp等以Git为底层的代码管理系统上获得Git-Repo批量操作代码库的体验。但是上文中也提到,Git-Repo和Gerrit是相辅相成的,Gerrit支持跨代码库进行代码评审,支持更丰富的权限管理模式,为多仓下的代码管理和评审提供了基础。而GitLab、GitHub等代码管理系统目前还是以分仓模式为主,原生不提供这种业务功能,所以导致Git-Repo-Go仅仅是实现了Git-Repo的部分功能,这时不免怀疑这么折腾为啥不直接用Gerrit。
终极方案,如果项目依赖相对复杂,需要在项目级别进行代码评审,且要考虑依赖解析、循环依赖、依赖追踪等问题,那么以上工具方案都不用考虑了,它们都无法从根本上解决问题,所以Conan这个工具必须死磕下来,不管是头文件、静态库还是动态库的管理,Conan都能在一定程度上满足,虽然它本身具备一定的复杂性,但目前没有更好的路可以走了。
当然如果愿意快速解决问题,知名制品库厂商JFrog提供了Conan Center以及相关解决方案,我就不多打广告了。如果愿意折腾,Conan本身开源,且可以通过SonaType的开源制品库Nexsus实现,这也变相提供了另一种“低成本”的方案。
围绕企业对于嵌入式开发场景的诉求,极狐GitLab提供了一整套的解决方案,可以较好的解决嵌入式开发场景下的种种问题,重点是以下几部分内容。
企业进行代码统一管理的前提是代码管理系统需要具备高可用、高性能以及容灾等特性,才能支撑企业安全可靠的管理代码数据资产。
极狐GitLab专业版提供高可用部署方案,是松耦合分布式架构,各组件均为多副本部署,各组件均可实现横向扩展。极狐GitLab高可用架构通过横向扩展同时实现了高性能,针对从1k到50k的用户数场景提供了不同的参考架构,可以满足不同用户规模的企业。
极狐GitLab同时提供GEO主从架构部署方案,该方案为一主多从架构,主从节点配置不必完全一致,主节点提供读写服务,从节点提供只读服务,数据在主从节点之间实时同步,可实现:
当主节点出现故障后可在几分钟内将从节点切换为主节点,恢复服务。GEO可在实现准高可用的同时极大降低基础设施资源成本、部署和运维成本。
极狐GitLab通过群组、子群组嵌套关系实现对企业复杂组织关系的映射,嵌套最多支持20层,可以有效管理企业中的部门、组织、虚拟组织、项目。代码库隶属于群组或子群组,可在任意群组、子群组对仓库和人员权限进行管理。
对于需要总览整个群组的管理人员,可以将其在父群组上分配权限,该用户的权限将被继承到所有子群组和代码库中。对于需要查看部分内容的项目经理,可以将其在某个子群组上分配权限,该用户只可看到子群组下所有子子群组和代码库的数据。而对于只专注于具体功能模块的开发人员,只需要将其在具体的代码库上分配权限即可,这样就实现了在分仓模式下的授权管理。
前文提到,分支管理是Git的优势,分支策略也是体现Git高效协同的重要价值,此外分支策略也直接决定了研发流程的标准化。从事嵌入式开发的企业进行DevOps转型,首先要考虑的就是分支策略怎么建立。以下是常见的几种分支模型,可供参考。
但从极狐GitLab自己实践的角度出发,我更倾向两种分支策略:
需要注意的是,没有所谓的最佳分支策略,因为不同企业的研发流程是不一样的,即便是同一家企业的不同团队或者同一个团队在不同时期的研发流程也是不一样的,这时候就需要基于这些常见的分支策略总结和提炼一套属于自己的分支模型,并且对它持续进行检验和迭代。比如对于小型嵌入式项目,如果存在对不同的芯片或用户有定制版本,那么更建议基于GitHub Flow,加入Release分支来管理不同的交付版本。
当分支策略制定完成,如何保证开发人员遵循这套流程,那一定是需要工具层面有约束手段。极狐GitLab提供分支保护功能,可以限制开发人员直接向主分支提交代码,必须通过向feature分支或dev分支提交代码,再通过Merge Requests的方式合并到主分支,可以降低代码冲突,提高协同效率,同时也为开发团队践行代码评审提供了工具侧的落地支撑。
此外极狐GitLab专业版实现了更加精细化的分支保护管理,可以对于指定的用户设置推送和合并权限,以此保证分支代码的可审核和可追溯性,而免费版只能较粗粒度的指定某一类用户角色,如Developer、Maintainer。
代码规范也是DevOps转型非常关注的内容,以往粗放式的代码提交方式容易导致代码提交记录极度混乱、不可识别,比如大量1111
、test1
之类的提交记录不仅让协同人员无法接手、难以理解,甚至提交人自己也无法基于这些记录进行拉取或者回滚操作。
极狐GitLab专业版提供的推送规则功能可以很好的解决这个问题,它可以实现:
验证提交人是否是GitLab用户。
自定义正则表达式,验证代码提交记录是否符合一定的规范。
自定义正则表达式,验证分支名称是否符合一定的规范。
自定义正则表达式,验证提交文件中是否有不符合规范的文件,比如.zip、.tar文件。
验证提交的文件是否超过一定的大小。
推送规则验证不通过,则代码无法被推送到GitLab代码库中,从而确保研发人员严格按照规范进行代码提交。
规范代码提交信息可以配合Commitizen这款工具,它可以按照一些行业内通用的提交规范引导开发人员填写提交信息。也可以配合.gitignore
文件过滤一些不需要上传的文件或文件类型。它们都是在客户端发挥检查作用,本身不具备约束性质,开发人员可用可不用,而推送规则是在服务端进行验证,确保最终合规。所以可以说Commitizen、.gitignore
是源头检查,推送规则是尽头把关。两者可配合,但前者不可替代后者。
由于嵌入式开发的周期相对较长,交付频率相对较慢,交付物多是固件,不具备互联网纯软件、高速迭代、灵活升级的特性,所以处理问题的成本也比较高,这也使得近些年从事嵌入式开发的企业对软件质量的要求逐渐提高,而提高软件质量最常用的方式就是进行代码评审。
极狐GitLab专业版提供了完善的代码评审机制,其中主要包括:
其中Code Owner可以为代码库的不同目录、不同文件、不同文件后缀设置代码评审人,比如:
1 | fileA@张三 |
这就实现了对Git代码库的目录、文件进行写入控制,也解答了上文中遗留的问题。
上文也提到了数据保护,代码防泄漏的相关问题,具体落实到系统层面,极狐GitLab可以从事前、事中、事后三方个面提供支撑。其中最重要的是对GitLab的操作行为进行记录和分析,实现审计功能。
极狐GitLab专业版支持对创建仓库、修改密码、权限变更等系统事件进行审计,也支持对代码推拉事件进行审计,并可以事件流的方式传递给第三方日志系统,以便对数据进行分析和展示,也可以制定一些规则并触发告警或通知。
此外,在与第三方DLP工具对接的过程中,我们发现有些客户的DLP工具只能对Git客户端或IDE进行加密,而GitLab本身还可以通过在网页上直接打包下载的方式获取源码,这给客户的数据保护工作造成一些麻烦。所以极狐GitLab为这个本土化需求增加了一项功能,可以在网页上禁用源代码下载,以便更好的解决数据保护问题。
除了以上在源代码管理方面的一些优势外,极狐GitLab还提供了一些其他的附加功能。
比如在工具方面,极狐GitLab提供了IDE插件,可以更方便的对GitLab中的项目、代码、流水线进行管理。提供了WebIDE,可以在线对代码进行查看、对比、编辑操作。
在项目管理方面,极狐GitLab自身提供轻量的偏敏捷的项目管理功能,且可以和主流的项目管理工具如Jira、禅道、PingCode以及和传统制造行业常用的PLM系统Windchill进行集成和打通,实现在需求管理系统中可以查看该需求任务关联了哪些GitLab的代码提交和合并请求,实现项目到开发的流程关联与追溯。
流水线是DevOps中的重要组成部分,也是研发效率提升的根本,极狐GitLab提供了开箱即用的CI/CD功能,相较于Jenkins,GitLab CI依靠其一体化、轻量化、声明式、开箱即用的特性,在开发者群体中的使用率越来越高,在国内企业中仅次于Jenkins排在第二位。也为从事嵌入式开发的企业做完源代码统一管理后,进行更进一步的DevOps转型提供支撑和帮助。
最后,随着物联网、人工智能等技术的不断发展,嵌入式系统的应用场景将会更加广泛,这将会催生更多的DevOps应用场景,也会进一步推动DevOps在嵌入式开发场景中的落地,期待那一天早点来到。
DAG Pipelines 全称是Directed Acyclic Graph Pipelines,即有向无环图流水线,官方的定义和介绍如下:
有向无环图 可以在 CI/CD 流水线的上下文中,用于在作业之间建立关系,以便以最快的方式执行,无论阶段如何设置。
例如,您可能拥有作为主要项目的一部分而构建的特定工具或单独的网站。使用 DAG,您可以指定这些作业之间的关系,系统会尽快执行作业,而不是等待每个阶段完成。
并且附上了一个不明觉厉的图:
相信这段介绍已经击败了95%的初学者,那DAG流水线到底是什么,它用在什么场景解决什么样的问题?
DAG流水线解决一个数学题。
主要功能:
- 消除木桶效应,降低构建时间,提高构建效率。
- 对流水线Job进行编排。
这段介绍相对比较简洁了,但要理解DAG流水线,还需要展开来看看这个数学题是什么,以及DAG是怎么解决问题的。
展开这个问题前,有些基础概念比如Runner、Stage、Job就不再复述了,如果对这些概念不了解,因该先去学习GitLab CI的入门知识,可以参考:
问题1-1:
假设有一个跨平台项目,它通过GitLab CI分别完成Android、iOS、PC三个平台的构建、测试和打包。流水线的Stage和Job如下所示,Job中标识了该Job执行所需的时间。忽略所有Job的启动时间,问PC平台打包需多长时间?Android平台打包需多长时间?
需要注意,GitLab CI中,默认每个Stage中的所有Job都执行完成才能执行下一个Stage。即build
需要等这个Stage中用时最久的Job即build_ios
执行完成后才能执行test
,也就是需要60s。
所以:
这就是所谓的“木桶效应”,理论上PC平台的打包与iOS和Android平台没有关系,但却要等待它们的相关Job执行,被严重拖了后腿。
为了解决这个问题,就可以使用DAG流水线。它的原理和使用方式非常简单,通过给Job加上needs
关键字,将Job的依赖关系进行编排,比如:
1 | build_pc_dll |
这样PC平台打包就仅与PC平台的构建和测试Job相关,与其他Job无关了,也不需要等待其他Job执行。当然,这个例子为了更丰富的体现DAG流水线的特性,又增加了一个build_pc_dll
Job,并且让test_pc
同时依赖build_pc
和build_pc_dll
。
问题1-2:
使用DAG流水线后,PC平台打包需多长时间?Android平台打包需多长时间?
解答:
可以看到不论是各平台最终Job的用时还是流水线的总用时都降低了,这也就是为什么说DAG流水线是解决一个数学题,以及它是如何消除木桶效应、降低构建时间、提高构建效率以及如何实现对流水线Job进行编排的。
最后,我们可以在极狐GitLab的”CI/CD——流水线”,选择指定的流水线,然后点击”依赖关系图“,就可以看到上文中这张不明觉厉的图了。这时候相信大家也能更好的理解这张图,更好的理解DAG流水线了。
总结一下DAG流水线的使用场景:
- 流水线中有多个并行的业务逻辑:比如Monorepo(一个代码仓库中有多个模块/包)中多个模块同时构建、测试、打包,或类似上文中的跨平台编译打包,这些业务彼此之间相对独立。可以使用DAG流水线降低构建时间,提高构建效率。
- 流水线Job有依赖关系:比如Monorepo中构建模块C需要模块A和模块B的构建产物,可以使用DAG流水线的
needs
关键字对这些Job进行编排。
Parent-Child Pipelines 即父子流水线,它和第三章的Multi-Project Pipelines 多项目流水线都属于下游流水线。所谓下游流水线:
是由另一个流水线触发的任何极狐GitLab CI/CD 流水线。下游流水线可以是:
- 一个父子流水线,它是与第一个流水线在同一个项目(代码库)中触发的下游流水线。
- 多项目流水线,它是在与第一个流水线不同的项目(代码库)中触发的下游流水线。
父子流水线,官方的定义和介绍如下:
父流水线是在同一项目(代码库)中触发下游流水线的流水线。 下游流水线称为子流水线。
- 子流水线仍然根据阶段顺序执行他们的每个工作,但可以自由地继续他们的阶段,而无需等待父流水线中不相关的工作完成。
- 该配置被拆分为更小的子流水线配置。每个子流水线只包含更容易理解的相关步骤,减少了理解整体配置的认知负担。
- 导入在子流水线级别完成,减少了冲突的可能性。
这个解释比DAG流水线要容易理解一些,但是我们依然可以换一种比较接地气的方式进行重新描述。
父子流水线解决一个判断题+选择题。
主要功能:
- (按条件触发并)执行同一个项目(代码库)中不同的流水线脚本。
接着问题1继续,还是那个跨平台项目。
问题2:
假如现在iOS平台应用有一些Bug,开发人员仅对iOS部分代码进行了修改,然后希望编译打包iOS平台应用并发布上线。但不希望再次打包PC和Android平台,避免浪费时间和资源,怎么办?假如是个通用问题在3个平台上都出现了,那么修改通用部分代码后又需要同时打包3个平台的应用,又该怎么办?这个跨平台项目文件目录如下:
1 | - common |
为了解决这个问题,就需要使用到父子流水线,主要会使用到rules:is:changes
和trigger
关键字,用来实现按条件触发,然后执行不同的流水线脚本,比如:
1 | stages: |
当修改ios目录文件后,只触发了ios/.gitlab-ci.yml
脚本的执行:
当修改common目录文件或直接手动执行流水线后,触发执行根目录的.gitlab-ci.yml
脚本,也就是触发所有构建。
当然这个判断题不是必要的,可以在一个正常的Job中直接做选择题,比如:
1 | microservice_a: |
也可以修改判断题的条件,比如使用GitLab的变量来进行条件控制:
1 | pc_trigger: |
这样就实现了按照条件触发不同的流水线脚本,这也就是说为什么父子流水线是解决一个判断题+选择题,以及他是如何(按条件触发并)执行同一个项目中不同的流水线脚本的。
总结一下父子流水线的使用场景:
- 按条件灵活触发并执行一个项目中不同的流水线脚本:比如在一个项目中,将一个复杂的流水线脚本拆分成多个简单的流水线脚本,通过
tigger
关键字组合,实现解耦和降低复杂度。或类似上文中提到的按条件单独执行Monorepo中部分模块的构建、测试、打包。- 父子流水线+DAG流水线:可以将父子流水线与DAG流水线结合使用,比如pc/.gitlab-ci.yml中依然使用DAG流水线使得
test_pc
依赖build_pc
和build_pc_dll
。
Multi-Project Pipelines 多项目流水线,它和第二章的父子流水线都属于下游流水线,官方的定义和介绍如下:
可以跨多个项目(代码库)设置极狐GitLab CI/CD,以便一个项目(代码库)中的流水线可以触发另一个项目(代码库)中的流水线。您可以在一个地方可视化整个流水线,包括所有跨项目的相互依赖关系。
熟悉了父子流水线后,再看多项目流水线就比较简单了。它们都是触发下游不同的流水线,只是面向的对象不同,父子流水线面向的是同一个项目(代码库),而多项目流水线是面向不同的项目(代码库),这也决定了它们使用的场景不同。
继续用通俗的语言来解释。
多项目流水线解决的是排列组合题。
主要功能:
- 编排并执行不同的项目(代码库)中的流水线脚本。
回顾上文中的DAG流水线和父子流水线,使用的场景大多都是在Monorepo模式下,对一个项目内的流水线或者Job进行编排。而现在架构设计领域的主流思想还是模块化和微服务,所以不少企业或开发人员还是习惯对项目进行拆分,用多个代码库进行管理。在这样的模式下,DAG和父子流水线使用的机会就相对较少了,而多项目流水线就派上了用场。举例如下:
问题3:
假设有个Web项目,在GitLab中建立了一个群组MyProject
来管理这个项目。前端代码放在代码库MyProject/Frontend
中,后台代码放在代码库MyProject/Server
中。测试团队对前端代码编写的UI自动化测试脚本放在代码库MyProject/Frontend-UI-Testing
中,对后台代码编写的API自动化测试脚本放在代码库MyProject/Server-API-Testing
中。要求部署时先部署后台代码,再部署前端代码,并同步进行后台的API测试,最后再进行前端的UI测试。
使用多项目流水线来解决这个问题,依然要使用trigger
关键字,由于该问题中,后台代码的流水线是整个业务链条的起点,所以先看代码库MyProject/Server
的流水线:
1 | stages: |
当前端项目部署成功后需要执行前端的UI测试,所以代码库MyProject/Frontend
的流水线如下:
1 | stages: |
流水线运行的效果如下,也就是官方定义中所说的”您可以在一个地方可视化整个流水线,包括所有跨项目的相互依赖关系”,但这个功能是属于极狐GitLab专业版及以上版本,免费版无法看到这个效果。
正因为多项目流水线能够编排多个项目(代码库)流水线,所以说它解决的是一个排列组合题。
总结一下多项目流水线的使用场景:
- 按顺序触发并执行不同项目的流水线脚本:比如部署后运行自动化测试,或按照一定的顺序部署不同的模块、服务等。
Merge Trains 即合并队列或者叫合并列车,我记得当初可能得花了2、3天才彻底弄明白这东西到底是干嘛的,先看看官方的定义:
使用合并队列对合并请求进行排队,并在将它们合并到目标分支之前验证它们的更改是否可以协同工作。
在频繁合并到默认分支的项目中,不同合并请求的更改可能会相互冲突。合并结果流水线确保更改适用于默认分支中的内容,但不适用于其他人同时合并的内容。
懵没懵?GitLab Inc甚至写了一整篇Blog来介绍Merge Trains以及Merge Trains的工作流,详见:《How starting merge trains improve efficiency for DevOps》,内容很丰富,但是我真的没看懂。
经过一番折腾,我发现要想理解Merge Trains,得先了解它的前世今生。
熟悉GitLab CI的朋友一定知道在GitLab的合并请求(MR)中是可以看到与这个MR相关的流水线的运行情况,如下图所示,共有两部分流水线,其中:
test
有代码提交就会运行流水线,也就是流水线运行在源分支上。main
上运行流水线。这个逻辑是说,当发起一个MR时,假设从test
分支合并到main
分支,那么GitLab首先会在test
分支下跑流水线,只有当test
分支的流水线跑成功时才说明至少test分支的代码是跑的通的,也意味着可以合并到main
分支。如果test
分支的流水线都跑不通,那么合并到main
分支后会导致main
分支的代码也无法正常执行,这就失去了多分支协同开发的意义。
当test
分支被成功合并到main
分支后,GitLab会在main
分支下再跑一次流水线,用来验证合并后的代码是否能够跑通流水线,或者直接执行部署任务。
基于这个逻辑,在合并请求的基础上,GitLab CI又延伸出3种用法。
回到上面那张图,假设这个项目的流水线脚本是:
1 | stages: |
那么如果在这个项目中发起一个MR,从test
分支合并到main
分支,首先会在test
分支下运行上面的流水线。
但假设开发人员仅仅想在test
分支下运行build和test阶段的任务,不希望执行deploy阶段,这时候就需要用到if
或only
关键字,比如:
1 | stages: |
这样设置之后,当开发人员向test
分支提交代码时,如果没有基于test
分支的MR,那么流水线脚本中的所有任务都会执行;如果有基于test
分支的MR,那么只在test
分支下执行流水线脚本中的build、test阶段的任务,不会执行deploy的任务。并且在MR中,GitLab会标识出来源分支的流水线是”合并请求流水线”。
所以:
当一条流水线中的某些Job仅在合并请求MR中运行时,则该流水线称为**合并请求流水线**。
接着上文的逻辑继续,从test
分支合并到main
分支,如果test
分支的合并请求流水线跑通了,那只能说明test
分支的代码可能没问题,并不能说明合并到main
分支后的代码或者流水线没问题。
因为基于多分支的开发是同步进行的,假如有人已经向main
分支提交了一些修改,虽然代码上可能没冲突,但运行逻辑上可能会产生一些影响。这时候可能会出现MR被执行合并后,目标分支流水线跑不通,需要进行回退或调试,从而影响其他人的情况。
很显然我们不希望这样的情况产生,所以GitLab为了解决这个问题,提供了“合并结果流水线”功能,可在项目中开启。需要注意的是这个功能属于极狐GitLab专业版及以上版本功能,免费版不提供该功能。
当开启“合并结果流水线”时,GitLab会在源分支的流水线任务中,本地模拟将源分支合并到目标分支(不会影响到服务端),然后再运行流水线,这样就能一定程度上实现“预测未来”的效果,从而避免或降低合并后流水线跑不通的情况。并且在MR中,GitLab会标识出来源分支的流水线是”合并结果流水线”。
所以:
在合并请求MR中,模拟将源分支合并到目标分支,然后再运行流水线,称为**合并结果流水线**。
书接上回,虽然合并结果流水线实现了“预测未来”,但这个预测是短暂的。因为即便合并结果流水线运行成功,还需要有权限的用户执行合并动作,如果忘记执行合并或者拖了很久的时间才执行合并,这中间就又产生时间差了,预测也就不准了。所以GitLab祭出了大招,就是Merge Trains合并列车。
问题4:
假设现在有3个开发人员分别在feature1
、feature2
、feature3
分支下进行开发,分别提交了合并请求MR1、MR2、MR3,彼此之间可能有代码冲突或潜在的功能影响,若在相近或同一时间内进行合并,如何高效率进行合并并尽可能的避免合并后的冲突以及流水线失败。
其实这就是系统架构中常见的高并发问题,只不过在DevOps中,如果进行协同开发的人比较多、MR的数量较多、流水线运行的频率较快也会出现类似的问题。而合并列车就像一个消息队列,开发人员就是生产者,消息就是合并请求MR,合并列车将并发生产的MR收集起来进行排队,然后转成串行任务并自动进行消费(合并),无法消费的任务就踢出,从而实现高效率合并并降低冲突和失败的概率。如下图所示,是合并请求流水线、合并结果流水线、合并列车的运行逻辑视图,也是它们之间的区别,更是合并列车的演进历程。
合并列车是基于合并结果流水线的,也是极狐GitLab专业版及以上版本的功能,也需要在项目中开启。
所以:
将多个MR进行排队,逐个运行合并结果流水线,运行通过就自动合并,运行不通过就踢出队列,这样的流水线称为合并列车。
最后用一张图对比MR中的三种流水线,需要说明的是合并结果流水线在实践中用到的更多,毕竟大部分企业和研发团队的协同效率和要求不会达到那么高,DevOps的建设也可以遵循架构设计的三原则:简单、适合、演进。
WIKI百科对于UI测试的解释是:
图形用户界面测试是测试产品的图形用户界面(GUI)以确保其符合其规格的过程。这通常是通过使用各种测试用例来完成的。
按照UI测试的目的,可分为逻辑测试和视觉测试:
通常来说,通过自动化手段做UI测试,应更多使用在逻辑测试的场景里,而视觉测试更多依靠人工来处理。
按照应用程序部署的平台不同,一般分为基于手机等移动设备的APP、基于B/S架构的Web应用、基于C/S架构的桌面应用(包括运行在硬件终端上的程序,如ATM机、自动购物机等)。
此外WIKI百科还对UI测试的难点进行了描述:
为了生成一组测试用例,测试设计人员尝试涵盖系统的所有功能并完全执行GUI本身。完成此任务的难度有两个:处理域大小和序列。
与 CLI(命令行界面)系统不同,GUI 可能具有需要测试的其他操作。一个相对较小的程序,如微软写字板有325个可能的GUI操作。在大型程序中,操作数量可以很容易地大一个数量级。
第二个问题是排序问题。系统的某些功能只能通过一系列 GUI 事件来完成。例如,要打开文件,用户可能首先必须单击“文件菜单”,然后选择“打开”操作,使用对话框指定文件名,并将应用程序集中在新打开的窗口上。增加可能的操作数量会使排序问题呈指数级增长。当测试人员手动创建测试用例时,这可能会成为一个严重的问题。
此外,测试人员在必须进行回归测试时面临更多困难。
为解决这些问题,UI自动化测试成为了行业内的大趋势,因为自动化测试的优势就是代替频繁的重复性操作,适合解决在大量测试用例下进行回归测试的难题。
虽然UI自动化测试有着其不可替代的优势,但在企业内优先开展UI自动化测试仍是不推荐的。《Selenium 2自动化测试实战》一书中介绍了什么样的项目适合自动化测试,列出了10个条件:
可以看到每一个条件或是凸显UI自动化测试的优势,如频繁回归、大量重复任务等;或是避免UI自动化测试的劣势,如项目相对稳定、需求变动较少、测试人员具备一定的编程能力等。其目的都是为了获得较高的投入产出比,这也是测试金字塔所表达的核心思想。
需要说明的是这10个条件和开展UI自动化测试不是充分必要关系,只是用来参考和评估,毕竟每个企业、每个团队对ROI的要求也是不一样的。
另外按照个人经验,这10个条件里最主要的是需求和界面不能够频繁变动。从时机来看,频繁变动不利于自动化测试发挥替代重复工作的优势;从环境来看,频繁变动不具备开展其他工作的土壤;从人员来看,频繁变动会影响开发和测试人员的心态。属于天时地利人和三不靠,在这样的项目里开展UI自动化测试有较高的风险和失败的几率。
正如前文所说,因为一个应用程序的UI操作逻辑往往很复杂,并且有多重排列组合方式,所以UI测试的用例数量会非常庞大,即使自动化工具可以替代重复操作,但初期编写测试用例还是需要依靠测试人员,如何降低这个工作量?
个人经验是不需要过度追求UI测试用例的高覆盖率,应该把更多精力投入在单元测试、接口测试上,而在UI测试方面,覆盖常规的、主要的业务流程即可。参考28原则,把核心的、相对稳定的主流程通过自动化手段实现,那么相当于做了自动化的冒烟测试。
相较于自动化测试的优势,它的劣势同样明显,工具是人写的,所以自动化测试不适合发现新的Bug。这时候就需要手动测试互补,通过手动测试进行探索性测试,并且把手动测试发现的一些问题编写为测试用例并集成到自动化测试脚本中,不断丰富自动化测试脚本。所以自动+手动,回归+探索的组合式测试方案是行业的主流做法。
UI自动化测试主要用来做回归测试,所以他的运行时机主要有三种:
UI自动化测试的运行模式有两种:
以Python开发的Selenium测试脚本为例,以下代码实现了基于Chrome的Headless模式运行Selenium,包含两个测试用例,分别是模拟点击https://www.oursky.com/
网站的header__logo
和header__cta
元素,找不到该元素时测试用例不通过。
1 | """ |
将以上测试脚本放在极狐GitLab代码仓上进行管理,并创建GitLab CI/CD脚本,运行该脚本需Docker/K8S类型的GitLab Runner,参考示例.gitlab-ci.yaml
如下:
1 | stages: |
按照UI自动化测试的运行时机,对应三种不同的情况:
当应用程序代码变更后触发测试:可在上游应用程序的CI/CD脚本中增加selenium测试项目为下游流水线,当上游应用程序代码变并更运行流水线时,会在部署完成后自动触发selenium测试项目的流水线,以执行自动化测试,从而验证应用程序本次代码变更是否可以通过回归测试。
1 | stages: |
定时执行测试:可在项目的“CI/CD——计划”中创建定时任务以执行自动化测试。
手动执行测试:可在项目的“CI/CD——流水线”中点“运行流水线”按钮触发流水线构建。
在上述配置中,已将Selenium的测试报告以HTML格式导出,上传到GitLab制品仓,并通过GitLab Pages展示,可在该项目“设置—Pages”中找到URL链接并访问。
在Squish中进行UI自动化测试就不能使用Headless模式了,需要专机专用,整体流程如下图所示:
所以需要在测试机上安装Squish,用于运行测试脚本。同时需要在测试机上安装并注册GitLab Runner,具体可参考Install GitLab Runner | GitLab , 支持x86, AMD64, ARM64, ARM, s390x, ppc64le等架构、支持Linux, Windows, macOS, FreeBSD等操作系统,支持CentOS, Debian, Ubuntu, RHEL, Fedora, Mint等Linux发行版。
注册Runner时需要给Runner设置一个Tag,用于区分,如Squish for X
。
需要记录测试机的Squish安装路径、许可证路径、Squish Server的端口,后续配置流水线脚本时使用。
以使用Squish for QT测试QT桌面端程序为例,安装Squish、录制脚本、编写脚本,可参考Squish官方文档froglogic Squish Manual 。
编写测试用例,简单来说分3步:
最后将整个测试项目上传到GitLab进行保存和版本控制。
基于上述测试项目,创建流水线脚本,参考示例.gitlab-ci.yaml
如下:
1 | variables: |
运行流水线,观察是否可以正常运行,需注意运行自动化测试期间不要在测试机上进行其他操作,以免引起测试失败。
在上述配置中,同时生成了XML和HTML格式的测试报告,并且把JUnit格式XML与GitLab的集成,实现在合并请求中查看Squish的测试结果,实现方式可参考 Unit test report examples | GitLab 。
同时测试报告也以HTML格式导出,上传到GitLab制品仓,并通过GitLab Pages展示,可在该项目“设置—Pages”中找到URL链接并访问。
回顾之前的4篇文章,从测试的发展史、方法论、工具到具体的接口测试、性能测试、单元测试再到这篇UI测试,也仅仅是概括性的介绍了软件测试很小的一部分内容。软件测试在软件开发过程中依然是不可或缺的重要组成部分,它的实践落地依然是要结合企业实际的环境、时机、人员能力去综合考虑,在摸索中前进。
而对于极狐GitLab来说,主要是通过两方面将软件测试更好的融入DevOps体系:
关于极狐GitLab与自动化测试实践的相关内容到这里就全部结束了,完结撒花。
WIKI百科对于单元测试的解释是:
单元测试又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
熟悉敏捷开发,尤其是XP极限编程的小伙伴对单元测试应该不陌生,XP是TDD(测试驱动开发),这里面的测试就是指单元测试。
狭义上来看,单元测试主要测的是一个函数、方法的功能是否正常可用。一般要通过多个测试用例,覆盖这个函数、方法的所有或主要变量、条件、路径,验证这个函数、方法的输入输出是否符合预期。
单元测试属于白盒测试,也就是根据已知的代码来编写测试用例,所以在实际应用中,推荐开发人员自己来做单元测试。虽然开发人员一般缺少测试思维,而且自己的问题很难自己发现,但相比让测试人员去熟悉开发代码,并具备一定的开发能力,可行性上会更高。
此外,单元测试的代码一般会伴随开发代码一起递交至代码库,所以结合CI/CD可以实现自动化单元测试,并作为代码合并请求(PR、MR)、代码评审(Code Review)的依据。
在某种意义上来讲,所有的开发人员已经在做“单元测试”了。比如通过Postman等API测试工具,或在最终的应用程序UI进行操作,目的是为了作为调试某个函数、方法的入口。也有在程序里某个地方临时写一些方法或者变量,来进行自测。这种马甲程序的目的是为了测试函数、方法的功能正确性,但是缺少规范性,效率低下,且容易引起混乱。
关于要不要做单元测试,一直都有两种声音:
类 别 | 支持 | 反对 |
---|---|---|
时间与效率 | 单元测试有助于提高Bug处理效率。下图来自微软的数据统计,在单元测试中发现的Bug平均处理时间为3.25小时,在系统测试中发现的Bug平均处理时间为11.5小时 | 开发人员已经很忙了,单元测试可能会占据开发人员20~40%的工作时间 |
作用 | 单元测试与系统测试是互补而非代替关系,单元测试注重“独立性”,系统测试注重“相关性” | 单元测试仅仅证明了这些函数做了什么,并不能保证系统功能正确,还是得交给后面的系统或集成测试 |
意义 | 单元测试对产品质量非常重要,它是软件测试中,最底层的一类测试 | 简单的程序没必要写单元测试,复杂的程序写了单元测试也覆盖不全,单纯是忽悠领导 |
文化思想 | 很多西方企业都写单元测试,对于开发人员来说这是理所应当的事 | 对于国内企业来说,测试工作基本上都是交给测试团队,而且整体更关注功能 |
所以到底要不要做单元测试?又要祭出小学课文《小马过河》了,人云亦云和纸上谈兵终究没有任何意义,觉得有必要就可以先进性尝试,买不了吃亏买不了上当。最终作出决定无非是看它产生的价值和付出的代价是否符合预期或者能够承受。这个没有绝对的标准:
做单元测试不容易,如果决定要去做单元测试,如何去评估这项工作的效果,一般分两个方面的指标:
直接指标:
间接指标:
虽然不能肯定,但目前任何有关效能评估和度量的指标,对于国内的企业来说,大部分是用来量化考核员工的工具,而非真正为了提升整体效率,对国内的开发者来说都会被认为是卷。这也是单元测试以及企业效能评估很难开展或难见成效的原因。这也需要国内企业和员工很长时间的磨合,也需要文化、流程、法规的不断建立和完善。
以Golang开发的项目为例,Golang官方使用go test
命令进行单元测试,推荐测试文件和源代码文件放在一块,测试文件以 _test.go
结尾,如:
1 | project/ |
以main.go
为例,实现了加减乘除四个方法:
1 | package main |
main_test.go
需要对main.go
的四个方法进行测试用例的编写,测试用例的名称一般命名为Test
加上待测试的方法名,如TestAdd
:
1 | package main |
运行go test -v -cover
命令即可运行单元测试,并输出通过率和覆盖率。
1 | go test -v -cover |
基于上面的例子,结合极狐GitLab CI/CD,就可以实现自动化单元测试:
需Docker/K8S类型的GitLab Runner,详见:GitLab Runner Executors | GitLab。参考示例.gitlab-ci.yaml
如下:
1 | stages: |
但测试报告和详细信息需要点进Job看,相对比较麻烦。即使可以通过邮件或者钉钉、企业微信的Webhook发送测试报告,但这与合并请求(Merge Requests)和代码评审(Code Review)割裂了。在实际应用中,我们更希望在代码评审时直接看到一个汇总的单元测试报告,不需要去别的地方翻数据。
基于GitLab提供的单元测试报告和可视化的功能,稍作修改:
1 | # 单元测试 |
这里主要实现了3方面的功能,其中:
通过率报告:
其原理是将GitLab CI/CD中的单元测试报告导出为JUnit的XML格式,并以制品(Artifacts)的方式上传到GitLab,GitLab就可以进行解析,然后汇总和展示,详见:Unit test reports | GitLab
通过率报告可在CI/CD流水线的“测试”页面中查看,如下图所示
通过率报告可在合并请求中查看,做代码评审时无需进入CI/CD Job中查看日志,提高代码评审效率
覆盖率可视化:
其原理是将GitLab CI/CD中的覆盖率报告导出为Cobertura XML格式,并以制品(Artifacts)的方式上传到GitLab,GitLab就可以进行解析,然后汇总和展示,详见:Test coverage visualization | GitLab
覆盖率可视化主要在合并请求中体现。在“变更”页面,对于覆盖到单元测试的方法,以绿色色块进行标识。对于未覆盖到单元测试的方法,以橙色色块进行标识。当鼠标移到色块上时,显示命中次数,即对该方法进行了几个测试用例的单元测试。在代码评审时,可以较直观的反应单元测试覆盖的情况。
覆盖率报告:
其原理是将GitLab CI/CD Job日志中的覆盖率信息以正则表达式的方法抠取出来,然后在合并请求页面进行展示,详见:Coverage in .gitlab-ci.yml file | GitLab
覆盖率报告可在合并请求中查看,并可查看增量代码是否会引起单元测试覆盖率的下降,从而提高代码评审效率
就呈现效果来看,将单元测试的结果汇总并展示,便于辅助代码评审,这个整体的目的是基本达到了。但还有一些待优化的功能点,如:
对于前者,GitLab可以查看项目级(免费版)或群组级(专业版)的单元测试历史覆盖率,详见:View code coverage history | GitLab。而综合其他指标进行评估,目前看来只能通过API的方式获取,然后自己做一些二次开发。毕竟每家企业分析的维度可能不同,数据也不一定都在GitLab上。
对于后者,极狐GitLab专业版提供了“合并请求批准”功能,可将单元测试覆盖率作为合并请求的门禁。启用该功能后,若在合并请求中出现了覆盖率降低的情况,会强制审批人对覆盖率进行审核确认,得到批准后才可以进行合并,否则无法直接进行代码的合并。详见:Coverage check approval rule | GitLab
WIKI百科对于性能测试的解释是:
性能测试用于评估和确定系统在特定工作量下的响应和稳定方面的性能。可以用于调查、测量、验证系统的其他质量属性,例如可扩展性,可靠性和资源使用情况。
性能测试按照测试目的不同,可分成不同类型,常见的类型有:
性能测试是一种非功能软件测试技术,是确保软件质量的关键。但大多数情况下,它被视为单独的事后步骤,并且在功能测试完成之后才进行,甚至在代码发布之后才进行。
企业一般出于以下原因之一运行性能测试:
性能测试的目的包括评估应用系统吞吐量、处理速度、数据传输速度、网络带宽使用、最大并发用户、内存利用率、工作负载效率和命令响应时间等。
某种意义上讲,性能测试不像接口测试、单元测试那么“刚需”。但当企业有上面提到的这些性能测试的需求,或者将系统性能也作为产品本身的重要评估指标,那就需要开展性能测试。性能测试的门槛相对较低,一般是基于接口测试编排。如上一篇文章《极狐GitLab自动化测试指南02——接口测试》中提到的Postman、Apifox也可以进行性能测试。所以我将性能测试作为该系列文章的第三篇,跟随接口测试,趁热打铁。
从流程层面来说,微软在《Performance Testing Guidance for Web Applications》中列出了Web应用性能测试的7个核心活动:
从工具层面来说,性能测试工具主要分为两个类别,目前市面上大多数性能测试工具已经是这两类工具的集合体:
从实践层面来说,有以下几个注意事项:
浏览器性能测试是极狐GitLab专业版提供的功能,主要是用于测试Web系统的性能,比如页面加载速度,渲染时间等。可以帮助测试人员找出哪些页面、JS、图片素材加载缓慢,这些问题都会直接影响访问者的用户体验。
极狐GitLab的浏览器性能测试是基于集成了sitespeed.io这款网站性能测试工具做了封装,无需编写测试脚本,所以在使用方面非常简单。
运行极狐GitLab Browser Performance,需要Docker或者Kubernetes的GitLab Runner,详见:GitLab Runner Executors | GitLab
此外还需进行设置,让Runner支持Docker-in-Docker,详见:Use Docker-in-Docker
浏览器性能测试可根据实际情况参考以下两种方式进行创建和管理:
独立管理
测试和开发项目分开管理,一般是由测试人员为浏览器性能测试创建一个独立的代码仓,这种方式适合在有需要的时候对指定的网站进行浏览器性能测试。.gitlab-ci.yaml
内容如下:
1 | # docker-in-docker |
统一管理
测试和开发项目统一管理,一般是由开发人员在当前项目的CI/CD设置中加上浏览器性能测试,可以使用极狐GitLab的环境变量结合rules
来控制测试的执行策略。.gitlab-ci.yaml
内容如下:
1 | browser_performance: |
这样就可以实现平时正常的构建不触发浏览器性能测试任务,当需要执行测试任务时,手动触发流水线任务,给PERFORMANCE_TEST
环境变量设置一个值,就可以只在这次构建中执行测试任务。
成功运行流水线后,可下载作业(Job)的制品(Artifacts),或配合极狐GitLab Pages功能,以HTML格式查看测试报告:
此外可以在合并请求(Merge Requests)页面展示源分支和目标分支的浏览器性能测试对比报告,这个功能有助于代码评审人员对代码进行综合评估。
值得一提的是这个对比报告基于一个固定格式的JSON文件,只需要在CI/CD中将这个格式的文件以制品(Artifacts)进行上传,就可以在MR中展示这个对比报告。如果有对极狐GitLab CI/CD功能比较熟悉的读者,相信已经知道可以利用这个功能实现和其他浏览器性能测试工具的集成。
极狐GitLab专业版也集成了Grafana Lab的性能测试工具K6,K6使用Golang开发,使用JS作为脚本语言,支持冒烟测试、负载测试、压力测试、长跑测试,是一款比较轻量的性能测试工具。但是它没有JMeter那种GUI,对于没有编码基础的测试人员,上手稍微有点难度,不过好在官方文档丰富,走得是简洁的Geek风。
使用集成了K6的极狐GitLab Load Performance功能,同样需要Docker或者Kubernetes的GitLab Runner,也需要支持Docker-in-Docker,这部分内容可以参考Browser Performance章节的内容。
基于K6做性能测试,还是需要在本地进行测试脚本的编写和调试,关于K6的安装可以参考K6的官方文档,这里以Docker为例:
1 | docker pull grafana/k6 |
拉取官方最新的镜像,就可以进行脚本编写了。
沿用上一篇文章《极狐GitLab自动化测试指南02——接口测试》的示例,假如我们要对“唱片管理系统”进行负载测试。目的是测试“发布唱片”这个功能在1000个用户,总共10000次请求下的性能情况。那么参考脚本script.js
如下:
1 | import http from 'k6/http'; |
大致内容有3部分:
脚本编写完成后,可以在本地进行调试,这里以Docker下的K6为例:
1 | docker run -it -v ~/workspace/performance-test-k6:/test -w /test grafana/k6 run --vus 1000 --iterations 10000 script.js |
通过vus
参数设置虚拟用户,通过iterations
参数设置请求次数,运行测试,观察结果是否正确。
最后测试脚本可以参考Browser Performance章节“创建测试项目”部分选择不同的方式放在极狐GitLab代码仓上进行管理。在极狐GitLab CI/CD中使用K6的.gitlab-ci.yaml
内容如下:
1 | # docker-in-docker |
与浏览器性能测试一样,可以在合并请求(Merge Requests)页面展示源分支和目标分支的负载性能测试对比报告。
性能测试最“出名”的两款工具是LoadRunner和JMeter,其中JMeter是Apache组织基于Java开发的性能测试工具,而且是开源免费工具,在国内开发者中使用的比例最高。JMeter除了有GUI便于测试人员上手外,功能也非常强大,网络上相关的教程、实践文章也比较多,这里仅展示JMeter的基础功能并实现和极狐GitLab的集成。
JMeter的下载地址可在官网上找到,因为JMeter是基于Java 8开发,所以需要JDK/JRE环境。下载解压后,打开bin\jmeter
即可看到软件界面。
与上文的示例场景一样,同样基于“唱片管理系统”进行负载测试。需要按照以下步骤完成JMeter的测试脚本编写:
修改测试计划
名称,如“图书管理系统负载测试”。
右键测试计划
,“添加——线程(用户)——tearDown线程组”,线程数
为1000,即代表1000个用户,循环次数
为10,即代表每个用户循环执行10次,共计执行10000次。类似K6的vus
、iterations
参数。
右键线程组
,“添加——配置元件——计数器”,内容如下,用于作为测试数据的唯一ID。其中Starting Value
是起始值,这里从4开始。引用名称
是这个计数器作为环境变量使用的名称,使用环境变量的格式是${引用名称}
。
右键线程组
,“添加——取样器——HTTP请求”,填写名称
、协议
、服务器名称或IP
、端口号
、HTTP请求
、路径
、内容编码
、消息体数据
,用于访问待测试的API。其中消息体数据
用到了上一步创建的计数器
,并且使用了${__intSum}
这个JMeter的内置函数,实现了两数相加。
右键线程组
,“添加——断言——响应断言/JSON断言”,下图展示的是JSON断言
,填写Assert JSON Path exists
和Expected Value
实现两者的对比。
右键线程组
,“添加——监听器——察看结果树/聚合报告”,用于查看测试结果。
菜单栏“运行——启动”,开启性能测试,在聚合报告
页面可以查看测试结果。需要说明的是在实际操作中,不建议在GUI下直接开启测试,这样会导致JMeter程序卡顿甚至卡死,官方建议在GUI模式下编写脚本和调试,在非GUI模式下进行测试。
菜单栏“工具——Generate HTML Report”可以导出HTML的测试报告。
将测试计划
保存为test.jmx
。
在非GUI模式下进行测试,并导出HTML报告,可使用以下命令:
1 | jmeter -n -t test.jmx -l result.jtl -e -o ./report |
其中-n
为非GUI模式,-l
为测试结果路径, -e
为生成HTML报告,-o
为导出HTML报告的路径。
同样可以把JMeter的测试计划test.jmx
放在极狐GitLab代码仓上进行管理。在极狐GitLab CI/CD中使用JMeter的.gitlab-ci.yaml
内容如下:
1 | stages: |
成功运行流水线后,可下载作业(Job)的制品(Artifacts),或配合极狐GitLab Pages功能,以HTML格式查看测试报告:
WIKI百科对于接口测试的解释是:
接口测试是软件测试的一种,它包括两种测试类型:狭义上指的是直接针对应用程序接口(下面使用缩写API指代,其中文简称为接口)的功能进行的测试;广义上指集成测试中,通过调用API测试整体的功能完成度、可靠性、安全性与性能等指标。
接口测试根据其测试目的不同,来决定测试人员和测试方法:
接口测试的大前提是要实现前后端分离,但国内目前还有很多企业是靠全干(栈)工程师写的前后端一体化的程序。倒不是说这种程序和系统不能做接口测试,而是因为这种一体化的程序前端后台都是同一个开发人员开发,往往功能耦合比较严重,修改起来牵一发而动全身。此外由于都是自己开发,一般也很少写接口文档,甚至不需要接口文档。所以意义和价值较低,实践起来也比较困难。
接口测试的意义和价值包括但不限于:
总体来说,接口测试门槛较低、投入较少、持续性收益较大,且容易实现自动化,所以推广和落地自动化测试,可以先以接口测试作为切入点。
接口的重要性体现在它是连接多个系统、组织、流程的节点,所以接口测试涉及的人、工具、流程较多,我仅根据个人经验给出一些参考建议,仅覆盖对某个接口的功能测试和对业务功能的集成测试。
人:
对于上述后台和前端开发人员的相关工作,无需赘述,基本上都是这么做的,属于分内之事。而对于测试人员来说,还是再强调一下人员配比和分工的问题。可参考以下方式,具体以实际工作和个人能力调整:
工具:
接口测试常见的工具如下:
整体来看,较多的工具会增加管理维护成本,此外不同工具由于数据割裂,也会导致重复工作,降低协同效率。比如接口在开发阶段比较容易发生变化,但接口与接口文档的不同步或者变更没有及时通知到相关人员,就会影响整个工作。再比如常见的工作流程是开发人员在接口文档工具中定义接口,在代码开发中需要再次定义接口,在调试阶段中需要用调试工具继续定义接口,最后到测试阶段测试人员还要再次定义。
所以近期也出现了一款比较火的API“测试”工具Apifox。官方介绍如下:
Apifox 是接口管理、开发、测试全流程集成工具,定位 Postman + Swagger + Mock + JMeter。通过一套系统、一份数据,解决多个系统之间的数据同步问题。只要定义好接口文档,接口调试、数据 Mock、接口测试就可以直接使用,无需再次定义;接口文档和接口开发调试使用同一个工具,接口调试完成后即可保证和接口文档定义完全一致。高效、及时、准确。
仅就接口测试工具本身来说,最常见的还是老牌工具Postman,也有不少公司会基于Python等开发语言编写自己的接口自动化测试框架。但对于接口测试起步阶段的企业来说,建议使用可视化的工具来降低门槛。
流程:
正如上文提到的,接口测试既可以围绕一个接口测试其边边界和功能, 也可以围绕多个接口的编排测试其业务功能。所以侧重点不同,工作的流程也不同。
对于起步阶段的企业或团队,可以只聚焦测试某个接口的边界和功能。而对于已经建立起接口自动化测试能力的企业或团队,可以将接口测试的范围扩大至业务功能测试甚至后面将要讲到的性能测试。以下工作流程仅供参考:
接口文档
接口文档
,定义主要的测试用例
测试用例
或 Mock数据
进行开发测试用例
对开发中的接口进行调试和自测,如接口有变更,需即时更新接口文档
并通知相关人员(前端开发、测试人员)测试用例
进行完善补充,包括边界条件,或形成一组接口调用流程以便测试业务功能测试环境
测试环境
执行测试用例
反馈测试结果
测试用例
与CI/CD集成,在开发人员发布到测试环境
后自动执行全量回归测试
,以确保接口功能无异常数据:
接口测试的本质还是测试接口输入、输出的数据是否准确,所以测试数据的准备和维护非常重要。假设要测试“注册账号”的接口,输入数据是:
1 | username: zhangsan |
那么这个数据在完成“注册账号”接口测试后,就无法再次测试“注册账号”接口了,因为理论上一个系统不允许存在同用户名的账号,除非使用其他测试数据,或删除这个测试数据。测试数据无法重复使用,意味着无法实现自动化测试。所以如何创建或者删除测试数据,常见的一般有两种方式:
两种方式各有优劣,可以根据实际情况选择,个人倾向后者。不论采用哪种方案,完成一轮测试后,一定需要删除相关的测试数据,才能满足自动化接口测试的条件。
假设我们要开发一个唱片管理系统,需要实现以下功能:
基于上述需求,先使用Apifox建立接口文档。Apifox官方提供了详细的介绍和操作文档,详见:快速上手 | Apifox 使用文档。目前Apifox有SaaS版和私有化部署版,其中SaaS版免费使用,而是私有化部署需要付费使用。Apifox的客户端又分为桌面版和Web版,以桌面版为例:
创建一个名为album
的项目。
创建数据模型
。如下图,使用了Apifox自带的“从JSON/XML智能识别”功能和“智能Mock”功能。详见:
创建接口文档
。如下图,引用上一步创建的数据模型
,就可以快速生成返回示例
,可用于运行Mock服务
,运行通过后可以保存为接口用例
。详见:
创建好接口文档
后,开发人员就可以根据文档中的示例数据
或者Mock数据
进行开发。而测试人员可以进一步完善接口用例
,比如设置一些测试数据以及设置断言,并基于接口用例
编写测试用例
。其中接口用例
指的主要是对接口的功能和边界进行测试,而测试用例
主要是对一组接口实现的业务功能进行测试。详见:
以Golang为例,开发一个服务端程序api-demo-golang
,实现接口文档的相关功能:
使用极狐GitLab托管源代码,服务默认端口9080,参考示例main.go
如下:
1 | package main |
创建该项目的流水线,实现自动构建,并发布到测试环境
。需Docker/K8S类型的GitLab Runner,详见:GitLab Runner Executors | GitLab。参考示例.gitlab-ci.yaml
如下:
1 | stages: |
待流水线成功运行后,确定测试环境IP:9080
可访问后就可以进行后续的测试。
当后台开发人员将接口服务发布到测试环境
后,测试人员就要基于测试环境
进行接口测试,而不是继续做Mock
。可在Apifox中配置管理不同的环境,并切换不同的环境进行测试。
在测试用例
中,选择测试环境
然后运行,查看测试报告
。若有未通过的用例,则走Bug提报流程或调试调整测试用例
;若用例全部通过,则可进行下一步,将这些测试用例
通过极狐GitLab CI/CD 进行集成,实现自动化测试。
在极狐GitLab中创建名为api-test-golang
的项目,用来管理接口的自动化测试用例
。
将手动测试通过的测试用例
导出为Apifox CLI
格式文件,并上传到极狐GitLab的api-test-golang
项目中,如果有多个测试用例
就有多个文件。
创建api-test-golang
项目的CI/CD流水线,详见:Apifox CLI 命令行运行 | Apifox 使用文档。参考示例.gitlab-ci.yaml
如下:
1 | stages: |
成功运行流水线后,可进入CI/CD作业(Job),通过日志查看测试报告:
也可下载作业(Job)的制品(Artifacts),或配合极狐GitLab Pages功能,以HTML格式查看测试报告:
利用极狐GitLab跨项目流水线功能,实现api-demo-golang
部署到测试环境
后自动触发api-test-golang
的接口自动化测试。修改api-demo-golang
的.gitlab-ci.yaml
:
1 | stages: |
运行api-demo-golang
的流水线,可以看到下游流水线api-test-golang
被成功触发。如果使用极狐GitLab专业版,还可以在上游流水线中直接查看下游流水线的构建状态和日志,如下图所示:
至此,就可以实现基于极狐GitLab和Apifox的自动化接口测试。持续维护api-test-golang
项目,每次服务端程序发布后,就可以自动的进行接口全量回归测试。
网络上有关Postman做接口测试的教程实在太多了,作为老牌工具功能也很强大,也支持团队协作、文档管理。但在实际工作中很多企业的测试人员只是拿Postman来做接口测试,不做接口文档的管理和协作。所以这里也仅介绍如何将Postman集成到极狐GitLab CI/CD做自动化接口测试,整体的工作流程可以参考上文。
在极狐GitLab中创建一个项目,用来管理Postman的接口自动化测试用例
。
将Postman的测试用例
导出为json
格式文件,并上传到上一步创建的极狐GitLab项目中,如果有多个测试用例
就有多个文件。
创建该项目的CI/CD流水线,参考示例.gitlab-ci.yaml
如下:
1 | stages: |
同样可以在流水线任务的日志中查看报告,也可以查看HTML格式的报告。依然可以参考前文,设置跨项目流水线,实现接口服务发布后自动进行接口测试。
WIKI百科对于软件测试的定义是:
在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。
软件测试是伴随着计算机和软件开发的发展而发展的,有记录的信息可以追溯到1958年的美国第一个载人航天计划——水星计划,在该计划中首次描述了软件测试团队及其工作内容。从软件测试出现到现在,大致可分为五个阶段:
调试为导向:20世纪50年代初期,当时还没有明确测试(Testing)和调试(Debugging)之间的区别,所以也没有测试或测试人员的概念。开发人员主要是以调试为主,验证程序是否符合预期。
证明为导向:1957年,Charles L Baker在对Dan McCracken的著作《Digital Computer Programming》进行评审时,提出了测试的概念,并对调试和测试进行了区分:
1957年到1978年,软件测试的主要目标是确保软件满足功能需求,也就是我们常说的“做了正确的事情”。
破坏为导向:1979年,Glenford J. Myers在《The Art of Software Testing》一书中阐述了一个成功的测试用例是检测到尚未发现的错误。说明测试不仅要证明软件做了正确的事情,也要保证它没做不该做的事情。这也使得软件测试和软件开发独立开来,测试需要更为专业的人员进行,毕竟开发人员在心理上总是不愿意给自己开发的软件找错。
评估为导向:1983年,出现了大名鼎鼎的V&V(验证和确认)理论,也就是现在测试人员熟悉的V模型,软件测试被应用在整个软件生命周期(SDLC)中。 这段时期软件测试的重点是检验它是否满足规定的需求或弄清预期结果与实际结果之间的差别,以及通过测试来评估和衡量软件质量。
预防为导向:1988年至2000年提出了一种新的测试思路。代码被分为可测试的和不可测试的,可测试的代码比难以测试的代码更少,所以测试的重点应该是在代码级别防止缺陷。20世纪的最后十年出现了探索性测试,测试人员探索并深入了解软件,试图找到更多的错误。2000年前后也出现了测试驱动开发(TDD)和行为驱动开发(BDD)等新概念的兴起。而2004以后,伴随敏捷开发模式的推广,自动化测试工具和持续集成等技术的应用,都体现出人们不再满足于传统的、后置的仅保证功能正确的软件测试,而是希望尽早的、高效的、全面的发现和识别问题。
软件测试按照其测试的目的、方法、级别不同,大体可分为以下几类:
因为测试方法种类繁多,且不同时期、不同企业、不同人对某一种方式方法的定义又有不同,所以这个分类某种意义上讲并不准确和规范。如何简化对软件测试的分类,Mike Cohn 在他的著作《Succeeding with Agile》一书中提出了“测试金字塔”这个概念。
它将测试按层划分,并且给出了一些参考经验:
同样为了摒弃一些模棱两可的术语,简化和统一测试分类,在《Google软件测试之道》一书中将测试分为:
这两种测试分类方法本质上都是基于测试层次的划分,不必拘泥于层次的命名或者大、中、小规模的量化指标,它们只是提供了一个参考框架,可以在企业内部或者团队内部进行沟通后达成一致即可。
在软件测试中,自动化测试指的是使用独立于待测软件的其他软件来自动执行测试、比较实际结果与预期并生成测试报告这一过程。其中“独立于待测软件的其他软件”指的就是自动化测试工具。
自动化测试的发展也伴随着自动化测试工具的发展。早在1985年的DOS时代,AutoTester公司就发布了同名的自动化测试工具。1989年Mercury Interactive公司成立,知名测试工具LoadRunner就诞生于这家公司,直到2006年Mercury Interactive公司被惠普软件收购。
2022年的今天,自动化测试工具早已百花齐放,并且处于高速发展中,为自动化测试提供了强有力的技术支撑。
实现自动化测试一般有两种常见的形式:
基于这两种模式,不同的自动化测试工具提供了不同类型的测试框架:
if-else
、while
、for
等语法。通过自动化测试工具,可以自动执行一些重复但必要测试工作,也可减少由于手工测试中繁复的重复工作所导致的人为差错,极大提高了测试效率。但短期来看,自动化测试还是可能产生巨大的开销,如流程机制的建立,人才的培养,测试脚本的编写和维护仍然需要人力、财力投入。
此外,目前国内企业大多很难满足测试人员与开发人员1:1的配比,不少企业依然是1:4或者1:5的配比。在这种场景下,测试人员只能手工进行一些基本的集成测试,来确保主要功能正常。所以大部分企业仍然采用手工测试与自动化测试结合的方式来完成测试工作。
考虑到自动化测试的投入和产出,以及其优势特性,并不是所有的项目都适合做自动化测试,可以根据以下几方面条件并结合实际情况来判断当前项目是否适合做自动化测试:
不管自动化测试前世、今生和未来如何。对行业来说,自动化测试符合大的趋势;对企业来说,能一定程度上提高效率和产品质量;对测试人员个人来说,也是职业发展的需要。所以了解或掌握自动化测试的相关技能,关注自动化测试的发展,也成为了每个IT从业人员的必修课。
正如开篇中提到的,自动化测试是在敏捷开发模式被推广和使用的时期得到快速发展的。敏捷开发以频繁的客户反馈为标志,通过频繁的迭代加速软件开发生命周期,从而推动了各类自动化工具的使用。
但是开发、测试和交付之类的关键功能是由各个独立运作的团队执行,团队间的协作效率低下,使得软件开发生命周期陷入困境。所以敏捷开发很快又被另一个更大的“概念”DevOps替代。正如这个词的来源,DevOps(Development和Operations的组合)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间合作关系的一种实践,以满足软件持续集成、持续部署(CI/CD)和现代软件交付的愿景。
然而,开发和运维团队的协同并不是完整的DevOps,测试团队在持续交付链中的作用同样不可或缺。毕竟企业需要的是在不牺牲产品质量的前提下提高交付效率。从根本上说,DevOps中的测试是为了平衡开发、测试、运维团队之间的共同目标、反馈周期和人员能力,从而促进三个团队的协作。所以在DevOps中进行的测试工作不是独立开展的,它与开发工作并行,利用自动化测试工具和CI/CD工具,加速反馈周期,以便让开发团队及时知道他们的工作将如何影响交付。
如同敏捷宣言一样,一些DevOps测试团队制定了“DevTestOps”声明,规定了大规模软件测试的指导原则:
基于这些原则,可以实现在软件开发过程中具备更好的可见性,从而确保常规部署可以顺利进行,不会因为一些产品质量问题导致部署失败或中断,从而影响到整体业务和最终客户。
将测试团队融入到DevOps中,最简单快捷的方式就是开展自动化测试。缺少自动化测试的最佳实践,我们将无法实现基于CI/CD打通来打通整个工作流程。解决不了协作问题,DevOps也就无从谈起。所以某种意义上来说,自动化测试也是DevOps的驱动者。
实施自动化测试的大前提是整个团队建立了基本的研发流程,具备相对完善的版本控制策略,并掌握了CI/CD的相关能力,这也是DevOps的入门条件。有了基本的文化、流程和工具的支撑,才能开展自动化测试工作。在此之前,可以参考我的另一篇文章《一段祖传代码引起的血案》做好前期建设工作。
按照测试金字塔的分层方式和经验建议,理应是先做单元测试。但根据国内的大多数企业的实际情况和我个人的工作经验,建议按以下路径来开展自动化测试工作:
首先是因为一系列的原因,导致国内的开发人员没有写单元测试的文化。让测试人员直接从UI层面做自动化测试,对于人员能力要求太高。此外UI层的变动相对频繁,所以在UI层做自动化测试整体效率较低。而接口测试可以借助一些可视化工具进行,这些工具一般都容易跟CI/CD工具集成从而实现自动化测试,对测试人员友好。相对于UI层,接口层也相对比较稳定,所以建议自动化测试先以接口测试作为切入点。最后,实现了自动化接口测试也就具备了做自动化性能测试的能力,它同样属于接口层的测试。
这个系列的后续文章也会按照这个参考路径来逐个介绍。同时以极狐GitLab作为DevOps平台,通过集成各种类型的自动化测试工具并结合相关理论知识,来给出基于极狐GitLab的自动化测试实践方式和建议。
统一身份认证服务平台一般包含以下几个部分:
而单点登录(SingleSignOn,SSO),不光可以实现一个账号登录所有系统,它通过用户的一次性登录认证,就可以访问多个应用。SSO一般会被包含在认证管理功能里。
GitLab支持多种身份认证和授权方式,可以与企业的统一身份认证服务平台集成。包括对接AD/LDAP实现统一账号,对接SAML、CAS、Auth0、OAuth2等实现SSO。GitLab对于AD/LDAP、SAML、CAS、Auth0的对接提供了详细的文档。而对接Generic OAuth2的文档较粗放,网络上也没有太多参考资料,所以整理了一篇GitLab对接OAuth2的实践文章。
该步骤用于部署OAuth2 SSO的示例服务,已经有OAuth2 SSO服务的用户可以跳过,但可参考其中的内容。演示项目基于SpringSecurity+OAuth2实现单点登录SSO修改。
相关配置
编辑auth-server/src/main/java/com/example/authserver/config/AuthServerConfig.java
的redirectUris
,添加或修改GitLab的CallBack URL
,相当于给OAuth2 SSO服务添加可信的重定向URL。
1 | public void configure(final ClientDetailsServiceConfigurer clients) throws Exception { |
用户名和密码默认配置在auth-server/src/main/java/com/example/authserver/config/SecurityConfig.java
下
1 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { |
OAuth2相关配置在client-a/target/classes/application.yml
下
1 | security: |
编译启动
1 | 项目根目录 |
运行测试
访问client-a localhost:8301
自动跳转到auth-server localhost:8300
登录用户
跳转回client-a localhost:8301
,并完成登录认证
该步骤用于获取OAuth2的user-info-uri
返回的数据结构,这里可以用Postman操作。
认证方式选OAuth2.0,根据上一章节的配置填写OAuth2的相关参数,然后点Get New Access Token
Postman会弹窗进入OAuth2 SSO服务的登录页面,输入用户账号和密码,确认是否认证成功
发送请求,获取响应结果,确认必须是Json格式
该步骤用于配置GitLab与OAuth2对接并实现SSO。
GitLab对接OAuth2的限制
修改/etc/gitlab/gitlab.rb
,并gitlab-ctl reconfigure
1 | gitlab_rails['omniauth_allow_single_sign_on'] = ['oauth2_generic'] |
运行测试
登录GitLab,选择使用SSO登录
自动跳转到OAuth SSO服务
登录认证成功,返回GitLab,并自动创建用户
OAuth2 返回的用户信息必须包含email信息,并映射到attributes
的email
。如本文示例中OAuth即便只返回username
,但其内容是邮箱信息,且映射到了attributes
的email
,否则GitLab会给出以下错误提示。
如果GitLab中已经存在同名、同邮箱的用户,使用SSO登录会出现以下错误提示。
可参考OmniAuth | GitLab 或参考下图将OAuth SSO关联到已存在的用户。
参考资料:
]]>使用GitLab CICD,在部署方面,主要有两种方式:
部署到K8S集群
Push模式:流水线通过kubectl
执行命令部署,这需要把K8S的权限给流水线,存在安全风险
Pull模式:使用GitLab Agent for Kubernetes或ArgoCD,通过GitOps的方式“监听”GitLab的变化,触发部署
部署到服务器
目前仍有不少企业因为行业性质或者场景所限,没有使用K8S等云原生技术,还在采用传统的服务器方式进行部署。一般使用ssh
、scp
、rsync
等命令部署到服务器。GitLab也提供了基于SSH keys的部署。详见:Using SSH keys with GitLab CI/CD | GitLab
需要说明的是,如果是使用专用的编译机进行编译构建,然后部署到指定的服务器,只需要实现编译机和部署服务器的免密SSH登陆即可,相对简单。但如果使用容器进行编译构建,然后部署到服务器,就需要按照上面文档中提到的,配合GitLab CI/CD环境变量,将SSH_PRIVATE_KEY
等变量存储到GitLab Project、Group或Instance中,实现复用。且可以通过GitLab CI/CD环境变量的Mask设置,掩藏这些变量在CICD日志中的显示。详见:GitLab CI/CD variables | GitLab
但遗憾的是Mask功能目前是有限制的,对于SSH_PRIVATE_KEY
这种多行的变量无法直接使用Mask功能。这样开发人员就可以在.gitlab-cti.yml
文件的脚本中执行echo $SSH_PRIVATE_KEY
,在流水线的日志中输出SSH Keys,存在密钥泄露风险。
The value of the variable must:
- Be a single line.
- Be 8 characters or longer, consisting only of:
- Characters from the Base64 alphabet (RFC4648).
- The
@
and:
characters (In GitLab 12.2 and later).- The
.
character (In GitLab 12.10 and later).- The
~
character (In GitLab 13.12 and later).- Not match the name of an existing predefined or custom CI/CD variable.
这个问题在GitLab的Issue上挂了有一年多 ,看样子短时间没法解决。有没有其他方式Mask SSH_PRIVATE_KEY
?于是开始了各种折腾。
SSH Keys不能直接Mask,但Mask的要求里面是支持Base64的。所以把SSH Keys先用Base64编码,存到CICD环境变量中,这样就可以Mask了,然后在.gitlab-ci.yml
中解码,就可以在不影响功能的前提下实现效果。看看操作步骤:
Base64编码SSH_PRIVATE_KEY
1 | 输入示例 |
将输出的Base64编码作为SSH_PRIVATE_KEY
存储GitLab CICD环境变量,并Mask
修改.gitlab-ci.yml
1 | before_script: |
运行测试,大功告成
这看上去是个不错的方案,但真的保证了SSH_PRIVATE_KEY
安全么?我们本着Geek(作死)精神,测试一下:
修改.gitlab-ci.yml
,通过各种方式看看能不能打印出SSH_PRIVATE_KEY
1 | before_script: |
运行测试
直接输出SSH_PRIVATE_KEY
是被Mask了,但执行echo "$SSH_PRIVATE_KEY" | base64 -d
,居然把SSH_PRIVATE_KEY
打印了出来,所以这个方法还是存在一定的问题。
SSH Keys头部和尾部的-----BEGIN RSA PRIVATE KEY-----
、-----END RSA PRIVATE KEY-----
不能Mask,但里面的内容,每一行可以单独作为一个环境变量存储并Mask,使用的时候再进行拼接,看看操作步骤:
将SSH_PRIVATE_KEY
每一行拆分成一个变量,进行存储,有多少行就要存多少变量,为了偷懒,此处只列了3行,实际上我这个SSH_PRIVATE_KEY
除去头尾,有26行……
修改.gitlab-ci.yml
,并加入一些测试
1 | before_script: |
运行测试
方案行是行,实际操作起来要存26个变量然后还要拼起来,实在是太土了,能不能减少行数,存一行。
Mask不支持空格,只支持@:.~
,那我们尝试把SSH_PRIVATE_KEY
除了头尾的部分合并成一行,把换行符替换成支持的符号,如.
,然后再与头尾进行拼接。操作步骤如下:
合并SSH_PRIVATE_KEY
1 | 输入示例 |
将除头尾部分存入环境变量并Mask
修改.gitlab-ci.yml
1 | before_script: |
运行测试
和“编码存储”的方案一样,跑的通,但依旧可以通过对应的方式,打印出SSH_PRIVATE_KEY
。
到这里,可以隐约猜到Mask变量的原理是简单做了一个是否包含字符串的判断。如果与环境变量的值匹配就显示[MASKED]
,如果不匹配就直接将变量显示出来。这也是为什么目前只允许值是单行且没有太多特殊符号的环境变量才可以MASK的原因。
为了验证上一步留下来的猜想,我设计了一个实验:
恢复环境变量中的SSH_PRIVATE_KEY
为原始内容,并且不做Mask,当然也无法Mask
新建一个环境变量,值为SSH_PRIVATE_KEY
的一部分内容,这里设置的是SSH_PRIVATE_KEY
内容的第一行,然后设置为Mask
恢复.gitlab-ci.yml
文件,需要注意的是这里面没有任何关于MOSAIC
环境变量的使用
1 | before_script: |
运行测试
正如猜想一样,即便没有使用MOSAIC
环境变量,但它依然作为判断是否包含字符串而被执行了。
利用这个特性,我们可以通过设置几个马赛克变量,给SSH_PRIVATE_KEY
的部分内容打码,看上去是个非常聪明的做法。但同样利用这个特性,可以让所有的Mask环境变量失效。
如果我们输出一个Mask环境变量的时候,往这个变量中插入点字符,破坏它判断包含字符串的逻辑,就可以把这个变量显示出来。我们尝试打印已经Mask的MOSAIC
环境变量,往这个变量的第一个字符后插入一个空格:
1 | echo "$MOSAIC" | awk 'BEGIN{FS=OFS=""}{$1=$1 " "}1' |
执行结果:
这样就可以打印并推断出Mask环境变量的真实值了,也意味着我们上面的所有方式都不完美。
作为一般场景下使用,上面的四种方式任意选一个都可以实现基本的安全防护,正所谓防君子不防小人。如果要进一步提高安全性,还是如官方所说,上专业的密钥管理工具,如Vault,或者期待下GitLab在管理密钥这块功能的完善。
参考资料:
]]>WARNING:本文含有强烈的刺激性气味,请勿在进食期间阅读。如感到血压上升、眩晕、呼吸急促,请立即停止阅读。
祖传代码(Legacy Code),就字面意思而言,就是无数的前任程序猿留给你的最后遗产。这些代码几乎没有可维护性,缺少注释、命名不规范、依赖错综复杂,你根本读不懂它,但神奇的是它们都能跑起来。不要试图修改它们,因为要么就无从下手,要么一改就出大问题。每家公司都会有那么些“历史遗留问题”。亚马逊的工程师亲切的形容他们的祖传代码为“屎山”:“每次你想修正一个bug,你的工作就是爬到屎山的正中心去”。
一家企业里,偶尔一两座屎山无伤大雅,毕竟每家企业都有很长的故事。但如果“你看那一座座山,一座座山川,一座座山川相连”,这种情况就很危险了。山路十八弯,难以持续的为企业快速增长提供高效支撑。企业发展的越快,屎山的债务积累就越多,形成恶性循环,最终企业员工只能望山兴叹,下山逃难去了。如何解决屎山问题?如何形成一个正向的发展循环?如何打造一个研发流程的最佳实践?如何加强质量内建?那我们就要先爬到屎山的正中心去一探究竟,明知山有屎,偏向屎山行。
正所谓罗马不是一日建成的,屎山形成于长期的撕X、扯皮、妥协当中,我们先来欣赏几段相声:
- 明早领导来参观/甲方提了新需求,明天就要,必须做完!
- 不要问我怎么办,这点事你都不会做?
- 能跑起来就行,实在不行加个假页面。
- 这个功能临时写死,来不及测试了,先发版!
- 怎么出问题了?!这个不是我说的啊!
接手了一段代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 var temp = 37.3°
var wendu = 36.5°
var a,b,c,d,e,f,g
// 这个方法没注释
func F1()
// 然我不知道下面的注释有什么用,但你不要尝试动它,因为删除了就会报错,不要尝试解决,我已经试过了,没用,最终恢复了这段注释
func F2()
// 注释了下面这段代码就能跑起来,我也不知道为啥
// 临时写死
func F3()
// 临时写死+10086
func F10086()看看代码的提交记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 Author: root
Date: xxxx-xx-xx xx:xx:xx
First Blood
Author: root
Date: xxxx-xx-xx xx:xx:xx
Double kill
Author: unkonwn
Date: xxxx-xx-xx xx:xx:xx
Triple kill
Author: zhangsan
Date: xxxx-xx-xx xx:xx:xx
Holy Shit问问即将跑路的张三同学,他说来的时候就是这样,他也不知道前面发生了什么
简单总结有四方面原因:
面对重重大山,企业管理者和打工人们并不是无动于衷,他们也尝试过去解决问题,但在屎山面前,无一不碰得头破血流。问题总要有交代,于是聪明的劳动人民总结出以下几个移山大法:
当局者迷:不识庐山真面目、只缘身在此山中。不管是不是真的习惯了,有一部分人已经适应了高原反应,作为山上的原住民,大家都表示出这就是正常生活,自然一片和谐,其乐融融。
装聋作哑:一个巴掌拍不响,不要回答!不要回答!!不要回答!!!
求神拜佛:很难想象,一群写代码的唯物主义者会因为屎山的压迫做出一些反常的事,比如
人工智能:质量不够,运维来凑。取运维同学适量、笔记本电脑少许、手机24小时开机,通过“智能策略”定时重启一下、负载高了重启一下、出问题了重启一下。还出问题,将运维同学祭天。
众所周知,冰山理论可以用在任何分析场景。试着分析一下屎山:
到这里,我们基本上可以总结出要避免屎山的产生,需要解决以下几个问题:
本着预防为主,治疗为辅的原则,我们需要三步走的实践,先来解决增量问题。
建立一套规范的研发管理流程这个话题实在是太大了,包括:
每一个环节拉出来讲都是写书的节奏。但个人在研发管理流程上面的建议就是统一,包括:
统一的好处就是把各个环节各个职能的人拉到同一个平台上,说一样的话,做一样的事。同时规模化效应也有利于降低各方面成本。
不统一的后果就是又建立了一个个屎山,到时候再去打这个山头可就非常困难了。
这里我就源代码管理这个点做一些展开,毕竟源码是产品的最底层也是核心。GitLab官方发布了2020 Version Control Best Practices | GitLab,提出了版本控制最佳实践的5个步骤,针对每个步骤的解释说明、意义价值和实践经验如下:
软件项目常依靠跨团队、多名员工的共同协作,工作流程中可能会出现代码冲突。为防止代码混乱,团队应确定并广泛推广唯一的分支策略。常见的分支策略如下:
Git Flow
诞生最早,Vincent Driessen在2010年提出了Git Flow模型。
它有2个主干分支:
还有其他3类分支:
一旦完成开发,它们就会被合并进Develop或Master,然后被删除。可以看到Git Flow管理的分支较多,优点是清晰可控,但缺点也很明显,维护比较复杂。
GitHub Flow
GitHub Flow是Git Flow的简化版,它是 Github.com 使用的工作流程。
它只有一个主分支Master,官方推荐的流程如下:
GitHub Flow非常简单,但他的缺点是有时候我们需要有多个环境,比如,苹果商店的APP提交审核以后,等一段时间才能上架。这时,如果还有新的代码提交,Master分支就会与刚发布的版本不一致。另一个例子是,有些公司有发布窗口,只有指定时间才能发布,这也会导致线上版本落后于Master分支。
上面这种情况,只有Master一个主分支就不够用了。通常不得不在Master分支以外,另外新建一个Production分支跟踪线上版本。
Trunk-based Development (TBD)
Paul Hammant 2013年提出的模型,是SVN常用的模型,在TBD模型中,主干分支用于开发,通过新的分支来交付。同GitHub Flow一样,对于多环境的需求,需要开多几个分支用于不同的环境。
GitLab Flow
Gitlab Flow 是 Git Flow 与 Github Flow 的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是 Gitlab.com 推荐的做法。Gitlab Flow 的最大原则叫做”上游优先”(upsteam first),即只存在一个主分支Master,它是所有其他分支的“上游”。只有上游分支采纳的代码变化,才能应用到其他分支。
它建议在Master分支以外,再建立不同的环境分支。比如,“开发环境”的分支是Master,“预发环境”的分支是Pre-Production,“生产环境”的分支是Production。
开发分支是预发分支的“上游”,预发分支又是生产分支的“上游”。代码的变化,必须由“上游”向“下游”发展。比如,生产环境出现了bug,这时就要新建一个个性分支,先把它合并到Master,确认没有问题,再cherry-pick到Pre-Production,这一步也没有问题,才进入Production。
传统的开发过程中,开发人员只有在项目上线前夕才会集中提交代码,不仅存在代码丢失的风险,还存在较多代码冲突的风险。此外大量的功能交叉堆叠在一起,通过一次commit根本难以识别上线了什么功能,对于选择性上线功能或者回退代码造成极大的影响。
小步快跑,按需求较细粒度的提交commit,有助于降低软件项目整体风险。此外,按照需求功能提交commit,也有助于形成透明协作的工作文化。可以随时知道小伙伴们什么时间做了什么功能。特别提醒,填格子是为了开发人员能“乐在其中”,千万别用来卷。
前文也介绍了屎山代码中的提交信息案例,不知道接手的人看到Commit Message是“First Blood”、“功能第一次修改”、“功能第二次修改”、“功能改回第一次修改”有什么感受,很多时候接手的人就是自己,真的“恶心一个人在家——恶心自己”。
对于每个Commit,应当反映Commit的意图,而不仅仅是Commit的内容。这有助于团队成员更直观的看到Commit可能引起的变化。这句话的意思是说:
1 | # <Fix>:修正了手机号码不能录入国外手机号的问题 (正确) |
此外,对于Commit Message也需要进行规范化管理,规范Commit Message的好处:
一个好的Commit Message规范可以参考:
1 | Angular 规范 |
可以通过Commitizen这款工具进行Commit Message规范化:
commitizen/cz-cli: The commitizen command line utility. #BlackLivesMatter (github.com)
安装完成后,就可以使用git cz
命令替代git commit
命令。Commitizen会通过引导,生成符合规范的 Commit Message。
在GitLab中,可以给项目、群组、实例设置推送规则,通过正则表达式判断Commit Message中是否包含符合规范的关键字,如fix
、feature
等。
书接上回,选择了合适分支策略后,就可以使用代码分支,团队成员可以在不影响主代码库的情况下进行代码修改。并在代码分支中跟踪修改历史。当代码准备就绪时,可以合并到主分支中。
在GitLab中,可以基于Issue快速创建一个个性(功能)分支,开发完成后就可以有选择的合并到Master分支。
定期进行代码审查,可防止不稳定的代码并确保代码质量持续改进。团队成员可以审核任何人的代码并提供建议。MR和Code Review就是产品、研发、项目的Battle Field。接下来将见证几场惨烈的战役:
第一次代码质量大战
很多中小型企业都会忽略Code Review,因为研发团队规模较小,基本上一个项目一个职能就1个人,认为没有人或者没有必要Review。正是这样的处理方式,导致项目的开发人员可以肆无忌惮的发挥,只要自己看得懂就行,只要自己现在还看得懂就行。这也是一个个萌芽中的屎山。
个人经验,不论团队规模大小,都应该进行Review,但丰俭由人。
Review前提是小步快跑和详细描述,不然一下子几百行代码,描述又没几行,很难把这个制度执行下去。Review的过程就是要人工找茬,看看编码规范,看看设计模式。花不了几分钟,相互喷一喷,怼一怼,在互相伤害中提升整体能力。(太卷了)
第二次代码质量大战
让研发人员相爱相杀,短时间固然奏效,但难免会有人出现人格觉醒,他们抱团了,他们发动阶级斗争了。所以代码审查不能只设置一层,还需要上一级管理人员抽查,这里可以拉一组技术管理人员,但不需要作为强制审核节点,给领导们一些自由发挥空间,反正我给你机会了,你没珍惜,没参与,那就没有发言权了。(太假了)
世界和平之战
打人工群中,有这么两个群体,天天杀得不可开交,那就是产品和开发。别打了,有话好好说。代码合并审批中把产品也拉进来,在MR的Message中大大写下(仅供参考):
1 | 因客户XXX明天要XXX(客观责任不在你我) |
来让产品和领导们都来画个押,屎山上都有我们到此一游。以后扯皮就有证据了,开发终于硬气了一回。什么,他们睁眼说瞎话,他们打死不认账?快跑吧,兄弟!(太惨了)
关于如何在GitLab中,多个分支下进行开发、MR和Code Review,基于极狐GitLab给出一个参考流程:
选择使用Gitlab Flow模式,即:
main
分支后删除。main
分支为主干开发分支(GitLab 14版本将默认分支名称从Master
改成main
)。pre-production
分支为预发布环境分支。production
分支为生产环境分支。设置分支保护,避免问题代码在未经过Review和管理人员不知道的情况下被提交。
进入“项目”–“设置”–“仓库”–“受保护分支”。
GitLab 默认将默认分支(main
)设置为保护分支。
新建一个保护分支,因为Gitlab Flow下pre-production
和production
分支都是环境分支,也需要保护起来,不允许直接push,所以可以通过*production
这种通配符匹配分支,允许合并选择为Maintainers
,允许推送选择为No One
。这样就只有具备该项目Maintainers
角色的用户可以对代码进行MR。
设置代码仓库人员角色权限,可以在群组级设置(群组信息——成员),也可以在项目级设置(项目信息——成员)。
Developer
。Maintainers
。Maintainers
角色,MR动作的执行者一般是技术管理者。参考“2.3 描述提交信息”章节,设置推送规则。
设置合并请求批准(Code Review)。
进入“项目”–“设置”–“通用”–“合并请求批准”。
添加一组或多组审批规则:
根据需要在“审批规设置”,勾选“禁止作者审批”,“禁止在合并请求中编辑批准规则”。
“合并请求批准”功能需GitLab专业版及以上版本。
将需求拆分成Issue,参考“2.4 在开发分支下进行开发”章节,从Issue创建一个个性(功能)分支。
在个性(功能)分支下进行开发,规范提交Commit Message。
当功能开发完成后,提交MR,并进行Code Review。
新建合并请求,从个性(功能)分支合并到main
分支。
Code Review人员可以在“提交”选项卡中查看这个MR中有几次Commit,可以在“变更”选项卡中查看这个MR中设计的代码变更。
合并请求批准的相关信息会显示在MR页面,只有当审批条件被完全满足,也就是审批通过,MR动作才变得可执行。此外已核准人信息也会被显示在页面上,所以把产品狗头挂上来把,免得他不认账。
当审批通过,MR动作变得可执行,可以勾选“删除源分支”,这样个性(功能)分支就彻底并入main
分支了。
当我们在main
分支完成一轮或多轮功能集成,就可以参考MR的步骤,合并到pre-production
和prodution
分支。
如果按照以上方式或步骤将版本控制实践落地,相信(不保证,不负责)你应该可以形成一个相对规范的代码管理流程,一定程度上保障了代码质量,同时也形成了一个初步的审核追踪记录体系,这也是为进一步解决问题打下了坚实的基础。
朋友:
大量的人工重复性操作、不统一的编译运行环境、割裂的开发交付环节严重影响了交付效率,在原本就很痛苦的困境下,雪上加霜。现在是2021年,我相信任何一个企业的研发团队就算没用过CICD也应该听过CICD,通过持续集成、持续部署可以实现高效的持续交付。
持续交付是将业务应用所有类型的变更,包括新功能、配置更改、缺陷修复和模拟验证等,通过安全、快速和持续的方式部署到生产或交付至用户的能力。—— Jez Humble
说简单点就是通过一系列的脚本,自动的完成代码编译、发布、部署,避免人为操作差异引起的错误,提高了交付效率。同时把传统模式下在项目上线前才进行部署的操作拆分并前置,每一个新功能开发完都执行一遍脚本编译打包并部署到测试环境,然后进行一轮测试,这样有什么问题、有什么bug、有什么环境差异导致编译打包部署失败,都可以把风险识别和处理前置,做到心中有数,避免在正式环境上线时决战到天亮。
在《中国 DevOps 现状调查报告(2021)》中,Jenkins依然占据CICD近2/3市场,GitLab CI排名第二。关于Jenkins,我在2017年写过一个系列文章 jenkins - Rang’s Note,但时至今日,也过去了4年,Jenkins和整个DevOps行业也经历了突飞猛进的发展。文章的部分内容也过时了,仅供参考,好在现在关于Jenkins和CICD的实践文章铺天盖地,我就不再挖这个坟了。
这里还是说说GitLab CICD,他的好处就是与GitLab整合,开箱即用。不需要额外部署一套系统,额外管理一套用户权限体系。Pipeline使用YAML语言编写,不论是新人上手还是Jenkins老鸟点亮新技术树,都非常容易。GitLab最大的好处就是文档全GitLab CI/CD | GitLab 。当然极狐GitLab也对文档进行了翻译,但还需要完善 GitLab CI/CD 入门 | GitLab 。
此外GitLab CI提供了Web版的流水线编辑器,可以方便的验证语法,如果使用 VS Code,还可以使用 GitLab Workflow VS Code 扩展验证 CI/CD 配置文件。GitLab CI也提供了可视化CI配置,可视化显示所有阶段和作业。任何 needs
关系都显示为将作业连接在一起的线,显示执行的层次结构。
最后,GitLab CI可以通过include
快速进行配置复用,将CICD配置模板化管理。对于没有历史包袱的团队建议直接上GitLab CI,也为以后实践GitOps打一个基础。
GitLab官方发布的GitLab CI与Jenkins功能对比见:
个人总结Jenkins和GitLab CI的部分功能对比:
GitLab CI | Jenkins |
---|---|
安装简单,开箱即用 | 自身及插件安装较复杂 |
与代码仓集成,CI配置在代码仓中 | 编译服务与代码仓库分离 |
脚本语法简单 | 插件丰富 |
GitLab 使用数据库存储数据 | Jenkins 使用文件系统来存储数据 |
Runner基于Golang开发,跨平台部署更友好 | Jenkins Agent基于Java开发,需要JDK支持 |
GitLab自身提供高可用高性能部署方案,Runner不会影响到GitLab自身性能 | Jenkins Master高可用高性能方案难以实现,Agent过多导致Master性能不够,或者使用CloudBees的企业版Jenkins |
不论使用Jenkins还是GitLab CI还是其他的CICD工具,将CICD实践落地,这也是所有团队向DevOps转型的第一步。有了CICD,一方面提高了交付效率,让团队人员把时间都能用到刀刃上,另一方面,团队有了一个自动化的产线,有了一个脚手架,后续就可以基于这个产线集成更多自动化的工具,进一步解放生产力,发展生产力。
上文说到CICD是团队向DevOps转型的第一步,那第二步是啥?第三步是啥?这个没有标准答案,但实际运用角度来看,代码扫描可以是DevOps转型的第二步,后续还有自动化测试、拥抱云原生等等。
我们日常所说的代码扫描,其实是指静态源代码扫描。
静态程序分析(英语:Static program analysis)是指在不运行程序的条件下,进行程序分析的方法。有些程序分析需要在程序运行时才能进行,这种程序分析称为动态程序分析。大部分的静态程序分析的对象是针对特定版本的源代码,也有些静态程序分析的对象是目标代码。静态程序分析一词多半是指配合静态程序分析工具进行的分析,人工进行的分析一般称为程序理解或代码审查。
静态程序分析的复杂程度依所使用的工具而异,简单的只考虑个别语句及声明的行为,复杂的可以分析程序的完整源代码。不同静态程序分析技术对分析得到的信息的用途也有所不同,简单的可以是高亮标识可能存在的代码错误(如lint),复杂的可以是形式化方法,也就是用数学的方式证明程序的某些行为符合其设计规约。
静态程序分析的商业用途可以用来验证安全关键计算机系统中的软件,并指出可能有计算机安全隐患的代码。
在信息安全的领域中,静态程序分析会称为静态应用程序安全检测(Static Application Security Testing,简称SAST)。
—— 维基百科
这段描述抛出了几个重要信息,静态源代码扫描:
所以将代码扫描作为DevOps转型的第二步,是因为相比接口/UI/性能/E2E的自动化测试,代码扫描的技术成本最低,基于CICD这个脚手架,只需要一些简单的无侵入的配置,无需编译运行,就可以完成代码质量,代码潜在安全隐患的分析。在已经实现版本控制和CICD的基础上,进一步提高了Code Review的效率。也就是说前文中的几场惨烈的战役就不全是PvP了,有一些就变成了PvE。开发人员之间不用拿着放大镜去找茬,去互相伤害,因为CICD流水线加代码扫描自动可以完成找茬任务。
如何评价一段代码的好坏,如何量化的去评价一个屎山。Bob大叔在他的著作《Clean Code》的前言中引用了这样一幅漫画:
通过“每分钟爆粗数量”来衡量代码质量是个很有趣的玩笑,就跟我们称呼祖传代码为屎山一样,是个调侃。回归严谨,还需要通过一些专业的可量化的角度去评价代码质量。《Sonar code quality testing essential》一书中从七个维度定义了代码的这种内在质量,Sonar开发团队戏称为开发人员七宗罪,并指出开发团队至少要解决前5项问题:
那么如何定义代码质量,如何度量代码质量,这里引用一篇文章的内容:
如何评估软件产品源代码质量一直是业界的一大挑战,SQALE(Software Quality Assessment based on Lifecycle Expectations)方法的出现提供一套科学的度量和分析方法,有效应对了这一挑战。SQALE方法整合了ISO-25010标准与代码规范,其目标是:以客观、准确、可复制和自动化的方式为评估软件应用程序的源代码提供支持;为管理技术债务提供一种有效的方法。SQALE是目前众多主流代码分析工具的参照标准,包括我们熟知的SonarQube,和CoderGears, SQUORE等商用代码扫描分析工具。
“技术债”这一概念最早出现在1992年,其本义是指,开发人员为了加速软件开发,在应该采用最佳方案时进行了妥协,改用了短期内能加速软件开发的方案,从而在未来给自己带来的额外开发负担。这个定义暗示了这种“负债”是一种刻意的、理性的经过权衡的行为,后文中我们进一步探讨技术债务的类型时会指出这一定义仅仅代表了技术债中相对良性的一类,是一个比较“温和”的定义。此处我们关注的重点是使用技术债这一隐喻来帮助大家理解度量代码质量的方法。
有了工具,有了方法,如何跟CICD做集成,如何与Code Review做结合,我依然拿GitLab CI举例,分别实践GitLab自带的代码质量扫描和集成SonarQube的代码质量扫描。
简介:
GitLab代码质量扫描集成的是Code Climate这款开源工具,它支持多种语言,多种质量类型,支持自定义规则,具体见 Available Analysis Plugins (codeclimate.com)
准备工作:
。操作步骤:
在需要做代码质量扫描的代码仓中,添加CICD配置文件.gitlab-ci.yml
,如果已经存在,则跳过。
在CICD配置文件中加入以下代码。
1 | include: |
运行流水线。
扫描报告:
在CICD流水线中,会出现“代码质量”选项卡,可查看代码质量扫描结果。
在CICD页面中,可以下载指定流水线的代码质量扫描报告,更友好的查看不同类型的代码质量问题。
在MR页面中,会出现“Code Quality Degrade”选项卡,可查看代码质量扫描结果,用于辅助代码Review,提高Review效率。
使用GitLab自带的代码质量扫描功能,好处是简单方便,但缺点是目前缺少分析和可视化,没有通过定义规则与MR形成自动化代码质量门禁,没有建立技术债和代码质量评级模型,不过好消息是这些功能已经在路上了。
简介:
SonarQube(曾用名Sonar),是一个使用Java开发的开源的代码质量管理系统,它有社区版和企业版。其中社区版仅能做代码质量扫描,且功能受限,如仅能扫描主干分支、不能在GitLab MR看到扫描结果(仅在SonarQube展示)。企业版解锁受限功能,且能做代码静态扫描。
准备工作:
操作步骤:
在SonarQube中,进入“Administrattion”页面,选择“DevOps Platform Integration” 选项卡(不同版本选项卡名称可能不同,具体参考官方文档),找到“GitLab”选项,创建一个配置。
填写配置名称、GitLab API地址,和用于做集成对接的账号的Token,引导做的非常人性化,如何获取Token也给与了指引。
回到SonarQube的“Projects”页面,创建项目,选择“GitLab”。
选择需要集成的项目,点击“Set up”。
因为我们跟GitLab CI做集成,此处选择“With GitLab CI”。
随后会进入一个更人性化的引导,需要按照指示逐个完成,包括需要在对应的GitLab项目代码仓下增加相应的文件,增加相应的CICD环境变量。
最后根据引导提示,在GitLab CICD配置文件.gitlab-ci.yml
中加入以下代码(以实际引导为准):
1 | sonarqube-check: |
运行流水线。
扫描报告:
在SonarQube中,可以查看SonarQube的代码质量扫描结果,包括Bug数、可靠评级、漏洞数、技术债、代码坏味道(即违规项数量)等等数据指标。
在GitLab MR页面中,会出现“SonarQube Code Analysis”选项卡,可查看SonarQube的代码质量扫描结果,用于辅助代码Review,提高Review效率。但该功能需要SonarQube企业版才能实现。
使用GitLab与SonarQube集成做代码质量扫描,配置相对复杂,但引导非常人性化。好处是SonarQube提供了丰富的技术债和代码质量评级模型,可以识别代码复杂度、重复率。但SonarQube免费版本功能有限,不能和GitLab MR做深度集成,不能扫描非主干分支,这也失去了作为MR质量门禁的意义。SonarQube企业版虽然可以将扫描结果关联到GitLab MR,且解锁了各个分支的扫描,但依然不能通过定义规则与MR形成自动化的代码质量门禁。
Web应用安全测试技术经过多年的发展,目前业界常用的技术主要分为3大类别。
DAST:动态应用程序安全测试(Dynamic Application Security Testing)技术在测试或运行阶段分析应用程序的动态运行状态。它模拟黑客行为对应用程序进行动态攻击,分析应用程序的反应,从而确定该Web应用是否易受攻击。
SAST:静态应用程序安全测试(Static Application Security Testing)技术通常在编码阶段分析应用程序的源代码或二进制文件的语法、结构、过程、接口等来发现程序代码存在的安全漏洞。
IAST:交互式应用程序安全测试(Interactive Application Security Testing)是2012年Gartner公司提出的一种新的应用程序安全测试方案,通过代理、VPN或者在服务端部署Agent程序,收集、监控Web应用程序运行时函数执行、数据传输,并与扫描器端进行实时交互,高效、准确的识别安全缺陷及漏洞,同时可准确确定漏洞所在的代码文件、行数、函数及参数。IAST相当于是DAST和SAST结合的一种互相关联运行时安全检测技术。
这里我们探讨的代码安全,依然还是静态代码安全。对于项目代码本身来说,能写出有安全漏洞的代码还真的不容易,因为现在广泛使用代码框架、ORM框架、IDE进行开发,都能一定程度上提醒或防止XSS、CSRF、SQL注入等问题,但不代表不会出现,比如直接基于原生编程语言撸代码,比如未按照框架的最佳实践进行配置等等。最容易引起代码安全问题的,还是项目代码引用的大量第三方库或开源项目,它们可能自身就存在安全问题,或者因为版本过低,没有解决安全问题。所以代码安全的范围就不仅仅局限在代码本身了,它包含了代码的上游——依赖项的安全。
当然有很多专业的安全软件,像SonarQube、Black Duck都提供了非常完善的安全扫描功能、软件成分(依赖项)分析功能,来识别和解决代码安全问题。但存在以下几个问题:
好在GitLab也集成了安全扫描功能,可以结合GitLab CICD做并行扫描,提供了DevSecOps解决方案。DevSecOps这个话题又很大,我就不展开细说了。这里还是做一个功能上的实践:
简介:
准备工作:
操作步骤:
通过.gitlab-ci.yaml
进行配置。
在文件中添加:
1 | # GitLab 安全扫描功能会自动识别编程语言,选择对应的分析工具进行扫描检测 |
运行流水线。
通过UI进行配置。
在项目“安全与合规”–“配置”中,选择不同的扫描工具,逐个进行配置。
运行流水线。
扫描报告:
在CICD流水线中,会出现“安全”、“许可证”选项卡,可查看代码安全扫描结果。
在MR页面中,会出现“安全扫描”选项卡,可查看代码安全扫描结果,用于辅助代码Review,提高Review效率。此外也可以在合并请求批准规则中启用“漏洞检查”、“许可证检查”,配置规则,配合MR实现自动安全门禁。
在项目“安全与合规”–“漏洞报告”页面,可汇总、过滤查看代码安全扫描的所有结果。可变更检测项状态,可忽略指定的检测结果。
至此,我们的过滤网补全计划基本完成。通过版本控制完成了基本流程建设、代码质量保障、审核与追踪体系。通过CICD提高了交付效率。最后通过代码扫描与CICD的集成,进一步提高了审核效率、代码质量和代码安全。随着这样一套模式的建立,相信您的团队已经进入了DevOps的新世界。新世界没有屎山林立,这里只有星辰大海。当然我们还记得家里的那些屎山还没处理。所以治完本,我们再看看如何治标。
不动如山(简单模式)
在处理屎山这件事上,还是奉劝各位千万不要冲动,毕竟十万卸岭兄弟都交代在路上了。绝大多数的屎山都是为了应急、演示形成的畸形产物,这种东西先天发育不良,几乎没有产品价值,一般过个把月就被扔在角落了,所以忘掉它,让它自生自灭吧。
当然也有少部分的屎山,经历了数年甚至数十年的风吹雨打,送走了一批又一批的开发团队。对于这种成精的屎山,是可以创造就业岗位的,专门组建一个团队,悉心照顾,24小时OnCall,头痛医头,脚痛医脚,必要时结合重启大法。团队的各位兄弟,你们都是天选之人,使命就是守卫这座大山。
改头换面(困难模式)
在这么多屎山中,也许有那么一两个还有产品价值,如果它病的不那么严重且规模不算很大(千万注意不要直接重构规模偏大的项目),建议直接换头吧。基于新体系的重构项目搞起来,好好考虑架构模式,重新做技术选型,我们是一个船新的版本。保留项目的名称,加上什么remastered、remake、reforged,或者v 2.0、v3.0,总之就是直接把山炸了。
精卫填海(地狱模式)
如果上面两种方式都不适用,那在走这条路之前,请务必做好心理建设。风萧萧兮易水寒,壮士,走好!
对于规模比较大的屎山项目,如果真的要去动它(真的真的要动么?),只能通过模块化替换,但一定一定要考虑:
依赖关系梳理、架构设计、技术选型、风险评估等等各种前期准备工作。
做好回退策略,给自己留好退路。
采用并行试运行模式,验证重构模块的稳定性。
总之解决增量问题是关键,消化存量问题需要根据实际情况具体分析,但一定不要贸然行动。希望大家都早日脱离苦海(虽然是不可能的)。
以上,完结撒花!
RSS介绍,摘自WIKI百科:
RSS(全称:RDF Site Summary;Really Simple Syndication),中文译作简易信息聚合,也称聚合内容,是一种消息来源格式规范,用以聚合经常发布更新资料的网站,例如博客文章、新闻、音频或视频的网摘。RSS文件(或称做摘要、网络摘要、或频更新,提供到频道)包含全文或是节录的文字,再加上发布者所订阅之网摘资料和授权的元数据。简单来说 RSS 能够让用户订阅个人网站个人博客,当订阅的网站有新文章时能够获得通知。
RSS摘要可以借由RSS阅读器、feed reader或是aggregator等网页或以桌面为架构的软件来阅读。标准的XML档式可允许信息在一次发布后透过不同的程序阅览。用户借由将网摘输入RSS阅读器或是用鼠标点取浏览器上指向订阅程序的RSS小图标之URI(非通常称为URL)来订阅网摘。RSS阅读器定期检阅是否有更新,然后下载给监看用户界面。
RSS的黄金年代早已随着2013年Google Reader的下线而逝去,随之而来的是各种聚合类新闻APP,它们利用实时推送提高了信息传递的速度,利用推荐算法实现了信息传递的精准度。然而在这个信息爆炸的时代,良莠不齐的内容、低俗偏激的评论,大数据+人工智能精心打造的信息茧房真的适合人们去阅读去思考吗?
对信息来源、信息内容、信息实时性有要求,希望操作界面简单、适合阅读、跨平台。在2021年,利用RSS打造这样一个信息聚合服务,依然是小众之选,但却可以获得更纯粹的阅读体验。
目前市面上依然活跃的RSS服务当属Feedly和Inoreader:
如果不愿意折腾,又有梯子,个人更建议使用成熟的第三方RSS服务。但我个人有轻量云服务器,所以最终选择使用RSSHUB+TTRSS自建RSS服务。
域名
RAM账号
SLL证书
使用Docker启动acme.sh,因为是daemon模式,容器会以最小资源保持运行,以便定期自动刷新证书
1 | 替换AK、SK |
进入容器,执行命令生成SSL通用域名证书
1 | 替换 -m 邮箱 |
RSSHUB的文档可参考 介绍 | RSSHub,可以直接使用RSSHUB的公共订阅源,但缓存时间为20分钟,意味着新信息刷新时间有20分钟延迟,此外因为某些网站反爬严格,公共订阅源会被ban,此时使用私有化部署就可以解决问题。使用私有化部署的订阅源,只需要替换RSSHUB官方订阅源的域名。如https://hub.yyyy.zz/36kr/news/lastest
1 | # 可修改redis挂载卷 /home/ubuntu/rsshub/redis |
参考以下方式进行部署,初始账号:admin,初始密码:password
1 | version: "3" |
可参考TTRSS的文档添加上一步私有化部署的RSS订阅源,进行主题设置等功能。
全文索引插件:用于获取原文
可在“偏好设置——插件”里开启mercury。
启用mercury后,还需在“订阅源——插件”中设置插件地址为localhost:3000
,然后在编辑订阅源中启用插件。
在TTRSS中使用mercury
Fever API插件:用于某些RSS客户端
可在“偏好设置——插件”里开启fever。
在“偏好设置——fever emulation”设置fever密码
在RSS客户端(见第6章)填写TTRSS域名的fefver api地址,如 https://rss.yyyy.zz/plugins/fever
2021年,符合界面美观、操作简洁的RSS客户端不多了,很多好用的APP如Press已经下架或者停更。好在RSS的小圈子依然有一些独立作者在开发。以下仅推荐几个个人觉得还不错的客户端。
Windows
iOS
Android
目前个人在Windows使用RSS追踪,Newsflow也不错,都需要开启TTRSS的Fever API。在Android使用FocusReader,知微也很符合使用习惯,但目前缺少字体设置功能。暂时不推荐Feedme,它的Mobilizer不是很好,页面经常出现混乱,此外图片也无法正常加载,待作者更新解决这些问题。
]]>面试必问:分布式事务六种解决方案 - 知乎 (zhihu.com)
10分钟说透Saga分布式事务 - 云+社区 - 腾讯云 (tencent.com)
是一种强一致性设计
引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段
正常情况
异常情况
协调者故障分析
总结
在参与者中也引入了超时机制
CanCommit、PreCommit 和 DoCommit
正常情况
优势
劣势
总结
TCC
思想上类似2PC,TCC 就是通过代码人为实现了两阶段提交
TCC模型还有个事务管理者的角色,变成多点,引入集群。用来记录TCC全局事务状态并提交或者回滚事务
引入超时,超时后进行补偿,并且不会锁定整个资源
优势
劣势
利用各系统本地的事务来实现分布式事务
有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候将业务的执行和将消息放入消息表中的操作放在同一个事务中,保证消息放入本地表中业务肯定是执行成功的
后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态
重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理
实现的是最终一致性
利用MQ事务
先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务
再根据本地事务的结果向 Broker 发送 Commit 或者RollBack命令
RocketMQ的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行Commit 或者RollBack命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可
如果是RollBack那么订阅方收不到这条消息,等于事务就没执行过
消息事务实现的也是最终一致性
长事务的解决方案,更适合于“业务流程长、业务流程多”的场景
特别是针对参与事务的服务是遗留系统服务,此类服务无法提供TCC模式下的三个接口,就可以采用Saga模式
针对每一个分布式事务的每个执行操作或者是步骤都是一个 Ti,例如扣减库存是T1、创建订单是T2、支付服务是T3。那么针对每个Ti都对应一个补偿动作Ci,例如回复库存C1、订单回滚C2、支付回滚C3
优势
劣势
两种恢复策略
向前恢复:对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功。这种方式适用于必须要成功的场景
向后恢复:在执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式。
两种模式
编排
控制
https://www.cnblogs.com/yiwangzhibujian/p/7047458.html
https://blog.csdn.net/striveb/article/details/95110502
https://mp.weixin.qq.com/s/2OTVJUTLOetYTD4Hpk-hFA
https://juejin.cn/post/6844903663224225806
纯内存操作
Redis是一个KV内存数据库,它内部构建了一个哈希表,根据指定的KEY访问时,只需要O(1)的时间复杂度就可以找到对应的数据。同时,Redis提供了丰富的数据类型,并使用高效的操作方式进行操作,这些操作都在内存中进行,并不会大量消耗CPU资源,所以速度极快。
单线程
使用IO多路复用技术
非CPU密集型任务
单点
主从
哨兵
sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
当master节点重新启动后,它将不再是master而是做为slave接收新的master节点的同步数据
当master节点挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中。
一个sentinel或sentinel集群可以管理多个主从Redis。
sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
sentinel监控的Redis集群都会定义一个master名字,这个名字代表Redis集群的master Redis
客户端就不要直接连接Redis,而是连接sentinel的ip和port
具有高可用性,但存储量不够时要选用集群模式
集群
sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能
适合数据量巨大的缓存要求,当数据量不是很大使用sentinel即可
群集至少需要3主3从
redis-master节点一般用于接收读写,而redis-slave节点则一般只用于备份,其与对应的master拥有相同的slot集合,若某个redis-master意外失效,则再将其对应的slave进行升级为临时redis-master
默认的,一般redis-master用于接收读写,而redis-slave则用于备份,当有请求是在向slave发起时,会直接重定向到对应key所在的master来处理。但如果不介意读取的是redis-cluster中有可能过期的数据并且对写请求不感兴趣时,则亦可通过readonly命令,将slave设置成可读,然后通过slave获取相关的key,达到读写分离
对Cluter读写有需求,可以水平扩展Master节点
单机上通过多线程建立新redis-master实例(一般线程数为CPU核数的倍数)
扩展更多的机器,部署新redis-master实例,如3主3从变成6主6从
redis-cluster进行新的水平扩容后,需要对master进行新的hash slot重新分配,这相当于需要重新加载所有的key,并按算法平均分配到各个Master的slot当中
maxmemory 设置最大内存,配合缓存释放策略
持久化
RDB
默认方式
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储
占用空间小,容易丢数据
AOF
Cache Aside Pattern(旁路缓存)
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
PS:要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。
Read/Write Through Pattern
可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache
Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。
Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)
Write Behind Caching Pattern
是Linux文件系统的Page Cache的算法
Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库
好处:I/O操作快(因为直接操作内存 ),因为异步,可以合并对同一个数据的多次操作,所以性能的提高是相当可观的
缺点:数据不是强一致性的,而且可能会丢失
B+树和Hash索引
B+树是左小右大的顺序存储结构,节点只包含id索引列,而叶子节点包含索引列和数据,这种数据和索引在一起存储的索引方式叫做聚簇索引,一张表只能有一个聚簇索引。假设没有定义主键,InnoDB会选择一个唯一的非空索引代替,如果没有的话则会隐式定义一个主键作为聚簇索引
非聚簇索引(二级索引)保存的是主键id值,这一点和myisam保存的是数据地址是不同的
区别
日志类型
binlog
binlog 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。 binlog 是 mysql的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志
binlog 是通过追加的方式进行写入的,可以通过 max_binlog_size 参数设置每个 binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志
binlog使用场景
binlog刷盘时机
对于 InnoDB 存储引擎而言,只有在事务提交时才会记录 binlog ,此时记录还在内存中,那么 binlog是什么时候刷到磁盘中的呢? mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机
binlog日志格式
redo log
持久性
redo log
在计算机操作系统中,用户空间(user space)下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间( kernel space )缓冲区( OS Buffer )。因此, redo log buffer 写入 redo log file 实际上是先写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file
innodb_flush_log_at_trx_commit 支持三种写入时机
undo log
MVCC 多版本并发控制
每行数实际上隐藏了两列,创建时间版本号,过期(删除)时间版本号,每开始一个新的事务,版本号都会自动递增
MVCC的原理是查找创建版本小于或等于当前事务版本,删除版本为空或者大于当前事务版本
select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);
在一次事务中,防止了数据的删除和修改导致的脏读,但不能防止新增
小明开启事务current_version=6查询名字为’王五’的记录,发现不存在
小红开启事务current_version=7插入一条数据,结果是这样
小明执行插入名字’王五’的记录,发现唯一索引冲突,无法插入,这就是幻读
WHERE id = 5 FOR UPDATE;
WHERE id BETWEEN 5 AND 7 FOR UPDATE;
流程
全同步复制
半同步复制
https://www.rabbitmq.com/cluster-formation.html#peer-discovery-dns
某一个 Queue 是在集群中的某一个 Broker 上,各个 Broker 会同步元数据,但不会同步 Queue 的消息数据
扩充 Broker 可以容纳更多的 Queue,提高吞吐量
没有达到高可用,扩展性较好
一个 Broker 中 Queue 的元数据和消息数据都会同步到其他 Broker 上,就是做了全量备份,所以称为 “镜像模式”
一个 Queue 的数据是全量存在 Broker 中的,所以 Queue 的消息容量、消息处理能力,都受限于 Broker
实现了高可用,但扩展性差
http://linyishui.top/2020101001.html
Federation 插件的设计目标是使 RabbitMQ 在不同的 Broker 节点之间进行消息传递而无须建立集群
Federation 插件可以让多个交换器或者多个队列进行联邦。一个联邦交换器(federated exchange)或者一个联邦队列(federated queue)接收上游(upstream)的消息,这里的上游是指位于其他 Broker 上的交换器或者队列
联邦交换器
Federation优化服务器通信网络延迟问题
联邦队列
可以在多个 Broker 节点(或者集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以连接一个或者多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息的需求
当有消费者 ClientA 连接 broker2 并通过 Basic.Consume 消费队列 queue1 (或 queue2 )中的消息时
如果此时队列已有消息堆积就可以直接被消费,且 broker2 的队列不会拉取 broker1 的队列中的消息
既可以消费联邦队列,又可以消费上游队列,这种分布式队列的部署可以提高单个队列的容量。如果上游一端部署的消费者来不及消费上游队列的消息,下游的消费者可以帮其分担消费,有一定的负载均衡的效果。
与联邦交换器不同的是,一条消息可以在联邦队列间转发无限次,因为队列可以互为联邦队列:消息会转向有多余消费能力的一方,所以可能会导致消费在队列间来回转发。
松耦合: Shovel 可以移动位于不同管理域中的 Broker(或者集群)上的消息,这些 Broker(或者集群)可以包含不同的用户和 vhost ,也可以使用不同的 RabbitMQ Erlang 版本。
支持广域网:Shovel 插件同样基于 AMQP 协议在 Broker 之间进行通信,被设计成可以容忍时断时续的连通情形,并且能够保证消息的可靠性。
高度定制:当 Shovel 成功连接后,可以对其进行配置以执行相关的 AMQP 命令。
Cluster:通常使用集群的部署方式来提高可靠性和吞吐量,不过集群只能部署在局域网内。
Federation:Federation 可以通过 AMQP 协议(可配置 SSL)让原本发送到某个 Broker (或集群)中的交换器(或队列)上的消息能够转发到另一个 Broker (或集群)中的交换器(或队列)上,两方的交换器(或队列)看起来是以一种“联邦”的形式在运作。当然必须要确保这些“联邦”的交换器或者队列都具备合适的用户和权限。一般用于异地多活。
Shovel:概念上 Federation 的情形类似,不过 Shovel 工作在更低一层。鉴于 Federation 从一个交换器中转发消息到另一个交换器(如果必要可以确认消息是否被转发), Shovel 只是简单地从某个 Broker 上的队列中消费消息,然后转发消息到 Broker 上的交换器而已。
kafka通过Zookeeper管理集群配置、选举leader、consumer group发生变化时进行rebalance
Apache Kafka 不是消息中间件的一种实现。相反,它只是一种分布式流式系统。
不同于基于队列和交换器的 RabbitMQ,Kafka 的存储层是使用分区事务日志来实现的。
Kafka 没有实现队列这种东西。相应的,Kafka 按照类别存储记录集,并且把这种类别称为主题。
Kafka 为每个主题维护一个消息分区日志。每个分区都是由有序的不可变的记录序列组成,并且消息都是连续的被追加在尾部。
Kafka 消费者使用pull模式
Kafka 把 Topic(主题/队列)分为了多个 Partition(分区),Topic 只是逻辑概念,Partition 才是实际的消息存储单元。
一个 Topic 的多个 Partition 分散在多个 Broker 中,每个 Partition 存放 Topic 的一部分数据。
Partition 的多个副本分为两种角色,Leader 和 Follower。
可以配置消息写入完成的标准
写入 Leader 既可:速度快,但可能会有消息丢失,例如在同步到 Follower 之前 Broker 故障了,则消息丢失。
写入 Follower 成功之后才算写入成功 :消息可靠性极高,但影响写入速度。
NameServer 集群:存放元数据。是一个几乎无状态节点,可集群部署,在消息队列RocketMQ版中提供命名服务,更新和发现Broker服务。
Broker 集群:存放队列数据。消息中转角色,负责存储消息,转发消息。分为Master Broker和Slave Broker,一个Master Broker可以对应多个Slave Broker,但是一个Slave Broker只能对应一个Master Broker。Broker启动后需要完成一次将自己注册至Name Server的操作;随后每隔30s定期向Name Server上报Topic路由信息。是物理概念每台机器要么是Master,要么是Slave。
生产者:与Name Server集群中的其中一个节点(随机)建立长连接(Keep-alive),定期从Name Server读取Topic路由信息,并向提供Topic服务的Master Broker建立长连接,且定时向Master Broker发送心跳。
消费者:与Name Server集群中的其中一个节点(随机)建立长连接,定期从Name Server拉取Topic路由信息,并向提供Topic服务的Master Broker、Slave Broker建立长连接,且定时向Master Broker、Slave Broker发送心跳。Consumer既可以从Master Broker订阅消息,也可以从Slave Broker订阅消息,订阅规则由Broker配置决定。
Topic:消息主题,一级消息类型,生产者向其发送消息。每个Topic包含一个或多个分区。
Tag:消息标签,二级消息类型,用来进一步区分某个Topic下的消息分类。
一个消费者集群对应一个Group ID,一个Group ID可以订阅多个Topic。
NameServer 集群独立运行
多主从
当 Master 故障之后,可以用 Slave 顶上去,数据和服务都不影响,但会有短暂的停顿,需要修改配置并重启才能完成切换动作。
数据同步的方式分为
异步:Master 写入完成即可,异步同步给 Slave。写入速度快,但同步会有延迟,可能会丢数据。
同步:Master 与 Slave 都写入之后才算成功。不会丢消息,但写入速度降低。
Dledger Group
是指一组相同名称的 Broker,至少需要 3 个节点,通过 Raft 自动选举出一个 Leader,其余节点 作为 Follower,并在 Leader 和 Follower 之间复制数据以保证高可用
RocketMQ 可以从组内选出一个新的 Master,完成自动切换
RabbitMQ
乱序
每个queue一个consumer,每个consumer单线程消费,效率慢
一个queue,只对应一个consumer,consumer内部用内存队列做排队,然后多线程消费
Kafka
写入一个 partition中的数据一定是有顺序的,比如订单id作为key,那么订单相关的数据,一定会被分发到一个 partition中区,此时这个 partition中的数据一定是有顺序的。消费者从partition中取出数据的时候 ,一定是有顺序的。
RocketMQ