คู่มือนี้จะครบถ้วนและสามารถสแกนได้เพื่อให้คุณสามารถเข้าถึงชิ้นส่วนที่คุณต้องการได้โดยตรง จะมี GitHub repo (มาเร็ว ๆ นี้) ที่คุณสามารถใช้เพื่อผ่านเรื่องนี้ อัตราส่วน: About Errata What This Guide Covers and Does Not. What about LLMs and tools? (Gemini, Claude, Claude Code, Cursor, etc.) Definitions Requirements What is Google Cloud Workload Identity Flotation? Architecture Overview Configure a custom OIDP (Open ID Provider) using Dex. How to configure a custom one using DEX, which is an identity provider. How to connect a Docker application to pull it. There's also going to be some code, some JavaScript code that we're going to use to connect directly to it and issue some tokens. Follow for more. เกี่ยวกับ การตั้งค่าบัญชี Google Cloud Platform สําหรับองค์กร (GCP) มีความท้าทายบางอย่าง (เช่นการเข้าถึง Gemini ผ่าน Vertex AI บนบริการภายนอกเช่น Vercel / Digital Ocean / Hetzner) ก่อนหน้านี้คุณจะสร้างบัญชีบริการที่เก็บสิทธิ์สําหรับบริการ (เช่น Vertex AI) และสร้างคีย์ที่มีอายุการใช้งานยาวนาน (ไม่มีวันที่หมดอายุ) ที่คุณจะใช้เพื่อเชื่อมต่อกับ GCP สิ่งนี้ไม่แนะนํา คีย์บัญชีบริการอาจถูกทําลายทําให้การหมุนเป็นเรื่องยาก Google เคล็ดลับว่าคุณจะใช้ tokens มีอายุการใช้งานสั้นผ่าน Workload Identity Federation และ Open ID Connect เพื่อสื่อสารกับบริการ แต่คุณสร้าง แทน __still use __a service account Token ชีวิตสั้น ๆ Token ชีวิตสั้น ๆ คําเตือนคือการกําหนดค่านี้ไม่เรียบง่าย การจัดเก็บเอกสารไม่เพียงพอและเป็นความผิดปกติ มันใช้เวลาฉัน 1.5 เดือนในระยะสั้นเพื่อให้ถูกต้อง อย่ากังวล คู่มือนี้จะสอนให้คุณทําขั้นตอนตามขั้นตอนนี้ เราจะกําหนดค่า Open ID Provider (Dex) ที่จะใช้ในการออก tokens และจะเชื่อมต่อ Docker และ app ของเราเพื่อให้เราสามารถดึงภาพจาก Artifact Registry และเชื่อมต่อกับ Vertex AI ทั้งหมดผ่าน VPS (Hetzner) ล้มเหลว ฉันได้ทําสิ่งนี้ให้ดีที่สุดของความสามารถของฉันรู้สึกอิสระที่จะเข้าถึงและแนะนําการปรับปรุงหรือแก้ไขข้อผิดพลาด! สิ่งที่คู่มือนี้ครอบคลุมและไม่ได้ ครอบคลุม: การตั้งค่าผู้ให้บริการประจําตัวภายนอกโดยใช้ Dex ที่รับรองความถูกต้องกับ Google Cloud ลากภาพผ่านจดทะเบียนอสังหาริมทรัพย์โดยใช้ Docker และ OpenID Connect/Workload Identity Federation การออก token OIDC ผ่านแอปพลิเคชัน NodeJS โดยใช้ google-auth-biblioteket ไม่ครอบคลุม: วิธีการเริ่มต้นเซิร์ฟเวอร์ใน VPS ภายนอกหรือ Google Cloud วิธีการอัปโหลดไฟล์ไปยัง VPS ของคุณ วิธีการใช้ IaC (Infrastructure as Code) ในการตั้งค่า Google Cloud Services วิธีการตั้งค่าการกระทํา GitHub เพื่อสื่อสารกับ GCP คู่มือนี้จะครอบคลุมวิธีการตั้งค่าผู้ให้บริการประจําตัวโดยใช้ Dex (Open Source) และสร้างโทเค็น OIDC กับมันเพื่อให้คุณสามารถเชื่อมต่อผ่านผู้ให้บริการอื่น ๆ ในโลก อะไรเกี่ยวกับ LLMs และเครื่องมือ? (Gemini, Claude, Claude Code, Cursor ฯลฯ) พวกเขาช่วย พวกเขาไม่รู้วิธีการผสมกลยุทธ์ที่จําเป็นสําหรับ Google Workload Identity Federation กับ Dex a lot แปลง LLM ไปยังหน้านี้เพื่อช่วยให้คุณตั้งค่า คําอธิบาย แพลตฟอร์มคลาวด์ Google (GCP) โซลูชั่นระบบคลาวด์ที่เราจะใช้ในการรับรองความถูกต้องด้วย OIDC ที่สั้นลงเป็น GCP สําหรับบทความนี้ Workload Identity Federation: เป็นวิธีในการทํางานของภาระงาน (แอปพลิเคชันบริการท่อ CI / CD VMs คอนเทนเนอร์ ฯลฯ ) ของซัพพลายเออร์คลาวด์เพื่อตรวจสอบความปลอดภัยกับ API ของซัพพลายเออร์ . outside without using long-lived service account keys แทนที่จะให้แอปพลิเคชันของคุณไฟล์คีย์แบบคงที่ (ซึ่งเป็นความเสี่ยงด้านความปลอดภัยอย่างมีนัยสําคัญหากมีการรั่วไหล) WIF ช่วยให้ผู้ให้บริการคลาวด์ไว้วางใจ เช่น GitHub Actions, GitLab, Kubernetes หรือผู้ให้บริการที่เข้ากันได้กับ OIDC / SAML external identity provider (IdP) กล่าวอีกนัยหนึ่งมันจะสร้างโทเค็นที่มีสิทธิ์ที่เฉพาะเจาะจงที่คุณสามารถส่งได้อย่างปลอดภัยผ่านโหลดประโยชน์ของคุณ (เช่น Cloud Storage get, list, upload) เพื่อเข้าถึงบริการ ผู้ให้บริการข้อมูลประจําตัว มันเป็นศูนย์กลางที่ผู้ใช้หรือเครื่อง (รหัสของเรา) สามารถใช้เพื่อ "เข้าสู่ระบบ" หรือรับรองความถูกต้องกับบริการหนึ่งหรือหลายบริการ ในกรณีของเรามันเป็นระบบที่จะใช้เพื่อสร้าง token ที่ถูกต้องที่จะถูกแลกเปลี่ยนกับ GCP เพื่อให้การเข้าถึงแอปพลิเคชันของเรา คุณจะเห็นในภายหลังเกี่ยวกับวิธีการทํางานในรายละเอียด ตัวอย่างผู้ให้บริการประจําตัว แพลตฟอร์มผู้พัฒนา (OIDC-native): เหล่านี้มีจุดสิ้นสุดเฉพาะที่คุณสามารถส่งไปยัง GCP และสร้าง tokens ที่จําเป็นโดยอัตโนมัติ GitHub (GitHub Actions OIDC แท็ก) GitLab (ท่อ CI / CD กับ OIDC) Bitbucket (pipelines OIDC) ผู้ให้บริการอื่น ๆ : ตุลาคม Auth0 Azure Active Directory แพลตฟอร์ม Google Identity Ping การระบุตัวตน AWS IAM Identity Center Keycloak Dex (หนึ่งที่เราจะใช้) Dex https://dexidp.io/ นี่คือแพลตฟอร์ม Identity ที่เราจะใช้ เรากําหนดค่าด้วยชื่อผู้ใช้และรหัสผ่านและผ่านจุดปลายทาง REST เราเรียกใช้เครื่องมือที่เลือกของเรา (cURL, wget, fetch / xhr / axios ของ JavaScript ฯลฯ) จากนั้นจะกลับ JWT ที่เราสามารถใช้เพื่อสื่อสารกับ GCP Why? มันเป็นแหล่งที่มาเปิดและสามารถโฮสต์ได้ฟรี ต้องใช้ไฟล์ YAML หนึ่งไฟล์เท่านั้น คุณไม่จําเป็นต้องใช้ Kubernetes เพื่อเรียกใช้ (แม้ว่ามันเป็นที่นิยมในสภาพแวดล้อมนั้น) เราจะครอบคลุมวิธีการใช้งานการเชื่อมต่อระหว่าง Google Cloud และแหล่งที่มาภายนอก (เช่น VPS) โดยใช้ Google Cloud Workload Identity Federation, Dex (ผู้ให้บริการตัวตน) และอื่น ๆ ฮิตเนอร์ https://www.hetzner.com/ มันเป็นผู้ให้บริการคลาวด์ที่เลือกสําหรับอัตราส่วนราคา / ประสิทธิภาพที่น่าประทับใจ นอกจากนี้ยังมีปลั๊กอิน UI และ IaC (โครงสร้างพื้นฐานเป็นรหัส) ที่ง่ายมากซึ่งทําให้การตั้งค่าได้ง่าย โดเพลอรี่ https://www.doppler.com/ เป็นผู้จัดการความลับ บางอย่างคล้ายกับ HashiCorp Vault หรือ AWS Secrets Manager Doppler ช่วยให้เราสามารถจัดเก็บตัวแปรสภาพแวดล้อมของเราในโครงการและสาขาที่แตกต่างกันซึ่งช่วยให้สมาชิกในทีมของเราสามารถแบ่งปันได้ เราสามารถปกป้องปลั๊กแยกและคัดลอกได้โดยไม่ต้องแบ่งปันไฟล์ .env ระหว่างทีม แม้จะทํางานเดี่ยวก็มีระดับฟรีที่อุดมสมบูรณ์มากซึ่งจะครอบคลุมความต้องการทั้งหมดของคุณ ฉันใช้มันตั้งแต่ปีที่ผ่านมาเพื่อจัดการรหัสผ่านทั้งหมดของฉันสําหรับบริการทั้งหมดของฉัน แม้ว่าจะเป็นตัวเลือกทั้งหมดโพสต์นี้จะคาดว่าคุณใช้ Doppler เพื่อโหลดสภาพแวดล้อม (คุณสามารถโหลด .env ใกล้กับคําสั่งและคุณจะดีที่จะไป) ข้อกําหนด A Google Cloud Platform (GCP) account Have a server you can connect to (SSH): Virtual Machine - VPS, Bare Metal, etc. A domain name (.com, .ai, .app, .io etc.) pointing to your server. Basic Knowledge of: Docker Google CLI Bash I assume: You have a working GCP project. You have Artifact Registry enabled with a docker image Have installed Docker in your VPS. ติดตั้ง Google CLI บนเครื่องของคุณ ดาวน์โหลดลิงก์การติดตั้ง: https://cloud.google.com/sdk/docs/install ดาวน์โหลดลิงก์การติดตั้ง: https://cloud.google.com/sdk/docs/install งานแรกของเราคือการกําหนดค่าสิทธิ์ที่จําเป็นและเปิดใช้งานบริการ เราใช้ Google CLI นี่คือสถานที่ที่คุณสามารถติดตั้งได้: (ทราบว่าขั้นตอนเพิ่มเติมจะถูกลืมจากบทความนี้) หน้าต่าง : ดาวน์โหลดโปรแกรมติดตั้งที่นี่ แม็กซ์ : Install Brew (in case you haven’t): /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ติดตั้ง Google CLI: brew install --cask gcloud-cli ลินุกซ์ (ดาวน์โหลดไฟล์) curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz เปิดมัน: tar -xf google-cloud-cli-linux-x86_64.tar.gz Create a Project in Google Cloud or use an existing one ไปที่: https://console.cloud.google.com/ เลือกบัญชีของคุณและดําเนินการสร้างโครงการใหม่หากคุณยังไม่ได้ทําเช่นนั้น Init Google Cloud CLI หลังจากติดตั้งแล้วเปิด terminal และเรียกใช้: gcloud init นี้จะเริ่มต้นกระบวนการเชื่อมต่อ CLI ทํางานโดยการเชื่อมโยงบัญชี Google ของคุณ บัญชีนี้ต้องมีสิทธิ์ที่จําเป็นในการดําเนินการทางบริหาร กล่าวอีกนัยหนึ่งหากคุณเริ่มใช้บัญชีหลักของคุณ CLI ทํางานโดยการเชื่อมโยงบัญชี Google ของคุณ บัญชีนี้ต้องมีสิทธิ์ที่จําเป็นในการดําเนินการทางบริหาร กล่าวอีกนัยหนึ่งหากคุณเริ่มใช้บัญชีหลักของคุณ Choose the project you’ve created. หลังจากการรับรองความถูกต้อง CLI จะขอให้คุณเลือกโครงการที่คุณต้องการทํางานบน นี่คือเพื่อความสะดวก This can be changed later on with: gcloud config set project PROJECT_ID OR, you can always pass the ภาษาไทย คําสั่ง --project PROJECT_ID all the gcloud Enable the Services: You will need to enable these. ใน terminal ของคุณ Run: gcloud services enable \ iamcredentials.googleapis.com \ sts.googleapis.com \ iam.googleapis.com \ cloudresourcemanager.googleapis.com Environment Variables ใช้ทั้งหมดโพสต์ เปลี่ยนด้วยของคุณเอง export PROJECT_ID="spiritual-slate-445211-i1" export PROJECT_NUMBER="1016670781645" export POOL_ID="hetzner-pool" export PROVIDER_ID="hetzner-provider" export SERVICE_ACCOUNT_ID="hetzner" export SERVICE_ACCOUNT="$SERVICE_ACCOUNT_ID@${PROJECT_ID}.iam.gserviceaccount.com" export DEX_ISSUER="https://auth.yourdomain.com" export ROLE_NAME="hetzner_role" คุณสามารถรับ Project ID และ Project Number จากหน้าหลัก เลือกโครงการของคุณจากปุ่มมุมด้านบนด้านซ้าย คุณสามารถรับ Project ID และ Project Number จากหน้าหลัก เลือกโครงการของคุณจากปุ่มมุมด้านบนด้านซ้าย การกําหนดค่า Workload Identity Federation วิธีการทํางานใน GCP: คุณจะสร้างฐานข้อมูลที่เชื่อมต่อกับผู้ให้บริการหรือลูกค้าจํานวนมาก ซัพพลายเออร์เหล่านี้เป็นผู้ที่ token ของ Dex จะใช้เพื่อรับรองความถูกต้องกับ We try to restrict these by using an “attribute mapping,”. It parses the JWT from Dex, compares the fields from the attribute mapping with the ones in the token. If one of them doesn’t match, it’s rejected. พิจารณาสระว่ายน้ําเป็นกลุ่มของวิธีการตรวจสอบที่อาจเกิดขึ้นที่คุณให้ GCP รู้ว่าบุคคลที่สามอื่น ๆ สามารถตรวจสอบความถูกต้องกับ พิจารณาสระว่ายน้ําเป็นกลุ่มของวิธีการตรวจสอบที่อาจเกิดขึ้นที่คุณให้ GCP รู้ว่าบุคคลที่สามอื่น ๆ สามารถตรวจสอบความถูกต้องกับ สร้างสระว่ายน้ํา ดําเนินคําสั่งนี้: gcloud iam workload-identity-pools create "$POOL_ID" \ --project="$PROJECT_ID" \ --location="global" \ --display-name="Hetzner Workload Identity Pool" \ --description="Pool for Hetzner VPS to access GCP services via OIDC" ตัวอย่าง : gcloud iam workload-identity-pools create "hetzner-pool" \ --project="spiritual-slate-445211-i1" \ --location="global" \ --display-name="Hetzner Workload Identity Pool" \ --description="Pool for Hetzner VPS to access GCP services via OIDC" Example PowerShell: gcloud iam workload-identity-pools create "hetzner-pool" ` --project="spiritual-slate-445211-i1" ` --location="global" ` --display-name="Hetzner Workload Identity Pool" ` --description="Pool for Hetzner VPS to access GCP services via OIDC" สร้างผู้ให้บริการ OIDC นี่จะเป็นจุดเชื่อมต่อศูนย์กลางกับ Google Cloud นี่จะเป็นจุดเข้าซึ่งยืนยัน JWT จาก Dex Be careful with the --attribute-mapping flag. This is the one that tells GCP to compare the fields with JWT for OIDC. I recommend you (while testing) to start broad and then narrow down the permission scopes as you become more knowledgeable. เคล็ดลับนี้จะบอก GCP เพื่อเปรียบเทียบฟิลด์กับ JWT สําหรับ OIDC ฉันขอแนะนําให้คุณ (ในระหว่างการทดสอบ) เริ่มกว้างแล้วลดขอบเขตอนุญาตเมื่อคุณได้รับความรู้มากขึ้น # Create OIDC provider pointing to your DEX instance gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \ --project="$PROJECT_ID" \ --location="global" \ --workload-identity-pool="$POOL_ID" \ --display-name="Hetzner Dex OIDC Provider" \ --description="OIDC provider using Dex for Hetzner VPS authentication" \ --attribute-mapping="google.subject=assertion.sub,attribute.email=assertion.email,attribute.groups=assertion.groups" \ --issuer-uri="$DEX_ISSUER" ตัวอย่าง : # Create OIDC provider pointing to your DEX instance gcloud iam workload-identity-pools providers create-oidc "hetzner-provider" \ --project="spiritual-slate-445211-i1" \ --location="global" \ --workload-identity-pool="hetzner-pool" \ --display-name="Hetzner Dex OIDC Provider" \ --description="OIDC provider using Dex for Hetzner VPS authentication" \ --attribute-mapping="google.subject=assertion.sub,attribute.email=assertion.email,attribute.groups=assertion.groups" \ --issuer-uri="https://auth.yourdomain.com" สิ่งนี้จะแจ้งให้ GCP ว่าหัวข้อและอีเมลของ JWT ควรตรงกับสิ่งที่จากสระว่ายน้ําที่จะถือว่าถูกต้อง การกําหนดค่าบัญชีบริการ ใช่คุณยังคงต้องสร้างบัญชีบริการที่มีสิทธิ์สุดท้ายที่จะใช้เพื่อเข้าถึงทรัพยากร GCP ของคุณ ความแตกต่างคือเราจะ this account using แทนที่จะใช้กุญแจบัญชีบริการถาวร impersonate ชีวิตสั้น ๆ Tokens ชีวิตสั้น ๆ Tokens Create a service account. gcloud iam service-accounts create "$SERVICE_ACCOUNT_ID"\ --project="$PROJECT_ID" \ --display-name="Hetzner VPS Service Account" \ --description="Service account for Hetzner VPS containers" Example: gcloud iam service-accounts create "hetzner" \ --project="spiritual-slate-445211-i1" \ --display-name="Hetzner VPS Service Account" \ --description="Service account for Hetzner VPS containers" PowerShell สําหรับ: gcloud iam service-accounts create "hetzner" ` --project="spiritual-slate-445211-i1" ` --display-name="Hetzner VPS Service Account" ` --description="Service account for Hetzner VPS containers" การกําหนดบทบาทไปยังบัญชีบริการ คุณสามารถกําหนดบทบาทที่กําหนดไว้ล่วงหน้าของ GCP โดยไปที่ลิงก์นี้: https://cloud.google.com/iam/docs/roles-permissions ฉันได้เลือกวิธีการรายละเอียดมากขึ้นซึ่งตั้งค่าอนุญาตโดยตรงไปยังบทบาทที่กําหนดเองซึ่งช่วยให้ฉันลดพื้นที่พื้นผิวการโจมตี OR - การกําหนดบทบาทหรืออนุญาตแบบ granular: วิธีการนี้จะนําไปใช้กับ GCP (403 ข้อผิดพลาดที่ต้องห้ามที่นี่และที่นั่น) แต่ในที่สุดคุณจะได้รับ infra ที่ปลอดภัยมากขึ้น คุณสามารถค้นหาอนุญาต granular ในลิงค์นี้: https://cloud.google.com/iam/docs/อนุญาตการอ้างอิง เราเริ่มต้นด้วยการสร้างบทบาทที่กําหนดเอง สร้างบทบาทที่กําหนดเองสําหรับบัญชีบริการ - ด้วยสิทธิ์ขั้นต่ํา To create a custom role, we need a YAML file that will hold each permission: สร้างไฟล์ที่มีชื่อว่า “roles-hetzner.gcp.yml” และคัดลอกและวางไว้ดังต่อไปนี้ (ทราบว่าไฟล์นี้มีชีวิตอยู่ในท้องถิ่นในเครื่องหรือ repo ของคุณ) # To update the role: gcloud iam roles update hetzner_role --project=spiritual-slate-445211-i1 --file=./roles-hetzner.gcp.yml title: Hetzner GCP Roles description: | This policy ensures that the Hetzner VPS has the required permissions to access all the Google Cloud services needed for running Docker containers with the same functionality as when they were deployed on Cloud Run. stage: GA # https://cloud.google.com/iam/docs/permissions-reference includedPermissions: # === IAM Permissions === # For service account creation and management - iam.serviceAccounts.getAccessToken - iam.serviceAccounts.signBlob # Required for signed URLs # === Artifact Registry Permissions === # For Docker image storage and management - artifactregistry.repositories.get - artifactregistry.repositories.list - artifactregistry.repositories.downloadArtifacts - artifactregistry.packages.get - artifactregistry.packages.list - artifactregistry.versions.get - artifactregistry.versions.list - artifactregistry.dockerimages.get - artifactregistry.dockerimages.list - artifactregistry.tags.get - artifactregistry.tags.list - artifactregistry.files.get - artifactregistry.files.list - artifactregistry.files.download - artifactregistry.versions.get - artifactregistry.versions.list - resourcemanager.projects.get - artifactregistry.attachments.get - artifactregistry.attachments.list # === Cloud Storage Permissions === # For bucket and object operations - storage.objects.create - storage.objects.delete # Optional: only include if you need to delete images - storage.objects.get - storage.objects.list - storage.objects.update - storage.objects.getIamPolicy - storage.objects.setIamPolicy # === Vertex AI Permissions === # For AI/ML platform operations - aiplatform.endpoints.predict - aiplatform.endpoints.get # Permissions for operations management - aiplatform.operations.list แล้วดําเนินการคําสั่ง: gcloud iam roles create "$ROLE_NAME" --project="$PROJECT_ID" --file="./roles-hetzner-gcp.yml" gcloud iam roles create hetzner_role --project="spiritual-slate-445211-i1" --file="./roles-hetzner-gcp.yml" ระบายความร้อน: พารามิเตอร์ --file ในคําสั่งหมายถึงเส้นทางที่เกี่ยวข้องที่ terminal ของคุณอยู่: เนื่องจากไฟล์อยู่ในสถานที่เดียวกันเราสามารถทําได้: gcloud iam roles create "herzner_role" --project="spiritual-slate-445211-i1" --file="roles-hetzner.gcp.yml" ระบายความร้อน: พารามิเตอร์ --file ในคําสั่งหมายถึงเส้นทางที่เกี่ยวข้องที่ terminal ของคุณอยู่: เนื่องจากไฟล์อยู่ในสถานที่เดียวกันเราสามารถทําได้: gcloud iam roles create "herzner_role" --project="spiritual-slate-445211-i1" --file="roles-hetzner.gcp.yml" /Users/joseasilis/Documents/programming/alertdown/libs/infrastructure/src/pulumi Updating the role To make changes to the role, you can update it like: gcloud iam roles update hetzner_role --project=spiritual-slate-445211-i1 --file="./roles-hetzner.gcp.yml" How to get granular use it: How to get granular use it: ค้นหาบริการที่ฉันต้องการใช้เช่น Vertex AI และคลิกที่: ใช้บทบาท LLM หรือบทบาท Editor/Viewer เพื่อขอบทบาทที่คุณต้องการให้ จากนั้นค้นหาบทบาทโดยตรงและเลือกสิทธิ์ที่คุณต้องการ ตัวอย่างเช่นฉันพบว่ามัน มี อนุญาตให้ฉันเรียก Gemini Vertex AI User aiplatform.endpoints.predict หากคุณสับสนกับอนุญาตคุณจะได้รับ 403 Forbidden จาก Google Cloud เพิ่มอนุญาตไปยังไฟล์ YAML และอัปเดตบทบาท ช่วยให้บริการที่หายไป You can enable missing services by executing: gcloud services enable <service-api>.googleapis.com Attaching the role to the service account gcloud iam service-accounts add-iam-policy-binding \ "$SERVICE_ACCOUNT" \ --project="$PROJECT_ID" \ --role="projects/$PROJECT_ID/roles/$ROLE_NAME" \ --member="serviceAccount:$SERVICE_ACCOUNT" gcloud iam service-accounts add-iam-policy-binding \ "hetzner@spiritual-slate-445211-i1.iam.gserviceaccount.com" \ --project="spiritual-slate-445211-i1" \ --role="projects/spiritual-slate-445211-i1/roles/hetzner_role" \ --member="serviceAccount:hetzner@spiritual-slate-445211-i1.iam.gserviceaccount.comT" Connecting the Service Account to Workload Identity Federation and allowing Service Account Impersonation Independent of the two steps above, we attach the workloadIdentityUser role directly to the user. (You can opt to add the permissions directly instead) gcloud iam service-accounts add-iam-policy-binding \ "$SERVICE_ACCOUNT" \ --project="$PROJECT_ID" \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/*" ซึ่งจะกลายเป็น: gcloud iam service-accounts add-iam-policy-binding \ hetzner \ --project="spiritual-slate-445211-i1" \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/1016670781645/locations/global/workloadIdentityPools/hetzner-pool/*" ภายใน VPS - Docker Compose ในขณะที่ฉันแนะนําให้คุณมีเซิร์ฟเวอร์แยกต่างหากสําหรับผู้ให้บริการ OpenID (Dex) ฉันตัดสินใจที่จะโฮสต์แอปพลิเคชันของฉันและ Dex ในเซิร์ฟเวอร์เดียวเพื่อลดต้นทุน ในขณะที่ฉันแนะนําให้คุณมีเซิร์ฟเวอร์แยกต่างหากสําหรับผู้ให้บริการ OpenID (Dex) ฉันตัดสินใจที่จะโฮสต์แอปพลิเคชันของฉันและ Dex ในเซิร์ฟเวอร์เดียวเพื่อลดต้นทุน ตอนนี้เราจะย้ายไปยัง VPS (ตัวอย่าง: Hetzner) เราจะ git pull / หรือ shell คัดลอกสคริปต์ของเราไปยังเซิร์ฟเวอร์ (Docker compose และบางไฟล์ shell) ซึ่งจะช่วยให้เรา: Bootstrap the Dex service and set up the IDP. ตรวจสอบความถูกต้องกับ Artifact Registry ด้วย Docker Setup a TLS certificate for our HTTPS domain. กําหนดค่า Nginx ด้วยการใช้งาน zero ไปยังแอป Remix/React-Router Docker ประกอบด้วย: services: # Certificate management (initial + renewal) certbot: image: certbot/dns-cloudflare restart: unless-stopped volumes: # Map certbot data directly to avoid double nesting - ./certbot/conf:/etc/letsencrypt - ./certbot/www:/var/www/certbot - ./certbot/logs:/var/log/letsencrypt - ./certbot-scripts/certbot-manager.sh:/certbot-manager.sh:ro - /run/secrets/cloudflare.ini:/etc/cloudflare/cloudflare.ini:ro environment: - EMAIL=${CERTBOT_EMAIL:-anyemail@mydomain.com} entrypoint: ['/bin/sh', '/certbot-manager.sh'] networks: - shared-network nginx: image: nginx:stable-alpine restart: unless-stopped ports: - '80:80' # HTTP for certbot challenges and redirects - '443:443' # HTTPS for auth.mydomain.com volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./certbot/www:/var/www/certbot:ro - ./certbot/conf:/etc/letsencrypt:ro depends_on: - dex networks: - shared-network # Dex OIDC server dex: build: context: ./dex dockerfile: Dockerfile restart: unless-stopped networks: - shared-network volumes: - ./dex:/etc/dex - ./dex/data:/data - ./dex/entrypoint.sh:/entrypoint.sh:ro - ./dex/config.yaml:/etc/dex/config.yaml:ro environment: - DOPPLER_TOKEN=${DOPPLER_TOKEN} - DEX_GCP_STATIC_CLIENT_ID=${DEX_GCP_STATIC_CLIENT_ID} - DEX_GCP_STATIC_CLIENT_SECRET=${DEX_GCP_STATIC_CLIENT_SECRET} - DEX_GCP_STATIC_PASSWORD_EMAIL=${DEX_GCP_STATIC_PASSWORD_EMAIL} - DEX_GCP_URL=${DEX_GCP_URL} healthcheck: test: ['CMD', 'wget', '-qO-', 'http://localhost:5556/healthz'] interval: 5s timeout: 5s retries: 3 start_period: 3s expose: - '5556' # Main application app: image: us-east1-docker.pkg.dev/myfirstproject/remix-app-artifact-repo-production/remix-app-production:latest restart: unless-stopped networks: - shared-network working_dir: /app/apps/remix-app-vite command: ['doppler', 'run', '--', 'bun', 'server.ts'] expose: - '8080' environment: - DOPPLER_TOKEN=${DOPPLER_REMIX_TOKEN} healthcheck: test: [ 'CMD', 'sh', '-c', 'test ! -f /tmp/drain && curl -f --max-time 5 http://localhost:8080/health', ] interval: 5s timeout: 10s retries: 3 start_period: 3s # Allow time for app startup labels: # Configure docker-rollout pre-stop hook for graceful shutdown - 'docker-rollout.pre-stop-hook=sleep 7' networks: shared-network: driver: bridge The docker compose is made of 5 parts: Certbot - ใช้เพื่อสร้างใบรับรอง TLS สําหรับเรา เราจําเป็นต้องปล่อยการเชื่อมต่อ HTTPS เพราะ Google จะเชื่อมต่อกับบริการของเราและตรวจสอบความถูกต้องของคําขอและเนื่องจากเป็นความปลอดภัย 101 Nginx จากนั้นจัดการใบรับรอง ฉันใช้ Cloudflare เป็นตัวแทนผู้ให้บริการ รุ่นนี้ของ Certbot เชื่อมต่อกับบัญชี Cloudflare ของเราจัดการการตรวจสอบ DNS สําหรับเราและสร้างใบรับรองโดยอัตโนมัติ Will consume the TLS certificate for both: Dex (IDP), and the React Router app. It will also serve as a load balancer when I’m trying to perform zero-downtime deployments with . Nginx - Docker Rollout - The star of the show. It will create the Identity Provider that will issue our OIDC tokens that will be used to exchange for short-lived credentials to impersonate the service account. The beastly characteristic of Dex is that it is configured with a single YAML file. Dex App - NodeJS Remix / React Router docker รูปภาพที่โฮสต์ใน Artifact Registry นี้ถูกสร้างขึ้นในท่อ CI / CD แล้วกดมัน (นอกเขตของบทเรียนนี้) รูปภาพ Docker ที่โฮสต์ใน Artifact Registry ซึ่งจะกําหนดค่าในภายหลังเพื่อดึงมันจาก Google โดยใช้ Workload Identity Federation และ OIDC หนึ่งนี้มีการตรวจสอบสุขภาพที่ใช้โดยการเปิดตัว Docker เพื่อฆ่าภาชนะเก่า (ดูด้านล่าง) - won’t cut it. We need to make sure certificates are issued first, and it was easier for me to handle the orchestration with a shell script. Some kung-fu was required to have Nginx up and running (As Dex needed to be booted up first before the app) The startup shell script docker compose up Docker Rollout (A bit outside scope, but I wanted to include it anyway) มันเป็นสคริปต์ไฟล์เดียวที่สร้างขึ้นโดย Wowu ซึ่งช่วยให้เรามีการอัปเดตคอนเทนเนอร์ Docker ปิดเวลาโดยไม่จําเป็นต้องใช้ Kubernetes https://github.com/Wowu/docker-rollout To install, execute in the VPS the following: # Create directory for Docker cli plugins mkdir -p ~/.docker/cli-plugins # Download docker-rollout script to Docker cli plugins directory curl https://raw.githubusercontent.com/wowu/docker-rollout/main/docker-rollout -o ~/.docker/cli-plugins/docker-rollout # Make the script executable chmod +x ~/.docker/cli-plugins/docker-rollout การใช้มันเป็นเรื่องง่ายมาก ในทุกสถานที่ที่เราจะใช้ (สังเกตเห็นซิงค์ใหม่) เราจะใช้ มันแทน docker compose docker rollout To deploy an updated image, you ภาพใหม่จาก Registry Artifact และดําเนินการ มันจะเปลี่ยนภาพโดยอัตโนมัติโดยไม่มีเวลาหยุดทํางาน docker compose pull app docker rollout บริการของคุณไม่สามารถมี container_name และพอร์ตที่กําหนดไว้ใน docker-compose.yml เนื่องจากไม่สามารถเรียกใช้ภาชนะหลายภาชนะที่มีชื่อเดียวกันหรือการทําแผนที่พอร์ตได้ บริการของคุณไม่สามารถ และ อธิบายใน เนื่องจากไม่สามารถเรียกใช้คอนเทนเนอร์หลายคอนเทนเนอร์ที่มีชื่อเดียวกันหรือการทําแผนที่พอร์ตได้ container_name ports docker-compose.yml How does it work? Docker rollout will spin up a new instance of the app (That’s why we don’t use a ) ในขณะที่รักษาที่เก่า intact เมื่อบริการใหม่กําลังทํางานแล้วจะเพิ่มไฟล์ sentinel ว่างเปล่า (อาจจะเป็นการโทร Endpoint) ที่เรียกว่า in the temporary directory Docker container: . container_name drain ภายใน /tmp/drain สิ่งนี้บังคับให้การตรวจสอบสุขภาพล้มเหลวและสัญญาณการเปิดตัว Docker เพื่อฆ่าคนเก่าและรักษาคนใหม่อยู่ Part of the magic is done via Nginx. It automatically load balances the upstream servers (there will be two app instances when making the switch), and once it detects one failing with a health check, it will stop serving it and redirect to the new one. ในแอปพลิเคชันอื่น ๆ ที่ไม่ได้วางไว้ด้านหน้าของเครื่องสมดุลโหลดเช่น Temporal.io worker (นอกเหนือจากวัตถุประสงค์ของบทเรียนนี้) คุณจะต้องเพิ่มกลไกเพื่อระบายน้ําคนงานและปล่อยให้มันทํางานโดยอัตโนมัติ ทําการเปลี่ยนแปลง docker rollout Doppler I stated that Doppler is a service responsible for handling all our environment variables. It’s straightforward to get started: You create an account with them, install the CLI, and add your environments to their service. คุณจะเห็นด้านล่างว่า Dex จะขอให้ hash bcrypt แฮชเหล่านี้มีสัญญาณดอลลาร์ ($) ซึ่งต้องหลีกเลี่ยงใน bash อย่าหลีกเลี่ยงพวกเขาใน Doppler เราจะจัดการพวกเขาโดยใช้กลไกการแทนที่ไฟล์แทน นี้ใช้เวลาฉัน 7 ชั่วโมงที่แข็งแกร่งที่จะได้รับถูกต้อง คุณจะเห็นด้านล่างว่า Dex จะขอให้ hash bcrypt แฮชเหล่านี้มีสัญญาณดอลลาร์ ($) ซึ่งต้องหลีกเลี่ยงใน bash อย่าหลีกเลี่ยงพวกเขาใน Doppler เราจะจัดการพวกเขาโดยใช้กลไกการแทนที่ไฟล์แทน นี้ใช้เวลาฉัน 7 ชั่วโมงที่แข็งแกร่งที่จะได้รับถูกต้อง CLI การติดตั้ง คุณจะต้องติดตั้ง Doppler ใน VPS หรือเซิร์ฟเวอร์ที่คุณใช้ในขณะนี้ คุณสามารถติดตั้งได้โดยใช้คําสั่งต่อไปนี้ curl -Ls --tlsv1.2 --proto "=https" --retry 3 https://cli.doppler.com/install.sh | sudo sh Generate a Doppler Token to connect to the Doppler Service การกําหนดค่า Dex Dex is a “Federated OpenID Connect Provider”. In other words, it acts as a middleman for connecting to Identity Providers (Think like these: Sign In With Google, Sign In With GiTHub). OpenID is an authentication protocol that is based on OAuth 2.0, which allows apps and users to get user profile information with a REST-like api. It is provided as a Docker image, and we will configure it using Docker Compose. What I love about Dex is its simplicity. You only need a single YAML file that will hold the entire configuration. It will do wonders with a few lines of code. สร้าง a file and place it in your VPS in (Create the directory with ) config.yaml ~/dex/config.yaml mkdir ~/dex # dex/config.yaml - Configuration for Google Cloud Workload Identity Federation issuer: $DEX_GCP_URL storage: type: sqlite3 config: file: /var/dex/dex.db web: # Listen on HTTP, assuming a reverse proxy handles TLS termination. http: 0.0.0.0:5556 # Enable the password database to allow authentication with static passwords. enablePasswordDB: true # Define a static user for authentication. # This user will be used to exchange a Dex ID token for a GCP token. staticPasswords: - email: $DEX_GCP_STATIC_PASSWORD_EMAIL hash: $DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED username: $DEX_GCP_STATIC_PASSWORD_EMAIL userID: '722ba69a-3cba-4007-8a24-2611d4c4d5f9' # The `staticClients` list contains OAuth2 clients that can connect to Dex. staticClients: # This is the client for Google Cloud Workload Identity Federation. # The `id` MUST be the full resource name of the GCP Workload Identity Provider. # This value will be the `aud` (audience) claim in the OIDC token. - id: $DEX_GCP_STATIC_CLIENT_ID secret: $DEX_GCP_STATIC_CLIENT_SECRET name: 'Google Cloud Workload Identity Federation' # Redirect URIs are not used in the token-exchange flow. redirectURIs: [] # This section configures OAuth2 behavior. oauth2: # Use the built-in password database as the connector for the password grant type. # This allows the static user defined above to authenticate. passwordConnector: local # By default, Dex supports the necessary grant types, including 'token-exchange' # and the response types 'code', 'token', and 'id_token'. # Explicitly defining them is not necessary unless you need to restrict them. skipApprovalScreen: true If we replace it with our environments, we get: # dex/config.yaml - Configuration for Google Cloud Workload Identity Federation issuer: https://auth.yourdomain.com storage: type: sqlite3 config: file: /var/dex/dex.db web: # Listen on HTTP, assuming a reverse proxy handles TLS termination. http: 0.0.0.0:5556 # Enable the password database to allow authentication with static passwords. enablePasswordDB: true # Define a static user for authentication. # This user will be used to exchange a Dex ID token for a GCP token. staticPasswords: - email: auth@mydomain.com hash: $2y$10$k2IZomh1UUyDlNrsuoNiFuNlOIn5Siw738AdFA6ukcMu07H0uGQ7K username: auth@mydomain.com userID: '722ba69a-3cba-4007-8a24-2611d4c4d5f9' # The `staticClients` list contains OAuth2 clients that can connect to Dex. staticClients: # This is the client for Google Cloud Workload Identity Federation. # The `id` MUST be the full resource name of the GCP Workload Identity Provider. # This value will be the `aud` (audience) claim in the OIDC token. - id: //iam.googleapis.com/projects/1016670781645/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider secret: 28XhU2xcgQnusLRlG4nZlUZbFdn3lfof21jvvbTG970 name: 'Google Cloud Workload Identity Federation' # Redirect URIs are not used in the token-exchange flow. redirectURIs: [] # This section configures OAuth2 behavior. oauth2: # Use the built-in password database as the connector for the password grant type. # This allows the static user defined above to authenticate. passwordConnector: local # By default, Dex supports the necessary grant types, including 'token-exchange' # and the response types 'code', 'token', and 'id_token'. # Explicitly defining them is not necessary unless you need to restrict them. skipApprovalScreen: true Here’s the breakdown: The Issuer issuer: $DEX_GCP_URL It’s the final URL that you will authenticate against. Define a path or subdomain that will be unique (You will need to update your namespace to match this - We’ll see this later on) กล่าวอีกนัยหนึ่งมันจะกลายเป็นสิ่งนี้: issuer: https://auth.mydomain.com การจัดเก็บ storage: type: sqlite3 config: file: /var/dex/dex.db Dex needs to store its state so it can handle refresh tokens, invalidate current JWTs, and more. Fortunately, we can keep it simple by using SQLite, which it will generate for us. About TLS certificates Dex ไม่จัดการการกําหนดค่า TLS, Nginx เป็น (คุณจะเห็นในการกําหนดค่า) In other deployments, you can let Dex handle the certificates directly by pointing it to the cert files. However, that will be outside the scope of this tutorial. The Web port web: # Listen on HTTP, assuming a reverse proxy handles TLS termination. http: 0.0.0.0:5556 นี่คือพอร์ตที่ถูกเปิดเผยภายในภาชนะ Docker เราจะเห็นในภายหลังเกี่ยวกับวิธีการที่เราจะทําแผนที่ไปยังภาชนะ Nginx ของเรา Generating a static password : enablePasswordDB: true # Define a static user for authentication. # This user will be used to exchange a Dex ID token for a GCP token. staticPasswords: - email: $DEX_GCP_STATIC_PASSWORD_EMAIL hash: $DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED username: $DEX_GCP_STATIC_PASSWORD_EMAIL userID: '722ba69a-3cba-4007-8a24-2611d4c4d5f9' # The `staticClients` list contains OAuth2 clients that can connect to Dex. staticClients: # This is the client for Google Cloud Workload Identity Federation. # The `id` MUST be the full resource name of the GCP Workload Identity Provider. # This value will be the `aud` (audience) claim in the OIDC token. - id: $DEX_GCP_STATIC_CLIENT_ID secret: $DEX_GCP_STATIC_CLIENT_SECRET name: 'Google Cloud Workload Identity Federation' # Redirect URIs are not used in the token-exchange flow. redirectURIs: [] # This section configures OAuth2 behavior. oauth2: # Use the built-in password database as the connector for the password grant type. # This allows the static user defined above to authenticate. passwordConnector: local # By default, Dex supports the necessary grant types, including 'token-exchange' # and the response types 'code', 'token', and 'id_token'. # Explicitly defining them is not necessary unless you need to restrict them. skipApprovalScreen: true This will allow us to provide a username and password to authenticate against Dex, which will generate the JWT for us to authenticate against GCP. This is also used in other scenarios to authenticate against other providers on your behalf. This means that if I provide the username and password, and I have an OAuth connection with GitHub, Microsoft, or Google, it will connect to those services on our behalf and return a token that can connect to those services. But again, this is outside the scope of the tutorial นี่หมายความว่าถ้าฉันให้ชื่อผู้ใช้และรหัสผ่านและฉันมีการเชื่อมต่อ OAuth กับ GitHub, Microsoft หรือ Google มันจะเชื่อมต่อกับบริการเหล่านั้นในนามของเราและกลับ token ที่สามารถเชื่อมต่อกับบริการเหล่านั้น แต่อีกครั้งนี่คือนอกเขตของบทเรียน staticClients: id - ส่วนมหัศจรรย์ นี่เป็นสิ่งสําคัญ สิ่งนี้จะระบุพื้นที่ผู้ชมของ JWT ที่สร้างขึ้น Google จะเปรียบเทียบกับสิ่งนี้และยืนยันคําขอ! ฉันไม่สามารถบอกคุณว่าฉันใช้เวลาเท่าไหร่ในการได้รับสิทธินี้ นี่เป็นสิ่งสําคัญ สิ่งนี้จะระบุพื้นที่ผู้ชมของ JWT ที่สร้างขึ้น Google จะเปรียบเทียบกับสิ่งนี้และยืนยันคําขอ! ฉันไม่สามารถบอกคุณว่าฉันใช้เวลาเท่าไหร่ในการได้รับสิทธินี้ We need to provide a 2 ของเรา : full URI hetzner-provider นาฬิกา //iam.googleapis.com/projects/1016670781645/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider หมายเหตุ เราเริ่มต้นด้วย: // หมายเหตุ เราเริ่มต้นด้วย: // : password Use your password manager and generate a strong password. using any , so escaping isn’t an issue. This will be used to protect your API from anyone accessing your service! Avoid dollar signs a-strong-password-generated-using-a-password-generator หลีกเลี่ยง สัญญาณดอลลาร์ You will pass this password when sending an HTTP payload to Dex later on! You pass to Dex the hashed string of the password above in the field. staticPasswords.password StaticPasswords : the auth@mydomain.com → This can be anything. Since this will not be propagated upward in the OpenID chain (i.e., used by another provider), it is only used to authenticate against the service. So you can provide something non-existent email: : a random UUID. You can use ULIDs or anything. This field isn’t used in our case. userID It’s a bcrypt hash that you generate using: password: htpasswd -bnBC 10 "" <-the-password-for-reference-> แพทย์ htpasswd -bnBC 10 "" a-strong-password-generated-using-a-password-generator It generates: $2y$10$s4ETxkQQeuJu4Kp58O607u.wiqlHnkyV8LkFK1g4cMKGFU959uusq (ทราบว่ามันเริ่มต้นด้วยสัญญาณดอลลาร์) รหัสผ่าน bcrypt hashed จะมีสัญญาณ 3 ดอลลาร์ตามที่แสดงไว้ข้างต้น สิ่งเหล่านี้ต้องหลีกเลี่ยงอย่างถูกต้องใน bash / shell ถ้าคุณพยายามที่จะฉีดพวกเขาเป็นตัวแปรสภาพแวดล้อม ตามที่กล่าวถึงในส่วน Dex มันจะกลายเป็นปัญหา ฉันจะแสดงให้คุณเห็นวิธีการเอาชนะสิ่งนี้โดยใช้ envsubst รหัสผ่าน bcrypt hashed จะมีสัญญาณ 3 ดอลลาร์ตามที่แสดงไว้ข้างต้น สิ่งเหล่านี้ต้องหลีกเลี่ยงอย่างถูกต้องใน bash / shell ถ้าคุณพยายามที่จะฉีดพวกเขาเป็นตัวแปรสภาพแวดล้อม ตามที่กล่าวถึงในส่วน Dex มันจะกลายเป็นปัญหา ฉันจะแสดงให้คุณเห็นวิธีการเอาชนะสิ่งนี้โดยใช้ envsubst Creating an entry point for Dex: #!/bin/sh set -e echo "🔧 Dex Custom Entrypoint - Starting initialization..." # Export all current environment variables echo "📝 Exporting current environment variables..." export $(printenv | grep -v '^_' | cut -d= -f1) # Check if doppler is configured if [ -z "$DOPPLER_TOKEN" ]; then echo "❌ Error: DOPPLER_TOKEN environment variable is required" exit 1 fi echo "🔐 Fetching DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED from Doppler..." # Get the bcrypt hashed password from doppler DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED=$(doppler secrets get DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED --plain) if [ -z "$DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED" ]; then echo "❌ Error: Failed to retrieve DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED from Doppler" exit 1 fi # Export the retrieved password hash export DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED # Apply environment variable substitution to config echo "🔄 Applying environment variable substitution..." envsubst < /etc/dex/config.yaml > /tmp/config.yaml # Cleanup function cleanup() { echo "🧹 Cleaning up temporary files..." if [ -f "/tmp/config.yaml" ]; then rm -f /tmp/config.yaml echo "✅ Removed /tmp/config.yaml" fi echo "👋 Dex container shutdown complete" exit 0 } # Set up signal handlers for graceful shutdown trap cleanup SIGTERM SIGINT SIGQUIT echo "🚀 Starting Dex server..." # Start dex server with processed config in background dex serve /tmp/config.yaml & DEX_PID=$! # Wait for dex process to finish wait $DEX_PID ไฟล์นี้มีวัตถุประสงค์สามประการ Load all the environments from Doppler VPS project Load specifically the Bcrypt password hash in a plain format so it does not add any escaping. DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED=$(doppler secrets get DEX_GCP_STATIC_PASSWORD_SECRET_BCRYPT_HASHED --plain) Use and generate a temporary file which we serve in the with all of our environments replaced. This was the only working mechanism I found that worked. We remove this temporary file in the cleanup function. env_subst config.yaml dex serve /tmp/config.yaml เมื่อใช้ไฟล์ .env สคริปต์นี้ไม่จําเป็น คุณสามารถบูต Dex โดยตรง อย่างไรก็ตามการดาวน์โหลด bcrypt hashed จาก Doppler ได้กลายเป็นความยุ่งยาก โซลูชันนี้ทําให้เกิดปัญหา When using an .env file, this script isn’t needed. You can boot Dex directly. However, downloading the hashed bcrypt from Doppler became a mess. This solution nailed it. Nginx: ฉันคิดว่ามันไม่จําเป็นต้องมีการแนะนําใด ๆ NGINX เป็นเว็บเซิร์ฟเวอร์ HTTP, proxy กลับ, content cache, load balancer, TCP / UDP proxy server และ mail proxy server มันเป็นหนึ่งในกระดูกสันหลังของเว็บทั้งหมด เราจะใช้มันเป็นจุดเข้าหลักสําหรับแอปพลิเคชัน Docker หลักของเราและบริการ DEX ของเรา นอกจากนี้ยังจะรับผิดชอบในการจัดการการเชื่อมต่อ TLS สําหรับเรา อย่างไรก็ตามการกําหนดค่านี้เป็นความท้าทายบางอย่าง Nginx will not load properly unless all the services are up and running. I will show you below how we utilize an automation script that leverages two Nginx configurations, providing zero downtime. อย่างไรก็ตามการกําหนดค่านี้เป็นความท้าทายบางอย่าง Nginx จะไม่โหลดอย่างถูกต้องเว้นแต่บริการทั้งหมดจะเปิดใช้งานและทํางาน ฉันจะแสดงให้คุณด้านล่างวิธีการที่เราใช้สคริปต์อัตโนมัติที่ใช้ประโยชน์จากสองการกําหนดค่า Nginx ให้เวลาหยุดทํางาน zero เราใช้สองการกําหนดค่าเหล่านี้เพราะ NGINX จะไม่โหลดอย่างถูกต้องจนกว่าจะมีการให้บริการทั้งหมด ตามที่คุณสามารถเห็นแอปพลิเคชันหลักของเราจะถูกเก็บไว้ใน Artifact Registry ซึ่งยังต้อง DEX เพื่อรับรองความถูกต้อง เพื่อเอาชนะสิ่งนี้เรา: Load a Dex-only configuration first. Authenticate Docker against it, generating an OIDC token that authenticates in GCP. Pull the image from Artifact Registry. Provide a second configuration that includes our app, and we reload it in real-time using nginx -s reload nginx.conf.init (Initial Dex only config): user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # Basic server for returning a maintenance page server { listen 443 ssl http2 default_server; server_name _; # Catch-all ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; # Return a 503 for all requests to the main app location / { return 503 '{"status": "initializing", "message": "Application is starting up, please try again shortly."}'; add_header Content-Type 'application/json'; } } # HTTPS server for auth.yourdomain.com (dex) server { listen 443 ssl http2; server_name auth.yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://dex:5556; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; } location /healthz { access_log off; proxy_pass http://dex:5556/healthz; } } # HTTP server for redirects and ACME challenges server { listen 80; server_name www.yourdomain.com yourdomain.com auth.yourdomain.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; } } } nginx.conf.prod (Full config) # https://www.digitalocean.com/community/tutorials/how-to-upgrade-nginx-in-place-without-dropping-client-connections # https://www.f5.com/company/blog/nginx/avoiding-top-10-nginx-configuration-mistakes user nginx; # Number of CPU in the server - cat /proc/cpuinfo worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 4096; use epoll; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # Performance optimizations sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 30; keepalive_requests 1000; server_tokens off; # Buffer optimizations client_body_buffer_size 128k; client_max_body_size 50m; client_header_buffer_size 1k; large_client_header_buffers 4 4k; output_buffers 1 32k; postpone_output 1460; # Gzip compression gzip on; gzip_vary on; gzip_min_length 1024; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # Rate limiting limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m; limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m; # Dynamic upstream for app service (supports docker rollout) upstream app_server { # Use Docker's internal DNS for service discovery # This allows nginx to discover all containers for the 'app' service server app:8080 max_fails=3 fail_timeout=5s; # Configure keepalive connections # Double of the upstream block keepalive 4; keepalive_requests 1000; keepalive_timeout 60s; } # Dynamic upstream for dex service (supports docker rollout) upstream dex_server { server dex:5556 ; } # HTTP server for redirects and ACME challenges server { listen 80; server_name www.yourdomain.com yourdomain.com auth.yourdomain.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; } } # HTTPS server for yourdomain.com (main app) server { listen 443 ssl http2; server_name www.yourdomain.com yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:50m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_buffer_size 8k; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; limit_req zone=api burst=20 nodelay; # Handle static assets specifically location ~* \.(js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { proxy_pass http://app_server; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Container draining support - retry on different upstream if current fails proxy_next_upstream error timeout http_500 http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_next_upstream_timeout 10s; # Optimized timeouts for faster failover during rollouts proxy_connect_timeout 3s; proxy_send_timeout 10s; proxy_read_timeout 10s; # Prevent caching of broken responses proxy_buffering off; # Static asset headers expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; } location / { proxy_pass http://app_server; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Container draining support - retry on different upstream if current fails proxy_next_upstream error timeout http_500 http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_next_upstream_timeout 15s; # Optimized timeouts for faster failover during rollouts proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 30s; # Proxy buffering proxy_buffering on; proxy_buffer_size 64k; proxy_buffers 4 64k; proxy_busy_buffers_size 64k; } # Health check endpoints location /health { access_log off; proxy_pass http://app_server/health; # Fast failover for health checks proxy_connect_timeout 2s; proxy_send_timeout 5s; proxy_read_timeout 5s; proxy_next_upstream error timeout http_500 http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_next_upstream_timeout 5s; } } # HTTPS server for auth.yourdomain.com (dex) server { listen 443 ssl http2; server_name auth.yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:50m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_buffer_size 8k; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; limit_req zone=auth burst=5 nodelay; location / { proxy_pass http://dex_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; # Container draining support - retry on different upstream if current fails proxy_next_upstream error timeout http_500 http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_next_upstream_timeout 10s; # Optimized timeouts for faster failover during rollouts proxy_connect_timeout 5s; proxy_send_timeout 15s; proxy_read_timeout 15s; } # Health check for Dex location /healthz { access_log off; proxy_pass http://dex_server/healthz; # Fast failover for health checks proxy_connect_timeout 2s; proxy_send_timeout 5s; proxy_read_timeout 5s; proxy_next_upstream error timeout http_500 http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_next_upstream_timeout 5s; } } } โบนัส F5 เป็น บริษัท ที่อยู่เบื้องหลัง NGINX และพวกเขามีคุกกี้ที่ยอดเยี่ยมที่ช่วยให้คุณเข้าใจวิธีกําหนดค่าได้ดีขึ้น มันทํางานสําหรับทั้งรุ่นฟรีและจ่าย https://www.f5.com/content/dam/f5/corp/global/pdf/ebooks/NGINX_Cookbook-final.pdf โบนัส F5 is the company behind NGINX, and they have a fantastic cookbook that helps you better understand how to configure it. It works for both the free and the paid versions. https://www.f5.com/content/dam/f5/corp/global/pdf/ebooks/NGINX_Cookbook-final.pdf Configuring Docker to connect to Artifact Registry using Workload Identity Federation and OIDC. We now need to tell Docker to connect to GCP via Workload Identity Federation. We use a We are going to configure this manually. Credential Helper for that. นี้จะประกอบด้วย 3 ไฟล์: (This authenticates against DEX and generates a JWT) fetch-id-token.sh (It takes the JWT from fetch-id-token.sh and generates an OIDC token from Google Cloud) fetch-google-oidc-token.sh (It orchestrates the two files above, and sends the OIDC token from fetch-google-oidc-token.sh to GCP in order to impersonate the service account) docker-credential-gcr.sh อย่าคัดลอกและแทรกไฟล์เหล่านี้อย่างหยาบคายเนื่องจากหลายคนมีค่าที่เข้ารหัสอย่างหนักเช่นผู้ใช้ที่ไม่ใช่รากปัจจุบันใน VPS: localuser อย่าคัดลอกและแทรกไฟล์เหล่านี้โดยมืดเนื่องจากหลายไฟล์มีค่าที่เข้ารหัสอย่างหนักเช่นผู้ใช้ที่ไม่ใช่รากปัจจุบันใน VPS: localuser ไฟล์เหล่านี้ทั้งหมดต้องมีการเข้าถึงการดําเนินการ: chmod +x ~/scripts/fetch-id-token.sh chmod +x ~/scripts/fetch-google-oidc-token.sh chmod +x /usr/local/bin/docker-credential-gcr การกําหนดค่าผู้ช่วยการรับรองภายใน Docker: การนําทาง or create the file if it doesn’t exist, and add the following: ~/.docker/config.json { "credHelpers": { "us-east1-docker.pkg.dev": "gcr" } } This will tell Docker to use the ตัวช่วยรับรองเพื่อรับรองความถูกต้องทุกครั้งที่คุณดึงหรือกดภาพ gcr Docker then tries to execute the binary named (extensionless - e.g: ) docker-credential-<helper> docker-credential-gcr It is a naming convention used by Docker, and it finds it via $PATH when needed. สารสกัดจากสารสกัดจากเชื้อรา ( ): /usr/local/bin/docker-credential-gcr #! /bin/bash # # This file is extensionless. # Update all the /home/localuser/scripts directory with your own! # A custom Docker credential helper for Workload Identity Federation. # This script performs the full OIDC-to-Google-Access-Token exchange. # # Prerequisites: # 1. `curl` and `jq` must be installed. # 2. The custom OIDC token must be available in a file. # 3. The script must be executable (`chmod +x /path/to/this/script.sh`). # 4. This script is copied to /usr/local/bin/docker-credential-gcr (This is a file) # set -euo pipefail $PROJECT_ID="spiritual-slate-445211-i1" # Add logging to a file for debugging exec 2>> /tmp/docker-credential-gcr.log echo "$(date): Starting docker-credential-gcr" >> /tmp/docker-credential-gcr.log set -a source /home/localuser/.env set +a # Step 1: Get the ID Token echo "$(date): Getting ID token" >> /tmp/docker-credential-gcr.log if ! SUBJECT_TOKEN=$(/home/localuser/scripts/fetch-id-token.sh 2>> /tmp/docker-credential-gcr.log); then echo "$(date): ERROR - Failed to get ID token" >> /tmp/docker-credential-gcr.log exit 1 fi echo "$(date): Got ID token: ${SUBJECT_TOKEN:0:20}..." >> /tmp/docker-credential-gcr.log # Step 2: Get federated token echo "$(date): Getting federated token" >> /tmp/docker-credential-gcr.log if ! FEDERATED_ACCESS_TOKEN=$(/home/localuser/scripts/fetch-google-oidc-token.sh "$SUBJECT_TOKEN" 2>> /tmp/docker-credential-gcr.log); then echo "$(date): ERROR - Failed to get federated token" >> /tmp/docker-credential-gcr.log exit 1 fi if [ "${FEDERATED_ACCESS_TOKEN}" == "null" ]; then echo "Error: Failed to get federated token from STS. Response: ${STS_RESPONSE}" >&2 echo "{}" exit 1 fi # Check required environment variable for service account if [[ -z "${GCP_SERVICE_ACCOUNT:-}" ]]; then echo "$(date): ERROR - GCP_SERVICE_ACCOUNT_EMAIL not set" >> /tmp/docker-credential-gcr.log exit 1 fi # Step 4: Use the federated token to impersonate the Service Account. # This calls the IAM Credentials API to get a final, usable Google Cloud access token. IAM_API_URL="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/hetzner@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken" IAM_PAYLOAD='{"scope": ["https://www.googleapis.com/auth/cloud-platform"]}' IAM_RESPONSE=$(curl -s -X POST "${IAM_API_URL}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${FEDERATED_ACCESS_TOKEN}" \ -d "${IAM_PAYLOAD}") GOOGLE_ACCESS_TOKEN=$(echo "${IAM_RESPONSE}" | jq -r .accessToken) if [ "${GOOGLE_ACCESS_TOKEN}" == "null" ]; then echo "Error: Failed to get Google access token via impersonation. Response: ${IAM_RESPONSE}" >&2 echo "{}" exit 1 fi # Step 5: Output the credentials in the JSON format that Docker expects. # The "Username" must be "oauth2accesstoken". # The "Secret" is the final Google Cloud access token. cat <<EOF { "Username": "oauth2accesstoken", "Secret": "${GOOGLE_ACCESS_TOKEN}" } EOF ไฟล์นี้จัดระเบียบสองไฟล์ต่อไปนี้ นอกจากนี้ยังเพิ่มกลไกการแก้ไขปัญหาบางอย่างเพื่อ to help you aid in debugging when things fail. /tmp/docker-credential-gcr.log วอลเลนไทน์ ( - สร้าง directory ): ~/scripts/fetch-id-token.sh scripts #!/usr/bin/env bash # set -euo pipefail # Load environment variables from .env and Doppler set -a source .env 2>/dev/null || true set +a # Check if any required variables are empty if [[ -z "$DEX_GCP_URL" || -z "$DEX_GCP_STATIC_CLIENT_ID" || -z "$DEX_GCP_STATIC_CLIENT_SECRET" || -z "$DEX_GCP_STATIC_PASSWORD_EMAIL" || -z "$DEX_GCP_STATIC_PASSWORD_SECRET" ]]; then echo "Error: One or more required DEX_GCP_* environment variables are empty!" >&2 echo "Make sure DOPPLER_TOKEN is set and you have access to the required secrets." >&2 exit 1 fi BASIC_AUTH_HEADER=$(echo -n "$DEX_GCP_STATIC_CLIENT_ID:$DEX_GCP_STATIC_CLIENT_SECRET" | base64 -w 0) # Request OIDC token from Dex using the correct password grant flow response=$(curl --silent --location "$DEX_GCP_URL/token" \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header "Authorization: Basic $BASIC_AUTH_HEADER" \ --data-urlencode 'grant_type=password' \ --data-urlencode "username=$DEX_GCP_STATIC_PASSWORD_EMAIL" \ --data-urlencode "password=$DEX_GCP_STATIC_PASSWORD_SECRET" \ --data-urlencode 'scope=openid email profile groups' 2>&1) curl_exit_code=$? if [[ $curl_exit_code -ne 0 ]]; then echo "Error: Failed to call Dex API (curl exit code: $curl_exit_code)" >&2 echo "Response: $response" >&2 exit 1 fi # Check if response contains an error if echo "$response" | grep -q '"error"'; then echo "Error: Authentication failed" >&2 echo "Response: $response" >&2 exit 1 fi # Extract the id_token from the JSON response id_token=$(echo "$response" | jq -r '.id_token') # Validate we got a token if [[ -z "$id_token" || "$id_token" == "null" ]]; then echo "Error: Failed to get id_token from Dex. Response: $response" >&2 exit 1 fi # Output the token echo "$id_token" fetch-google-oidc-token ( ) ~/scripts/fetch-google-oidc-token #!/usr/bin/env bash set -euo pipefail # Check if ID_TOKEN is provided as argument if [[ $# -eq 0 ]]; then echo "Error: ID_TOKEN parameter is required" >&2 echo "Usage: $0 <ID_TOKEN>" >&2 exit 1 fi ID_TOKEN="$1" # Validate ID_TOKEN is not empty if [[ -z "$ID_TOKEN" ]]; then echo "Error: ID_TOKEN parameter cannot be empty" >&2 exit 1 fi # Check required environment variables : "${DEX_GCP_STATIC_CLIENT_ID:?DEX_GCP_STATIC_CLIENT_ID environment variable not set}" # Build the audience URL AUDIENCE="$DEX_GCP_STATIC_CLIENT_ID" # Exchange ID_TOKEN for ACCESS_TOKEN using Google STS echo "Calling Google STS with audience: $AUDIENCE" >&2 response=$(curl --silent --location 'https://sts.googleapis.com/v1/token' \ --header 'Content-Type: application/json' \ --data "{ \"grant_type\": \"urn:ietf:params:oauth:grant-type:token-exchange\", \"subject_token_type\": \"urn:ietf:params:oauth:token-type:id_token\", \"subject_token\": \"$ID_TOKEN\", \"audience\": \"$AUDIENCE\", \"requested_token_type\": \"urn:ietf:params:oauth:token-type:access_token\", \"scope\": \"https://www.googleapis.com/auth/cloud-platform\" }") curl_exit_code=$? # Check if curl succeeded if [[ $curl_exit_code -ne 0 ]]; then echo "Error: Failed to call Google STS API (curl exit code: $curl_exit_code)" >&2 echo "STS Response: $response" >&2 exit 1 fi # Extract the access_token from the JSON response access_token=$(echo "$response" | jq -r '.access_token') # Validate we got a token if [[ -z "$access_token" || "$access_token" == "null" ]]; then echo "Error: Failed to get access_token from Google STS. Response: $response" >&2 exit 1 fi # Output the access token echo "$access_token" What these files do in summary: รับ JWT จาก Dex Send the JWT to Google to generate an OIDC Token. ใช้ OIDC Token เพื่อแสดงให้เห็นว่าบัญชีบริการ ใช้ token เป็นพื้นที่รหัสผ่านของภาชนะ Docker About these files: The (Note that this file is extensionless) must be placed in: docker-credential-gcr /usr/local/bin/ The full path should be: /usr/local/bin/docker-credential-gcr This makes it accessible for Docker to authenticate against Artifact Registry In my setup, I have a non-sudo user called . I’ve added the and inside the ’s home directory: localuser fetch-id-token.sh fetch-google-oidc-token.sh localuser /home/localuser/scripts/ The dir is one I created myself. scripts You must update to match the directories!!! ( docker-credential-gcr เก็บทุกอย่างเข้าด้วยกัน - The smart-start.sh smart-start.sh ดังที่ฉันได้กล่าวถึงการจัดระเบียบนี้เป็นเรื่องยากเล็กน้อยเนื่องจากฉันโฮสติ้งทุกอย่างภายในเซิร์ฟเวอร์เดียวและกระบวนการ Nginx เท่านั้นเราต้อง (กู้คืน) Generate the TLS certificate Load Nginx with Dex. Let Docker authenticate against Dex and GCP - Pull the image from Artifact Registry Reload Nginx config เพื่อให้บริการแอป For this, we’ve created a helper which also requires executable access. smart-start.sh You can place this in the or the leading directory next to the file: /home/localuser docker-compose.yml smart-start.sh #!/usr/bin/env bash # # smart-start.sh – one-shot bootstrap for the Hetzner VPS ## # Usage examples: # DOPPLER_TOKEN="dp.st.prd.xyz" ./smart-start.sh # Full deployment # DOPPLER_TOKEN="dp.st.prd.xyz" SKIP_CERT=1 ./smart-start.sh # Skip certificates set -euo pipefail ############################################################################### # Helpers ############################################################################### c_blue='\033[0;34m'; c_green='\033[0;32m'; c_yellow='\033[1;33m'; c_red='\033[0;31m'; c_nc='\033[0m' log(){ printf "${c_blue}ℹ︎ %s${c_nc}\n" "$*"; } ok (){ printf "${c_green}✔ %s${c_nc}\n" "$*"; } warn(){ printf "${c_yellow}⚠ %s${c_nc}\n" "$*"; } die (){ printf "${c_red}✖ %s${c_nc}\n" "$*" >&2; exit 1; } ############################################################################### # Environment & sanity ############################################################################### log "Pre-flight checks" [[ $EUID -eq 0 ]] && die "Run as an unprivileged user with docker group membership." command -v docker >/dev/null || die "Docker missing." command -v doppler >/dev/null || die "Doppler CLI missing." docker compose version >/dev/null || die "Docker Compose plugin missing." docker ps >/dev/null || die "User cannot talk to the Docker socket." # Validate DOPPLER_TOKEN [[ -z "${DOPPLER_TOKEN:-}" ]] && die "DOPPLER_TOKEN environment variable is required." # Helper function to run docker commands with doppler docker_compose() { DOPPLER_TOKEN="$DOPPLER_TOKEN" doppler run -- docker compose "$@" } docker_rollout() { DOPPLER_TOKEN="$DOPPLER_TOKEN" doppler run -- docker rollout "$@" } # # Helper function to check if service is running service_running() { local service="$1" docker_compose ps --services --filter "status=running" | grep -q "^${service}$" } # Helper function to wait for service health wait_for_service_health() { local service="$1" local timeout="${2:-60}" local count=0 log "Waiting for $service health check (timeout: ${timeout}s)..." while [ $count -lt $timeout ]; do if docker_compose ps "$service" --format "table {{.Health}}" 2>/dev/null | grep -q "healthy"; then ok "$service is healthy" return 0 fi # If service is not running, try to check if it's still starting if ! service_running "$service"; then warn "$service is not running, checking if it exited..." docker_compose logs --tail=10 "$service" return 1 fi sleep 2 count=$((count + 2)) done warn "$service health check timeout after ${timeout}s" docker_compose logs --tail=20 "$service" return 1 } ok "Environment looks good." ############################################################################### # TLS (Let's Encrypt via dns-01, can be skipped) ############################################################################### if [[ ${SKIP_CERT:-0} -eq 0 ]]; then log "Phase 1 – TLS certificates" cert_live="certbot/conf/live/alertdown.ai/fullchain.pem" if [[ ! -f $cert_live ]] || ! openssl x509 -checkend 604800 -noout -in "$cert_live" >/dev/null 2>&1 then log "Obtaining / renewing wildcard cert via Cloudflare" [[ -f /run/secrets/cloudflare.ini ]] || die "Cloudflare credentials missing at /run/secrets/cloudflare.ini (chmod 600)." mkdir -p certbot/{conf,logs,www} docker run --rm \ -v "$PWD/certbot/conf:/etc/letsencrypt" \ -v "$PWD/certbot/logs:/var/log/letsencrypt" \ -v /run/secrets/cloudflare.ini:/cloudflare.ini:ro \ certbot/dns-cloudflare certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /cloudflare.ini \ --dns-cloudflare-propagation-seconds 300 \ --email "${CERTBOT_EMAIL:-tech@alertdown.ai}" --agree-tos --no-eff-email \ # We tell Certbot to generate wildcard certificates. -d mydomain.com -d "*.mydomain.com" \ --non-interactive --rsa-key-size 4096 fi ok "TLS certificate ready." else warn "SKIP_CERT=1 → skipping Let's Encrypt" fi ############################################################################### # Bring up auth services (nginx + dex) ############################################################################### log "Phase 2 – Bring up auth services (nginx + dex)" # Use the initial maintenance config first log "Switching to initial NGINX config..." cp nginx/nginx.conf.init nginx/nginx.conf # Start dex first log "Starting dex service..." docker_compose up -d dex if ! wait_for_service_health "dex" 60; then die "Dex failed to become healthy" fi # Start nginx with the init config or reload if already running if service_running "nginx"; then log "Nginx already running, reloading with maintenance config..." if ! docker_compose exec nginx nginx -t -c /etc/nginx/nginx.conf; then die "New nginx config is invalid" fi if ! docker_compose exec nginx nginx -s reload; then die "Nginx reload failed - config may be invalid" fi else # Start nginx normally docker_compose up -d nginx fi # Give nginx a moment to start and verify it's running sleep 5 if ! service_running "nginx"; then docker_compose logs nginx die "Nginx failed to start" fi # Test auth endpoint log "Testing auth endpoint availability..." for i in {1..30}; do if curl -fs "https://auth.mydomain.com/healthz" >/dev/null 2>&1 || \ curl -fs "$DEX_GCP_URL/healthz" >/dev/null 2>&1; then ok "Auth endpoint is accessible" break fi sleep 2 [[ $i -eq 30 ]] && warn "Auth endpoint not yet accessible (may still be starting)" done ############################################################################### # Pull and start app services ############################################################################### log "Phase 3 – Pull and start app service" log "Pulling app image..." if ! docker_compose pull app; then warn "App image pull failed (check WIF setup) – continuing with cached image if available." fi log "Starting app service..." docker_rollout app if ! wait_for_service_health "app" 180; then warn "App service health check failed. The app may not be accessible." # Even if health check fails, we proceed to switch nginx config fi # Switch to the final production nginx config log "Switching to production NGINX config..." cp nginx/nginx.conf.prod nginx/nginx.conf # Reload nginx to apply the new configuration log "Reloading nginx with production config..." if docker_compose exec nginx nginx -s reload; then ok "Nginx reloaded successfully with production config." else warn "Nginx reload failed. Check logs." docker_compose logs nginx fi # Spin up certbot manager to listen for certbot renewals log "Phase 4 – Starting certbot manager" docker_compose up -d certbot ############################################################################### # Health probes ############################################################################### log "Comprehensive health probes" # Internal service health checks declare -A internal_services=( ["dex"]="dex" ["app"]="app" ) for name in "${!internal_services[@]}"; do service="${internal_services[$name]}" if service_running "$service"; then if docker_compose ps "$service" --format "table {{.Health}}" 2>/dev/null | grep -q "healthy"; then ok "Internal $name service ✓" else warn "Internal $name service ✗ (not healthy)" fi else warn "Internal $name service ✗ (not running)" fi done # External endpoint health checks declare -A external_probes=( ["Auth (HTTPS)"]="curl -fs --connect-timeout 10 https://auth.alertdown.ai/healthz" ["App (HTTPS)"]="curl -fs --connect-timeout 10 https://alertdown.ai/health" ["App (HTTPS/nginx-health)"]="curl -fs --connect-timeout 10 https://alertdown.ai/nginx-health" ["HTTP redirect"]="curl -fs --connect-timeout 5 http://alertdown.ai/ | grep -q '301'" ) for name in "${!external_probes[@]}"; do if timeout 15 bash -c "${external_probes[$name]}" >/dev/null 2>&1; then ok "External $name ✓" else warn "External $name ✗" fi done # Service status summary echo log "Service Status Summary:" docker_compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Health}}" echo -e "\n${c_green}🚀 AlertDown stack deployment complete:" echo -e "${c_green} Auth: https://auth.alertdown.ai${c_nc}" echo -e "${c_green} App: https://alertdown.ai${c_nc}" if ! service_running "app" || ! docker_compose ps "app" --format "table {{.Health}}" 2>/dev/null | grep -q "healthy"; then echo -e "${c_yellow} Note: App service may still be starting up${c_nc}" fi ดําเนินการสคริปต์: เพื่อเรียกใช้สิ่งนี้คุณ: DOPPLER_TOKEN=<insert-doppler-vps-token-here> bash smart-start.sh E.g: DOPPLER_TOKEN=dp.stasdasdsad22OmPSi bash smart-start.sh Breaking the script apart: #!/usr/bin/env bash # # smart-start.sh – one-shot bootstrap for the Hetzner VPS ## # Usage examples: # DOPPLER_TOKEN="dp.st.prd.xyz" ./smart-start.sh # Full deployment # DOPPLER_TOKEN="dp.st.prd.xyz" SKIP_CERT=1 ./smart-start.sh # Skip certificates set -euo pipefail Selects bash, shows examples, and enables strict mode: exit on error, fail on unset vars, catch pipe failures. -e -u -o pipefail 1) helper: colors + logger functions 1) helper: colors + logger functions c_blue='\033[0;34m'; c_green='\033[0;32m'; c_yellow='\033[1;33m'; c_red='\033[0;31m'; c_nc='\033[0m' log(){ printf "${c_blue}ℹ︎ %s${c_nc}\n" "$*"; } ok (){ printf "${c_green}✔ %s${c_nc}\n" "$*"; } warn(){ printf "${c_yellow}⚠ %s${c_nc}\n" "$*"; } die (){ printf "${c_red}✖ %s${c_nc}\n" "$*" >&2; exit 1; } ตัวช่วยการส่งออกสีสําหรับ info / ok / warn / error ( also exits). die 2) environment & sanity checks 2) environment & sanity checks log "Pre-flight checks" [[ $EUID -eq 0 ]] && die "Run as an unprivileged user with docker group membership." command -v docker >/dev/null || die "Docker missing." command -v doppler >/dev/null || die "Doppler CLI missing." docker compose version >/dev/null || die "Docker Compose plugin missing." docker ps >/dev/null || die "User cannot talk to the Docker socket." # Validate DOPPLER_TOKEN [[ -z "${DOPPLER_TOKEN:-}" ]] && die "DOPPLER_TOKEN environment variable is required." Ensures: not root (expects user in group), docker , , docker compose plugin available, docker doppler The user can reach the Docker socket, is set. DOPPLER_TOKEN 3) Doppler-wrapped Docker and service helpers 3) Doppler-wrapped Docker and service helpers docker_compose() { DOPPLER_TOKEN="$DOPPLER_TOKEN" doppler run -- docker compose "$@" } docker_rollout() { DOPPLER_TOKEN="$DOPPLER_TOKEN" doppler run -- docker rollout "$@" } service_running() { local service="$1" docker_compose ps --services --filter "status=running" | grep -q "^${service}$" } wait_for_service_health() { local service="$1" local timeout="${2:-60}" local count=0 log "Waiting for $service health check (timeout: ${timeout}s)..." while [ $count -lt $timeout ]; do if docker_compose ps "$service" --format "table {{.Health}}" 2>/dev/null | grep -q "healthy"; then ok "$service is healthy" return 0 fi if ! service_running "$service"; then warn "$service is not running, checking if it exited..." docker_compose logs --tail=10 "$service" return 1 fi sleep 2 count=$((count + 2)) done warn "$service health check timeout after ${timeout}s" docker_compose logs --tail=20 "$service" return 1 } ok "Environment looks good." มัน: / : Runs Docker commands with env injected by Doppler. docker_compose docker_rollout : Checks if a service is in “running” state. service_running wait_for_service_health Polls Docker Compose health for a service with a timeout; logs on failure. 4) TLS via Let’s Encrypt (dns-01) — skippable with SKIP_CERT=1 4) TLS ผ่าน Let's Encrypt (dns-01) – สามารถลบได้ด้วย SKIP_CERT=1 if [[ ${SKIP_CERT:-0} -eq 0 ]]; then log "Phase 1 – TLS certificates" cert_live="certbot/conf/live/alertdown.ai/fullchain.pem" if [[ ! -f $cert_live ]] || ! openssl x509 -checkend 604800 -noout -in "$cert_live" >/dev/null 2>&1 then log "Obtaining / renewing wildcard cert via Cloudflare" [[ -f /run/secrets/cloudflare.ini ]] || die "Cloudflare credentials missing at /run/secrets/cloudflare.ini (chmod 600)." mkdir -p certbot/{conf,logs,www} docker run --rm \ -v "$PWD/certbot/conf:/etc/letsencrypt" \ -v "$PWD/certbot/logs:/var/log/letsencrypt" \ -v /run/secrets/cloudflare.ini:/cloudflare.ini:ro \ certbot/dns-cloudflare certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /cloudflare.ini \ --dns-cloudflare-propagation-seconds 300 \ --email "${CERTBOT_EMAIL:-tech@alertdown.ai}" --agree-tos --no-eff-email \ # We tell Certbot to generate wildcard certificates. -d mydomain.com -d "*.mydomain.com" \ --non-interactive --rsa-key-size 4096 fi ok "TLS certificate ready." else warn "SKIP_CERT=1 → skipping Let's Encrypt" fi If not skipping: checks if cert exists and is valid for ≥7 days. alertdown.ai uses to issue/renew wildcard certs via dns-01 with Cloudflare creds at . certbot/dns-cloudflare /run/secrets/cloudflare.ini stores certs under . certbot/conf หมายเหตุ: Domains here are mydomain.com แต่ cert path ใช้ alertdown.ai ตรวจสอบให้แน่ใจว่าเหล่านี้ตรงกับโดเมนจริงของคุณ 5) Phase 2 — Bring up the auth stack (Nginx + Dex) 5) Phase 2 — Bring up the auth stack (Nginx + Dex) log "Phase 2 – Bring up auth services (nginx + dex)" log "Switching to initial NGINX config..." cp nginx/nginx.conf.init nginx/nginx.conf log "Starting dex service..." docker_compose up -d dex if ! wait_for_service_health "dex" 60; then die "Dex failed to become healthy" fi if service_running "nginx"; then log "Nginx already running, reloading with maintenance config..." if ! docker_compose exec nginx nginx -t -c /etc/nginx/nginx.conf; then die "New nginx config is invalid" fi if ! docker_compose exec nginx nginx -s reload; then die "Nginx reload failed - config may be invalid" fi else docker_compose up -d nginx fi sleep 5 if ! service_running "nginx"; then docker_compose logs nginx die "Nginx failed to start" fi log "Testing auth endpoint availability..." for i in {1..30}; do if curl -fs "https://auth.mydomain.com/healthz" >/dev/null 2>&1 || \ curl -fs "$DEX_GCP_URL/healthz" >/dev/null 2>&1; then ok "Auth endpoint is accessible" break fi sleep 2 [[ $i -eq 30 ]] && warn "Auth endpoint not yet accessible (may still be starting)" done It: เปลี่ยน nginx ไปยังการบํารุงรักษา / การกําหนดค่าเริ่มต้น Starts , waits until container health = healthy. dex ตรวจสอบให้แน่ใจว่า nginx อยู่ใน; ตรวจสอบการกําหนดค่าและการโหลดใหม่หากมีการทํางานแล้ว ทดสอบสุขภาพ auth ที่ https://auth.mydomain.com/healthz หรือ $DEX_GCP_URL/healthz again vs your actual domain; unify. Note: mydomain.com 6) Phase 3 — Pull & start app service, then switch nginx to prod log "Phase 3 – Pull and start app service" log "Pulling app image..." if ! docker_compose pull app; then warn "App image pull failed (check WIF setup) – continuing with cached image if available." fi log "Starting app service..." docker_rollout app if ! wait_for_service_health "app" 180; then warn "App service health check failed. The app may not be accessible." fi log "Switching to production NGINX config..." cp nginx/nginx.conf.prod nginx/nginx.conf log "Reloading nginx with production config..." if docker_compose exec nginx nginx -s reload; then ok "Nginx reloaded successfully with production config." else warn "Nginx reload failed. Check logs." docker_compose logs nginx fi It: การดึงภาพแอพ (แจ้งเตือนหากการดึงล้มเหลว ใช้ภาพแคช) Deploys app via (zero-downtime rollout tool). docker rollout Waits up to 180s for app health. สวิทช์ nginx ไปยังการกําหนดค่าการผลิตและโหลดใหม่ 7) Phase 4 — start certbot manager (for renew hooks) 7) Phase 4 — start certbot manager (for renew hooks) log "Phase 4 – Starting certbot manager" docker_compose up -d certbot 8) Comprehensive health probes + summary 8) Comprehensive health probes + summary log "Comprehensive health probes" declare -A internal_services=( ["dex"]="dex" ["app"]="app" ) for name in "${!internal_services[@]}"; do service="${internal_services[$name]}" if service_running "$service"; then if docker_compose ps "$service" --format "table {{.Health}}" 2>/dev/null | grep -q "healthy"; then ok "Internal $name service ✓" else warn "Internal $name service ✗ (not healthy)" fi else warn "Internal $name service ✗ (not running)" fi done declare -A external_probes=( ["Auth (HTTPS)"]="curl -fs --connect-timeout 10 https://auth.alertdown.ai/healthz" ["App (HTTPS)"]="curl -fs --connect-timeout 10 https://alertdown.ai/health" ["App (HTTPS/nginx-health)"]="curl -fs --connect-timeout 10 https://alertdown.ai/nginx-health" ["HTTP redirect"]="curl -fs --connect-timeout 5 http://alertdown.ai/ | grep -q '301'" ) for name in "${!external_probes[@]}"; do if timeout 15 bash -c "${external_probes[$name]}" >/dev/null 2>&1; then ok "External $name ✓" else warn "External $name ✗" fi done echo log "Service Status Summary:" docker_compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Health}}" echo -e "\n${c_green}🚀 AlertDown stack deployment complete:" echo -e "${c_green} Auth: https://auth.alertdown.ai${c_nc}" echo -e "${c_green} App: https://alertdown.ai${c_nc}" if ! service_running "app" || ! docker_compose ps "app" --format "table {{.Health}}" 2>/dev/null | grep -q "healthy"; then echo -e "${c_yellow} Note: App service may still be starting up${c_nc}" fi It: ตรวจสอบคอนเทนเนอร์ “ทํางาน” + “สุขภาพ” สําหรับ dex และ app hits external HTTPS health endpoints + HTTP→HTTPS redirect. prints a one-line docker compose status table and friendly success banner. บันทึกว่า app ยังคงเริ่มต้น Beware: โปรดตรวจสอบไฟล์นี้ก่อนที่จะดําเนินการ มีค่ารหัสหนักอื่น ๆ ที่คุณต้องอัปเดต Hooray 🥳 โฮมเมด With this, you’ll have ด้วยภาพ Docker ที่ถูกลบออกจาก Artifact Registry! Dex up and running Dex Up และ Running Authenticating using Postman In the case that you run into issues, you can always try using cURL or Postman to debug the token exchanges. We need to perform 3 token exchanges แด็กซ์ Google STS (Storage Transfer Service) Google IAM Credentials การรับรองความถูกต้องกับ Dex เราจะครอบคลุมเรื่องนี้ด้านล่างใน shell เราสามารถสร้างสคริปต์ auth เช่น: BASIC_AUTH_HEADER=$(echo -n "$DEX_GCP_STATIC_CLIENT_ID:$DEX_GCP_STATIC_CLIENT_SECRET" | base64 -w 0) เราจะครอบคลุมเรื่องนี้ด้านล่างใน shell เราสามารถสร้างสคริปต์ auth เช่น: BASIC_AUTH_HEADER=$(echo -n "$DEX_GCP_STATIC_CLIENT_ID:$DEX_GCP_STATIC_CLIENT_SECRET" | base64 -w 0) คําขอ fetch ที่สร้างขึ้น: const myHeaders = new Headers(); myHeaders.append("Content-Type", "application/x-www-form-urlencoded"); myHeaders.append("Authorization", "Basic Ly9pYW0uZ29vZ2xlYXBpcy5jb20vcHJvamVjdHMvMTA4MzM1MDgwNzQ5OC9sb2NhdGlvbnMvZ2xvYmFsL3dvcmtsb2FkSWRlbnRpdHlQb29scy9oZXR6bmVyLXBvb2wvcHJvdmlkZXJzL2hldHpuZXItcHJvdmlkZXI6YS1zdHJvbmctcGFzc3dvcmQtZ2VuZXJhdGVkLXVzaW5nLWEtcGFzc3dvcmQtZ2VuZXJhdG9y"); const urlencoded = new URLSearchParams(); urlencoded.append("grant_type", "password"); urlencoded.append("username", "auth@mydomain.com"); urlencoded.append("password", "pleaseuseastrongerpassword"); urlencoded.append("scope", "openid email profile groups"); const requestOptions = { method: "POST", headers: myHeaders, body: urlencoded, redirect: "follow" }; fetch("https://auth.yourdomain.com", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); And now in CURL: curl --location 'https://auth.yourdomain.com' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic Ly9pYW0uZ29vZ2xlYXBpcy5jb20vcHJvamVjdHMvMTA4MzM1MDgwNzQ5OC9sb2NhdGlvbnMvZ2xvYmFsL3dvcmtsb2FkSWRlbnRpdHlQb29scy9oZXR6bmVyLXBvb2wvcHJvdmlkZXJzL2hldHpuZXItcHJvdmlkZXI6YS1zdHJvbmctcGFzc3dvcmQtZ2VuZXJhdGVkLXVzaW5nLWEtcGFzc3dvcmQtZ2VuZXJhdG9y' \ --data-urlencode 'grant_type=password' \ --data-urlencode 'username=auth@mydomain.com' \ --data-urlencode 'password=pleaseuseastrongerpassword' \ --data-urlencode 'scope=openid email profile groups' Authenticating against Google STS Api: With the token in hand, you need to authenticate against Google STS (Storage Transfer Service) which allows us to move data across cloud providers: : Where ALERTDOWN_AUTH_ID_TOKEN => เป็น JWT ที่มาจาก Dex ใช้แท็ก ID => AUDIENCE //iam.googleapis.com/projects/1016670781645/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider Note the scope! นี่คือคําสั่ง fetch: const myHeaders = new Headers(); myHeaders.append("Content-Type", "application/json"); const raw = JSON.stringify({ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", "subject_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE1NjdmY2MyMTVhMmQxNGY2MzVhZmY1ZjI5MjNmZDkzMTVkZWNkNzQifQ.eyJpc3MiOiJodHRwczovL2F1dGguYWxlcnRkb3duLmFpIiwic3ViIjoiQ2lRM01qSmlZVFk1WVMwelkySmhMVFF3TURjdE9HRXlOQzB5TmpFeFpEUmpOR1ExWmprU0JXeHZZMkZzIiwiYXVkIjoiLy9pYW0uZ29vZ2xlYXBpcy5jb20vcHJvamVjdHMvMTA4MzM1MDgwNzQ5OC9sb2NhdGlvbnMvZ2xvYmFsL3dvcmtsb2FkSWRlbnRpdHlQb29scy9oZXR6bmVyLXBvb2wvcHJvdmlkZXJzL2hldHpuZXItcHJvdmlkZXIiLCJleHAiOjE3NTMyNTc4NDUsImlhdCI6MTc1MzE3MTQ0NSwiYXRfaGFzaCI6IjJ5bW1mQzFfUUZaYnRibTdCLWZuS3ciLCJlbWFpbCI6ImF1dGhAYWxlcnRkb3duLmFpIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJhdXRoQGFsZXJ0ZG93bi5haSJ9.kyI61WGR5m0h4YvflYV4sCHDY0G7ix7R4m59ITvE_Bq3oIwjbH2NxAMzmtPbUp9kCcsbosAeJTcfJWj2n03-LtRZKc1WjELrFytlnSDgt1KeNCqYYWsdG5eUORzYgvfl9ayNqf7QgDPc3Sr7XQElfk07F-uJPAGPssUXY-qxos6lZHrmComzEWkWqfbuq5e-cvLsBP6TmFAt58B2XKAcSLYSuFrp8eMDaCZ7zQ12z9NR9q0N7u7cVKsJT2429I27fh6LrQsthMaaDMEKzfhY-HskbmcvYO_z4U2M1plYvXAqJRJzGGrkArPSGklxvfFS6gIqLSI7MLnzzsJKpTUt1w", "audience": "//iam.googleapis.com/projects/1083350807498/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider", "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": "https://www.googleapis.com/auth/cloud-platform", "options": "{\"serviceAccount\": \"hetzner@alertdown.iam.gserviceaccount.com\"}" }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://sts.googleapis.com/v1/token", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); นี่คือคําสั่ง cURL เต็ม: curl --location 'https://sts.googleapis.com/v1/token' \ --header 'Content-Type: application/json' \ --data-raw '{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", "subject_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE1NjdmY2MyMTVhMmQxNGY2MzVhZmY1ZjI5MjNmZDkzMTVkZWNkNzQifQ.eyJpc3MiOiJodHRwczovL2F1dGguYWxlcnRkb3duLmFpIiwic3ViIjoiQ2lRM01qSmlZVFk1WVMwelkySmhMVFF3TURjdE9HRXlOQzB5TmpFeFpEUmpOR1ExWmprU0JXeHZZMkZzIiwiYXVkIjoiLy9pYW0uZ29vZ2xlYXBpcy5jb20vcHJvamVjdHMvMTA4MzM1MDgwNzQ5OC9sb2NhdGlvbnMvZ2xvYmFsL3dvcmtsb2FkSWRlbnRpdHlQb29scy9oZXR6bmVyLXBvb2wvcHJvdmlkZXJzL2hldHpuZXItcHJvdmlkZXIiLCJleHAiOjE3NTMyNTc4NDUsImlhdCI6MTc1MzE3MTQ0NSwiYXRfaGFzaCI6IjJ5bW1mQzFfUUZaYnRibTdCLWZuS3ciLCJlbWFpbCI6ImF1dGhAYWxlcnRkb3duLmFpIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJhdXRoQGFsZXJ0ZG93bi5haSJ9.kyI61WGR5m0h4YvflYV4sCHDY0G7ix7R4m59ITvE_Bq3oIwjbH2NxAMzmtPbUp9kCcsbosAeJTcfJWj2n03-LtRZKc1WjELrFytlnSDgt1KeNCqYYWsdG5eUORzYgvfl9ayNqf7QgDPc3Sr7XQElfk07F-uJPAGPssUXY-qxos6lZHrmComzEWkWqfbuq5e-cvLsBP6TmFAt58B2XKAcSLYSuFrp8eMDaCZ7zQ12z9NR9q0N7u7cVKsJT2429I27fh6LrQsthMaaDMEKzfhY-HskbmcvYO_z4U2M1plYvXAqJRJzGGrkArPSGklxvfFS6gIqLSI7MLnzzsJKpTUt1w", "audience": "//iam.googleapis.com/projects/1083350807498/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider", "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": "https://www.googleapis.com/auth/cloud-platform", "options": "{\"serviceAccount\": \"hetzner@alertdown.iam.gserviceaccount.com\"}" }' Google IAM Credentials This is the last part! With this we obtain the short lived JWT that impersonates our service account Fetch: const myHeaders = new Headers(); myHeaders.append("Accept", "application/json"); myHeaders.append("Content-Type", "application/json"); // The bearer is the token we receive from Google STS myHeaders.append("Authorization", "Bearer ya29.d.c0ASRK0GZhTkZi8JGyqpYLR_m7ikJXVUiOL0mOA4jtgydbRyRyNusJu0up5PGOebyIYAGZoKHkreDgaukv7DbNZm3OT6JPVB5HRW_KiqZZyUfgnQRloLrcTPOyf6bDl_yHyzEjVlOy-FYPs98RSt2C-POO3R8NZC-7jEYYzqVlimcNgt7E4QBmeSsxhXShegLCjlDcgSBOob3Kb_-Q-xeTvtkflhYo7jPFiE8ChJEqnIIxL6CzritqgOsrGB4VBNuR0BhghbrH_Sa4q7ELTMclLuRb_PuTJ1YtxW1Ia7az3ENOQSF1gxKyeubBAAvKGhtfEDJYVj3mkkPUjXYUTJHPzwV6HRNF2Rls3XWfPZpiTILbbBp1bPt45Up0-9cY6NMie9b1hL9FmtCHxJOzmxDPWWDhi9KDAsRx0OQkJKJWAtl_jNX7HbjNiMRXPF-WMqR3TQia9DEBsEqWNGSMg67uFCbxbtz0DWVjjQS9VlErmK_h1pwdwhaSMWSW5_Qz-VmZ5E2K2uPdPDMLWgdDjk01b4mVXVsDJGpIj2W-9D3C8yzV5avJcAhuWpDCjOtUTUxo5LKDvjZAFbPBaIa5-LfM54UlIvMnem2y9sgIIVxptmR1gslv0PJMgCyeYDjxlCcvQqv8W5IuFw61O7zIp3FlmU8EwcbMGdfs8PRulgLjep2jL9HhxBo8zAcz1aJgVnV0kHFPfYSTVsBUQcNHYCOBmmp_NkrgKW8Tjs1e7buzZpKz4Op6Up2rqMa69ArwasSkTiQ1cGZxM9_hEt9lt9bCjvlyns9JWcSxdBrRptk4fFXBhQyYbZQ90WW2BBxfTCopa_QKcwAiNqVgKHQZlxuGeVgXVuwpBdNqKMGT29BjuiqGlz_64fmLhPr37ivSjdt6YyCKB1S6-_sjDMFUAC73HtCyaMcF8WsNdbQDgBzvNYfZMg_YnsHkQdClj7EwOMymOna9hOZyKHbtFP89IZ852yzTUqEEGAn3WAaneiBbYQjjB1WtMtSGTTGVmggK65NG1HOjiuI8R7Ik0Dgx4kOSQQYD6fHVLcYPignZIzxm8TZhIcuBPIRKm5FHb9LD69lFtpDkJ8w0vfC2bAMz-Y15u-mla8jIkVTDi_3i1mYmJh8cCm0wwEDCBWEtNOJ5zwNFsYMHQwDso-9nc1cM6P57JXsq7g"); const raw = JSON.stringify({ "scope": [ "https://www.googleapis.com/auth/cloud-platform" ] }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/hetzner@alertdown.iam.gserviceaccount.com:generateAccessToken", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); cURL: curl --location 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/hetzner@alertdown.iam.gserviceaccount.com:generateAccessToken' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ya29.d.c0ASRK0GZhTkZi8JGyqpYLR_m7ikJXVUiOL0mOA4jtgydbRyRyNusJu0up5PGOebyIYAGZoKHkreDgaukv7DbNZm3OT6JPVB5HRW_KiqZZyUfgnQRloLrcTPOyf6bDl_yHyzEjVlOy-FYPs98RSt2C-POO3R8NZC-7jEYYzqVlimcNgt7E4QBmeSsxhXShegLCjlDcgSBOob3Kb_-Q-xeTvtkflhYo7jPFiE8ChJEqnIIxL6CzritqgOsrGB4VBNuR0BhghbrH_Sa4q7ELTMclLuRb_PuTJ1YtxW1Ia7az3ENOQSF1gxKyeubBAAvKGhtfEDJYVj3mkkPUjXYUTJHPzwV6HRNF2Rls3XWfPZpiTILbbBp1bPt45Up0-9cY6NMie9b1hL9FmtCHxJOzmxDPWWDhi9KDAsRx0OQkJKJWAtl_jNX7HbjNiMRXPF-WMqR3TQia9DEBsEqWNGSMg67uFCbxbtz0DWVjjQS9VlErmK_h1pwdwhaSMWSW5_Qz-VmZ5E2K2uPdPDMLWgdDjk01b4mVXVsDJGpIj2W-9D3C8yzV5avJcAhuWpDCjOtUTUxo5LKDvjZAFbPBaIa5-LfM54UlIvMnem2y9sgIIVxptmR1gslv0PJMgCyeYDjxlCcvQqv8W5IuFw61O7zIp3FlmU8EwcbMGdfs8PRulgLjep2jL9HhxBo8zAcz1aJgVnV0kHFPfYSTVsBUQcNHYCOBmmp_NkrgKW8Tjs1e7buzZpKz4Op6Up2rqMa69ArwasSkTiQ1cGZxM9_hEt9lt9bCjvlyns9JWcSxdBrRptk4fFXBhQyYbZQ90WW2BBxfTCopa_QKcwAiNqVgKHQZlxuGeVgXVuwpBdNqKMGT29BjuiqGlz_64fmLhPr37ivSjdt6YyCKB1S6-_sjDMFUAC73HtCyaMcF8WsNdbQDgBzvNYfZMg_YnsHkQdClj7EwOMymOna9hOZyKHbtFP89IZ852yzTUqEEGAn3WAaneiBbYQjjB1WtMtSGTTGVmggK65NG1HOjiuI8R7Ik0Dgx4kOSQQYD6fHVLcYPignZIzxm8TZhIcuBPIRKm5FHb9LD69lFtpDkJ8w0vfC2bAMz-Y15u-mla8jIkVTDi_3i1mYmJh8cCm0wwEDCBWEtNOJ5zwNFsYMHQwDso-9nc1cM6P57JXsq7g' \ --data '{ "scope": [ "https://www.googleapis.com/auth/cloud-platform" ] }' The Bearer token is the ya29.d token we receive from Google STS. Token Bearer เป็น token ya29.d ที่เราได้รับจาก Google STS การออกบัตร OIDC จาก NodeJS กระบวนการนี้ไม่ได้เป็นเรื่องยากเช่นเดียวกับของ Docker แต่ยังคงมีเส้นผมและเอกสารเล็ก ๆ น้อย ๆ Google สร้างกระแสการรับรองความถูกต้องลงใน . แต่ยังต้องมีการกําหนดค่าบางอย่าง google-auth-library นี่คือวิธีการ: 0. การกําหนดค่า OIDC (เราผ่านมันจาก Doppler): OIDC_CLIENT_ID="//iam.googleapis.com/projects/1016670781645/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider" OIDC_CLIENT_SECRET="This is the password that we pass in base64 format in the header" OIDC_ISSUER="https://auth.mydomain.com" OIDC_PASSWORD="This is the unhashed bcrypt password" OIDC_POOL_ID="hetzner-pool" OIDC_PROVIDER_ID="hetzner-provider" OIDC_USERNAME="DEX_GCP_STATIC_PASSWORD_EMAIL -> the email address we used in Dex" 1. ติดตั้งแพคเกจที่จําเป็น: google-auth-library pnpm add google-auth-library @google-cloud/vertexai 2. สร้าง Auth.ts : import { ExternalAccountClientOptions, SubjectTokenSupplier, } from 'google-auth-library'; import { GoogleOIDCFetchException } from './google.auth.exception'; export type OIDCToken = { access_token: string; token_type: 'bearer'; expires_in: number; id_token: string; }; export type OidcConfigParams = { oidcIssuer: string; oidcClientId: string; oidcClientSecret: string; oidcUsername: string; oidcPassword: string; googleProjectNumber: string; oidcPoolId: string; oidcProviderId: string; googleServiceAccount: string; }; class OidcTokenSupplier implements SubjectTokenSupplier { readonly #config: OidcConfigParams; constructor(config: OidcConfigParams) { this.#config = config; } async getSubjectToken( ): Promise<string> { // our Dex URL const tokenUrl = `${this.#config.oidcIssuer}/token`; const basicAuthHeader = Buffer.from(`${this.#config.oidcClientId}:${this.#config.oidcClientSecret}`).toString('base64'); const urlencoded = new URLSearchParams(); urlencoded.append("grant_type", "password"); urlencoded.append("username", this.#config.oidcUsername); urlencoded.append("password", this.#config.oidcPassword); urlencoded.append("scope", "openid email profile groups"); const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${basicAuthHeader}`, }, body: urlencoded, }); if (!response.ok) { throw new GoogleOIDCFetchException( `Failed to get OIDC token: ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText, body: await response.text(), } ); } const data = await response.json() as OIDCToken; return data.id_token; } } export function getOidcConfig( params: OidcConfigParams ): ExternalAccountClientOptions { const audience = `//iam.googleapis.com/projects/${params.googleProjectNumber}/locations/global/workloadIdentityPools/${params.oidcPoolId}/providers/${params.oidcProviderId}`; return { type: 'external_account', audience, subject_token_type: 'urn:ietf:params:oauth:token-type:id_token', token_url: 'https://sts.googleapis.com/v1/token', subject_token_supplier: new OidcTokenSupplier(params), service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${params.googleServiceAccount}:generateAccessToken`, scopes: ['https://www.googleapis.com/auth/cloud-platform'], }; } We inherit from the SubjectTokenSupplier, and we use it to point to our Dex configuration. โซ ฟังก์ชั่นแปลงการป้อนข้อมูลของเราเป็นรูปร่างที่ Google Auth Credentials กําลังคาดหวังจากทุกบริการแพคเกจของ Google นี้คล้ายกับการกําหนดค่า Postman ด้วยความแตกต่างที่จุดปลายทาง STS และ IAM มีการอบใน getOidcConfig 3. การกําหนดค่า Vertex AI: จากนั้นเราสามารถใช้มันใน Vertex AI: export class GoogleVertexLLM { #client: VertexAI; #projectId: string; #location: string; constructor(config: GoogleVertexLLMConstructorConfig) { this.#projectId = config.projectId; this.#location = config.location; this.#client = new VertexAI({ project: this.#projectId, location: this.#location, googleAuthOptions: { credentials: getOidcConfig(config.oidcConfig) }, }); } async generateChat<T>( systemPrompt: string ): PromiseExceptionResult<T> { const parts: Part[] = [ { text: systemPrompt, }, ]; const model = this.#client.getGenerativeModel({ model: 'gemini-2.0-flash', systemInstruction: { role: 'system', parts, }, }); const chat = model.startChat({ generationConfig: { temperature: 0, topP: 0.95, maxOutputTokens: 8192, responseMimeType: 'application/json', }, }); } } It’d then automatically perform all the token exchanges for us! เราได้ทํา Omgosh มันโกรธ มันใช้เวลานาน แต่เราสามารถเชื่อมต่อกับบริการและส่งสัญญาณหมุน! Hopefully, it won’t take you long. If there’s a part that wasn’t clear from the article, let me know! คุณสามารถหาฉันได้เสมอ and/or LinkedIn Twitter / X @javiasilis โบสถ์