[學習筆記] TypeScript 字串建議的小技巧

前情提要

最近在寫一個 Hero component,需求是讓使用者能指定英雄的種族。
我們的設計有一些既定的種族,例如 human 和 demon,同時也希望讓使用者能輸入任何自定義的種族名稱。
最初的想法是用以下的定義:

1
type Race = 'human' | 'demon' | string;

並在 Hero 的 props 中使用這個型別:

1
2
3
4
5
6
7
8
9
10
11
export type HeroProps = {
name: string;
race: Race;
}

// component 大概如下
export const Hero = ({ race,name }: HeroProps) => {
return (
<h1>Hero: {name} is {race}</h1>
);
};

這樣一來,使用者可以像這樣使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/components/HeroDisplay.tsx

import React from 'react';
import { Hero } from '../components/hero'; // 引入 Hero 型別

const HeroDisplay = () => {
return (
<div>
<Hero name="alice" race="human" />
<Hero name="mark" race="demon" />
<!--more heros -->
</div>
);
};

export default HeroDisplay;

一切看似沒問題,但問題是——在使用 Hero component 時,TypeScript 並不會自動給出 human 或 demon 這樣的建議。

既然我們希望能提供建議,該怎麼解決這個問題呢?

實作記錄

解決方法看起來有些奇怪,我們可以透過將字串類型與一個空的物件相交來達成目標:

1
type Race = 'human' | 'demon' | (string & {});

這樣一來,在使用 Hero component 時,TypeScript 就會正確地給出 primary 和 secondary 的建議。
為什麼這會起作用?這其實是 TypeScript 編譯器的一個小「怪癖」。
當你把字串常值類型(例如 “human”)與字串類型(string)進行聯集時,
TypeScript 會急切地將其轉換為單純的 string,因此在 Hover 時會看到類似這樣的結果:

1
2
type Race = 'human' | 'demon' | string ;
// Hover 時會顯示: type Race = string

換句話說,TypeScript 在使用前就忘記了 human 和 demon。
而透過與空物件 & {} 進行相交,我們能「欺騙」 TypeScript,讓它在更長時間內保留這些字串常值類型。

1
2
type Race = 'human' | 'demon' | (string & {});
// Hover 時會顯示: type Race = 'human' | 'demon' | (string & {});

這樣,我們在使用 Race 型別時,TypeScript 就能記得 human 和 demon,並給出對應的建議。

值得注意的是,string & {} 實際上和單純的 string 是相同的類型,因此不會影響我們傳入的任何字串:

1
2
<Hero name="alice" race="human" />
<Hero name="mark" race="demon" />

這感覺像是在利用 TypeScript 的漏洞。
不過,TypeScript 團隊其實是知道這個「技巧」的,他們甚至針對這種情況進行測試。
或許將來,TypeScript 會原生支援這樣的功能,但在那之前,這仍是一個實用的小技巧。

範例 Code

小結

總結來說,當你想允許使用者輸入任意字串但又想提供已知字串常值的自動補全建議時,可以考慮使用 string & {} 這個技巧:

它防止 TypeScript 過早將 string | 'literal' 合併成單純的 string。
實際使用時行為與 string 一樣,但會多提供自動補全功能。
這或許不是最正式的解法,但目前仍是一個可以信賴的方式。
也許未來 TypeScript 能夠原生解決這個問題,但在那之前,這個小技巧可以為開發帶來便利。

(fin)

[學習筆記] 淺談 TypeScript 方法簡寫與物件屬性語法的差異

前情提要

在 TypeScript 中,我們常見到兩種方法的定義方式:
**方法簡寫語法(Method Shorthand Syntax) 和 物件屬性語法(Object property syntax)**。
乍看之下,這兩種語法非常相似,但實際上,Method Shorthand Syntax 在類型檢查上的表現可能會導致潛在的運行時錯誤。
本篇將討論這個問題,並提供避免這類錯誤的最佳做法。

本文

在 TypeScript 中,我們可以用兩種不同的方式定義物件的方法:

Method Shorthand Syntax:

1
2
3
interface Animal {
makeSound(): void;
}

Object property syntax:

1
2
3
interface Animal {
makeSound: () => void;
}

兩者表面上似乎只是不同的語法選擇,但實際上,它們在類型檢查時有著不同的行為。
當我們使用 Method Shorthand Syntax 時,TypeScript 的類型檢查會出現雙變性(Bivariance),
這意味著參數的類型檢查會變得寬鬆,允許接受與定義不完全符合的類型。

問題例子
讓我們看一個新的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Character {
attack(character: Character): void;
}

interface Monster extends Character {
counterattack: () => void;
}

const hero: Character = {
attack(victim: Monster) {
// victim do something
},
};

const goblin: Character = {
attack() {},
};

hero.attack(goblin); // 編譯時無錯,運行時錯誤!

在這個例子中,我們有一個 Character 介面和一個繼承它的 Monster 介面。
Monster 介面具有一個 counterattack 方法,這表示怪物應該能夠進行反擊。
接著,我們定義了一個 hero 物件,它可以攻擊任何 Monster 角色並呼叫它的 counterattack 方法。
然而,我們創建了一個 goblin 物件,這個物件實現了 Character 介面,但並不符合 Monster 介面的要求。
當我們試圖讓 hero 攻擊 goblin 時,雖然 TypeScript 在編譯時不會報錯,但在運行時會導致錯誤,因為 goblin 並沒有實作 counterattack 方法。
這是由於參數類型的雙變性(Bivariance)造成的問題,因為 hero.attack 方法的參數類型過於寬鬆,導致運行時出現預期外的行為。

