У меня есть конвейер CDK Pipelines
, который обрабатывает самоизменение и развертывание моего приложения в ECS, и мне трудно понять, как реализовать миграцию базы данных.
Мои файлы миграции, а также команда миграции находятся внутри док-контейнера, который создается и развертывается в конвейере. Ниже приведены две вещи, которые я пробовал до сих пор:
Моей первой мыслью было просто создать шаг 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.
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... Однажды мои миграции завершились неудачно, и это заблокировало облачное формирование, поскольку проверка работоспособности не удалась как при развертывании, так и естественным образом на откате, а также вызывает полностью разорванную петлю боли.
Любые идеи или предложения о том, как это сделать, спасут меня от потери оставшихся волос!
@fedonev в настоящее время у меня все в одном стеке, но здесь есть 100% гибкость, чтобы настроить или разделить стек, поскольку приложение еще не запущено. Что касается того, когда запускать, в идеальном мире мне бы хотелось, чтобы команда migrate запускалась один раз при создании базы данных, а затем один раз при каждом запуске конвейера. Команда будет запущена до развертывания ECS, поэтому миграция произойдет до того, как новые изменения кода вступят в силу. Спасибо
Я бы не стал решать это на этапе сборки конвейера CDK.
Скорее я бы выбрал подход CustomResource
.
С Custom Resources, особенно в CDK, вы всегда знаете о зависимостях и о том, когда вам нужно их запустить.
Это полностью теряется в контексте конвейера CDK, и вам нужно выяснить/реализовать его самостоятельно.
Итак, как же выглядит пользовательский ресурс?
// 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.info(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
};
Custom-Resource
принимает вашу миграционную лямбда-функцию.
Lambda запускает фактическую команду для переноса вашей базы данных.
Пользовательский ресурс применяется каждый раз при запуске развертывания.
Это применяется через значение date
.
Вы можете контролировать выполнение, изменяя любое свойство в CustomResource
.
Мне нравится этот подход, и он кажется самым простым, но команда artisan
, используемая для миграции базы данных (а также файлов миграции), основана на Laravel (php). Вероятно, я мог бы заставить что-то работать с bref - bref.sh - но, может быть, я что-то упускаю в этом контексте?
Ааа, извините! Пропустил эту информацию. У меня нет опыта работы с PHP в AWS, но я проверил некоторые документы. Bref — это способ использования PHP в качестве среды выполнения в Lambda. Я отредактирую свой ответ.
Вы можете выполнять миграции (1) в рамках развертывания стека с помощью конструкции Custom Resource, (2) после развертывания стека или этапа с помощью шага post
, (3) или после запуска конвейера с помощью правила EventBridge.
Один из вариантов — определить ваши миграции как CustomResource . Это функция CloudFormation для выполнения определяемого пользователем кода (обычно в Lambda) в течение жизненного цикла развертывания стека. См. Ответ @mchlfchr для примера. Также рассмотрите конструкцию CDK Trigger, реализацию пользовательского ресурса более высокого уровня.
Если вы разделите свое приложение, скажем, на StatefulStack
(базу данных) и StatelessStack
(контейнеры приложений), вы можете запустить свой код миграции как post
Шаг между ними. Это подход, предпринятый в 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
});
Параметр stackSteps метода addStage
запускает пост-шаги после определенного стека на этапе. Опция post работает аналогично, но запускается после этапа.
Хотя это, вероятно, не лучший вариант, вы можете запускать миграции после выполнения конвейера. CodePipeline генерирует события во время выполнения конвейера. С помощью правила EventBridge прослушивайте CodePipeline Pipeline Execution State Change
события, где "state": "SUCCEEDED"
.
Примечание о режимах отказа: три варианта имеют разные режимы отказа. Если миграция завершается сбоем в качестве пользовательского ресурса, развертывание StatefulStack
завершится ошибкой (с откатом изменений) и выполнение конвейера завершится ошибкой. Если миграция реализована как шаг, выполнение конвейера завершится ошибкой, но StatefulStack
не будет откатываться. Наконец, если миграция запускается по событию, неудачная миграция не повлияет ни на стек, ни на выполнение, поскольку они уже будут завершены, когда миграция запустится.
Отличный ответ, спасибо! Теперь мне интересно, возможно ли вообще №2? Срок миграции истекает, потому что он не может получить доступ к RDS (я думаю, это имеет смысл, потому что CodeBuild не находится в VPC, SG и т. д.). Кажется, я не могу добавить VPC из-за того, что он «пересекает границы этапа». Видел ваш ответ здесь stackoverflow.com/a/72010255/650241, который, похоже, объясняет проблему с № 2. Полагаю, я собираюсь попробовать № 1, если я что-то не упустил?
@GiovanniS Рад помочь. # 2 должен работать в вашем случае. CodeBuild может прекрасно работать с VPC с некоторой дополнительной конфигурацией. Точно так же ограничения в упомянутом вопросе, похоже, здесь не применяются. Ваш пост-шаг просто использует переменную CodePipeline из более раннего действия. При этом № 1 также является хорошим идиоматическим решением, если Lambda является вариантом для вашей задачи миграции.
База данных также развернута с помощью того же конвейера? Вы хотите, чтобы миграции выполнялись каждый раз, когда выполняется конвейер? Или запускать только при создании ресурса БД?