CI/CD와 인프라 코드
커밋이 테스트를 거쳐 운영에 닿기까지의 길을 정리한다. 파이프라인을 빠르게 만드는 법, 롤링·블루-그린·카나리 배포, 기능 플래그, Terraform과 GitOps로 인프라를 코드로 다루는 법, 비밀값과 공급망을 지키는 법, 그리고 이 모든 것이 잘 돌아가는지 재는 DORA 지표까지를 숫자로 계산해 가며 본다.
목차 (11)
지속적 통합(CI, continuous integration)은 모두의 변경을 자주(하루에 적어도 한 번) main에 합치고, 합칠 때마다 자동으로 빌드·시험하는 습관이다. 지속적 전달(continuous delivery)은 main이 언제든 운영에 내보낼 수 있는 상태로 유지되는 것이고, 지속적 배포(continuous deployment)는 사람의 버튼 없이 실제로 내보내기까지 하는 것이다. 브랜치를 어떻게 합치는지는 버전 관리 노트, 파이프라인 안에서 도는 테스트는 소프트웨어 테스트 노트에 있다.
CI 파이프라인의 단계
파이프라인은 대개 준비(코드 받기, 의존성 설치) → 검사(린트, 타입 검사, 보안 스캔) → 빌드 → 테스트(단위, 통합, E2E) → 패키징(컨테이너 이미지, 서명) 순서로 돈다. 단계는 앞 단계가 통과해야 넘어가고, 한 단계 안의 서로 독립인 작업은 동시에 돌린다. 그러면 전체 시간은 작업 시간의 합이 아니라 단계마다 가장 긴 작업을 이은 임계 경로가 정한다.
- 빨리 실패하기: 싸고 자주 깨지는 검사를 앞에 둔다. 린트가 1분 만에 실패하면 8분짜리 E2E를 돌릴 필요가 없다.
- 10분 안팎: 피드백이 늦으면 개발자는 결과를 기다리지 않고 다음 일로 넘어가고, 깨진 main 위에 변경이 쌓인다. 예제는 차례로 돌리면 29분, 병렬이면 19분이다.
- main이 깨지면 그것부터: 깨진 main 위에서는 아무도 자기 변경이 안전한지 알 수 없다. 고치기 어렵다면 문제 커밋을 되돌리는 것이 기본이다. 병합 전에 main 최신과 합친 결과로 다시 시험하는 머지 큐(merge queue)도 같은 목적이다.
- 같은 환경에서: 파이프라인 정의를 저장소 안 파일(YAML 등)로 두고, 빌드는 컨테이너처럼 고정된 환경에서 돌린다. ‘내 컴퓨터에서는 되는데’를 없애는 첫걸음이다.
캐시와 산출물
둘 다 파이프라인 사이에 파일을 넘기지만 목적이 다르다. 캐시는 다시 만들 수 있지만 오래 걸리는 것(내려받은 의존성, 컴파일 중간 결과)을 재사용해 시간을 아낀다. 없거나 낡아도 결과는 같아야 한다. 산출물(artifact)은 파이프라인이 만든 결과 그 자체(바이너리, 컨테이너 이미지)이고, 뒤 단계와 배포가 이것을 그대로 쓴다.
- 캐시 키: lockfile이 같으면 같은 키, 한 글자라도 바뀌면 다른 키다. 설치가 캐시 없이 3분, 캐시로 0.5분이고 적중률이 90%면 기대 시간은 0.75분이다. 키가 너무 넓으면(lockfile을 안 넣으면) 낡은 의존성으로 시험하는 사고가 난다.
- 한 번 빌드해 승격: 스테이징에서 시험한 이미지와 운영에 올린 이미지가 같은 바이트여야 시험의 의미가 있다. 이미지를 태그(
:latest)가 아니라 내용 해시(digest)로 가리키면 중간에 바뀌지 않는다. 환경마다 다른 것은 설정과 비밀값으로만 넣는다.
배포 전략: 롤링, 블루-그린, 카나리
새 판을 운영에 올리는 방식은 ‘얼마나 많은 사용자가 새 판을 얼마나 빨리 만나는가’와 ‘문제가 생기면 얼마나 빨리 되돌리는가’ 사이의 선택이다. 세 전략을 같은 시간 축에 놓아 본다.
세 전략 모두 옛 판(v1) 4대가 모든 트래픽을 받는 상태에서 시작한다.
- 롤링: 인스턴스를 몇 대씩 차례로 바꾼다. 쿠버네티스 Deployment의 기본이고,
maxSurge(더 띄울 수)와maxUnavailable(내릴 수)로 속도와 여유 자원을 조절한다. 그림처럼 한 대씩 내리고 올리면 최대 4대로 추가 자원이 없지만, 배포 중에는 두 판이 섞여 돈다. - 블루-그린: 똑같은 환경을 하나 더 띄워(최대 8대) 시험한 뒤 라우터를 한 번에 돌린다. 되돌리기도 라우터만 돌리면 되니 순간이다. 두 배의 자원이 잠깐 들고, DB처럼 둘이 함께 쓰는 것은 두 판 모두와 맞아야 한다.
- 카나리: 새 판에 트래픽을 조금(그림은 5%)만 보내 오류율·지연을 옛 판과 비교하고, 괜찮으면 단계적으로 올린다. 분당 요청 2,000건인 서비스에 나쁜 판이 나가 감지까지 10분 걸린다면, 한 번에 바꿀 때는 20,000건, 5% 카나리면 1,000건이 나쁜 판을 만난다. 판정은 사람 눈보다 미리 정한 지표와 자동 비교에 맡기는 것이 좋다(SRE Workbook).
- DB 스키마는 따로: 어느 전략이든 옛 판과 새 판이 잠시 같은 DB를 쓴다. 열 이름 바꾸기 같은 변경은 ‘새 열 추가 → 양쪽에 쓰기 → 옮기기 → 옛 열 제거’처럼 여러 번의 배포로 나눈다(expand-contract).
기능 플래그: 배포와 출시를 떼어 놓기
배포는 코드를 운영에 올리는 일이고, 출시는 사용자가 기능을 보게 하는 일이다. 기능 플래그는 이 둘을 떼어 놓는다. 덜 만든 기능도 꺼진 채 main에 합쳐 배포하고, 나중에 설정만 바꿔 직원 → 일부 사용자 → 모두 순으로 켠다. 문제가 생기면 다시 배포할 필요 없이 끈다. 진화적 아키텍처 관점의 이야기는 시스템 설계 › 진화적 아키텍처에 있다.
- 종류마다 수명이 다르다: 출시용 플래그는 몇 주 안에 지워야 하고, 운영용 차단 스위치(kill switch)나 권한 플래그는 오래 산다(Feature Toggles).
- 플래그 빚: 다 켠 플래그를 지우지 않으면 코드에 아무도 안 타는 갈래가 쌓이고, 켜짐·꺼짐 조합만큼 시험할 경우가 늘어난다. 만들 때 지울 날짜와 담당자를 함께 적는다.
Terraform: plan, apply, state
인프라 코드(IaC, infrastructure as code)는 서버·네트워크·DB 같은 자원을 콘솔 클릭 대신 코드로 정의해 저장소에 두고 리뷰·이력·재현성을 얻는 방법이다. 대표 도구 Terraform은 HCL로 원하는 자원을 적고 세 가지를 비교한다. 코드(원하는 상태), state 파일(지난번 apply 때 만든 자원과 그 ID를 기억한 장부), 실제 클라우드다. terraform plan은 실제 상태를 다시 읽고(refresh) 코드와의 차이를 계획으로 보여 주고, terraform apply가 그 계획을 실행한다.
- plan을 리뷰하기: 그림의 요약 줄은
Plan: 2 to add, 1 to change, 2 to destroy.다. 코드 변경보다 이 plan 출력이 더 중요하다. 한 줄 고쳤는데 DB가 ‘교체’로 나오면 데이터가 사라진다는 뜻이다. PR에 plan 결과를 붙여 함께 리뷰하고, apply는 리뷰한 그 plan 파일로 한다. - 드리프트: 누군가 콘솔에서 직접 바꾸면 코드·state·실제가 어긋난다. 그림의 웹 서버는 state에 t3.small로 적혀 있지만 실제는 t3.medium이다. 다음 plan이 이것을 되돌리려 하므로, 정기적으로 plan을 돌려 드리프트를 감지하고 콘솔 쓰기 권한을 줄인다.
- state는 공유 자원: state에는 자원 ID와 때로 비밀번호까지 담기므로 암호화한 원격 저장소(S3, GCS 등)에 두고, 두 사람이 동시에 apply하지 못하게 잠근다. 잠금을 지원하는 백엔드라면 Terraform이 state를 쓸 수 있는 모든 작업에서 잠금을 저절로 걸고, 잠금을 못 얻으면 멈춘다. CI가 중간에 죽어 잠금이 남았을 때 쓰는
terraform force-unlock은 정말 아무도 apply하지 않는지 확인한 뒤에만 쓴다. 남이 잡고 있던 잠금을 풀면 두 작업이 같은 state를 동시에 쓴다. - 모듈: 자주 쓰는 묶음(VPC + 서브넷 + 라우팅)을 모듈로 만들어 입력 변수만 바꿔 재사용한다. 환경(dev·staging·prod)마다 같은 모듈을 다른 값으로 부르면 환경 사이 차이가 코드로 드러난다.
- OpenTofu: HashiCorp가 2023년 Terraform의 라이선스를 BSL로 바꾸자, 그 직전 판에서 갈라져 나온 오픈소스 판이 OpenTofu다. 문법과 동작은 대부분 같다.
선언형과 명령형
명령형은 ‘무엇을 할지’(서버 3대를 더 만들어라)를, 선언형은 ‘무엇이어야 할지’(서버는 6대다)를 적는다. 선언형 도구는 지금 상태를 읽어 차이만큼만 고치므로, 같은 정의를 몇 번 적용해도 결과가 같다. 이 성질을 멱등성(idempotence)이라 한다.
명령형 스크립트는 세 번 돌리면 6 → 9 → 12대가 되고, 선언형은 6 → 6 → 6대로 머문다. 선언형이 늘 낫지는 않다. ‘DB를 백업하고 → 점검 모드로 바꾸고 → 마이그레이션하라’처럼 순서 자체가 중요한 작업은 명령형이 자연스럽다. Terraform·쿠버네티스 매니페스트·CloudFormation은 선언형, 셸 스크립트는 명령형이고, Pulumi·CDK는 일반 프로그래밍 언어(명령형)로 선언형 자원 그래프를 만들어 낸다.
GitOps: 저장소가 곧 운영 상태
GitOps는 선언형을 배포까지 밀고 간 방식이다. 운영 환경이 어때야 하는지를 Git 저장소에 선언해 두고, 클러스터 안의 에이전트가 그 저장소를 계속 읽어 실제 상태를 맞춘다. OpenGitOps는 이를 네 원칙(선언형, 버전 관리되고 불변, 자동으로 끌어옴, 계속 맞춤)으로 정리했다. 쿠버네티스에서는 Argo CD와 Flux가 대표적인 에이전트다.
- 자격 증명이 안쪽에: CI가 클러스터 관리자 권한을 들고 있지 않아도 된다. CI가 털려도 할 수 있는 일은 저장소에 커밋을 남기는 것이고, 그것은 리뷰와 이력에 드러난다.
- 되돌리기 = 커밋 되돌리기: 운영 상태의 이력이 곧 Git 이력이라, 언제 누가 무엇을 바꿨는지 보이고
git revert로 되돌린다. - 드리프트를 스스로 고침: 누가
kubectl edit로 직접 고쳐도 에이전트가 저장소 상태로 되돌린다(설정에 따라 알리기만 할 수도 있다).
파이프라인의 비밀값
파이프라인은 클라우드, 레지스트리, 결제 같은 곳의 자격 증명이 모이는 곳이라 공격자가 노리기 좋다. 가장 흔한 실수는 오래 사는 접근 키를 CI 설정에 넣어 두는 것이다. 한 번 새면 누군가 알아채고 바꿀 때까지 유효하다.
- OIDC 연동: GitHub Actions 등은 실행마다 OpenID Connect 토큰을 발급한다. 클라우드 쪽 신뢰 정책에 ‘이 저장소의 main 브랜치’처럼 조건을 걸면, 포크나 다른 브랜치의 작업은 같은 권한을 얻지 못한다.
- 최소 권한과 나누기: 빌드 작업과 배포 작업의 권한을 나누고, 운영 배포 권한은 보호된 브랜치·환경에서만 쓰게 한다. 외부 기여자의 PR이 비밀값에 닿는 경로가 없는지 확인한다. GitHub Actions의
pull_request_target·workflow_run은 본 저장소의 권한과 비밀값을 갖고 도는 트리거라, 여기서 포크 PR의 코드를 체크아웃해 빌드하면 그 코드가 비밀값과 쓰기 권한을 얻는다(GitHub 보안 사용 안내). - 로그에 찍지 않기: CI는 등록된 비밀값을 로그에서 가리지만, 인코딩을 바꾸거나 잘라 찍으면 그대로 나온다. 비밀값은 환경 변수나 비밀 관리 서비스(Vault, 클라우드 Secret Manager)에서 실행 시점에 받는다.
공급망: 무엇을 배포하는지 증명하기
내가 쓴 코드가 안전해도, 빌드 서버가 오염되거나 레지스트리의 이미지가 바꿔치기되면 사용자에게는 다른 것이 간다. 소스에서 배포까지의 고리 전체가 소프트웨어 공급망이다. 의존성 나무와 lockfile, 코드 서명의 원리는 보안 › 시스템·소프트웨어 보안에서 다뤘고, 여기서는 파이프라인이 할 일을 본다.
- SLSA: 빌드 트랙의 수준이 올라갈수록 요구가 커진다. L1은 출처 증명(provenance: 어느 소스 커밋을 어느 빌더가 어떤 명령으로 빌드했나)이 있을 것, L2는 호스팅된 빌드 플랫폼이 그 증명에 서명할 것, L3은 빌드끼리 격리돼 한 빌드가 다른 빌드나 서명 키를 건드릴 수 없을 것이다.
- 서명: Sigstore(cosign)는 CI의 OIDC 신원으로 짧게 사는 인증서를 받아 서명하고, 서명 기록을 공개 투명성 로그에 남긴다. 오래 보관할 서명 키가 없어진다.
- SBOM: 산출물에 든 구성 요소와 판의 목록이다. SPDX와 CycloneDX가 표준 형식이다. 새 취약점이 공개되면 ‘우리 어느 서비스에 이 판이 들어 있나’를 바로 답할 수 있다.
- 파이프라인 자체의 의존성: CI에서 가져다 쓰는 액션·플러그인도 공급망이다. 태그(
@v4)는 나중에 다른 코드를 가리키게 바뀔 수 있으니 커밋 해시로 고정하고, 갱신은 봇이 PR로 올리게 한다.
DORA 지표: 잘 돌아가는지 재기
DORA(DevOps Research and Assessment) 연구는 소프트웨어 전달 성과를 몇 개의 지표로 잰다. 처음에는 네 지표였고, 지금은 다섯 지표를 처리량과 불안정성 두 묶음으로 나눈다. 처리량은 변경 리드 타임(커밋에서 운영까지), 배포 빈도, 실패 배포 복구 시간이고, 불안정성은 변경 실패율(곧바로 되돌리거나 핫픽스가 필요했던 배포 비율)과 재작업 배포 비율(사고 때문에 계획에 없이 한 배포 비율)이다.
예제 팀은 주 3회 배포하고, 리드 타임 중앙값은 11시간, 변경 실패율은 17%, 실패에서 복구하는 데 중앙값 68분이 걸렸다. DORA 연구가 되풀이해 보여 주는 결론은 속도와 안정성이 맞바꾸는 관계가 아니라는 것이다. 작은 변경을 자주 내보내는 팀이 대개 실패도 적고 복구도 빠르다. 다만 지표를 목표로 삼으면(‘하루 열 번 배포하라’) 숫자만 맞추는 행동이 생기므로, 서비스 하나 단위로 추세를 보며 병목을 찾는 데 쓴다.
핵심 자료
| 자료 | 한 줄 요지 |
|---|---|
| Continuous Integration (Martin Fowler) | CI의 원래 뜻을 정리한 글. 모두가 하루에 적어도 한 번 main에 합치고, 자동 빌드·테스트가 늘 초록이어야 한다. |
| BlueGreenDeployment (Martin Fowler) | 똑같은 환경 둘을 두고 라우터만 바꿔 배포·되돌리기를 순간에 하는 방법. |
| Canarying Releases (Google SRE Workbook) | 카나리의 크기·기간·지표 고르기와, 카나리와 대조군을 비교해 판정하는 방법을 실무 관점에서 다룬다. |
| Feature Toggles (Pete Hodgson) | 기능 플래그를 출시·실험·운영·권한 네 종류로 나누고, 수명과 관리 비용을 다룬 표준 글. |
| Terraform 문서: State | state 파일이 왜 필요한지, 원격 저장과 잠금, 민감 정보 취급을 설명한다. |
| OpenGitOps | CNCF 작업 그룹이 정한 GitOps 네 원칙: 선언형, 버전 관리·불변, 자동으로 끌어옴, 계속 맞춤. |
| SLSA | 빌드 출처 증명과 빌드 환경 강화를 수준별로 정한 공급망 보안 틀. |
| DORA 소프트웨어 전달 지표 | 처리량(리드 타임·배포 빈도·복구 시간)과 불안정성(변경 실패율·재작업 비율) 다섯 지표의 정의. |
- Continuous Integration (Martin Fowler)
- BlueGreenDeployment (Martin Fowler)
- Canarying Releases (Google SRE Workbook)
- Feature Toggles (Pete Hodgson)
- Terraform 문서: State
- OpenGitOps
- SLSA
- DORA 소프트웨어 전달 지표
- CanaryRelease (Danilo Sato)
- Kubernetes Deployments — 롤링 업데이트
- GitHub Actions 의존성 캐시
- terraform plan 명령
- Terraform 모듈
- OpenTofu
- Argo CD
- Flux 핵심 개념
- Terraform 문서: State Locking
- GitHub Actions 보안 사용 안내 (Secure use reference)
- GitHub Actions의 OpenID Connect
- Sigstore 문서
- CycloneDX
- SPDX