JD의 블로그

CDK(Cloud Development Kit) 개념(2) 본문

클라우드/AWS

CDK(Cloud Development Kit) 개념(2)

GDong 2020. 5. 22. 14:01

저번 포스팅에서 CDK의 가장 중요한 구성요소인 App, Stack, 그리고 Construct에 대해 정리해봤는데

오늘은 CDK의 EnvironmentsResources에 대해서 다뤄보고자 합니다.

 

Environments

각각의 Stack 인스턴스는 Environment(env)와 명시적으로나 간접적으로 연관되어 있습니다. Environment라는 것은 Stack 인스턴스가 배치될 AWS 계정과 AWS Region에 대한 정보를 가지고 있습니다.

 

만약, Stack을 정의할 때 Environment를 명시해주지 않으면 Stack은 Environment에 영향을 받지 않는 상태가 됩니다. 그래서 Stack으로부터 AWS CloudFormation 템플릿을 생성할 때, 배포가 되는 시점에서 stack.account, stack.region, 그리고 stack.availabilityZones와 같은 환경 변수를 참조하여 값을 참조하게 됩니다. 

 

Environment에 영향을 받지 않는 상태에서 cdk deploy 명령어를 입력하게 되면, AWS CDK CLI는 AWS CLI의 profile을 참조하여 어디에 배포할지 결정하게 됩니다. 즉 AWS CLI와 유사한 방식으로, AWS 계정에 대한 작업을 하기 위해 어떤 AWS 자격을 사용할지 결정하게 되는 것입니다.

 

Production 환경을 위한 Stack의 경우에는 각 Stack에 대해 명시적으로 env를 사용하여 Environment를 지정하는 것이 권장됩니다. 

 

다음은 두 가지 다른 Environment를 가지는 Stack을 보여줍니다.

env_EU = core.Environment(account="8373873873", region="eu-west-1")
env_USA = core.Environment(account="23838383", region="us-west-2")

MyFirstStack(app, "first-stack-us", env=env_USA)
MyFirstStack(App, "first-stack-eu", env=env_EU)

또한 위의 방식처럼 계정이나 Region 정보를 하드 코딩하는 것이 아니라, 상황에 따라 유동적으로 이를 지정하고 싶을 때에는 AWS CDK CLI에 의해 제공되는 환경 변수를 사용할 수 있습니다.

 

import os
MyDevStack(app, "dev", env=core.Environment(
    account=os.environ["CDK_DEFAULT_ACCOUNT"],
    region=os.environ["CDK_DEFAUT_REGION"]))

이런 방식으로 사용하게 되면 Environment에 독립적인 코드로 작업할 수 있습니다.

또한 이런 환경 변수는 사용자가 원하는대로 env에서 설정할 수 있습니다. 

 

그리고 이런 변수는 스크립트를 통해 한 번에 지정할 수 있습니다. 

#!/bin/bash
# cdk-deploy-to.sh
export CDK_DEPLOY_ACCOUNT=$1
shift
export CDK_DEPLOY_REGION=$1
shift
cdk deploy "$@"

여기서는 기본적으로 두 개의 환경 변수를 추가하였지만, 원한다면 더 많은 환경 변수를 추가할 수도 있습니다.

$ bash cdk-deploy-to.sh 123456789 us-east-1 "$@"

 

Resources

AWS CDK는 AWS constructs라는 풍부한 Construct 라이브러리를 제공하는데 이것들이 AWS Resource를 표현하고 있습니다. 여기서는 이런 Construct를 어떻게 best practice로 사용할 수 있을지에 대한 관점에 대해 다룹니다. 

 

CDK App에서 AWS Resource를 정의하는 것은 Construct를 정의하는 것과 유사한데, Construct class의 인스턴스는 생성할 때 Scope와 ID, 그리고 구성 정보(Props)를 순서대로 매개변수에 넘겨주게 됩니다. 

 

