iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
IT 管理

Backstage : 打造企業內部開發者整合平台系列 第 14

Day 14 : Backstage 插件開發 - 結合 Xterm.js 套件實現 Web Terminal

  • 分享至 

  • xImage
  •  

簡介

在前面的實作中,我們皆採用了 iframe 的方式將應用整合到 Backstage 中。這樣的方式主要是在外部先將應用架設完成,再以嵌入的方式加入 Backstage,另外透過 SSO 的身份驗證讓系統之間產生連通性。這樣的做法簡單明瞭,並且應用的邏輯大多在外部完成,依賴那些已經成熟穩定的系統,Backstage 的角色更多是作為一個承載平台,所需撰寫的程式碼也較少,主要集中於設定檔的設定。

隨著我們逐漸熟悉 Backstage 並掌握其開發方式,可以進一步探索插件開發,以及更原生的整合方式。假如我們將現有的外部套件整合 Backstage,並在此基礎上進行進一步的開發。Backstage 是否還能正常運作呢?這樣的開發方式與之前的概念不同,這樣的功能成為 Backstage 運轉的一部分,實現更原生的整合效果,反之也有可能牽一髮動全身。

帶著這個問題,我從 npm 上抓了 Xterm.js,並嘗試將其作為 Backstage 的前端插件,實現一個 Web Terminal 的功能。這不僅讓 Xterm.js 能夠無縫運行在 Backstage 內部,同時也為未來的原生後端開發打下了堅實的基礎。這種方式不再依賴 iframe,能夠更好地符合 Backstage 的框架與利用其功能,讓應用的彈性更強。

本篇的轉變展示了 Backstage 更深層的開發過程與觀念,更好地理解 Backstage 的插件系統,開發出更多符合需求的自定義功能,這將是往後繼續開發的墊腳石

本次實驗對象 Xterm.js

首先,我們透過簡單的故事來了解 Xterm.js 是什麼:

在科技城鎮的 B·Tech 公司,隨著團隊規模的迅速壯大,開發者們面臨伺服器管理的挑戰。頻繁切換終端進行操作,使每次系統調整和錯誤排查都變得繁瑣,導致工作效率下降,有時甚至因操作錯誤引發連鎖問題。

基礎架構技術專家查爾斯意識到這一問題的嚴重性,並明白若不解決,團隊效率將受限,創新進程會放緩。在他尋找解決方案時,偶然發現了 Xterm.js——一個能在瀏覽器中模擬終端的 JavaScript 庫。這讓他眼前一亮。

Xterm.js 正是查爾斯需要的工具,能將終端操作從多工具切換的繁瑣中解放。他立即展開研究,決定將 Xterm.js 整合進公司內部的開發平台——Backstage。Backstage 是 B·Tech 開發者日常使用的開源平台,雖然集成了多種工具,但缺乏靈活的終端環境。查爾斯知道,將 Xterm.js 作為原生插件嵌入 Backstage,將大幅提高團隊的效率。

經過一段時間的開發與測試,查爾斯成功將 Xterm.js 無縫整合到 Backstage 中。開發者們現在只需打開 Backstage,就能直接在網頁中進行終端操作,無論是檢查日誌、執行命令還是管理容器,一切都變得更簡便,效率顯著提升。他們不再受限於繁瑣的工具切換,能專注於技術創新。

隨著 Xterm.js 的推廣,開發團隊逐漸享受這一變革帶來的效益。查爾斯還設計了擴展功能,將操作記錄與 Backstage 其他功能整合,為未來的排查和技術分享提供支持。Xterm.js 與 Backstage 的結合成為團隊不可或缺的工具,激發了更多創新靈感,顯著提高工作效率。

若深入開發的話,我們確實能夠實現將 Web Terminal 概念整合到 Backstage 平台中,例如為專案新增一個頁籤並結合 Xterm.js,讓使用者能直接在網頁介面中操作遠端主機或 Kubernetes、Docker 等容器內的系統,不必在不同應用程序之間來回切換,能夠以更高效地找到並操作目標系統。

但這樣的整合並不僅僅是技術上的挑戰,更牽涉到權限管理與安全性的考量。在直接操作容器的情境下,我們必須確保只有適當授權的用戶能夠進行操作,並且這些操作的安全性還必須達到企業標準。

因此在後續,我們將會介紹 Backstage 的存取控制機制 (Permission),面對結合越多應用的 Backstage 而言,權限與安全性反而會是首要考量點。

為 Xterm.js 建立前端插件

如同前面幾篇建立前端插件的方式,我們也為 web-terminal 建立一個。不同的是我們還需要進入 web-terminal 的目錄資料夾下,為插件安裝 Xterm.js 套件:
https://ithelp.ithome.com.tw/upload/images/20240923/20128232Qhc3kOFLrt.png
https://ithelp.ithome.com.tw/upload/images/20240923/20128232E7AD1v1xfT.png
安裝 @types/xterm 型別宣告

為了讓 TypeScript 正確識別它的 API,需要安裝對應的型別宣告,只需再次輸入指令:

