CI/CD Practice 05

buildspec.yml: CodeBuild 빌드 계약 해부하기

CodeBuild가 빌드한다는 말은 알겠지만, buildspec.yml의 phases와 artifacts가 실제로 어떤 입력을 어떤 출력으로 바꾸는지 흐릿하다.

근거 · CI/CD 교안 p68-p97, buildspec.yml, my-angular-repo.zip

buildspec.yml: CodeBuild 빌드 계약 해부하기 대표 이미지

1장: 빌드 계약서: CodeBuild에게 ‘무엇을’ 빌드할지 명시하기

솔라는 모니터에 떠 있는 buildspec.yml 파일을 꾹 눌러보듯 응시했다. 파일 안에는 install, build 같은 익숙한 단어 아래에 낯선 명령어들이 줄지어 있었다. 분명 AWS CodeBuild가 이 파일을 사용해 프로젝트를 빌드한다고 했는데, 아무리 봐도 그저 쉘 스크립트 파일을 좀 더 멋지게 포장한 것처럼 보였다.

“언니, 이것 좀 봐봐. buildspec.yml 파일인데… CodeBuild가 이걸 읽고 빌드를 수행한다는 건 알겠어. 근데 결국엔 그냥 이 파일에 적힌 명령어들을 위에서부터 순서대로 실행하는 거 아니야? 이게 그냥 터미널에서 명령어 치는 거랑 근본적으로 뭐가 다른 건지 잘 모르겠어.”

솔라의 말에 옆에서 코드를 보던 루나가 잠시 고개를 돌렸다. 솔라가 가리키는 화면을 잠시 들여다보던 루나는 대뜸 다른 질문을 던졌다.

“솔라, 혹시 package.json 파일 열어본 적 있어?”

“응? package.json? 자바스크립트 프로젝트 만들 때마다 보는데.”

“거기 scripts 부분 말고, 다른 항목들도 기억나? 예를 들면 name이나 version, dependencies 같은 것들.”

루나의 말에 솔라는 잠시 생각에 잠겼다. 늘 npm startnpm test를 실행하기 위해 scripts 항목만 들여다봤던 터였다. name은 프로젝트 이름을 정의하고, version은 말 그대로 버전을, dependenciesnpm install 할 때 설치될 라이브러리 목록을 담고 있었다.

“아… 생각해 보니까 dependencies는 ‘이런 라이브러리들이 필요하니 설치해라’고 명령하는 게 아니라, ‘이 프로젝트는 이런 라이브러리들에 의존한다’고 선언하는 거네. name이나 version도 ‘이름을 이걸로 바꿔라’는 명령이 아니라, ‘이 프로젝트의 이름은 이것이다’라고 정의하는 거고. 그냥 실행할 명령어들을 모아놓은 거랑은 느낌이 좀 다른데. 규칙이나… 명세서 같은 느낌?”

솔라가 스스로 내린 결론에 고개를 끄덕였다. 그러자 루나가 다시 모니터의 buildspec.yml 파일을 가리켰다.

“다시 이 파일을 볼까? 파일 제일 위에 있는 version: 0.2나, phases, artifacts 같은 큰 구조들을 봐봐. 이게 그냥 위에서 아래로 쭉 실행되는 평범한 스크립트 파일처럼 보여, 아니면 방금 네가 말한 package.json처럼 어떤 약속을 정해놓은 문서처럼 보여?”

루나의 손가락을 따라 솔라의 시선이 움직였다. commands: 아래에 들여쓰기 된 명령어들에만 꽂혀 있던 시야가 한 걸음 물러나 파일 전체의 구조를 비췄다. version, phases, artifacts. 이 단어들은 단순한 주석이나 꾸밈이 아니었다. 파일의 형식을 정의하고, 빌드 과정을 단계별로 나누고, 최종 결과물을 지정하는 명백한 구조였다.

