Kong golang plugin

kong gateway는 https://pkg.go.dev/github.com/Kong/go-pdk 을 통해서 golang plugin을 지원한다. 이를 통해서 kong에서는 지원하지 않는 새로운 기능들을 추가할 수 있다.

물론 plugin을 만들 바에 kong을 왜 써야하는 지는 모르겠다.

How to build golang plugin

https://docs.konghq.com/gateway/latest/plugin-development/pluginserver/go/

docs에서 안내하는 방법은 다음과 같다.

  1. configuration을 갖는 구조체를 만든다.
  2. 해당 configuration 구조체를 생성하는 New()함수를 만든다.
  3. 각 phase를 처리하는 handle method를 구조체에 추가한다.
  4. go-pdk/server를 사용하면 다양한 도움을 받을 수 있다.
  5. main() function call에는 server.StartServer(New, Version, Priority)를 추가해야한다.
  6. go build를 통해서 executable한 binary로 compile하도록 한다.

plugin example들은 다음과 같다. https://github.com/Kong/go-plugins

Configuration

kong의 Admin API나 data store로부터 전달된 data를 저장할 공간이 필요한데, 이것이 바로 configuration 구조체이다. 즉, kong에서부터 오는 data를 저장하는 구조체라고 생각하면 된다.

type MyConfig struct {
    Path string `json:"path"`
    Reopen bool `json:"reopen"`
}

참고로 configuration value 옆에 tag로 붙은 이름은 추후에 config파일을 통해서 configuration값들을 설정할 때, 쓰이는 key값으로 생각하면 된다.

다음으로 New함수를 추가하여 configuration 구조체를 생성하도록 한다. 이 때 조심해야할 것이 return type을 interface로 해주도록 한다.

func New() interface{} {
    return &MyConfig{}
}

마지막으로 main() 함수에 go-pdkserver를 시작해주면 된다.

func main() {
    server.StartServer(New, Version, Priority)
}

주의: main에는 stdout으로 나가는 어떠한 log도 작성해서는 안된다. 만약 plugin을 적재했는데 Not a plugin info table error가 발생했다면 main함수에 쓴 logstdout으로 나왔기 때문에 발생했을 확률이 높다.

이는 kong이 plugin을 실행할 때, 아래에 나올 dump라는 명령어로 plugin의 정보를 얻어오는데, 여기서 main함수를 호출한다. 문제는 main함수가 호출되면서 plugin정보가 담긴 json table을 얻어오는데, 이 과정에서 main에 log가 있다면 stdout에 json data뿐만 아니라 log도 출력되어 kong lua에서 error를 발생시킨다. 정확히는 kong/runloop/plugin_servers/process.lua:145 code를 참고하면 된다.

현재까지의 전체 code를 정리하면 다음과 같다.

package main

type MyConfig struct {
    Path string `json:"path"`
    Reopen bool `json:"reopen"`
}

func New() interface{} {
	return &MyConfig{}
}

var Version = "0.1.0"
var Priority = 1

func main() {
	server.StartServer(New, Version, Priority)
}

이 상태에서 빌드를 해보자.

go build .

executable한 binary가 나온 것을 볼 수 있다.

go-pdk를 사용하게 됨으로써 이제 우리의 plugin은 몇 가지 명령어들을 내재할 수 있게되는데, 이를 확인하기 위해 binary에 -h옵션을 주어 실행시켜보도록 하자.

./go-plugin -h
Usage of ./go-plugin:
  -dump
        Dump info about plugins
  -help
        Show usage info
  -kong-prefix string
        Kong prefix path (specified by the -p argument commonly used in the kong cli) (default "/usr/local/kong")

어떠한 옵션도 주지 않고 해당 plugin을 실행하면 socket file을 kong-prefix directory안에 plugin의 이름 뒤에 .socket을 붙여 생성한다. 가령 go-plugin의 경우 /usr/local/kong/go-plugin.socket으로 socket file을 생성한다.

phase handlers

kong에는 여러 phase가 존재하는데, configuration을 담은 구조체에 method를 구현함으로서, 각 phase에 특정 logic을 실행시킬 수 있다.

가령 access phase에 대해서는 Access handler를 만들어 원하는 logic을 실행시킬 수 있다.

func (conf *MyConfig) Access (kong *pdk.PDK) {
 ...
}

