AWS CDK로 구축하는 Vision AI 모델 서빙 파이프라인 실전 가이드

Yeonggyoo Jeon·2026년 4월 23일

들어가며

Vision AI 모델을 개발하는 것과 그 모델을 실제 서비스로 제공하는 것은 전혀 다른 문제입니다. 연구 환경에서 높은 정확도를 달성한 모델이라도, 이를 안정적이고 확장 가능한 API 서비스로 전환하는 과정에는 수많은 엔지니어링 과제가 숨어 있습니다. 인프라 프로비저닝, 모델 배포 자동화, 요청 처리 파이프라인 설계, 비용 최적화, 그리고 운영 모니터링까지 — 이 모든 것을 체계적으로 관리하기 위해 하나의 단일화된 개발과 배포 즉 데브옵스를 위한 프레임워크가 필요합니다. AWS를 클라우드 인프라로 사용할 때, AWS CDK를 선택하는 것은 이를 위한 훌륭한 선택지가 될 수 있습니다.

이 글에서는 AWS클라우드 위에서 모델, 특히 Vision모델을 서빙하기 위한 아키텍처와 CDK 예제를 공유합니다. 단순한 튜토리얼이 아니라, 프로덕션 환경에서 마주친 문제들과 그 해결 과정을 솔직하게 담았습니다.


1. 왜 AWS CDK인가?

저는 이전 글(Developing service with AWS CDK)에서 CDK의 기본 개념과 Terraform과의 비교를 다룬 바 있습니다. AWS 클라우드 인프라 위에서 Vision AI 모델을 서빙하기 위한 서비스를 구축하면서 CDK를 선택한 이유는 더욱 명확해졌습니다.

Vision AI 모델 서빙을 위한 파이프라인은 단순한 웹 서비스와 달리, 여러 AWS 서비스 간의 복잡한 의존 관계를 가집니다. S3 버킷이 먼저 생성되어야 SageMaker 모델이 아티팩트를 참조할 수 있고, SageMaker 엔드포인트가 준비되어야 Lambda가 추론을 호출할 수 있습니다. 이러한 의존성 그래프를 YAML이나 HCL로 관리하는 것은 파일이 커질수록 점점 더 어려워집니다.

반면 CDK는 TypeScript의 타입 시스템과 클래스 상속을 활용하여 이 복잡성을 프로그래밍적으로 관리할 수 있게 해줍니다. StorageStack이 생성한 S3 버킷 참조를 ModelStack에 직접 전달하고, ModelStack의 엔드포인트 이름을 ApiStack의 Lambda 환경 변수로 주입하는 과정이 타입 안전하게 이루어집니다.


2. Vision AI 서비스 전체 아키텍처

아래는 여러 종류의 Vision AI 모델들을 End-to-End로 서빙하기 위한 가칭 'VAIaaS (Vision AI as a Service)' 프로젝트의 전체 아키텍처 다이어그램입니다.


요청 흐름을 단계별로 설명하면 다음과 같습니다.

클라이언트가 POST /v1/analyze 엔드포인트로 이미지 데이터를 전송하면, Amazon API Gateway가 요청을 수신합니다. Lambda Authorizer가 JWT 토큰 또는 API Key를 검증한 후, Router Lambda가 요청을 라우팅합니다. Pre-processing Lambda는 이미지를 S3에 업로드하고 모델 입력 형식에 맞게 전처리(리사이즈, 정규화)를 수행합니다. 전처리된 데이터는 SageMaker 실시간 엔드포인트로 전달되어 추론이 실행되고, 결과는 Post-processing Lambda를 통해 클라이언트 친화적인 JSON 형식으로 변환되어 반환됩니다.

CDK Stack 구조

전체 인프라는 4개의 CDK Stack으로 분리되어 있습니다.

