Multi-environment deployment, architecture, and operational guides
Your app now supports multiple environments (dev, staging, prod, demo) with separate GCP projects for cost tracking and security isolation.
Environment Logic:
FLASK_ENV=development automatically means ‘dev’ environmentFLASK_ENV=production + ENVIRONMENT variable (prod/staging/demo-xxx)You can use a single Strava API app for all your environments:
kaizencoach.training
localhost & 127.0.0.1 are allow listed by default
Note: Strava API apps are limited to 1 user (the owner) by default unless you apply for production access.
Since Strava limits apps to 1 user, each friend needs their own Strava API app:
process detailed here
demo-john.kaizencoach.training)STRAVA_CLIENT_IDSTRAVA_CLIENT_SECRETkaizencoach/demo-john/app-secretsSTRAVA_REDIRECT_URI environment variable to their deploymentENVIRONMENT=demo-johnAlternative: Apply for Strava API production access to support multiple users with one app.
⚠️ CRITICAL: Update config.py for every new environment BEFORE building Docker image!
The application needs to know about each environment for OAuth and GCP to work correctly.
Edit config.py to add new environments:
# Around line 35-39: Add callback URLs for OAuth
REDIRECT_URIS = {
'dev': 'http://127.0.0.1:5000/callback',
'staging': 'https://staging.kaizencoach.training/callback',
'prod': 'https://www.kaizencoach.training/callback',
'demo-shane': 'https://demo-shane.kaizencoach.training/callback', # ADD NEW
}
# Around line 45-50: Add GCP project IDs
GCP_PROJECTS = {
'dev': 'kaizencoach-dev',
'staging': 'kaizencoach-staging',
'prod': 'kaizencoach-prod',
'demo': 'kaizencoach-demo',
'demo-shane': 'kaizencoach-shane', # ADD NEW
}
What These Do:
REDIRECT_URIS: Tells Strava OAuth where to send users after authenticationGCP_PROJECTS: Maps environment name to GCP project for Vertex AIWithout These:
When to Update:
gcloud projects create kaizencoach-dev --name="kAIzen Coach - Development"
gcloud projects create kaizencoach-staging --name="kAIzen Coach - Staging"
gcloud projects create kaizencoach-prod --name="kAIzen Coach - Production"
gcloud projects create kaizencoach-demo --name="kAIzen Coach - Demo"
or single project
gcloud projects create kaizencoach-PROJECT --name="kAIzen Coach - PROJECT"
CRITICAL: Vertex AI requires billing to be enabled. Without this, you’ll get “BILLING_DISABLED” errors.
First, find your billing account ID:
gcloud billing accounts list
Output looks like:
ACCOUNT_ID NAME OPEN MASTER_ACCOUNT_ID
01234-56789-CDEF01 My Billing Account True
Then link billing to each project:
# Set your billing account ID (replace with your actual ID from above)
BILLING_ACCOUNT_ID="xxxxxxxxxx"
# Link billing to all projects
for project in kaizencoach-dev kaizencoach-staging kaizencoach-prod kaizencoach-demo; do
gcloud billing projects link $project --billing-account=$BILLING_ACCOUNT_ID
echo "✅ Enabled billing for $project"
done
Or enable it for a single project:
gcloud billing projects link kaizencoach-PROJECT --billing-account=$BILLING_ACCOUNT_ID
Verify billing is enabled:
for project in kaizencoach-dev kaizencoach-staging kaizencoach-prod kaizencoach-demo; do
gcloud billing projects describe $project --format="value(billingEnabled)"
done
Or check for a single project:
gcloud billing projects describe kaizencoach-PROJECT --format="value(billingEnabled)"
Should output “True” for each project.
for project in kaizencoach-dev kaizencoach-staging kaizencoach-prod kaizencoach-demo; do
gcloud services enable aiplatform.googleapis.com --project=$project
echo "✅ Enabled Vertex AI for $project"
done
Or check for a single project:
gcloud services enable aiplatform.googleapis.com --project=kaizencoach-PROJECT
# Example for prod (repeat for staging, demo)
gcloud iam service-accounts create vertex-ai-sa \
--display-name="Vertex AI Service Account" \
--project=kaizencoach-prod
# Grant necessary permissions
gcloud projects add-iam-policy-binding kaizencoach-prod \
--member="serviceAccount:vertex-ai-sa@kaizencoach-prod.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"
# Generate key for AWS Secrets Manager
gcloud iam service-accounts keys create prod-sa-key.json \
--iam-account=vertex-ai-sa@kaizencoach-prod.iam.gserviceaccount.com \
--project=kaizencoach-prod
Your secrets are created by Terraform with these names:
Secret Names:
dev-kaizencoach-app-secrets (if deploying dev to AWS)staging-kaizencoach-app-secretsmy-personal-coach-app-secrets (prod - legacy name)demo-kaizencoach-app-secrets (or demo-{name}-kaizencoach-app-secrets)⚠️ CRITICAL: Each Environment Must Have Unique Secrets
Read SECRETS_GUIDE.md FIRST - it explains what each secret is and how to generate them.
NEVER copy secrets between environments! Generate new values for:
FLASK_SECRET_KEY - Different per environment (security isolation)GARMIN_ENCRYPTION_KEY - Different per environment (security isolation)STRAVA_VERIFY_TOKEN - Can be same or different (your choice)STRAVA_CLIENT_ID/SECRET - Must be different (each environment has its own Strava app)GOOGLE_APPLICATION_CREDENTIALS_JSON - Must be different (each environment has its own GCP service account)Secret Content (JSON):
{
"STRAVA_CLIENT_ID": "your_client_id",
"STRAVA_CLIENT_SECRET": "your_client_secret",
"STRAVA_VERIFY_TOKEN": "your_verify_token",
"FLASK_SECRET_KEY": "your_flask_secret",
"GOOGLE_APPLICATION_CREDENTIALS_JSON": "<contents of service account JSON>",
"GARMIN_ENCRYPTION_KEY": "your_garmin_key"
}
For demo instances with friends’ Strava apps, also add:
{
"STRAVA_CLIENT_ID": "friends_client_id",
"STRAVA_CLIENT_SECRET": "friends_client_secret",
"STRAVA_REDIRECT_URI": "https://demo-john.kaizencoach.training/callback",
...
}
Note: Terraform creates the secret placeholders. You populate them manually using AWS Console or CLI.
Generate new secrets first (see SECRETS_GUIDE.md for details):
# Generate NEW Flask secret key for this environment
openssl rand -hex 32
# Generate NEW Garmin encryption key for this environment
openssl rand -base64 32
# Generate NEW Strava verify token (optional - can reuse)
openssl rand -hex 20
Prepare the Service Account JSON:
Go to https://jsonformatter.org/json-to-one-line and convert the JSON block generated by GCP to a single line JSON
Then go to https://www.freeformatter.com/json-escape.html paste the single line JSON in and escape it.
This will give the correct format for the value for the GOOGLE_APPLICATION_CREDENTIALS_JSON key in AWS secretsmanager
Populate via AWS CLI:
# For prod (Terraform already created my-personal-coach-app-secrets)
aws secretsmanager put-secret-value \
--secret-id my-personal-coach-app-secrets \
--secret-string file://prod-secrets.json \
--region eu-west-1
# For staging (Terraform already created staging-kaizencoach-app-secrets)
aws secretsmanager put-secret-value \
--secret-id staging-kaizencoach-app-secrets \
--secret-string file://staging-secrets.json \
--region eu-west-1
Or via AWS Console:
FLASK_ENV=development
# No ENVIRONMENT variable needed - automatically uses 'dev'
GOOGLE_APPLICATION_CREDENTIALS=/path/to/kaizencoach-dev-sa-key.json
# ... other credentials
# Authenticate with your dev project
gcloud auth application-default login --project=kaizencoach-dev
# Then you don't need GOOGLE_APPLICATION_CREDENTIALS in .env
Note: These are configured in Terraform and automatically set when App Runner service is deployed.
For each environment, the following variables are set:
FLASK_ENV=productionENVIRONMENT=prod (or staging, demo)The app will then:
my-personal-coach-app-secrets (legacy name){ENVIRONMENT}-kaizencoach-app-secretsmy-personal-coach-users (legacy name){ENVIRONMENT}-kaizencoach-userskaizencoach-data (legacy name){ENVIRONMENT}-kaizencoach-dataThis documents how the Terraform is structured to support multi-environment deployments with conditional legacy naming for prod.
variable "environment" {
description = "Environment name (dev, staging, prod, demo)"
type = string
}
# DynamoDB - prod keeps legacy name
resource "aws_dynamodb_table" "users" {
name = var.environment == "prod" ? "my-personal-coach-users" : "${var.environment}-kaizencoach-users"
# ...
}
# S3 - prod keeps legacy name
resource "aws_s3_bucket" "data" {
bucket = var.environment == "prod" ? "kaizencoach-data" : "${var.environment}-kaizencoach-data"
# ...
}
# Secrets Manager - prod keeps legacy name
resource "aws_secretsmanager_secret" "app_secrets" {
name = var.environment == "prod" ? "my-personal-coach-app-secrets" : "${var.environment}-kaizencoach-app-secrets"
# ...
}
Note: Production keeps legacy names to avoid data migration. Update in future when ready.
locals {
common_tags = {
Application = "kaizencoach"
Environment = var.environment
ManagedBy = "terraform"
CostCenter = "kaizencoach-${var.environment}"
}
}
# Apply to all resources
tags = local.common_tags
resource "aws_apprunner_service" "app" {
# ...
source_configuration {
image_repository {
image_configuration {
runtime_environment_variables = {
FLASK_ENV = "production"
ENVIRONMENT = var.environment
}
}
}
}
}
Environment = prod (or staging, demo)Cost Center or Environmentkaizencoach-prod (or staging, demo, dev)This gives you clean separation of costs across all environments!
Symptom:
Error generating content from prompt: 403 This API method requires billing to be enabled.
Please enable billing on project #kaizencoach-prod
[reason: "BILLING_DISABLED"]
Cause: Vertex AI requires billing to be enabled on the GCP project.
Solution:
# 1. Get your billing account ID
gcloud billing accounts list
# 2. Link billing to the project
gcloud billing projects link kaizencoach-prod --billing-account=YOUR_BILLING_ACCOUNT_ID
# 3. Verify billing is enabled
gcloud billing projects describe kaizencoach-prod --format="value(billingEnabled)"
# Should output: True
# 4. Wait 2-3 minutes for changes to propagate
# 5. Restart App Runner or test locally
Note: This is the most common issue when setting up new GCP projects!
Symptom: Authentication errors or “permission denied” when calling Vertex AI
Solution:
# Verify service account has correct role
gcloud projects get-iam-policy kaizencoach-prod \
--flatten="bindings[].members" \
--filter="bindings.members:serviceAccount:vertex-ai-sa@kaizencoach-prod.iam.gserviceaccount.com"
# Should show: roles/aiplatform.user
# If not, add it:
gcloud projects add-iam-policy-binding kaizencoach-prod \
--member="serviceAccount:vertex-ai-sa@kaizencoach-prod.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"
Symptom: “API not enabled” errors
Solution:
gcloud services enable aiplatform.googleapis.com --project=kaizencoach-prod
Future cleanup (track in GitHub issue):
my-personal-coach-users → kaizencoach-users-prod (via Terraform)kaizencoach-data → kaizencoach-data-prod (via Terraform)my-personal-coach-app → prod-kaizencoach-app (optional)Before deploying to staging/demo, ensure these files use Config instead of hardcoded values:
Files that need updating:
data_manager.py - Line 92-93: Use Config.DYNAMODB_TABLE and Config.AWS_REGIONs3_manager.py - Line 11-13: Use Config.S3_BUCKET and Config.AWS_REGIONThese files will work in dev (local file backend) but fail in staging/prod if not updated.
# Just run normally - FLASK_ENV=development automatically uses 'dev'
python app.py
# Should connect to kaizencoach-dev GCP project
# Uses dev AWS resources (DynamoDB, S3) or local development setup
# Deploy with ENVIRONMENT=staging and FLASK_ENV=production
# Verify logs show: "Environment: staging", "Project: kaizencoach-staging"
# Deploy with ENVIRONMENT=prod and FLASK_ENV=production
# Verify logs show: "Environment: prod", "Project: kaizencoach-prod"