아티팩트 기반 빌드 시스템

이 페이지에서는 아티팩트 기반 빌드 시스템과 이러한 시스템 생성의 기본 철학을 다룹니다. Bazel은 아티팩트 기반 빌드 시스템입니다. 작업 기반 빌드 시스템은 빌드 스크립트보다 한 단계 더 나은 시스템이지만, 개별 엔지니어에게 자체 작업을 정의할 수 있도록 허용하여 너무 많은 권한을 부여합니다.

아티팩트 기반 빌드 시스템에는 엔지니어가 제한된 방식으로 구성할 수 있는 시스템에서 정의한 작업이 소수 있습니다. 엔지니어는 여전히 시스템에 빌드할 항목을 알려주지만 빌드 시스템은 빌드방법을 결정합니다. 작업 기반 빌드 시스템과 마찬가지로 Bazel과 같은 아티팩트 기반 빌드 시스템에도 빌드 파일이 있지만 이러한 빌드 파일의 콘텐츠는 매우 다릅니다. Bazel의 빌드 파일은 출력을 생성하는 방법을 설명하는 튜링 완전 스크립팅 언어의 명령형 집합이 아니라 빌드할 아티팩트 집합, 종속 항목, 빌드 방법에 영향을 미치는 제한된 옵션 집합을 설명하는 선언적 매니페스트입니다. 엔지니어가 명령줄에서 bazel 을 실행할 때 빌드할 대상 집합 (항목)을 지정하고 Bazel은 컴파일 단계 (방법)를 구성, 실행, 예약합니다. 이제 빌드 시스템이 실행할 도구를 완전히 제어할 수 있으므로 정확성을 보장하면서 훨씬 더 효율적으로 사용할 수 있는 훨씬 더 강력한 보장을 제공할 수 있습니다.

기능적 관점

아티팩트 기반 빌드 시스템과 함수형 프로그래밍 간에 비유를 만드는 것은 쉽습니다. 기존 명령형 프로그래밍 언어 (예: Java, C, Python)는 작업 기반 빌드 시스템에서 프로그래머가 실행할 단계 시리즈를 정의할 수 있는 것과 동일한 방식으로 순서대로 실행할 문의 목록을 지정합니다. 반면 함수형 프로그래밍 언어 (예: Haskell 및 ML)는 일련의 수학 방정식과 더 유사하게 구조화됩니다. 함수형 언어에서 프로그래머는 수행할 계산을 설명하지만 해당 계산이 실행되는 시점과 정확한 방법의 세부정보는 컴파일러에 맡깁니다.

이는 아티팩트 기반 빌드 시스템에서 매니페스트를 선언하고 시스템에서 빌드를 실행하는 방법을 파악하도록 하는 아이디어에 매핑됩니다. 함수형 프로그래밍을 사용하여 쉽게 표현할 수 없는 문제가 많지만 함수형 프로그래밍의 이점을 크게 누릴 수 있는 문제도 있습니다. 언어는 이러한 프로그램을 간단하게 병렬화하고 명령형 언어에서는 불가능한 정확성에 대한 강력한 보장을 제공할 수 있는 경우가 많습니다. 함수형 프로그래밍을 사용하여 표현하기 가장 쉬운 문제는 일련의 규칙 또는 함수를 사용하여 한 데이터 조각을 다른 데이터 조각으로 변환하는 것과 관련된 문제입니다. 그리고 이것이 바로 빌드 시스템입니다. 전체 시스템은 소스 파일 (및 컴파일러와 같은 도구)을 입력으로 사용하고 바이너리를 출력으로 생성하는 수학 함수 입니다. 따라서 함수형 프로그래밍의 원칙을 기반으로 빌드 시스템을 구축하는 것이 효과적이라는 것은 놀라운 일이 아닙니다.

아티팩트 기반 빌드 시스템 이해

Google의 빌드 시스템인 Blaze는 최초의 아티팩트 기반 빌드 시스템이었습니다. Bazel 은 Blaze의 오픈소스 버전입니다.

Bazel에서 빌드 파일 (일반적으로 BUILD라는 이름 지정)은 다음과 같습니다.

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

Bazel에서 BUILD 파일은 대상을 정의합니다. 여기서 두 가지 유형의 대상은 java_binaryjava_library입니다. 모든 대상은 시스템에서 만들 수 있는 아티팩트에 해당합니다. 바이너리 대상은 직접 실행할 수 있는 바이너리를 생성하고 라이브러리 대상은 바이너리 또는 기타 라이브러리에서 사용할 수 있는 라이브러리를 생성합니다. 모든 대상에는 다음이 포함됩니다.

  • name: 명령줄 및 기타 대상에서 대상을 참조하는 방법
  • srcs: 대상의 아티팩트를 만들기 위해 컴파일할 소스 파일
  • deps: 이 대상 전에 빌드하고 이 대상에 연결해야 하는 기타 대상