Stack주요 리소스역할
StorageStackS3 (Input/Output/Model)데이터 및 모델 아티팩트 저장
ModelStackSageMaker Model, EndpointConfig, EndpointAI 모델 호스팅
ApiStackAPI Gateway, Lambda x3요청 처리 파이프라인
ObservabilityStackCloudWatch, X-Ray모니터링 및 추적

Stack을 분리한 이유는 독립적인 배포와 롤백이 가능하기 때문입니다. 모델을 업데이트할 때는 ModelStack만 재배포하면 되고, API 로직을 수정할 때는 ApiStack만 변경하면 됩니다. 이는 운영 환경에서 매우 중요한 설계 결정입니다.


3. StorageStack: 데이터 레이어 구성

// lib/storage-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class StorageStack extends cdk.Stack {
  // 다른 Stack에서 참조할 수 있도록 public으로 노출
  public readonly inputBucket: s3.Bucket;
  public readonly outputBucket: s3.Bucket;
  public readonly modelBucket: s3.Bucket;

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 입력 이미지 버킷: 수명 주기 정책으로 비용 최적화
    this.inputBucket = new s3.Bucket(this, 'InputBucket', {
      bucketName: `vaiaas-input-${this.account}-${this.region}`,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      lifecycleRules: [
        {
          // 처리 완료된 입력 이미지는 7일 후 자동 삭제
          expiration: cdk.Duration.days(7),
          prefix: 'processed/',
        },
      ],
      cors: [
        {
          allowedMethods: [s3.HttpMethods.PUT, s3.HttpMethods.POST],
          allowedOrigins: ['*'],
          allowedHeaders: ['*'],
        },
      ],
    });

    // 결과 버킷: Intelligent-Tiering으로 비용 최적화
    this.outputBucket = new s3.Bucket(this, 'OutputBucket', {
      bucketName: `vaiaas-output-${this.account}-${this.region}`,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      intelligentTieringConfigurations: [
        {
          name: 'EntireBucket',
          archiveAccessTierTime: cdk.Duration.days(90),
          deepArchiveAccessTierTime: cdk.Duration.days(180),
        },
      ],
    });

    // 모델 아티팩트 버킷: 버전 관리 활성화
    this.modelBucket = new s3.Bucket(this, 'ModelBucket', {
      bucketName: `vaiaas-models-${this.account}-${this.region}`,
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });
  }
}

실전 팁: S3 버킷 이름에 ${this.account}-${this.region}을 포함시키면 멀티 리전 배포 시 이름 충돌을 방지할 수 있습니다. 또한 removalPolicy: RETAIN은 실수로 스택을 삭제해도 데이터가 보존되도록 하는 중요한 설정입니다.


4. ModelStack: SageMaker 엔드포인트 구성

SageMaker 엔드포인트 구성은 CDK에서 가장 복잡한 부분 중 하나입니다. 모델 아티팩트 경로, 컨테이너 이미지, 인스턴스 타입, 오토스케일링 정책을 모두 코드로 관리해야 합니다.

// lib/model-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as sagemaker from 'aws-cdk-lib/aws-sagemaker';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

interface ModelStackProps extends cdk.StackProps {
  modelBucket: s3.Bucket;
  modelVersion: string; // e.g., 'v1.2.0'
}

export class ModelStack extends cdk.Stack {
  public readonly endpointName: string;

  constructor(scope: Construct, id: string, props: ModelStackProps) {
    super(scope, id, props);

    this.endpointName = `vaiaas-endpoint-${props.modelVersion.replace(/\./g, '-')}`;

    // SageMaker 실행 역할
    const sagemakerRole = new iam.Role(this, 'SageMakerRole', {
      assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'),
      ],
    });

    // 모델 버킷 읽기 권한 부여
    props.modelBucket.grantRead(sagemakerRole);

