Repository: aws-cloudformation/cfn-lint

Issue:
Single-subnet ELB detection
*cfn-lint version: 0.10.3*

*Description of issue.*

When configuring `AWS::ElasticLoadBalancingV2::LoadBalancer`, if you specify only 1 subnet, you get back from AWS:

> 2019-02-05 12:22:39 +1100 Elb AWS::ElasticLoadBalancingV2::LoadBalancer CREATE_FAILED At least two subnets in two different Availability Zones must be specified (Service: AmazonElasticLoadBalancingV2; Status Code: 400; Error Code: ValidationError; Request ID: ...)

This could be covered by a cfn-lint check.


Discussion:
Solid rule idea. I'll label this as a new rule until someone picks it up.

Thanks!
I think this becomes a definition in the spec once #759 is merged

Resolving patch:
diff --git a/src/cfnlint/rules/resources/elb/Elb.py b/src/cfnlint/rules/resources/elb/Elb.py
--- a/src/cfnlint/rules/resources/elb/Elb.py
+++ b/src/cfnlint/rules/resources/elb/Elb.py
@@ -44,6 +44,24 @@ def check_protocol_value(self, value, path, **kwargs):
 
         return matches
 
+    def check_alb_subnets(self, props, path):
+        """ Validate at least two subnets with ALBs"""
+        matches = []
+        elb_type = props.get('Type')
+        if elb_type == 'application':
+            subnets = props.get('Subnets')
+            if isinstance(subnets, list):
+                if len(subnets) < 2:
+                    path = path + ['Subnets']
+                    matches.append(RuleMatch(path, 'You must specify at least two Subnets for load balancers with type "application"'))
+            subnet_mappings = props.get('SubnetMappings')
+            if isinstance(subnet_mappings, list):
+                if len(subnet_mappings) < 2:
+                    path = path + ['SubnetMappings']
+                    matches.append(RuleMatch(path, 'You must specify at least two SubnetMappings for load balancers with type "application"'))
+
+        return matches
+
     def match(self, cfn):
         """Check ELB Resource Parameters"""
 
@@ -74,9 +92,12 @@ def match(self, cfn):
         results = cfn.get_resource_properties(['AWS::ElasticLoadBalancingV2::LoadBalancer'])
         for result in results:
             properties = result['Value']
-            if 'Type' in properties and properties['Type'] == 'network':
+            elb_type = properties.get('Type')
+            if elb_type == 'network':
                 if 'SecurityGroups' in properties:
                     path = result['Path'] + ['SecurityGroups']
                     matches.append(RuleMatch(path, 'Security groups are not supported for load balancers with type "network"'))
 
+            matches.extend(self.check_alb_subnets(properties, result['Path']))
+
         return matches


Test patch:
diff --git a/test/rules/resources/elb/test_elb.py b/test/rules/resources/elb/test_elb.py
--- a/test/rules/resources/elb/test_elb.py
+++ b/test/rules/resources/elb/test_elb.py
@@ -35,3 +35,66 @@ def test_file_positive(self):
     def test_file_negative(self):
         """Test failure"""
         self.helper_file_negative('test/fixtures/templates/bad/properties_elb.yaml', 6)
+
+    def test_alb_subnets(self):
+        """ Test ALB Subnet Logic"""
+        rule = Elb()
+
+        # Failure when 1 subnet defined
+        props = {
+            "Type": "application",
+            "Subnets": [
+                "subnet-123456"
+            ]
+        }
+
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        self.assertEqual(len(matches), 1)
+
+        # No Failure when 2 subnets defined
+        props = {
+            "Type": "application",
+            "Subnets": [
+                "subnet-123456",
+                "subnet-abcdef"
+            ]
+        }
+
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        self.assertEqual(len(matches), 0)
+
+        # Failure when 1 SubnetMapping defined
+        props = {
+            "Type": "application",
+            "SubnetMappings": [
+                {
+                    "SubnetId": "subnet-123456"
+                }
+            ]
+        }
+
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        self.assertEqual(len(matches), 1)
+
+        # No Failure when 2 SubnetMapping defined
+        props = {
+            "Type": "application",
+            "SubnetMappings": [
+                {"SubnetId": "subnet-123456"},
+                {"SubnetId": "subnet-abcdef"}
+            ]
+        }
+
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        self.assertEqual(len(matches), 0)
+
+        # No Failure when 1 Subnet and NLB
+        props = {
+            "Type": "network",
+            "Subnets": [
+                "subnet-123456"
+            ]
+        }
+
+        matches = rule.check_alb_subnets(props, ['Resources', 'NLB', 'Properties'])
+        self.assertEqual(len(matches), 0)