Packer를 확장하는 커스텀 플러그인 만들기

Packer를 확장하는 커스텀 플러그인 만들기

Packer는 확장이 가능하며, 커스텀 빌더(builder), 프로비저너(provisioner), 포스트 프로세서(post-processor), 데이터 소스(data source)를 만들고 사용할 수 있게 해 주는 플러그인을 지원해요. 이 페이지에서는 Packer 플러그인을 개발하는 방법을 설명합니다.

출처: Packer 공식 문서

본문

Packer는 확장이 가능하며 커스텀 빌더, 프로비저너, 포스트 프로세서, 데이터 소스를 만들고 사용할 수 있게 해 주는 플러그인을 지원합니다. 이 페이지는 Packer 플러그인을 개발하는 방법을 설명해요. 시작하기 전에 Packer 문서와 외부 플러그인 설치 지침을 검토해 보는 것을 권장합니다.

Warning 이 문서는 고급 주제예요. 플러그인을 작성하기 시작하기 전에 Packer에 대한 탄탄한 지식을 갖추어야 합니다.

언어 요구사항 (Language Requirements)

Packer 플러그인은 Go로 작성해야 해요.

플러그인 시스템 아키텍처 (Plugin System Architecture)

Packer 플러그인은 그냥 Go 바이너리예요. Packer는 플러그인을 실행 중인 애플리케이션에 직접 로드하는 대신, 각 플러그인을 별도의 애플리케이션으로 실행합니다. 여러 개의 분리된 Packer 플러그인 프로세스는 packer-plugin SDK에 정의된 RPC로 Core와 통신해요. Packer 코어 자체가 플러그인 프로세스를 시작하고 정리하는 역할을 담당합니다.

플러그인 개발 기초 (Plugin Development Basics)

Packer 플러그인에서 만들고 사용할 수 있는 컴포넌트는 빌더, 프로비저너, 포스트 프로세서, 데이터 소스입니다.

이 각 컴포넌트에는 대응하는 인터페이스가 있어요.

플러그인을 만들기 위해 필요한 것은 다음과 같습니다.

  • 원하는 인터페이스의 구현을 만들고,
  • packer-plugin-sdk에서 제공하는 서버로 그것을 서브serve(제공)하기.

코어와 SDK는 서버 안의 모든 통신 세부사항을 처리해요.

플러그인은 서버와 인터페이스를 구현하기 위해 SDK의 두 패키지를 사용해야 합니다. 나머지는 플러그인 구현에 원하는 패키지를 얼마든지 사용해도 좋아요. 플러그인은 각자 별도의 프로세스이기 때문에 의존성이 충돌할 위험이 없습니다.

  • github.com/hashicorp/packer-plugin-sdk/packer - 주어진 플러그인에 대해 구현해야 하는 모든 인터페이스를 담고 있어요.
  • github.com/hashicorp/packer-plugin-sdk/plugin - 플러그인을 서브하는 코드를 담고 있으며, 모든 프로세스 간 통신을 처리해요.

컴포넌트를 서빙하는 기본 예시는 아래와 같습니다.

// main.go
import (
  "github.com/hashicorp/packer-plugin-sdk/plugin"
)

// Assume this implements the packer.Builder interface
type ExampleBuilder struct{}
// Assume this implements the packer.PostProcessor interface
type FooPostProcessor struct{}
// Assume this implements the packer.Provisioner interface
type BarProvisioner struct{}

func main() {
    pps := plugin.NewSet()
    pps.RegisterBuilder("example", new(ExampleBuilder))
    pps.RegisterBuilder(plugin.DEFAULT_NAME, new(AnotherBuilder))
    pps.RegisterPostProcessor("foo", new(FooPostProcessor))
    pps.RegisterProvisioner("bar", new(BarProvisioner))
    err := pps.Run()
    if err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
        os.Exit(1)
    }
}

이 plugin.NewSet 호출은 Packer 코어와 통신하고 RPC로 컴포넌트를 서빙하는 모든 세부사항을 처리해요. 등록하는 struct가 컴포넌트 인터페이스 중 하나를 구현하기만 하면, Packer가 이제 플러그인을 실행하고 사용할 수 있게 됩니다.

컴포넌트를 자신만의 이름으로 등록하면, 컴포넌트 이름이 플러그인 이름에 붙어 고유한 이름이 만들어져요. 특수 문자열 상수 plugin.DEFAULT_NAME으로 컴포넌트를 등록하면 컴포넌트는 플러그인 이름만으로 참조됩니다. 예를 들어:

플러그인 이름이 packer-plugin-my라면 위의 set 정의는 다음 컴포넌트들을 사용 가능하게 만들어요.

  • my-example 빌더
  • my 빌더
  • my-foo 포스트 프로세서
  • my-bar 프로비저너

그다음 다른 Go 애플리케이션처럼 플러그인을 빌드하세요. 결과 바이너리가 플러그인이며, 표준 설치 절차로 설치할 수 있어요.

이 문서는 각 플러그인 인터페이스 타입(빌더, 데이터 소스, 프로비저너, 포스트 프로세서)을 구현하는 방법을 설명합니다.

