Repository: aws-cloudformation/cfn-lint

Issue:
E2504 incorrectly rejects "Iops" property for io2/gp3 volumes
*cfn-lint version: (`cfn-lint --version`)*

cfn-lint 0.44.6

*Description of issue.*

cfn-lint produces an error "E2504: Iops shouldn't be defined for type io2 for Resource ... LaunchConfiguration/Properties/BlockDeviceMappings/0/Ebs/Iops" when setting Iops on a io2 EBS volume.

The Iops property is required for io2 and optional for gp3. [1]

Cfn-lint treats the Iops property as required for io1 and forbidden for all other volume types, which is very much not correct 

[1] https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-blockdev-template.html#cfn-ec2-blockdev-template-iops




Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/ectwo/Ebs.py b/src/cfnlint/rules/resources/ectwo/Ebs.py
--- a/src/cfnlint/rules/resources/ectwo/Ebs.py
+++ b/src/cfnlint/rules/resources/ectwo/Ebs.py
@@ -9,10 +9,10 @@
 
 
 class Ebs(CloudFormationLintRule):
-    """Check if Ec2 Ebs Resource Properties"""
+    """Check Ec2 Ebs Resource Properties"""
     id = 'E2504'
     shortdesc = 'Check Ec2 Ebs Properties'
-    description = 'See if Ec2 Eb2 Properties are valid'
+    description = 'See if Ec2 Ebs Properties are valid'
     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-blockdev-template.html'
     tags = ['properties', 'ec2', 'ebs']
 
@@ -26,13 +26,15 @@ def _checkEbs(self, cfn, ebs, path):
                 for volume_type_obj in volume_types_obj:
                     volume_type = volume_type_obj.get('Value')
                     if isinstance(volume_type, six.string_types):
-                        if volume_type == 'io1':
+                        if volume_type in ('io1', 'io2'):
                             if iops_obj is None:
                                 pathmessage = path[:] + ['VolumeType']
-                                message = 'VolumeType io1 requires Iops to be specified for {0}'
+                                message = 'VolumeType {0} requires Iops to be specified for {1}'
                                 matches.append(
-                                    RuleMatch(pathmessage, message.format('/'.join(map(str, pathmessage)))))
-                        elif volume_type:
+                                    RuleMatch(
+                                        pathmessage,
+                                        message.format(volume_type, '/'.join(map(str, pathmessage)))))
+                        elif volume_type in ('gp2', 'st1', 'sc1', 'standard'):
                             if iops_obj is not None:
                                 pathmessage = path[:] + ['Iops']
                                 message = 'Iops shouldn\'t be defined for type {0} for {1}'


Test patch:
diff --git a/test/fixtures/templates/good/resources/ec2/ebs.yaml b/test/fixtures/templates/good/resources/ec2/ebs.yaml
--- a/test/fixtures/templates/good/resources/ec2/ebs.yaml
+++ b/test/fixtures/templates/good/resources/ec2/ebs.yaml
@@ -46,6 +46,12 @@ Resources:
             DeleteOnTermination: false
             VolumeSize: 20
         - DeviceName: /dev/sdn
+          Ebs:
+            VolumeType: io2
+            Iops: !Ref myIops
+            DeleteOnTermination: false
+            VolumeSize: 20
+        - DeviceName: /dev/sdo
           Ebs:
             VolumeType: standard
             DeleteOnTermination: false
diff --git a/test/unit/rules/resources/ec2/test_ec2_ebs.py b/test/unit/rules/resources/ec2/test_ec2_ebs.py
--- a/test/unit/rules/resources/ec2/test_ec2_ebs.py
+++ b/test/unit/rules/resources/ec2/test_ec2_ebs.py
@@ -23,4 +23,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/resources/ec2/ebs.yaml', 5)
+        self.helper_file_negative('test/fixtures/templates/bad/resources/ec2/ebs.yaml', 4)