Integración y entrega continuas (CI/CD) mediante CDK Pipelines - AWS Cloud Development Kit (AWS CDK) v2

Esta es la guía para AWS CDK desarrolladores de la versión 2. La versión anterior del CDK v1 entró en mantenimiento el 1 de junio de 2022 y dejó de ofrecer soporte el 1 de junio de 2023.

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Integración y entrega continuas (CI/CD) mediante CDK Pipelines

Utilice el módulo CDK Pipelines de la biblioteca Construct para configurar AWS la entrega continua de aplicaciones. AWS CDK Cuando depositas el código fuente de tu aplicación de CDK en AWS CodeCommit, o GitHub AWS CodeStar, CDK Pipelines puede compilar, probar e implementar automáticamente tu nueva versión.

Los CDK Pipelines se actualizan automáticamente. Si agrega etapas o pilas de aplicaciones, la canalización se reconfigura automáticamente para implementar esas nuevas etapas o pilas.

nota

CDK Pipelines admite dos API. Una es la API original que estaba disponible en la versión preliminar para desarrolladores de CDK Pipelines. La otra es una API moderna que incorpora los comentarios de los clientes de CDK recibidos durante la fase de vista previa. Los ejemplos de este tema utilizan la API moderna. Para obtener más información sobre las diferencias entre las dos API compatibles, consulta la API original de CDK Pipelines en el repositorio aws-cdk. GitHub

Inicie sus AWS entornos

Antes de poder usar CDK Pipelines, debe iniciar AWS el entorno en el que desplegará sus stacks.

Una canalización de CDK incluye al menos dos entornos. El primer entorno es donde se aprovisiona la canalización. El segundo entorno es donde desea implementar las pilas o etapas de la aplicación (las etapas son grupos de pilas relacionadas). Estos entornos pueden ser los mismos, pero una práctica recomendada es aislar las etapas unas de otras en entornos diferentes.

nota

Consulte Bootstrapping (Proceso de arranque) para obtener más información sobre los tipos de recursos que se crean mediante el arranque y sobre cómo personalizar la pila de arranque.

El despliegue continuo con CDK Pipelines requiere que se incluya lo siguiente en la pila de CDK Toolkit:

  • Un bucket de Amazon Simple Storage Service (Amazon S3)

  • Un repositorio de Amazon ECR.

  • Funciones de IAM para conceder a las distintas partes de una canalización los permisos que necesitan.

El kit de herramientas CDK actualizará tu pila de bootstrap existente o creará una nueva si es necesario.

Para iniciar un entorno que pueda aprovisionar una AWS CDK canalización, invoque cdk bootstrap como se muestra en el siguiente ejemplo. Si se invoca el AWS CDK kit de herramientas mediante el npx comando, se instala temporalmente si es necesario. También utilizará la versión del kit de herramientas instalada en el proyecto actual, si existe.

--cloudformation-execution-policiesespecifica el ARN de una política según la cual se ejecutarán los despliegues de CDK Pipelines en el futuro. La AdministratorAccess política predeterminada garantiza que su canalización pueda implementar todos los tipos de recursos. AWS Si utilizas esta política, asegúrate de confiar en todo el código y las dependencias que componen tu AWS CDK aplicación.

La mayoría de las organizaciones exigen controles más estrictos sobre los tipos de recursos que se pueden implementar mediante la automatización. Consulta con el departamento correspondiente de tu organización para determinar la política que debe utilizar tu canal.

Puede omitir --profile esta opción si su AWS perfil predeterminado contiene la configuración de autenticación necesaria y Región de AWS.

macOS/Linux
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE \ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
Windows
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE ^ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess

Para iniciar entornos adicionales en los que la canalización desplegará AWS CDK las aplicaciones, utilice los siguientes comandos en su lugar. La --trust opción indica qué otra cuenta debe tener permisos para implementar AWS CDK aplicaciones en este entorno. Para esta opción, especifique el ID de AWS cuenta de la canalización.

De nuevo, puedes omitir la --profile opción si tu AWS perfil predeterminado contiene la configuración de autenticación necesaria y Región de AWS.

macOS/Linux
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE \ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ --trust PIPELINE-ACCOUNT-NUMBER
Windows
npx cdk bootstrap aws://ACCOUNT-NUMBER/REGION --profile ADMIN-PROFILE ^ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess ^ --trust PIPELINE-ACCOUNT-NUMBER
sugerencia

Utilice las credenciales administrativas únicamente para iniciar y aprovisionar la canalización inicial. Después, utilice la propia canalización, no su máquina local, para implementar los cambios.

Si está actualizando un entorno de arranque heredado, el depósito anterior de Amazon S3 quedará huérfano cuando se cree el nuevo depósito. Elimínelo manualmente mediante la consola Amazon S3.

Proteja su pila de bootstrap para que no se elimine

Si se elimina una pila de bootstrap, también se eliminarán los AWS recursos que se aprovisionaron originalmente en el entorno para respaldar las implementaciones de CDK. Esto hará que la canalización deje de funcionar. Si esto sucede, no existe una solución general para la recuperación.

Una vez iniciado el entorno, no elimine ni vuelva a crear la pila de arranque del entorno. En su lugar, intente actualizar la pila de arranque a una nueva versión ejecutando el comando de nuevo. cdk bootstrap

