kong gateway는 https://pkg.go.dev/github.com/Kong/go-pdk 을 통해서 golang plugin을 지원한다. 이를 통해서 kong에서는 지원하지 않는 새로운 기능들을 추가할 수 있다.
물론 plugin을 만들 바에 kong을 왜 써야하는 지는 모르겠다.
https://docs.konghq.com/gateway/latest/plugin-development/pluginserver/go/
docs에서 안내하는 방법은 다음과 같다.
New()
함수를 만든다.go-pdk/server
를 사용하면 다양한 도움을 받을 수 있다.main()
function call에는 server.StartServer(New, Version, Priority)
를 추가해야한다.go build
를 통해서 executable한 binary로 compile하도록 한다. plugin example들은 다음과 같다. https://github.com/Kong/go-plugins
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-pdk
의 server
를 시작해주면 된다.
func main() {
server.StartServer(New, Version, Priority)
}
주의: main
에는 stdout으로 나가는 어떠한 log도 작성해서는 안된다. 만약 plugin을 적재했는데 Not a plugin info table
error가 발생했다면 main
함수에 쓴 log
가 stdout
으로 나왔기 때문에 발생했을 확률이 높다.
이는 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을 생성한다.
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
이제 kong
에 우리의 golang plugin을 load하는 방법이다. 만약 plugin의 이름이 my-plugin
과 other-one
이라면 다음과 같이 predefine된 configuration을 사용하면 된다.
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경우는 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으로 배포한 경우이다. 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
실행해보면 path
와 repoen
값을 kong plugin CRD를 보고 채워줄 것이다.
OAuth기능을 원하는 로직으로 만들기 위해서 OAuth plugin까지 만들어본 결과, 굳이 plugin까지 직접 만들어서 kong을 쓸 정도로 kong이 좋은 것 같진 않다. 간단하게만 kong을 쓴다면 추천하고 싶지만, 깊이 쓰게되는 순간부터는 opensource error가 너무 많고, kong을 이루는 lua script들도 생각보다 너무 엉성하다. 또한 유지 보수도 잘 안되고 있는 것이 kong의 현실이다. 사실 완벽한 opensource가 어디있겠나 싶지만 kong을 추천하고 싶지 않은 가장 큰 이유 중 하나는 kong에 대한 resource가 너무 부족하다. docs가 잘되어 있는 것처럼 보이지만, 막상 써보면 군데군데 비어있는 곳들이 너무 많고, 꼭 만들어줘야 하는 또는 꼭 설정해주어야 하는 것들에 대해서 docs에 없는 것들이 너무 많다. 이러한 내용들은 github issue나 문제 대응에 대한 답변에 간단히 써놓은 것들을 일일히 찾아야 한다.