Constructos da camada 2 - Recomendações da AWS

Constructos da camada 2

O repositório de código aberto do AWS CDK é escrito principalmente usando a linguagem de programação TypeScript e consiste em vários pacotes e módulos. A biblioteca de pacotes principal, chamada aws-cdk-lib, é aproximadamente dividida em um pacote por serviço da AWS, embora isso nem sempre seja o caso. Conforme discutido anteriormente, os constructos L1 são gerados automaticamente durante o processo de criação. Então, qual é todo esse código que você vê quando olha dentro do repositório? Estes são os constructos L2, que são abstrações dos constructos L1.

Os pacotes também contêm uma coleção de tipos, enums e interfaces do TypeScript, bem como classes auxiliares que adicionam mais funcionalidades, mas todos esses itens fornecem o constructo L2. Todos os constructos L2 chamam seus constructos L1 correspondentes em seus construtores após a instanciação, e o constructo L1 resultante que é criada pode ser acessado da camada 2 da seguinte forma:

const role = new Bucket(this, "amzn-s3-demo-bucket", {/*...BucketProps*/}); const cfnBucket = role.node.defaultChild;

O constructo L2 pega as propriedades padrão, os métodos de conveniência e outros açúcares sintáticos e os aplica ao constructo L1. Isso elimina grande parte da repetição e da verbosidade necessárias para provisionar recursos diretamente no CloudFormation.

Todos os constructos L2 criam seus constructos L1 correspondentes internamente. No entanto, os constructos L2, na verdade, não estendem os constructos L1. Os constructos L1 e L2 herdam uma classe especial chamada Construct. Na versão 1 do AWS CDK, a classe Construct foi incorporada ao kit de desenvolvimento, mas na versão 2 é um pacote independente separado. Isso é para que outros pacotes, como o Cloud Development Kit for Terraform (CDKTF), possam incluí-lo como uma dependência. Qualquer classe que herda a classe Construct é um constructo L1, L2 ou L3. Os constructos L2 estendem essa classe diretamente, enquanto os constructos L1 estendem uma classe chamada CfnResource, conforme mostrado na tabela a seguir.

Árvore de herança L1

Árvore de herança L2

Constructo L1

→ classe https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.CfnResource.htmlCfnResource

→→ classe abstrata CfnRefElement

→→→ classe abstrata CfnElement

→→→→ classe Construct

Constructo L2

→ classe Construct

Se os constructos L1 e L2 herdam a classe Construct, por que os constructos L2 simplesmente não estendem L1? Bem, as classes entre a classe Construct e a camada 1 bloqueiam o constructo L1 como uma imagem espelhada do recurso do CloudFormation. Elas contêm métodos abstratos (métodos que as classes downstream devem incluir), como _toCloudFormation, o que força o constructo a gerar diretamente a sintaxe do CloudFormation. Os constructos L2 ignoram essas classes e estendem a classe Construct diretamente. Isso lhes dá a flexibilidade de abstrair grande parte do código necessário para constructos L1, construindo-os separadamente em seus construtores.

A seção anterior apresentou uma comparação lado a lado de um bucket do S3 de um modelo do CloudFormation, e esse mesmo bucket do S3 renderizado como um constructo L1. Essa comparação mostrou que as propriedades e a sintaxe são quase idênticas, e o constructo L1 economiza apenas três ou quatro linhas em comparação com o constructo do CloudFormation. Agora vamos comparar o constructo L1 com o constructo L2 para o mesmo bucket do S3:

Constructo L1 para o bucket do S3

Constructo L2 para o bucket do S3

new CfnBucket(this, "amzns3demobucket", { bucketName: "amzn-s3-demo-bucket", bucketEncryption: { serverSideEncryptionConfiguration: [ { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } } ] }, metricsConfigurations: [ { id: "myConfig" } ], ownershipControls: { rules: [ { objectOwnership: "BucketOwnerPreferred" } ] }, publicAccessBlockConfiguration: { blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true }, versioningConfiguration: { status: "Enabled" } });
new Bucket(this, "amzns3demobucket", { bucketName: "amzn-s3-demo-bucket", encryption: BucketEncryption.S3_MANAGED, metrics: [ { id: "myConfig" }, ], objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, versioned: true });

Como você pode ver, o constructo L2 tem menos da metade do tamanho do constructo L1. Os constructos L2 usam várias técnicas para realizar essa consolidação. Algumas dessas técnicas se aplicam a um único constructo L2, mas outras podem ser reutilizadas em vários constructos para que sejam separadas em sua própria classe para reutilização. Os constructos L2 consolidam a sintaxe do CloudFormation de várias maneiras, conforme anaçisado nas seções a seguir.

