How to Use Reloader with GCP Secret Manager and CSI Driver#
This guide shows how to mount secrets from GCP Secret Manager into Kubernetes pods using the Secrets Store CSI Driver with the GCP Secret Manager provider, then use Reloader to automatically restart pods when those secrets change.
The GCP provider syncs secrets into a Kubernetes Secret via secretObjects. Reloader watches that Kubernetes Secret and triggers a rolling restart whenever it is updated.
See also: ESO Pattern — if you are already using External Secrets Operator, that pattern is simpler and does not require the CSI Driver.
Provider options on GCP#
There are two CSI paths for GCP Secret Manager. They are not interchangeable:
| Open-source provider | GKE managed add-on | |
|---|---|---|
spec.provider value |
gcp |
gke |
| Install method | kubectl apply from GitHub |
gcloud / Cloud Console |
secretObjects sync to Kubernetes Secret |
✅ | ❌ |
| Secret rotation polling | ✅ Alpha | ✅ GKE 1.32.2+ |
| Officially supported by Google | ❌ Community project | ✅ |
This guide uses the open-source provider (spec.provider: gcp), because it is the only path that supports secretObjects — which is required to create the Kubernetes Secret that Reloader watches. The GKE managed add-on mounts secrets as files only and cannot be used with this pattern.
How it works#
sequenceDiagram
actor Ops as Operator
participant GSM as GCP Secret Manager
participant CSI as CSI Driver +<br/>GCP Provider
participant K8s as Kubernetes Secret
participant RL as Reloader
participant Pod as Application Pod
Ops->>GSM: Add new secret version
loop Every rotationPollInterval (default: 2m)
CSI->>GSM: Get secret (latest version)
GSM-->>CSI: Updated secret value
CSI->>Pod: Refresh mounted files
CSI->>K8s: Sync via secretObjects
end
K8s-->>RL: Watch event (Secret changed)
RL->>Pod: Trigger rolling restart
Note over Pod: New pod starts with updated secret
Prerequisites#
- GKE cluster with Workload Identity enabled (
--workload-pool=<project-id>.svc.id.goog) gcloudCLI configured locally- Helm v3+
- GCP project with Secret Manager API enabled
- Stakater Reloader installed
Step 1 — Install the Secrets Store CSI Driver#
Install the CSI Driver with secret sync and rotation enabled:
helm repo add secrets-store-csi-driver \
https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm repo update
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system \
--set syncSecret.enabled=true \
--set enableSecretRotation=true \
--set rotationPollInterval=2m
syncSecret.enabled=true is required — without it, secretObjects does not create a Kubernetes Secret and Reloader has nothing to watch.
Step 2 — Install the GCP Secret Manager provider#
The GCP provider does not have a public Helm repository. Install it directly from the official GitHub source:
kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/secrets-store-csi-driver-provider-gcp/main/deploy/provider-gcp-plugin.yaml
This deploys the provider as a Daemonset in the kube-system namespace.
Verify it is running:
kubectl get pods -n kube-system -l app=csi-secrets-store-provider-gcp
Step 3 — Install Reloader#
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm install reloader stakater/reloader \
--namespace reloader \
--create-namespace
Step 4 — Grant Workload Identity access#
Workload Identity allows a Kubernetes ServiceAccount to access GCP APIs without a service account key file. This is the recommended authentication method on GKE.
Enable Secret Manager API#
gcloud services enable secretmanager.googleapis.com \
--project=<PROJECT_ID>
Grant the Kubernetes ServiceAccount access to the secret#
The preferred approach binds IAM permissions directly to the Kubernetes identity — no GCP Service Account is required:
export PROJECT_ID=<your-project-id>
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
export NAMESPACE=default
export KSA_NAME=app-sa
export SECRET_NAME=db-password
gcloud secrets add-iam-policy-binding $SECRET_NAME \
--project=$PROJECT_ID \
--role=roles/secretmanager.secretAccessor \
--member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/${NAMESPACE}/sa/${KSA_NAME}"
Create the Kubernetes ServiceAccount#
kubectl create serviceaccount $KSA_NAME --namespace $NAMESPACE
No annotation is needed on the ServiceAccount when using direct IAM binding to the Kubernetes identity.
Step 5 — Create a secret in GCP Secret Manager#
echo -n "initial-password" | \
gcloud secrets create db-password \
--data-file=- \
--project=$PROJECT_ID
Note the full resource name — you will need it in the SecretProviderClass:
gcloud secrets describe db-password --project=$PROJECT_ID --format="value(name)"
Output format: projects/<PROJECT_NUMBER>/secrets/db-password
Step 6 — Create the SecretProviderClass#
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: gcp-app-secrets
namespace: default
spec:
provider: gcp
parameters:
secrets: |
- resourceName: "projects/<PROJECT_ID>/secrets/db-password/versions/latest"
path: "db-password"
- resourceName: "projects/<PROJECT_ID>/secrets/api-key/versions/latest"
path: "api-key"
secretObjects:
- secretName: app-secrets
type: Opaque
annotations:
reloader.stakater.com/match: "true"
data:
- objectName: db-password
key: db-password
- objectName: api-key
key: api-key
Apply:
kubectl apply -f secret-provider-class.yaml
Key configuration points:
| Field | Description |
|---|---|
spec.provider |
Must be gcp (not gke, not google) |
parameters.secrets |
YAML list as a string; each entry needs resourceName and path |
resourceName |
Full GCP resource path: projects/<id>/secrets/<name>/versions/<version-or-latest> |
path |
Filename in the mounted volume; must match objectName in secretObjects |
secretObjects[].data[].objectName |
Must match the path value from the parameters.secrets entry |
Step 7 — Deploy the application#
The CSI volume mount is required even when your application reads secrets as environment variables. Without it, the GCP provider does not create the Kubernetes Secret via secretObjects.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: default
annotations:
reloader.stakater.com/search: "true"
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
serviceAccountName: app-sa
containers:
- name: app
image: busybox:latest
command: ["sh", "-c", "while true; do echo \"password=$DB_PASSWORD\"; sleep 30; done"]
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secrets
key: api-key
volumeMounts:
- name: gcp-secrets
mountPath: /mnt/secrets
readOnly: true
volumes:
- name: gcp-secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: gcp-app-secrets
Apply:
kubectl apply -f deployment.yaml
Step 8 — Verify the setup#
# Check the pod is running
kubectl get pods -n default -l app=myapp
# Confirm the Kubernetes Secret was created
kubectl get secret app-secrets -n default
kubectl get secret app-secrets -n default -o jsonpath='{.data.db-password}' | base64 -d
# Check the CSI mount status
kubectl get secretproviderclasspodstatuses -n default
Step 9 — Test secret rotation#
Add a new version to the secret in GCP Secret Manager:
echo -n "rotated-password" | \
gcloud secrets versions add db-password \
--data-file=- \
--project=$PROJECT_ID
Wait for the next rotation poll (default: 2 minutes). The CSI driver fetches the new version, updates the mounted files, and syncs the new value into the Kubernetes Secret. Reloader detects the Secret update and triggers a rolling restart.
# Confirm the Kubernetes Secret was updated
kubectl get secret app-secrets -n default -o jsonpath='{.data.db-password}' | base64 -d
# Confirm pods were restarted
kubectl get pods -n default -l app=myapp
Reloader annotations#
| Resource | Annotation | Effect |
|---|---|---|
| Deployment | reloader.stakater.com/search: "true" |
Restart when any referenced Secret with match: "true" changes |
Kubernetes Secret (via secretObjects) |
reloader.stakater.com/match: "true" |
Mark this Secret as eligible to trigger a restart |
File-based pattern (no Kubernetes Secret)#
If you want to mount secrets as files only, without creating a Kubernetes Secret, omit the secretObjects block and enable Reloader's CSI integration:
reloader:
enableCSIIntegration: true
Then annotate the Deployment with the SecretProviderClass name:
metadata:
annotations:
secretproviderclass.reloader.stakater.com/reload: "gcp-app-secrets"
In this mode, Reloader watches SecretProviderClassPodStatus resources for version hash changes instead of watching a Kubernetes Secret.
ESO vs CSI Driver for GCP#
| ESO | CSI Driver (provider-gcp) | |
|---|---|---|
| Kubernetes Secret created | ✅ Always | ✅ With secretObjects |
| Requires pod volume mount | ❌ | ✅ Required even for env var usage |
| Secret rotation | ESO refresh interval | CSI rotation poll interval |
| Officially supported | ESO is CNCF-graduated | Community project (not Google-supported) |
| Complexity | Lower | Higher |
For most GCP users, the ESO pattern is simpler and better supported. Use the CSI Driver pattern when you need file-based secret mounting or when your organisation already uses the Secrets Store CSI Driver across multiple cloud providers.