네트워킹과 컨테이너 통신

네트워킹과 컨테이너 통신

테스트 코드(호스트 머신)에서 컨테이너 안의 서비스에 접속하려면 포트를 어떻게 노출하는지 알아야 해요. Testcontainers에서는 컨테이너 포트를 호스트로 노출하고, 반대로 호스트의 포트를 컨테이너에서 접근하게 만드는 일까지 지원해요. 이 페이지에서는 이 두 방향의 통신을 다루는 방법과, 더 나아가 컨테이너끼리만 통신하는 커스텀 네트워크를 만드는 방법을 살펴볼게요.

출처: 공식문서 — Networking and communicating with containers

본문

컨테이너 포트를 호스트로 노출하기

테스트 호스트 머신에서 실행 중인 테스트 프로세스가 컨테이너에 접속하고 싶은 것은 흔한 일이에요. 예를 들어 백엔드나 데이터 저장소 컨테이너에 접속해야 하는 클래스를 테스트하는 경우죠.

보통 필요한 포트는 각각 명시적으로 노출해야 해요. 예를 들어 포트를 하나 이상 이렇게 지정할 수 있어요.

public GenericContainer<?> container = new GenericContainer<>(
    DockerImageName.parse("testcontainers/helloworld:1.1.0")
)
    .withExposedPorts(8080, 8081)
    .withLogConsumer(new Slf4jLogConsumer(log));

이 노출된 포트 번호는 컨테이너 관점의 포트라는 점을 기억하세요.

호스트 관점에서 Testcontainers는 실제로 이 포트를 임의의 빈 포트에 매핑해요. 이는 로컬에서 실행 중인 소프트웨어나 병렬 테스트 실행 사이에서 생길 수 있는 포트 충돌을 피하기 위한 설계예요.

이런 간접 계층이 있기 때문에 실제 매핑된 포트는 런타임에 Testcontainers에 물어봐야 해요. 원래(컨테이너) 포트를 인자로 받는 getMappedPort 메서드로 할 수 있어요.

Integer firstMappedPort = container.getMappedPort(8080);
Integer secondMappedPort = container.getMappedPort(8081);

주의: 임의의 포트 매핑은 컨테이너 시작 중에 일어나므로, getMappedPort를 호출할 때는 컨테이너가 반드시 실행 중이어야 해요. 테스트에서 컴포넌트의 시작 순서가 이 점을 고려하도록 해야 할 수 있어요.

포트를 하나만 노출하는 흔한 시나리오를 위해 편의 메서드 getFirstMappedPort도 있어요.

Integer firstMappedPort = container.getFirstMappedPort();

컨테이너 호스트 얻기

로컬 Docker 데몬으로 실행할 때는 노출된 포트가 보통 localhost에서 접근 가능해요. 하지만 일부 CI 환경에서는 다른 호스트에서 접근 가능할 수도 있어요.

그래서 Testcontainers는 호스트 머신에서 컨테이너에 접근 가능해야 하는 주소를 얻는 편의 메서드를 제공해요.

String ipAddress = container.getHost();

주소를 만들 때는 getHostgetMappedPort를 함께 쓰는 게 보통 바람직해요. 예를 들어 볼게요.

String address = container.getHost() + ":" + container.getMappedPort(8080);

팁: getHost()getContainerIpAddress()를 대체하며 같은 결과를 돌려줘요. getContainerIpAddress()는 이름이 혼란스럽다고 여겨져서 결국 deprecated 될 예정이에요.

호스트의 포트를 컨테이너로 노출하기

어떤 경우에는 컨테이너에서 호스트 머신에 리슨 중인 소켓으로 네트워크 연결을 만들어야 해요. Docker는 이 모델을 플랫폼 전반에 걸쳐 제한적으로 지원해요. 하지만 Testcontainers는 이것을 가능하게 해 줘요.

이 예시에서 localServerPort는 테스트 호스트 머신에서 실행 중인 서버(예: 웹 애플리케이션)의 포트라고 가정해요.

이 포트를 컨테이너에 노출하도록 Testcontainers에 준비시켜야 해요.

Testcontainers.exposeHostPorts(localServerPort);

주의: 위 명령은 컨테이너가 시작되기 이지만, 호스트의 서버가 시작된 에 호출해야 해요. 또는 container.withAccessToHost(true)를 써서 호스트 접근 메커니즘을 강제할 수도 있어요(그래도 포트를 사용 가능하게 하려면 exposeHostPorts를 호출해야 해요).

이렇게 하면 이후에 실행되는 어느 컨테이너에서든 이 포트에 접근할 수 있어요. 컨테이너 관점에서 호스트 이름은 host.testcontainers.internal이고, 포트는 localServerPort와 같은 값이에요.

예를 들어, 여기서 로컬 웹 애플리케이션의 HTTP URL을 만들고 Selenium 컨테이너에게 그 페이지를 가져오라고 시킬 수 있어요.

final String rootUrl = String.format("http://host.testcontainers.internal:%d/", localServerPort);

final RemoteWebDriver webDriver = new RemoteWebDriver(this.browser.getSeleniumAddress(), new ChromeOptions());
webDriver.get(rootUrl);

고급 네트워킹

Docker는 커스텀 네트워크를 만들고 컨테이너를 하나 이상의 네트워크에 배치할 수 있는 기능을 제공해요. 그러면 호스트를 거쳐 포트를 노출할 필요 없이 네트워크에 속한 컨테이너끼리 통신할 수 있어요. Testcontainers로도 이게 가능해요.

주의: Testcontainers는 현재 컨테이너 하나를 단일 네트워크에만 둘 수 있어요.

try (
    Network network = Network.newNetwork();
    GenericContainer<?> foo = new GenericContainer<>(TestImages.TINY_IMAGE)
        .withNetwork(network)
        .withNetworkAliases("foo")
        .withCommand(
            "/bin/sh",
            "-c",
            "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"
        );
    GenericContainer<?> bar = new GenericContainer<>(TestImages.TINY_IMAGE)
        .withNetwork(network)
        .withCommand("top")
) {
    foo.start();
    bar.start();

    String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout();
    assertThat(response).as("received response").isEqualTo("yay");
}

더 알아보기