작은 변경을 자주 통합
변경 크기를 줄이면 실패 원인 후보가 줄고, 리뷰와 롤백 단위도 작아진다.
긴 기능 브랜치를 오래 유지할수록 통합 비용이 비선형으로 커진다.분류: Layer 5 - 플랫폼 엔지니어링 & 자동화 | 선수지식: Docker Basics
이 문서는 Docker Basics 다음에 읽는 CI/CD 입문 문서다. Docker Basics에서 이미지는 “실행 환경째 패키징한 산출물”이었다. CI/CD에서는 그 산출물을 언제 만들고, 어떤 검증을 통과시킨 뒤, 어떤 방식으로 서버에 올릴지 다룬다.
본문에는 OIDC, DORA, CodeDeploy, ALB Target Group, Trivy 같은 도구와 지표가 나오지만 첫 회독에서 이 이름들을 미리 깊게 알 필요는 없다. 본문에서는 각각을 “임시 클라우드 권한”, “배포 성숙도 지표”, “Blue-Green 전환 도구”, “트래픽 가중치 분산”, “이미지 취약점 스캐너” 수준에서 먼저 설명한다.
CI/CD는 코드 변경을 자동으로 통합하고, 빌드하고, 테스트하고, 검증된 산출물을 배포 가능한 경로로 밀어 넣는 파이프라인이다.
CI(Continuous Integration)는 “변경을 자주 합치고 자동 검증으로 깨진 통합을 빨리 드러내는 것”이다. CD는 두 단계로 나뉜다. Continuous Delivery는 언제든 배포 가능한 상태까지 자동화하되 프로덕션 반영은 사람이 승인한다. Continuous Deployment는 검증을 통과한 변경을 프로덕션까지 자동으로 반영한다.
CI/CD의 핵심은 “배포 명령을 자동화한다”가 아니다. 핵심은 변경이 커지기 전에 자주 합치고, 같은 산출물을 단계별로 승격시키며, 실패를 사람의 기억이 아니라 파이프라인의 관측 가능한 신호로 바꾸는 것이다.
CI/CD 이전의 흔한 방식은 Pre-Release Integration이었다. 여러 개발자가 수 주에서 수 개월 동안 각자 브랜치에서 작업한 뒤, 출시 직전에 한꺼번에 합치는 방식이다. 개발 중에는 각 기능이 따로 잘 돌아가 보이지만, 출시 직전에는 서로 다른 가정이 충돌한다.
작은 예를 보자.
1일차- A는 User.email을 필수 값으로 바꾼다.- B는 외부 OAuth 사용자를 위해 email이 없을 수 있다고 가정한다.- C는 배포 스크립트에서 DB migration을 수동 실행하도록 둔다.
30일차 출시 직전 통합- A의 검증 로직이 B의 OAuth 가입을 막는다.- C의 migration 순서가 늦어 프로덕션에서 새 필드가 없다.- 테스트는 각 브랜치에서만 통과했기 때문에 어느 변경이 실제 원인인지 늦게 드러난다.문제는 충돌이 존재한다는 사실 자체가 아니다. 충돌은 언제든 생긴다. 진짜 문제는 충돌 발견 시점이 너무 늦다는 것이다. 늦게 발견된 통합 실패는 원인이 여러 변경에 섞이고, 출시 압박 때문에 임시 수정이 늘고, 다시 회귀를 만든다. Martin Fowler가 Continuous Integration을 설명할 때 강조한 지점도 “자주 통합해 통합을 특별한 이벤트가 아니라 일상으로 만드는 것”이다.
CI/CD는 이 한계를 두 가지 메커니즘으로 줄인다.
이 토픽이 사라지면 배포는 다시 개인 노트와 수동 명령에 의존한다. 배포 빈도는 낮아지고, 변경 실패율은 높아지고, 복구 시간은 사람이 어떤 명령을 기억하느냐에 좌우된다.
CI/CD를 처음 보면 YAML 문법이나 GitHub Actions 사용법이 먼저 보인다. 하지만 도구는 바뀐다. 남는 것은 다음 네 가지 철학이다.
변경 크기를 줄이면 실패 원인 후보가 줄고, 리뷰와 롤백 단위도 작아진다.
긴 기능 브랜치를 오래 유지할수록 통합 비용이 비선형으로 커진다.매 실행을 깨끗한 VM이나 컨테이너에서 시작해 로컬 머신의 우연한 상태를 배제한다.
GitHub Actions의 runner, GitLab CI의 job container, Jenkins의 agent를 같은 관점으로 읽는다.빌드 단계에서 만든 이미지나 바이너리를 staging과 production이 공유한다.
`latest`보다 커밋 SHA나 이미지 digest처럼 추적 가능한 식별자가 필요하다.배포 규칙과 검증 절차를 코드처럼 리뷰하고, 변경 이력을 남기고, 롤백 가능하게 둔다.
배포 방식 변경도 애플리케이션 변경만큼 장애를 만들 수 있다.CI와 CD의 경계
flowchart LR Change["작은 코드 변경"] --> Checkout["소스 체크아웃"] Checkout --> Build["빌드"] Build --> Test["테스트"] Test --> Package["Docker 이미지 / 바이너리 생성"] Package --> Registry["레지스트리 저장"] Registry --> Staging["스테이징 배포"] Staging --> Approval["승인 / 정책 게이트"] Approval --> Production["프로덕션 배포"] subgraph CI["CI: 통합 가능성 검증"] Checkout Build Test end subgraph CD["CD: 검증된 산출물 승격"] Package Registry Staging Approval Production end
CI가 깨졌다는 것은 “이 변경은 공유 코드베이스에 안전하게 합치기 어렵다”는 뜻이다. CD가 깨졌다는 것은 “코드는 통합 가능하지만, 산출물을 환경에 안전하게 승격시키는 데 실패했다”는 뜻이다. 이 둘을 구분해야 진단 방향이 달라진다.
GitHub Actions를 기준으로 설명하지만, 이 모델은 GitLab CI, Jenkins, CircleCI에서도 거의 그대로 적용된다.
파이프라인은 사람이 터미널에서 시작할 수도 있지만, 일반적으로 저장소 이벤트가 트리거한다.
on: pull_request: branches: [main] push: branches: [main]pull_request 이벤트는 “합치기 전에 통합 가능성을 검증하라”는 의미에 가깝다. push 이벤트는 “이미 main에 들어온 변경을 기준으로 산출물을 만들고 배포 경로로 보내라”는 의미에 가깝다. 같은 테스트라도 어느 이벤트에서 실행되는지에 따라 목적이 달라진다.
반례도 중요하다. 워크플로가 push: branches: [main]에만 묶여 있으면 PR 단계에서 실패를 빨리 보지 못한다. 반대로 fork PR에서 배포 job이 secrets를 요구하면 보안상 secrets가 주입되지 않아 실패하거나, 더 나쁘게는 외부 코드에 비밀을 노출할 위험이 생긴다.
GitHub Actions runner는 workflow job을 실행하는 VM 또는 머신이다. GitHub-hosted runner는 보통 매 실행마다 새로 준비되는 깨끗한 VM이다. 그래서 로컬에 남아 있는 node_modules, 로그인 세션, Docker 이미지 캐시, 환경변수에 의존하면 CI에서 깨진다.
PR push -> GitHub가 이벤트 감지 -> runner VM 할당 -> repository checkout -> 의존성 설치 -> build/test 실행 -> 결과를 PR check로 기록 -> runner 폐기이 모델을 알면 actions/checkout과 npm ci가 왜 매번 필요한지 이해된다. runner는 이전 실행의 작업 디렉터리를 기억하지 않는다. 캐시는 “runner가 우연히 보존한 파일”이 아니라, 키를 기준으로 명시적으로 복원하는 별도 저장소라고 보는 편이 안전하다.
workflow는 job과 step으로 나뉜다. job은 runner 하나에서 실행되는 큰 단위이고, step은 순서대로 실행되는 작은 명령 또는 action이다.
build-and-test job├── checkout├── setup node├── npm ci├── npm test└── npm run build
deploy job├── configure cloud credentials├── pull immutable image└── update servicenpm ci에서 실패하면 dependency lock과 Node 버전을 먼저 본다. configure cloud credentials에서 실패하면 secrets, OIDC 권한, environment 설정을 먼저 본다. update service는 성공했는데 서비스가 502라면 CI가 아니라 런타임 헬스체크나 태스크 정의를 봐야 한다.
CI 설정 파일은 단순한 설정이 아니라 운영 절차를 코드화한 문서다. 그래서 다음 원칙이 필요하다.
@main처럼 움직이는 브랜치보다 @v4 또는 커밋 SHA처럼 고정된 식별자를 쓴다.Docker Basics에서 Docker 이미지는 Dockerfile과 빌드 컨텍스트로 만든 실행 산출물이었다. CI/CD에서 중요한 질문은 “이미지를 만들 수 있는가”가 아니라 “어떤 이미지를 검증했고, 어떤 이미지를 배포했는가”다.
GITHUB_SHA 태그의 의미docker build -t registry.example.com/my-app:$GITHUB_SHA .docker push registry.example.com/my-app:$GITHUB_SHAGITHUB_SHA는 workflow를 트리거한 커밋의 식별자다. 이 값을 이미지 태그에 넣으면 “이 배포물은 어느 커밋에서 왔는가”를 역추적할 수 있다. latest 태그만 쓰면 같은 이름이 시간이 지나며 다른 이미지를 가리킬 수 있어, 장애 시점에 실제 배포물을 재구성하기 어렵다.
작은 관찰 예시:
staging에서 검증한 이미지: registry/my-app:8f4a1c2production에 배포한 이미지: registry/my-app:8f4a1c2=> 같은 산출물을 승격했다. 실패하면 환경 차이, 설정, 트래픽 조건을 의심한다.
staging에서 검증한 이미지: registry/my-app:8f4a1c2production에 배포한 이미지: registry/my-app:latest=> 실제로 같은 산출물인지 증명하기 어렵다. latest가 바뀌었을 수 있다.더 엄밀하게는 이미지 digest를 쓴다. 태그는 사람이 붙이는 이름이고, digest는 이미지 내용에서 계산된 식별자다.
태그: registry/my-app:8f4a1c2Digest: registry/my-app@sha256:4f8a...첫 회독에서는 “커밋 SHA 태그로 추적 가능하게 만들고, 중요한 환경에서는 digest로 내용 동일성을 더 강하게 보장한다” 정도만 잡으면 된다.
나쁜 패턴:
staging deploy job: docker build deploy to staging
production deploy job: docker build deploy to production좋은 패턴:
build job: docker build once push registry/my-app:<commit-sha>
staging deploy job: deploy registry/my-app:<commit-sha>
production deploy job: deploy the same registry/my-app:<commit-sha>두 번째 방식은 빌드 시간이 줄어서 좋은 것이 아니라, 검증한 대상과 배포한 대상이 같다는 점이 핵심이다. 환경마다 다시 빌드하면 base image 업데이트, lock 파일 차이, 빌드 캐시 차이 때문에 같은 커밋이어도 다른 결과물이 나올 수 있다.
전체 배포 YAML을 외우는 것은 목표가 아니다. 아래 스니펫에서 읽어야 할 것은 “이 파이프라인이 어떤 신뢰 단위를 만드는가”다.
permissions: contents: read id-token: write
jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" - run: npm ci - run: npm test - run: npm run build
package: needs: ci runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole aws-region: ap-northeast-2 - run: | docker build -t "$ECR_REGISTRY/my-app:$GITHUB_SHA" . docker push "$ECR_REGISTRY/my-app:$GITHUB_SHA"
deploy: needs: package runs-on: ubuntu-latest environment: production steps: - run: aws ecs update-service --cluster prod --service my-app --force-new-deployment읽는 순서:
permissions가 최소 권한인지 본다. OIDC를 쓰려면 id-token: write가 필요하지만, 모든 job에 쓰지 않아도 된다.needs: ci가 package job을 테스트 뒤로 묶고 있는지 본다.needs: package가 deploy job을 이미지 생성 뒤로 묶고 있는지 본다.@v4처럼 고정되어 있는지 본다.GITHUB_SHA처럼 추적 가능한 값인지 본다.CI/CD 파이프라인은 코드를 배포하므로 강한 권한을 가진다. 그래서 “테스트가 통과한다”만큼 “누가 어떤 권한으로 배포하는가”가 중요하다.
정적 키 방식은 AWS_ACCESS_KEY_ID와 AWS_SECRET_ACCESS_KEY 같은 장기 자격증명을 GitHub Secrets에 저장한다. 설정은 쉽지만 키가 유출되면 만료 전까지 계속 악용될 수 있다. 특히 배포 권한이 넓은 키라면 저장소, 로그, third-party action, 권한 오설정이 모두 공격면이 된다.
정적 키 방식GitHub Secrets에 장기 키 저장 -> workflow가 키를 환경변수로 읽음 -> cloud API 호출 -> 키가 유출되면 사람이 회수할 때까지 유효OIDC(OpenID Connect) Workload Identity Federation은 CI runner가 클라우드에 “이 workflow는 특정 저장소와 브랜치에서 실행 중이다”라는 서명된 토큰을 제시하고, 클라우드가 짧은 수명의 임시 자격증명을 발급하는 패턴이다.
sequenceDiagram participant Runner as CI Runner participant OIDC as GitHub OIDC Provider participant STS as Cloud STS participant Cloud as Cloud API Runner->>OIDC: 실행 컨텍스트로 OIDC 토큰 요청 OIDC-->>Runner: repo/ref 클레임이 담긴 토큰 발급 Runner->>STS: 토큰 제시 후 역할 가정 요청 STS->>STS: 신뢰 정책 검증 STS-->>Runner: 단기 자격증명 발급 Runner->>Cloud: 배포 API 호출
| 클라우드 | 구현 이름 | GitHub Actions에서 흔한 action |
|---|---|---|
| AWS | IAM OIDC Provider + Role Trust Policy | aws-actions/configure-aws-credentials |
| GCP | Workload Identity Pool + Provider | google-github-actions/auth |
| Azure | Entra ID Federated Credentials | azure/login |
핵심은 subject 클레임을 좁히는 것이다. 예를 들어 repo:org/repo:ref:refs/heads/main만 역할을 assume할 수 있게 하면, 같은 저장소라도 임의 브랜치나 fork PR이 프로덕션 배포 권한을 얻지 못한다.
GitHub Environments는 staging, production 같은 배포 환경마다 secrets와 보호 규칙을 다르게 두는 기능이다.
jobs: deploy-production: environment: production steps: - run: ./deploy.shenvironment: production이 붙으면 production environment에 묶인 secrets와 required reviewers, wait timer, 허용 브랜치 규칙이 적용된다. 즉, CD에서 사람의 승인이 필요하다면 shell script 안에 read를 넣는 것이 아니라 environment protection rule로 모델링하는 편이 추적 가능하다.
CI가 느리면 개발자는 PR을 작게 유지하기 어렵다. 피드백이 2분이면 실패를 빨리 고치지만, 20분이면 여러 변경을 쌓아두고 한꺼번에 보게 된다. 속도는 편의가 아니라 통합 주기를 짧게 유지하는 조건이다.
actions/setup-node의 cache: "npm"은 package-lock.json 같은 lock 파일을 기준으로 npm 다운로드 캐시를 복원한다. node_modules를 그대로 믿는 것이 아니라, lock 파일에 맞는 패키지 tarball을 재사용해 설치 시간을 줄이는 방식이다.
- uses: actions/setup-node@v4 with: node-version: "20" cache: "npm"- run: npm ci관찰 수치 예시:
| 단계 | 캐시 없음 | 캐시 있음 | 해석 |
|---|---|---|---|
npm ci | 1분 45초 | 8초 | lock 파일이 같으면 다운로드 비용이 줄어든다 |
| 전체 CI | 4분 30초 | 2분 50초 | 테스트 자체가 느리면 캐시만으로는 한계가 있다 |
Docker 레이어 캐시는 Dockerfile의 순서와 직접 연결된다. package*.json을 먼저 복사하고 npm ci를 실행한 뒤 소스 코드를 복사하면, 소스 파일만 바뀐 커밋에서는 의존성 설치 레이어를 재사용할 수 있다.
COPY package*.json ./RUN npm ciCOPY . .RUN npm run buildDocker build 캐시 없음: 2분 30초Docker build 캐시 있음: 40초반례: COPY . .를 먼저 쓰고 그 다음 npm ci를 실행하면 README 한 줄이 바뀌어도 의존성 설치 레이어가 무효화된다. 캐시는 “설정 한 줄”만의 문제가 아니라 Dockerfile 레이어 설계의 결과다.
배포 전략은 “새 버전을 서버에 올리는 방식”이 아니라 “실패를 얼마나 좁은 범위에서 발견하고 얼마나 빨리 되돌릴 수 있는가”를 결정한다.
기존 인스턴스를 조금씩 새 버전으로 교체한다. 추가 인프라 비용이 낮고 대부분의 기본 배포 방식이다.
내부 도구, 낮은 트래픽 서비스, 짧은 일시 혼재가 허용되는 서비스에 적합하다.기존 환경(Blue)과 새 환경(Green)을 나란히 띄운 뒤 트래픽을 한 번에 전환한다.
롤백을 1분 이내에 끝내야 하거나, 전환 전 새 환경을 별도로 검증해야 할 때 적합하다.새 버전에 소수 트래픽만 먼저 보내고 지표를 확인하며 점진적으로 확대한다.
사용자 영향이 큰 기능, 실데이터 검증, 충분한 관측 지표와 자동 롤백이 있을 때 적합하다.| 질문 | Rolling | Blue-Green | Canary |
|---|---|---|---|
| 추가 인프라 비용을 최소화해야 하는가 | 유리 | 불리 | 중간 |
| 롤백이 거의 즉시 끝나야 하는가 | 불리 | 유리 | 중간 |
| 일부 사용자에게만 먼저 노출해야 하는가 | 불리 | 불리 | 유리 |
| 관측 지표와 자동화가 부족한가 | 기본값으로 가능 | 가능하지만 비용 증가 | 위험 |
| 구버전과 신버전이 동시에 떠도 안전한가 | 필요 | 전환 순간만 고려 | 필요 |
반례를 보면 선택 기준이 선명해진다.
ECS Rolling 배포에서는 maximumPercent와 minimumHealthyPercent가 배포 중 동시에 떠 있을 수 있는 태스크 수와 최소 정상 태스크 수를 정한다.
현재 desired count = 2maximumPercent = 200minimumHealthyPercent = 100
1. 구버전 태스크 2개 실행 중2. 신버전 태스크 최대 2개 추가 가능 (총 4개까지 허용)3. 신버전 헬스체크 통과4. 구버전 태스크 종료Blue-Green은 AWS CodeDeploy 같은 오케스트레이터가 새 task set을 만들고, 테스트 리스너로 확인한 뒤 production 리스너를 전환하는 방식으로 구현할 수 있다. Canary는 ALB Target Group 가중치를 이용해 v1 90%, v2 10%처럼 트래픽을 나누고, 지표가 안전하면 25%, 50%, 100%로 늘린다.
첫 회독에서는 CodeDeploy와 ALB 세부 설정을 외울 필요가 없다. 중요한 것은 “Rolling은 태스크를 순차 교체한다”, “Blue-Green은 환경을 나란히 두고 전환한다”, “Canary는 트래픽 비율을 조절한다”는 메커니즘 차이다.
ECS Deployment Circuit Breaker는 신버전 태스크가 정상 상태에 도달하지 못할 때 배포를 실패로 판단하고 이전 배포로 되돌리는 안전장치다.
신버전 태스크 시작 -> 컨테이너가 계속 종료되거나 health check 실패 -> deployment failed -> rollback enabled이면 이전 task definition으로 복귀이 기능은 장애를 없애지 않는다. 대신 “잘못된 버전이 계속 확산되는 시간”을 줄인다. 그래서 Circuit Breaker가 켜져 있어도 health check가 부정확하면 실패를 감지하지 못한다. /health가 단순히 200만 반환하고 DB 연결 실패를 숨기면, ECS는 정상이라고 판단할 수 있다.
DORA 4 Key Metrics는 팀의 소프트웨어 전달 성능을 보는 네 가지 지표다.
| 지표 | 묻는 질문 | CI/CD와의 연결 |
|---|---|---|
| Deployment Frequency | 얼마나 자주 배포하는가 | 변경을 작게 유지하고 배포 자동화가 되어야 높아진다 |
| Lead Time for Changes | 커밋부터 배포까지 얼마나 걸리는가 | CI 속도, 리뷰 대기, 승인 게이트가 영향을 준다 |
| Change Failure Rate | 배포 중 몇 %가 장애나 롤백을 만드는가 | 테스트 품질, 배포 전략, feature flag가 영향을 준다 |
| MTTR | 장애 후 평균 복구 시간이 얼마나 걸리는가 | 롤백 자동화, 관측성, 아티팩트 추적성이 영향을 준다 |
배포 전략은 이 지표를 서로 다른 방향으로 민다.
이 숫자는 팀을 등급표에 넣기 위한 것이 아니라 방향 신호다. 예를 들어 한 팀이 주 1회 배포, 리드타임 4일, 변경 실패율 20%, MTTR 6시간이라면 가장 먼저 볼 것은 “배포를 더 자주 해야 한다” 하나가 아니다. 작은 변경으로 나누는 능력, 자동 테스트 신뢰도, 롤백 경로, 알림 품질이 함께 낮다는 신호로 읽어야 한다. 반대로 일 1회 이상 배포하면서 리드타임 1일 이하, 변경 실패율 10% 이하, MTTR 1시간 안쪽으로 내려가면 CI/CD가 개발 속도와 운영 회복력을 동시에 밀어 주고 있다고 볼 수 있다.
| 전략 | 배포 빈도 | 변경 실패율 | MTTR | 리드타임 |
|---|---|---|---|---|
| Rolling | 높이기 쉽다 | 전체 노출이 빨라 높아질 수 있음 | 재배포가 필요하면 느릴 수 있음 | 짧다 |
| Blue-Green | 중간 | 전환 전 검증으로 낮출 수 있음 | 트래픽 전환으로 빠르다 | 환경 준비 때문에 중간 |
| Canary | 낮아질 수 있음 | 소수 노출로 낮출 수 있음 | 지표와 자동화가 있으면 중간 | 단계별 검증 때문에 길다 |
지표는 점수표가 아니라 대화 도구다. 배포 빈도를 높이려다 Change Failure Rate가 급등하면 자동화는 빨라졌지만 신뢰 단위가 너무 약하다는 신호다. MTTR이 길다면 배포 전략보다 먼저 “어떤 버전이 나갔는지, 무엇을 되돌려야 하는지”를 추적할 수 있는지 확인한다.
CI/CD는 공급망 공격의 통로가 될 수 있다. 저장소 권한, third-party action, 컨테이너 이미지가 모두 프로덕션으로 이어지기 때문이다.
원칙:
흔한 실패 신호:
AWS 인증 실패├── secret 이름 오타├── environment를 지정하지 않아 Environment Secret을 읽지 못함├── fork PR이라 secrets가 주입되지 않음└── OIDC trust policy의 repo/ref 조건 불일치uses: actions/checkout@main처럼 움직이는 브랜치를 참조하면, 나중에 다른 코드가 실행될 수 있다. 최소한 major version tag(@v4)를 쓰고, 더 엄격한 환경에서는 commit SHA로 고정한다.
# 피한다- uses: some/action@main
# 일반적으로 허용- uses: actions/checkout@v4
# 더 엄격한 공급망 통제- uses: some/action@8f4a1c2...Trivy는 컨테이너 이미지 안의 OS 패키지와 언어 패키지 취약점을 스캔하는 도구다. CI에서 스캔을 넣는 목적은 “취약점이 하나라도 있으면 무조건 배포 금지”가 아니라, 팀의 위험 기준을 파이프라인 게이트로 명시하는 것이다.
- name: Scan image with Trivy uses: aquasecurity/trivy-action@<pinned-version-or-commit-sha> with: image-ref: my-app:${{ github.sha }} severity: "CRITICAL,HIGH" exit-code: "1"관찰 예시:
node CVE-2023-XXXXX CRITICAL fixed in 20.11.1express CVE-2024-XXXXX HIGH fixed in 4.19.2
CRITICAL/HIGH 기준에 걸림 -> package 또는 base image 업데이트 전까지 배포 중단반례: 모든 LOW 취약점까지 실패 처리하면 파이프라인이 자주 막히고 개발자는 스캔을 우회하려 한다. 반대로 CRITICAL을 경고만 남기면 게이트가 아니다. 팀의 서비스 노출면과 패치 가능성을 기준으로 severity 정책을 정해야 한다.
CI/CD 장애를 잘 보는 사람은 명령어를 많이 외운 사람이 아니라 실패 위치를 먼저 분류하는 사람이다.
파이프라인이 실패했다├── workflow가 시작되지 않았다│ ├── trigger branch가 실제 branch와 다른가│ ├── workflow 파일이 .github/workflows/ 아래에 있는가│ └── pull_request와 push 중 필요한 이벤트가 빠졌는가├── CI 단계가 실패했다│ ├── checkout/setup 실패 -> action 버전, 권한, runner 상태│ ├── npm ci 실패 -> package-lock.json, Node 버전, registry 접근│ ├── test 실패 -> 코드 회귀, flaky test, 환경변수│ └── build 실패 -> 타입 오류, Dockerfile, 빌드 컨텍스트├── package/push 단계가 실패했다│ ├── registry 로그인 실패 -> OIDC, secret, IAM 권한│ ├── image push 실패 -> repository 권한, tag 정책│ └── scan 실패 -> base image 또는 dependency 취약점└── deploy 뒤 서비스가 깨졌다 ├── ECS service event 확인 ├── task가 STOPPED인지 RUNNING인지 확인 ├── health check 경로와 timeout 확인 └── 배포된 이미지 tag/digest가 검증한 것과 같은지 확인증상: 코드를 push했는데 Actions 탭에 실행이 없다.
원리: 이벤트 조건이 맞지 않으면 runner도 할당되지 않는다. 이 경우 로그가 없는 것이 정상이다. 로그를 찾기 전에 trigger와 파일 위치를 본다.
관찰 포인트:
.github/workflows/deploy.yml 아래에 있는가push 대상 branch가 branches 조건과 맞는가PR 이벤트가 필요한데 pull_request가 빠지지 않았는가workflow 파일 자체가 default branch에 존재하는가npm ci가 실패하는 경우증상: 로컬에서는 설치되는데 CI에서 npm ci가 실패한다.
원리: npm ci는 lock 파일을 신뢰하고 재현 가능한 설치를 하려는 명령이다. package.json과 package-lock.json이 어긋나면 일부러 실패한다. 이는 나쁜 일이 아니라 “로컬의 우연한 상태를 CI가 거부했다”는 신호다.
먼저 볼 것:
npm install로 lock 파일이 변경됐는데 커밋되지 않았는가증상: GitHub Actions는 성공했는데 배포 후 ALB나 프록시가 502를 반환한다.
원리: CI는 “이미지를 만들고 배포 API 호출까지 성공했다”는 사실만 보장할 수 있다. 컨테이너가 실제 트래픽을 처리하는지는 런타임 헬스체크와 서비스 이벤트를 봐야 한다.
먼저 볼 것:
RUNNING인지, STOPPED라면 stopped reason이 무엇인가/health가 앱 준비 상태를 충분히 반영하는가증상: ${{ secrets.AWS_ACCESS_KEY_ID }} 또는 환경별 secret이 비어 있어 인증이 실패한다.
원리: GitHub Secrets는 값이 없거나 접근 권한이 없으면 빈 값처럼 동작할 수 있다. 또한 로그에서는 값이 마스킹되기 때문에 실제 값을 출력해서 확인하는 방식은 피해야 한다.
먼저 볼 것:
environment: production이 필요한데 빠지지 않았는가permissions: id-token: write가 있는가플랫폼 엔지니어링 관점에서 CI/CD는 개발자 경험(DX)의 핵심 경로다. 좋은 파이프라인은 개발자가 배포 절차를 외우지 않아도 작은 변경을 안전하게 흘려보낼 수 있게 한다. 나쁜 파이프라인은 자동화되어 있어도 실패 원인을 숨기고, 수동 복구 지식을 특정 사람에게 묶어 둔다.
| 개념 A | 개념 B | 차이점 |
|---|---|---|
| CI | CD | CI는 통합 가능성 검증, CD는 검증된 산출물의 배포 경로 자동화 |
| Continuous Delivery | Continuous Deployment | Delivery는 프로덕션 반영 전 승인 게이트가 있고, Deployment는 통과 즉시 반영 |
| 태그 | Digest | 태그는 사람이 붙인 이름이고, digest는 이미지 내용 기반 식별자 |
| Runner | Environment | Runner는 job 실행 머신이고, Environment는 배포 대상과 보호 규칙의 논리 단위 |
| Repository Secret | Environment Secret | 전자는 저장소 범위, 후자는 staging/production 같은 환경 범위 |
| 정적 키 | OIDC 임시 자격증명 | 정적 키는 장기 비밀이고, OIDC는 실행 컨텍스트 기반 단기 권한 교환 |
| Rolling | Blue-Green | Rolling은 순차 교체, Blue-Green은 별도 환경 준비 후 트래픽 전환 |
| Blue-Green | Canary | Blue-Green은 한 번에 전환하고, Canary는 트래픽 일부부터 점진 확대 |
아래는 본문 이해 뒤 실제 팀 파이프라인을 읽을 때 쓰는 관찰용 스니펫이다. 명령을 외우는 것이 목표가 아니라 “어떤 원리를 확인하는지”를 연결하는 것이 목표다.
gh run list --limit 5gh run view <run-id> --log-failed관찰할 것:
aws ecs describe-services \ --cluster prod \ --services my-app \ --query 'services[0].deploymentConfiguration'관찰할 것:
maximumPercent, minimumHealthyPercent로 Rolling 방식의 여유 태스크 수를 추정한다.deploymentCircuitBreaker.enable과 rollback이 켜져 있는지 본다.grep -R "id-token" .github/workflows/grep -R "configure-aws-credentials" .github/workflows/관찰할 것:
permissions: id-token: write가 있는가docker build -t my-app:${GITHUB_SHA:-local} .docker push registry.example.com/my-app:${GITHUB_SHA:-local}docker buildx imagetools inspect registry.example.com/my-app:${GITHUB_SHA:-local}관찰할 것:
퀴즈
힌트: 커밋 SHA 태그와 이미지 digest를 왜 쓰는지 떠올려본다.
불변 아티팩트 원칙이다. 환경마다 다시 빌드하거나 `latest`처럼 움직이는 태그를 쓰면, 같은 커밋이어도 실제 배포물이 달라질 수 있다.
퀴즈
힌트: 파이프라인 성공은 컨테이너가 실제 트래픽을 처리한다는 보장과 다르다.
먼저 CD 이후 런타임 영역을 의심한다. 배포 API 호출은 성공했지만 ECS task health check, target group port, `/health` 구현, 배포된 이미지 tag/digest가 문제일 수 있다.
GitHub Actions, GitLab CI, Jenkins, Argo CD, GitOps, Docker 멀티스테이지 빌드, ECR, 이미지 digest, SBOM, Trivy, SLSA, OIDC Workload Identity Federation, GitHub Environments, AWS CodeDeploy, ALB Target Group 가중치, ECS Deployment Circuit Breaker, Feature Flag, Trunk-Based Development, DORA 4 Key Metrics
.github/workflows/를 열고 이벤트, jobs, steps, permissions를 표시해본다.latest 중심인지, 커밋 SHA나 digest로 추적 가능한지 확인한다.GITHUB_SHA 태그와 digest는 어떤 커밋이 어떤 이미지로 배포됐는지 추적하게 해준다.