Repository: aws-cloudformation/cfn-lint

Issue:
New rule to validate lambda SnapStart configuration
### Is this feature request related to a new rule or cfn-lint capabilities?

rules

### Describe the feature you'd like to request

When configuring SnapStart you also need to configure a version.

A warning level rule will validate that when SnapStart is configured that there is associated `AWS::Lambda::Version` resource.

```
Resources:
    Function:
        Type: AWS::Lambda::Function
        Properties:
            Code:
                S3Key: Test
                S3Bucket: Test
            Role: !Ref Role
            Handler: index.main
            Runtime: java17
            SnapStart:
                ApplyOn: PublishedVersions
    Version:
        Type: AWS::Lambda::Version
        Properties:
            FunctionName: !Ref Function
```

### Describe the solution you'd like

When configuring SnapStart you also need to configure a version.

A warning level rule will validate that when SnapStart is configured that there is associated `AWS::Lambda::Version` resource.

### Additional context

https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html

### Is this something that you'd be interested in working on?

- [x] 👋 I may be able to implement this feature request

### Would this feature include a breaking change?

- [x] ⚠️ This feature might incur a breaking change


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/lmbd/SnapStart.py b/src/cfnlint/rules/resources/lmbd/SnapStart.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/lmbd/SnapStart.py
@@ -0,0 +1,77 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from cfnlint.rules import CloudFormationLintRule, RuleMatch
+
+
+class SnapStart(CloudFormationLintRule):
+    """Check if Lambda SnapStart is properly configured"""
+
+    id = "W2530"
+    shortdesc = "Validate that SnapStart is properly configured"
+    description = (
+        "To properly leverage SnapStart, you must configure both the lambda function "
+        "and attach a Lambda version resource"
+    )
+    source_url = "https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html"
+    tags = ["resources", "lambda"]
+
+    def __init__(self):
+        super().__init__()
+        self.resource_property_types.append("AWS::Lambda::Function")
+
+    def _check_value(self, value, path, **kwargs):
+        lambda_versions = kwargs["lambda_versions"]
+        cfn = kwargs["cfn"]
+
+        if value != "PublishedVersions":
+            return []
+
+        # SnapStart is enabled, validate if version is attached
+        matches = [
+            v
+            for v in lambda_versions
+            if any(edge == path[1] for edge in cfn.graph.graph.neighbors(v))
+        ]
+
+        if len(matches) < 1:
+            return [
+                RuleMatch(
+                    path,
+                    "SnapStart is enabled but Lambda version is not attached",
+                )
+            ]
+
+        return []
+
+    def match_resource_properties(self, properties, _, path, cfn):
+        """Check CloudFormation Properties"""
+        matches = []
+
+        # if there is no graph we can't validate
+        if not cfn.graph:
+            return matches
+
+        lambda_versions = cfn.get_resources(["AWS::Lambda::Version"])
+
+        for scenario in cfn.get_object_without_conditions(properties, ["SnapStart"]):
+            props = scenario.get("Object")
+
+            snap_start = props.get("SnapStart")
+            if not snap_start:
+                continue
+
+            matches.extend(
+                cfn.check_value(
+                    snap_start,
+                    "ApplyOn",
+                    path,
+                    check_value=self._check_value,
+                    lambda_versions=lambda_versions,
+                    cfn=cfn,
+                )
+            )
+
+        return matches
diff --git a/src/cfnlint/rules/resources/lmbd/SnapStartEnabled.py b/src/cfnlint/rules/resources/lmbd/SnapStartEnabled.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/lmbd/SnapStartEnabled.py
@@ -0,0 +1,38 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from cfnlint.rules import CloudFormationLintRule, RuleMatch
+
+
+class SnapStartEnabled(CloudFormationLintRule):
+    """Check if the SnapStart is enabled for certain java runtimes"""
+
+    id = "I2530"
+    shortdesc = "Validate that SnapStart is configured for >= Java11 runtimes"
+    description = (
+        "SnapStart is a no-cost feature that can increase performance up to 10x. "
+        "Enable SnapStart for Java 11 and greater runtimes"
+    )
+    source_url = "https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html"
+    tags = ["resources", "lambda"]
+
+    def __init__(self):
+        super().__init__()
+        self.resource_property_types.append("AWS::Lambda::Function")
+
+    def validate(self, runtime, path):
+        if not runtime:
+            return []
+
+        if not (runtime.startswith("java")) and runtime not in ["java8.al2", "java8"]:
+            return []
+
+        return [
+            RuleMatch(
+                path,
+                f"When using {runtime} configure SnapStart",
+                rule=self,
+            )
+        ]
diff --git a/src/cfnlint/rules/resources/lmbd/SnapStartSupported.py b/src/cfnlint/rules/resources/lmbd/SnapStartSupported.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/lmbd/SnapStartSupported.py
@@ -0,0 +1,54 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from cfnlint.rules import CloudFormationLintRule, RuleMatch
+
+
+class SnapStartSupported(CloudFormationLintRule):
+    """Check if Lambda function using SnapStart has the correct runtimes"""
+
+    id = "E2530"
+    shortdesc = "SnapStart supports the configured runtime"
+    description = (
+        "To properly leverage SnapStart, you must have a runtime of Java11 or greater"
+    )
+    source_url = "https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html"
+    tags = ["resources", "lambda"]
+
+    def __init__(self):
+        super().__init__()
+        self.resource_property_types.append("AWS::Lambda::Function")
+        self.child_rules = {"I2530": None}
+
+    def match_resource_properties(self, properties, _, path, cfn):
+        """Check CloudFormation Properties"""
+        matches = []
+
+        for scenario in cfn.get_object_without_nested_conditions(properties, path):
+            props = scenario.get("Object")
+
+            runtime = props.get("Runtime")
+            snap_start = props.get("SnapStart")
+            if not snap_start:
+                if self.child_rules["I2530"]:
+                    matches.extend(self.child_rules["I2530"].validate(runtime, path))
+                continue
+
+            if snap_start.get("ApplyOn") != "PublishedVersions":
+                continue
+
+            if (
+                runtime
+                and (not runtime.startswith("java"))
+                and runtime not in ["java8.al2", "java8"]
+            ):
+                matches.append(
+                    RuleMatch(
+                        path + ["SnapStart", "ApplyOn"],
+                        f"{runtime} is not supported for SnapStart enabled functions",
+                    )
+                )
+
+        return matches