종속 항목은 동일한 패키지 내 (예: MyBinary's :mylib에 대한 종속 항목) 또는 동일한 소스 계층 구조의 다른 패키지 (예: //java/com/example/common에 대한 mylib's 종속 항목)에 있을 수 있습니다.

작업 기반 빌드 시스템과 마찬가지로 Bazel의 명령줄 도구를 사용하여 빌드를 실행합니다. MyBinary 대상을 빌드하려면 bazel build :MyBinary를 실행합니다. 클린 저장소에서 처음으로 이 명령어를 입력한 후 Bazel은 다음을 실행합니다.

  1. 작업공간의 모든 BUILD 파일을 파싱하여 아티팩트 간의 종속 항목 그래프를 만듭니다.
  2. 그래프를 사용하여 MyBinary의 전이적 종속 항목을 결정합니다. 즉, MyBinary가 종속되는 모든 대상과 이러한 대상들이 종속되는 모든 대상을 재귀적으로 결정합니다.
  3. 이러한 각 종속 항목을 순서대로 빌드합니다. Bazel은 다른 종속 항목이 없는 각 대상을 빌드하는 것으로 시작하고 각 대상에 대해 아직 빌드해야 하는 종속 항목 을 추적합니다. 대상의 모든 종속 항목이 빌드되는 즉시 Bazel은 해당 대상 빌드를 시작합니다. 이 프로세스 MyBinary의 모든 전이적 종속 항목이 빌드될 때까지 계속됩니다.
  4. MyBinary를 빌드하여 3단계에서 빌드된 모든 종속 항목을 연결하는 최종 실행 가능한 바이너리를 생성합니다.

기본적으로 여기서 발생하는 일은 작업 기반 빌드 시스템을 사용할 때 발생하는 일과 크게 다르지 않은 것 같습니다. 실제로 최종 결과는 동일한 바이너리이며 이를 생성하는 프로세스에는 종속 항목을 찾기 위해 여러 단계를 분석한 후 이러한 단계를 순서대로 실행하는 작업이 포함되었습니다. 하지만 중요한 차이점이 있습니다. 첫 번째 차이점은 3단계에 나타납니다. Bazel은 각 대상이 Java 라이브러리만 생성한다는 것을 알고 있으므로 임의의 사용자 정의 스크립트가 아닌 Java 컴파일러만 실행하면 된다는 것을 알고 있으므로 이러한 단계를 병렬로 실행해도 안전하다는 것을 알고 있습니다. 이렇게 하면 멀티코어 머신에서 대상을 한 번에 하나씩 빌드하는 것보다 성능이 크게 향상될 수 있으며, 아티팩트 기반 접근 방식은 병렬 처리에 대한 더 강력한 보장을 제공할 수 있도록 빌드 시스템에 자체 실행 전략을 맡기기 때문에 가능합니다.

하지만 이점은 병렬 처리 이상으로 확장됩니다. 개발자가 변경하지 않고 bazel build :MyBinary를 두 번째로 입력하면 이 접근 방식의 다음 이점이 분명해집니다. Bazel은 1초 이내에 종료되고 대상이 최신 상태라는 메시지를 표시합니다. 이는 앞에서 설명한 함수형 프로그래밍 패러다임 덕분에 가능합니다. Bazel은 각 대상이 Java 컴파일러를 실행한 결과일 뿐이며 Java 컴파일러의 출력은 입력에만 종속된다는 것을 알고 있으므로 입력이 변경되지 않는 한 출력을 재사용할 수 있습니다. 그리고 이 분석은 모든 수준에서 작동합니다. MyBinary.java가 변경되면 Bazel은 MyBinary를 다시 빌드하지만 mylib를 재사용합니다. //java/com/example/common의 소스 파일이 변경되면 Bazel은 해당 라이브러리인 mylibMyBinary를 다시 빌드하지만 //java/com/example/myproduct/otherlib를 재사용합니다. Bazel은 모든 단계에서 실행하는 도구의 속성을 알고 있으므로 오래된 빌드를 생성하지 않도록 보장하면서 매번 최소한의 아티팩트 집합만 다시 빌드할 수 있습니다.

빌드 프로세스를 작업이 아닌 아티팩트 측면에서 재구성하는 것은 미묘하지만 강력합니다. 프로그래머에게 노출되는 유연성을 줄임으로써 빌드 시스템은 빌드의 모든 단계에서 수행되는 작업에 대해 더 많은 것을 알 수 있습니다. 이 지식을 사용하여 빌드 프로세스를 병렬화하고 출력을 재사용하여 빌드를 훨씬 더 효율적으로 만들 수 있습니다. 하지만 이는 첫 번째 단계일 뿐이며 이러한 병렬 처리 및 재사용 빌딩 블록은 분산되고 확장성이 뛰어난 빌드 시스템의 기반을 형성합니다.

