要敏捷,持续集成和持续部署必不可少。如何在 CICD 中对数据库进行变更呢?
原始社会版
手动操作。
上线服务的新版本前,将 SQL 准备好,连接数据库执行 SQL。
这样做不仅慢,还十分麻烦,又容易出错,让人提心吊胆。变更历史也难以维护,都2024年,建议永远别再用手动操作的方式来做数据库变更了吧!
在服务启动时自动执行数据库变更
开发时将数据库的变更脚本保存在代码库中,服务启动时自动读取这些变更脚本并和线上数据库的状态做对比,将差异应用上去。一般都有现成的库,对于开发者,只需要做调用现成的方法即可。
nodejs 示例
使用 sequelize
可以使用 sequelize,比如这个项目: https://github.com/Jeff-Tian/alpha/blob/master/package.json#L114 。其配置文件在: https://github.com/Jeff-Tian/alpha/blob/master/.sequelizerc,其数据库变更记录维护在 database 文件夹下:
对于每个数据库变更的脚本,都包含 up和 down两部分,比如其中一个是这样的:
javascript use strict
module.exports = { up: async (queryInterface, Sequelize) => { /* Add altering commands here. Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.createTable(users, { id: Sequelize.INTEGER });
*/
const {INTEGER, DATE, STRING, TEXT} = Sequelize
await queryInterface.createTable(user-passports, {
provider: STRING,
uid: STRING,
user_id: INTEGER,
created_at: DATE,
updated_at: DATE,
profile: TEXT,
})
},
down: async (queryInterface, Sequelize) => { /* Add reverting commands here. Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.dropTable(users);
*/
await queryInterface.dropTable(user-passports)
}, }
up是向前应用新的数据库变更,而 down则是在意外情况下需要回滚时,会执行到的。
要让应用在启动时自动执行数据库变更,就需要在应用里添加相关的代码。对于上面的示例,用了 egg js 框架,相关代码在 egg-sequelize 中,以插件形式运行。详细做法见: https://www.eggjs.org/zh-CN/tutorials/sequelize 。
使用 TypeORM
这里有一个 NestJs 项目示例,数据库变更工具使用了 TypeORM。
https://github.com/Jeff-Tian/uni-orders
详细做法可以参考: https://docs.nestjs.com/techniques/database#repository-pattern
Java 示例
比如可以采用 Flyway,在 pom.xml 里添加依赖:
xml
其数据库变更脚本统一维护在 db/migration 目录下:
dotnet 示例
使用 EntityFramework,这里有一个示例:https://github.com/jeff-tian/leg-godt 。其数据库变更也可以统一维护在一个文件夹下,如:
和 sequelize 比较像,数据库迁移脚本也有 Up 和 Down 两部分,如:
csharp using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Store.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: WecomCorps,
columns: table => new
{
Id = table.Column
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: WecomCorps);
}
}
}
在应用启动时,需要调用各个 DbContext 的 Database Migrate() 方法,如:
csharp
var context = app.ApplicationServices.GetService
context.Database.Migrate();
将数据库变更做为 CICD 过程中的一个单独步骤
以上实践方式,需要应用自己在启动时连接数据库并执行数据库变更,因此要保证应用能够有执行数据库模式变更的权限。如果希望限制应用的数据库权限,比如只允许做增删改查,而不能做数据库模式变更,那就得将数据库变更的步骤单独拿出来。
如以上图示,如果采用 GitHub Action,可以专门设置一个数据库变更的步骤,如下:
yaml run-db-migrations: needs: [ prepare-variables ] runs-on: 拥有连接数据库并执行数据库变更权限的 runner environment: ${{ needs.prepare-variables.outputs.deployment_environment }} name: Run DB Migrations timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v4
- name: Apply migrations
uses: ./.actions/db-migrations
with:
environment: ${{ needs.prepare-variables.outputs.deployment_environment }}
aws-region: aws 区域(这里假设数据库是托管在 AWS RDS 上)
aws-deployment-role-arn: ${{ secrets.AWS_DEPLOYMENT_ROLE_ARN }}
这里引用了 .actions 里的 db-migrations,其内容如下:
yaml
name: 执行数据库迁移
description: 检查是否有数据库迁移待执行,如果有就应用变更到数据库里
inputs:
aws-region:
description: 数据库托管所在的 AWS 区域
required: true
aws-deployment-role-arn:
description: AWS 角色,需要拥有 AWS secrets manager 和 RDS 的只读访问权限
required: true
runs:
using: composite
steps:
- name: 配置 AWS 凭据
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com.cn
role-to-assume: ${{ inputs.aws-deployment-role-arn }}
aws-region: ${{ inputs.aws-region }}
- name: 获取凭据
uses: aws-actions/aws-secretsmanager-get-secrets@v2.0.5
with:
secret-ids: |
DATABASE_凭据前缀_用来添加到环境变量,/AWS/secret/name-${{ inputs.environment }}-${{ inputs.aws-region }}
parse-json-secrets: true
- name: 获取 RDS Host
shell: bash
run: |
databaseHost=$(aws rds describe-db-clusters --output text --query DBClusters[?starts_with(Endpoint, xxxx-global-aurora-rdscluster)].{Endpoint:Endpoint})
echo ::add-mask::$databaseHost
echo DATABASE_HOST=$databaseHost >> $GITHUB_ENV
- name: 设置其他的凭据信息
shell: bash
run: |
echo DATABASE_PASSWORD=$DATABASE_PASSWORD >> $GITHUB_ENV
- name: 执行数据库迁移
id: run-db-migrations
shell: bash
run: |
dotnet run --project ./src/YourProject.Migrations/ --no-launch-profile
env:
SERVICE_VERSION: 1.1.1
SERVICE_NAME: your-migrations
SERVICE_PROJECT_NAME: your-project
ENVIRONMENT: ${{ inputs.environment }}
DATABASE_PORT: 3306
DATABASE_SSL_MODE: 1
DATABASE_IAM_AUTHENTICATION_ENABLED: false
DATABASE_CONNECTION_IDLE_TIMEOUT: 3600
DATABASE_CONNECTION_LIFETIME: 3600
DATABASE_NAME: 数据库名称
DATABASE_USERNAME: migrations-user
DATABASE_MAX_POOL_SIZE: 10
DATABASE_MIN_POOL_SIZE: 1
- name: 将总结写到 GitHub Actions 中
shell: bash
run: |
echo ${{ steps.run-db-migrations.outputs.summary-details }}. >> $GITHUB_STEP_SUMMARY
整个 Actions 代码比较长,但是核心其实就是
shell dotnet run --project ./src/YourProject.Migrations/ --no-launch-profile
与本地测试数据库变更时执行的是一样的脚本。
如何处理失败的情况?
我们并没有改变代码,所以这个问题和将数据库变更拿出来做为一个单独步骤之前的情况是一样的。
如果数据库迁移失败,我们需要手动评估情况,因为我们不知道哪里失败了以及数据库处于什么状态。在大多数情况下,SQL会被回滚,但这只会在数据库上下文中发生。如果迁移中的数据库上下文之间有依赖关系(我们应该避免这种情况),那么我们需要决定是回滚还是继续前进。错误也可能是逻辑上的,所以数据库迁移可能会成功。在这种情况下,我们可能需要创建一个热修复,并像常规发版一样部署它 - 但这取决于严重性、影响和紧急性。在最坏的情况下,应该将其视为涉及数据库恢复的灾难。
当然,我们还可以通过先在开发环境中发布,然后是QA,最后是生产环境这一策略来缓解一些风险。