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.
Path variables
Section titled “Path variables”Confirm $INSTANCE_ID and $EC2_SG_ID from EC2 capacity are still set.
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=setScale services to zero
Section titled “Scale services to zero”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"}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"}Delete services
Section titled “Delete services”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"}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"}Wait for routes to clear
Section titled “Wait for routes to clear”Routes (and the shared ALB) go away after the services disconnect. This often takes several minutes.
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 15doneroutes: 2routes: 1routes: 0Disable deletion protection
Section titled “Disable deletion protection”update-gateway requires the infrastructure role ARN and a configuration object. For a protection-only change, pass an empty hostSharedAlb.
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:
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 10done1: UPDATE_IN_PROGRESS2: UPDATE_COMPLETEDisassociate gateway from cluster
Section titled “Disassociate gateway from cluster”aws ecs disassociate-gateway-from-cluster \ --gateway "$GATEWAY" \ --cluster "$CLUSTER" >/dev/null(no stdout — success is silent)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 10doneassociations: 1associations: 0Delete gateway
Section titled “Delete gateway”aws ecs delete-gateway --gateway "$GATEWAY" \ --query '{name:name,status:status}'{ "name": "ecs-gw-lab-ec2", "status": "DELETED"}Terminate instance
Section titled “Terminate instance”aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" \ --query 'TerminatingInstances[0].CurrentState.Name' --output textshutting-downaws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID"echo "instance terminated"instance terminatedDelete cluster
Section titled “Delete cluster”aws ecs delete-cluster --cluster "$CLUSTER" \ --query 'cluster.{name:clusterName,status:status}'{ "name": "ecs-gw-lab-ec2", "status": "INACTIVE"}Deregister task definitions
Section titled “Deregister task definitions”aws ecs list-task-definitions --family-prefix "$PATH_NAME" \ --query 'length(taskDefinitionArns)' --output text2aws 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/nulldoneecho "deregistered"deregisteredDelete log group and instance security group
Section titled “Delete log group and instance security group”aws logs delete-log-group --log-group-name "/ecs/${PATH_NAME}" || trueecho "log group gone (or already missing)"log group gone (or already missing)aws ec2 delete-security-group --group-id "$EC2_SG_ID" || trueecho "instance security group deleted"instance security group deletedEC2 instance IAM
Section titled “EC2 instance IAM”aws iam remove-role-from-instance-profile \ --instance-profile-name "${NAME}-ec2" --role-name "${NAME}-ec2" || trueaws iam delete-instance-profile --instance-profile-name "${NAME}-ec2" || trueecho "instance profile removed"instance profile removedaws iam detach-role-policy \ --role-name "${NAME}-ec2" \ --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role || trueaws iam delete-role --role-name "${NAME}-ec2" || trueecho "instance role removed"instance role removedEC2-path resources are removed. Stop here if you still want the shared VPC for the Fargate path.
Shared VPC and roles
Section titled “Shared VPC and roles”Skip this section if you still plan to run (or keep) the Fargate path.
Shared IAM roles
Section titled “Shared IAM roles”aws iam detach-role-policy \ --role-name "${NAME}-gateway-infra" \ --policy-arn arn:aws:iam::aws:policy/AmazonECSInfrastructureRoleForGatewayHostSharedALB || trueaws iam delete-role --role-name "${NAME}-gateway-infra" || trueecho "gateway infra role removed"gateway infra role removedaws iam detach-role-policy \ --role-name "${NAME}-exec" \ --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy || trueaws iam delete-role --role-name "${NAME}-exec" || trueecho "exec role removed"exec role removedWait for ENIs
Section titled “Wait for ENIs”ALB / task / instance ENIs must clear before the VPC can delete.
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 10doneENIs: 3ENIs: 1ENIs: 0NAT, routes, subnets, VPC
Section titled “NAT, routes, subnets, VPC”IDs stay in shell variables — do not echo full resource ARNs into tickets.
aws ec2 delete-nat-gateway --nat-gateway-id "$NAT_GW_ID" \ --query 'NatGatewayId' --output textnat-<id>aws ec2 wait nat-gateway-deleted --nat-gateway-ids "$NAT_GW_ID"echo "NAT deleted"NAT deletedaws ec2 release-address --allocation-id "$EIP_ALLOC_ID"echo "EIP released"EIP releasedfor 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" donedoneecho "subnet route associations cleared"subnet route associations clearedaws ec2 delete-route --route-table-id "$PRIVATE_RTB_ID" --destination-cidr-block 0.0.0.0/0 || trueaws 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 || trueaws ec2 delete-route-table --route-table-id "$PUBLIC_RTB_ID"echo "route tables deleted"route tables deletedaws 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 deletedaws 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 deletedaws ec2 delete-vpc --vpc-id "$VPC_ID"echo "VPC deleted"VPC deleted