add opt. service deps, opt-in services to test selection
diff --git a/localstack/testing/testselection/matching.py b/localstack/testing/testselection/matching.py
index 8310a4cfe1486..00e78c2b4af7d 100644
--- a/localstack/testing/testselection/matching.py
+++ b/localstack/testing/testselection/matching.py
@@ -1,5 +1,6 @@
 import fnmatch
 import re
+from collections import defaultdict
 from typing import Callable, Iterable, Optional
 
 from localstack.aws.scaffold import is_keyword
@@ -34,7 +35,7 @@ def resolve_dependencies(module_name: str, api_dependencies) -> set[str]:
     :return: set of resolved _service names_ that the service depends on (e.g. sts)
     """
     svc_name = _map_to_service_name(module_name)
-    return _expand_api_dependencies(svc_name, api_dependencies)
+    return set(_reverse_dependency_map(api_dependencies).get(svc_name, []))
 
 
 # TODO: might want to cache that, but for now it shouldn't be too much overhead
@@ -50,18 +51,6 @@ def _reverse_dependency_map(dependency_map: dict[str, dict]) -> dict[str, set[st
     return result
 
 
-def _expand_api_dependencies(svc_name: str, api_dependencies) -> set[str]:
-    result = set()
-
-    dependencies = _reverse_dependency_map(api_dependencies).get(svc_name, [])
-    result.update(dependencies)
-
-    for dep in dependencies:
-        sub_deps = _expand_api_dependencies(dep, api_dependencies)  # recursive call
-        result.update(sub_deps)
-    return result
-
-
 def get_test_dir_for_service(svc: str):
     return f"tests/aws/services/{svc}"
 
@@ -114,9 +103,15 @@ def generic_service_test_matching_rule(
     # TODO: consider API_COMPOSITES
 
     if api_dependencies is None:
-        from localstack.utils.bootstrap import API_DEPENDENCIES
+        from localstack.utils.bootstrap import API_DEPENDENCIES, API_DEPENDENCIES_OPTIONAL
+
+        # merge the mandatory and optional service dependencies
+        api_dependencies = defaultdict(set)
+        for service, mandatory_dependencies in API_DEPENDENCIES.items():
+            api_dependencies[service].update(mandatory_dependencies)
 
-        api_dependencies = API_DEPENDENCIES
+        for service, optional_dependencies in API_DEPENDENCIES_OPTIONAL.items():
+            api_dependencies[service].update(optional_dependencies)
 
     match = re.findall("localstack/services/([^/]+)/.+", changed_file_path)
     if not match:
diff --git a/localstack/testing/testselection/opt_in.py b/localstack/testing/testselection/opt_in.py
index d6ca92f4e0a73..f5ed72d573d81 100644
--- a/localstack/testing/testselection/opt_in.py
+++ b/localstack/testing/testselection/opt_in.py
@@ -7,16 +7,37 @@
 from typing import Iterable, Optional
 
 OPT_IN = [
-    # SFN
-    "localstack/services/stepfunctions/**",
-    "tests/aws/services/stepfunctions/**",
-    # CFn
+    # acm
+    "localstack/services/acm/**",
+    "tests/aws/services/acm/**",
+    # cloudformation
     # probably the riskiest here since CFn tests are not as isolated as the rest
     "localstack/services/cloudformation/**",
     "tests/aws/services/cloudformation/**",
-    # Lambda
+    # elasticsearch
+    "localstack/services/es/**",
+    "tests/aws/services/es/**",
+    # lambda
     "localstack/services/lambda_/**",
     "tests/aws/services/lambda_/**",
+    # sns
+    "localstack/services/sns/**",
+    "tests/aws/services/sns/**",
+    # opensearch
+    "localstack/services/opensearch/**",
+    "tests/aws/services/opensearch/**",
+    # stepfunctions
+    "localstack/services/stepfunctions/**",
+    "tests/aws/services/stepfunctions/**",
+    # secretsmanager
+    "localstack/services/secretsmanager/**",
+    "tests/aws/services/secretsmanager/**",
+    # events
+    "localstack/services/events/**",
+    "tests/aws/services/events/**",
+    # SSM
+    "localstack/services/ssm/**",
+    "tests/aws/services/ssm/**",
 ]
 
 
diff --git a/localstack/utils/bootstrap.py b/localstack/utils/bootstrap.py
index e6c267a7e9283..f8ea5360136b0 100644
--- a/localstack/utils/bootstrap.py
+++ b/localstack/utils/bootstrap.py
@@ -41,16 +41,43 @@
 
 LOG = logging.getLogger(__name__)
 
-# maps from API names to list of other API names that they depend on
+# Mandatory dependencies of services on other services
+# - maps from API names to list of other API names that they _explicitly_ depend on: <service>:<dependent-services>
+# - an explicit service dependency is a service without which another service's basic functionality breaks
+# - this mapping is used when enabling strict service loading (use SERVICES env var to allow-list services)
+# - do not add "optional" dependencies of services here, use API_DEPENDENCIES_OPTIONAL instead
 API_DEPENDENCIES = {
     "dynamodb": ["dynamodbstreams"],
+    # dynamodbsteams uses kinesis under the hood
     "dynamodbstreams": ["kinesis"],
+    # es forwards all requests to opensearch (basically an API deprecation path in AWS)
     "es": ["opensearch"],
     "cloudformation": ["s3", "sts"],
-    "lambda": ["s3", "sqs", "sts"],
+    "lambda": ["s3", "sts"],
+    # firehose currently only supports kinesis as source, this could become optional when more sources are supported
     "firehose": ["kinesis"],
     "transcribe": ["s3"],
 }
+
+# Optional dependencies of services on other services
+# - maps from API names to list of other API names that they _optionally_ depend on: <service>:<dependent-services>
+# - an optional service dependency is a service without which a service's basic functionality doesn't break,
+#   but which is needed for certain features (f.e. for one of multiple integrations)
+# - this mapping is used f.e. used for the selective test execution (localstack.testing.testselection)
+# - only add optional dependencies of services here, use API_DEPENDENCIES for mandatory dependencies
+API_DEPENDENCIES_OPTIONAL = {
+    # firehose's optional dependencies are supported delivery stream destinations
+    "firehose": ["es", "opensearch", "s3", "redshift"],
+    "lambda": ["cloudwatch", "dynamodbstreams", "logs", "kafka", "kinesis", "msk", "sqs"],
+    "ses": ["sns"],
+    "sns": ["sqs", "lambda", "firehose", "ses", "logs"],
+    "sqs": ["cloudwatch"],
+    "logs": ["lambda", "kinesis", "firehose"],
+    "cloudformation": ["secretsmanager", "ssm", "lambda"],
+    "events": ["lambda", "kinesis", "firehose", "sns", "sqs", "stepfunctions", "logs"],
+    "stepfunctions": ["logs", "lambda", "dynamodb", "ecs", "sns", "sqs", "apigateway", "events"],
+}
+
 # composites define an abstract name like "serverless" that maps to a set of services
 API_COMPOSITES = {
     "serverless": [
diff --git a/tests/unit/testing/testselection/test_matching.py b/tests/unit/testing/testselection/test_matching.py
index 8e3d1dee30d9c..8cf7c9fdde7c9 100644
--- a/tests/unit/testing/testselection/test_matching.py
+++ b/tests/unit/testing/testselection/test_matching.py
@@ -61,6 +61,42 @@ def test_generic_service_matching_rule_with_dependencies():
     }
 
 
+def test_generic_service_matching_rule_defaults_to_api_deps():
+    """
+    Test that the generic service test matching rule uses both API_DEPENDENCIES and API_DEPENDENCIES_OPTIONAL
+    if no api dependencies are explicitly set.
+    """
+    # match on code associated with OpenSearch
+    result = generic_service_test_matching_rule("localstack/services/opensearch/test_somefile.py")
+    # the result needs to contain at least:
+    # - elasticsearch since it has opensearch as a mandatory requirement
+    assert "tests/aws/services/es/" in result
+    # - firehose since it has opensearch as an optional dependency used for one of its integrations
+    assert "tests/aws/services/firehose/" in result
+    # - opensearch because it is the actually changed service
+    assert "tests/aws/services/opensearch/"
+
+
+def test_service_dependency_resolving_with_co_dependencies():
+    """
+    Test to validate that we don't encounter issue when services are co-dependent on each other
+    """
+    api_dependencies = {
+        "ses": ["sns"],
+        "sns": ["sqs", "lambda", "firehose", "ses", "logs"],
+        "logs": ["lambda", "kinesis", "firehose"],
+        "lambda": ["logs", "cloudwatch"],
+    }
+    svc_including_deps = resolve_dependencies("ses", api_dependencies)
+    assert svc_including_deps >= {"sns"}
+
+    svc_including_deps = resolve_dependencies("logs", api_dependencies)
+    assert svc_including_deps >= {"sns", "lambda"}
+
+    svc_including_deps = resolve_dependencies("lambda", api_dependencies)
+    assert svc_including_deps >= {"sns", "logs"}
+
+
 @pytest.mark.skip(reason="mostly just useful for local execution as a sanity check")
 def test_rules_are_matching_at_least_one_file():
     root_dir = Path(__file__).parent.parent.parent.parent.parent
diff --git a/tests/unit/utils/test_bootstrap.py b/tests/unit/utils/test_bootstrap.py
index d172cc1596a70..3da62957739d3 100644
--- a/tests/unit/utils/test_bootstrap.py
+++ b/tests/unit/utils/test_bootstrap.py
@@ -156,16 +156,15 @@ def test_resolve_meta(self):
         with temporary_env({"SERVICES": "es,lambda", "STRICT_SERVICE_LOADING": "1"}):
             result = get_enabled_apis()
 
-        assert len(result) == 6
+        assert len(result) == 5
         assert result == {
             # directly given
             "lambda",
             "es",
             # a dependency of es
             "opensearch",
-            # lambda has internal dependencies on s3, sqs, and sts
+            # lambda has internal dependencies on s3 and sts
             "s3",
-            "sqs",
             "sts",
         }
 

## Motivation
Now that we have the selective test execution for opt-in services introduced in https://github.com/localstack/localstack/pull/10301, we can onboard additional services to the test selection.
In this PR I added the services I own to the list (😻), but it might make sense to collect the first iteration of opt ins here and merge them together.
So please feel free to push to this PR if you want the tests of your service being selectively executed in PRs here.
In order to to so, just:
1. Make sure the mandatory and optional service dependencies in `localstack/utils/bootstrap.py` are correct.
  - They should list your service (if it's not completely self-contained).
  - `API_DEPENDENCIES`
    - This mapping states that a service requires one or more other services to provide it's basic functionality.
    - f.e. the following example means that `firehose` depends on `kinesis` to work: 
      ```python
      "firehose": ["kinesis"],
      ```
 - `API_DEPENDENCIES_OPTIONAL`
   - This mapping states that a service needs one or more other services for certain non-basic parts, f.e. with optional integrations.
   - On the example of `firehose` that would be at least `opensearch` and `elasticsearch`, because these are _potential_ targets for `firehose` streams.
2. Add your service provider code and the test code to the `OPT_IN`. The list is alphabetically sorted, I expect it to grow quite fast (and then become obsolete).

## Changes
- Introduces the notion of optional service dependencies, in contrast to "normal" / mandatory service dependencies.
- Adds the following services to the Opt-In of the selective test execution:
  - ACM
  - ElasticSearch
  - OpenSearch
  - SNS

## TODO
- [x] Discuss if we should split the optional from the mandatory service dependency definition.
  - `API_DEPENDENCIES` is used for strict service loading, where it is used as a mandatory service definition.
    - f.e. `lambda` cannot work without `s3` and `sts`
  - For the test selection, we are using this mapping for all dependencies (including optional dependencies).
    - f.e. `firehose` can work without `opensearch` for as long as you don't use it as a target. However, it cannot work without `kinesis` at all.
  - /cc @bentsku @steffyP @thrau