Goal: deploy an ADK agent to Agent Runtime, Google's managed agent hosting (formerly "Agent Engine"), using only Terraform: no Docker, no web server code. Then see it appear automatically in Agent Registry, register the Cloud Run agent from project 01 there too, call the agent from Python and REST, and make it available to employees in Gemini Enterprise.
Do project 01 first; this one assumes you know what an agent, a tool, a session, and a Terraform resource are.
Contents
- Concepts: Agent Runtime, Agent Registry, Gemini Enterprise
- The files
- The agent code explained
- How the code gets to Google:
pack.py - The Terraform explained
- Step by step
- Calling the agent (Python, REST, console)
- Agent Registry: see, search, discover
- Add the agent to Gemini Enterprise
- Testing
- Monitoring
- Change and redeploy
- Best practices and next steps
- Troubleshooting
- Clean up
1. Concepts
Agent Runtime: "just give me your Python, I'll host it"
In project 01 you built the container, wrote the web server, chose where sessions live, and so on. With Agent Runtime you upload source code + requirements.txt, and Google:
- builds and runs the container, scales it (0 → N), and gives it an HTTPS API,
- provides managed sessions (conversation history stored by Google, shared by all instances, so there's no "max 1 instance" problem) and Memory Bank (long-term memory across sessions),
- plugs in tracing, logging, and a playground in the console,
- registers the agent in Agent Registry automatically,
- is the native way to plug a custom agent into Gemini Enterprise.
In APIs and Terraform it's still called ReasoningEngine: the resource name looks like
projects/123/locations/europe-west1/reasoningEngines/456, and the Terraform resource is google_vertex_ai_reasoning_engine.
AdkApp: the adapter
Agent Runtime doesn't call your Agent directly. It calls methods on a Python object: async_create_session,
async_stream_query, and so on. AdkApp(agent=root_agent) is the wrapper that provides those methods for an ADK agent.
client ──HTTPS──▶ Agent Runtime ──calls──▶ app.async_stream_query(user_id, session_id, message)
│ (AdkApp)
└─▶ runs root_agent (Gemini + tools), stores events in the managed session
Agent Registry: the company's "agent catalog"
When a company has dozens of agents and MCP servers (tool servers) spread over Cloud Run, GKE, and Agent Runtime,
nobody knows what exists. Agent Registry (agentregistry.googleapis.com) is the central inventory:
- Agents, MCP servers, Endpoints (plain APIs), and Skills each get an entry with name, description, protocols/URLs, and skills (from an A2A Agent Card).
- Automatic registration: agents on Agent Runtime (this project), on GKE (with a label), and on
Cloud Run (deployed with
gcloud alpha run deploy --functional-type=agent --identity-type=agent-identity). - Manual registration: anything else, including our project 01 agent, as a Service resource (Terraform
google_agent_registry_service). - Consumers: humans (console,
gcloud agent-registry agents search), other agents (ADK'sAgentRegistryclass turns an entry into a sub-agent or toolset), Agent Studio, and Gemini Enterprise.
Read-only vs writable: you write a Service; the registry projects it as a read-only Agent (or McpServer, or
Endpoint) that consumers read.
A2A (Agent2Agent protocol), in one paragraph
An open standard for agents to talk to each other. An A2A agent publishes an Agent Card (JSON: name, description, URL, skills) and accepts messages at a standard endpoint. If an agent speaks A2A, the registry knows its skills, so an orchestrator agent can pick it automatically. Our agents are plain REST for now, which is fine to start with.
Gemini Enterprise
The app employees use (a chat interface at vertexaisearch.cloud.google.com/..., formerly Agentspace).
Admins can add custom agents to it. An Agent Runtime agent is added via Agents → Add agent → Custom agent via
Agent Runtime. Then users see it in the agent list and chat with it, with SSO and their own identity.
you (Terraform) Google manages people
┌───────────────────────────────┐ ┌──────────────────────────────────┐ ┌──────────────────────────┐
│ agent/ source ──pack.py──▶ │ │ Agent Runtime: build, run, scale │ │ Gemini Enterprise app │
│ google_vertex_ai_reasoning_ │──▶│ sessions, memory, traces │──▶│ Google Chat (project 03) │
│ engine │ │ │ auto-register │ │ other agents via registry│
│ google_agent_registry_service │──▶│ Agent Registry (catalog) │ └──────────────────────────┘
└───────────────────────────────┘ └──────────────────────────────────┘
2. The files
02-agent-runtime-registry-terraform/
├── agent/ ← EXACTLY what gets uploaded to Agent Runtime
│ ├── requirements.txt ← installed by Agent Runtime when it builds
│ └── currency_agent/
│ ├── __init__.py
│ ├── agent.py ← tool + root_agent + `app = AdkApp(...)`
│ └── .env.example ← for local runs only (never uploaded)
├── scripts/
│ ├── pack.py ← makes agent/ into a base64 tar.gz for Terraform
│ ├── query.py ← call the deployed agent from Python
│ ├── discover.py ← list agents from Agent Registry (like an orchestrator would)
│ └── test.sh ← REST smoke test (fails if the tool wasn't called)
└── terraform/
├── versions.tf / variables.tf / outputs.tf / terraform.tfvars.example
├── main.tf ← APIs, source packaging, the Agent Runtime agent
├── registry.tf ← manual registration of the Cloud Run agent (optional)
└── class_methods.json ← the agent's API "contract" (which methods clients can call)
3. The agent code explained
File: agent/currency_agent/agent.py
RATES_TO_EUR = {"EUR": 1.0, "USD": 0.86, ...}
def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
"""Converts an amount of money from one currency to another. ..."""A tool with three typed arguments. Gemini reads the types and docstring and fills in
amount=250, from_currency="USD", to_currency="MAD" from a sentence like "how much is 250 dollars in dirhams?".
Unknown currencies → {"status": "error", ...} so the model can explain instead of making something up.
root_agent = Agent(name="currency_agent", model=Gemini(...), description=..., instruction=..., tools=[convert_currency])Same as project 01. The description matters even more here: Agent Registry and Gemini Enterprise show it
to people and to other agents.
app = AdkApp(agent=root_agent)The only new line. Agent Runtime imports the module currency_agent.agent and uses the object app
(Terraform: entrypoint_module / entrypoint_object). AdkApp comes from agentplatform.frameworks.adk
(the new package name; the old vertexai.agent_engines.AdkApp still works). Inside Agent Runtime, AdkApp
automatically uses the managed session service and Memory Bank. Locally it uses in-memory ones.
if __name__ == "__main__": assert ...A self-check of the tool logic: cd agent && python -m currency_agent.agent → convert_currency OK.
4. How the code gets to Google: pack.py
Terraform resource google_vertex_ai_reasoning_engine accepts the code inline, as a
base64-encoded .tar.gz of the source folder (source_code_spec.inline_source.source_archive).
scripts/pack.py builds that archive and prints {"archive": "<base64>"}. That's the JSON
format Terraform's external data source expects.
Why is it "deterministic"? A normal tar czf records file timestamps and the gzip creation time, so the bytes
change on every run. Terraform would then see a "change" on every plan and redeploy every time (≈ 5 minutes
each). pack.py sorts files, sets every timestamp and owner to 0, and uses gzip mtime=0. Result:
same code ⇒ identical bytes ⇒ plan says "No changes". The idea is the same as the image-tag hash in project 01.
It skips __pycache__, .env (secrets must never be uploaded), .adk, and .venv.
Archive contents (the paths Terraform refers to):
requirements.txt
currency_agent/__init__.py
currency_agent/agent.py
5. The Terraform explained
versions.tf
Same as project 01, plus the external provider (runs pack.py).
variables.tf
project_id, region (where the agent runs, europe-west1), display_name, model,
cloud_run_agent_url (optional, from project 01), registry_location (for manual entries, global).
main.tf
① APIs. aiplatform (Agent Runtime + Gemini), agentregistry, cloudresourcemanager (required by Agent
Runtime deployments), plus logging, monitoring, trace, and telemetry.
② data "external" "agent_source"
data "external" "agent_source" {
program = ["python3", "${path.module}/../scripts/pack.py"]
query = { src = abspath("${path.module}/../agent") }
}A data source (it reads and creates nothing): on every plan, Terraform runs pack.py, sends it
{"src": ".../agent"} on stdin, and reads the JSON it prints. The result is available as
data.external.agent_source.result.archive.
③ google_vertex_ai_reasoning_engine.agent: the agent itself
resource "google_vertex_ai_reasoning_engine" "agent" {
display_name = var.display_name # shown in console, registry, Gemini Enterprise
description = "..."
region = var.region
spec {
agent_framework = "google-adk" # tells Google it's ADK → enables ADK-specific features (UI, sessions)
class_methods = file("${path.module}/class_methods.json")-
class_methods: the list of methods clients may call onapp(async_create_session,async_stream_query, …), each with anapi_mode:""→ normal call through:query"async"→ async call through:query"stream"/"async_stream"→ streamed response through:streamQuery
When you deploy with the Python SDK it inspects
AdkAppand generates this list for you. Terraform can't run your Python, so the list is written by hand in class_methods.json (the 11 standard ADK methods, as documented by the provider). If it's missing, clients get "method not found".
source_code_spec {
inline_source { source_archive = data.external.agent_source.result.archive }
python_spec {
version = "3.12"
entrypoint_module = "currency_agent.agent" # → currency_agent/agent.py inside the archive
entrypoint_object = "app" # → the AdkApp object
requirements_file = "requirements.txt" # → pip install -r requirements.txt
}
}- This is source deployment: Google builds the container for you. The alternatives are
container_spec(your own image) orpackage_spec(old style: a pickled object in GCS, avoid it).
deployment_spec {
min_instances = 0 # scale to zero
max_instances = 2 # OK to scale: sessions are managed (shared), not in memory
env { name = "MODEL" value = var.model }
}
}
timeouts { create = "30m" update = "30m" } # building takes a few minutes
depends_on = [google_project_service.apis]
}- No
service_account→ the agent runs as the project's Reasoning Engine Service Agent (service-PROJECT_NUMBER@gcp-sa-aiplatform-re.iam.gserviceaccount.com), which Google creates and which can already call Gemini. See Best practices for a dedicated identity.
registry.tf
resource "google_agent_registry_service" "cloud_run_agent" {
count = var.cloud_run_agent_url == "" ? 0 : 1 # only if you gave the URL
location = var.registry_location # "global"
service_id = "weather-agent-cloud-run"
display_name = "weather-agent (Cloud Run)"
description = "Answers questions about the current weather in a city. ..."
agent_spec { type = "NO_SPEC" } # plain REST agent (no A2A card)
interfaces { protocol_binding = "HTTP_JSON" url = var.cloud_run_agent_url }
}- There is nothing in Terraform for the Agent Runtime agent: it's registered automatically.
- The project 01 agent runs on plain Cloud Run, so we register it manually as a
Service. WithNO_SPEC, the registry knows its name, description, and URL, but not its skills. Once it speaks A2A, switch totype = "A2A_AGENT_CARD"withcontent = jsonencode(<agent card>)(and nointerfaces).
outputs.tf
agent_resource_name (you'll need it everywhere), agent_id, query_url, console_url,
registered_cloud_run_agent.
6. Step by step
6.1 Try it locally first
cd ~/Bureau/trash && source .venv/bin/activate
cd 02-agent-runtime-registry-terraform/agent
python -m currency_agent.agent # tool self-check → "convert_currency OK"
cp currency_agent/.env.example currency_agent/.env # set your project ID
adk web . # http://localhost:8000 → currency_agent
# try: "How much is 250 USD in MAD?", "and in yen?", "convert 10 BTC to EUR" (→ polite error)6.2 Deploy
cd ../terraform
cp terraform.tfvars.example terraform.tfvars # set project_id
# optional: register project 01's agent too
echo "cloud_run_agent_url = \"$(terraform -chdir=../../01-adk-cloud-run-terraform/terraform output -raw url)\"" >> terraform.tfvars
terraform init
terraform plan
terraform apply # 5-10 min the first time: Google builds the image and installs requirements
terraform output agent_resource_name6.3 Check it in the console
terraform output console_url → Agent Platform → Agent Runtime (Deployments) → currency-agent:
- Playground tab: chat with it right away,
- Sessions, Traces, Logs, Metrics tabs,
- Deployment details: build status (if the build fails, the error is here).
7. Calling the agent
Python (scripts/query.py)
cd ~/Bureau/trash/02-agent-runtime-registry-terraform
python scripts/query.py "How much is 250 USD in MAD?"
# session: 1234567890
# -> tool call: convert_currency({'amount': 250, 'from_currency': 'USD', 'to_currency': 'MAD'})
# agent: 250 USD is 2336.96 MAD.What it does:
remote = agentplatform.Client(project=..., location=...).runtimes.get(name=AGENT_NAME) # a proxy object
session = await remote.async_create_session(user_id="demo-user") # → class_methods!
async for event in remote.async_stream_query(user_id=..., session_id=session["id"], message=question): ...remote.async_create_session exists because of class_methods.json: the SDK generates one Python method per
declared method.
REST (any language)
NAME=$(terraform -chdir=terraform output -raw agent_resource_name)
BASE="https://europe-west1-aiplatform.googleapis.com/v1/$NAME"
TOKEN=$(gcloud auth print-access-token) # ← ACCESS token here (Google API), not identity token
# create a session → {"output": {"id": "...", ...}}
curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" "$BASE:query" \
-d '{"class_method": "async_create_session", "input": {"user_id": "u1"}}'
# ask (streamed, Server-Sent Events)
curl -N -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" "$BASE:streamQuery?alt=sse" \
-d '{"class_method": "async_stream_query",
"input": {"user_id": "u1", "session_id": "SESSION_ID", "message": "100 GBP in EUR?"}}'
# list a user's sessions
curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" "$BASE:query" \
-d '{"class_method": "async_list_sessions", "input": {"user_id": "u1"}}'Access token vs identity token. Cloud Run (project 01) checks an identity token (
print-identity-token: "who you are"). Google APIs such as Agent Runtime take an access token (print-access-token: "what you may do"). Mixing them up gives 401/403.
Who can call it? Anyone with roles/aiplatform.user on the project. To allow one app or user on this agent only:
google_vertex_ai_reasoning_engine_iam_member (Terraform).
8. Agent Registry
# the Agent Runtime agent, auto-registered in its region
gcloud agent-registry agents list --location=europe-west1
# the manually registered Cloud Run agent
gcloud agent-registry agents list --location=global
gcloud agent-registry services list --location=global # the writable "Service" you created
# search (keyword / semantic)
gcloud agent-registry agents search --location=europe-west1 --search-string="currency conversion"
# details
gcloud agent-registry agents describe AGENT_ID --location=europe-west1Console: Agent Registry → Agents (filter by location or runtime).
Discovery from code (how an orchestrator agent finds others)
python scripts/discover.py europe-west1
python scripts/discover.py globalscripts/discover.py uses ADK's AgentRegistry class. In a real orchestrator you'd go further:
from google.adk.integrations.agent_registry import AgentRegistry
registry = AgentRegistry(project_id="...", location="global")
fx = registry.get_remote_a2a_agent("projects/.../agents/...") # A2A agents → usable as a sub-agent
tools = registry.get_mcp_toolset("projects/.../mcpServers/...") # MCP servers → usable as tools
boss = Agent(name="assistant", model=..., sub_agents=[fx], tools=[tools], instruction="Delegate...")(This needs pip install "google-adk[a2a,agent-identity]" mcp. get_remote_a2a_agent only works for agents that
speak A2A; see next steps.)
Manual registration with gcloud (same as registry.tf):
gcloud agent-registry services create my-agent --location=global \
--display-name="My Agent" --agent-spec-type=no-spec \
--interfaces="url=https://my-agent.example.com,protocolBinding=http-json"9. Add the agent to Gemini Enterprise
Prerequisites: a Gemini Enterprise subscription/app in your org (Console → Gemini Enterprise) and the
Gemini Enterprise Admin role. Location compatibility: an app in global or eu works with an agent in
europe-west1.
Option A: console (easiest)
Gemini Enterprise → your app → Agents → Add agent → Custom agent via Agent Runtime, then paste
terraform output -raw agent_resource_name, set a display name, a description, and an icon.
Option B: REST (scriptable)
PROJECT_ID=$(gcloud config get project)
APP_ID=your-gemini-enterprise-app-id # Console → Gemini Enterprise → Apps → ID column
LOC=global # "global", "us" or "eu": the app's location
AGENT=$(terraform -chdir=terraform output -raw agent_resource_name)
HOST=$([ "$LOC" = global ] && echo discoveryengine.googleapis.com || echo $LOC-discoveryengine.googleapis.com)
curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" -H "X-Goog-User-Project: $PROJECT_ID" \
"https://$HOST/v1alpha/projects/$PROJECT_ID/locations/$LOC/collections/default_collection/engines/$APP_ID/assistants/default_assistant/agents" \
-d "{
\"displayName\": \"Currency converter\",
\"description\": \"Converts amounts between EUR, USD, GBP, JPY and MAD.\",
\"adkAgentDefinition\": {\"provisionedReasoningEngine\": {\"reasoningEngine\": \"$AGENT\"}}
}"Then Agents → your agent → Sharing: choose who can use it. Users open the Gemini Enterprise web app → Agents → Currency converter.
If your tools need to act as the user (read their Drive, BigQuery, …), you add an OAuth authorizationConfig
to the registration; see the official guide.
10. Testing
| Level | Command |
|---|---|
| Tool logic | cd agent && python -m currency_agent.agent |
| Local, interactive | adk web agent (from the project folder) |
| Deployed, Python | python scripts/query.py "100 USD to EUR" |
| Deployed, REST smoke test | ./scripts/test.sh: creates a session, streams a query, fails if convert_currency wasn't called |
| Console | Agent Runtime → currency-agent → Playground |
11. Monitoring
Agent Runtime → currency-agent:
- Traces: every query as a waterfall (
invoke_agent→call_llm→execute_tool convert_currency), with timings and token counts. Also in Cloud Trace → Trace explorer. - Logs: in the tab, or in Logs Explorer:
resource.type="aiplatform.googleapis.com/ReasoningEngine" resource.labels.reasoning_engine_id="AGENT_ID" - Metrics: request count, latency, errors, instances, CPU/memory. Alert on them in Cloud Monitoring.
- Sessions: browse real conversations (great for debugging "why did it answer that?").
12. Change and redeploy
Edit anything in agent/ → terraform plan shows spec.source_code_spec.inline_source.source_archive changing →
terraform apply → Google rebuilds (a few minutes) and switches over. The resource name and ID stay the same,
so Gemini Enterprise, the registry, and the Chat bridge keep working with no changes.
Changing requirements.txt or MODEL works the same way.
13. Best practices
- Stable ID, changing code. Anything that points at the agent (Gemini Enterprise, the Chat bridge, other agents)
uses the resource name. Never destroy and recreate it just to update the code;
applyupdates in place. - Dedicated identity (production): either set
spec.service_accountto a service account with onlyroles/aiplatform.user(+ what your tools need), or use Agent Identity (spec.identity_type = "AGENT_IDENTITY"), a per-agent identity governed by the Agent Platform. Grant it only what the tools need. - Secrets:
deployment_spec.secret_env { name = "API_KEY" secret_ref { secret = "my-secret" version = "latest" } }(the secret must already exist in Secret Manager). Never commit a.env;pack.pyrefuses to upload them. - Pin requirements (done), and keep
class_methods.jsonin sync if you upgrade to a very different ADK. Deploy once withadk deploy agent_engineand compare if in doubt. - Descriptions are product: the registry, Gemini Enterprise, and orchestrator agents choose agents from their
descriptionand skills. Write them for a reader who has never seen your code. - Make it A2A (next step): serve an Agent Card so skills are discoverable. This is what lets the registry, Agent
Studio, and other agents use it as a sub-agent. In
class_methods.json, add"a2a_agent_card": "<agent card JSON string>"toasync_create_session(see the provider docs forgoogle_vertex_ai_reasoning_engine). - Memory Bank: to remember users across sessions, add
PreloadMemoryToolto the agent and configurecontext_spec.memory_bank_configin Terraform. - Evaluate before changing the model or instructions (
adk eval, or Agent Platform evaluation).
14. Troubleshooting
| Symptom | Fix |
|---|---|
apply fails with a build error | Console → Agent Runtime → the agent → deployment/build logs. Usually a missing package in requirements.txt or an import error |
apply shows changes every time | Something non-deterministic is in agent/ (e.g. a generated file): delete it, or add it to skip in pack.py |
method not found: async_stream_query | class_methods.json doesn't match AdkApp. Compare with AdkApp(...).register_operations() |
401/403 on REST | Use gcloud auth print-access-token, and check you have roles/aiplatform.user |
| Agent not in the registry | Check the right location (--location=europe-west1). Auto-registration only covers the same project. Wait a minute after deploy |
404 Publisher model not found | Change model in tfvars |
| Gemini Enterprise: "location not compatible" | App in us → deploy the agent to a US region (region = "us-central1"); eu → an EU region; global → any |
15. Clean up
cd terraform && terraform destroyRemove the agent from Gemini Enterprise first (console: Agents → ⋮ → Delete), or the registration will point at a deleted agent.