기타 유용한 Bazel 트릭

아티팩트 기반 빌드 시스템은 작업 기반 빌드 시스템에 내재된 병렬 처리 및 재사용 문제를 근본적으로 해결합니다. 하지만 앞에서 발생했지만 아직 해결하지 못한 몇 가지 문제가 있습니다. Bazel은 이러한 각 문제를 해결하는 영리한 방법을 제공하며 계속 진행하기 전에 이러한 방법을 논의해야 합니다.

종속 항목으로서의 도구

앞에서 발생한 한 가지 문제는 빌드가 머신에 설치된 도구에 종속되었고 시스템 간에 빌드를 재현하는 것이 도구 버전이나 위치가 다르기 때문에 어려울 수 있다는 것입니다. 프로젝트에서 빌드되거나 컴파일되는 플랫폼 (예: Windows와 Linux)에 따라 다른 도구가 필요한 언어를 사용하고 이러한 각 플랫폼에서 동일한 작업을 수행하는 데 약간 다른 도구 집합이 필요한 경우 문제가 훨씬 더 어려워집니다.

Bazel은 도구를 각 대상의 종속 항목으로 처리하여 이 문제의 첫 번째 부분을 해결합니다. 작업공간의 모든 java_library는 잘 알려진 컴파일러로 기본 설정되는 Java 컴파일러에 암시적으로 종속됩니다. Bazel이 java_library를 빌드할 때 지정된 컴파일러가 알려진 위치에서 사용 가능한지 확인합니다. 다른 종속 항목과 마찬가지로 Java 컴파일러가 변경되면 종속되는 모든 아티팩트가 다시 빌드됩니다.

Bazel은 빌드 구성을 설정하여 플랫폼 독립성이라는 문제의 두 번째 부분을 해결합니다. 대상은 도구에 직접 종속되는 것이 아니라 다음과 같은 구성 유형에 종속됩니다.

  • 호스트 구성: 빌드 중에 실행되는 도구 빌드
  • 대상 구성: 최종적으로 요청한 바이너리 빌드

빌드 시스템 확장

Bazel에는 여러 인기 있는 프로그래밍 언어의 대상이 기본 제공되지만 엔지니어는 항상 더 많은 작업을 수행하려고 합니다. 작업 기반 시스템의 이점 중 하나는 모든 종류의 빌드 프로세스를 지원하는 유연성이며 아티팩트 기반 빌드 시스템에서 이를 포기하지 않는 것이 좋습니다. 다행히 Bazel을 사용하면 맞춤 규칙을 추가하여 지원되는 대상 유형을 확장할 수 있습니다.

Bazel에서 규칙을 정의하려면 규칙 작성자가 규칙에 필요한 입력 (BUILD 파일에 전달된 속성 형식)과 규칙에서 생성하는 고정된 출력 집합을 선언합니다. 작성자는 해당 규칙에서 생성되는 작업도 정의합니다. 각 작업은 입력과 출력을 선언하고 특정 실행 파일을 실행하거나 특정 문자열을 파일에 쓰고 입력과 출력을 통해 다른 작업에 연결할 수 있습니다. 즉, 작업 은 빌드 시스템에서 가장 낮은 수준의 구성 가능한 단위입니다. 작업은 선언된 입력과 출력만 사용하는 한 원하는 작업을 수행할 수 있으며 Bazel은 작업을 예약하고 결과를 적절하게 캐시합니다.

작업 개발자가 작업의 일부로 비결정적 프로세스를 도입하는 것과 같은 작업을 수행하지 못하도록 하는 방법이 없으므로 시스템은 완벽하지 않습니다. 하지만 실제로 이러한 일은 자주 발생하지 않으며 악용 가능성을 작업 수준까지 낮추면 오류 발생 가능성이 크게 줄어듭니다. 많은 일반적인 언어와 도구를 지원하는 규칙은 온라인에서 널리 사용할 수 있으며 대부분의 프로젝트에서는 자체 규칙을 정의할 필요가 없습니다. 규칙을 정의해야 하는 경우에도 규칙 정의는 저장소의 한 중앙 위치에서만 정의하면 되므로 대부분의 엔지니어는 구현에 대해 걱정할 필요 없이 이러한 규칙을 사용할 수 있습니다.

환경 격리