解決方案
為了解決這個問題,應該使用物件屬性語法來定義方法,這樣 TypeScript 會進行更嚴格的類型檢查,並能在編譯時捕捉到類型不匹配的問題。

改寫後的範例:

1
2
3
4
5
6
7
8
9
10
11

interface Character {
attack: (character: Character) => void; // 改用 Object property syntax
}

// 這裡會報錯,因為 attack 應該傳入的是 Character
const hero: Character = {
attack(victim: Monster) {
// victim do something
},
};

在這個改寫後的範例中,TypeScript 會在編譯時警告我們 attack 方法的參數類型不匹配,從而避免了運行時錯誤。

結語

TypeScript 的類型系統非常強大,但也有一些容易被忽略的陷阱。
雙變性可能看似便利,但它也可能導致運行(Runtime)時的錯誤。
為了減少這類錯誤的風險,我們應該使用物件屬性語法來定義方法,
可以讓 TypeScript 進行更加嚴格的類型檢查,從而在開發過程中及早發現問題。

參考

(fin)

[學習筆記] 一些復盤的方法

引言

吾日三省吾身:為人謀而不忠乎?
與朋友交而不信乎?傳不習乎?
—論語‧學而

在現代化的管理手段,反省、復盤是一個系統化系統中重要的一環,本文將記錄我所知道一些復盤方法。

為什麼要復盤?

復盤的核心目的是通過回顧和分析過去的行動,提升未來的表現。復盤的主要理由有三個:

  1. 知其然與所以然
    幫助了解實際情況及其背後的原因,不僅知道「發生了什麼」,還能明白「為什麼會發生」。
  2. 從錯誤中學習,避免重蹈覆轍,進而提升表現。
  3. 總結經驗,持續改善;系統化地總結經驗教訓,轉化為改善的具體步驟或方法論。

經典的復盤方法

  1. PDCA 循環
    PDCA 是一種持續改進的工具,適用於長期項目和計劃管理。其四個步驟為:

    • Plan(計劃):設定目標和制定計劃。
    • Do(執行):實施計劃中的行動。
    • Check(檢查):檢查實施結果,與計劃進行對比。
    • Act(行動):根據檢查結果進行改進或調整,進入下一個循環。

    這種方法幫助我們在行動中不斷檢查和調整,達到持續改進的目的。

  2. SMART 目標
    SMART 是一種設定明確目標的框架,使復盤更具體化。其五個原則為:

    • Specific(具體的):目標應該明確,針對某一具體領域。
    • Measurable(可衡量的):確定可量化的指標。
    • Achievable(可達成的):設定合理且可實現的目標。
    • Relevant(相關的):確保目標與團隊或個人的發展方向一致。
    • Time-bound(有時限的):設定明確的時間範圍。

    SMART 方法幫助我們設立清晰且可衡量的目標,使復盤更具方向性和實效性。

  3. GARI 復盤法
    GARI 是一種結構化的復盤方式,幫助我們從整體回顧、分析到總結經驗教訓。
    其具體過程如下:

    • Goal(回顧目標):首先回顧當初設定的目標或期望,這有助於明確我們的初衷,為後續的結果分析奠定基礎。
    • Result(評估結果):對比實際結果與當初目標,找出成功的亮點以及不足之處。
    • Analysis(分析原因):深入分析事情成功或失敗的原因,這個過程需要考慮主觀和客觀的因素,例如個人行動、外部環境等。
    • Insight(總結規律):最後,從分析中總結出可供未來參考的經驗和規律,找出更符合本質規律的方法。

    這是一個全面的工具,適合用來結構化地反思和提升,幫助我們在下一次行動中表現得更好

  4. KISS 反思法
    KISS 是一種簡潔有效的反思框架,著眼於具體行動的調整與改進。
    它分為以下四個部分:

    Keep(需要保持的):回顧過程中哪些做法是有效的,應繼續保持並發揚光大。
    Improve(需要改進的):哪些環節還有提升的空間,並制定具體的改進計劃。
    Stop(需要停止的):有哪些不必要或無效的做法,應該果斷停止。
    Start(需要開始的):是否有新的嘗試或做法需要納入,以應對未來挑戰。

    KISS 反思法簡明扼要,能夠快速聚焦在關鍵點上,幫助我們更具效率地進行行動調整。

