356 lines
8.3 KiB
Markdown
356 lines
8.3 KiB
Markdown
# Runtime Configuration Pattern
|
|
|
|
## Overview
|
|
|
|
MotoVaultPro uses a **K8s-aligned runtime configuration pattern** where sensitive values (like API keys) are loaded at container startup from mounted secrets, not at build time.
|
|
|
|
This approach:
|
|
- Mirrors Kubernetes deployment patterns
|
|
- Allows configuration changes without rebuilding images
|
|
- Keeps secrets out of build artifacts and environment variables
|
|
- Enables easy secret rotation in production
|
|
|
|
## Architecture
|
|
|
|
### How It Works
|
|
|
|
1. **Build Time**: Container is built WITHOUT secrets (no API keys in image)
|
|
2. **Container Startup**:
|
|
- `/app/load-config.sh` reads `/run/secrets/google-maps-api-key` and `/run/secrets/google-maps-map-id`
|
|
- Generates `/usr/share/nginx/html/config.js` with runtime values
|
|
- Starts nginx
|
|
3. **App Load Time**:
|
|
- `index.html` loads `<script src="/config.js"></script>`
|
|
- `window.CONFIG` is available before React initializes
|
|
- React app reads configuration via `getConfig()` hook
|
|
|
|
### File Structure
|
|
|
|
```
|
|
frontend/
|
|
├── scripts/
|
|
│ └── load-config.sh # Reads secrets, generates config.js
|
|
├── src/
|
|
│ └── core/config/
|
|
│ └── config.types.ts # TypeScript types and helpers
|
|
├── index.html # Loads config.js before app
|
|
└── Dockerfile # Runs load-config.sh before nginx
|
|
```
|
|
|
|
## Usage in Components
|
|
|
|
### Reading Configuration
|
|
|
|
```typescript
|
|
import { getConfig, getGoogleMapsApiKey } from '@/core/config/config.types';
|
|
|
|
export function MyComponent() {
|
|
// Get full config object with error handling
|
|
const config = getConfig();
|
|
|
|
// Or get specific value with fallback
|
|
const apiKey = getGoogleMapsApiKey();
|
|
|
|
return <MapComponent apiKey={apiKey} />;
|
|
}
|
|
```
|
|
|
|
### Conditional Features
|
|
|
|
```typescript
|
|
import { isConfigLoaded } from '@/core/config/config.types';
|
|
|
|
export function FeatureGate() {
|
|
if (!isConfigLoaded()) {
|
|
return <LoadingFallback />;
|
|
}
|
|
|
|
return <AdvancedFeature />;
|
|
}
|
|
```
|
|
|
|
## Adding New Runtime Configuration Values
|
|
|
|
### 1. Update load-config.sh
|
|
|
|
```bash
|
|
# In frontend/scripts/load-config.sh
|
|
if [ -f "$SECRETS_DIR/new-api-key" ]; then
|
|
NEW_API_KEY=$(cat "$SECRETS_DIR/new-api-key")
|
|
echo "[Config] Loaded New API Key"
|
|
else
|
|
NEW_API_KEY=""
|
|
fi
|
|
|
|
# In the config.js generation:
|
|
cat > "$CONFIG_FILE" <<EOF
|
|
window.CONFIG = {
|
|
googleMapsApiKey: '$GOOGLE_MAPS_API_KEY',
|
|
googleMapsMapId: '$GOOGLE_MAPS_MAP_ID',
|
|
newApiKey: '$NEW_API_KEY'
|
|
};
|
|
EOF
|
|
```
|
|
|
|
### 2. Update config.types.ts
|
|
|
|
```typescript
|
|
export interface AppConfig {
|
|
googleMapsApiKey: string;
|
|
googleMapsMapId?: string;
|
|
newApiKey: string; // Add new field
|
|
}
|
|
|
|
export function getNewApiKey(): string {
|
|
try {
|
|
const config = getConfig();
|
|
return config.newApiKey || '';
|
|
} catch {
|
|
console.warn('New API Key not available.');
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export function getGoogleMapsMapId(): string {
|
|
try {
|
|
const config = getConfig();
|
|
return config.googleMapsMapId || '';
|
|
} catch {
|
|
console.warn('Google Maps Map ID not available.');
|
|
return '';
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Update docker-compose.yml
|
|
|
|
```yaml
|
|
mvp-frontend:
|
|
volumes:
|
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
|
- ./secrets/app/google-maps-map-id.txt:/run/secrets/google-maps-map-id:ro
|
|
- ./secrets/app/new-api-key.txt:/run/secrets/new-api-key:ro # Add new secret
|
|
```
|
|
|
|
### 4. Create Secret File
|
|
|
|
```bash
|
|
mkdir -p ./secrets/app
|
|
echo "your-api-key-value" > ./secrets/app/new-api-key.txt
|
|
```
|
|
|
|
## Docker-Compose Configuration
|
|
|
|
The frontend service mounts secrets from the host filesystem:
|
|
|
|
```yaml
|
|
mvp-frontend:
|
|
environment:
|
|
SECRETS_DIR: /run/secrets
|
|
volumes:
|
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
|
```
|
|
|
|
- `:ro` flag makes secrets read-only
|
|
- Secrets are available at container startup
|
|
- Changes require container restart (no image rebuild)
|
|
|
|
## Kubernetes Deployment
|
|
|
|
When deploying to Kubernetes, update the deployment manifest:
|
|
|
|
```yaml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: motovaultpro-frontend
|
|
spec:
|
|
template:
|
|
spec:
|
|
containers:
|
|
- name: frontend
|
|
image: motovaultpro-frontend:latest
|
|
env:
|
|
- name: SECRETS_DIR
|
|
value: /run/secrets
|
|
volumeMounts:
|
|
- name: google-maps-key
|
|
mountPath: /run/secrets/google-maps-api-key
|
|
subPath: google-maps-api-key
|
|
readOnly: true
|
|
volumes:
|
|
- name: google-maps-key
|
|
secret:
|
|
secretName: google-maps-api-key
|
|
items:
|
|
- key: api-key
|
|
path: google-maps-api-key
|
|
```
|
|
|
|
## Development Setup
|
|
|
|
### Local Development
|
|
|
|
For `npm run dev` (Vite dev server):
|
|
|
|
```bash
|
|
# Copy secrets to secrets directory
|
|
mkdir -p ./secrets/app
|
|
echo "your-test-api-key" > ./secrets/app/google-maps-api-key.txt
|
|
|
|
# Set environment variable
|
|
export SECRETS_DIR=./secrets/app
|
|
|
|
# Start Vite dev server
|
|
npm run dev
|
|
```
|
|
|
|
To access config in your app:
|
|
|
|
```typescript
|
|
// In development, config.js may not exist
|
|
// Use graceful fallback:
|
|
export function useConfig() {
|
|
const [config, setConfig] = useState<AppConfig | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (window.CONFIG) {
|
|
setConfig(window.CONFIG);
|
|
} else {
|
|
// Fallback for dev without config.js
|
|
setConfig({
|
|
googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_KEY || '',
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
return config;
|
|
}
|
|
```
|
|
|
|
### Container-Based Testing
|
|
|
|
Recommended approach (per CLAUDE.md):
|
|
|
|
```bash
|
|
# Ensure secrets exist
|
|
mkdir -p ./secrets/app
|
|
echo "your-api-key" > ./secrets/app/google-maps-api-key.txt
|
|
|
|
# Rebuild and start containers
|
|
make rebuild
|
|
make logs
|
|
|
|
# Verify config.js was generated
|
|
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
|
|
|
|
# Should output:
|
|
# window.CONFIG = {
|
|
# googleMapsApiKey: 'your-api-key'
|
|
# };
|
|
```
|
|
|
|
## Debugging
|
|
|
|
### Verify Secrets are Mounted
|
|
|
|
```bash
|
|
docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key
|
|
```
|
|
|
|
### Check Generated config.js
|
|
|
|
```bash
|
|
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
|
|
```
|
|
|
|
### View Container Logs
|
|
|
|
```bash
|
|
docker compose logs mvp-frontend
|
|
```
|
|
|
|
Look for lines like:
|
|
```
|
|
[Config] Loaded Google Maps API key from /run/secrets/google-maps-api-key
|
|
[Config] Generated /usr/share/nginx/html/config.js
|
|
```
|
|
|
|
### Browser Console
|
|
|
|
Check browser console for any config loading errors:
|
|
|
|
```javascript
|
|
console.log(window.CONFIG);
|
|
// Should show: { googleMapsApiKey: "your-key" }
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Never Log Secrets**: The load-config.sh script only logs that a key was loaded, never the actual value
|
|
|
|
2. **Always Validate**: Use `getConfig()` which throws errors if config is missing:
|
|
```typescript
|
|
try {
|
|
const config = getConfig();
|
|
} catch (error) {
|
|
// Handle missing config gracefully
|
|
}
|
|
```
|
|
|
|
3. **Use Fallbacks**: For optional features, use graceful fallbacks:
|
|
```typescript
|
|
const apiKey = getGoogleMapsApiKey(); // Returns empty string if not available
|
|
```
|
|
|
|
4. **Documentation**: Update this file when adding new configuration values
|
|
|
|
5. **Testing**: Test with and without secrets in containers
|
|
|
|
## Security Considerations
|
|
|
|
- Secrets are mounted as files, not environment variables
|
|
- Files are read-only (`:ro` flag)
|
|
- config.js is generated at startup, not included in image
|
|
- Browser console can see config values (like any JavaScript)
|
|
- For highly sensitive values, consider additional encryption
|
|
|
|
## Troubleshooting
|
|
|
|
### config.js not generated
|
|
|
|
**Symptom**: Browser shows `window.CONFIG is undefined`
|
|
|
|
**Solutions**:
|
|
1. Check secret file exists: `docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key`
|
|
2. Check load-config.sh runs: `docker compose logs mvp-frontend`
|
|
3. Verify permissions: `docker compose exec mvp-frontend ls -la /run/secrets/`
|
|
|
|
### Container fails to start
|
|
|
|
**Symptom**: Container crashes during startup
|
|
|
|
**Solution**:
|
|
1. Check logs: `docker compose logs mvp-frontend`
|
|
2. Verify script has execute permissions (in Dockerfile)
|
|
3. Test script locally: `sh frontend/scripts/load-config.sh`
|
|
|
|
### Secret changes not reflected
|
|
|
|
**Symptom**: Container still uses old secret after file change
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Restart container to reload secrets
|
|
docker compose restart mvp-frontend
|
|
|
|
# Or fully rebuild
|
|
make rebuild
|
|
```
|
|
|
|
## References
|
|
|
|
- [Kubernetes Secrets Documentation](https://kubernetes.io/docs/concepts/configuration/secret/)
|
|
- [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/)
|
|
- [12Factor Config](https://12factor.net/config)
|