Go 애플리케이션을 위한 서비스 메시 네이티브 통합
Go 애플리케이션을 위한 서비스 메시 네이티브 통합
이 문서에서는 대부분의 Go 애플리케이션에 Consul 서비스 메시를 드롭인(drop-in) 방식으로 쉽게 통합할 수 있게 해 주는 라이브러리를 소개해 드려요. 이 라이브러리를 사용해 메시 기반 연결을 수락하거나 수립하는 통합 예시를 보여드립니다. 대부분의 Go 애플리케이션에서 Consul 서비스 메시는 import와 구조체 초기화를 제외하고 단 한 줄의 코드로 네이티브하게 통합할 수 있어요.
여기에 더해, 서비스 메시 네이티브 통합 개요도 꼭 읽고 이해해 주세요. 특히 애플리케이션을 Consul 서비스 메시에 네이티브하게 통합한 후에는, 서비스 정의를 통해 메시 기반 연결을 수락한다고 선언해야 합니다.
이 문서와 Go API 전반에서 connect라는 명사는 Consul의 서비스 메시 기능을 제공하는 connect 하위 시스템을 가리키는 데 사용됩니다.
출처: 문서
본문
연결 수락하기
참고: 들어오는 연결에서
ConnectAuthorize()를 호출할 때, 일치하는 인텐션(intention)에Permissions가 정의되어 있으면 이 라이브러리는 deny를 반환해요. 현재 이 메서드는 네트워크 계층 4(예: TCP) 통합에만 적합합니다.
TLS를 지원하는 모든 서버(HTTP, gRPC, net/rpc 등)는 몇 줄의 코드만으로 메시 기반 연결을 수락하기 시작할 수 있어요. 대부분의 기존 애플리케이션에서 서버를 메시 기반 연결을 수락하도록 변환하려면 import와 구조체 초기화를 제외하고 한 줄만 변경하면 됩니다.
Go 라이브러리는 Consul과 자동으로 통신하여 인증서를 로드하고 TLS 핸드셰이크 중에 인바운드 연결을 승인하는 *tls.Config를 노출해요. 또한 변경되는 인증서를 갱신하기 위해 goroutine을 자동으로 시작합니다.
예시를 먼저 보여드리고, 이어서 자세한 설명을 드릴게요.
import (
"net/http"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/connect"
)
func main() {
// Create a Consul API client
client, _ := api.NewClient(api.DefaultConfig())
// Create an instance representing this service. "my-service" is the
// name of _this_ service. The service should be cleaned up via Close.
svc, _ := connect.NewService("my-service", client)
defer svc.Close()
// Creating an HTTP server that serves via service mesh
server := &http.Server{
Addr: ":8080",
TLSConfig: svc.ServerTLSConfig(),
// ... other standard fields
}
// Serve!
server.ListenAndServeTLS("", "")
}
첫 번째 단계는 Consul API 클라이언트를 만드는 일이에요. 로컬 에이전트와 통신하려 하므로 거의 항상 ACL 토큰이 설정된 기본 구성이 사용됩니다. 기본 구성은 환경 변수에서 ACL 토큰을 설정했다면 그 토큰도 읽어요. Go 라이브러리는 이 클라이언트를 사용해 인증서를 요청하고, 연결을 승인하는 등의 작업을 수행합니다.
다음으로 connect.NewService를 호출해 현재 실행 중인 서비스를 나타내는 서비스 구조체를 만듭니다. 이 구조체는 연결을 수락하고 수립하기 위한 모든 상태를 유지해요. 애플리케이션은 일반적으로 하나의 서비스를 만들고 모든 서버와 클라이언트에서 그 서비스를 재사용해야 합니다.
마지막으로 표준 *http.Server를 생성해요. 핵심이 되는 줄은 TLSConfig 값입니다. 이것은 서비스 구조체가 반환한 TLS 구성으로 설정됩니다. 이 TLS 구성은 백그라운드에서 자동으로 인증서를 로드하고 캐시하며 인바운드 연결을 승인하도록 구성되어 있어요. 서비스 구조체는 인증서가 변경될 경우 백그라운드에서 이를 갱신하기 위한 blocking query 유지를 자동으로 처리합니다.
서비스가 표준 *tls.Config를 반환하므로, TLS를 지원하는 모든 서버에서 구성할 수 있어요. 여기에는 gRPC, net/rpc, 기본 TCP 등이 포함됩니다. 아래는 일반적인 TLS 리스너만 사용하는 또 다른 예시예요.
import (
"crypto/tls"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/connect"
)
func main() {
// Create a Consul API client
client, _ := api.NewClient(api.DefaultConfig())
// Create an instance representing this service. "my-service" is the
// name of _this_ service. The service should be cleaned up via Close.
svc, _ := connect.NewService("my-service", client)
defer svc.Close()
// Creating an HTTP server that serves via service mesh
listener, _ := tls.Listen("tcp", ":8080", svc.ServerTLSConfig())
defer listener.Close()
// Accept
go acceptLoop(listener)
}
HTTP 클라이언트
HTTP 기반 업스트림 의존성에 연결해야 하는 Go 애플리케이션의 경우, Go 라이브러리는 Consul 기반 서비스 디스커버리가 사용되는 한 메시 기반 연결을 자동으로 수립하는 *http.Client를 구성할 수 있어요.
예시를 먼저 보여드리고, 이어서 자세한 설명을 드릴게요.
import (
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/connect"
)
func main() {
// Create a Consul API client
client, _ := api.NewClient(api.DefaultConfig())
// Create an instance representing this service. "my-service" is the
// name of _this_ service. The service should be cleaned up via Close.
svc, _ := connect.NewService("my-service", client)
defer svc.Close()
// Get an HTTP client
httpClient := svc.HTTPClient()
// Perform a request, then use the standard response
resp, _ := httpClient.Get("https://userinfo.service.consul/user/mitchellh")
}
첫 번째 단계는 Consul API 클라이언트와 서비스를 만드는 일이에요. 이는 연결 수락과 동일한 단계이며 위 섹션에서 자세히 설명했습니다. 애플리케이션이 클라이언트이자 서버인 경우, API 클라이언트와 서비스 구조체를 모두 공유하고 재사용할 수 있어요.
다음으로 svc.HTTPClient()를 호출해 특별히 구성된 *http.Client를 반환받습니다. 이 클라이언트는 Consul 서비스 디스커버리를 사용해 메시 기반 연결을 자동으로 수립해요.
마지막으로 가상의 userinfo 서비스에 HTTP GET 요청을 수행합니다. HTTP 클라이언트 구성은 올바른 클라이언트 인증서를 자동으로 전송하고, 서버 인증서를 검증하며, 필요에 따라 인증서를 갱신하는 백그라운드 goroutine을 관리해요.
애플리케이션이 이미 수동으로 구성된 *http.Client를 사용하고 있다면, svc.HTTPDialTLS 함수로 http.Transport.DialTLS 필드를 설정해 동일한 동작을 얻을 수 있습니다.
호스트 이름 요구 사항
요청 URL에 사용되는 호스트 이름은 대상에 대한 논리적 서비스 디스커버리 메커니즘을 식별하는 데 사용돼요. 실제로 DNS를 통해 해석되는 것이 아니라 Consul 서비스 디스커버리 메커니즘의 논리적 식별자로 사용됩니다. 다음과 같은 구체적인 제한이 있어요.
- 체계(scheme)는 반드시
https://여야 합니다. - 다음 형태 중 하나의 Consul DNS 이름이어야 해요.
<name>.service[.<datacenter>].consul은 특정 서비스의 정상(healthy) 서비스 인스턴스를 디스커버리하고,<name>.query[.<datacenter>].consul은 Prepared Query를 통해 인스턴스를 디스커버리합니다. - 클러스터의 DNS 인터페이스에 커스텀 도메인이 구성되어 있어도 최상위 도메인은 반드시
.consul이어야 해요. 이는 향후 완화될 수 있습니다. - 서비스에 대한 태그 필터는 현재 지원되지 않지만(예:
tag1.web.service.consul), prepared query를 사용해 동일한 동작을 얻을 수 있어요. - 외부 DNS 이름, 원시 IP 주소 등은 오류를 발생시키므로 별도의
HTTPClient로 가져와야 합니다.
원시 TLS 연결
원시 net.Conn TLS 연결의 경우 svc.Dial 함수를 사용할 수 있어요. 이 함수는 서비스 메시를 통해 원하는 서비스로 연결을 수립하고 net.Conn을 반환합니다. 그 후 이 연결을 원하는 대로 사용할 수 있어요.
import (
"context"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/connect"
)
func main() {
// Create a Consul API client
client, _ := api.NewClient(api.DefaultConfig())
// Create an instance representing this service. "my-service" is the
// name of _this_ service. The service should be cleaned up via Close.
svc, _ := connect.NewService("my-service", client)
defer svc.Close()
// Connect to the "userinfo" Consul service.
conn, _ := svc.Dial(context.Background(), &connect.ConsulResolver{
Client: client,
Name: "userinfo",
})
}
이 예시는 익숙한 Dial 형태의 함수를 사용해 원시 net.Conn 값을 수립해요. dial의 두 번째 매개변수는 connect.Resolver 인터페이스의 구현입니다. 위 예시는 *connect.ConsulResolver 구현을 사용해 Consul 기반 서비스 디스커버리를 수행해요. 또한 원격 서비스가 제공해야 하는 올바른 인증서 메타데이터도 자동으로 결정합니다.
정적 주소, 커스텀 리졸버
원시 TLS 연결 예시에서 connect.Resolver 구현의 사용을 볼 수 있어요. 이 인터페이스는 주소 해석을 수행하도록 구현할 수 있습니다. 이는 주소와 함께 원격 서비스가 제공하는 TLS 인증서에서 기대되는 URI SAN도 반환해야 해요.
Go 라이브러리는 두 가지 내장 리졸버를 제공합니다.
*connect.StaticResolver는 서비스 디스커버리가 필요 없는 정적 주소에 사용할 수 있어요. 기대되는 인증서 URI SAN은 수동으로 지정해야 합니다.*connect.ConsulResolver는 Consul API를 통해 서비스와 prepared query를 해석해요. 또한 기대되는 인증서 URI SAN도 자동으로 결정합니다.