yarn add -D @types/xterm

建立最初階介面

首先,可以參考官方提供的範例轉換為 React 版本,並建立一個最基本的界面嵌入 Backstage 進行測試。Page.tsx 的範例代碼如下 :
https://ithelp.ithome.com.tw/upload/images/20240923/20128232wu04ijIwUD.png

import React, { useEffect, useRef } from 'react';
import { Page, Header, Content } from '@backstage/core-components';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';

export const TerminalPage = () => {
  const terminalRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (terminalRef.current) {
      const term = new Terminal();
      term.open(terminalRef.current);
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');
      term.onData((val) => {
        term.write(val);
      }); 
    }
  }, []);

  return (
    <Page themeId="tool">
      <Header title="Terminal Page" subtitle="Interact with the terminal directly from Backstage" />
      <Content>
        <div ref={terminalRef} style={{ width: '100%', height: '100%', backgroundColor: '#000' }} />
      </Content>
    </Page>
  );
};

接著打開 Backstage 該插件的頁面,可以成功看到視窗了。由於目前只有做出前端的畫面,所以不管輸入什麼字都不會有反應,我們最終目標是要透過此頁面控制伺服器,所以我們可以透過 WebSocket 來傳遞兩邊的即時資訊。
https://ithelp.ithome.com.tw/upload/images/20240923/20128232A5S0HzSkhj.png

加入Websocket 與伺服器進行實時通訊

首先優化一下 Xterm 為它上能夠自動調整視窗大小的功能,提升使用體驗,先進行以下安裝。

yarn add xterm-addon-fit
  1. 新增引用

    為了避免寫死 WebSocket 連線伺服器的目標位置,我們一樣加入 configApiRef 讀取 app-config 設定檔,另外加入xterm-addon-fit 自動調整終端顯示尺寸。

import React, { useEffect, useRef } from 'react';
import { Page, Header, Content } from '@backstage/core-components';

import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';

import { useApi } from '@backstage/core-plugin-api';
import { configApiRef } from '@backstage/core-plugin-api';
  1. 初始化終端組件

    TerminalPage 組件內,我們首先取得 WebSocket 伺服器的 URL。接著在 useEffect 中初始化終端和自適應套件 FitAddon

export const TerminalPage = () => {
  const configApi = useApi(configApiRef);
  const websocketServer = configApi.getString('webterminal.host');

  const terminalRef = useRef<HTMLDivElement>(null);
  const socketRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    if (terminalRef.current) {
      const terminal = new Terminal();
      const fitAddon = new FitAddon();
      terminal.loadAddon(fitAddon);

      terminal.open(terminalRef.current);
      fitAddon.fit();

      terminal.writeln('Connecting to the server...');
  1. 初始化終端並建立 WebSocket 連接

    接著使用 WebSocket 來建立與伺服器的連接。一旦連接成功,Xterm 就可以接收來自伺服器的訊息並顯示在視窗上,同時也可以將在視窗輸入的資訊傳送到伺服器。

      const socket = new WebSocket(websocketServer);
      socketRef.current = socket;
      
      socket.onopen = () => {
        console.log('WebSocket connection opened');
      };

      socket.onmessage = (event) => {
        terminal.write(event.data);
      };

      terminal.onData((data) => {
        socket.send(data);
      });

  1. 監聽使用事件

    新增一個監聽畫面大小的事件,讓畫面變化的同時能夠自動調整,另外確保在畫面關閉時切斷 WebSocket 連接釋放監聽器資源。

      const handleResize = () => fitAddon.fit();
      window.addEventListener('resize', handleResize);

      return () => {
        window.removeEventListener('resize', handleResize);
        socket.close();
        terminal.dispose();
      };
    }
    return undefined;
  }, []);

最後的 Page.tsx 大致會長這樣

import React, { useEffect, useRef } from 'react';
import { Page, Header, Content } from '@backstage/core-components';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import { configApiRef, useApi } from '@backstage/core-plugin-api';

export const TerminalPage = () => {
  const configApi = useApi(configApiRef);
  const websocketServer = configApi.getString('webterminal.host');

  const terminalRef = useRef<HTMLDivElement>(null);
  const socketRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    if (terminalRef.current) {
      const terminal = new Terminal();
      const fitAddon = new FitAddon();
      terminal.loadAddon(fitAddon);

      terminal.open(terminalRef.current);
      fitAddon.fit();

      terminal.writeln('Connecting to the server...');

      // using ws or wss protocol to connect to the server
      // 舉例在 app-config.yaml 設定 host: ws://198.x.x.x:3838
      const socket = new WebSocket(websocketServer);
      socketRef.current = socket;
      
      socket.onopen = () => {
        console.log('WebSocket connection opened');
      };

      socket.onmessage = (event) => {
        terminal.write(event.data);
      };

      terminal.onData((data) => {
        socket.send(data);
      });

      const handleResize = () => fitAddon.fit();
      window.addEventListener('resize', handleResize);

      return () => {
        window.removeEventListener('resize', handleResize);
        socket.close();
        terminal.dispose();
      };
    }
    return undefined;
  }, []);

  return (
    <Page themeId="tool">
      <Header title="Terminal Page" subtitle="Interact with the terminal directly from Backstage" />
      <Content>
        <div ref={terminalRef} style={{ width: '100%', height: '100%', backgroundColor: '#000' }} />
      </Content>
    </Page>
  );
};