그제야 솔라의 눈이 번쩍 뜨였다. 이것은 명령의 나열이 아니었다.

“그러네! 이건 그냥 명령어 묶음이 아니구나. ‘버전 0.2 규약에 따라, 정해진 단계(phases)들을 순서대로 거쳐서, 최종적으로 이런 결과물(artifacts)을 만들어내야 한다.’ 이렇게 CodeBuild와 약속하는 거였어. 단순히 ‘어떻게’ 빌드할지 방법을 알려주는 게 아니라, ‘무엇을’ 받아서 ‘무엇으로’ 만들지 구체적으로 명시하는 일종의… 빌드 계약서 같아!”

솔라의 입에서 ‘빌드 계약서’라는 말이 튀어나왔다. CodeBuild는 계약서에 적힌 대로, 약속된 입력물인 소스 코드 뭉치, 즉 ‘소스 아티팩트’를 받는다. 그리고 계약서 조항에 따라 정해진 절차를 거쳐 가공한 뒤, 최종 결과물인 ‘배포 가능한 아티팩트’를 내놓는 충실한 계약 이행자였던 것이다. buildspec.yml 파일은 그 모든 변환 과정의 구체적인 내용과 최종 산출물에 대한 약속을 담은 법적 효력을 지닌 문서나 다름없었다.

혼란스럽던 머릿속이 명쾌해지는 느낌이었다. buildspec.yml은 더 이상 흐릿한 명령어의 집합이 아니었다. 하지만 하나의 의문이 풀리자 곧바로 새로운, 더 구체적인 질문이 꼬리를 물고 이어졌다.

“좋아, 이게 소스 아티팩트를 배포 가능한 아티팩트로 바꾸는 ‘빌드 계약서’라는 건 알겠어. 그런데 이 계약서의 핵심 조항처럼 보이는 ‘phases’는 구체적으로 어떻게 이 변환 과정을 실행하는 거지? 그냥 순서대로 실행하는 거랑 도대체 뭐가 다른 걸까?“

2장: Phases: 계약 이행을 위한 단계별 ‘실행 지시’

솔라는 buildspec.yml 파일을 다시 화면에 띄웠다. ‘빌드 계약서’라는 개념을 깨달은 뒤로는 파일이 다르게 보였다. 하지만 솔라의 시선은 곧 phases라는 항목 아래에 들여쓰기 된 install, pre_build, build 같은 단어들에 다시 한번 가로막혔다. 계약서의 핵심 실행 조항인 것은 알겠는데, 왜 이렇게 단계를 잘게 나누어 놓았는지 그 의도가 여전히 모호했다.

그때 옆에 있던 루나가 아무 말 없이 텍스트 편집기를 하나 열었다. 그리고는 buildspec.yml 파일에서 commands: 아래에 있는 명령어들만 복사해서 순서대로 붙여넣기 시작했다.

# 그냥 명령어 목록
npm install -g @angular/cli@17
npm install
ng build -c production
echo "Build completed."
echo "This is the finally block execution!"

루나는 나란히 뜬 두 개의 창을 손가락으로 가리켰다. 왼쪽에는 구조를 갖춘 buildspec.yml 파일이, 오른쪽에는 그 안의 명령어들만 빼내어 늘어놓은 단순한 텍스트 파일이 있었다.

“솔라, 만약 phases라는 단계 구분이 별 의미가 없다면, 왼쪽의 계약서나 오른쪽의 명령어 목록이나 결국 같은 일을 해야 하지 않을까?”

솔라는 잠시 말문이 막혔다. “그렇지… 않을까? 어차피 순서대로 실행될 테니까. 오히려 phasescommands니 하는 구조가 더 거추장스러워 보이는데. 왜 굳이 이렇게 나눠야 하는 거야?”

솔라의 퉁명스러운 반문에 루나는 오른쪽의 명령어 목록에서 npm installng build -c production의 순서를 슬쩍 바꾸었다.

