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
442 lines
14 KiB
Python
442 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Deploy Flink ECS infrastructure using boto3 SDK.
|
|
|
|
Creates:
|
|
- ECS cluster for running Flink batch tasks
|
|
- ECS task definition for PyFlink container
|
|
- S3 buckets for landing and processed data
|
|
- Lambda function for /trigger endpoint (ingestion + ECS task launcher)
|
|
- API Gateway HTTP API
|
|
- Mock External Vendor API Lambda (shared)
|
|
|
|
Key difference from Prefect stack:
|
|
- No long-running service (tasks run on-demand and exit)
|
|
- Trigger Lambda starts ECS tasks via RunTask API
|
|
"""
|
|
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
project_root = Path(__file__).resolve().parents[3]
|
|
|
|
from tests.shared.infrastructure_sdk import save_outputs
|
|
from tests.shared.infrastructure_sdk.resources import (
|
|
api_gateway,
|
|
ecr,
|
|
ecs,
|
|
iam,
|
|
lambda_,
|
|
logs,
|
|
s3,
|
|
secrets,
|
|
vpc,
|
|
)
|
|
|
|
STACK_NAME = "tracer-flink-ecs"
|
|
REGION = "us-east-1"
|
|
GRAFANA_SECRET_NAME = "tracer/grafana-cloud"
|
|
|
|
|
|
def deploy() -> dict:
|
|
"""Deploy all infrastructure and return outputs."""
|
|
start_time = time.time()
|
|
print(f"Deploying stack: {STACK_NAME}")
|
|
print("=" * 60)
|
|
|
|
# Generate unique suffix for S3 buckets
|
|
unique_suffix = uuid.uuid4().hex[:8]
|
|
|
|
# ========================================
|
|
# Phase 1: Parallel - VPC, S3, IAM, Logs, Security Group
|
|
# ========================================
|
|
print("\n[Phase 1] Creating VPC, S3, IAM, Logs, Security Group...")
|
|
|
|
# VPC lookup
|
|
print(" - Looking up default VPC...")
|
|
vpc_info = vpc.get_default_vpc(REGION)
|
|
vpc_id = vpc_info["vpc_id"]
|
|
subnet_ids = vpc.get_public_subnets(vpc_id, REGION)
|
|
print(f" VPC: {vpc_id}, Subnets: {subnet_ids}")
|
|
|
|
# S3 buckets
|
|
landing_bucket_name = f"tracer-flink-landing-{unique_suffix}"
|
|
processed_bucket_name = f"tracer-flink-processed-{unique_suffix}"
|
|
|
|
print(f" - Creating S3 bucket: {landing_bucket_name}")
|
|
s3.create_bucket(landing_bucket_name, STACK_NAME, REGION)
|
|
|
|
print(f" - Creating S3 bucket: {processed_bucket_name}")
|
|
s3.create_bucket(processed_bucket_name, STACK_NAME, REGION)
|
|
|
|
# CloudWatch log group
|
|
log_group_name = "/ecs/tracer-flink"
|
|
print(f" - Creating log group: {log_group_name}")
|
|
logs.create_log_group(log_group_name, retention_days=7, stack_name=STACK_NAME, region=REGION)
|
|
|
|
# IAM roles
|
|
print(" - Creating ECS task role...")
|
|
task_role = iam.create_ecs_task_role(f"{STACK_NAME}-task-role", STACK_NAME, REGION)
|
|
|
|
print(" - Creating ECS execution role...")
|
|
execution_role = iam.create_ecs_execution_role(
|
|
f"{STACK_NAME}-execution-role", STACK_NAME, REGION
|
|
)
|
|
|
|
print(" - Creating Lambda trigger role...")
|
|
trigger_lambda_role = iam.create_lambda_execution_role(
|
|
f"{STACK_NAME}-trigger-role", STACK_NAME, REGION
|
|
)
|
|
|
|
print(" - Creating Mock API Lambda role...")
|
|
mock_api_role = iam.create_lambda_execution_role(
|
|
f"{STACK_NAME}-mock-api-role", STACK_NAME, REGION
|
|
)
|
|
|
|
# Security group
|
|
print(" - Creating security group...")
|
|
security_group = vpc.create_security_group(
|
|
name=f"{STACK_NAME}-sg",
|
|
vpc_id=vpc_id,
|
|
description="Security group for Flink ECS tasks",
|
|
stack_name=STACK_NAME,
|
|
region=REGION,
|
|
)
|
|
|
|
# ========================================
|
|
# Phase 2: Add IAM permissions
|
|
# ========================================
|
|
print("\n[Phase 2] Configuring IAM permissions...")
|
|
|
|
# Task role: S3 access
|
|
print(" - Granting task role S3 read on landing bucket...")
|
|
s3.grant_read(landing_bucket_name, task_role["arn"], REGION)
|
|
|
|
print(" - Granting task role S3 read/write on processed bucket...")
|
|
s3.grant_read_write(processed_bucket_name, task_role["arn"], REGION)
|
|
|
|
# Execution role: Secrets Manager access for Grafana
|
|
print(" - Granting execution role Secrets Manager access...")
|
|
secret_arn = secrets.get_secret_arn(GRAFANA_SECRET_NAME, REGION)
|
|
iam.put_role_policy(
|
|
execution_role["name"],
|
|
"SecretsAccess",
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["secretsmanager:GetSecretValue"],
|
|
"Resource": [secret_arn],
|
|
}
|
|
],
|
|
},
|
|
REGION,
|
|
)
|
|
|
|
# Trigger Lambda role: S3 write + ECS RunTask (will update task definition ARN later)
|
|
print(" - Granting trigger role S3 write on landing bucket...")
|
|
s3.grant_write(landing_bucket_name, trigger_lambda_role["arn"], REGION)
|
|
|
|
# ========================================
|
|
# Phase 3: ECR and ECS Cluster
|
|
# ========================================
|
|
print("\n[Phase 3] Creating ECR repository and ECS cluster...")
|
|
|
|
# ECR repository
|
|
ecr_repo_name = f"{STACK_NAME}-flink"
|
|
print(f" - Creating ECR repository: {ecr_repo_name}")
|
|
ecr_repo = ecr.create_repository(ecr_repo_name, STACK_NAME, REGION)
|
|
|
|
# Build and push Docker image
|
|
# Build context is project root to include telemetry packages
|
|
context_dir = project_root
|
|
dockerfile_path = (
|
|
project_root
|
|
/ "tests"
|
|
/ "upstream_apache_flink_ecs"
|
|
/ "infrastructure_code"
|
|
/ "flink_image"
|
|
/ "Dockerfile"
|
|
)
|
|
|
|
print(" - Building and pushing Docker image (ARM64)...")
|
|
print(f" Dockerfile: {dockerfile_path}")
|
|
print(f" Context: {context_dir}")
|
|
|
|
image_uri = ecr.build_and_push(
|
|
dockerfile_path=dockerfile_path,
|
|
repository_uri=ecr_repo["uri"],
|
|
tag="latest",
|
|
platform="linux/arm64",
|
|
region=REGION,
|
|
context_dir=context_dir,
|
|
)
|
|
print(f" Image URI: {image_uri}")
|
|
|
|
# ECS cluster
|
|
cluster_name = "tracer-flink-cluster"
|
|
print(f" - Creating ECS cluster: {cluster_name}")
|
|
cluster = ecs.create_cluster(cluster_name, STACK_NAME, REGION)
|
|
|
|
# ========================================
|
|
# Phase 4: Task Definition
|
|
# ========================================
|
|
print("\n[Phase 4] Creating ECS task definition...")
|
|
|
|
# Build container definitions
|
|
flink_container = ecs.build_container_definition(
|
|
name="FlinkContainer",
|
|
image=image_uri,
|
|
cpu=256,
|
|
memory=1536,
|
|
environment={
|
|
"LANDING_BUCKET": landing_bucket_name,
|
|
"PROCESSED_BUCKET": processed_bucket_name,
|
|
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
|
|
"OTEL_SERVICE_NAME": "flink-etl-pipeline",
|
|
"OTEL_RESOURCE_ATTRIBUTES": "pipeline.name=upstream_downstream_pipeline_flink,pipeline.framework=flink,test_case=e2e_upstream_apache_flink_ecs",
|
|
},
|
|
secrets=[
|
|
{
|
|
"name": "GCLOUD_HOSTED_METRICS_URL",
|
|
"valueFrom": f"{secret_arn}:GCLOUD_HOSTED_METRICS_URL::",
|
|
},
|
|
{
|
|
"name": "GCLOUD_HOSTED_METRICS_ID",
|
|
"valueFrom": f"{secret_arn}:GCLOUD_HOSTED_METRICS_ID::",
|
|
},
|
|
{
|
|
"name": "GCLOUD_HOSTED_LOGS_URL",
|
|
"valueFrom": f"{secret_arn}:GCLOUD_HOSTED_LOGS_URL::",
|
|
},
|
|
{"name": "GCLOUD_HOSTED_LOGS_ID", "valueFrom": f"{secret_arn}:GCLOUD_HOSTED_LOGS_ID::"},
|
|
{"name": "GCLOUD_RW_API_KEY", "valueFrom": f"{secret_arn}:GCLOUD_RW_API_KEY::"},
|
|
{"name": "GCLOUD_OTLP_ENDPOINT", "valueFrom": f"{secret_arn}:GCLOUD_OTLP_ENDPOINT::"},
|
|
{
|
|
"name": "GCLOUD_OTLP_AUTH_HEADER",
|
|
"valueFrom": f"{secret_arn}:GCLOUD_OTLP_AUTH_HEADER::",
|
|
},
|
|
],
|
|
log_group=log_group_name,
|
|
region=REGION,
|
|
essential=True,
|
|
)
|
|
|
|
# Alloy sidecar for OTEL
|
|
alloy_container = ecs.build_container_definition(
|
|
name="alloy",
|
|
image="grafana/alloy:v1.4.2",
|
|
cpu=128,
|
|
memory=256,
|
|
command=[
|
|
"run",
|
|
"--server.http.listen-addr=0.0.0.0:12345",
|
|
"/etc/alloy/config.alloy",
|
|
],
|
|
secrets=[
|
|
{
|
|
"name": "GCLOUD_OTLP_ENDPOINT",
|
|
"valueFrom": f"{secret_arn}:GCLOUD_OTLP_ENDPOINT::",
|
|
},
|
|
{
|
|
"name": "GCLOUD_OTLP_AUTH_HEADER",
|
|
"valueFrom": f"{secret_arn}:GCLOUD_OTLP_AUTH_HEADER::",
|
|
},
|
|
],
|
|
log_group=log_group_name,
|
|
region=REGION,
|
|
essential=False,
|
|
)
|
|
|
|
# Add Alloy config file via environment or mount
|
|
# For simplicity, we use the embedded config approach
|
|
alloy_config = """
|
|
otelcol.receiver.otlp "default" {
|
|
grpc {
|
|
endpoint = "0.0.0.0:4317"
|
|
}
|
|
http {
|
|
endpoint = "0.0.0.0:4318"
|
|
}
|
|
output {
|
|
traces = [otelcol.exporter.otlphttp.grafana.input]
|
|
}
|
|
}
|
|
|
|
otelcol.exporter.otlphttp "grafana" {
|
|
client {
|
|
endpoint = env("GCLOUD_OTLP_ENDPOINT")
|
|
headers = {
|
|
"Authorization" = env("GCLOUD_OTLP_AUTH_HEADER"),
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
# Update alloy container with config
|
|
alloy_container.setdefault("environment", []).append(
|
|
{"name": "ALLOY_CONFIG", "value": alloy_config.strip()}
|
|
)
|
|
|
|
# Add container dependency
|
|
flink_container["dependsOn"] = [
|
|
{"containerName": "alloy", "condition": "START"},
|
|
]
|
|
|
|
# Create task definition
|
|
task_def = ecs.create_task_definition(
|
|
family=f"{STACK_NAME}-task",
|
|
container_definitions=[flink_container, alloy_container],
|
|
task_role_arn=task_role["arn"],
|
|
execution_role_arn=execution_role["arn"],
|
|
cpu="512",
|
|
memory="2048",
|
|
runtime_platform={"cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX"},
|
|
stack_name=STACK_NAME,
|
|
region=REGION,
|
|
)
|
|
print(f" Task Definition: {task_def['arn']}")
|
|
|
|
# ========================================
|
|
# Phase 5: Update Trigger Lambda Role with Task Definition ARN
|
|
# ========================================
|
|
print("\n[Phase 5] Updating Lambda role with ECS RunTask permissions...")
|
|
|
|
iam.put_role_policy(
|
|
trigger_lambda_role["name"],
|
|
"EcsRunTask",
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["ecs:RunTask"],
|
|
"Resource": [f"{task_def['arn'].rsplit(':', 1)[0]}:*"],
|
|
},
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["iam:PassRole"],
|
|
"Resource": [task_role["arn"], execution_role["arn"]],
|
|
},
|
|
],
|
|
},
|
|
REGION,
|
|
)
|
|
|
|
# ========================================
|
|
# Phase 6: Lambda Functions and API Gateways
|
|
# ========================================
|
|
print("\n[Phase 6] Creating Lambda functions and API Gateways...")
|
|
|
|
# Mock API Lambda
|
|
mock_api_code_dir = project_root / "tests/shared/external_vendor_api"
|
|
print(f" - Bundling Mock API Lambda from: {mock_api_code_dir}")
|
|
mock_api_code = lambda_.bundle_code(mock_api_code_dir)
|
|
|
|
mock_api_lambda_name = f"{STACK_NAME}-mock-api"
|
|
print(f" - Creating Mock API Lambda: {mock_api_lambda_name}")
|
|
|
|
mock_api_lambda = lambda_.create_function(
|
|
name=mock_api_lambda_name,
|
|
role_arn=mock_api_role["arn"],
|
|
handler="handler.lambda_handler",
|
|
code_zip=mock_api_code,
|
|
runtime="python3.11",
|
|
timeout=30,
|
|
memory=128,
|
|
stack_name=STACK_NAME,
|
|
region=REGION,
|
|
)
|
|
|
|
# Create Mock API Gateway
|
|
print(" - Creating Mock API Gateway...")
|
|
mock_api = api_gateway.create_simple_api_with_lambda(
|
|
api_name=f"{STACK_NAME}-mock-api",
|
|
lambda_arn=mock_api_lambda["arn"],
|
|
stack_name=STACK_NAME,
|
|
region=REGION,
|
|
)
|
|
print(f" Mock API URL: {mock_api['invoke_url']}")
|
|
|
|
# Trigger Lambda
|
|
trigger_code_dir = (
|
|
project_root / "tests/e2e/upstream_apache_flink_ecs/pipeline_code/trigger_lambda"
|
|
)
|
|
requirements_file = trigger_code_dir / "requirements.txt"
|
|
print(f" - Bundling Trigger Lambda from: {trigger_code_dir}")
|
|
trigger_code = lambda_.bundle_code(trigger_code_dir, requirements_file)
|
|
|
|
trigger_lambda_name = f"{STACK_NAME}-trigger"
|
|
print(f" - Creating Trigger Lambda: {trigger_lambda_name}")
|
|
|
|
trigger_lambda = lambda_.create_function(
|
|
name=trigger_lambda_name,
|
|
role_arn=trigger_lambda_role["arn"],
|
|
handler="handler.lambda_handler",
|
|
code_zip=trigger_code,
|
|
runtime="python3.11",
|
|
timeout=60,
|
|
memory=256,
|
|
environment={
|
|
"LANDING_BUCKET": landing_bucket_name,
|
|
"PROCESSED_BUCKET": processed_bucket_name,
|
|
"EXTERNAL_API_URL": mock_api["invoke_url"],
|
|
"ECS_CLUSTER": cluster["arn"],
|
|
"TASK_DEFINITION": task_def["arn"],
|
|
"SUBNET_IDS": ",".join(subnet_ids),
|
|
"SECURITY_GROUP_ID": security_group["group_id"],
|
|
},
|
|
stack_name=STACK_NAME,
|
|
region=REGION,
|
|
)
|
|
|
|
# Create Trigger API Gateway
|
|
print(" - Creating Trigger API Gateway...")
|
|
trigger_api = api_gateway.create_simple_api_with_lambda(
|
|
api_name=f"{STACK_NAME}-trigger",
|
|
lambda_arn=trigger_lambda["arn"],
|
|
stack_name=STACK_NAME,
|
|
region=REGION,
|
|
)
|
|
print(f" Trigger API URL: {trigger_api['invoke_url']}")
|
|
|
|
# ========================================
|
|
# Save Outputs
|
|
# ========================================
|
|
outputs = {
|
|
"LandingBucketName": landing_bucket_name,
|
|
"ProcessedBucketName": processed_bucket_name,
|
|
"TriggerApiUrl": trigger_api["invoke_url"],
|
|
"MockApiUrl": mock_api["invoke_url"],
|
|
"EcsClusterName": cluster_name,
|
|
"EcsClusterArn": cluster["arn"],
|
|
"LogGroupName": log_group_name,
|
|
"TaskDefinitionArn": task_def["arn"],
|
|
"SecurityGroupId": security_group["group_id"],
|
|
"SubnetIds": ",".join(subnet_ids),
|
|
"TriggerLambdaName": trigger_lambda_name,
|
|
"TriggerLambdaArn": trigger_lambda["arn"],
|
|
"MockApiLambdaName": mock_api_lambda_name,
|
|
"MockApiLambdaArn": mock_api_lambda["arn"],
|
|
"VpcId": vpc_id,
|
|
"EcrRepositoryUri": ecr_repo["uri"],
|
|
"ImageUri": image_uri,
|
|
}
|
|
|
|
save_outputs(STACK_NAME, outputs)
|
|
|
|
elapsed = time.time() - start_time
|
|
print("\n" + "=" * 60)
|
|
print(f"Deployment completed in {elapsed:.1f}s")
|
|
print("=" * 60)
|
|
print("\nOutputs:")
|
|
for key, value in outputs.items():
|
|
print(f" {key}: {value}")
|
|
|
|
return outputs
|
|
|
|
|
|
if __name__ == "__main__":
|
|
deploy()
|