Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

HashiCorp의 Waypoint의 커스텀 플러그인 작성하기 #2

이 글은 HashiCorp의 Waypoint의 커스텀 플러그인 작성하기 #1에서 이어진 글이다.


Builder 인터페이스 구현

설정을 하고 나면 Builder 인터페이스에 정의된 대로 BuildFunc 메서드를 구현한다. BuildFunc 메서드는 waypoint build 명령어가 실행되었을 때 Waypoint가 호출하는 함수에 대한 인터페이스를 반환 파라미터로 가진다.

1BuildFunc() interface{}

이 함수에서 입력 파라미터는 시그니처와 일치하지 않아도 된다. Waypoint 함수는 런타임에서 동적으로 주입된 파라미터를 가지고 있고 사용할 수 있는 파라미터 목록은 기본 파라미터 문서에 나와 있다.

입력 파라미터를 선택할 수 있는데 반해 출력 파라미터는 강제되어 있다. 반환되는 파라미터는 반드시 proto.Messageerror 타입이어야 한다. 여기서 proto.Message는 프로토콜 버퍼 Message 인터페이스를 구현한 struct다. Waypoint가 다른 단계에 메시지를 전달할 때 Protocol Buffer를 사용하고 데이터를 내부 데이터 스토어로 직렬화한다. 반환 튜플의 두 번째인 error는 빌드 단계가 성공했는지 실패했는지 알려준다.

예제 템플릿은 다음과 같이 BuildFunc 메서드를 구현했다.

 1// Implement Builder
 2func (b *Builder) BuildFunc() interface{} {
 3  // return a function which will be called by Waypoint
 4  return b.build
 5}
 6
 7func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
 8  u := ui.Status()
 9  defer u.Close()
10  u.Update("Building application")
11
12  return &Binary{}, nil
13}

이 함수에는 다음의 입력 파라미터가 있다.

  • context.Context - 서버가 빌드를 취소했는지 검사하는 데 사용된다.
  • terminal.UI - 출력을 보여주고 Waypoint CLI에 입력을 요청하는 데 사용된다.

출력 파라미터는 다음과 같다.

  • *Binary - output.proto 에서 생성된 struct
  • error - nil이 아닌 오류를 반환해서 실행을 중단하고 사용자에게 오류를 보여준다.

출력값인 Binary는 프로토콜 버퍼 바이너리 형식이다. builder/output.proto 파일에 다음과 같이 프로토콜 버퍼가 정의되어 있다.

1syntax = "proto3";
2
3package platform;
4
5option go_package = "github.com/hashicorp/waypoint-plugin-examples/gobuilder/builder";
6
7message Binary {
8  string location = 1;
9}

protoc가 실행될 때 이 정의를 사용해서 builder/output.pb.go 파일에 Go 코드를 생성한다. 이 예제에서는 이 코드를 고칠 필요는 없다.

1type Binary struct {
2  state         protoimpl.MessageState
3  sizeCache     protoimpl.SizeCache
4  unknownFields protoimpl.UnknownFields
5
6  Location string `protobuf:"bytes,1,opt,name=location,proto3" json:"location,omitempty"`
7}

이제 앞에서 본 build 메서드를 구현하자. terminal.UI를 사용해서 Waypoint 플러그인의 공통 UX를 사용해서 터미널에서 사용자에게 상태를 보여줄 수 있다.

1func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
2  u := ui.Status()
3  defer u.Close()
4  u.Update("Building application")
5
6  return &Binary{}, nil
7}

ui.Status() 메서드는 라이브로 갱신된 상태를 요청하므로 터미널에 갱신된 내용을 출력할 수 있다. Status를 가지고 있을 때 끝나면 st.Close()Status를 항상 닫아야 한다. 예제에서 Status는 마지막에 "Building application" 메시지로 갱신되고 이미 터미널에 작성된 내용도 terminal.Status로 교체할 수 있다.