“그럼 이 순서로 실행하면 어떨까? 빌드를 먼저 하고, 그다음에 필요한 라이브러리를 설치하는 거야.”

“말도 안 돼! ng build를 하려면 당연히 npm install로 의존성 라이브러리들이 먼저 설치되어 있어야지. 빌드에 필요한 도구도 없는데 어떻게 빌드를 해.”

스스로 당연한 소리를 하고 나서야 솔라는 무언가 이상하다는 것을 깨달았다. 명령어 사이에는 분명히 눈에 보이지 않는 의존성과 순서가 존재했다. 루나는 빙긋 웃으며 원래의 buildspec.yml 파일을 가리켰다.

“바로 그거야. buildspec.ymlphases는 단순히 명령어들을 보기 좋게 묶어놓은 그룹이 아니야. 각 단계가 명확한 역할과 책임, 그리고 실행 순서를 갖는 단계별 실행 지시인 셈이지.”

루나는 실제 Angular 프로젝트의 buildspec.yml 예시를 보여주었다.

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 20
    commands:
      - echo "Installing Angular CLI..."
      - npm install -g @angular/cli@17
  pre_build:
    commands:
      - echo "Installing project dependencies..."
      - npm install
  build:
    commands:
      - echo "Starting Angular build..."
      - ng build -c production
      - echo "Build completed."
finally:
  commands:
    - echo "This is the finally block execution!"

“자, 이 계약서의 실행 지시를 차근차근 따라가 보자. 각 단계가 끝나면 빌드 환경의 상태가 어떻게 변할까?”

솔라는 루나의 말에 따라 파일의 각 단계를 손가락으로 짚어가며 상상하기 시작했다.

“음… 먼저 install 단계. 여긴 빌드 환경 자체를 설정하는 곳 같아. Node.js 버전을 20으로 맞추고, ng 명령어를 쓸 수 있도록 Angular CLI를 설치하네. 이 단계가 끝나면, 이 컴퓨터는 Angular 프로젝트를 빌드할 ‘기본 장비’를 갖추게 되는 거구나.”

“맞아. 그럼 pre_build 단계는?”

pre_build… ‘빌드 전’이라는 뜻이네. npm install을 실행하니까, 내 소스 코드에 명시된 모든 라이브러리(package.json에 있는)를 내려받아 node_modules 폴더를 만드는 단계야. 이제 빌드에 필요한 ‘재료’까지 모두 준비된 상태가 됐어.”

“그다음 build 단계는?”

“드디어 ng build -c production 명령어로 실제 빌드를 실행해서, 최적화된 웹사이트 파일들을 만들어내는 거지. dist 폴더 안에 결과물이 생기겠네. 이게 바로 소스 아티팩트를 배포 가능한 아티팩트로 변환하는 핵심적인 과정이구나!”

솔라의 목소리에 확신이 차기 시작했다. install, pre_build, build는 그저 임의로 나눈 이름표가 아니었다. 환경 설정, 의존성 설치, 실제 빌드라는 명확한 역할과 순서를 가진, 논리적으로 분리된 작업 단위였다. 만약 pre_build 단계의 npm install이 실패하면, CodeBuild는 굳이 build 단계를 실행하지 않고 빌드 실패를 알려줄 것이다. 각 단계는 이전 단계의 성공적인 완료를 전제로 이어지는 연쇄적인 과정이었다.

“이제 알겠다! phases는 그냥 명령어 묶음이 아니었어. ‘환경 준비 → 재료 준비 → 조립 및 가공’ 처럼, 소스 코드가 최종 제품으로 만들어지기까지의 생산 공정을 정의하는 거였어. 각 단계는 이전 단계의 결과물을 입력으로 받아서 다음 단계가 필요한 결과물을 만들어 넘겨주는, 컨베이어 벨트 위의 작업 구간 같은 거였구나.”

