콘텐츠로 이동

CI/CD Basics

분류: 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의 핵심은 “배포 명령을 자동화한다”가 아니다. 핵심은 변경이 커지기 전에 자주 합치고, 같은 산출물을 단계별로 승격시키며, 실패를 사람의 기억이 아니라 파이프라인의 관측 가능한 신호로 바꾸는 것이다.

2. 선행 한계 → 등장 → 해결: Pre-Release Integration의 실패

섹션 제목: “2. 선행 한계 → 등장 → 해결: Pre-Release Integration의 실패”

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는 이 한계를 두 가지 메커니즘으로 줄인다.

  1. 통합 주기 단축: 변경을 작게 유지하고, 공유 브랜치에 합치기 전후로 자동 빌드와 테스트를 실행한다. 실패가 하루 안에 드러나면 작성자의 기억이 살아 있고, 원인 후보도 작다.
  2. 불변 아티팩트 승격: staging에서 검증한 산출물을 production에서도 그대로 사용한다. 같은 커밋에서 환경마다 다시 빌드하지 않기 때문에 “테스트한 것과 배포한 것이 다르다”는 실패를 줄인다.

이 토픽이 사라지면 배포는 다시 개인 노트와 수동 명령에 의존한다. 배포 빈도는 낮아지고, 변경 실패율은 높아지고, 복구 시간은 사람이 어떤 명령을 기억하느냐에 좌우된다.

3. CI/CD 철학: 빠른 자동화보다 작은 신뢰 단위

섹션 제목: “3. CI/CD 철학: 빠른 자동화보다 작은 신뢰 단위”

CI/CD를 처음 보면 YAML 문법이나 GitHub Actions 사용법이 먼저 보인다. 하지만 도구는 바뀐다. 남는 것은 다음 네 가지 철학이다.

CI/CD의 도구 독립 철학

작은 변경을 자주 통합

변경 크기를 줄이면 실패 원인 후보가 줄고, 리뷰와 롤백 단위도 작아진다.

긴 기능 브랜치를 오래 유지할수록 통합 비용이 비선형으로 커진다.

격리된 실행 환경

매 실행을 깨끗한 VM이나 컨테이너에서 시작해 로컬 머신의 우연한 상태를 배제한다.

GitHub Actions의 runner, GitLab CI의 job container, Jenkins의 agent를 같은 관점으로 읽는다.

불변 아티팩트

빌드 단계에서 만든 이미지나 바이너리를 staging과 production이 공유한다.

`latest`보다 커밋 SHA나 이미지 digest처럼 추적 가능한 식별자가 필요하다.

Pipeline as Code

배포 규칙과 검증 절차를 코드처럼 리뷰하고, 변경 이력을 남기고, 롤백 가능하게 둔다.

배포 방식 변경도 애플리케이션 변경만큼 장애를 만들 수 있다.

CI와 CD의 경계

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가 깨졌다는 것은 “코드는 통합 가능하지만, 산출물을 환경에 안전하게 승격시키는 데 실패했다”는 뜻이다. 이 둘을 구분해야 진단 방향이 달라진다.

4. 파이프라인 메커니즘: 이벤트에서 배포까지

섹션 제목: “4. 파이프라인 메커니즘: 이벤트에서 배포까지”

GitHub Actions를 기준으로 설명하지만, 이 모델은 GitLab CI, Jenkins, CircleCI에서도 거의 그대로 적용된다.

4.1 Event: 파이프라인은 왜 시작되는가

섹션 제목: “4.1 Event: 파이프라인은 왜 시작되는가”

파이프라인은 사람이 터미널에서 시작할 수도 있지만, 일반적으로 저장소 이벤트가 트리거한다.

on:
pull_request:
branches: [main]
push:
branches: [main]

pull_request 이벤트는 “합치기 전에 통합 가능성을 검증하라”는 의미에 가깝다. push 이벤트는 “이미 main에 들어온 변경을 기준으로 산출물을 만들고 배포 경로로 보내라”는 의미에 가깝다. 같은 테스트라도 어느 이벤트에서 실행되는지에 따라 목적이 달라진다.

반례도 중요하다. 워크플로가 push: branches: [main]에만 묶여 있으면 PR 단계에서 실패를 빨리 보지 못한다. 반대로 fork PR에서 배포 job이 secrets를 요구하면 보안상 secrets가 주입되지 않아 실패하거나, 더 나쁘게는 외부 코드에 비밀을 노출할 위험이 생긴다.

