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

계층 3 구문

L1 구문이 CloudFormation 리소스를 프로그래밍 코드로 리터럴 변환하는 작업을 수행하고 L2 구문이 상세한 CloudFormation 구문의 대부분을 헬퍼 메서드 및 사용자 지정 로직으로 대체하는 경우 L3 구문은 무엇을 하나요? 이에 대한 답변은 상상하는 그대로입니다. 특정 사용 사례에 맞게 계층 3을 생성할 수 있습니다. 프로젝트에 특정 속성 하위 세트가 있는 리소스가 필요한 경우 해당 요구 사항을 충족하기 위해 재사용 가능한 L3 구문을 생성할 수 있습니다.

L3 구문을 AWS CDK 내에서 패턴이라고 합니다. 패턴은 AWS CDK에서 Construct 클래스를 확장하거나 Construct 클래스를 확장하는 클래스를 확장하여 계층 2를 넘어 추상화된 로직을 수행하는 객체입니다. AWS CDK CLI를 통해 cdk init를 실행하여 새 AWS CDK 프로젝트를 시작하는 경우 app, lib, sample-app과 같은 세 가지 AWS CDK 애플리케이션 유형 중에서 선택해야 합니다.

AWS CDK 애플리케이션 유형

appsample-app은 모두 CloudFormation 스택을 빌드하고 AWS 환경에 배포하는 클래식 AWS CDK 애플리케이션을 나타냅니다. lib를 선택하는 경우 새로운 L3 구문을 빌드하기로 선택하는 것입니다. appsample-app을 사용하면 AWS CDK에서 지원하는 임의의 언어를 선택할 수 있지만 lib에서는 TypeScript만 선택할 수 있습니다. AWS CDK가 기본적으로 TypeScript로 작성되고 JSii라는 오픈 소스 시스템을 사용하여 원본 코드를 지원되는 다른 언어로 번역하기 때문입니다. lib를 선택하여 프로젝트를 시작하는 경우 AWS CDK에 대한 확장을 빌드하도록 선택하는 것입니다.

클래스를 확장하는 모든 Construct 클래스는 L3 구문일 수 있지만 계층 3의 가장 일반적인 사용 사례는 리소스 상호 작용, 리소스 확장 및 사용자 지정 리소스입니다. 대부분의 L3 구문은 AWS CDK 기능을 확장하기 위해 이 세 가지 사례 중 하나 이상을 사용합니다.

리소스 상호 작용

솔루션은 일반적으로 함께 작동하는 여러 AWS 서비스를 사용합니다. 예를 들어 Amazon CloudFront 배포는 종종 S3 버킷을 오리진으로 사용하고 일반적인 악용으로부터 AWS WAF를 보호합니다. AWS AppSync 및 Amazon API Gateway는 종종 Amazon DynamoDB 테이블을 API의 데이터 소스로 사용합니다. AWS CodePipeline의 파이프라인은 종종 Amazon S3를 소스로 사용하고 빌드 단계에 AWS CodeBuild를 사용합니다. 이러한 경우 둘 이상의 상호 연결된 L2 구문의 프로비저닝을 처리하는 단일 L3 구문을 생성하는 것이 유용하곤 합니다.

다음은 S3 오리진, 앞에 입력할 AWS WAF, Amazon Route 53 레코드, AWS Certificate Manager(ACM) 인증서와 함께 CloudFront 배포를 프로비저닝하여 전송 중 암호화를 적용해 사용자 지정 엔드포인트를 추가하는 L3 구문 예제입니다. 모두 하나의 재사용 가능한 구문으로 제공됩니다.

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

CloudFront, Amazon S3, Route 53 및 ACM은 모두 L2 구문을 사용하지만 웹 ACL(웹 요청 처리를 위한 규칙 정의)은 L1 구문을 사용합니다. AWS CDK가 완전히 완료되지 않은 진화하는 오픈 소스 패키지이며 아직 WebAcl에 대한 L2 구문이 없기 때문입니다. 그러나 누구라고 새 L2 구문을 생성하여 AWS CDK에 기여할 수 있습니다. 따라서 AWS CDK에서 WebAcl에 대한 L2 구문을 제공할 때까지 L1 구문을 사용해야 합니다. L3 구문 CloudFrontWebsite를 사용하여 새 웹 사이트를 생성하려면 다음 코드를 사용합니다.

