Skip to content

Secrets & ConfigMaps ​

Learn how to securely manage secrets and configuration in tsops.

Secret Management ​

tsops provides powerful secret management with automatic validation.

Tip: Root-level secret/configMap definitions run during Node execution, so read environment variables with process.env. The env() helper is only available inside app-level env functions.

Basic Usage ​

Define secrets at the config root level (not within apps):

typescript
export default defineConfig({
  project: 'my-app',
  
  namespaces: {
    dev: { production: false },
    prod: { production: true }
  },
  
  // Secrets are defined at config root level
  secrets: {
    'api-secrets': ({ production }) => ({
      JWT_SECRET: production
        ? process.env.JWT_SECRET ?? ''
        : 'dev-jwt',
      DB_PASSWORD: production
        ? process.env.DB_PASSWORD ?? ''
        : 'dev-password',
      API_KEY: production
        ? process.env.API_KEY ?? ''
        : 'dev-key'
    })
  },
  
  apps: {
    api: {
      // Reference secrets in env using secret() helper
      env: ({ secret }) => ({
        JWT_SECRET: secret('api-secrets', 'JWT_SECRET'),
        DB_PASSWORD: secret('api-secrets', 'DB_PASSWORD')
      })
    }
  }
})

envFrom: Reference Entire Secret ​

Use secret() helper to inject all keys from a secret:

typescript
// Define secrets at config root level
secrets: {
  'api-secrets': ({ production }) => ({
    JWT_SECRET: production
      ? process.env.JWT_SECRET ?? ''
      : 'dev-jwt',
    DB_PASSWORD: production
      ? process.env.DB_PASSWORD ?? ''
      : 'dev-password'
  })
},

apps: {
  api: {
    // Reference entire secret as envFrom
    env: ({ secret }) => secret('api-secrets')  // ← envFrom: secretRef
  }
}

This generates:

yaml
envFrom:
  - secretRef:
      name: api-secrets

valueFrom: Reference Specific Keys ​

Mix static values with secret references:

typescript
apps: {
  api: {
    env: ({ secret }) => ({
      PORT: '3000',                                    // Static
      JWT_SECRET: secret('api-secrets', 'JWT_SECRET'),  // From secret
      DB_PASSWORD: secret('api-secrets', 'DB_PASSWORD') // From secret
    })
  }
}

This generates:

yaml
env:
  - name: PORT
    value: "3000"
  - name: JWT_SECRET
    valueFrom:
      secretKeyRef:
        name: api-secrets
        key: JWT_SECRET
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: api-secrets
        key: DB_PASSWORD

Secret Validation ​

tsops automatically validates secrets before deployment.

What's Validated ​

✅ No undefined values
✅ No placeholder values (change-me, replace-me, todo, fixme)
✅ No references to missing environment variables

Example: Missing Values ​

typescript
secrets: {
  'api-secrets': {
    JWT_SECRET: process.env.PROD_JWT,  // ← undefined!
    DB_PASSWORD: 'change-me'            // ← placeholder!
  }
}

tsops will show:

❌ Secret "api-secrets" for app "api" contains missing or placeholder values.

Missing/placeholder keys:
  - JWT_SECRET = "undefined"
  - DB_PASSWORD = "change-me"

Secret does not exist in cluster (namespace: "production").

Please provide actual values by:
  1. Setting environment variables before deployment
  2. Updating your tsops.config.ts with real values
  3. Creating the secret manually in the cluster first

Fallback to Cluster Secrets ​

If validation fails, tsops checks if the secret exists in the cluster:

bash
kubectl get secret api-secrets -n production

If found, tsops uses the existing secret. This allows:

  • ✅ Manual secret creation
  • ✅ Secret rotation without config changes
  • ✅ Different secrets per environment

ConfigMaps ​

Similar to secrets, but for non-sensitive configuration.

Basic Usage ​

ConfigMaps are defined at config root level, similar to secrets:

