Skip to content

Commit a858d79

Browse files
vicentefbcopybara-github
authored andcommitted
feat: cli funcionality to deploy an Agent to a running GKE cluster
Merge #1607 - Added CLI functionality so that we can deploy and Agent onto a GKE cluster - Related documentation google/adk-docs#445 COPYBARA_INTEGRATE_REVIEW=#1607 from vicentefb:GkeDeployAgent 42f35d9 PiperOrigin-RevId: 786857789
1 parent c8f8b4a commit a858d79

File tree

4 files changed

+1299
-355
lines changed

4 files changed

+1299
-355
lines changed

src/google/adk/cli/cli_deploy.py

Lines changed: 231 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,11 @@ def to_cloud_run(
153153
app_name: The name of the app, by default, it's basename of `agent_folder`.
154154
temp_folder: The temp folder for the generated Cloud Run source files.
155155
port: The port of the ADK api server.
156-
allow_origins: The list of allowed origins for the ADK api server.
157156
trace_to_cloud: Whether to enable Cloud Trace.
158157
with_ui: Whether to deploy with UI.
159158
verbosity: The verbosity level of the CLI.
160159
adk_version: The ADK version to use in Cloud Run.
160+
allow_origins: The list of allowed origins for the ADK api server.
161161
session_service_uri: The URI of the session service.
162162
artifact_service_uri: The URI of the artifact service.
163163
memory_service_uri: The URI of the memory service.
@@ -182,7 +182,7 @@ def to_cloud_run(
182182
if os.path.exists(requirements_txt_path)
183183
else ''
184184
)
185-
click.echo('Copying agent source code complete.')
185+
click.echo('Copying agent source code completed.')
186186

187187
# create Dockerfile
188188
click.echo('Creating Dockerfile...')
@@ -425,7 +425,7 @@ def to_agent_engine(
425425
'async_stream': ['async_stream_query'],
426426
'stream': ['stream_query', 'streaming_agent_run_with_events'],
427427
},
428-
sys_paths=[temp_folder[1:]],
428+
sys_paths=[temp_folder],
429429
)
430430
agent_config = dict(
431431
agent_engine=agent_engine,
@@ -443,3 +443,231 @@ def to_agent_engine(
443443
finally:
444444
click.echo(f'Cleaning up the temp folder: {temp_folder}')
445445
shutil.rmtree(temp_folder)
446+
447+
448+
def to_gke(
449+
*,
450+
agent_folder: str,
451+
project: Optional[str],
452+
region: Optional[str],
453+
cluster_name: str,
454+
service_name: str,
455+
app_name: str,
456+
temp_folder: str,
457+
port: int,
458+
trace_to_cloud: bool,
459+
with_ui: bool,
460+
log_level: str,
461+
verbosity: str,
462+
adk_version: str,
463+
allow_origins: Optional[list[str]] = None,
464+
session_service_uri: Optional[str] = None,
465+
artifact_service_uri: Optional[str] = None,
466+
memory_service_uri: Optional[str] = None,
467+
a2a: bool = False,
468+
):
469+
"""Deploys an agent to Google Kubernetes Engine(GKE).
470+
471+
Args:
472+
agent_folder: The folder (absolute path) containing the agent source code.
473+
project: Google Cloud project id.
474+
region: Google Cloud region.
475+
cluster_name: The name of the GKE cluster.
476+
service_name: The service name in GKE.
477+
app_name: The name of the app, by default, it's basename of `agent_folder`.
478+
temp_folder: The local directory to use as a temporary workspace for preparing deployment artifacts. The tool populates this folder with a copy of the agent's source code and auto-generates necessary files like a Dockerfile and deployment.yaml.
479+
port: The port of the ADK api server.
480+
trace_to_cloud: Whether to enable Cloud Trace.
481+
with_ui: Whether to deploy with UI.
482+
verbosity: The verbosity level of the CLI.
483+
adk_version: The ADK version to use in GKE.
484+
allow_origins: The list of allowed origins for the ADK api server.
485+
session_service_uri: The URI of the session service.
486+
artifact_service_uri: The URI of the artifact service.
487+
memory_service_uri: The URI of the memory service.
488+
"""
489+
click.secho(
490+
'\n🚀 Starting ADK Agent Deployment to GKE...', fg='cyan', bold=True
491+
)
492+
click.echo('--------------------------------------------------')
493+
# Resolve project early to show the user which one is being used
494+
project = _resolve_project(project)
495+
click.echo(f' Project: {project}')
496+
click.echo(f' Region: {region}')
497+
click.echo(f' Cluster: {cluster_name}')
498+
click.echo('--------------------------------------------------\n')
499+
500+
app_name = app_name or os.path.basename(agent_folder)
501+
502+
click.secho('STEP 1: Preparing build environment...', bold=True)
503+
click.echo(f' - Using temporary directory: {temp_folder}')
504+
505+
# remove temp_folder if exists
506+
if os.path.exists(temp_folder):
507+
click.echo(' - Removing existing temporary directory...')
508+
shutil.rmtree(temp_folder)
509+
510+
try:
511+
# copy agent source code
512+
click.echo(' - Copying agent source code...')
513+
agent_src_path = os.path.join(temp_folder, 'agents', app_name)
514+
shutil.copytree(agent_folder, agent_src_path)
515+
requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt')
516+
install_agent_deps = (
517+
f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"'
518+
if os.path.exists(requirements_txt_path)
519+
else ''
520+
)
521+
click.secho('✅ Environment prepared.', fg='green')
522+
523+
allow_origins_option = (
524+
f'--allow_origins={",".join(allow_origins)}' if allow_origins else ''
525+
)
526+
527+
# create Dockerfile
528+
click.secho('\nSTEP 2: Generating deployment files...', bold=True)
529+
click.echo(' - Creating Dockerfile...')
530+
host_option = '--host=0.0.0.0' if adk_version > '0.5.0' else ''
531+
dockerfile_content = _DOCKERFILE_TEMPLATE.format(
532+
gcp_project_id=project,
533+
gcp_region=region,
534+
app_name=app_name,
535+
port=port,
536+
command='web' if with_ui else 'api_server',
537+
install_agent_deps=install_agent_deps,
538+
service_option=_get_service_option_by_adk_version(
539+
adk_version,
540+
session_service_uri,
541+
artifact_service_uri,
542+
memory_service_uri,
543+
),
544+
trace_to_cloud_option='--trace_to_cloud' if trace_to_cloud else '',
545+
allow_origins_option=allow_origins_option,
546+
adk_version=adk_version,
547+
host_option=host_option,
548+
a2a_option='--a2a' if a2a else '',
549+
)
550+
dockerfile_path = os.path.join(temp_folder, 'Dockerfile')
551+
os.makedirs(temp_folder, exist_ok=True)
552+
with open(dockerfile_path, 'w', encoding='utf-8') as f:
553+
f.write(
554+
dockerfile_content,
555+
)
556+
click.secho(f'✅ Dockerfile generated: {dockerfile_path}', fg='green')
557+
558+
# Build and push the Docker image
559+
click.secho(
560+
'\nSTEP 3: Building container image with Cloud Build...', bold=True
561+
)
562+
click.echo(
563+
' (This may take a few minutes. Raw logs from gcloud will be shown'
564+
' below.)'
565+
)
566+
project = _resolve_project(project)
567+
image_name = f'gcr.io/{project}/{service_name}'
568+
subprocess.run(
569+
[
570+
'gcloud',
571+
'builds',
572+
'submit',
573+
'--tag',
574+
image_name,
575+
'--verbosity',
576+
log_level.lower() if log_level else verbosity,
577+
temp_folder,
578+
],
579+
check=True,
580+
)
581+
click.secho('✅ Container image built and pushed successfully.', fg='green')
582+
583+
# Create a Kubernetes deployment
584+
click.echo(' - Creating Kubernetes deployment.yaml...')
585+
deployment_yaml = f"""
586+
apiVersion: apps/v1
587+
kind: Deployment
588+
metadata:
589+
name: {service_name}
590+
labels:
591+
app.kubernetes.io/name: adk-agent
592+
app.kubernetes.io/version: {adk_version}
593+
app.kubernetes.io/instance: {service_name}
594+
app.kubernetes.io/managed-by: adk-cli
595+
spec:
596+
replicas: 1
597+
selector:
598+
matchLabels:
599+
app: {service_name}
600+
template:
601+
metadata:
602+
labels:
603+
app: {service_name}
604+
app.kubernetes.io/name: adk-agent
605+
app.kubernetes.io/version: {adk_version}
606+
app.kubernetes.io/instance: {service_name}
607+
app.kubernetes.io/managed-by: adk-cli
608+
spec:
609+
containers:
610+
- name: {service_name}
611+
image: {image_name}
612+
ports:
613+
- containerPort: {port}
614+
---
615+
apiVersion: v1
616+
kind: Service
617+
metadata:
618+
name: {service_name}
619+
spec:
620+
type: LoadBalancer
621+
selector:
622+
app: {service_name}
623+
ports:
624+
- port: 80
625+
targetPort: {port}
626+
"""
627+
deployment_yaml_path = os.path.join(temp_folder, 'deployment.yaml')
628+
with open(deployment_yaml_path, 'w', encoding='utf-8') as f:
629+
f.write(deployment_yaml)
630+
click.secho(
631+
f'✅ Kubernetes deployment manifest generated: {deployment_yaml_path}',
632+
fg='green',
633+
)
634+
635+
# Apply the deployment
636+
click.secho('\nSTEP 4: Applying deployment to GKE cluster...', bold=True)
637+
click.echo(' - Getting cluster credentials...')
638+
subprocess.run(
639+
[
640+
'gcloud',
641+
'container',
642+
'clusters',
643+
'get-credentials',
644+
cluster_name,
645+
'--region',
646+
region,
647+
'--project',
648+
project,
649+
],
650+
check=True,
651+
)
652+
click.echo(' - Applying Kubernetes manifest...')
653+
result = subprocess.run(
654+
['kubectl', 'apply', '-f', temp_folder],
655+
check=True,
656+
capture_output=True, # <-- Add this
657+
text=True, # <-- Add this
658+
)
659+
660+
# 2. Print the captured output line by line
661+
click.secho(
662+
' - The following resources were applied to the cluster:', fg='green'
663+
)
664+
for line in result.stdout.strip().split('\n'):
665+
click.echo(f' - {line}')
666+
667+
finally:
668+
click.secho('\nSTEP 5: Cleaning up...', bold=True)
669+
click.echo(f' - Removing temporary directory: {temp_folder}')
670+
shutil.rmtree(temp_folder)
671+
click.secho(
672+
'\n🎉 Deployment to GKE finished successfully!', fg='cyan', bold=True
673+
)

0 commit comments

Comments
 (0)