[{"data":1,"prerenderedAt":1597},["ShallowReactive",2],{"navigation":3,"\u002Farticles\u002Fgke-deployment":34,"\u002Farticles\u002Fgke-deployment-surround":1592},[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":14,"author":36,"body":40,"date":1584,"description":1585,"extension":1586,"externalUrl":1587,"image":1588,"meta":1589,"minRead":163,"navigation":159,"path":15,"seo":1590,"stem":16,"__hash__":1591},"blog\u002Farticles\u002Fgke-deployment.md",{"name":37,"avatar":38},"Gary",{"src":39,"alt":37},"\u002Fimages\u002Fselfie.webp",{"type":41,"value":42,"toc":1557},"minimark",[43,47,94,97,100,105,222,229,233,311,315,348,352,414,418,468,470,473,477,495,499,557,561,674,678,759,763,899,901,905,909,951,956,960,971,974,980,982,986,989,995,999,1141,1152,1156,1397,1402,1404,1407,1553],[44,45,46],"h2",{"id":46},"架構",[48,49,50,58,64,70,76,82,88],"ul",{},[51,52,53,57],"li",{},[54,55,56],"strong",{},"gshop-api"," — Node.js\u002FExpress，Port 3001",[51,59,60,63],{},[54,61,62],{},"gshop-dashboard"," — Nuxt.js SSR，Port 3002",[51,65,66,69],{},[54,67,68],{},"gshop-web"," — Nuxt.js SSR，Port 3003",[51,71,72,75],{},[54,73,74],{},"GKE Autopilot Cluster"," — gshop-cluster，asia-east1",[51,77,78,81],{},[54,79,80],{},"Artifact Registry"," — asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop",[51,83,84,87],{},[54,85,86],{},"Database"," — Supabase（Session Pooler）",[51,89,90,93],{},[54,91,92],{},"Domain"," — garydemo.com（Cloudflare）",[95,96],"hr",{},[44,98,99],{"id":99},"部署流程",[101,102,104],"h3",{"id":103},"_1-build-push-image每次更新都要做","1. Build & Push Image（每次更新都要做）",[106,107,112],"pre",{"className":108,"code":109,"language":110,"meta":111,"style":111},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# API\ngcloud builds submit .\u002Fgshop-api \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n\n# Dashboard\ngcloud builds submit .\u002Fgshop-dashboard \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n\n# Web\ngcloud builds submit .\u002Fgshop-web \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest\n","bash","",[113,114,115,124,145,154,161,167,181,189,194,200,214],"code",{"__ignoreMap":111},[116,117,120],"span",{"class":118,"line":119},"line",1,[116,121,123],{"class":122},"sHwdD","# API\n",[116,125,127,131,135,138,141],{"class":118,"line":126},2,[116,128,130],{"class":129},"sBMFI","gcloud",[116,132,134],{"class":133},"sfazB"," builds",[116,136,137],{"class":133}," submit",[116,139,140],{"class":133}," .\u002Fgshop-api",[116,142,144],{"class":143},"sTEyZ"," \\\n",[116,146,148,151],{"class":118,"line":147},3,[116,149,150],{"class":133},"  --tag",[116,152,153],{"class":133}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[116,155,157],{"class":118,"line":156},4,[116,158,160],{"emptyLinePlaceholder":159},true,"\n",[116,162,164],{"class":118,"line":163},5,[116,165,166],{"class":122},"# Dashboard\n",[116,168,170,172,174,176,179],{"class":118,"line":169},6,[116,171,130],{"class":129},[116,173,134],{"class":133},[116,175,137],{"class":133},[116,177,178],{"class":133}," .\u002Fgshop-dashboard",[116,180,144],{"class":143},[116,182,184,186],{"class":118,"line":183},7,[116,185,150],{"class":133},[116,187,188],{"class":133}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n",[116,190,192],{"class":118,"line":191},8,[116,193,160],{"emptyLinePlaceholder":159},[116,195,197],{"class":118,"line":196},9,[116,198,199],{"class":122},"# Web\n",[116,201,203,205,207,209,212],{"class":118,"line":202},10,[116,204,130],{"class":129},[116,206,134],{"class":133},[116,208,137],{"class":133},[116,210,211],{"class":133}," .\u002Fgshop-web",[116,213,144],{"class":143},[116,215,217,219],{"class":118,"line":216},11,[116,218,150],{"class":133},[116,220,221],{"class":133}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest\n",[223,224,225],"blockquote",{},[226,227,228],"p",{},"Cloud Build 在雲端 build（解決本地 Apple Silicon arm64\u002Famd64 問題）",[101,230,232],{"id":231},"_2-建立-kubernetes-secret","2. 建立 Kubernetes Secret",[106,234,236],{"className":108,"code":235,"language":110,"meta":111,"style":111},"kubectl create secret generic gshop-api-secret \\\n  --from-literal=DATABASE_URL=\"...\" \\\n  --from-literal=JWT_SECRET=\"...\" \\\n  --from-literal=GCS_BUCKET_NAME=\"...\" \\\n  --from-literal=ANTHROPIC_API_KEY=\"...\"\n",[113,237,238,257,273,286,299],{"__ignoreMap":111},[116,239,240,243,246,249,252,255],{"class":118,"line":119},[116,241,242],{"class":129},"kubectl",[116,244,245],{"class":133}," create",[116,247,248],{"class":133}," secret",[116,250,251],{"class":133}," generic",[116,253,254],{"class":133}," gshop-api-secret",[116,256,144],{"class":143},[116,258,259,262,266,269,271],{"class":118,"line":126},[116,260,261],{"class":133},"  --from-literal=DATABASE_URL=",[116,263,265],{"class":264},"sMK4o","\"",[116,267,268],{"class":133},"...",[116,270,265],{"class":264},[116,272,144],{"class":143},[116,274,275,278,280,282,284],{"class":118,"line":147},[116,276,277],{"class":133},"  --from-literal=JWT_SECRET=",[116,279,265],{"class":264},[116,281,268],{"class":133},[116,283,265],{"class":264},[116,285,144],{"class":143},[116,287,288,291,293,295,297],{"class":118,"line":156},[116,289,290],{"class":133},"  --from-literal=GCS_BUCKET_NAME=",[116,292,265],{"class":264},[116,294,268],{"class":133},[116,296,265],{"class":264},[116,298,144],{"class":143},[116,300,301,304,306,308],{"class":118,"line":163},[116,302,303],{"class":133},"  --from-literal=ANTHROPIC_API_KEY=",[116,305,265],{"class":264},[116,307,268],{"class":133},[116,309,310],{"class":264},"\"\n",[101,312,314],{"id":313},"_3-建立-cloudflare-origin-certificate-tls-secret","3. 建立 Cloudflare Origin Certificate TLS Secret",[106,316,318],{"className":108,"code":317,"language":110,"meta":111,"style":111},"kubectl create secret tls cloudflare-origin-cert \\\n  --cert=certificate.pem \\\n  --key=private.key\n",[113,319,320,336,343],{"__ignoreMap":111},[116,321,322,324,326,328,331,334],{"class":118,"line":119},[116,323,242],{"class":129},[116,325,245],{"class":133},[116,327,248],{"class":133},[116,329,330],{"class":133}," tls",[116,332,333],{"class":133}," cloudflare-origin-cert",[116,335,144],{"class":143},[116,337,338,341],{"class":118,"line":126},[116,339,340],{"class":133},"  --cert=certificate.pem",[116,342,144],{"class":143},[116,344,345],{"class":118,"line":147},[116,346,347],{"class":133},"  --key=private.key\n",[101,349,351],{"id":350},"_4-套用-k8s-設定","4. 套用 K8s 設定",[106,353,355],{"className":108,"code":354,"language":110,"meta":111,"style":111},"kubectl apply -f k8s\u002Fbackend-config.yaml\nkubectl apply -f k8s\u002Fapi-deployment.yaml\nkubectl apply -f k8s\u002Fdashboard-deployment.yaml\nkubectl apply -f k8s\u002Fweb-deployment.yaml\nkubectl apply -f k8s\u002Fingress.yaml\n",[113,356,357,370,381,392,403],{"__ignoreMap":111},[116,358,359,361,364,367],{"class":118,"line":119},[116,360,242],{"class":129},[116,362,363],{"class":133}," apply",[116,365,366],{"class":133}," -f",[116,368,369],{"class":133}," k8s\u002Fbackend-config.yaml\n",[116,371,372,374,376,378],{"class":118,"line":126},[116,373,242],{"class":129},[116,375,363],{"class":133},[116,377,366],{"class":133},[116,379,380],{"class":133}," k8s\u002Fapi-deployment.yaml\n",[116,382,383,385,387,389],{"class":118,"line":147},[116,384,242],{"class":129},[116,386,363],{"class":133},[116,388,366],{"class":133},[116,390,391],{"class":133}," k8s\u002Fdashboard-deployment.yaml\n",[116,393,394,396,398,400],{"class":118,"line":156},[116,395,242],{"class":129},[116,397,363],{"class":133},[116,399,366],{"class":133},[116,401,402],{"class":133}," k8s\u002Fweb-deployment.yaml\n",[116,404,405,407,409,411],{"class":118,"line":163},[116,406,242],{"class":129},[116,408,363],{"class":133},[116,410,366],{"class":133},[116,412,413],{"class":133}," k8s\u002Fingress.yaml\n",[101,415,417],{"id":416},"_5-更新部署有新-commit-時","5. 更新部署（有新 commit 時）",[106,419,421],{"className":108,"code":420,"language":110,"meta":111,"style":111},"# 重新 build image\ngcloud builds submit .\u002Fgshop-dashboard \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n\n# 重啟 pod（Rolling Update，不會 downtime）\nkubectl rollout restart deployment\u002Fgshop-dashboard\n",[113,422,423,428,440,446,450,455],{"__ignoreMap":111},[116,424,425],{"class":118,"line":119},[116,426,427],{"class":122},"# 重新 build image\n",[116,429,430,432,434,436,438],{"class":118,"line":126},[116,431,130],{"class":129},[116,433,134],{"class":133},[116,435,137],{"class":133},[116,437,178],{"class":133},[116,439,144],{"class":143},[116,441,442,444],{"class":118,"line":147},[116,443,150],{"class":133},[116,445,188],{"class":133},[116,447,448],{"class":118,"line":156},[116,449,160],{"emptyLinePlaceholder":159},[116,451,452],{"class":118,"line":163},[116,453,454],{"class":122},"# 重啟 pod（Rolling Update，不會 downtime）\n",[116,456,457,459,462,465],{"class":118,"line":169},[116,458,242],{"class":129},[116,460,461],{"class":133}," rollout",[116,463,464],{"class":133}," restart",[116,466,467],{"class":133}," deployment\u002Fgshop-dashboard\n",[95,469],{},[44,471,472],{"id":472},"遇到的狀況與解決",[101,474,476],{"id":475},"_1-arm64amd64-平台不符","1. arm64\u002Famd64 平台不符",[48,478,479,485],{},[51,480,481,484],{},[54,482,483],{},"問題","：本地 Apple Silicon Mac build 出 arm64 image，GKE 需要 amd64",[51,486,487,490,491,494],{},[54,488,489],{},"解法","：改用 ",[113,492,493],{},"gcloud builds submit","，Cloud Build 在 amd64 機器上 build",[101,496,498],{"id":497},"_2-imagepullbackoff沒權限拉-image","2. ImagePullBackOff（沒權限拉 image）",[48,500,501,506],{},[51,502,503,505],{},[54,504,483],{},"：GKE Service Account 沒有 Artifact Registry 讀取權限",[51,507,508,510,511],{},[54,509,489],{},"：\n",[106,512,514],{"className":108,"code":513,"language":110,"meta":111,"style":111},"gcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:620172615694-compute@developer.gserviceaccount.com\" \\\n  --role=\"roles\u002Fartifactregistry.reader\"\n",[113,515,516,531,545],{"__ignoreMap":111},[116,517,518,520,523,526,529],{"class":118,"line":119},[116,519,130],{"class":129},[116,521,522],{"class":133}," projects",[116,524,525],{"class":133}," add-iam-policy-binding",[116,527,528],{"class":133}," gshop-497319",[116,530,144],{"class":143},[116,532,533,536,538,541,543],{"class":118,"line":126},[116,534,535],{"class":133},"  --member=",[116,537,265],{"class":264},[116,539,540],{"class":133},"serviceAccount:620172615694-compute@developer.gserviceaccount.com",[116,542,265],{"class":264},[116,544,144],{"class":143},[116,546,547,550,552,555],{"class":118,"line":147},[116,548,549],{"class":133},"  --role=",[116,551,265],{"class":264},[116,553,554],{"class":133},"roles\u002Fartifactregistry.reader",[116,556,310],{"class":264},[101,558,560],{"id":559},"_3-ingress-遲遲拿不到-ipneg-not-ready","3. Ingress 遲遲拿不到 IP（NEG not ready）",[48,562,563,573,612,622],{},[51,564,565,568,569,572],{},[54,566,567],{},"問題 1","：用了 ",[113,570,571],{},"spec.ingressClassName: gce","，GKE 的 controller 不認這個，要用 annotation",[51,574,575,577,578],{},[54,576,489],{},"：改成：\n",[106,579,583],{"className":580,"code":581,"language":582,"meta":111,"style":111},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","metadata:\n  annotations:\n    kubernetes.io\u002Fingress.class: gce\n","yaml",[113,584,585,594,601],{"__ignoreMap":111},[116,586,587,591],{"class":118,"line":119},[116,588,590],{"class":589},"swJcz","metadata",[116,592,593],{"class":264},":\n",[116,595,596,599],{"class":118,"line":126},[116,597,598],{"class":589},"  annotations",[116,600,593],{"class":264},[116,602,603,606,609],{"class":118,"line":147},[116,604,605],{"class":589},"    kubernetes.io\u002Fingress.class",[116,607,608],{"class":264},":",[116,610,611],{"class":133}," gce\n",[51,613,614,617,618,621],{},[54,615,616],{},"問題 2","：Service 上有舊的 ",[113,619,620],{},"networking.gke.io\u002Ftarget-pool"," annotation 造成衝突",[51,623,624,510,626,673],{},[54,625,489],{},[106,627,629],{"className":108,"code":628,"language":110,"meta":111,"style":111},"kubectl annotate svc gshop-api networking.gke.io\u002Ftarget-pool-\nkubectl annotate svc gshop-dashboard networking.gke.io\u002Ftarget-pool-\nkubectl annotate svc gshop-web networking.gke.io\u002Ftarget-pool-\n",[113,630,631,647,660],{"__ignoreMap":111},[116,632,633,635,638,641,644],{"class":118,"line":119},[116,634,242],{"class":129},[116,636,637],{"class":133}," annotate",[116,639,640],{"class":133}," svc",[116,642,643],{"class":133}," gshop-api",[116,645,646],{"class":133}," networking.gke.io\u002Ftarget-pool-\n",[116,648,649,651,653,655,658],{"class":118,"line":126},[116,650,242],{"class":129},[116,652,637],{"class":133},[116,654,640],{"class":133},[116,656,657],{"class":133}," gshop-dashboard",[116,659,646],{"class":133},[116,661,662,664,666,668,671],{"class":118,"line":147},[116,663,242],{"class":129},[116,665,637],{"class":133},[116,667,640],{"class":133},[116,669,670],{"class":133}," gshop-web",[116,672,646],{"class":133},"\n再刪掉重建 Ingress",[101,675,677],{"id":676},"_4-502-bad-gateway健康檢查失敗","4. 502 Bad Gateway（健康檢查失敗）",[48,679,680,713],{},[51,681,682,684,685,688,689],{},[54,683,483],{},"：GCP Load Balancer 預設用 ",[113,686,687],{},"GET \u002F"," 做健康檢查，但各服務行為不同：\n",[48,690,691,698,707],{},[51,692,693,694,697],{},"gshop-api：",[113,695,696],{},"\u002F"," 沒有 route，回非 200",[51,699,700,701,703,704],{},"gshop-dashboard：",[113,702,696],{}," 302 redirect 到 ",[113,705,706],{},"\u002Flogin",[51,708,709,710,712],{},"gshop-web：",[113,711,696],{}," 正常回 200（不需要處理）",[51,714,715,717,718,721,722,737,738],{},[54,716,489],{},"：建 ",[113,719,720],{},"BackendConfig"," 指定正確的健康檢查路徑：\n",[106,723,725],{"className":580,"code":724,"language":582,"meta":111,"style":111},"# gshop-api → \u002Fhealth\n# gshop-dashboard → \u002Flogin\n",[113,726,727,732],{"__ignoreMap":111},[116,728,729],{"class":118,"line":119},[116,730,731],{"class":122},"# gshop-api → \u002Fhealth\n",[116,733,734],{"class":118,"line":126},[116,735,736],{"class":122},"# gshop-dashboard → \u002Flogin\n","\n並在 Service 加 annotation：\n",[106,739,741],{"className":580,"code":740,"language":582,"meta":111,"style":111},"cloud.google.com\u002Fbackend-config: '{\"default\": \"gshop-api-backend-config\"}'\n",[113,742,743],{"__ignoreMap":111},[116,744,745,748,750,753,756],{"class":118,"line":119},[116,746,747],{"class":589},"cloud.google.com\u002Fbackend-config",[116,749,608],{"class":264},[116,751,752],{"class":264}," '",[116,754,755],{"class":133},"{\"default\": \"gshop-api-backend-config\"}",[116,757,758],{"class":264},"'\n",[101,760,762],{"id":761},"_5-supabase-連線失敗enotfound","5. Supabase 連線失敗（ENOTFOUND）",[48,764,765,774],{},[51,766,767,769,770,773],{},[54,768,483],{},"：Supabase 直連（",[113,771,772],{},"db.xxx.supabase.co:5432","）是 IPv6 only，GKE 是 IPv4 only",[51,775,776,778,779,782,783,791,792,894],{},[54,777,489],{},"：改用 Supabase ",[54,780,781],{},"Session Pooler"," connection string：\n",[106,784,789],{"className":785,"code":787,"language":788},[786],"language-text","postgresql:\u002F\u002Fpostgres.vqjzzlutlovqoqshwbza:PASSWORD@aws-1-ap-southeast-1.pooler.supabase.com:5432\u002Fpostgres\n","text",[113,790,787],{"__ignoreMap":111},"\n更新 K8s secret：\n",[106,793,795],{"className":108,"code":794,"language":110,"meta":111,"style":111},"NEW_URL=\"postgresql:\u002F\u002F...\"\nkubectl patch secret gshop-api-secret -p \"{\\\"data\\\":{\\\"DATABASE_URL\\\":\\\"$(echo -n $NEW_URL | base64)\\\"}}\"\nkubectl rollout restart deployment\u002Fgshop-api\n",[113,796,797,812,883],{"__ignoreMap":111},[116,798,799,802,805,807,810],{"class":118,"line":119},[116,800,801],{"class":143},"NEW_URL",[116,803,804],{"class":264},"=",[116,806,265],{"class":264},[116,808,809],{"class":133},"postgresql:\u002F\u002F...",[116,811,310],{"class":264},[116,813,814,816,819,821,823,826,829,832,835,838,840,843,845,848,850,852,854,857,861,864,867,870,873,876,878,881],{"class":118,"line":126},[116,815,242],{"class":129},[116,817,818],{"class":133}," patch",[116,820,248],{"class":133},[116,822,254],{"class":133},[116,824,825],{"class":133}," -p",[116,827,828],{"class":264}," \"",[116,830,831],{"class":133},"{",[116,833,834],{"class":143},"\\\"",[116,836,837],{"class":133},"data",[116,839,834],{"class":143},[116,841,842],{"class":133},":{",[116,844,834],{"class":143},[116,846,847],{"class":133},"DATABASE_URL",[116,849,834],{"class":143},[116,851,608],{"class":133},[116,853,834],{"class":143},[116,855,856],{"class":264},"$(",[116,858,860],{"class":859},"s2Zo4","echo",[116,862,863],{"class":133}," -n ",[116,865,866],{"class":143},"$NEW_URL",[116,868,869],{"class":264}," |",[116,871,872],{"class":129}," base64",[116,874,875],{"class":264},")",[116,877,834],{"class":143},[116,879,880],{"class":133},"}}",[116,882,310],{"class":264},[116,884,885,887,889,891],{"class":118,"line":147},[116,886,242],{"class":129},[116,888,461],{"class":133},[116,890,464],{"class":133},[116,892,893],{"class":133}," deployment\u002Fgshop-api\n",[223,895,896],{},[226,897,898],{},"本地開發不需要改，Mac 支援 IPv6 直連沒問題",[95,900],{},[44,902,904],{"id":903},"dns-https-設定","DNS & HTTPS 設定",[101,906,908],{"id":907},"cloudflare-a-records","Cloudflare A Records",[910,911,912,925],"table",{},[913,914,915],"thead",{},[916,917,918,922],"tr",{},[919,920,921],"th",{},"子網域",[919,923,924],{},"IP",[926,927,928,937,944],"tbody",{},[916,929,930,934],{},[931,932,933],"td",{},"api.garydemo.com",[931,935,936],{},"34.160.168.110",[916,938,939,942],{},[931,940,941],{},"dashboard.garydemo.com",[931,943,936],{},[916,945,946,949],{},[931,947,948],{},"web.garydemo.com",[931,950,936],{},[223,952,953],{},[226,954,955],{},"三個都指向同一個 Ingress IP，由 Ingress 依 Host header 分流",[101,957,959],{"id":958},"cloudflare-ssltls-模式","Cloudflare SSL\u002FTLS 模式",[48,961,962,968],{},[51,963,964,965],{},"設為 ",[54,966,967],{},"Full (Strict)",[51,969,970],{},"使用 Cloudflare Origin Certificate 存為 K8s TLS Secret",[101,972,973],{"id":973},"流量路徑",[106,975,978],{"className":976,"code":977,"language":788},[786],"瀏覽器 → Cloudflare（Proxy + TLS）→ GCP Load Balancer → Ingress → Pod\n",[113,979,977],{"__ignoreMap":111},[95,981],{},[44,983,985],{"id":984},"cicdgithub-actions","CI\u002FCD（GitHub Actions）",[226,987,988],{},"每個 service 各自有獨立 repo，push 到 main 自動 build + deploy。",[106,990,993],{"className":991,"code":992,"language":788},[786],"push main → GitHub Actions → build image → push Artifact Registry → kubectl rollout restart\n",[113,994,992],{"__ignoreMap":111},[101,996,998],{"id":997},"前置設定一次性","前置設定（一次性）",[106,1000,1002],{"className":108,"code":1001,"language":110,"meta":111,"style":111},"gcloud iam service-accounts create github-actions \\\n  --display-name=\"GitHub Actions\"\n\ngcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:github-actions@gshop-497319.iam.gserviceaccount.com\" \\\n  --role=\"roles\u002Fartifactregistry.writer\"\n\ngcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:github-actions@gshop-497319.iam.gserviceaccount.com\" \\\n  --role=\"roles\u002Fcontainer.developer\"\n\ngcloud iam service-accounts keys create sa-key.json \\\n  --iam-account=github-actions@gshop-497319.iam.gserviceaccount.com\n",[113,1003,1004,1021,1033,1037,1049,1062,1073,1077,1089,1101,1112,1116,1135],{"__ignoreMap":111},[116,1005,1006,1008,1011,1014,1016,1019],{"class":118,"line":119},[116,1007,130],{"class":129},[116,1009,1010],{"class":133}," iam",[116,1012,1013],{"class":133}," service-accounts",[116,1015,245],{"class":133},[116,1017,1018],{"class":133}," github-actions",[116,1020,144],{"class":143},[116,1022,1023,1026,1028,1031],{"class":118,"line":126},[116,1024,1025],{"class":133},"  --display-name=",[116,1027,265],{"class":264},[116,1029,1030],{"class":133},"GitHub Actions",[116,1032,310],{"class":264},[116,1034,1035],{"class":118,"line":147},[116,1036,160],{"emptyLinePlaceholder":159},[116,1038,1039,1041,1043,1045,1047],{"class":118,"line":156},[116,1040,130],{"class":129},[116,1042,522],{"class":133},[116,1044,525],{"class":133},[116,1046,528],{"class":133},[116,1048,144],{"class":143},[116,1050,1051,1053,1055,1058,1060],{"class":118,"line":163},[116,1052,535],{"class":133},[116,1054,265],{"class":264},[116,1056,1057],{"class":133},"serviceAccount:github-actions@gshop-497319.iam.gserviceaccount.com",[116,1059,265],{"class":264},[116,1061,144],{"class":143},[116,1063,1064,1066,1068,1071],{"class":118,"line":169},[116,1065,549],{"class":133},[116,1067,265],{"class":264},[116,1069,1070],{"class":133},"roles\u002Fartifactregistry.writer",[116,1072,310],{"class":264},[116,1074,1075],{"class":118,"line":183},[116,1076,160],{"emptyLinePlaceholder":159},[116,1078,1079,1081,1083,1085,1087],{"class":118,"line":191},[116,1080,130],{"class":129},[116,1082,522],{"class":133},[116,1084,525],{"class":133},[116,1086,528],{"class":133},[116,1088,144],{"class":143},[116,1090,1091,1093,1095,1097,1099],{"class":118,"line":196},[116,1092,535],{"class":133},[116,1094,265],{"class":264},[116,1096,1057],{"class":133},[116,1098,265],{"class":264},[116,1100,144],{"class":143},[116,1102,1103,1105,1107,1110],{"class":118,"line":202},[116,1104,549],{"class":133},[116,1106,265],{"class":264},[116,1108,1109],{"class":133},"roles\u002Fcontainer.developer",[116,1111,310],{"class":264},[116,1113,1114],{"class":118,"line":216},[116,1115,160],{"emptyLinePlaceholder":159},[116,1117,1119,1121,1123,1125,1128,1130,1133],{"class":118,"line":1118},12,[116,1120,130],{"class":129},[116,1122,1010],{"class":133},[116,1124,1013],{"class":133},[116,1126,1127],{"class":133}," keys",[116,1129,245],{"class":133},[116,1131,1132],{"class":133}," sa-key.json",[116,1134,144],{"class":143},[116,1136,1138],{"class":118,"line":1137},13,[116,1139,1140],{"class":133},"  --iam-account=github-actions@gshop-497319.iam.gserviceaccount.com\n",[226,1142,1143,1144,1147,1148,1151],{},"GitHub org → Settings → Secrets → New organization secret，名稱 ",[113,1145,1146],{},"GCP_SA_KEY","，貼入 ",[113,1149,1150],{},"sa-key.json"," 內容。",[101,1153,1155],{"id":1154},"workflow-範例","Workflow 範例",[106,1157,1159],{"className":580,"code":1158,"language":582,"meta":111,"style":111},"name: Deploy gshop-api\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: google-github-actions\u002Fauth@v2\n        with:\n          credentials_json: ${{ secrets.GCP_SA_KEY }}\n      - uses: google-github-actions\u002Fsetup-gcloud@v2\n      - run: gcloud auth configure-docker asia-east1-docker.pkg.dev\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      - uses: google-github-actions\u002Fget-gke-credentials@v2\n        with:\n          cluster_name: gshop-cluster\n          location: asia-east1\n      - run: kubectl rollout restart deployment\u002Fgshop-api\n",[113,1160,1161,1171,1175,1183,1190,1197,1205,1209,1216,1223,1233,1240,1252,1263,1271,1282,1294,1307,1320,1332,1338,1344,1356,1363,1374,1385],{"__ignoreMap":111},[116,1162,1163,1166,1168],{"class":118,"line":119},[116,1164,1165],{"class":589},"name",[116,1167,608],{"class":264},[116,1169,1170],{"class":133}," Deploy gshop-api\n",[116,1172,1173],{"class":118,"line":126},[116,1174,160],{"emptyLinePlaceholder":159},[116,1176,1177,1181],{"class":118,"line":147},[116,1178,1180],{"class":1179},"sfNiH","on",[116,1182,593],{"class":264},[116,1184,1185,1188],{"class":118,"line":156},[116,1186,1187],{"class":589},"  push",[116,1189,593],{"class":264},[116,1191,1192,1195],{"class":118,"line":163},[116,1193,1194],{"class":589},"    branches",[116,1196,593],{"class":264},[116,1198,1199,1202],{"class":118,"line":169},[116,1200,1201],{"class":264},"      -",[116,1203,1204],{"class":133}," main\n",[116,1206,1207],{"class":118,"line":183},[116,1208,160],{"emptyLinePlaceholder":159},[116,1210,1211,1214],{"class":118,"line":191},[116,1212,1213],{"class":589},"jobs",[116,1215,593],{"class":264},[116,1217,1218,1221],{"class":118,"line":196},[116,1219,1220],{"class":589},"  deploy",[116,1222,593],{"class":264},[116,1224,1225,1228,1230],{"class":118,"line":202},[116,1226,1227],{"class":589},"    runs-on",[116,1229,608],{"class":264},[116,1231,1232],{"class":133}," ubuntu-latest\n",[116,1234,1235,1238],{"class":118,"line":216},[116,1236,1237],{"class":589},"    steps",[116,1239,593],{"class":264},[116,1241,1242,1244,1247,1249],{"class":118,"line":1118},[116,1243,1201],{"class":264},[116,1245,1246],{"class":589}," uses",[116,1248,608],{"class":264},[116,1250,1251],{"class":133}," actions\u002Fcheckout@v4\n",[116,1253,1254,1256,1258,1260],{"class":118,"line":1137},[116,1255,1201],{"class":264},[116,1257,1246],{"class":589},[116,1259,608],{"class":264},[116,1261,1262],{"class":133}," google-github-actions\u002Fauth@v2\n",[116,1264,1266,1269],{"class":118,"line":1265},14,[116,1267,1268],{"class":589},"        with",[116,1270,593],{"class":264},[116,1272,1274,1277,1279],{"class":118,"line":1273},15,[116,1275,1276],{"class":589},"          credentials_json",[116,1278,608],{"class":264},[116,1280,1281],{"class":133}," ${{ secrets.GCP_SA_KEY }}\n",[116,1283,1285,1287,1289,1291],{"class":118,"line":1284},16,[116,1286,1201],{"class":264},[116,1288,1246],{"class":589},[116,1290,608],{"class":264},[116,1292,1293],{"class":133}," google-github-actions\u002Fsetup-gcloud@v2\n",[116,1295,1297,1299,1302,1304],{"class":118,"line":1296},17,[116,1298,1201],{"class":264},[116,1300,1301],{"class":589}," run",[116,1303,608],{"class":264},[116,1305,1306],{"class":133}," gcloud auth configure-docker asia-east1-docker.pkg.dev\n",[116,1308,1310,1312,1315,1317],{"class":118,"line":1309},18,[116,1311,1201],{"class":264},[116,1313,1314],{"class":589}," name",[116,1316,608],{"class":264},[116,1318,1319],{"class":133}," Build and push image\n",[116,1321,1323,1326,1328],{"class":118,"line":1322},19,[116,1324,1325],{"class":589},"        run",[116,1327,608],{"class":264},[116,1329,1331],{"class":1330},"s7zQu"," |\n",[116,1333,1335],{"class":118,"line":1334},20,[116,1336,1337],{"class":133},"          docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest .\n",[116,1339,1341],{"class":118,"line":1340},21,[116,1342,1343],{"class":133},"          docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[116,1345,1347,1349,1351,1353],{"class":118,"line":1346},22,[116,1348,1201],{"class":264},[116,1350,1246],{"class":589},[116,1352,608],{"class":264},[116,1354,1355],{"class":133}," google-github-actions\u002Fget-gke-credentials@v2\n",[116,1357,1359,1361],{"class":118,"line":1358},23,[116,1360,1268],{"class":589},[116,1362,593],{"class":264},[116,1364,1366,1369,1371],{"class":118,"line":1365},24,[116,1367,1368],{"class":589},"          cluster_name",[116,1370,608],{"class":264},[116,1372,1373],{"class":133}," gshop-cluster\n",[116,1375,1377,1380,1382],{"class":118,"line":1376},25,[116,1378,1379],{"class":589},"          location",[116,1381,608],{"class":264},[116,1383,1384],{"class":133}," asia-east1\n",[116,1386,1388,1390,1392,1394],{"class":118,"line":1387},26,[116,1389,1201],{"class":264},[116,1391,1301],{"class":589},[116,1393,608],{"class":264},[116,1395,1396],{"class":133}," kubectl rollout restart deployment\u002Fgshop-api\n",[223,1398,1399],{},[226,1400,1401],{},"GitHub Actions runner 是 ubuntu amd64，build 出的 image 天生就是 amd64，不需要 Cloud Build",[95,1403],{},[44,1405,1406],{"id":1406},"常用指令",[106,1408,1410],{"className":108,"code":1409,"language":110,"meta":111,"style":111},"# 查 pod 狀態\nkubectl get pods\n\n# 查 ingress IP\nkubectl get ingress gshop-ingress\n\n# 查某服務 log\nkubectl logs -l app=gshop-api --tail=50\n\n# 查 backend 健康狀態\ngcloud compute backend-services get-health \u003Cbackend-name> --global\n\n# 列出所有 backend\ngcloud compute backend-services list --global\n\n# 查 NEG\ngcloud compute network-endpoint-groups list\n",[113,1411,1412,1417,1427,1431,1436,1448,1452,1457,1473,1477,1482,1510,1514,1519,1532,1536,1541],{"__ignoreMap":111},[116,1413,1414],{"class":118,"line":119},[116,1415,1416],{"class":122},"# 查 pod 狀態\n",[116,1418,1419,1421,1424],{"class":118,"line":126},[116,1420,242],{"class":129},[116,1422,1423],{"class":133}," get",[116,1425,1426],{"class":133}," pods\n",[116,1428,1429],{"class":118,"line":147},[116,1430,160],{"emptyLinePlaceholder":159},[116,1432,1433],{"class":118,"line":156},[116,1434,1435],{"class":122},"# 查 ingress IP\n",[116,1437,1438,1440,1442,1445],{"class":118,"line":163},[116,1439,242],{"class":129},[116,1441,1423],{"class":133},[116,1443,1444],{"class":133}," ingress",[116,1446,1447],{"class":133}," gshop-ingress\n",[116,1449,1450],{"class":118,"line":169},[116,1451,160],{"emptyLinePlaceholder":159},[116,1453,1454],{"class":118,"line":183},[116,1455,1456],{"class":122},"# 查某服務 log\n",[116,1458,1459,1461,1464,1467,1470],{"class":118,"line":191},[116,1460,242],{"class":129},[116,1462,1463],{"class":133}," logs",[116,1465,1466],{"class":133}," -l",[116,1468,1469],{"class":133}," app=gshop-api",[116,1471,1472],{"class":133}," --tail=50\n",[116,1474,1475],{"class":118,"line":196},[116,1476,160],{"emptyLinePlaceholder":159},[116,1478,1479],{"class":118,"line":202},[116,1480,1481],{"class":122},"# 查 backend 健康狀態\n",[116,1483,1484,1486,1489,1492,1495,1498,1501,1504,1507],{"class":118,"line":216},[116,1485,130],{"class":129},[116,1487,1488],{"class":133}," compute",[116,1490,1491],{"class":133}," backend-services",[116,1493,1494],{"class":133}," get-health",[116,1496,1497],{"class":264}," \u003C",[116,1499,1500],{"class":133},"backend-nam",[116,1502,1503],{"class":143},"e",[116,1505,1506],{"class":264},">",[116,1508,1509],{"class":133}," --global\n",[116,1511,1512],{"class":118,"line":1118},[116,1513,160],{"emptyLinePlaceholder":159},[116,1515,1516],{"class":118,"line":1137},[116,1517,1518],{"class":122},"# 列出所有 backend\n",[116,1520,1521,1523,1525,1527,1530],{"class":118,"line":1265},[116,1522,130],{"class":129},[116,1524,1488],{"class":133},[116,1526,1491],{"class":133},[116,1528,1529],{"class":133}," list",[116,1531,1509],{"class":133},[116,1533,1534],{"class":118,"line":1273},[116,1535,160],{"emptyLinePlaceholder":159},[116,1537,1538],{"class":118,"line":1284},[116,1539,1540],{"class":122},"# 查 NEG\n",[116,1542,1543,1545,1547,1550],{"class":118,"line":1296},[116,1544,130],{"class":129},[116,1546,1488],{"class":133},[116,1548,1549],{"class":133}," network-endpoint-groups",[116,1551,1552],{"class":133}," list\n",[1554,1555,1556],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}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":111,"searchDepth":126,"depth":126,"links":1558},[1559,1560,1567,1574,1579,1583],{"id":46,"depth":126,"text":46},{"id":99,"depth":126,"text":99,"children":1561},[1562,1563,1564,1565,1566],{"id":103,"depth":147,"text":104},{"id":231,"depth":147,"text":232},{"id":313,"depth":147,"text":314},{"id":350,"depth":147,"text":351},{"id":416,"depth":147,"text":417},{"id":472,"depth":126,"text":472,"children":1568},[1569,1570,1571,1572,1573],{"id":475,"depth":147,"text":476},{"id":497,"depth":147,"text":498},{"id":559,"depth":147,"text":560},{"id":676,"depth":147,"text":677},{"id":761,"depth":147,"text":762},{"id":903,"depth":126,"text":904,"children":1575},[1576,1577,1578],{"id":907,"depth":147,"text":908},{"id":958,"depth":147,"text":959},{"id":973,"depth":147,"text":973},{"id":984,"depth":126,"text":985,"children":1580},[1581,1582],{"id":997,"depth":147,"text":998},{"id":1154,"depth":147,"text":1155},{"id":1406,"depth":126,"text":1406},"2026-06-16","記錄將個人電商（GShop）部署到 GKE Autopilot 的完整流程。","md",null,"\u002Fimages\u002Fgke.jpg",{},{"title":14,"description":1585},"nwSAGWy6EGbowdd_9ERLH4cormr59zF4uBKDtY7Wdco",[1593,1595],{"title":10,"path":11,"stem":12,"description":1594,"children":-1},"資料量龐大導致查詢變慢，透過 Cron Job 將每日統計好的資料寫入 Snapshot Table，查詢不再需要跨表掃描改為單表讀取。",{"title":18,"path":19,"stem":20,"description":1596,"children":-1},"從 Token、Context、Prompt，到 Tool、MCP、Agent，完整梳理 AI 應用開發的核心概念與底層運作原理。",1781661890755]