    // SageMaker 모델 정의
    // PyTorch 추론 컨테이너 사용 (AWS Deep Learning Container)
    const model = new sagemaker.CfnModel(this, 'VisionAIModel', {
      modelName: `vaiaas-model-${props.modelVersion.replace(/\./g, '-')}`,
      executionRoleArn: sagemakerRole.roleArn,
      primaryContainer: {
        // AWS 제공 PyTorch 추론 컨테이너
        image: `763104351884.dkr.ecr.${this.region}.amazonaws.com/pytorch-inference:2.1.0-gpu-py310-cu118-ubuntu20.04-sagemaker`,
        modelDataUrl: `s3://${props.modelBucket.bucketName}/models/${props.modelVersion}/model.tar.gz`,
        environment: {
          SAGEMAKER_PROGRAM: 'inference.py',
          SAGEMAKER_SUBMIT_DIRECTORY: '/opt/ml/model/code',
          MODEL_VERSION: props.modelVersion,
        },
      },
    });

    // 엔드포인트 설정: GPU 인스턴스 + 데이터 캡처
    const endpointConfig = new sagemaker.CfnEndpointConfig(this, 'EndpointConfig', {
      endpointConfigName: `vaiaas-config-${props.modelVersion.replace(/\./g, '-')}`,
      productionVariants: [
        {
          variantName: 'AllTraffic',
          modelName: model.modelName!,
          instanceType: 'ml.g4dn.xlarge', // NVIDIA T4 GPU
          initialInstanceCount: 1,
          initialVariantWeight: 1,
        },
      ],
      // 추론 데이터 캡처: 모델 품질 모니터링에 활용
      dataCaptureConfig: {
        enableCapture: true,
        initialSamplingPercentage: 10,
        destinationS3Uri: `s3://${props.modelBucket.bucketName}/data-capture/`,
        captureOptions: [
          { captureMode: 'Input' },
          { captureMode: 'Output' },
        ],
      },
    });

    // 실시간 엔드포인트 배포
    const endpoint = new sagemaker.CfnEndpoint(this, 'Endpoint', {
      endpointName: this.endpointName,
      endpointConfigName: endpointConfig.endpointConfigName!,
    });

    // 오토스케일링 설정 (Application Auto Scaling)
    // 최소 1개, 최대 4개 인스턴스로 트래픽에 따라 자동 조정
    const scalingTarget = new cdk.CfnResource(this, 'ScalingTarget', {
      type: 'AWS::ApplicationAutoScaling::ScalableTarget',
      properties: {
        MaxCapacity: 4,
        MinCapacity: 1,
        ResourceId: `endpoint/${this.endpointName}/variant/AllTraffic`,
        ScalableDimension: 'sagemaker:variant:DesiredInstanceCount',
        ServiceNamespace: 'sagemaker',
        RoleARN: sagemakerRole.roleArn,
      },
    });
    scalingTarget.addDependency(endpoint);

    new cdk.CfnOutput(this, 'EndpointNameOutput', {
      value: this.endpointName,
      exportName: 'VaiaasSageMakerEndpointName',
    });
  }
}

실전 팁: ml.g4dn.xlarge는 NVIDIA T4 GPU를 탑재한 인스턴스로, Vision AI 추론에 비용 대비 성능이 가장 좋습니다. 처음에는 ml.g4dn.2xlarge를 사용했다가 실제 부하 테스트 결과 ml.g4dn.xlarge로도 충분하다는 것을 확인하고 변경했습니다. 인스턴스 타입 선택 전에 반드시 실제 모델의 메모리 사용량과 추론 시간을 측정하세요.


5. ApiStack: 요청 처리 파이프라인

API 레이어는 세 개의 Lambda 함수로 구성됩니다. 각 Lambda는 단일 책임 원칙(SRP)에 따라 설계되어 독립적으로 배포하고 테스트할 수 있습니다.

// lib/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

interface ApiStackProps extends cdk.StackProps {
  inputBucket: s3.Bucket;
  outputBucket: s3.Bucket;
  sagemakerEndpointName: string;
}

export class ApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: ApiStackProps) {
    super(scope, id, props);