Para evitar que se elimine accidentalmente la pila de bootstrap, le recomendamos que proporcione la --termination-protection opción junto con el cdk bootstrap comando para habilitar la protección de terminación. Puede activar la protección de terminación en pilas de bootstrap nuevas o existentes. Para obtener más información sobre esta opción, consulte --termination-protection.

Tras activar la protección de terminación, puede utilizar la CloudFormation consola AWS CLI o para realizar la verificación.

Para activar la protección de terminación
  1. Ejecute el siguiente comando para habilitar la protección de terminación en una pila de bootstrap nueva o existente:

    $ cdk bootstrap --termination-protection
  2. Use la CloudFormation consola AWS CLI o para verificarlo. A continuación, se muestra un ejemplo mediante la AWS CLI. Si has modificado el nombre de tu pila de bootstrap, CDKToolkit sustitúyelo por el nombre de tu pila:

    $ aws cloudformation describe-stacks --stack-name CDKToolkit --query "Stacks[0].EnableTerminationProtection" true

Inicializa un proyecto

Cree un GitHub proyecto nuevo y vacío y clónelo en su estación de trabajo en el my-pipeline directorio. (Nuestros ejemplos de código de este tema utilizan GitHub. También puede usar AWS CodeStar o AWS CodeCommit.)

git clone GITHUB-CLONE-URL my-pipeline cd my-pipeline
nota

Puedes usar un nombre que no sea my-pipeline el del directorio principal de la aplicación. Sin embargo, si lo hace, tendrá que modificar los nombres de los archivos y las clases más adelante en este tema. Esto se debe a que el AWS CDK kit de herramientas basa algunos nombres de archivos y clases en el nombre del directorio principal.

Tras la clonación, inicialice el proyecto como de costumbre.

TypeScript
$ cdk init app --language typescript
JavaScript
$ cdk init app --language javascript
Python
$ cdk init app --language python

Una vez creada la aplicación, introduzca también los dos comandos siguientes. Estos activan el entorno virtual Python de la aplicación e instalan las dependencias AWS CDK principales.

$ source .venv/bin/activate # On Windows, run `.\venv\Scripts\activate` instead $ python -m pip install -r requirements.txt
Java
$ cdk init app --language java

Si utilizas un IDE, ahora puedes abrir o importar el proyecto. En Eclipse, por ejemplo, elija Archivo > Importar > Maven > Proyectos Maven existentes. Asegúrese de que los ajustes del proyecto estén configurados para utilizar Java 8 (1.8).

C#
$ cdk init app --language csharp

Si utiliza Visual Studio, abra el archivo de la solución en el src directorio.

Go
$ cdk init app --language go

Una vez creada la aplicación, introduzca también el siguiente comando para instalar los módulos de AWS Construct Library que requiere la aplicación.

$ go get
importante

Asegúrese de asignar sus archivos cdk.json y cdk.context.json archivos al control de código fuente. La información contextual (como los indicadores de características y los valores en caché recuperados de tu AWS cuenta) forma parte del estado de tu proyecto. Los valores pueden ser diferentes en otro entorno, lo que puede provocar cambios inesperados en los resultados. Para obtener más información, consulte Contexto del tiempo de ejecución.

Defina una canalización

Su aplicación CDK Pipelines incluirá al menos dos pilas: una que represente la propia canalización y una o más pilas que representen la aplicación desplegada a través de ella. Las pilas también se pueden agrupar en etapas, que puede utilizar para implementar copias de las pilas de infraestructura en diferentes entornos. Por ahora, consideraremos la canalización y, más adelante, profundizaremos en la aplicación que se implementará.

La construcción CodePipelinees la construcción que representa una canalización de CDK que se utiliza AWS CodePipeline como motor de despliegue. Al crear una instancia CodePipeline en una pila, se define la ubicación de origen de la canalización (por ejemplo, un GitHub repositorio). También defines los comandos para crear la aplicación.

Por ejemplo, lo siguiente define una canalización cuya fuente se almacena en un GitHub repositorio. También incluye un paso de creación de una aplicación de TypeScript CDK. Complete la información sobre su GitHub repositorio donde se indica.

nota

De forma predeterminada, la canalización se autentica GitHub con un token de acceso personal almacenado en Secrets Manager con ese nombregithub-token.

También tendrás que actualizar la instanciación de la pila de canalizaciones para especificar la AWS cuenta y la región.

TypeScript

En lib/my-pipeline-stack.ts (puede variar si no se nombra my-pipeline la carpeta de tu proyecto):

import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines'; export class MyPipelineStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); } }

En bin/my-pipeline.ts (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import { MyPipelineStack } from '../lib/my-pipeline-stack'; const app = new cdk.App(); new MyPipelineStack(app, 'MyPipelineStack', { env: { account: '111111111111', region: 'eu-west-1', } }); app.synth();
JavaScript

En lib/my-pipeline-stack.js (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

const cdk = require('aws-cdk-lib'); const { CodePipeline, CodePipelineSource, ShellStep } = require('aws-cdk-lib/pipelines'); class MyPipelineStack extends cdk.Stack { constructor(scope, id, props) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); } } module.exports = { MyPipelineStack }