4.2 Runner: 매번 새 환경에서 실행된다

섹션 제목: “4.2 Runner: 매번 새 환경에서 실행된다”

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/checkoutnpm ci가 왜 매번 필요한지 이해된다. runner는 이전 실행의 작업 디렉터리를 기억하지 않는다. 캐시는 “runner가 우연히 보존한 파일”이 아니라, 키를 기준으로 명시적으로 복원하는 별도 저장소라고 보는 편이 안전하다.

4.3 Jobs와 Steps: 실패 위치가 곧 원인 범위다

섹션 제목: “4.3 Jobs와 Steps: 실패 위치가 곧 원인 범위다”

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 service

npm ci에서 실패하면 dependency lock과 Node 버전을 먼저 본다. configure cloud credentials에서 실패하면 secrets, OIDC 권한, environment 설정을 먼저 본다. update service는 성공했는데 서비스가 502라면 CI가 아니라 런타임 헬스체크나 태스크 정의를 봐야 한다.

4.4 Pipeline as Code: YAML은 실행 문서다

섹션 제목: “4.4 Pipeline as Code: YAML은 실행 문서다”

CI 설정 파일은 단순한 설정이 아니라 운영 절차를 코드화한 문서다. 그래서 다음 원칙이 필요하다.

  • workflow 파일 변경도 PR 리뷰 대상이어야 한다.
  • 배포 권한이 있는 workflow는 더 좁은 branch, environment, approval 조건을 가져야 한다.
  • action 버전은 @main처럼 움직이는 브랜치보다 @v4 또는 커밋 SHA처럼 고정된 식별자를 쓴다.
  • 긴 shell script가 늘어나면 “무엇을 보장하는 step인지”가 흐려지므로, 검증 단위에 맞게 step을 나눈다.

5. 불변 아티팩트: Docker Basics가 CI/CD로 이어지는 지점

섹션 제목: “5. 불변 아티팩트: Docker Basics가 CI/CD로 이어지는 지점”

Docker Basics에서 Docker 이미지는 Dockerfile과 빌드 컨텍스트로 만든 실행 산출물이었다. CI/CD에서 중요한 질문은 “이미지를 만들 수 있는가”가 아니라 “어떤 이미지를 검증했고, 어떤 이미지를 배포했는가”다.

Terminal window
docker build -t registry.example.com/my-app:$GITHUB_SHA .
docker push registry.example.com/my-app:$GITHUB_SHA

GITHUB_SHA는 workflow를 트리거한 커밋의 식별자다. 이 값을 이미지 태그에 넣으면 “이 배포물은 어느 커밋에서 왔는가”를 역추적할 수 있다. latest 태그만 쓰면 같은 이름이 시간이 지나며 다른 이미지를 가리킬 수 있어, 장애 시점에 실제 배포물을 재구성하기 어렵다.

작은 관찰 예시:

staging에서 검증한 이미지: registry/my-app:8f4a1c2
production에 배포한 이미지: registry/my-app:8f4a1c2
=> 같은 산출물을 승격했다. 실패하면 환경 차이, 설정, 트래픽 조건을 의심한다.
staging에서 검증한 이미지: registry/my-app:8f4a1c2
production에 배포한 이미지: registry/my-app:latest
=> 실제로 같은 산출물인지 증명하기 어렵다. latest가 바뀌었을 수 있다.

더 엄밀하게는 이미지 digest를 쓴다. 태그는 사람이 붙이는 이름이고, digest는 이미지 내용에서 계산된 식별자다.

태그: registry/my-app:8f4a1c2
Digest: registry/my-app@sha256:4f8a...

첫 회독에서는 “커밋 SHA 태그로 추적 가능하게 만들고, 중요한 환경에서는 digest로 내용 동일성을 더 강하게 보장한다” 정도만 잡으면 된다.

5.2 다시 빌드하지 않고 승격한다

섹션 제목: “5.2 다시 빌드하지 않고 승격한다”

나쁜 패턴:

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

읽는 순서:

  1. permissions가 최소 권한인지 본다. OIDC를 쓰려면 id-token: write가 필요하지만, 모든 job에 쓰지 않아도 된다.
  2. needs: ci가 package job을 테스트 뒤로 묶고 있는지 본다.
  3. needs: package가 deploy job을 이미지 생성 뒤로 묶고 있는지 본다.
  4. action 버전이 @v4처럼 고정되어 있는지 본다.
  5. 이미지 태그가 GITHUB_SHA처럼 추적 가능한 값인지 본다.
  6. secrets가 코드에 박혀 있지 않고, 환경이나 OIDC로 주입되는지 본다.

