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"