계층 2 구문 - AWS 권장 가이드

계층 2 구문

AWS CDK 오픈 소스 리포지토리는 주로 TypeScript 프로그래밍 언어를 사용하여 작성되며 다양한 패키지와 모듈로 구성됩니다. aws-cdk-lib라는 기본 패키지 라이브러리는 대략적으로 AWS 서비스당 하나의 패키지로 분할되지만, 항상 그렇지는 않습니다. 앞서 설명한 대로 L1 구문은 빌드 프로세스 중에 자동으로 생성됩니다. 그렇다면 리포지토리 내부를 살펴볼 때 표시되는 모든 코드는 무엇인가요? 이는 L1 구문의 추상화에 해당하는 L2 구문입니다.

패키지에는 TypeScript 유형, 열거형 및 인터페이스의 컬렉션과 더 많은 기능을 추가하는 헬퍼 클래스가 포함됩니다. 이러한 항목은 모두 L2 구문도 지원합니다. 모든 L2 구문은 인스턴스화할 때 생성자에서 해당 L1 구문을 직접 호출하고, 그 결과 생성된 L1 구문은 다음과 같이 계층 2에서 액세스할 수 있습니다.

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

L2 구문은 기본 속성, 편의 메서드 및 기타 간결한 구문(syntactic sugar)을 사용하여 L1 구문에 적용합니다. 이 방식에서는 CloudFormation에서 리소스를 직접 프로비저닝하는 데 필요한 많은 반복과 상세 정보가 제거됩니다.

모든 L2 구문은 해당 L1 구문을 후드 아래에 빌드합니다. 그러나 L2 구문은 실제로 L1 구문을 확장하지 않습니다. L1 및 L2 구문 모두 Construct라는 특수 클래스를 상속합니다. AWS CDK의 버전 1에서는 Construct 클래스가 개발 기트에 빌드되지만, 버전 2에서는 별도의 독립형 패키지에 해당합니다. 그래서 Cloud Development Kit for Terraform(CDKTF)과 같은 다른 패키지가 이를 종속성으로 포함할 수 있습니다. Construct 클래스를 상속하는 모든 클래스는 L1, L2 또는 L3 구문입니다. 다음 표와 같이 L2 구문은 이 클래스를 직접 확장하는 반면, L1 구문은 CfnResource라는 클래스를 확장합니다.

L1 상속 트리

L2 상속 트리

L1 구문

→ 클래스 CfnResource

→→ 추상 클래스 CfnRefElement

→→→ 추상 클래스 CfnElement

→→→→ 클래스 Construct

L2 구문

→ 클래스 Construct

L1 및 L2 구문이 모두 Construct 클래스를 상속하는 경우 L2 구문이 L1을 확장하지 않는 이유는 무엇인가요? Construct 클래스와 계층 1 사이의 클래스는 CloudFormation 리소스의 미러 이미지로 L1 구문을 제자리에서 잠급니다. 여기에는 _toCloudFormation과 같은 추상 메서드(다운스트림 클래스가 포함해야 하는 필수 메서드)가 포함되며, 이는 구문에서 CloudFormation 구문을 직접 강제 출력합니다. L2 구문은 해당 클래스를 건너뛰고 Construct 클래스를 직접 확장합니다. 이 경우 해당 생성자 내에서 별도로 빌드하여 L1 구문에 필요한 대부분의 코드를 추상화하는 유연성을 활용할 수 있습니다.

이전 섹션에서는 CloudFormation 템플릿의 S3 버킷과 L1 구문으로 렌더링된 동일한 S3 버킷을 항목별로 비교했습니다. 이 비교를 통해 속성과 구문은 거의 동일했지만, L1 구문은 CloudFormation 구문에 비해 3~4줄만 저장하는 것으로 나타났습니다. 이제 L1 구문을 동일한 S3 버킷의 L2 구문과 비교해 보겠습니다.

S3 버킷에 대한 L1 구문

S3 버킷에 대한 L2 구문

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 });

보시다시피 L2 구문은 L1 구문 크기의 절반 미만입니다. L2 구문은 이러한 통합을 위해 많은 기술을 사용합니다. 이러한 기법 중 일부는 단일 L2 구문에 적용되지만, 재사용성을 위해 자체 클래스로 분리되도록 여러 구문에서 재사용할 수 있는 기법도 있습니다. L2 구문은 다음 섹션에서 설명한 대로 여러 가지 방법으로 CloudFormation 구문을 통합합니다.

기본 속성

리소스를 프로비저닝하기 위해 코드를 통합하는 가장 간단한 방법은 가장 일반적인 속성 설정을 기본값으로 설정하는 것입니다. AWS CDK는 강력한 프로그래밍 언어에 액세스할 수 있고 CloudFormation은 액세스할 수 없으므로, 이러한 기본값은 종종 조건부로 적용됩니다. AWS CDK 코드에서 여러 줄의 CloudFormation 구성을 제거할 수 있는 경우가 있습니다. 해당 설정은 구문에 전달되는 다른 속성의 값에서 CloudFormation 구성을 추론할 수 있기 때문입니다.

