Constructos de la capa 2 - Recomendaciones de AWS

Constructos de la capa 2

El repositorio de código abierto de AWS CDK se escribe sobre todo con el lenguaje de programación TypeScript y consta de numerosos paquetes y módulos. La biblioteca de paquetes principal, llamada aws-cdk-lib, se divide más o menos en un paquete por servicio de AWS, aunque no siempre es así. Como se mencionó anteriormente, los constructos de la capa 1 se generan de manera automática durante el proceso de compilación. Entonces, ¿qué es todo ese código que ve cuando revisa el repositorio? Se trata de constructos de la capa 2, que son abstracciones de los constructos de la capa 1.

Los paquetes también contienen una colección de tipos, enumeraciones e interfaces de TypeScript, así como clases auxiliares que agregan funcionalidad, pero todos esos elementos sirven para los constructos de la capa 2. Todos los constructos de la capa 2 llaman a sus constructos correspondientes de la capa 1 en sus constructores tras la creación de instancias. Se puede acceder al constructo de la capa 1 resultante que se crea desde la capa 2 de la manera siguiente:

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

El constructo de la capa 2 toma las propiedades predeterminadas, los métodos prácticos y otros elementos sintácticos y los aplica al constructo de la capa 1. Esto elimina gran parte de la repetición y la verbosidad necesarias para aprovisionar los recursos de manera directa en CloudFormation.

Todos los constructos de la capa 2 crean sus constructos correspondientes de la capa 1 de manera clandestina. Sin embargo, los constructos de la capa 2 en realidad no amplían los constructos de la capa 1. Los constructos de la capa 1 y de la capa 2 heredan una clase especial denominada Construct. En la versión 1 de AWS CDK, la clase Construct estaba integrada en el kit de desarrollo, pero en la versión 2, se trata un paquete independiente. Esto es para que otros paquetes, como Cloud Development Kit for Terraform (CDKTF), puedan incluirla como una dependencia. Cualquier clase que herede la clase Construct es un constructo de la capa 1, 2 y 3. Los constructos de la capa 2 amplían esta clase de manera directa, mientras que los constructos de la capa 1 amplían una clase llamada CfnResource, como se muestra en la tabla siguiente.

Árbol de herencia de la capa 1

Árbol de herencia de la capa 2

Constructo de la capa 1

→ clase CfnResource

→→ clase abstracta CfnRefElement

→→→ clase abstracta CfnElement

→→→→ clase Construct

Constructo de la capa 2

→ clase Construct

Si los constructos de la capa 1 y 2 heredan la clase Construct, ¿por qué los constructos de la capa 2 no solo amplían la capa 1? Las clases entre la clase Construct y la capa 1 bloquean el constructo de la capa 1 en su lugar como una imagen reflejada del recurso de CloudFormation. Contienen métodos abstractos (métodos que las clases posteriores deben incluir), como _toCloudFormation, que obligan al constructo a generar de manera directa la sintaxis de CloudFormation. Los constructos de la capa 2 omiten esas clases y amplían la clase Construct de manera directa. Esto les da la flexibilidad de abstraer gran parte del código necesario para los constructos de la capa 1 al crearlos por separado en sus constructores.

En la sección anterior se presentó una comparación paralela de un bucket de S3 de una plantilla de CloudFormation y ese mismo bucket de S3 renderizado como un constructo de la capa 1. En esa comparación se mostró que las propiedades y la sintaxis son casi idénticas y que el constructo de la capa 1 guarda solo tres o cuatro líneas en comparación con el constructo de CloudFormation. Ahora, comparemos el constructo de la capa 1 con el constructo de la capa 2 para el mismo bucket de S3:

Constructo de la capa 1 para el bucket de S3

Constructo de la capa 2 para el bucket de 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 puede ver, el constructo de la capa 2 es menos de la mitad del tamaño del constructo de la capa 1. Los constructos de la capa 2 utilizan numerosas técnicas para lograr esta consolidación. Algunas de estas técnicas se aplican a un solo constructo de la capa 2, pero otras se pueden reutilizar en varios constructos para separarlos en su propia clase para su reutilización. Los constructos de la capa 2 consolidan la sintaxis de CloudFormation de varias maneras, como se explica en las secciones siguientes.

Propiedades predeterminadas