En bin/my-pipeline.js (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

#!/usr/bin/env node const cdk = require('aws-cdk-lib'); const { MyPipelineStack } = require('../lib/my-pipeline-stack'); const app = new cdk.App(); new MyPipelineStack(app, 'MyPipelineStack', { env: { account: '111111111111', region: 'eu-west-1', } }); app.synth();
Python

En my-pipeline/my-pipeline-stack.py (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

import aws_cdk as cdk from constructs import Construct from aws_cdk.pipelines import CodePipeline, CodePipelineSource, ShellStep class MyPipelineStack(cdk.Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=ShellStep("Synth", input=CodePipelineSource.git_hub("OWNER/REPO", "main"), commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"] ) )

En app.py:

#!/usr/bin/env python3 import aws_cdk as cdk from my_pipeline.my_pipeline_stack import MyPipelineStack app = cdk.App() MyPipelineStack(app, "MyPipelineStack", env=cdk.Environment(account="111111111111", region="eu-west-1") ) app.synth()
Java

En src/main/java/com/myorg/MyPipelineStack.java (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

package com.myorg; import java.util.Arrays; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.pipelines.CodePipeline; import software.amazon.awscdk.pipelines.CodePipelineSource; import software.amazon.awscdk.pipelines.ShellStep; public class MyPipelineStack extends Stack { public MyPipelineStack(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(ShellStep.Builder.create("Synth") .input(CodePipelineSource.gitHub("OWNER/REPO", "main")) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build()) .build(); } }

En src/main/java/com/myorg/MyPipelineApp.java (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

package com.myorg; import software.amazon.awscdk.App; import software.amazon.awscdk.Environment; import software.amazon.awscdk.StackProps; public class MyPipelineApp { public static void main(final String[] args) { App app = new App(); new MyPipelineStack(app, "PipelineStack", StackProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build()); app.synth(); } }
C#

En src/MyPipeline/MyPipelineStack.cs (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

using Amazon.CDK; using Amazon.CDK.Pipelines; namespace MyPipeline { public class MyPipelineStack : Stack { internal MyPipelineStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) { var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = new ShellStep("Synth", new ShellStepProps { Input = CodePipelineSource.GitHub("OWNER/REPO", "main"), Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }) }); } } }

En src/MyPipeline/Program.cs (puede variar si la carpeta de tu proyecto no tiene nombremy-pipeline):

using Amazon.CDK; namespace MyPipeline { sealed class Program { public static void Main(string[] args) { var app = new App(); new MyPipelineStack(app, "MyPipelineStack", new StackProps { Env = new Amazon.CDK.Environment { Account = "111111111111", Region = "eu-west-1" } }); app.Synth(); } } }
Go
package main import ( "github.com/aws/aws-cdk-go/awscdk/v2" codebuild "github.com/aws/aws-cdk-go/awscdk/v2/awscodebuild" ssm "github.com/aws/aws-cdk-go/awscdk/v2/awsssm" pipeline "github.com/aws/aws-cdk-go/awscdk/v2/pipelines" "github.com/aws/constructs-go/constructs/v10" "github.com/aws/jsii-runtime-go" "os" ) // my CDK Stack with resources func NewCdkStack(scope constructs.Construct, id *string, props *awscdk.StackProps) awscdk.Stack { stack := awscdk.NewStack(scope, id, props) // create an example ssm parameter _ = ssm.NewStringParameter(stack, jsii.String("ssm-test-param"), &ssm.StringParameterProps{ ParameterName: jsii.String("/testparam"), Description: jsii.String("ssm parameter for demo"), StringValue: jsii.String("my test param"), }) return stack } // my CDK Application func NewCdkApplication(scope constructs.Construct, id *string, props *awscdk.StageProps) awscdk.Stage { stage := awscdk.NewStage(scope, id, props) _ = NewCdkStack(stage, jsii.String("cdk-stack"), &awscdk.StackProps{Env: props.Env}) return stage } // my CDK Pipeline func NewCdkPipeline(scope constructs.Construct, id *string, props *awscdk.StackProps) awscdk.Stack { stack := awscdk.NewStack(scope, id, props) // GitHub repo with owner and repository name githubRepo := pipeline.CodePipelineSource_GitHub(jsii.String("owner/repo"), jsii.String("main"), &pipeline.GitHubSourceOptions{ Authentication: awscdk.SecretValue_SecretsManager(jsii.String("my-github-token"), nil), }) // self mutating pipeline myPipeline := pipeline.NewCodePipeline(stack, jsii.String("cdkPipeline"), &pipeline.CodePipelineProps{ PipelineName: jsii.String("CdkPipeline"), // self mutation true - pipeline changes itself before application deployment SelfMutation: jsii.Bool(true), CodeBuildDefaults: &pipeline.CodeBuildOptions{ BuildEnvironment: &codebuild.BuildEnvironment{ // image version 6.0 recommended for newer go version BuildImage: codebuild.LinuxBuildImage_FromCodeBuildImageId(jsii.String("aws/codebuild/standard:6.0")), }, }, Synth: pipeline.NewCodeBuildStep(jsii.String("Synth"), &pipeline.CodeBuildStepProps{ Input: githubRepo, Commands: &[]*string{ jsii.String("npm install -g aws-cdk"), jsii.String("cdk synth"), }, }), }) // deployment of actual CDK application myPipeline.AddStage(NewCdkApplication(stack, jsii.String("MyApplication"), &awscdk.StageProps{ Env: targetAccountEnv(), }), &pipeline.AddStageOpts{ Post: &[]pipeline.Step{ pipeline.NewCodeBuildStep(jsii.String("Manual Steps"), &pipeline.CodeBuildStepProps{ Commands: &[]*string{ jsii.String("echo \"My CDK App deployed, manual steps go here ... \""), }, }), }, }) return stack } // main app func main() { defer jsii.Close() app := awscdk.NewApp(nil) // call CDK Pipeline NewCdkPipeline(app, jsii.String("CdkPipelineStack"), &awscdk.StackProps{ Env: pipelineEnv(), }) app.Synth(nil) } // env determines the AWS environment (account+region) in which our stack is to // be deployed. For more information see: https://docs.aws.amazon.com/cdk/latest/guide/environments.html func pipelineEnv() *awscdk.Environment { return &awscdk.Environment{ Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")), Region: jsii.String(os.Getenv("CDK_DEFAULT_REGION")), } } func targetAccountEnv() *awscdk.Environment { return &awscdk.Environment{ Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")), Region: jsii.String(os.Getenv("CDK_DEFAULT_REGION")), } }

Debes implementar una canalización manualmente una vez. Después de eso, la canalización se mantiene actualizada desde el repositorio de código fuente. Así que asegúrate de que el código del repositorio sea el código que deseas implementar. Registra los cambios, presiona para GitHub, luego, despliega:

git add --all git commit -m "initial commit" git push cdk deploy
sugerencia

Ahora que ha realizado la implementación inicial, su AWS cuenta local ya no necesita acceso administrativo. Esto se debe a que todos los cambios en tu aplicación se implementarán a través de la canalización. Lo único que tienes que hacer es presionar para GitHub.

Etapas de aplicación

Para definir una AWS aplicación de varias pilas que se pueda añadir a la canalización de una sola vez, defina una subclase de. Stage (Esto es diferente al del CdkStage módulo CDK Pipelines).

La etapa contiene las pilas que componen la aplicación. Si hay dependencias entre las pilas, las pilas se añaden automáticamente a la canalización en el orden correcto. Las pilas que no dependen unas de otras se despliegan en paralelo. Puede añadir una relación de dependencia entre las pilas mediante una llamada. stack1.addDependency(stack2)

Las etapas aceptan un env argumento predeterminado, que se convierte en el entorno predeterminado para las pilas que contiene. (Las pilas aún pueden tener su propio entorno especificado).

Para añadir una aplicación a la canalización, se llama addStage() con instancias de Stage. Se puede crear una instancia de una etapa y añadirla a la canalización varias veces para definir las distintas etapas de la canalización de aplicaciones de DTAP o multirregional.

Crearemos una pila que contenga una función Lambda simple y la colocaremos en una etapa. A continuación, añadiremos la etapa a la canalización para que se pueda implementar.

TypeScript

Cree el nuevo archivo lib/my-pipeline-lambda-stack.ts para almacenar nuestra pila de aplicaciones que contiene una función Lambda.

import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { Function, InlineCode, Runtime } from 'aws-cdk-lib/aws-lambda'; export class MyLambdaStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new Function(this, 'LambdaFunction', { runtime: Runtime.NODEJS_18_X, handler: 'index.handler', code: new InlineCode('exports.handler = _ => "Hello, CDK";') }); } }

Crea el nuevo archivo lib/my-pipeline-app-stage.ts para albergar nuestro escenario.

import * as cdk from 'aws-cdk-lib'; import { Construct } from "constructs"; import { MyLambdaStack } from './my-pipeline-lambda-stack'; export class MyPipelineAppStage extends cdk.Stage { constructor(scope: Construct, id: string, props?: cdk.StageProps) { super(scope, id, props); const lambdaStack = new MyLambdaStack(this, 'LambdaStack'); } }

Edita lib/my-pipeline-stack.ts para añadir el escenario a nuestra canalización.

import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines'; import { MyPipelineAppStage } from './my-pipeline-app-stage'; export class MyPipelineStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); pipeline.addStage(new MyPipelineAppStage(this, "test", { env: { account: "111111111111", region: "eu-west-1" } })); } }
JavaScript

Cree el nuevo archivo lib/my-pipeline-lambda-stack.js para almacenar nuestra pila de aplicaciones que contiene una función Lambda.

const cdk = require('aws-cdk-lib'); const { Function, InlineCode, Runtime } = require('aws-cdk-lib/aws-lambda'); class MyLambdaStack extends cdk.Stack { constructor(scope, id, props) { super(scope, id, props); new Function(this, 'LambdaFunction', { runtime: Runtime.NODEJS_18_X, handler: 'index.handler', code: new InlineCode('exports.handler = _ => "Hello, CDK";') }); } } module.exports = { MyLambdaStack }

Crea el nuevo archivo lib/my-pipeline-app-stage.js para albergar nuestro escenario.

const cdk = require('aws-cdk-lib'); const { MyLambdaStack } = require('./my-pipeline-lambda-stack'); class MyPipelineAppStage extends cdk.Stage { constructor(scope, id, props) { super(scope, id, props); const lambdaStack = new MyLambdaStack(this, 'LambdaStack'); } } module.exports = { MyPipelineAppStage };

Edita lib/my-pipeline-stack.ts para añadir el escenario a nuestra canalización.

const cdk = require('aws-cdk-lib'); const { CodePipeline, CodePipelineSource, ShellStep } = require('aws-cdk-lib/pipelines'); const { MyPipelineAppStage } = require('./my-pipeline-app-stage'); class MyPipelineStack extends cdk.Stack { constructor(scope, id, props) { super(scope, id, props); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); pipeline.addStage(new MyPipelineAppStage(this, "test", { env: { account: "111111111111", region: "eu-west-1" } })); } } module.exports = { MyPipelineStack }
Python

Cree el nuevo archivo my_pipeline/my_pipeline_lambda_stack.py para almacenar nuestra pila de aplicaciones que contiene una función Lambda.

import aws_cdk as cdk from constructs import Construct from aws_cdk.aws_lambda import Function, InlineCode, Runtime class MyLambdaStack(cdk.Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) Function(self, "LambdaFunction", runtime=Runtime.NODEJS_18_X, handler="index.handler", code=InlineCode("exports.handler = _ => 'Hello, CDK';") )

Crea el nuevo archivo my_pipeline/my_pipeline_app_stage.py para albergar nuestro escenario.

import aws_cdk as cdk from constructs import Construct from my_pipeline.my_pipeline_lambda_stack import MyLambdaStack class MyPipelineAppStage(cdk.Stage): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) lambdaStack = MyLambdaStack(self, "LambdaStack")