u.Update() 뒤에 build 메서드에 설정이 제공되지 않았을 때 기본값을 설정하는 코드를 추가하자.

 1func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
 2  u := ui.Status()
 3  defer u.Close()
 4  u.Update("Building application")
 5
 6  // setup the defaults
 7  if b.config.OutputName == "" {
 8    b.config.OutputName = "app"
 9  }
10
11  if b.config.Source == "" {
12    b.config.Source = "./"
13  }
14
15  return &Binary{}, nil
16}

기본값을 설정했으니 Go 애플리케이션을 빌드해보자. Go 표준 라이브러니 exec 패키지를 사용해서 go build를 실행할 수 있다.

 1func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
 2  u := ui.Status()
 3  defer u.Close()
 4  u.Update("Building application")
 5
 6  // setup the defaults
 7  if b.config.OutputName == "" {
 8    b.config.OutputName = "app"
 9  }
10
11  if b.config.Source == "" {
12    b.config.Source = "./"
13  }
14
15  c := exec.Command(
16       "go",
17       "build",
18       "-o",
19       b.config.OutputName,
20       b.config.Source,
21  )
22
23  err := c.Run()
24
25  return &Binary{}, nil
26}

빌드 중에 오류가 발생하면 터미널에 실패 메시지를 보여줘야 한다. Step 메서드를 사용해서 이 메시지를 보여줄 수 있다. 이 코드를 추가해 보자.

 1func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
 2  u := ui.Status()
 3  defer u.Close()
 4  u.Update("Building application")
 5
 6  // setup the defaults
 7  if b.config.OutputName == "" {
 8    b.config.OutputName = "app"
 9  }
10
11  if b.config.Source == "" {
12    b.config.Source = "./"
13  }
14
15  c := exec.Command(
16       "go",
17       "build",
18       "-o",
19       b.config.OutputName,
20       b.config.Source,
21  )
22
23  err := c.Run()
24
25  if err != nil {
26    u.Step(terminal.StatusError, "Build failed")
27
28    return nil, err
29  }
30
31  return &Binary{}, nil
32}

이제 실패하면 다음과 같이 출력될 것이다.

1» Building...
2x Build failed

마지막으로 빌드가 성공하면 상태를 성공으로 갱신하고 다음 단계에 전달하는 proto.Message를 반환해야 한다. build 함수에 오류 검사 뒤 이 코드를 추가하자.

 1func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
 2  u := ui.Status()
 3  defer u.Close()
 4  u.Update("Building application")
 5
 6  // setup the defaults
 7  if b.config.OutputName == "" {
 8    b.config.OutputName = "app"
 9  }
10
11  if b.config.Source == "" {
12    b.config.Source = "./"
13  }
14
15  c := exec.Command(
16       "go",
17       "build",
18       "-o",
19       b.config.OutputName,
20       b.config.Source,
21  )
22
23  err := c.Run()
24
25  if err != nil {
26    u.Step(terminal.StatusError, "Build failed")
27
28    return nil, err
29  }
30
31  u.Step(terminal.StatusOK, "Application built successfully")
32
33  return &Binary{
34    Location: path.Join(b.config.Source, b.config.OutputName),
35  }, nil
36}

플러그인 컴파일

Makefile 파일을 열어보면 protos에 다음과 같은 부분이 있다.

1protos:
2  @echo ""
3  @echo "Build Protos"
4
5  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./builder/output.proto
6  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./registry/output.proto
7  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./platform/output.proto
8  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./release/output.proto

이 플러그인에서는 builder만 사용하므로 다른 부분은 제거하자.

1protos:
2  @echo ""
3  @echo "Build Protos"
4
5  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./builder/output.proto

준비가 끝났으니 다시 플러그인을 빌드하자.

 1$ make
 2
 3Build Protos
 4protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./builder/output.proto
 5
 6Compile Plugin
 7# Clear the output
 8rm -rf ./bin
 9GOOS=linux GOARCH=amd64 go build -o ./bin/linux_amd64/waypoint-plugin-gobuilder ./main.go
