AWS CDK에서 배포 시점 인텔리전스: 커스텀 리소스 실전

발행: (2026년 1월 20일 오전 12:48 GMT+9)
9 min read
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line and all formatting exactly as you requested.

소개

실제 AWS 플랫폼에서는 단일 CDK 코드베이스가 여러 AWS 계정에 배포되는 경우가 많으며, 각 계정은 development, staging, production과 같은 서로 다른 환경을 나타냅니다.

AWS CDK가 인프라 정의에 뛰어나지만, 한계가 있습니다:

계정 내부에 저장된 값(예: AWS Systems Manager (SSM) Parameter Store에 저장된 값)을 기반으로 배포 시점에 결정을 내릴 수 없습니다, 예를 들어 AWS Systems Manager (SSM) Parameter Store에 저장된 값과 같이.

이 블로그에서는 Lambda‑backed Custom Resource를 사용하여 EKS Helm add‑on을 설치할 때 환경 인식 결정을 내리는 실용적인 플랫폼 엔지니어링 문제를 해결합니다.

실제 문제

여러 AWS 계정에 걸쳐 EKS 클러스터를 관리하는 플랫폼 엔지니어입니다:

환경기대
개발낮은 비용, 최소한의 중복성
스테이징프로덕션과 유사하지만 규모가 작음
프로덕션고가용성

조직에서는 이미 환경 유형을 SSM 파라미터로 중앙에 저장하고 있습니다:

/platform/account/env

값 예시:

  • development
  • staging
  • production

이제 모든 EKS 클러스터에 ingress‑nginx를 설치하고, 환경에 따라 다르게 구성하고자 합니다:

  • developmentreplicaCount = 1
  • staging / productionreplicaCount = 2

CDK 컨텍스트를 사용하지 않는 이유 (간단히)

처음 보면 CDK 컨텍스트 변수는 환경 기반 구성을 위한 더 간단한 해결책처럼 보일 수 있습니다. 그러나 컨텍스트 값은 **시합 시점(synthesis time)**에 해결되며, 배포 중에는 해결되지 않습니다. 즉, 외부(cdk.json 또는 CI/CD 파이프라인)에서 제공되어야 하고, SSM 파라미터 스토어에 저장된 값과 같은 계정 수준 메타데이터를 알지 못합니다.

다중 계정 플랫폼에서는 종종 다음과 같은 문제가 발생합니다:

  • 수동 조정
  • 구성 드리프트
  • 거버넌스 문제

환경 분류가 이미 AWS 계정 내부에 존재하고 플랫폼이 소유해야 하므로, **배포 시점(Custom Resource)**을 사용하면 구성의 정확성, 일관성 및 중앙 제어를 보장할 수 있습니다.

CDK만으로는 충분하지 않은 이유

AWS CDK는 synthesis 단계에 로직을 평가하지만, SSM 파라미터 값은 배포 단계에서만 안정적으로 사용할 수 있습니다. 따라서 다음을 할 수 없습니다:

  • CDK if 문을 사용해 값을 결정하기
  • 스택에 환경 값을 하드코딩하기
  • CDK 컨텍스트에 신뢰하게 의존하기

필요한 것은 배포 시점 로직입니다.

솔루션: Lambda‑Backed Custom Resource

Custom Resource는 CloudFormation이 다음을 수행하도록 합니다:

  1. 스택 생성 또는 업데이트 중에 Lambda 함수를 호출
  2. 결과를 기다림
  3. 반환된 속성을 다른 리소스의 입력값으로 사용

이 경우, Custom Resource는:

  • SSM에서 환경 값을 읽음
  • 올바른 Helm 값을 계산
  • 이를 CDK에 반환
  • CDK가 Helm 차트에 전달

아키텍처 개요

배포 흐름

  1. CDK가 생성합니다
    • EKS 클러스터
    • SSM 파라미터 /platform/account/env
    • Lambda 함수
  2. Custom Resource가 Lambda를 트리거합니다
  3. Lambda가 Helm 값을 계산합니다
  4. Helm 차트가 반환된 값을 사용하여 설치됩니다

