LabHub
배우기 러닝패스 코스

CI/CDパイプライン

シェルで作る3段階パイプライン

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

이 실습은 진짜 VM 에서 돕니다

이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이 따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라 진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가 되고 docker execdocker logs 도 그대로 동작합니다.

예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라 컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어 보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.

알아 둘 것이 둘 있습니다.

목표

셸 스크립트만으로 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/outapp-*.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 taglabhub/ci:latest 를 추가로 붙인다. 그리고 build-info.jsontags 배열을 넣어 labhub/ci:<해시12>labhub/ci:latest 두 값을 모두 기록한다(2개 이상).

참고

파이프라인 골격과 안전 옵션

/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/outapp-*.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 taglabhub/ci:latest 를 추가로 붙인다. 그리고 build-info.jsontags 배열을 넣어 labhub/ci:<해시12>labhub/ci:latest 두 값을 모두 기록한다(2개 이상).

docker tag 로 같은 이미지에 이름을 하나 더 붙입니다. 다시 빌드해서 latest 를 만들면 이미지 ID 가 달라져 실패합니다. build-info.json 의 tags 배열에 두 태그를 모두 기록하세요.