HashiCorp의 배포 도구인 Waypoint를 HashiCorp의 Waypoint 살펴보기
, HashiCorp Waypoint로 Kubernetes 클러스터에 배포하기에서 설명했는데 사용해보면 Waypoint는 아주 간단해서 금방 익힐 수 있지만, 라이프사이클을 담당하고 있는 build, deploy, release에서 사용하는 플러그인이 핵심이라는 것을 알 수 있다.
다음과 waypoint.hcl
을 보면 build
, deploy
, release
단계를 정의하고 있고 각 단계에서 use
스탠자(Stanza, {}
로 묶이는 코드 블록을 이렇게 부른다.)를 이용해서 플러그인을 사용하고 있다.
project = "project-name"
app "app-name" {
build {
use "pack" {}
}
deploy {
use "kubernetes" {}
}
release {
use "kubernetes" {}
}
여기서 pack
, kubernetes
플러그인을 사용하고 있는데 결국 이 플러그인을 사용해서 어떻게 빌드하고 배포하고 릴리스하는지를 정하는 것이므로 환경에 맞게 필요한 기능이 있다면 플러그인을 어떻게 확장할 수 있는지가 궁금해진다.
내장 플러그인
Waypoint의 플러그인 페이지에 가면 내장된 플러그인을 볼 수 있는데 이 글을 쓰는 시점의 기준으로는 다음과 같은 내장 플러그인이 있다.
- AWS EC2
- AWS ECS
- AWS SSM
- Azure Container Instances
- Docker
- Exec
- Google Cloud Run
- Kubernetes
- Netlify
- Nomad
- CloudNative Buildpacks: Buildpacks
- Vault
이름을 보면 어떤 서비스 혹은 프로그램과 연관되었는지 알 수 있긴 한데 각 플러그인은 build
, deploy
, release
단계에 맞춰져 있고 한 단계만을 위한 플러그인도 여러 단계에서 같이 사용할 수 있는 플러그인도 있다.
Waypoint의 버전이 초기이므로 플러그인이 많지는 않고 다 써보진 않았지만 약간 복잡한 실 서비스에 쓰려면 아쉬운 점이 보일 거로 생각한다. 그래서 원하는 기능을 적용하려면 플러그인을 직접 만들어야 한다.
Waypoint 플러그인
플러그인은 Waypoint 애플리케이션과 통신하는 별도의 바이너리라고 할 수 있고 통신은 gRPC를 사용한다. 그래서 gRPC를 사용할 수 있는 어떤 언어로도 플러그인을 만들 수는 있지만 Go 언어로 Waypoint SDK를 제공하고 있으므로 이를 사용하면 쉽게 플러그인을 만들 수 있다.
플러그인과 관련된 용어를 먼저 살펴보자.
- 플러그인: Waypoint 애플리케이션이 실행하는 컴포넌트가 담긴 컴파일된 바이너리이다.
- 컴포넌트: 애플리케이션의 라이프 사이클(빌드, 디플로이, 릴리스)을 담당하고 여러 인터페이스를 구현해서 만들어진다.
- 인터페이스: 컴포넌트에서
Configurable
,ReleaseManager
같은 동작을 구현한다. - 출력값: Protocol Buffer로 직렬화해서 컴포넌트 간에 주고받을 수 있다.
Go 프로젝트를 빌드하는 플러그인
이 글은 공식 사이트에서 제공하는 Creating Waypoint Plugins를 따라 해 본 것이다. 한번 따라 해보면 플러그인을 통해서 어느 정도 확장할 수 있을지 감을 잡을 수 있다. 앞에서 얘기한 대로 Go 언어용 SDK를 이용해서 만들 것이다.
Go 언어로 플러그인을 작성하려면 아래 의존성이 설치되어 있어야 한다.
- Go 1.14+
- Protocol Buffer 컴파일러
- Go 용 Protocol Buffer 컴파일 플러그인
여기서 만들 플러그인은 Builder
컴포넌트를 만들어서 Waypoint의 build
단계에서 Go 프로젝트를 빌드해서 바이너리로 만들어주는 플러그인을 만들 것이다.
Waypoint가 제공하는 예제 프로젝트를 클론 받는다.
$ git clone git@github.com:hashicorp/waypoint-plugin-examples.git
$ cd waypoint-plugin-examples/template
이 예제프로젝트에서 플러그인 제작을 시작할 수 있는 템플릿을 제공한다. 이 템플릿에서 컴포넌트와 인터페이스가 구현되어 있는데 복사해서 사용하기 쉽게 clone.sh
를 제공한다.
$ ./clone.sh gobuilder ../gobuilder github.com/hashicorp/waypoint-plugin-examples/gobuilder
Created new plugin in gobuilder
You can build this plugin by running the following command
cd ../gobuilder && make
clone.sh
를 실행하면서 플러그인 이름, 복사할 경로, Go 패키지 명을 지정하면 코드에서 이름과 패키지도 알아서 바꿔주면서 복사한다. cd ../gobuilder
로 복사된 폴더로 이동한다.
├── Dockerfile
├── Makefile
├── README.md
├── builder
│ ├── builder.go
│ ├── output.pb.go
│ └── output.proto
├── clone.sh
├── go.mod
├── go.sum
├── main.go
├── platform
│ ├── auth.go
│ ├── deploy.go
│ ├── destroy.go
│ ├── output.pb.go
│ └── output.proto
├── print_arch
├── registry
│ ├── auth.go
│ ├── output.pb.go
│ ├── output.proto
│ └── registry.go
└── release
├── destroy.go
├── output.pb.go
├── output.proto
└── release.go
복사된 파일은 위와 같은 파일을 가지고 있다. 여기서는 builder
만 사용할 것이지만 플러그인 템플릿이므로 다른 파일도 포함되어 있다.
의존성이 잘 설치되었는지 일단 make
로 빌드를 해보자. 앞에서 언급한 의존성이 제대로 설치되지 않았다면 빌드 단계에서 오류가 날 것이다.
$ make
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
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/linux_amd64/waypoint-plugin-gobuilder
처럼 bin
폴더 아래 OS와 아키텍처별로 컴파일된 Go 바이너리가 생기고 이 파일이 플러그인이다.
플러그인 컴포넌트 등록
소스를 수정해 보자. main.go
파일은 아래와 같이 작성되어 있다.
func main() {
// sdk.Main allows you to register the components which should
// be included in your plugin
// Main sets up all the go-plugin requirements
sdk.Main(sdk.WithComponents(
// Comment out any components which are not
// required for your plugin
&builder.Builder{},
®istry.Registry{},
&platform.Platform{},
&release.ReleaseManager{},
))
}
여기서 sdk.Main
함수로 Waypoint SDK를 설정하고 go-plugin
시스템에 인터페이스를 등록하는데 sdk.WithComponents
함수를 이용해서 플러그인에서 구현한 컴포넌트를 정리한다. 모든 컴포넌트가 등록되어 있지만 Builder
만 필요하므로 나머지는 제거한다.
func main() {
// sdk.Main allows you to register the components which should
// be included in your plugin
// Main sets up all the go-plugin requirements
sdk.Main(sdk.WithComponents(
// Comment out any components which are not
// required for your plugin
&builder.Builder{},
))
}
설정 처리
컴포넌트는 Waypoint 인터페이스를 구현한 Go Struct인데 builder/builder.go
파일을 보면 Builder
Struct를 볼 수 있다. 템플릿에서는 Directory
를 가진 BuildConfig
타입의 config
필드를 가지고 있다.
type BuildConfig struct {
Directory string `hcl:"directory,optional"`
}
type Builder struct {
config BuildConfig
}
여기서 만들 gobuilder
라는 플러그인을 생각하면 Go 프로젝트를 빌드하기 위해 빌드한 바이너리의 이름과 소스 코드의 위치를 입력받아야 할 것이다. 상상해보면 다음과 같이 waypoint.hcl
을 통해 설정을 컴포넌트에 전달하게 될 것이다.
project = "guides"
app "example" {
build {
use "gobuilder" {
output_name = "server"
source = "./"
}
}
}
Waypoint가 이 설정 파라미터를 Struct로 매핑하는 방법을 알고 있으므로 컴포넌트의 Struct에서는 config
에서 직렬화할 필드와 태그를 추가하면 된다. builder/builder.go
파일의 BuildConfig
를 다음과 같이 변경한다.
type BuildConfig struct {
OutputName string `hcl:"output_name,optional"`
Source string `hcl:"source,optional"`
}
각 필드는 생략할 수 있으므로 반드시 optional
로 지정해야 한다.
Waypoint가 설정을 파싱할 때 컴포넌트가 Configurable
인터페이스를 구현했는지 확인하고 구현했다면 config
struct
가 반환한 참조에서 Config
메서드를 호출한다. 여기서 Waypoint는 이 참조를 사용해서 설정을 직렬화한다.
builder/builder.go
을 보면 이미 Configurable
인터페이스를 구현한 코드를 볼 수 있다.
// Implement Configurable
func (b *Builder) Config() (interface{}, error) {
return &b.config, nil
}
컴포넌트는 ConfigurableNotify
인터페이스를 사용해서 설정의 유효성을 검사한다. ConfigurableNotify
는 Waypoint가 HCL 설정을 읽을 수 이를 Config
가 반환한 struct
로 직렬화한 후에 호출된 ConfigSet
메서드를 정의하고 있다. 이 인터페이스는 다음과 같이 생겼다.
type ConfigurableNotify interface {
Configurable
// ConfigSet is called with the value of the configuration after
// decoding is complete successfully.
ConfigSet(interface{}) error
}
BuildFunc
같은 컴포넌트용 인터페이스 메서드가 호출되기 전에는 항상 ConfigSet
이 호출되는데 여기서 설정을 검사하고 오류를 반환할 수 있다. 템플릿 코드에서는 이미 Builder
에 ConfigSet
이 다음과 같이 구현되어 있다.
// Implement ConfigurableNotify
func (b *Builder) ConfigSet(config interface{}) error {
c, ok := config.(*BuildConfig)
if !ok {
// The Waypoint SDK should ensure this never gets hit
return fmt.Errorf("Expected *BuildConfig as parameter")
}
// validate the config
if c.Directory == "" {
return fmt.Errorf("Directory must be set to a valid directory")
}
return nil
}
이를 전달받은 소스 코드의 폴더인 Source
필드의 폴더가 실제로 존재하는지 검사하도록 수정해 보자. os.Stat
으로 해당 폴더가 있는지 검사하고 없으면 오류를 반환하게 다음과 같이 수정한다.
func (b *Builder) ConfigSet(config interface{}) error {
c, ok := config.(*BuildConfig)
if !ok {
return fmt.Errorf("Expected type BuildConfig")
}
// validate the config
_, err := os.Stat(c.Source)
if err != nil {
return fmt.Errorf("Source folder does not exist")
}
// config validated ok
return nil
}
글이 길어져서 이 글은 HashiCorp의 Waypoint의 커스텀 플러그인 작성하기 #2으로 이어진다.
Comments