const siteADotCom = new CloudFrontWebsite(stack, "siteA", siteAProps); const siteBDotCom = new CloudFrontWebsite(stack, "siteB", siteBProps); const siteCDotCom = new CloudFrontWebsite(stack, "siteC", siteCProps);

이 예제에서 CloudFront Distribution L2 구문은 L3 구문의 퍼블릭 속성으로 노출됩니다. 필요에 따라 여전히 이와 같은 L3 속성을 노출해야 하는 경우도 있습니다. 사실 나중에 사용자 지정 리소스 섹션에서 Distribution을 다시 살펴보겠습니다.

AWS CDK에는 이와 같은 리소스 상호 작용 패턴에 대한 몇 가지 예제가 포함되어 있습니다. Amazon Elastic Container Service(Amazon ECS)에 대한 L2 구문을 포함하는 aws-ecs 패키지 외에도 AWS CDK에는 aws-ecs-patterns라는 패키지가 있습니다. 이 패키지에는 Amazon ECS를 Application Load Balancer, Network Load Balancer 및 대상 그룹과 결합하는 동시에 Amazon Elastic Compute Cloud(Amazon EC2) 및 AWS Fargate에 대해 사전 설정된 여러 버전을 제공하는 여러 L3 구문이 포함되어 있습니다. 많은 서버리스 애플리케이션은 Fargate에서만 Amazon ECS를 사용하기 때문에 이러한 L3 구문은 개발자의 시간과 고객의 비용을 절약할 수 있는 편의를 제공합니다.

리소스 확장

일부 사용 사례에서는 리소스에 L2 구문의 기본이 아닌 특정 기본 설정이 있어야 합니다. 스택 수준에서 측면을 사용하여 처리할 수 있지만 L2 구문에 새 기본값을 제공하는 또 다른 편리한 방법은 계층 2를 확장하는 것입니다. 구문은 Construct 클래스를 상속하는 클래스이고 L2 구문은 해당 클래스를 확장하므로 L2 구문을 직접 확장하여 L3 구문을 생성할 수도 있습니다.

고객의 단일 요구 사항을 지원하는 사용자 지정 비즈니스 로직에 특히 유용할 수 있습니다. 회사에 모든 AWS Lambda 함수 코드를 src/lambda라는 단일 디렉터리에 저장하고, 대부분의 Lambda 함수가 매번 동일한 런타임 및 핸들러 이름을 재사용하는 리포지토리가 있다고 가정합니다. 새 Lambda 함수를 구성할 때마다 코드 경로를 구성하는 대신 새 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 }); }

그런 다음 다음과 같이 리포지토리의 어디서든 L2 Function 구문을 교체할 수 있습니다.

new MyCompanyLambdaFunction(this, "MyFunction"); new MyCompanyLambdaFunction(this, "MyOtherFunction"); new MyCompanyLambdaFunction(this, "MyThirdFunction", { runtime: Runtime.PYTHON_3_11 });

기본값을 사용하면 한 줄에 새 Lambda 함수를 생성할 수 있으며, 필요한 경우 기본 속성을 재정의할 수 있도록 L3 구문이 설정됩니다.

기존 L2 구문에 새 기본값을 추가하려는 경우 L2 구문을 직접 확장하는 것이 가장 좋습니다. 다른 사용자 지정 로직도 필요한 경우 Construct 클래스를 확장하는 것이 좋습니다. 이 이유는 생성자 내에서 직접 호출되는 super 메서드 때문입니다. 다른 클래스를 확장하는 클래스에서 super 메서드는 상위 클래스의 생성자를 직접 호출하는 데 사용되며, 반드시 생성자 내에서 나타나는 첫 번째 항목이어야 합니다. 즉, 전달된 인수 또는 기타 사용자 지정 로직의 조작은 원래 L2 구문이 생성된 에만 수행될 수 있습니다. L2 구문을 인스턴스화하기 전에 이 사용자 지정 로직을 수행해야 하는 경우 리소스 상호 작용 섹션에서 앞서 설명한 패턴을 따르는 것이 좋습니다.

