Workhorse를 위한 웹소켓 채널 지원

일부 상황에서 GitLab은 웹소켓을 통해 다음과 같은 기능을 제공할 수 있습니다.

  • 브라우저 내 터미널을 통한 환경 접근: 실행 중인 서버 또는 컨테이너에 프로젝트가 배포된 경우.
  • CI에서 실행 중인 서비스에 대한 접근.

Workhorse는 웹소켓 업그레이드 및 웹소켓 연결을 관리하여 GitLab이 다른 요청을 처리할 수 있도록 해줍니다. 본 문서에서는 이러한 연결의 아키텍처에 대해 설명합니다.

웹소켓 소개

웹소켓은 “업그레이드된” HTTP/1.1 요청입니다. 이를 통해 클라이언트와 서버 간 양방향 통신이 가능합니다. 웹소켓은 HTTP가 아닙니다. 클라이언트는 언제든지 메시지(프레임이라고도 함)를 서버로 보낼 수 있으며, 반대로도 가능합니다. 클라이언트 메시지는 반드시 요청이 아니며, 서버 메시지 역시 반드시 응답이 아닐 수 있습니다. 웹소켓 URL은 ws://(암호화되지 않음) 또는 wss://(TLS로 보호됨)와 같은 스키마를 가집니다.

웹소켓을 요청할 때, 브라우저는 다음과 같은 HTTP/1.1 요청을 보냅니다:

GET /path.ws HTTP/1.1
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Protocol: terminal.gitlab.com
# 보안 조치를 포함한 다양한 헤더들

이 시점에서 연결은 여전히 HTTP 상태이므로 이는 요청입니다. 서버는 404 Not Found 또는 500 Internal Server Error와 같은 표준 HTTP 응답을 보낼 수 있습니다.

서버가 업그레이드를 허용하기로 결정하면 HTTP 101 Switching Protocols 응답을 보냅니다. 이후로, 연결은 더 이상 HTTP가 아닌 웹소켓이 되며 HTTP 요청이 아닌 프레임이 이를 통과합니다. 이 연결은 클라이언트 또는 서버가 연결을 닫을 때까지 지속됩니다.

서브 프로토콜 외에도, 개별 웹소켓 프레임은 다음과 같은 메시지 유형을 지정할 수도 있습니다:

  • BinaryMessage
  • TextMessage
  • Ping
  • Pong
  • Close

이진 프레임만이 임의 데이터를 포함할 수 있습니다. 여기에 포함된 프레임은 서브 프로토콜의 기대에 따라 유효한 UTF-8 문자열이어야 합니다.

브라우저에서 Workhorse로

터미널을 예로 들어 설명하면 다음과 같습니다:

  1. GitLab은 브라우저에 https://gitlab.com/group/project/-/environments/1/terminal과 같은 URL로 JavaScript 터미널 에뮬레이터를 제공합니다.
  2. 이 URL은 wss://gitlab.com/group/project/-/environments/1/terminal.ws와 같이 웹소켓 연결을 엽니다. 이 엔드포인트는 GitLab에 존재하지 않고 Workhorse에만 존재합니다.
  3. Workhorse는 연결을 받으면 먼저 사전 인증(preauthentication) 요청을 GitLab에 수행하여 요청된 터미널에 액세스할 수 있는 권한이 있는지 확인합니다:
    • 클라이언트가 적절한 권한을 가지고 있고 터미널이 존재하는 경우, GitLab은 성공적인 응답으로 응답하고 클라이언트가 연결해야 하는 터미널의 세부 정보를 포함합니다.
    • 그렇지 않으면 Workhorse는 적절한 HTTP 오류 응답을 반환합니다.
  4. GitLab이 유효한 터미널 세부 정보를 반환하면 다음을 수행합니다:
    1. 지정된 터미널에 연결합니다.
    2. 브라우저를 웹소켓으로 업그레이드합니다.
    3. 브라우저의 자격 증명이 유효한 동안 두 연결 간을 프록시합니다.
    4. 브라우저가 유효한 동안 간격없이 PingMessage 제어 프레임을 보냄으로써 중간 프록시가 브라우저가 존재하는 동안 연결을 종료하지 않도록 합니다.

브라우저는 특정의 서브 프로토콜로 업그레이드를 요청해야 합니다:

terminal.gitlab.com

이 서브 프로토콜은 TextMessage 프레임을 잘못된 것으로 간주합니다. PingMessage 또는 CloseMessage와 같은 제어 프레임은 일반적인 의미를 가집니다.

  • 브라우저에서 서버로 보내는 BinaryMessage 프레임은 임의의 텍스트 입력입니다.
  • 서버에서 브라우저로 보내는 BinaryMessage 프레임은 임의의 텍스트 출력입니다.

이러한 프레임은 ANSI 텍스트 제어 코드를 포함하고 어떠한 인코딩을 사용할 수 있습니다.

base64.terminal.gitlab.com

이 서브 프로토콜은 BinaryMessage 프레임을 잘못된 것으로 간주합니다. PingMessage 또는 CloseMessage같은 제어 프레임은 일반적인 의미를 가집니다.

  • 브라우저에서 서버로 보내는 TextMessage 프레임은 base64로 인코딩된 임의의 텍스트 입력입니다. 서버는 입력을 하기 전 base64로 디코딩해야 합니다.
  • 서버에서 브라우저로 보내는 TextMessage 프레임은 base64로 인코딩된 임의의 텍스트 출력입니다. 브라우저는 출력을 하기 전 base64로 디코딩해야 합니다.