7. 시크릿과 OIDC: 배포 권한은 어디서 오는가

섹션 제목: “7. 시크릿과 OIDC: 배포 권한은 어디서 오는가”

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는 특정 저장소와 브랜치에서 실행 중이다”라는 서명된 토큰을 제시하고, 클라우드가 짧은 수명의 임시 자격증명을 발급하는 패턴이다.

OIDC Workload Identity Federation 흐름
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
AWSIAM OIDC Provider + Role Trust Policyaws-actions/configure-aws-credentials
GCPWorkload Identity Pool + Providergoogle-github-actions/auth
AzureEntra ID Federated Credentialsazure/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.sh

environment: production이 붙으면 production environment에 묶인 secrets와 required reviewers, wait timer, 허용 브랜치 규칙이 적용된다. 즉, CD에서 사람의 승인이 필요하다면 shell script 안에 read를 넣는 것이 아니라 environment protection rule로 모델링하는 편이 추적 가능하다.

8. 파이프라인 속도: 캐시는 우연한 빠름이 아니라 설계다

섹션 제목: “8. 파이프라인 속도: 캐시는 우연한 빠름이 아니라 설계다”

CI가 느리면 개발자는 PR을 작게 유지하기 어렵다. 피드백이 2분이면 실패를 빨리 고치지만, 20분이면 여러 변경을 쌓아두고 한꺼번에 보게 된다. 속도는 편의가 아니라 통합 주기를 짧게 유지하는 조건이다.

actions/setup-nodecache: "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 ci1분 45초8초lock 파일이 같으면 다운로드 비용이 줄어든다
전체 CI4분 30초2분 50초테스트 자체가 느리면 캐시만으로는 한계가 있다

Docker 레이어 캐시는 Dockerfile의 순서와 직접 연결된다. package*.json을 먼저 복사하고 npm ci를 실행한 뒤 소스 코드를 복사하면, 소스 파일만 바뀐 커밋에서는 의존성 설치 레이어를 재사용할 수 있다.

COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
Docker build 캐시 없음: 2분 30초
Docker build 캐시 있음: 40초

반례: COPY . .를 먼저 쓰고 그 다음 npm ci를 실행하면 README 한 줄이 바뀌어도 의존성 설치 레이어가 무효화된다. 캐시는 “설정 한 줄”만의 문제가 아니라 Dockerfile 레이어 설계의 결과다.

9. 배포 전략: 같은 아티팩트를 어떻게 트래픽에 노출할 것인가

섹션 제목: “9. 배포 전략: 같은 아티팩트를 어떻게 트래픽에 노출할 것인가”

배포 전략은 “새 버전을 서버에 올리는 방식”이 아니라 “실패를 얼마나 좁은 범위에서 발견하고 얼마나 빨리 되돌릴 수 있는가”를 결정한다.

배포 전략 선택 기준

Rolling

기존 인스턴스를 조금씩 새 버전으로 교체한다. 추가 인프라 비용이 낮고 대부분의 기본 배포 방식이다.

내부 도구, 낮은 트래픽 서비스, 짧은 일시 혼재가 허용되는 서비스에 적합하다.

Blue-Green

기존 환경(Blue)과 새 환경(Green)을 나란히 띄운 뒤 트래픽을 한 번에 전환한다.

롤백을 1분 이내에 끝내야 하거나, 전환 전 새 환경을 별도로 검증해야 할 때 적합하다.

Canary

새 버전에 소수 트래픽만 먼저 보내고 지표를 확인하며 점진적으로 확대한다.

사용자 영향이 큰 기능, 실데이터 검증, 충분한 관측 지표와 자동 롤백이 있을 때 적합하다.
질문RollingBlue-GreenCanary
추가 인프라 비용을 최소화해야 하는가유리불리중간
롤백이 거의 즉시 끝나야 하는가불리유리중간
일부 사용자에게만 먼저 노출해야 하는가불리불리유리
관측 지표와 자동화가 부족한가기본값으로 가능가능하지만 비용 증가위험
구버전과 신버전이 동시에 떠도 안전한가필요전환 순간만 고려필요