    // Lambda 공통 레이어 (공통 유틸리티, boto3 최신 버전 등)
    const commonLayer = new lambda.LayerVersion(this, 'CommonLayer', {
      code: lambda.Code.fromAsset('lambda/layers/common'),
      compatibleRuntimes: [lambda.Runtime.PYTHON_3_11],
      description: 'Common utilities and dependencies',
    });

    // 1. Lambda Authorizer: JWT 검증
    const authorizerFn = new lambda.Function(this, 'AuthorizerFunction', {
      runtime: lambda.Runtime.PYTHON_3_11,
      code: lambda.Code.fromAsset('lambda/authorizer'),
      handler: 'index.handler',
      environment: {
        JWT_SECRET_ARN: 'arn:aws:secretsmanager:...',
      },
      timeout: cdk.Duration.seconds(5),
    });

    const authorizer = new apigateway.TokenAuthorizer(this, 'JwtAuthorizer', {
      handler: authorizerFn,
      resultsCacheTtl: cdk.Duration.minutes(5), // 인증 결과 캐싱으로 레이턴시 감소
    });

    // 2. Router Lambda: 요청 라우팅 및 S3 업로드
    const routerFn = new lambda.Function(this, 'RouterFunction', {
      runtime: lambda.Runtime.PYTHON_3_11,
      code: lambda.Code.fromAsset('lambda/router'),
      handler: 'index.handler',
      layers: [commonLayer],
      environment: {
        INPUT_BUCKET: props.inputBucket.bucketName,
        OUTPUT_BUCKET: props.outputBucket.bucketName,
        SAGEMAKER_ENDPOINT: props.sagemakerEndpointName,
      },
      timeout: cdk.Duration.seconds(30),
      memorySize: 512,
      tracing: lambda.Tracing.ACTIVE, // X-Ray 추적 활성화
    });

    // S3 및 SageMaker 권한 부여
    props.inputBucket.grantReadWrite(routerFn);
    props.outputBucket.grantWrite(routerFn);
    routerFn.addToRolePolicy(new iam.PolicyStatement({
      actions: ['sagemaker:InvokeEndpoint'],
      resources: [`arn:aws:sagemaker:${this.region}:${this.account}:endpoint/${props.sagemakerEndpointName}`],
    }));

