15 분 소요


HASHICORP’S GO-PLUGIN EXTENSIVE TUTORIAL 을 정리한 글입니다.


에제 코드는 다음을 참고하세요.


go-plugin 을 처음 접하는 분들을 위해 간단히 소개하겠습니다.
go-plugin 은 RPC를 지원하는 언어로 구현한 로컬 인터페이스를 RPC로 제공하는 플러그인 시스템입니다.


그림. go-plugin 구조.


수년동안 실전을 통해 강화된 솔루션으로 Terraform, Vault, ConsulPacker 에서 사용하고 있습니다.
모두 유연성이 필요하기 때문에 go-plugin 을 사용합니다.
go-plugin의 플러그인은 작성하기도 쉽습니다.


Basic 플러그인

간단한 Go gRPC 플러그인을 작성해 보겠습니다.
basic 예제는 go-plugin 저장소에 포함되어 있습니다.
이 튜토리얼은 단계별로 진행하겠습니다.


기본 컨셉


그림. Basic 플러그인 구조.


  • 서버(Server):
    플러그인에서 서버는 플러그인의 구현을 제공합니다.
    이는 서버가 인터페이스의 구현을 제공한다는 의미입니다.

  • 클라이언트(Client):
    클라이언트는 원하는 동작을 실행하기 위해 서버를 호출합니다.
    기본 로직은 임의의 상위 포트로 localhost 에서 실행 중인 서버에 연결하고,
    원하는 함수의 구현을 호출하고 응답을 기다립니다.
    응답이 수신되면, 호출한 클라이언트에 다시 전달합니다.


구현

main 함수

  • Logger
    여기에 정의된 플러그인은 stdout을 특별한 방식으로 사용합니다.
    Go 기반 플러그인을 작성하지 않는 경우 직접 다음과 같이 출력해야합니다.
    1|1|tcp|127.0.0.1:1234|grpc
    

    이 부분은 나중에 다시 설명하겠습니다.
    프레임워크가 이것을 선택하고 출력을 기반으로 플러그인에 연결한다고 이해하면 충분합니다.
    출력을 돌려주려면 특별한 logger 를 정의해야 합니다:

    // Create an hclog.Logger
    logger := hclog.New(&hclog.LoggerOptions{
        Name:   "plugin",
        Output: os.Stdout,
        Level:  hclog.Debug,
    })
    


  • NewClient
    // We're a host! Start by launching the plugin process.
    client := plugin.NewClient(&plugin.ClientConfig{
        HandshakeConfig: handshakeConfig,
        Plugins:         pluginMap,
        Cmd:             exec.Command("./plugin/greeter"),
        Logger:          logger,
    })
    defer client.Kill()
    


어떻게 동작하는 지 하나하나 살펴보겠습니다:

  • HandshakeConfig: shared.Handshake :
    플러그인과 호스트가 공유하는 handshake 입니다.
    주석에 설명이 잘 나와있습니다.

      // handshakeConfigs are used to just do a basic handshake between
      // a plugin and host. If the handshake fails, a user friendly error is shown.
      // This prevents users from executing bad plugins or executing a plugin
      // directory. It is a UX feature, not a security feature.
      var handshakeConfig = plugin.HandshakeConfig{
          ProtocolVersion:  1,
          MagicCookieKey:   "BASIC_PLUGIN",
          MagicCookieValue: "hello",
      }
    
    • ProtocolVersion : 플러그인 버전의 호환성 유지를 위해서 사용됩니다. API버전과 같습니다.
      만약 이 버전을 증가시킨다면, 두 가지 옵션이 있습니다.
      낮은 프로토콜 버전을 허용하거나 버전 번호로 전환하지 말고, 낮은 버전은
      다른 클라이언트 구현을 사용하는 것이 좋습니다.
      이렇게 하면 역호환성을 유지할 수 있습니다.
    • MagicCookieKeyMagicCookieValue :
      기본 handshake 에서 사용됩니다.
      앱을 위해서 반드시 설정이 필요하고 수정하면 동작하지 않을 수 있습니다.
      고유성을 위해서, UUID를 사용하는 것이 좋습니다.
  • Cmd : 플러그인에서 가장 중요한 부분 중 하나입니다.
    플러그인은 실행가능한 컴파일된 바이너리이고 이 바이너리를 RPC 서버로 시작합니다.
    여기에서 실행될 바이너리를 정의하고 실행합니다.
    이 동작은 모두 로컬에서 발생하기 때문에(go-plugin 은 localhost 만 지원합니다)
    플러그인 바이너리는 대부분 애플리케이션의 바이너리와 같은 위치거나 설정된 전역 위치에 있습니다.
    (예를 들면, ~/.config/my-app/plugins 같은 위치로 물론 각 플러그인마다 다릅니다.)
    플러그인은 지정된 경로에서 검색하거나 glob 명령을 통해 autoload 될 수 있습니다.

  • Plugins map : 마지막으로 중요한 것은 Plugins map 입니다.
    이 맵은 Dispense 라는 플러그인을 식별하는 함수에서 사용됩니다.
    이 맵은 전역으로 사용할 수 있고, 모든 플러그인이 동작할 수 있도록 일관성을 유지해야 합니다:

    // pluginMap is the map of plugins we can dispense.
    var pluginMap = map[string]plugin.Pluglin   "greeter": &example.GreeterPlugin{},
    }
    


