[實作筆記] 靜態網站部署整合 GCP (二) --- Load Balancing & Cloud Storage

前請提要

去年有記錄一篇有關 GCP 部署靜態網站時遇到情境與問題,
不過比較偏像流水帳,不同的部署策略都在同一篇文章,另外有一些問題也得到解決,
除了更新原有文章外,這篇的目的是為了將 Load Balancing 與 Cloud Storage 抽出,
並加以潤飾。

概要

  • 在 Cloud Storage 建立一個 Bucket
  • 在 Network Service 建立一個 Load Balancing
  • 設定 Load Balancing 的 Backend 綁定到 Bucket
  • 作為網站,需要允許所有人讀取 Bucket 的內容

必要條件

GCP 平台的帳戶與足夠的權限,並且已建立專案

在 Cloud Storage 建立一個 Bucket

第一步前往Cloud Storage
點擊 +CREATE BUCKET,
在 Choose where to store your data 的區塊,
Location type 有三種

  • multi-region
  • dual-region
  • region

這是地理位置與高可用性的相關設定,越後面的設定成本越便宜,但是可用性也越低。
即使如此 google 仍保証了 SLA: 99.95% 的高可用性。

接下來是 Choose a default storage class for your data 的區塊,有

  • Standard
  • Nearline
  • Coldline
  • Archive

等四種不同的設定,
與檔案的使用頻率有關,對於網站來說建議使用 Standard 。
收費可以參考下表

Standard Nearline Coldline Archive
Storage(per GB-Month) $0.026 $0.01 $0.007 $0.004
retrieval(per GB-Month) Free $0.01 $0.02 $0.05
Class A Operations(per 1000ops) $0.005 $0.01 $0.01 $0.05
Class B Operations(per 1000 ops) $0.0004 $0.001 $0.005 $0.05
SLA 99.95% 99.9% 99.9% 99.9%

接下來是 Choose how to control access to objects 的設定,
不要勾選 Enforce public access prevention on this bucket,
Access control 選擇 Uniform , 這裡的設定是為了避免從 internet 存取 bucket 的資料,
但是我們的目的是放置靜態網站的資料,所以不需設定。

再來是 Choose how to protect object data
這是保護資料的策略,有版本(versioning)與備份(retention)兩種策略,
我們不需要所以選擇 None

按下 Create 以建立 Bucket,
接下來為了讓 web 存取我們選擇 more action(3 個點的 Icon ) > Edit Access
New Principals > 選擇 allUsers > Storage Object Viewer.
接下來可以上傳你的靜態網站的資源了,這裡我們多作一個設定,通常我們要指定網站的首頁為何,
約定成俗是 index.html,一樣 more action(3 個點的 Icon ) > edit website configuration
將 Index (main) page suffix 設定為 index.html(記得 bucket 裡要有這個檔)

Load Balancing 相關設定

接下來設定 Load Balancing,
這只需要設定一次,在目前的實作未涉及 VPC、CDN、Path Rules 等相關議題,
實務上需要的話,請再加上去考量。

  1. GCP > Network Service > Load Balancing
  2. Create Load Balancer
  3. Backend configuration > Create A Backend Bucket
    • 自已取一個 Backend Bucket name
    • Cloud Storage Bucket > Browse > 選取之前所建立的 Bucket
    • 不勾選 Enable Cloud CDN, 相關的設定與應用程式的應用有關, 較為複雜之後再進行處理
  4. Host and path rules > Simple host and path rule
  5. Frontend IP and PORT > Add Frontend IP And Port
    • Protocol > HTTP (正常應使用 HTTPS, 這次為了求快而未作相關設定)
    • Network Service Tier > Premium

可以參考 GCP 的教學與我們實作上的細微差異

  • Create a bucket. → 手動建立只需要處理一次
  • Upload and share your site’s files. → CI 執行
  • Set up a load balancer and SSL certificate. → 手動建立只需要處理一次
  • Connect your load balancer to your bucket. → 手動建立只需要處理一次
  • Point your domain to your load balancer using an A record. → 未處理
  • Test the website.

CI/CD 的相關設定

參考以下部份的 .gitlab-ci.yml 檔
非常簡單,只需要 gsutil rsync -R 將前一個 job 建置的檔案推到 Cloud Storage 即可

1
2
3
4
5
6
7
8
9
10
deploy-job: # This job runs in the deploy stage.
stage: deploy # It only runs when *both* jobs in the test stage complete successfully.
image: google/cloud-sdk
needs:
- job: build-job
artifacts: true
script:
# - gcloud auth list # Show the ACTIVE ACCOUNT *
- gsutil rsync -R build gs://your_bucket_name
- echo "Application successfully deployed."

其它

這裡仍有一個未知的設定,gsutil 命令會需要權限才能對 Cloud Storage 寫入,
如何讓 Gitlab-Runner 底下的 Container 擁有指定的 GCP 權限呢 ?
試著下 gcloud auth list 會發現確實有一個可工作的 Account 所以一定有相關的設定要處理,
因為沒有實作到,故不作記錄,但未來再有機會不要忘了這一段的工程。

大部份的工作只需要設定一次,未來只需要 CI 將新版的靜態網站上傳到 Cloud Storage 網站就會更新。

參考

(fin)

[生活筆記] 2022 貓劇感想

第一次看到貓劇是在高中音樂課上,老師所播放了 DVD 的片段,
具體的內容也記不清了——當時的老師常常放影片上課,
比如說用廚具演奏的樂團、貓劇等,——
當初只覺得特別,甚至有點詭異,並沒有什麼感觸。

