Repository: localstack/localstack

Issue:
feature request: Pass all networks assigned from localstack to lambda container
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Feature description

Allow lambda container to have multiple networks not just one.
With the current implementation Lambda network only gets a single network, even though the Localstack container accepts multiple network values.
this is how it works currently:

> Gets the main network of the LocalStack container (if we run in one, bridge otherwise)
    If there are multiple networks connected to the LocalStack container, we choose the first as "main" network

### 🧑‍💻 Implementation

This is where the "main" network value is retrieved, may be it should be a List?
https://github.com/localstack/localstack/blob/a71e661fa6b0189e54b468713f710f0ae1b93791/localstack/utils/container_networking.py#L14

### Anything else?

_No response_


Discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [contributing guide](https://docs.localstack.cloud/contributing/).
I do not think it would be wise to do this per default for all containers. We can only add one network on start, so would have to have additional network connect requests afterwards. While this is possible, it would slow down all container creations significantly, most notable in lambda.

Can you share a little more about your usecase? Would only adding multiple ones if you specify multiple ones in `LAMBDA_DOCKER_NETWORK`, comma separated do the trick for you? I think this is a quite specialized usecase, as for most users only one connected network should suffice.
hi @dfangl ,
Our use case is that we have docker compose setup which involves multiple containers in multiple docker compose files. and we have 2 networks for that, as we don't want everything be in the same network to force some containers not contact each others directly.
We have localstack configured in both networks so resources from both networks can use it and mostly can contact via SQS.

> Would only adding multiple ones if you specify multiple ones in LAMBDA_DOCKER_NETWORK, comma separated do the trick for you?

yes indeed that will work for us.
Seems like a reasonable addition! Will put this on accepted for now, I cannot promise an ETA though.

Resolving patch:
diff --git a/localstack/services/awslambda/invocation/docker_runtime_executor.py b/localstack/services/awslambda/invocation/docker_runtime_executor.py
--- a/localstack/services/awslambda/invocation/docker_runtime_executor.py
+++ b/localstack/services/awslambda/invocation/docker_runtime_executor.py
@@ -21,7 +21,7 @@
 )
 from localstack.services.awslambda.lambda_utils import (
     HINT_LOG,
-    get_container_network_for_lambda,
+    get_all_container_networks_for_lambda,
     get_main_endpoint_from_container,
 )
 from localstack.services.awslambda.packages import awslambda_runtime_package
@@ -265,12 +265,12 @@ def _generate_container_name(self):
 
     def start(self, env_vars: dict[str, str]) -> None:
         self.executor_endpoint.start()
