How would you structure environment variables for API keys in production?
#gen-ai#python#mlops
Answer
Environment Variables and API Key Management in Production
Managing secrets securely is non-negotiable. Hardcoding API keys causes security incidents and billing nightmares.
Development: text.env
Files
text
.envbash# .env (NEVER commit to git) OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... PINECONE_API_KEY=... LANGCHAIN_API_KEY=ls_... DATABASE_URL=postgresql://user:pass@localhost/mydb # Environment name ENVIRONMENT=development LOG_LEVEL=DEBUG
bash# .gitignore — always include .env .env.local .env.*.local *.pem *.key
Loading with Pydantic Settings (Recommended)
pythonfrom pydantic_settings import BaseSettings from pydantic import SecretStr, validator from functools import lru_cache from enum import Enum class Environment(str, Enum): DEVELOPMENT = "development" STAGING = "staging" PRODUCTION = "production" class Settings(BaseSettings): # Secrets — masked in logs and repr openai_api_key: SecretStr anthropic_api_key: SecretStr | None = None pinecone_api_key: SecretStr | None = None # Config environment: Environment = Environment.DEVELOPMENT log_level: str = "INFO" llm_model: str = "gpt-4o" max_tokens: int = 2048 @validator("openai_api_key") def validate_api_key(cls, v): if not str(v.get_secret_value()).startswith("sk-"): raise ValueError("Invalid OpenAI API key format") return v class Config: env_file = ".env" env_file_encoding = "utf-8" case_sensitive = False @lru_cache() def get_settings() -> Settings: '''Singleton — settings loaded once and cached.''' return Settings() # Usage settings = get_settings() # Access secret: settings.openai_api_key.get_secret_value() # In logs: settings.openai_api_key → "**********" (masked)
Production: Use a Secrets Manager
Never store secrets as environment variables in production containers. Use:
pythonimport boto3 import json from functools import lru_cache @lru_cache() def get_secret(secret_name: str, region: str = "us-east-1") -> dict: '''Fetch secrets from AWS Secrets Manager.''' client = boto3.client("secretsmanager", region_name=region) response = client.get_secret_value(SecretId=secret_name) return json.loads(response["SecretString"]) # Usage secrets = get_secret("myapp/production/api-keys") openai_key = secrets["OPENAI_API_KEY"]
Alternative services:
- GCP Secret Manager: text
google-cloud-secret-manager - HashiCorp Vault: Python clienttext
hvac - Azure Key Vault: text
azure-keyvault-secrets
Different Keys per Environment
pythonimport os def get_llm_client(): env = os.getenv("ENVIRONMENT", "development") if env == "production": # High-capacity key from secrets manager api_key = get_secret("prod/openai")["key"] model = "gpt-4o" elif env == "staging": # Separate staging key with spending limits api_key = get_secret("staging/openai")["key"] model = "gpt-4o-mini" else: # Local dev key from .env api_key = os.getenv("OPENAI_API_KEY") model = "gpt-4o-mini" # Cheaper for dev from openai import OpenAI return OpenAI(api_key=api_key), model
Security Checklist
| Rule | Implementation |
|---|---|
| Never hardcode secrets | Use text |
| Never commit text | Add to text |
| Rotate keys regularly | Automate via secrets manager |
| Separate keys per env | Dev, staging, prod keys |
| Set spending limits | OpenAI dashboard → Usage limits |
| Monitor usage | Alerts for anomalous API spend |
| Least privilege | Each service gets only keys it needs |
Incident prevention: Run
ortextgit-secretsin your CI pipeline to automatically detect accidentally committed secrets before they reach the remote repository.texttruffleHog