大學畢業後,等待當兵的時間,不知道在什麼時間點,我再次接觸了貓劇。
這次就陷入狂熱(FEVER),一開始是在網路上找片段看,後來也買了 DVD,
整個部片應該看了不下 30 次,甚至是可以聽著音樂睡覺。
我最喜歡的是看每隻貓咪的互動與細節。從那個時候開始,我就希望可以前往看貓劇的演出。

我已經不確定當時有沒有確認過停演資訊,但是我知道倫敦是貓的首演場地,
所以我心中一直想去倫敦參觀(更多的部份是阿森納)
而目前知道倫敦已經沒有演出了,順代一提,歌詞有偷婊熱刺的部份 XDD

1
2
3
4
Grizabella, The Glamour Cat

She haunted many a low resort
Near the grimy road of Tottenham Court

2018 年我開始學習 Lindy Dance 與聽爵士樂,2020 在課堂上認識了一些朋友有在看音樂劇,
聊天過後,再次激起心中的火花。2021 雖然歷經疫情,但是台灣國內的狀況一直相對穩定,
所以開售當天馬上買了票,在朋友的推薦下,大手筆直接買了衛武營 5800 的票,
結果疫情爆發,就拖到了 2022 年。
雖然票價不便宜,但是這次的體驗真的值得。

好的初體驗,第一次看音樂劇就是史上有名的劇,位置在前排中間的位置;
先說說硬體設施,前排是看不到字幕的,不過貓不是一部需要字幕的戲劇,
特別是我以前看過無數次的前提下,除了 Growltiger 的部份沒有看過外,
其它的部份我都相當的熟悉,加上文本本身是詩集的原因,所以其實並不需要太過依賴字幕,
如果英聽夠好的話更是不需要。

第一排的好處是可以看到細節,這點非常適合貓劇,比起劇情與主要出場貓咪的表演來說,
更重要的是散落在背景的貓咪表情、肢體其實正在無時無刻演出,有時他們的互動會讓我誤以為是演員在聊天,
但是隨著演員的動作,才會發現即時聚光燈不在他們身上,他們也仍然在演出,只要在舞台上就是戲的一部份。
而這個距離也可以看到布景的細節
聲音部份是真的可以聽到人聲,不是透過音響的聲音,可以感覺到人聲的媚力,
這次我的位置聽 Cassandra 聲音真得超清楚(聽說麥克風是藏在他們的頭上,
但是他們會互相撥頭時也不會有聲音,所以收音對我來說還是個迷)

  • 會被演員當 Target Point Out ,被點到時真得很有觸電感
  • 終場 Rum Tum Tugger 會超近

但是也是有缺點的部份,首先是視覺的角度較低,在

  • 衛武營第一排有時會被燈箱(或是風扇、提詞機)或貓檔住
  • 當貓咪圍成圈的時候也會被檔住
  • 側面進場的貓咪要轉頭才知道,沒有機會互動

下次看建議的位子是可以平/俯視舞台的地方,靠走道有機會互動

這次補上更多細節是 DVD 沒有的

  • 三大母貓的舞蹈很多也很好看(dvd 看不到完整的)
    • Demeter >> Munkustrap
    • Bombalurina >> Rum Tum Tugger
    • Jellylorum >> Gus(Growltiger >> Griddlebone)
  • JennyAnyDots 有一大段的 TAP 表演(有點像火焰之舞)在 DVD 是刪減版
  • 群舞時有很多三層次的編排在 DVD 看不出來,太近也會看不清楚
  • Mungojerrie And Rumpelteazer 的排舞有點像馬戲團的表演,不太確定舞蹈風格
  • Growltiger >> Griddlebone 的表演像義大利歌劇
  • Tantomile & Coricopat 通常會率先作出反應
  • Victoria 在 Old Deuteronomy 歌詞中 Victoria’s accession 被提到時會被 Munkustrap Cue 一下這點很可愛
    • 常在 C 位
    • solo 的部份我覺得更像體操或芭蕾
    • 舞會 spotlight 的部份搭配的是 Plato/George/Admetus(名字不一定)
  • Jemima 才是第一個用唱歌的方式回應 Grizabella 的小貓,一直記錯成 Victoria (Victoria 是用肢體接觸),初演是 Sarah Brightman 飾演的角色,這次有一段用中文演唱(但是我其實希望他能唱原文就好…中文讓人出戲)
  • Mistoffelees 是芭蕾的表演,特別是連續轉圈(The Conjuring Turn)
  • 我以為演員會讓我認不出來,但是化裝真的很厲害,就像是有臉譜一樣,跟 DVD 版本沒有什麼差別
  • Grizabella 被趕走的時候讓我想到取消文化與 Lindy Dancer Max 的事件
  • 2019 電影版是邪典加悲劇,我反而希望可以買到 DVD 版本的數位檔
  • 2022 年在台灣有幫 Old Deuteronomy 脫口罩(好像是 Alonzo), 有一種現實的可愛無奈

參考

-https://leticiaschic.com/cats-musical-actor-part1/

(fin)

[實作筆記] SonarCloud with .NET 6.0.x

前情提要

承上篇,在升級了 .NET 6.0.x 後發現,
SonarCloud 的測試含概率不知何時變成 0 ,
相對應的 Code Smell 也沒有作檢查, 加上原本的 github/workflow 並沒特別分門別類,
所以真正的錯誤原因已經不可考, 修復 SonarCloud 的過程中特別撰文記錄.