分析法

  1. SWOT 分析
    SWOT 分析是一種評估內外部環境的工具,幫助識別成功因素和改進空間。其四個要素為:

    • Strengths(優勢):識別內部的優勢。
    • Weaknesses(劣勢):了解內部的劣勢。
    • Opportunities(機會):評估外部可能帶來的機會。
    • Threats(威脅):分析外部的潛在威脅。
      這種方法可以幫助我們全面了解內外部環境,制定有效的策略。
  2. 魚骨圖分析(Ishikawa Diagram)
    魚骨圖用於分析問題的各種潛在原因,圖形像魚骨,因此得名。其步驟包括:
    問題作為「魚頭」,根據不同類別(如人員、設備、流程等)畫出「魚刺」。
    每條「魚刺」代表可能的原因,進行討論分析。
    這種方法能夠系統化地分析問題的多種可能原因。

  3. 德魯克的五個問題(Drucker’s five questions)
    這是一種基於商業戰略方法。其問題包括:

    • 我們的使命是什麼?What is your mission?
    • 我們的顧客是誰?Who is your customer?
    • 顧客真正重視的是什麼?What does your customer value?
    • 我們的成果是什麼? What results do you seek?
    • 我們的計劃是什麼? What is your plan?
      有一種說法是上面五個問題可以被濃縮成我們怎樣满足顧客的需求?
  4. 5 Why 分析法
    5 Why 分析法是一種深挖問題根源的方法,通過不斷追問「為什麼」來找到問題的核心原因。其步驟包括:
    從問題開始,問「為什麼」。
    每次回答後,再次問「為什麼」,重複五次或直到找到根本原因。
    這種方法能夠幫助我們深入分析問題的根本原因,避免表面化的解決方案。

其他參考的方法

  • AAR(After Action Review)
  • KPT(Keep, Problem, Try)
  • OKR(Objectives and Key Results)

結論

復盤能夠幫助我們從過去的行動中汲取智慧,避免重蹈覆轍,並促進持續進步。
「學而不思則罔,思而不學則殆」,復盤正是「思」與「學」的結合。
前面引言的白話文是:我每天都再三自我反省:替別人做事有沒有盡心竭力?
和朋友相處有沒有言而無信?學習到的道理,我有沒有好好認真實踐?

(fin)

[實作筆記] Azure Function Queue Trigger 開發(以Python為例)

前情提要

Azure Functions 提供了在雲端執行無伺服器函數的強大能力,
但在本地環境中開發和測試這些函數可以大大提高開發效率。
為了模擬 Azure Storage 服務,我們可以使用 Azurite,這是一個 Storage 的本地模擬器。
本文將記錄我如何在本機上設置建立本機的 Azurite Queue 進行開發。

註:本文假設你已具備建立 Azure Functions 的前置基礎

實作記錄

1. 建立和設定本機開發環境

安裝 Azurite

Azurite 是用於模擬 Azure Storage 服務的本地工具,您可以通過以下命令進行安裝:

1
npm install -g azurite

啟動 Azurite

啟動 Azurite 並指定 Storage 位置和日誌文件。

下面的語法會建立 .azurite 資料夾為 Azurite 的默認資料夾,
你可以根據需要修改路徑或刪除並重建此資料夾:

1
azurite --silent --location ./.azurite --debug ./.azurite/debug.log

啟動後,你會看到以下輸出,表示 Azurite 成功啟動並監聽相關端口:

1
2
3
Azurite Blob service is starting at http://127.0.0.1:10000
Azurite Queue service is starting at http://127.0.0.1:10001
Azurite Table service is starting at http://127.0.0.1:10002

設定本機 Azure Storage 連線字串

為了方便操作本機 Azure Storage,
我們需要設置 AZURE_STORAGE_CONNECTION_STRING 環境變數:
這裡要查看微軟官方文件取得地端連線字串
你們可以看到它包含了一組 AccountKey 與 Account(devstoreaccount1)
這個例子中我們只使用了 Azurite Queue Service

1
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;"

開發完成後,記得刪除此連線字串:

1
unset AZURE_STORAGE_CONNECTION_STRING

常用語法

檢視所有 Queue

1
az storage queue list

建立 Local Queue

1
az storage queue create --name myqueue

建立資料到指定 Queue 中, 下面的例子會建立一包 JSON 檔,當然你也可以使用純文字(text)

1
az storage message put --queue-name myqueue --content "{\"message\": \"Hello, World\!\", \"id\": 123, \"status\": \"active\"}"

顯示前 5 筆指定 Queue 中的資料

1
az storage message peek -q myqueue --num-messages 5

取出 Queue 中的資料

1
az storage message get --queue-name myqueue --num-messages 1

刪除 Queue 中的資料

1
az storage message delete --queue-name myqueue --id <message-id> --pop-receipt <pop-receipt>

補充說明:

popReceipt 是 Azure Queue 中用來確認消息取出和刪除操作的唯一識別碼。
當取出消息時,Azure 會返回 popReceipt,確保只有取出的客戶端能夠刪除該消息。
如果 popReceipt 顯示為 null,通常表示消息尚未取出或命令不正確。
要獲取 popReceipt,使用 az storage message get 命令取出訊息。

2. Azure Functions 的本機開發與執行

啟動 Azure Functions

當本機環境設置完成後,你可以使用以下命令來啟動 Azure Functions:

1
func start

這條命令會啟動你的本地 Azure Functions 執行環境,使你可以在本機上測試和調試你的函數。

3. 加碼,代碼檢查和格式化

安裝與配置 Pylint

Pylint 是一個 Python 代碼靜態分析工具,可以檢查代碼中的錯誤和不符合最佳實踐的地方。首先,安裝 Pylint:

1
pip install pylint

配置 Pylint,創建或修改 .pylintrc 文件來包含你的檢查規則:

1
2
3
4
5
[MESSAGES CONTROL]
disable=C0114,C0115,C0116

[FORMAT]
max-line-length=120

使用 Pylint 進行代碼檢查

運行以下命令來檢查所有 Python 文件:

1
pylint *.py

