← posts/b.log()

blog92@web:~$ cat posts/devops-patterns-04-pipeline.md

DEVOPS10 min read

DevOps 패턴과 안티패턴 4강 — 파이프라인과 아티팩트

Deployment Pipeline과 Build Once Deploy Many, latest 태그가 검증을 무력화하는 이유, Twelve-Factor 설정 분리와 시크릿 관리, Pipeline as Code·GitOps·Shift-Left.

이전 강의: 3강. 코드 흐름: 병합 지옥에서 벗어나기 이번 강의 키워드: Deployment Pipeline, Continuous Delivery, Build Once Deploy Many, latest 태그, Twelve-Factor App, 시크릿 관리, Pipeline as Code, GitOps, Shift-Left

들어가며

스테이징에서 일주일 동안 QA를 마친 버전을 운영에 배포했습니다. 그런데 운영에서만 장애가 납니다. 원인을 찾아보니 운영 배포 때 이미지를 다시 빌드했고, 그 사이 베이스 이미지의 latest 태그가 가리키는 버전이 바뀌어 있었습니다. QA가 검증한 것과 운영에 나간 것은 이름만 같은 다른 물건이었던 셈입니다.

3강에서 코드가 trunk에 모이는 흐름을 정리했다면, 이번 강의는 trunk의 코드가 운영에 도착하기까지의 흐름을 다룹니다. 핵심 질문은 하나입니다. "우리가 검증한 것이 정말 운영에 나간 것과 같은가?"

Deployment Pipeline: 변경이 지나가는 컨베이어

2010년 Jez Humble과 David Farley는 『Continuous Delivery』에서 배포 파이프라인(Deployment Pipeline)이라는 개념을 체계화했습니다. 커밋 하나가 운영에 도달하기까지 거치는 모든 단계를 자동화된 흐름으로 만든 것입니다.

text
커밋 → 빌드 → 단위 테스트 → 아티팩트 생성 → 통합 테스트
     → 스테이징 배포 → 인수 테스트 → 운영 배포

각 단계는 관문(gate) 역할을 합니다. 앞 단계를 통과하지 못한 변경은 다음으로 넘어가지 못합니다. 앞쪽 단계는 빠르고 싸게, 뒤쪽 단계는 느리지만 운영과 비슷하게 구성해서, 문제가 있으면 최대한 앞에서 걸러냅니다. 3강의 테스트 피라미드가 파이프라인의 시간축 위에 펼쳐진 모양이라고 볼 수 있습니다.

여기서 두 용어를 구분해 둘 필요가 있습니다. Continuous Delivery(지속적 전달)는 모든 변경이 언제든 운영에 배포 가능한 상태로 유지되는 것을 말합니다. 실제 배포 버튼은 사람이 누를 수도 있습니다. Continuous Deployment(지속적 배포)는 파이프라인을 통과한 모든 변경이 사람의 개입 없이 자동으로 운영에 나가는 것입니다. 둘 다 CD로 줄여 부르기 때문에 대화할 때 어느 쪽인지 확인하는 게 좋습니다. 대부분의 조직에게 현실적인 목표는 전자이고, 후자는 테스트와 관측 체계가 충분히 성숙한 뒤의 선택지입니다.

Build Once, Deploy Many

도입부 사례의 문제를 푸는 패턴이 Build Once, Deploy Many입니다. 빌드는 파이프라인 초반에 딱 한 번만 하고, 그 결과물(아티팩트)을 개발, 스테이징, 운영 환경으로 그대로 승격(promote)시키는 것입니다.

환경마다 다시 빌드하면 무엇이 달라질 수 있을까요? 생각보다 많습니다. 의존성 버전이 범위로 지정돼 있다면 빌드 시점마다 다른 패키지가 설치될 수 있습니다. 베이스 이미지가 갱신됐을 수도 있고, 빌드 서버의 도구 버전이 바뀌었을 수도 있습니다. 결국 스테이징에서의 검증은 다른 물건에 대한 검증이 됩니다. 3강 끝에서 본 환경별 브랜치 안티패턴도 같은 문제를 안고 있습니다. 환경마다 다른 브랜치에서 빌드하니, 같은 코드라는 보장조차 없습니다.

Build Once, Deploy Many를 지키려면 두 가지가 필요합니다.