의존성을 고정(Lock)하세요! Packer 코드베이스는 계속 개선되고 안정 릴리스가 나오기 전에는 중간에 API가 깨질 수 있으므로 go mod 사용을 적극 권장합니다. 의존성을 고정하면 플러그인이 고정한 Packer 버전에서 계속 동작할 거예요.

로깅과 디버깅 (Logging and Debugging)

플러그인은 표준 Go log 패키지를 사용해 로깅할 수 있어요. 이걸로 로깅된 모든 것은 Packer 로그 파일에 자동으로 나타납니다. Packer 로그는 PACKER_LOG 환경 변수가 설정되면 stderr에서 볼 수 있어요.

Packer는 플러그인의 로그 앞에 해당 플러그인의 경로를 붙여 어디서 로그가 왔는지 식별할 수 있게 해 줍니다. 몇 가지 예시 로그는 아래와 같습니다.

2013/06/10 21:44:43 Loading builder: custom
2013/06/10 21:44:43 packer-builder-custom: 2013/06/10 21:44:43 Plugin minimum port: 10000
2013/06/10 21:44:43 packer-builder-custom: 2013/06/10 21:44:43 Plugin maximum port: 25000
2013/06/10 21:44:43 packer-builder-custom: 2013/06/10 21:44:43 Plugin address: :10000

보시다시피 커스텀 빌더 플러그인의 로그 메시지에는 "packer-builder-custom"이라는 접두어가 붙어요. 로그 출력은 문제 디버깅에 매우 유용하며, 로그가 도움이 되도록 필요한 만큼 상세하게 쓰는 것을 권장합니다.

GitHub 릴리스 만들기 (Creating a GitHub Release)

packer init는 중앙 집중식 레지스트리를 사용하지 않아요. 대신 packer-plugin-*라는 이름의 GitHub 저장소에 플러그인을 게시해야 하는데, 여기서 *는 플러그인의 이름입니다. 또한 packer init 다운로드가 동작하도록 특정 자산(asset)을 가진 GitHub 릴리스를 만들어야 해요. 정해진 릴리스 워크플로우 구성을 GitHub Actions로 제공합니다. 릴리스가 Packer가 packer init 설치를 활용할 수 있도록 올바른 이름의 올바른 자산을 포함하도록, 유지 관리자들이 이 구성을 사용하는 것을 적극 권장해요.

GitHub Actions로 릴리스를 만들기 위해 필요한 것은 다음과 같습니다.

  • 릴리스 서명에 사용할 GPG 키를 생성하세요 (이 단계는 GitHub의 상세 지침을 참고).
  • packer-plugin-scaffolding 저장소의 GoReleaser 구성을 저장소 루트에 복사하세요.
  • packer-plugin-scaffolding 저장소의 GitHub Actions 워크플로우를 저장소의 .github/workflows/release.yml에 복사하세요.
  • GitHub의 저장소 페이지로 가서 Settings > Secrets로 이동하세요. 다음 시크릿을 추가합니다:
    • GPG_PRIVATE_KEY - ASCII-armored GPG 개인 키. gpg --armor --export-secret-keys [key ID or email]로 내보낼 수 있어요.
    • GPG_PASSPHRASE - GPG 개인 키의 패스프레이즈.
  • 새 유효한 버전 태그(예: v1.2.3)를 푸시해 GitHub Actions releaser가 동작하는지 테스트하세요. 태그는 v로 시작하는 유효한 시맨틱 버전이어야 해요. 태그를 푸시하면 방금 구성한 GitHub Actions가 Packer가 packer init으로 다운로드할 수 있는 릴리스 바이너리를 자동으로 빌드합니다. packer init으로 플러그인을 설치하는 방법 자세한 내용은 init 문서를 참고하세요.

플러그인 등록 (Registering Plugins)

Note: 플러그인을 통합(integration)으로 등록하려면 문서가 Scaffolding 예시 레이아웃과 일치해야 합니다.

Packer 플러그인의 발견을 돕기 위해, 플러그인 유지 관리자는 플러그인을 Packer Integration으로 등록할 수 있어요.

등록 과정은 Packer 통합 파이프라인을 구성하기 위한 플러그인 저장소에 메타데이터 구성을 추가하고, Packer Integrations 포털에서 플러그인 문서가 렌더링되도록 특정 디렉터리 구조를 요구합니다.

