Skip to content

EC2 teardown

Tear down in reverse: remove the services (routes drain, ALB deprovisions), then the gateway, then the instance and cluster, then the EC2 instance IAM. The shared VPC and IAM roles come last — and only if you are finished with both paths.

1Servicesroutes drain · ALB deprovisions
2Gatewayprotection off · disassociate · delete
3Instance + clusterterminate · task defs · logs · SG
4Instance IAMprofile + role
5Shared VPC + IAMonly when both paths are done

Confirm $INSTANCE_ID and $EC2_SG_ID from EC2 capacity are still set.

Terminal window
export PATH_NAME="${NAME}-ec2"
export CLUSTER="$PATH_NAME"
export GATEWAY="$PATH_NAME"
echo "path ready: CLUSTER=$CLUSTER GATEWAY=$GATEWAY INSTANCE_ID=${INSTANCE_ID:+set}"
path ready: CLUSTER=ecs-gw-lab-ec2 GATEWAY=ecs-gw-lab-ec2 INSTANCE_ID=set
Terminal window
aws ecs update-service --cluster "$CLUSTER" --service "${PATH_NAME}-a" \
--desired-count 0 \
--query 'service.{name:serviceName,desired:desiredCount,status:status}'
{
"name": "ecs-gw-lab-ec2-a",
"desired": 0,
"status": "ACTIVE"
}
Terminal window
aws ecs update-service --cluster "$CLUSTER" --service "${PATH_NAME}-b" \
--desired-count 0 \
--query 'service.{name:serviceName,desired:desiredCount,status:status}'
{
"name": "ecs-gw-lab-ec2-b",
"desired": 0,
"status": "ACTIVE"
}
Terminal window
aws ecs delete-service --cluster "$CLUSTER" --service "${PATH_NAME}-a" --force \
--query 'service.{name:serviceName,status:status}'
{
"name": "ecs-gw-lab-ec2-a",
"status": "DRAINING"
}
Terminal window
aws ecs delete-service --cluster "$CLUSTER" --service "${PATH_NAME}-b" --force \
--query 'service.{name:serviceName,status:status}'
{
"name": "ecs-gw-lab-ec2-b",
"status": "DRAINING"
}

Routes (and the shared ALB) go away after the services disconnect. This often takes several minutes.

Terminal window
for i in $(seq 1 60); do
COUNT=$(aws ecs list-gateway-routes --gateway "$GATEWAY" --cluster "$CLUSTER" \
--query 'length(gatewayRoutes)' --output text 2>/dev/null || echo 0)
echo "routes: $COUNT"
[ "$COUNT" = "0" ] && break
sleep 15
done
routes: 2
routes: 1
routes: 0

update-gateway requires the infrastructure role ARN and a configuration object. For a protection-only change, pass an empty hostSharedAlb.

Terminal window
aws ecs update-gateway --gateway "$GATEWAY" \
--infrastructure-role-arn "$GATEWAY_INFRA_ROLE_ARN" \
--no-deletion-protection \
--configuration '{"hostSharedAlb":{}}' \
--query 'gateway.{name:name,deletionProtection:deletionProtection}'
{
"name": "ecs-gw-lab-ec2",
"deletionProtection": false
}

Wait until the gateway leaves UPDATE_IN_PROGRESS before deleting:

Terminal window
for i in $(seq 1 36); do
STATUS=$(aws ecs describe-gateway --gateway "$GATEWAY" \
--query 'gateway.status' --output text)
echo "$i: $STATUS"
[ "$STATUS" = "UPDATE_COMPLETE" ] && break
sleep 10
done
1: UPDATE_IN_PROGRESS
2: UPDATE_COMPLETE
Terminal window
aws ecs disassociate-gateway-from-cluster \
--gateway "$GATEWAY" \
--cluster "$CLUSTER" >/dev/null
(no stdout — success is silent)
Terminal window
for i in $(seq 1 36); do
ASSOC=$(aws ecs list-gateway-cluster-associations --gateway "$GATEWAY" \
--query 'length(gatewayClusterAssociations)' --output text 2>/dev/null || echo 0)
echo "associations: $ASSOC"
[ "$ASSOC" = "0" ] && break
sleep 10
done
associations: 1
associations: 0
Terminal window
aws ecs delete-gateway --gateway "$GATEWAY" \
--query '{name:name,status:status}'
{
"name": "ecs-gw-lab-ec2",
"status": "DELETED"
}
Terminal window
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" \
--query 'TerminatingInstances[0].CurrentState.Name' --output text
shutting-down
Terminal window
aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID"
echo "instance terminated"
instance terminated
Terminal window
aws ecs delete-cluster --cluster "$CLUSTER" \
--query 'cluster.{name:clusterName,status:status}'
{
"name": "ecs-gw-lab-ec2",
"status": "INACTIVE"
}
Terminal window
aws ecs list-task-definitions --family-prefix "$PATH_NAME" \
--query 'length(taskDefinitionArns)' --output text
2
Terminal window
aws ecs list-task-definitions --family-prefix "$PATH_NAME" --query 'taskDefinitionArns[]' \
--output text | tr '\t' '\n' | while read -r ARN; do
[ -n "$ARN" ] && aws ecs deregister-task-definition --task-definition "$ARN" >/dev/null
done
echo "deregistered"
deregistered