첫째, 아티팩트는 불변이어야 합니다. 한 번 만든 아티팩트는 수정하지 않고, 고유한 버전으로 식별합니다. 2강의 Immutable Infrastructure가 서버에 적용한 원칙을 빌드 결과물에 적용한 것입니다.

둘째, 아티팩트는 환경을 몰라야 합니다. 같은 이미지가 개발에서도 운영에서도 돌아야 하니, 환경에 따라 달라지는 값은 이미지 밖에서 주입해야 합니다. 이 부분은 뒤의 Twelve-Factor 절에서 다룹니다.

latest 태그라는 함정

컨테이너 이미지의 태그는 움직이는 포인터입니다. myapp:1.4.2든 myapp:latest든, 태그는 레지스트리에서 언제든 다른 이미지를 가리키도록 바뀔 수 있습니다. 특히 latest는 관례상 "가장 최근에 푸시된 것"을 가리키도록 계속 덮어쓰입니다.

그래서 latest에 의존하면 세 가지 문제가 생깁니다. 지금 운영에서 어떤 버전이 돌고 있는지 태그만 보고는 알 수 없습니다. 같은 매니페스트로 서버를 다시 띄워도 어제와 다른 이미지가 올라올 수 있습니다. 문제가 생겨 "이전 버전으로 롤백"하려 해도 이전 버전이 무엇이었는지 기록이 없습니다. 모두 1강의 첫 번째 질문, 재현성을 해칩니다.

대응은 단계적입니다. 최소한 명시적인 버전 태그를 씁니다. 버전과 함께 Git 커밋 해시를 태그에 넣으면 어떤 코드에서 나온 이미지인지 추적할 수 있습니다. 가장 엄격한 방법은 다이제스트(digest)로 고정하는 것입니다.

yaml
# 움직이는 포인터
image: myapp:latest
 
# 사람이 읽기 쉬운 버전 (그래도 덮어쓰일 수는 있음)
image: myapp:1.4.2-a1b2c3d
 
# 내용 기반 식별자: 이미지 내용이 다르면 값도 다름
image: myapp@sha256:7f3e...c91a

다이제스트는 이미지 내용의 해시이므로, 같은 다이제스트는 반드시 같은 이미지입니다. 같은 원칙이 애플리케이션 의존성에도 적용됩니다. package-lock.json 같은 lockfile을 커밋하고 CI에서 npm ci로 설치하는 것, Dockerfile의 FROM에 구체적인 버전을 쓰는 것이 모두 같은 맥락입니다.

Twelve-Factor App: 설정은 환경에

2011년 Heroku의 Adam Wiggins가 정리한 Twelve-Factor App은 클라우드에서 운영하기 좋은 애플리케이션의 12가지 원칙입니다. 이 중 파이프라인과 직접 관련된 원칙이 몇 가지 있습니다.

설정(Config)은 환경변수로. DB 주소, 외부 API 엔드포인트처럼 배포 환경마다 달라지는 값은 코드나 이미지에 넣지 않고 실행 시점에 환경에서 주입합니다. 그래야 같은 아티팩트가 모든 환경에서 쓰일 수 있습니다.

빌드, 릴리스, 실행의 분리. 빌드는 코드를 실행 가능한 번들로 만들고, 릴리스는 그 번들에 특정 환경의 설정을 결합하고, 실행은 릴리스를 구동합니다. 각 단계는 엄격히 분리되어야 합니다.

개발/운영 환경 일치(Dev/Prod Parity). 개발 환경과 운영 환경의 차이를 최소화합니다. 개발에서는 SQLite, 운영에서는 PostgreSQL을 쓰는 식의 차이가 "제 PC에선 되는데요"를 만듭니다. 컨테이너가 이 차이를 크게 줄였습니다.

실무에서 이 원칙을 깨뜨리기 쉬운 예를 하나 들어보겠습니다. Next.js에서 NEXT_PUBLIC_ 접두사가 붙은 환경변수는 빌드 시점에 클라이언트 번들에 문자열로 박혀 들어갑니다. 여기에 환경별 API 주소를 넣으면, 스테이징용으로 빌드한 이미지는 운영에서 쓸 수 없고 환경마다 다시 빌드해야 합니다. Build Once, Deploy Many가 조용히 깨지는 것이죠. 이런 값은 서버 측에서 런타임에 읽어 전달하거나, 빌드 결과와 무관한 경로로 주입하도록 설계해야 합니다. 사용하는 프레임워크가 어떤 값을 빌드 시점에 굳히는지 알아두는 것이 중요합니다.