다음을 보고 키가 플러그인 이름이고 값이 플러그인임을 알 수 있습니다.
그 다음에 RPC 클라이언트를 생성합니다:

  // Connect via RPC
  rpcClient, err := client.Client()
  if err != nil {
    log.Fatal(err)
  }


특별한 건 없고, 다음 부분이 흥미롭습니다.

  // Request the plugin
  raw, err := rpcClient.Dispense("greeter")
  if err != nil {
      log.Fatal(err)
  }


Dispense는 위에서 만든 맵을 보고 플러그인을 검색합니다.
만약 찾지 못하면, 에러가 발생됩니다.
이 플러그인이 발견되면 RPC 또는 GRPC 타입 플러그인으로 cast 합니다.
그 다음 RPC 또는 GRPC 클라이언트를 생성합니다.
아직 호출을 하지 않습니다. 이것은 단지 클라이언트를 만들고 각각의 대표로 분석하는 것입니다.
이제 마법이 발생합니다:

  // We should have a Greeter now! This feels like a normal interface
  // implementation but is in fact over an RPC connection.
  greeter := raw.(example.Greeter)
  fmt.Println(greeter.Greet())


여기에서 raw gRPC 클라이언트를 플러그인 타입으로 변환합니다.
이렇게 함으로써 플러그인에 있는 각각의 함수를 호출할 수 있습니다!
완료되면 우리는 간단한 함수처럼 호출할 수 있는 {client,struct,implementation} 을 얻습니다.
당장 구현은 greeter_impl.go 에 있지만, protoc 을 사용하면 다릅니다.
내부에서 go-plugin 은 TCP 연결을 멀티플렉싱하고 우리 플러그인으로 원격 프로시저를 호출을 할 것입니다.
그러면 우리 플러그인은 함수를 실행하고, 어떤 종류의 출력을 생성한 다음, 대기 중인 클라이언트로 다시 전송합니다.
그 다음 클라이언트는 메시지를 지정된 응답 유형으로 분석하여 클라이언트의 수신자(callee)로 다시 전송합니다.
이것으로 main 을 마칩니다.



인터페이스

이제 인터페이스를 들여다보겠습니다.
이 인터페이스는 호출 세부사항을 제공하는 데 사용됩니다.
이 인터페이스는 플러그인의 기능을 정의합니다. 우리의 Greeter는 어떨까요?

  // Greeter is the interface that we're exposing as a plugin.
  type Greeter interface {
      Greet() string
  }


플러그인은 꽤 간단합니다.
문자열 타입의 값을 반환하는 함수를 정의하고 있습니다.
이제, 이것을 동작시키려면 몇 가지가 필요합니다.
먼저 RPC 작업을 정의해야 합니다. go-plugin 은 내부에서 net/http 로 동작하고 있습니다.
또 연결 멀티플렉싱을 위해 Yamux 를 사용하지만, 우리는 이 세부 사항에 대해서 신경쓸 필요가 없습니다.
RPC 상세 내역에 대한 구현은 다음과 같습니다:

  // Here is an implementation that talks over RPC
  type GreeterRPC struct {
      client *rpc.Client
  }
  
  func (g *GreeterRPC) Greet() string {
      var resp string
      err := g.client.Call("Plugin.Greet", new(interface{}), &resp)
      if err != nil {
          // You usually want your interfaces to return errors. If they don't,
          // there isn't much other choice here.
          panic(err)
      }
  
      return resp
  }