솔라는 이제 phases를 보면 단순히 명령어를 읽는 것이 아니라, 각 단계가 어떤 상태를 만들어 다음 단계로 넘겨주는지 그 흐름을 추적할 수 있게 되었다. ‘빌드 계약서’를 실제로 이행하는 구체적인 행동 지침을 읽어내는 단계별 명령 추적 능력이 생긴 것이다.

하지만 build 단계의 결과물을 생각하던 솔라의 머릿속에 새로운 질문이 떠올랐다.

“좋아, 이제 빌드 과정이 어떻게 흘러가는지는 알겠어. build 단계가 끝나면 dist 폴더에 배포할 파일들이 생기겠지. 그런데 빌드 로그도 터미널에 찍히고, 중간에 임시 파일도 생길 수 있잖아. 이 모든 결과물 중에서 CodeBuild는 대체 ‘어떤’ 파일이 우리가 원했던 최종 산출물, 즉 ‘배포 가능한 아티팩트’인지 어떻게 알고 콕 집어서 다음 단계로 넘겨주는 거지?“

3장: Artifacts: 배포 가능한 ‘최종 산출물 지정’ 및 전달

솔라는 build 단계의 마지막 줄까지 눈으로 좇았다. ‘Build completed.’라는 메시지가 선명했다. 머릿속에서는 ng build 명령어가 실행되며 수많은 파일이 생겨나는 과정이 그려졌다. 소스 코드가 dist 폴더 안에 담긴, 브라우저가 읽을 수 있는 파일들로 변환되는 생산 공정의 마지막 단계였다.

하지만 그와 동시에, 빌드 환경에는 다른 흔적들도 가득할 터였다. 빌드 과정을 기록한 로그, 의존성 라이브러리가 담긴 거대한 node_modules 폴더, 그 외에 생성되었을지 모를 임시 파일들까지. 작업이 끝난 공장 바닥에 제품과 함께 작업 도구, 부품 상자, 포장재가 뒤섞여 있는 모습과 비슷했다. 이 모든 것들 중에서 대체 무엇이 ‘진짜’ 최종 결과물이란 말인가.

솔라의 시선은 buildspec.yml 파일의 build 섹션 아래, 텅 빈 공간으로 향했다. 다음 단계로 넘겨줄 최종 산출물을 지정하는 조항이 이 계약서 어딘가에 반드시 있어야 했다. 그렇지 않으면 CodeBuild는 약속된 ‘배포 가능한 아티팩트’를 전달할 수 없게 된다.

“언니, 여기 좀 봐봐.”

솔라가 화면 한쪽을 가리키며 말했다. 그녀는 루나가 보여줬던 buildspec.yml 예제에서 artifacts 부분을 마우스로 선택해 블록 처리했다.

artifacts:
  base-directory: dist/my-angular-project
  files:
    - '**/*'

“이 artifacts 부분이 없다면 어떻게 될까? build 단계까지 잘 끝냈다고 쳐도, CodePipeline의 다음 단계인 Deploy는 뭘 받아야 할지 어떻게 알지? 빌드 서버에 남아있는 파일 전부를? 아니면 dist 폴더만? 그것도 아니면 ng build 명령어가 출력한 로그 파일?”

솔라의 질문은 혼란이 아니라, 명확한 가설을 세우고 빈틈을 찾아낸 탐정의 그것과 같았다. phases가 ‘어떻게’ 변환하는지에 대한 실행 지침이었다면, 그 결과물 중 ‘무엇을’ 최종본으로 인정할지 정하는 규칙이 빠져있다는 것을 정확히 짚어낸 것이다.

루나는 솔라가 블록 처리한 부분을 보며 조용히 고개를 끄덕였다.

“좋은 지적이야. 만약 그 부분이 정말 없다면 어떻게 될까? CodeBuild는 계약서에 명시된 대로 phases의 명령들을 성실히 수행하겠지. 빌드도 성공적으로 끝날 거야. 하지만 거기까지야. 계약서에 ‘최종 산출물은 이것이다’라고 명시된 조항이 없으니, CodeBuild는 무엇을 포장해서 다음 단계로 넘겨줘야 할지 알 수 없어. 결국 Deploy 단계는 아무것도 전달받지 못하고 파이프라인은 실패하게 돼.”