Edita my_pipeline/my-pipeline-stack.py para añadir el escenario a nuestra canalización.

import aws_cdk as cdk from constructs import Construct from aws_cdk.pipelines import CodePipeline, CodePipelineSource, ShellStep from my_pipeline.my_pipeline_app_stage import MyPipelineAppStage class MyPipelineStack(cdk.Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=ShellStep("Synth", input=CodePipelineSource.git_hub("OWNER/REPO", "main"), commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"])) pipeline.add_stage(MyPipelineAppStage(self, "test", env=cdk.Environment(account="111111111111", region="eu-west-1")))
Java

Cree el nuevo archivo src/main/java/com.myorg/MyPipelineLambdaStack.java para almacenar nuestra pila de aplicaciones que contiene una función Lambda.

package com.myorg; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.lambda.Function; import software.amazon.awscdk.services.lambda.Runtime; import software.amazon.awscdk.services.lambda.InlineCode; public class MyPipelineLambdaStack extends Stack { public MyPipelineLambdaStack(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineLambdaStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); Function.Builder.create(this, "LambdaFunction") .runtime(Runtime.NODEJS_18_X) .handler("index.handler") .code(new InlineCode("exports.handler = _ => 'Hello, CDK';")) .build(); } }

Crea el nuevo archivo src/main/java/com.myorg/MyPipelineAppStage.java para albergar nuestro escenario.

package com.myorg; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.Stage; import software.amazon.awscdk.StageProps; public class MyPipelineAppStage extends Stage { public MyPipelineAppStage(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineAppStage(final Construct scope, final String id, final StageProps props) { super(scope, id, props); Stack lambdaStack = new MyPipelineLambdaStack(this, "LambdaStack"); } }

Edita src/main/java/com.myorg/MyPipelineStack.java para añadir el escenario a nuestra canalización.

package com.myorg; import java.util.Arrays; import software.constructs.Construct; import software.amazon.awscdk.Environment; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.StageProps; import software.amazon.awscdk.pipelines.CodePipeline; import software.amazon.awscdk.pipelines.CodePipelineSource; import software.amazon.awscdk.pipelines.ShellStep; public class MyPipelineStack extends Stack { public MyPipelineStack(final Construct scope, final String id) { this(scope, id, null); } public MyPipelineStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); final CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(ShellStep.Builder.create("Synth") .input(CodePipelineSource.gitHub("OWNER/REPO", "main")) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build()) .build(); pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); } }
C#