여기서 GreeterRPC struct는 RPC를 통해 통신을 처리할 RPC 구현입니다.
이 설정의 클라이언트 입니다.
RPC 클라이언트는 RPC 프로토콜을 통해 다시 스트리밍되는 서버가 구현을 제공하는 Greet 이라는
플러그인 함수를 호출합니다. 서버는 따라하기 매우 쉽습니다:

  // Here is the RPC server that GreeterRPC talks to, conforming to
  // the requirements of net/rpc
  type GreeterRPCServer struct {
      // This is the real implementation
      Impl Greeter
  }


Imple 은 서버의 Greet 플러그인 구현에서 호출할 구체적인 구현(concrete implementation)입니다.
이제 RPCServer 가 원격 코드를 호출할 수 있도록 GreetRPCServer 에 정의해야 합니다.
이것은 다음과 같습니다:

  func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error {
      *resp = s.Impl.Greet()
      return nil
  }

여전히 RPC 작업을 위한 상용구(boilerplate)입니다.
이제 플러그인입니다. 이를 위해 실제로 주석도 매우 잘 설명하고 있습니다.

  // This is the implementation of plugin.Plugin so we can serve/consume this
  //
  // This has two methods: Server must return an RPC server for this plugin
  // type. We construct a GreeterRPCServer for this.
  //
  // Client must return an implementation of our interface that communicates
  // over an RPC client. We return GreeterRPC for this.
  //
  // Ignore MuxBroker. That is used to create more multiplexed streams on our
  // plugin connection and is a more advanced use case.
  type GreeterPlugin struct {
      // Impl Injection
      Impl Greeter
  }
  
  func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
      return &GreeterRPCServer{Impl: p.Impl}, nil
  }
  
  func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
      return &GreeterRPC{client: c}, nil
  }

위 코드를 설명하겠습니다:
GreeterRPCServer 는 실제 구현을 호출하는 반면 Client는 해당 호출의 결과를 수신합니다.
GreeterPlugin 에는 RPCServer 와 마찬가지로 Greeter 인터페이스가 포함되어 있습니다.
GreeterPlugin 은 플러그인 맵의 struct로 사용할 것입니다. 이는 실제로 사용할 플러그인입니다.
이것은 모두 여전히 일반적인 것입니다. 이러한 것들은 둘 다 볼 수 있어야합니다.
플러그인의 구현은 인터페이스를 사용하여 구현해야하는 사항을 확인합니다.
클라이언트는이를 사용하여 호출 할 항목과 사용 가능한 API를 확인합니다.
마치, Greet 와 비슷합니다. 구현에 대해서 알아보겠습니다.


boilerplate 란?
컴퓨팅에서 boilerplate 란 변경없이 재사용할 수 있는 코드를 의미합니다.



구현

완전히 별도의 패키지이지만 인터페이스 정의에 계속 접근할 수 있는 이 플러그인은 다음과 같습니다:

  // Here is a real implementation of Greeter
  type GreeterHello struct {
      logger hclog.Logger
  }
  
  func (g *GreeterHello) Greet() string {
      g.logger.Debug("message from GreeterHello.Greet")
      return "Hello!"
  }


struct를 만든 다음 플러그인의 인터페이스에 정의된 함수를 추가합니다.
이 인터페이스는 양 당사자가 필요로 하기 때문에 SDK와 같은, 두 프로그램 외부의 공통 패키지에
위치할 수 있습니다.
두 코드 모두 import 해서 공통 종속성으로 사용할 수 있습니다.
이런 식으로 우리는 플러그인 호출 클라이언트에서 인터페이스를 분리했습니다.
main 함수는 다음과 같습니다:

  logger := hclog.New(&hclog.LoggerOptions{
      Level:      hclog.Trace,
      Output:     os.Stderr,
      JSONFormat: true,
  })
  
  greeter := &GreeterHello{
      logger: logger,
  }
  // pluginMap is the map of plugins we can dispense.
  var pluginMap = map[string]plugin.Plugin{
      "greeter": &example.GreeterPlugin{Impl: greeter},
  }
  
  logger.Debug("message from plugin", "foo", "bar")
  
  plugin.Serve(&plugin.ServeConfig{
      HandshakeConfig: handshakeConfig,
      Plugins:         pluginMap,
  })