아래 예시는 KMS 암호화를 하는 Amazon SQS 큐를 생성하는 것을 보여줍니다.

import aws_cdk.aws_sqs as sqs

sqs_Queue(self, "MyQueue", encryption=sqs.QueueEncryption.KMS_MANAGED)
# sqs_Queue(scope, id, props)

 

- 속성

Resource에는 속성이라는 것이 있는데, 이는 AWS CloudFormation에 의해 배포되는 시점에 결정되는 정보입니다. 이러한 속성 값은 Resource의 이름이 접두어로 하여 구성되며, 아래는 Amazon SQS 큐의 URL 속성을 가져오는 예제입니다.

from aws_cdk.aws_sqs as sqs

queue = sqs.Queue(self, "MyQueue")
url = queue.queue_url # 배포 시점에 결정되는 string 값이다.

 

- 참조

AWS CDK 클래스는 AWS CDK Resource 객체들을 나타내는 특징(Property)를 필요로 하는데, 이를 전달하는 방식이 크게 두 가지로 나뉩니다.

  • Resource를 직접적으로 전달하는 방법
  • 보통 ARN,이나 ID 그리고 이름과 같이 Resource를 나타내는 고유한 식별자를 전달하는 방법

예를 들자면, Amazon ECS 서비스는 클러스터를 참조해야하며, Amazon CloudFront 배포에는 소스 코드를 가진 S3 버킷을 참조할 필요가 있습니다.

 

아래는 Amazon ECS 서비스를 생성하기 위해 클러스터를 만들고 이를 참조하게 만드는 예제입니다.