Cree el nuevo archivo src/MyPipeline/MyPipelineLambdaStack.cs para almacenar nuestra pila de aplicaciones que contiene una función Lambda.

using Amazon.CDK; using Constructs; using Amazon.CDK.AWS.Lambda; namespace MyPipeline { class MyPipelineLambdaStack : Stack { public MyPipelineLambdaStack(Construct scope, string id, StackProps props=null) : base(scope, id, props) { new Function(this, "LambdaFunction", new FunctionProps { Runtime = Runtime.NODEJS_18_X, Handler = "index.handler", Code = new InlineCode("exports.handler = _ => 'Hello, CDK';") }); } } }

Crea el nuevo archivo src/MyPipeline/MyPipelineAppStage.cs para albergar nuestro escenario.

using Amazon.CDK; using Constructs; namespace MyPipeline { class MyPipelineAppStage : Stage { public MyPipelineAppStage(Construct scope, string id, StageProps props=null) : base(scope, id, props) { Stack lambdaStack = new MyPipelineLambdaStack(this, "LambdaStack"); } } }

Edita src/MyPipeline/MyPipelineStack.cs para añadir el escenario a nuestra canalización.

using Amazon.CDK; using Constructs; using Amazon.CDK.Pipelines; namespace MyPipeline { public class MyPipelineStack : Stack { internal MyPipelineStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) { var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = new ShellStep("Synth", new ShellStepProps { Input = CodePipelineSource.GitHub("OWNER/REPO", "main"), Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }) }); pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); } } }

Cada etapa de la aplicación que addStage() añada implica la adición de una etapa de canalización correspondiente, representada por una StageDeploymentinstancia devuelta por la addStage() llamada. Puede añadir acciones previas o posteriores a la implementación a la etapa llamando a su método addPre() oaddPost().

