NixOS 가상 머신을 이용한 통합 테스트#

Authors:

@olafklingt, Domen Kožar @domenkozar

Editor:

Valentin Gagarin @fricklerhandwerk

무엇을 배우게 됩니까?#

이 튜토리얼에서는 NixOS 설정을 테스트하기 위한 Nixpkgs 기능을 소개합니다. 또한 여러 머신이 관여하는 분산 테스트 시나리오를 구성하는 방법도 보여 줍니다.

무엇이 필요합니까?#

소개#

Nixpkgs는 분산 시스템의 통합 테스트를 자동화하는 테스트 환경을 제공합니다. 이 환경을 사용하면 선언적 NixOS 설정의 집합을 바탕으로 테스트를 정의하고, QEMU를 백엔드로 삼아 Python 셸로 해당 설정들과 상호 작용할 수 있습니다. 이러한 테스트는 NixOS가 의도대로 동작하는지 확인하는 데 널리 사용되므로, 일반적으로 NixOS 테스트라고 부릅니다. 이 테스트는 NixOS 외부의 어떤 Linux 머신[1]에서도 작성하고 실행할 수 있습니다.

Nix의 설계 특성 덕분에 통합 테스트는 재현 가능하며, 이 때문에 지속적 통합(CI) 파이프라인에서 유용합니다.

testers.runNixOSTest 함수#

NixOS VM 테스트는 testers.runNixOSTest 함수를 사용하여 정의합니다. NixOS VM 테스트의 패턴은 다음과 같습니다.

 1let
 2  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
 3  pkgs = import nixpkgs { config = {}; overlays = []; };
 4in
 5
 6pkgs.testers.runNixOSTest {
 7  name = "test-name";
 8  nodes = {
 9    machine1 = { config, pkgs, ... }: {
10      # ...
11    };
12    machine2 = { config, pkgs, ... }: {
13      # ...
14    };
15  };
16  testScript = { nodes, ... }: ''
17    # ...
18  '';
19}

testers.runNixOSTest 함수는 테스트 옵션을 지정하는 모듈을 인자로 받습니다. 이 모듈은 설정 값만 지정하므로 축약된 모듈 표기법을 사용할 수 있습니다.

다음 설정 값을 반드시 지정해야 합니다.

  • name은 테스트의 이름을 정의합니다.

  • 테스트 스크립트에는 가상 머신이 둘 이상 관여할 수 있으므로, nodes에는 이름이 붙은 설정의 집합이 들어 있습니다. 각 가상 머신은 NixOS 설정으로부터 생성됩니다.

  • testScript는 Python 테스트 스크립트를 리터럴 문자열 또는 nodes 속성을 받는 함수 형태로 정의합니다. 이 Python 테스트 스크립트는 nodes에 사용된 이름을 통해 가상 머신에 접근할 수 있습니다. 이 스크립트는 가상 머신에서 슈퍼유저 권한을 가집니다. Python 스크립트에서 각 가상 머신은 machine 객체를 통해 접근할 수 있습니다. NixOS는 이러한 설정에 대해 테스트를 실행하는 메서드를 제공합니다.

테스트 프레임워크는 가상 머신을 자동으로 시작하고 Python 스크립트를 실행합니다.

최소 예제#

기본 설정에 대한 최소한의 테스트로, 사용자 root와 alice가 Firefox를 실행할 수 있는지 확인하겠습니다. 예제는 처음부터 직접 만들어 나가겠습니다.

  1. 고정된 버전의 Nixpkgs를 사용하고, 전역 설정에 의해 의도치 않게 오버라이드되지 않도록 설정 옵션과 오버레이를 명시적으로 지정하십시오:

    1let
    2  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
    3  pkgs = import nixpkgs { config = {}; overlays = []; };
    4in
    5
    6pkgs.testers.runNixOSTest {
    7  # ...
    8}
    
  2. 테스트에 내용을 잘 설명하는 이름을 붙이십시오:

    1name = "minimal-test";
    
  3. 이 예제에서는 가상 머신을 하나만 사용하므로, 지정하는 노드의 이름은 간단히 machine입니다. 이 이름은 임의로 정한 것이며 자유롭게 선택할 수 있습니다. 설정으로는 이전 튜토리얼에서 사용했던 기본 설정 중 관련된 부분을 사용합니다:

     1nodes.machine = { config, pkgs, ... }: {
     2  users.users.alice = {
     3    isNormalUser = true;
     4    extraGroups = [ "wheel" ];
     5    packages = with pkgs; [
     6      firefox
     7      tree
     8    ];
     9  };
    10
    11  system.stateVersion = "23.11";
    12};
    
  4. 테스트 스크립트는 다음과 같습니다:

    1machine.wait_for_unit("default.target")
    2machine.succeed("su -- alice -c 'which firefox'")
    3machine.fail("su -- root -c 'which firefox'")
    

    이 Python 스크립트는 nodes 속성 집합에서 사용한 가상 머신 설정에 붙인 이름인 machine을 참조합니다.

    이 스크립트는 systemd가 default.target에 도달할 때까지 기다립니다. su 명령으로 사용자를 전환하고, which 명령으로 해당 사용자가 firefox에 접근할 수 있는지 확인합니다. which firefox 명령이 사용자 alice에게는 성공하고 root에게는 실패할 것으로 기대합니다.

    이 스크립트는 testScript 속성의 값이 됩니다.

