[實作筆記] Gitlab CI/CD 與 GCP - 架構全貌

前言

在一個團隊合作開發的場景中,多個開發人員可能同時對同一個程式碼庫進行更改,
而 CI/CD 可以自動進行建置、測試和部署,以確保程式碼的品質和穩定性。
此外,CI/CD 還可以幫助提高交付速度,減少錯誤,並支持快速反饋和迭代開發。

在這個背景下,選擇 GitLab CI/CD 與 Google Cloud Platform(GCP)作為 CI/CD 解決方案有幾個優點。
首先,GitLab CI/CD 提供了完整的 CI/CD 功能,包括強大的持續整合和持續部署工具,能夠無縫集成到 GitLab 的版本控制平台中,
方便團隊進行版本控制、自動建置和測試,並實現自動化的程式碼部署。
其次,GCP 是一個廣泛使用的雲端平台,提供了豐富的計算、儲存、網路和資料庫等服務,
方便團隊在雲端環境中進行應用程式部署和運行,並且與 GitLab CI/CD 有良好的整合性,可以輕鬆設定和管理 CI/CD 流程。

替代方案:若不選擇 GitLab CI/CD 與 GCP,團隊可能需要考慮其他 CI/CD 工具和雲端平台的組合。
例如,替代 GitLab 的版本控制工具可以是 GitHub、Bitbucket 等;替代 GCP 的雲端平台可以是 AWS、Azure 等。

架構

在我們的架構之中,我們使用 Docker 作為 GitLab Runner 的執行環境,
並在 GCP 上使用虛擬機器(VM)作為運行應用程式(web server)部署環境。
透過 GitLab Runner 和 Docker,我們能夠自動執行 CI/CD 任務,
並根據需要動態調整運行環境,確保我們的應用程式在開發和部署過程中保持高品質和穩定性。
如下圖。

GCP 與 Gitlab

步驟

接下來我將進行以下動作:

  • 在 GCP 上建立兩個虛擬機器(VM),一個作為我們的 Web Server,另一個作為 GitLab Runner。
  • 將 GitLab Runner VM 註冊為 GitLab Group runner,以便進行自動化的建置和部署。
  • 設定適當的防火牆規則,讓 Gitlab Runner 可以存取 Web Server
  • 讓 GitLab Runner VM 的 Docker 容器可以存取我們的 Web Server,我會設定相應的公鑰和私鑰,以確保安全的連線。
  • 我會撰寫一個適合的 gitlab-ci.yml 檔案,來定義建置和部署的流程,並將其配置在 GitLab CI/CD 中,以實現自動化的流程。
  • 最後,我會在 Web Server 上設定好使用者、群組與 Nginx 的 Hot reload。

以上的步驟將協助我們建立基於 Gitlab 與 GCP 的 CI/CD 流程,並以此提供程式開發的品質和穩定性。

參考

(fin)

[翻譯] TypeScript 寫給 C#/Java 工程師

前言

工作上有開發前端專案的需求,主流使用 JavaScript。
而聽說了一些強型別語言的優點,加上我有開發 C# 的經驗,我常常改用 TypeScript,
但實際上確常常覺得反而更笨重了,覺得開發上不太順暢。

一個是 node_module 的問題

相依性的管理十分麻煩,常常有新版的模組更新,但其相依的模組卻尚未更新。
運氣好等幾周就會有更新,運氣不好是模組的開發者已經不在維護,目前我的處理方式是使用 ncu ,
但也有其極限,而在其上花費的大量時間,反而拖累開發的速度。

TypeScript

另一個麻煩的點是使用 TypeScript ,雖然我有 C# 的經驗,也熟悉 JavaScript,
但是常常覺得開發仍然不順,直到閱讀官方的這篇文章,才稍解一些疑惑。
並稍作記錄如下。

翻譯

本文出處為TypeScript for Java/C# Programmers
我不會逐字翻譯,只針對我認為重要的觀念作筆記。

Class 的反思

在 C#/Java Class 是程式的基本單位,這類的程式語言我們稱之為 mandatory OOP
而在 TypeScript/JavaScript 當中 Function 才是程式語言的基本單位。
Function 可以自由的存在,而不需要寄生在 Class 之中。
這帶來了靈活性的優點,在思考 TypeScript 時,應以此作為考量。
因此,C#和 Java 的某些結構,如 Singleton 和 Static Class 在 TypeScript 中是不必要的。

Type 的反思

Nominal Reified Type Systems vs Structural System
C#/Java 使用 Nominal Reified Type Systems ,
這表示 C#/Java 程式中所使用的值或物件,一定會是 null 或基本型別(int、string、boolean 等…)或具體的 Class。
而在 TypeScript 中,類別只是個集合。
你可以這樣描述一個值同時可能是 string 或 number

1
2
3
let stringNumber: string | number;
stringNumber = 1;
stringNumber = "123";

同時在作型別的推導的時候, C#/Java 會有具體的型別,而 TypeScript 會使用結構作推導,
參考下面的例子,我們沒有給 obj 具體的型別,但是在結構上符合 Pointlike 與 Named 所擁有的屬性,
使得呼叫方法時的型別推導在 TypeScript 是合法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Pointlike {
x: number;
y: number;
}
interface Named {
name: string;
}

function logPoint(point: Pointlike) {
console.log("x = " + point.x + ", y = " + point.y);
}

function logName(x: Named) {
console.log("Hello, " + x.name);
}

const obj = {
x: 0,
y: 0,
name: "Origin",
};

logPoint(obj);
logName(obj);

這樣的設計在 runtime 的時候,我們無法具體的知道型別是什麼,
下面的概念在 TypeScript 將不可行,
即使用 JavaScript 的 typeof 或 instanceof 你也只會拿到 “object”,而非具體的型別

1
2
3
4
// C#
static void LogType<T>() {
Console.WriteLine(typeof(T).Name);
}

即使如此,這樣的設計在編譯時期的檢查在實務上是足夠的,
另外兩個例子與長時間開發 C#/Java 的概念上可能會有所不同,

Empty Type

Typescript 允許設計無屬性的 Type,而基於 Structural System
你可以建立一個方法傳遞任何物件進去(而不是用 any 或 object 去定義傳入值)

1
2
3
4
5
6
7
8
class Empty {}

function fn(arg: Empty) {
// do something?
}

// No error, but this isn't an 'Empty' ?
fn({ k: 10 });

Identical Types

下面的程式不會發生錯誤,但是可能會讓 C#/Java 的開發者有點意外

1
2
3
4
5
6
7
8
9
10
11
12
class Car {
drive() {
// hit the gas
}
}
class Golfer {
drive() {
// hit the ball far
}
}
// No error?
let w: Car = new Golfer();

一樣基於 Structural System 的設計,有相同的屬性與方法簽章的兩個不同物件,
是允許這樣子的行為,而以官方的看法來說,實務上並不太容易發生這種情形(兩個不同的模型,卻有相同的方法、屬性等…)。

小結

以下是 C#/Java 與 TypeScript 的相同和不同之處:

相同之處:

  • C#/Java 和 TypeScript 都是物件導向編程語言。
  • C#/Java 和 TypeScript 都使用類(class)和物件(object)的概念。
  • C#/Java 和 TypeScript 都有靜態類型系統。

不同之處:

  • C#/Java 的類型系統基於類型聲明,而 TypeScript 的類型系統基於屬性的兼容性。
  • C#/Java 的類型在 runtime 是存在的,而 TypeScript 的類型在 runtime 時是不存在的。
  • C#/Java 類型之間的關係通過繼承關係或共同實現的接口來定義。而在 TypeScript 中,類型之間的關係是通過屬性的兼容性來定義。

心得

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

TypeScript 的類型設計讓我想起了鴨子測試,也讓我想起 golang 對 interface 的處理方式。
甚至想起一些對物種起源的探討,先有分類還是先有特性呢 ? 真實的世界分類反而是人類主觀強加上去的。
TypeScript 這樣的設計似乎比起強制性的類別設計(C#/Java),更貼近真實的世界。

參考

(fin)

[實作筆記] QA DB 的連線方案決擇

前情提要

QA 環境的 GCP Cloud SQL 沒有開 public IP。
不開 public IP 有兩個好處:資安性提高、省錢(GCP 的 public IP 要額外收費)。
但這樣一來要怎麼從本機連到資料庫?研究了一下,有三個方向。

三個方案比較

1. Cloud SQL Auth Proxy

GCP 官方工具,在本機起一個 proxy process,讓應用程式連 127.0.0.1 即可。

優點:不需要 VPN 或 SSH,安全性高,官方維護。
缺點:需要安裝設定,而且有個陷阱——

只有 private IP 的 instance,啟動時必須加 --private-ip flag:

1
./cloud-sql-proxy --private-ip INSTANCE_CONNECTION_NAME

沒加這個 flag 的話,預設會嘗試用 public IP 連,然後噴這個錯誤:

1
Failed to connect to instance: Config error: instance does not have IP of type "PUBLIC"

另外,Auth Proxy 的機器本身必須在同一個 VPC 內才能透過 private IP 連到 Cloud SQL。

2. VPN 連線 VPC

在 GCP 上建立 Cloud VPN,讓本機透過 VPN 加入 VPC 網路,直接連 private IP。

優點:連線後就像在同一個內網,所有服務都能存取。
缺點:設定成本高,需要維護 VPN gateway,適合團隊長期使用,對單一 QA 環境來說有點殺雞用牛刀。

3. SSH Tunnel

透過一台在 VPC 內的 VM(跳板機)建立 SSH tunnel,把本機的 port 轉到 Cloud SQL 的 private IP。

優點:不需要在 GCP 上另外設定網路,只要有一台跳板機就好。
缺點:需要管理 SSH key,每個開發者都要自己設定,高流量下有效能瓶頸。

實作:DataGrip + SSH Tunnel

我選了 SSH Tunnel,用 DataGrip 設定最方便。

步驟一:開 DataGrip → 左上角「+」→「Data Source」→ 選 MySQL(或你的 DB 類型)。

步驟二:填寫 DB 連線資訊。Host 填 Cloud SQL 的 private IP,Port、User、Password 照填。

步驟三:切到「SSH/SSL」頁籤 → 勾選「Use SSH tunnel」→ 點「Add SSH configuration」:

  • Host:跳板機的 IP 或 hostname
  • Port:22
  • User name:SSH 登入帳號
  • Auth type:選 Key pair
  • Private key file:本機的 SSH private key 路徑

注意:這裡填的 Host 是跳板機,不是 Cloud SQL。

步驟四:按「Test Connection」確認連線成功,儲存設定。

小結

三個方案各有適用場景:

  • Auth Proxy:適合 CI/CD 或應用程式連線,記得加 --private-ip
  • VPN:適合整個團隊需要長期存取多個 GCP 資源
  • SSH Tunnel:適合個人開發者臨時連 QA DB,DataGrip 設定簡單直覺

參考

(fin)

[實作筆記] 不要用 Homebrew 安裝 nvm

前言

在開發 Node.js 應用程式時,我們可能需要在不同的 Node.js 版本之間進行切換。
在這種情況下,一個方便的解決方法是使用 Node Version Manager(nvm)。
而 Homebrew 則是一個流行的 macOS 套件管理工具。
在這篇文章中,我們將探討為什麼官方不建議使用 homebrew 安裝 nvm。

情況

首先,值得注意的是,nvm 官方文件明確表示不建議使用 Homebrew 安裝 nvm。
主要是因為 Homebrew 的安裝方式可能會干擾 nvm 的運作,導致一些奇怪的問題。
nvm 官方建議使用其提供的安裝方式,以確保 nvm 能夠正常運作。

那麼,標準的 nvm 安裝方式是什麼呢?
您可以在 nvm 的 GitHub 頁面上找到完整的安裝指南,這裡不再贅述。
簡而言之,您需要在終端機中運行一個安裝腳本,該腳本將安裝 nvm,並更新您的 shell 啟動腳本以使其能夠正常運行。

如果您仍然希望使用 Homebrew 安裝 nvm,可以參考一些額外的資源。
這裡有一篇詳細的文章,介紹了如何使用 Homebrew 安裝並配置 nvm。
不過,需要注意的是,這種方法仍然不是官方建議的方式。

實作

不管您選擇哪種方式安裝 nvm,一旦完成後,您需要在終端機中設置 nvm 的環境變數才能正常運行。
如果您希望在不關閉終端機窗口的情況下讓變數生效,可以使用以下命令:

1
source ~/.bashrc

如果您使用的是 zsh,則需要使用以下命令:

1
source ~/.zshrc

這將重新載入您的 shell 啟動檔案,使 nvm 環境變數生效。

參考:

(fin)

[AI 共筆] macOS Monterey 5000 Port 佔用原因與解方

前言

在 macOS Monterey 上如何處理開發伺服器使用端口被佔用的問題,
若使用其他 macOS 版本,請查看相關頁面。

問題

更新到最新的 macOS 作業系統時,發現我的前端網站,
顯示類似於「Port 5000 already in use」的訊息。

透過執行

1
lsof -i :5000

發現正在佔用該端口的進程名稱為 ControlCenter,這是一個原生的 macOS 應用程式。
即使你使用強制終止它,它還是會自動重新啟動。

處理方式

原來使用該端口的進程是 AirPlay 伺服器,
你可以在「系統偏好設定」 >「共享」中取消勾選「AirPlay Receiver」以釋放 5000 端口。

小結

網路大多的中文解法是關閉”隔空播放接收器”,在新版的 OS 找不到,可能是語言差異。
所幸找到英文的解法,順利停止佔用 port 的服務。

參考

(fin)

[AI 共筆] 自動重新加載 Node 專案工具比較

前言

在開發Node.js應用程序時,開發人員通常需要經常重新啟動應用程序來看到他們所做的更改。
這真的很煩,因為每次修改都需要重新啟動應用程序。
為了解決這個問題,開發人員可以使用許多自動重新加載工具,
例如 nodemon 和 pm2。

比較

首先,讓我們來比較 nodemon 和 pm2 自動重新加載功能。

nodemon

一個簡單易用的自動重新加載工具,可以在幾分鐘內完成安裝和設置。
它會監聽文件的更改,並在發現更改時自動重新加載應用程序。
此外,它還可以在命令行中顯示有用的日誌信息,例如錯誤和堆棧跟踪。
但是 nodemon 需要不斷監聽文件系統的更改,因此它會帶來一些性能開銷。
此外,它也不能用於在分佈式系統中部署 Node.js 應用程序。

pm2

pm2 是另一種自動重新加載工具,具有進程管理功能,例如重啟和停止應用程序,監聽應用程序的日誌等等。
它可以輕鬆地在分佈式系統中部署 Node.js 應用程序,並可以與其他 pm2 命令進行集成,
例如性能監測和自動縮放。 然而,相對於 nodemon,pm2 更為複雜,需要更多的配置和設置。
此外,某些 pm2 命令可能會影響系統的性能,例如性能監測和自動縮放。

小結

在開發 Node.js 應用程序時,自動重新加載工具是非常重要的。
如果開發人員只需要簡單的自動重新加載功能,並且不需要在分佈式系統中部署應用程序,
那麼 nodemon 是一個不錯的選擇。
如果需要進程管理功能和在分佈式系統中部署Node.js應用程序,
那麼 pm2 是一個更好的選擇。

參考

(fin)

[AI 共筆] tsconfig 中的 tsx 設定

前言

本篇是 AI Knows 的第一篇,ChatGPT 問世後,
在學習開發上,我認為又到了一個新的領域,
很多人開始作分享對 ChatGPT 的看法,在我看來還是社群時代的蹭流量作法。
更進一步的有人開始透過 AI 實作商業應用,商人果然是跑最快的。
另外一種,用嘴涮夢、涮 AI,開始賣書賣課的,我認為接近灰色地帶,就不予置評了。

問題

我正在替專案配置 EsLint 文件,過程當然直接請 AI 代勞,
但是有一些設置我不那麼清楚也懶得查文件。
就一樣也請教 AI 囉。

對話記錄

Q:tsconfig.json 中的 jsx 屬性為何?**

jsx 屬性是用來指定 TypeScript 如何處理 JSX 語法的選項。
JSX 是一種 JavaScript 語法擴展,允許在 JavaScript 代碼中直接使用
HTML 標籤來描述 UI 組件,使得開發 Web 應用程式更為方便。

在 tsconfig.json 中,jsx 屬性可以設置為以下值:

“react”:表示使用 React 的 JSX 語法。
“preserve”:表示將 JSX 語法保留為原始的 JSX 代碼。
“react-native”:表示使用 React Native 的 JSX 語法。
當設置為 “react” 時,TypeScript 會將 JSX 轉換為對應的 JavaScript 代碼,
這樣就可以在瀏覽器中運行。而當設置為 “preserve” 時,TypeScript 不會對
JSX 進行任何轉換,而是將其保留為原始的 JSX 代碼。

Q:為什麼我看到的設定是 “jsx”:”react-jsx”

在 TypeScript 4.1 之前,jsx 選項僅有兩個可選值,分別是 preserve 與 react。
其中 preserve 會保留 JSX 並將其轉換為對應的 React.createElement 函式,
而 react 則是直接轉換為對應的 JavaScript 代碼。

在 TypeScript 4.1 中,又新增了一個選項 react-jsx,用來代表 TypeScript
將會使用 @babel/plugin-transform-react-jsx 進行轉換,以此來與 Babel 兼容。

因此,如果你的 TypeScript 版本較新,通常會看到的 jsx 設定是 “jsx”: “react-jsx”,
表示 TypeScript 將會與 Babel 兼容。

小結

綜合以上問答,我們可以得知 JSX 是一種 JavaScript 語法擴展,讓開發 Web 應用程式更加方便,
而 tsconfig.json 中的 jsx 屬性用來指定 TypeScript 如何處理 JSX 語法的選項,
可以設置為 react、preserve 或 react-native 三種值,分別代表將 JSX 轉換為對應的 JavaScript 代碼、保留原始的 JSX 代碼或使用 React Native 的 JSX 語法。

而在 TypeScript 4.1 中,新增了 react-jsx 選項,用來與 Babel 兼容。
因此,如果 TypeScript 版本較新,通常會看到的 jsx 設定是 “jsx”: “react-jsx”。

寫在最後

通篇八成為 AI 牙慧,正確性未考究。

(fin)

[實作筆記] 建立團隊可用的 ESLint(一)

前情提要

在寫 JavaScript / TypeScript 的時候,常見的問題有三種:語法錯誤、風格不一致、潛在的 bug。
ESLint 可以在開發期間即時抓出這些問題,並且強制團隊遵守同一套規範。

這篇的目標:在一個 React + TypeScript 專案裡,建立一套團隊可用的 ESLint 設定。
具體來說要達成以下幾件事:

  • 涵蓋 React 與 TSX 的語法檢查
  • 套用現成的 style guide,不從零開始訂規則
  • 讓任何人拉下專案就自動受到約束
  • 之後還能整合進 CI/CD

選擇 Airbnb style guide——業界主流、規則嚴格,站在巨人的肩膀上,不用從零開始訂規範。

安裝與初始化

步驟 1:安裝 ESLint

1
npm install eslint --save-dev

步驟 2:初始化設定

1
npx eslint --init

init 過程是互動式問答,以下是我的選擇:

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
✔ How would you like to use ESLint?
❯ To check syntax, find problems, and enforce code style

✔ What type of modules does your project use?
❯ JavaScript modules (import/export)

✔ Which framework does your project use?
❯ React

✔ Does your project use TypeScript?
Yes

✔ Where does your code run?
✔ Browser

✔ How would you like to define a style for your project?
❯ Use a popular style guide

✔ Which style guide do you want to follow?
❯ Airbnb

✔ What format do you want your config file to be in?
❯ JavaScript

✔ Would you like to install them now? Yes

init 過程中選 Airbnb 後,它不會自動處理 TypeScript 支援,需要額外安裝:

1
2
3
4
5
6
7
npm install --save-dev eslint-config-airbnb-typescript \
@typescript-eslint/eslint-plugin \
@typescript-eslint/parser \
eslint-plugin-import \
eslint-plugin-jsx-a11y \
eslint-plugin-react \
eslint-plugin-react-hooks

完成後產生的 .eslintrc.js 要調整成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'airbnb',
'airbnb-typescript',
'airbnb/hooks',
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
},
plugins: ['react'],
rules: {},
}

步驟 3:調整規則

Airbnb 的規則偏嚴,第一次跑通常會噴出大量警告。建議先用 --fix 自動修正能修的:

1
npx eslint . --fix

剩下無法自動修正的,在 rules 裡逐一覆蓋:

1
2
3
4
rules: {
'no-console': 'warn', // console.log 只警告不報錯
'react/prop-types': 'off', // 用 TypeScript 就不需要 prop-types
}

如果覺得 Airbnb 太嚴、導入阻力太大,也可以改用 eslint-config-standard-with-typescript,規則較寬鬆,不需要分號。

驗證設定是否生效

package.json 加入 script:

1
2
3
4
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix"
}

執行 npm run lint,確認規則有跑起來。

參考

(fin)

[實作筆記] passport 與 passport-local 原始碼分析

前言

最近接觸的專案有一部份是 express 與登入機制有相關
使用的套件是 Passport
Passport 是這樣介紹自已的

Simple, unobtrusive authentication for Node.js
Passport is authentication middleware for Node.js. Extremely flexible and modular,
Passport can be unobtrusively dropped in to any Express-based web application.
A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.

這裡我們不作使用教學,官方都有而且相當簡單。
passport.authenticate 本質上就是一個 middleware。
而 passport.use 會用來註冊各種 Strategy。
比如說 facebook login、google login 等…
更多可見這裡

