아티팩트 저장소
고친 사람 github-actions[bot]
아티팩트 저장소는 빌드가 만들어 낸 결과물을 받아 두었다가 필요한 쪽에 다시 내주는 서버입니다. 결과물마다 이름과 버전을 붙여 두고 그 둘로 찾습니다. 빌드하는 쪽과 배포하는 쪽은 파일을 직접 주고받지 않고 이곳을 거칩니다.
쉽고 빠른 이해
어느 팀 빌드가 만든 파일 하나를 올려 두면, 다른 팀 빌드와 배포 도구가 그것을 가져다 씁니다. 자바 프로젝트를 빌드해서 나온 압축 파일 하나, 컨테이너 이미지 한 장이 그런 파일입니다.
이것이 없으면 같은 코드를 배포할 때마다 다시 빌드하게 됩니다. 그러면 검증을 통과한 파일과 운영에 올라간 파일이 같은 파일이라는 보장이 사라집니다.
- 빌드가 끝나면 결과물에 이름과 버전을 붙여 올립니다
- 저장소는 그 버전을 덮어쓰지 않고 보관합니다
- 배포하는 쪽과 다른 빌드는 같은 이름과 버전으로 내려받습니다
대가는 저장소가 팀 전체의 길목이 된다는 것입니다. 저장소가 멈추면 빌드와 배포가 같이 멈춥니다. 쌓이는 파일을 언제 지울지도 따로 정해야 합니다. 빌드한 기계가 곧 배포하는 기계인 작은 규모면 안 두기도 합니다.
상세
회사 창고를 떠올려 봅니다. 공장에서 나온 물건에 품번을 붙여 선반에 올려 두면, 가져가는 사람은 품번만 대고 받아 갑니다. 창고는 물건을 내주면 재고가 줄지만, 아티팩트 저장소는 몇 번을 내줘도 원본이 남습니다.
아티팩트 저장소는 빌드 결과물을 이름과 버전으로 보관하고 내주는 서버입니다. 자바 프로젝트를 빌드해서 나온 파일 하나, 파이썬 패키지 하나, 컨테이너 이미지 한 장이 그 결과물입니다.
아티팩트에 드는 산출물
아티팩트는 빌드가 소스 코드로부터 찍어 낸 결과물입니다. 무엇이 아티팩트인지는 언어와 배포 방식이 정합니다.
자바면 클래스 파일을 묶은 압축 파일입니다. 파이썬이면 배포용 패키지 파일입니다. 컨테이너로 배포하면 이미지 한 장입니다.
셋의 공통점은 소스 코드가 아니라는 것입니다. 소스는 버전관리가 맡습니다. 소스에서 나온 결과물은 이 저장소가 맡습니다.
둘을 가르는 까닭은 성질이 다르기 때문입니다. 소스는 한 줄씩 바뀐 곳을 비교해 쌓습니다. 결과물은 통짜 파일이라 비교할 것이 없습니다. 크기만 큽니다.
이름과 버전으로 정해지는 주소
저장소에 올린 파일은 디렉터리 경로가 아니라 이름과 버전으로 찾습니다. 이 둘을 묶어 적은 것을 좌표라고 부릅니다.
example:billing:1.4.2 // 요청이 적는 좌표
→ billing-1.4.2.jar // 저장소가 내주는 파일
윗줄은 받아 가는 쪽이 적는 값입니다. 아랫줄은 저장소가 돌려주는 파일입니다. 받아 가는 쪽은 그 파일이 어느 기계 어느 디렉터리에 놓여 있는지 몰라도 됩니다.
내 빌드가 가져다 쓰는 남의 파일, 곧 의존성을 좌표로 적어 둘 수 있습니다. 빌드 설정 파일에 좌표만 적어 두면 빌드 도구가 저장소에서 그 파일을 찾아 받아 옵니다.
빌드를 한 번만 하는 까닭
같은 코드라도 빌드할 때마다 결과 파일이 달라질 수 있습니다. 컴파일러 버전, 딸려 들어간 라이브러리, 빌드한 기계의 환경이 조금씩 다르기 때문입니다.
그래서 검증한 파일과 배포한 파일이 갈릴 위험이 생깁니다. 배포 전에 시험을 돌려 보는 곳을 검증 환경이라고 부릅니다. 검증 환경에서 통과시킨 파일을 운영에 올리려고 다시 빌드하면, 운영에 올라가는 것은 검증을 안 거친 파일입니다.
아티팩트 저장소는 이 위험을 없앱니다. 빌드는 한 번만 합니다. 그 결과물을 저장소에 올리면 검증 환경도 운영 환경도 같은 좌표로 같은 파일을 받아 갑니다. 지속적 통합과 지속적 배포가 이 저장소를 사이에 두고 이어집니다.
올릴 때와 내려받을 때
올리는 쪽과 내려받는 쪽이 저장소를 사이에 두고 어떻게 갈리는지를 그림으로 봅니다.
flowchart TD
C["버전관리"] --> B["빌드"]
B -->|"올린다"| R["아티팩트 저장소"]
R -->|"내려받는다"| S["검증 환경"]
R -->|"내려받는다"| P["운영 환경"]
R -->|"내려받는다"| O["다른 팀 빌드"]
화살표가 저장소 하나로 모였다가 다시 갈라집니다. 올리는 쪽은 빌드 하나뿐입니다. 내려받는 쪽은 여럿입니다.
올리기는 대개 빌드 파이프라인만 할 수 있게 막아 둡니다. 사람이 자기 노트북에서 만든 파일을 올릴 수 있으면, 저장소 안의 파일이 어느 소스에서 나왔는지 아무도 대답할 수 없게 됩니다.
내려받기는 반대로 넓게 엽니다. 다른 팀 빌드, 배포 도구, 운영 서버가 모두 같은 주소를 바라봅니다.
덮어쓰지 않는 버전
저장소가 지키는 약속 가운데 첫째는 올린 버전을 덮어쓰지 않는 것입니다. 같은 좌표로 두 번 요청하면 언제나 같은 내용이 와야 합니다.
덮어쓰기를 허용하면 어제 통과한 검증이 오늘 실패하는 일이 생깁니다. 코드도 설정도 안 바뀌었습니다. 받아 온 파일만 바뀐 것이라 어디서부터 봐야 할지 잡기 어렵습니다.
그래서 고칠 것이 생기면 같은 버전을 손보는 대신 버전을 올려 새로 올립니다. 버전 번호를 어떤 규칙으로 올릴지는 시맨틱 버저닝 같은 약속이 정합니다.
예외를 두는 갈래가 하나 있습니다. 아직 배포하지 않은 개발 중 산출물은 같은 이름으로 계속 덮어쓰게 둡니다. 대신 배포용 구역과 나누어 보관합니다.
올린 파일 하나가 이 두 갈림을 어떤 순서로 지나는지를 그림으로 봅니다.
flowchart TD
U["올린다"] --> Q{"그 좌표가 이미 있나"}
Q -->|"없다"| K["보관한다"]
Q -->|"있다"| D{"배포용 구역인가"}
D -->|"예"| X["거부한다 · 버전을 올려 새로 올린다"]
D -->|"아니오"| W["덮어쓴다"]
거부가 기본입니다. 덮어쓰기는 개발 구역에만 남겨 둔 예외입니다.
파일에 딸려 오는 정보
저장소가 담는 것은 파일 하나가 아닙니다. 파일마다 그것을 설명하는 메타데이터가 따라붙습니다. 의존성 목록과 체크섬과 패키지 서명 셋이 그것입니다.
셋이 한 파일에 어떻게 붙는지를 그림으로 봅니다.
flowchart TD
subgraph G["저장소가 보관하는 한 묶음"]
A["아티팩트 파일"]
A --> M1["의존성 목록"]
A --> M2["체크섬"]
A --> M3["패키지 서명"]
end
셋은 따로 보관되는 것이 아니라 그 파일에 딸려 다닙니다. 하나씩 봅니다.
먼저 의존성 목록입니다. 이 파일이 돌아가려면 또 무엇이 필요한지를 적어 두면, 받아 가는 쪽이 그 목록을 따라 나머지도 같이 받아 옵니다. 하나를 부르면 딸려 오는 이것들을 전이 의존성이라고 부릅니다.
받아 온 파일이 또 다른 파일을 부르는 모양을 그림으로 봅니다.
flowchart TD
subgraph L1["내려받는 아티팩트"]
A["아티팩트 A"]
end
subgraph L2["A 의 의존성 목록에 적힌 것"]
B["아티팩트 B"]
C["아티팩트 C"]
end
subgraph L3["B 의 의존성 목록에 적힌 것"]
D["아티팩트 D"]
end
A --> B
A --> C
B --> D
하나를 부르면 그 아래 층이 따라옵니다. 따라온 것이 또 제 아래 층을 부릅니다. 몇 층까지 내려갈지는 각 파일의 목록이 정합니다.
다음은 체크섬입니다. 파일 내용을 해시 함수에 넣어 얻은 짧은 값입니다. 받은 쪽이 같은 값을 다시 계산해 전송 중에 내용이 깨지지 않았는지 확인합니다.
패키지 서명을 같이 두기도 합니다. 체크섬이 깨짐을 잡는다면, 서명은 이 파일을 올린 쪽이 누구인지를 확인해 줍니다.
바깥 저장소를 대신 받아 두기
팀이 쓰는 파일이 전부 자기가 만든 것은 아닙니다. 오픈소스 라이브러리는 바깥의 공개 저장소에 있습니다.
빌드마다 바깥으로 직접 나가면 두 가지가 걸립니다. 바깥이 멈추면 이쪽 빌드도 멈춥니다. 어떤 파일을 언제 받아 왔는지 팀이 기록을 갖지도 못합니다.
이 둘을 피하려고 사내 저장소를 앞에 세우고 바깥 저장소를 대신 받아 오게 합니다. 캐싱을 저장소 단위로 하는 셈입니다.
처음 요청은 바깥까지 나갑니다. 받아 온 파일은 보관해 둡니다. 그다음 요청부터는 사내 저장소가 답합니다.
사내에서 만든 파일과 바깥에서 받아 온 파일은 구역을 나누어 두는 것이 보통입니다. 받아 가는 쪽에는 둘을 합쳐 한 주소처럼 보이게 합니다.
구역 둘이 한 저장소 안에 어떻게 들어가는지를 그림으로 봅니다.
flowchart TD
B["빌드"] -->|"좌표 하나 · 주소 하나"| G
subgraph G["사내 저장소"]
I["사내에서 만든 파일 구역"]
E["바깥에서 받아 온 파일 구역"]
end
E -->|"처음 요청만"| P["바깥의 공개 저장소"]
빌드가 아는 주소는 하나뿐입니다. 그 뒤에서 구역이 둘로 갈리는 것은 저장소가 감춥니다.
저장소를 두는 대가
저장소가 팀 전체의 길목이 됩니다. 이곳이 멈추면 빌드도 배포도 같이 멈춥니다. 여느 서비스처럼 이중화와 백업과 복구를 챙겨야 합니다.
저장 용량도 계속 늡니다. 빌드마다 파일이 쌓입니다. 버전은 덮어쓰지 않으니 줄지도 않습니다. 무엇을 얼마나 오래 둘지 보존 기간 규칙을 세우지 않으면 디스크가 찹니다.
공격 대상이 되기도 합니다. 저장소에 가짜 파일을 하나 넣으면 그것을 받아 가는 모든 빌드로 퍼집니다. 그래서 올릴 수 있는 쪽을 좁히는 접근 제어와 누가 무엇을 올렸는지 남기는 감사 로그가 함께 갑니다. 이 갈래를 통틀어 소프트웨어 공급망 보안이라고 부릅니다.
규모가 작으면 안 두기도 합니다. 배포 대상이 서버 한 대입니다. 빌드한 기계가 곧 배포하는 기계이기도 합니다. 그러면 파일 하나를 옮기려고 서버를 하나 더 두는 셈이 됩니다.
검증과 배포가 갈라지고 받아 가는 쪽이 여럿이 되는 때부터 값이 생깁니다.
관련 항목
아티팩트 저장소에 담기는 산출물
패키지 · 빌드 아티팩트 · 컨테이너 이미지 · 클래스 파일 · 공유 라이브러리 · 소스 배포판
아티팩트를 만들어 올리고 받아 쓰는 도구
빌드 도구 · Maven · Gradle · npm · Docker · 클래스패스
저장소를 채우고 비우는 파이프라인 단계
빌드 · 지속적 통합 · 지속적 전달 · 지속적 배포 · 배포 · 릴리스 엔지니어링 · 플랫폼 엔지니어링
아티팩트 하나를 가리키는 이름 규칙
아티팩트 좌표 · 시맨틱 버저닝 · 버전 고정 · 스냅샷 버전 · 의존성 · 전이 의존성 · 의존성 충돌
저장소가 파일과 함께 보관하는 값
메타데이터 · 체크섬 · 해시 함수 · 패키지 서명 · 소프트웨어 자재 명세서
아티팩트 저장소를 구현한 제품과 공개 저장소
Nexus · Artifactory · Maven Central · PyPI · 컨테이너 레지스트리 · Amazon S3
저장소를 노리는 공급망 공격과 그 방어
소프트웨어 공급망 보안 · 의존성 혼동 · 타이포스쿼팅 · 재현 가능한 빌드 · 접근 제어 · 감사 로그 · 보존 기간
아티팩트 저장소와 역할이 갈리는 저장소
다른 이름: artifact repository · 아티팩트 리포지터리 · 바이너리 저장소