luizmachado.dev

PT EN

Sessão 012 — CDK: Context, feature flags e cdk.json production-grade

Duração estimada: 60 minutos
Pré-requisitos: session-011-cdk-custom-resources-aspects


Objetivo

Ao final, você conseguirá usar cdk.context.json para cachear lookups (VPCs, AMIs) sem depender de resolução em runtime, configurar feature flags para controlar comportamentos de migração, e estruturar cdk.json para equipes (o que vai no gitignore vs o que é versionado).


Contexto

[FATO] Uma das propriedades fundamentais de uma boa pipeline de infraestrutura é a reprodutibilidade: dado o mesmo código-fonte, dois engenheiros em máquinas diferentes ou dois builds de CI produzem exatamente o mesmo CloudFormation template. O CDK quebra essa propriedade se qualquer lookup de conta AWS é feito em tempo de síntese sem cache — porque a resposta pode mudar (uma nova AMI foi publicada, uma nova AZ foi habilitada) entre o build do desenvolvedor e o build do CI.

[FATO] O mecanismo que o CDK usa para tornar lookups reproduzíveis chama-se Context. Context é um dicionário chave-valor que a CLI do CDK popula automaticamente na primeira vez que um lookup é necessário e persiste em cdk.context.json. Nas sínteses seguintes, o valor cacheado é usado sem nova consulta à AWS. O arquivo cdk.context.json é versionado no repositório — essa é a garantia de reprodutibilidade.

[CONSENSO] Feature flags são uma sobreposição ao mesmo mecanismo de Context: são booleanos armazenados no cdk.json sob a chave "context" que controlam comportamentos do CDK que mudaram entre versões e que poderiam quebrar stacks existentes se ativados silenciosamente. Entendê-los é obrigatório ao migrar stacks de CDK v1 para v2, e ao atualizar projetos CDK v2 entre versões menores que introduziram mudanças opt-in.


Conceitos principais

1. O ciclo de vida de um Context lookup

Quando seu código CDK chama algo como ec2.Vpc.fromLookup(this, 'Vpc', { vpcName: 'prod-vpc' }), o CDK precisa consultar a conta AWS para descobrir o ID da VPC, suas subnets e AZs. Esse processo tem dois modos:

                    ┌─────────────────────────────────────────────┐
                    │  cdk synth (primeira vez, sem cache)        │
                    │                                             │
  Código CDK        │  1. Tentativa de síntese                   │
  Vpc.fromLookup()  │     → CDK detecta lookup pendente           │
        │           │     → Sintetiza com valores DUMMY           │
        │           │     → NÃO gera template válido              │
        │           │  2. CLI faz describe-vpcs na AWS            │
        │           │     → armazena em cdk.context.json          │
        │           │  3. CDK tenta síntese novamente             │
        │           │     → lookup resolvido do cache             │
        │           │     → template válido gerado                │
        └───────────┘

                    ┌─────────────────────────────────────────────┐
                    │  cdk synth (com cache / CI)                 │
                    │                                             │
  Código CDK        │  1. CDK lê cdk.context.json                │
  Vpc.fromLookup()  │     → lookup já resolvido                  │
        │           │  2. Template gerado diretamente             │
        │           │     → zero chamadas à AWS necessárias       │
        └───────────┘

[FATO] No modo de CI (sem credenciais de conta, ou com --no-lookups), se o cdk.context.json não tiver o valor cacheado, a síntese falha com erro explícito. Isso é intencional — garante que o CI não depende de acesso de escrita ao cache para funcionar.

[FATO] O conteúdo de cdk.context.json após um lookup de VPC se parece com isto:

{
  "vpc-provider:account=123456789012:filter.vpc-id=vpc-0abc:region=us-east-1:returnAsymmetricSubnets=true": {
    "vpcId": "vpc-0abc",
    "vpcCidrBlock": "10.0.0.0/16",
    "availabilityZones": ["us-east-1a", "us-east-1b", "us-east-1c"],
    "subnetGroups": [
      {
        "name": "Public",
        "type": "Public",
        "subnets": [
          { "subnetId": "subnet-pub1", "cidr": "10.0.0.0/24", "availabilityZone": "us-east-1a", "routeTableId": "rtb-1" }
        ]
      }
    ]
  }
}

A chave é deterministicamente gerada a partir dos parâmetros do lookup. Se você mudar qualquer parâmetro (ex: o vpcName), o CDK gera uma chave diferente e faz um novo lookup.


