CI/CD Practice 06
Build 단계가 아티팩트 흐름을 바꾸는 이유
Build 단계를 추가한 뒤에도 pipeline 화면은 단계 몇 개처럼 보이지만, 실제로는 deploy가 더 이상 source zip을 그대로 받지 않는다는 점이 잘 드러나지 않는다.
근거 · CI/CD 교안 p83-p97
1장: Build 단계 없는 파이프라인: Deploy는 ‘원본 코드’를 배포한다
솔라의 손가락이 모니터 위 파이프라인 다이어그램의 한 지점을 맴돌았다. Source 단계에서 Deploy 단계로 곧장 이어지는 단순한 화살표. 그 옆에는 방금 읽은 실습 안내서의 문장이 떠올랐다. ‘기존 파이프라인에 CodeBuild 프로젝트를 Build 단계로 추가한다.’
솔라는 고개를 갸웃했다. ‘단계를 추가한다’는 말은 직관적으로 들렸다. 선 하나를 지우고 그 자리에 네모 상자 하나와 화살표 두 개를 그려 넣는 이미지. 하지만 그게 전부일까? 마음 한구석에서 무언가 석연치 않은 느낌이 들었다.
“언니, 이것 좀 봐봐.”
솔라의 부름에 옆에서 조용히 책을 읽던 루나가 고개를 들었다.
“파이프라인에 Build 단계를 넣는다는 게, 그냥 중간에 처리 과정 하나가 더 생기는 거 아냐? Source에서 코드를 받아서, Build가 뭔가 한 다음에, Deploy가 그걸 배포하는 거잖아. 어차피 Deploy 단계가 배포하는 건 내 코드인 건 똑같지 않아?”
솔라의 질문에는 ‘흐름의 본질은 바뀌지 않는다’는 확신이 섞여 있었다. Build라는 이름의 정거장 하나가 추가될 뿐, 출발지와 목적지 사이를 오가는 화물은 그대로라는 생각이었다.
루나는 솔라가 가리키는 화면을 잠시 들여다보더니, 이내 시선을 돌려 솔라의 책상 위에 놓인 빈 노트와 펜을 가리켰다.
“잠깐 그 파이프라인은 잊어보자. 우리가 직접 가장 단순한 파이프라인이 되어보는 거야. Build 단계가 아예 없는 걸로.”
“Build가 없는 파이프라인?” 솔라가 되물었다. “그럼 Source랑 Deploy만 있겠네.”
“맞아. 솔라, 네가 개발자야. 아주 간단한 웹사이트를 하나 만들어줘. 인사말을 보여주는 index.html 파일 하나랑, 글자색을 파랗게 만드는 style.css 파일 하나. 딱 두 개면 돼.”
루나의 말에 솔라는 신나서 키보드를 두드렸다. 금세 두 개의 파일이 만들어졌다.
index.html
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
style.css
h1 {
color: blue;
}
“자, 여기. 다 됐어.”
“좋아. 이제 파이프라인의 Source 단계 역할을 해보자.”
루나는 솔라가 만든 두 파일을 마우스로 선택하더니, 하나의 압축 파일로 묶었다. my-website.zip이라는 이름이 붙었다.
“Source 단계는 보통 이렇게 개발자가 작성한 코드들을 하나의 묶음으로 만들어서 다음 단계로 넘겨. 이 압축 파일이 바로 Source 단계의 결과물, 즉 ‘아티팩트(artifact)‘야.”
루나는 my-website.zip 파일을 화면 한쪽으로 끌어다 놓았다. 마치 컨베이어 벨트 위에 놓인 상자처럼 보였다.
“이제 Deploy 단계 차례네. 이 파이프라인의 목적지는 S3 웹사이트라고 가정하자. Deploy 단계는 저 zip 파일을 받아서 뭘 해야 할까?”
“음… 압축을 풀어서 S3 버킷에 파일들을 올려놓겠지?”
“바로 그거야.”
루나는 my-website.zip 파일의 압축을 풀었다. deployed-files라는 새 폴더 안에는 index.html과 style.css 파일이 나타났다. 솔라가 몇 분 전에 만들었던 원본과 글자 하나 다르지 않은 파일들이었다.
솔라는 잠시 두 폴더의 내용을 번갈아 보았다. 자신이 처음 코드를 작성했던 폴더와, ‘배포되었다’고 가정한 폴더. 그 안의 파일들은 완벽히 똑같았다.
“아…”
짧은 탄성과 함께 솔라의 표정이 바뀌었다.
“그러니까… Build가 없을 땐, Deploy 단계가 배포하는 건 그냥 내가 짠 ‘원본 코드’ 그 자체구나. zip 파일은 그냥 그걸 운반하기 위한 상자일 뿐이고.”
솔라는 방금 전까지 가졌던 모호한 생각이 선명해지는 것을 느꼈다. 파이프라인을 흐르는 ‘무언가’의 정체가 처음으로 명확하게 잡혔다. 그것은 가공되거나 변형된 무언가가 아니었다. 개발자의 손을 떠난 원본, 그 자체였다.
“맞아. 그 ‘원본 코드 묶음’을 우리는 특별히 SourceArtifact라고 불러. 개발자가 의도한 그대로의 소스 코드라는 의미를 담아서.”
루나의 말에 솔라가 고개를 끄덕였다. 이제 SourceArtifact라는 단어는 더 이상 추상적인 개념이 아니었다. 방금 직접 만들었던 my-website.zip 파일의 구체적인 이미지와 연결되었다.
“알겠어. SourceArtifact는 ‘원본 코드 묶음’이고, 가장 단순한 파이프라인에서 Deploy는 그 원본을 그대로 배포하는 역할을 하는구나.”
자신감이 붙은 솔라는 다시 원래의 질문으로 돌아갔다. 이제 퍼즐의 한 조각을 맞춘 기분이었다.
“좋아. 그럼 이제 Source와 Deploy 사이에 Build 단계를 넣어보자. Build는 SourceArtifact를 입력으로 받겠지. 그리고… 아마 CSS 파일을 압축하거나 자바스크립트 파일을 하나로 합치는 같은 일을 한 다음에 Deploy로 넘겨주는 거겠지? 그럼 결국 Deploy가 받는 건 여전히 내 코드, 그냥 조금 다듬어진 버전이겠네. 맞지?“
2장: Build 단계의 본질: ‘원본’을 ‘산출물’로 변환하다
솔라의 마지막 질문이 공중에 떠도는 동안, 루나는 대답 대신 행동을 택했다. 솔라가 방금 전 my-website.zip 파일을 만들었던 폴더로 마우스를 움직였다. 그리고는 main.js와 utility.js라는 두 개의 새 파일을 더 만들었다. 이전과 달리, 이번에는 파일 내용이 조금 더 복잡했다.
루나는 아무 말 없이 buildspec.yml이라는 이름의 파일 하나를 더 추가했다. 솔라는 파일 목록이 늘어나는 것을 그저 지켜볼 뿐이었다. 단순했던 웹사이트 프로젝트는 이제 제법 그럴듯한 개발 폴더처럼 보이기 시작했다. 이 변화된 풍경 자체가 이전과는 다른 무언가가 시작되었음을 암시했다.
“네 말이 맞아, 솔라. Build 단계는 SourceArtifact를 입력으로 받아.”
루나는 새로운 파일들이 추가된 폴더를 가리키며 입을 열었다.
“그리고 Deploy로 넘겨주는 것도 맞아. 하지만 ‘그냥 조금 다듬어진 버전’일까? 그게 정말 맞는지 확인해 보자.”
솔라는 루나가 새로 만든 파일들을 열어보았다.
utility.js
// 두 숫자를 더하는 간단한 함수
export const add = (a, b) => a + b;
main.js
// utility.js에서 add 함수를 가져와 사용
import { add } from './utility.js';
const result = add(5, 10);
document.body.innerHTML += `<p>5 + 10 = ${result}</p>`;
“어? import? export? 이거 그냥 브라우저에서 열면 에러 나지 않아?” 솔라가 날카롭게 지적했다. 최신 자바스크립트 문법인 모듈 시스템은 웹 서버나 별도의 설정 없이는 제대로 동작하지 않는 경우가 많았다.
“정확해. 그리고 Build 단계의 역할이 바로 여기서 시작돼.”
루나는 buildspec.yml 파일을 열어 그 내용을 보여주었다.
version: 0.2
phases:
install:
commands:
- echo "설치 단계 시작..."
- npm install
build:
commands:
- echo "빌드 단계 시작..."
- npm run build
artifacts:
files:
- '**/*'
base-directory: 'dist'
“이건 Build 단계의 ‘레시피’ 같은 거야. CodeBuild라는 요리사에게 우리가 만든 SourceArtifact를 어떻게 요리할지 알려주는 거지.”
루나는 설명을 이었다. install 단계에서 npm install 명령으로 필요한 도구(예: 코드를 하나로 합쳐주는 번들러)를 설치하고, build 단계에서 npm run build 명령으로 실제 변환 작업을 수행한다고 했다. 그리고 가장 중요한 artifacts 섹션. 여기서 Build 단계가 최종 결과물로 무엇을 포장할지 정의했다. 주목할 점은 base-directory: 'dist' 라는 부분이었다.
“자, 이제 Build 단계를 실행해 볼까?”
루나는 이 모든 파일들(index.html, style.css, main.js, utility.js, 관련 설정 파일들)을 source-v2.zip이라는 이름으로 압축했다. 이것이 새로운 SourceArtifact였다.
그리고 루나는 마치 CodeBuild가 된 것처럼, buildspec.yml에 적힌 명령어를 순서대로 실행하는 시늉을 했다. 잠시 후, dist라는 이름의 새로운 폴더가 생겨났다. 솔라는 호기심에 가득 차 그 폴더를 열었다.
안에는 index.html 파일과 bundle.js라는 처음 보는 파일 단 두 개만 들어 있었다. main.js와 utility.js는 보이지 않았다. 솔라가 bundle.js 파일을 열자, 화면에는 알아보기 힘든 기계적인 코드가 가득 찼다. 하지만 자세히 살펴보니 add 함수와 5 + 10 같은 익숙한 조각들이 뒤섞여 있었다.
“이건…”
솔라는 순간 할 말을 잃었다. 이것은 자신이 작성한 코드를 ‘다듬은’ 수준이 아니었다. 여러 개였던 자바스크립트 파일은 하나의 파일로 합쳐지고, 최신 문법은 모든 브라우저가 이해할 수 있는 옛날 방식으로 바뀌어 있었다. 원본의 형태는 거의 남아있지 않았다.
루나는 dist 폴더를 build-output.zip이라는 이름으로 압축했다.
“솔라, 이제 두 개의 아티팩트를 비교해 봐.”
루나는 source-v2.zip과 build-output.zip을 나란히 놓았다.
| 아티팩트 종류 | source-v2.zip (SourceArtifact) | build-output.zip (BuildArtifact) |
|---|---|---|
| 내용물 | index.html, style.css, main.js, utility.js, buildspec.yml, package.json… | index.html, bundle.js |
| 목적 | 개발자가 작성한 원본 코드. 빌드 지침 포함. | 브라우저에서 즉시 실행 가능한 실행 산출물. |
| 가독성 | 사람이 읽고 수정하기 쉬움. | 기계가 처리하기에 최적화됨. |
표를 물끄러미 보던 솔라의 눈이 커졌다.
“완전히 다른 물건이잖아! 왼쪽은 요리를 위한 ‘재료와 레시피’ 묶음이고, 오른쪽은 그걸로 다 만들어진 ‘완성된 요리’네. 이건 단순한 수정이 아니라… ‘변환’이야.”
솔라는 마침내 Build 단계의 본질을 깨달았다. Build는 단순히 파일을 정리하거나 압축하는 보조 역할이 아니었다. 개발자가 작성한 ‘원본(Source)‘이라는 한 종류의 아티팩트를 입력받아, 서버에 배포되어 최종 사용자가 소비할 수 있는 ‘산출물(Build)‘이라는 전혀 다른 종류의 아티팩트를 만들어내는 핵심적인 변환 단계였던 것이다. 흐름의 중간에 끼어든 것이 아니라, 흐름 자체를 바꾸고 있었다.
“맞아. 그래서 우리는 이 두 아티팩트를 구분해서 불러. 하나는 여전히 SourceArtifact지만, Build 단계를 거쳐 나온 새로운 결과물은 BuildArtifact라고 해.”
솔라가 고개를 주억거렸다. 머릿속에서 파이프라인의 그림이 재구성되었다. Source에서 나온 아티팩트와 Build에서 나온 아티팩트는 이름만 다른 게 아니라, 태생과 목적 자체가 달랐다.
“알겠어. Build 단계는 원본 코드를 실행 가능한 산출물로 바꾸는 공장이구나. 그렇다면 Deploy 단계는 이제 당연히 이 완성품, BuildArtifact를 받아서 배포해야겠네. 그게 훨씬 효율적이니까. 뭐, 그래도 원한다면 Deploy 단계가 SourceArtifact를 직접 받도록 설정할 수도 있는 거지?“
3장: Deploy의 새로운 역할: 오직 ‘빌드 산출물’만 배포한다
솔라의 질문이 남긴 여운 위로, 루나는 조용히 화면을 바꿨다. 이전 장에서 비교했던 두 개의 압축 파일, source-v2.zip과 build-output.zip이 나란히 떠 있었다. 그 옆에는 파이프라인의 설정을 흉내 낸 아주 간단한 텍스트 상자가 나타났다. 마치 빈칸 채우기 문제처럼 보였다.
이 새로운 풍경은 솔라의 마지막 질문—Deploy 단계가 여전히 SourceArtifact를 받을 수 있지 않냐는 그 가능성에 대한—을 말없이 받아치고 있었다. 루나는 말 대신, 솔라가 자신의 생각을 직접 시험해볼 판을 깔아준 것이다.
Deploy Stage Configuration
--------------------------
Input Artifact: [ ]
루나는 텍스트 상자의 빈칸을 가리켰다.
“솔라, 네가 이 파이프라인의 Deploy 단계를 설정하는 사람이야. 입력으로 받을 아티팩트를 저 둘 중 하나로 지정해야 해. source-v2.zip을 가리키는 SourceArtifact를 선택할 수도 있고, build-output.zip을 가리키는 BuildArtifact를 선택할 수도 있겠지.”
“음… BuildArtifact를 쓰는 게 당연히 맞지. 이미 다 준비된 ‘완성된 요리’니까. 하지만 내 질문은… 원한다면 SourceArtifact를 선택해도 되냐는 거였어. 기술적으로 불가능한 건 아니잖아?”
솔라는 자신의 논리에 자신이 있었다. 설정 값 하나만 바꾸면 될 문제라고 생각했다.
“좋은 가설이야. 직접 해보는 것만큼 확실한 건 없지.”
루나는 파이프라인 설정 화면을 열었다. 그리고 솔라가 보는 앞에서 Deploy 단계의 입력 아티팩트를 BuildArtifact가 아닌, Source 단계의 결과물인 SourceArtifact로 변경했다. 마치 솔라의 가설을 그대로 실행에 옮기는 것 같았다.
“자, 이제 파이프라인을 다시 실행해 보자. Source 단계는 성공적으로 source-v2.zip을 만들었어. Build 단계도 성공적으로 build-output.zip을 만들었지. 이제 Deploy 단계가 Source 단계의 source-v2.zip을 받아서 배포할 차례야.”
솔라는 숨을 죽이고 화면을 지켜보았다. 잠시 후, Deploy 단계 옆에 빨간색 ‘실패’ 아이콘이 떴다.
“어? 왜 실패했지?”
솔라는 당황했다. 루나는 아무 말 없이 실패 로그를 화면에 띄워주었다. 복잡한 메시지 속에서 솔라의 눈에 익숙한 단어들이 들어왔다.
Deployment failed. Cannot find file bundle.js in the package.
bundle.js. 솔라는 그 파일 이름을 기억했다. 그건 Build 단계가 만들어낸 결과물, build-output.zip 안에만 들어있던 파일이었다. 당연히 원본 코드 묶음인 source-v2.zip 안에는 존재하지 않았다.
“아…”
솔라는 두 개의 아티팩트 내용을 다시 떠올렸다.
| 아티팩트 종류 | source-v2.zip (SourceArtifact) | build-output.zip (BuildArtifact) |
|---|---|---|
| 내용물 | main.js, utility.js 등 원본 파일 | bundle.js 등 실행 파일 |
실패의 원인은 명확했다. Deploy 단계는 bundle.js를 배포하도록 설정되어 있었지만, 정작 입력으로 받은 source-v2.zip에는 그 파일이 없었다. Deploy 단계는 자신이 기대했던 ‘완성된 요리’가 아니라 엉뚱한 ‘재료 묶음’이 들어오자 작업을 거부한 것이다.
“이건… 선택의 문제가 아니었구나.”
솔라가 중얼거렸다.
“Deploy 단계는 Build 단계가 만들어 낼 ‘결과물’의 형태를 미리 약속하고 기다리고 있었던 거야. 그래서 약속과 다른 형태의 아티팩트가 오니까, 아예 작업을 못 하는 거였어. 흐름이 강제되는 거네.”
파이프라인의 각 단계는 독립적으로 움직이는 부품이 아니었다. 앞 단계의 출력을 다음 단계가 입력으로 받는, 단단한 계약 관계로 묶여 있었다. Build 단계를 추가하는 순간, 파이프라인은 ‘이제부터 Deploy는 BuildArtifact만을 받는다’는 새로운 규칙을 선언한 것과 같았다. SourceArtifact가 Deploy 단계로 갈 수 있는 길은 원천적으로 차단된 것이었다.
“바로 그거야. Build 단계를 추가하는 것은 파이프라인을 흐르는 아티팩트의 종류를 근본적으로 바꾸는 일이야. 그리고 그 순간, Deploy 단계의 역할도 변해. 더 이상 ‘원본 코드’를 다루지 않아. 오직 약속된 ‘빌드 산출물’만 인식하고 배포하는 전문적인 단계가 되는 거지.”
솔라는 이제 완전히 이해했다. 고개를 끄덕이며 펜을 들었다. 그리고 노트 위에 파이프라인의 흐름을 다시 그리기 시작했다.
-
Source →
[SourceArtifact]→ Deploy (Deploy는 SourceArtifact의 구조를 이해하고 처리하도록 설정됨) -
Source →
[SourceArtifact]→ Build →[BuildArtifact]→ Deploy (Deploy는 오직 BuildArtifact의 구조만 처리하도록 강제됨. SourceArtifact로 가는 길은 없음!)
자신이 그린 두 번째 다이어그램의 SourceArtifact에서 Deploy로 가는 가상의 화살표 위에, 솔라는 단호하게 X 표시를 그었다. 이제 그녀에게 파이프라인의 각 단계는 단순히 화면 위에 놓인 상자가 아니었다. 어떤 종류의 아티팩트를 입력으로 받아 처리하는지, 그 역할과 책임이 명확하게 정의된 존재로 보였다. Build 단계의 추가가 단순한 ‘기능 추가’가 아니라, 파이프라인의 핵심적인 아티팩트 흐름을 바꾸는 ‘구조적 변화’라는 사실을 마침내 체감한 순간이었다.