cluster = ecs.Cluster(self, "Cluster)
service = ecs.Ec2Service(self, "Service", cluster=cluster)

 

- 다른 Stack의 리소스에 접근하기

같은 계정이나 같은 AWS Region에 있는 Resource의 경우 다른 Stack에 있더라도 접근이 가능합니다. 

아래의 예제는 Amazon S3 버킷을 정의하는 Stack1을 정의하고 그리고 Stack2에서 이를 사용하는 예제입니다.

prod = core.Environment(account="123456789012", region="us-east-1")

Stack1 = StackThatProvidesABucket(app, "Stack1", env=prod)

Stack2 = StackThatExpectsABucket(app, "Stack2", bucket=Stack1.bucket, env=prod)

AWS CDK는 Resource가 동일한 계정과 Region에 있지만 다른 Stack에 있다고 판단하면 자동으로 AWS CloudFormation exportFn::ImportValue(CloudFormation 함수) 를 사용하여 해당 정보를 하나의 Stack에서 다른 Stack으로 전달해줍니다. 

 

- 실제 이름(pythsical name)

AWS CloudFormation에 의해 지칭된 Resource의 이름은 논리적인 이름이기 때문에 배포된 후에 생성된 Resource의 실제 이름과 다릅니다. 예를 들면, Amazon S3를 생성한다면 논리적인 이름이 Stack2MyBucket4DD88B4F 였지만, 배포 후에 확인해본 실제 Resource의 이름은 stack2mybucket4dd88b4f-iuv1rbv9z3to로 이런 차이점을 확인할 수 있습니다.

 

그리고 실제 Resource의 이름은 Resource를 표현하는 Construct를 생성할 때 지정할 수 있습니다. 

아래의 예제는 Amazon S3 Bucket의 실제 이름을 지정하는 예제입니다.

bucket = s3.Bucket(self, "MyBucket", bucket_name="my-bucket-name")

그러나, 이렇게 실제 Resource 이름을 할당하는 방식에는 단점이 있는데, 기존에 배포된 Resource에 대한 변화로 Resource가 교체되어야 하는 상황에서 실제 Resource 이름이 할당되어 있다면 재배포시에 에러가 생길 수도 있다는 위험이 있습니다. 이런 상황에서는 기존의 AWS CloudFormation Stack을 삭제하고 AWS CDK App을 재배포하는 것이 해결책이 될 수 있긴 합니다.

 

또 다른 케이스를 살펴보면, AWS CDK App을 생성할 때 다른 Environment들이 서로 참조를 하는 상황에서는 AWS CDK가 정상적으로 동작하기 위해 실제 Resource의 이름이 필요하게 됩니다. 이런 경우에 실제 Resource의 이름을 생각하기 어렵다면 PhysicalName.GENERATE_IF_NEEDED 옵션을 통해 이를 해결할 수 있습니다. 

bucket = s3.Bucket(self, "MyBucket", bucket_name=core.PhysicalName.GENERATE_IF_NEEDED)

- 고유한 참조값 전달하기

가능하다면, Resource를 전달하여 참조하는게 좋긴 하지만 Resource의 고유한 참조값을 통해 전달해야하는 경우도 있습니다. 예를 들면, low-level의 CloudFormation Resource를 사용할 때나, AWS CDK 어플리케이션의 런타임 구성요소에 Resource를 드러낼 필요가 있을 때를 생각해볼 수 있습니다. 잘 와닿지 않으면 아래의 예시를 한 번 보시길 바랍니다.

 

이는 Amazon S3 버킷을 생성하고 이 버킷의 이름을 AWS Lambda로 전달해주는 예제입니다.

# 고유한 식별자 예시
# bucket.bucket_name
# lambda_func.function_arn
# security_group_arn

bucket = s3.Bucket(self, "Bucket")

lambda.Function(self, "MyLambda", environment=dict(BUCKET_NAME=bucket.bucket_name))

- 존재하는 외부 Resource 가져오기

AWS 계정에 이미 존재하는 Resource를 AWS CDK App에서 사용하고 싶을 때, Resource의 ARN이나 구별 가능한 식별자를 현재 작업중인 Stack 내의 AWS CDK 객체에 지정함으로써 사용할 수 있습니다.

 

아래의 예제는 현재 이미 존재하는 S3 객체와 VPC를 이용하는 예시입니다.

# 단순히 버킷 이름을 이용하여 사용하기 (같은 계정에 존재해야함)
s3.Bucket.from_bucket_name(self,"MyBucket", "my-bucket-name")

# ARN으로 참조하기 (다른 계정의 Resource도 참조 가능)
s3.Bucket.from_arn(self, "MyBucket", "arn:aws:s3:::my-bucket-name")

# 참조값을 통해서 사용하기
ec2.Vpc.from_vpc_attributes(self, "MyVpc", vpc_id="vpc-1234567890abcdef")

 

ec2.Vpc 같은 경우 VPC 그 자체, 서브넷, 보안 그룹, 그리고 라우팅 테이블과 같이 많은 AWS Resource로 이루어져 있는 복잡한 Contruct입니다. 그래서 단순히 참조값을 통해서 가져오기가 힘들 수 있는데, 이를 좀 더 정확하게 나타내기 위해 VPC Construct는 fromLookup(Python: from_lookup)이란 메서드(함수)를 가지고 있습니다. 이는 구성을 하는 시점에 필요한 모든 참조값을 그 상황에 맞게 결정해주며 미래에 그 값을 다시 사용할 것을 염두해 cdk.context.json 파일에 캐싱합니다. 그러나 AWS 계정에서 그 VPC를 식별하게 충분한 정보를 전달해줘야 한다는 주의점이 있습니다.

 

아래는 이를 활용해 default VPC를 가져오는 예제입니다.

ec2.Vpc.from_lookup(self, "DefaultVpc", is_default=True)

그리고 충분한 정보를 전달하기 위해 tags라는 것을 사용할 수 있는데, tags라는 것은 CloudFormation이나 AWS CDK에 의해 생성되는 시점에 추가되는 정보입니다. 예를 들면, AWS CDK는 자동적으로 VPC에 아래와 같은 tags를 추가합니다.

  • Name - VPC의 이름
  • aws-cdk:subnet-name - 서브넷의 이름
  • aws-cdk:subnet-type - 서브넷의 타입 : Public, Private, 또는 Isolated

이를 활용하여, 내 계정에 있는 Public VPC를 지정하고자 한다면 다음과 같은 방식으로 찾을 수 있습니다.

ec2.Vpc.from_lookup(self, "PublicVpc", tags={"aws-cdk:subnet-type": "Public"})

주의해야할 점은 from_lookup에서 찾을 수 있는 Resource는 env에 정의된 계정 및 Region 내에 있는 Resource만 찾을 수 있다는 것입니다.

 

- 권한 부여

AWS Construct는 Resource를 사용하기 위한 최소한의 권한만 허용하도록 설정되어 있습니다. 그리고 많은 AWS Construct는 grant 메서드를 통해 사용자가 쉽게 IAM 역할, 사용자, 허가를 통해 Resource로 작업할 수 있는 권한을 부여할 수 있게 해줍니다. 

 

아래의 예시는 Lambda 함수가 특정한 Amazon S3 버킷 객체에 대해 읽기 또는 쓰기 권한을 가질 수 있게 해줍니다. 또한 Amazon S3 버킷이 Amazon KMS 키를 이용해 암호화되었다면, 자동으로 복호화까지 할 수 있는 권한을 허용해줍니다. 

bucket.grant_read_write(func)

- 메트릭과 경보

많은 Resource들은 모니터링 대시보드와 경보를 만들기 위한 CloudWatch 메트릭을 생성합니다. AWS Construct는 정확한 이름을 몰라서 이런 메트릭에 접근하기 쉽게 해주는 메트릭 메서드를 가지고 있습니다. 

 

아래의 예제는 Amazon SQS 큐에 ApproximateNumberOfMessageNotVisible 메트릭이 100개가 쌓이면 경보를 울리는 예시입니다.

import aws_cdk.aws_cloudwatch as cw
import aws_cdk.aws_sqs as sqs
from aws_cdk.core import Duration

queue = sqs.Queue(self, "MyQueue")

# 메트릭 객체 생성
metric = queue.metric_approximate_number_of_messages_not_visible(
    label="Messages Visible (Approx)",
    period=Duration.minutes(5),
)

# 메트릭 객체를 기반으로 경보 생성
metric.create_alarm(self, "TooManyMessageAlarm",
    comparison_operator=cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
    threshold=100,
)




 

- 네트워크 트래픽

연결을 수신하기 위해서는 보안 그룹 규칙이나 네트워크 접근 허용 규칙을 통해 트래픽 흐름을 허용하는 방법을 지정해줘야 합니다.  IConnectable Resource들은 네트워크 트래픽 규칙을 구성하는 게이트웨이인 connections라는 특성을 가지고 있습니다. 

 

그리고 사용자는 특정 네트워크 경로에 대한 데이터 흐름을 허용하기 위해 allow 메서드를 사용할 수 있습니다. 

아래의 예제는 웹에 대한 HTTPS 연결을 허용하고, Amazon EC2 Auto Scaling 그룹인 fleet2에서 들어오는 트래픽을 허용해주는 예시입니다.

import aws_cdk.aws_autoscaling as asg
import aws_cdk.aws_ec2 as ec2

fleet1 = asg.AutoScalingGroup( ... )

# HTTPS 연결 허용
fleet1.connections.allow_to(ec2.Peer.any_ipv4(),
    ec2.Port(PortProps(from_port=443, to_port=443)))

# fleet1은 fleet2에서 오는 모든 트래픽을 허용함.
fleet2 = asg.AutoScalingGroup( ... )
fleet1.connections.allow_from(fleet2, ec2.Port.all_traffic())

 

특정 Resource의 경우 이와 관련되어 기본적으로 연관된 포트를 가질 수 있습니다. 예를 들어, 로드 밸런서의 리스너에 대한 퍼블릭 포트(TCP, 80번 포트)와 Amazon RDS 데이터베이스의 인스턴스와 데이터베이스 엔진 사이의 연결(TCP, 3306번 포트)를 생각해볼 수 있습니다. 이러한 경우에는 allowDefaultPortFrom(Python: allow_default_port_from) 이나 allowToDefaultPort(Python: allow_to_deafult_port) 메서드를 통해 자동으로 네트워크 설정을 할 수 있습니다. 

 

아래의 예시는 어떤 IPv4 주소에 대해서도 연결을 허용하고, Auto Scaling Group이 데이터베이스에 액세스할 수 있는 네트워크 설정을 할 수 있게 해주는 것을 보여줍니다.

listener.connections.allow_default_port_from_any_ipv4("Allow public access")

fleet.connections.allow_to_default_port(rds_database, "Fleet can access database")

 

- 이벤트 핸들링

몇몇 Resource의 경우 이벤트를 기반하여 동작하게 되는데, 이 때 addEventNotification(Python: add_event_notification) 메서드를 사용하게 되면 특정 Resource에서 발생한 이벤트를 기반으로 타겟이 동작할 수 있게 해줍니다. 

 

아래의 예시는 Amazon S3 버킷에 객체가 추가되면 Lambda 함수를 트리거할 수 있게 해주는 것을 보여줍니다.

import aws_cdk.aws_s3_notification as s3_nots

handler = lambda_.Function(self, "Handler", ...) 
# lamdba는 Python에서 시스템 변수로 지정되어 있기 때문에 lambda_로 선언

bucket = s3.Bucket(self, "Bucket")
bucket.add_object_created_notification(s3_nots.LambdaDestination(handler))

 

- 제거 정책

데이터베이스나 Amazon S3 버킷과 같은 Resource들은 영구적인 데이터를 저장하기 때문에 AWS CDK Stack에 의해 해당 Resource가 삭제될 때 영구적인 객체를 삭제할지 결정할 수 있는 제거 정책(Removal Policy)을 설정할 수 있습니다. 

이는 AWS CDK의 core 모듈에 있는 RemovalPolicy를 통해 설정할 수 있습니다.

 

참고) RemovalPolicy의 경우 Amazon S3 버킷과 DynamoDB 테이블의 제거 정책과 다른 목적으로도 사용될 수 있는데, 예를 들어 Lambda 함수의 경우 새로운 버전으로 업데이트할 때 이전 버전을 유지할 지 결정할 때 RemovalPolicy를 사용할 수도 있습니다.

 

