Skip to content

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).

Orders

service -a · port web

Catalog

service -b · port web

One gateway · one shared ALBhost-based routing · HTTPS 443
<id-a>.ecs.<region>.on.aws<id-b>.ecs.<region>.on.aws
Terminal window
echo "CLUSTER=$CLUSTER GATEWAY=$GATEWAY PATH_NAME=$PATH_NAME"
CLUSTER=ecs-gw-lab-ec2 GATEWAY=ecs-gw-lab-ec2 PATH_NAME=ecs-gw-lab-ec2

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).

Terminal window
aws logs create-log-group --log-group-name "/ecs/${PATH_NAME}" 2>/dev/null || true
(no stdout — created, or already exists)
Terminal window
aws logs put-retention-policy --log-group-name "/ecs/${PATH_NAME}" --retention-in-days 7
(no stdout — success is silent)

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):

Terminal window
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):

Terminal window
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.html

Register Orders:

Terminal window
register_web_task "${PATH_NAME}-a" /tmp/${PATH_NAME}-orders.html
{
"family": "ecs-gw-lab-ec2-a",
"revision": 1
}

Register Catalog:

Terminal window
register_web_task "${PATH_NAME}-b" /tmp/${PATH_NAME}-catalog.html
{
"family": "ecs-gw-lab-ec2-b",
"revision": 1
}

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.

Terminal window
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
}

Same connector shape — platform already defined ingress on the EC2-path gateway.

Terminal window
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
}

EC2 verify