How to Use Reloader with GCP Secret Manager and External Secrets Operator#
This guide explains how to sync secrets from GCP Secret Manager into Kubernetes using External Secrets Operator (ESO), and use Stakater Reloader to automatically restart pods when those secrets change.
Authentication Options#
ESO supports two authentication methods for GCP Secret Manager:
| Method | Description | Use Case |
|---|---|---|
| Workload Identity | GCP IAM bound to a Kubernetes ServiceAccount via GKE OIDC | Recommended for GKE — no static credentials |
| Service Account Key | GCP service account JSON key stored in a K8s Secret | Any cluster — simpler but requires key rotation |
How It Works#
sequenceDiagram
actor Ops as Operator / Cloud Function
participant GSM as GCP Secret Manager
participant Meta as GKE Metadata Server
participant ESO as External Secrets Operator
participant K8s as Kubernetes Secret
participant RL as Reloader
participant Pod as Application Pod
Note over ESO: Authenticates via Workload Identity or SA Key
ESO->>Meta: Request access token (Workload Identity)
Meta-->>ESO: Short-lived access token
Ops->>GSM: Add new secret version
loop Every refreshInterval
ESO->>GSM: Access secret (latest version)
GSM-->>ESO: Updated secret value
end
ESO->>K8s: Update Secret data
Note over K8s: annotation: reloader.stakater.com/match: "true"
K8s-->>RL: Watch event (Secret changed)
RL->>Pod: Trigger rolling restart
Note over Pod: New pod starts with updated secret
Prerequisites#
- Kubernetes cluster (v1.19+)
- Helm v3+
- GCP project with Secret Manager API enabled
gcloudCLI configured locally- Stakater Reloader installed
- External Secrets Operator installed
Enable the Secret Manager API#
gcloud services enable secretmanager.googleapis.com --project=my-project
Install Stakater Reloader#
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm install reloader stakater/reloader --namespace reloader --create-namespace
Install External Secrets Operator#
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace \
--wait
Verify:
kubectl get pods -n external-secrets
Create a secret in GCP Secret Manager#
# Create the secret
gcloud secrets create myapp-database \
--project=my-project \
--replication-policy=automatic
# Add a secret version (JSON value)
echo -n '{"username":"admin-user","password":"super-secret-password"}' | \
gcloud secrets versions add myapp-database \
--project=my-project \
--data-file=-
Option 1: Workload Identity#
Workload Identity lets a Kubernetes ServiceAccount impersonate a GCP service account without any static key. It is the recommended approach for GKE clusters.
Workload Identity: Step 1 — Enable Workload Identity on your GKE cluster#
gcloud container clusters update my-cluster \
--workload-pool=my-project.svc.id.goog \
--region=us-central1
For new clusters:
gcloud container clusters create my-cluster \
--workload-pool=my-project.svc.id.goog \
--region=us-central1
Workload Identity: Step 2 — Create a GCP service account#
gcloud iam service-accounts create eso-sa \
--project=my-project \
--display-name="ESO Secret Manager SA"
Workload Identity: Step 3 — Grant Secret Manager access to the GCP service account#
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:eso-sa@my-project.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
To restrict access to specific secrets rather than all secrets in the project:
gcloud secrets add-iam-policy-binding myapp-database \
--project=my-project \
--member="serviceAccount:eso-sa@my-project.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Workload Identity: Step 4 — Create namespace and Kubernetes ServiceAccount#
kubectl create namespace gcp-eso-test
kubectl create serviceaccount eso-ksa -n gcp-eso-test
Workload Identity: Step 5 — Bind the Kubernetes ServiceAccount to the GCP service account#
# Allow the K8s SA to impersonate the GCP SA
gcloud iam service-accounts add-iam-policy-binding \
eso-sa@my-project.iam.gserviceaccount.com \
--project=my-project \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:my-project.svc.id.goog[gcp-eso-test/eso-ksa]"
# Annotate the K8s ServiceAccount
kubectl annotate serviceaccount eso-ksa -n gcp-eso-test \
iam.gke.io/gcp-service-account=eso-sa@my-project.iam.gserviceaccount.com
Workload Identity: Step 6 — Create SecretStore#
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: gcp-secret-store
namespace: gcp-eso-test
spec:
provider:
gcpsm:
projectID: my-project
auth:
workloadIdentity:
clusterLocation: us-central1
clusterName: my-cluster
serviceAccountRef:
name: eso-ksa
Apply and verify:
kubectl apply -f secret-store.yaml
kubectl get secretstore -n gcp-eso-test
Should show STATUS: Valid and READY: True.
Now proceed to Create ExternalSecret.
Option 2: Service Account Key#
Use a GCP service account JSON key stored in a Kubernetes Secret. Works with any Kubernetes cluster, not just GKE.
Key: Step 1 — Create a GCP service account and key#
gcloud iam service-accounts create eso-sa \
--project=my-project \
--display-name="ESO Secret Manager SA"
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:eso-sa@my-project.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
gcloud iam service-accounts keys create eso-sa-key.json \
--iam-account=eso-sa@my-project.iam.gserviceaccount.com \
--project=my-project
Important: Store eso-sa-key.json securely. Treat it as a long-lived credential that must be rotated if compromised.
Key: Step 2 — Store the key in a Kubernetes Secret#
kubectl create namespace gcp-eso-test
kubectl create secret generic gcp-credentials -n gcp-eso-test \
--from-file=key.json=eso-sa-key.json
Key: Step 3 — Create SecretStore#
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: gcp-secret-store
namespace: gcp-eso-test
spec:
provider:
gcpsm:
projectID: my-project
auth:
secretRef:
secretAccessKeySecretRef:
name: gcp-credentials
key: key.json
Apply and verify:
kubectl apply -f secret-store.yaml
kubectl get secretstore -n gcp-eso-test
Should show STATUS: Valid and READY: True.
Now proceed to Create ExternalSecret.
Common Configuration#
The following steps apply to both authentication methods.
Create ExternalSecret#
GCP Secret Manager stores secrets as opaque byte strings (not structured key-value pairs). A single secret version holds the entire value.
If your secret is a JSON object (as created above), use dataFrom to extract individual keys:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: gcp-eso-test
spec:
refreshInterval: 1h
secretStoreRef:
name: gcp-secret-store
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
template:
metadata:
annotations:
reloader.stakater.com/match: "true"
dataFrom:
- extract:
key: myapp-database
If your secret is a plain string, use data with a single key:
spec:
...
data:
- secretKey: password
remoteRef:
key: myapp-database
Apply and verify:
kubectl apply -f external-secret.yaml
kubectl get externalsecret -n gcp-eso-test
Should show STATUS: SecretSynced and READY: True.
Deploy Application#
apiVersion: apps/v1
kind: Deployment
metadata:
name: gcp-eso-test-app
namespace: gcp-eso-test
annotations:
reloader.stakater.com/search: "true"
spec:
replicas: 1
selector:
matchLabels:
app: gcp-eso-test-app
template:
metadata:
labels:
app: gcp-eso-test-app
spec:
containers:
- name: app
image: busybox:latest
command:
- "sh"
- "-c"
- |
while true; do
echo "Username: $APP_USERNAME"
echo "Password: $APP_PASSWORD"
sleep 30
done
env:
- name: APP_USERNAME
valueFrom:
secretKeyRef:
name: app-secrets
key: username
- name: APP_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: password
Apply:
kubectl apply -f deployment.yaml
Verify the Setup#
# SecretStore
kubectl get secretstore -n gcp-eso-test
# ExternalSecret
kubectl get externalsecret -n gcp-eso-test
# Secret (created by ESO)
kubectl get secret app-secrets -n gcp-eso-test
# Pod
kubectl get pods -n gcp-eso-test
# Verify secret contents
kubectl get secret app-secrets -n gcp-eso-test -o jsonpath='{.data.password}' | base64 -d
# Check app logs
kubectl logs -n gcp-eso-test -l app=gcp-eso-test-app
Test Secret Rotation#
Add a new version in GCP Secret Manager#
echo -n '{"username":"admin-user","password":"new-rotated-password"}' | \
gcloud secrets versions add myapp-database \
--project=my-project \
--data-file=-
GCP Secret Manager keeps all previous versions. ESO always reads the latest version unless you pin a specific version in the ExternalSecret.
Wait and verify#
To force an immediate ESO sync without waiting for refreshInterval:
kubectl annotate externalsecret app-secrets -n gcp-eso-test \
force-sync=$(date +%s) --overwrite
Then verify:
# Check secret was updated
kubectl get secret app-secrets -n gcp-eso-test -o jsonpath='{.data.password}' | base64 -d
# Check pod was restarted
kubectl get pods -n gcp-eso-test -l app=gcp-eso-test-app
# Check app logs show new password
kubectl logs -n gcp-eso-test -l app=gcp-eso-test-app --tail=5
Configuration Reference#
SecretStore — Workload Identity#
| Field | Description |
|---|---|
provider.gcpsm.projectID |
GCP project ID |
provider.gcpsm.auth.workloadIdentity.clusterLocation |
GKE cluster region or zone |
provider.gcpsm.auth.workloadIdentity.clusterName |
GKE cluster name |
provider.gcpsm.auth.workloadIdentity.serviceAccountRef.name |
Kubernetes ServiceAccount annotated with the GCP service account |
SecretStore — Service Account Key#
| Field | Description |
|---|---|
provider.gcpsm.projectID |
GCP project ID |
provider.gcpsm.auth.secretRef.secretAccessKeySecretRef.name |
K8s Secret containing the JSON key |
provider.gcpsm.auth.secretRef.secretAccessKeySecretRef.key |
Key within the Secret holding the JSON key content |
ExternalSecret#
| Field | Description |
|---|---|
refreshInterval |
How often ESO polls GCP for changes (e.g. 1h, 5m) |
secretStoreRef |
Reference to the SecretStore |
target.name |
Name of the K8s Secret to create |
target.template.metadata.annotations |
Annotations to add to the created Secret |
dataFrom[].extract.key |
GCP secret name — extracts all keys from a JSON-structured secret |
data[].remoteRef.key |
GCP secret name for plain string secrets |
Reloader Annotations#
| Resource | Annotation |
|---|---|
| Deployment | reloader.stakater.com/search: "true" |
| Secret (via ExternalSecret template) | reloader.stakater.com/match: "true" |
Comparison: Workload Identity vs Service Account Key#
| Aspect | Workload Identity | Service Account Key |
|---|---|---|
| Credentials | No static key — short-lived tokens | Long-lived JSON key file |
| Security | No credentials to leak or rotate | Key must be rotated if compromised |
| Cluster requirement | GKE with Workload Identity enabled | Any Kubernetes cluster |
| Setup complexity | Requires IAM binding + K8s SA annotation | Just a service account key + K8s Secret |
| Best for | Production on GKE | Development, non-GKE clusters |