Repository: aws-cloudformation/cfn-lint (language: python)

Issue:
Feature Request: AWS::ECS::Service - Add check for `HealthCheckGracePeriodSeconds needs ALB/NLB`
### Is this feature request related to a new rule or cfn-lint capabilities?

_No response_

### Describe the feature you'd like to request

Currently cfn-lint is unable to see that if you add

```
HealthCheckGracePeriodSeconds
The period of time, in seconds, that the Amazon ECS service scheduler ignores unhealthy Elastic Load Balancing target health checks after a task has first started.
This is only used when your service is configured to use a load balancer.
If your service has a load balancer defined and you don't specify a health check grace period value, the default value of 0 is used.

If you do not use an Elastic Load Balancing, we recommend that you use the startPeriod in the task definition health check parameters.
For more information, see Health check.

If your service's tasks take a while to start and respond to Elastic Load Balancing health checks, you can specify a health check grace period of up to 2,147,483,647 seconds (about 69 years).
During that time, the Amazon ECS service scheduler ignores health check status.
This grace period can prevent the service scheduler from marking tasks as unhealthy and stopping them before they have time to come up.

Required: No

Type: Integer

Update requires: No interruption
```

to `AWS::ECS::Service`, it doesn't know that you need an ALB/NLB attached or the deployment will fail with the following error message:

```
Resource handler returned message:
"Invalid request provided: UpdateService error: Health check grace period is only valid for services configured to use load balancers
(Service: AmazonECS; Status Code: 400; Error Code: InvalidParameterException;
```

### Describe the solution you'd like

cfn-lint finds this error.

### Additional context

Example:

```yaml
  ECSService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref ECSCluster
      TaskDefinition: !GetAtt ECSTaskDefinition.TaskDefinitionArn
      DesiredCount: 1
      LaunchType: FARGATE
      PropagateTags: SERVICE
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !GetAtt ECSSecurityGroup.GroupId
          Subnets:
            - <subnet>
      EnableExecuteCommand: true
      HealthCheckGracePeriodSeconds: 36000
```

### Is this something that you'd be interested in working on?

- [ ] 👋 I may be able to implement this feature request

### Would this feature include a breaking change?

- [ ] ⚠️ This feature might incur a breaking change


Discussion / hints:

Fix patch:
diff --git a/src/cfnlint/data/schemas/extensions/aws_ecs_service/healthcheckgraceperiodseconds.json b/src/cfnlint/data/schemas/extensions/aws_ecs_service/healthcheckgraceperiodseconds.json
new file mode 100644
index 0000000000..666f84b3e0
--- /dev/null
+++ b/src/cfnlint/data/schemas/extensions/aws_ecs_service/healthcheckgraceperiodseconds.json
@@ -0,0 +1,35 @@
+{
+ "if": {
+  "properties": {
+   "HealthCheckGracePeriodSeconds": {
+    "type": [
+     "string",
+     "integer"
+    ]
+   }
+  },
+  "required": [
+   "HealthCheckGracePeriodSeconds"
+  ],
+  "type": "object"
+ },
+ "then": {
+  "if": {
+   "properties": {
+    "LoadBalancers": {
+     "type": "array"
+    }
+   }
+  },
+  "required": [
+   "LoadBalancers"
+  ],
+  "then": {
+   "properties": {
+    "LoadBalancers": {
+     "minItems": 1
+    }
+   }
+  }
+ }
+}
diff --git a/src/cfnlint/rules/resources/ecs/ServiceHealthCheckGracePeriodSeconds.py b/src/cfnlint/rules/resources/ecs/ServiceHealthCheckGracePeriodSeconds.py
new file mode 100644
index 0000000000..6510a9c846
--- /dev/null
+++ b/src/cfnlint/rules/resources/ecs/ServiceHealthCheckGracePeriodSeconds.py
@@ -0,0 +1,58 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+import cfnlint.data.schemas.extensions.aws_ecs_service
+from cfnlint.jsonschema import ValidationResult
+from cfnlint.jsonschema.protocols import Validator
+from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails
+
+
+class ServiceHealthCheckGracePeriodSeconds(CfnLintJsonSchema):
+    id = "E3056"
+    shortdesc = (
+        "ECS service using HealthCheckGracePeriodSeconds "
+        "must also have LoadBalancers specified"
+    )
+    description = (
+        "When using a HealthCheckGracePeriodSeconds on an ECS "
+        "service, the service must also have a LoadBalancers specified "
+        "with at least one LoadBalancer in the array."
+    )
+    source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html#cfn-ecs-service-healthcheckgraceperiodseconds"
+    tags = ["properties", "ecs", "service", "container"]
+
+    def __init__(self) -> None:
+        super().__init__(
+            keywords=["Resources/AWS::ECS::Service/Properties"],
+            schema_details=SchemaDetails(
+                module=cfnlint.data.schemas.extensions.aws_ecs_service,
+                filename="healthcheckgraceperiodseconds.json",
+            ),
+            all_matches=True,
+        )
+
+    def validate(
+        self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
+    ) -> ValidationResult:
+
+        cfn_validator = self.extend_validator(
+            validator=validator.evolve(
+                function_filter=validator.function_filter.evolve(
+                    validate_dynamic_references=False,
+                    add_cfn_lint_keyword=False,
+                )
+            ),
+            schema=self._schema,
+            context=validator.context.evolve(
+                functions=["Fn::If", "Ref"],
+                strict_types=True,
+            ),
+        )
+
+        yield from self._iter_errors(cfn_validator, instance)