우리가 필요로 하는 두 가지를 주목해야 합니다.
하나는 handshakeConfig 입니다.
클라이언트 코드에서 정의한 것과 동일한 쿠키 세부 정보를 사용하여 여기서 정의하거나
SDK를 사용하여 핸드셰이크 정보를 추출할 수 있습니다.
이것은 당신에게 달려 있습니다.
그 다음 흥미로운 것은 plugin.Serve 메서드입니다. 여기서 마법이 발생합니다.
플러그인은 RPC 통신 소켓을 열고 획득한 stdout 을 통해 그것의 가용성을 다음과 같은 형식으로
호출한 클라이언트에 브로드캐스트한다.

  CORE-PROTOCOL-VERSION | APP-PROTOCOL-VERSION | NETWORK-TYPE | NETWORK-ADDR | PROTOCOL

Go 플러그인에 대해서 여러분은 신경쓰지 않아도 됩니다.
go-plugin 이 알아서 처리합니다.

  # Output information
  print("1|1|tcp|127.0.0.1:1234|grpc")
  sys.stdout.flush()

gRPC 플러그인의 경우 HealthChecker 도 의무적으로 구현해야 합니다.
gRPC는 조금 더 복잡해지지만 너무 많이 복잡하지는 않습니다.
protoc 을 사용하여 구현을 위한 프로토콜 설명을 작성해야 하며, 그 다음으로 호출할 수 있습니다.




gRPC Basic 플러그인

API

이제 기본적인 Greeter 예제를 gRPC로 변환하여 살펴보겠습니다.
무엇보다도, protoc 로 구현할 API를 정의해야 합니다.
우리의 Basic 예제에서는, protoc 파일은 다음과 같습니다:

  syntax = "proto3";
  package proto;
  
  message GreetResponse {
      string message = 1;
  }
  
  message Empty {}
  
  service GreeterService {
      rpc Greet(Empty) returns (GreetResponse);
  }

구문은 꽤 간단하고 읽기 쉽습니다.
이것이 정의하는 것은 문자열 타입 메시지를 포함하는 응답 메시지 입니다.
이 서비스는 Greet 라는 메서드를 가진 서비스를 정의합니다.
서비스 정의(service definition)는 플러그인을 통해 구체적인 구현을 제공할 인터페이스입니다.
protoc 에 대해 자세히 알아보려면 Google 프로토콜 버퍼를 참고하세요.



코드 생성

이제 protoc 정의를 사용하여 로컬 클라이언트 구현이 호출할 수있는 스텁(Stub)을 생성해야 합니다.
그런 다음 해당 클라이언트 호출은 RPC를 통해 구체적인 구현이 준비된 서버에서 올바른 함수를 호출합니다.
그것을 실행하고 지정된 형식으로 결과를 반환합니다.
스텁은 양쪽(클라이언트와 서버) 모두에서 사용할 수 있어야 하므로 공유된 위치에 있어야합니다.
왜냐하면, 클라이언트가 스텁을 호출하고 서버가 스텁을 구현하기 때문입니다.
둘 다 무엇을 호출/구현할지를 알기 위해 필요합니다.
코드를 생성하려면 다음 명령을 실행합니다:

  $ protoc -I proto/ proto/greeter.proto --go_out=plugins=grpc:proto


생성된 코드를 읽어보는 것을 권장합니다.
처음에는 이해하기 힘들 것 입니다.
그 안에는 gRPC 패키지가 그 기능을 지원하기 위해 사용할 많은 struct와 정의(definition)들이 있습니다.
흥미로운 점은 다음과 같습니다:

  func (m *GreetResponse) GetMessage() string {
      if m != nil {
          return m.Message
      }
      return ""
  }


응답 안에 있는 메시지를 사용할 수 있습니다.

  type GreeterServiceClient interface {
      Greet(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GreetResponse, error)
  }


이것은 Greet 함수의 토폴로지를 정의하는 ServiceClient 인터페이스 입니다.
그리고 다음은 마지막입니다:

  func RegisterGreeterServiceServer(s *grpc.Server, srv GreeterServiceServer) {
      s.RegisterService(&_GreeterService_serviceDesc, srv)
  }

서버에 대한 구현을 등록하기 위해 필요합니다.
나머지는 무시할 수 있습니다.