10GOOS=darwin GOARCH=amd64 go build -o ./bin/darwin_amd64/waypoint-plugin-gobuilder ./main.go
11GOOS=windows GOARCH=amd64 go build -o ./bin/windows_amd64/waypoint-plugin-gobuilder.exe ./main.go
12GOOS=windows GOARCH=386 go build -o ./bin/windows_386/waypoint-plugin-gobuilder.exe ./main.go

앞에서 봤듯이 bin 아래 컴파일된 Go 바이너리 파일이 생성된다.

1bin
2├── darwin_amd64
3│   └── waypoint-plugin-gobuilder
4├── linux_amd64
5│   └── waypoint-plugin-gobuilder
6├── windows_386
7│   └── waypoint-plugin-gobuilder.exe
8└── windows_amd64
9    └── waypoint-plugin-gobuilder.exe

플러그인 설치

make install을 이용해서 플러그인을 설치해야 Waypoint 프로젝트에서 사용할 수 있다. ~/.config/waypoint/plugins 위치에 설치가 되므로 이 디렉터리를 먼저 만든 뒤에 설치한다. 설치라고 하지만 해당 위치에 Go 바이너리를 복사하는 것일 뿐이다.

1$ mkdir -p ~/.config/waypoint/plugins
2
3$ make install
4
5Installing Plugin
6cp ./bin/darwin_amd64/waypoint-plugin-gobuilder* /Users/outsider/.config/waypoint/plugins/

Waypoint 프로젝트에서 플러그인은 다음의 세 곳에서 로드된다.

  • waypoint.hcl와 같은 디렉터리
  • <waypoint_app_folder>/.waypoint/plugins
  • $XDG_CONFIG_HOME/waypoint/plugins

플러그인 사용

플러그인을 다 만들었으니 잘 동작하는지 테스트해보자. 별도의 프로젝트에서 waypoint.hcl 파일을 아래와 같이 만든다.

 1project = "guides"
 2
 3app "example" {
 4
 5  build {
 6    use "gobuilder" {
 7      output_name = "app"
 8      source = "./"
 9    }
10  }
11
12  deploy {
13    use "gobuilder" {}
14  }
15}

Go 애플리케이션이 필요하므로 간단한 Go 코드를 main.go에 작성한다.

1package main
2
3import "fmt"
4
5func main() {
6  fmt.Println("Hello Waypoint")
7}

플러그인을 ~/.config/waypoint/plugins에 설치했으므로 플러그인을 제대로 찾도록 XDG_CONFIG_HOME 환경 변수를 ~/.config로 설정한다. 그리고 초기화하면 잘 초기화되는 것을 볼 수 있다.

 1$ export XDG_CONFIG_HOME=~/.config
 2
 3$ waypoint init
 4✓ Configuration file appears valid
 5✓ Local mode initialized successfully
 6✓ Project "guides" and all apps are registered with the server.
 7✓ Plugins loaded and configured successfully
 8
 9Project initialized!
10
11You may now call 'waypoint up' to deploy your project or
12commands such as 'waypoint build' to perform steps individually.

이제 waypoint build로 빌드를 하면 Go 프로그램이 잘 빌드되어서 설정한 이름인 app이라는 바이너리가 생긴 것을 볼 수 있다. 이 파일을 실행하면 작성한 프로그램이 잘 실행되는 것을 알 수 있다.

1$ waypoint build
2✓ Application built successfully
3
4$ ./app
5Hello Waypoint



다른 단계까지 다 구현하려면 많이 찾아봐야 하겠지만 대충 플러그인이 어떻게 동작하고 원하는 플러그인을 작성하는 방법을 알게 되었다. 플러그인을 작성할 수 있다면 Waypoint를 사용해서 원하는 배포를 쉽게 작성할 수 있다. 플러그인 쪽은 좀 더 봐야겠지만 아직 플러그인 레지스트리 같은 것은 존재하지 않기 때문에 커스텀 플러그인을 공유해서 사용하는 것은 불편해 보이지만 시간이 지나면 구조상 이 부분도 추가로 구현될 것으로 보인다.

Valid HTML5 Valid CSS WCAG 2.1 AA tested