CI/CD 파이프라인 · 빌드 자동화 · 실습
셸로 만드는 3단계 파이프라인
이 실습은 진짜 VM 에서 돕니다
이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이
따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라
진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가
되고 docker exec 도 docker logs 도 그대로 동작합니다.
예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라
컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어
보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.
알아 둘 것이 둘 있습니다.
- 처음 뜨는 데 1분 남짓 걸립니다. VM 이 부팅하고 도커를 설치하기
- 브라우저 미리보기가 없습니다. VM 으로 들어오는 연결은 채점 포트
때문입니다. 파드 실습(보통 40초)보다 느립니다.
하나만 열려 있습니다. 웹 서버를 띄웠다면 VM 안에서 curl 로 확인하세요.
목표
셸 스크립트만으로 build → test → package 3단계 파이프라인을 만들고, 소스 해시로 이름 붙인 불변 아티팩트와 이미지 태그까지 직접 만들어 본다.
왜 중요한가
CI 도구는 몇 년마다 바뀌지만 원리는 바뀌지 않는다. 단계는 순서가 있고, 앞 단계가 실패하면 뒤 단계는 돌면 안 되며, 산출물은 무엇으로 만들었는지 이름만 보고 알 수 있어야 한다. 태그는 사람이 옮길 수 있는 이름표라서 tj-actions/changed-files 사건처럼 통째로 재지정될 수 있고, 그래서 배포 식별자는 커밋 SHA 처럼 변경 불가능한 값이어야 한다. 프로덕션에 latest 만 있으면 무엇이 배포됐는지 추적할 수 없고 되돌릴 대상도 없다. 이 실습은 그 원리를 벤더 UI 없이 손으로 재현한다.
단계
1. /root/ci1/pipeline.sh 를 만들고 chmod +x 로 실행 권한을 준다. 스크립트 앞부분에 set -euo pipefail 을 넣는다(채점은 set -e 계열, set -u, pipefail 을 각각 확인한다). 빌드 대상으로 /root/ci1/src/ 디렉터리를 만들고 파일을 2개 이상 넣는다.
2. 파이프라인은 각 단계 시작 때 ::stage build, ::stage test, ::stage package 를 이 순서로 한 번씩 출력하고, 끝나면 SUCCESS 를 출력한다. 성공한 실행의 출력을 /root/ci1/run1.log 에 저장한다.
3. package 단계는 /root/ci1/out/app-<해시>.tar.gz 를 정확히 1개 만든다. <해시> 는 cat /root/ci1/src/* | sha256sum | cut -c1-12 로 계산한 12자다. 파일은 tar tzf 로 열리는 진짜 gzip tar 여야 한다.
4. 같은 소스로 다시 실행하면 이미 있는 아티팩트를 재사용한다. 이때 ::artifact-exists 를 출력하고 그 실행 로그를 /root/ci1/run2.log 에 저장한다. /root/ci1/out 의 app-*.tar.gz 는 여전히 1개여야 한다.
5. /root/ci1/out/build-info.json 을 만든다. 필드는 source_hash(위 12자 해시), status(문자열 success), stages(숫자 3), created_at(시각 문자열)이다.
6. /root/ci1/tests/fail-flag 파일을 만들어 test 단계가 실패하게 하고 파이프라인을 다시 실행한다. 출력은 /root/ci1/fail.log, 종료코드는 /root/ci1/fail-exit.txt 에 저장한다(0 이 아니어야 한다). fail.log 에는 ::stage test 는 있고 ::stage package 는 없어야 한다. pipeline.sh 에는 || true 를 쓰지 않는다. 확인이 끝나면 /root/ci1/tests/fail-flag 를 지워 원상복구한다.
7. 아티팩트를 담은 이미지를 labhub/ci:<해시12> 태그로 빌드한다. 베이스 이미지는 파드에 미리 있는 alpine:3.20, busybox:1.36, python:3.12-alpine, nginx:1.27-alpine 중에서만 고른다.
8. 같은 이미지에 docker tag 로 labhub/ci:latest 를 추가로 붙인다. 그리고 build-info.json 에 tags 배열을 넣어 labhub/ci:<해시12> 와 labhub/ci:latest 두 값을 모두 기록한다(2개 이상).
참고
- 해시는 반드시 채점과 같은 방식으로 계산한다:
H=$(cat /root/ci1/src/* | sha256sum | cut -c1-12). 글롭 순서가 달라지면 값이 달라진다. - 3번 이후로
/root/ci1/src의 내용을 바꾸면 해시가 달라져 아티팩트가 2개가 된다. 소스를 고쳤다면 예전 아티팩트를 지우고 다시 만든다. - JSON 은 문자열을 손으로 이어 붙이기보다
jq -n --arg h "$H" '{source_hash:$h, status:"success", stages:3}'처럼 만드는 편이 안전하다. - 흔한 실수:
|| true로 실패를 삼키기(게이트가 아니게 된다), fail-flag 를 지우지 않고 끝내기(이후 채점이 계속 실패한다), latest 를 별도 빌드로 만들어 이미지 ID 가 달라지기.
단계 8개
- 파이프라인 골격과 안전 옵션
- build → test → package 순서
- 소스 해시로 이름 붙이기
- 같은 입력이면 다시 만들지 않기
- 빌드 메타데이터 남기기
- 실패는 삼키지 말고 멈춘다
- 커밋마다 바뀌는 태그로 이미지 굽기
- latest 는 별칭, 해시는 식별자