인터페이스

RPC와 마찬가지로 클라이언트와 서버가 사용할 인터페이스를 정의해야 합니다.
서버와 클라이언트가 모두 알아야 하므로 공유 위치에 있어야 합니다.
SDK에 넣으면 동료들이 SDK를 가져와서 정의하고 실행할 수 있는 기능을 구현할 수 있습니다.
gRPC 영역의 인터페이스 정의는 다음과 같습니다:

  // Greeter is the interface that we're exposing as a plugin.
  type Greeter interface {
      Greet() string
  }
  
  // This is the implementation of plugin.GRPCPlugin so we can serve/consume this.
  type GreeterGRPCPlugin struct {
      // GRPCPlugin must still implement the Plugin interface
      plugin.Plugin
      // Concrete implementation, written in Go. This is only used for plugins
      // that are written in Go.
      Impl Greeter
  }
  
  func (p *GreeterGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
      proto.RegisterGreeterServer(s, &GRPCServer{Impl: p.Impl})
      return nil
  }
  
  func (p *GreeterGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
      return &GRPCClient{client: proto.NewGreeterClient(c)}, nil
  }

이것으로 우리는 hashicorp에 필요한 플러그인 구현이 있습니다.
플러그인은 기본 구현을 호출하고 플러그인에 제공/소비(serve/consume)할 것입니다.
우리는 gRPC 부분을 작성할 수 있습니다.
proto는 프로토콜 스텁이 있는 공유 라이브러리이기도 합니다.
그것은 경로 어딘가에나 별도의 SDK와 같은 곳에 있으면 되지만, 반드시 찾을 수 있어야 합니다.



gRPC 클라이언트 구현

먼저, gRPC 클라이언트 struct 를 정의합니다:

  // GRPCClient is an implementation of Greeter that talks over RPC.
  type GRPCClient struct{ client proto.GreeterClient }


그 다음으로 클라이언트가 어떻게 원격 함수를 호출할 지 정의합니다:

  func (m *GRPCClient) Greet() string {
      ret, _ := m.client.Greet(context.Background(), &proto.Empty{})
      return ret.Message
  }

이것은 GRPCClient 에서 클라이언트를 가져와서 그에 대한 메소드를 호출합니다.
완료되면 결과의 Message 속성에 “Hello!” 저장됩니다. proto.Empty 는 빈 구조체입니다;
정의된 메소드에서 매개 변수가 없거나 리턴 값이 없는 경우에 사용합니다.
그냥 비워 둘 수는 없습니다.
protoc 은 매개 변수 나 반환 값이 없다는 것을 명시적으로 알려야합니다.



gRPC 서버 구현

서버 구현도 유사합니다. 구체적인 플러그인 구현을 Impl 이라고 하겠습니다.

  // Here is the gRPC server that GRPCClient talks to.
  type GRPCServer struct {
      // This is the real implementation
      Impl Greeter
  }
  
  func (m *GRPCServer) Greet(
      ctx context.Context,
      req *proto.Empty) *proto.GreeterResponse {
      v := m.Impl.Greet()
      return &proto.GreeterResponse{Message: v}
  }


그리고 우리는 protoc 정의의 메시지 응답을 사용할 것입니다.
v 는 Greet의 구체적인 플러그인 구현에서 전달한 응답 “Hello!” 를 받을 것입니다.
그런 다음 자동으로 생성된 protoc 스텁 코드에서 제공하는 GreeterResponse struct의
Message 속성을 설정하여 이를 protoc 타입으로 변환합니다.



플러그인 구현

이 모든 것은 몇 가지 작은 수정과 변화만으로도 RPC 구현과 많이 유사합니다.
이것은 모든 것이 완전히 외부에 있을 수도 있고 제 3 자가 구현하여 제공할 수도 있습니다.

  // Here is a real implementation of KV that writes to a local file with
  // the key name and the contents are the value of the key.
  type Greeter struct{}
  
  func (Greeter) Greet() error {
      return "Hello!"
  }
  
  func main() {
      plugin.Serve(&plugin.ServeConfig{
          HandshakeConfig: shared.Handshake,
          Plugins: map[string]plugin.Plugin{
              "greeter": &shared.GreeterGRPCPlugin{Impl: &Greeter{}},
          },
  
          // A non-nil value here enables gRPC serving for this plugin...
          GRPCServer: plugin.DefaultGRPCServer,
      })
  }



