Integração e entrega contínuas (CI/CD) usando CDK Pipelines - AWS Cloud Development Kit (AWS CDK) v2

Este é o Guia do Desenvolvedor AWS CDK v2. A versão CDK 1 mais antiga entrou em manutenção em 1º de junho de 2022 e encerrou o suporte em 1º de junho de 2023.

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Integração e entrega contínuas (CI/CD) usando CDK Pipelines

Use o módulo CDK Pipelines da AWS Construct Library para configurar a entrega contínua de aplicativos. AWS CDK Quando você confirma o código-fonte do seu aplicativo CDK em AWS CodeCommitGitHub, ou AWS CodeStar, o CDK Pipelines pode criar, testar e implantar automaticamente sua nova versão.

O CDK Pipelines é atualizado automaticamente. Se você adicionar estágios ou pilhas de aplicativos, o pipeline se reconfigura automaticamente para implantar esses novos estágios ou pilhas.

nota

O CDK Pipelines oferece suporte a duas APIs. Uma delas é a API original que foi disponibilizada no CDK Pipelines Developer Preview. A outra é uma API moderna que incorpora o feedback dos clientes do CDK recebido durante a fase de pré-visualização. Os exemplos neste tópico usam a API moderna. Para obter detalhes sobre as diferenças entre as duas APIs compatíveis, consulte a API original do CDK Pipelines no repositório aws-cdk. GitHub

Inicialize seus ambientes AWS

Antes de usar o CDK Pipelines, você deve inicializar AWS o ambiente no qual implantará suas pilhas.

Um pipeline de CDK envolve pelo menos dois ambientes. O primeiro ambiente é onde o pipeline é provisionado. O segundo ambiente é onde você deseja implantar as pilhas ou estágios do aplicativo (os estágios são grupos de pilhas relacionadas). Esses ambientes podem ser os mesmos, mas uma recomendação de melhores práticas é isolar os estágios uns dos outros em ambientes diferentes.

nota

Consulte AWS CDK bootstrapping para obter mais informações sobre os tipos de recursos criados pelo bootstrap e como personalizar a pilha de bootstrap.

A implantação contínua com o CDK Pipelines exige que o seguinte seja incluído na pilha do CDK Toolkit:

  • Um bucket do Amazon Simple Storage Service (Amazon S3).

  • Um repositório Amazon ECR.

  • Funções do IAM para dar às várias partes de um pipeline as permissões de que elas precisam.

O CDK Toolkit atualizará sua pilha de bootstrap existente ou criará uma nova, se necessário.

Para inicializar um ambiente que possa provisionar um AWS CDK pipeline, invoque cdk bootstrap conforme mostrado no exemplo a seguir. Invocar o AWS CDK kit de ferramentas por meio do npx comando o instala temporariamente, se necessário. Ele também usará a versão do kit de ferramentas instalada no projeto atual, se houver.

--cloudformation-execution-policiesespecifica o ARN de uma política sob a qual as futuras implantações do CDK Pipelines serão executadas. A AdministratorAccess política padrão garante que seu pipeline possa implantar todo tipo de AWS recurso. Se você usar essa política, certifique-se de confiar em todos os códigos e dependências que compõem seu AWS CDK aplicativo.

A maioria das organizações exige controles mais rígidos sobre quais tipos de recursos podem ser implantados pela automação. Consulte o departamento apropriado da sua organização para determinar a política que seu funil deve usar.

Você pode omitir a --profile opção se seu AWS perfil padrão contiver a configuração de autenticação necessária e. Região da 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 inicializar ambientes adicionais nos quais os AWS CDK aplicativos serão implantados pelo pipeline, use os comandos a seguir. A --trust opção indica qual outra conta deve ter permissões para implantar AWS CDK aplicativos nesse ambiente. Para essa opção, especifique o ID da AWS conta do funil.

Novamente, você pode omitir a --profile opção se seu AWS perfil padrão contiver a configuração de autenticação necessária e. Região da 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
dica

Use credenciais administrativas somente para inicializar e provisionar o pipeline inicial. Depois, use o pipeline em si, não sua máquina local, para implantar as alterações.

Se você estiver atualizando um ambiente antigo inicializado, o bucket anterior do Amazon S3 ficará órfão quando o novo bucket for criado. Exclua-o manualmente usando o console do Amazon S3.

Protegendo sua pilha de bootstrap contra exclusão

Se uma pilha de bootstrap for excluída, os AWS recursos que foram originalmente provisionados no ambiente para suportar implantações de CDK também serão excluídos. Isso fará com que o pipeline pare de funcionar. Se isso acontecer, não há solução geral para recuperação.

Depois que seu ambiente for inicializado, não exclua e recrie a pilha de bootstrap do ambiente. Em vez disso, tente atualizar a pilha de bootstrap para uma nova versão executando o cdk bootstrap comando novamente.

