shell 프로비저너

shell 프로비저너

shell Packer 프로비저너는 Packer가 빌드하는 머신에서 shell 스크립트를 실행해요. 머신에 소프트웨어를 설치하고 구성하는 데 shell 프로비저너를 사용하면 돼요.

Windows 이미지를 빌드하고 있나요? 그렇다면 PowerShell 또는 Windows Shell 프로비저너를 사용하는 게 좋을 거예요.

출처: Packer 공식 문서

본문

기본 예시 (Basic Example)

아래 예시는 완전히 동작해요.

HCL2:

provisioner "shell" {
    inline = ["echo foo"]
}

JSON:

{
  "type": "shell",
  "inline": ["echo foo"]
}

설정 레퍼런스 (Configuration Reference)

사용 가능한 설정 옵션의 레퍼런스는 아래에 정리되어 있어요. 유일한 필수 요소는 inline 또는 script 중 하나예요. 다른 모든 옵션은 선택 사항이에요.

다음 중 정확히 하나가 필수예요.

  • inline (array of strings) — 실행할 명령어들의 배열. 명령어는 새 줄로 연결되어 단일 파일로 바뀌기 때문에 모두 같은 컨텍스트에서 실행돼요. 그래서 한 명령어에서 디렉터리로 이동한 뒤 다음 명령어에서 그 디렉터리의 무언가를 사용할 수 있죠. 인라인 스크립트는 머신 안에서 간단한 작업을 처리하는 가장 쉬운 방법이에요.
  • script (string) — 머신에 업로드하고 실행할 스크립트의 경로. 이 경로는 절대 경로일 수도 있고 상대 경로일 수도 있어요. 상대 경로라면 Packer가 실행되는 시점의 작업 디렉터리를 기준으로 해요.
  • scripts (array of strings) — 실행할 스크립트들의 배열. 스크립트는 지정된 순서대로 업로드되고 실행돼요. 각 스크립트는 격리되어 실행되므로, 한 스크립트의 변수 같은 상태가 다음 스크립트로 이어지지 않아요.

선택 매개변수:

  • binary (boolean) — true면 스크립트가 바이너리 파일임을 지정하는 거라, Packer가 Windows 줄바꿈을 Unix 줄바꿈으로 변환하지 않아요(있더라도요). 기본값은 false.
  • valid_exit_codes (list of ints) — 스크립트의 유효한 종료 코드. 기본값은 0뿐이에요.
  • env (map of strings) — execute_command 전에 주입할 key/value 쌍의 맵. Packer는 기본적으로도 일부 환경 변수를 주입하는데, 이는 아래 섹션에서 다뤄요. 중복된 env 설정은 environment_vars 설정을 덮어써요.
  • environment_vars (array of strings) — execute_command 전에 주입할 key/value 쌍의 배열. 형식은 key=value여야 해요. Packer는 기본적으로도 일부 환경 변수를 주입하는데, 이는 아래 섹션에서 다뤄요.
  • env_var_format (string) — 제공한 environment_vars를 파싱할 때, 환경 변수를 올바르게 설정하기 위해 사용하는 문자열 템플릿이에요. 기본값은 "%s='%s' ". use_env_var_file과 함께 사용하면 기본값은 "export %s='%s'\n".
  • use_env_var_file (boolean) — true면 Packer가 환경 변수를 임시 파일에 쓰고, execute_command에 인라인으로 선언하는 대신 그 파일에서 source 해요. 기본 execute_command는 chmod +x {{.Path}}; . {{.EnvVarFile}} && {{.Path}}가 돼요. 대부분의 경우 이 옵션은 불필요하지만, 커스텀 execute_command에 추가 인용(quoting)이 있다면 스크립트를 제대로 실행하기 위해 필요할 수 있어요. 기본값: false.
  • execute_command (string) — 스크립트를 실행하는 데 사용할 명령어. 기본값은 chmod +x {{ .Path }}; {{ .Vars }} {{ .Path }}이고, 사용자가 "use_env_var_file": true를 설정했다면 기본 execute_command는 chmod +x {{.Path}}; . {{.EnvVarFile}} && {{.Path}}예요. 이는 템플릿 엔진이므로 이 필드에 사용자 변수와 템플릿 함수를 사용할 수 있어요. 추가로 세 가지 변수를 사용할 수 있어요: Path는 실행할 스크립트의 경로, Vars는 설정된 경우 environment_vars 목록, EnvVarFile은 use_env_var_file이 true일 때 env 변수를 담은 파일 경로.
  • expect_disconnect (boolean) — 기본값은 false. true면 서버가 오류를 던지지 않고 Packer와의 연결을 끊는 것을 허용해요. SSH 서버를 재시작하거나 호스트를 재부팅할 때 연결 끊김이 발생할 수 있어요.
  • inline_shebang (string) — inline으로 지정한 명령어를 실행할 때 사용할 shebang 값. 기본값은 /bin/sh -e. inline을 사용하지 않는다면 이 설정은 아무 효과가 없어요. 중요: 이 값을 커스텀하면 -e 플래그 같은 것을 꼭 포함해야 해요. 그렇지 않으면 개별 단계가 실패해도 프로비저너가 실패하지 않아요.
  • remote_folder (string) — 업로드된 스크립트가 머신에 위치할 폴더. 기본값은 /tmp.
  • remote_file (string) — 업로드된 스크립트가 머신에서 가질 파일 이름. 기본값은 script_nnn.sh.
  • remote_path (string) — 업로드된 스크립트가 머신에서 가질 전체 경로. 기본값은 remote_folder/remote_file. 이 옵션을 설정하면 remote_folder와 remote_file 둘 다를 덮어써요.
  • skip_clean (boolean) — true면 시스템에 업로드된 헬퍼 스크립트를 Packer가 제거하지 않아요. 기본값은 false(시스템에서 스크립트를 정리).
  • start_retry_timeout (string) — 원격 프로세스를 시작하려 시도하는 시간. 기본값은 5m 즉 5분. 이 설정은 시스템 재부팅처럼 SSH가 재시작될 수 있는 때를 다루기 위해 존재해요. 재부팅에 더 오래 걸린다면 더 높은 값으로 설정하세요.
  • pause_after (string) — shell 스크립트를 프로비저닝한 후 대기하는 시간. 이전 단계가 모두 성공했을 때만 이 일시 정지가 적용돼요.