Delete log group and instance security group

Section titled “Delete log group and instance security group”
Terminal window
aws logs delete-log-group --log-group-name "/ecs/${PATH_NAME}" || true
echo "log group gone (or already missing)"
log group gone (or already missing)
Terminal window
aws ec2 delete-security-group --group-id "$EC2_SG_ID" || true
echo "instance security group deleted"
instance security group deleted
Terminal window
aws iam remove-role-from-instance-profile \
--instance-profile-name "${NAME}-ec2" --role-name "${NAME}-ec2" || true
aws iam delete-instance-profile --instance-profile-name "${NAME}-ec2" || true
echo "instance profile removed"
instance profile removed
Terminal window
aws iam detach-role-policy \
--role-name "${NAME}-ec2" \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role || true
aws iam delete-role --role-name "${NAME}-ec2" || true
echo "instance role removed"
instance role removed

EC2-path resources are removed. Stop here if you still want the shared VPC for the Fargate path.

Skip this section if you still plan to run (or keep) the Fargate path.

Terminal window
aws iam detach-role-policy \
--role-name "${NAME}-gateway-infra" \
--policy-arn arn:aws:iam::aws:policy/AmazonECSInfrastructureRoleForGatewayHostSharedALB || true
aws iam delete-role --role-name "${NAME}-gateway-infra" || true
echo "gateway infra role removed"
gateway infra role removed
Terminal window
aws iam detach-role-policy \
--role-name "${NAME}-exec" \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy || true
aws iam delete-role --role-name "${NAME}-exec" || true
echo "exec role removed"
exec role removed

ALB / task / instance ENIs must clear before the VPC can delete.

Terminal window
for i in $(seq 1 36); do
COUNT=$(aws ec2 describe-network-interfaces \
--filters "Name=vpc-id,Values=${VPC_ID}" \
--query 'length(NetworkInterfaces)' --output text)
echo "ENIs: $COUNT"
[ "$COUNT" = "0" ] && break
sleep 10
done
ENIs: 3
ENIs: 1
ENIs: 0

IDs stay in shell variables — do not echo full resource ARNs into tickets.

Terminal window
aws ec2 delete-nat-gateway --nat-gateway-id "$NAT_GW_ID" \
--query 'NatGatewayId' --output text
nat-<id>
Terminal window
aws ec2 wait nat-gateway-deleted --nat-gateway-ids "$NAT_GW_ID"
echo "NAT deleted"
NAT deleted
Terminal window
aws ec2 release-address --allocation-id "$EIP_ALLOC_ID"
echo "EIP released"
EIP released
Terminal window
for RTB in "$PRIVATE_RTB_ID" "$PUBLIC_RTB_ID"; do
for ASSOC_ID in $(aws ec2 describe-route-tables --route-table-ids "$RTB" \
--query 'RouteTables[0].Associations[?SubnetId!=`null`].RouteTableAssociationId' \
--output text); do
aws ec2 disassociate-route-table --association-id "$ASSOC_ID"
done
done
echo "subnet route associations cleared"
subnet route associations cleared
Terminal window
aws ec2 delete-route --route-table-id "$PRIVATE_RTB_ID" --destination-cidr-block 0.0.0.0/0 || true
aws ec2 delete-route-table --route-table-id "$PRIVATE_RTB_ID"
aws ec2 delete-route --route-table-id "$PUBLIC_RTB_ID" --destination-cidr-block 0.0.0.0/0 || true
aws ec2 delete-route-table --route-table-id "$PUBLIC_RTB_ID"
echo "route tables deleted"
route tables deleted
Terminal window
aws ec2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID"
aws ec2 delete-internet-gateway --internet-gateway-id "$IGW_ID"
echo "IGW deleted"
IGW deleted
Terminal window
aws ec2 delete-subnet --subnet-id "$PRIVATE_SUBNET_A"
aws ec2 delete-subnet --subnet-id "$PRIVATE_SUBNET_B"
aws ec2 delete-subnet --subnet-id "$PUBLIC_SUBNET_A"
aws ec2 delete-subnet --subnet-id "$PUBLIC_SUBNET_B"
echo "subnets deleted"
subnets deleted
Terminal window
aws ec2 delete-vpc --vpc-id "$VPC_ID"
echo "VPC deleted"
VPC deleted