base64로 인코딩된 형태로, 이러한 프레임은 ANSI 터미널 제어 코드를 포함하고 어떠한 인코딩도 사용할 수 있습니다.

Workhorse에서 GitLab로

터미널을 예로 들어 설명하면, 브라우저를 업그레이드하기 전, Workhorse는 https://gitlab.com/group/project/environments/1/terminal.ws/authorize와 같은 URL로 GitLab에 표준 HTTP 요청을 보냅니다. 이 요청은 터미널의 위치 및 연결 방법에 대한 세부 정보를 포함하는 JSON 응답을 반환합니다. 특히, 다음과 같은 세부 사항은 성공한 경우에 반환됩니다:

  • 연결할 WebSocket URL, 예: wss://example.com/terminals/1.ws?tty=1.
  • 지원할 웹소켓 서브 프로토콜, 예: ["channel.k8s.io"].
  • 보낼 헤더, 예: Authorization: Token xxyyz.
  • 선택 사항. wss 연결을 확인하기 위한 인증서 기관.

Workhorse는 이 엔드포인트를 주기적으로 재확인합니다. 오류 응답을 받거나 터미널의 세부 정보가 변경된 경우, 웹소켓 세션을 종료합니다.

Workhorse에서 웹소켓 서버로

GitLab에서는 환경 또는 CI 작업이 연관된 배포 서비스(예: KubernetesService)가 있을 수 있습니다. 이 서비스는 환경의 터미널 또는 서비스를 찾을 수 있으며, GitLab은 이러한 세부 정보를 Workhorse에 반환합니다.

이러한 URL 또한 웹소켓 URL입니다. GitLab은 Workhorse에 통신할 때 사용할 서브 프로토콜과 원격 끝단에서 요구되는 모든 인증 세부 정보를 알려줍니다.

브라우저의 연결을 웹소켓으로 업그레이드하기 전에, Workhorse는 다음을 수행합니다:

  1. Workhorse로부터 받은 세부 정보에 따라 HTTP 클라이언트 연결을 엽니다.
  2. 해당 연결을 웹소켓으로 업그레이드를 시도합니다.
    • 실패할 경우, 브라우저에 오류 응답이 전송됩니다.
    • 성공한다면, 브라우저도 업그레이드됩니다.

이제 Workhorse는 서로 다른 서브 프로토콜을 가진 두 웹소켓 연결을 가지게 됩니다. 이후로:

  • 브라우저로부터 수신된 프레임을 디코딩하여 채널의 서브 프로토콜로 다시 인코딩하고 채널로 보냅니다.
  • 채널로부터 수신된 프레임을 디코딩하여 브라우저의 서브 프로토콜로 다시 인코딩하고 브라우저로 보냅니다.

두 연결 중 하나가 닫히거나 오류 상태가 되면, Workhorse는 이를 감지하고 다른 연결을 닫아 채널 세션을 종료합니다. 브라우저 연결이 끊어진 경우, Workhorse는 적절한 서브 프로토콜에 따라 ANSI End of Transmission 제어 코드(0x04 바이트)를 채널로 보냅니다. 연결이 끊겨 있는 것을 피하기 위해, Workhorse는 채널이 보낸 임의의 웹소켓 핑 프레임에 응답합니다.

Workhorse는 다음과 같은 서브 프로토콜만 지원합니다:

새로운 배포 서비스를 지원하려면 새로운 서브 프로토콜을 지원해야 합니다.

channel.k8s.io

Kubernetes에서 사용되며, 이 하위 프로토콜은 다중화된 채널을 정의합니다.

제어 프레임은 평상시와 같은 의미를 갖습니다. TextMessage 프레임은 유효하지 않습니다. BinaryMessage 프레임은 특정 파일 서술자(descriptor)에 대한 입출력을 나타냅니다.

BinaryMessage 프레임의 첫 번째 바이트는 uint8로 표시된 파일 서술자(fd) 번호입니다. 예를 들어:

  • 0x00fd 0, STDIN에 해당합니다.
  • 0x01fd 1, STDOUT에 해당합니다.

나머지 바이트는 임의의 데이터를 나타냅니다. 서버로부터 수신된 프레임의 경우, 해당 fd에서 수신된 바이트입니다. 서버로 보낸 프레임의 경우, 해당 fd에 기록해야 하는 바이트입니다.

base64.channel.k8s.io

Kubernetes에서도 사용되며, 이 하위 프로토콜은 channel.k8s.io와 유사한 다중화된 채널을 정의합니다. 주된 차이점은 다음과 같습니다:

  • TextMessage 프레임은 BinaryMessage 프레임이 아닌 유효합니다.
  • TextMessage 프레임의 첫 번째 바이트는 파일 서술자를 숫자 형태의 UTF-8 문자로 나타내며, 따라서 문자 U+0030 또는 “0”은 fd 0, STDIN을 나타냅니다.
  • 남은 바이트는 base64로 인코딩된 임의의 데이터를 나타냅니다.