Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

752 lines
25 KiB
Python

"""EKS cluster lifecycle management using boto3."""
from __future__ import annotations
import contextlib
import json
import subprocess
import time
from pathlib import Path
from typing import Any
from botocore.exceptions import ClientError
from tests.shared.infrastructure_sdk.config import delete_outputs, save_outputs
from tests.shared.infrastructure_sdk.deployer import (
DEFAULT_REGION,
get_boto3_client,
get_standard_tags,
get_standard_tags_dict,
)
from tests.shared.infrastructure_sdk.resources import api_gateway, ecr, iam, lambda_, s3, vpc
STACK_NAME = "tracer-eks-k8s-test"
CLUSTER_NAME = "tracer-eks-test"
NODE_GROUP_NAME = "tracer-eks-test-nodes"
ECR_REPO_NAME = "tracer-eks/etl-job"
REGION = DEFAULT_REGION
K8S_VERSION = "1.35"
EKS_ADDONS = ["kube-proxy", "vpc-cni", "coredns"]
CLUSTER_ROLE_NAME = "tracer-eks-cluster-role"
NODE_ROLE_NAME = "tracer-eks-node-role"
EKS_CLUSTER_TRUST_POLICY = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "eks.amazonaws.com"},
"Action": "sts:AssumeRole",
}
],
}
EC2_TRUST_POLICY = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole",
}
],
}
EKS_CLUSTER_POLICIES = [
"arn:aws:iam::aws:policy/AmazonEKSClusterPolicy",
]
EKS_NODE_POLICIES = [
"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
"arn:aws:iam::aws:policy/AmazonS3FullAccess",
]
EKS_UNSUPPORTED_AZS = {"us-east-1e"}
PIPELINE_DIR = Path(__file__).parent.parent / "pipeline_code"
def _run(
cmd: list[str], *, check: bool = True, capture: bool = True
) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, check=check, capture_output=capture, text=True)
# ---------------------------------------------------------------------------
# IAM
# ---------------------------------------------------------------------------
def _create_role_with_trust(
name: str,
trust_policy: dict,
managed_policies: list[str],
) -> dict[str, Any]:
"""Create an IAM role with a trust policy and attach managed policies."""
iam_client = get_boto3_client("iam", REGION)
tags = get_standard_tags(STACK_NAME)
try:
resp = iam_client.create_role(
RoleName=name,
AssumeRolePolicyDocument=json.dumps(trust_policy),
Description=f"Role for {STACK_NAME}",
Tags=tags,
)
role_arn = resp["Role"]["Arn"]
print(f"Created IAM role {name}")
except ClientError as e:
if e.response["Error"]["Code"] == "EntityAlreadyExists":
resp = iam_client.get_role(RoleName=name)
role_arn = resp["Role"]["Arn"]
print(f"IAM role {name} already exists, reusing")
else:
raise
for policy_arn in managed_policies:
iam.attach_policy(name, policy_arn, REGION)
time.sleep(5)
return {"arn": role_arn, "name": name}
def _create_cluster_role() -> dict[str, Any]:
return _create_role_with_trust(
CLUSTER_ROLE_NAME, EKS_CLUSTER_TRUST_POLICY, EKS_CLUSTER_POLICIES
)
def _create_node_role() -> dict[str, Any]:
return _create_role_with_trust(NODE_ROLE_NAME, EC2_TRUST_POLICY, EKS_NODE_POLICIES)
# ---------------------------------------------------------------------------
# EKS Cluster
# ---------------------------------------------------------------------------
def _cluster_exists() -> str | None:
"""Return cluster status if it exists, None otherwise."""
eks = get_boto3_client("eks", REGION)
try:
resp = eks.describe_cluster(name=CLUSTER_NAME)
return resp["cluster"]["status"]
except ClientError as e:
if e.response["Error"]["Code"] == "ResourceNotFoundException":
return None
raise
def _create_cluster(cluster_role_arn: str, subnet_ids: list[str]) -> None:
status = _cluster_exists()
if status:
print(f"EKS cluster {CLUSTER_NAME} already exists (status={status})")
if status == "ACTIVE":
return
else:
eks = get_boto3_client("eks", REGION)
print(f"Creating EKS cluster {CLUSTER_NAME}...")
eks.create_cluster(
name=CLUSTER_NAME,
version=K8S_VERSION,
roleArn=cluster_role_arn,
resourcesVpcConfig={"subnetIds": subnet_ids, "endpointPublicAccess": True},
tags=get_standard_tags_dict(STACK_NAME),
)
_wait_for_cluster("ACTIVE", timeout=900)
print(f"EKS cluster {CLUSTER_NAME} is ACTIVE")
def _wait_for_cluster(target_status: str, timeout: int = 900) -> None:
eks = get_boto3_client("eks", REGION)
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
resp = eks.describe_cluster(name=CLUSTER_NAME)
status = resp["cluster"]["status"]
if status == target_status:
return
if status == "FAILED":
raise RuntimeError("EKS cluster entered FAILED state")
elapsed = int(time.monotonic() - (deadline - timeout))
print(f" Cluster status: {status} ({elapsed}s elapsed)")
except ClientError as e:
if (
target_status == "DELETED"
and e.response["Error"]["Code"] == "ResourceNotFoundException"
):
return
raise
time.sleep(15)
raise TimeoutError(f"EKS cluster did not reach {target_status} within {timeout}s")
# ---------------------------------------------------------------------------
# Node Group
# ---------------------------------------------------------------------------
def _node_group_exists() -> str | None:
eks = get_boto3_client("eks", REGION)
try:
resp = eks.describe_nodegroup(clusterName=CLUSTER_NAME, nodegroupName=NODE_GROUP_NAME)
return resp["nodegroup"]["status"]
except ClientError as e:
if e.response["Error"]["Code"] == "ResourceNotFoundException":
return None
raise
def _create_node_group(node_role_arn: str, subnet_ids: list[str]) -> None:
status = _node_group_exists()
if status:
print(f"Node group {NODE_GROUP_NAME} already exists (status={status})")
if status == "ACTIVE":
return
else:
eks = get_boto3_client("eks", REGION)
print(f"Creating managed node group {NODE_GROUP_NAME}...")
eks.create_nodegroup(
clusterName=CLUSTER_NAME,
nodegroupName=NODE_GROUP_NAME,
nodeRole=node_role_arn,
subnets=subnet_ids,
instanceTypes=["t3.medium"],
scalingConfig={"minSize": 1, "maxSize": 2, "desiredSize": 1},
amiType="AL2023_x86_64_STANDARD",
tags=get_standard_tags_dict(STACK_NAME),
)
_wait_for_node_group("ACTIVE", timeout=600)
print(f"Node group {NODE_GROUP_NAME} is ACTIVE")
def _wait_for_node_group(target_status: str, timeout: int = 600) -> None:
eks = get_boto3_client("eks", REGION)
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
resp = eks.describe_nodegroup(clusterName=CLUSTER_NAME, nodegroupName=NODE_GROUP_NAME)
status = resp["nodegroup"]["status"]
if status == target_status:
return
if status in ("CREATE_FAILED", "DELETE_FAILED"):
raise RuntimeError(f"Node group entered {status}")
elapsed = int(time.monotonic() - (deadline - timeout))
print(f" Node group status: {status} ({elapsed}s elapsed)")
except ClientError as e:
if (
target_status == "DELETED"
and e.response["Error"]["Code"] == "ResourceNotFoundException"
):
return
raise
time.sleep(15)
raise TimeoutError(f"Node group did not reach {target_status} within {timeout}s")
# ---------------------------------------------------------------------------
# EKS Add-ons
# ---------------------------------------------------------------------------
def _get_latest_addon_version(addon_name: str) -> str:
"""Look up the latest compatible version for an EKS add-on."""
eks = get_boto3_client("eks", REGION)
resp = eks.describe_addon_versions(
kubernetesVersion=K8S_VERSION,
addonName=addon_name,
)
return resp["addons"][0]["addonVersions"][0]["addonVersion"]
def _install_addon(addon_name: str) -> None:
"""Install or update a single EKS managed add-on."""
eks = get_boto3_client("eks", REGION)
try:
resp = eks.describe_addon(clusterName=CLUSTER_NAME, addonName=addon_name)
status = resp["addon"]["status"]
print(f"Add-on {addon_name} already exists (status={status})")
return
except ClientError as e:
if e.response["Error"]["Code"] != "ResourceNotFoundException":
raise
version = _get_latest_addon_version(addon_name)
print(f"Installing add-on {addon_name} ({version})...")
eks.create_addon(
clusterName=CLUSTER_NAME,
addonName=addon_name,
addonVersion=version,
resolveConflicts="OVERWRITE",
tags=get_standard_tags_dict(STACK_NAME),
)
def _wait_for_addon(addon_name: str, timeout: int = 300) -> None:
eks = get_boto3_client("eks", REGION)
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
resp = eks.describe_addon(clusterName=CLUSTER_NAME, addonName=addon_name)
status = resp["addon"]["status"]
if status == "ACTIVE":
return
if status in ("CREATE_FAILED", "DEGRADED"):
raise RuntimeError(f"Add-on {addon_name} entered {status}")
time.sleep(10)
raise TimeoutError(f"Add-on {addon_name} did not become ACTIVE within {timeout}s")
def _install_addons() -> None:
"""Install all EKS managed add-ons and wait for them to become active."""
for addon in EKS_ADDONS:
_install_addon(addon)
for addon in EKS_ADDONS:
_wait_for_addon(addon)
print(f"Add-on {addon} is ACTIVE")
def _delete_addons() -> None:
"""Delete all EKS managed add-ons (best effort)."""
eks = get_boto3_client("eks", REGION)
for addon in EKS_ADDONS:
try:
eks.delete_addon(clusterName=CLUSTER_NAME, addonName=addon)
print(f"Deleted add-on {addon}")
except ClientError as e:
if e.response["Error"]["Code"] != "ResourceNotFoundException":
print(f"Warning deleting add-on {addon}: {e}")
# ---------------------------------------------------------------------------
# ECR + Image
# ---------------------------------------------------------------------------
def _setup_ecr_and_push_image() -> str:
"""Create ECR repo, build and push the ETL job image. Returns full image URI."""
repo = ecr.create_repository(ECR_REPO_NAME, STACK_NAME, REGION)
image_uri = ecr.build_and_push(
dockerfile_path=PIPELINE_DIR,
repository_uri=repo["uri"],
tag="latest",
platform="linux/amd64",
region=REGION,
)
print(f"Pushed image: {image_uri}")
return image_uri
# ---------------------------------------------------------------------------
# kubeconfig
# ---------------------------------------------------------------------------
def update_kubeconfig() -> None:
"""Configure kubectl to talk to the EKS cluster."""
print(f"Updating kubeconfig for {CLUSTER_NAME}...")
_run(
["aws", "eks", "update-kubeconfig", "--name", CLUSTER_NAME, "--region", REGION],
capture=False,
)
def cluster_exists() -> bool:
"""Return True if the EKS cluster exists."""
eks_client = get_boto3_client("eks", REGION)
try:
eks_client.describe_cluster(name=CLUSTER_NAME)
return True
except ClientError as exc:
if exc.response["Error"]["Code"] == "ResourceNotFoundException":
return False
raise
def ensure_nodegroup_capacity() -> None:
"""Ensure the EKS cluster has an ACTIVE managed node group without redeploying cluster."""
status = _cluster_exists()
if status != "ACTIVE":
raise RuntimeError(
f"EKS cluster {CLUSTER_NAME} is not ACTIVE (status={status}). "
"Create or recover the cluster before running tests."
)
node_status = _node_group_exists()
if node_status == "ACTIVE":
return
node_role = _create_node_role()
vpc_info = vpc.get_default_vpc(REGION)
subnet_ids = vpc.get_public_subnets(vpc_info["vpc_id"], REGION)
subnet_ids = _filter_eks_subnets(subnet_ids)
_create_node_group(node_role["arn"], subnet_ids)
# ---------------------------------------------------------------------------
# Subnet filtering
# ---------------------------------------------------------------------------
def _filter_eks_subnets(subnet_ids: list[str]) -> list[str]:
"""Remove subnets in AZs that EKS doesn't support."""
ec2 = get_boto3_client("ec2", REGION)
resp = ec2.describe_subnets(SubnetIds=subnet_ids)
filtered = [
s["SubnetId"] for s in resp["Subnets"] if s["AvailabilityZone"] not in EKS_UNSUPPORTED_AZS
]
excluded = len(subnet_ids) - len(filtered)
if excluded:
print(f"Excluded {excluded} subnet(s) in unsupported AZs: {EKS_UNSUPPORTED_AZS}")
return filtered
# ---------------------------------------------------------------------------
# EKS access management
# ---------------------------------------------------------------------------
CI_IAM_PRINCIPAL = "arn:aws:iam::395261708130:user/github-actions-ci-readonly"
def _enable_api_auth_mode() -> None:
"""Switch cluster to API_AND_CONFIG_MAP auth so access entries work."""
eks_client = get_boto3_client("eks", REGION)
resp = eks_client.describe_cluster(name=CLUSTER_NAME)
mode = resp["cluster"]["accessConfig"]["authenticationMode"]
if mode == "API_AND_CONFIG_MAP":
return
print("Enabling API_AND_CONFIG_MAP authentication mode...")
try:
eks_client.update_cluster_config(
name=CLUSTER_NAME,
accessConfig={"authenticationMode": "API_AND_CONFIG_MAP"},
)
except ClientError as exc:
if exc.response["Error"]["Code"] in (
"InvalidRequestException",
"ResourceInUseException",
):
# Already in the desired mode; update is a no-op
return
raise
_wait_for_cluster("ACTIVE", timeout=120)
def _wait_for_auth_mode(expected: str, timeout: int = 180) -> bool:
"""Wait until cluster accessConfig.authenticationMode reaches expected value."""
eks_client = get_boto3_client("eks", REGION)
start = time.monotonic()
while time.monotonic() - start < timeout:
try:
resp = eks_client.describe_cluster(name=CLUSTER_NAME)
mode = resp["cluster"]["accessConfig"]["authenticationMode"]
if mode == expected:
return True
except ClientError:
# Cluster may not be reachable yet during creation; retry
time.sleep(5)
continue
time.sleep(5)
return False
def _grant_ci_access() -> None:
"""Grant the CI IAM principal cluster admin access."""
eks_client = get_boto3_client("eks", REGION)
if not _wait_for_auth_mode("API_AND_CONFIG_MAP", timeout=240):
raise RuntimeError(
"EKS auth mode did not become API_AND_CONFIG_MAP in time; cannot create access entry yet."
)
try:
eks_client.create_access_entry(
clusterName=CLUSTER_NAME,
principalArn=CI_IAM_PRINCIPAL,
type="STANDARD",
)
print(f"Created access entry for {CI_IAM_PRINCIPAL}")
except ClientError as e:
if e.response["Error"]["Code"] != "ResourceInUseException":
raise
with contextlib.suppress(ClientError):
eks_client.associate_access_policy(
clusterName=CLUSTER_NAME,
principalArn=CI_IAM_PRINCIPAL,
policyArn="arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy",
accessScope={"type": "cluster"},
)
# ---------------------------------------------------------------------------
# Deploy / Destroy orchestration
# ---------------------------------------------------------------------------
def deploy_eks_stack() -> dict[str, Any]:
"""Deploy the full EKS stack: IAM, cluster, nodes, ECR image."""
print(f"\n{'=' * 60}")
print(f"Deploying EKS stack: {STACK_NAME}")
print(f"{'=' * 60}\n")
cluster_role = _create_cluster_role()
node_role = _create_node_role()
vpc_info = vpc.get_default_vpc(REGION)
subnet_ids = vpc.get_public_subnets(vpc_info["vpc_id"], REGION)
subnet_ids = _filter_eks_subnets(subnet_ids)
print(f"Using VPC {vpc_info['vpc_id']} with {len(subnet_ids)} subnets")
_create_cluster(cluster_role["arn"], subnet_ids)
_enable_api_auth_mode()
_grant_ci_access()
_install_addons()
_create_node_group(node_role["arn"], subnet_ids)
image_uri = _setup_ecr_and_push_image()
import uuid
suffix = uuid.uuid4().hex[:8]
landing_bucket = s3.create_bucket(f"tracer-k8s-landing-{suffix}", STACK_NAME, REGION)
processed_bucket = s3.create_bucket(f"tracer-k8s-processed-{suffix}", STACK_NAME, REGION)
print(f"S3 buckets: {landing_bucket['name']}, {processed_bucket['name']}")
update_kubeconfig()
outputs = {
"stack_name": STACK_NAME,
"cluster_name": CLUSTER_NAME,
"node_group_name": NODE_GROUP_NAME,
"k8s_version": K8S_VERSION,
"cluster_role_arn": cluster_role["arn"],
"node_role_arn": node_role["arn"],
"ecr_repo_name": ECR_REPO_NAME,
"ecr_image_uri": image_uri,
"vpc_id": vpc_info["vpc_id"],
"subnet_ids": subnet_ids,
"region": REGION,
"landing_bucket": landing_bucket["name"],
"processed_bucket": processed_bucket["name"],
}
save_outputs(STACK_NAME, outputs)
print("\nEKS stack deployed. Outputs saved.")
return outputs
def destroy_eks_stack() -> None:
"""Tear down the EKS stack in reverse order."""
print(f"\n{'=' * 60}")
print(f"Destroying EKS stack: {STACK_NAME}")
print(f"{'=' * 60}\n")
eks = get_boto3_client("eks", REGION)
if _node_group_exists():
print(f"Deleting node group {NODE_GROUP_NAME}...")
try:
eks.delete_nodegroup(clusterName=CLUSTER_NAME, nodegroupName=NODE_GROUP_NAME)
_wait_for_node_group("DELETED", timeout=600)
print("Node group deleted")
except ClientError as e:
if e.response["Error"]["Code"] != "ResourceNotFoundException":
print(f"Warning: {e}")
_delete_addons()
if _cluster_exists():
print(f"Deleting EKS cluster {CLUSTER_NAME}...")
try:
eks.delete_cluster(name=CLUSTER_NAME)
_wait_for_cluster("DELETED", timeout=600)
print("EKS cluster deleted")
except ClientError as e:
if e.response["Error"]["Code"] != "ResourceNotFoundException":
print(f"Warning: {e}")
for role_name, policies in [
(CLUSTER_ROLE_NAME, EKS_CLUSTER_POLICIES),
(NODE_ROLE_NAME, EKS_NODE_POLICIES),
]:
for policy_arn in policies:
iam.detach_policy(role_name, policy_arn, REGION)
iam.delete_role(role_name, REGION)
print(f"Deleted IAM role {role_name}")
ecr.delete_repository(ECR_REPO_NAME, REGION)
print(f"Deleted ECR repository {ECR_REPO_NAME}")
delete_outputs(STACK_NAME)
print("\nEKS stack destroyed.")
def get_ecr_image_uri() -> str:
"""Load saved outputs and return the ECR image URI."""
from tests.shared.infrastructure_sdk.config import load_outputs
outputs = load_outputs(STACK_NAME)
return outputs["ecr_image_uri"]
# ---------------------------------------------------------------------------
# Trigger Lambda constants
# ---------------------------------------------------------------------------
TRIGGER_LAMBDA_NAME = "tracer-eks-etl-trigger"
TRIGGER_LAMBDA_ROLE_NAME = "tracer-eks-trigger-lambda-role"
TRIGGER_API_NAME = "tracer-eks-trigger-api"
TRIGGER_LAMBDA_DIR = Path(__file__).parent.parent / "trigger_lambda"
TRIGGER_LAMBDA_POLICIES = [
iam.LAMBDA_BASIC_EXECUTION_POLICY,
"arn:aws:iam::aws:policy/AmazonS3FullAccess",
]
_EKS_DESCRIBE_POLICY_DOCUMENT = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["eks:DescribeCluster"],
"Resource": "*",
}
],
}
_TRIGGER_INLINE_POLICY_NAME = "eks-describe"
def deploy_trigger_lambda(outputs: dict[str, Any]) -> str:
"""Create the trigger Lambda + API Gateway. Returns the invoke URL."""
print("\nDeploying trigger Lambda...")
# 1. IAM role for the Lambda
role = iam.create_lambda_execution_role(TRIGGER_LAMBDA_ROLE_NAME, STACK_NAME, REGION)
for policy_arn in TRIGGER_LAMBDA_POLICIES[1:]: # basic policy already attached
iam.attach_policy(TRIGGER_LAMBDA_ROLE_NAME, policy_arn, REGION)
# Inline policy: describe EKS cluster
iam_client = get_boto3_client("iam", REGION)
iam_client.put_role_policy(
RoleName=TRIGGER_LAMBDA_ROLE_NAME,
PolicyName=_TRIGGER_INLINE_POLICY_NAME,
PolicyDocument=json.dumps(_EKS_DESCRIBE_POLICY_DOCUMENT),
)
time.sleep(5) # IAM propagation
# 2. Grant Lambda role access to EKS cluster
_enable_api_auth_mode()
eks_client = get_boto3_client("eks", REGION)
for attempt in range(10):
try:
eks_client.create_access_entry(
clusterName=CLUSTER_NAME,
principalArn=role["arn"],
type="STANDARD",
)
print(" Created EKS access entry for Lambda role")
break
except ClientError as e:
code = e.response["Error"]["Code"]
if code == "ResourceInUseException":
break
if (
code == "InvalidRequestException"
and "authentication mode" in str(e)
and attempt < 9
):
time.sleep(10)
continue
raise
with contextlib.suppress(ClientError):
eks_client.associate_access_policy(
clusterName=CLUSTER_NAME,
principalArn=role["arn"],
policyArn="arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy",
accessScope={"type": "cluster"},
)
# 3. Fetch cluster info for Lambda env vars
cluster_info = eks_client.describe_cluster(name=CLUSTER_NAME)["cluster"]
cluster_endpoint = cluster_info["endpoint"]
cluster_ca = cluster_info["certificateAuthority"]["data"]
# 4. Build and deploy Lambda
code_zip = lambda_.bundle_single_file(TRIGGER_LAMBDA_DIR / "handler.py")
func = lambda_.create_function(
name=TRIGGER_LAMBDA_NAME,
role_arn=role["arn"],
handler="handler.lambda_handler",
code_zip=code_zip,
timeout=120,
memory=256,
environment={
"CLUSTER_NAME": CLUSTER_NAME,
"CLUSTER_ENDPOINT": cluster_endpoint,
"CLUSTER_CA_DATA": cluster_ca,
"LANDING_BUCKET": outputs["landing_bucket"],
"PROCESSED_BUCKET": outputs["processed_bucket"],
"IMAGE_URI": outputs["ecr_image_uri"],
"NAMESPACE": "tracer-test",
"SERVICE_ACCOUNT": "etl-pipeline-sa",
},
stack_name=STACK_NAME,
region=REGION,
)
print(f" Lambda deployed: {func['arn']}")
# 5. API Gateway
api = api_gateway.create_simple_api_with_lambda(
api_name=TRIGGER_API_NAME,
lambda_arn=func["arn"],
stack_name=STACK_NAME,
region=REGION,
)
print(f" API Gateway: {api['invoke_url']}")
return api["invoke_url"]
def destroy_trigger_lambda() -> None:
"""Tear down trigger Lambda and API Gateway."""
print("\nDestroying trigger Lambda...")
# Find and delete API Gateway
api_client = get_boto3_client("apigateway", REGION)
try:
apis = api_client.get_rest_apis()["items"]
for a in apis:
if a["name"] == TRIGGER_API_NAME:
api_gateway.delete_api(a["id"], REGION)
print(f" Deleted API {TRIGGER_API_NAME}")
break
except Exception as e:
print(f" Warning deleting API: {e}")
lambda_.delete_function(TRIGGER_LAMBDA_NAME, REGION)
print(f" Deleted Lambda {TRIGGER_LAMBDA_NAME}")
iam.detach_policy(TRIGGER_LAMBDA_ROLE_NAME, iam.LAMBDA_BASIC_EXECUTION_POLICY, REGION)
for policy_arn in TRIGGER_LAMBDA_POLICIES[1:]:
iam.detach_policy(TRIGGER_LAMBDA_ROLE_NAME, policy_arn, REGION)
iam.delete_role(TRIGGER_LAMBDA_ROLE_NAME, REGION)
print(f" Deleted role {TRIGGER_LAMBDA_ROLE_NAME}")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "destroy":
destroy_eks_stack()
else:
deploy_eks_stack()