모든 프로비저너에 공통인 매개변수:

  • pause_before (duration) — 실행 전에 해당 시간만큼 sleep해요.

  • max_retries (int) — 실패 시 프로비저너가 재시도할 최대 횟수. 기본값은 0. 0이면 오류를 재시도하지 않아요.

  • only (array of string) — 이름으로 나열된 빌더에 대해서만 프로비저너를 실행해요.

  • override (object) — 특정 빌더에 대해 다른 설정으로 빌더를 덮어써요. 예:

    HCL2:

    source "null" "example1" { communicator = "none" }
    source "null" "example2" { communicator = "none" }
    build {
      sources = [ "source.null.example1", "source.null.example2" ]
      provisioner "shell-local" {
        inline = [ "echo not overridden" ]
        override = {
          example1 = {
            inline = [ "echo yes overridden" ]
          }
        }
      }
    }
    

    JSON:

    {
      "builders" : [
        { "type" : "null", "name" : "example1", "communicator" : "none" },
        { "type" : "null", "name" : "example2", "communicator" : "none" }
      ],
      "provisioners" : [
        {
          "type" : "shell-local",
          "inline" : [ "echo not overridden" ],
          "override" : {
            "example1" : { "inline" : [ "echo yes overridden" ] }
          }
        }
      ]
    }
    
  • timeout (duration) — 프로비저너가 예를 들어 1h10m1s나 10m보다 오래 걸리면 프로비저너는 timeout으로 실패해요.

Execute Command 예시 (Execute Command Example)

많은 새 사용자에게 execute_command는 헷갈리기 쉬워요. 하지만 여기에는 명령어 실행 방법을 커스터마이즈한다는 중요한 기능이 있어요. 가장 흔한 용도는 sudo 비밀번호 프롬프트를 다루는 거예요. FreeBSD의 tcsh 같은 비-POSIX 셸을 사용한다면 이것도 커스터마이즈해야 할 수 있어요.

Sudo 예시

일부 운영체제는 기본적으로 non-root 사용자를 사용해요. 예를 들어 ubuntu로 로그인하고 비밀번호 packer로 sudo를 사용할 수 있다면, execute_command를 이렇게 바꾸고 싶을 거예요:

"echo 'packer' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"

-S 플래그는 sudo에게 stdin에서 비밀번호를 읽으라고 지시해요. 이 경우 그 값인 packer가 파이프로 들어오죠.

