微服务应用持续交付
我们公司有一个项目,采用 Spring Boot 框架,以微服务架构进行开发,源代码中分了十几个项目文件,项目之间还有依赖关系,因此我在搭建 CI/CD 管道时就要想办法解决以下三个问题
- 如何并行编译这些项目,避免过长的编译时间
- 如何在管道中处理项目之间的依赖关系
- 相似项目存在大量相似的任务,如何通过封装精简 YAML 文件
接下来我会讲解三个 Azure Pipeline 的进阶技巧,分别解决这几个问题
通过 Job 拆分自动编译任务
Azure pipeline 支持把一组任务放到一个 Job 里,一个管道中可以写多个 Job。多个 Job 就像多个管道那样可以并行运行,同时还可以在 Job 中定义局部变量和修改编译环境。
- job: API-Common
variables:
projectPath: 'Gty.Training.APICommon' # 定义 Job 中的局部变量
steps:
... # 此处省略
- job: Web
pool:
vmImage: 'windows-latest' # 修改 Web 编译环境为 Windows
variables:
projectPath: 'Gty.Training.WebDemo'
steps:
... # 此处省略
通过 Artifacts 处理项目依赖
使用 Job 除了刚刚提到的好处,还有一点我们可以在 Job 中加入 dependsOn 来指定运行顺序,如下两行脚本所示,API_User 中的任务会等 API_Common 这个 Job 完成后后再运行
- job: API-Common
variables:
projectPath: 'Gty.Training.APICommon'
steps:
... # 此处省略
- job: API-User
dependsOn: API-Common # 如果管道中找不到名为 API_Common 的 Job 会导致运行失败
variables:
projectPath: 'Gty.Training.APIUser'
steps:
... # 此处省略
指定好运行顺序,我们就可以先发布依赖项到 Artifacts ,让其他项目可以引用,我们以 Maven 项目为例演示如何运用 Artifacts 处理依赖
首先打开 Artifacts,点击新建 Feed
新建后点击 Connect to feed,可以参考如下的官方指引在本地修改 Maven 配置文件和项目的 pom.xml 文件,实现本地项目发布 Maven 包和引用 feed 中的包
在我的应用场景里,我需要实现不修改源代码解决依赖问题,同时我们也无法修改 Microsoft-host 的编译服务器中 Maven 的配置文件,如果这也符合你的需要,可以参考下面的步骤
对于被依赖项目,需要在 pom.xml 的
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
$a = [xml](Get-Content "pom.xml")
($a.project.ChildNodes | Where-Object { "repositories" -contains $_.Name }) | ForEach-Object { [void]$_.ParentNode.RemoveChild($_) }
$artifactRepo = [xml]"<repository><id>common-part</id><url>https://pkgs.dev.azure.com/Gtyrande/Gty.Training/_packaging/common-part/maven/v1</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository>"
$dist = $a.CreateElement("distributionManagement", $a.project.NamespaceURI)
$dist.RemoveAttribute("xmlns");
$dist.AppendChild($a.ImportNode($artifactRepo.repository, $true))
$a.project.AppendChild($dist)
$a.Save("pom.xml")
$b = get-content "pom.xml"
$b = $b -replace (' xmlns=""', "")
set-content -Path pom.xml -Value $b
errorActionPreference: 'continue'
workingDirectory: $(projectPath)
对于需要引用包的项目,需要在 pom.xml 的
$dist = $a.CreateElement("repositories", $a.project.NamespaceURI)
在官方引导中,还包括了对 settings.xml 文件的修改,这一步的用处是授予 Maven 访问 feed 的权限,在管道中直接使用 Maven Authenticate 任务即可
最后,使用 Maven 任务进行编译和发布包到 feed
- job: API-Common
variables:
projectPath: 'Gty.Training.APICommon'
steps:
- task: task: PowerShell@2
# 此处省略
- task: MavenAuthenticate@0
inputs:
artifactsFeeds: 'common-part'
- task: Maven@3
inputs:
mavenPomFile: '$(projectPath)/pom.xml'
goals: deploy
类似的,其他项目使用 Maven 任务进行编译,并通过 Artifacts 自动获取已发布的包
- job: API-User
variables:
projectPath: 'Gty.Training.APIUser'
artifactName: 'common-part'
steps:
- task: PowerShell@2
# 此处省略
- task: MavenAuthenticate@0
inputs:
artifactsFeeds: $(artifactName)
- task: Maven@3
inputs:
mavenPomFile: '$(projectPath)/pom.xml'
mavenOptions: '-Xmx8192m'
goals: 'clean package'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.8'
jdkArchitectureOption: 'x64'
在最后一步添加的 Maven Authenticate 任务是通过修改编译服务器上的 settings.xml 实现授权的。如果你还有其他要对 settings.xml 做的修改,可以参考以下手动授权的操作步骤
点击网站右上角的用户设置,进入 Personal access tokens
生成一个 token 并授予 Packaging - Read & write 的权限,token 的有效期最长为两年
注意在这一步复制并保存好 token,关闭后没地方复制,忘记就只能重新生成一个了
接下来我们新建一个 xml 文件
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>common-part</id>
<username>Gtyrande</username> <!-- Azure DevOps 组织的名字 -->
<password>xxxxxxxxx</password> <!-- 填入上一步生成的 token -->
</server>
</servers>
<!-- 你需要添加的其他修改 -->
</settings>
由于 token 属于机密信息,不建议上传到源代码中,我选择上传到 Pipelines - Library - Secure files 里
通过以下步骤在管道中获取并使用 settings.xml 文件
- task: DownloadSecureFile@1
name: mvnSettings
inputs:
secureFile: 'settings.xml'
- task: Maven@3
inputs:
mavenPomFile: '$(projectPath)/pom.xml'
goals: deploy
options: '-settings $(mvnSettings.secureFilePath)'
通过 Template 封装公共任务
上面讲解了如何处理有依赖关系的两个 Maven 项目,假如我有数十个 Maven 项目,把相似的任务复制粘贴,明显违反了 DRY 原则。那么 Azure pipeline 是否有方法想代码中的方法那样封装一段操作呢?答案是有的
我们把刚刚 common 以外项目的操作步骤封装到一个单独的 YAML 文件中来,定义好输入参数
# common-tasks.yml
parameters:
- name: projectPath # 在 steps 中通过 ${{ parameters.projectPath }} 引用传入的值
type: string
default: 'MyAPP'
- name: artifactName
type: string
default: 'MyArtifact'
steps:
- task: PowerShell@2
# 此处省略
- task: MavenAuthenticate@0
inputs:
artifactsFeeds: ${{ parameters.artifactName }}
- task: Maven@3
inputs:
mavenPomFile: '${{ parameters.projectPath }}/pom.xml'
# 此处省略
在管道中通过 template 任务来调用刚刚定义的 YAML 文件
- job: API-User
variables:
projectPath: 'Gty.Training.APIUser'
artifactName: 'common-part'
steps:
- template: common-tasks.yml # 绝对路径,此时代表它放在代码仓库根目录
parameters:
projectPath: $(projectPath)
artifactName: $(artifactName)
数据库持续交付
什么是 DDL, DML, DCL
数据定义语言(Data Defination Language, DDL),指对数据库中表结构进行操作的语句,例如 CREATE, ALERT 和 DROP
-- DDL in SQL Server 示例
-- 删除旧表,创建新表
IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[Users]') AND type IN ('U'))
DROP TABLE [dbo].[Users]
GO
CREATE TABLE [dbo].[Users] (
[id] int IDENTITY(1,1) NOT NULL,
[name] nvarchar(50),
[department_id] int,
CONSTRAINT PK_Users_id PRIMARY KEY CLUSTERED (id)
)
-- 修改数据类型
ALTER TABLE [dbo].[Users]
ALTER COLUMN [name] nvarchar(100)
-- 修改外键关系
ALTER TABLE [dbo].[Users]
ADD CONSTRAINT [FK_DepartmentUser]
FOREIGN KEY (department_id) REFERENCES [dbo].[Departments](id)
数据操纵语言(Data Manipulation Language, DML),指对数据库中数据进行操作的语句,例如 INSERT, UPDATE 和 DELETE
-- DML in SQL Server 示例
-- 初始化主数据
INSERT INTO [dbo].[Departments] ([id], [name])
VALUES(1, N'Information Technology')
-- 修改业务数据
UPDATE [dbo].[Messages]
SET [date_end] = '2020-11-20T15:59:59Z'
WHERE [department_id] = 1
数据库控制语言(Data Control Language, DCL),指对数据库中用户及权限进行操作的语句,例如 CREATE USER 和 ALTER ROLE
-- DCL in SQL Server 示例
-- 添加 Azure Mooncake AD 登录账号
CREATE USER [user@company.partner.onmschina.cn] FROM EXTERNAL PROVIDER;
-- 修改用户权限
ALTER ROLE db_datareader ADD MEMBER [user@company.partner.onmschina.cn];
EXEC sp_droprolemember
@rolename = N'db_datareader',
@membername = N'other@company.partner.onmschina.cn';
GO
数据库的部署即是把对数据库的修改实施到测试和生产数据库的过程:
- 通过 DDL 增删改数据库中的表,修改表结构和修改主外键
- 通过 DML 插入原始数据和必要时修整业务数据
- 通过 DCL 用于为不同的用户配置合适的角色
在实际的项目过程中,数据库权限是极少变化的,在需要时手动调整即可,因此本文仅涉及 DDL 和 DML 的编写和部署流程。
如何记录数据库变更
通过 DDL 和 DML 将数据库变更实施到测试和生产数据库的第一步,就是将数据库修改记录通过 DDL 和 DML 的方式记录下来。
传统流程中,数据库的变更一般由 VBA 来完成,此时有两种方式可以记录数据库的修改:
- VBA 直接编写 SQL 脚本来进行变更;
- VBA 手动操作数据库,通过 Redgate、Liquibase 等工具监控数据库修改,自动记录。
随着 ORM 框架的流行,很多团队中的数据库变更由后端开发人员通过 code first 的方式来进行变更
数据库部署的流程
根据实践经验,笔者推荐两种易于实施的数据库部署流程,不同的流程对于 DDL、DML 等脚本的要求也有一定的差异。
待补充
有的团队中,由 VBA 对数据库进行修改
有的团队中,由后端开发人员借助 ORM 框架,通过 code first 的方式修改数据库
在项目初期就引入数据库即代码的流程,且人手充足时
在项目中后期才实施,或没有足够的人力投入在数据库脚本的维护上,
根据项目的实际情况选择数据库部署的流程
应用数据库修改至生产环境应注意
生产环境业务数据需要修改时应注意
数据库部署示例
待补充
机器学习模型持续交付
待补充
单页应用持续交付
随着前端工程化的成熟发展,构建一个网站应用是非常容易实现的,三大前端框架都可以非常方便地通过 Webpack 构建出部署所需的静态文件。本节将会更进一步,讲解如何多环境部署单页应用。
环境变量的配置方法
传统上,我们在项目中定义不同环境所需的配置文件,在编译时通过指定 npm run build 的 -mode 参数来针对不同环境进行编译。
我们以 Vue 项目为例
更详细的信息可以参考官方文档。
多环境部署问题探讨
虽然可以通过 .env 文件实现针对不同环境进行编译,但这种方式并不符合一次编译多次部署的理念。多次编译造成了时间上的浪费,随着环境数量的增加(例如扩张到新市场,为新客户做定制化开发部署),维护配置文件的工作量会随之增加,这种方式应对变化的灵活性较低,容易出错。
但由于框架本身的限制,单页应用无法实现像 ASP.NET Core MVC 应用那样直接在运行时读取环境变量,这为灵活部署带来一定的挑战。本文将以 Vue 项目为例,介绍两种绕过限制的方法,尽管实现方式不尽优雅,却能在工程上获得足够的益处。
通过修改 HTML 标签的属性,注入环境变量
- 在 main.js 中添加代码,获取属性值,并设置为全局变量
- 在 API 帮助类中添加代码,获取全局变量并创建 axios 实例
- 在应用构建后,运行 bash 命令
- 通过 Docker 化网站应用,可以在镜像启动时注入环境变量
通过 public 文件夹中的 JSON 文件,注入环境变量
- 在 src/public 文件夹中新建一个 .json 文件,添加如下内容,编译时该文件会被原样复制到 dist 中
- 在 API 帮助类中添加代码,读取 .json 文件的内容,获取属性值并创建 axios 实例
- 在自动部署配置中创建 JSON 文件,填入环境变量,并替换 dist 中的文件
总结
本文讲解了一些数据库、机器学习模型、微服务应用和前端网站的持续交付实战技巧,希望能够对大家的日常工作有所帮助,更进一步地利用 DevOps 工具改进工程实践,更深刻地体会做持续改进应如何发力。