Test patch:
diff --git a/test/fixtures/templates/bad/resources/lambda/snapstart-enabled.yaml b/test/fixtures/templates/bad/resources/lambda/snapstart-enabled.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/lambda/snapstart-enabled.yaml
@@ -0,0 +1,24 @@
+Resources:
+  LambdaRole:
+    Type: AWS::IAM::Role
+    Properties:
+      RoleName: LambdaRole
+      AssumeRolePolicyDocument:
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+            Action: sts:AssumeRole
+
+  FunctionJava17:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      TracingConfig:
+        Mode: Active
diff --git a/test/fixtures/templates/bad/resources/lambda/snapstart-supported.yaml b/test/fixtures/templates/bad/resources/lambda/snapstart-supported.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/lambda/snapstart-supported.yaml
@@ -0,0 +1,26 @@
+Resources:
+  LambdaRole:
+    Type: AWS::IAM::Role
+    Properties:
+      RoleName: LambdaRole
+      AssumeRolePolicyDocument:
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+            Action: sts:AssumeRole
+
+  FunctionJava17:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: python3.10
+      SnapStart:
+        ApplyOn: PublishedVersions
+      TracingConfig:
+        Mode: Active
diff --git a/test/fixtures/templates/bad/resources/lambda/snapstart.yaml b/test/fixtures/templates/bad/resources/lambda/snapstart.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/lambda/snapstart.yaml
@@ -0,0 +1,41 @@
+Resources:
+  LambdaRole:
+    Type: AWS::IAM::Role
+    Properties:
+      RoleName: LambdaRole
+      AssumeRolePolicyDocument:
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+            Action: sts:AssumeRole
+
+  FunctionJava17:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      SnapStart:
+        ApplyOn: PublishedVersions
+      TracingConfig:
+        Mode: Active
+
+  FunctionJava11:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      SnapStart:
+        ApplyOn: PublishedVersions
+      TracingConfig:
+        Mode: Active
diff --git a/test/fixtures/templates/good/resources/lambda/snapstart-enabled.yaml b/test/fixtures/templates/good/resources/lambda/snapstart-enabled.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/lambda/snapstart-enabled.yaml
@@ -0,0 +1,41 @@
+Resources:
+  LambdaRole:
+    Type: AWS::IAM::Role
+    Properties:
+      RoleName: LambdaRole
+      AssumeRolePolicyDocument:
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+            Action: sts:AssumeRole
+
+  FunctionJava17:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      SnapStart:
+        ApplyOn: PublishedVersions
+      TracingConfig:
+        Mode: Active
+
+  FunctionJava11:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java11
+      SnapStart:
+        ApplyOn: None
+      TracingConfig:
+        Mode: Active
diff --git a/test/fixtures/templates/good/resources/lambda/snapstart-supported.yaml b/test/fixtures/templates/good/resources/lambda/snapstart-supported.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/lambda/snapstart-supported.yaml
@@ -0,0 +1,26 @@
+Resources:
+  LambdaRole:
+    Type: AWS::IAM::Role
+    Properties:
+      RoleName: LambdaRole
+      AssumeRolePolicyDocument:
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+            Action: sts:AssumeRole
+
+  FunctionJava17:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      SnapStart:
+        ApplyOn: PublishedVersions
+      TracingConfig:
+        Mode: Active
\ No newline at end of file
diff --git a/test/fixtures/templates/good/resources/lambda/snapstart.yaml b/test/fixtures/templates/good/resources/lambda/snapstart.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/lambda/snapstart.yaml
@@ -0,0 +1,97 @@
+Conditions:
+  IsUsEast1: !Equals [!Ref AWS::Region, "us-east-1"]
+
+Resources:
+  LambdaRole:
+    Type: AWS::IAM::Role
+    Properties:
+      RoleName: LambdaRole
+      AssumeRolePolicyDocument:
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+            Action: sts:AssumeRole
+
+  FunctionJava17:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      SnapStart:
+        ApplyOn: !If [IsUsEast1, PublishedVersions, None]
+
+      TracingConfig:
+        Mode: Active
+
+  VersionJava17:
+    Type: AWS::Lambda::Version
+    Properties:
+      FunctionName: !Ref FunctionJava17
+      Description: v1
+      ProvisionedConcurrencyConfig:
+        ProvisionedConcurrentExecutions: 20
+
+  FunctionJava11:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      SnapStart:
+        ApplyOn: PublishedVersions
+      TracingConfig:
+        Mode: Active
+
+  VersionJava11:
+    Type: AWS::Lambda::Version
+    Properties:
+      FunctionName: !Ref FunctionJava11
+      Description: v1
+      ProvisionedConcurrencyConfig:
+        ProvisionedConcurrentExecutions: 20
+
+  FunctionPython:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: python3.10
+      TracingConfig:
+        Mode: Active
+
+  FunctionJava17NoSnapStart:
+    Type: AWS::Lambda::Function
+    Properties:
+      Handler: index.handler
+      Role: !GetAtt LambdaRole.Arn
+      ReservedConcurrentExecutions: 20
+      Code:
+        S3Bucket: my-bucket
+        S3Key: function.zip
+      Runtime: java17
+      SnapStart:
+        ApplyOn: None
+      TracingConfig:
+        Mode: Active
+
+  VersionJava17NoSnapStart:
+    Type: AWS::Lambda::Version
+    Properties:
+      FunctionName: !Ref FunctionJava17NoSnapStart
+      Description: v1
+      ProvisionedConcurrencyConfig:
+        ProvisionedConcurrentExecutions: 20
diff --git a/test/unit/rules/resources/lmbd/test_snapstart.py b/test/unit/rules/resources/lmbd/test_snapstart.py
new file mode 100644
--- /dev/null
+++ b/test/unit/rules/resources/lmbd/test_snapstart.py
@@ -0,0 +1,29 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+from test.unit.rules import BaseRuleTestCase
+
+from cfnlint.rules.resources.lmbd.SnapStart import SnapStart
+
+
+class TestSnapStart(BaseRuleTestCase):
+    """Test Lambda SnapStarts"""
+
+    def setUp(self):
+        """Setup"""
+        super(TestSnapStart, self).setUp()
+        self.collection.register(SnapStart())
+        self.success_templates = [
+            "test/fixtures/templates/good/resources/lambda/snapstart.yaml"
+        ]
+
+    def test_file_positive(self):
+        """Test Positive"""
+        self.helper_file_positive()
+
+    def test_file_negative(self):
+        """Test failure"""
+        self.helper_file_negative(
+            "test/fixtures/templates/bad/resources/lambda/snapstart.yaml", 2
+        )
diff --git a/test/unit/rules/resources/lmbd/test_snapstart_enabled.py b/test/unit/rules/resources/lmbd/test_snapstart_enabled.py
new file mode 100644
--- /dev/null
+++ b/test/unit/rules/resources/lmbd/test_snapstart_enabled.py
@@ -0,0 +1,31 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+from test.unit.rules import BaseRuleTestCase
+
+from cfnlint.rules.resources.lmbd.SnapStartEnabled import SnapStartEnabled
+from cfnlint.rules.resources.lmbd.SnapStartSupported import SnapStartSupported
+
+
+class TestSnapStartEnabled(BaseRuleTestCase):
+    """Test Lambda SnapStart enabled"""
+
+    def setUp(self):
+        """Setup"""
+        super(TestSnapStartEnabled, self).setUp()
+        self.collection.register(SnapStartEnabled())
+        self.collection.register(SnapStartSupported())
+        self.success_templates = [
+            "test/fixtures/templates/good/resources/lambda/snapstart-enabled.yaml"
+        ]
+
+    def test_file_positive(self):
+        """Test Positive"""
+        self.helper_file_positive()
+
+    def test_file_negative(self):
+        """Test failure"""
+        self.helper_file_negative(
+            "test/fixtures/templates/bad/resources/lambda/snapstart-enabled.yaml", 1
+        )
diff --git a/test/unit/rules/resources/lmbd/test_snapstart_supported.py b/test/unit/rules/resources/lmbd/test_snapstart_supported.py
new file mode 100644
--- /dev/null
+++ b/test/unit/rules/resources/lmbd/test_snapstart_supported.py
@@ -0,0 +1,29 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+from test.unit.rules import BaseRuleTestCase
+
+from cfnlint.rules.resources.lmbd.SnapStartSupported import SnapStartSupported
+
+
+class TestSnapStartSupported(BaseRuleTestCase):
+    """Test Lambda SnapStart supported"""
+
+    def setUp(self):
+        """Setup"""
+        super(TestSnapStartSupported, self).setUp()
+        self.collection.register(SnapStartSupported())
+        self.success_templates = [
+            "test/fixtures/templates/good/resources/lambda/snapstart-supported.yaml"
+        ]
+
+    def test_file_positive(self):
+        """Test Positive"""
+        self.helper_file_positive()
+
+    def test_file_negative(self):
+        """Test failure"""
+        self.helper_file_negative(
+            "test/fixtures/templates/bad/resources/lambda/snapstart-supported.yaml", 1
+        )