AWS ML CI/CD와 CDC 파이프라인 구축 (3) CodePipeline 조립과 트러블슈팅
마법사로 만든 Source와 Build 스테이지부터 Edit로 추가한 Build-Image와 Deploy, 스테이지 간 변수 배선과 과금 게이트, 트러블슈팅 3건까지 CodePipeline 조립 과정을 정리합니다.
AWS ML CI/CD와 CDC 파이프라인 구축 프로젝트 시리즈의 3편입니다. 프로젝트 소개는 0편에 있습니다.
2026-07-24부터 2026-07-25까지, 2편에서 만든 리소스들(ECR, S3, IAM 역할, CodeConnections, CodeBuild 프로젝트 3개)을 CodePipeline 하나로 묶었습니다. 파이프라인을 조립하고, 스테이지 사이에 변수를 배선하고, 과금 구간 앞에 수동 게이트를 걸고, 실행이 초록불이 될 때까지 만난 문제 3건을 해결한 기록입니다.
파이프라인 구조
파이프라인 이름은 ml-cicd-pipeline이고, 4개 스테이지가 직렬로 이어집니다.
flowchart LR
SRC["Source<br/>(GitHub main)"] --> TEST["Build<br/>(CodeBuild: ml-test)<br/>lint + pytest"]
TEST --> BUILD["Build-Image<br/>(CodeBuild: ml-build)<br/>docker build, ECR push"]
BUILD -->|"transition Disable<br/>(수동 게이트)"| DEPLOY["Deploy<br/>(CodeBuild: ml-deploy)<br/>SageMaker Pipeline 시작"]
| 스테이지 | CodeBuild 프로젝트 | 하는 일 |
|---|---|---|
| Source | 없음 | main에서 코드를 받아 뒤 스테이지에 전달 |
| Build | ml-test | buildspec/test.yml 실행 (lint + 단위 테스트) |
| Build-Image | ml-build | buildspec/build.yml 실행 (이미지 빌드 후 commit SHA 태그로 ECR push) |
| Deploy | ml-deploy | buildspec/deploy.yml 실행 (SageMaker Pipeline upsert와 실행 시작) |
각 스테이지가 실행할 명령(buildspec)은 저장소 안에 있고, 무엇을 어떤 순서로 실행할지는 CodePipeline 정의가 갖습니다. 이 분리가 아래 변수 배선 문제로 다시 등장합니다.
마법사로는 절반만 만든 이유
CodePipeline 생성 마법사는 Source, Build, Test, Deploy 자리를 차례로 제안하는데, 이 자리 순서가 제가 원하는 순서(테스트가 먼저, 이미지 빌드가 다음, 배포가 마지막)와 달랐습니다. 그래서 마법사로는 Source(GitHub App, main 브랜치)와 Build(ml-test)까지만 만들고 나머지는 Skip 한 다음, 생성된 파이프라인의 Edit 화면에서 Build-Image(ml-build)와 Deploy(ml-deploy) 스테이지를 직접 추가했습니다.
Execution mode: Superseded
생성 시 Execution mode를 Superseded로 선택했습니다. 실행 중에 새 push가 오면 대기 중이던 옛 실행을 새 실행이 대체하는 방식입니다. Queued를 선택했다면 push마다 실행이 전부 쌓여서 차례로 돌고, 그때마다 SageMaker 학습까지 반복 실행되어 과금이 반복됩니다. 이 프로젝트처럼 혼자 자주 push하는 상황에서는 최신 커밋 하나만 끝까지 가면 충분합니다.
변수 배선: IMAGE_URI를 Deploy까지 전달하기
build.yml은 빌드한 이미지의 ECR 주소를 exported-variables로 IMAGE_URI라는 이름으로 내보냅니다. 그런데 이 값이 Deploy 단계에 자동으로 전달되지는 않습니다. 전달 경로는 CodePipeline 정의에서만 표현할 수 있고, 두 곳을 설정해야 합니다.
- 내보내는 쪽(ml-build 액션): Variable namespace에
BuildVars지정 - 받는 쪽(deploy 액션): 환경변수
IMAGE_URI=#{BuildVars.IMAGE_URI}
전체 흐름을 이으면 다음과 같습니다.
1
2
3
4
5
build.yml: exported-variables로 IMAGE_URI 내보냄
→ CodePipeline: ml-build 액션에 Variable namespace = BuildVars
→ deploy 액션 환경변수: IMAGE_URI = #{BuildVars.IMAGE_URI}
→ deploy.yml: pipeline.py에 이미지 주소 전달
→ SageMaker Pipeline 파라미터로 주입
여기서 한 번 헷갈렸던 것이 namespace의 위치입니다. 처음에는 변수를 받는 쪽에 붙이는 설정으로 생각했는데, namespace는 변수를 내보내는 액션에 붙이는 이름표입니다. 받는 쪽은 #{namespace.변수} 문법으로 참조만 합니다.
과금 게이트: transition Disable
Build-Image와 Deploy 사이의 transition을 Disable로 걸어 두었습니다. Deploy가 실행되면 SageMaker Pipeline이 시작되고, SageMaker 학습 인스턴스가 뜨는 순간부터 과금됩니다. transition을 닫아 두면 push가 와도 파이프라인은 Build-Image까지만 진행하고 멈추고, 사람이 콘솔에서 전환을 열어 줄 때만 Deploy가 실행됩니다. 학습 실행 여부를 사람이 통제하는 구조입니다.
첫 실행
조립을 마치고 push를 하니 Source와 Build(ml-test)가 초록불이 되었습니다. 로컬에서 uv run pytest로 돌리던 lint와 단위 테스트가, push만 하면 AWS 위에서 자동으로 실행되는 것을 처음 확인한 시점입니다. Build-Image부터는 아래 트러블슈팅을 거쳐야 했습니다.
왜 GitHub Actions가 아니라 CodeBuild와 CodePipeline인가
여기까지 만든 기능은 GitHub Actions로도 전부 가능합니다. 그래도 AWS 도구를 쓴 차이는 두 가지입니다.
- 권한 연결: CodeBuild가 ECR push를 하고 SageMaker를 호출하는 권한이 전부 AWS 안에서 IAM 역할 부착으로 해결됩니다. GitHub Actions였다면 GitHub 서버가 제 AWS를 조작할 자격증명을 내보내는 설정(OIDC나 secrets)이 필요하고, 계정 열쇠가 AWS 밖으로 나가는 지점이 생깁니다.
- 조직 통제: 기업 환경은 배포 파이프라인도 자기 계정 안의 리소스로 두고 감사 로그와 권한 통제를 일원화하려 합니다. 이 프로젝트가 AWS 도구를 지정한 맥락도 여기에 있습니다.
역할 분담을 정리하면 CodePipeline은 순서 관리(스테이지, 변수 전달, 전환 게이트)를 맡고, CodeBuild는 각 단계의 명령 실행을 맡는 일회용 서버입니다(분당 과금, 월 100분 무료). 실행할 명령인 buildspec은 저장소에 있고, 구조인 파이프라인 정의는 AWS 리소스로 존재합니다. CI/CD 개념 자체가 처음이라면 CI/CD 기초 시리즈에 따로 정리해 두었습니다.
트러블슈팅 3건
Build-Image가 초록불이 될 때까지 세 번 실패했습니다. 전부 해결했고, 원인과 조치는 다음과 같습니다.
| 증상 | 원인 | 조치 |
|---|---|---|
Build-Image AccessDeniedException: codebuild:StartBuild | 파이프라인 서비스 역할은 생성 시점의 리소스에만 권한이 발급됨 | 역할 정책 JSON에 project/ml-* statement 추가 |
YAML_FILE_ERROR ... buildsper/build.yml | buildspec 경로 오타(buildsper) | CodeBuild 프로젝트 설정 수정 |
name unknown: repository 'ml-training' does not exist | ECR 리포지토리를 ml_training(언더스코어)으로 생성해서 build.yml의 ml-training(하이픈)과 불일치 | ml-training으로 재생성 |
첫 번째 건이 구조적으로 흥미로웠습니다. CodePipeline을 마법사로 만들 때 서비스 역할이 자동 생성되는데, 이 역할에는 그 시점에 존재하던 리소스(ml-test)에 대한 권한만 발급됩니다. 나중에 Edit로 추가한 Build-Image 스테이지는 codebuild:StartBuild가 거부됩니다. 역할의 정책에 project/ml-* statement를 추가해 해결했습니다. 최소권한이 실제로 강제되고 있다는 증거이기도 합니다.
세 번째 건은 이름 불일치입니다. ECR 리포지토리를 콘솔에서 ml_training으로 만들어 놓고 build.yml에는 ml-training으로 적어서, push 단계에서 리포지토리가 없다는 에러가 났습니다. 리포지토리를 ml-training으로 다시 만들어 해결했지만, 근본 원인은 리소스 이름이 코드(buildspec)와 콘솔(수동 생성)에 이중으로 존재한다는 점입니다. 리소스 이름을 코드와 함께 IaC로 관리해야 이런 불일치를 예방할 수 있고, 이 교훈이 5편의 Terraform 작업으로 이어집니다.
마친 상태
세 건을 해결한 뒤 최종 상태는 다음과 같습니다. push 한 번에 Source, Build(ml-test), Build-Image(ECR push)까지 초록불이 켜지고, Deploy는 수동 게이트 앞에서 대기합니다. SageMaker 학습은 게이트를 여는 순간에만 시작되므로, 실증은 비용 통제 아래에서 선택적으로 실행하는 구조가 되었습니다.
다음 편은 두 번째 파이프라인입니다. MySQL의 변경을 Kafka를 거쳐 PostgreSQL로 나르는 CDC 파이프라인을 docker compose로 구축하고 검증한 기록을 다룹니다.