Dev Dissection — Week 9: Environment Variables & Secrets Management

In Week 8, you deployed your TODO app to Google Cloud and encountered the challenge of managing sensitive data like database passwords and JWT secrets. This week, we’re diving deep into the art of environment variables and secrets management—a critical skill that separates amateur projects from production-ready applications.

You probably ran into that ValueError: Invalid secret spec error when trying to deploy. By the end of this lesson, you’ll master Google Secret Manager, understand the hierarchy of configuration management, and build secure CI/CD pipelines that handle secrets like a pro.

Prerequisites

Before you start, you need:

  • Your Week 8 deployment working (or attempted!)
  • Google Cloud Platform project set up
  • Basic understanding of environment variables
  • A GitHub repository for your TODO app

The Configuration Hierarchy: From Chaos to Order

The Problem: Configuration Hell

Without proper environment management, projects become unmaintainable:

Bad: Hardcoded in the source code
const DATABASE_URL = "mongodb://localhost:27017/todos";
const JWT_SECRET = "super-secret-key";

Worse: Different configs scattered everywhere  
- .env.local
- .env.development  
- .env.prod
- .env.staging.backup
- config.js
- package.json scripts

The Solution: Configuration Hierarchy

Modern applications follow a clear hierarchy for configuration:

1. Defaults (in code)           ← Least specific, always present
2. Environment files (.env)     ← Development convenience  
3. Environment variables        ← Runtime configuration
4. Secret Manager              ← Sensitive data (highest security)
5. Command line arguments      ← Override everything

Part 1: Understanding Environment Variables

What Are Environment Variables?

Environment variables are key-value pairs that exist outside your application code, allowing you to configure behavior without changing source code.

# Setting environment variables
export NODE_ENV=production
export PORT=8080
export DEBUG=false

# Accessing in Node.js
console.log(process.env.NODE_ENV);  // "production"
console.log(process.env.PORT);      // "8080"

Types of Configuration Data

Public Configuration (safe for environment variables):

  • NODE_ENV (development, staging, production)
  • PORT (server port)
  • LOG_LEVEL (debug, info, warn, error)
  • FEATURE_FLAGS (boolean switches)

Sensitive Configuration (requires Secret Manager):

  • Database connection strings
  • API keys and tokens
  • JWT secrets
  • Third-party service credentials
  • Encryption keys

Part 3: Google Secret Manager Mastery

Why Secret Manager?

Google recommends avoiding passing secrets to your application through the file system or through environment variables for security reasons. Secret Manager encrypts your secret data in transit using TLS and at rest with AES-256-bit encryption keys.

Setting Up Secret Manager

1. Enable the API:

gcloud services enable secretmanager.googleapis.com

2. Create Your Secrets:

# Staging secrets
echo -n "mongodb+srv://staging-user:different-password@cluster.net/todos-staging?retryWrites=true&w=majority" | \
  gcloud secrets create mongo-staging-uri --data-file=-

echo -n "staging-jwt-secret-super-secure-different-from-dev" | \
  gcloud secrets create jwt-staging-secret --data-file=-

# Production secrets
echo -n "mongodb+srv://prod-user:very-secure-password@cluster.net/todos-production?retryWrites=true&w=majority" | \
  gcloud secrets create mongo-production-uri --data-file=-

echo -n "production-jwt-secret-super-secure-completely-different" | \
  gcloud secrets create jwt-production-secret --data-file=-

3. Verify Your Secrets:

# List all secrets
gcloud secrets list

# View secret details (not the actual value)
gcloud secrets describe mongo-staging-uri

# Access secret value (for testing only)
gcloud secrets versions access latest --secret="mongo-staging-uri"

Secret Manager Best Practices

1. Secret Naming Convention:

# Good naming pattern: <service>-<env>-<type>
mongo-dev-uri
mongo-staging-uri  
mongo-production-uri
jwt-dev-secret
jwt-staging-secret
jwt-production-secret
stripe-production-key

