Skip to content

Commit b363f16

Browse files
ChingEnLinclaude
andauthored
chore: merge dev into production (#34)
* feat: harden Cloud Run security with Secret Manager, VPC connector, and private backend - Secret Manager: move all sensitive env vars (Azure credentials, Gemini key, DB credentials) out of GitHub Secrets and into GCP Secret Manager; Cloud Run reads them at runtime via --set-secrets, so secrets are never exposed in workflow logs or build args. - VPC Connector: add Serverless VPC Access connector (terraform/network.tf) so Cloud Run services can reach Cloud SQL and each other over the private VPC network. - Private backend: set backend Cloud Run ingress to 'internal', blocking all public internet access. Frontend nginx now proxies /api/* to the backend's internal URL (with BACKEND_URL injected as a runtime env var), so the browser never needs a direct connection to the backend. - Terraform IaC: terraform/ directory manages the VPC connector, Secret Manager secrets, Cloud Run service account, and Cloud SQL (importable via import.sh). CI continues to own image builds and Cloud Run deployments. - Data migration script: scripts/migrate_db.sh migrates PostgreSQL data between Cloud SQL instances via Cloud SQL Auth Proxy if the database ever needs to be rebuilt. https://claude.ai/code/session_01SRRzCWrpwgMpdYFurMVn7m * fix: construct Cloud Run SA email inline to use PROJECT_ID, clarify VITE_API_BASE_URL GitHub Actions does not interpolate ${{ env.X }} inside the top-level env: block, so the full SA email could not reference PROJECT_ID there. Replaced CLOUD_RUN_SA with CLOUD_RUN_SA_NAME and build the email inline in the flags blocks where expression context is available. Added a comment explaining VITE_API_BASE_URL=/api — it is the nginx location prefix, not a full URL, because the browser calls the frontend's own origin and nginx proxies /api/* to the internal backend. https://claude.ai/code/session_01SRRzCWrpwgMpdYFurMVn7m * docs: add infrastructure documentation with architecture diagrams to README - Add production architecture Mermaid diagram showing Cloud Run services, VPC connector, Secret Manager, Cloud SQL, and external dependencies - Add network security model table (frontend public / backend internal) - Add secret management table listing all Secret Manager secrets - Add IaC ownership table (Terraform vs CI pipeline) - Add CI/CD pipeline Mermaid flowchart showing Workload Identity auth, image build/push, and deploy steps with secret injection - Add Terraform to technology stack table Also remove overly broad github_actions_secret_accessor IAM binding from terraform/iam.tf — the GitHub Actions SA never reads secret values directly; Cloud Run reads them at startup using the Cloud Run SA identity. https://claude.ai/code/session_01SRRzCWrpwgMpdYFurMVn7m * fix: align Terraform DB config and reorganize docs (#33) * fix: align database.tf backup config with actual Cloud SQL instance state Backup was disabled on the real instance; syncing so terraform plan is clean with no spurious diff on next apply. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: reorganize README into focused docs/ files README trimmed to a lean landing page. Detailed content moved to: - docs/ARCHITECTURE.md (BFF pattern, ReAct agent, security model) - docs/INFRASTRUCTURE.md (Cloud topology, Terraform, Secret Manager, CI/CD) - docs/AZURE_SETUP.md (Entra ID registration, Cosmos DB, frontend config) - docs/DEVELOPMENT.md (local setup, testing, code style) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3ba242d commit b363f16

20 files changed

Lines changed: 923 additions & 464 deletions
Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# This workflow builds and pushes Docker containers to Google Artifact Registry
2-
# and deploys both backend and frontend on Cloud Run when a commit is pushed to the "production"
3-
# branch.
1+
# Build and deploy QueryPal to Cloud Run.
2+
# Runs on pushes to the production branch.
3+
#
4+
# Infrastructure changes (VPC connector, Secret Manager, IAM) are managed by
5+
# Terraform in the terraform/ directory and must be applied before first deploy.
46

57
name: 'Build and Deploy QueryPal to Cloud Run'
68

@@ -16,6 +18,11 @@ env:
1618
BACKEND_SERVICE: 'querypal-backend'
1719
FRONTEND_SERVICE: 'querypal-frontend'
1820
WORKLOAD_IDENTITY_PROVIDER: 'projects/874216619692/locations/global/workloadIdentityPools/github/providers/querypal'
21+
# Short name of the Cloud Run SA and VPC connector created by Terraform.
22+
# The full SA email is constructed inline in flags using ${{ env.PROJECT_ID }}
23+
# because GitHub Actions does not interpolate env vars inside the env: block.
24+
CLOUD_RUN_SA_NAME: 'querypal-cloudrun-sa'
25+
VPC_CONNECTOR: 'querypal-vpc-connector'
1926

2027
jobs:
2128
deploy:
@@ -29,80 +36,96 @@ jobs:
2936
- name: 'Checkout'
3037
uses: 'actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332' # actions/checkout@v4
3138

32-
# Configure Workload Identity Federation and generate an access token.
3339
- id: 'auth'
3440
name: 'Authenticate to Google Cloud'
3541
uses: 'google-github-actions/auth@f112390a2df9932162083945e46d439060d66ec2' # google-github-actions/auth@v2
3642
with:
3743
workload_identity_provider: '${{ env.WORKLOAD_IDENTITY_PROVIDER }}'
38-
service_account: 'github-actions@gen-lang-client-0698668474.iam.gserviceaccount.com'
44+
service_account: 'github-actions@${{ env.PROJECT_ID }}.iam.gserviceaccount.com'
3945

40-
# Set up Cloud SDK
4146
- name: 'Set up Cloud SDK'
4247
uses: 'google-github-actions/setup-gcloud@98ddc00a17442e89a24bbf282954a3b65ce6d200' # google-github-actions/setup-gcloud@v2
4348

44-
# Configure Docker to use gcloud as a credential helper
4549
- name: 'Configure Docker for GCR'
46-
run: |-
47-
gcloud auth configure-docker --quiet
50+
run: gcloud auth configure-docker --quiet
51+
52+
# ── Backend ──────────────────────────────────────────────────────────────
4853

49-
# Build and Push Backend Container
5054
- name: 'Build and Push Backend Container'
5155
run: |-
5256
cd backend
5357
DOCKER_TAG="gcr.io/${{ env.PROJECT_ID }}/${{ env.BACKEND_SERVICE }}:${{ github.sha }}"
5458
docker build --tag "${DOCKER_TAG}" --platform linux/amd64 .
5559
docker push "${DOCKER_TAG}"
5660
57-
# Deploy Backend to Cloud Run
5861
- id: 'deploy-backend'
5962
name: 'Deploy Backend to Cloud Run'
6063
uses: 'google-github-actions/deploy-cloudrun@33553064113a37d688aa6937bacbdc481580be17' # google-github-actions/deploy-cloudrun@v2
6164
with:
6265
service: '${{ env.BACKEND_SERVICE }}'
6366
region: '${{ env.REGION }}'
6467
image: 'gcr.io/${{ env.PROJECT_ID }}/${{ env.BACKEND_SERVICE }}:${{ github.sha }}'
68+
# Non-secret runtime configuration only.
6569
env_vars: |
6670
ENVIRONMENT=production
67-
AZURE_TENANT_ID=${{ secrets.AZURE_TENANT_ID }}
68-
AZURE_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }}
69-
AZURE_CLIENT_SECRET=${{ secrets.AZURE_CLIENT_SECRET }}
7071
ARM_SCOPE=https://management.azure.com/.default
71-
GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}
72-
DB_USER=${{ secrets.DB_USER }}
73-
DB_PASS=${{ secrets.DB_PASS }}
7472
DB_NAME=querypal
75-
DB_UNIX_SOCKET=/cloudsql/gen-lang-client-0698668474:europe-west1:querypal-db
73+
DB_UNIX_SOCKET=/cloudsql/${{ env.PROJECT_ID }}:${{ env.REGION }}:querypal-db
74+
# Sensitive values are read directly from Secret Manager at runtime.
75+
# Secret must exist before first deploy (created by terraform/secrets.tf).
76+
secrets: |
77+
AZURE_TENANT_ID=querypal-azure-tenant-id:latest
78+
AZURE_CLIENT_ID=querypal-azure-client-id:latest
79+
AZURE_CLIENT_SECRET=querypal-azure-client-secret:latest
80+
GEMINI_API_KEY=querypal-gemini-api-key:latest
81+
DB_USER=querypal-db-user:latest
82+
DB_PASS=querypal-db-pass:latest
7683
flags: |
7784
--port=8000
78-
--add-cloudsql-instances=gen-lang-client-0698668474:europe-west1:querypal-db
85+
--service-account=${{ env.CLOUD_RUN_SA_NAME }}@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
86+
--add-cloudsql-instances=${{ env.PROJECT_ID }}:${{ env.REGION }}:querypal-db
87+
--vpc-connector=${{ env.VPC_CONNECTOR }}
88+
--vpc-egress=private-ranges-only
89+
--ingress=internal
7990
--allow-unauthenticated
8091
81-
# Build and Push Frontend Container
92+
# ── Frontend ─────────────────────────────────────────────────────────────
93+
8294
- name: 'Build and Push Frontend Container'
8395
run: |-
8496
cd frontend
8597
DOCKER_TAG="gcr.io/${{ env.PROJECT_ID }}/${{ env.FRONTEND_SERVICE }}:${{ github.sha }}"
98+
# VITE_API_BASE_URL=/api tells the React app to send all API calls to
99+
# the /api/* path on its own origin instead of a full backend URL.
100+
# Nginx then proxies those requests to the internal backend service.
86101
docker build --tag "${DOCKER_TAG}" --platform linux/amd64 \
87-
--build-arg VITE_API_BASE_URL=${{ steps.deploy-backend.outputs.url }} \
102+
--build-arg VITE_API_BASE_URL=/api \
88103
--build-arg VITE_AZURE_REDIRECT_URI=https://querypal.virtonomy.io \
89104
.
90105
docker push "${DOCKER_TAG}"
91106
92-
# Deploy Frontend to Cloud Run
93107
- id: 'deploy-frontend'
94108
name: 'Deploy Frontend to Cloud Run'
95109
uses: 'google-github-actions/deploy-cloudrun@33553064113a37d688aa6937bacbdc481580be17' # google-github-actions/deploy-cloudrun@v2
96110
with:
97111
service: '${{ env.FRONTEND_SERVICE }}'
98112
region: '${{ env.REGION }}'
99113
image: 'gcr.io/${{ env.PROJECT_ID }}/${{ env.FRONTEND_SERVICE }}:${{ github.sha }}'
114+
# BACKEND_URL is the internal Cloud Run URL; nginx uses it at runtime to
115+
# proxy /api/* requests to the backend (which is not publicly reachable).
116+
env_vars: |
117+
BACKEND_URL=${{ steps.deploy-backend.outputs.url }}
100118
flags: |
101119
--port=4000
120+
--service-account=${{ env.CLOUD_RUN_SA_NAME }}@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
121+
--vpc-connector=${{ env.VPC_CONNECTOR }}
122+
--vpc-egress=all-traffic
123+
--ingress=all
102124
--allow-unauthenticated
103125
104-
# Show output URLs
126+
# ── Summary ───────────────────────────────────────────────────────────────
127+
105128
- name: 'Show deployment URLs'
106129
run: |-
107-
echo "Backend URL: ${{ steps.deploy-backend.outputs.url }}"
108130
echo "Frontend URL: ${{ steps.deploy-frontend.outputs.url }}"
131+
echo "Backend URL: ${{ steps.deploy-backend.outputs.url }} (internal only)"

0 commit comments

Comments
 (0)