모든 것을 main 에서 호출하기

모든 작업이 완료되면 main 함수는 RPC의 경우에서 약간의 수정이 필요합니다.

  // We're a host. Start by launching the plugin process.
  client := plugin.NewClient(&plugin.ClientConfig{
      HandshakeConfig: shared.Handshake,
      Plugins:         shared.PluginMap,
      Cmd:             exec.Command("./plugin/greeter"),
      AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
  })


NewClient 는 이제 AllowedProtocolsProtocolGRPC 로 정의합니다.
나머지는 Dispense를 호출하기 전과 동일하며 플러그인을 올바른 타입으로 암시한 다음 Greet() 를 호출합니다.




결론

이제 플러그인은 protoc 에서 정의한 API를 사용하여 gRPC에서 동작합니다.
플러그인의 구현은 우리가 원하는 위치에 있을 수 있지만, 공유 데이터가 필요합니다. 이것들은:

  • protoc에 의해 생성된 코드
  • 정의된 플러그인 인터페이스
  • gRPC 서버와 클라이언트

이는 클라이언트와 서버 모두가 찾을 수 있는 위치에 있어야 합니다.
여기서 서버는 플러그인입니다.
사람들이 go-plugin 으로 애플리케이션 확장을 계획하고 있다면, 별도의 SDK로 제공해야 합니다.
그렇게 하면 사람들이 전체 프로젝트를 포함하지 않고, 단지 인터페이스를 구현하고 protoc을 사용하면 됩니다.
실제로 SDK에서 가져올 수 있도록 protoc 정의를 별도의 저장소로 추출할 수도 있습니다.
세 개의 저장소가 있을 수 있습니다:

  • 당신의 애플리케이션
  • 인터페이스와 gRPC 서버 및 클라이언트 구현을 제공하는 SDK
  • protoc 정의 파일 및 생성된 스켈레톤(Go 기반 플러그인 용)

다른 언어는 자체 protoc 코드를 생성하여 플러그인에 포함시켜야 합니다.
여기에 있는 Python 구현 예제와 같습니다: Go-plugin Python 예제.
또 이 문서를 주의 깊게 읽으십시오: non-go go-plugin.
이 문서에는 1|1|tcp|127.0.0.1:1234|grpc 의 의미를 명확히 설명하고 플러그인 작동 방식에 대해 설명합니다.

마지막으로 go-plugin이 어떻게 생거났는지 자세한 설명이 필요하다면 Mitchell의이 비디오를 시청하십시오:
go-plugin explanation video.
한 시간 정도의 길이지만 그만한 가치가 있습니다!
이것이 go-plugin 사용 방법에 대한 혼란을 없애는 데 도움이 되었기를 바랍니다.




go-plugin

HashiCorp tooling 에서 사용하는 Plugin 시스템 go-plugin 에 대한 소개입니다.


기능

