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 Found500 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는 먼저 요청된 터미널에 액세스할 권한이 있는지를 확인하기 위해 GitLab에 preauthentication 요청을 수행합니다:
    • 클라이언트가 적절한 권한을 가지고 터미널이 있는 경우, GitLab은 성공적인 응답을 반환하여 클라이언트가 연결해야 하는 터미널의 세부 정보를 포함합니다.
    • 그렇지 않을 경우, Workhorse는 적절한 HTTP 오류 응답을 반환합니다.
  4. GitLab이 Workhorse에 유효한 터미널 세부 정보를 반환하면:
    1. 지정된 터미널에 연결합니다.
    2. 브라우저를 웹소켓으로 업그레이드합니다.
    3. 브라우저의 자격 증명이 유효한 동안 두 연결 사이를 프록시합니다.
    4. 브라우저가 있을 때, 프록시가 연결을 종료하지 않도록 주기적으로 PingMessage 제어 프레임을 보냅니다.

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

terminal.gitlab.com

이 서브 프로토콜은 TextMessage 프레임을 유효하지 않은 것으로 간주합니다. PingMessageCloseMessage와 같은 제어 프레임에는 일반적인 의미가 있습니다.

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

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

base64.terminal.gitlab.com

이 서브 프로토콜은 BinaryMessage 프레임을 유효하지 않은 것으로 간주합니다. PingMessageCloseMessage와 같은 제어 프레임에는 일반적인 의미가 있습니다.

  • 브라우저에서 서버로 보내는 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 응답을 반환합니다. 특히, 성공의 경우 다음과 같은 세부 정보가 반환됩니다:

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

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

Workhorse to the WebSocket server

깃랩에서는 환경이나 CI 작업에는 배포 서비스(KubernetesService와 같은)가 연결될 수 있습니다. 이 서비스는 터미널이나 환경의 서비스를 찾을 수 있는 위치를 알고 있으며, 깃랩은 이러한 세부 정보를 Workhorse에게 반환합니다.

이 URL들은 WebSocket URL들이기도 합니다. 깃랩은 Workhorse에게 연결을 통해 대화할 서브 프로토콜과 원격 엔드포인트에서 요구되는 인증 세부 정보를 함께 알려줍니다.

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

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

이제 Workhorse에는 서로 다른 서브 프로토콜을 갖는 두 개의 웹소켓 연결이 있으며, 그 후에:

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

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

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

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

channel.k8s.io

Kubernetes에서 사용되는 이 서브 프로토콜은 간단한 다중화된 채널을 정의합니다.

제어 프레임은 일반적인 의미를 가집니다. TextMessage 프레임은 유효하지 않습니다. BinaryMessage 프레임은 특정 파일 디스크립터(fd)로의 I/O를 나타냅니다.

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로 인코딩된 임의의 데이터를 나타냅니다.