Featured image of post DevContainer Desktop Lite Feature

DevContainer Desktop Lite Feature

이제 DevContainer에서도 GUI를 볼 수 있습니다

연관 포스트

예제 프로젝트 GitHub 링크


들어가기 앞서

얼마 전에(어제) “DevContainer 톺아보기”라는 제목의 포스트를 올렸다.

주된 내용을 요약하면 다음과 같다.

  • 개발 환경 구축 시 겪는 어려움(일관성, 재현성, 격리성) 소개

  • DevContainer를 사용해서 이런 문제를 극복할 수 있다는 것

  • 기본적인 설치 방법과 약간의 심화 과정

본래 이번 포스트를 먼저 작성하고 있었는데,
쓰다보니 DevContainer 자체에 대한 내용이 지나치게 장황하다라는 것을 깨달았다.
고로 내용을 둘로 나눠 DevContainer에 대해 정리하는 포스트를 먼저 올린 것이다.
이거 보여주려고 어그로 끌었다


문제의 시작

내겐 너무 무거운 Docker

내가 작업할 때 사용하는 PC는 총 3대이다.

  1. 회사에 있는 Windows Desktop

  2. 집에 있는 Windows Desktop

  3. 회사에서 지원받은 맥북

처음에 DevContainer를 구성했을 때는 각 PC에 Native하게 Docker 환경을 구축해서 사용했었다.
Windows는 WSL과 DockerDesktop, Mac은 DockerDesktop만 설치하면 되는 아주 간단한 작업이다.

그러다 이슈가 발생했다.

이 Docker라는 녀석… 컴퓨터의 자원을 엄청나게 소모한다.

그도 그럴 것이 내가 실행하길 원하는 건 어디까지나 Linux Container이고 근본적으로 OS Level부터 다른
Mac이나 Windows에서는 어떤 방식으로든 Linux를 가상화시켜 Container를 띄워야만 한다.

다시 말해 Linux와 Docker가 하이퍼바이저 위에서 동작한다는 의미이다.

그나마 Windows PC는 둘 다 Desktop이고 WSL을 통해서 하는 거라 비교적 견딜만 하다.

진짜 문제는 Mac이다.

나름 MacBook Pro인데도 불구, 조금 무거운 DevContainer 2~3개 정도만 띄워도
방열 팬에서 당장이라도 이륙할듯한 기세로 고열의 제트를 내뿜는다. F-35가 따로 없다.
심한 날은 격한 스로틀링 끝에 아예 Mac이 Docker를 죽여버리는😱 경우도 심심찮게 발생한다.

내 개인적인 평은 "Abstraction Layer가 쌓일 수록 자연스레 발생하는 오버헤드"이다.

요컨대

사람이 편해지면 기계가 고생한다


별도 Linux 서버를 사용하기로 함

사실 이런 문제는 특별히 Mac이라 그런 것은 아니다. 애플은 억울합니다
그보단 노트북이라는 환경 자체가 발열 해소에 취약할 수 밖에 없다는 구조적 이슈이다.

내가 택한 해결책은 단순하다. 그냥 개발용 Linux 서버 하나를 구축하는 것.

추후 기회가 되면 그 구체적인 과정에 대해 포스트 하겠지만 아주 간단하게 요약하면 다음과 같다.

  1. 안 쓰는 Desktop PC 1대를 준비한다.

  2. 원하는 Linux OS를 설치한다.

  3. Docker와 Docker Compose를 설치한다.

  4. 외부에서 접속 가능하도록 SSH 서버를 설치한다.

  5. WOL을 통해 원격으로 전원을 켤 수 있도록 설정한다. (선택)


이렇게 해두면 사용하는 로컬 PC에는 vsCode나 Cursor 정도만 설치하면 그만이다.

IDE에서 SSH로 원격 개발 서버에 연결 -> 프로젝트를 Clone하고 -> 개발 컨테이너 시작!

실질적인 작업은 모두 원격 개발 서버에서 실행되므로 내가 지금 사용하는 PC에는 아무런 부담도 없는 것이다.

그러다… 새로운 문제에 직면했다.


크롤링(Crawling)