2. Lookups disponíveis nativamente no CDK

[FATO] O CDK oferece os seguintes lookups de conta de primeira classe (todos cacheados em cdk.context.json):

Método de lookup O que consulta na AWS Chave usada
ec2.Vpc.fromLookup() ec2:DescribeVpcs + ec2:DescribeSubnets filter params
ec2.MachineImage.lookup() ec2:DescribeImages filtros de AMI
ssm.StringParameter.valueFromLookup() ssm:GetParameter param name + account/region
HostedZone.fromLookup() route53:ListHostedZonesByName domínio
ec2.SecurityGroup.fromLookupByName() ec2:DescribeSecurityGroups nome + VPC ID

[FATO] ssm.StringParameter.valueFromLookup() é diferente de ssm.StringParameter.valueForStringParameter():

// valueFromLookup: resolve em tempo de síntese, cacheia em cdk.context.json
// → valor concreto no template; NUNCA use para secrets!
const amiId = ssm.StringParameter.valueFromLookup(this, '/shared/ami-id');
// resultado: "ami-0abc123" (string concreta)

// valueForStringParameter: resolve em tempo de deploy (CloudFormation)
// → gera um token {{resolve:ssm:/shared/ami-id}} no template
// → roda uma chamada SSM no momento do deploy, não da síntese
const endpoint = ssm.StringParameter.valueForStringParameter(this, '/shared/db-endpoint');
// resultado: Token (resolvido pelo CloudFormation)

[CONSENSO] Para parâmetros que mudam com frequência (configurações, endpoints de ambiente), prefira valueForStringParameter — o template sempre usa o valor mais recente ao ser deployado. Para parâmetros estáveis que são referenciados em constructs que precisam do valor em tempo de síntese (ex: ID de uma VPC para fazer subnets lookup), valueFromLookup é necessário.


3. Gerenciamento do cdk.context.json em equipes

[FATO] Segundo a documentação oficial do CDK: cdk.context.json deve ser versionado no repositório. A razão é que ambientes de CI frequentemente não têm (e não devem ter) credenciais com permissão de describe na conta de produção. O cache é a âncora de reprodutibilidade.

[FATO] O comando cdk context gerencia o cache:

# lista todas as entradas do cache
cdk context

# remove uma entrada específica (força re-lookup na próxima síntese)
cdk context --reset "vpc-provider:account=123456789012:..."

# limpa todo o cache
cdk context --clear

# força síntese ignorando lookups (falha se algum lookup não estiver cacheado)
cdk synth --no-lookups

[CONSENSO] A política recomendada pela comunidade CDK para times é:

cdk.json          → versionado (configuração do projeto, feature flags)
cdk.context.json  → versionado (cache de lookups — necessário para CI)
cdk.out/          → gitignore (artefatos de síntese, gerados automaticamente)
node_modules/     → gitignore (dependências npm)

Nunca coloque cdk.context.json no .gitignore. Se fizer isso, todo desenvolvedor novo e todo build de CI vai fazer lookups na conta, o que:
1. Exige credenciais com permissões elevadas no CI.
2. Torna o build não-determinístico (um novo recurso na conta pode mudar o resultado).
3. Lentifica a síntese (chamadas de API extras).


4. Context como mecanismo de configuração explícita

Além dos lookups automáticos, você pode usar Context como um sistema de configuração intencional — passando valores ao app via cdk.json, variáveis de ambiente ou CLI:

// lib/app.ts
const app = new App();

// Lê o ambiente alvo do context
// Pode ser passado via: cdk deploy --context env=production
const env = app.node.tryGetContext('env') ?? 'development';

const config = {
  development: { accountId: '111111111111', region: 'us-east-1', instanceType: 't3.micro' },
  staging:     { accountId: '222222222222', region: 'us-east-1', instanceType: 't3.small' },
  production:  { accountId: '333333333333', region: 'us-east-1', instanceType: 'm5.large' },
};

const targetEnv = config[env as keyof typeof config];
if (!targetEnv) throw new Error(`Ambiente inválido: ${env}`);

new ApiStack(app, `Api-${env}`, {
  env: { account: targetEnv.accountId, region: targetEnv.region },
  instanceType: new ec2.InstanceType(targetEnv.instanceType),
});

No cdk.json, você pode definir o valor padrão:

{
  "app": "npx ts-node --prefer-ts-exts bin/app.ts",
  "context": {
    "env": "development"
  }
}

[FATO] A precedência de Context é, da mais alta para a mais baixa:
1. --context key=value na linha de comando da CLI
2. ~/.cdk.json (arquivo global do usuário)
3. cdk.context.json (cache de lookups no projeto)
4. cdk.json (configuração do projeto)
5. app.node.setContext() no código


5. Feature Flags: o que são e quando importam

[FATO] Feature flags no CDK são booleanos de Context que têm o prefixo de módulo como namespace (ex: @aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy). Eles controlam comportamentos que foram alterados em alguma versão do CDK mas que precisavam ser opt-in para não quebrar stacks existentes.

[FATO] No CDK v2, os feature flags históricos do v1 foram todos habilitados por padrão. Mas o CDK v2 continua introduzindo novos feature flags para mudanças que afetam stacks existentes. Quando você cria um novo projeto com cdk init, o cdk.json gerado inclui todos os feature flags da versão atual ativados — isso garante que novos projetos usam o comportamento mais moderno.

Exemplo de cdk.json gerado por cdk init (versão ~2.100+):

{
  "app": "npx ts-node --prefer-ts-exts bin/myapp.ts",
  "watch": {
    "include": ["**"],
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "**/*.js",
      "node_modules",
      "test"
    ]
  },
  "context": {
    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
    "@aws-cdk/core:checkSecretUsage": true,
    "@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
    "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
    "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
    "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
    "@aws-cdk/aws-iam:minimizePolicies": true,
    "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
    "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
    "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
    "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
    "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
    "@aws-cdk/core:enablePartitionLiterals": true,
    "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
    "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
    "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
    "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
    "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
    "@aws-cdk/aws-route53-patters:useCertificate": true,
    "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
    "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
    "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
    "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
    "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
    "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
    "@aws-cdk/aws-redshift:columnId": true,
    "@aws-cdk/aws-cloudfront-origins:useOriginAccessControlForS3Origins": true,
    "@aws-cdk/core:enableCfnParameters": false
  }
}

[FATO] O flag @aws-cdk/customresources:installLatestAwsSdkDefault merece atenção: quando true, a Lambda interna do AwsCustomResource usa o SDK AWS mais recente disponível (não o bundled com o runtime Node). Isso pode causar falhas se a API que você está chamando mudou. O padrão mudou para false em versões recentes justamente por isso — preferível usar o SDK bundled e previsível.


6. Estrutura production-grade do cdk.json

[CONSENSO] Para projetos de time, o cdk.json deve ser tratado como um arquivo de configuração do projeto, não apenas um arquivo gerado pelo cdk init. Uma estrutura completa comentada:

{
  // comando de entrada do app CDK
  "app": "npx ts-node --prefer-ts-exts bin/app.ts",

  // configuração do modo watch (cdk watch / cdk deploy --watch)
  "watch": {
    "include": ["**"],
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "**/*.js",
      "node_modules",
      "test"
    ]
  },

  // configuração do build antes de cdk deploy/diff/synth
  // "build": "npm run build",  ← descomente se não usar ts-node diretamente

  // todo context vai aqui
  "context": {
    // ---- Configuração do projeto ----
    "env": "development",          // substituído via --context no CI/CD
    "owner": "platform-team",      // metadado para tagging
    "costCenter": "infra-001",

    // ---- Feature flags CDK ----
    // (os flags abaixo são os padrão para projetos novos)
    "@aws-cdk/aws-iam:minimizePolicies": true,
    "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
    "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
    // ... outros flags conforme a versão do CDK usada

    // ---- Lookups cacheados ----
    // (preenchidos automaticamente pelo CDK — não edite manualmente)
    // "vpc-provider:account=...": { ... }
  }
}

[CONSENSO] Separar contexto de configuração de projeto (como env, owner) dos feature flags melhora a legibilidade, mas não é uma estrutura imposta pelo CDK — é apenas organização por comentários.

O que vai no .gitignore:

# Artefatos de síntese (gerados pelo cdk synth)
cdk.out/

# Dependências Node
node_modules/

# TypeScript compilado (se não usar ts-node direto)
# *.js
# *.d.ts

O que NÃO vai no .gitignore:

cdk.json          ← configuração do projeto, feature flags
cdk.context.json  ← cache de lookups (crítico para CI reprodutível)