Test patch:
diff --git a/test/unit/rules/resources/ecs/test_service_health_check_grace_period_seconds.py b/test/unit/rules/resources/ecs/test_service_health_check_grace_period_seconds.py
new file mode 100644
index 0000000000..dcabd42182
--- /dev/null
+++ b/test/unit/rules/resources/ecs/test_service_health_check_grace_period_seconds.py
@@ -0,0 +1,115 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from collections import deque
+
+import pytest
+
+from cfnlint.jsonschema import ValidationError
+from cfnlint.rules.resources.ecs.ServiceHealthCheckGracePeriodSeconds import (
+    ServiceHealthCheckGracePeriodSeconds,
+)
+
+
+@pytest.fixture(scope="module")
+def rule():
+    rule = ServiceHealthCheckGracePeriodSeconds()
+    yield rule
+
+
+@pytest.fixture
+def template():
+    return {
+        "Conditions": {
+            "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]}
+        },
+    }
+
+
+@pytest.mark.parametrize(
+    "instance,expected",
+    [
+        (
+            {"HealthCheckGracePeriodSeconds": "Foo", "LoadBalancers": ["Bar"]},
+            [],
+        ),
+        (
+            {"LoadBalancers": []},
+            [],
+        ),
+        (
+            [],  # wrong type
+            [],
+        ),
+        (
+            {"HealthCheckGracePeriodSeconds": "Foo", "LoadBalancers": []},
+            [
+                ValidationError(
+                    "[] is too short (1)",
+                    rule=ServiceHealthCheckGracePeriodSeconds(),
+                    path=deque(["LoadBalancers"]),
+                    validator="minItems",
+                    schema_path=deque(
+                        ["then", "then", "properties", "LoadBalancers", "minItems"]
+                    ),
+                )
+            ],
+        ),
+        (
+            {
+                "HealthCheckGracePeriodSeconds": "Foo",
+            },
+            [
+                ValidationError(
+                    "'LoadBalancers' is a required property",
+                    rule=ServiceHealthCheckGracePeriodSeconds(),
+                    path=deque([]),
+                    validator="required",
+                    schema_path=deque(["then", "required"]),
+                )
+            ],
+        ),
+        (
+            {
+                "HealthCheckGracePeriodSeconds": "Foo",
+                "LoadBalancers": [
+                    {"Fn::If": ["IsUsEast1", "Bar", {"Ref": "AWS::NoValue"}]}
+                ],
+            },
+            [
+                ValidationError(
+                    "[] is too short (1)",
+                    rule=ServiceHealthCheckGracePeriodSeconds(),
+                    path=deque(["LoadBalancers"]),
+                    validator="minItems",
+                    schema_path=deque(
+                        ["then", "then", "properties", "LoadBalancers", "minItems"]
+                    ),
+                )
+            ],
+        ),
+        (
+            {
+                "HealthCheckGracePeriodSeconds": "Foo",
+                "LoadBalancers": {
+                    "Fn::If": ["IsUsEast1", ["Bar"], {"Ref": "AWS::NoValue"}]
+                },
+            },
+            [
+                ValidationError(
+                    "'LoadBalancers' is a required property",
+                    rule=ServiceHealthCheckGracePeriodSeconds(),
+                    path=deque([]),
+                    validator="required",
+                    schema_path=deque(["then", "required"]),
+                )
+            ],
+        ),
+    ],
+)
+def test_validate(instance, expected, rule, validator):
+    errs = list(rule.validate(validator, "", instance, {}))
+
+    assert errs == expected, f"Expected {expected} got {errs}"