🤷♂️
Load Balancer에 대해 알아보던 중 Nginx가 L7 Layer에서 Load Balancing 기능을 하는 것을 보고 프로젝트에 nginx를 통해 reverse proxy 설정을 했던 것이 떠올랐다.
그 당시 nginx를 통해 https 통신이 가능하게 끔 설정하는 것과 route 별로 다른 서버에 전달되도록 설정하는 것 때문에 고생했던 게 기억나는데 지금 보니 별것 아닌 것 같은데?라는 생각이 들어 무작정 따라 해보기로 했다. (별것 아닌 게 아니었다.)
Github: https://github.com/Son0-0/Goginx
구현하기 전 Load Balancer에 대해 공부하고 있던 중이라 Load Balancer에 대해 알아보고 정리했다.
로드 밸런싱은 애플리케이션을 지원하는 리소스 풀 전체에 네트워크 트래픽을 균등하게 배포하는 방법
그렇다면 Reverse Proxy는 무엇일까?
평소 Proxy를 사용해 미국 ip로만 접속 가능한 쇼핑몰에 자주 접속해서 둘러보았기 때문에 Proxy라는 단어는 익숙했다.
그렇지만 Proxy가 어떻게 동작하는지 알게 된 건 정글 과정 중 Proxy Server를 간단하게 구현했을 때 어느 정도 알게 되었다. Github 링크
Nginx를 사용해 본 경험이 있기 때문에 대충 어떤 식으로 동작하는지 "알고만" 있다. 여러 검색어를 통해 본 글에서 Golang으로 Reverse Proxy Server를 구현하는 글이 있길래 참고하여 Nginx의 기능을 따라 해보기로 했다.
참고자료: https://dev.to/b0r/implement-reverse-proxy-in-gogolang-2cp4#s1
package handlers
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"runtime"
"time"
)
var OS = runtime.GOOS
type PortNumHandler struct {
PortNum string
}
func (pn *PortNumHandler) Handler(rw http.ResponseWriter, req *http.Request) {
fmt.Printf("[reverse proxy server] received request at: %s\n", time.Now())
originServerURL, err := url.Parse("http://localhost:" + pn.PortNum)
if err != nil {
log.Fatal("invalid origin serer URL")
}
// set req Host, URL and Request URI to foward a request to the origin server
req.Host = originServerURL.Host
req.URL.Host = originServerURL.Host
req.URL.Scheme = originServerURL.Scheme
req.RequestURI = ""
// save the response from the origin server
originServerResponse, err := http.DefaultClient.Do(req)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
_, _ = fmt.Fprint(rw, err)
return
}
// return response to the client
switch OS {
case "windows":
OS = "Windows"
case "darwin":
OS = "macOS"
case "linux":
OS = "Linux"
}
rw.Header().Set("Server", fmt.Sprintf("Goginx (%s)", OS))
rw.WriteHeader(http.StatusOK)
io.Copy(rw, originServerResponse.Body)
}
프로젝트 진행시 nginx를 통해 특정 route로 들어온 요청은 특정 port 번호로 실행시킨 웹 서버로 요청을 전달해 준 경험이 있다.
위 코드는 해당 기능을 따라한 것인데, Handler가 각자 portNumber를 가질 수 있게 구현하였다. 또한 Client로 Response를 전달할 때 Server 헤더를 추가하여 Nginx의 응답과 비슷하게 작성하였다.
nginx
Goginx
package main
import (
"fmt"
"log"
"net/http"
"github.com/Son0-0/Goginx/handlers"
)
func main() {
target1Handler := &handlers.PortNumHandler{PortNum: "8081"}
http.HandleFunc("/target1/", target1Handler.Handler)
target2Handler := &handlers.PortNumHandler{PortNum: "8082"}
http.HandleFunc("/target2/", target2Handler.Handler)
fmt.Println("Goginx Running")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
// * HTTPS 통신
// err := http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)
// if err != nil {
// log.Fatal("ListenAndServe: ", err)
// }
}
Let's Encrypt를 통해 발급받은 SSL 인증서 정보를 활용하여 https 통신이 가능하도록 구현하였다. (local에서 테스트할 경우 해당 인증서로 https 통신이 불가능하여 주석 처리하였다.)
"cert.pem", "key.pem" 파일의 경우 main.go를 실행시키는 유저가 인증서 파일에 접근할 수 있도록 권한을 부여해 주어야만 파일에 접근할 수 있는데 이는 보안상 위험하니 테스트할 때만 권한을 잠시 풀어주는 것이 좋을 것 같다. (sudo chmod 707 cer.pem 명령어로 유저에게 파일 접근 권한 부여)
또한 작성한 웹 서버는 443 포트 번호로 서버를 실행하지 못하는데(1024번 이하 포트는 특정 권한이 있어야 한다.) 이를 해결하기 위해 443 포트로 들어오는 요청을 8443번으로 포트 포워딩 하는 작업을 해주어야 한다.
sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
각각의 요청이 8081 포트, 8082 포트로 전달되었고 그에 대한 응답 또한 전달받을 수 있다.
Server 헤더의 경우 Goginx (특정 OS)로 설정되어 있는 것을 확인할 수 있다.