LabHub
배우기 러닝패스 코스

CI/CD 파이프라인 · 아티팩트와 승격 · 실습

태그는 그대로인데 운영에 다른 이미지가 올라갔다

LabHub 에서 이어서 보기

목표

한 번 만든 산출물을 다시 만들지 않고 환경 사이로 옮기는 일을 손으로 합니다. 다이제스트로 지목해 승격하고, 태그가 움직인 것을 잡아내는 검사기와 기록에 없는 것을 막는 게이트를 세우고, 되돌리기를 옛 다이제스트의 재승격으로 실행하고, 어디에 무엇이 있는지 한 파일로 남깁니다.

왜 중요한가

환경마다 다시 빌드하면 환경마다 다른 것이 나옵니다. 베이스 이미지 태그가 움직이고, 의존성의 패치가 올라가고, 빌드 기계의 도구 판이 다릅니다. 그래서 개발계에서 통과한 시험은 운영 산출물에 대해서는 아무것도 말해 주지 않습니다 — 시험한 것과 배포한 것이 다른 물건이기 때문입니다. 한 번 만들고 그것을 옮기기만 하면 이 변수는 사라지는데, 그 원칙은 무엇을 옮기는지 가리키는 이름에서 새어 나갑니다. 태그는 사람이 붙인 이름표라 언제든 다른 것을 가리키게 만들 수 있고, 그 순간 어제 검증한 것과 오늘 배포되는 것이 갈라집니다. 다이제스트는 내용에서 계산된 주소라 옮겨지지 않습니다. 그래서 현장의 규칙은 하나로 모입니다 — 태그는 읽기 위해, 다이제스트는 지목하기 위해. 되돌리기도 같은 규칙 위에 섭니다. 옛 다이제스트가 레지스트리에 남아 있기만 하면 되돌리기는 다시 빌드하는 일이 아니라 옛 주소를 다시 가리키는 일이 되고, 되돌린 것이 예전과 같은 물건이라는 것을 바이트로 증명할 수 있습니다.

단계