安裝與使用 Black 進行代碼格式化

Black 是一個自動格式化 Python 代碼的工具,能夠保持代碼風格的一致性。首先,安裝 Black:

1
pip install black

格式化整個專案的所有 Python 文件:

1
black .

整合檢查和格式化工具

你可以將代碼檢查和格式化工具整合到一個命令中,這樣可以簡化工作流程:

1
pylint *.py && black .

例外

在 Azure Functions 中,某些函數參數不符合 Pylint 的命名規則,這可能會導致部署失敗。你可以忽略這些特定的 Pylint 警告,例如:

1
2
# pylint: disable=C0103
def dispatch_worker(inputQueue: func.QueueMessage, watchQueue: func.Out[str]):

問題與排除

  • 如果 Azurite 無法啟動,請檢查是否已正確安裝 Azurite 以及是否有其他應用程序佔用了相關端口。
  • 如果 Azure Functions 無法啟動,請確保所有相關的配置文件和依賴項已正確設置。

參考

(fin)

[踩雷筆記] Gitlab 整合 Azure DevOps Pipeline 以 python on Azure Functions 為例

前情提要

我目前主要使用 GitLab 進行版本控制和持續集成(CI/CD),
主要整合 Google Cloud Platform (GCP) ,許多專案都運行在 GCP 上。
因商務需求最近開始探索第二朵雲 Azure。
雖然 Azure DevOps 也有提供 CI/CD 與 Repo 的解決方案;
但為了減少邏輯與認知負擔,我希望能將 GitLab 與 Azure DevOps Pipeline 進行整合。
具體來說,這次要在 Azure Functions 上部署 Python 應用程式,
我想要 RD 往 Gitlab 推送並執行 CI/CD 就好,而不用特別因為服務在不同的雲上,而需要推送到不到同 Repo 中 。
面對這樣的需求,下面是我找到的解決方案。

實作

以這次的例子來說,我需要控管 Azure 的 Serverless 解決方案「Azure Functions」的程式。
但是 Azure DevOps Pipeline 有相當高度的整合 Azure Cloud,只要能將程式推送到 Azure DevOps Repo,
部署就會相當簡單,而無需處理繁鎖的授權問題
CI/CD 流程大致如下

  • 建立相對應的權限與憑証並提供給 Gitlab-Runner
  • RD 推送新版本程式給 Gitlab,觸發 Gitlab-Runner
  • Gitlab-Runner 執行測試、建置等相關作業後,部署到 Azure Functions
  1. 設置 Azure DevOps Pipeline

    1. 選擇 New pipeline
    2. Other Git
    3. 設定連線方式 & 選擇分支
      1. Connection name (任意命名)
      2. Repo URL 輸入 Gitlab Repo URL
      3. User Name (任意命名)
      4. Password / Token Key (Gitlab PAT,需注意效期)
    4. 使用「Azure Functions for Python」Template
      • Build extensions
      • Use Python 3.10(可以更換合適的版本)
      • Install Application Dependencies
      • Archive files
      • Publish Artifact: drop
    5. 追加 Agent job Task
      • 搜尋「Azure Functions Deploy」
      • 填寫
        • Azure Resource Manager connection
          • Manage > Service Connection > New Service Connection > Azure Resource Manager > Service principal(automatic)
            • Service connection name
            • Description (optional)
          • 也可以選擇 > Service principal (manual),需要先加上 App registrations 具體流程如下:
            • 在 Azure Portal 上建立一個新的 Azure Registration。
            • 選擇 Certificates & secrets,建立一組 Certificates & secrets。
            • 回到 Service principal (manual)
              • Subscription Id
              • Subscription Name
              • Service Principal Id (App registrations Client secrets 的 Secret ID)
              • Service principal key (App registrations Client secrets 的 Value)
              • Tenant ID
        • App type (我的情況是選 Function App on Linux)
        • Azure Functions App name
  2. 配置 GitLab CI/CD

    • 在 GitLab 中,建立 .gitlab-ci.yml 如下:
    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
    image: debian:stable-slim
    variables:
    AZURE_PIPELINE_NAME: "dispatch-worker-deploy-pipeline"

    before_script:
    - apt-get update && apt-get install -y curl jq

    stages:
    - deploy

    trigger_pipeline:
    stage: deploy
    script:
    - |
    json=$(curl -u $AZURE_DEVOPS_USER:$AZURE_DEVOPS_PAT \
    -H "Content-Type: application/json" \
    "https://dev.azure.com/Aiplux/Inpas/_apis/build/definitions?api-version=6.0")
    id=$(echo $json | jq -r --arg pipeline_name "$AZURE_PIPELINE_NAME" '.value[] | select(.name==$pipeline_name) | .id')
    echo -e "\033[1;33mPipeline: $AZURE_PIPELINE_NAME ID is $id\033[0m"
    RESPONSE_CODE=$(curl -X POST "https://dev.azure.com/My_Organization/My_Project/_apis/build/builds?api-version=6.0" \
    --data '{"definition": {"id": '$id'}}' \
    -u ${AZURE_DEVOPS_USER}:${AZURE_DEVOPS_PAT} \
    -H 'Content-Type: application/json' \
    -w "%{http_code}" -o /dev/null -s)
    if [ "$RESPONSE_CODE" -ne 200 ]; then
    echo -e "\033[1;31mRequest failed with status code $RESPONSE_CODE\033[0m"
    exit 1
    fi

    rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

    可以看到 AZURE_DEVOPS_USER 與 AZURE_DEVOPS_PAT 兩個參數,
    可以在登入 Azure DevOps 後,在 User Settings >> Personal Access Tokens 取得,
    實務上由管理者提供,但是有時效性,仍然需要定期更新(1年),應該有更簡便的方法才對。

