Constructos da camada 3
Se os constructos L1 realizam uma conversão literal dos recursos do CloudFormation em código programático, e os constructos L2 substituem grande parte da sintaxe detalhada do CloudFormation por métodos auxiliares e lógica personalizada, o que os constructos L3 fazem? A resposta para isso é limitada apenas pela sua imaginação. Você pode criar a camada 3 para se adequar a qualquer caso de uso específico. Se seu projeto precisar de um recurso que tenha um subconjunto específico de propriedades, você poderá criar um constructo L3 reutilizável para atender a essa necessidade.
Os constructos L3 são chamados de padrões no AWS CDK. Um padrão é qualquer objeto que estende a classe Construct no AWS CDK (ou estende uma classe que estende a classe Construct ) para realizar qualquer lógica abstrata além da camada 2. Ao usar a CLI do AWS CDK para executar cdk init para iniciar um novo projeto do AWS CDK, você deve escolher entre três tipos de aplicações do AWS CDK: app, lib e sample-app.
app e sample-app, ambos representam aplicações clássicas do AWS CDK em que você cria e implanta pilhas do CloudFormation em ambientes da AWS. Ao escolher lib, você está optando por criar um novo constructo L3. app e sample-app permitem que você escolha qualquer linguagem compatível com o AWS CDK, mas você só pode escolher o TypeScript com lib. Isso ocorre porque o AWS CDK é escrito nativamente em TypeScript e usa um sistema de código aberto chamado JSiilib para iniciar seu projeto, você está optando por criar uma extensão para o AWS CDK.
Qualquer classe que estenda a classe Construct pode ser um constructo L3, mas os casos de uso mais comuns da camada 3 são interações de recursos, extensões de recursos e recursos personalizados. A maioria dos constructos L3 usa um ou mais desses três casos para estender a funcionalidade do AWS CDK.
Interações de recursos
Uma solução normalmente emprega vários serviços da AWS que funcionam juntos. Por exemplo, uma distribuição do Amazon CloudFront geralmente usa um bucket do S3 como origem e AWS WAF como proteção contra explorações comuns. O AWS AppSync e o Amazon API Gateway costumam usar tabelas do Amazon DynamoDB como fontes de dados para suas APIs. Um pipeline no AWS CodePipeline geralmente usa o Amazon S3 como fonte e o AWS CodeBuild para suas etapas de criação. Nesses casos, geralmente é útil criar um único constructo L3 que manipule o provisionamento de dois ou mais constructos L2 interconectados.
Confira um exemplo de um constructo L3 que provisiona uma distribuição do CloudFront junto com sua origem do S3, um AWS WAF para colocar na frente dele, um registro do Amazon Route 53 e um certificado do AWS Certificate Manager (ACM) para adicionar um endpoint personalizado com criptografia em trânsito, tudo em um constructo reutilizável:
// Define the properties passed to the L3 construct export interface CloudFrontWebsiteProps { distributionProps: DistributionProps bucketProps: BucketProps wafProps: CfnWebAclProps zone: IHostedZone } // Define the L3 construct export class CloudFrontWebsite extends Construct { public distribution: Distribution constructor( scope: Construct, id: string, props: CloudFrontWebsiteProps ) { super(scope, id); const certificate = new Certificate(this, "Certificate", { domainName: props.zone.zoneName, validation: CertificateValidation.fromDns(props.zone) }); const defaultBehavior = { origin: new S3Origin(new Bucket(this, "bucket", props.bucketProps)) } const waf = new CfnWebACL(this, "waf", props.wafProps); this.distribution = new Distribution(this, id, { ...props.distributionProps, defaultBehavior, certificate, domainNames: [this.domainName], webAclId: waf.attrArn, }); } }
Observe que o CloudFront, o Amazon S3, o Route 53 e o ACM usam constructos L2, mas a Web ACL (que define regras para lidar com solicitações da web) usa um constructo L1. Isso ocorre porque o AWS CDK é um pacote de código aberto em evolução que não está totalmente completo e ainda não existe um constructo L2 para a WebAcl. No entanto, qualquer pessoa pode contribuir com o AWS CDK criando novos constructos L2. Então, até que o AWS CDK ofereça um constructo L2 para a WebAcl, você precisa usar um constructo L1. Para criar um novo site usando o constructo L3 CloudFrontWebsite, você usa o seguinte código:
const siteADotCom = new CloudFrontWebsite(stack, "siteA", siteAProps); const siteBDotCom = new CloudFrontWebsite(stack, "siteB", siteBProps); const siteCDotCom = new CloudFrontWebsite(stack, "siteC", siteCProps);
Neste exemplo, o constructo L2 Distribution do CloudFront é exposto como uma propriedade pública do constructo L3. Ainda haverá casos em que você precisará expor propriedades do L3 como esta, conforme necessário. Na verdade, veremos Distribution novamente mais tarde, na seção Recursos personalizados.
O AWS CDK inclui alguns exemplos de padrões de interação de recursos, como este. Além do pacote aws-ecs que contém os constructos L2 para o Amazon Elastic Container Service (Amazon ECS), o AWS CDK tem um pacote chamado aws-ecs-patterns. Esse pacote contém vários constructos L3 que combinam o Amazon ECS com Application Load Balancers, Network Load Balancers e grupos de destino, ao mesmo tempo em que oferecem versões diferentes que são predefinidas para o Amazon Elastic Compute Cloud (Amazon EC2) e o AWS Fargate. Como muitas aplicações sem servidor usam o Amazon ECS somente com o Fargate, esses constructos L3 oferecem uma conveniência que pode economizar o tempo dos desenvolvedores e o dinheiro dos clientes.
Extensões de recursos
Alguns casos de uso exigem que os recursos tenham configurações padrão específicas que não sejam nativas do constructo L2. No nível da pilha, isso pode ser tratado usando aspects, mas outra maneira conveniente de dar novos padrões a um constructo L2 é estendendo a camada 2. Como um constructo é qualquer classe que herda a classe Construct, e os constructos L2 estendem essa classe, você também pode criar um constructo L3 estendendo diretamente um constructo L2.
Isso pode ser especialmente útil para uma lógica de negócios personalizada que ofereça suporte às necessidades singulares de um cliente. Suponhamos que uma empresa tenha um repositório que armazene todo o seu código de função do AWS Lambda em um único diretório chamado src/lambda, e que a maioria das funções do Lambda reutilize sempre o mesmo runtime e nome de manipulador. Em vez de configurar o caminho do código toda vez que você configura uma nova função do Lambda, você pode criar um novo constructo L3:
export class MyCompanyLambdaFunction extends Function { constructor( scope: Construct, id: string, props: Partial<FunctionProps> = {} ) { super(scope, id, { handler: 'index.handler', runtime: Runtime.NODEJS_LATEST, code: Code.fromAsset(`src/lambda/${props.functionName || id}`), ...props }); }
Você pode então substituir o constructo L2 Function em todos os lugares do repositório da seguinte forma:
new MyCompanyLambdaFunction(this, "MyFunction"); new MyCompanyLambdaFunction(this, "MyOtherFunction"); new MyCompanyLambdaFunction(this, "MyThirdFunction", { runtime: Runtime.PYTHON_3_11 });
Os padrões permitem que você crie novas funções do Lambda em uma única linha, e o constructo L3 é configurado para que você ainda possa substituir as propriedades padrão, se necessário.
Estender constructos L2 diretamente funciona melhor quando você deseja apenas adicionar novos padrões aos constructos L2 existentes. Se você também precisar de outra lógica personalizada, é melhor estender a classe Construct. A razão para isso decorre do método super, que é chamado dentro do construtor. Em classes que estendem outras classes, o método super é usado para chamar o construtor da classe primária, e isso deve ser a primeira coisa que acontece em seu construtor. Isso significa que qualquer manipulação dos argumentos passados ou de outra lógica personalizada só pode acontecer após a criação do constructo L2 original. Se você precisar executar alguma dessas lógicas personalizadas antes de instanciar seu constructo L2, é melhor seguir o padrão descrito anteriormente na seção Interações de recursos.
Recursos personalizados
Os recursos personalizados são um recurso avançado no CloudFormation que permite executar lógica personalizada de uma função do Lambda que é ativada durante a implantação da pilha. Sempre que precisar de algum processo durante a implantação que não seja diretamente compatível com o CloudFormation, você pode usar um recurso personalizado para que isso aconteça. O AWS CDK oferece classes que também permitem criar recursos personalizados de forma programática. Ao usar recursos personalizados em um construtor L3, você poderá criar um constructo de quase tudo.
Uma das vantagens de usar o Amazon CloudFront são seus recursos globais robustos de armazenamento em cache. Se você quiser redefinir manualmente esse cache para que seu site reflita imediatamente as novas alterações feitas em sua origem, você pode usar uma invalidação do CloudFront. No entanto, as invalidações são processos executados em uma distribuição do CloudFront em vez de serem propriedades de uma distribuição do CloudFront. Elas podem ser criadas e aplicadas a uma distribuição existente a qualquer momento, portanto, não são parte nativa do processo de provisionamento e implantação.
Nesse cenário, talvez você queira criar e executar uma invalidação após cada atualização na origem de uma distribuição. Por causa dos recursos personalizados, você pode criar um constructo L3 que seja parecido com:
export interface CloudFrontInvalidationProps { distribution: Distribution region?: string paths?: string[] } export class CloudFrontInvalidation extends Construct { constructor( scope: Construct, id: string, props: CloudFrontInvalidationProps ) { super(scope, id); const policy = AwsCustomResourcePolicy.fromSdkCalls({ resources:AwsCustomResourcePolicy.ANY_RESOURCE }); new AwsCustomResource(scope, `${id}Invalidation`, { policy, onUpdate: { service: 'CloudFront', action: 'createInvalidation', region: props.region || 'us-east-1', physicalResourceId: PhysicalResourceId.fromResponse('Invalidation.Id'), parameters: { DistributionId: props.distribution.distributionId, InvalidationBatch: { Paths: { Quantity: props.paths?.length || 1, Items: props.paths || ['/*'] }, CallerReference: crypto.randomBytes(5).toString('hex') } } } } } }
Usando a distribuição que criamos anteriormente no constructo L3 CloudFrontWebsite, você pode fazer isso com muita facilidade:
new CloudFrontInvalidation(this, 'MyInvalidation', { distribution: siteADotCom.distribution });
Esse constructo L3 usa um constructo L3 do AWS CDK chamado AWSCustomResource para criar um recurso personalizado que executa a lógica personalizada. AwsCustomResource é muito conveniente quando você precisa fazer exatamente uma chamada do AWS SDK, pois permite que você faça isso sem precisar escrever nenhum código do Lambda. Se você tiver requisitos mais complexos e quiser implementar sua própria lógica, poderá usar diretamente a classe CustomResource básica.
Outro bom exemplo do AWS CDK usando um constructo L3 de recurso personalizado é a implantação do bucket do S3. A função do Lambda criada pelo recurso personalizado dentro do construtor desse constructo L3 adiciona funcionalidades que o CloudFormation não conseguiria manipular de outra forma: ela adiciona e atualiza objetos em um bucket do S3. Sem a implantação do bucket do S3, você não conseguiria colocar conteúdo no bucket do S3 que acabou de criar como parte da sua pilha, o que seria muito inconveniente.
O melhor exemplo do AWS CDK eliminando a necessidade de escrever uma grande quantidade de sintaxe do CloudFormation é este básico S3BucketDeployment:
new BucketDeployment(this, 'BucketObjects', { sources: [Source.asset('./path/to/amzn-s3-demo-bucket')], destinationBucket: amzn-s3-demo-bucket });
Compare isso com o código do CloudFormation que você precisaria escrever para realizar a mesma coisa:
"lambdapolicyA5E98E09": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Statement": [ { "Action": "lambda:UpdateFunctionCode", "Effect": "Allow", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-function" } ], "Version": "2012-10-17" }, "PolicyName": "lambdaPolicy", "Roles": [ { "Ref": "myiamroleF09C7974" } ] }, "Metadata": { "aws:cdk:path": "CdkScratchStack/lambda-policy/Resource" } }, "BucketObjectsAwsCliLayer8C081206": { "Type": "AWS::Lambda::LayerVersion", "Properties": { "Content": { "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, "S3Key": "e2277687077a2abf9ae1af1cc9565e6715e2ebb62f79ec53aa75a1af9298f642.zip" }, "Description": "/opt/awscli/aws" }, "Metadata": { "aws:cdk:path": "CdkScratchStack/BucketObjects/AwsCliLayer/Resource", "aws:asset:path": "asset.e2277687077a2abf9ae1af1cc9565e6715e2ebb62f79ec53aa75a1af9298f642.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Content" } }, "BucketObjectsCustomResourceB12E6837": { "Type": "Custom::CDKBucketDeployment", "Properties": { "ServiceToken": { "Fn::GetAtt": [ "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", "Arn" ] }, "SourceBucketNames": [ { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" } ], "SourceObjectKeys": [ "f888a9d977f0b5bdbc04a1f8f07520ede6e00d4051b9a6a250860a1700924f26.zip" ], "DestinationBucketName": { "Ref": "amzn-s3-demo-bucket77F80CC0" }, "Prune": true }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete", "Metadata": { "aws:cdk:path": "CdkScratchStack/BucketObjects/CustomResource/Default" } }, "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" } } ], "Version": "2012-10-17" }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition" }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ] ] } ] }, "Metadata": { "aws:cdk:path": "CdkScratchStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource" } }, "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "s3:GetBucket*", "s3:GetObject*", "s3:List*" ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition" }, ":s3:::", { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, "/*" ] ] }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition" }, ":s3:::", { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" } ] ] } ] }, { "Action": [ "s3:Abort*", "s3:DeleteObject*", "s3:GetBucket*", "s3:GetObject*", "s3:List*", "s3:PutObject", "s3:PutObjectLegalHold", "s3:PutObjectRetention", "s3:PutObjectTagging", "s3:PutObjectVersionTagging" ], "Effect": "Allow", "Resource": [ { "Fn::GetAtt": [ "amzns3demobucket77F80CC0", "Arn" ] }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "amzns3demobucket77F80CC0", "Arn" ] }, "/*" ] ] } ] } ], "Version": "2012-10-17" }, "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", "Roles": [ { "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" } ] }, "Metadata": { "aws:cdk:path": "CdkScratchStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource" } }, "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, "S3Key": "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd.zip" }, "Role": { "Fn::GetAtt": [ "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", "Arn" ] }, "Environment": { "Variables": { "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" } }, "Handler": "index.handler", "Layers": [ { "Ref": "BucketObjectsAwsCliLayer8C081206" } ], "Runtime": "python3.9", "Timeout": 900 }, "DependsOn": [ "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" ], "Metadata": { "aws:cdk:path": "CdkScratchStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource", "aws:asset:path": "asset.9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd", "aws:asset:is-bundled": false, "aws:asset:property": "Code" } }
4 linhas versus 241 linhas é uma grande diferença! E este é apenas um exemplo do que é possível quando você aproveita a camada 3 para personalizar suas pilhas.