반례를 보면 선택 기준이 선명해진다.

  • 결제 승인 API에서 오류율 1%가 곧바로 매출 손실이면 Rolling만으로는 위험할 수 있다. 빠른 트래픽 전환과 롤백이 가능한 Blue-Green을 우선 검토한다.
  • 추천 문구 변경처럼 비즈니스 지표를 일부 사용자에게 먼저 확인해야 하면 Blue-Green보다 Canary가 낫다.
  • 관측 지표가 부족한 팀이 Canary를 선택하면 “10%에서 문제가 있었는지”를 판단하지 못해 단계만 늘어난다. Canary는 전략 이름이 아니라 지표와 자동화가 함께 있어야 의미가 있다.

ECS Rolling 배포에서는 maximumPercentminimumHealthyPercent가 배포 중 동시에 떠 있을 수 있는 태스크 수와 최소 정상 태스크 수를 정한다.

현재 desired count = 2
maximumPercent = 200
minimumHealthyPercent = 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는 정상이라고 판단할 수 있다.

10. DORA 지표로 보는 배포 성숙도

섹션 제목: “10. DORA 지표로 보는 배포 성숙도”

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이 길다면 배포 전략보다 먼저 “어떤 버전이 나갔는지, 무엇을 되돌려야 하는지”를 추적할 수 있는지 확인한다.

11. 보안 게이트: Secrets, Action Pinning, Trivy

섹션 제목: “11. 보안 게이트: Secrets, Action Pinning, Trivy”

CI/CD는 공급망 공격의 통로가 될 수 있다. 저장소 권한, third-party action, 컨테이너 이미지가 모두 프로덕션으로 이어지기 때문이다.

원칙:

  • 비밀 값은 코드, Dockerfile, workflow 로그에 남기지 않는다.
  • Repository secrets와 Environment secrets의 범위를 구분한다.
  • production secrets는 production environment에 묶고 required reviewers를 둔다.
  • 가능하면 장기 키보다 OIDC 임시 자격증명을 쓴다.

흔한 실패 신호:

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.1
express CVE-2024-XXXXX HIGH fixed in 4.19.2
CRITICAL/HIGH 기준에 걸림 -> package 또는 base image 업데이트 전까지 배포 중단

반례: 모든 LOW 취약점까지 실패 처리하면 파이프라인이 자주 막히고 개발자는 스캔을 우회하려 한다. 반대로 CRITICAL을 경고만 남기면 게이트가 아니다. 팀의 서비스 노출면과 패치 가능성을 기준으로 severity 정책을 정해야 한다.

12. 실패 신호: 어디가 깨졌는지 먼저 나눈다

섹션 제목: “12. 실패 신호: 어디가 깨졌는지 먼저 나눈다”

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가 검증한 것과 같은지 확인

12.1 workflow가 시작되지 않는 경우

섹션 제목: “12.1 workflow가 시작되지 않는 경우”

증상: 코드를 push했는데 Actions 탭에 실행이 없다.

원리: 이벤트 조건이 맞지 않으면 runner도 할당되지 않는다. 이 경우 로그가 없는 것이 정상이다. 로그를 찾기 전에 trigger와 파일 위치를 본다.

관찰 포인트:

.github/workflows/deploy.yml 아래에 있는가
push 대상 branch가 branches 조건과 맞는가
PR 이벤트가 필요한데 pull_request가 빠지지 않았는가
workflow 파일 자체가 default branch에 존재하는가

증상: 로컬에서는 설치되는데 CI에서 npm ci가 실패한다.

원리: npm ci는 lock 파일을 신뢰하고 재현 가능한 설치를 하려는 명령이다. package.jsonpackage-lock.json이 어긋나면 일부러 실패한다. 이는 나쁜 일이 아니라 “로컬의 우연한 상태를 CI가 거부했다”는 신호다.

먼저 볼 것:

  • lock 파일이 커밋되어 있는가
  • Node 버전이 로컬과 CI에서 같은가
  • private registry 토큰이 CI에 주입되는가
  • npm install로 lock 파일이 변경됐는데 커밋되지 않았는가

12.3 파이프라인은 초록색인데 서비스가 502인 경우

섹션 제목: “12.3 파이프라인은 초록색인데 서비스가 502인 경우”

증상: GitHub Actions는 성공했는데 배포 후 ALB나 프록시가 502를 반환한다.

원리: CI는 “이미지를 만들고 배포 API 호출까지 성공했다”는 사실만 보장할 수 있다. 컨테이너가 실제 트래픽을 처리하는지는 런타임 헬스체크와 서비스 이벤트를 봐야 한다.