Exemplo prático

Cenário: Você tem uma CDK app multi-ambiente que precisa:
1. Usar a VPC existente de cada conta (não criar uma nova).
2. Ter comportamento determinístico no CI sem credenciais de describe.
3. Suportar cdk deploy --context env=staging para deployar em staging sem mudar código.

Estrutura do projeto

bin/
  app.ts                 ← entry point, lê context 'env'
lib/
  stacks/
    api-stack.ts         ← usa Vpc.fromLookup()
config/
  environments.ts        ← mapa de configuração por ambiente
cdk.json                 ← feature flags + default env
cdk.context.json         ← cache de lookups (versionado)

config/environments.ts

import { Environment } from 'aws-cdk-lib';

export interface EnvConfig {
  env: Environment;
  vpcName: string;
  hostedZoneName: string;
  instanceType: string;
}

export const environments: Record<string, EnvConfig> = {
  development: {
    env: { account: '111111111111', region: 'us-east-1' },
    vpcName: 'dev-vpc',
    hostedZoneName: 'dev.example.com',
    instanceType: 't3.micro',
  },
  staging: {
    env: { account: '222222222222', region: 'us-east-1' },
    vpcName: 'staging-vpc',
    hostedZoneName: 'staging.example.com',
    instanceType: 't3.small',
  },
  production: {
    env: { account: '333333333333', region: 'us-east-1' },
    vpcName: 'prod-vpc',
    hostedZoneName: 'example.com',
    instanceType: 'm5.large',
  },
};

lib/stacks/api-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
import { EnvConfig } from '../../config/environments';

interface ApiStackProps extends StackProps {
  config: EnvConfig;
}

export class ApiStack extends Stack {
  constructor(scope: Construct, id: string, props: ApiStackProps) {
    super(scope, id, props);

    // Lookup da VPC existente na conta
    // → na primeira execução: CDK consulta a AWS e salva em cdk.context.json
    // → nas execuções seguintes (inclusive CI): lê do cache
    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', {
      vpcName: props.config.vpcName,
    });

    // Lookup da Hosted Zone
    const zone = route53.HostedZone.fromLookup(this, 'Zone', {
      domainName: props.config.hostedZoneName,
    });

    // O resto usa vpc e zone como se fossem constructs normais
    // (mesmo sendo lookups, têm os métodos/propriedades esperados)
  }
}

bin/app.ts

import { App } from 'aws-cdk-lib';
import { ApiStack } from '../lib/stacks/api-stack';
import { environments } from '../config/environments';

const app = new App();

const envName = app.node.tryGetContext('env') ?? 'development';
const config = environments[envName];

if (!config) {
  throw new Error(
    `Ambiente '${envName}' não encontrado. ` +
    `Ambientes disponíveis: ${Object.keys(environments).join(', ')}`
  );
}

new ApiStack(app, `Api-${envName}`, {
  env: config.env,
  config,
  // Tags globais aplicadas a todos os recursos desta stack
  tags: {
    Environment: envName,
    ManagedBy: 'CDK',
  },
});

Workflow de onboarding de um novo ambiente

# 1. Primeiro deploy em staging: CDK vai fazer lookups na conta de staging
AWS_PROFILE=staging-profile cdk deploy --context env=staging

# → CDK consulta a AWS, popula cdk.context.json com as entradas de staging
# → commit o cdk.context.json atualizado

git add cdk.context.json
git commit -m "chore: cache context lookups for staging environment"

# 2. Builds de CI em staging agora são reproduzíveis sem credenciais de describe
# (o CI usa as entradas cacheadas)

Armadilhas comuns

Armadilha 1: Colocar cdk.context.json no .gitignore

O erro: Um desenvolvedor vê cdk.context.json como "arquivo gerado" e o adiciona ao .gitignore. O projeto funciona localmente porque cada dev tem credenciais AWS. O CI começa a falhar com erros como:

Error: Cannot retrieve value from context provider vpc-provider since account/region
are not specified at the stack level. Configure "env" for the stack...

Ou pior: o CI funciona mas produz templates diferentes em dias diferentes porque um novo recurso apareceu na conta (nova AMI, nova AZ).

Por que acontece: Sem o cache versionado, o CI precisa de credenciais de describe para fazer lookups em tempo real, e esses lookups são não-determinísticos.