完成以上的設定後,只要推送到 Gitlab Repo 的 main 分支,就會觸發 Azure DevOps Pipeline 部署。

踩雷

在整合過程中遇到了一個問題。
儘管使用了 Azure Pipeline 提供的官方模板,部署過程依然出現了錯誤。
具體而言並沒有明顯的錯誤,但是 Log 會記錄

1 function found
0 function loaded

導致 Azure Functions Apps 無法正常工作。

解法

參考

在 Azure Functions for Python 其中一步驟 Install Application Dependencies
Template 如下:

1
2
3
4
python3.6 -m venv worker_venv
source worker_venv/bin/activate
pip3.6 install setuptools
pip3.6 install -r requirements.txt

需要修改成才能作用,不確定 Azure 會不會提出修正或新的 template

1
2
3
4
python3.10 -m venv .python_packages
source .python_packages/bin/activate
pip3.10 install setuptools
pip3.10 install --target="./.python_packages/lib/site-packages" -r ./requirements.txt

查閱了 Azure 和 GitLab 的官方文檔以及技術社群中的討論,找到了有效的解決方案。
通過修改 template,成功解決了部署問題,
GitLab 和 Azure DevOps Pipeline 的整合成功。

參考

(fin)

[踩雷筆記] Azure Function 開關虛擬機與錯誤排除

前情提要

在 Azure Functions 和 Queue 的架構下,我們試圖通過自動化開關虛擬機器來節省成本。  
這是因為 GPU 設備的價格相當高昂,而我們的 AI 服務又離不開這些昂貴的機器。

實作記錄

最初的構想非常簡單:通過 Queue 接收特定的任務,然後由 Azure Function 判斷需要哪些資源,
如果需要進行 AI 計算,就必須啟動特定的虛擬機器,而在任務完成後,再將機器關閉。
這樣的方式不僅能夠節省資源,還能有效控制成本。

以下是 Python 程式碼的大致實作,主要為虛擬機器的開關控制邏輯:

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
import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.mgmt.compute import ComputeManagementClient
import logging, os

app = func.FunctionApp()

@app.queue_trigger(arg_name="args", queue_name="vm-queue", connection="CONNECTION_STRING")
def main(args: func.QueueMessage):
logging.info('Queue Msg: %s', args.get_body().decode('utf-8'))

try:
credential = DefaultAzureCredential()
subscription_id = os.getenv("SUBSCRIPTION")
resource_group = os.getenv("RG")
vm_name = os.getenv("VM_NAME")

compute_client = ComputeManagementClient(credential, subscription_id)
vm = compute_client.virtual_machines.get(resource_group, vm_name, expand='instanceView')
vm_status = vm.instance_view.statuses[1].display_status
logging.info('VM 狀態: %s', vm_status)

if vm_status == "VM running":
logging.info('正在關閉VM: %s', vm_name)
operation = compute_client.virtual_machines.begin_deallocate(resource_group, vm_name)
operation.result()
logging.info('%s 已關閉', vm_name)
elif vm_status in ["VM deallocated", "VM stopped"]:
logging.info('正在啟動VM: %s', vm_name)
operation = compute_client.virtual_machines.begin_start(resource_group, vm_name)
operation.result()
logging.info('VM %s 已啟動', vm_name)
else:
logging.info('非預期的 VM 狀態: %s', vm_status)

except Exception as e:
logging.error('異常錯誤: %s', str(e))

異常問題:EnvironmentCredential

1
2
3
4
5
6
7
8
9
10
DefaultAzureCredential failed to retrieve a token from the included credentials.
Attempted credentials:
EnvironmentCredential: EnvironmentCredential authentication unavailable. Environment variables are not fully configured.
Visit https://aka.ms/azsdk/python/identity/environmentcredential/troubleshoot to troubleshoot this issue.
ManagedIdentityCredential: ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint.
SharedTokenCacheCredential: SharedTokenCacheCredential authentication unavailable. No accounts were found in the cache.
AzureCliCredential: Azure CLI not found on path
AzurePowerShellCredential: PowerShell is not installed
AzureDeveloperCliCredential: Azure Developer CLI could not be found. Please visit https://aka.ms/azure-dev for installation instructions and then,once installed, authenticate to your Azure account using 'azd auth login'.
To mitigate this issue, please refer to the troubleshooting guidelines here at https://aka.ms/azsdk/python/identity/defaultazurecredential/troubleshoot.

這個問題表面上看似由於權限不足引起的,但實際上根本原因是缺乏正確的環境變數配置。
要使 Azure Function 能夠正常運作並獲得所需的權限,我們需要在 Function 的環境設置中正確配置相應的環境變數。
這些環境變數包括關鍵的憑證和授權信息,它們使得 Azure Function 能夠在執行過程中獲取必要的存取權限,從而能夠正常與 Azure 資源進行交互。
如果環境變數配置不當或缺失,Azure Function 將無法獲得所需的授權,從而導致權限不足的錯誤。
確保這些環境變數被正確設置和管理,是解決此類問題的關鍵步驟