먼저 볼 것:

  • 배포된 task definition이 기대한 이미지 tag/digest를 참조하는가
  • 새 task가 RUNNING인지, STOPPED라면 stopped reason이 무엇인가
  • 컨테이너가 listening하는 port와 target group health check port가 일치하는가
  • /health가 앱 준비 상태를 충분히 반영하는가
  • Circuit Breaker가 rollback을 수행했는가

12.4 Secrets가 빈 값처럼 보이는 경우

섹션 제목: “12.4 Secrets가 빈 값처럼 보이는 경우”

증상: ${{ secrets.AWS_ACCESS_KEY_ID }} 또는 환경별 secret이 비어 있어 인증이 실패한다.

원리: GitHub Secrets는 값이 없거나 접근 권한이 없으면 빈 값처럼 동작할 수 있다. 또한 로그에서는 값이 마스킹되기 때문에 실제 값을 출력해서 확인하는 방식은 피해야 한다.

먼저 볼 것:

  • Repository Secret과 Environment Secret을 혼동하지 않았는가
  • job에 environment: production이 필요한데 빠지지 않았는가
  • fork PR에서 secrets를 읽으려 하지 않았는가
  • secret 이름의 대소문자가 정확한가
  • OIDC라면 permissions: id-token: write가 있는가
  • PR 생성 시 자동 테스트와 린트 실행
  • main merge 후 Docker 이미지 빌드, 레지스트리 push, staging 배포
  • production environment에서 승인 후 같은 이미지 승격
  • 배포 실패 시 GitHub Actions 로그와 ECS service event를 연결해 원인 범위 축소
  • 캐시, 병렬화, 테스트 분리로 피드백 시간 단축
  • 팀의 배포 전략, secrets 정책, action pinning 기준 표준화

플랫폼 엔지니어링 관점에서 CI/CD는 개발자 경험(DX)의 핵심 경로다. 좋은 파이프라인은 개발자가 배포 절차를 외우지 않아도 작은 변경을 안전하게 흘려보낼 수 있게 한다. 나쁜 파이프라인은 자동화되어 있어도 실패 원인을 숨기고, 수동 복구 지식을 특정 사람에게 묶어 둔다.

개념 A개념 B차이점
CICDCI는 통합 가능성 검증, CD는 검증된 산출물의 배포 경로 자동화
Continuous DeliveryContinuous DeploymentDelivery는 프로덕션 반영 전 승인 게이트가 있고, Deployment는 통과 즉시 반영
태그Digest태그는 사람이 붙인 이름이고, digest는 이미지 내용 기반 식별자
RunnerEnvironmentRunner는 job 실행 머신이고, Environment는 배포 대상과 보호 규칙의 논리 단위
Repository SecretEnvironment Secret전자는 저장소 범위, 후자는 staging/production 같은 환경 범위
정적 키OIDC 임시 자격증명정적 키는 장기 비밀이고, OIDC는 실행 컨텍스트 기반 단기 권한 교환
RollingBlue-GreenRolling은 순차 교체, Blue-Green은 별도 환경 준비 후 트래픽 전환
Blue-GreenCanaryBlue-Green은 한 번에 전환하고, Canary는 트래픽 일부부터 점진 확대

아래는 본문 이해 뒤 실제 팀 파이프라인을 읽을 때 쓰는 관찰용 스니펫이다. 명령을 외우는 것이 목표가 아니라 “어떤 원리를 확인하는지”를 연결하는 것이 목표다.

GitHub Actions 실행 목록과 실패 로그 확인
Terminal window
gh run list --limit 5
gh run view <run-id> --log-failed

관찰할 것:

  • workflow가 어떤 이벤트로 시작됐는가
  • 실패 job과 step이 어디인가
  • 실패가 CI 문제인지, package 문제인지, deploy 이후 런타임 문제인지
ECS 배포 설정과 Circuit Breaker 확인
Terminal window
aws ecs describe-services \
--cluster prod \
--services my-app \
--query 'services[0].deploymentConfiguration'

관찰할 것:

  • maximumPercent, minimumHealthyPercent로 Rolling 방식의 여유 태스크 수를 추정한다.
  • deploymentCircuitBreaker.enablerollback이 켜져 있는지 본다.
  • 이 값만으로 Blue-Green/Canary 전체를 판단하지 말고 CodeDeploy, ALB Target Group 설정과 함께 본다.
