BazelCon 2022는 11월 16~17일에 뉴욕과 온라인에서 개최됩니다.
지금 등록하기

아티팩트 기반 빌드 시스템

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

이 페이지에서는 아티팩트 기반 빌드 시스템과 이 시스템을 만드는 철학을 설명합니다. Bazel은 아티팩트 기반 빌드 시스템입니다. 작업 기반 빌드 시스템은 빌드 스크립트보다 좋은 단계이지만 각 엔지니어가 자신의 작업을 정의할 수 있도록 허용함으로써 너무 많은 전력을 공급할 수 있습니다.

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

기능적 관점

아티팩트 기반 빌드 시스템과 기능 프로그래밍을 쉽게 구분할 수 있습니다. 기존의 명령형 프로그래밍 언어 (예: 자바, C, Python)는 작업 기반 빌드 시스템을 사용하여 프로그래머가 일련의 단계를 정의할 수 있는 것과 같은 방식으로 차례로 실행될 문 목록을 지정합니다. 101}입니다. 반면 함수 프로그래밍 언어 (예: Haskell 및 ML)는 일련의 수학 방정식과 같이 구조화됩니다. 함수 언어에서 프로그래머는 실행할 계산을 설명하지만, 계산이 컴파일러에 실행되는 시기와 방법에 대한 세부정보를 남겨둡니다.

이는 아티팩트 기반 빌드 시스템에서 매니페스트를 선언하고 시스템에서 빌드를 실행하는 방법을 파악한다는 아이디어에 부합합니다. 기능적 프로그래밍을 사용하여 쉽게 표현할 수 없는 문제도 많지만, 이를 통한 이점이 많은 문제일 수 있습니다. 이러한 언어는 일반적으로 이러한 프로그램을 간단하게 병렬 처리하고 유연성에 대해 강력한 보장을 할 수 있습니다. 명령어로 사용할 수 없습니다. 함수 프로그래밍을 사용하여 표현하기 가장 쉬운 문제는 일련의 규칙 또는 함수를 사용하여 특정 데이터를 다른 데이터로 변환하는 문제입니다. 바로 이 점이 빌드 시스템이 정확히 무엇인지에 관한 것입니다. 전체 시스템은 사실상 소스 파일 (및 컴파일러와 같은 도구)을 입력으로 받아들이고 바이너리를 출력으로 생성하는 수학 함수입니다. 따라서 기능 프로그래밍 원칙에 따라 빌드 시스템을 잘 작동시키는 것은 당연한 일입니다.

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

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

빌드 파일 (일반적으로 BUILD)은 Bazel에서 다음과 같이 표시됩니다.

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: 이 타겟 이전에 빌드되어야 하는 다른 타겟

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

태스크 기반 빌드 시스템과 마찬가지로 Bazel의 명령줄 도구를 사용하여 빌드를 수행합니다. MyBinary 타겟을 빌드하려면 bazel build :MyBinary를 실행합니다. 클린 저장소에서 이 명령어를 처음 입력하면 Bazel이 다음을 수행합니다.

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

기본적으로 진행 중인 작업이 작업 기반 빌드 시스템을 사용할 때와 다른 방식으로 보일 수 있습니다. 실제로 최종 결과는 동일한 바이너리이고 이를 생성하는 과정에는 여러 단계를 분석하여 종속 항목을 찾은 다음 해당 단계를 순서대로 실행해야 했습니다. 하지만 중요한 차이점이 있습니다. 첫 번째는 3단계에 표시됩니다. Bazel은 각 대상이 자바 라이브러리만 생성한다는 것을 알고 있기 때문에 임의의 사용자 정의 스크립트가 아닌 자바 컴파일러를 실행하기만 하면 된다는 것을 알고 있습니다. 를 사용하면 이 단계를 병렬로 실행해도 안전합니다. 이렇게 하면 멀티코어 머신에서 대상을 한 번에 하나씩 빌드하는 것보다 엄청나게 성능이 개선될 수 있으며, 아티팩트 기반 접근 방식은 빌드 시스템을 자체 실행으로 맡기 때문에 가능합니다. 전략으로 삼아 병렬 처리를 더욱 강력하게 보장합니다.

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

태스크가 아닌 아티팩트 측면에서 빌드 프로세스를 재구성하는 작업은 미미하지만 강력합니다. 프로그래머에게 노출되는 유연성을 줄여 빌드 시스템은 빌드의 모든 단계에서 수행되고 있는 작업에 관해 자세히 파악할 수 있습니다. 이 정보를 통해 빌드 프로세스를 병렬 처리하고 출력을 재사용하여 빌드를 훨씬 더 효율적으로 만들 수 있습니다. 하지만 이는 진정한 첫 번째 단계에 불과하며 이러한 병렬 처리와 재사용의 기본 구성 요소는 분산형 확장성과 확장성을 갖춘 빌드 시스템의 기반이 됩니다.

기타 Bazel 트릭

아티팩트 기반 빌드 시스템은 본질적으로 작업 기반 빌드 시스템에 고유한 문제 및 병렬 처리 문제를 해결합니다. 하지만 아직 해결되지 않은 몇 가지 문제가 아직 남아 있습니다. Bazel은 각각 현명하게 문제를 해결했으며 계속 진행하기 전에 이에 대해 논의해야 합니다.