實作

第一件事是在升級 .NET 6.0 後, 試著在本地環境上執行檢查並上傳到 SonarCloud.

前置條件

本機的執行步驟

  • dotnet sonarscanner begin

    1
    2
    3
    4
    5
    6
    7
    dotnet sonarscanner begin \
    /o:"marsen-github" \
    /k:"Marsen.NetCore.Dojo" \
    /n:"Marsen.NetCore.Dojo" \
    /d:sonar.host.url="https://sonarcloud.io" \
    /d:sonar.cs.opencover.reportsPaths="./test/_/TestResults/_/coverage.opencover.xml" \
    /d:sonar.login="****"

    參數說明
    /o: Organization Key
    /k: Project Key
    /n: Project Name
    /d:sonar.host.url: sonar server url
    /d:sonar.cs.opencover.reportsPaths: OpenCover coverage report
    /d:sonar.login: 登入 sonar server 的 token

  • dotnet build

    1
    dotnet build Marsen.NetCore.Dojo.sln
  • dotnet test

    1
    2
    3
    dotnet test Marsen.NetCore.Dojo.sln \
    --logger trx --collect:"XPlat Code Coverage" \
    -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
  • dotnet sonarscanner end

    1
    dotnet-sonarscanner end /d:sonar.login="****"

Github Workflow 的設定

目前並沒有官方的解決方案 ,
我們使用 SonarScanner for .NET 來執行 workflow ,
這個專案十分簡潔,可以快速看一下內容,了解到怎麼實作一個 workflow ,
因為非官方解決方案,所以我 fork 這個方案避免意外發生.

而在 workflow 中設定的也是使用 fork 的方案
設定參考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- name: SonarCloud Scan
uses: marsen/[email protected]
with:
# The key of the SonarQube project
sonarProjectKey: Marsen.NetCore.Dojo
# The name of the SonarQube project
sonarProjectName: Marsen.NetCore.Dojo
# The name of the SonarQube Organization
sonarOrganization: marsen-github
# Optional extra command arguments the the SonarScanner 'begin' command
sonarBeginArguments: /d:sonar.cs.opencover.reportsPaths="./test/*/TestResults/*/coverage.opencover.xml"
# Optional. Set to 1 or true to not run 'dotnet test' command
# dotnetDisableTests: true
dotnetTestArguments: Marsen.NetCore.Dojo.Integration.Test.sln --logger trx -p:CoverletOutputFormat="opencover" --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
dotnetBuildArguments: Marsen.NetCore.Dojo.sln

其它

注意到 opencover 的開發者已經不再維護(2021 年 6 月),並建議使用 AltCover.
但是 sonarcloud 並未支援 altcover 的報表, 這裡有相關的 issue 追蹤中.

參考

(fin)

[實作筆記] Stryker 升級與 CI 調整

前情提要

最近換了工作,一頭栽進了前端的領域,
生活又開始忙錄了起來,過年檢視了一下自已的 Side Project ,
意外發生 CI 竟然壞了一陣子,
這是我使用的變異測試工具 Stryker 從原本的 0.17 升級到了 1.3.1 版本
這是一個蠻大幅度的升版,所以設定上相當的不一樣。

我的 OS 是 macOS Monterey 12.1
安裝了 .NET 6.0

Local 環境

首先全域安裝 dotnet-stryker

dotnet tool install -g dotnet-stryker

接下安裝 tool-manifest

dotnet new tool-manifest

這時在你專案底下應該會建立一個 .config 資料夾,
內含 dotnet-tools.json 檔,內容如下

1
2
3
4
5
6
7
8
9
10
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-stryker": {
"version": "1.3.1",
"commands": ["dotnet-stryker"]
}
}
}

下一步,在專案資料下執行以下命令

dotnet tool install dotnet-stryker

建立 stryker-config.json 檔案
最後設定你的 config 檔 stryker-config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"stryker-config": {
"project": "Marsen.NetCore.Dojo.csproj",
"test-projects": [
"./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj",
"./test/Marsen.NetCore.Dojo.Integration.Tests/Marsen.NetCore.Dojo.Integration.Tests.csproj"
],
"reporters": ["progress", "json", "dashboard"],
"project-info": {
"name": "github.com/marsen/Marsen.NetCore.Dojo",
"version": "main"
}
}
}

與之前最大的不同是,大部份的參數你都可以設定在 stryker-config 之中,
而不用在執行命令中傳入,不僅可以為其提供版本控制,更能有序的組織你的設定檔,
使其更好閱讀。
如果你的 reporters 中有設定 dashboard 需要額外加上 --dashboard-api-key
ex:

dotnet stryker –dashboard-api-key=$STRYKER_DASHBOARD_API_KEY

參考

(fin)

[實作筆記] React Practice (一) Firebase 與 React Login

前情提要

本文不會介紹 FirebaseReact.
依照這篇文章完成後,你會

  • 透過 CRA 建立一個專案
  • 透過 MUI 建立符合 Material Design 的登入畫面
  • 建立 Firsbase 專案並且透過 Firsbase 完成登入登出的功能
  • 建立登入後才能瀏覽的頁面,並且實作登入/登出功能,登出後將無法瀏覽

Firebase 設定

建立 Firebase 專案

這裡的操作非常簡單,可以直接參考下面的影片。