2. Version Management:

# Create a new version of an existing secret
echo -n "new-password-value" | \
  gcloud secrets versions add mongo-production-uri --data-file=-

# Pin to specific versions in production (recommended)
--set-secrets MONGO_URI=mongo-production-uri:3
  
# Use latest for development/staging (acceptable)
--set-secrets MONGO_URI=mongo-staging-uri:latest

3. Access Control:

# Grant specific service account access to secrets
gcloud secrets add-iam-policy-binding mongo-production-uri \
  --member="serviceAccount:my-app@project.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

Part 4: Cloud Run Integration with Secret Manager

Deploying with Secret Manager

Here’s the corrected deployment from Week 8 that actually works:

Backend Deployment:

gcloud run deploy todo-backend-staging \
  --image us-central1-docker.pkg.dev/your-project-id/todo-app/backend:staging \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars NODE_ENV=staging,PORT=8080,LOG_LEVEL=info \
  --set-secrets MONGO_URI=mongo-staging-uri:latest,JWT_SECRET=jwt-staging-secret:latest \
  --memory 512Mi \
  --cpu 1 \
  --concurrency 80 \
  --max-instances 3 \
  --min-instances 0 \
  --cpu-throttling \
  --port 8080

---

gcloud run deploy todo-backend-production \
  --image us-central1-docker.pkg.dev/your-project-id/todo-app/backend:staging \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars NODE_ENV=staging,PORT=8080,LOG_LEVEL=info \
  --set-secrets MONGO_URI=mongo-production-uri:latest,JWT_SECRET=jwt-production-secret:latest \
  --memory 512Mi \
  --cpu 1 \
  --concurrency 80 \
  --max-instances 3 \
  --min-instances 0 \
  --cpu-throttling \
  --port 8080

Extra: Environment-Specific Features

Feature Flags with Environment Variables

Implement environment-specific behavior:

Enhanced configuration:

export interface AppConfig {
  // ... existing config
  FEATURES: {
    ENABLE_REGISTRATION: boolean;
    ENABLE_RATE_LIMITING: boolean;
    ENABLE_DEBUG_LOGS: boolean;
    MAX_TODOS_PER_USER: number;
    ENABLE_EMAIL_VERIFICATION: boolean;
  };
}

export function loadConfig(): AppConfig {
  // ... existing code
  
  return {
    // ... existing config
    FEATURES: {
      ENABLE_REGISTRATION: process.env.ENABLE_REGISTRATION !== 'false',
      ENABLE_RATE_LIMITING: process.env.NODE_ENV === 'production',
      ENABLE_DEBUG_LOGS: process.env.NODE_ENV === 'development',
      MAX_TODOS_PER_USER: config.NODE_ENV === 'production' ? 50 : 1000,
      ENABLE_EMAIL_VERIFICATION: process.env.NODE_ENV === 'production',
    }
  };
}

Feature-aware API routes:

import { loadConfig } from './loadEnv';

const config = loadConfig();

// Registration endpoint with feature flag
router.post('/register', async (req, res) => {
  if (!config.FEATURES.ENABLE_REGISTRATION) {
    return res.status(403).json({ 
      error: 'Registration is currently disabled' 
    });
  }
  
  // ... existing registration logic
});

// Todo creation with user limits
router.post('/todos', authenticate, async (req, res) => {
  const userTodoCount = await Todo.countDocuments({ 
    user: req.user!.userId 
  });
  
  if (userTodoCount >= config.FEATURES.MAX_TODOS_PER_USER) {
    return res.status(429).json({ 
      error: `Maximum ${config.FEATURES.MAX_TODOS_PER_USER} todos allowed` 
    });
  }
  
  // ... existing todo creation logic
});

Part 5: Monitoring and Debugging

Secret Manager Monitoring

