커스텀 빌더 만들기
커스텀 빌더 만들기 (Create Custom Builders)
Packer 빌더는 가상 머신을 만들고, 프로비저닝을 위해 그 가상 머신을 준비한 뒤, 그 프로비저닝된 가상 머신을 머신 이미지로 바꾸는 역할을 해요. 우리는 Amazon EC2, VMware, Google Compute Engine 등 여러 플랫폼용 빌더를 포함해 몇 가지 빌더를 공식적으로 유지·배포해요. 자세한 내용은 Builders 문서를 참고하세요.
이 페이지는 Packer 플러그인 인터페이스를 사용해 커스텀 빌더를 작성하는 방법을 설명해요. 빌더가 HashiCorp Cloud Platform (HCP) Packer를 지원하길 원한다면 HCP Packer Support 문서도 검토해야 해요.
경고: 이는 Packer와 Packer 플러그인에 대한 깊은 지식이 필요한 고급 주제예요.
출처: Packer 공식 문서
본문
Before You Begin (시작 전에)
개발을 시작하기 전에 다음 리소스를 검토할 것을 권장해요.
- Developing Plugins - Overview
- Go 언어. 커스텀 플러그인은 Go로 작성해야 하므로, 이 가이드는 Go 언어에 익숙하다고 가정해요.
The Interface (인터페이스)
자체 빌더를 만들려면 packer.Builder 인터페이스를 구현하는 struct를 만들어야 해요. 참조용으로 아래에 다시 제시해요.
type Builder interface {
ConfigSpec() hcldec.ObjectSpec
Prepare(...interface{}) ([]string, []string, error)
Run(context.Context, ui Ui, hook Hook) (Artifact, error)
}
The "ConfigSpec" Method (ConfigSpec 메서드)
이 메서드는 Packer와 함께 HCL2 템플릿을 사용하는 데 필요한 스펙인 hcldec.ObjectSpec을 반환해요. 이 함수를 사용·구현하는 방법에 대한 정보는 object spec 문서를 확인하세요.
The "Prepare" Method (Prepare 메서드)
각 빌더의 Prepare 메서드는 빌드 시작 시 Packer 코어가 호출해요. 그 목적은 packer build your_packer_template.json 으로 Packer에 제공된 구성 템플릿을 파싱·검증하는 것이지만, API 호출을 실행하거나 리소스·아티팩트 생성을 시작하지는 않아요.
Packer 템플릿의 구성은 interface{} 타입 배열로 Prepare() 메서드에 전달되지만, 일반적으로는 map[string]interface{} 예요. Prepare 메서드는 이 구성을 내부 구조로 변환하고, 검증하며, 오류를 반환하는 역할을 해요.
Prepare()에 여러 파라미터가 전달되면 최종 구성으로 병합되어야 하며, 나중 파라미터가 이전 구성을 덮어써요. 병합의 정확한 의미는 빌더 작성자에게 맡겨져요.
interface{} 를 의미 있는 구조로 디코딩하려면 mapstructure 라이브러리를 사용할 것을 권장해요. Mapstructure는 interface{} 를 받아 임의로 복잡한 struct로 디코딩해요. 오류가 있으면 prepare 메서드에서 직접 반환할 수 있는 매우 인간 친화적인 오류를 생성해요. 이 라이브러리의 사용 예시는 HashiCorp가 유지하는 Packer 플러그인의 Prepare() 메서드에서 많이 찾을 수 있어요.
Packer가 적극적으로 강제하지는 않지만, Prepare 메서드 실행에서 부작용(side effects)이 발생하면 안 돼요. 구체적으로 파일을 만들지 말고, 가상 머신을 실행하지 마세요. Prepare의 목적은 템플릿의 구성을 빌더가 사용할 수 있는 형식으로 로드하고, 그 구성을 검증하며, 그 구성에 필요한 기본값을 적용하는 것뿐이에요.
Packer 템플릿에 제공된 구성 외에도 Packer는 빌드 이름, 빌더 타입, 코어 버전 등과 같은 메타 정보를 포함하고 map[string]interface{} 로 인코딩된 common.PackerConfig 도 제공해요. 이 map의 중요한 메타 정보 한 가지는, 빌드에 디버그 모드가 활성화되어 있으면 불리언 true 로 설정되는 packer.DebugConfigKey 예요. 이 값이 true로 설정되면 빌더는 빌드 중 무슨 일이 일어나는지 빌더 개발자와 고급 사용자가 들여다볼 수 있게 돕는 디버그 모드를 활성화해야 해요. 디버그 빌드 중에는 병렬 처리가 엄격히 비활성화되므로 stdin에서 입력을 요청하는 것 등이 안전해요.
Prepare()는 문자열 배열과 오류를 반환해요. 문자열 배열은 런타임에 생성된 변수들의 특수 키 목록으로, 빌더가 generatedData 메커니즘(자세한 내용은 아래 참조)을 사용해 프로비저너에 접근 가능하게 만들어요. 예시로 빌더가 만든 클라우드 인스턴스의 인스턴스 ID가 될 수 있어요. generatedData 기능을 사용할 계획이 없다면 빈 목록을 반환하면 돼요. 오류는 사용자 제공 구성에 문제가 있어 빌드가 진행되어서는 안 될 때 사용해야 해요.
The "Run" Method (Run 메서드)
Run 은 (여러 빌더에 대해 종종 병렬로) 실행되어 머신을 실제로 빌드하고, 프로비저닝하고, 결과 머신 이미지를 만들어 내며, 이를 packer.Artifact 인터페이스의 구현으로 반환해요.
Run 메서드는 세 개의 파라미터를 받아요. 빌드를 취소하는 데 사용되는 context.Context. packer.Ui 객체는 콘솔에 출력을 보내는 데 사용돼요. packer.Hook 은 (아래 Provisioning 섹션에서 더 자세히 다루는) 훅을 실행하는 데 사용돼요.
빌더 실행은 일반적으로 복잡한 여러 단계의 집합이므로, packer-plugin-sdk 에는 multistep 모듈이 있어요. Multistep을 사용하면 빌드 로직을 실행·정리 단계가 분리된 여러 개의 개별 "steps"로 나누고 순서대로 실행할 수 있어요. 중간 단계 취소, 디버깅 시 단계 사이 일시 정지, CLI의 on-error 플래그 등을 지원해요. HashiCorp가 유지하는 모든 빌더는 이 모듈을 사용하며, 빌더 구현에 필수는 아니지만, 사용자와 Packer Core의 가정에 부합하는 방식으로 빌더를 만들 수 있게 도와줘요. SDK는 HashiCorp 관리자들이 이미 수행한 작업을 다시 구현하지 않도록 하는 여러 "헬퍼" 일반 스텝도 제공해요. 예를 들어 부트 커맨드 전송, SSH 연결, VM에 마운트할 가상 CD 만들기 등이 있어요. SDK의 communicator 및 multistep/commonsteps 모듈을 살펴보고 어떤 도구가 있는지 확인하세요.
마지막으로 Run 은 packer.Artifact 의 구현을 반환해야 해요. packer.Artifact 만드는 방법에 대한 자세한 내용은 아래의 artifact 섹션에서 다뤄요. 빌드 중 아티팩트를 올바르게 만들지 못하게 하는 무언가가 잘못되면 Run 은 오류와 nil 아티팩트를 반환해야 해요. 빌더가 아티팩트와 오류 모두를 만들지 않는 것도 허용되지만, 이는 드문 사용 사례예요.
Cancellation (취소)
With the "Cancel" Method (for plugins for Packer < v1.3) — Cancel 메서드 사용 (Packer < v1.3용 플러그인)
Cancel 메서드는 언제든지 호출될 수 있으며 진행 중인 빌더 실행의 취소를 요청해요. 이 메서드는 실행이 실제로 멈출 때까지 블록되어야 해요. Cancel 메서드는 Packer 버전 >= 1.4.0에서는 호출되지 않는다는 점에 유의하세요.
Context cancellation (from Packer v1.4) — 컨텍스트 취소 (Packer v1.4부터)
<-ctx.Done() 은 언제든지 블록을 해제할 수 있으며 진행 중인 빌더 실행의 취소 요청을 의미해요.
취소는 사용자가 Ctrl-C 를 누르는 것 같은 외부 인터럽트로 가장 흔히 발생해요. Packer는 모든 빌더가 정리된 후에만 종료하므로, 빌더가 이러한 취소에 빠르게 응답하고 스스로 정리하도록 아키텍처를 설계하는 것이 중요해요. 빌더가 오래 실행되는 호출을 한다면, 사용자가 그 호출 중에 빌드를 취소할 가능성을 고려하고 그러한 취소가 차단되지 않도록 해야 해요.
Creating an Artifact (아티팩트 만들기)
Run 메서드는 packer.Artifact 인터페이스의 구현을 반환할 것으로 예상돼요. 각 빌더는 이 인터페이스의 자체 구현을 만들어야 해요.
아티팩트의 대부분은 packer.Artifact 인터페이스 문서를 읽으면 꽤 자명해요.
다만 헷갈릴 수 있는 아티팩트 부분 하나는 BuilderId 메서드예요. 이 메서드는 빌더의 절대적으로 고유한 ID를 반환해야 해요. 일반적으로 합리적인 ID는 빌더를 만든 GitHub 사용자 이름이나 조직과 빌드하는 플랫폼을 이어 붙인 것이에요. 예를 들어 VMware 빌더의 빌더 ID는 "hashicorp.vmware"예요.
포스트-프로세서는 빌더 ID 값을 사용해 아티팩트 결과에 대해 몇 가지 가정을 하고, 주어진 아티팩트에 실행할 수 있는지도 결정해요. 따라서 빌더가 게시된 후에는 이 ID가 절대 바뀌지 않는 것이 중요해요.
각 빌더의 빌더 ID는 관련 문서 페이지에 포함돼 있어요.
Provisioning (프로비저닝)
Packer는 Provisioner 플러그인을 사용한 프로비저닝을 내장 지원해요. 하지만 언제 프로비저너를 호출할지는 Packer 코어가 아니라 빌더 스스로가 결정해야 해요. 머신이 언제 실행되고 통신할 준비가 됐는지 아는 것은 빌더뿐이기 때문이에요.
머신이 프로비저닝될 준비가 되면, 커뮤니케이터가 nil이 아닌지 확인하고 packer.HookProvision 훅을 실행해요. 이것은 프로비저너에 필요하기 때문이에요. 훅 호출 예시는 아래와 같아요.
hook.Run(context.Context, packer.HookProvision, ui, comm, nil)
이 시점에서 Packer가 프로비저너를 실행하며 추가 작업은 필요 없어요.
multistep 도구를 사용한다면 Packer 플러그인 SDK에 StepProvision 이라는 일반 스텝이 있는데, 이 스텝이 프로비전 훅 실행을 처리하고 프로비저너에 제공하려는 커스텀 빌더 generatedData를 자동으로 공급해요(자세한 내용은 아래 generatedData 참조).
Template Engine (템플릿 엔진)
참고: HCL2에서 JSON 템플릿 엔진과 생성된 데이터는 HCL2 객체를 선호하며 점차 사용 중단될 예정이에요. 우리는 계속 지원하겠지만 가능하면 사용을 피할 것을 권장해요.
Build variables (빌드 변수)
Packer JSON은 build 함수로 프로비저너와 포스트-프로세서와 공유할 커스텀 템플릿 엔진 변수를 제공할 수 있게 해 줘요. JSON 템플릿 build 문서는 여기, HCL 템플릿 build 문서는 여기 있어요.
Packer v1.5.0부터 빌더 Prepare() 메서드는 우리가 generated data 라고 부르는 커스텀 변수 목록을 반환해요. 우리는 이 변수 목록을 사용해 빌더별 커스텀 플레이스홀더(placeholder) map을 생성하고, 커스텀 변수를 Packer가 만든 기본 빌드 변수의 플레이스홀더 map과 결합해요. 다음은 빌더가 무엇을 사용할 수 있게 할지 Packer에 알려주는 예시 스니펫이에요.
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
// ...
generatedData := []string{"SourceImageName"}
return generatedData, warns, nil
}
사용자가 build 함수를 사용하는 Packer 템플릿을 제공하면, Packer는 이 generatedData 배열을 사용해 build 함수의 키가 특정 빌더에 존재하는지 검증해요. 존재하지 않으면 Packer 검증이 실패해요.
플레이스홀더가 설정되면 프로비저너를 호출할 때 변수의 실제 값을 전달해야 해요. 이는 아래 예시처럼 할 수 있어요.
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
// ...
// Create map of custom variable
generatedData := map[string]interface{}{"SourceImageName": "the source image name value"}
// Pass map to provisioner
hook.Run(context.Context, packer.HookProvision, ui, comm, generatedData)
// ...
}
같은 변수들과 Packer 기본 변수들을 포스트-프로세서에서도 사용할 수 있게 하려면, 빌더가 이것들을 Artifact에 추가해야 해요. 이는 map[string]interface{} 타입의 속성을 Artifact에 추가하고 generated data를 넣어서 할 수 있어요. 포스트-프로세서는 나중에 Artifact의 State 메서드로 이 데이터에 접근해요.
Artifact 코드는 아래와 유사하게 구현해야 해요.
type Artifact struct {
// ...
// StateData should store data such as GeneratedData
// to be shared with post-processors
StateData map[string]interface{}
}
// ...
func (a *Artifact) State(name string) interface{} {
return a.StateData[name]
}
// ...
빌더는 generated data를 담은 위 Artifact를 반환해야 하고, 코드는 아래 예시 스니펫과 유사해야 해요.
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
// ...
return &Artifact{
// ...
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
}, nil
}
위 코드는 generated_data 상태를 키 generated_data 로 StateData map에 할당해요.
포스트-프로세서가 이 데이터를 사용하는 예시는 다음과 같아요.
func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, source packer.Artifact) (packer.Artifact, bool, bool, error) {
generatedData := source.State("generated_data")
// generatedData will then be used for interpolation
// ...
}
Putting it all together (전체 정리)
이 페이지는 지금까지 Builder 인터페이스의 구현 세부 사항에 집중했어요. 플러그인으로서 Packer 코어가 사용할 수 있게 하려면 서버를 만들고 빌더를 바이너리에 저장해야 해요. 우리는 저장소 안에서 빌더 구현과 서버 구현의 관계에 대한 아이디어를 주는 scaffolding 저장소를 만들었고, 그다음 Plugins가 어떻게 동작하는지의 기초를 읽어보세요. 거기서 모든 서버 세부 사항을 설명해요.
더 알아보기 (Learn more)
- 이 페이지는 Packer 공식 문서에서 가져왔어요.