위 예시는 환경 변수에 공백이나 작은따옴표가 있으면 동작하지 않아요. 그런 경우에는 작은따옴표를 제거해 보세요:

"echo 'packer' | sudo -S env {{ .Vars }} {{ .Path }}"

execute_command를 이렇게 설정하면 비밀번호 프롬프트 걱정 없이 스크립트를 root 권한으로 실행할 수 있어요.

FreeBSD 예시

FreeBSD의 기본 셸은 tcsh인데, 이는 POSIX 의미론에서 벗어나 있어요. Packer가 환경 변수를 전달하려면 execute_command를 이렇게 바꿔야 해요:

chmod +x {{ .Path }}; env {{ .Vars }} {{ .Path }}

{{ .Vars }} 앞에 env가 추가된 점을 주목하세요.

기본 환경 변수 (Default Environmental Variables)

environment_vars 설정으로 커스텀 환경 변수를 지정할 수 있는 것에 더해, 프로비저너는 자동으로 다음과 같이 흔히 유용한 환경 변수들을 정의해요.

  • PACKER_BUILD_NAME은 Packer가 실행 중인 빌드의 이름으로 설정돼요. Packer가 여러 빌드를 만들 때 공통 프로비저닝 스크립트에서 그것들을 조금 다르게 구분하고 싶을 때 가장 유용해요.
  • PACKER_BUILDER_TYPE은 스크립트가 실행 중인 머신을 만드는 데 사용된 빌더의 유형이에요. 특정 빌더로 만든 시스템에서만 스크립트의 특정 부분을 실행하고 싶을 때 유용해요.
  • PACKER_HTTP_ADDR — 파일 전송을 위한 HTTP 서버를 제공하는 빌더(hyperv, parallels, qemu, virtualbox, vmware 등)를 사용한다면 이 값이 그 주소로 설정돼요. 프로비저너에서 이 주소를 사용해 HTTP로 큰 파일을 다운로드할 수 있어요. 기본 file 프로비저너를 쓸 때 속도가 느리게 느껴진다면 유용할 수 있어요. winrm communicator를 사용하는 file 프로비저너에서 이런 어려움이 생길 수 있답니다.

재부팅 처리 (Handling Reboots)

프로비저닝은 보통 운영체제를 업데이트할 때 재시작을 수반하기도 해요. Packer는 shell 프로비저너를 통해 재시작을 견딜 수 있어요. Packer는 실패하기 전에 일정 시간 동안 스크립트 시작을 재시도함으로써 이를 처리해요. 이렇게 하면 머신이 시작되어 스크립트를 실행할 준비를 할 시간을 주죠. 프로비저너가 기다릴 시간은 start_retry_timeout으로 설정하며, 기본값은 몇 분이에요.

reboot 같은 명령어를 실행할 때, shell 스크립트가 반환되고 SSH가 실제로 종료되고 머신이 재시작되기 전에 Packer가 다음 것을 실행하기 시작할 수 있어요. 이때는 pause_before를 사용해 Packer가 다음 스크립트를 실행하기 전에 기다리게 하면 돼요.

HCL2:

provisioner "shell" {
  script       = "script.sh"
  pause_before = "10s"
  timeout      = "10s"
}

JSON:

{
  "type": "shell",
  "script": "script.sh",
  "pause_before": "10s",
  "timeout": "10s"
}

일부 OS 설정은 재부팅 시 모든 네트워크 연결을 제대로 끊지 못해서, 재부팅이 발생했는데도 프로비저너가 멈춰 있을 수 있어요. 이런 경우 재부팅 시 또는 shell 스크립트에서 네트워크 인터페이스를 종료해야 해요. 예를 들어 Gentoo에서는:

/etc/init.d/net.eth0 stop

SSH 에이전트 포워딩 (SSH Agent Forwarding)

일부 프로비저닝은 packer 인스턴스 내에서 원격 SSH 서버에 연결해야 해요. 아래 예시는 클라이언트에서 openssh를 사용해 사설 git 저장소에서 코드를 끌어오는 경우예요. ssh-agent를 실행 중이고 ssh-add /path/to/key로 git 저장소 SSH 키를 추가했는지 확인하세요. Packer 인스턴스가 SSH 키에 접근해야 할 때, 에이전트가 요청을 여러분의 ssh-agent로 포워딩해요.