typescript
export default defineConfig({
  // ConfigMaps at root level
  configMaps: {
    'api-config': {
      LOG_LEVEL: 'info',
      MAX_CONNECTIONS: '100',
      FEATURE_FLAGS: 'auth,payments,notifications'
    }
  },
  
  apps: {
    api: {
      // Reference in env
      env: ({ configMap }) => ({
        LOG_LEVEL: configMap('api-config', 'LOG_LEVEL')
      })
    }
  }
})

envFrom: Reference Entire ConfigMap ​

typescript
// Define at root level
configMaps: {
  'api-config': {
    LOG_LEVEL: 'info',
    FEATURE_FLAGS: 'auth,payments'
  }
},

apps: {
  api: {
    // Reference entire configMap as envFrom
    env: ({ configMap }) => configMap('api-config')
  }
}

valueFrom: Reference Specific Keys ​

typescript
apps: {
  api: {
    env: ({ configMap }) => ({
      LOG_LEVEL: configMap('api-config', 'LOG_LEVEL'),
      MAX_CONNECTIONS: configMap('api-config', 'MAX_CONNECTIONS')
    })
  }
}

Best Practices ​

✅ Use environment variables for secrets ​

typescript
secrets: {
  'api-secrets': ({ production }) => ({
    JWT_SECRET: production
      ? process.env.PROD_JWT ?? ''
      : 'dev-jwt'
  })
}

Then deploy with:

bash
PROD_JWT=xxx PROD_DB_PWD=yyy pnpm tsops deploy --namespace prod

✅ Different secrets per environment ​

typescript
secrets: {
  'api-secrets': ({ production }) => ({
    JWT_SECRET: production
      ? process.env.JWT_SECRET ?? ''
      : 'dev-jwt-secret',
    DB_PASSWORD: production
      ? process.env.DB_PASSWORD ?? ''
      : 'dev-password'
  })
}

✅ Use template for external database URLs ​

typescript
secrets: {
  'api-secrets': ({ template, env }) => ({
    // For EXTERNAL databases with credentials:
    DATABASE_URL: template('postgresql://{user}:{pwd}@{host}:{port}/{db}', {
      user: 'myuser',
      pwd: env('DB_PASSWORD', ''),
      host: env('DB_HOST', 'external-db.example.com'),  // External host
      port: '5432',
      db: 'myapp'
    })
  })
}

// ⚠️ For INTERNAL services, use runtime config in your app code:
// import config from './tsops.config'
// const POSTGRES_URL = config.url('postgres', 'service')

❌ Don't hardcode secrets ​

typescript
secrets: {
  'api-secrets': {
    JWT_SECRET: 'hardcoded-secret'  // ❌ Never do this!
  }
}

❌ Don't commit secrets to git ​

bash
# .gitignore
.env
.env.local
.env.production

Runtime Access ​

Access resolved environment variables at runtime directly from your config object:

typescript
// server.ts
import config from './tsops.config'

// Set TSOPS_NAMESPACE to determine which namespace to use
process.env.TSOPS_NAMESPACE = 'prod'

// Get resolved environment for an app
const dbPassword = config.env('api', 'DB_PASSWORD')
console.log('JWT_SECRET:', config.env('api', 'JWT_SECRET'))
console.log('DB_PASSWORD:', dbPassword)

// Or get URLs directly
console.log('Internal endpoint:', config.url('api', 'cluster'))
console.log('External endpoint:', config.url('api', 'ingress'))

This provides:

  • ✅ Single source of truth
  • ✅ Type-safe environment access
  • ✅ Works in dev and production
  • ✅ No duplication
  • ✅ Built-in - no extra packages needed

Security Checklist ​

  • [ ] All production secrets from environment variables
  • [ ] No secrets in git
  • [ ] .env files in .gitignore
  • [ ] Different secrets per environment
  • [ ] Secret rotation plan
  • [ ] Access controls on secrets
  • [ ] Audit logging enabled

Next Steps ​

Released under the MIT License.