配置的方式有三種

如果要使用 Service Principal 的 Client Secret,配置

  • AZURE_CLIENT_ID
  • AZURE_TENANT_ID
  • AZURE_CLIENT_SECRET

如果要使用 Service Principal 的 Certificate 驗證,配置

  • AZURE_CLIENT_ID
  • AZURE_TENANT_ID
  • AZURE_CLIENT_CERTIFICATE_PATH
  • AZURE_CLIENT_CERTIFICATE_PASSWORD (Optional)

若要使用密碼進行用戶身份驗證,配置

  • AZURE_USERNAME
  • AZURE_PASSWORD

我選擇第一種配置,需要先加上 App registrations
具體流程如下:

  • 在 Azure Portal 上建立一個新的 Azure Registration。
  • 選擇 Certificates & secrets,建立一組 Certificates & secrets。
  • 到 Azure Function Apps 設定環境變數 AZURE_CLIENT_ID 與 AZURE_CLIENT_SECRET,
  • Tenant_ID 可以在透過 Azure Portal 找 Tenant Properties 查詢,一樣設定到環境變數。
  • 接下來將 Registration 設定為 Virtual Machine 的 Contributor
    (待確認開關機是不是有更小的權限?ex:Virtual Machine Contributor、Virtual Machine Operator:)

如此一來在開關機時就能有足夠的權限。

參考

(fin)

[學習筆記] SQL 軟刪除與索引 (MySQL/MSSQL/PostgreSQL)

前情提要

最近需要實現軟刪除功能,但在設計索引時遇到了一些問題。
商務情境如下,
有效的 ACCOUNT 必須是唯一的。
然而,帳戶有可能會被軟刪除,所以被刪除的 Account 可能會有多筆相同的資料。

表格設計

1
2
3
4
5
CREATE TABLE User (
Id SERIAL PRIMARY KEY,
Username VARCHAR(255) NOT NULL,
IsDeleted BOOLEAN DEFAULT TRUE
);

簡化設計的用戶表格包含 Id、Username 和 IsDeleted 欄位。
依商業需求,當 IsDeleted 為 0 時,Username 必須唯一。
而且很有可能會有多筆相同的帳號被刪除,當 IsDeleted 為 1 時,Account 不會限制只有一筆(允許多筆)

後端工程師建議使用觸發器(Trigger)或在應用層(Backend)實現這個約束,
根據我的記憶,在微軟的 SQL Server 中,有一種稱為「條件約束」的設定,能夠針對特定條件創建索引。
可以大幅節省開發成本,我認為這類的功能不應該只有微軟專有,故作了一些搜尋後,特別以此篇記錄。

實作

我找到一個測試的網站,你可以直接在這裡測試,不需要花費額外心力建立 SQL Server

MySQL

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
-- 創建表格
CREATE TABLE Users (
Id INT AUTO_INCREMENT PRIMARY KEY,
Username VARCHAR(255) NOT NULL,
IsDeleted BOOLEAN DEFAULT FALSE
);

-- 設置 AUTO_INCREMENT 起始值為 1000
ALTER TABLE Users AUTO_INCREMENT = 1000;