크롤링은 웹상의 정보를 탐색하고 수집하는 작업이다.
외부 서버에서 데이터를 가져와야 하는데, 상황에 따라 접근 방법이 달라진다.

  • 별도로 API를 제공하는 경우
    제공하는 API를 그대로 사용하면 된다. 가장 이상적인 상황이다.

  • 정적 페이지로 구성되어 있는 경우
    현재 이 블로그처럼 별도의 SiteMap을 통해 페이지 목록 확인이 가능한 경우가 많다.
    해당 URL을 통해 HTML을 가져와 파싱하면 된다. curl, axios 등을 사용해서 간단하게 처리 가능하다.

  • 동적 페이지로 구성되어 있는 경우
    이런게 문제가 된다. 흔히 웹 페이지가 아니라 웹 애플리케이션이라고 더 많이 칭하는데,
    일련의 정보보단 서비스를 제공하는데 더 초점을 맞춘 경우다.
    Headless 브라우저를 열어서 직접 페이지에 방문, 데이터를 추출 및 파싱해야 한다.

그런데 돌연 이게 DevContainer와 무슨 관계가 있다는 걸까?


GUI를 볼 수가 없다

DevContainer에서 GUI가 포함된 어떤 프로그램을 실행해야 하는 경우가 아주 간혹 있다.
그 중에 비교적 가장 빈번한 것이 앞에서 언급한 브라우저를 통한 크롤링이다.

회사에서 맡게 되는 작업 중에는 이렇게 브라우저로 외부 웹 페이지를 방문해서 데이터를 추출해야 하는 때가 있는데, 크롤링 자체는 DevContainer건 서버에 올린 Production Container건 별 문제 없이 아주 잘 동작한다.

문제는 디버깅이다.

브라우저라는 것은 일종의 GUI 프로그램이다.
스크립트가 브라우저를 띄울 때는 대개 headless모드로 동작하는데,
이는 브라우저 그래픽은 띄우지 말고 백그라운드에서 실행하라는 의미이다.

개발을 할 때는 이 headless 모드를 꺼놓고 작업을 해야한다.
직접 브라우저에서 Bot이 동작하는 모습을 보면서 스크립트를 짜야 하니까…

그런데 DevContainer에서는 브라우저의 GUI를 볼 수가 없다.

내 기억이 맞다면 WSL 위에 올린 DevContainer에서는 가능 했던 거 같은데, 정확하지 않다.
확실한 건 지금 사용하는 방식, 원격 개발 서버의 DevContainer에서는 GUI를 볼 방법이 없다는 것이다.


해결방안

“문제의 시작” 섹션을 길게 쓴 것에 비하면 해결방안은 의외로 매우 심플하다.

기존 포스트“Features로 개발도구 설치하기” 단락에 보면
여러가지 개발 도구들을 간편하게 설치할 수 있다는 걸 알 수 있다.

사용 가능한 기능 목록 페이지에 있는 “Lightweight Desktop”을 설치해서 브라우저를 통해 DevContainer에서 실행되는 GUI 프로그램을(이 경우, 브라우저) 눈으로 확인할 수 있도록 해보겠다.


DevContainer 구성

이번 포스트에선 이 문제를 해결하는 예시 소스가 담긴 GitHub Repository를 아예 따로 만들었다.

1
git clone https://github.com/ApexCaptain/postExample.noVncDesktopLiteFeature.git

직접 작업할 시간이 없다면 위 커맨드로 프로젝트를 Clone 해서 봐도 좋다.

  • devcontainer.json

    Lite-weight Desktop Feature에 따르면 사용 가능한 옵션은 다음과 같다.

    Options IdDescriptionTypeDefault Value
    versionCurrently Unused!stringlatest
    noVncVersionThe noVNC version to usestring1.2.0
    passwordEnter a password for desktop connections. If “noPassword”, connections from the local host can be established without entering a passwordstringvscode
    webPortEnter a port for the VNC web client (noVNC)string6080
    vncPortEnter a port for the desktop VNC server (TigerVNC)string5901

    이에 따라 .devcontainer/devcontainer.json 파일을 다음과 같이 구성해보자.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    // .devcontainer/devcontainer.json
    {
        // Basic
        "name": "PostExample.noVncDesktopLiteFeature Dev Container",
        "dockerComposeFile": "docker-compose.dev.yml",
        "service": "workspace",
        "workspaceFolder": "/home/vscode/postExample.noVncDesktopLiteFeature",
        // 혹은 다음과 같이 적어도 된다
        // "workspaceFolder": "/home/vscode/${localWorkspaceFolderBasename}",
    
        // Featuring
        "features": {
    
            // https://github.com/devcontainers/features/tree/main/src/desktop-lite
            "ghcr.io/devcontainers/features/desktop-lite:1": {
                // 기본 설정값들
                // "version": "latest",
                // "noVncVersion": "1.2.0",
                // "password": "vscode",
                // "webPort": "6080",
                // "vncPort": "5901"
            },
    
            // https://github.com/devcontainers/features/tree/main/src/node
            // Puppeteer로 테스트 할 예정이므로 nodejs를 설치
            "ghcr.io/devcontainers/features/node:1": {}
        },
    
        // Ports
        // Desktop Lite의 기본 WebSocket 포트를 포워딩
        "forwardPorts": [
            6080
        ],
        "portsAttributes": {
            "6080": {
                "label": "Desktop (noVNC)"
            }
        }
    }
    
  • docker-compose.dev.yml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    # .devcontainer/docker-compose.dev.yml
    services:
        workspace:
            container_name: post_example_novnc_desktoplitefeature_devcon_workspace
            image: mcr.microsoft.com/devcontainers/base:bullseye
            # Shared Memory의 크기를 1GB로 설정
            shm_size: '1gb'
            volumes:
                # Workspace Cache
                - ..:/home/vscode/postExample.noVncDesktopLiteFeature:cached
            command: sleep infinity
    