이외에도 다음과 같은 phase들이 있다.

  • Certificate: TLS connection이 이루어졌을 때 발생하는 phase이다. 이 단계에서의 context value는 다음 단계까지 가지 못한다.
  • Rewrite: certificate다음 단계로 router가 실행되기 직전이다.
  • Access: router가 실행된 이후로, route와 매칭된 service의 정보를 전달해준다. 이 단계에서 plugin의 추가적인 기능들을 덧붙일 수 있다.
  • Response: Upstream으로 전달된 request의 응답을 처리하는 단계이다. kong에서 peer들에게 응답을 전달하기 전에 처리하는 phase이다.
  • Preread: access단계와 유사하게 preread phase는 TCP 또는 UDP connection단계와 동일하다. 즉, plugins이 TCP와 UDP protocol을 다루는데 도움을 준다.
  • Log: response가 전달된 이후에 실행되는 단계이다.

참고로, method signature는 모두 똑같다.

func (m *MyConfig) Certificate(kong *pdk.PDK) {
	...
}

func (k *MyConfig) Rewrite(kong *pdk.PDK) {
	...
}

func (k *MyConfig) Access(kong *pdk.PDK) {
	...
}

func (k *MyConfig) Response(kong *pdk.PDK) {
	...
}

func (k *MyConfig) Preread(kong *pdk.PDK) {
	...
}

func (k *MyConfig) Log(kong *pdk.PDK) {
  ...
}

pdk.PDK를 사용하면 전달된 request의 path와 method, body, header 등을 모두 알 수 있다.

func (k *MyConfig) Access(kong *pdk.PDK) {
  ...
	service, _ := kong.Router.GetService()
  kong.Log.Info("service host: ", service.Host)
  ...
}

위와 같이 log를 작성할 때는 반드시 pdk에서 제공하는 Log를 사용해야한다. 그래야 kong proxy의 stdout, stderr에 우리의 log가 찍힌다.

또한, kong.Response.Exit()을 통해서 응답을 보낼 수도 있다.

func (k *MyConfig) Access(kong *pdk.PDK) {
  ...
	kong.Response.Exit(statusCode, body, header)
  return
}

Access에서 Response.Exit을 실행하면 응답을 보내고, upstream으로는 요청을 보내지 않는다. 참고로, pdk로 response를 보내지 않으면 알아서 upstream으로 request를 전달한다.

예제들을 참고하면 좋다. https://github.com/Kong/go-plugins/blob/master/go-hello.go

Example configuration

이제 kong에 우리의 golang plugin을 load하는 방법이다. 만약 plugin의 이름이 my-pluginother-one이라면 다음과 같이 predefine된 configuration을 사용하면 된다.

  • kong.conf
pluginserver_names = my-plugin,other-one

pluginserver_my_plugin_socket = /usr/local/kong/my-plugin.socket
pluginserver_my_plugin_start_cmd = /usr/local/bin/my-plugin
pluginserver_my_plugin_query_cmd = /usr/local/bin/my-plugin -dump

pluginserver_other_one_socket = /usr/local/kong/other-one.socket
pluginserver_other_one_start_cmd = /usr/local/bin/other-one
pluginserver_other_one_query_cmd = /usr/local/bin/other-one -dump

위의 configuration에서 my_plugin, other-one부분은 우리가 만든 golang plugin 이름이라는 것을 잊지 말도록 하자.

참고로, socket과 start부분은 default값과 다를바없으니 생략이 가능하다. 따라서 정리하면 다음과 같다.

pluginserver_names = my-plugin,other-one
pluginserver_my_plugin_query_cmd = /usr/local/bin/my-plugin -dump
pluginserver_other_one_query_cmd = /usr/local/bin/other-one -dump

docker

docker경우는 kong image에 plugin binary를 추가해야한다. 즉, 우리의 golang plugin을 먼저 빌드한 다음에 해당 binary를 kong image의 특정 위치에 넣어주어야 한다는 것이다.

얼핏보면 어려워보이지만 Dockerfile을 보면 그닥 어렵지 않다.

# STAGE-1
# Build the kong plugin
FROM golang:1.19-alpine3.15 AS go-plugin

WORKDIR /builder

COPY . .

RUN go build -o ./my-plugin .
RUN chmod 755 ./my-plugin
RUN ./my-plugin -dump