시크릿: 저장소에 올라가면 끝이다

설정 중에서도 비밀번호, API 키, 인증서 같은 시크릿은 따로 다뤄야 합니다. 가장 흔하면서 가장 치명적인 안티패턴은 시크릿을 코드 저장소에 커밋하는 것입니다.

많은 사람이 "실수로 올렸으면 다음 커밋에서 지우면 된다"고 생각합니다. 하지만 Git은 이력을 보존하는 도구입니다. 파일에서 지워도 과거 커밋에는 그대로 남고, 저장소를 클론한 모든 사람이 그 이력을 갖게 됩니다. 이력을 재작성하더라도 이미 복제된 사본까지 회수할 수는 없습니다. 그래서 시크릿이 한 번이라도 커밋됐다면 유출된 것으로 간주하고 즉시 교체(rotation)하는 것이 원칙입니다.

예방을 위한 패턴은 이렇습니다. 시크릿은 HashiCorp Vault나 클라우드 제공자의 시크릿 매니저 같은 전용 저장소에 두고, 실행 시점에 주입합니다. 커밋 전에 gitleaks 같은 도구로 시크릿 패턴을 검사하는 훅을 걸어 둡니다. Kubernetes를 쓴다면 기본 Secret 객체는 base64 인코딩일 뿐 암호화가 아니라는 점도 기억해야 합니다. 저장 시 암호화 설정이나 외부 시크릿 관리 도구와의 연동이 필요합니다.

Pipeline as Code

2강에서 서버를 클릭으로 설정하면 눈송이가 된다고 했습니다. 파이프라인도 마찬가지입니다. 초창기 Jenkins에서는 웹 UI에서 빌드 단계를 클릭으로 구성하는 것이 일반적이었습니다. 그 결과 아무도 구성 이력을 모르고, Jenkins 서버가 죽으면 재구성할 수 없는 눈송이 파이프라인이 생겼습니다.

Pipeline as Code는 파이프라인 정의를 애플리케이션 코드와 같은 저장소에 파일로 두는 패턴입니다. Jenkins는 2016년 2.0 버전에서 Jenkinsfile을 전면에 내세웠고, 이후 등장한 GitHub Actions와 GitLab CI는 처음부터 YAML 파일로 파이프라인을 정의합니다.

yaml
# .github/workflows/ci.yml (개념 예시)
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
      - run: docker build -t myapp:${{ github.sha }} .

이렇게 하면 파이프라인 변경도 코드 리뷰를 거치고, 이력이 남고, 브랜치마다 다른 파이프라인을 실험할 수 있습니다. 2강의 IaC가 인프라에 준 이점을 파이프라인에도 그대로 가져오는 것입니다.

GitOps: 수렴 모델의 귀환

2강 말미에서 "누군가의 노트북에서 실행하는 IaC"를 안티패턴으로 꼽았습니다. 코드는 저장소에 있지만, 그 코드를 실제 환경에 적용하는 행위는 여전히 사람의 손에 달려 있다는 문제였죠. 이 문제에 대한 답이 GitOps입니다.

GitOps라는 이름은 2017년 Weaveworks가 붙였습니다. 이후 CNCF 산하 OpenGitOps 프로젝트가 네 가지 원칙으로 정리했습니다. 시스템의 원하는 상태를 선언형으로 기술하고, 그 선언을 버전 관리되는 불변 저장소(보통 Git)에 두고, 에이전트가 그 상태를 자동으로 끌어오며(pull), 실제 상태를 원하는 상태에 지속적으로 맞춘다(reconcile)는 것입니다.

마지막 원칙을 보면 익숙한 개념이 떠오를 겁니다. 2강의 수렴 모델입니다. Argo CD나 Flux 같은 GitOps 도구는 클러스터 안에서 계속 돌면서 Git의 선언과 클러스터의 실제 상태를 비교하고, 차이가 있으면 맞춥니다. 2강에서 수렴 모델의 약점으로 꼽았던 "실행 사이의 공백"이 지속적인 조정으로 거의 사라지고, 누군가 클러스터를 수동으로 바꾸면 드리프트로 감지되거나 자동으로 되돌려집니다.