이걸로 DevContainer 구성은 끝났다.

F1 키를 누르고 Dev Containers: Rebuild Container를 실행하자.

DevContainer 빌드가 끝났다면,
Host PC에서 브라우저를 열고 localhost:6080으로 접속해보자.

이 화면까지 봤다면 성공이다 🎉

좀 전에 설치한 Desktop Lite에 웹을 통해 접속한 것이다. 여기서 연결 버튼을 눌러주자.
DevContainer 내부에서 실행되는 GUI를 볼 수 있을 것이다.


크롤링 예제 프로젝트

이번 예시에선 네이버 뉴스 페이지에 접속해서
가장 좌측 상단에 있는 칼럼의 타이틀내용을 추출하는 간단한 크롤링 스크립트를 짜보도록 하겠다.

  • 프로젝트 초기화 및 의존성 설치

    1. yarn 명령어로 nodejs 프로젝트를 초기화한다.

      1
      
      yarn init -y
      
    2. 생성된 package.jsontypemodule로 설정한다.

      1
      2
      3
      4
      5
      6
      
      {
          "name": "post-example.no-vnc-desktop-lite-feature",
          "version": "1.0.0",
          "type": "module", // Type을 Module로 지정
          "main": "index.js"
      }
      
    3. PuppeteerCheerio를 설치한다.

      1
      2
      3
      
      yarn add \
          puppeteer \
          cheerio
      
  • 스크립트 작성

    index.js 파일 하나를 만들고 다음의 스크립트를 복사해보자.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    
    // index.js
    import puppeteer from 'puppeteer';
    import * as cheerio from 'cheerio';
    
    const main = async () => {
        let browser;
    
        try {
            // #1 Puppeteer 인스턴스 생성
            browser = await puppeteer.launch({
                // Browser를 GUI 없이 사용할지 여부, 운영 환경에선 true로 설정 해주자
                headless: false, 
                args: [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                ],
            });
    
            // #2 새로운 페이지 (탭) 생성
            const page = await browser.newPage();
    
            // #3 네이버 뉴스 페이지 이동
            await page.goto('https://news.naver.com/', { 
                waitUntil: 'networkidle0',
                timeout: 30000  // 30초 타임아웃
            });
    
            // #4 대상 뉴스 카드 element selector를 선언하고 화면에 보이면 클릭
            const newsCardElementSelector = '#ct > div > section.main_content > div.main_brick > div > div:nth-child(1)'
            // 확실히 화면에 들어올 때까지 대기 (10초 타임아웃)
            await page.waitForSelector(newsCardElementSelector, { 
                visible: true,
                timeout: 10000
            }); 
            await page.click(newsCardElementSelector);
    
            // #5 대상 뉴스의 제목과 본문의 텍스트를 추출
            const titleElementSelector = '#title_area > span'
            const articleElementSelector = '#newsct_article'
            await page.waitForSelector(titleElementSelector, {
                visible: true,
                timeout: 10000
            });
            const $ = cheerio.load(await page.content());
            const title = $(titleElementSelector).text();
            const article = $(articleElementSelector).text();
    
            // #6 제목과 본문의 텍스트를 콘솔에 출력
            console.log({ title, article })
    
        } catch (error) {
            console.error('에러가 발생했습니다:', error.message);
        } finally {
            // #7 브라우저 인스턴스 종료 (에러가 나도 반드시 실행)
            if (browser) {
                await browser.close();
            }
        }
    };
    main();
    

실행

이제 작성한 스크립트를 실행해보자.

1
node index.js

출력되는 결과 자체는 그때마다 다를 것이다.

이제 Desktop Lite를 통해 보면 어떠한지 확인해보자.


브라우저를 통해 브라우저가 실행되는 걸 바라보는 모습이다
그리고 그걸 녹화해서 또 다른 브라우저로 보고있는 나


주의사항

호환성

