← Posts

DevOps · CI/CD · GitHub Actions

CI/CD - 사람이 안 하게 만든다

배포 절차를 사람이 기억하면 언젠가 한 단계를 빠뜨린다. 이 블로그를 실제로 배포하는 워크플로를 재료로 각 단계가 왜 있는지 본다.

목차
  1. 배포하다 한 단계를 빠뜨렸다
  2. 사람이 하는 절차는 조용히 어긋난다
  3. 절차를 코드로 적으면 실행되는 문서가 된다
  4. 방아쇠: 언제 도는가
  5. 검증이 배포 앞을 막는다
  6. 왜 링크 검사가 따로 한 단계인가
  7. CI와 CD는 무엇이 다른가
  8. 로컬 검증과 CI 게이트는 역할이 다르다
  9. 실패하면 어떻게 되나
  10. 무엇을 파이프라인에 넣지 않는가
  11. 실무에서 파이프라인이 커지면
  12. 정리

만들고, 담고, 돌보고, 앞에서 받는 데까지 왔다. 그런데 지금까지 그 일을 실행한 건 전부 사람이다.

배포하다 한 단계를 빠뜨렸다

글을 하나 쓰고 배포한다. 절차는 이렇다. 마크다운을 쓰고, 타입 검사를 돌리고, 빌드하고, 링크가 안 깨졌는지 확인하고, 결과물을 올린다.

금요일 저녁에 급하게 하나 고쳤다. 오타 하나였으니 검사는 건너뛰었다. 그런데 그 오타가 섹션 제목에 있었고, 다른 글이 그 제목의 앵커로 링크를 걸어두고 있었다.

배포는 성공했다. 화면도 멀쩡하다. 에러 로그도 없다. 그 링크를 누른 사람만 글 맨 위로 떨어진다. 아무도 모른다.

이 사고의 특징이 중요하다. 어렵지 않다. 그냥 한 단계를 빠뜨렸을 뿐이다. 그리고 사람이 하는 한 언젠가 반드시 빠뜨린다.

사람이 하는 절차는 조용히 어긋난다

이 시리즈에서 이미 같은 모양을 두 번 봤다.

  • README에 적은 세팅 절차는 코드와 함께 실행되지 않아서 조용히 낡았다.
  • 새벽에 죽은 컨테이너는 사람이 자고 있어서 아무도 안 살렸다.

배포 절차도 똑같다. 사람의 기억과 성실함에 기대는 단계는 평소엔 잘 되다가 가장 급할 때 빠진다. 급할 때 건너뛰는 게 검증이고, 급할 때가 사고가 나기 쉬울 때다.

그러니 답도 같다. 사람이 기억할 것을 기계가 실행하게 만든다. 그게 CI/CD다.

절차를 코드로 적으면 실행되는 문서가 된다

이 블로그는 GitHub Actions로 배포된다. 절차는 저장소 안 .github/workflows/deploy.yml에 적혀 있다. 사람이 읽는 문서가 아니라 기계가 실제로 실행하는 파일이다.

Dockerfile 때와 같은 성질이다. 코드와 같은 저장소에 있고, 매번 실행되고, 틀리면 실패한다. 낡을 수가 없다.

전체 흐름은 이렇게 생겼다.

diagramdiagram

여덟 단계인데, 하나하나가 “왜 여기 있나”에 답할 수 있어야 한다. 답할 수 없는 단계는 그냥 시간을 쓰는 단계다. 아래에서 그 답들을 본다.

방아쇠: 언제 도는가

먼저 언제 도는지부터다.

yaml
on:
  push:
    branches: [main]
  workflow_dispatch:

main에 push하면 돈다. 여기에 결정이 하나 들어 있다. 배포를 별도의 행위로 두지 않고 push에 붙였다는 것이다.

배포가 따로 있으면 “언제 배포하지”를 매번 사람이 정해야 하고, 그러면 배포가 뜸해지고, 뜸해지면 한 번에 나가는 변경이 커지고, 커지면 뭐가 문제를 일으켰는지 찾기 어려워진다. push에 붙이면 반대 방향으로 간다. 작게 자주 나간다.

