| name | add-app-with-secrets |
| description | Wire application secrets from Pulumi ESC into an app in homelab-apps using the External Secrets Operator. Use when the user says "this app needs an API key", "add a secret to this app", "configure credentials for this service", or "pull a secret from ESC into the app". |
| compatibility | Requires the homelab cluster with External Secrets Operator running and the pulumi-esc ClusterSecretStore configured. App workspace must already exist under apps/<name>/. |
| metadata | {"author":"mrsimpson","homelab-apps-repo":"https://github.com/digitaleraluhut/homelab-apps"} |
Add Secrets to an App
Wire secrets from Pulumi ESC into a homelab-apps app via the External Secrets Operator (ESO).
Secrets are stored centrally in Pulumi ESC and synced automatically to Kubernetes every hour.
Architecture
Pulumi ESC (digitaleraluhut/homelab/dev)
↓ ClusterSecretStore: pulumi-esc
External Secrets Operator
↓ ExternalSecret (per app)
Kubernetes Secret (synced every hour)
↓ envFrom / env / volume
App Pod
Step 1 — Add the secret to Pulumi ESC
pulumi env open digitaleraluhut/homelab/dev
pulumi env edit digitaleraluhut/homelab/dev
Naming convention: {service}-{type}-{identifier}, e.g.:
stripe-api-key
sendgrid-api-key
myapp-admin-token
Step 2 — Create an ExternalSecret in src/index.ts
import * as k8s from '@pulumi/kubernetes';
const appSecrets = new k8s.apiextensions.CustomResource(`${APP_NAME}-secrets`, {
apiVersion: 'external-secrets.io/v1beta1',
kind: 'ExternalSecret',
metadata: {
name: `${APP_NAME}-secrets`,
namespace: ns.metadata.name,
},
spec: {
refreshInterval: '1h',
secretStoreRef: {
name: 'pulumi-esc',
kind: 'ClusterSecretStore',
},
target: {
name: `${APP_NAME}-secrets`,
creationPolicy: 'Owner',
},
data: [
{
secretKey: 'STRIPE_API_KEY',
remoteRef: { key: 'stripe-api-key' },
},
{
secretKey: 'SENDGRID_API_KEY',
remoteRef: { key: 'sendgrid-api-key' },
},
],
},
}, { dependsOn: ns });
Step 3 — Mount the secret into the app
export const app = homelab.createExposedWebApp(APP_NAME, {
namespace: ns,
image: pulumi.output(image),
domain: pulumi.interpolate`${APP_NAME}.${domain}`,
port: APP_PORT,
auth: AuthType.OAUTH2_PROXY,
envFrom: [{ secretRef: { name: `${APP_NAME}-secrets` } }],
});
Make sure appSecrets is created before the app:
export const app = homelab.createExposedWebApp(APP_NAME, {
}, { dependsOn: [ns, appSecrets] });
Step 4 — Deploy and verify
cd apps/<name>
pulumi up --stack digitaleraluhut/<name>/dev
kubectl describe externalsecret <name>-secrets -n <name>
kubectl get secret <name>-secrets -n <name>
Rotating secrets
-
Update the value in Pulumi ESC:
pulumi env edit digitaleraluhut/homelab/dev
-
Force immediate sync (instead of waiting up to 1h):
kubectl annotate externalsecret <name>-secrets \
force-sync="$(date +%s)" -n <name>
-
Restart the app to pick up the new value:
kubectl rollout restart deployment/<name> -n <name>
Troubleshooting
ExternalSecret not syncing — check status and ESO logs:
kubectl describe externalsecret <name>-secrets -n <name>
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets
Pod can't read secret — verify the secret exists and check env var names:
kubectl get secret <name>-secrets -n <name> -o yaml
kubectl exec -n <name> deploy/<name> -- env | grep <KEY_NAME>
Key not found in ESC — the remoteRef.key must exactly match the key in the ESC environment:
pulumi env open digitaleraluhut/homelab/dev | grep <key-name>
See also