이를 통해 유지됩니다:

  • 하나의 CDK 코드베이스
  • 수동 단계 제로
  • 환경 인식 동작

CDK 스택 코드 (Python)

아래는 CDK 스택으로, 다음을 생성합니다:

  • EKS 클러스터
  • SSM 파라미터
  • Lambda 함수
  • 커스텀 리소스
  • 동적 값을 가진 Helm 차트
from aws_cdk import (
    Stack,
    aws_eks as eks,
    aws_ec2 as ec2,
    aws_iam as iam,
    aws_ssm as ssm,
    aws_signer as signer,
    aws_lambda as _lambda,
    custom_resources as cr,
    CustomResource,
    Token,
)
from aws_cdk.lambda_layer_kubectl_v34 import KubectlV34Layer
from constructs import Construct
import os


class TestStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, *, vpc: ec2.IVpc = None, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        aws_account_id = self.node.try_get_context("aws-account-id")

        # -------------------------------------------------
        # EKS Cluster
        # -------------------------------------------------
        cluster = eks.Cluster(
            self,
            "MyEKS",
            version=eks.KubernetesVersion.V1_34,
            endpoint_access=eks.EndpointAccess.PUBLIC_AND_PRIVATE,
            default_capacity=0,
            default_capacity_instance=ec2.InstanceType.of(
                ec2.InstanceClass.T3,
                ec2.InstanceSize.MEDIUM,
            ),
            kubectl_layer=KubectlV34Layer(self, "kubectl"),
            vpc=vpc,
            vpc_subnets=[
                ec2.SubnetSelection(
                    subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
                )
            ],
            cluster_name="MyEKS",
            tags={"Name": "MyEKS", "Purpose": "Swisscom-Interview"},
        )

        # -------------------------------------------------
        # EKS Admin Access
        # -------------------------------------------------
        admin_user = iam.User(self, "EKSAdmin")
        cluster.aws_auth.add_user_mapping(admin_user, groups=["system:masters"])

        # -------------------------------------------------
        # Store environment in SSM
        # -------------------------------------------------
        ssm.StringParameter(
            self,
            "MyEnvParam",
            parameter_name="/platform/account/env",
            string_value="development",
            description="Environment Name",
        )

        # -------------------------------------------------
        # Lambda code signing
        # -------------------------------------------------
        signing_profile = signer.SigningProfile(
            self,
            "SigningProfile",
            platform=signer.Platform.AWS_LAMBDA_SHA384_ECDSA,
        )

        code_signing_config = _lambda.CodeSigningConfig(
            self,
            "CodeSigningConfig",
            signing_profiles=[signing_profile],
        )

        # -------------------------------------------------
        # Lambda Function (reads the SSM parameter)
        # -------------------------------------------------
        fn = _lambda.Function(
            self,
            "MySSMParamLambda",
            runtime=_lambda.Runtime.PYTHON_3_13,
            handler="index.lambda_handler",
            code=_lambda.Code.from_asset(
                os.path.join(os.path.dirname(__file__), "lambda_functions")
            ),
            environment={"SSM_PARAM_NAME": "/platform/account/env"},
            code_signing_config=code_signing_config,
        )

        fn.add_to_role_policy(
            iam.PolicyStatement(
                actions=["ssm:GetParameter"],
                resources=["*"],  # Adjust to the specific parameter ARN in production
            )
        )

        # -------------------------------------------------
        # Custom Resource that invokes the Lambda
        # -------------------------------------------------
        custom_resource = cr.AwsCustomResource(
            self,
            "EnvLookupCustomResource",
            on_create=cr.AwsSdkCall(
                service="Lambda",
                action="invoke"
",
                parameters={
                    "FunctionName": fn.function_name,
                    "Payload": Token.as_string(
                        {
                            "RequestType": "Create",
                            "ResourceProperties": {"SSM_PARAM_NAME": "/platform/account/env"},
                        }
                    ),
                },
                physical_resource_id=cr.PhysicalResourceId.of("EnvLookup"),
            ),
            policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
                resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
            ),
        )

        # -------------------------------------------------
        # Use the value returned by the Custom Resource in the Helm chart
        # -------------------------------------------------
        replica_count = custom_resource.get_response_field("Payload.body.replicaCount")

        eks.HelmChart(
            self,
            "IngressNginx",
            cluster=cluster,
            chart="ingress-nginx",
            repository="https://kubernetes.github.io/ingress-nginx",
            release="ingress-nginx",
            namespace="ingress-nginx",
            values={"controller": {"replicaCount": replica_count}},
        )