go-plugin 플러그인 시스템이 지원하는 기능은 다음과 같습니다:

  • Go 인터페이스 구현인 플러그인
    플러그인 시스템이 작성자와 사용자 간의 통신을 처리하기 때문에,
    동일한 프로세스에 있는 것처럼 플러그인을 호출합니다.
  • 여러 언어 지원
    플러그인은 여러 언어로 작성할 수 있습니다.
    이 라이브러리가 제공하는 gRPC 기반 플러그인을 사용하면 모든 언어로 플러그인을 작성할 수 있습니다.
  • 복잡한 인자와 리턴 값 지원
    이 라이브러리는 io.Reader/Writer 인터페이스와 같은 복잡한 인자와 리턴값을 처리할 수 있는 API를 제공합니다.
    이 기능을 위해 추가 인터페이스를 제공하거나 원시(raw) 데이터를 전송하기 위해 클라이언트/서버 간에
    연결을 생성하는 MuxBroker 라이브러리를 제공합니다.
  • 양방향 통신
    플러그인 시스템은 복잡한 인자를 지원하기 때문에, 호스트 프로세스가 인터페이스 구현을 보내고
    플러그인이 호스트 프로세스에서 이를 호출할 수 있습니다.
  • 내장된 로깅
    표준 log 라이브러리를 사용하는 모든 플러그인에는 호스트 프로세스로 자동으로 전송되는 로그 데이터가 있습니다.
    호스트 프로세스는 플러그인 바이너리 경로가 접두사로 붙은 이 출력을 미러링합니다.
    이렇게하면 플러그인을 사용한 디버깅이 간단해집니다.
    호스트 시스템이 hclog 를 사용하는 경우 로그 데이터가 구조화됩니다.
    플러그인도 hclog 사용하는 경우 플러그인의 로그가 호스트 hclog 로 전송되고 구조화됩니다.
  • 프로토콜 버전 관리
    이전 플러그인을 무효화하기 위해 증분되는(incremented) 매우 기본적인 “프로토콜 버전”이 지원됩니다.
    이는 인터페이스 서명이 변경되거나 프로토콜 수준의 변경이 필요한 경우 등의 상황에 유용합니다.
    프로토콜 버전이 호환되지 않으면 최종 사용자에게 친숙한 오류 메시지가 표시됩니다.
  • *Stdout / Stderr 동기화
    플러그인은 하위 프로세스(subprocess)이지만 평소와 같이 stdout/stderr을 계속 사용할 수 있으며
    출력은 호스트 프로세스로 다시 미러링됩니다.
    호스트 프로세스는 어떤 io.Writer 로 이 스트림을 보낼 지 컨트롤할 수 있습니다.
  • TTY 보존
    플러그인 하위 프로세스는 호스트 프로세스와 동일한 stdin 파일 디스크립터에 연결되어
    TTY가 필요한 소프트웨어가 작동하도록 합니다.
    예를 들어 플러그인은 ssh 를 실행할 수 있고 여러 하위 프로세스와 RPC가 발생하더라도
    최종 사용자에게 완벽하게 보이고 작동합니다.
  • 플러그인이 실행되는 동안 호스트 업그레이드
    플러그인을 “다시 연결(reattached)” 하여 플러그인이 실행되는 동안에도 호스트 프로세스를 업그레이드
    할 수 있습니다.
    이를 위해서는 호스트/플러그인이 이것이 가능하다는 것을 알고 적절히 데몬화해야 한다.
    NewClient는 ReattachConfig를 사용하여 다시 연결할지 여부와 방법을 결정합니다.
  • 암호화 보안 플러그인
    예상 체크섬으로 플러그인을 확인할 수 있으며 TLS를 사용하도록 RPC 통신을 구성할 수 있습니다.
    이 구성을 보호하려면 호스트 프로세스를 적절하게 보호해야합니다.




설계

HashiCorp 플러그인 시스템은 하위 프로세스를 시작하고 RPC(standard net/rpc 또는 gRPC 사용) 를 통해
통신하는 방식으로 작동합니다.
플러그인과 호스트 프로세스 간에는 단일 연결이 생성됩니다. net/rpc 기반 플러그인의 경우
연결 다중화 라이브러리를 사용하여 상위의 다른 모든 연결을 다중화하고,
gRPC 기반 플러그인의 경우 HTTP2 프로토콜이 멀티플렉싱을 처리합니다.

이 아키텍처에는 다음과 같은 여러 가지 장점이 있습니다:

  • 플러그인은 호스트 프로세스를 충돌시킬 수 없습니다:
    플러그인의 패닉은 플러그인 사용자를 당황시키지 않습니다.
  • 플러그인을 작성하기가 매우 쉽습니다:
    Go 애플리케이션을 작성하고 go build . 또는 여러 언어를 사용하여 go-plugin 을 지원하는
    약간의 코드로 gRPC 서버를 작성합니다.
  • 플러그인 설치가 매우 쉽습니다:
    호스트가 찾을 수 있는 위치에 바이너리를 넣으면(호스트에 따라 다르지만 이 라이브러리에서 툴을 제공합니다)
    나머지는 플러그인 호스트가 처리합니다.
  • 플러그인은 상대적으로 안전할 수 있습니다:
    플러그인은 프로세스의 전체 메모리 공간이 아닌 인터페이스와 인수에만 액세스 할 수 있습니다.
    또한 go-plugin 은 TLS를 통해 플러그인과 통신할 수 있습니다.




사용법

