EC2 services
Confirm $CLUSTER, $GATEWAY, and $PATH_NAME are ${NAME}-ec2, and $EXEC_ROLE_ARN / private subnet IDs are set. Application step: two small apps + gateway connector
Opt-in parameter on an ECS service that names the container port for gateway traffic. Can filter PUBLIC or PRIVATE access types and set health checks.
Connect services.
Stand up two different apps — Orders and Catalog — so each managed hostname clearly shows which service the gateway routed to. Same nginx image; different HTML baked into each task definition. Same connector as the Fargate path — only the launch type differs (EC2 instead of FARGATE).
<id-a>.ecs.<region>.on.aws<id-b>.ecs.<region>.on.awsConfirm path
Section titled “Confirm path”echo "CLUSTER=$CLUSTER GATEWAY=$GATEWAY PATH_NAME=$PATH_NAME"CLUSTER=ecs-gw-lab-ec2 GATEWAY=ecs-gw-lab-ec2 PATH_NAME=ecs-gw-lab-ec2Log group
Section titled “Log group”AmazonECSTaskExecutionRolePolicy can write log streams, but not create the group. Create the group with your caller identity (do not use awslogs-create-group — that needs logs:CreateLogGroup on the exec role).
aws logs create-log-group --log-group-name "/ecs/${PATH_NAME}" 2>/dev/null || true(no stdout — created, or already exists)aws logs put-retention-policy --log-group-name "/ecs/${PATH_NAME}" --retention-in-days 7(no stdout — success is silent)Task definitions
Section titled “Task definitions”awsvpc + named port web (must match the connector portName). HTML is base64-encoded so the task JSON stays tidy.
Define helpers once in this shell (no AWS call yet):
write_app_html() { local out="$1" title="$2" accent="$3" blurb="$4" cat > "$out" <<HTML<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>${title} · ECS Gateway lab</title> <style> :root { color-scheme: light dark; --accent: ${accent}; } * { box-sizing: border-box; } body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: "Segoe UI", system-ui, sans-serif; background: radial-gradient(1200px 600px at 10% -10%, color-mix(in oklab, var(--accent) 28%, transparent), transparent 60%), radial-gradient(900px 500px at 100% 0%, color-mix(in oklab, var(--accent) 16%, transparent), transparent 55%), #0f1419; color: #e8eef5; } main { width: min(36rem, calc(100% - 2rem)); padding: 2rem 2.1rem 1.75rem; border: 1px solid color-mix(in oklab, var(--accent) 45%, #2a3542); border-radius: 1rem; background: color-mix(in oklab, #15202b 88%, var(--accent)); box-shadow: 0 24px 60px rgba(0,0,0,.35); } .eyebrow { margin: 0 0 .65rem; font-size: .75rem; letter-spacing: .14em; text-transform: uppercase; color: color-mix(in oklab, #c9d6e5 70%, var(--accent)); } h1 { margin: 0 0 .75rem; font-size: clamp(1.8rem, 4vw, 2.4rem); letter-spacing: -0.03em; } p { margin: 0 0 .9rem; line-height: 1.55; color: #c5d0dc; } code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .9em; color: #fff; background: rgba(0,0,0,.28); padding: .12em .4em; border-radius: .35em; } .meta { margin-top: 1.25rem; font-size: .85rem; color: #9aabbc; } </style></head><body> <main data-app="${title}"> <p class="eyebrow">ECS Gateway lab · host-shared ALB</p> <h1>${title}</h1> <p>${blurb}</p> <p class="meta">If you open the other app’s URL, you should get a different page on the <strong>same</strong> load balancer.</p> </main></body></html>HTML}
register_web_task() { local family="$1" html_file="$2" local b64 b64=$(base64 -w0 < "$html_file") cat > /tmp/${family}-task.json <<EOF{ "family": "${family}", "networkMode": "awsvpc", "requiresCompatibilities": ["EC2"], "cpu": "256", "memory": "512", "executionRoleArn": "${EXEC_ROLE_ARN}", "containerDefinitions": [{ "name": "web", "image": "public.ecr.aws/nginx/nginx:stable-alpine", "essential": true, "entryPoint": ["sh", "-c"], "command": ["echo '${b64}' | base64 -d > /usr/share/nginx/html/index.html && exec nginx -g 'daemon off;'"], "portMappings": [{ "name": "web", "containerPort": 80, "protocol": "tcp", "appProtocol": "http" }], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/${PATH_NAME}", "awslogs-region": "${AWS_REGION}", "awslogs-stream-prefix": "web" } } }]}EOF aws ecs register-task-definition \ --cli-input-json "file:///tmp/${family}-task.json" \ --query 'taskDefinition.{family:family,revision:revision}'}Write the two pages (local files only):
write_app_html /tmp/${PATH_NAME}-orders.html "Orders" "#3d8bfd" \ "Checkout and order history for the shop. This page is served by <code>${PATH_NAME}-a</code> behind the EC2-path gateway."write_app_html /tmp/${PATH_NAME}-catalog.html "Catalog" "#3ecf8e" \ "Product listing and search. This page is served by <code>${PATH_NAME}-b</code> on a different hostname — same ALB, different route."ls /tmp/${PATH_NAME}-orders.html /tmp/${PATH_NAME}-catalog.html/tmp/ecs-gw-lab-ec2-orders.html/tmp/ecs-gw-lab-ec2-catalog.htmlRegister Orders:
register_web_task "${PATH_NAME}-a" /tmp/${PATH_NAME}-orders.html{ "family": "ecs-gw-lab-ec2-a", "revision": 1}Register Catalog:
register_web_task "${PATH_NAME}-b" /tmp/${PATH_NAME}-catalog.html{ "family": "ecs-gw-lab-ec2-b", "revision": 1}Service A (Orders)
Section titled “Service A (Orders)”Tasks run in private subnets with assignPublicIp=DISABLED on the EC2 capacity from capacity. First connector attachment provisions the shared ALB in the public subnets for this gateway.
aws ecs create-service \ --cluster "$CLUSTER" \ --service-name "${PATH_NAME}-a" \ --task-definition "${PATH_NAME}-a" \ --desired-count 1 \ --launch-type EC2 \ --network-configuration "awsvpcConfiguration={ subnets=[${PRIVATE_SUBNET_A},${PRIVATE_SUBNET_B}], assignPublicIp=DISABLED }" \ --gateway-connectors '[{ "portName": "web", "filters": { "accessTypes": ["PUBLIC"] }, "healthCheck": { "healthCheckPath": "/" } }]' \ --query 'service.{name:serviceName,status:status,desired:desiredCount}'{ "name": "ecs-gw-lab-ec2-a", "status": "ACTIVE", "desired": 1}Service B (Catalog)
Section titled “Service B (Catalog)”Same connector shape — platform already defined ingress on the EC2-path gateway.
aws ecs create-service \ --cluster "$CLUSTER" \ --service-name "${PATH_NAME}-b" \ --task-definition "${PATH_NAME}-b" \ --desired-count 1 \ --launch-type EC2 \ --network-configuration "awsvpcConfiguration={ subnets=[${PRIVATE_SUBNET_A},${PRIVATE_SUBNET_B}], assignPublicIp=DISABLED }" \ --gateway-connectors '[{ "portName": "web", "filters": { "accessTypes": ["PUBLIC"] }, "healthCheck": { "healthCheckPath": "/" } }]' \ --query 'service.{name:serviceName,status:status,desired:desiredCount}'{ "name": "ecs-gw-lab-ec2-b", "status": "ACTIVE", "desired": 1}