대신 조건이 붙는다. main에 들어가는 것은 언제든 나가도 되는 상태여야 한다. 아직 내보내기 싫은 글은 브랜치를 따로 파는 대신 draft: true로 표시한다. 나가는 것과 안 나가는 것을 브랜치가 아니라 데이터로 가른다.

workflow_dispatch는 손잡이 하나를 남겨둔 것이다. 코드 변경 없이 다시 배포해야 할 때 버튼으로 돌린다.

검증이 배포 앞을 막는다

여덟 단계 중 셋이 검증이다.

단계무엇을 잡나
npm run check타입 오류, 그리고 글의 front matter가 스키마에 맞는지
npm run build빌드가 실제로 되는지
npm run check:links내부 링크·앵커·이미지가 실재하는지

순서가 중요하다. 이 셋은 배포 에 있다. 앞 단계가 실패하면 뒤로 못 간다.

diagramdiagram

이게 파이프라인의 값 절반이다. 검증이 배포와 이어져 있어야 게이트가 된다. 검증을 따로 돌리고 배포를 따로 하면, 검증을 안 돌린 채 배포하는 게 언제든 가능하다. 앞의 금요일 저녁이 정확히 그 경우였다.

그래서 원칙은 하나다. 검증에 실패한 것은 나갈 수 없다. 성공한 것만 나간다.

참고

check가 front matter까지 본다는 게 정적 블로그에서는 꽤 중요하다. 글의 시리즈 슬러그를 오타 내면 등록되지 않은 슬러그라 빌드가 깨진다. 일부러 그렇게 해뒀다. 안 그러면 오타 하나로 시리즈가 조용히 둘로 갈라지고, 아무도 에러를 안 본다.

왜 링크 검사가 따로 한 단계인가

check:links는 조금 특이하다. 타입 검사도 빌드도 아닌 것이 왜 게이트에 있나.

빌드가 이걸 안 잡기 때문이다.

앵커는 제목 글자에서 자동으로 만들어진다. ### 많이 읽을 거면이라는 제목은 #많이-읽을-거면이라는 앵커가 된다. 그런데 나중에 제목을 다듬으면 앵커가 바뀌고, 그 앵커로 걸어둔 링크는 조용히 깨진다.

깨진 결과가 무엇이냐가 핵심이다. 404가 아니다. 페이지는 정상적으로 뜨고, 그냥 원하던 위치가 아니라 맨 위로 떨어질 뿐이다. 에러도 없고 로그도 없다.

이런 종류의 문제에는 특징이 있다.

  • 사람이 놓치기 쉽다. 증상이 조용하고, 확인하려면 링크를 하나하나 눌러봐야 한다.
  • 기계가 잡기 쉽다. 산출물에서 링크를 모아 대상이 실재하는지 대조하면 끝이다.

파이프라인에 무엇을 넣을지 고를 때 이 두 조건이 좋은 기준이다. 사람이 놓치기 쉽고 기계가 잡기 쉬운 것을 기계에게 준다. 반대로 사람만 판단할 수 있는 것(글이 읽을 만한가)은 파이프라인이 대신해줄 수 없다.

CI와 CD는 무엇이 다른가

용어를 여기서 정리한다.

무엇답하는 질문
CI (지속적 통합)변경을 자주 합치고 합칠 때마다 검증한다이 변경이 기존 것을 깨뜨렸나
CD (지속적 전달/배포)검증을 통과한 것을 자동으로 내보낸다통과했으면 사람 손 없이 나갈 수 있나

CD의 D를 전달(Delivery)로 읽으면 “언제든 배포 가능한 상태로 만들어두되 나가는 버튼은 사람이 누른다”이고, 배포(Deployment)로 읽으면 “그 버튼까지 자동”이다. 이 블로그는 뒤쪽이다. push하면 끝까지 간다.

