Repository: cloud-custodian/cloud-custodian

Issue:
support aws elb v2 for app lb
also need to investigate and potentially try to minimalize overlap with extant elb resources



Discussion:

Resolving patch:
diff --git a/c7n/filters/metrics.py b/c7n/filters/metrics.py
--- a/c7n/filters/metrics.py
+++ b/c7n/filters/metrics.py
@@ -93,6 +93,7 @@ class MetricsFilter(Filter):
         'elasticache': 'AWS/ElastiCache',
         'ec2': 'AWS/EC2',
         'elb': 'AWS/ELB',
+        'elbv2': 'AWS/ApplicationELB',
         'emr': 'AWS/EMR',
         'es': 'AWS/ES',
         'events': 'AWS/Events',
diff --git a/c7n/resources/__init__.py b/c7n/resources/__init__.py
--- a/c7n/resources/__init__.py
+++ b/c7n/resources/__init__.py
@@ -21,6 +21,7 @@ def load_resources():
     import c7n.resources.ami
     import c7n.resources.acm
     import c7n.resources.apigw
+    import c7n.resources.appelb
     import c7n.resources.asg
     import c7n.resources.awslambda
     import c7n.resources.cfn
diff --git a/c7n/resources/appelb.py b/c7n/resources/appelb.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/appelb.py
@@ -0,0 +1,190 @@
+# Copyright 2016 Capital One Services, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Application Load Balancers
+"""
+import logging
+
+from c7n.actions import ActionRegistry, BaseAction
+from c7n.filters import Filter, FilterRegistry, DefaultVpcBase
+from c7n import tags
+from c7n.manager import resources
+from c7n.query import QueryResourceManager
+from c7n.utils import local_session, chunks, type_schema, get_retry
+
+log = logging.getLogger('custodian.app-elb')
+
+filters = FilterRegistry('app-elb.filters')
+actions = ActionRegistry('app-elb.actions')
+
+filters.register('tag-count', tags.TagCountFilter)
+filters.register('marked-for-op', tags.TagActionFilter)
+
+
+@resources.register('app-elb')
+class AppELB(QueryResourceManager):
+    """Resource manager for v2 ELBs (AKA ALBs).
+    """
+
+    class Meta(object):
+
+        service = 'elbv2'
+        type = 'app-elb'
+        enum_spec = ('describe_load_balancers', 'LoadBalancers', None)
+        name = 'LoadBalancerName'
+        id = 'LoadBalancerArn'
+        filter_name = None
+        filter_type = None
+        dimension = None
+        date = 'CreatedTime'
+
+    resource_type = Meta
+    filter_registry = filters
+    action_registry = actions
+    retry = staticmethod(get_retry(('Throttling',)))
+
+    def augment(self, albs):
+        _describe_appelb_tags(
+            albs, self.session_factory, self.executor_factory, self.retry)
+
+        return albs
+
+
+def _describe_appelb_target_groups(albs, session_factory, executor_factory, retry):
+    def _process_target_groups(alb):
+        client = local_session(session_factory).client('elbv2')
+
+        results = retry(
+            client.describe_target_groups,
+            LoadBalancerArn=alb['LoadBalancerArn'])
+        alb['TargetGroups'] = results['TargetGroups']
+
+    with executor_factory(max_workers=2) as w:
+        list(w.map(_process_target_groups, albs))
+
+
+def _describe_appelb_tags(albs, session_factory, executor_factory, retry):
+    def _process_tags(alb_set):
+        client = local_session(session_factory).client('elbv2')
+        alb_map = {alb['LoadBalancerArn']: alb for alb in alb_set}
+
+        results = retry(client.describe_tags, ResourceArns=alb_map.keys())
+        for tag_desc in results['TagDescriptions']:
+            alb_map[tag_desc['ResourceArn']]['Tags'] = tag_desc['Tags']
+
+    with executor_factory(max_workers=2) as w:
+        list(w.map(_process_tags, chunks(albs, 20)))
+
+
+def _add_appelb_tags(albs, session_factory, ts):
+    client = local_session(session_factory).client('elbv2')
+    client.add_tags(
+        ResourceArns=[alb['LoadBalancerArn'] for alb in albs],
+        Tags=ts)
+
+
+def _remove_appelb_tags(albs, session_factory, tag_keys):
+    client = local_session(session_factory).client('elbv2')
+    client.remove_tags(
+        ResourceArns=[alb['LoadBalancerArn'] for alb in albs],
+        TagKeys=tag_keys)
+
+
+@actions.register('mark-for-op')
+class AppELBMarkForOpAction(tags.TagDelayedAction):
+
+    batch_size = 1
+
+    def process_resource_set(self, resource_set, ts):
+        _add_appelb_tags(
+            resource_set,
+            self.manager.session_factory,
+            ts)
+
+
+@actions.register('tag')
+class AppELBTagAction(tags.Tag):
+
+    batch_size = 1
+
+    def process_resource_set(self, resource_set, ts):
+        _add_appelb_tags(
+            resource_set,
+            self.manager.session_factory,
+            ts)
+
+
+@actions.register('remove-tag')
+class AppELBRemoveTagAction(tags.RemoveTag):
+
+    batch_size = 1
+
+    def process_resource_set(self, resource_set, tag_keys):
+        _remove_appelb_tags(
+            resource_set,
+            self.manager.session_factory,
+            tag_keys)
+
+
+@actions.register('delete')
+class AppELBDeleteAction(BaseAction):
+
+    schema = type_schema('delete')
+
+    def process(self, load_balancers):
+        with self.executor_factory(max_workers=2) as w:
+            list(w.map(self.process_alb, load_balancers))
+
+    def process_alb(self, alb):
+        client = local_session(self.manager.session_factory).client('elbv2')
+        client.delete_load_balancer(LoadBalancerArn=alb['LoadBalancerArn'])
+
+
+class AppELBListenerFilter(Filter):
+    """ Base class for filters that query listeners.
+    """
+    def describe_appelb_listeners(self, albs):
+        def _process_listeners(alb):
+            client = local_session(self.manager.session_factory).client('elbv2')
+            results = client.describe_listeners(
+                LoadBalancerArn=alb['LoadBalancerArn'])
+            alb['Listeners'] = results['Listeners']
+
+        with self.manager.executor_factory(max_workers=2) as w:
+            list(w.map(_process_listeners, albs))
+
+
+@filters.register('is-ssl')
+class AppELBIsSSLFilter(AppELBListenerFilter):
+
+    schema = type_schema('is-ssl')
+
+    def process(self, albs, event=None):
+        def _is_ssl(alb):
+            for listener in alb.get('Listeners', []):
+                if listener['Protocol'] == 'HTTPS':
+                    return True
+            return False
+
+        self.describe_appelb_listeners(albs)
+        return [alb for alb in albs if _is_ssl(alb)]
+
+
+@filters.register('default-vpc')
+class AppELBDefaultVpcFilter(DefaultVpcBase):
+
+    schema = type_schema('default-vpc')
+
+    def __call__(self, alb):
+        return alb.get('VPCId') and self.match(alb.get('VPCId')) or False


Test patch:
diff --git a/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.AddTags_1.json b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.AddTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.AddTags_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "b1b78f0c-8663-11e6-84e2-b1164fcb826c", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b1b78f0c-8663-11e6-84e2-b1164fcb826c", 
+                "date": "Thu, 29 Sep 2016 16:42:21 GMT", 
+                "content-length": "229", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeListeners_1.json b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeListeners_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeListeners_1.json
@@ -0,0 +1,30 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Listeners": [
+            {
+                "Protocol": "HTTP", 
+                "DefaultActions": [
+                    {
+                        "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                        "Type": "forward"
+                    }
+                ], 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Port": 80, 
+                "ListenerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:listener/app/alb-1/1e0ac88a139a5177/c5a0110226720d39"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+                "date": "Fri, 30 Sep 2016 13:12:17 GMT", 
+                "content-length": "948", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeLoadBalancers_1.json b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeLoadBalancers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeLoadBalancers_1.json
@@ -0,0 +1,93 @@
+{
+    "status_code": 200, 
+    "data": {
+        "LoadBalancers": [
+            {
+                "VpcId": "vpc-47b2b823", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-1-2087922814.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-ce548cb7"
+                ], 
+                "LoadBalancerName": "alb-1", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 51, 
+                    "microsecond": 200000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 43
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-77b23a2f", 
+                        "ZoneName": "us-west-2c"
+                    }, 
+                    {
+                        "SubnetId": "subnet-bfd682c9", 
+                        "ZoneName": "us-west-2b"
+                    }, 
+                    {
+                        "SubnetId": "subnet-c99aa7ad", 
+                        "ZoneName": "us-west-2a"
+                    }
+                ]
+            }, 
+            {
+                "VpcId": "vpc-4a9ff72e", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-2-1990881227.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-f9cc4d9f"
+                ], 
+                "LoadBalancerName": "alb-2", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 37, 
+                    "microsecond": 750000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 45
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-15452171", 
+                        "ZoneName": "us-west-2a"
+                    }, 
+                    {
+                        "SubnetId": "subnet-5fb47507", 
+                        "ZoneName": "us-west-2c"
+                    }
+                ]
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+                "vary": "Accept-Encoding", 
+                "content-length": "2484", 
+                "content-type": "text/xml", 
+                "date": "Thu, 29 Sep 2016 14:49:04 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeTags_1.json b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeTags_1.json
@@ -0,0 +1,35 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+                "date": "Thu, 29 Sep 2016 14:49:52 GMT", 
+                "content-length": "877", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TagDescriptions": [
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Tags": [
+                    {
+                        "Value": "VALUE2", 
+                        "Key": "KEY2"
+                    }, 
+                    {
+                        "Value": "VALUE1", 
+                        "Key": "KEY1"
+                    }
+                ]
+            }, 
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "Tags": []
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeTargetGroups_1.json b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeTargetGroups_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_add_tag/elasticloadbalancing.DescribeTargetGroups_1.json
@@ -0,0 +1,38 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "b5a109be-871d-11e6-adc8-49939253181f", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b5a109be-871d-11e6-adc8-49939253181f", 
+                "date": "Fri, 30 Sep 2016 14:53:54 GMT", 
+                "content-length": "1312", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TargetGroups": [
+            {
+                "HealthCheckPath": "/", 
+                "HealthCheckIntervalSeconds": 30, 
+                "VpcId": "vpc-47b2b823", 
+                "Protocol": "HTTP", 
+                "HealthCheckTimeoutSeconds": 5, 
+                "HealthCheckProtocol": "HTTP", 
+                "LoadBalancerArns": [
+                    "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177"
+                ], 
+                "UnhealthyThresholdCount": 2, 
+                "HealthyThresholdCount": 5, 
+                "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                "Matcher": {
+                    "HttpCode": "200"
+                }, 
+                "HealthCheckPort": "traffic-port", 
+                "Port": 80, 
+                "TargetGroupName": "target-group-1"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DeleteLoadBalancer_1.json b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DeleteLoadBalancer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DeleteLoadBalancer_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "c95bcb6e-865f-11e6-a073-a7fd2edf0b0d", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "c95bcb6e-865f-11e6-a073-a7fd2edf0b0d", 
+                "date": "Thu, 29 Sep 2016 16:14:23 GMT", 
+                "content-length": "262", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeListeners_1.json b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeListeners_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeListeners_1.json
@@ -0,0 +1,30 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Listeners": [
+            {
+                "Protocol": "HTTP", 
+                "DefaultActions": [
+                    {
+                        "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                        "Type": "forward"
+                    }
+                ], 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Port": 80, 
+                "ListenerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:listener/app/alb-1/1e0ac88a139a5177/c5a0110226720d39"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+                "date": "Fri, 30 Sep 2016 13:12:17 GMT", 
+                "content-length": "948", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeLoadBalancers_1.json b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeLoadBalancers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeLoadBalancers_1.json
@@ -0,0 +1,93 @@
+{
+    "status_code": 200, 
+    "data": {
+        "LoadBalancers": [
+            {
+                "VpcId": "vpc-47b2b823", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-1-2087922814.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-ce548cb7"
+                ], 
+                "LoadBalancerName": "alb-1", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 51, 
+                    "microsecond": 200000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 43
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-77b23a2f", 
+                        "ZoneName": "us-west-2c"
+                    }, 
+                    {
+                        "SubnetId": "subnet-bfd682c9", 
+                        "ZoneName": "us-west-2b"
+                    }, 
+                    {
+                        "SubnetId": "subnet-c99aa7ad", 
+                        "ZoneName": "us-west-2a"
+                    }
+                ]
+            }, 
+            {
+                "VpcId": "vpc-4a9ff72e", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-2-1990881227.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-f9cc4d9f"
+                ], 
+                "LoadBalancerName": "alb-2", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 37, 
+                    "microsecond": 750000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 45
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-15452171", 
+                        "ZoneName": "us-west-2a"
+                    }, 
+                    {
+                        "SubnetId": "subnet-5fb47507", 
+                        "ZoneName": "us-west-2c"
+                    }
+                ]
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+                "vary": "Accept-Encoding", 
+                "content-length": "2484", 
+                "content-type": "text/xml", 
+                "date": "Thu, 29 Sep 2016 14:49:04 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeTags_1.json b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeTags_1.json
@@ -0,0 +1,35 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+                "date": "Thu, 29 Sep 2016 14:49:52 GMT", 
+                "content-length": "877", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TagDescriptions": [
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Tags": [
+                    {
+                        "Value": "VALUE2", 
+                        "Key": "KEY2"
+                    }, 
+                    {
+                        "Value": "VALUE1", 
+                        "Key": "KEY1"
+                    }
+                ]
+            }, 
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "Tags": []
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeTargetGroups_1.json b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeTargetGroups_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_delete/elasticloadbalancing.DescribeTargetGroups_1.json
@@ -0,0 +1,38 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "b5a109be-871d-11e6-adc8-49939253181f", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b5a109be-871d-11e6-adc8-49939253181f", 
+                "date": "Fri, 30 Sep 2016 14:53:54 GMT", 
+                "content-length": "1312", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TargetGroups": [
+            {
+                "HealthCheckPath": "/", 
+                "HealthCheckIntervalSeconds": 30, 
+                "VpcId": "vpc-47b2b823", 
+                "Protocol": "HTTP", 
+                "HealthCheckTimeoutSeconds": 5, 
+                "HealthCheckProtocol": "HTTP", 
+                "LoadBalancerArns": [
+                    "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177"
+                ], 
+                "UnhealthyThresholdCount": 2, 
+                "HealthyThresholdCount": 5, 
+                "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                "Matcher": {
+                    "HttpCode": "200"
+                }, 
+                "HealthCheckPort": "traffic-port", 
+                "Port": 80, 
+                "TargetGroupName": "target-group-1"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.AddTags_1.json b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.AddTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.AddTags_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "b1b78f0c-8663-11e6-84e2-b1164fcb826c", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b1b78f0c-8663-11e6-84e2-b1164fcb826c", 
+                "date": "Thu, 29 Sep 2016 16:42:21 GMT", 
+                "content-length": "229", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeListeners_1.json b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeListeners_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeListeners_1.json
@@ -0,0 +1,30 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Listeners": [
+            {
+                "Protocol": "HTTP", 
+                "DefaultActions": [
+                    {
+                        "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                        "Type": "forward"
+                    }
+                ], 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Port": 80, 
+                "ListenerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:listener/app/alb-1/1e0ac88a139a5177/c5a0110226720d39"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+                "date": "Fri, 30 Sep 2016 13:12:17 GMT", 
+                "content-length": "948", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeLoadBalancers_1.json b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeLoadBalancers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeLoadBalancers_1.json
@@ -0,0 +1,93 @@
+{
+    "status_code": 200, 
+    "data": {
+        "LoadBalancers": [
+            {
+                "VpcId": "vpc-47b2b823", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-1-2087922814.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-ce548cb7"
+                ], 
+                "LoadBalancerName": "alb-1", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 51, 
+                    "microsecond": 200000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 43
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-77b23a2f", 
+                        "ZoneName": "us-west-2c"
+                    }, 
+                    {
+                        "SubnetId": "subnet-bfd682c9", 
+                        "ZoneName": "us-west-2b"
+                    }, 
+                    {
+                        "SubnetId": "subnet-c99aa7ad", 
+                        "ZoneName": "us-west-2a"
+                    }
+                ]
+            }, 
+            {
+                "VpcId": "vpc-4a9ff72e", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-2-1990881227.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-f9cc4d9f"
+                ], 
+                "LoadBalancerName": "alb-2", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 37, 
+                    "microsecond": 750000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 45
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-15452171", 
+                        "ZoneName": "us-west-2a"
+                    }, 
+                    {
+                        "SubnetId": "subnet-5fb47507", 
+                        "ZoneName": "us-west-2c"
+                    }
+                ]
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+                "vary": "Accept-Encoding", 
+                "content-length": "2484", 
+                "content-type": "text/xml", 
+                "date": "Thu, 29 Sep 2016 14:49:04 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeTags_1.json b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeTags_1.json
@@ -0,0 +1,35 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+                "date": "Thu, 29 Sep 2016 14:49:52 GMT", 
+                "content-length": "877", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TagDescriptions": [
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Tags": [
+                    {
+                        "Value": "VALUE2", 
+                        "Key": "KEY2"
+                    }, 
+                    {
+                        "Value": "VALUE1", 
+                        "Key": "KEY1"
+                    }
+                ]
+            }, 
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "Tags": []
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeTargetGroups_1.json b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeTargetGroups_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_mark_for_delete/elasticloadbalancing.DescribeTargetGroups_1.json
@@ -0,0 +1,38 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "b5a109be-871d-11e6-adc8-49939253181f", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b5a109be-871d-11e6-adc8-49939253181f", 
+                "date": "Fri, 30 Sep 2016 14:53:54 GMT", 
+                "content-length": "1312", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TargetGroups": [
+            {
+                "HealthCheckPath": "/", 
+                "HealthCheckIntervalSeconds": 30, 
+                "VpcId": "vpc-47b2b823", 
+                "Protocol": "HTTP", 
+                "HealthCheckTimeoutSeconds": 5, 
+                "HealthCheckProtocol": "HTTP", 
+                "LoadBalancerArns": [
+                    "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177"
+                ], 
+                "UnhealthyThresholdCount": 2, 
+                "HealthyThresholdCount": 5, 
+                "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                "Matcher": {
+                    "HttpCode": "200"
+                }, 
+                "HealthCheckPort": "traffic-port", 
+                "Port": 80, 
+                "TargetGroupName": "target-group-1"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeListeners_1.json b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeListeners_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeListeners_1.json
@@ -0,0 +1,30 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Listeners": [
+            {
+                "Protocol": "HTTP", 
+                "DefaultActions": [
+                    {
+                        "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                        "Type": "forward"
+                    }
+                ], 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Port": 80, 
+                "ListenerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:listener/app/alb-1/1e0ac88a139a5177/c5a0110226720d39"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+                "date": "Fri, 30 Sep 2016 13:12:17 GMT", 
+                "content-length": "948", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeLoadBalancers_1.json b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeLoadBalancers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeLoadBalancers_1.json
@@ -0,0 +1,93 @@
+{
+    "status_code": 200, 
+    "data": {
+        "LoadBalancers": [
+            {
+                "VpcId": "vpc-47b2b823", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-1-2087922814.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-ce548cb7"
+                ], 
+                "LoadBalancerName": "alb-1", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 51, 
+                    "microsecond": 200000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 43
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-77b23a2f", 
+                        "ZoneName": "us-west-2c"
+                    }, 
+                    {
+                        "SubnetId": "subnet-bfd682c9", 
+                        "ZoneName": "us-west-2b"
+                    }, 
+                    {
+                        "SubnetId": "subnet-c99aa7ad", 
+                        "ZoneName": "us-west-2a"
+                    }
+                ]
+            }, 
+            {
+                "VpcId": "vpc-4a9ff72e", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-2-1990881227.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-f9cc4d9f"
+                ], 
+                "LoadBalancerName": "alb-2", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 37, 
+                    "microsecond": 750000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 45
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-15452171", 
+                        "ZoneName": "us-west-2a"
+                    }, 
+                    {
+                        "SubnetId": "subnet-5fb47507", 
+                        "ZoneName": "us-west-2c"
+                    }
+                ]
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+                "vary": "Accept-Encoding", 
+                "content-length": "2484", 
+                "content-type": "text/xml", 
+                "date": "Thu, 29 Sep 2016 14:49:04 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeTags_1.json b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeTags_1.json
@@ -0,0 +1,35 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+                "date": "Thu, 29 Sep 2016 14:49:52 GMT", 
+                "content-length": "877", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TagDescriptions": [
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Tags": [
+                    {
+                        "Value": "VALUE2", 
+                        "Key": "KEY2"
+                    }, 
+                    {
+                        "Value": "VALUE1", 
+                        "Key": "KEY1"
+                    }
+                ]
+            }, 
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "Tags": []
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeTargetGroups_1.json b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeTargetGroups_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.DescribeTargetGroups_1.json
@@ -0,0 +1,38 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "b5a109be-871d-11e6-adc8-49939253181f", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b5a109be-871d-11e6-adc8-49939253181f", 
+                "date": "Fri, 30 Sep 2016 14:53:54 GMT", 
+                "content-length": "1312", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TargetGroups": [
+            {
+                "HealthCheckPath": "/", 
+                "HealthCheckIntervalSeconds": 30, 
+                "VpcId": "vpc-47b2b823", 
+                "Protocol": "HTTP", 
+                "HealthCheckTimeoutSeconds": 5, 
+                "HealthCheckProtocol": "HTTP", 
+                "LoadBalancerArns": [
+                    "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177"
+                ], 
+                "UnhealthyThresholdCount": 2, 
+                "HealthyThresholdCount": 5, 
+                "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                "Matcher": {
+                    "HttpCode": "200"
+                }, 
+                "HealthCheckPort": "traffic-port", 
+                "Port": 80, 
+                "TargetGroupName": "target-group-1"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.RemoveTags_1.json b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.RemoveTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_remove_tag/elasticloadbalancing.RemoveTags_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "bb72b5e9-8663-11e6-9612-e99914e6ef67", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "bb72b5e9-8663-11e6-9612-e99914e6ef67", 
+                "date": "Thu, 29 Sep 2016 16:42:37 GMT", 
+                "content-length": "238", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeListeners_1.json b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeListeners_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeListeners_1.json
@@ -0,0 +1,30 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Listeners": [
+            {
+                "Protocol": "HTTP", 
+                "DefaultActions": [
+                    {
+                        "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                        "Type": "forward"
+                    }
+                ], 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Port": 80, 
+                "ListenerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:listener/app/alb-1/1e0ac88a139a5177/c5a0110226720d39"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "83b82008-870f-11e6-bba7-bdde0cb07e13", 
+                "date": "Fri, 30 Sep 2016 13:12:17 GMT", 
+                "content-length": "948", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeLoadBalancers_1.json b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeLoadBalancers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeLoadBalancers_1.json
@@ -0,0 +1,93 @@
+{
+    "status_code": 200, 
+    "data": {
+        "LoadBalancers": [
+            {
+                "VpcId": "vpc-47b2b823", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-1-2087922814.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-ce548cb7"
+                ], 
+                "LoadBalancerName": "alb-1", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 51, 
+                    "microsecond": 200000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 43
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-77b23a2f", 
+                        "ZoneName": "us-west-2c"
+                    }, 
+                    {
+                        "SubnetId": "subnet-bfd682c9", 
+                        "ZoneName": "us-west-2b"
+                    }, 
+                    {
+                        "SubnetId": "subnet-c99aa7ad", 
+                        "ZoneName": "us-west-2a"
+                    }
+                ]
+            }, 
+            {
+                "VpcId": "vpc-4a9ff72e", 
+                "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "State": {
+                    "Code": "active"
+                }, 
+                "DNSName": "internal-alb-2-1990881227.us-west-2.elb.amazonaws.com", 
+                "SecurityGroups": [
+                    "sg-f9cc4d9f"
+                ], 
+                "LoadBalancerName": "alb-2", 
+                "CreatedTime": {
+                    "hour": 14, 
+                    "__class__": "datetime", 
+                    "month": 9, 
+                    "second": 37, 
+                    "microsecond": 750000, 
+                    "year": 2016, 
+                    "day": 29, 
+                    "minute": 45
+                }, 
+                "Scheme": "internal", 
+                "Type": "application", 
+                "CanonicalHostedZoneId": "Z1H1FL5HABSF5", 
+                "AvailabilityZones": [
+                    {
+                        "SubnetId": "subnet-15452171", 
+                        "ZoneName": "us-west-2a"
+                    }, 
+                    {
+                        "SubnetId": "subnet-5fb47507", 
+                        "ZoneName": "us-west-2c"
+                    }
+                ]
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "de4574a5-8653-11e6-b76c-c5e64157571b", 
+                "vary": "Accept-Encoding", 
+                "content-length": "2484", 
+                "content-type": "text/xml", 
+                "date": "Thu, 29 Sep 2016 14:49:04 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeTags_1.json b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeTags_1.json
@@ -0,0 +1,35 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "fae221d1-8653-11e6-9612-e99914e6ef67", 
+                "date": "Thu, 29 Sep 2016 14:49:52 GMT", 
+                "content-length": "877", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TagDescriptions": [
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177", 
+                "Tags": [
+                    {
+                        "Value": "VALUE2", 
+                        "Key": "KEY2"
+                    }, 
+                    {
+                        "Value": "VALUE1", 
+                        "Key": "KEY1"
+                    }
+                ]
+            }, 
+            {
+                "ResourceArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-2/4dec284f036410cb", 
+                "Tags": []
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeTargetGroups_1.json b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeTargetGroups_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_appelb_simple/elasticloadbalancing.DescribeTargetGroups_1.json
@@ -0,0 +1,38 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "b5a109be-871d-11e6-adc8-49939253181f", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "b5a109be-871d-11e6-adc8-49939253181f", 
+                "date": "Fri, 30 Sep 2016 14:53:54 GMT", 
+                "content-length": "1312", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TargetGroups": [
+            {
+                "HealthCheckPath": "/", 
+                "HealthCheckIntervalSeconds": 30, 
+                "VpcId": "vpc-47b2b823", 
+                "Protocol": "HTTP", 
+                "HealthCheckTimeoutSeconds": 5, 
+                "HealthCheckProtocol": "HTTP", 
+                "LoadBalancerArns": [
+                    "arn:aws:elasticloadbalancing:us-west-2:644160558196:loadbalancer/app/alb-1/1e0ac88a139a5177"
+                ], 
+                "UnhealthyThresholdCount": 2, 
+                "HealthyThresholdCount": 5, 
+                "TargetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:644160558196:targetgroup/target-group-1/f9e509bf6804a101", 
+                "Matcher": {
+                    "HttpCode": "200"
+                }, 
+                "HealthCheckPort": "traffic-port", 
+                "Port": 80, 
+                "TargetGroupName": "target-group-1"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/test_appelb.py b/tests/test_appelb.py
new file mode 100644
--- /dev/null
+++ b/tests/test_appelb.py
@@ -0,0 +1,151 @@
+# Copyright 2016 Capital One Services, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from .common import BaseTest
+from c7n.executor import MainThreadExecutor
+from c7n.resources.appelb import AppELB
+
+class AppELBTest(BaseTest):
+
+    def test_appelb_simple(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_simple')
+        p = self.load_policy({
+            'name': 'appelb-simple',
+            'resource': 'app-elb'},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 2)
+
+    def test_appelb_simple_filter(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_simple')
+        p = self.load_policy({
+            'name': 'appelb-simple-filter',
+            'resource': 'app-elb',
+            'filters': [
+                {'type': 'value',
+                 'key': 'LoadBalancerName',
+                 'value': 'alb-1'}]},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+    def test_appelb_tags_filter(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_simple')
+        p = self.load_policy({
+            'name': 'appelb-tags-filter',
+            'resource': 'app-elb',
+            'filters': [{"tag:KEY1": "VALUE1"}]},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+        p = self.load_policy({
+            'name': 'appelb-tags-filter',
+            'resource': 'app-elb',
+            'filters': [{"tag:KEY1": "VALUE2"}]},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 0)
+
+    def test_appelb_is_ssl_filter(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_simple')
+        p = self.load_policy({
+            'name': 'appelb-is-ssl-filter',
+            'resource': 'app-elb',
+            'filters': ['is-ssl']},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 0)
+
+    def test_appelb_default_vpc_filter(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_simple')
+        p = self.load_policy({
+            'name': 'appelb-default-vpc-filter',
+            'resource': 'app-elb',
+            'filters': ['default-vpc']},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 0)
+
+    def test_appelb_add_tag(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_add_tag')
+        p = self.load_policy({
+            'name': 'appelb-add-tag',
+            'resource': 'app-elb',
+            'filters': [
+                {'type': 'value',
+                 'key': 'LoadBalancerName',
+                 'value': 'alb-1'}],
+            'actions': [
+                {'type': 'tag', 'key': 'KEY42', 'value': 'VALUE99'}]
+            },
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+    def test_appelb_remove_tag(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_remove_tag')
+        p = self.load_policy({
+            'name': 'appelb-remove-tag',
+            'resource': 'app-elb',
+            'filters': [
+                {'type': 'value',
+                 'key': 'LoadBalancerName',
+                 'value': 'alb-1'}],
+            'actions': [
+                {'type': 'remove-tag', 'tags': ['KEY42']}]
+            },
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+    def test_appelb_mark_for_delete(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_mark_for_delete')
+        p = self.load_policy({
+            'name': 'appelb-mark-for-delete',
+            'resource': 'app-elb',
+            'filters': [
+                {'type': 'value',
+                 'key': 'LoadBalancerName',
+                 'value': 'alb-1'}],
+            'actions': [
+                {'type': 'mark-for-op', 'op': 'delete',
+                 'tag': 'custodian_next', 'days': 1}]
+            },
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+    def test_appelb_delete(self):
+        self.patch(AppELB, 'executor_factory', MainThreadExecutor)
+        session_factory = self.replay_flight_data('test_appelb_delete')
+        p = self.load_policy({
+            'name': 'appelb-delete',
+            'resource': 'app-elb',
+            'filters': [
+                {'type': 'value',
+                 'key': 'LoadBalancerName',
+                 'value': 'alb-2'}],
+            'actions': [
+                {'type': 'delete'}]},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)