(삽질을 하면서 배운) CDK 사용 팁 2탄

Dongkyu Ko
딜리버스
Published in
8 min readFeb 6, 2023

Daas(Delivus as a Service) 인프라를 AWS CDK v2(ver2.0.0-rc9)로 삽질을 하면서 구축한지 약 1년 6개월이 되었습니다. 그렇게 시간이 지나니 CDK v2도 제가 처음 사용하던 개발 버전에서 현재는 정식으로 v2.63.2까지 릴리스가 되어있었습니다. 그래서 CDK 관련 글을 올린지도 1년 4개월 지났고 그동안 유지보수 하면서 생긴 팁들에 대해 이야기하고자 합니다.

글을 쓰기 전에 전에 AWS CDK(Cloud Development Kit)을 참고하셔도 좋습니다. (1년이 지난 옛날 글이긴 하네요)

AWS CDK 사용 환경 구성(Python)

Github Actions 기반의 CD(Continuous Delivery) 구축기

AWS CDK를 사용한 Daas 인프라 구축 및 사용 팁

L1(Cfn000), L2(intent-based API)의 차이점

편의성이 높은 L2 사용을 권장 하지만 CDK에서 L2를 사용할 수 있게 기능 업데이트가 되지 않았다면 L1 사용

참고 자료

(1) L1 접두사(Prefix)는 항상 “Cfn”으로 시작

import aws_cdk.aws_s3 as s3
# L1
cfn_s3 = s3.CfnBucket()

# L2
s3 = s3.Bucket()

(2) L1은 Cloudformation을 코드로 정의한 것과 같고 L2는 L1처럼 세부적인 정보를 하나하나 정의할 필요도 없고 Cloudformation을 잘 모르더라도 사용 가능

  • L1을 사용하게 되면 자연스럽게 Cloudformation 문서를 자주 보게 됨

(3) L1의 경우 특정 값이 필요한 paramters를 정의할 때 string 또는 int로 정의하지만 L2의 경우 CDK에서 지원하는 데이터 타입 사용 (L2가 데이터 타입에 대한 빌드 에러 확률 낮음)

import aws_cdk.aws_sqs as sqs

# L1
cfn_queue = sqs.CfnQueue(self, "MyCfnQueue",
visibility_timeout=123
)

# L2
queue = sqs.Queue(self, "L2Queue",
visibility_timeout=cdk.Duration.seconds(180),
)

(4) L2가 L1보다 편의성이 높은 다양한 메서드(Methods) 지원

  • 예시- L1의 경우 Metric 정의를 따로 CloudWatch 모듈을 사용해서 정의해야하지만 L2의 경우 Metric관련 메서드가 대부분 존재

(5) L1은 Cloudformation을 코드로 정의한 것과 같기 때문에 크리티컬한 버그가 존재하지 않는 이상 CDK 업데이트를 기다릴 필요가 없지만 L2의 경우 특정 기능이 존재하지 않으면 CDK 업데이트를 기다리거나 필수적으로 L1 사용

  • 필자의 경우 CDK 초기에는 Cfn을 많이 사용했지만 점점 L2로 전환하고 있는 중 (편의성 높은 메서드 제공 및 L1 → L2 전환하는 코드를 사용하지 않기 위해)

CDK 사용 전 원하는 기능이 지원하는지 항상 확인

CDK로 새로운 리소스를 추가하거나 리소스에 기능을 추가하고 싶을 때 CDK 문서를 확인하고 사용이 가능한지 먼저 확인

CDK 사용하기 전 구성하려는 리소스에 대한 이해도 낮다면 콘솔에서 직접 구현하고 CDK를 사용하면 수월합니다.

공식 문서

CDK 공식 저장소

필자의 로직

배포 요구 사항에 맞게 스택 분리 (필수)

CDK로 인프라를 구축할 때 용도, 환경 별스택 정의는 필수

처음 CDK를 접하고 인프라를 구축 했을 때 하나의 스택에 모든 리소스들을 전부 정의해서 배포했습니다. 약 1년 6개월 동안 운영과 새로운 리소스 추가를 하면서 아래와 같은 문제가 발생했습니다.