La manera más sencilla de consolidar el código para aprovisionar un recurso es convertir la configuración de propiedades más común en valores predeterminados. AWS CDK tiene acceso a lenguajes de programación potentes y CloudFormation no, por lo que estos valores predeterminados suelen ser de naturaleza condicional. A veces, se pueden eliminar varias líneas de configuración de CloudFormation del código de AWS CDK porque se pueden deducir de los valores de otras propiedades que se pasan al constructo.

Estructuras, tipos e interfaces

Aunque AWS CDK está disponible en varios lenguajes de programación, está escrito de forma nativa en TypeScript, por lo que el sistema de tipos de ese lenguaje se utiliza para definir los tipos que componen los constructos de la capa 2. Entrar en detalle sobre ese sistema de tipos supera el ámbito de esta guía. Consulte la documentación de TypeScript para más información. En resumen, type de TypeScript describe qué tipo de datos contiene una variable concreta. Pueden ser datos básicos, como string, o datos más complejos, como object. interface de TypeScript es otra manera de expresar el tipo de objeto de TypeScript y struct es otro nombre para una interfaz.

TypeScript no usa el término estructura, pero si consulta la referencia de la API del AWS CDK, verá que una estructura es en realidad solo otra interfaz de TypeScript en el código. La referencia de la API también se refiere a interfaces determinadas como “interfaces”. Si las estructuras y las interfaces son lo mismo, ¿por qué la documentación de AWS CDK hace una distinción entre estas?

Lo que AWS CDK denomina estructuras son interfaces que representan a los objetos que utiliza un constructo de la capa 2. Esto incluye los tipos de objeto de los argumentos de propiedad que se pasan al constructo de la capa 2 durante la creación de instancias, como BucketProps para el constructo de un bucket de S3 y TableProps para el constructo de la tabla de DynamoDB, así como otras interfaces de TypeScript que se utilizan en AWS CDK. En resumen, si se trata de una interfaz de TypeScript en AWS CDK y su nombre no tiene el prefijo de la letra I, AWS CDK la llama estructura.

Por el contrario, AWS CDK utiliza el término interfaz para representar los elementos básicos. Un objeto simple debería considerarse una representación adecuada de un constructo o clase auxiliar en particular. Es decir, una interfaz describe cuáles deben ser las propiedades públicas de un constructo de la capa 2. Todos los nombres de las interfaces de AWS CDK son los nombres de los constructos o clases auxiliares existentes con el prefijo de la letra I. Todos los constructos de la capa 2 amplían la clase Construct, pero también implementan su interfaz correspondiente. Por lo tanto, Bucket del constructo de la capa 2 implementa la interfaz de IBucket.

Métodos estáticos

Cada instancia de un constructo de la capa 2 también es una instancia de su interfaz correspondiente, pero no ocurre lo contrario. Esto es importante al momento de analizar una estructura para ver qué tipos de datos son necesarios. Si una estructura tiene una propiedad llamada bucket, que requiere el tipo de datos IBucket, puede pasar un objeto que contenga las propiedades enumeradas en la interfaz de IBucket o una instancia de una propiedad Bucket de la capa 2. Ambas funcionarían. Sin embargo, si esa propiedad bucket llamará a una propiedad Bucket de la capa 2, solo podría pasar una instancia de Bucket en ese campo.

Esta distinción se vuelve muy importante cuando se importan recursos preexistentes a la pila. Puede crear un constructo de la capa 2 para los recursos que nativos de la pila, pero si tiene que hacer referencia a un recurso que se creó fuera de la pila, tiene que utilizar la interfaz de ese constructo de la capa 2. Esto se debe a que al crear un constructo de la capa 2 se crea un recurso nuevo si aún no existe uno en esa pila. Las referencias a los recursos existentes tienen que ser objetos simples que se ajusten a la interfaz de ese constructo de la capa 2.

Para facilitar esto en la práctica, la mayoría de los constructos de la capa 2 tienen un conjunto de métodos estáticos asociados que devuelven la interfaz de ese constructo de la capa 2. Estos métodos estáticos suelen empezar con la palabra from. Los dos primeros argumentos que se pasan a estos métodos son los mismos argumentos scope y id necesarios para un constructo estándar de la capa 2. Sin embargo, el tercer argumento no es props, sino un subconjunto pequeño de propiedades (o, a veces, solo una propiedad) que define una interfaz. Por este motivo, cuando se pasa un constructo de la capa 2, en la mayoría de los casos solo son necesarios los elementos de la interfaz. Esto permite utilizar también los recursos importados, siempre que sea posible.

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