구조, 유형 및 인터페이스

AWS CDK는 여러 프로그래밍 언어로 제공되지만 기본적으로 TypeScript로 작성되므로 언어 유형 시스템을 사용하여 L2 구문을 구성하는 유형을 정의합니다. 이 가이드에서는 해당 유형 시스템을 자세히 살펴보지 않습니다. 자세한 내용은 TypeScript 설명서를 참조하세요. 요약하자면 TypeScript type은 특정 변수가 보유한 데이터의 종류를 설명합니다. 이는 string과 같은 기본 데이터 또는 object와 같은 보다 복잡한 데이터일 수 있습니다. TypeScript interface는 TypeScript 객체 유형을 표현하는 또 다른 방법이며, struct는 인터페이스의 또 다른 이름입니다.

TypeScript는 구조체라는 용어를 사용하지 않지만 AWS CDK API 참조를 보면 구조체가 실제로 코드 내 다른 TypeScript 인터페이스임을 알 수 있습니다. API 참조는 특정 인터페이스도 인터페이스로 참조합니다. 구조체와 인터페이스가 동일하다면 AWS CDK 설명서에서 구조체와 인터페이스를 구분하는 이유는 무엇인가요?

AWS CDK에서 구조체라고 하는 요소는 L2 구문에서 사용하는 객체를 나타내는 인터페이스입니다. 여기에는 인스턴스화 중에 L2 구문에 전달되는 속성 인수의 객체 유형(예: S3 버킷 구문의 경우 BucketProps, DynamoDB 테이불 구문의 경우 TableProps)과 AWS CDK에서 사용되는 기타 TypeScript 인터페이스가 포함됩니다. 즉, AWS CDK 내 TypeScript 인터페이스이고 이름 앞에 I 접두사가 붙지 않은 경우 AWS CDK에서는 이를 구조체라고 합니다.

반대로, AWS CDK에서는 일반 객체를 특정 구문이나 헬퍼 클래스의 적절한 표현으로 간주해야 하는 기본 요소를 나타내기 위해 인터페이스라는 용어를 사용합니다. 즉, 인터페이스는 L2 구문의 퍼블릭 속성이 어떠해야 하는지 설명합니다. 모든 AWS CDK 인터페이스 이름은 I 문자 접두사가 추가된 기존 구문 또는 헬퍼 클래스의 이름입니다. 모든 L2 구문은 Construct 클래스를 확장하지만 대응하는 인터페이스도 구현합니다. 따라서 L2 구문 BucketIBucket 인터페이스를 구현합니다.

정적 메서드

L2 구문의 모든 인스턴스는 해당 인터페이스의 인스턴스이기도 하지만, 반대의 경우에는 해당되지 않습니다. 이는 구조체를 보면서 필요한 데이터 유형을 확인할 때 중요합니다. 구조체에 데이터 유형 IBucket이 필요한 bucket 속성이 있는 경우 L2 Bucket의 인스턴스 또는 IBucket 인터페이스에 나열된 속성이 포함된 객체를 전달할 수 있습니다. 둘 중 하나가 작동합니다. 그러나 해당 bucket 속성이 L2 Bucket에 대해 직접 호출되는 경우 해당 필드에서 Bucket 인스턴스만 전달할 수 있습니다.

기존 리소스를 스택으로 가져올 때 이러한 구분이 매우 중요합니다. 스택에서 기본적인 리소스에 대해 L2 구문을 생성할 수 있지만 스택 외부에서 생성된 리소스를 참조해야 하는 경우 해당 L2 구문의 인터페이스를 사용해야 합니다. L2 구문을 생성하면 해당 스택 내 아직 없는 경우 새 리소스가 생성되기 때문입니다. 기존 리소스에 대한 참조는 해당 L2 구문의 인터페이스를 준수하는 일반 객체여야 합니다.

실제로 더 쉽게 수행하기 위해 대부분의 L2 구문에는 해당 L2 구문의 인터페이스를 반환하는 정적 메서드 세트가 연결되어 있습니다. 이러한 정적 메서드는 일반적으로 from 단어로 시작합니다. 이러한 메서드에 전달되는 처음 두 인수는 표준 L2 구문에 필요한 scopeid 인수와 동일합니다. 그러나 세 번째 인수는 props가 아니라 인터페이스를 정의하는 속성의 작은 하위 세트(또는 경우에 따라 하나의 속성)입니다. 이러한 이유로 L2 구문을 전달할 때 대부분의 경우 인터페이스의 요소만 필요합니다. 그러면 가능한 경우 가져온 리소스도 사용할 수 있습니다.

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

그러나 인터페이스에 크게 의존해서는 안 됩니다. 인터페이스는 L2 구문 성능을 더욱 강화하는 헬퍼 메서드와 같은 많은 속성을 제공하지 않으므로 반드시 필요한 경우에만 리소스를 가져오고 인터페이스를 직접 사용해야 합니다.