詳細的步驟如下

  1. 進入 firebase console
  2. 選擇建立專案(Add Project),並且為你的專案取一個名字
  3. 進入剛剛建立的專案後,選擇建立應用程式(Add App),
    在這裡我們選擇 Web 類型的應用程式
    建立完成後我們會得到一個範例檔如下, 部份機敏資料先上遮罩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "A****************",
authDomain: "******.firebaseapp.com",
projectId: "******",
storageBucket: "******.appspot.com",
messagingSenderId: "******",
appId: "1:******:web:******",
measurementId: "G-*********",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

抽出環境變數

在 React 當中的機敏資料,建議的作法是使用環境變數,
我們可以建立一個.env.local提供給開發測試用,
而 Production 環境請直接設定在環境變數中, 相關資料不會進版控.
以下幾點注意事項

  • 必需使用 REACT_APP_* 開頭才可讓 REACT APP 使用 ex:REACT_APP_APIKEY
  • 一般的建議是全大寫
  • 環境變數的更新不適用 Hot Reload,請重啟環境

啟用 Firebase 認証(Authentication)

  1. 首先要建立 Sign-in method > 原生供應商 > 電子郵件/密碼
  2. 建立第一個使用者 Users > Add User

React

Create React Application

我們透過 TypeScript 建立 React 專案

1
npx create-react-app my-app --template typescript

我們可以試著啟動專案看一下,

1
cd my-app
1
npm i
1
npm start

安裝相關的套件

emotion 相關

1
npm i @emotion/react @emotion/styled

material 相關

1
npm i @mui/material @mui/icons-material

firebase 相關

1
npm i firebase react-firebase-hooks

react-router-dom 相關

1
npm i react-router-dom@6

建立登入頁面

登入頁面與路由

在 src 中建立 pages 資料夾,
之後我們再建立 Login.tsx 檔案如下

1
2
3
4
5
import React from "react";

export const Login = () => {
return <>Login Page</>;
};

接下來我們要透過 react-router-dom 來建立路由
開啟 index.tsx 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Login } from "./pages/Login";

ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="" element={<App />} />
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

重新啟動專案,在瀏覽器輸入不同的網址將會看到不同的畫面,如此我們完成了頁面的路由。

快速建立符合 MUI 的登入頁面

我們可以透過 MUI 提供的 Template 來建立 Login 頁面
實作細節如下:

  • 參考 Source Code 建立 src/components/SigninSide 組件
  • 修改 src/pages/Login.tsx 如下:
1
2
3
4
5
import SignInSide from "../components/SignInSide";

export const Login = () => {
return <SignInSide />;
};

重新瀏覽登入畫面,就可以看到一個美觀的登入頁 .

實作登入功能

首先請花點時間看一下 Firebase 帳號密碼登入的文件
接下來我們將依照文件的解釋修改我們的 SigninSide 組件。
打開 SigninSide 組件,找到handleSubmit 函數如下:

1
2
3
4
5
6
7
8
9
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
// eslint-disable-next-line no-console
console.log({
email: data.get("email"),
password: data.get("password"),
});
};

修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import { app } from "../firebase-config";
....
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const auth = getAuth(app);
signInWithEmailAndPassword(
auth,
data.get("email")!.toString(),
data.get("password")!.toString()
)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
console.log("user", userCredential.user);
// ...
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log("error", error);
});
};

題外話,data.get(“email”)!.toString()
當中的 !. 運算子可以參考 TypeScript Non-null assertion operator 的說明

用之前在 firebase 建立的帳密登入,登入成功的話可以在 Console 看到類似以下的資訊

1
2
UserImpl {providerId: 'firebase', emailVerified: false, isAnonymous:
false, tenantId: null, providerData: Array(1), …}

實作登入後進頁面的跳轉

這裡要使用 react-dom-router 提供的 hook useNavigate,
注意 React Hook 的使用限制

CRA 建立專案時同時會幫我們安裝檢查用的 Lint 所以不用太擔心。
同時我們把 getAuth(app) 也提取到 SignInSide Component 之外

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
////getAuth 可以不用寫在 handleSubmit 中
const auth = getAuth(app);

