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