Get API keys and passwords into a Go service on Cloud Run: environment variables, mounted volumes, the client library, or secrets declared in code.
Google Secret Manager stores API keys, database passwords and signing keys for services on Google Cloud, and Cloud Run can hand them to a container without the value ever appearing in a deploy config. The setup is short but easy to get subtly wrong: the wrong service account gets the accessor role, a pinned version never rotates, or developers copy production keys into .env files to test locally.
This guide wires a Resend API key into a Go email service on Cloud Run. It starts with Encore (a tool that works alongside your framework), where secrets are declared in code, then covers the three manual options: environment variables, mounted volumes and the Go client library.
Each option is covered below, starting with Encore.
Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. On GCP, secrets are stored in Secret Manager in your project and injected into each Cloud Run service as environment variables. Cloud access sits with Encore's service identity, not the agent.
In Go, a service declares its secrets as an unexported struct named secrets:
// notify/notify.go
package notify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"encore.dev/beta/errs"
)
// Secret Manager on GCP, Secrets Manager on AWS, .secrets.local.cue locally.
var secrets struct {
ResendAPIKey string
}
type SendParams struct {
To string `json:"to"`
Subject string `json:"subject"`
Text string `json:"text"`
}
type SendResponse struct {
Status int `json:"status"`
}
//encore:api private method=POST path=/notify/send
func Send(ctx context.Context, p *SendParams) (*SendResponse, error) {
body, _ := json.Marshal(map[string]any{
"from": "Acme <hello@acme.dev>",
"to": []string{p.To},
"subject": p.Subject,
"text": p.Text,
})
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.resend.com/emails", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+secrets.ResendAPIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return nil, &errs.Error{Code: errs.Unavailable, Message: fmt.Sprintf("resend returned %d", resp.StatusCode)}
}
return &SendResponse{Status: resp.StatusCode}, nil
}
Values are set per environment type from the CLI, which prompts for each value, so the production key and the local key are different secrets from the start:
For a value only on your machine, a .secrets.local.cue file next to encore.app overrides it. We ran the service under encore run with a placeholder key in that file, and the endpoint returned resend returned 401, which confirms the key reached the request header. If a secret has no value for an environment, the deploy to that environment fails instead of starting a service with an empty key. The secrets docs also cover per-environment overrides with --env.
For a coding agent this is a useful boundary: it can add a secret to the struct and write the code that uses it, while the value itself is set by a person and stored in Encore's secrets manager and Secret Manager, never in the repository or a .env file the agent could read, print or commit. The exception is a .secrets.local.cue override, a plaintext file in the app directory, so keep it out of anything an agent should not read.
The TypeScript secrets docs cover the same model for TypeScript services, and environments explains how secret values differ between environments.
Run in your terminal to get started locally.
The quickest manual route, from Google's Cloud Run secrets guide, is to create the secret, grant access and reference it at deploy time:
printf '%s' "$RESEND_KEY" | gcloud secrets create resend-api-key --data-file=-
gcloud secrets add-iam-policy-binding resend-api-key \
--member="serviceAccount:notify@my-project.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
gcloud run deploy notify \
--image=europe-west1-docker.pkg.dev/my-project/apps/notify:latest \
--service-account=notify@my-project.iam.gserviceaccount.com \
--set-secrets=RESEND_API_KEY=resend-api-key:latest
The Go code reads os.Getenv("RESEND_API_KEY"). The value is resolved when an instance starts, so a rotated key reaches running instances only as they are replaced. Grant the role to the service's own service account, not the default Compute Engine account, or every service in the project can read the key.
With --set-secrets=/secrets/resend=resend-api-key:latest, Cloud Run mounts the secret as a file. Reading the file fetches the current version, so rotation takes effect without a restart. This suits certificates and keys that rotate on a schedule, at the cost of reading a file instead of an environment variable.
The Secret Manager Go client fetches a version at runtime with AccessSecretVersion, using the service account's credentials. It adds a network call and error handling to startup, which makes it most useful for secrets you need to reload in-process or look up by name. Locally, it runs with your own gcloud auth application-default login credentials, which means developers need access to a project that holds the secrets.
Secret Manager bills per active secret version and per access operation, so a client that fetches a secret on every request costs more and adds latency. Read once at startup or cache the value in memory.
Pick Encore when you want secrets declared next to the code that uses them, stored in Secret Manager in your own GCP project, with separate values for production and local work with no shared .env files.
Pick environment variables for the simplest manual setup, mounted volumes when rotation without restarts matters, and the client library when the application has to look up secrets by name at runtime.
Grant the Cloud Run service account the Secret Manager Secret Accessor role on the secret, then reference it at deploy time with --set-secrets, either as an environment variable (NAME=secret:version) or as a mounted file (/path=secret:version). With Encore, you declare var secrets struct { ResendAPIKey string } in Go, set the value with encore secret set --type prod ResendAPIKey, and Encore stores it in Secret Manager in your GCP project and injects it into the Cloud Run service on deploy.
Environment variable secrets are read when an instance starts, so a new version reaches the service only when new instances start, and pinning a version number needs a redeploy. A secret mounted as a volume with version latest is read from Secret Manager when the file is read, so it picks up new versions.
Keep separate values per environment. With plain Cloud Run, a .env file kept out of git or a separate development project gives developers their own values.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.