export default function SignInSide() {
////遵循 React Hook 的規則在最上層呼叫 Hook
const navigate = useNavigate();//取得 navigate
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
signInWithEmailAndPassword(
auth,
data.get("email")!.toString(),
data.get("password")!.toString()
)
.then((userCredential) => {
// Signed in
// console.log("user", userCredential.user);
navigate("/");// navigate 到首頁

實作登出的功能

調整 App.tsx 如下,移除不相關的程式,建立一個 Button 並準備好 onClick 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Button } from "@mui/material";
import "./App.css";
function App() {
return (
<div className="App">
<Button variant="outlined" onClick={() => {}}>
LOGOUT
</Button>
</div>
);
}

export default App;

路由設定,只有登入者才能看的頁面

透過 getAuth().currentUser 取得目前的登入者資料, 如果沒有登入者轉導到 Login 頁面,

1
2
3
4
5
6
7
8
9
10
11
12
13
import { getAuth } from "firebase/auth";
....
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route
path=""
element={getAuth().currentUser ? <App /> : <Navigate to="/login" /> }
/>
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>

不過這樣的作法會有問題,
getAuth().currentUser 是非同步取得的資訊,
所以你很有可能都會拿到 null 值,
在官方的建議作法是透過 onAuthStateChanged 註冊 Observer, index.tsx 會類似下面這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const auth = getAuth();

onAuthStateChanged(auth, (user) => {
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="" element={user ? <App /> : <Navigate to="login" />} />
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
});

Todo

  • Firebase Email link(passwordless sign-in)
  • Firebase Host Project
  • PageNotFound
  • 管理 Routes
  • more…

(つづく)

[學習筆記] JavaScript function 幾種不同的寫法

回傳純值的函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//return value
function foo1(a, b) {
return a + b;
}
// arrow function
let foo2 = (a, b) => {
return a + b;
};
// skip { } and return
let foo3 = (a, b) => a + b;

console.log("foo1(1,2) is", foo1(1, 2));
console.log("foo2(1,2) is", foo2(1, 2));
console.log("foo3(1,2) is", foo3(1, 2));

回傳物件的函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//return object

function foo4(a, b) {
return { sum: a + b };
}
// arrow function
let foo5 = (a, b) => {
return { sum: a + b };
};
// add () so you can skip { } and return
let foo6 = (a, b) => ({ sum: a + b });

console.log("foo4(1,2) is", foo4(1, 2));
console.log("foo5(1,2) is", foo5(1, 2));
console.log("foo6(1,2) is", foo6(1, 2));

科里(Curry)化函數

1
2
3
4
5
6
//return function, execute foo7(1,2)()
let foo7 = (a, b) => () => a + b;
console.log("foo7(1,2) is", foo7(1, 2)());
//curry
let foo8 = (a) => (b) => a + b;
console.log("foo8(1,2) is", foo8(1)(2));

(fin)

[心得筆記] 學習圈有多厚?

前情提要

這是 2021 年 3 月的一些隨筆, 不知何故沒有整理上傳 Blog,
剛好最近翻到,作一次反芻並且記錄。

隨筆 1、學習圈有多厚?

常常聽到舒適圈、學習圈、恐慌圈, 再搭配上下圖
舒適圈、學習圈、恐慌圈

這個圖,我覺得比例不對,
所謂的 Growth Zone 應該只有簿簿的一層,
大多數的時候我們只能延著這層膜多踏出一步是一步。

舒適圈、學習膜、恐慌圈

偶有突破,才能擴大 Comfort Zone

我覺得下圖比較適合,你要找到正確的方向突破
才能從 Comfort Zone 連結到 Success

作個有梗的人

那個縫縫才是你成長區(膜)

作個有梗的人

突破是困難的,一但突破了,紅色的部份就會自動補上,這就是它神奇的地方

作個有梗的人

隨筆 2、福原愛事件

  • 甘我屁事? 除非我想當 KOL 用來蹭流量,或是一般大眾需要談資
  • 如果已離婚,她的自由
  • 如果未離婚,感情淡了,感情本來就存在灰色地帶
  • 如果未離婚,感情好,那就是假新聞

2022 王力宏事件

恰巧 2022 也有類似的事件

  • 甘我屁事 ?
  • 渣男是否違法 ? 法律是道德的底限 ?
    • 群眾觀感是否約束力,或只是群眾暴力 ?
    • 文革式審判
  • 有錢中國賺,出事回台灣(羅志祥)
  • 一個人的作品與其道德是否有關 ?
    • 好的事就是是會改變的,而且不一定會變好
    • 昨是今非(時空背景不同)

隨筆 3、錢、價格、價值

最重要的是價值,價格只是落後指標,
你應該持有資產(農田、房子、工廠、生意、股票),
錢只是流通的工具,只有被消費掉的時候會產生價值。

  • 置裝:10000
  • 孝親:104000
  • 生活:120000
  • 娛樂:40000
  • 教育:60000
    => 年度儲蓄目標: 334,000+額外家庭開支(房貸、網路、電話、水電費、第四台、瓦斯)
  • 緊急預備金:180000

邊際效應具體感受

月費 600 元的運動中心,假設每次去 3 小時,
只去一次的話,那一次就價值 600 元,
當次數變多的時候,每次的時間成本不變,現金成本卻是下降的,
運動中心能帶給我的價值,具體量化的話有

  1. 運動次數
  2. 運動時數
  3. 運動頻率*

背後的抽象目的可能包含:

  1. 健康
  2. 社交

這些「目的」與「價值」就是所謂的「效應」,
並不是越多越好,而是取得平衡*(參考系統思考)
比如說,運動過量反而會受傷變得不健康。

下表顯示,當我第一次去運動中心時,
性價比是最高的,單次的價格是 600 元,
隨著次數的提昇,單次的價格是持續下降的,
理論上每天一次的話,單次價格會下降到 20 元/次

«表一»

次數 均價 時數
1 600 3
2 300 6
3 200 9
4 150 12
5 120 15
6 100 18
10 60 30
20 30 60
30 20 90

但價值就不是這樣衡量的,
一開始的價值遞增的,但是當超過了某個奇點,
價值就會開始遞減。

長萎

那麼錢呢?
其實錢只是一個交流價值用的媒介,
以物易物本身是價值的交換,
但是會耗消很大的時間,才能找到等價的物品,
比如一頭牛可以換多少的白米呢?
更不用說是情感需求等抽象層面的東西。

所以當有了代幣、指示物(Token)、貨幣將抽象的價值具象化,
(背後要有強而有力的組織支持,如:國家,強而有力的信用才能維持貨幣價值),
不同的國家有不同的貨幣,錢則是這些貨幣的代稱。

Cat

所以真正有價值的東西並不是錢,錢只有在需要交易之時才有用,
真正有價值的是一首好聽的歌,(太好聽了,怎麼才能反覆聽呢?)
一顆蘋果、一段關係…
一直持續對社會有益的組織
ex:

  • 讓人生活有意義(專家、奴隸),
  • 提供讓人更便利生活、
  • 提供食物、衣服、交通、娛樂、
  • 提供五大需求所需事務、
  • 建立關係

隨筆 4、左右

我很左,但是極致的左是不夠務實的,
我要務實,為了實現某些左邊的理想,
我要右,極致的右才能帶來極致的左,
這樣我還左嗎?

2202 反思

就像跳舞或走路一樣,單足難行千里,
我們總要左右才能前行。
重要的是平衡與推動力。

參考

https://www.books.com.tw/products/0010653417
https://pttcareers.com/Soft_Job/1WQm1aXz

(fin)

[實作筆記] 靜態網站部署整合 GCP (一) --- Artifacts Registry & Cloud Run

目的

有機會在 GCP 上實作了兩種不同方式的靜態網站部署,
一種是透過 Artifacts Registry 與 Cloud Run,
另一種是透過 Google Cloud Storage 與 Load Balancing
特別記錄一下。

Build Web

build 建置網站, EX: flutter build webnpm rub build

  • flutter 建置的 web 靜態檔位置在 build/web
  • react-scripts build 靜態檔位置在 build 底下

問題與解決方式

react build 不同環境的靜態檔

開發上會有不同環境需
預設執行 react-scripts build 會建置出 production 的靜態檔,
故需要透過套件 env-cmd 來幫我們建立不同的靜態檔,
請參考以下的 package.json > scripts

1
2
"build": "react-scripts build"
"build:dev": "env-cmd -f .env.development react-scripts build"

ERR_OSSL_EVP_UNSUPPORTED

截至 2021 年 12,nodejs LTS 版本為 v16.13.1
Latest Features 為 v17.3.0
如果 nodejs 的版本在 v17.*.* 以上
執行 react-scripts build 會發生以下錯誤

1
2
3
4
5
6
7
Error: error:0308010C:digital envelope routines::unsupported
… …

opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
library: 'digital envelope routines',
reason: 'unsupported',
code: 'ERR_OSSL_EVP_UNSUPPORTED'

解決方法,執行以下語法

export NODE_OPTIONS=–openssl-legacy-provider

或是將 node version 換回 v16.*.* 的版本

nvm use v16.13.1

Build Image

如果選擇 Artifacts Registry 與 Cloud Run 的 solution
必須撰寫 Dockerfile 來建立 Docker Images

問題 [Dart] no active package dhttpd

dhttpd 是 Dart 的靜態網站解決方案,
不過在部署到 Cloud Run 時會發生錯誤,
更多資訊請參考 stackoverflow 的提問與 issueTracker 的後續

20220308 已有解決方案,更新

加上 dart global activate 指令,這會在執行前啟用 dhttpd 的服務,
RUN 是建立鏡像檔的語法, CMD 才是 docker image 啟動時會執行的語法,
所以應該將 activate 指令放在啟動

1
2
3
4
5
6
7
8
9
10
11
FROM docker.io/dart

WORKDIR /flutter

ENV PATH $PATH:$HOME/.pub-cache/bin

COPY build/web ./
COPY start.sh ./
EXPOSE 8082

CMD dart pub global activate dhttpd && dart pub global run dhttpd --port=8082 --host=0.0.0.0

參考聯結

暫解是使用其它的靜態網站服務或套件 ,
EX: NGINX 、 npm 的 serve 套件,
或是其它你熟悉的靜態網站服務。
請參考以下 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# pull official base image
FROM node

# set working directory
WORKDIR /flutter

# install app dependencies
RUN npm i -g serve

# add `/app/node_modules/.bin` to $PATH
ENV PATH $PATH:$HOME/.pub-cache/bin

# add app
COPY build/web ./

EXPOSE 8080

# start app and run at 0.0.0.0
CMD serve -p 8080

Artifacts Registry & Cloud Run

  1. 登入私有的 Container Registry
  2. 透過 CI 建置 image
  3. 加上版本的 Tag
  4. 推上 Container Registry

參考部份的 .gitlab-ci.yml

1
2
3
4
5
6
7
script:
- echo "Compiling the image..."
- echo -n $GCR_KEY | docker login -u _json_key_base64 --password-stdin https://asia-east1-docker.pkg.dev
- docker build -t $CI_PROJECT_NAME . --no-cache
- docker tag "$CI_PROJECT_NAME" "asia-east1-docker.pkg.dev/company_registry/docker/$CI_PROJECT_NAME:$ver"
- docker push "asia-east1-docker.pkg.dev/company_registry/docker/$CI_PROJECT_NAME:$ver"
- echo "Compile image complete."

接下手動設定 Cloud Run(這部份應該也可以自動化,未實作故無記錄)

  1. Create Service
  2. Container > General > Container Image URL > SELECT > ARTIFACT REGISTRY > 選取上面部署上去的 images
  3. Container Port 8080
  4. DEPLOY

缺點是每次 CI 跑完 Image 推上 Artifact 後,仍要手動到 Cloud Run 執行 Deploy。
這個步驟應該放在 CI Server 上(即使是需要手動觸發也是),才不會有斷點。

(fin)

[實作筆記] Gitlab Runner

前情提要

我想完成什麼 ?
如何用最佳解完成全端應用的開發 ?

  1. 前端應包含 Web、App(iOS、Android 或簡稱三螢) 與其它
  2. 後端不限語言、框架(.Net Core、nodejs)
  3. 測試部署應該自動化(CI/CD)
  4. 成本應優化
  5. 應該使用雲原生的技術與 know how

這次我們專注在第 3 點的 CI/CD 上,為了第 1 點,我建置的專案為 flutter
選用的 CI/CD Server 為 Gitlab , 我將會在這裡作較多的著墨,
最終的產出物是 flutter app image, 並推送到 image registry 上 。

概觀,每個多邊型都應該可以被置換

如上圖,每個多邊型都應該可以被置換,

  • Application 可以被換成 Nodejs/.Net Core/ …
  • Gitlab Runner 有三種,本文中會使用 specific runners ,另外有 2 種
    • Shared runners(需要信用卡認証身分,每月限制 400 分鐘)
    • Group runners (在 Gitlab 上可以)
      我將 Gitlab 設定在本機 ( MacBook Pro ) 環境上,下文會講解細節,
      也可以置換到雲端的伺服器上,搭配 K8S
      ex:
      • GKE: Google Cloud Platform Kubernetes Engine
      • EKS: Amazon Web Services’ Elastic Kubernetes Services
      • AKS: Microsoft Azure Kubernetes Service
  • Container Registry 這裡我使用最主流的 Docker Hub Registry
    以雲原生的三大平台都有對應的功能,如果有機會應該優先選用。

工具準備

首先需要 Gitlab Account ,我們選用 Gitlab 作為 CI/CD Server,
這個階段我們只會作到持續整合,而未部署,相當於只有 CI 的部份。
理論上也可以選用其它的 CI/CD 服務,像是 Azure 或是 GitHub , 這裡就不作過多的展開。

Docker , 簡單說我們的工作只有兩個步驟

  1. 在 Gitlab 你的專案 > Settings > CI/CD > Runners 註冊 Runner Executors
    • 一般來說,我們會選用 docker 或 Kubernetes,本章我會用 docker 為例
  2. 撰寫 Pipeline 與 Job 腳本,
    Pipeline
    我們預計執行以下的工作(Pipeline)
    • Test
      • Run Test
      • Run Lint
    • Build
      • Build Application
      • Build Image

開始

註冊 Gitlab Runner

我們要在 docker 建立來執行 gitlab Runner

  1. Create the Docker volume:

    1
    docker volume create gitlab-runner-config
  2. Start the GitLab Runner container using the volume we just created:

    1
    2
    3
    4
    docker run -d --name gitlab-runner --restart always \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v gitlab-runner-config:/etc/gitlab-runner \
    gitlab/gitlab-runner:latest
  3. Register Docker Runner

    依序輸入 Gitlab URL、gitlab-ci 的 token、runner 說明/名稱、Runner 的 tag、executor、docker image

    1
    2
    docker run --rm -it -v gitlab-runner-config:/etc/gitlab-runner \
    gitlab/gitlab-runner:latest register
    • Enter your GitLab instance URL (also known as the gitlab-ci coordinator URL).
    • Enter the token you obtained to register the runner.
    • Enter a description for the runner. You can change this value later in the GitLab user interface.
    • Enter the tags associated with the runner, separated by commas. You can change this value later in the GitLab user interface.
    • Provide the runner executor. For most use cases, enter docker.
    • If you entered docker as your executor, you’ll be asked for the default image to be used for projects that do not define one in .gitlab-ci.yml.
      GitLab instance URL 是 https://gitlab.com/
      你可以在專案中的 Settings > CI/CD 找到 token,
      description 會顯示在 Runner List 中,可以用易懂的描述,
      tags 可以更多的參考這本篇文章設定
      executor 選用 docker 記得需要安裝 docker daemon
      executor 為 docker 時,需要註明預設的 image,我是選用 docker:stable

執行 RUNNER

gitlab-runner run

移除 RUNNER(為了重新安裝)

  1. 驗証 runner 狀態是否 alive

    gitlab-runner verify –delete

  2. 移除 gitlab-runner

    gitlab-runner unregister –all-runners

參考

錯誤: 當使用 MacBook M1 當作 RUNNER

在 RUNNER JOB 中執行 flutter pub get 時
$ flutter pub get
發生錯誤訊息如下,當我換了一個 intel 晶片的 MacBook 當作 RUNNER 就不會有問題了。

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
Running "flutter pub get" in rettulf...
===== CRASH =====
si_signo=Trace/breakpoint trap(5), si_code=128, si_addr=(nil)
version=2.14.4 (stable) (Wed Oct 13 11:11:32 2021 +0200) on "linux_x64"
pid=57, thread=116, isolate_group=main(0x4002e9fc00), isolate=main(0x4002e7d000)
isolate_instructions=4001cf2ec0, vm_instructions=4001cf2ec0
pc 0x0000ffffab7e28ec fp 0x000000400a1af838 Unknown symbol
pc 0x0000ffffab49ed0f fp 0x000000400a1af898 Unknown symbol
pc 0x0000ffffab78114d fp 0x000000400a1af8d8 Unknown symbol
pc 0x0000ffffacaa294c fp 0x000000400a1af940 Unknown symbol
pc 0x0000ffffacaa220b fp 0x000000400a1af980 Unknown symbol
pc 0x0000ffffacd028ff fp 0x000000400a1af9f8 Unknown symbol
pc 0x0000004001e6a123 fp 0x000000400a1afaa0 dart::DartEntry::InvokeCode(dart::Code const&, unsigned long, dart::Array const&, dart::Array const&, dart::Thread*)+0x153
pc 0x0000004001e69f75 fp 0x000000400a1afb00 dart::DartEntry::InvokeFunction(dart::Function const&, dart::Array const&, dart::Array const&, unsigned long)+0x165
pc 0x0000004001e6c55d fp 0x000000400a1afb50 dart::DartLibraryCalls::HandleMessage(dart::Object const&, dart::Instance const&)+0x15d
pc 0x0000004001e93e46 fp 0x000000400a1afc30 dart::IsolateMessageHandler::HandleMessage(std::__2::unique_ptr<dart::Message, std::__2::default_delete<dart::Message> >)+0x596
pc 0x0000004001ebe7ac fp 0x000000400a1afca0 dart::MessageHandler::HandleMessages(dart::MonitorLocker*, bool, bool)+0x14c
pc 0x0000004001ebeecf fp 0x000000400a1afd00 dart::MessageHandler::TaskCallback()+0x1df
pc 0x0000004001fdde48 fp 0x000000400a1afd80 dart::ThreadPool::WorkerLoop(dart::ThreadPool::Worker*)+0x148
pc 0x0000004001fde27c fp 0x000000400a1afdb0 dart::ThreadPool::Worker::Main(unsigned long)+0x5c
pc 0x0000004001f57a48 fp 0x000000400a1afe70 /sdks/flutter/bin/cache/dart-sdk/bin/dart+0x1f57a48
-- End of DumpStackTrace
qemu: uncaught target signal 6 (Aborted) - core dumped
/sdks/flutter/bin/internal/shared.sh: line 225: 57 Aborted "$DART" --disable-dart-dev --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
Cleaning up project directory and file based variables

DIND(Docker In Docker)

我們的 Gitlab-Runner 是執行在 Docker 上,
而當我需要 build Docker Images 時,我會在 Container 中建立 docker
這時 Container 所使用的 Image 會是 Docker Image
就被稱為 DIND (Docker In Docker)

在 Gitlab CI 中 Docker Login

參考以下的 .gitlab-ci.yml

1
2
before_script:
- echo "$DOCKER_REGISTRY_PASS" | docker login -u $DOCKER_REGISTRY_USER --password-stdin

異常紀錄與解決方案

$ echo -n $LOGIN_KEY | docker login -u _json_key_base64 –password-stdin https://xxxx-docker.pkg.dev
Error: Cannot perform an interactive login from a non TTY device

主因是讀不到 $LOGIN_KEY 的環境變數,使用 Gitlab-CI 的話要注意 Protect Variable

無法對 docker hub registry

在 RUNNER JOB 中執行 docker push
錯誤訊息如下:

dial tcp: lookup docker on x.x.x.x:53: no such host error runner inside docker on armhf

這裡要修改 RUNNER 中的設定檔 config.toml , 路徑參考如下:

You can find the config.toml file in:

  • /etc/gitlab-runner/ on *nix systems when GitLab Runner is executed as root (this is also the path for service configuration)
  • ~/.gitlab-runner/ on*nix systems when GitLab Runner is executed as non-root
  • ./ on other systems

在 [[runners]] > [runner.docker] 加入 image = docker:stable 或是 privileged = true
just add image = docker:stable  and privileged = true
這可能不是一個正確的 solution , 可以更多的參考這篇討論

在 DIND 編輯文件

如上題,我們需要在 container 之中編輯文件,
這裡我會使用 vim,
不過安裝前記得先更新 apt-get
不然會出現以下錯誤

E: Unable to locate package vim on Debian jessie simplified Docker container

1
2
3
4
apt-get update
apt-get install apt-file
apt-file update
apt-get install vim # now finally this will work !!!

GCP Artifact Registry

這裡都以台灣的 region 為範例,
所以都是 ‘asia-east1-docker.pkg.dev‘ 不同的 region 請再參考 GCP 文件

推上去

記得要先登入

docker login -u _json_key_base64 –password-stdin https://asia-east1-docker.pkg.dev

推上去

docker push “asia-east1-docker.pkg.dev/{env_name}/docker/{project_name}:{version_tag}

拉下來

需要先調整 Docker configuration file 取得授權,請參考

gcloud auth configure-docker asia-east1-docker.pkg.dev

再執行

docker pull asia-east1-docker.pkg.dev/{env_name}/docker/{project_name}:{version_tag}

註冊

問題

  1. gitlab-ci-multi-runner 與 gitlab-runner 的差異為何 ?
  2. 當 docker gitlab-runner image 的 instance 執行 gitlab-runner run 會產生以下訊息, 這代表什麼意思 ?
    Configuration loaded builds=0
    listen_address not defined, metrics & debug endpoints disabled builds=0
    [session_server].listen_address not defined, session endpoints disabled builds=0

(fin)

[實作筆記] 在 terminal 中控制字串

目的

在 CI/CD 的過程需要透過 shell 處理環境變數產生版號,
以前沒有這方面的經驗,實務上也不像一般的應用程式語言那麼常操作,
避免未來忘記,特別記錄下來,如果未來有相關的語法也會在此同步更新。

宣告變數

直接給值

var=””

切割字串

只想看結果的話

echo ${HOME}|cut -d”/“ -f2
指定給變數
var=$(echo ${HOME}|cut -d”/“ -f2)

串接字串

var=$(echo ${HOME}|cut -d”/“ -f2).$USER

(fin)

Please enable JavaScript to view the LikeCoin. :P