종속 항목로서의 도구

이전에는 빌드 중에 컴퓨터에 설치된 도구에 따라 빌드가 달라지고 여러 시스템 버전 또는 위치에 따라 빌드를 재현하기가 어려울 수 있다는 문제가 있었습니다. 빌드 중인 플랫폼이나 컴파일되는 플랫폼 (예: Windows 및 Linux)에 따라 다른 도구가 필요한 언어를 프로젝트에서 사용하는 경우 문제가 더욱 복잡해집니다. 동일한 작업을 하려면 약간 다른 도구 집합이 필요합니다.

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

Bazel은 빌드 구성을 설정하여 플랫폼의 두 번째 부분인 플랫폼 독립성을 해결합니다. 도구에 직접 의존하는 대신 구성 유형에 따라 다릅니다.

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

빌드 시스템 확장

Bazel은 널리 사용되는 여러 프로그래밍 언어의 타겟을 즉시 포함하지만 엔지니어는 항상 더 많은 작업을 하려고 합니다. 작업 기반 시스템의 이점은 모든 종류의 빌드 프로세스를 유연하게 지원할 수 있다는 점입니다. 아티팩트 기반 빌드 시스템에서는 이러한 작업을 포기하지 않는 것이 좋습니다. 다행히 Bazel을 사용하면 커스텀 규칙을 추가하여 지원되는 대상 유형을 확장할 수 있습니다.

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

시스템은 작업 개발자가 작업의 일부로 비확정적 프로세스를 도입하는 등의 작업을 방지할 방법이 없기 때문에 완벽한 방법이 아닙니다. 하지만 실질적으로는 자주 발생하지 않으며 오용 가능성을 작업 수준까지 내려가면 오류 가능성이 크게 줄어듭니다. 많은 일반 언어와 도구를 지원하는 규칙은 온라인에서 광범위하게 사용할 수 있으며 대부분의 프로젝트는 자체 규칙을 정의할 필요가 없습니다. 그러한 경우에도 규칙 정의는 저장소의 한곳에 정의하기만 하면 되므로 대부분의 엔지니어는 구현에 대해 걱정할 필요 없이 이러한 규칙을 사용할 수 있습니다.

환경 격리

작업이 다른 시스템의 작업과 동일한 문제에 직면할 수 있을 것 같습니다. 같은 파일에 쓰고 동시에 서로 충돌하는 작업을 계속 작성할 수 없나요? 실제로 Bazel은 샌드박스를 사용하여 이러한 충돌을 허용하지 않습니다. 지원되는 시스템에서 모든 작업은 파일 시스템 샌드박스를 통해 다른 모든 작업과 격리됩니다. 사실상 각 작업은 선언된 입력과 이 출력으로 생성된 출력이 포함된 파일 시스템의 제한된 보기만 볼 수 있습니다. 이는 Docker 뒤에 있는 동일한 기술인 Linux의 LXC와 같은 시스템에서 시행합니다. 즉, 선언하지 않은 파일은 읽을 수 없기 때문에 작업이 서로 충돌할 수 없으며, 작성하지만 선언하지 않은 파일은 작업이 삭제될 때 버려집니다. 있습니다. 또한 Bazel은 샌드박스를 사용하여 네트워크가 네트워크를 통해 통신하지 못하도록 제한합니다.

외부 종속 항목을 확정적으로 만들기

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

현재 작업공간 외부에 있는 파일에 따라 위험합니다. 이러한 파일은 언제든지 변경될 수 있으므로 빌드 시스템에서 최신 상태인지 계속 확인해야 할 수 있습니다. 원격 파일이 작업공간 소스 코드에 해당하는 변경사항 없이 변경되면 재현할 수 없는 빌드로 이어질 수 있습니다. 빌드는 예기치 않은 이유로 인해 어느 날은 작동하고 또 어떤 이유로는 실패하게 됩니다. 종속 항목 변경 마지막으로 외부 종속 항목은 타사가 소유할 경우 심각한 보안 위험을 초래할 수 있습니다. 공격자가 타사 서버에 침입할 수 있으면 종속 항목을 {101 }이 단계에 따라 자체 설계를 구현할 수 있으며, 이를 통해 해당 사용자는 빌드 환경 및 그 출력을 완전히 제어할 수 있습니다.

근본적인 문제는 빌드 시스템이 소스 제어에 체크인하지 않고도 이러한 파일을 인식할 수 있어야 한다는 것입니다. 종속 항목 업데이트는 의식적인 선택이지만, 개별 엔지니어가 관리하거나 시스템에서 자동으로 관리하지 않고 중앙에서 한 번 선택해야 합니다. 그 이유는 'Live at Head' 모델에서도 여전히 확정적으로 빌드해야 하기 때문입니다. 즉, 지난주에 커밋을 확인할 경우 종속 항목이 그대로 유지된다는 것을 알 수 있습니다. 지금과 같은 상태가 아닙니다.

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

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

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