완성된 minimal-test.nix 파일의 전체 내용은 다음과 같습니다:

 1let
 2  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
 3  pkgs = import nixpkgs { config = {}; overlays = []; };
 4in
 5
 6pkgs.testers.runNixOSTest {
 7  name = "minimal-test";
 8
 9  nodes.machine = { config, pkgs, ... }: {
10
11    users.users.alice = {
12      isNormalUser = true;
13      extraGroups = [ "wheel" ];
14      packages = with pkgs; [
15        firefox
16        tree
17      ];
18    };
19
20    system.stateVersion = "23.11";
21  };
22
23  testScript = ''
24    machine.wait_for_unit("default.target")
25    machine.succeed("su -- alice -c 'which firefox'")
26    machine.fail("su -- root -c 'which firefox'")
27  '';
28}

테스트 실행하기#

모든 머신을 준비하고 테스트 스크립트를 실행하려면 다음과 같이 하십시오:

$ nix-build minimal-test.nix
...
test script finished in 10.96s
cleaning up
killing machine (pid 10)
(0.00 seconds)
/nix/store/bx7z3imvxxpwkkza10vb23czhw7873w2-vm-test-run-minimal-test

가상 머신의 대화형 Python 셸#

테스트를 개발하거나 무언가가 잘못되었을 때는 테스트를 대화형으로 조작해 보거나 머신의 터미널에 접속하는 것이 유용합니다.

테스트 프레임워크로 대화형 Python 세션을 시작하려면 다음과 같이 하십시오:

$ $(nix-build -A driverInteractive minimal-test.nix)/bin/nixos-test-driver

여기에서는 어떤 테스트 작업이든 실행할 수 있습니다. test_script() 함수로 minimal-test.nix의 testScript 속성을 실행하십시오.

가상 머신이 아직 시작되지 않았다면, machine 객체의 메서드를 처음 호출할 때 테스트 환경이 가상 머신을 시작합니다.

하지만 다음 명령으로 가상 머신을 직접 시작할 수도 있습니다:

>>> machine.start()

특정 노드의 경우,

또는

>>> start_all()

모든 노드의 경우입니다.

다음 명령으로 가상 머신의 대화형 셸에 들어갈 수 있습니다:

>>> machine.shell_interact()

그리고 다음과 같은 셸 명령을 실행할 수 있습니다:

uname -a
Linux server 5.10.37 #1-NixOS SMP Fri May 14 07:50:46 UTC 2021 x86_64 GNU/Linux
성공한 테스트 다시 실행하기

테스트 결과는 Nix 스토어에 보관되므로, 성공한 테스트는 캐시됩니다. 즉, 테스트 설정(노드 설정과 테스트 스크립트)이 의미상 동일하게 유지되는 한 Nix는 테스트를 다시 실행하지 않습니다. 따라서 테스트를 다시 실행하려면 결과를 삭제해야 합니다.

심볼릭 링크를 통해 결과를 삭제하려고 하면 다음과 같은 오류가 발생합니다:

nix-store --delete ./result
finding garbage collector roots...
0 store paths deleted, 0.00 MiB freed
error: Cannot delete path '/nix/store/4klj06bsilkqkn6h2sia8dcsi72wbcfl-vm-test-run-unnamed' since it is still alive. To find out why, use: nix-store --query --roots

대신 심볼릭 링크를 먼저 제거한 다음에 캐시된 결과를 제거하십시오:

rm ./result
nix-store --delete /nix/store/4klj06bsilkqkn6h2sia8dcsi72wbcfl-vm-test-run-unnamed

이 작업은 명령 하나로도 수행할 수 있습니다:

result=$(readlink -f ./result) rm ./result && nix-store --delete $result

여러 가상 머신을 사용하는 테스트#

테스트에는 여러 가상 머신이 포함될 수 있습니다. 예를 들어 클라이언트-서버 간 통신을 테스트할 수 있습니다.

다음 예제 구성에는 다음이 포함됩니다:

  • 기본 설정으로 nginx를 실행하는 server라는 이름의 가상 머신.

  • HTTP 요청을 보낼 수 있도록 curl을 사용할 수 있는 client라는 이름의 가상 머신.

  • client와 server 사이의 테스트 로직을 조율하는 testScript.

client-server-test.nix 파일의 전체 내용은 다음과 같습니다:

let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
  pkgs = import nixpkgs { config = {}; overlays = []; };
in

pkgs.testers.runNixOSTest {
  name = "client-server-test";

  nodes.server = { pkgs, ... }: {
    networking = {
      firewall = {
        allowedTCPPorts = [ 80 ];
      };
    };
    services.nginx = {
      enable = true;
      virtualHosts."server" = {};
    };
  };

  nodes.client = { pkgs, ... }: {
    environment.systemPackages = with pkgs; [
      curl
    ];
  };

  testScript = ''
    server.wait_for_unit("default.target")
    client.wait_for_unit("default.target")
    client.succeed("curl http://server/ | grep -o \"Welcome to nginx!\"")
  '';
}

테스트 스크립트는 다음 단계를 수행합니다:

  1. 서버를 시작하고 준비될 때까지 기다립니다.

  2. 클라이언트를 시작하고 준비될 때까지 기다립니다.

  3. 클라이언트에서 curl을 실행하고 grep을 사용하여 예상되는 반환 문자열을 확인합니다. 테스트는 반환 값에 따라 통과하거나 실패합니다.

테스트를 실행하십시오:

$ nix-build client-server-test.nix

NixOS 테스트에 관한 추가 정보#

다음 단계#