여기서 흔한 오해를 하나 짚는다. CI는 도구 이름이 아니다. GitHub Actions를 쓰면 CI를 하는 게 아니라, 자주 합치고 매번 검증하면 CI를 하는 것이다. 도구를 붙여놓고 검증을 아무것도 안 돌리면 그냥 자동 배포일 뿐이다.

로컬 검증과 CI 게이트는 역할이 다르다

이 저장소에는 CI와 같은 순서를 로컬에서 돌리는 명령이 있다.

bash
npm run verify   # check → build → check:links

같은 걸 두 군데서 돌리는 게 낭비 같지만, 역할이 다르다.

  • 로컬 verify - 미리 확인하는 수단. 빨리 알수록 고치기 쉽다.
  • CI - 게이트. 우회할 수 없는 마지막 문.

그리고 이 셋을 &&로 묶어둔 이유가 따로 있다. check:links빌드 산출물만 본다. 그래서 글을 고치고 빌드를 안 한 채 링크 검사만 돌리면, 낡은 산출물을 검사하고 조용히 통과한다. 셋을 묶어두면 그 실수를 할 수가 없다.

한편 이 저장소는 git 훅을 두지 않는다. 커밋할 때마다 자동으로 검증을 돌리는 방법이 있는데도 안 쓴다. 이유는 둘이다. 훅은 --no-verify 한 줄로 우회되니 게이트가 못 되고, 커밋마다 빌드를 돌리면 커밋이 부담스러워진다. 게이트는 우회할 수 없는 곳에 하나만 두고, 로컬은 편의로 남긴다.

실패하면 어떻게 되나

파이프라인을 만들 때 자주 안 따지는 게 이거다. 실패했을 때 무슨 일이 벌어지나.

아무 일도 안 벌어진다. 그게 정답이다.

검증이 실패하면 배포 단계에 도달하지 않고, 배포가 안 됐으니 이미 나가 있는 이전 버전이 그대로 서비스된다. 사용자는 아무것도 못 느낀다. 망가진 게 나가서 롤백하는 게 아니라, 애초에 안 나간다.

이 성질이 앞 글들과 이어진다. 나가는 것이 매번 새로 만들어진 완전한 결과물이면, 실패는 “새것이 안 나간 상태”일 뿐 “망가진 상태”가 아니다. 컨테이너를 고쳐 쓰지 않고 버리고 새로 만드는 것과 같은 태도다.

동시성도 한 줄 정해뒀다.

yaml
concurrency:
  group: pages
  cancel-in-progress: false

배포는 한 번에 하나만 돈다. 짧은 간격으로 두 번 push했을 때 두 배포가 겹치면 나중 것이 먼저 끝나 옛 버전이 최종본이 되는 일이 생길 수 있다. 그리고 진행 중인 배포는 취소하지 않는다. 배포를 중간에 끊는 게 끝까지 가는 것보다 위험하기 때문이다.

무엇을 파이프라인에 넣지 않는가

이 저장소의 파이프라인에는 일부러 빼둔 단계가 있다. 다이어그램 렌더링이다.

이 블로그의 mermaid 다이어그램은 빌드 전에 미리 SVG로 렌더해두고, 빌드는 그 파일을 찾아 끼워 넣기만 한다. 렌더하려면 브라우저를 띄워야 하는데, 그걸 파이프라인에 넣으면 매 배포마다 그 시간을 낸다. 그리고 같은 소스에서 매번 새로 그린 그림이 미세하게 달라지면, 바뀐 게 없는데도 산출물이 달라진다.

그래서 선택은 이렇게 됐다. 다이어그램은 로컬에서 렌더해서 결과물을 저장소에 커밋한다. CI는 커밋된 파일을 읽기만 한다.

여기엔 대가가 있고, 정직하게 적어두는 게 맞다. 다이어그램을 고치고 렌더를 안 돌리면 낡은 그림이 그대로 나간다. 이건 사람이 기억해야 하는 단계가 하나 남아 있다는 뜻이고, 이 글의 첫 장면과 같은 종류의 위험이다.