Propriedades padrão

A maneira mais simples de consolidar o código para provisionar um recurso é transformar as configurações de propriedade mais comuns em padrões. O AWS CDK tem acesso a linguagens de programação avançadas e o CloudFormation não, então esses padrões geralmente são de natureza condicional. Às vezes, várias linhas de configuração do CloudFormation podem ser eliminadas do código do AWS CDK porque essas configurações podem ser inferidas dos valores de outras propriedades que são passadas para o constructo.

Estruturas, tipos e interfaces

Embora o AWS CDK esteja disponível em várias linguagens de programação, ele é escrito nativamente em TypeScript, de modo que o sistema de tipos de linguagem é usado para definir os tipos que compõem os constructos L2. Aprofundar-se nesse sistema de tipos está além do escopo deste guia. Consulte a documentação do TypeScript para obter detalhes. Para resumir, um TypeScript type descreve o tipo de dados que uma determinada variável contém. Podem ser dados básicos, como uma string, ou dados mais complexos, como um object. Um TypeScript interface é outra forma de expressar o tipo de objeto TypeScript, e um struct é outro nome para uma interface.

O TypeScript não usa o termo struct, mas se você olhar na Referência de APIs do AWS CDK, verá que um struct é, na verdade, apenas outra interface do TypeScript dentro do código. A Referência de APIs também se refere a determinadas interfaces como interfaces. Se structs e interfaces são a mesma coisa, por que a documentação do AWS CDK faz uma distinção entre eles?

O que o AWS CDK chama de structs são interfaces que representam qualquer objeto usado por um constructo L2. Isso inclui os tipos de objeto para os argumentos de propriedade que são passados para o constructo L2 durante a instanciação, como BucketProps para o constructo do bucket do S3 e TableProps para o constructo da tabela do DynamoDB, bem como outras interfaces do TypeScript usadas no AWS CDK. Resumindo, se for uma interface do TypeScript no AWS CDK, e seu nome não for prefixado pela letra I, o AWS CDK o chamará de struct.

Por outro lado, o AWS CDK usa o termo interface para representar os elementos básicos de que um objeto simples precisaria para ser considerado uma representação adequada de um determinado constructo ou classe auxiliar. Ou seja, uma interface descreve quais devem ser as propriedades públicas de um constructo L2. Todos os nomes de interface do AWS CDK são nomes de constructos ou classes auxiliares existentes prefixadas pela letra I. Todos os constructos L2 estendem a classe Construct, mas também implementam a interface correspondente. Portanto, o constructo L2 Bucket implementa a interface IBucket.

Métodos estáticos

Cada instância de um constructo L2 também é uma instância de sua interface correspondente, mas o inverso não é verdadeiro. Isso é importante ao examinar um struct para ver quais tipos de dados são necessários. Se um struct tiver uma propriedade chamada bucket, que requer o tipo de dadosIBucket, você poderá passar um objeto que contenha as propriedades listadas na interface IBucket ou uma instância de um L2 Bucket. Qualquer um funcionará. No entanto, se essa propriedade bucket exigir um L2 Bucket, você poderá passar somente uma instância Bucket nesse campo.

Essa distinção torna-se muito importante quando você importa recursos preexistentes para sua pilha. Você pode criar um constructo L2 para qualquer recurso nativo da sua pilha, mas se precisar referenciar um recurso que foi criado fora da pilha, precisará usar a interface desse constructo L2. Isso porque a criação de um constructo L2 cria um novo recurso, caso ainda não exista um dentro dessa pilha. As referências aos recursos existentes devem ser objetos simples que estejam em conformidade com a interface do constructo L2.

Para facilitar isso na prática, a maioria dos constructos L2 tem um conjunto de métodos estáticos associados a eles que retornam a interface desse constructo L2. Esses métodos estáticos geralmente começam com a palavra from. Os dois primeiros argumentos passados para esses métodos são os mesmos argumentos scope e id necessários para um contructo L2 padrão. No entanto, o terceiro argumento não é props, mas sim um pequeno subconjunto de propriedades (ou às vezes apenas uma propriedade) que define uma interface. Por esse motivo, quando você passa um constructo L2, na maioria dos casos, somente os elementos da interface são necessários. Isso é para que você também possa usar recursos importados, sempre que possível.

// Example of referencing an external S3 bucket const preExistingBucket = Bucket.fromBucketName(this, "external-bucket", "name-of-bucket-that-already-exists");

No entanto, você não deve depender muito de interfaces. Você deve importar recursos e usar interfaces diretamente somente quando for absolutamente necessário, porque as interfaces não fornecem muitas das propriedades, como métodos auxiliares, que conferem tanta capacidade a um constructo L2.