또한, AWS CloudFormation의 경우 Amazon S3 버킷에 파일이 들어있을 경우 RemovalPolicy.DESTROY로 설정해도 삭제되지 않도록 설정되어 있습니다. 그러나 이러한 경우에 서드파티 Construct인 auto-delete-bucket과 같은 사용자 Resource를 사용해 자동적으로 파일을 삭제한 후에 S3 버킷을 제거할 수 있습니다. 

 

참고 ) AWS CDK의 RemovalPolicy 메서드는 AWS CloudFormation의 DeletePolicy와 같은 역할을 한다고 볼 수 있는데, AWS CDK의 기본 설정은 데이터를 보존하는 것이지만 AWS CloudFormation의 기본 설정은 이와 반대라는 것을 알아두는 것이 좋습니다. 

 

마무리

이번 포스트로 인해서 CDK가 기존의 CloudFormation 등과 비교하여 얼마나 간편하게 인프라를 셋팅하고 설정할 수 있는지 약간이나마 느낄 수 있었으면 좋겠다는 생각을 하면서 정리를 하게 되었습니다. 다양한 기능을 굉장히 추상화하여 인프라를 셋팅하는 데 드는 노력을 많이 줄일 수 있으며 공식 Github 레포지토리에 다양한 예제가 있으니 이를 통해 CDK를 배워볼 수도 있을 것 같습니다.

 

읽어주셔서 감사합니다.