Hướng dẫn này sẽ được toàn diện và dễ dàng để bạn có thể truy cập các mảnh bạn muốn trực tiếp. sẽ có một repo GitHub đi kèm (Sẽ đến sớm) mà bạn có thể sử dụng để đi qua điều này. Chỉ số : 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. Về Thiết lập tài khoản Google Cloud Platform (GCP) doanh nghiệp mang lại một số thách thức (Giống như truy cập vào Gemini thông qua Vertex AI trên một dịch vụ bên ngoài như Vercel / Digital Ocean / Hetzner). Trước đây, bạn sẽ tạo một tài khoản dịch vụ sẽ giữ các quyền cho các dịch vụ (ví dụ: Vertex AI) và tạo ra một khóa lâu dài (không có ngày hết hạn) mà bạn sẽ sử dụng để kết nối với GCP. Điều này không được khuyến cáo. Chìa khóa tài khoản dịch vụ có thể bị xâm phạm, khiến việc quay trở nên khó khăn. Google khuyên bạn nên sử dụng token ngắn hạn thông qua Workload Identity Federation và Open ID Connect để giao tiếp với các dịch vụ. Nhưng bạn tạo ra Thay vào __still use __a service account Một token ngắn hạn Một token ngắn hạn Cảnh báo là cấu hình này không phải là đơn giản. tài liệu là hiếm, và nó là một mớ hỗn độn. Nó mất tôi 1,5 tháng bán thời gian để có được nó đúng. Đừng lo lắng, Hướng dẫn này sẽ dạy bạn làm thế nào để làm điều này từng bước. Chúng tôi sẽ cấu hình một nhà cung cấp ID mở (Dex) sẽ được sử dụng để phát hành token, và sẽ kết nối Docker và ứng dụng của chúng tôi để chúng tôi có thể rút hình ảnh từ Registry Artifact và kết nối với Vertex AI, tất cả thông qua một VPS (Hetzner). sai lầm Tôi đã làm điều này với khả năng tốt nhất của tôi. cảm thấy tự do để tiếp cận và đề xuất cải tiến và hoặc sửa lỗi! Hướng dẫn này bao gồm những gì và không. Các cover: Thiết lập nhà cung cấp danh tính bên ngoài bằng Dex để xác thực với Google Cloud Kéo một hình ảnh thông qua sổ đăng ký hiện vật bằng cách sử dụng Docker và OpenID Connect/Workload Identity Federation. Phát hành mã thông báo OIDC thông qua ứng dụng NodeJS bằng cách sử dụng google-auth-library. Không bao gồm: Làm thế nào để khởi tạo máy chủ trong một VPS bên ngoài hoặc Google Cloud. Làm thế nào để tải các tập tin lên VPS của bạn Cách sử dụng IAC (Infrastructure as Code) để thiết lập Google Cloud Services. Cách thiết lập các hành động GitHub để giao tiếp với GCP. Hướng dẫn này sẽ bao gồm cách thiết lập nhà cung cấp danh tính bằng cách sử dụng Dex (Mã nguồn mở), và tạo token OIDC chống lại nó, vì vậy bạn có thể kết nối thông qua bất kỳ nhà cung cấp nào khác trên thế giới. Làm thế nào về LLMs và công cụ? (Gemini, Claude, Claude Code, Cursor, vv) Họ đã giúp Họ không biết làm thế nào để trộn logic cần thiết cho Google Workload Identity Federation với Dex. a lot Chỉ LLM đến trang này để giúp bạn thiết lập nó. định nghĩa Nền tảng Google Cloud (GCP) Giải pháp đám mây mà chúng tôi sẽ sử dụng để xác thực với OIDC. rút ngắn là GCP cho bài viết này. Workload Identity Federation (Hiệp hội nhận dạng công việc): Nó là một cách để tải công việc (các ứng dụng, dịch vụ, đường ống CI / CD, VM, container, v.v.) chạy của nhà cung cấp dịch vụ đám mây để xác thực một cách an toàn với API của nhà cung cấp đó . Bên ngoài without using long-lived service account keys Thay vì cung cấp cho ứng dụng của bạn tệp khóa tĩnh (đó là một rủi ro bảo mật đáng kể nếu bị rò rỉ), WIF cho phép nhà cung cấp điện toán đám mây tin tưởng một như GitHub Actions, GitLab, Kubernetes, hoặc bất kỳ nhà cung cấp tương thích OIDC / SAML nào. external identity provider (IdP) Nói cách khác, nó tạo ra token với các quyền cụ thể mà bạn có thể gửi an toàn thông qua tải trọng sử dụng của mình (ví dụ: Cloud Storage get, list, upload) để truy cập các dịch vụ. Nhà cung cấp Identity Nó là một trung tâm trung tâm mà một người dùng hoặc máy (mã của chúng tôi) có thể sử dụng để "đăng nhập" hoặc xác thực với một hoặc nhiều dịch vụ. trong trường hợp của chúng tôi, đó là hệ thống sẽ được sử dụng để tạo ra một token hợp lệ sẽ được trao đổi với GCP để cung cấp quyền truy cập vào các ứng dụng của chúng tôi. Bạn sẽ thấy sau đó về cách nó hoạt động chi tiết. Ví dụ về nhà cung cấp danh tính Nền tảng nhà phát triển (OIDC-native): Chúng có các điểm cuối cụ thể mà bạn có thể chuyển sang GCP và tự động tạo ra các token cần thiết. GitHub (GitHub Actions OIDC token) GitLab (CI / CD pipelines với OIDC) Bitbucket (Dây ống OIDC) Các nhà cung cấp khác: ôtô ôtô Quản trị Azure Active Directory Nền tảng Google Identity Ping danh tính Trung tâm nhận dạng AWS IAM Keycloak Dex (mà chúng ta sẽ sử dụng) Dex https://dexidp.io/ Chúng tôi cấu hình nó với tên người dùng và mật khẩu, và thông qua một điểm cuối REST, chúng tôi gọi nó bằng cách sử dụng công cụ của chúng tôi (cURL, wget, fetch/xhr/axios của JavaScript, vv) Sau đó nó sẽ trả về một JWT mà chúng tôi có thể sử dụng để giao tiếp với GCP. Why? Nó là mã nguồn mở và có thể được lưu trữ tự do. Nó chỉ yêu cầu một tập tin YAML đơn giản. Bạn không cần Kubernetes để chạy (Mặc dù nó phổ biến trong môi trường đó). Chúng tôi sẽ bao gồm cách thực hiện kết nối giữa Google Cloud và một nguồn bên ngoài (chẳng hạn như VPS) bằng cách sử dụng Google Cloud Workload Identity Federation, Dex (một nhà cung cấp danh tính), và nhiều hơn nữa. Hetzner https://www.hetzner.com/ Nó là nhà cung cấp điện toán đám mây được chọn vì tỷ lệ giá / hiệu suất ấn tượng của nó.Thêm vào đó, nó có một UI rất đơn giản và các plugin IaC (Infrastructure as Code) làm cho nó trở thành một cơn gió để thiết lập. Doppler https://www.doppler.com/ Một cái gì đó tương tự như HashiCorp Vault hoặc AWS Secrets Manager. Doppler cho phép chúng tôi lưu trữ các biến môi trường của chúng tôi trong các dự án và chi nhánh khác nhau, cho phép tất cả các thành viên trong nhóm của chúng tôi chia sẻ chúng. Ngay cả khi làm việc solo, nó có một cấp độ miễn phí rất hào phóng sẽ bao gồm tất cả các nhu cầu của bạn. tôi đã sử dụng nó từ năm ngoái để quản lý tất cả các mật khẩu của tôi cho tất cả các dịch vụ của tôi. Mặc dù hoàn toàn tùy chọn, bài đăng này giả định rằng bạn đang sử dụng Doppler để tải môi trường (Bạn có thể tải một .env bên cạnh các lệnh và bạn sẽ tốt để đi) Yêu cầu 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. Cài đặt Google CLI trên máy tính của bạn Download link cài đặt: https://cloud.google.com/sdk/docs/install Download link cài đặt: https://cloud.google.com/sdk/docs/install Nhiệm vụ đầu tiên của chúng tôi là cấu hình các quyền cần thiết và kích hoạt các dịch vụ.Chúng tôi sử dụng Google CLI Đây là những địa điểm mà bạn có thể cài đặt nó: (Lưu ý rằng các bước bổ sung được bỏ qua từ bài viết này.) Cửa sổ : Download Installer tại đây Các macOS: Cài đặt Brew (trong trường hợp bạn chưa có): /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" Cài đặt Google CLI: brew install --cask gcloud-cli Linux: (Tải xuống file) curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz Mở nó ra: tar -xf google-cloud-cli-linux-x86_64.tar.gz Tạo một dự án trong Google Cloud hoặc sử dụng một dự án hiện có Go to: https://console.cloud.google.com/ Chọn tài khoản của bạn và tiếp tục tạo một dự án mới nếu bạn chưa làm như vậy. Cài đặt Google Cloud CLI Sau khi cài đặt, mở một thiết bị đầu cuối và chạy: gcloud init Điều này sẽ bắt đầu quá trình kết nối. CLI hoạt động bằng cách liên kết tài khoản Google của bạn. Tài khoản này phải có quyền cần thiết để thực hiện các hoạt động quản trị. Nói cách khác, nếu bạn bắt đầu, hãy sử dụng tài khoản chính của bạn. CLI hoạt động bằng cách liên kết tài khoản Google của bạn. Tài khoản này phải có quyền cần thiết để thực hiện các hoạt động quản trị. Nói cách khác, nếu bạn bắt đầu, hãy sử dụng tài khoản chính của bạn. Chọn dự án bạn đã tạo. Sau khi xác thực, CLI sẽ yêu cầu bạn chọn dự án mà bạn muốn làm việc. Điều này có thể được thay đổi sau với: gcloud config set project PROJECT_ID Hoặc, bạn luôn có thể vượt qua flag to lệnh --project PROJECT_ID all the gcloud Cho phép các dịch vụ: Bạn sẽ cần phải cho phép những điều này. Trong terminal của bạn, chạy: gcloud services enable \ iamcredentials.googleapis.com \ sts.googleapis.com \ iam.googleapis.com \ cloudresourcemanager.googleapis.com Các biến môi trường Sử dụng trên toàn bộ bài viết.Thay thế chúng bằng của riêng bạn. 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" Bạn có thể nhận ID dự án và Số dự án từ trang chính.Chọn dự án của bạn từ nút ở góc trên bên trái. Bạn có thể nhận ID dự án và Số dự án từ trang chính.Chọn dự án của bạn từ nút ở góc trên bên trái. Thiết lập Workload Identity Federation Làm thế nào nó hoạt động trong GCP: Bạn tạo một nhóm danh tính sẽ có một loạt các nhà cung cấp hoặc khách hàng kết nối với nó. Những nhà cung cấp này là những người mà token Dex sẽ được sử dụng để xác thực chống lại. 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. Hãy nghĩ về nhóm như một nhóm các phương pháp xác thực có thể mà bạn cho GCP biết rằng các bên thứ 3 khác có thể xác thực chống lại. Hãy nghĩ về nhóm như một nhóm các phương pháp xác thực có thể mà bạn cho GCP biết rằng các bên thứ 3 khác có thể xác thực chống lại. Tạo Pool Run this command: 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" Ví dụ : 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" Ví dụ về 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" Tạo nhà cung cấp OIDC This will be the central connecting point with Google Cloud. This will be the entry point that validates the JWT from Dex. Hãy cẩn thận với cờ --attribute-mapping.Đây là cờ nói với GCP để so sánh các trường với JWT cho OIDC. Tôi khuyên bạn (trong khi thử nghiệm) để bắt đầu rộng và sau đó thu hẹp phạm vi quyền khi bạn trở nên hiểu biết hơn. 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. # 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" Ví dụ : # 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" Điều này sẽ thông báo cho GCP rằng chủ đề và email của JWT phải khớp với các chủ đề từ nhóm để được coi là hợp lệ. Dịch vụ Account Configuration Yes, you still need to create a service account with the final permissions that will be used to access your GCP resources. The difference is that we will Tài khoản này sử dụng So với việc sử dụng khóa dịch vụ vĩnh viễn. impersonate Token ngắn hạn Token ngắn hạn Tạo tài khoản dịch vụ gcloud iam service-accounts create "$SERVICE_ACCOUNT_ID"\ --project="$PROJECT_ID" \ --display-name="Hetzner VPS Service Account" \ --description="Service account for Hetzner VPS containers" Ví dụ : gcloud iam service-accounts create "hetzner" \ --project="spiritual-slate-445211-i1" \ --display-name="Hetzner VPS Service Account" \ --description="Service account for Hetzner VPS containers" Sử dụng 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" Assigning the roles to the service account You can assign the GCP’s predefined Roles by visiting this link: https://cloud.google.com/iam/docs/roles-permissions Tôi đã chọn một cách tiếp cận chi tiết hơn, đặt quyền trực tiếp vào một vai trò tùy chỉnh, giúp tôi giảm bớt diện tích bề mặt tấn công. OR - Gán vai trò hạt hoặc quyền: Cách tiếp cận này đưa ra một số trở lại với GCP (403 Lỗi cấm ở đây và ở đó). Bạn có thể tìm thấy các quyền granular trong liên kết này: https://cloud.google.com/iam/docs/thông tin tham khảo Chúng tôi bắt đầu bằng cách tạo ra một vai trò tùy chỉnh. Tạo một vai trò tùy chỉnh cho tài khoản dịch vụ - với quyền tối thiểu Để tạo một vai trò tùy chỉnh, chúng tôi cần một tập tin YAML sẽ chứa mỗi quyền: Tạo một tệp có tên là “roles-hetzner.gcp.yml” và sao chép và dán những điều sau đây (Lưu ý tệp này sống cục bộ trong máy hoặc repo của bạn) # 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 Sau đó thực hiện lệnh: 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" Refresher: The --file parameter in the command points to a relative path where your terminal is: As the file is in the same location, we can do: gcloud iam roles create "herzner_role" --project="spiritual-slate-445211-i1" --file="roles-hetzner.gcp.yml" Tủ lạnh: Các --file tham số trong lệnh chỉ ra một con đường tương đối nơi thiết bị đầu cuối của bạn là: As the file is in the same location, we can do: 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 Cập nhật vai trò 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: Tìm kiếm dịch vụ tôi muốn sử dụng, ví dụ, Vertex AI, và nhấp vào nó: Sử dụng LLM hoặc vai trò Editor/Viewer, yêu cầu các vai trò bạn muốn cấp, sau đó tìm kiếm vai trò trực tiếp và chọn các quyền bạn muốn. Ví dụ, tôi phát hiện ra rằng nó Có cái Lời bài hát: Let Me Call Gemini Vertex AI User aiplatform.endpoints.predict If you mess up with a permission, you will receive a 403 Forbidden from Google Cloud. Thêm quyền vào tệp YAML và cập nhật vai trò. Cho phép các dịch vụ bị mất 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 Bất kể hai bước trên, chúng tôi gắn vai trò WorkloadIdentityUser trực tiếp với người dùng. (Bạn có thể chọn thêm quyền trực tiếp thay vào đó) 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/*" mà trở thành: 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/*" Inside the VPS - Docker Compose Mặc dù tôi khuyên bạn nên có một máy chủ riêng biệt cho nhà cung cấp OpenID (Dex), tôi đã quyết định đồng lưu trữ ứng dụng của tôi và Dex trong một máy chủ duy nhất để giảm chi phí. Mặc dù tôi khuyên bạn nên có một máy chủ riêng biệt cho nhà cung cấp OpenID (Dex), tôi đã quyết định đồng lưu trữ ứng dụng của tôi và Dex trong một máy chủ duy nhất để giảm chi phí. Now we’re moving to the VPS (E.g: Hetzner). We will git pull/or shell copy our scripts to the server (Docker compose and some shell files) that will help us: Bắt đầu dịch vụ Dex và thiết lập IDP. Xác thực chống lại Registry Artifact với Docker. Thiết lập chứng chỉ TLS cho miền HTTPS của chúng tôi. Cấu hình Nginx với không triển khai cho một ứng dụng Remix/React-Router. Docker thành phần: 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 - Được sử dụng để tạo chứng chỉ TLS cho chúng tôi.Chúng tôi cần phải phát hành kết nối HTTPS bởi vì Google sẽ kết nối với dịch vụ của chúng tôi và xác minh tính xác thực của yêu cầu, và bởi vì nó là bảo mật 101.Nginx sau đó xử lý chứng chỉ.Tôi đang sử dụng Cloudflare như một proxy. Phiên bản này của Certbot kết nối với tài khoản Cloudflare của chúng tôi, quản lý xác minh DNS cho chúng tôi, và tạo chứng chỉ tự động. 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 - The NodeJS Remix/React Router docker image hosted in Artifact Registry. This was built in a CI/CD pipeline and then pushed to it (Outside the scope of this tutorial). The Docker image hosted in Artifact Registry, which will configure later on to pull it from Google using Workload Identity Federation and OIDC. This one has a health check that is used by Docker rollout to kill the old container (see down below). App Script shell khởi động - docker compose up sẽ không cắt nó. chúng ta cần phải đảm bảo giấy chứng nhận được phát hành đầu tiên, và nó dễ dàng hơn cho tôi để xử lý ban nhạc với một kịch bản shell. Một số kung-fu được yêu cầu để có Nginx lên và chạy (Vì Dex cần phải được khởi động đầu tiên trước khi ứng dụng) Docker Rollout (Một chút ngoài phạm vi, nhưng tôi muốn bao gồm nó dù sao) Nó là một kịch bản tệp duy nhất được tạo ra bởi Wowu Điều này giúp chúng tôi có các bản cập nhật container Docker không thời gian ngừng hoạt động mà không cần Kubernetes. https://github.com/Wowu/docker-rollout Để cài đặt, thực hiện trong VPS như sau: # 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 Sử dụng nó rất đơn giản. ở mọi nơi chúng ta sẽ sử dụng (chú ý đến syntax mới), chúng tôi sẽ sử dụng Thay vào đó docker compose docker rollout Để triển khai một hình ảnh cập nhật, bạn the new image from Artifact Registry and execute Nó sẽ tự động chuyển đổi hình ảnh mà không có thời gian ngừng hoạt động. docker compose pull app docker rollout Dịch vụ của bạn không thể có container_name và cổng được xác định trong docker-compose.yml, vì không thể chạy nhiều container với cùng một tên hoặc bản đồ cổng. Your service cannot have and định nghĩa trong , vì không thể chạy nhiều container có cùng tên hoặc bản đồ cổng. container_name ports docker-compose.yml How does it work? Việc triển khai Docker sẽ tạo ra một phiên bản mới của ứng dụng (đó là lý do tại sao chúng tôi không sử dụng ) whilst maintaining the old one intact. Once the new service is up and running, it adds an empty extensionless sentinel file (It can be an endpoint call)- called in the temporary directory Container Docker : . container_name drain within the /tmp/drain Điều này buộc kiểm tra sức khỏe thất bại và báo hiệu triển khai Docker để giết chết người cũ và giữ cho người mới sống. 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. Trong các loại ứng dụng khác, không được đặt trước bộ cân bằng tải, chẳng hạn như công nhân Temporal.io (ngoài phạm vi của hướng dẫn này), bạn sẽ cần phải thêm một cơ chế để thoát nước cho người lao động và cho phép nó tự động. Hãy làm sự thay đổi 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. You'll see down below that Dex will ask for a bcrypt hash. These hashes contain dollar signs ($) which need to be escaped in bash. Don't escape them in Doppler. We will handle them using a file replacement mechanism instead. Điều này đã mất tôi một 7 giờ vững chắc để có được đúng. Bạn sẽ thấy dưới đây rằng Dex sẽ yêu cầu một bcrypt hash. Những hash này chứa ký tự đô la ($) cần phải được trốn thoát trong bash. Đừng trốn thoát chúng trong Doppler. chúng tôi sẽ xử lý chúng bằng cách sử dụng một cơ chế thay thế tệp thay thế. This took me a solid 7 hours to get right. CLI cài đặt Bạn sẽ cần phải cài đặt Doppler trong VPS hoặc máy chủ mà bạn đang sử dụng. You can install it using the following command: curl -Ls --tlsv1.2 --proto "=https" --retry 3 https://cli.doppler.com/install.sh | sudo sh Tạo Token Doppler để kết nối với Dịch vụ Doppler Dex Configuration Dex là một “Federated OpenID Connect Provider”.Nói cách khác, nó hoạt động như một trung gian để kết nối với Identity Providers (Hãy nghĩ như thế này: Đăng nhập với Google, Đăng nhập với GiTHub). OpenID là một giao thức xác thực dựa trên OAuth 2.0, cho phép các ứng dụng và người dùng có được thông tin hồ sơ người dùng với một API giống như REST. Nó được cung cấp dưới dạng hình ảnh Docker, và chúng tôi sẽ cấu hình nó bằng cách sử dụng Docker Compose. Điều tôi yêu thích về Dex là sự đơn giản của nó. Bạn chỉ cần một tập tin YAML duy nhất sẽ chứa toàn bộ cấu hình. Nó sẽ làm điều kỳ diệu với một vài dòng mã. Tạo a file and place it in your VPS in (Tạo thư mục với ) 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 Nếu chúng ta thay thế nó bằng môi trường của chúng ta, chúng ta nhận được: # 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: Người phát hành 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) In other words, it will become something like this: issuer: https://auth.mydomain.com Lưu trữ 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. Về chứng chỉ TLS Dex không xử lý cấu hình TLS, Nginx là (Bạn sẽ thấy nó trong cấu hình). Trong các triển khai khác, bạn có thể để Dex xử lý các chứng chỉ trực tiếp bằng cách chỉ nó đến các tệp cert. cổng web web: # Listen on HTTP, assuming a reverse proxy handles TLS termination. http: 0.0.0.0:5556 Đây là cổng được phơi bày bên trong container Docker. chúng ta sẽ thấy sau đó về cách chúng ta sẽ lập bản đồ nó ra container Nginx của chúng ta. Tạo mật khẩu tĩnh: 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. Điều này cũng được sử dụng trong các kịch bản khác để xác thực chống lại các nhà cung cấp khác thay mặt bạn. Điều này có nghĩa là nếu tôi cung cấp tên người dùng và mật khẩu, và tôi có kết nối OAuth với GitHub, Microsoft hoặc Google, nó sẽ kết nối với các dịch vụ đó thay mặt chúng tôi và trả về một token có thể kết nối với các dịch vụ đó. 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 staticClients: - the magical part: id This is critical. This will specify the audience field of the generated JWT. Google will compare against this and validate the request! I cannot tell you how much time I spent on getting this right. This is critical. This will specify the audience field of the generated JWT. Google will compare against this and validate the request! I cannot tell you how much time I spent on getting this right. We need to provide a to our : Đồng hồ đầy đủ hetzner-provider Đồng hồ đầy đủ //iam.googleapis.com/projects/1016670781645/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider Note we start with: // Note we start with: // : 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 Avoid dollar signs Bạn sẽ thông qua mật khẩu này khi gửi tải trọng HTTP đến Dex sau này! Bạn chuyển đến Dex chuỗi hash của mật khẩu ở trên trong lĩnh vực 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-> E. G : htpasswd -bnBC 10 "" a-strong-password-generated-using-a-password-generator It generates: $2y$10$s4ETxkQQeuJu4Kp58O607u.wiqlHnkyV8LkFK1g4cMKGFU959uusq (Lưu ý rằng nó bắt đầu với một ký hiệu đô la) Một mật khẩu bcrypt hashed sẽ có 3 ký tự đô la như hiển thị ở trên. Những điều này cần phải được thoát đúng cách trong bash / shell nếu bạn cố gắng tiêm chúng như các biến môi trường. Như đã đề cập trong phần Dex, nó trở nên có vấn đề. tôi sẽ chỉ cho bạn cách vượt qua điều này bằng cách sử dụng envsubst. A hashed bcrypt password will have 3 dollar signs as shown above. These need to be escaped correctly in bash/shell if you try to inject them as environment variables. As mentioned in the Dex section, it becomes problematic. I’ll show you how to overcome this using envsubst. Tạo một điểm nhập cảnh cho 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 File này phục vụ 3 mục đích: 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 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. Khi sử dụng tệp .env, kịch bản này không cần thiết. Bạn có thể khởi động Dex trực tiếp. Tuy nhiên, tải xuống bcrypt hash từ Doppler đã trở thành một mớ hỗn độn. Nginx : I don't think it needs any introduction. NGINX is an HTTP web server, reverse proxy, content cache, load balancer, TCP/UDP proxy server, and mail proxy server. It's one of the skeletons of the entire web. We will use it as the main entry point for our main Docker application and our DEX service. It will also be responsible for handling the TLS connections for us. Tuy nhiên, cấu hình này là một chút thách thức. Nginx sẽ không tải đúng trừ khi tất cả các dịch vụ được bật và chạy. tôi sẽ chỉ cho bạn dưới đây làm thế nào chúng tôi sử dụng một kịch bản tự động hóa mà tận dụng hai cấu hình Nginx, cung cấp không thời gian ngừng hoạt động. Tuy nhiên, cấu hình này là một chút thách thức. Nginx sẽ không tải đúng trừ khi tất cả các dịch vụ được bật và chạy. tôi sẽ chỉ cho bạn dưới đây làm thế nào chúng tôi sử dụng một kịch bản tự động hóa mà tận dụng hai cấu hình Nginx, cung cấp không thời gian ngừng hoạt động. Chúng tôi sử dụng hai cấu hình này bởi vì NGINX sẽ không tải đúng trừ khi tất cả các dịch vụ có sẵn. như bạn có thể thấy, ứng dụng chính của chúng tôi được giữ trong Registry Artifact, mà cũng cần DEX để được xác thực chống lại. To overcome this ,we: 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; } } } Tiền thưởng ! F5 là công ty đằng sau NGINX, và họ có một cuốn sách nấu ăn tuyệt vời giúp bạn hiểu rõ hơn về cách cấu hình nó. https://www.f5.com/content/dam/f5/corp/global/pdf/ebooks/NGINX_Cookbook-final.pdf Tiền thưởng ! 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. This will be composed of 3 files: (This authenticates against DEX and generates a JWT) fetch-id-token.sh fetch-google-oidc-token.sh (Nó lấy JWT từ fetch-id-token.sh và tạo ra một token OIDC từ Google Cloud) (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 Don’t blindly copy and paste these files, as many of them have hardcoded values like the current non-root user in the VPS: localuser Đừng mù quáng sao chép và dán các tệp này, vì nhiều trong số chúng có giá trị mã cứng như người dùng không gốc hiện tại trong VPS: localuser All of these files need to have execution access: chmod +x ~/scripts/fetch-id-token.sh chmod +x ~/scripts/fetch-google-oidc-token.sh chmod +x /usr/local/bin/docker-credential-gcr Cấu hình Credential Helper trong Docker: Navigate to 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 Trợ lý xác thực để xác thực bất cứ khi nào kéo hoặc đẩy hình ảnh gcr Docker sau đó cố gắng thực hiện các nhị phân được đặt tên (extensionless - e.g: ) docker-credential-<helper> docker-credential-gcr Nó là một quy ước đặt tên được sử dụng bởi Docker, và nó tìm thấy nó thông qua $PATH khi cần thiết. The docker-credential-gcr ( ): /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 This file orchestrates the following two. It also adds some debugging mechanisms to để giúp bạn giải quyết khi mọi thứ thất bại. /tmp/docker-credential-gcr.log Thảo luận:Thuyết minh.com ( - create the 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" Thảo luận:Thuyết minh ( ) ~/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: Tải về JWT từ Dex. Gửi JWT đến Google để tạo Token OIDC. Use the OIDC Token to impersonate the service account. Sử dụng token làm trường mật khẩu của container 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 Bạn phải cập nhật docker-credential-gcr để phù hợp với các thư mục!!! ( Putting everything together - The smart-start.sh smart-start.sh As I’ve mentioned, orchestrating this is a bit tricky, as I’m hosting everything within a single server and a single Nginx process, we need to (recap): Tạo chứng chỉ TLS Load Nginx with Dex. Let Docker authenticate against Dex and GCP - Pull the image from Artifact Registry Reload Nginx config to serve the app. Đối với điều này, chúng tôi đã tạo ra một trợ lý Điều này cũng đòi hỏi quyền truy cập thực thi. 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 Chạy kịch bản: Để chạy điều này, bạn: 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 Chọn bash, hiển thị ví dụ và cho phép chế độ nghiêm ngặt: -e exit on error, -u fail on unset vars, -o pipefail catch pipe fail. 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; } Thông tin liên quan: Thông tin liên quan / Thông tin liên quan / Thông tin liên quan / Thông tin liên quan / Thông tin liên quan ( 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) Docker đóng gói Doppler và trợ lý dịch vụ 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." Nó : docker_compose / docker_rollout: chạy các lệnh Docker với env được tiêm bởi Doppler. service_running: Kiểm tra xem một dịch vụ có ở trạng thái “đang chạy” hay không. 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 thông qua Let’s Encrypt (dns-01) – có thể bỏ qua với 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 Lưu ý: các tên miền ở đây là mydomain.com Nhưng cert path sử dụng alertdown.ai. Đảm bảo những tên miền này khớp với tên miền thực của bạn. 5) Phase 2 — Bring up the auth stack (Nginx + Dex) 5) Giai đoạn 2 – Đưa ra các 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: Chuyển nginx sang bảo trì / cấu hình ban đầu. Bắt đầu dex, chờ cho đến khi container sức khỏe = khỏe mạnh. Đảm bảo nginx được bật; xác nhận config và tải lại nếu đã chạy. Thử nghiệm auth sức khỏe tại https://auth.mydomain.com/healthz hoặc $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: Kéo ảnh ứng dụng (cảnh báo nếu kéo thất bại; sử dụng hình ảnh bộ nhớ cache). Deploys app via (zero-downtime rollout tool). docker rollout Chờ đến 180s cho ứng dụng sức khỏe. Chuyển đổi nginx để sản xuất config và reloads. 7) Phase 4 — start certbot manager (for renew hooks) 7) Giai đoạn 4 – bắt đầu quản lý certbot (đối với hooks renew) 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: Kiểm tra container “running” + “healthy” cho dex và ứng dụng. HTTPS Health Endpoints + HTTP → HTTPS Redirect in một docker một dòng tạo bảng trạng thái và biểu ngữ thành công thân thiện. Lưu ý nếu ứng dụng vẫn đang khởi động. Beware: Please inspect this file before proceeding. There are other hardcoded values within them that you need to update. Hooray 🥳 Hà Nội With this, you’ll have với một hình ảnh Docker được lấy từ Registry Artifact! Dex up and running Dex up and running Xác thực bằng cách sử dụng 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 Dex Dịch vụ Google STS (Storage Transfer Service) Tài khoản Google IAM Credentials Xác thực chống lại Dex Chúng tôi sẽ bao gồm điều này dưới đây, trong vỏ chúng tôi có thể tạo ra một kịch bản auth như: BASIC_AUTH_HEADER=$(echo -n "$DEX_GCP_STATIC_CLIENT_ID:$DEX_GCP_STATIC_CLIENT_SECRET" | base64 -w 0) Chúng tôi sẽ bao gồm điều này dưới đây, trong vỏ chúng tôi có thể tạo ra một kịch bản auth như: BASIC_AUTH_HEADER=$(echo -n "$DEX_GCP_STATIC_CLIENT_ID:$DEX_GCP_STATIC_CLIENT_SECRET" | base64 -w 0) Yêu cầu fetch được tạo: 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: Với token trong tay, bạn cần xác thực với Google STS (Dịch vụ chuyển giao lưu trữ) cho phép chúng tôi di chuyển dữ liệu qua các nhà cung cấp đám mây: : Where ALERTDOWN_AUTH_ID_TOKEN => Là JWT đến từ Dex. AUDIENCE => //iam.googleapis.com/projects/1016670781645/locations/global/workloadIdentityPools/hetzner-pool/providers/hetzner-provider Chú ý đến phạm vi! Đây là lệnh 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)); Here’s the full cURL command: 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\"}" }' Tài khoản Google IAM Credentials Đây là phần cuối cùng! Với điều này, chúng tôi có được JWT sống ngắn giả hình tài khoản dịch vụ của chúng tôi Tải 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" ] }' Token Bearer là token ya29.d mà chúng tôi nhận được từ Google STS. Token Bearer là token ya29.d mà chúng tôi nhận được từ Google STS. Issuing an OIDC Token from NodeJS This process isn’t as cumbersome as Docker’s, but it’s still hairy and little documented. Google builds the authentication flow right into Nhưng nó vẫn đòi hỏi một số cấu hình. google-auth-library Đây là cách: Các OIDC Config (Chúng tôi thông qua nó từ 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. Install the required packages: google-auth-library pnpm add google-auth-library @google-cloud/vertexai Tạo một 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. Các chức năng chuyển đổi đầu vào của chúng tôi thành hình dạng mà Google Auth Credentials mong đợi từ mỗi dịch vụ gói Google. Điều này tương tự như cấu hình Postman, với sự khác biệt là các điểm cuối STS và IAM được nướng vào. getOidcConfig Cấu hình Vertex AI: Sau đó chúng ta có thể tiêu thụ nó trong 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', }, }); } } Sau đó nó sẽ tự động thực hiện tất cả các trao đổi token cho chúng tôi! Chúng tôi đã làm Omgosh. Nó điên rồ. Phải mất một thời gian, nhưng chúng tôi đã có thể kết nối với các dịch vụ và phát hành một token quay! Hy vọng, bạn sẽ không mất nhiều thời gian. Nếu có một phần không rõ ràng từ bài viết, hãy cho tôi biết! Bạn luôn có thể tìm thấy tôi trên and/or LinkedIn Twitter / X @javiasilis @Nhân Trí