為伺服器建立 WebSocket 連線並整合 node-pty 套件

要實現完整的 Web Terminal 功能,我們還需要 node-pty 套件(pseudo-terminal,虛擬終端) 搭配使用,pty 可以模擬一個終端並處理用戶端的指令,然後將結果返回給前端的 Term.js 進行顯示,中間透過 Websocket 進行傳輸,各自的任務如下圖所示:
https://ithelp.ithome.com.tw/upload/images/20240923/20128232KLdJ9tTwR4.png
先在伺服器建立一個資料夾,並建立以下程式碼命名為 server.js

const express = require('express');
const WebSocket = require('ws');
const pty = require('node-pty');

const app = express();
const port = 3838;

app.use(express.static('build'));

const server = app.listen(port, () => {
  console.log(`伺服器正在監聽埠號 ${port}`);
});

// 建立 WebSocket 伺服器
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  // 建立新的 pty (偽終端) 過程
  const shell = process.env.SHELL || 'bash';
  const ptyProcess = pty.spawn(shell, [], {
    name: 'xterm-color', 
    cols: 80,            
    rows: 30,             
    cwd: process.env.HOME, // 設定當前工作目錄為使用者的家目錄
    env: process.env,  
  });

  // 發送來自 pty 的數據
  ptyProcess.onData((data) => {
    ws.send(data); //
  });

  // 將前端發送的命令寫入 pty
  ws.on('message', (msg) => {
    ptyProcess.write(msg); //
  });

  // 當連接關閉時進行清理
  ws.on('close', () => {
    ptyProcess.kill(); // 
  });
});

在該資料夾底下初始化並安裝必要的依賴套件

npm init -y
npm install express ws node-pty

啟動 Websocket 監聽,完成後我們就可以回到 Backstage 上測試連結功能是否正常。

node server.js

https://ithelp.ithome.com.tw/upload/images/20240923/20128232dPXyixHgHC.png

(可選) 使用 wss 加密通訊

由於我們的網頁使用 HTTPS ,為了確保通訊的安全性,WebSocket 連接也必須使用加密的 wss:// 協定,而非不安全的 ws://,瀏覽器會阻止這樣的連接並產生錯誤。

解決方法是將 WebSocket 的連接 URL 改為 wss://,確保在加密的環境中傳輸資料。我們必須幫伺服器端設定 SSL 證書來加密通訊,或是把兩邊都更改為 http 與 ws 通訊。
https://ithelp.ithome.com.tw/upload/images/20240923/201282324VLfDpeC3v.png
假設使用 nginx 處裡 SSL,反向代理 WebSocket 的話,需要在 header 加上 Upgrade $http_upgradeConnection 'upgrade' 兩行。這兩行是 WebSocket 連接成功的關鍵,它們告訴 Nginx 將 HTTP/1.1 升級為 WebSocket。則在 app-config.yaml 設定的 host 就會是 wws://webterminal.xxx.com

server {
    listen 443 ssl;

    ssl_certificate /etc/nginx/ssl/xxx.crt;
    ssl_certificate_key /etc/nginx/ssl/xxx.key;

    server_name webterminal.xxx.com;

    location / {
        proxy_pass http://10.x.x.x:3838;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
    }
}

記得為 connect-src 設定 wss:來允許對 WebSocket 連接
https://ithelp.ithome.com.tw/upload/images/20240923/20128232JC7bbfh0jh.png

成果展示

https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmQ5cnNjcmJha2diN3FycmRmajNhN3A4bG5vdjlsd3VwamE0aWNsMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gC6JeWg5wHAGMEpx6c/giphy.gif

結論

恭喜!我們已成功為 Backstage 平台增加了 Web Terminal 功能。現在,使用者可以直接透過 Backstage 的網頁介面遠端操作伺服器,提供了更方便且即時的管理體驗。這次的開發過程中,我們引入了外部套件來實現這項功能,使得前端開發更加靈活與強大。

接下來,我們將深入介紹如何在 Backstage 平台上進行後端開發,並探索將後端資料傳遞至前端顯示的方式。藉由這些步驟將引導我們了解 Backstage 原生插件的開發,從而擴展 Backstage 的更多功能。

參考文獻

https://xtermjs.org/
https://juejin.cn/post/6918911964009725959
https://tutorials.tinkink.net/zh-hant/nginx/nginx-websocket-reverse-proxy.html
http://justfly.idv.tw/?p=1160


上一篇
Day 13 : Backstage 插件開發 - 打造 Discrouse 內部論壇與 SSO 完全整合
下一篇
Day 15 : Backstage 插件開發 - 以 "Feedback" 為例的翻譯改造
系列文
Backstage : 打造企業內部開發者整合平台18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言