[{"data":1,"prerenderedAt":675},["ShallowReactive",2],{"navigation":3,"\u002Fprojects\u002Fgshop-api":34},[4],{"title":5,"path":6,"stem":7,"children":8,"page":33},"Articles","\u002Farticles","articles",[9,13,17,21,25,29],{"title":10,"path":11,"stem":12},"用 Daily Snapshot 提升統計查詢速度","\u002Farticles\u002Fdaily-snapshot","articles\u002Fdaily-snapshot",{"title":14,"path":15,"stem":16},"GKE 部署","\u002Farticles\u002Fgke-deployment","articles\u002Fgke-deployment",{"title":18,"path":19,"stem":20},"從 LLM 到 Agent：打通底層邏輯","\u002Farticles\u002Fllm-to-agent","articles\u002Fllm-to-agent",{"title":22,"path":23,"stem":24},"資訊安全實踐","\u002Farticles\u002Fsecurity-best-practices","articles\u002Fsecurity-best-practices",{"title":26,"path":27,"stem":28},"單機架構的性能優化","\u002Farticles\u002Fsingle-machine-performance","articles\u002Fsingle-machine-performance",{"title":30,"path":31,"stem":32},"伺服器渲染 SSR","\u002Farticles\u002Fssr","articles\u002Fssr",false,{"id":35,"title":36,"author":37,"body":41,"date":658,"demoUrl":659,"description":660,"extension":661,"image":662,"meta":663,"navigation":542,"path":664,"seo":665,"stem":666,"tags":667,"url":673,"__hash__":674},"projectPages\u002Fprojects\u002Fgshop-api.md","gshop-api",{"name":38,"avatar":39},"Gary",{"src":40,"alt":38},"\u002Fimages\u002Fselfie.webp",{"type":42,"value":43,"toc":648},"minimark",[44,48,52,87,90,101,104,110,113,118,128,138,144,150,153,280,283,287,292,305,318,402,405,426,428,432,440,445,449,496,500,503,641,644],[45,46,47],"h2",{"id":47},"架構概覽",[49,50,51],"p",{},"gshop-api 是整個電商系統的後端核心，負責處理商品、訂單、會員認證等 API 請求。",[53,54,55,63,69,75,81],"ul",{},[56,57,58,62],"li",{},[59,60,61],"strong",{},"Runtime","：Node.js \u002F Express",[56,64,65,68],{},[59,66,67],{},"Port","：3001",[56,70,71,74],{},[59,72,73],{},"資料庫","：Supabase PostgreSQL（透過 Session Pooler 連線）",[56,76,77,80],{},[59,78,79],{},"部署環境","：GKE Autopilot，asia-east1",[56,82,83,86],{},[59,84,85],{},"Image Registry","：Google Artifact Registry",[45,88,89],{"id":89},"部署架構",[91,92,97],"pre",{"className":93,"code":95,"language":96},[94],"language-text","GitHub (main) → GitHub Actions → docker build → Artifact Registry\n                                                      ↓\n                                              GKE pull image\n                                                      ↓\n                                         kubectl rollout restart\n","text",[98,99,95],"code",{"__ignoreMap":100},"",[49,102,103],{},"流量路徑：",[91,105,108],{"className":106,"code":107,"language":96},[94],"Cloudflare → GCP Load Balancer → Ingress → gshop-api Pod\n",[98,109,107],{"__ignoreMap":100},[45,111,112],{"id":112},"遇到的問題與解法",[114,115,117],"h3",{"id":116},"_1-supabase-連線失敗enotfound","1. Supabase 連線失敗（ENOTFOUND）",[49,119,120,123,124,127],{},[59,121,122],{},"問題","：部署後 API 無法連線到 Supabase，log 顯示 ",[98,125,126],{},"ENOTFOUND","。",[49,129,130,133,134,137],{},[59,131,132],{},"原因","：Supabase 直連位址 ",[98,135,136],{},"db.xxx.supabase.co:5432"," 僅支援 IPv6，而 GKE Autopilot 的節點只有 IPv4。本地 Mac 可以直連是因為 Mac 同時支援 IPv4 與 IPv6。",[49,139,140,143],{},[59,141,142],{},"解法","：改用 Supabase Session Pooler connection string，走 IPv4：",[91,145,148],{"className":146,"code":147,"language":96},[94],"postgresql:\u002F\u002Fpostgres.xxx:PASSWORD@aws-1-ap-southeast-1.pooler.supabase.com:5432\u002Fpostgres\n",[98,149,147],{"__ignoreMap":100},[49,151,152],{},"更新 K8s Secret：",[91,154,158],{"className":155,"code":156,"language":157,"meta":100,"style":100},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","NEW_URL=\"postgresql:\u002F\u002F...\"\nkubectl patch secret gshop-api-secret \\\n  -p \"{\\\"data\\\":{\\\"DATABASE_URL\\\":\\\"$(echo -n $NEW_URL | base64)\\\"}}\"\nkubectl rollout restart deployment\u002Fgshop-api\n","bash",[98,159,160,183,202,266],{"__ignoreMap":100},[161,162,165,169,173,176,180],"span",{"class":163,"line":164},"line",1,[161,166,168],{"class":167},"sTEyZ","NEW_URL",[161,170,172],{"class":171},"sMK4o","=",[161,174,175],{"class":171},"\"",[161,177,179],{"class":178},"sfazB","postgresql:\u002F\u002F...",[161,181,182],{"class":171},"\"\n",[161,184,186,190,193,196,199],{"class":163,"line":185},2,[161,187,189],{"class":188},"sBMFI","kubectl",[161,191,192],{"class":178}," patch",[161,194,195],{"class":178}," secret",[161,197,198],{"class":178}," gshop-api-secret",[161,200,201],{"class":167}," \\\n",[161,203,205,208,211,214,217,220,222,225,227,230,232,235,237,240,244,247,250,253,256,259,261,264],{"class":163,"line":204},3,[161,206,207],{"class":178},"  -p",[161,209,210],{"class":171}," \"",[161,212,213],{"class":178},"{",[161,215,216],{"class":167},"\\\"",[161,218,219],{"class":178},"data",[161,221,216],{"class":167},[161,223,224],{"class":178},":{",[161,226,216],{"class":167},[161,228,229],{"class":178},"DATABASE_URL",[161,231,216],{"class":167},[161,233,234],{"class":178},":",[161,236,216],{"class":167},[161,238,239],{"class":171},"$(",[161,241,243],{"class":242},"s2Zo4","echo",[161,245,246],{"class":178}," -n ",[161,248,249],{"class":167},"$NEW_URL",[161,251,252],{"class":171}," |",[161,254,255],{"class":188}," base64",[161,257,258],{"class":171},")",[161,260,216],{"class":167},[161,262,263],{"class":178},"}}",[161,265,182],{"class":171},[161,267,269,271,274,277],{"class":163,"line":268},4,[161,270,189],{"class":188},[161,272,273],{"class":178}," rollout",[161,275,276],{"class":178}," restart",[161,278,279],{"class":178}," deployment\u002Fgshop-api\n",[281,282],"hr",{},[114,284,286],{"id":285},"_2-502-bad-gateway-健康檢查失敗","2. 502 Bad Gateway — 健康檢查失敗",[49,288,289,291],{},[59,290,122],{},"：服務部署完成，但 GCP Load Balancer 回傳 502。",[49,293,294,296,297,300,301,304],{},[59,295,132],{},"：GCP Load Balancer 預設對 ",[98,298,299],{},"GET \u002F"," 做健康檢查，但 gshop-api 沒有 ",[98,302,303],{},"\u002F"," 路由，回傳非 200，LB 判定為不健康，停止轉發流量。",[49,306,307,309,310,313,314,317],{},[59,308,142],{},"：建立 ",[98,311,312],{},"BackendConfig","，將健康檢查路徑改為 ",[98,315,316],{},"\u002Fhealth","：",[91,319,323],{"className":320,"code":321,"language":322,"meta":100,"style":100},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","apiVersion: cloud.google.com\u002Fv1\nkind: BackendConfig\nmetadata:\n  name: gshop-api-backend-config\nspec:\n  healthCheck:\n    requestPath: \u002Fhealth\n    type: HTTP\n","yaml",[98,324,325,336,346,354,364,372,380,391],{"__ignoreMap":100},[161,326,327,331,333],{"class":163,"line":164},[161,328,330],{"class":329},"swJcz","apiVersion",[161,332,234],{"class":171},[161,334,335],{"class":178}," cloud.google.com\u002Fv1\n",[161,337,338,341,343],{"class":163,"line":185},[161,339,340],{"class":329},"kind",[161,342,234],{"class":171},[161,344,345],{"class":178}," BackendConfig\n",[161,347,348,351],{"class":163,"line":204},[161,349,350],{"class":329},"metadata",[161,352,353],{"class":171},":\n",[161,355,356,359,361],{"class":163,"line":268},[161,357,358],{"class":329},"  name",[161,360,234],{"class":171},[161,362,363],{"class":178}," gshop-api-backend-config\n",[161,365,367,370],{"class":163,"line":366},5,[161,368,369],{"class":329},"spec",[161,371,353],{"class":171},[161,373,375,378],{"class":163,"line":374},6,[161,376,377],{"class":329},"  healthCheck",[161,379,353],{"class":171},[161,381,383,386,388],{"class":163,"line":382},7,[161,384,385],{"class":329},"    requestPath",[161,387,234],{"class":171},[161,389,390],{"class":178}," \u002Fhealth\n",[161,392,394,397,399],{"class":163,"line":393},8,[161,395,396],{"class":329},"    type",[161,398,234],{"class":171},[161,400,401],{"class":178}," HTTP\n",[49,403,404],{},"並在 Service 加上 annotation：",[91,406,408],{"className":320,"code":407,"language":322,"meta":100,"style":100},"cloud.google.com\u002Fbackend-config: '{\"default\": \"gshop-api-backend-config\"}'\n",[98,409,410],{"__ignoreMap":100},[161,411,412,415,417,420,423],{"class":163,"line":164},[161,413,414],{"class":329},"cloud.google.com\u002Fbackend-config",[161,416,234],{"class":171},[161,418,419],{"class":171}," '",[161,421,422],{"class":178},"{\"default\": \"gshop-api-backend-config\"}",[161,424,425],{"class":171},"'\n",[281,427],{},[114,429,431],{"id":430},"_3-imagepullbackoff-gke-無法拉取-image","3. ImagePullBackOff — GKE 無法拉取 Image",[49,433,434,436,437,127],{},[59,435,122],{},"：Pod 啟動失敗，狀態顯示 ",[98,438,439],{},"ImagePullBackOff",[49,441,442,444],{},[59,443,132],{},"：GKE 的 Service Account 沒有 Artifact Registry 的讀取權限。",[49,446,447,317],{},[59,448,142],{},[91,450,452],{"className":155,"code":451,"language":157,"meta":100,"style":100},"gcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:620172615694-compute@developer.gserviceaccount.com\" \\\n  --role=\"roles\u002Fartifactregistry.reader\"\n",[98,453,454,470,484],{"__ignoreMap":100},[161,455,456,459,462,465,468],{"class":163,"line":164},[161,457,458],{"class":188},"gcloud",[161,460,461],{"class":178}," projects",[161,463,464],{"class":178}," add-iam-policy-binding",[161,466,467],{"class":178}," gshop-497319",[161,469,201],{"class":167},[161,471,472,475,477,480,482],{"class":163,"line":185},[161,473,474],{"class":178},"  --member=",[161,476,175],{"class":171},[161,478,479],{"class":178},"serviceAccount:620172615694-compute@developer.gserviceaccount.com",[161,481,175],{"class":171},[161,483,201],{"class":167},[161,485,486,489,491,494],{"class":163,"line":204},[161,487,488],{"class":178},"  --role=",[161,490,175],{"class":171},[161,492,493],{"class":178},"roles\u002Fartifactregistry.reader",[161,495,182],{"class":171},[45,497,499],{"id":498},"cicd-設定","CI\u002FCD 設定",[49,501,502],{},"每次 push 到 main，GitHub Actions 自動執行：",[91,504,506],{"className":320,"code":505,"language":322,"meta":100,"style":100},"- uses: google-github-actions\u002Fauth@v2\n  with:\n    credentials_json: ${{ secrets.GCP_SA_KEY }}\n\n- name: Build and push image\n  run: |\n    docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest .\n    docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n\n- uses: google-github-actions\u002Fget-gke-credentials@v2\n  with:\n    cluster_name: gshop-cluster\n    location: asia-east1\n\n- run: kubectl rollout restart deployment\u002Fgshop-api\n",[98,507,508,521,528,538,544,556,567,572,577,582,594,601,612,623,628],{"__ignoreMap":100},[161,509,510,513,516,518],{"class":163,"line":164},[161,511,512],{"class":171},"-",[161,514,515],{"class":329}," uses",[161,517,234],{"class":171},[161,519,520],{"class":178}," google-github-actions\u002Fauth@v2\n",[161,522,523,526],{"class":163,"line":185},[161,524,525],{"class":329},"  with",[161,527,353],{"class":171},[161,529,530,533,535],{"class":163,"line":204},[161,531,532],{"class":329},"    credentials_json",[161,534,234],{"class":171},[161,536,537],{"class":178}," ${{ secrets.GCP_SA_KEY }}\n",[161,539,540],{"class":163,"line":268},[161,541,543],{"emptyLinePlaceholder":542},true,"\n",[161,545,546,548,551,553],{"class":163,"line":366},[161,547,512],{"class":171},[161,549,550],{"class":329}," name",[161,552,234],{"class":171},[161,554,555],{"class":178}," Build and push image\n",[161,557,558,561,563],{"class":163,"line":374},[161,559,560],{"class":329},"  run",[161,562,234],{"class":171},[161,564,566],{"class":565},"s7zQu"," |\n",[161,568,569],{"class":163,"line":382},[161,570,571],{"class":178},"    docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest .\n",[161,573,574],{"class":163,"line":393},[161,575,576],{"class":178},"    docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[161,578,580],{"class":163,"line":579},9,[161,581,543],{"emptyLinePlaceholder":542},[161,583,585,587,589,591],{"class":163,"line":584},10,[161,586,512],{"class":171},[161,588,515],{"class":329},[161,590,234],{"class":171},[161,592,593],{"class":178}," google-github-actions\u002Fget-gke-credentials@v2\n",[161,595,597,599],{"class":163,"line":596},11,[161,598,525],{"class":329},[161,600,353],{"class":171},[161,602,604,607,609],{"class":163,"line":603},12,[161,605,606],{"class":329},"    cluster_name",[161,608,234],{"class":171},[161,610,611],{"class":178}," gshop-cluster\n",[161,613,615,618,620],{"class":163,"line":614},13,[161,616,617],{"class":329},"    location",[161,619,234],{"class":171},[161,621,622],{"class":178}," asia-east1\n",[161,624,626],{"class":163,"line":625},14,[161,627,543],{"emptyLinePlaceholder":542},[161,629,631,633,636,638],{"class":163,"line":630},15,[161,632,512],{"class":171},[161,634,635],{"class":329}," run",[161,637,234],{"class":171},[161,639,640],{"class":178}," kubectl rollout restart deployment\u002Fgshop-api\n",[49,642,643],{},"GitHub Actions runner 是 ubuntu amd64，build 出的 image 天生就是 amd64，不需要 Cloud Build。",[645,646,647],"style",{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":100,"searchDepth":185,"depth":185,"links":649},[650,651,652,657],{"id":47,"depth":185,"text":47},{"id":89,"depth":185,"text":89},{"id":112,"depth":185,"text":112,"children":653},[654,655,656],{"id":116,"depth":204,"text":117},{"id":285,"depth":204,"text":286},{"id":430,"depth":204,"text":431},{"id":498,"depth":185,"text":499},"2026-06-15",null,"電商系統的後端核心，負責商品、訂單與會員 API，串接 Supabase PostgreSQL 與 Google Cloud Storage。","md","\u002Fprojects\u002Fapi.jpg",{},"\u002Fprojects\u002Fgshop-api",{"title":36,"description":660},"projects\u002Fgshop-api",[668,669,670,671,672],"Node.js","GKE","Kubernetes","CI\u002FCD","PostgreSQL","https:\u002F\u002Fgithub.com\u002Forgs\u002Fvery-cool-gshop\u002Frepositories","Bk3TDKYUGHzvJG2wh1J2WJxeQ9q6fv87Vrhfl3mYQAA",1781661891535]