Sin embargo, no debe confiar demasiado en las interfaces. Debe importar los recursos y utilizar las interfaces de manera directa solo cuando sea absolutamente necesario, ya que las interfaces no proporcionan muchas de las propiedades (como los métodos de ayuda) que hacen que un constructo de la capa 2 sea tan potente.

Métodos auxiliares

Un constructo de la capa 2 es una clase programática y no un objeto simple, por lo que puede exponer los métodos de clase que permiten manipular la configuración de los recursos una vez creada la instancia. Un buen ejemplo de ello es el constructo Role de la capa 2 de AWS Identity and Access Management (IAM). Los fragmentos siguientes muestran dos maneras de crear el mismo rol de IAM mediante el constructo Role de la capa 2.

Sin un 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' ] }) ] }) } });

Con un 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' ] }) ] }));

La capacidad de utilizar los métodos de la instancia para manipular la configuración de los recursos después de la creación de instancias proporciona a los constructos de la capa 2 mucha flexibilidad adicional con respecto a la capa anterior. Los constructos de la capa 1 también heredan algunos métodos de los recursos (por ejemplo addPropertyOverride), pero no se obtienen métodos diseñados en específico para ese recurso y sus propiedades hasta la segunda capa.

Enums

La sintaxis de CloudFormation a menudo requiere que se especifiquen muchos detalles para aprovisionar un recurso de manera correcta. Sin embargo, la mayoría de los casos de uso suelen incluir solo unas pocas configuraciones. La representación de esas configuraciones mediante una serie de valores enumerados puede reducir de manera considerable la cantidad de código necesaria.

En el ejemplo de código de la capa 2 del bucket de S3 que aparece anteriormente en esta sección, debe utilizar la propiedad bucketEncryption de la plantilla de CloudFormation para proporcionar todos los detalles, tal como el nombre del algoritmo de cifrado que se va a utilizar. En su lugar, AWS CDK proporciona la enumeración BucketEncryption, que utiliza las cinco maneras más comunes de cifrado de buckets y permite expresarlas mediante nombres de variables individuales.

¿Qué sucede con los casos extremos que no se cubren mediante las enumeraciones? Uno de los objetivos de un constructo de la capa 2 es simplificar la tarea de aprovisionamiento de un recurso de la capa 1, por lo que es posible que algunos casos extremos que se utilizan con menos frecuencia no se admitan en la capa 2. Para admitir estos casos extremos, AWS CDK permite manipular las propiedades de los recursos subyacentes de CloudFormation de manera directa mediante el método addPropertyOverride. Para más información sobre las anulaciones de las propiedades, consulte la sección Prácticas recomendadas de esta guía y la sección Abstracciones y vías de escape de la documentación. AWS CDK

Clases auxiliares

En algunas ocasiones, una enumeración no puede cumplir con la lógica programática necesaria para configurar un recurso de un caso de uso determinado. En estas situaciones, AWS CDK a menudo ofrece una clase auxiliar en su lugar. Una enumeración es un objeto simple que ofrece una serie de pares clave-valor, mientras que una clase auxiliar ofrece todas las funcionalidades de una clase de TypeScript. Una clase auxiliar aún puede actuar como una enumeración al exponer las propiedades estáticas, pero esas propiedades podrían tener sus valores establecidos de manera interna con lógica condicional en el constructor de la clase auxiliar o en un método auxiliar.

Por lo tanto, aunque la enumeración BucketEncryption puede reducir la cantidad de código necesaria para configurar un algoritmo de cifrado en un bucket de S3, esa misma estrategia no funcionaría al establecer duraciones temporales, ya que hay demasiados valores posibles entre los que elegir. Crear una enumeración para cada valor sería mucho más complicado que lo que vale la pena. Por este motivo, se utiliza una clase auxiliar para los valores predeterminados de la configuración de Bloqueo de objetos de S3 de un bucket de S3, representados por la clase ObjectLockRetention. ObjectLockRetention contiene dos métodos estáticos: uno para la retención del cumplimiento y otro para la retención de la gobernanza. Ambos métodos utilizan una instancia de la clase auxiliar Duration como argumento para expresar el tiempo durante el que se debe configurar el bloqueo.

Otro ejemplo es la clase auxiliar Runtime de AWS Lambda. A simple vista, podría parecer que las propiedades estáticas asociadas a esta clase podrían gestionarse mediante una enumeración. Sin embargo, en realidad, el valor de cada propiedad representa una instancia de la propia clase Runtime, por lo que la lógica que se utiliza en el constructor de la clase no podría lograrse en una enumeración.