Secrets and environment variables
Read configuration and credentials on a worker, and see what the platform will and will not give you back.
Introduction
Configuration reaches your worker as environment variables, whether you set it with --env-var or attached a secret. This app reports which of the names it expects arrived, and never what they hold.
There are no weights, so the configuration is the whole subject.
The app
"""What a worker can see of its own configuration, and what it must never answer with."""
from __future__ import annotations
import os
from pydantic import BaseModel, Field
from runware_serverless import serve
EXPECTED = ("LOG_LEVEL", "CACHE_DIR", "API_TOKEN")
"""The names this app reads. Two are plain configuration and one is a secret, and
nothing below can tell which is which."""
class Binding(BaseModel):
name: str
present: bool = Field(description="Whether the name reached the worker. Never the value.")
class Report(BaseModel):
bindings: list[Binding]
missing: list[str] = Field(description="The expected names that did not arrive.")
@serve
class ConfigAndSecrets:
def load(self) -> None:
# Read once, not per request. Changing a variable or an attachment records a
# new version and rolls the app, so a running worker never holds a stale
# value: it is replaced by one that read the new set here.
self.present = {name: name in os.environ for name in EXPECTED}
def predict(self) -> Report:
return Report(
bindings=[Binding(name=name, present=ok) for name, ok in self.present.items()],
missing=[name for name, ok in self.present.items() if not ok],
)How both arrive
LOG_LEVEL comes from --env-var and API_TOKEN from a secret, and model.py reads both with the same line. Your code cannot tell them apart, so they share one namespace. Name a plain variable the same as an attached secret's injected name and the platform rejects it, and the reverse too.
What differs is what can be read back:
apps env-vars list CACHE_DIR = /weights the value is printed
attachments DOCS_EXAMPLE_TOKEN -> API_TOKEN only the namesYou can read a plain variable's value back from the API and you can never read a secret's, and an attachment is recorded as an audit event on your app.
Keeping the value in the worker
A handler that returned a secret would publish a credential to every caller and into the task record, where it outlives the request. The response here carries presence and nothing else, and a test asserts the value cannot appear in it.
That means your app cannot tell you whether a secret holds the right value. You check that on the attachment.
Reading it once
Changing a variable or an attachment records a new version and rolls the app, so the worker holding the old value is replaced by one that read the new set in load.
Re-reading os.environ on every request guards against something that cannot happen.
Renaming on attachment
The attachment decides which stored secret fills a variable, so the same code runs against a production and a staging credential without an edit. This app always reads API_TOKEN.
Deploying it
runware serverless deploy model.py \
--id config-and-secrets \
--gpu-type rtx-pro-6000 \
--env-var LOG_LEVEL=info \
--env-var CACHE_DIR=/weightsThen the secret, which is two steps because one secret usually serves several apps:
printf '%s' "$TOKEN" | runware serverless secrets set DOCS_EXAMPLE_TOKEN --value-file -
runware serverless secrets attach config-and-secrets DOCS_EXAMPLE_TOKEN --env-var-name API_TOKEN--value-file - reads from stdin, so the value never reaches the process list. A value passed as --env-var does, which is one more reason anything sensitive belongs in a secret.
Calling it
import { createClient } from '@runware/sdk'
const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })
const task = await client.invoke({
appId: 'config-and-secrets',
endpointPath: 'predict',
payload: {}
})import asyncio
import os
from runware import Runware
async def main():
client = Runware(api_key=os.environ["RUNWARE_API_KEY"])
try:
task = await client.invoke(
"config-and-secrets",
"predict",
{},
)
finally:
await client.close()
asyncio.run(main())curl https://api.serverless.runware.ai/v1/apps/config-and-secrets/invoke-async/predict \
-H "Authorization: Bearer $RUNWARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {}
}'echo '{}' | runware serverless apps invoke config-and-secrets predict -f -{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {}
}Before the attach, API_TOKEN comes back under missing. After it, present, while the stored secret is still named DOCS_EXAMPLE_TOKEN.
The first call answered in 56 seconds, under the sync deadline, so there is no 202 to poll here. An app that declares no dependencies has a small image to pull, and the difference against one carrying torch is minutes.