# STAGE-2
# Build kong including the plugin that already build in previous stage
FROM kong:3.1.0-alpine

COPY --from=go-plugin /builder/my-plugin /kong/plugins/
RUN /kong/plugins/my-plugin -dump

USER kong

두개의 phase로 되어있는 것을 확인할 수 있다. 첫번째 단계에서 우리의 plugin을 빌드하고, 해당 binary를 kong image의 특정 directory에 삽입하는 것이다.

kubernetes, helm

kubernetes환경은 조금 다른데, 정확히는 helm으로 배포한 경우이다. https://github.com/Kong/charts

kong helm repo를 통해서 배포한 사람들은 위의 plugin을 아무리 동일한 environment로 주어도 실행이되지 않는다. 이는 kubernetes에서의 security와 관련이 깊은데, root환경에서 read-only권한만 주었기 때문에 socket을 읽고 쓰지 못하는 것이다. 이를 위해서 kong helm에서는 kong_prefix라는 것을 준비했는데, 이곳에 우리의 plugin socket을 넣어주면 된다. 여기 나와 똑같이 고생한 사람이 있다. https://tech.aufomm.com/debug-a-kong-go-plugin-on-kubernetes/

또한, plugin을 실행하는 start_cmd, query_cmd에서도 -kong-prefix으로 /kong_prefix/를 넣어주어야 한다. 참고로, /kong_prefix/는 환경변수를 어떻게 설정했는 가에 따라 다를 수 있으므로 개개인이 알아서 확인하여 넣도록 하자. KONG_PREFIX라는 환경변수로 설정되어있을 확률이 놓다. 필자의 경우는 /kong_prefix/였다.

helm upgrade -i my-kong kong/kong \
  -n kong --create-namespace --values - <<EOF
  image:
    repository: kong-my-plugin
    tag: 3.3-ubuntu
  env:
    log_level: debug
    plugins: bundled,my-plugin
    pluginserver_names: my-plugin
    pluginserver_go_hello_start_cmd: /kong/plugins/my-plugin -kong-prefix /kong_prefix/
    pluginserver_go_hello_query_cmd: /kong/plugins/my-plugin -dump -kong-prefix /kong_prefix/
    pluginserver_go_hello_socket: /kong_prefix/my-plugin.socket
  admin:
    enabled: true
    http:
      enabled: true
  ingressController:
    installCRDs: false
EOF

마지막으로 우리의 configuration 구조체의 값을 kubernetes의 kong plugin CRD를 통해서 채워줄 수 있는 방법이 있는데, 다음과 같다.

우리의 경우 configuration 구조체의 값이 다음과 같다.

type MyConfig struct {
    Path string `json:"path"`
    Reopen bool `json:"reopen"`
}

이 값들을 KongClusterPlugin을 통해서 채워줄 수 있다.

apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
  name: my-plugin-config
  namespace: kong
  annotations:
    kubernetes.io/ingress.class: kong
  labels:
    global: "true"
config:
  path: "/kong_prefix/"
  reopen: false
plugin: my-plugin

실행해보면 pathrepoen값을 kong plugin CRD를 보고 채워줄 것이다.

결론

OAuth기능을 원하는 로직으로 만들기 위해서 OAuth plugin까지 만들어본 결과, 굳이 plugin까지 직접 만들어서 kong을 쓸 정도로 kong이 좋은 것 같진 않다. 간단하게만 kong을 쓴다면 추천하고 싶지만, 깊이 쓰게되는 순간부터는 opensource error가 너무 많고, kong을 이루는 lua script들도 생각보다 너무 엉성하다. 또한 유지 보수도 잘 안되고 있는 것이 kong의 현실이다. 사실 완벽한 opensource가 어디있겠나 싶지만 kong을 추천하고 싶지 않은 가장 큰 이유 중 하나는 kong에 대한 resource가 너무 부족하다. docs가 잘되어 있는 것처럼 보이지만, 막상 써보면 군데군데 비어있는 곳들이 너무 많고, 꼭 만들어줘야 하는 또는 꼭 설정해주어야 하는 것들에 대해서 docs에 없는 것들이 너무 많다. 이러한 내용들은 github issue나 문제 대응에 대한 답변에 간단히 써놓은 것들을 일일히 찾아야 한다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN