[實作筆記] Gitlab CI/CD 與 GCP - Workload Identity Federation

前情提介

現況用 Service Account 會有什麼問題?
當我們希望與第三方的服務與 GCP 作整合時,
傳統的(我之前的)作法是透過建立 Service Account Key 去提供給第三方資源存取的權限。
這會產生資安隱患,主要是這把 Key 的權粒度大難以稽核,有效期長風險,
而要定期更換 Key 會變成一個麻煩的管理問題。

需求介紹

我目前透過 GCS 並掛載 Load Balancing 部署靜態網站,
而 CI/CD 是透過 Service Account 的 Key 去執行工作,
這是一種有資安隱憂的作法,所以我試著使用 Workload Identity Federation 取代

概念

TLDR;

而 Workload Identity Federation 是基於 IAM 機制,允許第三方服務整合 GCP 資源,
背後的技術原理是基於 OIDC, 在這裡我們不過度展開,簡單描述如下:

  1. Gitlab CI/CD 首先取 Gitlab OIDC Token,取得 Token 的作法可以參考官方文件,下面是個簡單的範例:
1
2
3
4
5
6
7
8
9
10

job_with_id_tokens:
id_tokens:
FIRST_ID_TOKEN:
aud: https://first.service.com
SECOND_ID_TOKEN:
aud: https://second.service.com
script:
- first-service-authentication-script.sh $FIRST_ID_TOKEN
- second-service-authentication-script.sh $SECOND_ID_TOKEN

|OIDC 是基於 Oauth2 的標準,簡單可以想成 Oauth2 再加上身份驗証。
2. 有了 Gitlab OIDC Token,我們可以透過 GOOGLE STS(Security Token Service) API 取得 Federated Token
在這裡我們需要先建立好 Workload Identity Provider(IdP),而可以設定 Attribute Conditions 來作限制
3. 這個時候可以用 Federated Token 與 GCP IAM API 交換來一個短周期的 Access Token
4. 本質上還是用 Service Account 在作事,但是用短周期的 Access Token 取代 Key, 從而簡化了 Key 的管理工作

實作步驟

  1. 建立 Workload Identity Pool

    1
    2
    3
    4
    5
    6
    #Update $GCP_PROJECT_ID value
    gcloud iam workload-identity-pools create gitlab-test-wip \
    --location="global" \
    --description="Gitlab demo workload Identity pool" \
    --display-name="gitlab-test-wip" \
    --project=$GCP_PROJECT_ID
  2. 設定 workload identity pool provider 並建立 Attribute conditions,
    這步的關鍵是讓只符合你條件設定的 User 才能取得 Token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #Update GITLAB_NAMESPACE_PATH value
    gcloud iam workload-identity-pools providers create-oidc gitlab-identity-provider --location="global" \
    --workload-identity-pool="gitlab-test-wip" \
    --issuer-uri="https://gitlab.com" \
    --allowed-audiences=https://gitlab.com \
    --attribute-mapping="google.subject=assertion.sub,attribute.aud=assertion.aud,attribute.project_path=assertion.project_path,attribute.project_id=assertion.project_id,attribute.namespace_id=assertion.namespace_id,attribute.namespace_path=assertion.namespace_path,attribute.user_email=assertion.user_email,attribute.ref=assertion.ref,attribute.ref_type=assertion.ref_type" \
    #--attribute-condition="assertion.namespace_path.startsWith(\"$GITLAB_NAMESPACE_PATH\")" \
    --attribute-condition="assertion.namespace_path.startsWith(\"marsen\")" \
    --project=$GCP_PROJECT_ID
  3. 建立 GCP Service Account
    在我的例子中

    1
    2
    3
    4
    5
    6
    7
    #Create a service account
    gcloud iam service-accounts create gitlab-runner-sa --project=$GCP_PROJECT_ID

    #Add sample permissions to the Service account
    gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
    --member=serviceAccount:gitlab-wif-demo@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
    --role=roles/storage.admin
  4. 建立 Service Account 與 WIP 的角色關係綁定

    可以先取得專案的 GCP Project Id

    1
    PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value core/project) --format=value\(projectNumber\) --project $GCP_PROJECT_ID)

    設定 Service Account 的角色為 workloadIdentityUser,並將其設定為 workloadIdentityPools 的服務帳戶

    1
    2
    3
    gcloud iam service-accounts add-iam-policy-binding gitlab-runner-sa@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
    --role=roles/iam.workloadIdentityUser \
    --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/gitlab-test-wip/*"
  5. 建立 Gitlab CI/CD 進行測試

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
61
62
63
64
65
66
67
68
69
70
71
image: node:20.9.0-alpine

.gcp_wif_auth: &gcp_wif_auth
#id_tokens to create JSON web tokens (JWT) to authenticate with third party services.This replaces the CI_JOB_JWT_V2
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
before_script:
- apt-get update && apt-get install -yq jq
#Get temporary credentials using the ID token
- |
PAYLOAD=$(cat <<EOF
{
"audience": "//iam.googleapis.com/${GCP_WORKLOAD_IDENTITY_PROVIDER}",
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"scope": "https://www.googleapis.com/auth/cloud-platform",
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
"subjectToken": "${GITLAB_OIDC_TOKEN}"
}
EOF
)
- |
echo "Payload: ${PAYLOAD}"
- |
FEDERATED_TOKEN=$(curl -s -X POST "https://sts.googleapis.com/v1/token" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "${PAYLOAD}" \
| jq -r '.access_token'
)
#- |
# echo "Federated Token: ${FEDERATED_TOKEN}"
#Use the federated token to impersonate the service account linked to workload identity pool
#The resulting access token is stored in CLOUDSDK_AUTH_ACCESS_TOKEN environment variable and this will be passed to the gcloud CLI
- |
WHAT_IT_IS=$(curl -s -X POST "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateAccessToken" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer ${FEDERATED_TOKEN}" \
--data '{"scope": ["https://www.googleapis.com/auth/cloud-platform"]}' \
| jq -r '.'
)
- |
echo "WHAT_IT_IS: ${WHAT_IT_IS}"
- |
export CLOUDSDK_AUTH_ACCESS_TOKEN=$(curl -s -X POST "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateAccessToken" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer ${FEDERATED_TOKEN}" \
--data '{"scope": ["https://www.googleapis.com/auth/cloud-platform"]}' \
| jq -r '.accessToken'
)

stages:
- deploy-prod

deploy-prod:
variables:
GCP_PROJECT_NAME: my-project-9527
GCP_WORKLOAD_IDENTITY_PROVIDER: "projects/000009527/locations/global/workloadIdentityPools/gitlab-test-wip/providers/gitlab-identity-provider"
SERVICE_ACCOUNT_EMAIL: "[email protected]"
<<: *gcp_wif_auth
stage: deploy-prod
image: google/cloud-sdk:latest
script:
- echo "Deploying artifacts to PROD GCS🚀🚀🚀"
- echo $CLOUDSDK_AUTH_ACCESS_TOKEN
- gcloud config set project ${GCP_PROJECT_NAME}
- gcloud storage cp -r $CI_PROJECT_DIR/dist/* gs://my-static-website/

參考

(fin)

[學習筆記] 為前端框架 Component 建立 Props Typing 的幾種方式,以 React 為例

前言

在開發 React 應用程式時(其它框架其實也適用),
使用 TypeScript 來強制定義 props 是一個常見的做法。
然而,有許多不同的方法可以實現這一目標。這篇文章討論了三種主要的方法。

比較

Inline Object Literals

在這個方法中,我們直接在函式的參數位置定義了 props 的型別,如下所示:

1
2
3
4
5
const Wrapper = (props: {
children?: ReactNode;
}) => {
return <div>{props.children}</div>;
};

優點:快速簡潔。
缺點:不太適合長期維護。

另一種解構的寫法會有更少的字數,
它會有一個 {}:{}特殊寫法,前者表示傳入的參數,後者表示要被解構的物件與型別
這樣的寫法最主要在 component 當中要使用傳入參數時可以省略 prop. 的語法,
但是理解上會不會更困難(對於不熟悉的開發者而言)就見團隊見智了…

1
2
3
4
5
6
7
8
 
const Wrapper = ({
children,
}: {
children?: ReactNode;
}) => {
return <div>{children}</div>;
};

Type Aliases

在這個方法中,我們把 props 的型別定義抽取到一個類型別名中:

1
2
3
4
5
6
7
export type WrapperProps = {
children?: ReactNode;
};

const Wrapper = (props: WrapperProps) => {
return <div>{props.children}</div>;
};

優點:可以在其他文件中重用。
缺點:在大型代碼庫中可能會讓 TypeScript 變慢。

Interfaces

在這個方法中,我們使用介面來定義 props:

1
2
3
4
5
6
7
export interface WrapperProps {
children?: ReactNode;
}

const Wrapper = (props: WrapperProps) => {
return <div>{props.children}</div>;
};

優點:性能較好,在大型代碼庫中表現較好。
缺點:需要較多的代碼。

小結

儘量使用 interface 會有較好的效能,同時可以共用這些代碼並幫助理解。

參考

(fin)

[學習筆記] Vim 學習資源

介紹

Vim 是一款強大高效的文本編輯器,強調效率和快速操作。
學習 Vim 能顯著提升編輯速度和效率,無需滑鼠,節省操作時間。
這裡記錄著我的學習資源,提供給未來的我複習使用。

TLDR Tips

  1. gg:移動到檔案的第一行
  2. G:移動到檔案的最後一行
  3. gg=G:重新縮排整個檔案
  4. gv:重新選取上一次的視覺選取
  5. `` <`:跳到上一次視覺選取的開始
  6. `` >`:跳到上一次視覺選取的結尾
  7. ^:移動到行的第一個非空白字符
  8. g_:移動到行的最後一個非空白字符
  9. g_lD:刪除行上的所有尾部空白
  10. ea:在當前單字的末尾插入
  11. gf:跳到游標下的文件名
  12. xp:向前交換字符
  13. Xp:向後交換字符
  14. yyp:複製當前行
  15. yapP:複製當前段落
  16. dat:刪除包括標籤在內的 HTML 標籤
  17. dit:刪除 HTML 標籤內的內容,但不包括標籤本身
  18. w:向右移動一個單字
  19. b:向左移動一個單字
  20. dd:刪除當前行
  21. zc:關閉當前摺疊
  22. zo:打開當前摺疊
  23. za:切換當前摺疊
  24. zi:完全切換摺疊
  25. <<:向左移動當前行的縮排
  26. >>:向右移動當前行的縮排
  27. z=:顯示拼寫更正
  28. zg:添加到拼寫字典
  29. zw:從拼寫字典中刪除
  30. ~:切換當前字符的大小寫
  31. gUw:將大小寫轉換到單字的末尾(u 用於小寫,~ 用於切換)
  32. gUiw:將整個單字轉換為大寫(u 用於小寫,~ 用於切換)
  33. gUU:將整行轉換為大寫
  34. gu$:將直到行尾的文本轉換為小寫
  35. da":刪除下一個雙引號括起來的字符串
  36. +:移動到下一行的第一個非空白字符
  37. S:刪除當前行並進入插入模式
  38. I:在行的開頭插入
  39. ci":更改下一個雙引號括起來的字符串內容
  40. ca{:更改大括號內的內容(也可以試試 [, ( 等)
  41. vaw:視覺選取單字
  42. dap:刪除整個段落
  43. r:替換字符
  44. ``[`:跳轉到上次複製的文本的開始
  45. ``]`:跳轉到上次複製的文本的結尾
  46. g;:跳轉到上次更改的位置
  47. g,:向前跳轉到更改列表
  48. &:在當前行上重複上次的替換
  49. g&:在所有行上重複上次的替換
  50. ZZ:儲存當前檔案並關閉

學習資源

內建學習工具

1
> vimtutor

(fin)

[實作筆記] 一些關於 Azure Resource Group 的冷知識

前情提要

Resource Group 是 Azure 上比較特別的一個設計,
這裡拿來記錄一些知道就知道,不知道就不知道的小事務。

本文

NetworkWatcherRG

注意

當您使用 Azure 入口網站 建立網路監看員實例時:

網路監看員實例的名稱會自動設定為NetworkWatcher_region,其中region會對應至 網路監看員 實例的 Azure 區域。
例如,在美國東部區域中啟用的網路監看員名為NetworkWatcher_eastus。
網路監看員實例會建立在名為NetworkWatcherRG的資源群組中。 若尚無該資源群組,將會加以建立。

Azure DevOps

當我們將 Azure DevOps 的 Billing 綁定之時,
會建立一組 VisualStudioOnline-XXXX 的 Resource Group

(fin)

[實作筆記] Macbook 壓縮檔案

前情提要

假設分享機密文件給同事或者發送文件到郵件或雲端儲存時,就必須手動處理壓縮和加密,
可以參考一些文章,大致有三個作法

  • 購買付款壓縮軟體
  • 使用雲端服務
  • 手動執行 terminal 指令

手動執行就可以處理的問題,我不會特別想要付款買一個軟體,
而雲端服務會擔心資訊安全,特別是要加密的資料代表有一定程度的重要性。
Terminal 大概是網路文章的主流解。
當我需要經常這樣做時,就太麻煩了,還要記得操作指令,額外增加心智負擔。
理想上,我希望只要選擇文件或資料夾,右鍵點擊”Zip with Password”,就能創建加密的 zip 文件。

實作

  1. 打開 Automator app。

  2. 創建一個新 Automator 文件:File > New (或按 ⌘N),選擇 “Quick Action” 類型。

  3. 將輸入類型更改為 “files or folders”。

  4. 在左側的 Actions library 中,雙擊 “Run AppleScript”,或拖放到右側工作區。刪除示例代碼,替換為第 5 步的腳本。

  5. 複製並粘貼下面的 AppleScript 代碼。

    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

    set prompt_text to "請輸入壓縮密碼"

    repeat
    set zip_password to text returned of (display dialog prompt_text default answer "" with hidden answer)
    set verify_password to text returned of (display dialog "再次輸入壓縮密碼" buttons {"OK"} default button 1 default answer "" with hidden answer)
    considering case and diacriticals
    if (zip_password = verify_password) then
    exit repeat
    else
    set prompt_text to "密碼不一致,請重新設定"
    end if
    end considering
    end repeat

    tell application "Finder"
    set the_items to selection
    if ((class of the_items is list) and (count of the_items) > 0) then
    set items_to_zip to ""
    repeat with each_item in the_items
    set each_item_alias to each_item as alias
    set item_name to name of each_item_alias
    set item_name to quoted form of (item_name & "")
    set items_to_zip to items_to_zip & item_name & " "
    end repeat

    set first_item to (item 1 of the_items) as alias
    set containing_folder to POSIX path of (container of first_item as alias)
    set zip_name to text returned of (display dialog "輸入壓檔名" default answer "")
    set zip_file_name to quoted form of (zip_name & ".zip")

    if zip_password is not equal to "" then
    -- 如果存在密碼,執行加密壓縮
    do shell script "cd '" & containing_folder & "'; zip -x .DS_Store -r0 -P '" & zip_password & "' " & zip_file_name & " " & items_to_zip
    else
    -- 否則執行單純壓縮
    do shell script "cd '" & containing_folder & "'; zip -x .DS_Store -r0 " & zip_file_name & " " & items_to_zip
    end if
    else
    display dialog "你未選擇任何檔案。" buttons {"OK"} default button 1
    end if
    end tell

  6. 儲存工作流程:File > Save (或按 ⌘S),給它一個名字,如 “Zip with Password”

現在,選擇文件或文件夾,右擊,選擇 “Quick Actions” 中的 “Zip with Password”,
按照提示輸入密碼、驗證密碼和設置 zip 文件的名字。測試新創建的 zip 文件,確保一切運作正常。

參考

(fin)

[實作筆記] ClamAV 安裝

提要

因 ISO 需要在 GCP VM(Linux 系統)上安裝防毒。
ClamAV 是一款強大的開源防毒引擎,專為檢測和清除惡意程式而設計。
這篇用來記錄終端機中安裝和配置 ClamAV,包括系統更新、病毒庫更新、自動運行設定等步驟,
以確保系統有效抵禦各種威脅。

實作

首先,在終端機中輸入以下指令,展開系統更新的步驟:

1
sudo apt update && sudo apt upgrade

出現警示請按 Y

1
2
After this operation, 12.1 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y

接著,透過以下指令安裝 ClamAV,一強大的防護工具,以確保系統免受惡意程式的侵害:

1
sudo apt install clamav clamav-daemon -y 

當安裝完成後

確保沒有其他 freshclam 進程在運行,您可以使用以下指令查看:

1
ps aux | grep freshclam

如果有其他 freshclam 進程在運行,請終止(kill)它們。

執行以下指令可更新 ClamAV 的病毒定義庫:

1
sudo freshclam

隨後,您可以進行系統掃描,以查找並清除潛在的威脅,請使用以下指令:

1
sudo clamscan -r /path/to/folder

若欲使 ClamAV 在系統啟動時自動運行,請執行以下指令:

1
sudo systemctl enable clamav-daemon

這將設定 ClamAV 在每次系統啟動時主動保護您的系統。

啟動 ClamAV 服務(如果它沒有在系統啟動時自動啟動):

1
sudo systemctl start clamav-daemon

檢查 ClamAV 服務的運行狀態:

1
sudo systemctl status clamav-daemon

這樣,您可以確保 ClamAV 已經更新、服務已經啟動,並檢查服務的當前運行狀態。

參考

(fin)

[生活筆記] 2023 的回顧與展望

引言

2023 年過去了, 作個回顧

工作相關

2023 年對我來說是個不穩定的一年,甚至考慮出國去唸語言學校,但最終留在了台灣。
T 社的本來想說點什麼,一年內提筆多次想想就又算了。
一年都過了還是作個簡單記錄。

T 社事件

本來想加入一個前輩 S 作為主管的團隊,
與這位前輩其實沒那麼多接觸,但知道技術實力不差,本來也是 N 社早期的創業員工之一。
預計是希望能近距離合作並學習,並以創業心態開發一款國際產品。
實際上加入 T 社後,S 桑進公司的次數屈指可數,
產品的發展也與預期不符,本質上市場已有大量類似的競品,
T 社的母公司 W 集團,本質上只是要一個可以隨時改動的開發者或團隊,
而缺乏軟體工程的管理專業,使得他們沒有辦法有效追蹤。

需求與溝通

後續的結果是開發團隊與 W 集團互信不足,拍過戲的老闆,只能帶著兒子、特助或是咖啡甜點來信心喊話,
團隊內的產品部門變成了對講機與美工,唯一的功能就是轉達集團的需求與繪製 Figma。
整個產品團隊無法回答開發團隊的疑問,反而不斷的抱怨集團、特助或老闆,
整個溝通變得無效且變形

開發

反過來是招募時,餅畫得太大,導致 RD 明顯過度設計,
比如說,RD 跑了一個沒有 Domain Expert 的 DDD,
在沒有上線、沒有賺錢沒有用戶的情況下,用自已的腦補進行了領域的分工,
不到10人的團隊切分了 10 幾個微服務,並且濫用了 GraphQL。
這樣又帶來後續的問題,
第一、語言不一致,一開始的想法是讓各個開發者有開發的自主,但問題變成無法互相支援,各自畫地為王
第二、開發風格不一致,即使是使用相同的語言,使用的套件與寫作風格也未統一,所以要支援也有難度
第三、開發準備作業多,很多需求經討論後都需要新建成服務,RD 需要從建立 Repo,
到自行建置 GCP 相關服務(網路、資料庫、CI/CD 等),
第三、串接複雜,API 重重呼叫,為了要追蹤錯誤,需要延申很多額外的設計,
一個簡單的 CRUD,需要兩個 GraphQL 如果有要異動到呼叫的 API,更新一個 API 就要動到三個專案
第四,但是因為團隊缺乏領導者(S 桑)的情況下,開發者沒有有效的權責分工,GCP 的權限被一些早期入職的員工掌握,而需要等待。
第五,無溝通與不透明,晨會就形同虛設,整個團隊每個人變成一個一個穀倉

缺乏用戶

整個產品核心的兩個功能(點餐與收費)在兩個系統上,一個外部公司開發的軟體,一個在老舊的 POS。
而我們開發的系統比較像是一個單純的 App 加周邊整合。
這麼簡單的東西,卻用了一個複雜的架構,導致修改速度緩慢,遲遲無法上線,面對需求調整時也沒有彈性。
更進一步加深集團與開發團隊的不信任。

團隊狀況

資料與雲端權限在一個菜鳥身上,不論有意無意都是開發上的瓶頸,
前端上班睡覺,開發品質低下的 React,產品設計 UX 體驗差但其實也沒有人在使用的後台,
APP 組用了 Flutter 思維確停留在 IOS/Android/Web 的分工,到後來也無法互相支援,
想學 C# 的 RD 整天吹噓 N 社的失敗專案(XmiERP),而在代碼品質可讀、可測試性極差,
想學 Golang 的 RD 應該是整體過度設計的原兇,第一時間離職了。
S 桑遲遲未進入團隊後,來了一個之前也在 N 社待過”幾天”的 R 主管,缺乏開發能力(至少在這樣的穀倉下),也缺乏軟體開發管理的素養,  
唯一的能力是簡報,但是產品設計不合理,比如說,他花了3個月搞了一個動態標籤點餐,
然後跟廠商要了一個動態標籤機,丟在那裡再放3個月,然後拒絕承認有過這件事ಠ_ಠ

我的努力

試著找 S 桑與團隊開了一場大會,總算把 APP 推上架。
上架唯一可以收費的功能,並給了團隊與主管中實的踐言。
儘可能的 Change Your Company。
只可惜殘磚破瓦築的城,塗上黃澄澄的牛糞也不會變成黃金城。

以上是 T 社的故事概述,至於關於 R 與 W 集團的非法行為就另一個故事了(🍵

顧問

年初有找到很多的機會,例如在 C 社擔任顧問職,
或是接案,也算是個不錯的經驗,主要的發現如下。

  • 沒有識別專業能力的能力→如何確定軟體開發的產品如期如質?
  • 沒有正確分工的能力→大環境太偏向技能分工(ex:前後端、SRE、DBA etc…, 我認為應該更偏向需求端與實作端)
  • 沒有建立與管理流程的能力,版控流程、權限控管、需求流程

這剛好是我擅長且有經驗的,
資深的開發者的問題在停止學習,執著於自已的開發習慣,
資淺的開發太想炫技,而過度設計,兩者都需要透過管理手段去調控。

A 社相關

目前的事就先不多說,至少我開發的東西已經幫公司賺到 3000 萬以上,
遺憾的事是,幾個下屬想拉拉不起來,只好協議讓他離開,一切流程也合法合情,  
這也是我說的,有資金的人不見得有識別專業的能力,千里馬常有伯樂不常有。
其他一些甘苦還在進行式,至少比 T 社好多了,就不多談。

  • 主管技能學習中…
  • GCP 技術大躍進
  • ISO 認証學習中
  • Azure 技術學習中

生活

  • 潛水終於考到 AWO,下次想換個潛點了  
  • 跳舞方面
    • 跟知名的老師跳了一首,2023 足夠了
    • 上了幾個國際老師的課,不過還是沒有感到進步
      • Jo
      • Meti & Andante
      • Bianca and Nils (沒上課但是有跟 Bianca 跳到舞)
      • Lisa & Fabien
      • Dasha & Roman (有跟 Dasha 跳到舞)
      • Melanie & Jeremy
      • Chloe & Rico
      • Moondewti
      • Laura & Youngbo
    • 在誠品表演、嚐試一些空中動作 
    • 喜歡的樂手
      • Charlie Christian
      • YOASOBI
    • 喜歡的漫畫
      • 莽送的莉芙蓮
      • Völundio(休刊中)

     

  • 換了護照準備出國但是還不知道要去哪裡?
    • 打算再帶媽媽出國一次
    • 也想去倫敦看阿森納比賽
    • 也想再參加國外的 workshop
    • 不想變成只會希望而不行動的麵包師傅

展望

投資

Q1 總資產有望達到一個重大里程碑。下一個目標是持股資產達這個里程碑

英文

制定計劃中

工作

仍是目標全遠端/斜槓或接案

社交

毫無頭序,也應該制定計劃嗎???

健康

踢球?健身?跳舞

(fin)

[生活筆記] 修正 A 校的題目缺陷

導言

在 A 校擔任雲端助教一陣子,主要有幾個原因,
我也是資策會出身,所以我對轉職的學生可以感同身受,
我在教學的過程中,自已也會成長,很多時候可以學習到新知也得到成就感,
最後就是可以獲得額外的收入。

問題發現

這次是一個我有興趣的題目,簡單說 A 社利用 Replit 網站提供作業,
由學生進行撰寫,很聰明的整合方式,而且幾乎零成本。
而且我覺得最棒的是,他可以寫測試腳本,透過測試腳本,就可以驗收大部份的作業,加速批改的時間。

但是,這次同學的作業在執行時期,發生了異常掉入了一個無限廻圈的狀態
問題不難,是一個邊際值的問題。 應該可以加上測試保護,這是我的第一個直覺。
我去檢查了 A 校提供的標準答案,不出意外的也有相同的問題。

另外一個問題是,寫邊際值的測試案例不是基本的嗎?
查看了測試案例,竟然還真的沒有寫

問題排查

我選擇下載了 Replit 到地端開發,
在開始前先簡單描述題目,在一個限範圍內進行猜數字

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
const answer = Math.floor(Math.random() * 100) + 1
// start coding

let min = 1
let max = 100
let guess = Math.floor(Math.random() * 100) + 1
let count = 1

function getResult() {
while ( ) {
if ( ) {

} else if ( ) {

}
guess = Math.floor((max + min) / 2)
count ++
}
console.log( )
}

// 以下為測試檔,請勿更動
getResult()

module.exports = {
guess,
answer,
count,
getResult
}

這是很好的題目,可以同時使用到迴圈與判斷,也可以接觸 Math 模組。
我們來看一下測試案例,在 Replit 右下角的 Unit Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
test("", async function() {
const spy = jest.spyOn(console, 'log')
const {
guess,
answer,
count,
getResult
} = index;

getResult()
if (guess === answer) {
expect(count).toBeLessThanOrEqual(10)
}
});

測試很單純,當答案在 1~100 之間時,執行迴圈的次數不應該超過 10 次(其實 7 次內應該都猜得出來)
我們應該加上一些邊際測試。
例如 1 與 100 的案例,

1
2
3
4
5
6
7
8
9
10
11
12
13
it("Answer_is_1", async function() {
const spy = jest.spyOn(console, 'log');
const originalMathRandom = Math.random;
Math.random = jest.fn()
.mockImplementationOnce(() => 0.001)
.mockImplementation(() => originalMathRandom());
//console.log('now random is', Math.random());
const index = require('./index');
index.getResult();

// Clean up
spy.mockRestore();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
it("Answer_is_100", async function() {
const spy = jest.spyOn(console, 'log');
const originalMathRandom = Math.random;
Math.random = jest.fn()
.mockImplementationOnce(() => 0.999)
.mockImplementation(() => originalMathRandom());
//console.log('now random is', Math.random());
const index = require('./index');
index.getResult();

// Clean up
spy.mockRestore();
});

這裡注意到的是我們 mock 了Math.random,因為這才會影響我們的答案。

問題後的問題

當我們在測試案例為 100 時,會限入無窮迴圈,而 JavaScript 單緒的特性將無法離開這個測試,
雖然也不會報錯…但是測試將永遠跑不完。
這驅使我再加上一個測試案例,當執行次數超過 10 次時拋出例外。
而學生使用的範本,我希望儘可能不去修改它,
這裡要對 jest 與 javascript 要有足夠的理解才可以寫的好,
所幸在這個時代,有 AI 與 google 的加持下,很快就解決了。

最後的測試程式如下

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
describe('Guessing Game', () => {
let originalMathRandom;
let spy;

beforeEach(() => {
// Save the original functions
originalMathRandom = Math.random;
spy = jest.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
// Restore the original functions
Math.random = originalMathRandom;
spy.mockRestore();
jest.resetModules()
});

it("Answer_is_1", async function() {
Math.random = jest.fn().mockReturnValue(0.001);
const index = require('./index');
index.getResult();
expect(index.answer).toBe(1);
});

it("Answer_is_100", async function() {
Math.random = jest.fn().mockReturnValue(0.999);
const index = require('./index');
index.getResult();
expect(index.answer).toBe(100);
});

it('guess under 10 times', () => {
Math.random = jest.fn().mockReturnValue(0.5);
const index = require('./index');
for(let i=0; i<9; i++) {
index.getResult();
}
expect(() => index.getResult()).not.toThrow();
});

it('should throw error if count is more than 10', () => {
const originalMathFloor = Math.floor;
Math.floor = jest.spyOn(global.Math, 'floor')
.mockImplementationOnce(() => Math.floor(Math.random() * 100) + 1)
.mockImplementationOnce(() => 1000);
const index = require('./index');
expect(() => index.getResult()).toThrow('超過10次了,請重新開始');

});


// Add more tests as needed for the getResult function
});

給學生的出題程式

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
const answer = Math.floor(Math.random() * 100) + 1;

// start coding

let min = 1;
let max = 100;
let guess = Math.floor(Math.random() * 100) + 1;
let count = 1;

function getResult() {
console.log(`Guess number is ${guess}`);
while ( ) {
if ( ) {

} else if () {

}

count++;
if (count > 10) {
throw new Error('超過10次了,請重新開始');
}
}
console.log(`第${count}回合,您猜${guess},猜對了`);
}

// 以下為測試檔,請勿更動
module.exports = {
guess,
answer,
count,
getResult,
};

解答範本

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
const answer = Math.floor(Math.random() * 100) + 1;

// start coding

let min = 1;
let max = 100;
let guess = Math.floor(Math.random() * 100) + 1;
let count = 1;

function getResult() {
console.log(`Guess number is ${guess}`);
while (guess !== answer) {
if (answer < guess) {
max = guess; //猜太大取代最大值
console.log(`第${count}回合,您猜${guess},太大了,請猜介於${min}~${max}之間的數字`);
} else if (answer > guess) {
min = guess; //猜太小取代最小值
console.log(`第${count}回合,您猜${guess},太小了,請猜介於${min}~${max}之間的數字`);
}
guess = (max === min + 1) ? max : Math.floor((max + min) / 2);

count++;
if (count > 10) {
throw new Error('超過10次了,請重新開始');
}
}
console.log(`第${count}回合,您猜${guess},猜對了`);
}

// 以下為測試檔,請勿更動
module.exports = {
guess,
answer,
count,
getResult,
};

小結

整個課程的調整與測試改寫,大概花了我 4~12 小時處理。
在改制後,互動環節變少了,計時制改為定時制的給薪,也讓無法花太多的時間幫學生排查問題。
這題目我有興趣就順手解決了。
可惜的是,我拿不到任何費用。

也再一次印証 AI 的強大,未來的人材需要有更高的整合能力,
寫測試寫程式,讀技術文章賺取資訊落的錢應該會越來越難賺。
但是能高度整合的人應該會更為搶手。

(fin)

[實作筆記] GCP Armor 設定

前情提要

在加入負載平衡(load balancer)後,我們發現防火牆的某些規則失效了,
尤其是那些僅允許公司內網存取站台的規則。
這是相當重要的規則,因為我們的部分開發資訊與半成品儲存在這些機器上。

之所以建立了負載平衡(load balancer),有兩個原因
首先,我需要作後端 API 的版本切換,而因為使用的程式語言不同並部署在不同主機,
而 load balancer 透過路由規則將流量導向不同的機器群的機制非常適合。
第二,我們的正式環境一直有 load balancer,而測試環境沒有,趁這個機會將沙盒/測試環境的配置調整成一致。

問題

本來有設定某些防火牆的規則,在掛載 load balancer 後就失效了。
原因是規則會識別流量的 IP,而掛載後,所有的流量對於機器來說,所有流量都來自 load balancer 的 IP 了。
這樣的規則形同虛設,這個時候 Google Armor 就是一個不錯的替代方案

Google Armor Tips

設定上十分簡單,在 Cloud Armor 頁面上選擇 policies,
我的情況是在建立 load balancer 後,就已經自動建立,
所以就直接修改。

參考規則如下

Action Type IP Addresses/Ranges Priority
Allow IP addresses/ranges 5*.1**.***.205/32 999
10.140.0.0/20
Deny IP addresses/ranges 0.0.0.0/0 1,000
(block all)

一個是對指定來源 IP 與內網允許流量進來,一個是拒絕所有流量,這裡的設定與防火牆蠻像的。
接下來是目標的部份,要設定你的機器群,設定好套用,大約 10 分鐘內就會生效(實測不到3分鐘就生效了)

小結

Google Armor的主要優勢之一是其簡單易用的設定。
在Cloud Armor頁面上,我們可以輕鬆地設定我們的安全策略,包括允許特定IP範圍的流量進入,同時拒絕不受歡迎的流量。
這些設定反映在我們的參考規則中,確保了對特定IP和內部網路的控制,同時阻止不受歡迎的流量。

特別是對於我們使用負載平衡器進行後端API版本切換和環境配置調整的需求,Google Armor提供了一個理想的解決方案。
其能夠在不同機器群之間巧妙地分發流量,確保我們的應用程式在不同版本之間平穩運作,同時維持良好的安全性。

這樣的調整不僅解決了我們遇到的具體問題,也提升了整體系統的安全性和可靠性。

(fin)

[實作筆記] Hexo 7 升級的錯誤處理心得

前情提要

最近在對Hexo進行升級至7.0版本的過程中,遇到了一些錯誤與問題。
這篇部落格將記錄下這次升級的過程中所遇到的問題以及解決方法,希望對有需要的讀者有所幫助。

錯誤記錄

Hexo Action 錯誤

升級後,遇到了Hexo Action報錯的問題,錯誤訊息如下:

1
2
3
4
5
6
node:internal/modules/cjs/loader:1051
throw err;
^

Error: Cannot find module 'hexo-util/lib/spawn'
Require stack:

解決方式是查找相應的模組,更新配置文件,在本案中,hexo-util/lib/spawn 路徑不存在,應該改為 hexo-util/dist/spawn。

地端執行 Hexo d 時 Permission Denied

在地端執行Hexo d時,遇到了權限問題,無法提交到GitHub的問題。
暫時解決方式是將repository的協議由ssh改為https,暫時繞過了問題。

1
2
3
4
deploy:
type: git
repository: https://github.com/marsen/marsen.github.io.git
branch: master

後來因 https 需要提供帳號密碼,而這些資訊不適合簽入版本控制,故改回了 ssh,
為了解決相應的 SSH 連線問題,需配置 SSH 連線 .ssh/config
參考:

1
2
3
4
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa

YouTube Tag 無法處理

升級至Hexo 7.0後,發現內建的YouTube標籤無法使用,原因是Hexo 7.0中刪除了這些內建的標籤。
解決方式是引入安裝 hexo-tag-embed package。

Hexo 部署錯誤快取

在部署過程中,遇到了Hexo的快取問題,導致地端和CI都出現異常。
解決方式是清除 Hexo 的快取 hexo clean,完整 SOP 如下:

1
2
3
4
sudo rm -rf .deploy_git
hexo clean
hexo g
hexo d

使用 Docker 偵錯

Hexo Action本身是以容器去運行的,可以在本地端執行測試,不需推版
使用 Docker 進行 Hexo Action 時,進行偵錯的方法:

建置與執行

1
2
docker build -t hexo-action .
docker run -d hexo-action

觀察 logs 與 files 去偵錯,Docker Desktop 是很好的工具。

小結

這次Hexo 7.0的升級過程中,遇到了不少問題,但通過查找相應的解決方案,一一解決了這些問題。
在升級過程中,需要留意Hexo的官方文檔以及相關插件的更新,以確保能夠順利完成升級。

參考

(fin)

Please enable JavaScript to view the LikeCoin. :P