TypeScript
// import { ManualApprovalStep } from 'aws-cdk-lib/pipelines'; const testingStage = pipeline.addStage(new MyPipelineAppStage(this, 'testing', { env: { account: '111111111111', region: 'eu-west-1' } })); testingStage.addPost(new ManualApprovalStep('approval'));
JavaScript
// const { ManualApprovalStep } = require('aws-cdk-lib/pipelines'); const testingStage = pipeline.addStage(new MyPipelineAppStage(this, 'testing', { env: { account: '111111111111', region: 'eu-west-1' } })); testingStage.addPost(new ManualApprovalStep('approval'));
Python
# from aws_cdk.pipelines import ManualApprovalStep testing_stage = pipeline.add_stage(MyPipelineAppStage(self, "testing", env=cdk.Environment(account="111111111111", region="eu-west-1"))) testing_stage.add_post(ManualApprovalStep('approval'))
Java
// import software.amazon.awscdk.pipelines.StageDeployment; // import software.amazon.awscdk.pipelines.ManualApprovalStep; StageDeployment testingStage = pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); testingStage.addPost(new ManualApprovalStep("approval"));
C#
var testingStage = pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); testingStage.AddPost(new ManualApprovalStep("approval"));

Puede añadir etapas a una ola para desplegarlas en paralelo, por ejemplo, al implementar una etapa en varias cuentas o regiones.