GitOps의 또 다른 특징은 pull 방식이라는 점입니다. 전통적인 CI/CD는 파이프라인이 운영 클러스터에 접속해 배포를 밀어 넣습니다(push). 그러려면 CI 시스템이 운영 환경의 강력한 인증 정보를 가져야 합니다. GitOps에서는 클러스터 안의 에이전트가 Git을 읽어 옵니다. 운영 환경의 인증 정보가 밖으로 나갈 필요가 줄어듭니다.

결과적으로 배포 절차는 "Git에 커밋한다"로 단순해집니다. 누가 언제 무엇을 배포했는지는 Git 이력이 되고, 롤백은 커밋 되돌리기가 됩니다.

Shift-Left: 검사를 앞으로 당기기

파이프라인의 마지막 패턴은 Shift-Left입니다. 타임라인을 왼쪽에서 오른쪽으로 그렸을 때, 테스트와 보안 검사를 왼쪽(앞쪽)으로 옮기자는 뜻입니다. 1강의 세 번째 질문, 빠른 피드백을 보안과 품질 영역에 적용한 것이죠.

전통적으로 보안 검토는 출시 직전 한 번 이뤄지는 관문이었습니다. 그 시점에 취약점이 발견되면 설계부터 다시 해야 하거나, 일정 때문에 "일단 출시하고 나중에 고치자"는 결정이 내려집니다. 이 안티패턴을 흔히 막판 보안 게이트라고 부릅니다.

Shift-Left를 적용하면 파이프라인의 각 단계에 검사가 들어갑니다. 커밋 시점에 시크릿 스캔, 빌드 시점에 정적 분석(SAST)과 의존성 취약점 검사, 이미지 생성 후 컨테이너 이미지 스캔을 돌립니다. 아티팩트에 어떤 구성 요소가 들어 있는지 목록(SBOM)을 함께 생성해 두면, 나중에 특정 라이브러리의 취약점이 공개됐을 때 영향 범위를 빠르게 파악할 수 있습니다. 이렇게 보안을 개발 흐름 안에 녹이는 접근을 DevSecOps라고도 부릅니다.

다만 Shift-Left에도 함정이 있습니다. 검사를 너무 많이, 너무 엄격하게 넣어 파이프라인이 30분씩 걸리거나 오탐으로 자주 실패하면, 3강에서 본 것처럼 사람들은 우회로를 찾습니다. 검사는 빠른 것부터 앞에, 느린 것은 뒤에 두고, 오탐은 적극적으로 관리해야 합니다.

안티패턴 한눈에 보기

안티패턴증상대응 패턴
환경별 재빌드스테이징과 운영의 결과물이 다름Build Once, Deploy Many
latest 태그 의존운영 버전을 알 수 없음, 롤백 불가버전·커밋 해시 태그, 다이제스트 고정
설정이 박힌 아티팩트환경마다 이미지가 따로 있음설정의 외부 주입 (Twelve-Factor)
시크릿 커밋저장소 이력에 비밀번호시크릿 매니저, 커밋 전 스캔, 즉시 교체
눈송이 파이프라인CI 서버 UI에만 존재하는 설정Pipeline as Code
사람이 실행하는 배포특정인의 PC와 권한에 의존GitOps
막판 보안 게이트출시 직전 대량의 취약점 발견Shift-Left, DevSecOps

정리

배포 파이프라인은 커밋이 운영에 도달하는 모든 단계를 자동화된 관문으로 만든 것입니다. 그 파이프라인이 의미를 가지려면 검증한 것과 배포한 것이 같아야 하고, 이를 위해 한 번 빌드한 불변 아티팩트를 환경 간에 승격시키고, 태그 대신 버전이나 다이제스트로 식별하며, 환경별 설정과 시크릿은 밖에서 주입합니다. 파이프라인 자체도 코드로 관리하고, GitOps는 2강의 수렴 모델을 지속적인 조정으로 되살려 배포의 실행 주체를 사람에서 시스템으로 옮깁니다.

다음 5강에서는 파이프라인의 마지막 한 걸음, 운영에 배포하는 순간을 다룹니다. 몇 달치 변경을 한 번에 내보내는 빅뱅 릴리스를 Blue-Green, Canary, Feature Flag로 어떻게 쪼개는지, 그리고 데이터베이스 스키마는 어떻게 함께 바꾸는지 살펴봅니다.

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..