Repository: cloud-custodian/cloud-custodian

Issue:
aws.ecs-service | subnet filter 
**Is your feature request related to a problem? Please describe.**
Trying to male sure that all ECS tasks/services run in a private subnet.

**Describe the solution you'd like**
`subnet` filter for ecs-service that would return all the subnets returned as per describe-services call (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#ECS.Client.describe_services)
Internal subnets are tagged.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


Discussion:
linux-foundation-easycla[bot]: <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/14202841/52837350/6453/#/?version=1"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-missing-id.svg" alt="CLA Missing ID" align="left" height="28" width="328" ></a><br/><br /><ul><li> :x: The commit (7ed646ef82fe98ed9e7147ef513b6481c4bb45df) is missing the User's ID, preventing the EasyCLA check. <a href='https://help.github.com/en/github/committing-changes-to-your-project/why-are-my-commits-linked-to-the-wrong-user' target='_blank'>Consult GitHub Help</a> to resolve.For further assistance with EasyCLA, <a href='https://jira.linuxfoundation.org/servicedesk/customer/portal/4' target='_blank'>please submit a support request ticket</a>.</li></ul>

Patch:
diff --git a/c7n/resources/ecs.py b/c7n/resources/ecs.py
index 10c373c29..ae6326157 100644
--- a/c7n/resources/ecs.py
+++ b/c7n/resources/ecs.py
@@ -8,8 +8,10 @@ from c7n.filters import MetricsFilter, ValueFilter, Filter
 from c7n.manager import resources
 from c7n.utils import local_session, chunks, get_retry, type_schema, group_by
 from c7n import query
+import jmespath
 from c7n.tags import Tag, TagDelayedAction, RemoveTag, TagActionFilter
 from c7n.actions import AutoTagUser
+import c7n.filters.vpc as net_filters
 
 
 def ecs_tag_normalize(resources):
@@ -235,6 +237,25 @@ class ServiceTaskDefinitionFilter(RelatedTaskDefinitionFilter):
     """
 
 
+@Service.filter_registry.register('subnet')
+class SubnetFilter(net_filters.SubnetFilter):
+
+    RelatedIdsExpression = ""
+    expressions = ('taskSets[].networkConfiguration.awsvpcConfiguration.subnets[]',
+                'deployments[].networkConfiguration.awsvpcConfiguration.subnets[]',
+                'networkConfiguration.awsvpcConfiguration.subnets[]')
+
+    def get_related_ids(self, resources):
+        subnet_ids = set()
+        for exp in self.expressions:
+            cexp = jmespath.compile(exp)
+            for r in resources:
+                ids = cexp.search(r)
+                if ids:
+                    subnet_ids.update(ids)
+        return list(subnet_ids)
+
+
 @Service.action_registry.register('modify')
 class UpdateService(BaseAction):
     """Action to update service


Test patch:
diff --git a/tests/data/placebo/test_ecs_service_subnet/ec2.DescribeSubnets_1.json b/tests/data/placebo/test_ecs_service_subnet/ec2.DescribeSubnets_1.json
new file mode 100644
index 000000000..91c07503e
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ec2.DescribeSubnets_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 400,
+    "data": {
+        "Error": {
+            "Code": "InvalidSubnetID.NotFound",
+            "Message": "The subnet ID 'subnet-06261c743949f093d' does not exist"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_service_subnet/ec2.DescribeSubnets_2.json b/tests/data/placebo/test_ecs_service_subnet/ec2.DescribeSubnets_2.json
new file mode 100644
index 000000000..a59d1bcbd
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ec2.DescribeSubnets_2.json
@@ -0,0 +1,30 @@
+{
+    "status_code": 200,
+    "data": {
+        "Subnets": [
+            {
+                "AvailabilityZone": "us-east-1b",
+                "AvailabilityZoneId": "use1-az6",
+                "AvailableIpAddressCount": 251,
+                "CidrBlock": "10.0.0.0/24",
+                "DefaultForAz": false,
+                "MapPublicIpOnLaunch": false,
+                "MapCustomerOwnedIpOnLaunch": false,
+                "State": "available",
+                "SubnetId": "subnet-05b58b4afe5124322",
+                "VpcId": "vpc-0a59f94e3fca9fbc0",
+                "OwnerId": "644160558196",
+                "AssignIpv6AddressOnCreation": false,
+                "Ipv6CidrBlockAssociationSet": [],
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "implied"
+                    }
+                ],
+                "SubnetArn": "arn:aws:ec2:us-east-1:644160558196:subnet/subnet-05b58b4afe5124322"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_service_subnet/ecs.DescribeServices_1.json b/tests/data/placebo/test_ecs_service_subnet/ecs.DescribeServices_1.json
new file mode 100644
index 000000000..4d42060ee
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ecs.DescribeServices_1.json
@@ -0,0 +1,240 @@
+{
+    "status_code": 200,
+    "data": {
+        "services": [
+            {
+                "serviceArn": "arn:aws:ecs:us-east-1:644160558196:service/test/c7n-test",
+                "serviceName": "c7n-test",
+                "clusterArn": "arn:aws:ecs:us-east-1:644160558196:cluster/test",
+                "loadBalancers": [],
+                "serviceRegistries": [],
+                "status": "ACTIVE",
+                "desiredCount": 1,
+                "runningCount": 0,
+                "pendingCount": 0,
+                "launchType": "EC2",
+                "taskDefinition": "arn:aws:ecs:us-east-1:644160558196:task-definition/awsvpc:8",
+                "deploymentConfiguration": {
+                    "deploymentCircuitBreaker": {
+                        "enable": false,
+                        "rollback": false
+                    },
+                    "maximumPercent": 150,
+                    "minimumHealthyPercent": 100
+                },
+                "deployments": [
+                    {
+                        "id": "ecs-svc/2716610221985162177",
+                        "status": "PRIMARY",
+                        "taskDefinition": "arn:aws:ecs:us-east-1:644160558196:task-definition/awsvpc:8",
+                        "desiredCount": 1,
+                        "pendingCount": 0,
+                        "runningCount": 0,
+                        "failedTasks": 10,
+                        "createdAt": {
+                            "__class__": "datetime",
+                            "year": 2021,
+                            "month": 2,
+                            "day": 15,
+                            "hour": 21,
+                            "minute": 36,
+                            "second": 45,
+                            "microsecond": 893000
+                        },
+                        "updatedAt": {
+                            "__class__": "datetime",
+                            "year": 2021,
+                            "month": 2,
+                            "day": 15,
+                            "hour": 21,
+                            "minute": 36,
+                            "second": 45,
+                            "microsecond": 893000
+                        },
+                        "launchType": "EC2",
+                        "networkConfiguration": {
+                            "awsvpcConfiguration": {
+                                "subnets": [
+                                    "subnet-05b58b4afe5124322"
+                                ],
+                                "securityGroups": [
+                                    "sg-012477875710867e7"
+                                ],
+                                "assignPublicIp": "DISABLED"
+                            }
+                        },
+                        "rolloutState": "IN_PROGRESS",
+                        "rolloutStateReason": "ECS deployment ecs-svc/2716610221985162177 in progress."
+                    }
+                ],
+                "roleArn": "arn:aws:iam::644160558196:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS",
+                "events": [
+                    {
+                        "id": "e39bf60c-fc86-4975-87d5-2ba605c2f028",
+                        "createdAt": {
+                            "__class__": "datetime",
+                            "year": 2021,
+                            "month": 2,
+                            "day": 15,
+                            "hour": 21,
+                            "minute": 36,
+                            "second": 50,
+                            "microsecond": 338000
+                        },
+                        "message": "(service c7n-test) was unable to place a task because no container instance met all of its requirements. Reason: No Container Instances were found in your cluster. For more information, see the Troubleshooting section of the Amazon ECS Developer Guide."
+                    }
+                ],
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 2,
+                    "day": 15,
+                    "hour": 21,
+                    "minute": 36,
+                    "second": 45,
+                    "microsecond": 893000
+                },
+                "placementConstraints": [],
+                "placementStrategy": [
+                    {
+                        "type": "spread",
+                        "field": "attribute:ecs.availability-zone"
+                    },
+                    {
+                        "type": "spread",
+                        "field": "instanceId"
+                    }
+                ],
+                "networkConfiguration": {
+                    "awsvpcConfiguration": {
+                        "subnets": [
+                            "subnet-05b58b4afe5124322"
+                        ],
+                        "securityGroups": [
+                            "sg-012477875710867e7"
+                        ],
+                        "assignPublicIp": "DISABLED"
+                    }
+                },
+                "schedulingStrategy": "REPLICA",
+                "createdBy": "arn:aws:iam::644160558196:user/pratyush.mishra@capitalone.com",
+                "enableECSManagedTags": true,
+                "propagateTags": "NONE"
+            },
+            {
+                "serviceArn": "arn:aws:ecs:us-east-1:644160558196:service/test-no-tag",
+                "serviceName": "test-no-tag",
+                "clusterArn": "arn:aws:ecs:us-east-1:644160558196:cluster/test",
+                "loadBalancers": [],
+                "serviceRegistries": [],
+                "status": "ACTIVE",
+                "desiredCount": 0,
+                "runningCount": 0,
+                "pendingCount": 0,
+                "launchType": "FARGATE",
+                "platformVersion": "LATEST",
+                "taskDefinition": "arn:aws:ecs:us-east-1:644160558196:task-definition/ecs-read-only-root:1",
+                "deploymentConfiguration": {
+                    "deploymentCircuitBreaker": {
+                        "enable": false,
+                        "rollback": false
+                    },
+                    "maximumPercent": 200,
+                    "minimumHealthyPercent": 0
+                },
+                "deployments": [
+                    {
+                        "id": "ecs-svc/9223370485788438197",
+                        "status": "PRIMARY",
+                        "taskDefinition": "arn:aws:ecs:us-east-1:644160558196:task-definition/ecs-read-only-root:1",
+                        "desiredCount": 0,
+                        "pendingCount": 0,
+                        "runningCount": 0,
+                        "failedTasks": 0,
+                        "createdAt": {
+                            "__class__": "datetime",
+                            "year": 2019,
+                            "month": 2,
+                            "day": 24,
+                            "hour": 22,
+                            "minute": 45,
+                            "second": 37,
+                            "microsecond": 602000
+                        },
+                        "updatedAt": {
+                            "__class__": "datetime",
+                            "year": 2020,
+                            "month": 11,
+                            "day": 22,
+                            "hour": 16,
+                            "minute": 46,
+                            "second": 51,
+                            "microsecond": 262000
+                        },
+                        "launchType": "FARGATE",
+                        "platformVersion": "1.3.0",
+                        "networkConfiguration": {
+                            "awsvpcConfiguration": {
+                                "subnets": [
+                                    "subnet-06261c743949f093d"
+                                ],
+                                "securityGroups": [
+                                    "sg-034c1e48ec56c8bcb"
+                                ],
+                                "assignPublicIp": "DISABLED"
+                            }
+                        },
+                        "rolloutState": "COMPLETED",
+                        "rolloutStateReason": "ECS deployment ecs-svc/9223370485788438197 completed."
+                    }
+                ],
+                "roleArn": "arn:aws:iam::644160558196:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS",
+                "events": [
+                    {
+                        "id": "02d6c431-cd24-4c56-b778-5cbfad501335",
+                        "createdAt": {
+                            "__class__": "datetime",
+                            "year": 2021,
+                            "month": 1,
+                            "day": 21,
+                            "hour": 22,
+                            "minute": 55,
+                            "second": 36,
+                            "microsecond": 933000
+                        },
+                        "message": "(service test-no-tag) has reached a steady state."
+                    }
+                ],
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 24,
+                    "hour": 22,
+                    "minute": 45,
+                    "second": 37,
+                    "microsecond": 602000
+                },
+                "placementConstraints": [],
+                "placementStrategy": [],
+                "networkConfiguration": {
+                    "awsvpcConfiguration": {
+                        "subnets": [
+                            "subnet-06261c743949f093d"
+                        ],
+                        "securityGroups": [
+                            "sg-034c1e48ec56c8bcb"
+                        ],
+                        "assignPublicIp": "DISABLED"
+                    }
+                },
+                "schedulingStrategy": "REPLICA",
+                "createdBy": "arn:aws:iam::644160558196:user/robert.mohr@capitalone.com",
+                "enableECSManagedTags": false,
+                "propagateTags": "NONE"
+            }
+        ],
+        "failures": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_service_subnet/ecs.ListClusters_1.json b/tests/data/placebo/test_ecs_service_subnet/ecs.ListClusters_1.json
new file mode 100644
index 000000000..115300df8
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ecs.ListClusters_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "clusterArns": [
+            "arn:aws:ecs:us-east-1:644160558196:cluster/Default",
+            "arn:aws:ecs:us-east-1:644160558196:cluster/default-cluster",
+            "arn:aws:ecs:us-east-1:644160558196:cluster/test-comp-environment_Batch_46372d58-df1f-3602-bf52-25989526c72b",
+            "arn:aws:ecs:us-east-1:644160558196:cluster/test"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_1.json b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_1.json
new file mode 100644
index 000000000..1e54bf0c7
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "serviceArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_2.json b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_2.json
new file mode 100644
index 000000000..1e54bf0c7
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "serviceArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_3.json b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_3.json
new file mode 100644
index 000000000..1e54bf0c7
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_3.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "serviceArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_4.json b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_4.json
new file mode 100644
index 000000000..274464016
--- /dev/null
+++ b/tests/data/placebo/test_ecs_service_subnet/ecs.ListServices_4.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "serviceArns": [
+            "arn:aws:ecs:us-east-1:644160558196:service/test/c7n-test",
+            "arn:aws:ecs:us-east-1:644160558196:service/test-no-tag"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_ecs.py b/tests/test_ecs.py
index e3b1f3b86..350035d88 100644
--- a/tests/test_ecs.py
+++ b/tests/test_ecs.py
@@ -192,6 +192,26 @@ class TestEcsService(BaseTest):
         self.assertEqual(len(resources), 1)
         self.assertTrue(resources[0]['serviceName'], 'test-yes-tag')
 
+    def test_ecs_service_subnet(self):
+        session_factory = self.replay_flight_data("test_ecs_service_subnet")
+        p = self.load_policy(
+            {
+                "name": "ecs-service-subnets",
+                "resource": "ecs-service",
+                "filters": [
+                    {
+                        "type": "subnet",
+                        "key": "tag:Name",
+                        "value": "implied"
+                    }
+                ],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]["serviceName"], "c7n-test")
+
 
 class TestEcsTaskDefinition(BaseTest):