Multi-environment deployment, architecture, and operational guides
This guide explains every secret used in kAIzen Coach, what it’s for, where it comes from, and how to generate it.
All application secrets are stored in AWS Secrets Manager and loaded at runtime by the Flask application. Terraform creates the secret placeholder, but you must manually populate it.
Secret Name Pattern:
my-personal-coach-app-secrets (legacy){environment}-kaizencoach-app-secretsWhat it is: Your Strava API application’s client ID (public identifier)
Where it comes from: Strava API Settings page
How to get it:
Format: Numeric string
"STRAVA_CLIENT_ID": "176694"
Used for:
What it is: Your Strava API application’s client secret (private key)
Where it comes from: Strava API Settings page
How to get it:
Format: Hexadecimal string
"STRAVA_CLIENT_SECRET": "a1b2c3d4e5f6789012345678901234567890abcd"
Used for:
What it is: A secret token YOU CREATE to verify webhook requests are from Strava
Where it comes from: YOU GENERATE THIS YOURSELF
How to generate it:
# Generate a random 40-character hex string
openssl rand -hex 20
# Example output:
# cb4fda9a37786db2cbfc7905e5458fe75874ed5a
Format: Any random string (recommend 40+ characters)
"STRAVA_VERIFY_TOKEN": "cb4fda9a37786db2cbfc7905e5458fe75874ed5a"
Used for:
Important:
What it is: Flask’s session encryption key
Where it comes from: YOU GENERATE THIS YOURSELF
How to generate it:
# Generate a random 64-character hex string
openssl rand -hex 32
# Or use Python:
python3 -c "import secrets; print(secrets.token_hex(32))"
# Example output:
# 8f7d6e5c4b3a2918e7f6d5c4b3a29180f7e6d5c4b3a29180e7f6d5c4b3a2918
Format: Long random string (recommend 64+ characters)
"FLASK_SECRET_KEY": "8f7d6e5c4b3a2918e7f6d5c4b3a29180f7e6d5c4b3a29180e7f6d5c4b3a2918"
Used for:
Security:
What it is: GCP service account credentials (entire JSON key file)
Where it comes from: Google Cloud Console
How to get it:
vertex-ai-sa@kaizencoach-staging.iam.gserviceaccount.com)How to format for Secrets Manager:
# Convert to single-line JSON (no newlines)
cat .keys/kaizencoach-staging-sa-key.json | jq -c '.'
# This produces a compact single-line JSON string
Format: Complete JSON key file as a single-line string
"GOOGLE_APPLICATION_CREDENTIALS_JSON": "{\"type\":\"service_account\",\"project_id\":\"kaizencoach-staging\",\"private_key_id\":\"abc123...\",\"private_key\":\"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\"client_email\":\"vertex-ai-sa@kaizencoach-staging.iam.gserviceaccount.com\",\"client_id\":\"123456789\",\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\"token_uri\":\"https://oauth2.googleapis.com/token\",\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\",\"client_x509_cert_url\":\"https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-sa%40kaizencoach-staging.iam.gserviceaccount.com\"}"
Used for:
Critical Notes:
\n - these must be preservedWhat it is: Symmetric encryption key for storing Garmin credentials
Where it comes from: YOU GENERATE THIS YOURSELF
How to generate it:
# Generate a 32-byte (256-bit) key, base64 encoded
openssl rand -base64 32
# Or use Python:
python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"
# Example output:
# XyZ9AbC8dEf7GhI6JkL5MnO4PqR3StU2VwX1YzA0BcD=
Format: Base64-encoded 32-byte key
"GARMIN_ENCRYPTION_KEY": "XyZ9AbC8dEf7GhI6JkL5MnO4PqR3StU2VwX1YzA0BcD="
Used for:
Security:
What it is: The GCP project ID for this environment
Where it comes from: Set when creating GCP project
Format: String
"GCP_PROJECT_ID": "kaizencoach-staging"
Used for:
Note: This is technically optional as config.py can determine it from the environment variable or parse it from the service account JSON.
What it is: GCP region for Vertex AI
Format: String
"GCP_LOCATION": "europe-west1"
Note: This is typically hardcoded in config.py and doesn’t need to be in secrets.
{
"STRAVA_CLIENT_ID": "your_strava_app_client_id",
"STRAVA_CLIENT_SECRET": "your_strava_app_client_secret",
"STRAVA_VERIFY_TOKEN": "random_40_char_token_you_generated",
"FLASK_SECRET_KEY": "random_64_char_key_you_generated",
"GOOGLE_APPLICATION_CREDENTIALS_JSON": "{\"type\":\"service_account\",\"project_id\":\"kaizencoach-staging\",...}",
"GARMIN_ENCRYPTION_KEY": "base64_encoded_32_byte_key"
}
www.kaizencoach.trainingkaizencoach-prodstaging.kaizencoach.trainingkaizencoach-stagingdemo-alice.kaizencoach.training)kaizencoach-demo (can be shared or separate)# DON'T commit this file!
cat > /tmp/staging-secrets.json << 'EOF'
{
"STRAVA_CLIENT_ID": "188207",
"STRAVA_CLIENT_SECRET": "abc123def456...",
"STRAVA_VERIFY_TOKEN": "cb4fda9a37786db2cbfc7905e5458fe75874ed5a",
"FLASK_SECRET_KEY": "8f7d6e5c4b3a2918e7f6d5c4b3a29180...",
"GOOGLE_APPLICATION_CREDENTIALS_JSON": "{\"type\":\"service_account\"...}",
"GARMIN_ENCRYPTION_KEY": "XyZ9AbC8dEf7GhI6JkL5MnO4PqR3StU2VwX1YzA0BcD="
}
EOF
# For staging
aws secretsmanager put-secret-value \
--secret-id staging-kaizencoach-app-secrets \
--secret-string file:///tmp/staging-secrets.json \
--region eu-west-1
# For prod
aws secretsmanager put-secret-value \
--secret-id my-personal-coach-app-secrets \
--secret-string file:///tmp/staging-secrets.json \
--region eu-west-1
rm /tmp/staging-secrets.json
# Force App Runner to reload secrets
aws apprunner start-deployment \
--service-arn <your-service-arn> \
--region eu-west-1
Check application logs after restart:
aws logs tail /aws/apprunner/staging-kaizencoach-service/service --since 5m --region eu-west-1 | grep "Secrets loaded"
Should show:
✅ Secrets loaded - STRAVA_CLIENT_ID: 188207...
# 1. Generate new secrets (don't overwrite existing yet!)
openssl rand -hex 32 # New Flask key
openssl rand -base64 32 # New Garmin key
# 2. Update secrets in Secrets Manager
# (follow same steps as populating)
# 3. For Garmin key rotation:
# WARNING: Rotating Garmin key will invalidate all stored passwords!
# Users will need to re-authenticate with Garmin
# 4. Restart App Runner
aws apprunner start-deployment --service-arn <arn> --region eu-west-1
# 5. Verify new secrets loaded in logs
.gitignore: *secrets*.json, *.key, .keys/Symptom: App crashes on startup with “SECRET_KEY not found”
Fix: Verify secrets exist in Secrets Manager and App Runner IAM role has secretsmanager:GetSecretValue permission
Symptom: “Invalid client_id” error Fix: Check STRAVA_CLIENT_ID matches the Strava app for this environment’s domain
Symptom: Webhook subscription fails with 403 Fix: Ensure STRAVA_VERIFY_TOKEN in secrets matches the token you’re using in the API call
Symptom: Error decrypting Garmin password Fix: GARMIN_ENCRYPTION_KEY may have changed - users need to re-authenticate
Symptom: “Could not load default credentials” or 403 from Vertex AI Fix:
roles/aiplatform.user in GCP# Generate Flask secret
openssl rand -hex 32
# Generate Garmin encryption key
openssl rand -base64 32
# Generate Strava verify token
openssl rand -hex 20
# Format GCP service account JSON
cat key.json | jq -c '.'
# View secrets (sanitized)
aws secretsmanager get-secret-value \
--secret-id staging-kaizencoach-app-secrets \
--region eu-west-1 \
--query SecretString --output text | jq .
# Update secrets
aws secretsmanager put-secret-value \
--secret-id staging-kaizencoach-app-secrets \
--secret-string file:///tmp/secrets.json \
--region eu-west-1
# Force app to reload secrets
aws apprunner start-deployment --service-arn <arn> --region eu-west-1