TypeScript
const wave = pipeline.addWave('wave'); wave.addStage(new MyApplicationStage(this, 'MyAppEU', { env: { account: '111111111111', region: 'eu-west-1' } })); wave.addStage(new MyApplicationStage(this, 'MyAppUS', { env: { account: '111111111111', region: 'us-west-1' } }));
JavaScript
const wave = pipeline.addWave('wave'); wave.addStage(new MyApplicationStage(this, 'MyAppEU', { env: { account: '111111111111', region: 'eu-west-1' } })); wave.addStage(new MyApplicationStage(this, 'MyAppUS', { env: { account: '111111111111', region: 'us-west-1' } }));
Python
wave = pipeline.add_wave("wave") wave.add_stage(MyApplicationStage(self, "MyAppEU", env=cdk.Environment(account="111111111111", region="eu-west-1"))) wave.add_stage(MyApplicationStage(self, "MyAppUS", env=cdk.Environment(account="111111111111", region="us-west-1")))
Java
// import software.amazon.awscdk.pipelines.Wave; final Wave wave = pipeline.addWave("wave"); wave.addStage(new MyPipelineAppStage(this, "MyAppEU", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); wave.addStage(new MyPipelineAppStage(this, "MyAppUS", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("us-west-1") .build()) .build()));
C#
var wave = pipeline.AddWave("wave"); wave.AddStage(new MyPipelineAppStage(this, "MyAppEU", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); wave.AddStage(new MyPipelineAppStage(this, "MyAppUS", new StageProps { Env = new Environment { Account = "111111111111", Region = "us-west-1" } }));

Probar despliegues

Puede añadir pasos a una canalización de CDK para validar las implementaciones que está realizando. Por ejemplo, puede utilizar la biblioteca CDK Pipeline ShellStep para realizar tareas como las siguientes:

  • Intentando acceder a una Amazon API Gateway recién implementada y respaldada por una función Lambda

  • Comprobar la configuración de un recurso desplegado mediante la emisión de un comando AWS CLI

En su forma más simple, añadir acciones de validación tiene el siguiente aspecto:

TypeScript
// stage was returned by pipeline.addStage stage.addPost(new ShellStep("validate", { commands: ['../tests/validate.sh'], }));
JavaScript
// stage was returned by pipeline.addStage stage.addPost(new ShellStep("validate", { commands: ['../tests/validate.sh'], }));
Python
# stage was returned by pipeline.add_stage stage.add_post(ShellStep("validate", commands=[''../tests/validate.sh''] ))
Java
// stage was returned by pipeline.addStage stage.addPost(ShellStep.Builder.create("validate") .commands(Arrays.asList("'../tests/validate.sh'")) .build());
C#
// stage was returned by pipeline.addStage stage.AddPost(new ShellStep("validate", new ShellStepProps { Commands = new string[] { "'../tests/validate.sh'" } }));

Muchas AWS CloudFormation implementaciones dan como resultado la generación de recursos con nombres impredecibles. Por ello, los CDK Pipelines proporcionan una forma de AWS CloudFormation leer los resultados después de una implementación. Esto permite pasar (por ejemplo) la URL generada de un balanceador de cargas a una acción de prueba.

Para usar los resultados, muestra el CfnOutput objeto que te interesa. A continuación, páselo a la envFromCfnOutputs propiedad de un paso para que esté disponible como variable de entorno dentro de ese paso.

TypeScript
// given a stack lbStack that exposes a load balancer construct as loadBalancer this.loadBalancerAddress = new cdk.CfnOutput(lbStack, 'LbAddress', { value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` }); // pass the load balancer address to a shell step stage.addPost(new ShellStep("lbaddr", { envFromCfnOutputs: {lb_addr: lbStack.loadBalancerAddress}, commands: ['echo $lb_addr'] }));
JavaScript
// given a stack lbStack that exposes a load balancer construct as loadBalancer this.loadBalancerAddress = new cdk.CfnOutput(lbStack, 'LbAddress', { value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` }); // pass the load balancer address to a shell step stage.addPost(new ShellStep("lbaddr", { envFromCfnOutputs: {lb_addr: lbStack.loadBalancerAddress}, commands: ['echo $lb_addr'] }));
Python
# given a stack lb_stack that exposes a load balancer construct as load_balancer self.load_balancer_address = cdk.CfnOutput(lb_stack, "LbAddress", value=f"https://{lb_stack.load_balancer.load_balancer_dns_name}/") # pass the load balancer address to a shell step stage.add_post(ShellStep("lbaddr", env_from_cfn_outputs={"lb_addr": lb_stack.load_balancer_address} commands=["echo $lb_addr"]))
Java
// given a stack lbStack that exposes a load balancer construct as loadBalancer loadBalancerAddress = CfnOutput.Builder.create(lbStack, "LbAddress") .value(String.format("https://%s/", lbStack.loadBalancer.loadBalancerDnsName)) .build(); stage.addPost(ShellStep.Builder.create("lbaddr") .envFromCfnOutputs( // Map.of requires Java 9 or later java.util.Map.of("lbAddr", loadBalancerAddress)) .commands(Arrays.asList("echo $lbAddr")) .build());
C#
// given a stack lbStack that exposes a load balancer construct as loadBalancer loadBalancerAddress = new CfnOutput(lbStack, "LbAddress", new CfnOutputProps { Value = string.Format("https://{0}/", lbStack.loadBalancer.LoadBalancerDnsName) }); stage.AddPost(new ShellStep("lbaddr", new ShellStepProps { EnvFromCfnOutputs = new Dictionary<string, CfnOutput> { { "lbAddr", loadBalancerAddress } }, Commands = new string[] { "echo $lbAddr" } }));

Puede escribir pruebas de validación sencillas directamente en elShellStep, pero este enfoque se vuelve difícil de manejar cuando la prueba consta de más de unas pocas líneas. Para pruebas más complejas, puede incorporar archivos adicionales (como scripts de shell completos o programas en otros lenguajes) a ShellStep través de la propiedad. inputs Las entradas pueden ser cualquier paso que tenga una salida, incluida una fuente (como un GitHub repositorio) u otraShellStep.

Incorporar archivos del repositorio fuente es adecuado si los archivos se pueden utilizar directamente en la prueba (por ejemplo, si ellos mismos son ejecutables). En este ejemplo, declaramos nuestro GitHub repositorio como source (en lugar de instanciarlo en línea como parte del). CodePipeline Luego, pasamos este conjunto de archivos tanto a la canalización como a la prueba de validación.

TypeScript
const source = CodePipelineSource.gitHub('OWNER/REPO', 'main'); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: source, commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); const stage = pipeline.addStage(new MyPipelineAppStage(this, 'test', { env: { account: '111111111111', region: 'eu-west-1' } })); stage.addPost(new ShellStep('validate', { input: source, commands: ['sh ../tests/validate.sh'] }));
JavaScript
const source = CodePipelineSource.gitHub('OWNER/REPO', 'main'); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: new ShellStep('Synth', { input: source, commands: ['npm ci', 'npm run build', 'npx cdk synth'] }) }); const stage = pipeline.addStage(new MyPipelineAppStage(this, 'test', { env: { account: '111111111111', region: 'eu-west-1' } })); stage.addPost(new ShellStep('validate', { input: source, commands: ['sh ../tests/validate.sh'] }));
Python
source = CodePipelineSource.git_hub("OWNER/REPO", "main") pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=ShellStep("Synth", input=source, commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"])) stage = pipeline.add_stage(MyApplicationStage(self, "test", env=cdk.Environment(account="111111111111", region="eu-west-1"))) stage.add_post(ShellStep("validate", input=source, commands=["sh ../tests/validate.sh"], ))
Java
final CodePipelineSource source = CodePipelineSource.gitHub("OWNER/REPO", "main"); final CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(ShellStep.Builder.create("Synth") .input(source) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build()) .build(); final StageDeployment stage = pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); stage.addPost(ShellStep.Builder.create("validate") .input(source) .commands(Arrays.asList("sh ../tests/validate.sh")) .build());
C#
var source = CodePipelineSource.GitHub("OWNER/REPO", "main"); var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = new ShellStep("Synth", new ShellStepProps { Input = source, Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }) }); var stage = pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); stage.AddPost(new ShellStep("validate", new ShellStepProps { Input = source, Commands = new string[] { "sh ../tests/validate.sh" } }));

Obtener los archivos adicionales del paso de sintetizador es adecuado si es necesario compilar las pruebas, lo que se hace como parte de la síntesis.

