[Go] Go로 Nginx 따라하기

🧠·2022년 10월 29일
2

Go

목록 보기
4/5
post-thumbnail

🤷‍♂️

Load Balancer에 대해 알아보던 중 Nginx가 L7 Layer에서 Load Balancing 기능을 하는 것을 보고 프로젝트에 nginx를 통해 reverse proxy 설정을 했던 것이 떠올랐다.

그 당시 nginx를 통해 https 통신이 가능하게 끔 설정하는 것과 route 별로 다른 서버에 전달되도록 설정하는 것 때문에 고생했던 게 기억나는데 지금 보니 별것 아닌 것 같은데?라는 생각이 들어 무작정 따라 해보기로 했다. (별것 아닌 게 아니었다.)

Github: https://github.com/Son0-0/Goginx

Load Balancer

구현하기 전 Load Balancer에 대해 공부하고 있던 중이라 Load Balancer에 대해 알아보고 정리했다.

로드 밸런싱은 애플리케이션을 지원하는 리소스 풀 전체에 네트워크 트래픽을 균등하게 배포하는 방법

참고

L4 Layer Load Balancer

  • L4 Layer (Transport Layer) 수준의 Load Balancing
  • IP 주소와 포트 번호 부하 분산이 가능
  • 예) iptables 명령어를 통한 포트 포워딩 (맞는지 모르겠다...)

L7 Layer Load Balancer

  • L7 Layer (Application Layer) 수준의 Load Balancing
  • URL 또는 HTTP 헤더에서 부하 분산이 가능
  • 예) Nginx를 통한 Reverse Proxy

Reverse Proxy

그렇다면 Reverse Proxy는 무엇일까?

평소 Proxy를 사용해 미국 ip로만 접속 가능한 쇼핑몰에 자주 접속해서 둘러보았기 때문에 Proxy라는 단어는 익숙했다.

그렇지만 Proxy가 어떻게 동작하는지 알게 된 건 정글 과정 중 Proxy Server를 간단하게 구현했을 때 어느 정도 알게 되었다. Github 링크

  • Forward Proxy
    - Client와 서버 사이에 존재하며, Client가 어플리케이션 서버로 보내는 요청은 프록시 서버를 거쳐 전달된다. 이 때문에 서버에서 받는 IP는 Client IP가 아닌 프록시 서버 IP이기 때문에 어플리케이션 서버는 Client가 누군지 알 수 없다.
    - 캐싱 / IP 우회 / 제한 등의 기능을 한다.
    - 예) 한국에서 미국 IP로만 접속 가능한 사이트에 접근할 때 미국 프록시 서버를 거쳐 요청 => 어플리케이션 서버는 이 요청이 미국으로부터 왔다고 생각한다.
  • Reverse Proxy
    - 서버와 어플리케이션 서버 사이에 존재하며 Client가 어플리케이션 서버에 요청을 보내면 Reverse Proxy 서버가 해당 요청을 받아 어플리케이션 서버에 전달 후 응답을 받고, 그 응답을 다시 Client로 전달해 준다.
    • 로드 밸런싱 / 보안 등의 기능을 한다.
    • 예) 웨이터가 가져다준 음식을 고객은 어느 요리사로부터 요리된 지 모른다. 이때 웨이터가 리버스 프록시 역할 (맞는 비유인지 모르겠다.)

Go로 Nginx 따라하기

Nginx를 사용해 본 경험이 있기 때문에 대충 어떤 식으로 동작하는지 "알고만" 있다. 여러 검색어를 통해 본 글에서 Golang으로 Reverse Proxy Server를 구현하는 글이 있길래 참고하여 Nginx의 기능을 따라 해보기로 했다.

참고자료: https://dev.to/b0r/implement-reverse-proxy-in-gogolang-2cp4#s1

요구 사항

  • Reverse Proxy 기능
  • HTTPS 통신

코드

Reverse Proxy 기능

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

HTTPS 통신

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)로 설정되어 있는 것을 확인할 수 있다.

한계

  • 아직까지 간단한 REST API 에서만 작동하는 것을 확인했다.
  • 308, 302 Redirect 되는 경우 자동으로 Redirect 되지 않는다.
  • Socket 통신이 안된다.

Reference

profile
: )

0개의 댓글