Intial commit for deployment script p2
This commit is contained in:
+384
@@ -0,0 +1,384 @@
|
||||
# Deploying to Google Cloud Run
|
||||
|
||||
This folder contains everything needed to deploy the Htmx app to Google Cloud Run — completely isolated from the application code.
|
||||
|
||||
## Folder structure
|
||||
|
||||
```
|
||||
GCR/
|
||||
├── .env.example ← copy to .env and fill in your values
|
||||
├── Dockerfile ← multi-stage AOT build (Linux/amd64)
|
||||
├── entrypoint.sh ← maps Cloud Run's PORT var to ASP.NET Core
|
||||
├── docker-compose.yml ← Cloud Run service definition (used by gcloud)
|
||||
├── run-all.sh ← smart Linux runner (checks + prompts)
|
||||
├── run-all.ps1 ← smart Windows runner (checks + prompts)
|
||||
└── scripts/
|
||||
├── 00-install-gcloud.sh / .ps1 ← install Google Cloud CLI
|
||||
├── 01-login.sh / .ps1 ← authenticate + configure Docker
|
||||
├── 02-setup-project.sh / .ps1 ← one-time GCP project setup
|
||||
├── 03-create-secrets.sh / .ps1 ← manage MongoDB secret
|
||||
└── 04-deploy.sh / .ps1 ← build, push, and deploy
|
||||
```
|
||||
|
||||
`.sh` scripts are for **Linux**. `.ps1` scripts are for **Windows** (PowerShell 5.1+).
|
||||
|
||||
### One-command flow (recommended)
|
||||
|
||||
Instead of running each step manually, use the root runner. It checks each step,
|
||||
shows completed items, and prompts to run only missing steps.
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
bash GCR/run-all.sh
|
||||
# non-interactive (auto-run missing steps):
|
||||
bash GCR/run-all.sh --yes
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\GCR\run-all.ps1
|
||||
# non-interactive (auto-run missing steps):
|
||||
.\GCR\run-all.ps1 -Yes
|
||||
```
|
||||
|
||||
## Security on Untrusted Machines
|
||||
|
||||
Do **not** run these scripts on machines you don't control unless absolutely necessary.
|
||||
|
||||
Why this matters:
|
||||
1. The scripts grant project-level IAM roles to your user, including `roles/secretmanager.admin`.
|
||||
2. `gcloud auth login` stores local credentials/tokens that can be reused if the machine is compromised.
|
||||
3. Docker auth is configured for Artifact Registry and may persist in local Docker config.
|
||||
4. A local `GCR/.env` file contains project identifiers and deployment metadata.
|
||||
|
||||
Minimum cleanup if you ever used a shared/untrusted machine:
|
||||
|
||||
1. Revoke IAM roles from your user account in the GCP project.
|
||||
2. Revoke local gcloud credentials and clear config.
|
||||
3. Remove Docker credential entries for Artifact Registry.
|
||||
4. Delete local `GCR/.env` and any temporary files.
|
||||
|
||||
Example role cleanup (Linux/macOS shell):
|
||||
```bash
|
||||
USER_EMAIL="your-user@company.com"
|
||||
PROJECT_ID="your-project-id"
|
||||
|
||||
for ROLE in \
|
||||
roles/run.developer \
|
||||
roles/artifactregistry.writer \
|
||||
roles/iam.serviceAccountUser \
|
||||
roles/secretmanager.admin \
|
||||
roles/secretmanager.secretAccessor \
|
||||
roles/secretmanager.secretVersionAdder; do
|
||||
gcloud projects remove-iam-policy-binding "$PROJECT_ID" \
|
||||
--member="user:$USER_EMAIL" \
|
||||
--role="$ROLE" \
|
||||
--quiet
|
||||
done
|
||||
```
|
||||
|
||||
Credential cleanup:
|
||||
```bash
|
||||
gcloud auth revoke --all
|
||||
gcloud config configurations delete default --quiet || true
|
||||
```
|
||||
|
||||
Credential cleanup (Windows PowerShell):
|
||||
```powershell
|
||||
gcloud auth revoke --all
|
||||
gcloud config configurations delete default --quiet
|
||||
```
|
||||
|
||||
Prefer a dedicated personal/admin workstation, or use a tightly scoped CI service account instead of broad user credentials.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Configure your .env file
|
||||
|
||||
Copy the example and fill it in:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
cp GCR/.env.example GCR/.env
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
Copy-Item GCR\.env.example GCR\.env
|
||||
```
|
||||
|
||||
Open `GCR/.env` in any editor and set:
|
||||
|
||||
| Variable | Description | Example |
|
||||
|---|---|---|
|
||||
| `GCP_PROJECT_ID` | Your GCP project ID | `my-htmx-project` |
|
||||
| `GCP_REGION` | Cloud Run region | `us-central1` |
|
||||
| `GCP_REPOSITORY` | Artifact Registry repo name | `htmx` |
|
||||
| `SERVICE_NAME` | Cloud Run service name | `htmx-app` |
|
||||
| `MONGODB_DATABASE_NAME` | Database name | `HtmxAppDb` |
|
||||
|
||||
> **Security note:** `GCR/.env` is gitignored. Never commit it.
|
||||
|
||||
> **MongoDB note:** The app connects to MongoDB at startup. Cloud Run containers do not have access to `localhost:27017` — use MongoDB Atlas (cloud-hosted) or a MongoDB instance reachable over the internet/VPC.
|
||||
|
||||
> **Secrets note:** The MongoDB connection string is **not stored in .env**. It's stored securely in Google Cloud Secret Manager. See **Step 4** below.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Install the Google Cloud CLI
|
||||
|
||||
Run **once** on a new machine.
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
bash GCR/scripts/00-install-gcloud.sh
|
||||
```
|
||||
|
||||
**Windows** (PowerShell, run as Administrator):
|
||||
```powershell
|
||||
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # first time only
|
||||
.\GCR\scripts\00-install-gcloud.ps1
|
||||
```
|
||||
|
||||
After installation, open a new terminal and verify:
|
||||
```
|
||||
gcloud version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Log in
|
||||
|
||||
Authenticates your machine to GCP and configures Docker to push to Artifact Registry.
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
bash GCR/scripts/01-login.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\GCR\scripts\01-login.ps1
|
||||
```
|
||||
|
||||
A browser window opens for Google sign-in. Use the account that owns or has access to your GCP project.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Set up the GCP project (one time)
|
||||
|
||||
Enables APIs, creates the Artifact Registry repository, and grants your account the required IAM roles. If billing is not yet linked, the script prompts you to choose a billing account.
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
bash GCR/scripts/02-setup-project.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\GCR\scripts\02-setup-project.ps1
|
||||
```
|
||||
|
||||
This is **safe to re-run** — all operations are idempotent.
|
||||
|
||||
### What it enables
|
||||
|
||||
| API | Purpose |
|
||||
|---|---|
|
||||
| `run.googleapis.com` | Cloud Run service |
|
||||
| `artifactregistry.googleapis.com` | Docker image storage |
|
||||
| `secretmanager.googleapis.com` | Available for future use |
|
||||
| `cloudresourcemanager.googleapis.com` | IAM and project management |
|
||||
|
||||
### What IAM roles it grants (to your account)
|
||||
|
||||
| Role | Purpose |
|
||||
|---|---|
|
||||
| `roles/run.developer` | Deploy and manage Cloud Run services |
|
||||
| `roles/artifactregistry.writer` | Push container images |
|
||||
| `roles/iam.serviceAccountUser` | Run the service under a service account |
|
||||
| `roles/secretmanager.admin` | Create/manage secrets and IAM policies (includes `secretmanager.secrets.create`) |
|
||||
| `roles/secretmanager.secretAccessor` | Read secret payloads (for validation/access workflows) |
|
||||
| `roles/secretmanager.secretVersionAdder` | Add/set new secret versions (rotate values safely) |
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Create secrets in Google Cloud Secret Manager
|
||||
|
||||
Store the MongoDB connection string securely:
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
bash GCR/scripts/03-create-secrets.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\GCR\scripts\03-create-secrets.ps1
|
||||
```
|
||||
|
||||
The script prompts for your MongoDB connection string, creates the secret in Secret Manager, and grants Cloud Run permission to access it. The secret is referenced by name (`mongodb-connection-string`) in the deploy script — never stored in .env.
|
||||
|
||||
This is a **one-time setup**. Re-run only if you need to **update** the MongoDB connection string.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Deploy
|
||||
|
||||
Builds the Docker image, pushes it to Artifact Registry, and deploys to Cloud Run.
|
||||
|
||||
If secrets are missing, the deploy script now performs a pre-check and prompts to run
|
||||
the secrets setup script before continuing.
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
bash GCR/scripts/04-deploy.sh
|
||||
# or with a custom image tag:
|
||||
bash GCR/scripts/04-deploy.sh v1.0.0
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\GCR\scripts\04-deploy.ps1
|
||||
# or with a custom image tag:
|
||||
.\GCR\scripts\04-deploy.ps1 -Tag v1.0.0
|
||||
```
|
||||
|
||||
The script:
|
||||
1. Checks for (or generates) `Htmx.ApiDemo/package-lock.json`
|
||||
2. Builds the Docker image from the repo root using `GCR/Dockerfile`
|
||||
3. Pushes the image to Artifact Registry
|
||||
4. Deploys to Cloud Run using `GCR/docker-compose.yml`
|
||||
5. Opens the service to public access (no authentication required)
|
||||
6. Prints the live service URL
|
||||
|
||||
By default the image tag is the short git commit SHA (e.g. `a3f4b7c`). A timestamp is used if the directory is not a git repo.
|
||||
|
||||
---
|
||||
|
||||
## How configuration reaches the app
|
||||
|
||||
The app reads configuration from environment variables. Cloud Run injects them at container startup — no config files needed in the image.
|
||||
|
||||
| Environment variable | Maps to | Set by |
|
||||
|---|---|---|
|
||||
| `ConnectionStrings__DefaultConnection` | `appsettings.json` → `ConnectionStrings.DefaultConnection` | Secret Manager (via `--set-secrets`) → deploy script |
|
||||
| `MongoDbName` | `appsettings.json` → `MongoDbName` | `GCR/.env` → deploy script → docker-compose.yml |
|
||||
| `ASPNETCORE_ENVIRONMENT` | ASP.NET Core environment | `docker-compose.yml` (hardcoded `Production`) |
|
||||
| `PORT` | Listening port | Injected by Cloud Run (default `8080`) |
|
||||
|
||||
**Secret Manager workflow:**
|
||||
- Step 4 stores the MongoDB connection string in Cloud Run Secret Manager
|
||||
- Step 5 (deploy script) injects it via `gcloud run services update --set-secrets=...`
|
||||
- The container never sees the raw connection string; Cloud Run mounts it as an env var at runtime
|
||||
- Each time you update the secret, Cloud Run automatically uses the latest version
|
||||
|
||||
The `GCR/entrypoint.sh` script translates Cloud Run's `PORT` variable into `ASPNETCORE_HTTP_PORTS` at container startup, since ASP.NET Core does not read `PORT` directly.
|
||||
|
||||
---
|
||||
|
||||
## Re-deploying after code changes
|
||||
|
||||
Just run Step 5 again. Each deployment gets a new image tag (git SHA), and Cloud Run creates a new immutable revision. Traffic is shifted to the new revision automatically.
|
||||
|
||||
---
|
||||
|
||||
## Updating configuration
|
||||
|
||||
### Non-sensitive config (MONGODB_DATABASE_NAME, etc.)
|
||||
|
||||
To change a non-sensitive value without rebuilding:
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
source GCR/.env
|
||||
gcloud run services update $SERVICE_NAME \
|
||||
--region=$GCP_REGION \
|
||||
--update-env-vars "MongoDbName=NewDatabaseName"
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
gcloud run services update htmx-app `
|
||||
--region=us-central1 `
|
||||
--update-env-vars "MongoDbName=NewDatabaseName"
|
||||
```
|
||||
|
||||
### Sensitive config (MongoDB connection string)
|
||||
|
||||
To update the MongoDB connection string:
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
source GCR/.env
|
||||
# Read from stdin (paste the connection string and press Ctrl+D):
|
||||
gcloud secrets versions add mongodb-connection-string \
|
||||
--data-file=- \
|
||||
--project=$GCP_PROJECT_ID
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
# Use a temp file to avoid adding trailing newlines to the secret
|
||||
$connectionString = Read-Host -AsSecureString "Enter MongoDB connection string"
|
||||
$connectionStringPlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($connectionString))
|
||||
$TempFile = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
[System.IO.File]::WriteAllText($TempFile, $connectionStringPlainText, [System.Text.Encoding]::UTF8)
|
||||
gcloud secrets versions add mongodb-connection-string --data-file=$TempFile --project=your-project-id
|
||||
} finally {
|
||||
Remove-Item $TempFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
```
|
||||
|
||||
Or re-run the creation script:
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
bash GCR/scripts/03-create-secrets.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\GCR\scripts\03-create-secrets.ps1
|
||||
```
|
||||
|
||||
Cloud Run automatically uses the latest secret version on the next container start.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker build fails on `npm ci`
|
||||
|
||||
`npm ci` requires `Htmx.ApiDemo/package-lock.json` to exist. Generate it locally:
|
||||
|
||||
```bash
|
||||
cd Htmx.ApiDemo && npm install
|
||||
```
|
||||
|
||||
Then commit `package-lock.json` to the repository.
|
||||
|
||||
### `gcloud: command not found` after install
|
||||
|
||||
Close and reopen your terminal. The installer adds `gcloud` to `PATH`, but the current shell session won't see it until restarted.
|
||||
|
||||
### `PERMISSION_DENIED` errors during deploy
|
||||
|
||||
Run `02-setup-project` again — it grants the required IAM roles. It is safe to re-run.
|
||||
|
||||
### Cloud Run container crashes on startup
|
||||
|
||||
View logs in the GCP console (Cloud Run → your service → Logs tab), or:
|
||||
|
||||
```bash
|
||||
gcloud run services logs read $SERVICE_NAME --region=$GCP_REGION --limit=50
|
||||
```
|
||||
|
||||
The most common causes:
|
||||
- MongoDB connection string is wrong or unreachable from Cloud Run
|
||||
- `ASPNETCORE_ENVIRONMENT` is `Production` but `appsettings.Production.json` overrides something unexpectedly
|
||||
|
||||
### Service URL returns 404 for all routes
|
||||
|
||||
The service is running but no route matched. Confirm the app started correctly by checking logs for `Now listening on`.
|
||||
Reference in New Issue
Block a user