Como evitar: cdk.context.json sempre no git. O único momento legítimo de não versionar é em projetos de prova de conceito estritamente locais.


Armadilha 2: Cache de lookups obsoleto causando erros silenciosos

O erro: Você deletou e recriou a VPC prod-vpc na AWS (por qualquer razão). O cdk.context.json ainda tem as subnet IDs antigas. O cdk synth passa sem erro (usa o cache), o deploy começa, e no meio do deploy o CloudFormation tenta referenciar subnet-old123 que não existe mais.

Por que acontece: O CDK nunca invalida o cache automaticamente — é um cache write-once, invalidado apenas manualmente. Ele não tem como saber que o recurso externo mudou.

Como reconhecer: Erros de deploy do tipo subnet-id does not exist ou security group sg-XXX not found quando você não mudou o código CDK.

Como evitar: Após qualquer mudança manual de infraestrutura que afete lookups (recriar VPCs, mudar names de recursos, etc.), execute cdk context --clear localmente, re-sintetize, commite o cdk.context.json atualizado.


Armadilha 3: Feature flag novo na versão do CDK muda comportamento silenciosamente

O erro: Você atualiza aws-cdk-lib de 2.80.0 para 2.120.0. O package.json e package-lock.json são atualizados, mas o cdk.json não recebe os novos feature flags da versão 2.120.0. Alguns constructs passam a ter comportamento diferente do esperado (um novo recurso IAM aparece, a lógica de nomeação de um recurso muda), mas nenhum erro é emitido.

Por que acontece: Feature flags novos não são adicionados retroativamente ao cdk.json de projetos existentes — só ao gerado por cdk init com a nova versão. Projetos existentes ficam com a configuração da versão de criação do projeto.

Como reconhecer: Depois de uma atualização de versão, execute cdk diff em um ambiente de não-produção e revise cada mudança. Se houver mudanças não esperadas (novos recursos, mudanças de nomes), verifique os changelogs e feature flags da versão.

Como evitar: Ao atualizar o CDK, compare o cdk.json do seu projeto com o cdk.json que cdk init geraria na nova versão. A documentação da versão lista os novos feature flags introduzidos. Ative-os deliberadamente após entender o impacto.


Exercício de reflexão

Você está migrando uma CDK app de um único desenvolvedor para um repositório de time com uma pipeline de CI/CD automatizada. A app tem três stacks, e dois deles usam Vpc.fromLookup() e MachineImage.lookup(). O CI roda em um container Docker sem acesso às contas AWS de produção (por política de segurança — apenas a pipeline de deploy tem as credenciais, não o build de CI).

Como você estruturaria o processo para garantir que:
1. O CI produz templates idênticos aos que o desenvolvedor produziu localmente.
2. Quando a infraestrutura subjacente muda (ex: a VPC é recriada), há um processo definido e controlado para atualizar o cache.
3. Novos desenvolvedores não precisam ter credenciais das contas de produção para trabalhar no código.
4. Os feature flags são atualizados de forma deliberada e revisada quando o CDK é atualizado.

Descreva o workflow de commit, os arquivos versionados e não versionados, e o processo de atualização de cache que você adotaria.


Recursos para aprofundar

1. Context values and the AWS CDK

URL: https://docs.aws.amazon.com/cdk/v2/guide/context.html
O que encontrar: Documentação completa do mecanismo de Context: tipos de lookups disponíveis, como o cache funciona, precedência de valores, comandos cdk context, e a distinção entre valueFromLookup e valueForStringParameter.
Por que é a fonte certa: É o guia oficial e canônico do mecanismo — mais completo que qualquer blog post.

2. AWS CDK feature flags

URL: https://docs.aws.amazon.com/cdk/v2/guide/featureflags.html
O que encontrar: Lista completa de todos os feature flags do CDK v2, o que cada um controla, quando foi introduzido, e o comportamento com e sem o flag. Essencial ao migrar ou atualizar versões do CDK.
Por que é a fonte certa: É a referência autoritativa — os changelogs do CDK no GitHub referenciam este documento para cada flag introduzido.

3. CDK Best Practices — Context e projeto

URL: https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html
O que encontrar: Seções sobre estrutura de projeto para times, gerenciamento de context em pipelines CI/CD, e a política de o que versionar vs o que ignorar no .gitignore.
Por que é a fonte certa: É o guia de boas práticas oficial da equipe do CDK, consolidando lições aprendidas de usuários em produção.