Para se proteger contra a exclusão acidental de sua pilha de bootstrap, recomendamos que você forneça a --termination-protection opção com o cdk bootstrap comando para ativar a proteção contra encerramento. Você pode ativar a proteção contra encerramento em pilhas de bootstrap novas ou existentes. Para saber mais sobre essa opção, consulte--termination-protection.

Depois de ativar a proteção contra encerramento, você pode usar o CloudFormation console AWS CLI ou para verificar.

Para ativar a proteção contra rescisão
  1. Execute o comando a seguir para ativar a proteção contra encerramento em uma pilha de bootstrap nova ou existente:

    $ cdk bootstrap --termination-protection
  2. Use o CloudFormation console AWS CLI ou para verificar. Veja a seguir um exemplo de como usar a AWS CLI. Se você modificou o nome da pilha de bootstrap, CDKToolkit substitua pelo nome da pilha:

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

Inicializar um projeto

Crie um novo GitHub projeto vazio e clone-o em sua estação de trabalho no my-pipeline diretório. (Nossos exemplos de código neste tópico usam GitHub. Você também pode usar AWS CodeStar ou AWS CodeCommit.)

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

Você pode usar um nome diferente do my-pipeline diretório principal do seu aplicativo. No entanto, se você fizer isso, precisará ajustar os nomes dos arquivos e das classes posteriormente neste tópico. Isso ocorre porque o AWS CDK Toolkit baseia alguns nomes de arquivos e classes no nome do diretório principal.

Após a clonagem, inicialize o projeto normalmente.

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

Depois que o aplicativo for criado, insira também os dois comandos a seguir. Eles ativam o ambiente virtual Python do aplicativo e instalam as dependências AWS CDK principais.

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

Se você estiver usando um IDE, agora você pode abrir ou importar o projeto. No Eclipse, por exemplo, escolha Arquivo > Importar > Maven > Projetos Maven existentes. Certifique-se de que as configurações do projeto estejam definidas para usar o Java 8 (1.8).

C#
$ cdk init app --language csharp

Se você estiver usando o Visual Studio, abra o arquivo da solução no src diretório.

Go
$ cdk init app --language go

Depois que o aplicativo for criado, insira também o comando a seguir para instalar os módulos do AWS Construct Library que o aplicativo exige.

$ go get
Importante

Certifique-se de enviar seus arquivos cdk.json e cdk.context.json arquivos para o controle de origem. As informações de contexto (como sinalizadores de recursos e valores em cache recuperados da sua AWS conta) fazem parte do estado do seu projeto. Os valores podem ser diferentes em outro ambiente, o que pode causar alterações inesperadas nos resultados. Para ter mais informações, consulte Valores de contexto e o AWS CDK.

Definir um pipeline

Seu aplicativo CDK Pipelines incluirá pelo menos duas pilhas: uma que representa o próprio pipeline e uma ou mais pilhas que representam o aplicativo implantado por meio dele. As pilhas também podem ser agrupadas em estágios, que você pode usar para implantar cópias das pilhas de infraestrutura em diferentes ambientes. Por enquanto, consideraremos o pipeline e, posteriormente, nos aprofundaremos no aplicativo que ele implantará.

A construção CodePipelineé a construção que representa um pipeline de CDK usado AWS CodePipeline como mecanismo de implantação. Ao instanciar CodePipeline em uma pilha, você define o local de origem do pipeline (como um GitHub repositório). Você também define os comandos para criar o aplicativo.

Por exemplo, o seguinte define um pipeline cuja fonte é armazenada em um GitHub repositório. Também inclui uma etapa de criação para um aplicativo TypeScript CDK. Preencha as informações sobre seu GitHub repositório onde indicado.

nota

Por padrão, o pipeline se autentica GitHub usando um token de acesso pessoal armazenado no Secrets Manager sob o nomegithub-token.

Você também precisará atualizar a instanciação da pilha do pipeline para especificar a AWS conta e a região.

TypeScript

Em lib/my-pipeline-stack.ts (pode variar se a pasta do seu projeto não tiver um nomemy-pipeline):

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'] }) }); } }