작업은 다른 시스템의 작업과 동일한 문제를 겪을 수 있는 것처럼 들립니다. 동일한 파일에 쓰고 서로 충돌하는 작업을 작성하는 것이 여전히 가능하지 않나요? 실제로 Bazel은 이러한 충돌을 샌드박스를 사용하여 불가능하게 만듭니다. 지원되는 시스템에서 모든 작업은 파일 시스템 샌드박스를 통해 다른 모든 작업과 격리됩니다. 실제로 각 작업은 선언된 입력과 생성된 출력을 포함하는 파일 시스템의 제한된 뷰만 볼 수 있습니다. 이는 Docker의 기반 기술인 Linux의 LXC와 같은 시스템에서 적용됩니다. 즉, 작업은 선언하지 않은 파일을 읽을 수 없으며 작성했지만 선언하지 않은 파일은 작업이 완료되면 삭제되므로 작업이 서로 충돌할 수 없습니다. Bazel은 샌드박스를 사용하여 네트워크를 통해 통신하는 작업을 제한합니다.

외부 종속 항목 결정

아직 한 가지 문제가 남아 있습니다. 빌드 시스템은 종종 직접 빌드하는 대신 외부 소스에서 종속 항목 (도구 또는 라이브러리)을 다운로드해야 합니다. 이는 Maven에서 JAR 파일을 다운로드하는 @com_google_common_guava_guava//jar 종속 항목을 통해 예시에서 확인할 수 있습니다.

현재 작업공간 외부의 파일에 종속되는 것은 위험합니다. 이러한 파일은 언제든지 변경될 수 있으므로 빌드 시스템에서 최신 상태인지 지속적으로 확인해야 할 수 있습니다. 작업공간 소스 코드의 해당 변경사항 없이 원격 파일이 변경되면 재현할 수 없는 빌드가 발생할 수도 있습니다. 눈에 띄지 않는 종속 항목 변경으로 인해 빌드가 하루는 작동하고 다음 날은 명확한 이유 없이 실패할 수 있습니다. 마지막으로 외부 종속 항목은 서드 파티가 소유할 때 큰 보안 위험을 초래할 수 있습니다. 공격자가 서드 파티 서버에 침투할 수 있는 경우 종속 항목 파일을 자체 설계된 것으로 대체하여 빌드 환경과 출력을 완전히 제어할 수 있습니다.

근본적인 문제는 소스 제어에 체크인하지 않고도 빌드 시스템에서 이러한 파일을 인식하도록 하는 것입니다. 종속 항목 업데이트는 의식적인 선택이어야 하지만 이러한 선택은 개별 엔지니어가 관리하거나 시스템에서 자동으로 관리하는 것이 아니라 중앙 위치에서 한 번 이루어져야 합니다. 이는 "Live at Head" 모델에서도 빌드 가 결정적이어야 하기 때문입니다. 즉, 지난주 커밋을 체크아웃하면 현재 종속 항목이 아닌 당시 종속 항목이 표시되어야 합니다.

Bazel 및 일부 다른 빌드 시스템은 작업공간의 모든 외부 종속 항목에 대한 암호화 해시 를 나열하는 작업공간 전체 매니페스트 파일을 요구하여 이 문제를 해결합니다. 해시는 전체 파일을 소스 제어에 체크인하지 않고도 파일을 고유하게 나타내는 간결한 방법입니다. 작업공간에서 새 외부 종속 항목이 참조될 때마다 해당 종속 항목의 해시가 매니페스트에 수동 또는 자동으로 추가됩니다. Bazel이 빌드를 실행할 때 캐시된 종속 항목의 실제 해시를 매니페스트에 정의된 예상 해시와 비교하고 해시가 다른 경우에만 파일을 다시 다운로드합니다.

다운로드한 아티팩트의 해시가 매니페스트에 선언된 해시와 다른 경우 매니페스트의 해시가 업데이트되지 않으면 빌드가 실패합니다. 이는 자동으로 수행할 수 있지만 빌드에서 새 종속 항목을 수락하기 전에 변경사항을 승인하고 소스 제어에 체크인해야 합니다. 즉, 종속 항목이 업데이트된 시점을 항상 기록하고 외부 종속 항목을 작업공간 소스의 해당 변경사항 없이 변경할 수 없습니다. 또한 이전 버전의 소스 코드를 체크아웃할 때 빌드는 해당 버전이 체크인된 시점에 사용 중이던 종속 항목을 사용하도록 보장됩니다 (또는 이러한 종속 항목을 더 이상 사용할 수 없는 경우 실패함).

물론 원격 서버를 사용할 수 없게 되거나 손상된 데이터를 제공하기 시작하면 문제가 될 수 있습니다. 이러한 종속 항목의 다른 사본을 사용할 수 없는 경우 모든 빌드가 실패하기 시작할 수 있습니다. 이 문제를 방지하려면 중요하지 않은 프로젝트의 경우 신뢰하고 제어하는 서버 또는 서비스에 모든 종속 항목을 미러링하는 것이 좋습니다. 그렇지 않으면 체크인된 해시가 보안을 보장하더라도 빌드 시스템의 가용성에 대해 항상 서드 파티의 자비에 의존하게 됩니다.