사용자 지정 리소스

사용자 지정 리소스는 스택 배포 중에 활성화되는 Lambda 함수에서 사용자 지정 로직을 실행할 수 있는 CloudFormation의 강력한 기능입니다. 배포 중에 CloudFormation에서 직접 지원하지 않는 프로세스가 필요할 때마다 사용자 지정 리소스를 사용하여 프로세스를 실행할 수 있습니다. AWS CDK에서는 프로그래밍 방식으로 사용자 지정 리소스를 생성할 수 있는 클래스도 제공합니다. L3 생성자 내에서 사용자 지정 리소스를 사용하면 거의 모든 것에서 구문을 만들 수 있습니다.

Amazon CloudFront 사용 시 이점 중 하나는 강력한 글로벌 캐싱 기능입니다. 웹 사이트에서 오리진에 대한 새로운 변경 사항을 즉시 반영하도록 해당 캐시를 수동으로 재설정하려는 경우 CloudFront 무효화를 사용할 수 있습니다. 그러나 무효화는 CloudFront 배포의 속성이 아닌 CloudFront 배포에서 실행되는 프로세스입니다. 언제든지 생성하여 기존 배포에 적용할 수 있으므로 기본적으로 프로비저닝 및 배포 프로세스의 일부가 아닙니다.

이 시나리오에서는 배포의 오리진을 업데이트할 때마다 이후에 무효화를 생성하고 실행할 수 있습니다. 사용자 지정 리소스로 인해 다음과 비슷한은 L3 구문을 생성할 수 있습니다.

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

CloudFrontWebsite L3 구문에서 앞서 생성한 배포를 사용하면 다음과 같은 작업을 매우 쉽게 수행할 수 있습니다.

new CloudFrontInvalidation(this, 'MyInvalidation', { distribution: siteADotCom.distribution });

이 L3 구문은 AwsCustomResource라는 AWS CDK L3 구문을 사용하여 사용자 지정 로직을 수행하는 사용자 지정 리소스를 생성합니다. AwsCustomResource는 정확히 하나의 AWS SDK 직접 호출을 수행해야 할 때 매우 편리합니다. Lambda 코드를 작성하지 않고도 가능하기 때문입니다. 요구 사항이 더 복잡하고 자체 로직을 구현하려는 경우 기본 CustomResource 클래스를 직접 사용할 수 있습니다.

사용자 지정 리소스 L3 구문을 사용하는 AWS CDK에 대한 또 다른 좋은 예는 S3 버킷 배포입니다. 이 L3 구문의 생성자 내에서 사용자 지정 리소스에 의해 생성된 Lambda 함수는 기능을 추가합니다. 이를 통해 CloudFormation에서 해당 기능을 처리할 수 있습니다. 즉, S3 버킷에서 객체를 추가하고 업데이트합니다. S3 버킷 배포가 없으면 스택의 일부로 방금 생성한 S3 버킷에 콘텐츠를 넣을 수 없으므로 매우 불편합니다.

CloudFormation 구문을 작성할 필요가 없는 가장 좋은 AWS CDK에 대한 예제는 기본 S3BucketDeployment입니다.

new BucketDeployment(this, 'BucketObjects', { sources: [Source.asset('./path/to/amzn-s3-demo-bucket')], destinationBucket: amzn-s3-demo-bucket });

동일한 작업을 수행하기 위해 작성해야 하는 CloudFormation 코드와 비교합니다.

"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번째 줄과 241번째 줄은 차이가 있습니다! 이는 계층 3을 활용하여 스택을 사용자 지정할 때 가능한 사항을 보여주는 한 가지 예에 불과합니다.