我有一个
CDK Pipelines
管道正在处理 ECS 上应用程序的自我突变和部署,但我很难弄清楚如何实现数据库迁移。
我的迁移文件以及迁移命令驻留在管道中构建和部署的 docker 容器内。以下是我迄今为止尝试过的两件事:
我的第一个想法只是在舞台上创造一个
pre
台阶,但我相信这是先有鸡还是先有蛋的情况。由于迁移命令要求数据库存在(以及具有端点和凭据)并且迁移步骤是 pre
,因此运行此命令时堆栈不存在...
const pipeline = new CodePipeline(this, "CdkCodePipeline", {
// ...
// ...
}
pipeline.addStage(applicationStage).addPre(new CodeBuildStep("MigrateDatabase", {
input: pipeline.cloudAssemblyFileSet,
buildEnvironment: {
environmentVariables: {
DB_HOST: { value: databaseProxyEndpoint },
// ...
// ...
},
privileged: true,
buildImage: LinuxBuildImage.fromAsset(this, 'Image', {
directory: path.join(__dirname, '../../docker/php'),
}),
},
commands: [
'cd /var/www/html',
'php artisan migrate --force',
],
}))
在上面的代码中,
databaseProxyEndpoint
涵盖了从 CfnOutput、SSM 参数到普通旧打字稿引用的所有内容,但由于值为空、缺失或尚未生成,所有内容都失败了。
我觉得这已经很接近了,因为它工作得很好,直到我尝试参考
databaseProxyEndpoint
。
我的第二次尝试是在ECS中创建一个init容器。
const migrationContainer = webApplicationLoadBalancer.taskDefinition.addContainer('init', {
image: ecs.ContainerImage.fromDockerImageAsset(webPhpDockerImageAsset),
essential: false,
logging: logger,
environment: {
DB_HOST: databaseProxy.endpoint,
// ...
// ...
},
secrets: {
DB_PASSWORD: ecs.Secret.fromSecretsManager(databaseSecret, 'password')
},
command: [
"sh",
"-c",
[
"php artisan migrate --force",
].join(" && "),
]
});
// Make sure migrations run and our init container return success
serviceContainer.addContainerDependencies({
container: migrationContainer,
condition: ecs.ContainerDependencyCondition.SUCCESS,
});
这有效,但我根本不喜欢。迁移命令应该在部署时的 ci/cd 管道中运行一次,而不是在 ECS 服务启动/重新启动或扩展时运行...我的迁移失败了一次,并且锁定了 cloudformation,因为运行状况检查在部署时和自然情况下都失败了回滚也会导致完全打破的疼痛循环。
任何关于如何解决这个问题的想法或建议都会让我免于失去剩下的头发!
您可以 (1) 使用自定义资源构造在堆栈部署内运行迁移,(2) 在使用
post
步骤部署堆栈或阶段之后,(3) 或在使用 EventBridge 规则运行管道之后运行迁移。
一种选择是将您的迁移定义为 CustomResource。它是一项 CloudFormation 功能,用于在堆栈部署生命周期中执行用户定义的代码(通常在 Lambda 中)。有关示例,请参阅@mchlfchr 的答案。另请考虑 CDK Trigger 构造,这是一种更高级别的自定义资源实现。
如果您将应用程序拆分为
StatefulStack
(数据库)和 StatelessStack
(应用程序容器),您可以将迁移代码作为两者之间的 post
Step 运行。这是OP中尝试的方法。
在变量生成器
StatefulStack
中,公开环境变量值的 CfnOutput
实例变量:readonly databaseProxyEndpoint: CfnOutput
。然后通过将变量作为 post
传递到 envFromCfnOutputs
步骤来使用管道迁移操作中的变量。 CDK 会将它们合成到 CodePipeline Variables:
pipeline.addStage(myStage, { // myStage includes the StatefulStack and StatelessStack instances
stackSteps: [
{
stack: statefulStack,
post: [
new pipelines.CodeBuildStep("Migrate", {
commands: [ 'cd /var/www/html', 'php artisan migrate --force',],
envFromCfnOutputs: { TABLE_ARN: stack1.tableArn },
// ... other step config
}),
],
},
],
post: // steps to run after the stage
});
addStage
方法的stackSteps选项在阶段中的特定堆栈之后运行后步骤。 post 选项的工作原理类似,但在舞台之后运行。
虽然这可能不是最佳选择,但您可以在管道执行后运行迁移。 CodePipeline 在管道执行期间发出事件。使用 EventBridge 规则,侦听
CodePipeline Pipeline Execution State Change
事件,其中 "state": "SUCCEEDED"
。
故障模式注意事项: 三个选项具有不同的故障模式。如果作为自定义资源的迁移失败,
StatefulStack
部署将失败(更改将回滚)并且管道执行将失败。如果迁移作为步骤实施,则管道执行将失败,但 StatefulStack
不会回滚。最后,如果迁移是事件触发的,则失败的迁移不会影响堆栈或执行,因为它们在迁移运行时已经完成。
我不会在 CDK 管道的构建步骤中解决它。
我宁愿采用
CustomResource
方法。
使用自定义资源,尤其是在 CDK 中,您始终了解依赖项以及何时需要运行它们。
这在 CDK Pipeline 上下文中完全丢失,您需要自己找出/实现。
那么,自定义资源是什么样的?
// this lambda function is an example definition, where you would run your actual migration commands
const migrationFunction = new lambda.Function(this, 'MigrationFunction', {
runtime: lambda.Runtime.PROVIDED_AL2,
code: lambda.Code.fromAsset('path/to/migration.ts'),
layers: [
// find the layers here:
// https://bref.sh/docs/runtimes/#lambda-layers-in-details
// https://bref.sh/docs/runtimes/#layer-version-
lambda.LayerVersion.fromLayerVersionArn(this, 'BrefPHPLayer', 'arn:aws:lambda:us-east-1:209497400698:layer:php-80:21')
],
timeout: cdk.Duration.seconds(30),
memorySize: 256,
});
const migrationFunctionProvider = new Provider(this, 'MigrationProvider', {
onEventHandler: migrationFunction,
});
new CustomResource(this, 'MigrationCustomResource', {
serviceToken: migrationFunctionProvider.serviceToken,
properties: {
date: new Date(Date.now()).toUTCString(),
},
});
}
// grant your migration lambda the policies to read secrets for your DB connection etc.
// migration.ts
import child_process from 'child_process';
import AWS from 'aws-sdk';
const sm = new AWS.SecretsManager();
export const handler = async (event, context) => {
// an event provides more flexibility than env vars
const { dbName, secretName } = event;
// Retrieve the database credentials from AWS Secrets Manager
const secret = await sm.getSecretValue({ SecretId: secretName }).promise();
const { username, password } = JSON.parse(secret.SecretString);
// Run the migration command with the database credentials
const command = `php artisan migrate --database=mysql --host=your-database-host --port=3306 --database=${dbName} --username=${username} --password=${password}`;
child_process.exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
};
Custom-Resource
采用您的迁移 lambda 函数。
Lambda 运行实际命令来执行数据库迁移。
每次运行部署时都会应用自定义资源。
这是通过 date
值应用的。
您可以通过更改 CustomResource
中的任何属性来控制执行。
我还做了另一件事,那就是在构建步骤之后、部署步骤之前添加 lambda 调用操作。 lambda 创建一个 ECS 任务定义(使用新创建的容器映像)并运行它。 lambda 等待 ECS 任务完成运行,然后在完成后报告作业成功。
到目前为止,这对我来说效果很好,并且 lambda 函数使用与我在本地运行临时任务和针对环境进行迁移相同的代码。