나온 지 얼마 안 된 기능이라 그런지 OS 및 하드웨어 제약사항이 있다.

  • Container Image 제약

    예시에서 사용한 DevContainer Docker Image는 Debian 기반이다.
    desktop lite feature는 현재 Debian/Ubuntu 기반의 이미지만 지원한다.
    Alpine 이미지에서는 사용할 수 없다.


  • CPU Architecture 제약

    desktop lite feature를 적용하려면 DevContainer를 동작하는 PC의
    CPU Architecture가 AMD64여야만 한다.

    Intel이나 AMD에서 만든 CPU라면 관계 없으나,
    Arm을 사용하고 있다면 이 기능은 사용할 수 없다.

    대표적으로 Apple Silicon 칩이 장착된 Mac은 안 된다.


패스워드 설정

desktop lite feature는 내부적으로 VNC 서버
그 위에 얹혀 있는 noVnc 웹 클라이언트를 사용해서 GUI 화면을 노출시킨다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
+---------------------------+
|       Web Browser         |
| (noVNC Client over 6080)  |
+------------▲--------------+
             │ WebSocket
+---------------------------+
|   noVNC (Websockify)      |
+------------▲--------------+
             │ VNC Protocol
+---------------------------+
|     Xvfb + Fluxbox        |
|  (Virtual Display Server) |
+------------▲--------------+
+---------------------------+
| GUI Application (ex: Chrome) |
+---------------------------+

그리고 .devcontainer/devcontainer.json에서 해당 포트를(6080) 그대로 포워딩 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// .devcontainer/devcontainer.json
{
    // ...
    "forwardPorts": [
      6080
    ],
    "portsAttributes": {
      "6080": {
        "label": "Desktop (noVNC)"
      }
    }
}

로컬 PC에 DevContainer를 실행중이라면 큰 문제가 되진 않겠지만,
Remote Devcontainer를 사용하는 경우, 가령

  • SSH (자체 호스팅 혹은 클라우드 서버)
  • Codespaces

vsCode 혹은 Cursor는 원격 서버의 6080 포트를 “클라이언트 측으로 포워딩” 한다.

만일 서버 방화벽에서 허용되어 있다면 외부에서도 noVnc로 접근할 수 있다. (!!)

사실 실제 그렇게 되어있을 가능성은 희박하지만, 그래도 안전하게 해서 나쁠 건 없다.
다음의 예시처럼 패스워드 설정을 확실하게 해두도록 하자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// .devcontainer/devcontainer.json
{
    // ...
    "features": {

        // https://github.com/devcontainers/features/tree/main/src/desktop-lite
        "ghcr.io/devcontainers/features/desktop-lite:1": {
            "password": "my-extremely-complex-password"
        },

    }
    // ...
}

Git 저장소에 패스워드가 올라가는 게 꺼려진다면 다음과 같이 설정해도 된다.

1
2
# .devcontainer/.env
NOVNC_PASSWORD=my-extremely-complex-password

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// .devcontainer/devcontainer.json
{
    // ...
    "features": {

        // https://github.com/devcontainers/features/tree/main/src/desktop-lite
        "ghcr.io/devcontainers/features/desktop-lite:1": {
            "password": "${localEnv:NOVNC_PASSWORD}"
        },

    }
    // ...
}

.env 파일은 .gitignore에 추가 해두도록 하자.


마치며

이번 포스트에서는 DevContainer에서 GUI 프로그램을 실행할 때 겪는 문제와 그 해결책에 대해 다뤄봤다.

처음에는 단순히 Docker의 무거움 때문에 원격 개발 서버를 구축했는데,
그 과정에서 크롤링 작업을 할 때 GUI를 볼 수 없다는 새로운 문제에 직면하게 되었다.

다행히 DevContainer의 Features 시스템 덕분에 Desktop Lite를 통해 이 문제를 깔끔하게 해결할 수 있었다.
noVNC를 통해 웹 브라우저로 DevContainer 내부의 GUI에 접근할 수 있게 된 것이다.

이제 원격 개발 서버의 장점을 그대로 유지하면서도,
필요할 때는 GUI 프로그램을 시각적으로 확인하며 개발할 수 있게 되었다.

이번에는 사람도 편하고 기계도 편한 해결책을 찾은 것 같다 😊

당장 마땅한 GUI 프로그램이 없어서 크롤링을 예시로 들었다.
예상컨데 이외에도 활용할 수 있는 여지는 많을 것으로 보인다.

혹시나 비슷한 고민을 겪고 있는 분들이 있었다면 부디 도움이 되었길 바란다.

참고자료

Hugo로 만듦
JimmyStack 테마 사용 중