Lambda 코드(표시되지 않음)는 /platform/account/env를 읽고, 값을 적절한 replicaCount에 매핑한 뒤 응답 페이로드에 반환합니다.

resources = [
    f"arn:aws:ssm:{self.region}:{self.account}:parameter/platform/account/env"
]

# Custom Resource Provider
provider = cr.Provider(
    self,
    "EnvToHelmProvider",
    on_event_handler=fn,
)

env_cr = CustomResource(
    self,
    "EnvToHelmValues",
    service_token=provider.service_token,
)

replica_count = Token.as_number(
    env_cr.get_att("ReplicaCount")
)

# Install ingress‑nginx Helm chart
cluster.add_helm_chart(
    "nginx-ingress",
    chart="nginx-ingress",
    repository="https://helm.nginx.com/stable",
    namespace="kube-system",
    values={
        "controller": {
            "replicaCount": replica_count
        }
    },
)

Lambda 코드 (맞춤 리소스 로직)

  • SSM에서 환경을 읽습니다
  • 올바른 복제본 수를 계산합니다
  • 값을 CloudFormation에 반환합니다
import boto3
import os
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

ssm = boto3.client("ssm")
PARA_NAME = os.environ["SSM_PARAM_NAME"]


def get_parameter(para_name):
    parameter = ssm.get_parameter(Name=para_name)
    env = parameter["Parameter"]["Value"].strip().lower()

    if env == "development":
        value = 1
    elif env in ["staging", "production"]:
        value = 2
    else:
        raise ValueError(
            f"Invalid environment {env} in SSM Parameter {para_name}"
        )

    logger.info(f"Computed replicaCount={value} for env={env}")

    return {
        "Environment": env,
        "ReplicaCount": value,
    }


def lambda_handler(event, context):
    if event.get("RequestType") == "Delete":
        return {
            "PhysicalResourceId": event.get("PhysicalResourceId", "env"),
            "Data": {},
        }

    data = get_parameter(PARA_NAME)

    return {
        "PhysicalResourceId": f"{PARA_NAME}:{data['Environment']}",
        "Data": data,
    }

왜 이 패턴이 잘 작동하는가

이 접근 방식은 다음을 제공합니다:

  • 단일 CDK 코드베이스
  • 환경 인식 동작
  • 수동 Helm 오버라이드 없음
  • 배포 시점 의사결정
  • 테스트 가능한 비즈니스 로직

다음과 같은 추가 기능을 더할 때도 잘 확장됩니다:

  • cert‑manager
  • external‑dns
  • cluster‑autoscaler
  • 로깅 에이전트

결론

AWS CDK는 본질적으로 선언형이지만 실제 플랫폼은 배포‑시점 인텔리전스가 필요합니다. Lambda‑backed Custom Resources와 CDK를 결합하면 실제 계정 메타데이터를 기반으로 인프라 결정을 내릴 수 있으며, 하드‑코딩된 가정에 의존하지 않게 됩니다.

이 패턴은 환경 전반에 걸친 일관성, 안전성 및 자동화를 목표로 하는 플랫폼 팀에게 강력한 도구가 됩니다.

Back to Blog

관련 글

더 보기 »

AWS Bedrock이란 무엇인가요??

Bedrock가 왜 존재하는가? 잠시 되돌아보자. 2022‑2023년경, 기업들은 생성 AI에 미쳐 있었다. ChatGPT가 막 폭발적으로 인기를 끌었다. 모든 …