談談 passport-local

開發的過程中,有小朋友反應,如果他不輸入帳密,
頁面就會直接轉導,無法看到錯誤訊(有關 req.flash 的部份,本文不會談到)。

1
2
3
4
5
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/users/login",
failureFlash: true,
});

當然我們很清楚有設定 failureRedirect
不過讓我們不得其解的是,我們寫的整個 LocalStrategy 並沒有被執行,連 log 都沒有

這裡要翻一下原碼
這個作者的專案我覺得很棒,大多數都有測項,
除了文件外,你可以直接看測項理解它的想法。
在 Strategy.prototype.authenticate 可以發現以下程式

1
2
3
4
5
6
if (!username || !password) {
return this.fail(
{ message: options.badRequestMessage || "Missing credentials" },
400
);
}

簡單的說,它預設回傳一個 400 錯誤,而我們又設定了一個錯誤轉導。
使用這類的第三方套件就怕這類的非預期行為,
好在這個套件的文件與測項開源且清楚,我們才能快速定位問題。

參考

(fin)

[實作筆記] .ssh 與 Dropbox

警語

這不是很安全的做法。因為 .ssh 的檔案通常包含了敏感的訊息,比如私鑰。
如果放到雲端服務(Dropbox)上,就會有風險。

為什麼要這樣作 ?

現在混合的工作型態,有時會需要用家中的電腦工作,
每次都要花時間重建或是搬移 ssh pub key 顯得有點麻煩
Dropbox 本身就是一個適合的資料同步工具,之前我也設定過 zshrcvimrc 的同步機制。
同理 .ssh 也可適用,但風險需要評估

實作

  1. 將你的 .ssh 內所有檔案移至 ~/Dropbox/.ssh
  2. 刪除 .ssh 資料夾
  3. 執行以下語法
1
ln -s ~/Dropbox/.ssh ~/.ssh

(fin)

Please enable JavaScript to view the LikeCoin. :P