# Check secret access logs
gcloud logging read 'resource.type="secret_manager_secret"' \
  --limit=50 \
  --format="table(timestamp,protoPayload.methodName,protoPayload.authenticationInfo.principalEmail)"

# Monitor secret usage
gcloud secrets list --format="table(name,createTime,updateTime)"

Environment Variable Debugging

Add a debug endpoint for non-production environments:

// Debug endpoint (non-production only)
if (config.NODE_ENV !== 'production') {
  app.get('/debug/config', (req, res) => {
    res.json({
      NODE_ENV: config.NODE_ENV,
      PORT: config.PORT,
      LOG_LEVEL: config.LOG_LEVEL,
      FEATURES: config.FEATURES,
      // Never expose actual secrets!
      SECRETS_LOADED: {
        MONGO_URI: !!config.MONGO_URI,
        JWT_SECRET: !!config.JWT_SECRET,
      }
    });
  });
}

Common Issues and Solutions

Issue 1: Secret not found

Error: Secret [projects/123/secrets/mongo-staging-uri] not found

Solution: Check secret exists and has proper IAM permissions:

gcloud secrets list | grep mongo-staging-uri
gcloud secrets get-iam-policy mongo-staging-uri

Issue 2: Environment variables not loading

Error: MONGO_URI is required

Solution: Check Cloud Run service configuration:

gcloud run services describe todo-backend-staging \
  --region=us-central1 \
  --format="value(spec.template.spec.template.spec.containers[0].env[].name)"

Part 6: Security Best Practices

Secret Rotation Strategy

# 1. Create new secret version
echo -n "new-super-secure-password" | \
  gcloud secrets versions add mongo-production-uri --data-file=-

# 2. Update Cloud Run service to use new version
gcloud run services update todo-backend-production \
  --set-secrets MONGO_URI=mongo-production-uri:latest

# 3. Test thoroughly

# 4. Disable old version (after confirming new one works)
gcloud secrets versions disable 1 --secret=mongo-production-uri

Access Control

# Principle of least privilege - only grant access to specific secrets
gcloud secrets add-iam-policy-binding mongo-production-uri \
  --member="serviceAccount:todo-backend@project.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

# Don't use the default compute service account
# Create dedicated service accounts for each service

Audit and Compliance

# Regular secret audit
gcloud secrets list --format="table(name,createTime,updateTime)" \
  --filter="createTime<'-30d'"

# Review access patterns  
gcloud logging read 'protoPayload.serviceName="secretmanager.googleapis.com"' \
  --limit=100 \
  --format="table(timestamp,protoPayload.methodName,protoPayload.resourceName)"

What You’ve Learned

This week, you mastered the critical skill of configuration management:

  • Environment Variable Hierarchy – Understanding the order of configuration precedence
  • Local Development Setup – Proper .env file organization for different environments
  • Google Secret Manager Mastery – Securely storing and accessing sensitive configuration
  • Cloud Run Integration – Deploying applications with proper secret management
  • Feature Flags – Environment-specific application behavior
  • Security Best Practices – Secret rotation, access control, and auditing

Coming Up: Tracking & Events

Your app is secure and functional, but how do you understand how users actually interact with it? Next week, we’ll explore the world of user analytics and behavioral tracking:

  • Amplitude integration – Setting up event tracking, user properties, and conversion funnels
  • User behavior analytics – Understanding user journeys, retention metrics, and feature adoption
  • Hotjar implementation – Heatmaps, session recordings, and user feedback collection

By the end, you’ll have comprehensive insights into user behavior, enabling data-driven decisions for your product’s growth.


Need Help?

Configuration management can be tricky! If you get stuck:

  • Check secret permissions: gcloud secrets get-iam-policy your-secret-name
  • Verify environment loading: Add debug logs to your loadEnv.ts
  • Test locally first: Use .env.development files before cloud deployment
  • Join our Discord for help with specific error messages

Your TODO app now handles secrets and configuration like a Fortune 500 company!