플러그인 시스템을 사용하려면 다음 단계를 수행해야 합니다.
이는 반드시 수행해야 하는 단계입니다.
예제는 examples/ 디렉토리에 있습니다.

  1. 플러그인에 대해 노출하려는 인터페이스를 선택합니다.
  2. 각 인터페이스에 대해 net/rpc 연결이나 gRPC 연결 또는 둘 다 사용하는 통신을 인터페이스를 구현합니다.
    클라이언트 및 서버 구현을 모두 구현해야합니다.
  3. 플러그인 유형에 대해 RPC 클라이언트/서버를 만드는 방법을 알고 있는 Plugin 구현을 만듭니다.
  4. 플러그인 작성자는 플러그인을 제공하기 위해 main 함수에서 plugin.Serve 를 호출합니다.
  5. 플러그인 사용자가 plugin.Client 를 사용하여 하위 프로세스를 시작하고 RPC를 통해 인터페이스 구현을
    요청합니다.

이게 전부입니다! 실제로 2 단계는 가장 지루하고 시간이 많이 걸리는 단계입니다.
그럼에도 불구하고 그리 어렵지 않으며 examples/ 디렉토리뿐만 아니라 다양한 오픈 소스 프로젝트 전체에서
예제를 볼 수 있습니다.

전체 API 문서는 GoDoc을 참조하세요.




로드맵

우리의 플러그인 시스템은 끊임없이 진화하고 있습니다. 새 프로젝트나 기존 프로젝트의 새 기능에
플러그인 시스템을 사용하면서 지속적으로 개선할 수있는 부분을 찾습니다.

이 시점에서 플러그인 시스템의 로드맵은 다음과 같습니다.

시맨틱 버전 관리

플러그인은 시맨틱 버전을 구현할 수 있습니다.
이 플러그인 시스템은 호스트 프로세스에 버전 제한을 위한 시스템을 제공합니다.
이것은 이미 존재하는 프로토콜 버전 관리에 더 크고 근본적인 변경 사항이 추가됩니다.




공유 라이브러리는 어떻습니까?

플러그인 사용을 시작했을 때 (2012 년 말, 2013 년 초) Go가 동적 라이브러리 로딩을 지원하지 않았기 때문에
RPC를 통한 플러그인이 유일한 옵션이었습니다.
현재 Go는 여러 제한 사항이 있는 plugin 표준 라이브러리를 지원합니다.
2012 년 이후로 이 플러그인 시스템은 수천만 명의 사용자들을 통해 안정화되었으며 개발자가 크게 가치를 두는
많은 장점이 있습니다.

예를 들어, 보안상의 이유로 동적 라이브러리 로드가 허용되지 않는 Vault 에서도 이 플러그인 시스템을 사용합니다.
이것은 극단적인 예이지만, 우리는 라이브러리 시스템이 동적 라이브러리 로딩보다 더 많은 장점을 가지고 있다고
믿고 있으며, 우리가 수년간 구축하고 테스트했기 때문에 계속 사용할 것입니다.

공유 라이브러리는 이 시스템에 비해 훨씬 높은 성능이라는 한 가지 주요 장점이 있습니다.
다양한 도구의 실제 시나리오에서 플러그인 시스템의 성능이 더 이상 필요하지 않았고 처리량이 매우 높으므로
현재로서는 문제가 되지 않습니다.




예제

Basic 예제

plugin.Plugin 을 구현하여 serve/consume 을 실행합니다.
plugin.Plugin는 두 개의 메서드가 있습니다:

  • Server 는 이 플러그인 타입의 RPC server를 리턴합니다.
    여기서는 GreeterRPCServer 를 리턴합니다.
  • Client 는 RPC client를 통해 통신하는 우리의 인터페이스 구현을 리턴합니다.
    여기서는 GreeterRPC 를 리턴합니다.

여기서MuxBroker는 무시합니다.
이는 플러그인 연결에서 더 많은 다중 스트림을 생성해야할 경우에 사용됩니다.




gRPC를 사용한 KV 예제




참고자료


그림. go-plugin 구조.


gRPC(gRPC Remote Procedure Calls)는 구글이 개발한 오픈 소스 RPC(원격 프로시저 호출) 시스템입니다.
HTTP/2 로 전송하고, 인터페이스 정의 언어로 프로토콜 버퍼를 사용하며 인증, 양방향 스트리밍 및 흐름 제어,
블로킹 및 넌블로킹 바인딩, 취소 및 타임아웃 등의 기능을 제공합니다.(from 위키피디아 GRPC)

댓글남기기