하나의 스택에 모든 리소스를 정의 했을 경우 발생했던 문제

  1. 맨 마지막에 배포되는 리소스에 에러가 발생했을 경우 업데이트된 모든 리소스가 롤백 (배포 및 디버깅 시간 증가)
  2. 새로운 리소스가 추가 되었을 경우 기존 스택에 추가 (배포 및 디버깅 시간 증가)
  3. 각 환경 별로 배포하는 리소스가 다르기 때문에 스택 정의의 어려움 및 배포 스크립트 복잡도 상승 (+ 각 환경 브랜치 별 코드 매우 상이)
  4. 각 리소스 사양 별로 정의하는 편의성이 낮음 (Parameters를 특정 조건에 맞게 정의하기 어려움)
  5. 특별한 정의없이 계속 리소스를 추가하다보니 점점 스파게티 코드가 되어 가고 있음

위의 문제를 해결하고자 아래 그림과 같이 코드 리팩토링을 진행했습니다.

기존 스택 -> 개선된 스택

코드 리팩토링 기준

  1. 카테고리 별 스택 나누기 (Base, Data, ECS 등)
  2. 각 스택 및 리소스의 의존(Dependency)관계 줄이기
  3. 리소스는 언제든지 재사용 가능하게 정의 (인스턴스 클래스, 개수 등 )
  4. 각 환경(Dev, Stage, Prod) 별 스택은 따로 정의 — 각 환경 별 필요한 리소스가 다르기 때문

코드 리팩토링 후 아래와 같이 개선되었습니다.

개선된 배포 파이프라인

코드 리팩토링 후 개선된 사항

  1. Github 저장소의 각 브랜치 별 코드 통일
  2. 배포 스크립트의 병렬 처리 가능
  3. 디버깅 시간 감소
  4. 각 환경 별 스택 정의 및 새로운 리소스 추가에 대한 편의성 증가
  5. 이전보다 다른 사람에게 코드를 공유하고 설명하기 수월해짐

스택 간 Parameters 공유

CfnOutput, Fn.import_value를 사용해서 Cloudformation 내에 정의된 Parametrs 불러오기

# a_stack.py
# export value
cdk.CfnOutput(
self,
"public-subnet-1",
# subnet ARN value export
value=public_subnet_1.ref,
export_name="public-subnet-1-id",
)

# b_stack.py
# import value
cdk.Fn.import_value("public-subnet-1-id")

L1에서 정의한 내용을 기반으로 L2에서 사용하기

L1으로 리소스를 정의할 때 Return 값이 L2 Parameters에서 요구하는 Interface값(IVpc, IEc2 등)으로 Return 되지 않아 L2에서 사용하기 어려운데 L2 스태틱 메서드 중 “from_”으로 시작하는 값을 사용해서 L2에서 사용할 수 있게 변환

  • L2로 정의된 공식 문서를 Static Methods가 있습니다. Static Methods에는 그림에서 보이는 “from_” 으로 시작하는 Static Methods를 사용해서 L1을 L2에서 요구하는Inteface 형태로 변환 가능
(예시) L2 S3 from_bucket_arn
  • 사전에 “from_00”에서 필요한 정보들을 L1 Attributes를 활용해서 미리 정의
import aws_cdk.aws_s3 as s3

#L1
cfn_s3 = s3.CfnBucket(
self,
"cfn-s3",
bucket_name="example"
)

#L1 S3 Arn
cfn_s3_arn = cfn_s3.attr_arn

#L1 -> L2 Interface
s3_l2_interface = s3.Bucket.from_bucket_arn(
self,
"s3-l2-interface",
bucket_arn = cfn_s3_arn
)

# s3_l2_interface return "IBucket"

L2에서 특정 기능 지원이 되지 않을 때

L2에서 지원하지 않는 기능이 있을 경우 “.node.default_child” 메서드를 사용해서 JSON 형태의 CloudFormation 기반으로 정의 가능 (CloudFormation에서 기능을 지원해야 사용 가능)

import aws_cdk.aws_sqs as sqs

# L2기반 SQS 생성
queue = sqs.Queue(self, "L2Queue",
queue_name="L2Queue"
)

# L2에서 지원하지 않는 기능 (Cloudformation 문서 참조 후 지원하는 기능인지 확인 필요)
queue_node_child = queue.node.default_child
queue_node_child.add_property_override(
"RedriveAllowPolicy",
{
"redrivePermission": "allowAll",
},
)

리소스 모니터링을 위해 metric 관련 메서드를 적극적으로 활용 (L2)

L2 사용을 권장하는 이유는 metric 관련 메서드 정의가 편리하기 때문, L1을 사용할 경우 Cloudwatch 모듈을 불러와서 따로 정의

딜리버스 제품팀의 원칙

  • 측정 할 수 없는 것은 개선 할 수 없다.
  • If you can’t measure it, you can’t improve it — Lord Kelvin (1824–1907)
Metric Methods 예시

--

--