Métodos auxiliares

Um constructo L2 é uma classe programática em vez de um objeto simples, portanto, ele pode expor métodos de classe que permitem manipular a configuração do recurso após a instanciação. Um bom exemplo disso é o constructo L2 Role do AWS Identity and Access Management (IAM). Os trechos a seguir mostram duas maneiras de criar o mesmo perfil do IAM usando o constructo L2 Role.

Sem um método auxiliar:

const role = new Role(this, "my-iam-role", { assumedBy: new FederatedPrincipal('my-identity-provider.com'), managedPolicies: [ ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess") ], inlinePolicies: { lambdaPolicy: new PolicyDocument({ statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'lambda:UpdateFunctionCode' ], resources: [ 'arn:aws:lambda:us-east-1:123456789012:function:my-function' ] }) ] }) } });

Com um método auxiliar:

const role = new Role(this, "my-iam-role", { assumedBy: new FederatedPrincipal('my-identity-provider.com') }); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")); role.attachInlinePolicy(new Policy(this, "lambda-policy", { policyName: "lambdaPolicy", statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'lambda:UpdateFunctionCode' ], resources: [ 'arn:aws:lambda:us-east-1:123456789012:function:my-function' ] }) ] }));

A capacidade de usar métodos de instância para manipular a configuração de recursos após a instanciação dá aos constructos L2 muita flexibilidade adicional em relação à camada anterior. Os constructos L1 também herdam alguns métodos de recursos (como addPropertyOverride), mas é somente na camada dois que você obtém métodos projetados especificamente para esse recurso e suas propriedades.

Enumerações

A sintaxe do CloudFormation geralmente exige que você especifique muitos detalhes para provisionar um recurso adequadamente. No entanto, a maioria dos casos de uso geralmente é abrangida por apenas algumas configurações. Representar essas configurações usando uma série de valores enumerados pode reduzir consideravelmente a quantidade necessária de código.

Por exemplo, no exemplo de código L2 do bucket do S3 apresentado anteriormente nesta seção, você precisa usar a propriedade bucketEncryption do modelo do CloudFormation para fornecer todos os detalhes, incluindo o nome do algoritmo de criptografia a ser usado. Em vez disso, o AWS CDK fornece o enum BucketEncryption, que usa as cinco formas mais comuns de criptografia de bucket e permite que você expresse cada uma usando nomes de variáveis únicas.

E quanto aos casos de exceção que não são abrangidos pelos enums? Um dos objetivos de um constructo L2 é simplificar a tarefa de provisionar um recurso de camada 1, de modo que certos casos de exceção que são menos usados podem não ser suportados na camada 2. Para oferecer suporte a esses casos de exceção, o AWS CDK permite manipular diretamente as propriedades dos recursos subjacentes do CloudFormation usando o método addPropertyOverride. Para saber mais sobre substituições de propriedades, consulte a seção Práticas recomendadas deste guia e a seção Abstrações e portas de escape na documentação do AWS CDK.

Classes auxiliares

Às vezes, um enum não consegue realizar a lógica programática necessária para configurar um recurso para um determinado caso de uso. Nessas situações, o AWS CDK geralmente oferece uma classe auxiliar. Um enum é um objeto simples que oferece uma série de pares de chave/valor, enquanto uma classe auxiliar oferece todos os recursos de uma classe TypeScript. Uma classe auxiliar ainda pode agir como um enum expondo propriedades estáticas, mas essas propriedades poderão então ter seus valores definidos internamente com lógica condicional no construtor da classe auxiliar ou em um método auxiliar.

Portanto, embora ao enum BucketEncryption possa reduzir a quantidade de código necessária para definir um algoritmo de criptografia em um bucket do S3, essa mesma estratégia não funcionaria para definir durações de tempo porque simplesmente há muitos valores possíveis para escolher. Criar um enum para cada valor seria muito mais problemático do que vale a pena. Por esse motivo, uma classe auxiliar é usada para as configurações padrão do Bloqueio de Objetos do S3 de um bucket do S3, conforme representado pela classe ObjectLockRetention. ObjectLockRetention contém dois métodos estáticos: um para retenção de conformidade e outro para retenção de governança. Ambos os métodos usam uma instância da classe auxiliar Duration como argumento para expressar a quantidade de tempo para a qual o bloqueio deve ser configurado.

Outro exemplo é a classe auxiliar Runtime do AWS Lambda. À primeira vista, pode parecer que as propriedades estáticas associadas a essa classe podem ser tratadas por um enum. No entanto, internamente, cada valor de propriedade representa uma instância da própria classe Runtime, portanto, a lógica executada no construtor da classe não poderia ser alcançada em um enum.