팀 파이프라인에서 OIDC 사용 여부 확인
Terminal window
grep -R "id-token" .github/workflows/
grep -R "configure-aws-credentials" .github/workflows/

관찰할 것:

  • permissions: id-token: write가 있는가
  • 정적 AWS 키를 직접 읽는 job이 남아 있는가
  • OIDC role trust policy가 저장소와 브랜치를 좁게 제한하는가
이미지 태그와 digest 확인
Terminal window
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가 실제 이미지 내용 동일성을 더 강하게 보장한다는 점을 구분한다
  • staging과 production이 같은 tag 또는 digest를 쓰는지 확인한다

CI/CD 복습 체크리스트

  • CI와 CD의 차이를 '통합 가능성 검증'과 '산출물 승격' 관점에서 설명할 수 있다.
  • Pre-Release Integration이 왜 출시 직전 리스크를 키우는지 작은 예로 설명할 수 있다.
  • GitHub Actions runner가 매 실행마다 새 환경이라는 점이 `checkout`, `npm ci`, 캐시에 어떤 영향을 주는지 설명할 수 있다.
  • `GITHUB_SHA` 태그와 이미지 digest가 불변 아티팩트 원칙에 어떻게 연결되는지 설명할 수 있다.
  • 정적 클라우드 키와 OIDC 임시 자격증명의 보안 차이를 설명할 수 있다.
  • Rolling, Blue-Green, Canary를 비용, 롤백 속도, 관측성 기준으로 비교할 수 있다.
  • 배포 실패 시 GitHub Actions 단계, 레지스트리, ECS service event 중 어디를 먼저 볼지 판단할 수 있다.
  • Trivy 같은 이미지 스캔을 무조건적 차단이 아니라 팀의 위험 기준으로 해석할 수 있다.

퀴즈

staging에서 검증한 이미지와 production에 배포된 이미지가 다르면 어떤 CI/CD 원칙이 깨진 것인가?

힌트: 커밋 SHA 태그와 이미지 digest를 왜 쓰는지 떠올려본다.

정답 보기

불변 아티팩트 원칙이다. 환경마다 다시 빌드하거나 `latest`처럼 움직이는 태그를 쓰면, 같은 커밋이어도 실제 배포물이 달라질 수 있다.

퀴즈

GitHub Actions는 초록색인데 사용자는 502를 본다. CI 단계와 CD 단계 중 어디부터 의심해야 하는가?

힌트: 파이프라인 성공은 컨테이너가 실제 트래픽을 처리한다는 보장과 다르다.

정답 보기

먼저 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를 표시해본다.
  • 최근 실패한 workflow 하나를 골라 실패 step이 CI, package, deploy, runtime 중 어디에 속하는지 분류한다.
  • 팀 이미지 태그가 latest 중심인지, 커밋 SHA나 digest로 추적 가능한지 확인한다.
  • staging과 production이 같은 이미지 tag/digest를 승격하는지 확인한다.
  • production 배포 job에 GitHub Environment 보호 규칙이 있는지 확인한다.
  • 정적 클라우드 키를 쓰는지, OIDC 임시 자격증명을 쓰는지 확인한다.
  • ECS라면 Deployment Circuit Breaker와 health check 기준이 실제 장애를 감지할 수 있는지 확인한다.
  • Trivy 또는 유사 스캐너가 어느 severity에서 배포를 막는지 확인한다.
  1. CI는 작은 변경을 자주 합치고 자동 검증으로 통합 실패를 빨리 드러내는 활동이다.
  2. CD는 검증된 산출물을 staging과 production으로 승격시키는 경로를 자동화하는 활동이다.
  3. CI/CD의 중심 원칙은 격리된 runner, Pipeline as Code, 불변 아티팩트, 좁은 권한이다.
  4. Docker 이미지는 CI/CD에서 “다시 빌드할 재료”가 아니라 “검증 후 승격할 아티팩트”가 된다.
  5. GITHUB_SHA 태그와 digest는 어떤 커밋이 어떤 이미지로 배포됐는지 추적하게 해준다.
  6. OIDC는 장기 클라우드 키를 저장하지 않고 workflow 실행 컨텍스트로 단기 권한을 얻는 방식이다.
  7. Rolling, Blue-Green, Canary는 속도, 비용, 롤백 시간, 관측성 요구가 다르다.
  8. 장애 진단은 명령어 암기가 아니라 workflow 시작 여부, CI 실패, package 실패, deploy 이후 runtime 실패를 먼저 나누는 일이다.