Constructos de la capa 3
Si los constructos de la capa 1 hacen una traducción literal de los recursos de CloudFormation a código programático y los constructos de la capa 2 sustituyen gran parte de la sintaxis detallada de CloudFormation por métodos auxiliares y lógica personalizada, ¿qué hacen los constructos de la capa 3? Solo su imaginación limita la respuesta a esa pregunta. Puede crear la capa 3 para satisfacer cualquier caso de uso concreto. Si su proyecto necesita un recurso que tenga un subconjunto específico de propiedades, puede crear un constructo de la capa 3 reutilizable para satisfacer esa necesidad.
Los constructos de la capa 3 se denominan patrones en AWS CDK. Un patrón es cualquier objeto que amplía la clase Construct en AWS CDK (o amplía una clase que amplía la clase Construct ) para hacer cualquier lógica abstracta más allá de la capa 2. Cuando utiliza la CLI de AWS CDK para ejecutar cdk init e iniciar un nuevo proyecto de AWS CDK, debe elegir entre tres tipos de aplicaciones de AWS CDK: app, lib y sample-app.
app y sample-app representan las aplicaciones clásicas de AWS CDK en las que se crean e implementan pilas de CloudFormation en entornos de AWS. Al elegir lib, elige crear un constructo de la capa 3 completamente nuevo. app y sample-app le permiten elegir cualquier lenguaje que AWS CDK admita, pero solo puede elegir TypeScript con lib. Esto se debe a que AWS CDK está escrito de manera nativa en TypeScript y utiliza un sistema de código abierto llamado JSiilib para iniciar el proyecto, elige crear una ampliación para AWS CDK.
Cualquier clase que amplíe la clase Construct puede ser un constructo de la capa 3, pero los casos de uso más comunes de la capa 3 son las interacciones de recursos, las ampliaciones de recursos y los recursos personalizados. La mayoría de los constructos de la capa 3 utilizan uno o varios de estos tres casos para ampliar la funcionalidad de AWS CDK.
Interacciones de los recursos
Por lo general, una solución utiliza varios servicios de AWS que funcionan juntos. Por ejemplo, una distribución de Amazon CloudFront suele utilizar un bucket de S3 como origen y AWS WAF como protección contra las vulnerabilidades de seguridad más comunes. AWS AppSync y Amazon API Gateway suelen utilizar tablas de Amazon DynamoDB como orígenes de datos para sus API. Una canalización de AWS CodePipeline suele utilizar Amazon S3 como origen y AWS CodeBuild para sus etapas de creación. En estos casos, suele ser útil crear un constructo único de la capa 3 que se encargue del aprovisionamiento de dos o varios constructos de la capa 2 interconectadas.
A continuación, se muestra un ejemplo de un constructo de la capa 3 que aprovisiona una distribución de CloudFront junto con su origen de S3, un AWS WAF para ponerlo delante, un registro de Amazon Route 53 y un certificado de AWS Certificate Manager (ACM) para agregar un punto de conexión personalizado con cifrado en tránsito, todo en un constructo reutilizable:
// 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 CloudFront, Amazon S3, Route 53 y ACM utilizan constructos de la capa 2, pero la ACL web (que define las reglas para gestionar las solicitudes web) utiliza un constructo de la capa 1. Esto se debe a que AWS CDK es un paquete de código abierto en evolución que no está completo del todo y aún no existe un constructo de la capa 2 para WebAcl. Sin embargo, cualquiera puede contribuir a AWS CDK al crear nuevos constructos de la capa 2. Por lo tanto, hasta que AWS CDK no ofrezca un constructo de la capa 2 para WebAcl, tendrá que utilizar un constructo de la capa 1. Para crear un nuevo sitio web mediante el constructo de la capa 3 CloudFrontWebsite, utilice el código siguiente:
const siteADotCom = new CloudFrontWebsite(stack, "siteA", siteAProps); const siteBDotCom = new CloudFrontWebsite(stack, "siteB", siteBProps); const siteCDotCom = new CloudFrontWebsite(stack, "siteC", siteCProps);
En este ejemplo, el constructo de la capa 2 Distribution de CloudFront se expone como una propiedad pública del constructo de la capa 3. Seguirán existiendo casos en los que deba exponer propiedades de la capa 3 como esta, según sea necesario. De hecho, volveremos a ver Distribution más adelante, en la sección Recursos personalizados.
AWS CDK incluye algunos ejemplos de patrones de interacción de recursos como este. Además del paquete de aws-ecs que contiene los constructos de la capa 2 para Amazon Elastic Container Service (Amazon ECS), AWS CDK tiene un paquete llamado aws-ecs-patterns. Este paquete contiene varios constructos de la capa 3 que combinan Amazon ECS con equilibradores de carga de aplicaciones, equilibradores de carga de red y grupos de destinos, además de que ofrece distintas versiones predefinidas para Amazon Elastic Compute Cloud (Amazon EC2) y AWS Fargate. Dado que muchas aplicaciones sin servidor utilizan Amazon ECS solo con Fargate, estos constructos de la capa 3 ofrecen una comodidad que permite a los desarrolladores ahorrar tiempo y dinero a los clientes.
Extensiones de recursos
Algunos casos de uso requieren que los recursos tengan una configuración predeterminada específica que no sea nativa del constructo de la capa 2. Para las pilas, esto se puede gestionar mediante los aspectos, pero otra manera práctica de dotar a un constructo nuevo de la capa 2 de nuevos valores predeterminados consiste en ampliar la capa 2. Como un constructo es cualquier clase que hereda la clase Construct y los constructos de la capa 2 amplían esa clase, también puede crear un constructo de la capa 3 mediante la ampliación directa de un constructo de la capa 2.
Esto puede resultar muy útil para la lógica empresarial personalizada que respalde las necesidades concretas de un cliente. Supongamos que una empresa tiene un repositorio que almacena todo su código de la función de AWS Lambda en un único directorio llamado src/lambda y que la mayoría de las funciones de Lambda reutilizan el mismo tiempo de ejecución y el mismo nombre de controlador cada vez. En lugar de configurar la ruta del código cada vez que configure una nueva función de Lambda, podría crear un constructo nuevo de la capa 3:
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 }); }
A continuación, puede reemplazar el constructo de la capa 2 Function en todas partes del repositorio de la manera siguiente:
new MyCompanyLambdaFunction(this, "MyFunction"); new MyCompanyLambdaFunction(this, "MyOtherFunction"); new MyCompanyLambdaFunction(this, "MyThirdFunction", { runtime: Runtime.PYTHON_3_11 });
Los valores predeterminados le permiten crear nuevas funciones de Lambda en una sola línea, y el constructo de la capa 3 está configurada para que pueda anular las propiedades predeterminadas si es necesario.
La extensión directa de los constructos de la capa 2 funciona mejor cuando solo se desean agregar nuevos valores predeterminados a los constructos de la capa 2 existentes. Si también necesita otra lógica personalizada, es mejor ampliar la clase Construct. El motivo de esto se debe al método super, al que se llama desde el constructor. En las clases que amplían otras clases, el método super se utiliza para llamar al constructor de la clase principal. Esto debe ser lo primero que suceda en el constructor. Esto significa que las manipulaciones de los argumentos pasados u otra lógica personalizada solo puede producirse después de que se haya creado el constructo de la capa 2 original. Si necesita hacer alguna de estas lógicas personalizadas antes de crear una instancia del constructo de la capa 2, es mejor seguir el patrón descrito anteriormente en la sección Interacciones entre recursos.
Recursos personalizados
Los recursos personalizados son una característica eficaz de CloudFormation que permite ejecutar una lógica personalizada desde una función de Lambda que se activa durante la implementación de la pila. Siempre que necesite algún proceso durante la implementación que no sea compatible directamente con CloudFormation, puede utilizar un recurso personalizado para lograrlo. AWS CDK ofrece clases que también le permiten crear recursos personalizados mediante programación. Al utilizar los recursos personalizados en un constructor de la capa 3, puede hacer un constructo a partir de casi cualquier cosa.
Una de las ventajas de utilizar Amazon CloudFront es su funcionalidad sólida de almacenamiento en caché global. Si desea restablecer de manera manual la caché para que el sitio web refleje de inmediato los cambios nuevos hechos en el origen, puede utilizar una invalidación de CloudFront. Sin embargo, las invalidaciones son procesos que se ejecutan en una distribución de CloudFront en lugar de ser propiedades de una distribución de CloudFront. Se pueden crear y aplicar a una distribución existente en cualquier momento, por lo que no forman parte de manera nativa del proceso de aprovisionamiento e implementación.
En este escenario, es posible que quiera crear y ejecutar una invalidación después de cada actualización del origen de una distribución. Debido a los recursos personalizados, puede crear un constructo de la capa 3 similar a la siguiente:
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') } } } } } }
Con la distribución que se creó anteriormente en el constructo CloudFrontWebsite de la capa 3, podría hacerlo de manera muy fácil:
new CloudFrontInvalidation(this, 'MyInvalidation', { distribution: siteADotCom.distribution });
Este constructo de la capa 3 utiliza un constructo AWS CDK de la capa 3 llamado AwsCustomResource para crear un recurso personalizado que ejecuta una lógica personalizada. AwsCustomResource es muy práctico cuando se debe hacer exactamente una llamada al SDK de AWS, ya que le permite hacerla sin tener que escribir ningún código de Lambda. Si tiene requisitos más complejos y desea implementar su propia lógica, puede utilizar de manera directa la clase básica CustomResource.
Otro buen ejemplo de AWS CDK en el que se utiliza un constructo de la capa 3 del recurso personalizado es la implementación de buckets de S3. La función de Lambda creada por el recurso personalizado del constructor de este constructo de la capa 3 agrega una funcionalidad que CloudFormation no podría gestionar. Agrega y actualiza objetos en un bucket de S3. Sin la implementación de un bucket de S3, no podría colocar contenido en el bucket que acaba de crear como parte de su pila, lo que sería muy inconveniente.
El mejor ejemplo de cómo AWS CDK elimina la necesidad de escribir montones de sintaxis de CloudFormation es este constructo S3BucketDeployment básico:
new BucketDeployment(this, 'BucketObjects', { sources: [Source.asset('./path/to/amzn-s3-demo-bucket')], destinationBucket: amzn-s3-demo-bucket });
Compárelo con el código de CloudFormation que tendría que escribir para lograr lo mismo:
"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 líneas contra 241 es una diferencia enorme. Y esto es solo un ejemplo de lo que es posible cuando se aprovecha la capa 3 para personalizar las pilas.