이 글은 HashiCorp의 Waypoint의 커스텀 플러그인 작성하기 #1에서 이어진 글이다.
Builder
인터페이스 구현
설정을 하고 나면 Builder
인터페이스에 정의된 대로 BuildFunc
메서드를 구현한다. BuildFunc
메서드는 waypoint build
명령어가 실행되었을 때 Waypoint가 호출하는 함수에 대한 인터페이스를 반환 파라미터로 가진다.
BuildFunc() interface{}
이 함수에서 입력 파라미터는 시그니처와 일치하지 않아도 된다. Waypoint 함수는 런타임에서 동적으로 주입된 파라미터를 가지고 있고 사용할 수 있는 파라미터 목록은 기본 파라미터 문서에 나와 있다.
입력 파라미터를 선택할 수 있는데 반해 출력 파라미터는 강제되어 있다. 반환되는 파라미터는 반드시 proto.Message
와 error
타입이어야 한다. 여기서 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를 사용해서 원하는 배포를 쉽게 작성할 수 있다. 플러그인 쪽은 좀 더 봐야겠지만 아직 플러그인 레지스트리 같은 것은 존재하지 않기 때문에 커스텀 플러그인을 공유해서 사용하는 것은 불편해 보이지만 시간이 지나면 구조상 이 부분도 추가로 구현될 것으로 보인다.
Comments