:::note git으로 프로비저닝할 때는 git 서버 키를 ~/.ssh/known_hosts 파일에 추가해야 해요. 그렇지 않으면 git 명령어가 입력을 기다리며 멈출 수 있어요. 이는 file 프로비저너로 파일을 복사하거나(더 안전), ssh-keyscan으로 파일을 채우는(덜 안전) 방식으로 할 수 있어요. 후자의 github 접근 예시는 이래요. :::

HCL2:

provisioner "shell" {
  inline = [
    "sudo apt-get install -y git",
    "ssh-keyscan github.com >> ~/.ssh/known_hosts",
    "git clone [email protected]:exampleorg/myprivaterepo.git"
  ]
}

JSON:

{
  "type": "shell",
  "inline": [
    "sudo apt-get install -y git",
    "ssh-keyscan github.com >> ~/.ssh/known_hosts",
    "git clone [email protected]:exampleorg/myprivaterepo.git"
  ]
}

문제 해결 (Troubleshooting)

Ubuntu에서 내 shell 스크립트가 제대로 동작하지 않아요

  • Ubuntu에서 /bin/sh 셸은 dash예요. 스크립트에 bash 전용 명령어가 있다면 스크립트 맨 위에 #!/bin/bash -e를 넣으세요. dash와 bash의 차이는 DashAsBinSh Ubuntu wiki 페이지에서 볼 수 있어요.

로그인하면 shell이 동작하는데 shell 프로비저너에선 실패해요

  • 위 팁을 참고하세요. 로그인 셸은 /bin/bash를 쓰는데 프로비저너는 /bin/sh를 쓰는 경우가 많아요.

apt-get이나 yum 사용 시 설치가 멈춰요

  • 진행 전 사용자 입력을 요구하지 않도록 명령어에 -y를 꼭 추가하세요.

내 shell 스크립트가 무엇을 하고 있는지 어떻게 알 수 있나요?

  • 스크립트 맨 위 shebang에 -x 플래그를 추가하면(#!/bin/sh -x) 실행되는 대로 스크립트 문장을 출력해요.

내 빌드가 항상 같게 동작하지 않아요

  • 일부 배포판은 다른 핵심 서비스보다 먼저 SSH 데몬을 시작해서 경쟁 조건(race condition)이 생길 수 있어요. 첫 번째 프로비저너가 머신이 완전히 부팅될 때까지 기다리라고 지시할 수 있어요.

HCL2:

provisioner "shell" {
  inline = ["sleep 10"]
}

JSON:

{
  "type": "shell",
  "inline": ["sleep 10"]
}

환경 변수 인용 (Quoting Environment Variables)

Packer가 인용 처리를 관리해 주기 때문에 걱정할 필요가 없어요. 아래는 Packer 템플릿 입력과 그로부터 기대할 수 있는 출력의 예시예요.

HCL2:

provisioner "shell" {
  environment_vars = [
    "FOO=foo",
    "BAR=bar's",
    "BAZ=baz=baz",
    "QUX==qux",
    "FOOBAR=foo bar",
    "FOOBARBAZ='foo bar baz'",
    "QUX2=\"qux\""
  ]
  inline = [
    "echo \"FOO is $FOO\"",
    "echo \"BAR is $BAR\"",
    "echo \"BAZ is $BAZ\"",
    "echo \"QUX is $QUX\"",
    "echo \"FOOBAR is $FOOBAR\"",
    "echo \"FOOBARBAZ is $FOOBARBAZ\"",
    "echo \"QUX2 is $QUX2\""
  ]
}

JSON:

"provisioners": [
  {
    "type":  "shell",
    "environment_vars": ["FOO=foo",
                         "BAR=bar's",
                         "BAZ=baz=baz",
                         "QUX==qux",
                         "FOOBAR=foo bar",
                         "FOOBARBAZ='foo bar baz'",
                         "QUX2=\"qux\""],
    "inline": ["echo \"FOO is $FOO\"",
               "echo \"BAR is $BAR\"",
               "echo \"BAZ is $BAZ\"",
               "echo \"QUX is $QUX\"",
               "echo \"FOOBAR is $FOOBAR\"",
               "echo \"FOOBARBAZ is $FOOBARBAZ\"",
               "echo \"QUX2 is $QUX2\""]
  }
]

Output:

docker: FOO is foo
docker: BAR is bar's
docker: BAZ is baz=baz
docker: QUX is =qux
docker: FOOBAR is foo bar
docker: FOOBARBAZ is 'foo bar baz'
docker: QUX2 is "qux"

더 알아보기 (Learn more)