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가 호출하는 함수에 대한 인터페이스를 반환 파라미터로 가진다.

BuildFunc() interface{}

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

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

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

// Implement Builder
func (b *Builder) BuildFunc() interface{} {
  // return a function which will be called by Waypoint
  return b.build
}

func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
  u := ui.Status()
  defer u.Close()
  u.Update("Building application")

  return &Binary{}, nil
}

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

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

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

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

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

syntax = "proto3";

package platform;

option go_package = "github.com/hashicorp/waypoint-plugin-examples/gobuilder/builder";

message Binary {
  string location = 1;
}

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

type Binary struct {
  state         protoimpl.MessageState
  sizeCache     protoimpl.SizeCache
  unknownFields protoimpl.UnknownFields

  Location string `protobuf:"bytes,1,opt,name=location,proto3" json:"location,omitempty"`
}

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

func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
  u := ui.Status()
  defer u.Close()
  u.Update("Building application")

  return &Binary{}, nil
}

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

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

func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
  u := ui.Status()
  defer u.Close()
  u.Update("Building application")

  // setup the defaults
  if b.config.OutputName == "" {
    b.config.OutputName = "app"
  }

  if b.config.Source == "" {
    b.config.Source = "./"
  }

  return &Binary{}, nil
}

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

func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
  u := ui.Status()
  defer u.Close()
  u.Update("Building application")

  // setup the defaults
  if b.config.OutputName == "" {
    b.config.OutputName = "app"
  }

  if b.config.Source == "" {
    b.config.Source = "./"
  }

  c := exec.Command(
       "go",
       "build",
       "-o",
       b.config.OutputName,
       b.config.Source,
  )

  err := c.Run()

  return &Binary{}, nil
}

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

func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
  u := ui.Status()
  defer u.Close()
  u.Update("Building application")

  // setup the defaults
  if b.config.OutputName == "" {
    b.config.OutputName = "app"
  }

  if b.config.Source == "" {
    b.config.Source = "./"
  }

  c := exec.Command(
       "go",
       "build",
       "-o",
       b.config.OutputName,
       b.config.Source,
  )

  err := c.Run()

  if err != nil {
    u.Step(terminal.StatusError, "Build failed")

    return nil, err
  }

  return &Binary{}, nil
}

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

» Building...
x Build failed

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

func (b *Builder) build(ctx context.Context, ui terminal.UI) (*Binary, error) {
  u := ui.Status()
  defer u.Close()
  u.Update("Building application")

  // setup the defaults
  if b.config.OutputName == "" {
    b.config.OutputName = "app"
  }

  if b.config.Source == "" {
    b.config.Source = "./"
  }

  c := exec.Command(
       "go",
       "build",
       "-o",
       b.config.OutputName,
       b.config.Source,
  )

  err := c.Run()

  if err != nil {
    u.Step(terminal.StatusError, "Build failed")

    return nil, err
  }

  u.Step(terminal.StatusOK, "Application built successfully")

  return &Binary{
    Location: path.Join(b.config.Source, b.config.OutputName),
  }, nil
}


플러그인 컴파일

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

protos:
  @echo ""
  @echo "Build Protos"

  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./builder/output.proto
  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./registry/output.proto
  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./platform/output.proto
  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./release/output.proto

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

protos:
  @echo ""
  @echo "Build Protos"

  protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./builder/output.proto

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

$ make

Build Protos
protoc -I . --go_out=plugins=grpc:. --go_opt=paths=source_relative ./builder/output.proto

Compile Plugin
# Clear the output
rm -rf ./bin
GOOS=linux GOARCH=amd64 go build -o ./bin/linux_amd64/waypoint-plugin-gobuilder ./main.go
GOOS=darwin GOARCH=amd64 go build -o ./bin/darwin_amd64/waypoint-plugin-gobuilder ./main.go
GOOS=windows GOARCH=amd64 go build -o ./bin/windows_amd64/waypoint-plugin-gobuilder.exe ./main.go
GOOS=windows GOARCH=386 go build -o ./bin/windows_386/waypoint-plugin-gobuilder.exe ./main.go

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

bin
├── darwin_amd64
│   └── waypoint-plugin-gobuilder
├── linux_amd64
│   └── waypoint-plugin-gobuilder
├── windows_386
│   └── waypoint-plugin-gobuilder.exe
└── windows_amd64
    └── waypoint-plugin-gobuilder.exe


플러그인 설치

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

$ mkdir -p ~/.config/waypoint/plugins

$ make install

Installing Plugin
cp ./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 파일을 아래와 같이 만든다.

project = "guides"

app "example" {

  build {
    use "gobuilder" {
      output_name = "app"
      source = "./"
    }
  }

  deploy {
    use "gobuilder" {}
  }
}

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

package main

import "fmt"

func main() {
  fmt.Println("Hello Waypoint")
}

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

$ export XDG_CONFIG_HOME=~/.config

$ waypoint init
✓ Configuration file appears valid
✓ Local mode initialized successfully
✓ Project "guides" and all apps are registered with the server.
✓ Plugins loaded and configured successfully

Project initialized!

You may now call 'waypoint up' to deploy your project or
commands such as 'waypoint build' to perform steps individually.

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

$ waypoint build
✓ Application built successfully

$ ./app
Hello Waypoint



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

2021/03/20 19:55 2021/03/20 19:55