Em bin/my-pipeline.ts (pode variar se a pasta do seu projeto não tiver um nomemy-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

Em lib/my-pipeline-stack.js (pode variar se a pasta do seu projeto não tiver um nomemy-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 }

Em bin/my-pipeline.js (pode variar se a pasta do seu projeto não tiver um nomemy-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

Em my-pipeline/my-pipeline-stack.py (pode variar se a pasta do seu projeto não tiver um nomemy-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"] ) )

Em 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

Em src/main/java/com/myorg/MyPipelineStack.java (pode variar se a pasta do seu projeto não tiver um nomemy-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(); } }

Em src/main/java/com/myorg/MyPipelineApp.java (pode variar se a pasta do seu projeto não tiver um nomemy-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#

Em src/MyPipeline/MyPipelineStack.cs (pode variar se a pasta do seu projeto não tiver um nomemy-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" } }) }); } } }

Em src/MyPipeline/Program.cs (pode variar se a pasta do seu projeto não tiver um nomemy-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")), } }

Você deve implantar um pipeline manualmente uma vez. Depois disso, o pipeline se mantém atualizado a partir do repositório do código-fonte. Portanto, certifique-se de que o código no repositório seja o código que você deseja implantar. Verifique suas alterações, envie para GitHub, em seguida, implante:

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

Agora que você fez a implantação inicial, sua AWS conta local não precisa mais de acesso administrativo. Isso ocorre porque todas as alterações em seu aplicativo serão implantadas por meio do pipeline. Tudo o que você precisa fazer é pressionar para GitHub.

Etapas de aplicação

Para definir um AWS aplicativo de várias pilhas que possa ser adicionado ao pipeline de uma só vez, defina uma subclasse de. Stage (Isso é diferente do CdkStage módulo CDK Pipelines.)

O estágio contém as pilhas que compõem seu aplicativo. Se houver dependências entre as pilhas, as pilhas serão adicionadas automaticamente ao pipeline na ordem correta. As pilhas que não dependem umas das outras são implantadas paralelamente. Você pode adicionar uma relação de dependência entre as pilhas chamando. stack1.addDependency(stack2)

Os estágios aceitam um env argumento padrão, que se torna o ambiente padrão para as pilhas dentro dele. (As pilhas ainda podem ter seu próprio ambiente especificado.).

Um aplicativo é adicionado ao pipeline chamando addStage() com instâncias de Stage. Um estágio pode ser instanciado e adicionado ao pipeline várias vezes para definir diferentes estágios do seu pipeline de aplicativos DTAP ou multirregional.

Criaremos uma pilha contendo uma função Lambda simples e colocaremos essa pilha em um estágio. Em seguida, adicionaremos o estágio ao pipeline para que ele possa ser implantado.

TypeScript

Crie o novo arquivo lib/my-pipeline-lambda-stack.ts para armazenar nossa pilha de aplicativos contendo uma função 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";') }); } }

Crie o novo arquivo lib/my-pipeline-app-stage.ts para manter nosso palco.

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'); } }

Edite lib/my-pipeline-stack.ts para adicionar o estágio ao nosso pipeline.

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

Crie o novo arquivo lib/my-pipeline-lambda-stack.js para armazenar nossa pilha de aplicativos contendo uma função 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 }

Crie o novo arquivo lib/my-pipeline-app-stage.js para manter nosso palco.

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 };

Edite lib/my-pipeline-stack.ts para adicionar o estágio ao nosso pipeline.

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

Crie o novo arquivo my_pipeline/my_pipeline_lambda_stack.py para armazenar nossa pilha de aplicativos contendo uma função 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';") )

Crie o novo arquivo my_pipeline/my_pipeline_app_stage.py para manter nosso palco.

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")

Edite my_pipeline/my-pipeline-stack.py para adicionar o estágio ao nosso pipeline.

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

Crie o novo arquivo src/main/java/com.myorg/MyPipelineLambdaStack.java para armazenar nossa pilha de aplicativos contendo uma função 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(); } }

Crie o novo arquivo src/main/java/com.myorg/MyPipelineAppStage.java para manter nosso palco.

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"); } }

Edite src/main/java/com.myorg/MyPipelineStack.java para adicionar o estágio ao nosso pipeline.

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#

Crie o novo arquivo src/MyPipeline/MyPipelineLambdaStack.cs para armazenar nossa pilha de aplicativos contendo uma função 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';") }); } } }

Crie o novo arquivo src/MyPipeline/MyPipelineAppStage.cs para manter nosso palco.

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"); } } }

Edite src/MyPipeline/MyPipelineStack.cs para adicionar o estágio ao nosso pipeline.

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 estágio do aplicativo adicionado por addStage() resulta na adição de um estágio de pipeline correspondente, representado por uma StageDeploymentinstância retornada pela addStage() chamada. Você pode adicionar ações de pré-implantação ou pós-implantação ao estágio chamando seu método addPre() ouaddPost().

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"));

Você pode adicionar estágios a um Wave para implantá-los paralelamente, por exemplo, ao implantar um estágio em várias contas ou regiões.

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" } }));

Testando implantações

Você pode adicionar etapas a um pipeline de CDK para validar as implantações que você está realizando. Por exemplo, você pode usar a biblioteca CDK Pipeline ShellStep para realizar tarefas como as seguintes:

  • Tentando acessar um Amazon API Gateway recém-implantado apoiado por uma função Lambda

  • Verificando a configuração de um recurso implantado emitindo um comando AWS CLI

Em sua forma mais simples, adicionar ações de validação tem a seguinte aparência:

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'" } }));

Muitas AWS CloudFormation implantações resultam na geração de recursos com nomes imprevisíveis. Por esse motivo, o CDK Pipelines fornece uma maneira de AWS CloudFormation ler as saídas após uma implantação. Isso possibilita passar (por exemplo) a URL gerada de um balanceador de carga para uma ação de teste.

Para usar saídas, exponha o CfnOutput objeto em que você está interessado. Em seguida, passe-o na envFromCfnOutputs propriedade de uma etapa para disponibilizá-lo como uma variável de ambiente nessa etapa.

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" } }));

Você pode escrever testes de validação simples diretamente noShellStep, mas essa abordagem se torna complicada quando o teste tem mais do que algumas linhas. Para testes mais complexos, você pode trazer arquivos adicionais (como scripts de shell completos ou programas em outras linguagens) para a propriedade ShellStep por meio da inputs propriedade. As entradas podem ser qualquer etapa que tenha uma saída, incluindo uma fonte (como um GitHub repositório) ou outra. ShellStep

Trazer arquivos do repositório de origem é apropriado se os arquivos puderem ser usados diretamente no teste (por exemplo, se eles próprios forem executáveis). Neste exemplo, declaramos nosso GitHub repositório como source (em vez de instanciá-lo em linha como parte do). CodePipeline Em seguida, passamos esse conjunto de arquivos para o pipeline e para o teste de validação.

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" } }));

Obter os arquivos adicionais da etapa de sintetização é apropriado se seus testes precisarem ser compilados, o que é feito como parte da síntese.

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" } }));

Observações de segurança

Qualquer forma de entrega contínua tem riscos de segurança inerentes. De acordo com o Modelo de Responsabilidade AWS Compartilhada, você é responsável pela segurança de suas informações na AWS nuvem. A biblioteca CDK Pipelines oferece uma vantagem inicial ao incorporar padrões seguros e melhores práticas de modelagem.

No entanto, por sua própria natureza, uma biblioteca que precisa de um alto nível de acesso para cumprir a finalidade pretendida não pode garantir segurança total. Há muitos vetores de ataque fora da AWS sua organização.

Em particular, tenha em mente o seguinte:

  • Esteja atento ao software do qual você depende. Verifique todos os softwares de terceiros que você executa em seu pipeline, pois eles podem mudar a infraestrutura que é implantada.

  • Use o bloqueio de dependências para evitar atualizações acidentais. A CDK Pipelines package-lock.json respeita yarn.lock e garante que suas dependências sejam as que você espera.

  • O CDK Pipelines é executado em recursos criados em sua própria conta, e a configuração desses recursos é controlada pelos desenvolvedores que enviam o código pelo pipeline. Portanto, o CDK Pipelines por si só não pode se proteger contra desenvolvedores mal-intencionados que tentam contornar as verificações de conformidade. Se seu modelo de ameaça incluir desenvolvedores que escrevem código CDK, você deve ter mecanismos externos de conformidade, como AWS CloudFormation Hooks (preventivos) ou AWS Config (reativos), que a AWS CloudFormation Função de Execução não tenha permissão para desativar.

  • As credenciais para ambientes de produção devem durar pouco. Após a inicialização e o provisionamento inicial, não é necessário que os desenvolvedores tenham as credenciais da conta. As mudanças podem ser implantadas por meio do pipeline. Reduza a possibilidade de vazamento de credenciais ao não precisar delas em primeiro lugar.

Solução de problemas

Os problemas a seguir são comumente encontrados ao começar a usar o CDK Pipelines.

Pipeline: falha interna
CREATE_FAILED  | AWS::CodePipeline::Pipeline | Pipeline/Pipeline
Internal Failure

Verifique seu token de GitHub acesso. Ele pode estar ausente ou pode não ter as permissões para acessar o repositório.

Chave: A política contém uma declaração com um ou mais princípios inválidos
CREATE_FAILED | AWS::KMS::Key | Pipeline/Pipeline/ArtifactsBucketEncryptionKey
Policy contains a statement with one or more invalid principals.

Um dos ambientes de destino não foi inicializado com a nova pilha de bootstrap. Certifique-se de que todos os seus ambientes de destino estejam inicializados.

A pilha está no estado ROLLBACK_COMPLETE e não pode ser atualizada.
Stack STACK_NAME is in ROLLBACK_COMPLETE state and can not be updated. (Service:
AmazonCloudFormation; Status Code: 400; Error Code: ValidationError; Request
ID: ...)

A pilha falhou em sua implantação anterior e está em um estado que não pode ser repetido. Exclua a pilha do AWS CloudFormation console e repita a implantação.