    // 3. API Gateway 구성
    const api = new apigateway.RestApi(this, 'VaiaaasApi', {
      restApiName: 'VAIaaS API',
      description: 'Vision AI as a Service REST API',
      deployOptions: {
        stageName: 'v1',
        tracingEnabled: true,
        metricsEnabled: true,
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        // 스로틀링: 초당 100 요청, 버스트 200 요청
        throttlingRateLimit: 100,
        throttlingBurstLimit: 200,
      },
      // CORS 설정
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: ['POST', 'OPTIONS'],
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    // POST /v1/analyze 엔드포인트
    const analyzeResource = api.root.addResource('analyze');
    analyzeResource.addMethod(
      'POST',
      new apigateway.LambdaIntegration(routerFn, {
        timeout: cdk.Duration.seconds(29), // API Gateway 최대 타임아웃은 29초
      }),
      {
        authorizer,
        authorizationType: apigateway.AuthorizationType.CUSTOM,
        // 요청 검증: Content-Type 헤더 필수
        requestValidator: new apigateway.RequestValidator(this, 'RequestValidator', {
          restApi: api,
          validateRequestBody: true,
          validateRequestParameters: true,
        }),
      }
    );

    new cdk.CfnOutput(this, 'ApiUrl', {
      value: api.url,
      description: 'VAIaaS API Gateway URL',
    });
  }
}

6. 실전에서 마주친 문제들

문제 1: SageMaker 엔드포인트 콜드 스타트

SageMaker 실시간 엔드포인트는 인스턴스가 항상 실행 중이므로 콜드 스타트 문제는 없습니다. 그러나 처음 배포 시 엔드포인트가 Creating 상태에서 InService 상태로 전환되는 데 5~15분이 소요됩니다. CDK 배포 중에 이 대기 시간이 포함되어 전체 배포 시간이 길어집니다.

해결책으로는 CDK의 waitForDeployment 옵션을 false로 설정하고, 별도의 배포 파이프라인에서 엔드포인트 상태를 폴링하는 방식을 채택했습니다.

문제 2: Lambda에서 SageMaker 호출 시 타임아웃

API Gateway의 최대 통합 타임아웃은 29초입니다. 고해상도 이미지에 대한 Vision AI 추론이 이 한계를 초과하는 경우가 발생했습니다.

해결책으로 두 가지 접근을 병행했습니다. 첫째, Pre-processing Lambda에서 이미지를 모델 입력 크기(예: 640×640)로 리사이즈하여 추론 시간을 단축했습니다. 둘째, 처리 시간이 긴 요청에 대해서는 비동기 처리 패턴을 도입했습니다. 클라이언트는 요청 ID를 즉시 받고, SQS를 통해 비동기로 처리된 결과를 나중에 폴링하는 방식입니다.

문제 3: SageMaker 엔드포인트 비용 최적화

ml.g4dn.xlarge 인스턴스는 시간당 약 $0.736입니다. 24시간 운영하면 월 약 $530이 발생합니다. 트래픽이 없는 야간 시간대에도 비용이 발생하는 것이 문제였습니다.

해결책으로 SageMaker Serverless Inference를 검토했으나, GPU를 지원하지 않아 CPU 추론으로 전환해야 했습니다. 결국 트래픽 패턴을 분석하여 오토스케일링의 MinCapacity를 야간에는 0으로 조정하는 스케줄링을 적용했습니다. 이를 통해 월 비용을 약 40% 절감했습니다.


7. 배포 및 운영

CDK 배포 명령

# 전체 스택 배포 (처음 배포 시)
cdk bootstrap aws://ACCOUNT_ID/REGION
cdk deploy --all

# 특정 스택만 배포 (모델 업데이트 시)
cdk deploy ModelStack --parameters modelVersion=v1.3.0

# 배포 전 변경 사항 확인
cdk diff ApiStack

모델 업데이트 워크플로우

새로운 모델 버전을 배포할 때는 Blue/Green 배포 전략을 사용합니다. SageMaker는 엔드포인트 업데이트 시 기존 인스턴스를 유지하면서 새 인스턴스를 준비하고, 준비가 완료되면 트래픽을 전환합니다. CDK에서는 EndpointConfig를 새로 생성하고 EndpointendpointConfigName을 업데이트하는 것으로 이를 구현할 수 있습니다.

모니터링 대시보드

CloudWatch 대시보드에서 다음 지표를 모니터링합니다.

지표임계값알람 조건
SageMaker ModelLatency5,000msP99 초과 시
SageMaker Invocations-분당 호출 수 추적
Lambda Duration25,000msP95 초과 시
API Gateway 5XXError1%에러율 초과 시
SageMaker CPUUtilization80%오토스케일링 트리거

마치며

AWS CDK로 Vision AI모델을 위한 서빙 파이프라인을 구축하면서 가장 크게 느낀 점은, 인프라를 코드로 관리한다는 것이 단순히 편의성의 문제가 아니라 서비스 품질의 문제라는 것입니다. CDK 덕분에 모델 업데이트, 인프라 변경, 환경 복제가 모두 코드 리뷰 프로세스를 거치게 되었고, 이는 운영 안정성을 크게 향상시켰습니다.

다음 글에서는 이 파이프라인에 SageMaker Model Monitor를 연동하여 프로덕션 환경에서 모델 드리프트를 감지하는 방법을 다룰 예정입니다.

이 글이 Vision AI 모델을 활용한 서비스를 구축하려는 분들께 도움이 되길 바랍니다. 질문이나 피드백은 댓글로 남겨주세요!


참고 자료

0개의 댓글