플러그인을 통합으로 등록하려면 다음 단계를 실행할 수 있어요.

  • 플러그인 문서 구조를 Scaffolding 예시 레이아웃과 일치하도록 업데이트하세요. 이 템플릿에서 생성된 새 플러그인은 필요한 구조가 이미 갖춰져 있어요. 그렇다면 3단계로 건너뛰면 됩니다.
  • 통합 라이브러리의 경우 통합당 최상위 README 하나만 지원돼요. 플러그인의 기존 문서에 존재하는 최상위 index.mdx 파일들은 최상위 README로 마이그레이션해야 합니다.
  • 최상위 통합 README를 업데이트해 설명, 플러그인 설치 단계, 사용 가능한 컴포넌트 섹션, 그리고 사용자에게 통합 사용법을 안내하는 데 필요한 추가 섹션을 포함하세요. 예시는 Packer scaffolding 플러그인을 참고하세요.
  • 통합 내 각 컴포넌트의 최상위 README를 scaffolding 템플릿에 정의된 구조를 따르도록 업데이트하세요.
  • 플러그인의 .web-docs 디렉터리에 통합 구성 파일 metadata.hcl을 추가하세요.
  • Packer 팀에 통합 요청 이슈를 열어 주세요 - Open Request. 요청 처리를 빠르게 하려면 요청된 정보를 모두 제공하세요.

[예시] 기존 플러그인 저장소에 통합 파일 추가하기

## Update Plugin repository with integration config, workflows, and scripts
cd packer-plugin-name
mkdir -p .web-docs/scripts
# Download packer-plugin-scaffolding repo copy files
wget https://github.com/hashicorp/packer-plugin-scaffolding/archive/refs/heads/main.zip
unzip main.zip
cp packer-plugin-scaffolding-main/.web-docs/metadata.hcl .web-docs/
cp -r packer-plugin-scaffolding-main/.web-docs/scripts/ .web-docs/scripts/
cp packer-plugin-scaffolding-main/.github/workflows/notify-integration-release-via-* .github/workflows/
# Remove downloaded scaffolding project
rm main.zip
rm -rf packer-plugin-scaffolding-main
# Add the following commands to your plugin GNUmakefile
generate: install-packer-sdc
    @go generate ./...
    @rm -rf .docs
    @packer-sdc renderdocs -src docs -partials docs-partials/ -dst .docs/
    @./.web-docs/scripts/compile-to-webdocs.sh "." ".docs" ".web-docs" "<orgname>"
    @rm -r ".docs"

통합 요청을 열면 Packer 팀 멤버에게 플러그인 통합 구성, 플러그인 문서를 검토하고, 마지막으로 통합 설정을 마무리하기 위해 내부 풀 리퀘스트를 열도록 요청하게 됩니다.

플러그인 통합은 Packer Integration으로 나열되며, 플러그인 설치 및 사용 방법에 대한 세부사항이 함께 제공돼요.

한번 배포된 플러그인 통합은 플러그인 작성자가 수동으로, 또는 새 릴리스에 따라 자동으로 업데이트할 수 있어요. 정의된 문서 구조나 상위 저장소의 변경은 Packer 팀에 전달해 동작하는 통합 파이프라인을 보장해야 합니다.

플러그인 개발 팁과 FAQ (Plugin Development Tips and FAQs)

작동 예시 (Working Examples)

확인해 볼 수 있는 Packer 플러그인의 완전하지 않은 목록입니다.

  • github.com/hashicorp/packer-plugin-docker
  • github.com/exoscale/packer-plugin-exoscale
  • github.com/sylviamoss/packer-plugin-comment

그들의 코드를 보면 좋은 예시가 됩니다.

명명 규칙 (Naming Conventions)

결과 플러그인 애플리케이션을 packer-plugin-NAME 형식으로 이름 짓는 것이 표준 관행이에요. 예를 들어 CustomCloud용 새 빌더를 만든다면 결과 플러그인을 packer-plugin-customcloud로 이름 짓는 것이 표준 관행입니다. 이 명명 규칙은 사용자가 플러그인의 범위를 식별하는 데 도움이 돼요.

플러그인 테스트 (Testing Plugins)

로컬 소스에서 플러그인을 설치하려면 packer plugins install 커맨드를 --path 플래그와 함께 사용할 수 있어요.

$ packer plugins install --path <path-to-binary> <hostname>/<namespace>/<plugin-name>

예를 들어 로컬 소스 바이너리에서 happycloud 플러그인을 설치해 봅시다.

$ packer plugins install --path packer-plugin-happycloud github.com/hashicorp/happycloud

이렇게 하면 packer-plugin-happycloud 바이너리에서 happycloud 플러그인이 설치되어 Packer가 발견할 수 있게 돼요. HCL2 템플릿에서 이걸 사용하고 싶다면 선택적으로 required_plugins 섹션에 추가할 수 있습니다.

  required_plugins {
    happycloud = {
      source = "github.com/hashicorp/happycloud"
      version = ">=0.0.1"
  }
}

Packer가 플러그인을 발견하고 로드하는 방법에 대한 추가 정보는 관련 문서를 참고하세요.

플러그인 배포 (Distributing Plugins)

Go 애플리케이션은 플랫폼별로 다르므로, Packer가 지원하는 모든 플랫폼용으로 플러그인을 크로스 컴파일하는 GoReleaser 같은 도구를 사용할 것을 권장해요. packer-plugin-scaffolding 저장소에서 플러그인을 만들었다면, 커밋에 태그를 달고 그 태그를 GitHub로 푸시하기만 해도 GoReleaser로 바이너리가 올바르게 빌드·릴리스됩니다.