1. /root/promote/app 에 연습용 git 저장소를 만드세요 — 파일 main.py, orders.conf, README.md 를 두고 커밋을 두 개 이상 쌓은 뒤 마지막 커밋에 주석 없는 태그 v1.2.0 을 답니다. 그다음 /root/promote/release-name.sh <저장소경로> 를 만드세요. 저장소에서 판 이름 한 줄을 뽑아 표준출력에 내는 스크립트입니다. 규칙은 <가장 가까운 태그>-<그 태그 이후 커밋 수>-g<커밋 7자리> 이고, 커밋되지 않은 변경이 있으면 끝에 -dirty 를 붙입니다. 태그가 하나도 없는 저장소면 태그 자리에 v0.0.0 을, 커밋 수 자리에 전체 커밋 수를 씁니다. git 저장소가 아닌 경로를 받으면 표준출력에 아무것도 내지 말고 0 이 아닌 코드로 끝냅니다. 실행 권한을 주고 /root/promote/app 에 돌린 결과를 /root/promote/release.txt 에 한 줄로 저장하세요.
2. 환경별 레지스트리를 OCI 이미지 레이아웃 디렉터리로 흉내 냅니다. /root/promote/reg 아래에 dev·staging·prod 를 둡니다. 빌드 기계가 이번 판의 산출물로 내놓은 것이 /opt/images/busybox_1.36.tar(oci-archive) 라고 봅니다. 이것을 개발계 레이아웃 /root/promote/reg/dev판 이름을 태그로 달아 한 번만 넣으세요(판 이름은 /root/promote/release.txt 의 값입니다). 그다음 /root/promote/resolve.sh <레이아웃> <태그> 를 만드세요 — 그 태그가 지금 가리키는 다이제스트를 sha256:<64자리 16진수> 한 줄로 내고 0 으로 끝냅니다. 그런 태그가 없으면 표준출력에 아무것도 내지 말고 3 으로, 레이아웃이 아닌 경로를 받으면 2 로 끝냅니다. 실행 권한을 준 뒤 그 다이제스트를 /root/promote/dev-digest.txt 에 한 줄로 저장하세요. 마지막으로 /root/promote/releases.tsv 를 만들고 첫 줄에 <다이제스트><탭><판이름><탭><커밋40자리> 를 적으세요. 커밋은 그 판을 만든 커밋의 전체 해시입니다.
3. 누군가 같은 판 이름 태그로 다른 산출물을 개발계에 밀어 넣은 상황을 만듭니다. /opt/images/nginx_1.27-alpine.tar/root/promote/reg/dev같은 태그(판 이름)로 복사하세요. 그다음 /root/promote/tag-moved.txt 에 정확히 세 줄을 적으세요 — 첫 줄 before <다이제스트> 는 밀어 넣기 전에 그 태그가 가리키던 값, 둘째 줄 after <다이제스트> 는 지금 가리키는 값, 셋째 줄 orphan yes 또는 orphan no옛 다이제스트의 항목이 index.json 에 아직 남아 있는지입니다 (이름표를 잃었어도 항목과 블롭이 남아 있으면 yes). 값은 외워 적지 말고 그 자리에서 확인해 적으세요.
4. /root/promote/promote.sh <원본레이아웃> <대상레이아웃> <다이제스트> <대상태그> 를 만드세요. 원본에서 그 다이제스트를 찾아 대상 레이아웃에 그 태그로 올립니다. 다시 만들지 않습니다 — 옮긴 뒤 대상의 매니페스트 바이트가 원본과 한 바이트도 달라서는 안 됩니다. 원본에 그 다이제스트의 항목이나 블롭이 없으면 첫 줄에 NOTFOUND <다이제스트> 를 내고 3 으로, 성공하면 PROMOTED <다이제스트> 를 내고 0 으로, 그 밖의 잘못(원본이 레이아웃이 아니다, 다이제스트 꼴이 아니다, 복사가 실패했다)에는 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 대상 레이아웃이 아직 없으면 만들어야 하고, 이름표가 없는(태그를 잃은) 다이제스트도 옮길 수 있어야 합니다. 실행 권한을 준 뒤 /root/promote/dev-digest.txt 의 다이제스트를 /root/promote/reg/dev 에서 /root/promote/reg/staging 으로 판 이름 태그를 달아 옮기고, 옮긴 뒤 스테이징에서 다시 읽은 다이제스트를 /root/promote/staging-digest.txt 에 한 줄로 저장하세요. 개발계에 적어 둔 값과 같아야 합니다.
5. /root/promote/tag-drift.sh <레이아웃> <태그> <기대다이제스트> 를 만드세요. 그 태그가 지금 가리키는 값을 기대값과 대조합니다. 같으면 OK <다이제스트> 를 내고 0 으로, 다르면 MOVED <기대> <실제> 를 내고 3 으로, 그런 태그가 아예 없으면 MISSING <태그> 를 내고 4 로, 레이아웃이 아닌 경로면 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 /root/promote/dev-digest.txt 를 기대값으로 삼아 개발계에 돌린 결과를 /root/promote/drift-dev.txt 에, 스테이징에 돌린 결과를 /root/promote/drift-staging.txt 에 저장하세요. 개발계는 MOVED, 스테이징은 OK 가 나와야 합니다.
6. /root/promote/approved.txt 를 만들고 스테이징에서 확인을 마친 다이제스트(개발계에 적어 둔 그 값)를 한 줄로 적으세요. # 로 시작하는 주석 줄과 빈 줄은 있어도 됩니다. 그다음 /root/promote/promote-gate.sh <승인목록> <원본레이아웃> <대상레이아웃> <다이제스트> <대상태그> 를 만드세요. 승인 목록에 그 다이제스트가 한 줄 전체로 적혀 있을 때만 승격하고, 없으면 REFUSED <다이제스트> 를 내고 5 로 끝냅니다 — 이때 대상 레이아웃은 한 바이트도 달라지면 안 됩니다. 승격은 앞 단계의 promote.sh 에 넘기고 그 출력과 종료 코드를 그대로 전합니다(PROMOTED 0 · NOTFOUND 3 · ERROR 2). 승인 목록 파일이 없으면 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 실행 권한을 준 뒤 승인된 다이제스트를 /root/promote/reg/staging 에서 /root/promote/reg/prod 로 태그 current 를 달아 승격하세요. 마지막으로 개발계의 태그가 지금 가리키는(승인되지 않은) 다이제스트로 같은 승격을 시도해 그 출력을 /root/promote/gate-refused.txt 에 저장하세요. 첫 줄은 REFUSED 로 시작해야 합니다.
7. 새 판을 하나 더 내고 되돌려 봅니다. /root/promote/app 에 커밋을 하나 더 쌓고 태그 v1.3.0 을 단 뒤, 새 판 이름을 release-name.sh 로 뽑으세요. /opt/images/alpine_3.20.tar 를 그 판의 산출물로 보고 /root/promote/reg/dev 에 새 판 이름 태그로 넣고, /root/promote/releases.tsv 에 둘째 줄을 같은 형식으로 덧붙이고, /root/promote/approved.txt 에도 그 다이제스트를 한 줄 덧붙이세요. 그다음 게이트로 새 판을 /root/promote/reg/dev 에서 /root/promote/reg/prod 의 태그 current 로 승격하고, 바로 이어서 옛 판의 다이제스트를 같은 태그로 다시 승격해 되돌리세요. 마지막으로 /root/promote/rollback.txt 에 세 줄을 적으세요 — before <되돌리기 직전 current 가 가리키던 다이제스트>, after <되돌린 뒤의 다이제스트>, 그리고 identical yes 또는 identical no(되돌린 매니페스트의 바이트가 개발계에 있는 옛 판의 바이트와 같은지 cmp 로 확인한 결과).
8. /root/promote/ledger.sh <레지스트리루트> <릴리스표> <출력파일> 을 만드세요. 레지스트리 루트 아래의 각 디렉터리를 환경 하나로 보고(그 안에 index.json 이 있는 것만), 이름표가 붙은 항목만 모아 탭으로 나눈 표를 출력 파일에 씁니다. 첫 줄은 머리글 env<탭>tag<탭>digest<탭>release<탭>commit 이고, 이어지는 줄은 <환경><탭><태그><탭><다이제스트><탭><판이름><탭><커밋> 입니다. 판 이름과 커밋은 릴리스 표에서 다이제스트로 찾고, 없으면 두 칸 모두 - 를 씁니다. 본문 줄은 LC_ALL=C sort 로 환경·태그·다이제스트 순서로 정렬합니다. 표준출력에는 LEDGER <본문 줄 수> 한 줄을 내고 0 으로 끝냅니다. 루트가 없거나 릴리스 표가 없으면 ERROR 로 시작하는 줄을 내고 2 로 끝냅니다. 출력 파일의 상위 디렉터리가 없으면 만듭니다. 실행 권한을 준 뒤 /root/promote/reg/root/promote/releases.tsv 로 돌려 /root/promote/ledger.tsv 를 남기세요.

참고

단계 8개

  1. 판 이름을 사람이 짓기 시작하자 어느 커밋인지 아무도 몰랐다
  2. 산출물을 한 번만 만들고 그 다이제스트를 적어 두었다
  3. 같은 태그에 다른 이미지를 밀어 넣자 이름표만 옮겨 갔다
  4. 태그가 아니라 다이제스트를 지목해 스테이징으로 옮겼다
  5. 검사기를 붙이자 개발계의 태그가 움직인 것이 드러났다
  6. 기록에 없는 다이제스트가 운영 문턱에서 멈춰 섰다
  7. 되돌리기는 다시 빌드하는 일이 아니라 옛 다이제스트를 다시 승격하는 일이었다
  8. 어디에 무엇이 떠 있는지 한 파일로 답하게 만들었다