-        network = self._get_network_for_executor()
+        main_network, *additional_networks = self._get_networks_for_executor()
         container_config = LambdaContainerConfiguration(
             image_name=None,
             name=self.container_name,
             env_vars=env_vars,
-            network=network,
+            network=main_network,
             entrypoint=RAPID_ENTRYPOINT,
             platform=docker_platform(self.function_version.config.architectures[0]),
             dns=config.LAMBDA_DOCKER_DNS,
@@ -337,9 +337,16 @@ def start(self, env_vars: dict[str, str]) -> None:
             for source, target in container_config.copy_folders:
                 CONTAINER_CLIENT.copy_into_container(self.container_name, source, target)
 
+        if additional_networks:
+            for additional_network in additional_networks:
+                CONTAINER_CLIENT.connect_container_to_network(
+                    additional_network, self.container_name
+                )
+
         CONTAINER_CLIENT.start_container(self.container_name)
+        # still using main network as main entrypoint
         self.ip = CONTAINER_CLIENT.get_container_ipv4_for_network(
-            container_name_or_id=self.container_name, container_network=network
+            container_name_or_id=self.container_name, container_network=main_network
         )
         if config.LAMBDA_DEV_PORT_EXPOSE:
             self.ip = "127.0.0.1"
@@ -366,8 +373,8 @@ def get_address(self) -> str:
     def get_endpoint_from_executor(self) -> str:
         return get_main_endpoint_from_container()
 
-    def _get_network_for_executor(self) -> str:
-        return get_container_network_for_lambda()
+    def _get_networks_for_executor(self) -> list[str]:
+        return get_all_container_networks_for_lambda()
 
     def invoke(self, payload: Dict[str, str]):
         LOG.debug(
diff --git a/localstack/services/awslambda/lambda_executors.py b/localstack/services/awslambda/lambda_executors.py
--- a/localstack/services/awslambda/lambda_executors.py
+++ b/localstack/services/awslambda/lambda_executors.py
@@ -24,7 +24,7 @@
 from localstack.services.awslambda.lambda_utils import (
     API_PATH_ROOT,
     LAMBDA_RUNTIME_PROVIDED,
-    get_container_network_for_lambda,
+    get_main_container_network_for_lambda,
     get_main_endpoint_from_container,
     is_java_lambda,
     is_nodejs_runtime,
@@ -1024,7 +1024,7 @@ def create_container(
         env_vars.pop("AWS_LAMBDA_EVENT_BODY", None)
         container_config.env_vars = env_vars
 
-        container_config.network = get_container_network_for_lambda()
+        container_config.network = get_main_container_network_for_lambda()
         container_config.additional_flags = docker_flags
 
         container_config.dns = config.LAMBDA_DOCKER_DNS
@@ -1242,7 +1242,7 @@ def execute_in_container(
             inv_context.lambda_command = inv_context.handler
 
         # add Docker Lambda env vars
-        container_config.network = get_container_network_for_lambda()
+        container_config.network = get_main_container_network_for_lambda()
         if container_config.network == "host":
             port = get_free_tcp_port()
             container_config.env_vars["DOCKER_LAMBDA_API_PORT"] = port
diff --git a/localstack/services/awslambda/lambda_utils.py b/localstack/services/awslambda/lambda_utils.py
--- a/localstack/services/awslambda/lambda_utils.py
+++ b/localstack/services/awslambda/lambda_utils.py
@@ -190,16 +190,23 @@ def store_lambda_logs(
 def get_main_endpoint_from_container() -> str:
     if config.HOSTNAME_FROM_LAMBDA:
         return config.HOSTNAME_FROM_LAMBDA
-    return get_endpoint_for_network(network=get_container_network_for_lambda())
+    return get_endpoint_for_network(network=get_main_container_network_for_lambda())
 
 
-def get_container_network_for_lambda() -> str:
+def get_main_container_network_for_lambda() -> str:
     global LAMBDA_CONTAINER_NETWORK
     if config.LAMBDA_DOCKER_NETWORK:
-        return config.LAMBDA_DOCKER_NETWORK
+        return config.LAMBDA_DOCKER_NETWORK.split(",")[0]
     return get_main_container_network()
 
 
+def get_all_container_networks_for_lambda() -> list[str]:
+    global LAMBDA_CONTAINER_NETWORK
+    if config.LAMBDA_DOCKER_NETWORK:
+        return config.LAMBDA_DOCKER_NETWORK.split(",")
+    return [get_main_container_network()]
+
+
 def rm_docker_container(container_name_or_id, check_existence=False, safe=False):
     # TODO: remove method / move to docker module
     if not container_name_or_id:


Test patch:
diff --git a/tests/integration/awslambda/functions/lambda_networks.py b/tests/integration/awslambda/functions/lambda_networks.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/awslambda/functions/lambda_networks.py
@@ -0,0 +1,10 @@
+from urllib.request import Request, urlopen
+
+
+def handler(event, context):
+    url = event.get("url")
+
+    httprequest = Request(url, headers={"Accept": "application/json"})
+
+    with urlopen(httprequest) as response:
+        return {"status": response.status, "response": response.read().decode()}
diff --git a/tests/integration/awslambda/test_lambda_developer_tools.py b/tests/integration/awslambda/test_lambda_developer_tools.py
--- a/tests/integration/awslambda/test_lambda_developer_tools.py
+++ b/tests/integration/awslambda/test_lambda_developer_tools.py
@@ -7,9 +7,12 @@
 from localstack import config
 from localstack.aws.api.lambda_ import Runtime
 from localstack.testing.aws.lambda_utils import is_old_provider
-from localstack.utils.docker_utils import get_host_path_for_path_in_docker
+from localstack.utils.container_networking import get_main_container_network
+from localstack.utils.docker_utils import DOCKER_CLIENT, get_host_path_for_path_in_docker
 from localstack.utils.files import load_file, mkdir, rm_rf
 from localstack.utils.strings import short_uid, to_str
+from localstack.utils.sync import retry
+from localstack.utils.testutil import create_lambda_archive
 from tests.integration.awslambda.test_lambda import TEST_LAMBDA_ENV, THIS_FOLDER
 
 HOT_RELOADING_NODEJS_HANDLER = os.path.join(
@@ -18,6 +21,7 @@
 HOT_RELOADING_PYTHON_HANDLER = os.path.join(
     THIS_FOLDER, "functions/hot-reloading/python/handler.py"
 )
+LAMBDA_NETWORKS_PYTHON_HANDLER = os.path.join(THIS_FOLDER, "functions/lambda_networks.py")
 
 
 @pytest.mark.skipif(condition=is_old_provider(), reason="Focussing on the new provider")
@@ -145,3 +149,52 @@ def test_additional_docker_flags(self, create_lambda_function, monkeypatch, aws_
         result_data = result["Payload"].read()
         result_data = json.loads(to_str(result_data))
         assert {"Hello": env_value} == result_data
+
+    def test_lambda_docker_networks(self, lambda_su_role, monkeypatch, aws_client, cleanups):
+        function_name = f"test-network-{short_uid()}"
+        container_name = f"server-{short_uid()}"
+        additional_network = f"test-network-{short_uid()}"
+
+        # networking setup
+        main_network = get_main_container_network()
+        DOCKER_CLIENT.create_network(additional_network)
+
+        def _delete_network():
+            retry(lambda: DOCKER_CLIENT.delete_network(additional_network))
+
+        cleanups.append(_delete_network)
+        DOCKER_CLIENT.run_container(
+            image_name="nginx",
+            remove=True,
+            detach=True,
+            name=container_name,
+            network=additional_network,
+        )
+        cleanups.append(lambda: DOCKER_CLIENT.stop_container(container_name=container_name))
+        monkeypatch.setattr(config, "LAMBDA_DOCKER_NETWORK", f"{main_network},{additional_network}")
+
+        # we need to create a lambda manually here for the right cleanup order
+        # (we need to destroy the function before the network, not the other way around. This is only guaranteed
+        # with the cleanups fixture)
+        zip_file = create_lambda_archive(
+            load_file(LAMBDA_NETWORKS_PYTHON_HANDLER),
+            get_content=True,
+            runtime=Runtime.python3_9,
+        )
+        aws_client.awslambda.create_function(
+            FunctionName=function_name,
+            Code={"ZipFile": zip_file},
+            Handler="handler.handler",
+            Runtime=Runtime.python3_9,
+            Role=lambda_su_role,
+        )
+        cleanups.append(lambda: aws_client.awslambda.delete_function(FunctionName=function_name))
+
+        aws_client.awslambda.get_waiter("function_active_v2").wait(FunctionName=function_name)
+        result = aws_client.awslambda.invoke(
+            FunctionName=function_name, Payload=json.dumps({"url": f"http://{container_name}"})
+        )
+        result_data = result["Payload"].read()
+        result_data = json.loads(to_str(result_data))
+        assert result_data["status"] == 200
+        assert "nginx" in result_data["response"]