[{"data":1,"prerenderedAt":1427},["ShallowReactive",2],{"navigation":3,"projects-page":34,"projects":52},[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,"body":37,"description":38,"extension":39,"links":40,"meta":46,"navigation":47,"path":48,"seo":49,"stem":50,"__hash__":51},"pages\u002Fprojects.yml","最新專案",null,"這裡記錄了一些我做過的專案，每個專案都讓我在系統設計上學到一些新東西。","yml",[41,44],{"label":42,"color":43},"工作委託","neutral",{"label":45},"寄信給我",{},true,"\u002Fprojects",{"title":36,"description":38},"projects","R2yxa-GR5kh0g3m-uzirjAq8QF7NMbF-KmXP_3J8Pss",[53,692,1054],{"id":54,"title":55,"author":56,"body":60,"date":676,"demoUrl":37,"description":677,"extension":678,"image":679,"meta":680,"navigation":47,"path":681,"seo":682,"stem":683,"tags":684,"url":690,"__hash__":691},"projectPages\u002Fprojects\u002Fgshop-api.md","gshop-api",{"name":57,"avatar":58},"Gary",{"src":59,"alt":57},"\u002Fimages\u002Fselfie.webp",{"type":61,"value":62,"toc":666},"minimark",[63,67,71,106,109,120,123,129,132,137,147,157,163,169,172,299,302,306,311,324,337,421,424,445,447,451,459,464,468,515,519,522,659,662],[64,65,66],"h2",{"id":66},"架構概覽",[68,69,70],"p",{},"gshop-api 是整個電商系統的後端核心，負責處理商品、訂單、會員認證等 API 請求。",[72,73,74,82,88,94,100],"ul",{},[75,76,77,81],"li",{},[78,79,80],"strong",{},"Runtime","：Node.js \u002F Express",[75,83,84,87],{},[78,85,86],{},"Port","：3001",[75,89,90,93],{},[78,91,92],{},"資料庫","：Supabase PostgreSQL（透過 Session Pooler 連線）",[75,95,96,99],{},[78,97,98],{},"部署環境","：GKE Autopilot，asia-east1",[75,101,102,105],{},[78,103,104],{},"Image Registry","：Google Artifact Registry",[64,107,108],{"id":108},"部署架構",[110,111,116],"pre",{"className":112,"code":114,"language":115},[113],"language-text","GitHub (main) → GitHub Actions → docker build → Artifact Registry\n                                                      ↓\n                                              GKE pull image\n                                                      ↓\n                                         kubectl rollout restart\n","text",[117,118,114],"code",{"__ignoreMap":119},"",[68,121,122],{},"流量路徑：",[110,124,127],{"className":125,"code":126,"language":115},[113],"Cloudflare → GCP Load Balancer → Ingress → gshop-api Pod\n",[117,128,126],{"__ignoreMap":119},[64,130,131],{"id":131},"遇到的問題與解法",[133,134,136],"h3",{"id":135},"_1-supabase-連線失敗enotfound","1. Supabase 連線失敗（ENOTFOUND）",[68,138,139,142,143,146],{},[78,140,141],{},"問題","：部署後 API 無法連線到 Supabase，log 顯示 ",[117,144,145],{},"ENOTFOUND","。",[68,148,149,152,153,156],{},[78,150,151],{},"原因","：Supabase 直連位址 ",[117,154,155],{},"db.xxx.supabase.co:5432"," 僅支援 IPv6，而 GKE Autopilot 的節點只有 IPv4。本地 Mac 可以直連是因為 Mac 同時支援 IPv4 與 IPv6。",[68,158,159,162],{},[78,160,161],{},"解法","：改用 Supabase Session Pooler connection string，走 IPv4：",[110,164,167],{"className":165,"code":166,"language":115},[113],"postgresql:\u002F\u002Fpostgres.xxx:PASSWORD@aws-1-ap-southeast-1.pooler.supabase.com:5432\u002Fpostgres\n",[117,168,166],{"__ignoreMap":119},[68,170,171],{},"更新 K8s Secret：",[110,173,177],{"className":174,"code":175,"language":176,"meta":119,"style":119},"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",[117,178,179,202,221,285],{"__ignoreMap":119},[180,181,184,188,192,195,199],"span",{"class":182,"line":183},"line",1,[180,185,187],{"class":186},"sTEyZ","NEW_URL",[180,189,191],{"class":190},"sMK4o","=",[180,193,194],{"class":190},"\"",[180,196,198],{"class":197},"sfazB","postgresql:\u002F\u002F...",[180,200,201],{"class":190},"\"\n",[180,203,205,209,212,215,218],{"class":182,"line":204},2,[180,206,208],{"class":207},"sBMFI","kubectl",[180,210,211],{"class":197}," patch",[180,213,214],{"class":197}," secret",[180,216,217],{"class":197}," gshop-api-secret",[180,219,220],{"class":186}," \\\n",[180,222,224,227,230,233,236,239,241,244,246,249,251,254,256,259,263,266,269,272,275,278,280,283],{"class":182,"line":223},3,[180,225,226],{"class":197},"  -p",[180,228,229],{"class":190}," \"",[180,231,232],{"class":197},"{",[180,234,235],{"class":186},"\\\"",[180,237,238],{"class":197},"data",[180,240,235],{"class":186},[180,242,243],{"class":197},":{",[180,245,235],{"class":186},[180,247,248],{"class":197},"DATABASE_URL",[180,250,235],{"class":186},[180,252,253],{"class":197},":",[180,255,235],{"class":186},[180,257,258],{"class":190},"$(",[180,260,262],{"class":261},"s2Zo4","echo",[180,264,265],{"class":197}," -n ",[180,267,268],{"class":186},"$NEW_URL",[180,270,271],{"class":190}," |",[180,273,274],{"class":207}," base64",[180,276,277],{"class":190},")",[180,279,235],{"class":186},[180,281,282],{"class":197},"}}",[180,284,201],{"class":190},[180,286,288,290,293,296],{"class":182,"line":287},4,[180,289,208],{"class":207},[180,291,292],{"class":197}," rollout",[180,294,295],{"class":197}," restart",[180,297,298],{"class":197}," deployment\u002Fgshop-api\n",[300,301],"hr",{},[133,303,305],{"id":304},"_2-502-bad-gateway-健康檢查失敗","2. 502 Bad Gateway — 健康檢查失敗",[68,307,308,310],{},[78,309,141],{},"：服務部署完成，但 GCP Load Balancer 回傳 502。",[68,312,313,315,316,319,320,323],{},[78,314,151],{},"：GCP Load Balancer 預設對 ",[117,317,318],{},"GET \u002F"," 做健康檢查，但 gshop-api 沒有 ",[117,321,322],{},"\u002F"," 路由，回傳非 200，LB 判定為不健康，停止轉發流量。",[68,325,326,328,329,332,333,336],{},[78,327,161],{},"：建立 ",[117,330,331],{},"BackendConfig","，將健康檢查路徑改為 ",[117,334,335],{},"\u002Fhealth","：",[110,338,342],{"className":339,"code":340,"language":341,"meta":119,"style":119},"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",[117,343,344,355,365,373,383,391,399,410],{"__ignoreMap":119},[180,345,346,350,352],{"class":182,"line":183},[180,347,349],{"class":348},"swJcz","apiVersion",[180,351,253],{"class":190},[180,353,354],{"class":197}," cloud.google.com\u002Fv1\n",[180,356,357,360,362],{"class":182,"line":204},[180,358,359],{"class":348},"kind",[180,361,253],{"class":190},[180,363,364],{"class":197}," BackendConfig\n",[180,366,367,370],{"class":182,"line":223},[180,368,369],{"class":348},"metadata",[180,371,372],{"class":190},":\n",[180,374,375,378,380],{"class":182,"line":287},[180,376,377],{"class":348},"  name",[180,379,253],{"class":190},[180,381,382],{"class":197}," gshop-api-backend-config\n",[180,384,386,389],{"class":182,"line":385},5,[180,387,388],{"class":348},"spec",[180,390,372],{"class":190},[180,392,394,397],{"class":182,"line":393},6,[180,395,396],{"class":348},"  healthCheck",[180,398,372],{"class":190},[180,400,402,405,407],{"class":182,"line":401},7,[180,403,404],{"class":348},"    requestPath",[180,406,253],{"class":190},[180,408,409],{"class":197}," \u002Fhealth\n",[180,411,413,416,418],{"class":182,"line":412},8,[180,414,415],{"class":348},"    type",[180,417,253],{"class":190},[180,419,420],{"class":197}," HTTP\n",[68,422,423],{},"並在 Service 加上 annotation：",[110,425,427],{"className":339,"code":426,"language":341,"meta":119,"style":119},"cloud.google.com\u002Fbackend-config: '{\"default\": \"gshop-api-backend-config\"}'\n",[117,428,429],{"__ignoreMap":119},[180,430,431,434,436,439,442],{"class":182,"line":183},[180,432,433],{"class":348},"cloud.google.com\u002Fbackend-config",[180,435,253],{"class":190},[180,437,438],{"class":190}," '",[180,440,441],{"class":197},"{\"default\": \"gshop-api-backend-config\"}",[180,443,444],{"class":190},"'\n",[300,446],{},[133,448,450],{"id":449},"_3-imagepullbackoff-gke-無法拉取-image","3. ImagePullBackOff — GKE 無法拉取 Image",[68,452,453,455,456,146],{},[78,454,141],{},"：Pod 啟動失敗，狀態顯示 ",[117,457,458],{},"ImagePullBackOff",[68,460,461,463],{},[78,462,151],{},"：GKE 的 Service Account 沒有 Artifact Registry 的讀取權限。",[68,465,466,336],{},[78,467,161],{},[110,469,471],{"className":174,"code":470,"language":176,"meta":119,"style":119},"gcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:620172615694-compute@developer.gserviceaccount.com\" \\\n  --role=\"roles\u002Fartifactregistry.reader\"\n",[117,472,473,489,503],{"__ignoreMap":119},[180,474,475,478,481,484,487],{"class":182,"line":183},[180,476,477],{"class":207},"gcloud",[180,479,480],{"class":197}," projects",[180,482,483],{"class":197}," add-iam-policy-binding",[180,485,486],{"class":197}," gshop-497319",[180,488,220],{"class":186},[180,490,491,494,496,499,501],{"class":182,"line":204},[180,492,493],{"class":197},"  --member=",[180,495,194],{"class":190},[180,497,498],{"class":197},"serviceAccount:620172615694-compute@developer.gserviceaccount.com",[180,500,194],{"class":190},[180,502,220],{"class":186},[180,504,505,508,510,513],{"class":182,"line":223},[180,506,507],{"class":197},"  --role=",[180,509,194],{"class":190},[180,511,512],{"class":197},"roles\u002Fartifactregistry.reader",[180,514,201],{"class":190},[64,516,518],{"id":517},"cicd-設定","CI\u002FCD 設定",[68,520,521],{},"每次 push 到 main，GitHub Actions 自動執行：",[110,523,525],{"className":339,"code":524,"language":341,"meta":119,"style":119},"- 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",[117,526,527,540,547,557,562,574,585,590,595,600,612,619,630,641,646],{"__ignoreMap":119},[180,528,529,532,535,537],{"class":182,"line":183},[180,530,531],{"class":190},"-",[180,533,534],{"class":348}," uses",[180,536,253],{"class":190},[180,538,539],{"class":197}," google-github-actions\u002Fauth@v2\n",[180,541,542,545],{"class":182,"line":204},[180,543,544],{"class":348},"  with",[180,546,372],{"class":190},[180,548,549,552,554],{"class":182,"line":223},[180,550,551],{"class":348},"    credentials_json",[180,553,253],{"class":190},[180,555,556],{"class":197}," ${{ secrets.GCP_SA_KEY }}\n",[180,558,559],{"class":182,"line":287},[180,560,561],{"emptyLinePlaceholder":47},"\n",[180,563,564,566,569,571],{"class":182,"line":385},[180,565,531],{"class":190},[180,567,568],{"class":348}," name",[180,570,253],{"class":190},[180,572,573],{"class":197}," Build and push image\n",[180,575,576,579,581],{"class":182,"line":393},[180,577,578],{"class":348},"  run",[180,580,253],{"class":190},[180,582,584],{"class":583},"s7zQu"," |\n",[180,586,587],{"class":182,"line":401},[180,588,589],{"class":197},"    docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest .\n",[180,591,592],{"class":182,"line":412},[180,593,594],{"class":197},"    docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[180,596,598],{"class":182,"line":597},9,[180,599,561],{"emptyLinePlaceholder":47},[180,601,603,605,607,609],{"class":182,"line":602},10,[180,604,531],{"class":190},[180,606,534],{"class":348},[180,608,253],{"class":190},[180,610,611],{"class":197}," google-github-actions\u002Fget-gke-credentials@v2\n",[180,613,615,617],{"class":182,"line":614},11,[180,616,544],{"class":348},[180,618,372],{"class":190},[180,620,622,625,627],{"class":182,"line":621},12,[180,623,624],{"class":348},"    cluster_name",[180,626,253],{"class":190},[180,628,629],{"class":197}," gshop-cluster\n",[180,631,633,636,638],{"class":182,"line":632},13,[180,634,635],{"class":348},"    location",[180,637,253],{"class":190},[180,639,640],{"class":197}," asia-east1\n",[180,642,644],{"class":182,"line":643},14,[180,645,561],{"emptyLinePlaceholder":47},[180,647,649,651,654,656],{"class":182,"line":648},15,[180,650,531],{"class":190},[180,652,653],{"class":348}," run",[180,655,253],{"class":190},[180,657,658],{"class":197}," kubectl rollout restart deployment\u002Fgshop-api\n",[68,660,661],{},"GitHub Actions runner 是 ubuntu amd64，build 出的 image 天生就是 amd64，不需要 Cloud Build。",[663,664,665],"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":119,"searchDepth":204,"depth":204,"links":667},[668,669,670,675],{"id":66,"depth":204,"text":66},{"id":108,"depth":204,"text":108},{"id":131,"depth":204,"text":131,"children":671},[672,673,674],{"id":135,"depth":223,"text":136},{"id":304,"depth":223,"text":305},{"id":449,"depth":223,"text":450},{"id":517,"depth":204,"text":518},"2026-06-15","電商系統的後端核心，負責商品、訂單與會員 API，串接 Supabase PostgreSQL 與 Google Cloud Storage。","md","\u002Fprojects\u002Fapi.jpg",{},"\u002Fprojects\u002Fgshop-api",{"title":55,"description":677},"projects\u002Fgshop-api",[685,686,687,688,689],"Node.js","GKE","Kubernetes","CI\u002FCD","PostgreSQL","https:\u002F\u002Fgithub.com\u002Forgs\u002Fvery-cool-gshop\u002Frepositories","Bk3TDKYUGHzvJG2wh1J2WJxeQ9q6fv87Vrhfl3mYQAA",{"id":693,"title":694,"author":695,"body":697,"date":676,"demoUrl":1042,"description":1043,"extension":678,"image":1044,"meta":1045,"navigation":47,"path":1046,"seo":1047,"stem":1048,"tags":1049,"url":1052,"__hash__":1053},"projectPages\u002Fprojects\u002Fgshop-dashboard.md","gshop-dashboard",{"name":57,"avatar":696},{"src":59,"alt":57},{"type":61,"value":698,"toc":1033},[699,701,704,727,729,734,736,742,744,748,753,767,777,842,844,862,864,868,881,890,895,942,944,946,1030],[64,700,66],{"id":66},[68,702,703],{},"gshop-dashboard 是電商後台管理介面，提供商品上架、訂單管理、會員查詢等功能，使用 Nuxt.js SSR 渲染，確保 SEO 與首屏載入效能。",[72,705,706,712,717,721],{},[75,707,708,711],{},[78,709,710],{},"Framework","：Nuxt.js（SSR 模式）",[75,713,714,716],{},[78,715,86],{},"：3002",[75,718,719,99],{},[78,720,98],{},[75,722,723,726],{},[78,724,725],{},"網域","：dashboard.garydemo.com",[64,728,108],{"id":108},[110,730,732],{"className":731,"code":114,"language":115},[113],[117,733,114],{"__ignoreMap":119},[68,735,122],{},[110,737,740],{"className":738,"code":739,"language":115},[113],"Cloudflare → GCP Load Balancer → Ingress (Host: dashboard.garydemo.com) → gshop-dashboard Pod\n",[117,741,739],{"__ignoreMap":119},[64,743,131],{"id":131},[133,745,747],{"id":746},"_1-502-bad-gateway-健康檢查失敗","1. 502 Bad Gateway — 健康檢查失敗",[68,749,750,752],{},[78,751,141],{},"：部署完成後，dashboard 持續回傳 502。",[68,754,755,315,757,759,760,762,763,766],{},[78,756,151],{},[117,758,318],{}," 做健康檢查，但 Nuxt SSR 的 ",[117,761,322],{}," 會 302 redirect 到 ",[117,764,765],{},"\u002Flogin","。GCP LB 不把 302 算成健康狀態，因此判定服務異常，停止轉發流量。",[68,768,769,328,771,773,774,776],{},[78,770,161],{},[117,772,331],{},"，直接將健康檢查路徑指定為 ",[117,775,765],{},"（回傳 200）：",[110,778,780],{"className":339,"code":779,"language":341,"meta":119,"style":119},"apiVersion: cloud.google.com\u002Fv1\nkind: BackendConfig\nmetadata:\n  name: gshop-dashboard-backend-config\nspec:\n  healthCheck:\n    requestPath: \u002Flogin\n    type: HTTP\n",[117,781,782,790,798,804,813,819,825,834],{"__ignoreMap":119},[180,783,784,786,788],{"class":182,"line":183},[180,785,349],{"class":348},[180,787,253],{"class":190},[180,789,354],{"class":197},[180,791,792,794,796],{"class":182,"line":204},[180,793,359],{"class":348},[180,795,253],{"class":190},[180,797,364],{"class":197},[180,799,800,802],{"class":182,"line":223},[180,801,369],{"class":348},[180,803,372],{"class":190},[180,805,806,808,810],{"class":182,"line":287},[180,807,377],{"class":348},[180,809,253],{"class":190},[180,811,812],{"class":197}," gshop-dashboard-backend-config\n",[180,814,815,817],{"class":182,"line":385},[180,816,388],{"class":348},[180,818,372],{"class":190},[180,820,821,823],{"class":182,"line":393},[180,822,396],{"class":348},[180,824,372],{"class":190},[180,826,827,829,831],{"class":182,"line":401},[180,828,404],{"class":348},[180,830,253],{"class":190},[180,832,833],{"class":197}," \u002Flogin\n",[180,835,836,838,840],{"class":182,"line":412},[180,837,415],{"class":348},[180,839,253],{"class":190},[180,841,420],{"class":197},[68,843,423],{},[110,845,847],{"className":339,"code":846,"language":341,"meta":119,"style":119},"cloud.google.com\u002Fbackend-config: '{\"default\": \"gshop-dashboard-backend-config\"}'\n",[117,848,849],{"__ignoreMap":119},[180,850,851,853,855,857,860],{"class":182,"line":183},[180,852,433],{"class":348},[180,854,253],{"class":190},[180,856,438],{"class":190},[180,858,859],{"class":197},"{\"default\": \"gshop-dashboard-backend-config\"}",[180,861,444],{"class":190},[300,863],{},[133,865,867],{"id":866},"_2-ingress-長時間無法取得-ipneg-not-ready","2. Ingress 長時間無法取得 IP（NEG not ready）",[68,869,870,872,873,876,877,880],{},[78,871,141],{},"：套用 ",[117,874,875],{},"ingress.yaml"," 後，",[117,878,879],{},"kubectl get ingress"," 顯示 IP 欄位一直是空的。",[68,882,883,885,886,889],{},[78,884,151],{},"：Service 上殘留舊的 ",[117,887,888],{},"networking.gke.io\u002Ftarget-pool"," annotation，與 GKE Ingress Controller 的 NEG（Network Endpoint Group）機制衝突，導致 LB backend 無法正常建立。",[68,891,892,894],{},[78,893,161],{},"：移除舊 annotation，刪除後重建 Ingress：",[110,896,898],{"className":174,"code":897,"language":176,"meta":119,"style":119},"kubectl annotate svc gshop-dashboard networking.gke.io\u002Ftarget-pool-\nkubectl delete ingress gshop-ingress\nkubectl apply -f k8s\u002Fingress.yaml\n",[117,899,900,916,929],{"__ignoreMap":119},[180,901,902,904,907,910,913],{"class":182,"line":183},[180,903,208],{"class":207},[180,905,906],{"class":197}," annotate",[180,908,909],{"class":197}," svc",[180,911,912],{"class":197}," gshop-dashboard",[180,914,915],{"class":197}," networking.gke.io\u002Ftarget-pool-\n",[180,917,918,920,923,926],{"class":182,"line":204},[180,919,208],{"class":207},[180,921,922],{"class":197}," delete",[180,924,925],{"class":197}," ingress",[180,927,928],{"class":197}," gshop-ingress\n",[180,930,931,933,936,939],{"class":182,"line":223},[180,932,208],{"class":207},[180,934,935],{"class":197}," apply",[180,937,938],{"class":197}," -f",[180,940,941],{"class":197}," k8s\u002Fingress.yaml\n",[64,943,518],{"id":517},[68,945,521],{},[110,947,949],{"className":339,"code":948,"language":341,"meta":119,"style":119},"- name: Build and push image\n  run: |\n    docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest .\n    docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard: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-dashboard\n",[117,950,951,961,969,974,979,983,993,999,1007,1015,1019],{"__ignoreMap":119},[180,952,953,955,957,959],{"class":182,"line":183},[180,954,531],{"class":190},[180,956,568],{"class":348},[180,958,253],{"class":190},[180,960,573],{"class":197},[180,962,963,965,967],{"class":182,"line":204},[180,964,578],{"class":348},[180,966,253],{"class":190},[180,968,584],{"class":583},[180,970,971],{"class":182,"line":223},[180,972,973],{"class":197},"    docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest .\n",[180,975,976],{"class":182,"line":287},[180,977,978],{"class":197},"    docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n",[180,980,981],{"class":182,"line":385},[180,982,561],{"emptyLinePlaceholder":47},[180,984,985,987,989,991],{"class":182,"line":393},[180,986,531],{"class":190},[180,988,534],{"class":348},[180,990,253],{"class":190},[180,992,611],{"class":197},[180,994,995,997],{"class":182,"line":401},[180,996,544],{"class":348},[180,998,372],{"class":190},[180,1000,1001,1003,1005],{"class":182,"line":412},[180,1002,624],{"class":348},[180,1004,253],{"class":190},[180,1006,629],{"class":197},[180,1008,1009,1011,1013],{"class":182,"line":597},[180,1010,635],{"class":348},[180,1012,253],{"class":190},[180,1014,640],{"class":197},[180,1016,1017],{"class":182,"line":602},[180,1018,561],{"emptyLinePlaceholder":47},[180,1020,1021,1023,1025,1027],{"class":182,"line":614},[180,1022,531],{"class":190},[180,1024,653],{"class":348},[180,1026,253],{"class":190},[180,1028,1029],{"class":197}," kubectl rollout restart deployment\u002Fgshop-dashboard\n",[663,1031,1032],{},"html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}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 .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 .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}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":119,"searchDepth":204,"depth":204,"links":1034},[1035,1036,1037,1041],{"id":66,"depth":204,"text":66},{"id":108,"depth":204,"text":108},{"id":131,"depth":204,"text":131,"children":1038},[1039,1040],{"id":746,"depth":223,"text":747},{"id":866,"depth":223,"text":867},{"id":517,"depth":204,"text":518},"https:\u002F\u002Fdashboard.garydemo.com\u002F","使用 Nuxt.js SSR 建構電商管理後台，部署於 GKE Autopilot，透過 GitHub Actions 自動化部署。","\u002Fprojects\u002Fdashboard.jpg",{},"\u002Fprojects\u002Fgshop-dashboard",{"title":694,"description":1043},"projects\u002Fgshop-dashboard",[1050,1051,686,687,688],"Nuxt.js","SSR","https:\u002F\u002Fgithub.com\u002Fvery-cool-gshop\u002Fgshop-dashboard","y-HIaZTm5o2IN-ZQ_G9da3CjcV0whonX8qxHZiExKCU",{"id":1055,"title":1056,"author":1057,"body":1059,"date":676,"demoUrl":1416,"description":1417,"extension":678,"image":1418,"meta":1419,"navigation":47,"path":1420,"seo":1421,"stem":1422,"tags":1423,"url":1425,"__hash__":1426},"projectPages\u002Fprojects\u002Fgshop-web.md","gshop-web",{"name":57,"avatar":1058},{"src":59,"alt":57},{"type":61,"value":1060,"toc":1406},[1061,1063,1066,1092,1094,1100,1102,1108,1110,1114,1123,1131,1139,1141,1145,1150,1155,1159,1168,1202,1214,1260,1262,1266,1273,1282,1287,1315,1317,1319,1403],[64,1062,66],{"id":66},[68,1064,1065],{},"gshop-web 是面向消費者的電商購物前台，提供商品瀏覽、加入購物車、結帳等功能，使用 Nuxt.js SSR 確保 SEO 效果與首屏載入速度。",[72,1067,1068,1072,1077,1081,1086],{},[75,1069,1070,711],{},[78,1071,710],{},[75,1073,1074,1076],{},[78,1075,86],{},"：3003",[75,1078,1079,99],{},[78,1080,98],{},[75,1082,1083,1085],{},[78,1084,725],{},"：web.garydemo.com",[75,1087,1088,1091],{},[78,1089,1090],{},"CDN \u002F Proxy","：Cloudflare（Full Strict TLS）",[64,1093,108],{"id":108},[110,1095,1098],{"className":1096,"code":1097,"language":115},[113],"GitHub (main) → GitHub Actions → docker build (amd64) → Artifact Registry\n                                                               ↓\n                                                       GKE pull image\n                                                               ↓\n                                                  kubectl rollout restart\n",[117,1099,1097],{"__ignoreMap":119},[68,1101,122],{},[110,1103,1106],{"className":1104,"code":1105,"language":115},[113],"使用者瀏覽器\n    ↓ HTTPS\nCloudflare Proxy（DNS + TLS 終止）\n    ↓ HTTPS + Origin Certificate\nGCP Load Balancer（34.160.168.110）\n    ↓\nIngress（Host: web.garydemo.com）\n    ↓\ngshop-web Pod（:3003）\n",[117,1107,1105],{"__ignoreMap":119},[64,1109,131],{"id":131},[133,1111,1113],{"id":1112},"_1-本地-build-的-image-在-gke-無法執行","1. 本地 Build 的 Image 在 GKE 無法執行",[68,1115,1116,1118,1119,1122],{},[78,1117,141],{},"：本地 ",[117,1120,1121],{},"docker build"," 後推上去，GKE Pod 啟動失敗。",[68,1124,1125,1127,1128,1130],{},[78,1126,151],{},"：開發機是 Apple Silicon（arm64），",[117,1129,1121],{}," 預設產出 arm64 image，但 GKE 節點是 amd64 架構，無法執行。",[68,1132,1133,1135,1136,1138],{},[78,1134,161],{},"：CI\u002FCD 改用 GitHub Actions ubuntu runner 執行 ",[117,1137,1121],{},"，runner 本身是 amd64，build 出的 image 天生就是 amd64，不需要額外設定或使用 Cloud Build。",[300,1140],{},[133,1142,1144],{"id":1143},"_2-cloudflare-full-strict-tls-設定","2. Cloudflare Full (Strict) TLS 設定",[68,1146,1147,1149],{},[78,1148,141],{},"：設定 Cloudflare SSL\u002FTLS 為 Full (Strict) 後，網站無法正常載入。",[68,1151,1152,1154],{},[78,1153,151],{},"：Full (Strict) 要求 Cloudflare 到 GCP 這段也必須使用合法憑證，自簽憑證不被接受。",[68,1156,1157,336],{},[78,1158,161],{},[1160,1161,1162,1165],"ol",{},[75,1163,1164],{},"在 Cloudflare Dashboard → SSL\u002FTLS → Origin Server 建立 Origin Certificate 並下載",[75,1166,1167],{},"存為 Kubernetes TLS Secret：",[110,1169,1171],{"className":174,"code":1170,"language":176,"meta":119,"style":119},"kubectl create secret tls cloudflare-origin-cert \\\n  --cert=certificate.pem \\\n  --key=private.key\n",[117,1172,1173,1190,1197],{"__ignoreMap":119},[180,1174,1175,1177,1180,1182,1185,1188],{"class":182,"line":183},[180,1176,208],{"class":207},[180,1178,1179],{"class":197}," create",[180,1181,214],{"class":197},[180,1183,1184],{"class":197}," tls",[180,1186,1187],{"class":197}," cloudflare-origin-cert",[180,1189,220],{"class":186},[180,1191,1192,1195],{"class":182,"line":204},[180,1193,1194],{"class":197},"  --cert=certificate.pem",[180,1196,220],{"class":186},[180,1198,1199],{"class":182,"line":223},[180,1200,1201],{"class":197},"  --key=private.key\n",[1160,1203,1204],{"start":223},[75,1205,1206,1207,1209,1210,1213],{},"在 ",[117,1208,875],{}," 的 ",[117,1211,1212],{},"tls"," 區塊掛載：",[110,1215,1217],{"className":339,"code":1216,"language":341,"meta":119,"style":119},"spec:\n  tls:\n    - secretName: cloudflare-origin-cert\n      hosts:\n        - web.garydemo.com\n",[117,1218,1219,1225,1232,1245,1252],{"__ignoreMap":119},[180,1220,1221,1223],{"class":182,"line":183},[180,1222,388],{"class":348},[180,1224,372],{"class":190},[180,1226,1227,1230],{"class":182,"line":204},[180,1228,1229],{"class":348},"  tls",[180,1231,372],{"class":190},[180,1233,1234,1237,1240,1242],{"class":182,"line":223},[180,1235,1236],{"class":190},"    -",[180,1238,1239],{"class":348}," secretName",[180,1241,253],{"class":190},[180,1243,1244],{"class":197}," cloudflare-origin-cert\n",[180,1246,1247,1250],{"class":182,"line":287},[180,1248,1249],{"class":348},"      hosts",[180,1251,372],{"class":190},[180,1253,1254,1257],{"class":182,"line":385},[180,1255,1256],{"class":190},"        -",[180,1258,1259],{"class":197}," web.garydemo.com\n",[300,1261],{},[133,1263,1265],{"id":1264},"_3-ingress-class-設定錯誤","3. Ingress Class 設定錯誤",[68,1267,1268,872,1270,1272],{},[78,1269,141],{},[117,1271,875],{}," 後，GKE 沒有建立對應的 Cloud Load Balancer。",[68,1274,1275,1277,1278,1281],{},[78,1276,151],{},"：使用了 ",[117,1279,1280],{},"spec.ingressClassName: gce","，GKE Ingress Controller 不認識這個欄位。",[68,1283,1284,1286],{},[78,1285,161],{},"：改用 annotation 方式指定：",[110,1288,1290],{"className":339,"code":1289,"language":341,"meta":119,"style":119},"metadata:\n  annotations:\n    kubernetes.io\u002Fingress.class: gce\n",[117,1291,1292,1298,1305],{"__ignoreMap":119},[180,1293,1294,1296],{"class":182,"line":183},[180,1295,369],{"class":348},[180,1297,372],{"class":190},[180,1299,1300,1303],{"class":182,"line":204},[180,1301,1302],{"class":348},"  annotations",[180,1304,372],{"class":190},[180,1306,1307,1310,1312],{"class":182,"line":223},[180,1308,1309],{"class":348},"    kubernetes.io\u002Fingress.class",[180,1311,253],{"class":190},[180,1313,1314],{"class":197}," gce\n",[64,1316,518],{"id":517},[68,1318,521],{},[110,1320,1322],{"className":339,"code":1321,"language":341,"meta":119,"style":119},"- name: Build and push image\n  run: |\n    docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest .\n    docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb: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-web\n",[117,1323,1324,1334,1342,1347,1352,1356,1366,1372,1380,1388,1392],{"__ignoreMap":119},[180,1325,1326,1328,1330,1332],{"class":182,"line":183},[180,1327,531],{"class":190},[180,1329,568],{"class":348},[180,1331,253],{"class":190},[180,1333,573],{"class":197},[180,1335,1336,1338,1340],{"class":182,"line":204},[180,1337,578],{"class":348},[180,1339,253],{"class":190},[180,1341,584],{"class":583},[180,1343,1344],{"class":182,"line":223},[180,1345,1346],{"class":197},"    docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest .\n",[180,1348,1349],{"class":182,"line":287},[180,1350,1351],{"class":197},"    docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest\n",[180,1353,1354],{"class":182,"line":385},[180,1355,561],{"emptyLinePlaceholder":47},[180,1357,1358,1360,1362,1364],{"class":182,"line":393},[180,1359,531],{"class":190},[180,1361,534],{"class":348},[180,1363,253],{"class":190},[180,1365,611],{"class":197},[180,1367,1368,1370],{"class":182,"line":401},[180,1369,544],{"class":348},[180,1371,372],{"class":190},[180,1373,1374,1376,1378],{"class":182,"line":412},[180,1375,624],{"class":348},[180,1377,253],{"class":190},[180,1379,629],{"class":197},[180,1381,1382,1384,1386],{"class":182,"line":597},[180,1383,635],{"class":348},[180,1385,253],{"class":190},[180,1387,640],{"class":197},[180,1389,1390],{"class":182,"line":602},[180,1391,561],{"emptyLinePlaceholder":47},[180,1393,1394,1396,1398,1400],{"class":182,"line":614},[180,1395,531],{"class":190},[180,1397,653],{"class":348},[180,1399,253],{"class":190},[180,1401,1402],{"class":197}," kubectl rollout restart deployment\u002Fgshop-web\n",[663,1404,1405],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}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":119,"searchDepth":204,"depth":204,"links":1407},[1408,1409,1410,1415],{"id":66,"depth":204,"text":66},{"id":108,"depth":204,"text":108},{"id":131,"depth":204,"text":131,"children":1411},[1412,1413,1414],{"id":1112,"depth":223,"text":1113},{"id":1143,"depth":223,"text":1144},{"id":1264,"depth":223,"text":1265},{"id":517,"depth":204,"text":518},"https:\u002F\u002Fweb.garydemo.com\u002F","使用 Nuxt.js SSR 建構電商購物前台，部署於 GKE Autopilot，透過 Cloudflare Proxy 以 Full (Strict) TLS 對外提供服務。","\u002Fprojects\u002Fweb.jpg",{},"\u002Fprojects\u002Fgshop-web",{"title":1056,"description":1417},"projects\u002Fgshop-web",[1050,1051,686,1424,688],"Cloudflare","https:\u002F\u002Fgithub.com\u002Fvery-cool-gshop\u002Fgshop-web","lrc9zCX2veeid0IpdMehNxWXLODrtE-gikhMEWvQY2Y",1781661890755]