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 CDK v1 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 do CDK Pipelines da Biblioteca de Constructos da AWS para configurar a entrega contínua de aplicativos do AWS CDK. Quando você confirma o código-fonte do seu aplicativo do CDK no AWS CodeCommit, no GitHub ou no 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 na Demonstração para desenvolvedores do CDK Pipelines. A outra é uma API moderna que incorpora o feedback dos clientes do CDK recebido durante a fase de pré-visualização. Os exemplos deste tópico usam a API moderna. Para obter detalhes sobre as diferenças entre as duas APIs com suporte, consulte API original do CDK Pipelines no repositório aws-cdk do GitHub.

Fazer bootstrapping em seus ambientes da AWS

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

Um CDK Pipeline 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 é uma prática recomendada isolar os estágios uns dos outros em ambientes diferentes.

nota

Consulte Bootstrapping do AWS CDK 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 do Amazon ECR.

  • Perfis 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 fazer o bootstrapping em um ambiente que possa provisionar um pipeline do AWS CDK, invoque cdk bootstrap conforme mostrado no exemplo a seguir. Invocar o AWS CDK Toolkit por meio do comando npx o instala temporariamente, se necessário. Ele também usará a versão do Toolkit instalada no projeto atual, se houver.

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

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 pipeline deve usar.

Você pode omitir a opção --profile se seu perfil da AWS padrão contiver a configuração de autenticação e Região da AWS necessárias.

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 fazer o bootstrapping em ambientes adicionais nos quais os aplicativos do AWS CDK serão implantados pelo pipeline, use os comandos a seguir. A opção --trust indica qual outra conta deve ter permissões para implantar aplicativos do AWS CDK nesse ambiente. Para essa opção, especifique o ID da conta da AWS do pipeline.

Novamente, você pode omitir a opção --profile se seu perfil da AWS padrão contiver a configuração de autenticação e Região da AWS necessárias.

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 fazer o bootstrapping e provisionar o pipeline inicial. Depois, use o próprio pipeline, não sua máquina local, para implantar as alterações.

Se você estiver atualizando um ambiente legado em que foi feito o bootstrapping, o bucket anterior do Amazon S3 ficará órfão quando o novo bucket for criado. Exclua-o manualmente usando o console do Amazon S3.

Proteger sua pilha de bootstrap contra exclusão

Se uma pilha de bootstrap for excluída, os recursos da AWS que foram originalmente provisionados no ambiente para oferecer suporte a implantações do 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 receber o bootstrapping, 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 comando cdk bootstrap novamente.

Para se proteger contra a exclusão acidental de sua pilha de bootstrap, recomendamos que você forneça a opção --termination-protection com o comando cdk bootstrap 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 a AWS CLI ou o console do CloudFormation para verificar.

Para habilitar a proteção contra encerramento
  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 console do CloudFormation ou a AWS CLI para verificar. Veja a seguir um exemplo de como usar a AWS CLI. Se você modificou o nome da pilha de bootstrap, substitua CDKToolkit pelo nome da pilha:

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

Inicializar um projeto

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

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

Você pode usar um nome diferente de my-pipeline no diretório principal do 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 principais do AWS CDK.

$ 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 existentes do Maven. 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 diretório src.

Go
$ cdk init app --language go

Depois que o aplicativo for criado, insira também o comando a seguir para instalar os módulos da Biblioteca de Constructos da AWS que o aplicativo exige.

$ go get
Importante

Envie seus arquivos cdk.json e cdk.context.json para o controle de origem. As informações de contexto (como sinalizadores de atributos e valores armazenados em cache recuperados da sua conta da AWS) 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 do 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á.

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

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

nota

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

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

TypeScript

Em lib/my-pipeline-stack.ts (pode variar se o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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 o nome da pasta do seu projeto não for my-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, verifique se o código no repositório é o código que você deseja implantar. Verifique suas alterações, envie para o GitHub e, 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 conta da AWS 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 é enviar para o GitHub.

Estágios de aplicativos

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

O estágio contém as pilhas do que compõem o 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 argumento env 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 multirregional ou DTAP.

Criaremos uma pilha contendo uma função do 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 do 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 estágio.

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 do 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 estágio.

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 do 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 estágio.

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 do 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 estágio.

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 do 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 estágio.

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 instância StageDeployment retornada pela chamada addStage(). Você pode adicionar ações de pré-implantação ou pós-implantação ao estágio chamando seu método addPre() ou addPost().

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

Teste de implantações

Você pode adicionar etapas a um pipeline do CDK para validar as implantações que você está realizando. Por exemplo, é possível usar o ShellStep da biblioteca do CDK Pipelines para realizar tarefas como as seguintes:

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

  • Verificar a configuração de um recurso implantado emitindo um comando da 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 implantações do AWS CloudFormation resultam na geração de recursos com nomes imprevisíveis. Por esse motivo, o CDK Pipelines fornece uma maneira de ler as saídas do AWS CloudFormation após uma implantação. Isso possibilita passar (por exemplo) o URL gerado de um balanceador de carga para uma ação de teste.

Para usar as saídas, exponha o objeto CfnOutput no qual você tem interesse. Em seguida, passe-o na propriedade envFromCfnOutputs 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 no ShellStep, 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 propriedade inputs. As entradas podem ser qualquer etapa que tenha uma saída, incluindo uma fonte (como um repositório do GitHub) ou outro 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 repositório do GitHub 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 Compartilhada da AWS, você é responsável pela segurança de suas informações na Nuvem AWS. A biblioteca do CDK Pipelines oferece uma vantagem inicial ao incorporar padrões seguros e práticas recomendadas 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 sua organização da AWS.

Em especial, considere o seguinte:

  • Leve em consideração o 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. O CDK Pipelines respeita package-lock.json e yarn.lock para garantir 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 burlar as verificações de conformidade. Se seu modelo de ameaça incluir desenvolvedores que escrevem código do CDK, você deve ter mecanismos externos de conformidade, como Hooks do AWS CloudFormation (preventivos) ou AWS Config (reativos), que a Função de Execução do AWS CloudFormation não tenha permissão para desativar.

  • As credenciais para ambientes de produção devem durar pouco. Após o bootstrapping 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 o token de acesso do GitHub. 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 uma ou mais entidades principais inválidas
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 fazer o bootstrapping em todos os seus ambientes de destino.

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 console do AWS CloudFormation e repita a implantação.