도우미 메서드

L2 구문은 단순한 객체가 아닌 프로그래밍 방식의 클래스이므로 인스턴스화된 후 리소스 구성을 조작할 수 있는 클래스 메서드를 노출할 수 있습니다. 이에 대한 좋은 예는 AWS Identity and Access Management(IAM) L2 Role 구문입니다. 다음 코드 조각에서는 L2 Role 구문을 사용하여 동일한 IAM 역할을 생성하는 두 가지 방법을 보여줍니다.

헬퍼 메서드가 없는 경우:

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

헬퍼 메서드가 있는 경우:

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

인스턴스화된 후 인스턴스 메서드를 사용하여 리소스 구성을 조작하는 기능을 통해 L2 구문은 이전 계층에 비해 많은 추가 유연성을 제공합니다. 또한 L1 구문은 일부 리소스 메서드(예: addPropertyOverride)도 상속하지만 계층 2까지는 해당 리소스 및 해당 속성에 맞게 특별히 설계된 메서드를 얻을 수 없습니다.

Enums

CloudFormation 구문을 사용하려면 리소스를 올바르게 프로비저닝하기 위해 종종 많은 세부 정보를 지정해야 합니다. 그러나 몇 개의 구성으로도 대부분의 사용 사례를 지원할 수 있습니다. 일련의 열거형 값을 사용하여 이러한 구성을 표현하면 필요한 코드 양을 크게 줄일 수 있습니다.

예를 들어 이 섹션 앞부분에 나온 S3 버킷 L2 코드 예제에서는 CloudFormation 템플릿의 bucketEncryption 속성을 사용하여 사용할 암호화 알고리즘의 이름을 포함한 모든 세부 정보를 제공해야 합니다. AWS CDK에서는 대신 BucketEncryption 열거형을 제공합니다. 여기에서는 가장 일반적인 5가지 형태의 버킷 암호화를 사용하고 단일 변수 이름을 사용하여 각각을 표현할 수 있습니다.

열거형에서 다루지 않는 엣지 사례는 어떤가요? L2 구문의 목표 중 하나는 계층 1 리소스를 프로비저닝하는 태스크를 단순화하는 것이므로, 덜 일반적으로 사용되는 특정 엣지 사례는 계층 2에서 지원되지 않을 수 있습니다. 이러한 엣지 사례를 지원하기 위해 AWS CDK에서는 addPropertyOverride 메서드를 사용하여 기본 CloudFormation 리소스 속성을 직접 조작할 수 있습니다. 속성 재정의에 대한 자세한 내용은 이 가이드의 모범 사례 섹션과 AWS CDK 설명서의 Abstractions and escape hatches를 참조하세요.

헬퍼 클래스

때로는 열거형을 통해 지정된 사용 사례에 맞게 리소스를 구성하는 데 필요한 프로그래밍 로직을 수행하지 못할 수 있습니다. 이러한 상황에서 AWS CDK는 대신 헬퍼 클래스를 제공하곤 합니다. 열거형은 일련의 키 값 페어를 제공하는 간단한 객체인 반면, 헬퍼 클래스는 TypeScript 클래스의 전체 기능을 제공합니다. 헬퍼 클래스는 여전히 정적 속성을 노출하여 열거형과 같이 작동할 수 있지만, 이러한 속성은 헬퍼 클래스 생성자 또는 헬퍼 메서드에서 조건부 로직을 통해 내부적으로 해당 값을 설정할 수 있습니다.

따라서 BucketEncryption 열거형은 S3 버킷에서 암호화 알고리즘을 설정하는 데 필요한 코드 양을 줄일 수 있지만, 선택할 수 있는 값이 너무 많기 때문에 기간을 설정하는 데 동일한 전략이 작동하지 않습니다. 각 값에 열거형을 생성하는 작업은 이점보다 단점이 더 많습니다. 이러한 이유로 ObjectLockRetention 클래스로 표시되는 S3 버킷의 기본 S3 Object Lock 구성 설정에 헬퍼 클래스가 사용됩니다. ObjectLockRetention에는 두 가지 정적 메서드(규정 준수 보존을 위한 메서드 및 거버넌스 보존을 위한 메서드)가 있습니다. 두 메서드 모두 Duration 헬퍼 클래스의 인스턴스를 인수로 사용하여 잠금을 구성해야 하는 시간을 표시합니다.

또 다른 예로 AWS Lambda 헬퍼 클래스 Runtime이 있습니다. 얼핏 보면 이 클래스와 연결된 정적 속성을 열거형에서 처리할 수 있는 것처럼 보입니다. 그러나 자세히 보면 각 속성 값은 Runtime 클래스 자체의 인스턴스를 나타내므로, 클래스 생성자에서 수행된 로직은 열거형 내에서 달성할 수 없습니다.