파이프라인 설계는 대개 이런 저울질이다.

넣으면빼면
사람이 안 잊는다파이프라인이 빠르고 단순하다
매번 시간과 자원을 쓴다사람이 기억해야 할 게 남는다

전부 넣는 게 정답은 아니다. 자주 틀리고 조용히 틀리는 것부터 넣는다. 링크 검사가 들어간 것도, 다이어그램이 빠진 것도 이 기준이었다.

실무에서 파이프라인이 커지면

이 블로그는 정적 사이트라 파이프라인이 짧다. 서버 앱이면 몇 단계가 더 붙는데, 붙는 위치는 늘 같다. 검증은 앞, 배포는 뒤.

붙는 단계
테스트 실행검증의 본체. 이 시리즈의 검사들보다 훨씬 앞자리
정적 분석·보안 검사취약한 의존성이나 새는 시크릿을 기계가 잡는다
이미지 빌드Docker 편의 그 이미지를 여기서 만든다
레지스트리 푸시만든 이미지에 태그를 붙여 창고에 올린다
배포쿠버네티스 편의 그 선언에서 이미지 태그를 바꾼다

마지막 줄이 이 시리즈가 한 바퀴 도는 지점이다. 선언적 상태를 쓰면 배포 단계가 하는 일은 “원하는 이미지 태그를 새것으로 고치는 것” 하나이고, 실제 교체는 조정 루프가 한다. 롤백도 태그를 되돌리는 같은 동작이다.

그 교체를 어떤 순서로 할지에는 이름 붙은 전략들이 있다. 새 버전을 옆에 다 띄워놓고 한 번에 트래픽을 넘기는 블루그린, 일부 트래픽만 새 버전에 보내며 지켜보는 카나리 같은 것들이다. 어느 쪽이든 전제는 같다. 버릴 수 있는 실행 단위와 재현 가능한 이미지가 있어야 성립한다. 이 시리즈가 여기까지 온 이유다. 각 전략이 무엇을 사고 무엇을 파는지는 무중단 배포 글에서 따로 다룬다.

그리고 파이프라인을 만들 때 계속 챙길 게 둘 있다.

빠르게 유지한다. 파이프라인이 느리면 사람이 기다리기 싫어서 우회할 길을 찾는다. 게이트는 통과하기 부담스러워지는 순간부터 게이트가 아니게 된다.

시크릿을 리포에 넣지 않는다. 이 저장소는 정적 사이트라 런타임 시크릿이 아예 없고, 배포 인증도 Actions가 자동으로 주입하는 토큰만 쓴다. 필요한 경우에도 값은 저장소 밖의 시크릿 저장소에 두고 실행할 때 주입한다. Docker 편에서 이미지에 시크릿을 넣지 않는 이유와 같다.

정리

문제배포 절차를 사람이 기억하면 가장 급할 때 한 단계가 빠진다
CI자주 합치고 합칠 때마다 검증한다
CD검증을 통과한 것을 사람 손 없이 내보낸다
게이트검증이 배포와 이어져 있어야 우회가 불가능하다
무엇을 자동화하나사람이 놓치기 쉽고 기계가 잡기 쉬운 것부터
실패하면아무 일도 안 일어난다. 이전 버전이 그대로 있다
다 넣지는 않는다자주 틀리고 조용히 틀리는 것부터. 나머지는 값을 따진다

이 시리즈는 “내 컴퓨터에선 되는데”라는 문장에서 출발했다. 그 문장을 없애는 방법은 결국 하나였다.

사람의 기억에 기대던 것을 하나씩 코드로 옮기는 것. 환경은 이미지로, 돌보는 일은 선언과 루프로, 앞단의 처리는 설정으로, 배포 절차는 워크플로 파일로 옮겼다. 옮긴 것들에는 공통점이 있다. 저장소 안에 있고, 실제로 실행되고, 틀리면 소리를 낸다.

자동화의 값은 시간을 아끼는 데 있지 않다. 틀렸을 때 조용하지 않게 만드는 데 있다.

관련 글