TypeScript
const synthStep = new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'], }); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: synthStep }); const stage = pipeline.addStage(new MyPipelineAppStage(this, 'test', { env: { account: '111111111111', region: 'eu-west-1' } })); // run a script that was transpiled from TypeScript during synthesis stage.addPost(new ShellStep('validate', { input: synthStep, commands: ['node tests/validate.js'] }));
JavaScript
const synthStep = new ShellStep('Synth', { input: CodePipelineSource.gitHub('OWNER/REPO', 'main'), commands: ['npm ci', 'npm run build', 'npx cdk synth'], }); const pipeline = new CodePipeline(this, 'Pipeline', { pipelineName: 'MyPipeline', synth: synthStep }); const stage = pipeline.addStage(new MyPipelineAppStage(this, "test", { env: { account: "111111111111", region: "eu-west-1" } })); // run a script that was transpiled from TypeScript during synthesis stage.addPost(new ShellStep('validate', { input: synthStep, commands: ['node tests/validate.js'] }));
Python
synth_step = ShellStep("Synth", input=CodePipelineSource.git_hub("OWNER/REPO", "main"), commands=["npm install -g aws-cdk", "python -m pip install -r requirements.txt", "cdk synth"]) pipeline = CodePipeline(self, "Pipeline", pipeline_name="MyPipeline", synth=synth_step) stage = pipeline.add_stage(MyApplicationStage(self, "test", env=cdk.Environment(account="111111111111", region="eu-west-1"))) # run a script that was compiled during synthesis stage.add_post(ShellStep("validate", input=synth_step, commands=["node test/validate.js"], ))
Java
final ShellStep synth = ShellStep.Builder.create("Synth") .input(CodePipelineSource.gitHub("OWNER/REPO", "main")) .commands(Arrays.asList("npm install -g aws-cdk", "cdk synth")) .build(); final CodePipeline pipeline = CodePipeline.Builder.create(this, "pipeline") .pipelineName("MyPipeline") .synth(synth) .build(); final StageDeployment stage = pipeline.addStage(new MyPipelineAppStage(this, "test", StageProps.builder() .env(Environment.builder() .account("111111111111") .region("eu-west-1") .build()) .build())); stage.addPost(ShellStep.Builder.create("validate") .input(synth) .commands(Arrays.asList("node ./tests/validate.js")) .build());
C#
var synth = new ShellStep("Synth", new ShellStepProps { Input = CodePipelineSource.GitHub("OWNER/REPO", "main"), Commands = new string[] { "npm install -g aws-cdk", "cdk synth" } }); var pipeline = new CodePipeline(this, "pipeline", new CodePipelineProps { PipelineName = "MyPipeline", Synth = synth }); var stage = pipeline.AddStage(new MyPipelineAppStage(this, "test", new StageProps { Env = new Environment { Account = "111111111111", Region = "eu-west-1" } })); stage.AddPost(new ShellStep("validate", new ShellStepProps { Input = synth, Commands = new string[] { "node ./tests/validate.js" } }));

Notas de seguridad

Cualquier forma de entrega continua conlleva riesgos de seguridad inherentes. Según el modelo de responsabilidad AWS compartida, usted es responsable de la seguridad de su información en la AWS nube. La biblioteca CDK Pipelines le ofrece una ventaja inicial al incorporar valores predeterminados seguros y mejores prácticas de modelado.

Sin embargo, por su propia naturaleza, una biblioteca que necesita un alto nivel de acceso para cumplir con el propósito previsto no puede garantizar una seguridad completa. Existen muchos vectores de ataque fuera AWS de su organización.

En particular, tenga en cuenta lo siguiente:

  • Tenga en cuenta el software del que depende. Examina todo el software de terceros que utilices en proceso, ya que puede cambiar la infraestructura que se implementa.

  • Usa el bloqueo de dependencias para evitar actualizaciones accidentales. CDK Pipelines package-lock.json respeta yarn.lock y se asegura de que sus dependencias sean las que usted espera.

  • CDK Pipelines se ejecuta con recursos creados en tu propia cuenta, y la configuración de esos recursos la controlan los desarrolladores que envían el código a través de la canalización. Por lo tanto, CDK Pipelines por sí solo no puede proteger a los desarrolladores malintencionados que intentan eludir las comprobaciones de conformidad. Si su modelo de amenazas incluye desarrolladores que escriben código CDK, debe contar con mecanismos de cumplimiento externos, como AWS CloudFormation Hooks (preventivo) o AWS Config (reactivo), que el rol de AWS CloudFormation ejecución no tenga permiso para deshabilitar.

  • Las credenciales para los entornos de producción deben ser efímeras. Tras el arranque y el aprovisionamiento inicial, no es necesario que los desarrolladores tengan credenciales de cuenta en absoluto. Los cambios se pueden implementar a través del proceso. Reduzca la posibilidad de que las credenciales se filtren al no necesitarlas en primer lugar.

Resolución de problemas

Los siguientes problemas suelen surgir al empezar a usar CDK Pipelines.

Tubería: fallo interno
CREATE_FAILED  | AWS::CodePipeline::Pipeline | Pipeline/Pipeline
Internal Failure

Comprueba tu token de GitHub acceso. Puede que falte o que no tenga los permisos para acceder al repositorio.

Clave: la política contiene una declaración con uno o más principios no válidos
CREATE_FAILED | AWS::KMS::Key | Pipeline/Pipeline/ArtifactsBucketEncryptionKey
Policy contains a statement with one or more invalid principals.

Uno de los entornos de destino no se ha iniciado con la nueva pila de arranque. Asegúrese de que todos los entornos de destino estén iniciados.

La pila está en estado ROLLBACK_COMPLETE y no se puede actualizar.
Stack STACK_NAME is in ROLLBACK_COMPLETE state and can not be updated. (Service:
AmazonCloudFormation; Status Code: 400; Error Code: ValidationError; Request
ID: ...)

La pila falló en su implementación anterior y no se puede volver a intentar. Elimine la pila de la AWS CloudFormation consola y vuelva a intentar la implementación.