Amith M
10/21/2025, 4:52 PM"name": secret_base_name,
"valueFrom": {
"secretKeyRef": {
"name": final_secret_name,
"key": "latest"
}
}
secret key ref based env vars in the job_bodyMarvin
10/21/2025, 4:52 PMMarvin
10/21/2025, 4:59 PMjson
{
"env": [
{
"name": "<ENV_NAME>",
"valueSource": {
"secretKeyRef": {
"secret": "<SECRET_MANAGER_NAME>",
"version": "latest"
}
}
}
]
}
Option B — bake it into the pool’s base job template (job_body)
For a Cloud Run Job v2 template, env goes under template.template.containers[0].env:
json
{
"template": {
"template": {
"containers": [
{
"env": [
{
"name": "<ENV_NAME>",
"valueSource": {
"secretKeyRef": {
"secret": "<SECRET_MANAGER_NAME>",
"version": "latest"
}
}
}
]
}
]
}
}
}
You can update the pool template with:
bash
prefect work-pool update "<your-pool>" --base-job-template ./base_job_template.json
If your pool is the legacy Cloud Run (v1) worker, use the Kubernetes-style schema you posted:
json
{
"name": "<ENV_NAME>",
"valueFrom": {
"secretKeyRef": {
"name": "<SECRET_MANAGER_NAME>",
"key": "latest"
}
}
}
Can you confirm whether your work pool is the Cloud Run v2 worker? If so, switch to valueSource.secretKeyRef.secret/version as shown above.
References:
- GCP worker guide
- Customize job variablesAmith M
10/21/2025, 5:05 PMMarvin
10/21/2025, 5:05 PMNate
10/21/2025, 5:08 PMAmith M
10/21/2025, 5:08 PMMarvin
10/21/2025, 5:08 PM"job_body": {
"apiVersion": "<http://run.googleapis.com/v1|run.googleapis.com/v1>",
"kind": "Job",
"metadata": {
"name": "{{ name }}",
"annotations": {
"<http://run.googleapis.com/launch-stage|run.googleapis.com/launch-stage>": "BETA"
}
},
"spec": {
"template": {
"metadata": {
"annotations": {
"<http://run.googleapis.com/cloudsql-instances|run.googleapis.com/cloudsql-instances>": "windy-furnace-465017-c3:us-east1:armor-adk-db",
"<http://run.googleapis.com/vpc-access-connector|run.googleapis.com/vpc-access-connector>": "{{ vpc_connector_name }}"
}
},
"template": {
"spec": {
"containers": [
{
"image": "{{ image }}",
"command": "{{ command }}",
"env": "{{ env }}", // see B below
"resources": {
"limits": {"cpu": "{{ cpu }}", "memory": "{{ memory }}"},
"requests": {"cpu": "{{ cpu }}", "memory": "{{ memory }}"}
}
}
],
"serviceAccountName": "{{ service_account_name }}",
"timeoutSeconds": "{{ timeout }}"
}
}
}
}
}
Notes:
- The “env” line uses a template variable so Prefect can inject env from job_variables.
- In Cloud Run v1, secret-based env uses valueFrom.secretKeyRef with name/key (your use of key: "latest" is correct).
B. Put env vars into job_variables (deployment or pool defaults)
Example job_variables you can set on the deployment or in the pool’s defaults:
```
{
"env": [
{ "name": "NAME", "value": "armor-app" },
{ "name": "VERSION", "value": "0.2.0" },
{ "name": "BUILD_TAGS", "value": "[\"v0.2.0\"]" },
{ "name": "ENV", "value": "stage" },
{ "name": "DEBUG", "value": "false" },
{ "name": "GCS_PROJECT_ID", "value": "windy-furnace-465017-c3" },
{ "name": "DB_DIALECT", "value": "postgresql" },
{ "name": "DB_SYNC_DRIVER", "value": "pg8000" },
{ "name": "DB_ASYNC_DRIVER", "value": "asyncpg" },
{ "name": "DB_USER", "value": "cloud-run-service-account@windy-furnace-465017-c3.iam.gserviceaccount.com" },
{ "name": "DB_SUPPORTS_SCHEMA", "value": "true" },
{ "name": "DB_NAME", "value": "postgres" },
{ "name": "DB_GCP_SQL_INSTANCE_CONNECTION_NAME", "value": "windy-furnace-465017-c3us east1armor-adk-db" },
{ "name": "PREFECT_API_URL",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_URL", "key": "latest" } }
},
{ "name": "PREFECT_SERVER_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_SERVER_API_AUTH_STRING", "key": "latest" } }
},
{ "name": "PREFECT_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_AUTH_STRING", "key": "latest" } }
},
{ "name": "LANGFUSE_SECRET_KEY",
"valueFrom": { "secretKeyRef": { "name": "LANGFUSE_SECRET_KEY", "key": "latest" } }
},
{ "name": "LANGFUSE_PUBLIC_KEY",
"valueFrom": { "secretKeyRef": { "name": "LANGFUSE_PUBLIC_KEY", "key": "latest" } }
},
{ "name": "LANGFUSE_HOST",
"valueFrom": { "secretKeyRef": { "name": "LANGFUSE_HOST", "key": "latest" } }
},
{ "name": "GH_TOKEN",Marvin
10/21/2025, 5:08 PMNate
10/21/2025, 5:08 PMAmith M
10/21/2025, 5:09 PMNate
10/21/2025, 5:10 PMNate
10/21/2025, 5:11 PMAmith M
10/21/2025, 5:12 PMAmith M
10/21/2025, 5:18 PMMarvin
10/21/2025, 5:18 PMMarvin
10/21/2025, 5:20 PM{
"apiVersion": "<http://run.googleapis.com/v1|run.googleapis.com/v1>",
"kind": "Job",
"metadata": {
"name": "example-job"
},
"spec": {
"template": {
"template": {
"spec": {
"containers": [
{
"image": "<http://gcr.io/PROJECT/IMAGE:TAG|gcr.io/PROJECT/IMAGE:TAG>",
"env": [
{
"name": "PLAIN_VAR",
"value": "value"
},
{
"name": "PREFECT_API_URL",
"valueFrom": {
"secretKeyRef": {
"name": "PREFECT_API_URL",
"key": "latest"
}
}
}
]
}
],
"serviceAccountName": "<mailto:SERVICE-ACCOUNT@PROJECT.iam.gserviceaccount.com|SERVICE-ACCOUNT@PROJECT.iam.gserviceaccount.com>",
"timeoutSeconds": 3600,
"restartPolicy": "Never"
}
}
}
}
}
Notes:
- For Job v1, secret-based env uses valueFrom.secretKeyRef with name (Secret Manager secret) and key (version, e.g., "latest").
- If a manifest with spec.template.template.spec “does not deploy,” double-check you’re applying it as a Job (not a Service), and that the API version is <http://run.googleapis.com/v1|run.googleapis.com/v1> with kind: Job. The Service schema is different and will reject the Jobs structure.
- In Prefect 3, if you also define job_variables.env, that can overwrite env from your job_body. If env still isn’t present at runtime, verify you don’t set an empty env list at the pool or deployment level.
If you want to move to the Cloud Run v2 worker later, switch env refs to:
{
"name": "PREFECT_API_URL",
"valueSource": {
"secretKeyRef": {
"secret": "PREFECT_API_URL",
"version": "latest"
}
}
}
If you share the exact error you hit when deploying the spec.template.template.spec form, I can help pinpoint what Cloud Run is rejecting.Amith M
10/21/2025, 5:36 PMMarvin
10/21/2025, 5:36 PMMarvin
10/21/2025, 5:36 PM{
"env": [
{ "name": "test", "value": "test" },
{ "name": "PREFECT_API_URL",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_URL", "key": "latest" } }
},
{ "name": "PREFECT_SERVER_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_SERVER_API_AUTH_STRING", "key": "latest" } }
},
{ "name": "PREFECT_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_AUTH_STRING", "key": "latest" } }
}
// ...your other envs
]
}
Then in your base_job_template, either omit env from job_body or set it to the template variable so Prefect can merge/inject:
"env": "{{ env }}"
A few notes:
- Your Job v1 nesting is fine: spec.template.template.spec.containers[].env is the correct spot for Jobs.
- Prefect will also override command/image/resources/etc. from job_variables and required defaults. If you need to change those, set them via job_variables (not by hard-coding in job_body).
- If you later switch to the Cloud Run v2 worker, change secret refs to valueSource.secretKeyRef with secret/version.
Docs:
- Customize job variables: https://docs-3.prefect.io/v3/how-to-guides/deployments/customize-job-variables
- GCP worker guide: https://docs-3.prefect.io/integrations/prefect-gcp/gcp-worker-guide
If you share how you’re setting job_variables today (pool defaults vs deployment), I can suggest the exact place/snippet to add your env list.Amith M
10/21/2025, 5:50 PMcontainer_env_vars = {
"NAME": "armor-app",
"VERSION": "0.2.0",
"BUILD_TAGS": '["v0.2.0"]',
"ENV": env,
"DEBUG": "false",
"GCS_PROJECT_ID": _PROJECT_ID,
"DB_DIALECT": "postgresql",
"DB_SYNC_DRIVER": "pg8000",
"DB_ASYNC_DRIVER": "asyncpg",
"DB_USER": _SERVICE_ACCOUNT,
"DB_SUPPORTS_SCHEMA": "true",
"DB_NAME": "postgres",
"DB_GCP_SQL_INSTANCE_CONNECTION_NAME": _CLOUDSQL_INSTANCE_NAME,
}
secret_client = secretmanager.SecretManagerServiceAsyncClient()
print("Checking for environment-specific secrets...")
for secret_base_name in secrets_to_map:
env_secret_name = f"{secret_base_name}_{env}"
try:
# Construct the full resource name of the secret to check for its existence
secret_path = secret_client.secret_path(_PROJECT_ID, env_secret_name)
await secret_client.get_secret(request={"name": secret_path})
print(f"✅ Found environment-specific secret: {env_secret_name}")
final_secret_name = env_secret_name
except (NotFound, PermissionDenied):
print(f"⚠️ '{env_secret_name}' not found. Falling back to default: {secret_base_name}")
final_secret_name = secret_base_name
container_env_vars[secret_base_name] = {
"valueFrom": {
"secretKeyRef": {
"name": final_secret_name,
"key": "latest"
}
}
}
used the above and got:
env:
- name: PREFECT_API_URL
value: '{''valueFrom'': {''secretKeyRef'': {''key'': ''latest'', ''name'':
''PREFECT_API_URL''}}}'
- name: PREFECT_API_AUTH_STRING
value: '{''valueFrom'': {''secretKeyRef'': {''key'': ''latest'', ''name'':
''PREFECT_API_AUTH_STRING''}}}'
- name: PREFECT_SERVER_API_AUTH_STRING
value: '{''valueFrom'': {''secretKeyRef'': {''key'': ''latest'', ''name'':
''PREFECT_SERVER_API_AUTH_STRING''}}}'
- name: PREFECT__FLOW_RUN_ID
value: 94f40e2e-e1a3-4d47-b04e-82b17e8e56b5
- name: ENV
value: stage
- name: NAME
value: armor-app
- name: DEBUG
value: 'false'
- name: DB_NAME
value: postgres
- name: DB_USER
value: cloud-run-service-account@windy-furnace-465017-c3.iam.gserviceaccount.com
all the normal ones work as expected but the secret based env vars are not working correctlyMarvin
10/21/2025, 5:50 PMMarvin
10/21/2025, 5:51 PMjob_variables.env.
Right now you’re building a dict like:
{
"PREFECT_API_URL": { "valueFrom": {...} },
"NAME": "armor-app",
...
}
When Prefect builds the Cloud Run Job, it expects env to be a list of objects. Since you passed a dict, the worker converts it to [{"name": k, "value": str(v)}], which stringifies your valueFrom dict. That’s why you see:
- name: PREFECT_API_URL
value: '{''valueFrom'': { ... }}'
Fix: build a list of env objects. Cloud Run v1 uses <http://valueFrom.secretKeyRef.name/key|valueFrom.secretKeyRef.name/key>, so do this:
- Build env_items as a list (not a dict)
- For secrets, use {"name": "<ENV>", "valueFrom": {"secretKeyRef": {"name": "<SECRET>", "key": "latest"}}}
Example adapting your code:
container_env_vars = {
"NAME": "armor-app",
"VERSION": "0.2.0",
"BUILD_TAGS": '["v0.2.0"]',
"ENV": env,
"DEBUG": "false",
"GCS_PROJECT_ID": _PROJECT_ID,
"DB_DIALECT": "postgresql",
"DB_SYNC_DRIVER": "pg8000",
"DB_ASYNC_DRIVER": "asyncpg",
"DB_USER": _SERVICE_ACCOUNT,
"DB_SUPPORTS_SCHEMA": "true",
"DB_NAME": "postgres",
"DB_GCP_SQL_INSTANCE_CONNECTION_NAME": _CLOUDSQL_INSTANCE_NAME,
}
# Add secret-backed envs by name
secret_client = secretmanager.SecretManagerServiceAsyncClient()
for secret_base_name in secrets_to_map:
env_secret_name = f"{secret_base_name}_{env}"
try:
secret_path = secret_client.secret_path(_PROJECT_ID, env_secret_name)
await secret_client.get_secret(request={"name": secret_path})
final_secret_name = env_secret_name
except (NotFound, PermissionDenied):
final_secret_name = secret_base_name
container_env_vars[secret_base_name] = {
"valueFrom": {
"secretKeyRef": {
"name": final_secret_name,
"key": "latest",
}
}
}
# Convert to the list shape Prefect/Cloud Run expects
env_items = []
for name, val in container_env_vars.items():
if isinstance(val, dict) and "valueFrom" in val:
env_items.append({"name": name, "valueFrom": val["valueFrom"]})
else:
env_items.append({"name": name, "value": str(val)})
# Pass as job_variables to Prefect (deployment or pool defaults)
job_variables = {"env": env_items}
Important notes:
- Avoid overriding Prefect’s own system envs (PREFECT_API_URL, PREFECT_API_AUTH_STRING, etc.). The worker injects these and may overwrite your values. If you want those to come from Secret Manager, you can:
- Put new names in Secret Manager (e.g., MY_PREFECT_API_URL) and set your own code to read them; or
- Try the same names, but be aware the worker may still set them from config (last write wins is not guaranteed).
- Your Job v1 nesting is fine: spec.template.template.spec.containers[].env.
References:
- Customize job variables: https://docs-3.prefect.io/v3/how-to-guides/deployments/customize-job-variables
- Cloud Run secrets as env vars (v1): https://cloud.google.com/run/docs/configuring/secrets#reference-secret-env
If you share how you’re attaching job_variables (deployment vs pool), I can show the exact spot to drop env_items.Amith M
10/21/2025, 6:00 PMAmith M
10/21/2025, 6:00 PMMarvin
10/21/2025, 6:00 PMMarvin
10/21/2025, 6:02 PMjob_variables correctly.
Here’s the working pattern for Cloud Run v1:
1) Ensure your base_job_template uses the env template variable
In the work pool’s base job template, point container env at the template variable:
...
"containers": [
{
"image": "{{ image }}",
"command": "{{ command }}",
"env": "{{ env }}", // important: let Prefect inject job_variables.env here
"resources": {
"limits": {"cpu": "{{ cpu }}", "memory": "{{ memory }}"},
"requests": {"cpu": "{{ cpu }}", "memory": "{{ memory }}"}
}
}
],
...
Note: For Cloud Run Jobs v1, the nesting is spec.template.template.spec.containers[].env. Keep your double “template” and put the line above there.
2) Pass env as a list of env objects in job_variables
job_variables.env must be a list, not a dict. Each item is { "name": ..., "value": ... } or { "name": ..., "valueFrom": { "secretKeyRef": ... } }.
Example:
job_variables = {
"env": [
{ "name": "NAME", "value": "armor-app" },
{ "name": "ENV", "value": "stage" },
{ "name": "PREFECT_API_URL",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_URL", "key": "latest" } }
},
{ "name": "PREFECT_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_AUTH_STRING", "key": "latest" } }
},
{ "name": "PREFECT_SERVER_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_SERVER_API_AUTH_STRING", "key": "latest" } }
},
{ "name": "GOOGLE_API_KEY",
"valueFrom": { "secretKeyRef": { "name": "GOOGLE_API_KEY", "key": "latest" } }
}
// ...add the rest
]
}
3) Attach job_variables at deployment or pool defaults
- In Python (Prefect 3.x):
flow.from_source("<https://github.com/org/repo>", entrypoint="path:flow.py:flow").deploy(
name="my-deployment",
work_pool_name="my-cloud-run-pool",
image="<your-image>",
job_variables=job_variables,
)
- Or in prefect.yaml under your deployment:
deployments:
- name: my-deployment
work_pool:
name: my-cloud-run-pool
job_variables:
env:
- name: NAME
value: armor-app
- name: PREFECT_API_URL
valueFrom:
secretKeyRef:
name: PREFECT_API_URL
key: latest
# ...
Why your secret envs were stringified
When you previously built a dict like:
{"PREFECT_API_URL": {"valueFrom": { ... }}}
Prefect treated it as a single env var with name "PREFECT_API_URL" and value equal to the stringified dict, because it expects a list of objects, not a dict mapping. That’s why you saw:
- name: PREFECT_API_URL
value: '{''valueFrom'': {...}}'
Final tips
- Keep your Job v1 nesting: spec.template.template.spec.containers[].env is correct for Jobs.
- Don’t hard-code an env array in job_body; it will be overwritten. Use the template hook "env": "{{ env }}".
- For Cloud Run v1, secret references use <http://valueFrom.secretKeyRef.name/key|valueFrom.secretKeyRef.name/key>.
- If you ever move to Cloud Run v2 worker, switch to valueSource.secretKeyRef.secret/version.
Docs:
- Customize job variables: https://docs-3.prefect.io/v3/how-to-guides/deployments/customize-job-variables
- GCP worker guide: https://docs-3.prefect.io/integrations/prefect-gcp/gcp-worker-guide
- Cloud Run secrets as env vars (v1): https://cloud.google.com/run/docs/configuring/secrets#reference-secret-env
If you share how you’re injecting job_variables (Python deploy vs prefect.yaml) and your current base_job_template snippet for the container, I can validate it end-to-end.Amith M
10/21/2025, 6:06 PM"env": {
"title": "Environment Variables",
"description": "Environment variables to set when starting a flow run.", "type": "object",
"additionalProperties": {"type": "string"},
"default": [
{"name": "test", "value": "test"},
{"name": "env", "valueFrom": {
"secretKeyRef": {
"name": "test",
"key": "latest"
}
}},
],
}
i get: File "/Users/amithmundur/PycharmProjects/armor-adk/.venv/lib/python3.12/site-packages/pydantic/main.py", line 253, in init
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/amithmundur/PycharmProjects/armor-adk/.venv/lib/python3.12/site-packages/prefect/workers/base.py", line 146, in _coerce_env
return {k: str(v) if v is not None else None for k, v in v.items()}
^^^^^^^
AttributeError: 'list' object has no attribute 'items'Marvin
10/21/2025, 6:06 PMMarvin
10/21/2025, 6:07 PM{
"variables": {
"type": "object",
"properties": {
"container_env": {
"title": "Container environment variables",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": [
"string",
"null"
]
},
"valueFrom": {
"type": "object",
"properties": {
"secretKeyRef": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"key": {
"type": "string"
}
},
"required": [
"name",
"key"
]
}
}
}
},
"required": [
"name"
]
},
"default": []
},
"image": {
"type": "string"
},
"command": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"cpu": {
"type": "string"
},
"memory": {
"type": "string"
},
"timeout": {
"type": "integer"
},
"service_account_name": {
"type": "string"
},
"vpc_connector_name": {
"type": "string"
}
},
"required": [
"image"
]
},
"job_configuration": {
"job_body": {
"apiVersion": "<http://run.googleapis.com/v1|run.googleapis.com/v1>",
"kind": "Job",
"metadata": {
"name": "{{ name }}"
},
"spec": {
"template": {
"metadata": {
"annotations": {
"<http://run.googleapis.com/vpc-access-connector|run.googleapis.com/vpc-access-connector>": "{{ vpc_connector_name }}"
}
},
"template": {
"spec": {
"containers": [
{
"image": "{{ image }}",
"command": "{{ command }}",
"env": "{{ container_env }}",
"resources": {
"limits": {
"cpu": "{{ cpu }}",
"memory": "{{ memory }}"
},
"requests": {
"cpu": "{{ cpu }}",
"memory": "{{ memory }}"
}
}
}
],
"serviceAccountName": "{{ service_account_name }}",
"timeoutSeconds": "{{ timeout }}"
}
}
}
}
}
}
}
2) Provide container_env via job_variables at deployment (or pool defaults)
Cloud Run v1 secret refs use valueFrom.secretKeyRef.name/key.
job_variables = {
"container_env": [
{ "name": "NAME", "value": "armor-app" },
{ "name": "ENV", "value": "stage" },
{ "name": "GOOGLE_API_KEY",
"valueFrom": { "secretKeyRef": { "name": "GOOGLE_API_KEY", "key": "latest" } }
},
{ "name": "PREFECT_API_URL",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_URL", "key": "latest" } }
}
// ...more
]
}
3) Don’t use the reserved env in variables for container env
- Keep BaseVariables.env (dict) for simple worker-level env if you need it.
- Use container_env (array) for Cloud Run container env. This avoids the Pydantic coercion error and allows secret refs to be passed through as objects.
Notes
- Prefect will still inject its own required env into the container (e.g., PREFECT_API_URL, PREFECT__FLOW_RUN_ID). Your container_env will be merged in addition to those.Marvin
10/21/2025, 6:07 PMAmith M
10/21/2025, 6:16 PMMarvin
10/21/2025, 6:16 PMMarvin
10/21/2025, 6:17 PMprefect work-pool get-default-base-job-template --type cloud-run --file base_job_template.json
- Edit base_job_template.json:
- Add a new variables property container_env (array of objects)
- Map containers[].env to "{{ container_env }}"
Example minimal patch:
{
"job_configuration": {
...
"job_body": {
"apiVersion": "<http://run.googleapis.com/v1|run.googleapis.com/v1>",
"kind": "Job",
"spec": {
"template": {
"spec": {
"template": {
"spec": {
"containers": [
{
"image": "{{ image }}",
"command": "{{ command }}",
"env": "{{ container_env }}", // <-- wire your array here
"resources": {
"limits": { "cpu": "{{ cpu }}", "memory": "{{ memory }}" },
"requests": { "cpu": "{{ cpu }}", "memory": "{{ memory }}" }
}
}
],
"timeoutSeconds": "{{ timeout }}",
"serviceAccountName": "{{ service_account_name }}"
}
}
},
"metadata": {
"annotations": {
"<http://run.googleapis.com/vpc-access-connector|run.googleapis.com/vpc-access-connector>": "{{ vpc_connector_name }}"
}
}
}
}
},
...
},
"variables": {
"type": "object",
"properties": {
...
"container_env": {
"title": "Container environment variables",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": ["string", "null"] },
"valueFrom": {
"type": "object",
"properties": {
"secretKeyRef": {
"type": "object",
"properties": {
"name": { "type": "string" },
"key": { "type": "string" }
},
"required": ["name", "key"]
}
}
}
},
"required": ["name"]
},
"default": []
}
}
}
}
- Update the pool with your edited template:
prefect work-pool update "<your-pool>" --base-job-template base_job_template.json
2) Provide your secret-based env as an array via job_variables.container_env
Cloud Run v1 secret refs use valueFrom.secretKeyRef.name/key.
job_variables = {
"container_env": [
{ "name": "NAME", "value": "armor-app" },
{ "name": "ENV", "value": "stage" },
{ "name": "PREFECT_API_URL",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_URL", "key": "latest" } }
},
{ "name": "PREFECT_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_API_AUTH_STRING", "key": "latest" } }
},
{ "name": "PREFECT_SERVER_API_AUTH_STRING",
"valueFrom": { "secretKeyRef": { "name": "PREFECT_SERVER_API_AUTH_STRING", "key": "latest" } }
},
{ "name": "GOOGLE_API_KEY",
"valueFrom": { "secretKeyRef": { "name": "GOOGLE_API_KEY", "key": "latest" } }
}
// add the rest...
]
}
Then deploy with:
```
flow.from_source("https://github.com/org/repo", entrypoint="path/to:flow").deploy(
name="my-deployment",
work_pool_name="<your-pool>",
image="<your-image>",Marvin
10/21/2025, 6:17 PMAmith M
10/21/2025, 6:19 PMMarvin
10/21/2025, 6:19 PMMarvin
10/21/2025, 6:20 PMvariables.env as a dict[str, str] and does not pass through valueFrom objects. As a result:
- Any containers[].env you hardcode in job_body is ignored/overwritten
- variables.env cannot carry valueFrom.secretKeyRef because it’s coerced to a dict of strings
That’s why you only see the Prefect-injected envs (PREFECT_API_URL, PREFECT__FLOW_RUN_ID, etc.) in the resulting Job.
Your options:
Option 1 — Switch to the Cloud Run v2 worker (recommended)
- v2 is more flexible and you can wire an array of env objects directly to the container.
Steps:
1) Create a v2 pool and export its template
prefect work-pool create my-cr-v2 --type cloud-run-v2
prefect work-pool get-default-base-job-template --type cloud-run-v2 --file base_v2.json
2) Edit base_v2.json: add a new variable container_env (array of objects) and map it to the container’s env:
- Add this under variables.properties:
"container_env": {
"title": "Container environment variables",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": ["string", "null"] },
"valueSource": {
"type": "object",
"properties": {
"secretKeyRef": {
"type": "object",
"properties": {
"secret": { "type": "string" },
"version": { "type": "string" }
},
"required": ["secret", "version"]
}
}
}
},
"required": ["name"]
},
"default": []
}
- Map it into job_body at:
job_body.template.template.containers[0].env = "{{ container_env }}"
3) Update the pool with your edited template
prefect work-pool update my-cr-v2 --base-job-template base_v2.json
4) Provide your secrets in deployment/job_variables using v2 syntax:
job_variables = {
"container_env": [
{ "name": "NAME", "value": "armor-app" },
{
"name": "PREFECT_API_URL",
"valueSource": {
"secretKeyRef": { "secret": "PREFECT_API_URL", "version": "latest" }
}
}
// …
]
}
Option 2 — Stay on Cloud Run v1 but use a custom worker or fetch secrets at runtime
- Custom worker: Subclass the Cloud Run v1 worker to accept a container_env array and merge it into containers[].env before submit. This bypasses the dict[str, str] coercion.
- Fetch at runtime: Read from Secret Manager in your flow or entrypoint and export to os.environ before running your app.
Why hardcoding containers[].env in v1 job_body is ignored
- The v1 worker reconstructs the container spec and sets env from its internal logic (Prefect-required vars + variables.env dict). It doesn’t merge or respect a pre-populated job_body.containers[].env.
Docs for reference:
- GCP worker guide: https://docs-3.prefect.io/integrations/prefect-gcp/gcp-worker-guide
- Customize job variables: https://docs-3.prefect.io/v3/how-to-guides/deployments/customize-job-variables
- Cloud Run secrets as env vars: https://cloud.google.com/run/docs/configuring/secrets#reference-secret-env
If you’re open to v2, I can share a ready-to-paste base_v2.json patch with container_env wired correctly. If you must stay on v1, I can sketch a minimal custom worker that preserves valueFrom.secretKeyRef into the container.Amith M
10/21/2025, 6:49 PMMarvin
10/21/2025, 6:49 PMMarvin
10/21/2025, 6:49 PM{
"name": "PREFECT_API_URL",
"valueSource": {
"secretKeyRef": {
"secret": "PREFECT_API_URL",
"version": "latest"
}
}
}
- Make sure the service account on the Job has Secret Manager Secret Accessor (roles/secretmanager.secretAccessor) to read those secrets.
If you haven’t already, a good starting point is exporting the default v2 template and patching it:
prefect work-pool get-default-base-job-template --type cloud-run-v2 --file base_v2.json
Then wire your container env:
- job_body.template.template.containers[0].env = "{{ container_env }}"
- variables.properties.container_env = array of objects, each with name/value or name/valueSource
Refs:
- GCP worker guide: https://docs-3.prefect.io/integrations/prefect-gcp/gcp-worker-guide
- Customize job variables: https://docs-3.prefect.io/v3/how-to-guides/deployments/customize-job-variables
If you want, share your final base_v2.json and I can sanity-check it.Amith M
10/21/2025, 6:52 PMMarvin
10/21/2025, 6:53 PMMarvin
10/21/2025, 6:53 PMNate
10/21/2025, 6:54 PM