
Vertex AI Agent Engine: Deploy an ADK Agent with Terraform & CI/CD | 2026
Vertex AI Agent Engine is Google Cloud's managed runtime for agents — you write an agent with the Agent Development Kit (ADK), deploy it with `adk deploy agent_engine` (or the `agent_engines.create` Python SDK), and query it over a managed `:streamQuery` endpoint. This tutorial scaffolds an ADK agent, adds a custom tool, deploys it, codifies the engine as a Terraform `google_vertex_ai_reasoning_engine`, and wires a GitHub Actions CI/CD pipeline with Workload Identity Federation.
By Mateusz Chmielewski · Aug 19, 2026 · 16 min read
What Is Vertex AI Agent Engine?
Vertex AI Agent Engine (API surface: Reasoning Engine) is Google Cloud's fully managed runtime for deploying AI agents built with frameworks like the Agent Development Kit (ADK), LangChain, or LlamaIndex. You hand it a Python object — an ADK `Agent` with a model, instruction, and tools — and Agent Engine packages your code into a container (built by Cloud Build), runs it on managed infrastructure with autoscaling, and exposes it as a regional resource with `:streamQuery` and `:query` methods plus built-in session management. You pay for runtime compute while queries execute plus the Gemini model tokens your agent consumes; there is no cluster, VM, or server to maintain.
Think of Agent Engine like a managed coffee-shop franchise for your barista. You write the recipe book (the ADK agent — instructions, the tools it may use, which coffee machine model it prefers). The franchise (Agent Engine) builds the shop, staffs it, opens the till, and handles queues of customers (sessions) — you never sign a lease or fix an espresso machine. Terraform and CI/CD are your operations manual: they rebuild an identical shop, on demand, in any city (region), every time the recipe changes.
| Concept | Explanation | When to use |
|---|---|---|
| ADK Agent (root_agent) | A Python `Agent` object (name, model, instruction, tools) exposed as `root_agent` at module top level of your agent directory. | Always — the deploy tooling discovers your agent by importing the module and reading `root_agent`. |
| Agent Engine / Reasoning Engine | The managed Vertex AI resource that hosts your agent code and serves streamQuery/query calls. | When a prototype agent needs to become a real, autoscaling, auditable production endpoint. |
| Tools | Plain Python functions the model can call; ADK generates the function schema from type hints and docstrings. | Whenever the agent must act on the world — query an API, read a ticket, compute a value. |
| Staging Bucket | A Cloud Storage bucket where your agent package and dependencies are staged before Cloud Build builds the runtime container. | Required at deploy time; keep it in the same region as the engine to avoid cross-region friction. |
| Sessions | Server-side conversation state keyed by user_id; Agent Engine persists session history across queries. | For any multi-turn chat — create a session per end user and let the engine track context. |
| google_vertex_ai_reasoning_engine | The Terraform resource (google provider v6.x) that declares an Agent Engine instance as infrastructure as code. | To make engine provisioning reproducible and reviewable; pin provider >= 6.0 and keep an `adk deploy` fallback. |
Why Deploy Agents on Agent Engine Instead of Rolling Your Own?
The default path for 'shipping' an agent is a long-running Cloud Run service or a GKE deployment wrapping LangChain. It works, but you inherit everything: container base-image patching, session state storage (Redis? Firestore?), autoscaling tuned for bursty LLM latency, IAM and audit logging, and a bespoke query protocol nobody else understands. Worse, every team invents a slightly different wrapper, so there is no consistent way to list, version, or revoke agents across an organization.
Agent Engine collapses that to one managed resource: `adk deploy` builds and ships your agent directory, the runtime scales and serves `:streamQuery`, and sessions are managed for you — while IAM, Cloud Logging, and regional placement behave like every other Vertex AI resource. Terraform's `google_vertex_ai_reasoning_engine` makes the engine itself declarative, and GitHub Actions with Workload Identity Federation turns redeploys into a reviewed, keyless pipeline. If your agent talks to Gemini directly, start with [Gemini API on Vertex AI: setup, IAM, and quotas](/tutorial/gemini-api-vertex-ai-setup-iam-quotas-python); if it needs retrieval over your own data, pair it with [RAG on Vertex AI with Vector Search](/tutorial/rag-vertex-ai-vector-search-embeddings-grounding-terraform). And watch token spend from day one with [Gemini API cost control on Vertex AI](/tutorial/gemini-api-cost-control-vertex-ai-quotas-budget-alerts).
| Feature | thisService | altA | altB |
|---|---|---|---|
| Runtime to operate | None — fully managed Reasoning Engine | Cloud Run service you containerize and patch | Self-managed LangServe/LlamaIndex on GKE |
| Session management | Built-in sessions via :query/session APIs | Hand-rolled (Memorystore, Firestore) | Framework-dependent, self-hosted |
| Deployment | adk deploy / agent_engines.create / Terraform | docker build + gcloud run deploy | Helm/manifests + CI you maintain |
| Framework support | ADK first-class; LangChain, LlamaIndex supported | Anything you can containerize | Anything you can containerize |
| IAM & audit | Native Vertex AI resource IAM + Cloud Logging | Cloud Run IAM + your own logging discipline | GKE RBAC + your own logging discipline |
| Terraform support | google_vertex_ai_reasoning_engine (provider v6.x) | google_cloud_run_v2_service (mature) | Full Kubernetes/GKE module stack |
Prerequisites
- GCP project with billing enabled
- gcloud CLI v450.0+, Python 3.10+, and Terraform >= 1.6 installed and authenticated
- roles/aiplatform.admin, roles/storage.admin, and roles/iam.serviceAccountAdmin on the project (or a CI service account with the equivalent)
- Python packages: google-adk and google-cloud-aiplatform[agent_engines]
- A Gemini API key is NOT required — Agent Engine runs on Vertex AI with application default credentials
Step-by-Step Guide
Step 1: Scaffold the ADK Agent and Test It Locally
Install the Agent Development Kit and create the agent directory layout ADK expects — a Python package with `agent.py` exposing `root_agent` at module top level. Then run the local dev UI to verify the agent answers before anything touches the cloud. The entire deploy toolchain discovers your agent by importing the module and reading a top-level `root_agent` variable. If that contract is wrong, every later step fails with a discovery error — so prove the agent works locally with `adk web` first.
pip install "google-adk" "google-cloud-aiplatform[agent_engines]"
mkdir -p support_agent
touch support_agent/__init__.py
# support_agent/agent.py
cat > support_agent/agent.py <<'EOF'
from google.adk.agents import Agent
root_agent = Agent(
name="support_triage_agent",
model="gemini-2.5-flash",
description="Triages incoming support tickets and drafts replies.",
instruction=(
"You are a support triage assistant. Classify each ticket by "
"severity (P1-P4), summarize the issue in one sentence, and "
"suggest the owning team. Be concise and factual."
),
tools=[],
)
EOF
# Launch the local test UI on http://localhost:8000
adk web
# Headless alternative: adk run support_agent
Step 2: Add a Custom Tool Function
Extend the agent with a real capability — a tool that looks up ticket priority from an internal system. ADK turns any typed Python function into a tool schema the Gemini model can call; the docstring becomes the tool description. An agent without tools is a chatbot. Tools are where agents earn their keep — but they are also where runtime import errors happen, because the tool's dependencies must be declared in the deploy requirements or the engine crashes on first invocation.
from google.adk.agents import Agent
def lookup_ticket_priority(ticket_id: str) -> dict:
"""Look up the current priority and status of a support ticket.
Args:
ticket_id: The ticket identifier, e.g. "TCK-1042".
Returns:
A dict with keys: ticket_id, priority (P1-P4), status.
"""
# Demo implementation — swap for your ticketing API call.
mock_db = {
"TCK-1042": {"priority": "P2", "status": "open"},
"TCK-2077": {"priority": "P1", "status": "escalated"},
}
row = mock_db.get(ticket_id)
if row is None:
return {"ticket_id": ticket_id, "priority": "unknown", "status": "not_found"}
return {"ticket_id": ticket_id, **row}
root_agent = Agent(
name="support_triage_agent",
model="gemini-2.5-flash",
description="Triages incoming support tickets and drafts replies.",
instruction=(
"You are a support triage assistant. Classify each ticket by "
"severity (P1-P4), summarize the issue in one sentence, and "
"suggest the owning team. Use lookup_ticket_priority when the "
"user mentions a ticket ID."
),
tools=[lookup_ticket_priority],
)
Step 3: Enable APIs and Create the Staging Bucket with Terraform
Enable the required Google APIs (Vertex AI, Cloud Build, Cloud Storage) and create the regional staging bucket the deploy flow uses to stage your agent package before Cloud Build builds the runtime container. Deploy is not a single-API operation — `adk deploy` uploads your package to the staging bucket and triggers a Cloud Build build of the agent container. Missing cloudbuild or storage access is the number-one PERMISSION_DENIED source, and Terraform makes the whole prerequisite set reproducible.
variable "project_id" { type = string }
variable "region" { type = string default = "us-central1" }
provider "google" {
project = var.project_id
region = var.region
}
resource "google_project_service" "apis" {
for_each = toset([
"aiplatform.googleapis.com",
"cloudbuild.googleapis.com",
"storage.googleapis.com",
])
service = each.key
disable_on_destroy = false
}
resource "google_storage_bucket" "agent_staging" {
name = "${var.project_id}-agent-engine-staging"
location = var.region
uniform_bucket_level_access = true
force_destroy = false
depends_on = [google_project_service.apis]
}
output "staging_bucket" { value = google_storage_bucket.agent_staging.name }
Step 4: Deploy the Agent to Agent Engine
Deploy the local agent to the managed runtime. Two equivalent paths: the `adk deploy agent_engine` CLI (simplest) or the Python SDK `agent_engines.create` (scriptable). Both package your directory, stage it in the bucket, build a container with Cloud Build, and create the Reasoning Engine resource. This is the moment a prototype becomes a managed resource with IAM, logging, and a stable resource name. Passing `requirements` explicitly is critical — anything your agent imports that is not listed will fail with ModuleNotFoundError inside the engine.
# Path A — ADK CLI
adk deploy agent_engine \
--project=$PROJECT_ID \
--region=us-central1 \
--staging_bucket=gs://$PROJECT_ID-agent-engine-staging \
./support_agent
# Path B — Python SDK (deploy_agent.py)
import vertexai
from vertexai import agent_engines
from support_agent.agent import root_agent
vertexai.init(
project="PROJECT_ID",
location="us-central1",
staging_bucket="gs://PROJECT_ID-agent-engine-staging",
)
remote_app = agent_engines.create(
agent_engine=root_agent,
requirements=["google-adk"],
extra_packages=["./support_agent"],
display_name="support-triage-agent",
)
print(remote_app.resource_name)
Step 5: Query the Deployed Agent
Call the live engine from Python with `agent_engines.get(...).stream_query(...)`, creating a session first for multi-turn state. The REST equivalent posts to the `:streamQuery` method on the reasoning engine resource — useful for smoke tests from CI. The deployed agent is a server-streaming API, not a chat completion endpoint. Understanding sessions (per-user conversation state) and streaming responses is required before you can put a UI or another service in front of it.
import vertexai
from vertexai import agent_engines
vertexai.init(project="PROJECT_ID", location="us-central1")
remote_agent = agent_engines.get(
"projects/PROJECT_ID/locations/us-central1/reasoningEngines/ENGINE_ID"
)
session = remote_agent.create_session(user_id="ci-smoke-user")
for event in remote_agent.stream_query(
user_id="ci-smoke-user",
session_id=session["id"],
message="Ticket TCK-1042: payment webhook retries failing since 02:00 UTC.",
):
# Events stream as the agent reasons and calls tools;
# the final text chunk is the answer.
print(event)
# REST equivalent (CI-friendly smoke test):
# curl -X POST \
# -H "Authorization: Bearer $(gcloud auth print-access-token)" \
# -H "Content-Type: application/json" \
# "https://us-central1-aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/us-central1/reasoningEngines/ENGINE_ID:streamQuery" \
# -d '{"class_method": "stream_query", "input": {"user_id": "ci-smoke-user", "message": "health check"}}'
Step 6: Codify the Engine in Terraform
Manage the Reasoning Engine itself as infrastructure with `google_vertex_ai_reasoning_engine`, available in the google provider v6.x. The provider attribute surface for this resource is newer than the API, so pin the provider and keep the `adk deploy` path as the code-upload mechanism. Declaring the engine in Terraform gives you reviewable, reproducible provisioning and a single place that records region, display name, and labels. But because the resource is young, over-relying on provider attributes that lag the API will block applies — pin >= 6.0 and treat the deploy CLI as the fallback.
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">= 6.0"
}
}
}
resource "google_vertex_ai_reasoning_engine" "support_agent" {
display_name = "support-triage-agent"
description = "ADK support triage agent (gemini-2.5-flash)"
region = var.region
# NOTE: the resource schema is newer than the API surface.
# If the provider version you pinned lacks an attribute the
# API supports (or vice versa), fall back to `adk deploy
# agent_engine` in CI and import the engine here:
# terraform import google_vertex_ai_reasoning_engine.support_agent \
# projects/PROJECT_ID/locations/us-central1/reasoningEngines/ENGINE_ID
}
Step 7: Build the GitHub Actions CI/CD Pipeline with Workload Identity Federation
Wire a keyless pipeline on every push: lint and unit-test the agent locally, then deploy to Agent Engine with `adk deploy`, then smoke-test the live `:streamQuery` endpoint. Auth is Workload Identity Federation — no service-account keys in repo secrets. Full WIF setup is covered in the linked guide. An agent that only exists on a laptop is a demo. Keyless OIDC auth plus a smoke test against the real engine means every merged change is proven against production infrastructure without long-lived credentials leaking into CI logs.
name: agent-cicd
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # required for Workload Identity Federation
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Lint and unit test
run: |
pip install "google-adk" pytest
python -m py_compile support_agent/agent.py
pytest tests/ -q
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
service_account: agent-deployer@PROJECT_ID.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- name: Deploy to Agent Engine
run: |
adk deploy agent_engine \
--project=$PROJECT_ID \
--region=us-central1 \
--staging_bucket=gs://$PROJECT_ID-agent-engine-staging \
./support_agent
- name: Smoke test streamQuery
run: python ci/smoke_test.py
env:
ENGINE_RESOURCE: projects/PROJECT_ID/locations/us-central1/reasoningEngines/ENGINE_ID
Verification & Health Check
Best Practices
- Expose root_agent at Module Top Level
- Declare Deploy Requirements Explicitly
- Keyless CI with Workload Identity Federation
- Assert Stream Shape, Not Exact Text, in Smoke Tests
Common Mistakes
- {"errorCode":"AGENT_NOT_FOUND (discovery failure)","symptoms":"`adk deploy` or adk web reports the agent cannot be found; deploy exits before any upload happens.","rootCause":"`root_agent` is not exposed at module top level of agent.py — it is nested in a function/class, named differently, or the directory lacks `__init__.py`, so the import-based discovery finds nothing.","fixCommand":"python -c \"from support_agent.agent import root_agent; print(root_agent.name)\"\n","code":"# support_agent/agent.py — correct discovery contract\nfrom google.adk.agents import Agent\n\nroot_agent = Agent(\n name=\"support_triage_agent\",\n model=\"gemini-2.5-flash\",\n instruction=\"...\",\n tools=[lookup_ticket_priority],\n)\n","language":"python","filename":"fix-root-agent.py","prevention":"Add the one-line import check above as a CI step before deploy — it fails in seconds instead of after a Cloud Build run."}
- {"errorCode":"MODULE_NOT_FOUND in the deployed engine","symptoms":"Deploy succeeds, but stream_query returns an error event with ModuleNotFoundError for a package your agent imports.","rootCause":"The requirements list passed to the deploy did not include every dependency of the agent code and its tools — the engine container is built from exactly that list, not from your local environment.","fixCommand":"adk deploy agent_engine --project=$PROJECT_ID --region=us-central1 \\\n --staging_bucket=gs://$PROJECT_ID-agent-engine-staging ./support_agent\n","code":"remote_app = agent_engines.create(\n agent_engine=root_agent,\n requirements=[\"google-adk\", \"requests==2.32.3\"],\n extra_packages=[\"./support_agent\"],\n)\n","language":"python","filename":"fix-requirements.py","prevention":"Keep a requirements.txt in the agent directory and reference it in CI; never let the deploy requirements drift from what tests import."}
- {"errorCode":"PERMISSION_DENIED (cloudbuild)","symptoms":"Deploy fails during the build phase with 403 on cloudbuild.googleapis.com or on writing to the staging bucket.","rootCause":"The deploying identity (user or CI service account) lacks `roles/cloudbuild.builds.editor` for the container build, or object admin on the staging bucket for the package upload.","fixCommand":"gcloud projects add-iam-policy-binding $PROJECT_ID \\\n --member=\"serviceAccount:agent-deployer@$PROJECT_ID.iam.gserviceaccount.com\" \\\n --role=\"roles/cloudbuild.builds.editor\"\n","code":"resource \"google_project_iam_member\" \"deployer_cloudbuild\" {\n project = var.project_id\n role = \"roles/cloudbuild.builds.editor\"\n member = \"serviceAccount:agent-deployer@${var.project_id}.iam.gserviceaccount.com\"\n}\n\nresource \"google_storage_bucket_iam_member\" \"deployer_staging\" {\n bucket = google_storage_bucket.agent_staging.name\n role = \"roles/storage.objectAdmin\"\n member = \"serviceAccount:agent-deployer@${var.project_id}.iam.gserviceaccount.com\"\n}\n","language":"hcl","filename":"fix-deployer-iam.tf","prevention":"Codify the deployer service account's full role set (aiplatform.user, cloudbuild.builds.editor, storage.objectAdmin on the bucket) in Terraform from day one."}
- {"errorCode":"CROSS_REGION_STAGING errors / slow builds","symptoms":"Deploy works but Cloud Build staging is slow, or fails with location-mismatch errors on the bucket.","rootCause":"The staging bucket was created in a different region (or multi-region) than the Agent Engine location — package upload and build pull across regions.","fixCommand":"gcloud storage buckets describe gs://$PROJECT_ID-agent-engine-staging \\\n --format=\"value(location)\"\n","code":"resource \"google_storage_bucket\" \"agent_staging\" {\n name = \"${var.project_id}-agent-engine-staging\"\n location = var.region # same region as the reasoning engine\n uniform_bucket_level_access = true\n}\n","language":"hcl","filename":"fix-bucket-region.tf","prevention":"Parameterize region once in Terraform and use it for both the bucket and the engine; never hand-type locations in two places."}
- {"errorCode":"TERRAFORM_UNSUPPORTED_ARGUMENT (reasoning_engine)","symptoms":"terraform plan fails with unsupported argument, or apply succeeds but subsequent plans show perpetual diffs on google_vertex_ai_reasoning_engine.","rootCause":"The Terraform resource is newer than the API it wraps — a pinned provider older than v6.0 lacks the resource, and some v6.x releases lag behind newly added API attributes.","fixCommand":"terraform init -upgrade\nterraform providers lock -platform=linux_amd64\n","code":"terraform {\n required_providers {\n google = {\n source = \"hashicorp/google\"\n version = \">= 6.0\"\n }\n }\n}\n# If the attribute surface still lags: deploy agent code with\n# `adk deploy agent_engine` in CI and import the engine:\n# terraform import google_vertex_ai_reasoning_engine.support_agent \\\n# projects/PROJECT_ID/locations/us-central1/reasoningEngines/ENGINE_ID\n","language":"hcl","filename":"fix-provider-pin.tf","prevention":"Pin provider >= 6.0, run terraform plan in CI on provider upgrades, and keep the `adk deploy` fallback documented in the pipeline README."}
Cost Analysis
| Feature | metric | cost1k | cost10k | cost100k | cost1m |
|---|---|---|---|---|---|
| Agent Engine runtime (vCPU + memory while querying, approximate) | $0.50 | $5.00 | $50.00 | $500.00 | |
| Gemini 2.5 Flash input tokens (~2K per conversation, ~$0.30/1M, approximate) | $0.60 | $6.00 | $60.00 | $600.00 | |
| Gemini 2.5 Flash output tokens (~1K per conversation, ~$2.50/1M, approximate) | $2.50 | $25.00 | $250.00 | $2500.00 | |
| Staging bucket storage + Cloud Build deploys (amortized, approximate) | $0.50 | $1.00 | $2.00 | $5.00 | |
| Total (approximate) | $4.10 | $37.00 | $362.00 | $3605.00 |