루나의 말에 솔라의 머릿속에 흩어져 있던 조각들이 맞춰졌다. artifacts는 빌드가 끝난 파일들을 단순히 모아두는 창고 같은 개념이 아니었다.

“아! 그럼 이건 ‘선언’이구나! 빌드 과정에서 생긴 수많은 결과물 중에서, 오직 여기에 명시된 파일들만이 다음 단계로 넘어갈 자격이 있는 ‘배포 가능한 아티팩트’라고 콕 집어서 지정해주는 거였어. 마치 수료증을 발급하는 것처럼!”

솔라는 흥분하며 artifacts 섹션을 다시 자세히 들여다보았다.

base-directory: dist/my-angular-project… 이건 ‘최종 산출물은 dist/my-angular-project 폴더 안에 있다’고 기준 위치를 알려주는 거고, files: '**/*'는 ‘그 폴더 안에 있는 모든 파일과 모든 하위 폴더를 전부 포함하라’는 뜻이네. 이 두 줄의 선언이 있기 때문에 CodeBuild는 비로소 뒤죽박죽 섞인 결과물 속에서 옥석을 가려낼 수 있는 거구나.”

그렇다. artifacts는 ‘빌드 계약서’의 마지막이자 가장 중요한 조항, 바로 최종 산출물 지정 조항이었다. 이 조항을 통해 CodeBuild는 자신의 임무(소스 아티팩트 → 배포 가능한 아티팩트 변환)를 완수했음을 증명하고, 그 결과물을 CodePipeline의 다음 주자에게 넘겨줄 수 있게 된다. Source 단계에서 들어온 소스 아티팩트가 Build 단계를 거치며 Build Output Artifact라는 이름의 새로운 아티팩트로 변환되어 Deploy 단계의 입력으로 전달되는, CI/CD 파이프라인의 핵심적인 연결고리가 바로 이 artifacts 섹션에 의해 정의되고 있었다.

솔라는 이제 buildspec.yml 파일을 처음부터 끝까지 온전히 이해할 수 있게 되었다. 이 파일은 더 이상 알 수 없는 명령어와 구조의 나열이 아니었다.

‘버전 0.2 규격에 따라(version), 빌드 환경을 준비하고(install), 소스 코드의 의존성을 설치한 뒤(pre_build), 실제 빌드를 실행하여(build), 최종적으로 dist/my-angular-project 폴더 안의 모든 파일을(artifacts) 배포 가능한 아티팩트로 인정한다.’

하나의 완결된 계약서였다.

“언니, 그럼 만약에 내가 아주 간단한 정적 웹사이트를 만들었다고 생각해 봐. 빌드 과정 같은 건 없고, 그냥 public 폴더 안에 index.html 파일이랑 style.css 파일만 있어. 이 프로젝트의 buildspec.yml을 만든다면 artifacts 부분은 어떻게 써야 할까?”

스스로 질문을 던진 솔라는 잠시 고민하더니, 망설임 없이 키보드를 두드려 새로운 artifacts 블록을 작성했다.

artifacts:
  base-directory: public
  files:
    - index.html
    - style.css

“이렇게 하면 되겠네! 혹은 그냥 files: '**/*' 라고 해도 되고. 이제 어떤 프로젝트를 만나든, 최종 결과물이 어디에 어떻게 담기는지만 알면 buildspec.yml 계약서의 마지막 도장을 찍을 수 있겠어.”

솔라의 얼굴에 환한 미소가 번졌다. 흐릿하기만 했던 빌드 과정의 내부가, 명확한 규칙과 약속으로 이루어진 투명한 계약으로 보이기 시작했다.