-- 創建唯一索引,只針對 IsDeleted = FALSE 的行
CREATE UNIQUE INDEX unique_active_account ON Users ((CASE WHEN IsDeleted THEN Username END));
-- 插入數據
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user2', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user3', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制
INSERT INTO Users (Username, IsDeleted) VALUES ('user4', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user5', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 測試重複的 Username 插入,應該失敗
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 失敗,因為 user1 已經存在且 IsDeleted = FALSE
-- 測試重複的 Username 插入,但 IsDeleted = TRUE,應該成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 檢查插入結果
SELECT * FROM Users;

PostgreSQL

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
-- 創建表
CREATE TABLE Users (
Id SERIAL PRIMARY KEY,
Username VARCHAR(255) NOT NULL,
IsDeleted BOOLEAN DEFAULT FALSE
);

ALTER SEQUENCE users_id_seq RESTART WITH 1000;
-- 創建部分索引,只針對 IsDeleted = FALSE 的行
CREATE UNIQUE INDEX unique_active_username ON Users (Username)
WHERE IsDeleted = FALSE;


-- 插入範例數據
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user2', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user3', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制
INSERT INTO Users (Username, IsDeleted) VALUES ('user4', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user5', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 測試重複的 Username 插入,應該失敗
-- INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 失敗,因為 user1 已經存在且 IsDeleted = FALSE
-- 測試重複的 Username 插入,但 IsDeleted = TRUE,應該成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 檢查插入結果
SELECT * FROM Users;

MSSQL

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
-- 設置正確的 SET 選項
SET QUOTED_IDENTIFIER ON;
SET ANSI_NULLS ON;

-- 創建表
CREATE TABLE [User] (
Id INT IDENTITY(1000, 1) PRIMARY KEY,
Username VARCHAR(255) NOT NULL,
IsDeleted BIT DEFAULT 0
);

-- 創建篩選唯一索引
CREATE UNIQUE INDEX unique_active_account ON [User](Username)
WHERE IsDeleted = 0;

-- 插入範例數據
INSERT INTO [User] (Username, IsDeleted) VALUES ('user1', 0); -- 成功
INSERT INTO [User] (Username, IsDeleted) VALUES ('user2', 0); -- 成功
INSERT INTO [User] (Username, IsDeleted) VALUES ('user3', 1); -- 成功,因為 IsDeleted = 1 不受唯一索引限制
INSERT INTO [User] (Username, IsDeleted) VALUES ('user4', 0); -- 成功
INSERT INTO [User] (Username, IsDeleted) VALUES ('user5', 1); -- 成功,因為 IsDeleted = 1 不受唯一索引限制

-- 測試重複的 Username 插入,應該失敗
INSERT INTO [User] (Username, IsDeleted) VALUES ('user1', 0); -- 失敗,因為 user1 已經存在且 IsDeleted = 0

-- 測試重複的 Username 插入,但 IsDeleted = 1,應該成功
INSERT INTO [User] (Username, IsDeleted) VALUES ('user1', 1); -- 成功,因為 IsDeleted = 1 不受唯一索引限制

-- 檢查插入結果

SELECT * FROM [User];

小結

不太需要複雜的後端程式或是 DB Trigger,只需要在建立索引時加上條件,
特別注意 MySQL 的語法是使用 CASE WHEN,其他 DB 是使用 WHERE,
這與 DB 版本也有關係,使用前應該進一步去查詢官方文件。

另外關於遞增欄位,在不同的 DB 也有不同的實作方式。
實務上通常不用了解這麼多 DB 的差異,僅僅是我個人的好奇補充罷了,
業界主推還是 PostgreSQL,我個人不夠專業,但三種都略有碰過,最熟的還是 MSSQL。
僅為個人學習記錄,如果要在三者中擇一還是需要多方考慮自身的 Context 再作決定。

參考

(fin)

[實作筆記] Bash 輸出彩色技巧

前情提要

在 Bash 腳本中,有時我需要以提高日誌的可讀性或突顯重要資訊。
通過 ANSI escape codes 改變文本的顏色、背景顏色和樣式。
可以讓腳本的輸出更加直觀且友善。

本文

在 Bash 中,我們可以使用 ANSI escape codes 來實現文本的高亮顯示和顏色選擇。
以下是一些常見的 ANSI 顏色碼:

前景顏色

  • 黑色\033[30m
  • 紅色\033[31m
  • 綠色\033[32m
  • 黃色\033[33m
  • 藍色\033[34m
  • 洋紅色\033[35m
  • 青色\033[36m
  • 白色\033[37m

背景顏色

  • 黑色\033[40m
  • 紅色\033[41m
  • 綠色\033[42m
  • 黃色\033[43m
  • 藍色\033[44m
  • 洋紅色\033[45m
  • 青色\033[46m
  • 白色\033[47m

樣式

  • 粗體\033[1m
  • 下劃線\033[4m
  • 反向\033[7m
  • 重置\033[0m

透過這些代碼,你可以靈活地控制文本的外觀。例如,使用 \033[1;31m 可以讓文本變為紅色粗體,使用 \033[0m 可以重置樣式回到預設。

示例

下面是如何使用這些顏色碼來高亮顯示 echo 輸出的示例:

1
2
3
4
5
6
7
echo -e "\033[1;31m這是一段紅色粗體文本\033[0m"
echo -e "\033[1;32m這是一段綠色粗體文本\033[0m"
echo -e "\033[1;33m這是一段黃色粗體文本\033[0m"
echo -e "\033[1;34m這是一段藍色粗體文本\033[0m"
echo -e "\033[1;35m這是一段洋紅色粗體文本\033[0m"
echo -e "\033[1;36m這是一段青色粗體文本\033[0m"
echo -e "\033[1;37m這是一段白色粗體文本\033[0m"

參考

ANSI escape codes - Wikipedia

(fin)

[實作筆記] 初體驗設定 Nvidia GPU 的 Azure VM -- 錯誤排除

前情提要

RD 們反應因為 Driver 更新導致服務異常,停止運作。
重新安裝時又遇到一些奇怪的問題,例如:
錯誤:

1
2
3
4
5
sudo apt install -y linux-modules-nvidia-550-azure nvidia-driver-550
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package linux-modules-nvidia-550-azure

或是

1
2
Failed to initialize NVML: Driver/library version mismatch
NVML library version: 535.183

依實際狀況與 RD 討論,發現他們沒有一個標準的作業程序,調研時也沒有任何記錄,
東作一塊,西作一塊,沒有辦法很清楚了交互的作用關係。
故由我重新實作一遍,記錄並排除相關錯誤。

實作記錄

  1. Azure 開機,指定作業系統為 Ubuntu 22.04,並且需注意 A100 系列機器只在特定的 Zone 才有資源開機,本次使用東日本 zone2 的資源

  2. 安裝 CUDA, 參考官方文件 3.9. Ubuntu

  3. 安裝 ubuntu-drivers-common

    • sudo apt install ubuntu-drivers-common
    • sudo apt install alsa-utils #音效相關,不一定要裝,但是可以減少指令時的錯誤訊息
    • ubuntu-drivers device
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ ubuntu-drivers devices
    == /sys/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0004:00/VMBUS:00/00000041-0001-0000-3130-444532304235/pci0001:00/0001:00:00.0 ==
    modalias : pci:v000010DEd000020B5sv000010DEsd00001533bc03sc02i00
    vendor : NVIDIA Corporation
    driver : nvidia-driver-555-open - third-party non-free
    driver : nvidia-driver-470-server - distro non-free
    driver : nvidia-driver-535-server-open - distro non-free
    driver : nvidia-driver-555 - third-party non-free recommended
    driver : nvidia-driver-535-server - distro non-free
    driver : nvidia-driver-470 - distro non-free
    driver : nvidia-driver-550 - third-party non-free
    driver : nvidia-driver-550-open - third-party non-free
    driver : nvidia-driver-535-open - distro non-free
    driver : nvidia-driver-545-open - third-party non-free
    driver : xserver-xorg-video-nouveau - distro free builtin
  4. 更新套件

    1. 加上drivers的 repo 路徑

      sudo add-apt-repository ppa:graphics-drivers/ppa

    2. 更新

      sudo apt update

    3. 安裝趨動, 讓系統自已判斷安裝什麼版本, 參考 NVIDIA drivers installation | Ubuntu

      sudo ubuntu-drivers install

    4. 重開機

      sudo reboot

    5. 查一下,他幫你裝哪個版本

      dpkg -l | grep nvidia-driver

    6. 再安裝指定版本的 Ubuntu Azure Package,也可以至 Ubuntu 網站 搜尋確認

      sudo apt install -y linux-modules-nvidia-<version>-azure

確認是否成功

1
nvidia-smi

 看到下面的畫面就是成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ nvidia-smi
Tue Jul 30 08:07:18 2024
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.90.07 Driver Version: 550.90.07 CUDA Version: 12.4 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA A100 80GB PCIe Off | 00000001:00:00.0 Off | 0 |
| N/A 32C P0 43W / 300W | 1MiB / 81920MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+

參考

(fin)

[實作筆記] 初體驗設定 Nvidia GPU 的 Azure VM

前情提要

隨著深度學習和 AI 的普及,許多工作和研究需要強大的運算能力,
而 GPU 提供了相較於傳統 CPU 更高效的計算能力。
因此,我選擇在 Azure 上設定 Nvidia GPU 虛擬機來滿足這些需求。
這篇文章將分享我在 Azure 上設定 Nvidia GPU 虛擬機的初體驗,並記錄實作過程中的一些重點。

前置作業

在開始設定 GPU 虛擬機之前,需要先完成以下準備工作:

  1. Azure 帳戶:確保你已經擁有 Azure 的帳戶,並且帳戶中有足夠的配額來創建 GPU 虛擬機。
  2. 選擇適合的虛擬機規格:Azure 提供多種 GPU 虛擬機型號,如 NV 系列和 NC 系列,根據需求選擇合適的型號。
  3. 安裝 Azure CLI:透過 Azure CLI 可以更方便地管理和配置虛擬機,可以在本地環境中安裝並配置 Azure CLI。
  4. 創建資源群組與虛擬機
1
az group create --name <myResourceGroup> --location eastus
1
2
3
4
5
6
7
az vm create \
--resource-group myResourceGroup \
--name myT4VM \
--image UbuntuLTS \
--size Standard_NC6s_v3 \
--admin-username azureuser \
--generate-ssh-keys

實作步驟

可以參考官方手冊),
本文大量引用 3. Package Manager Installation 中 3.9 的篇幅

登入到虛擬機後,安裝 Nvidia 驅動程式

1
2
3
sudo apt-get update
sudo apt-get install -y nvidia-driver-470
sudo reboot

安裝當前運行的內核版本所需的 Linux 標頭文件

1
sudo apt-get install linux-headers-$(uname -r)

刪除過時的金鑰(實作上,跟本沒有這個金鑰,所以跳過也沒關係)

1
sudo apt-key del 7fa2af80

查詢作業系統與晶片架構

1
2
3
4
5
6
7
8
> lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.4 LTS
Release: 22.04
Codename: jammy
> uname -m
x86_64

安裝 CUDA-Keyring

1
wget https://developer.download.nvidia.com/compute/cuda/repos/$distro/$arch/cuda-keyring_1.1-1_all.deb

參考前一步驟將 $distro 換成 ubuntu2204$arch 換成 x86-64,
也可以直接到此 https://developer.download.nvidia.com/compute/cuda/repos 找到你合適的 deb 檔
下載:

1
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86-64/cuda-keyring_1.1-1_all.deb

安裝:

1
sudo dpkg -i cuda-keyring_1.1-1_all.deb

安裝 CUDA SDK

1
sudo apt-get install cuda-toolkit

安裝 Nvidia GDS 驅動,提升 GPU 和存儲間的高效數據傳輸性能

1
sudo apt-get install nvidia-gds

重啟主機

1
sudo reboot

確認是否安裝成功

1
nvidia-smi

 看到下面的畫面就是成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> nvidia-smi
Mon Jul 1 09:21:24 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.183.01 Driver Version: 535.183.01 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 Tesla T4 Off | 00000001:00:00.0 Off | Off |
| N/A 31C P8 9W / 70W | 140MiB / 16384MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| 0 N/A N/A 1095 G /usr/lib/xorg/Xorg 130MiB |
| 0 N/A N/A 1356 G /usr/bin/gnome-shell 7MiB |
+---------------------------------------------------------------------------------------+

20240730 更新

追加異常記錄

參考

(fin)

Please enable JavaScript to view the LikeCoin. :P