Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.dvely.agent.infrastructure.docker;

/**
* 컨테이너를 무엇에 쓰는지. 생성 시점에 <b>반드시</b> 정한다.
*
* <p>둘은 같은 이미지를 쓰지만 수명도 노출면도 다르다. 그런데 한동안 같은 생성 경로를 구분 없이
* 써서, 프리뷰 쪽 격리 정책을 손대면 배포 빌드까지 함께 흔들렸다 — 바꿀 때마다 배포 파이프라인
* 전체를 다시 검증해야 한다는 뜻이고, 그래서 아무도 손대지 않게 된다.</p>
*
* <p>기본값을 두지 않는 것이 요점이다. 새 호출부가 생기면 무엇인지 고르게 강제한다 — 기본값이
* 있으면 고르지 않은 것과 고른 것이 구분되지 않고, 잘못 고른 쪽이 조용히 돈다.</p>
*/
public enum ContainerRole {

/**
* 사용자에게 보여줄 결과물을 <b>서빙</b>한다. 게이트웨이가 프록시할 수 있도록 포트를 게시하고,
* 세션이 살아 있는 동안 유지된다.
*/
PREVIEW,

/**
* 저장소를 받아 <b>빌드 산출물만 꺼내고 즉시 버린다</b>. 아무것도 서빙하지 않으므로 포트를
* 게시하지 않는다 — 게시해 봐야 아무도 연결하지 않고, 루프백이라도 열려 있는 면은 없는 편이 낫다.
*/
BUILD;

public boolean publishesPort() {
return this == PREVIEW;
}

/** 컨테이너 라벨에 넣는 값. 운영자가 `docker ps` 에서 둘을 갈라 볼 수 있어야 한다. */
public String label() {
return name().toLowerCase(java.util.Locale.ROOT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
Expand All @@ -69,6 +70,7 @@ public class DockerContainerService {
private static final String PROJECT_ID_LABEL = "qeploy.projectId";
private static final String CONVERSATION_ID_LABEL = "qeploy.conversationId";
private static final String TASK_ID_LABEL = "qeploy.taskId";
private static final String ROLE_LABEL = "qeploy.role";
private static final String LEGACY_AGENT_LABEL = "dvely.agent";

// --- Preview container isolation policy (BI-194). Kept as plain constants rather than
Expand Down Expand Up @@ -123,12 +125,13 @@ public DockerContainerService() {
this.dockerClient = dockerClient;
}

public String createAndStartContainer(Long userId,
public String createAndStartContainer(ContainerRole role,
Long userId,
String previewSessionId,
Long projectId,
Long conversationId,
String taskId) {
return createAndStartContainer(userId, previewSessionId, projectId,
return createAndStartContainer(role, userId, previewSessionId, projectId,
conversationId, taskId, MEMORY_LIMIT_BYTES);
}

Expand All @@ -137,12 +140,14 @@ public String createAndStartContainer(Long userId,
* {@link #JAVA_MEMORY_LIMIT_BYTES} 를 넘긴다. swap 은 메모리와 같게 둬(추가 swap 없음) OOM 이
* 느린 디스크 뒤로 숨지 않고 깨끗하게 kill 되도록 한다.
*/
public String createAndStartContainer(Long userId,
public String createAndStartContainer(ContainerRole role,
Long userId,
String previewSessionId,
Long projectId,
Long conversationId,
String taskId,
long memoryBytes) {
Objects.requireNonNull(role, "role");
pullImageIfNeeded();
ensurePreviewNetwork();

Expand All @@ -164,10 +169,16 @@ public String createAndStartContainer(Long userId,
// same host". If preview containers ever move to a remote/multi-host Docker daemon, this
// loopback bind must be revisited together with the gateway's proxy target — otherwise
// the gateway simply can't reach the container at all.
portBindings.bind(exposedPort, Ports.Binding.bindIpAndPort(HOST_BIND_IP, 0));
// 빌드 컨테이너는 아무것도 서빙하지 않는다 — 저장소를 받아 산출물만 꺼내고 버린다.
// 게시해 봐야 연결하는 쪽이 없고(getMappedPort 는 프리뷰 경로만 부른다), 루프백이라도
// 열려 있는 면은 없는 편이 낫다.
if (role.publishesPort()) {
portBindings.bind(exposedPort, Ports.Binding.bindIpAndPort(HOST_BIND_IP, 0));
}

Map<String, String> labels = new HashMap<>();
labels.put(AGENT_LABEL, "true");
labels.put(ROLE_LABEL, role.label());
labels.put(USER_ID_LABEL, String.valueOf(userId));
putLabel(labels, PREVIEW_SESSION_ID_LABEL, previewSessionId);
putLabel(labels, PROJECT_ID_LABEL, projectId);
Expand All @@ -181,7 +192,7 @@ public String createAndStartContainer(Long userId,
// disabled. Rootfs stays read-write (the agent writes project files into the container)
// and no restart policy is set (a dead container surfaces via the status API instead).
CreateContainerResponse container = dockerClient.createContainerCmd(IMAGE)
.withExposedPorts(exposedPort)
.withExposedPorts(role.publishesPort() ? List.of(exposedPort) : List.<ExposedPort>of())
.withHostConfig(HostConfig.newHostConfig()
.withPortBindings(portBindings)
.withMemory(memoryBytes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.example.dvely.preview.infrastructure.persistence.entity.PreviewSessionEntity;
import com.example.dvely.preview.infrastructure.persistence.repository.SpringDataPreviewSessionRepository;
import com.example.dvely.preview.infrastructure.security.PreviewAccessCookies;
import com.example.dvely.agent.infrastructure.docker.ContainerRole;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
Expand Down Expand Up @@ -67,6 +68,7 @@ public PreviewSessionInfo acquire(String taskId) {
String accessToken = UUID.randomUUID().toString().replace("-", "");
long memoryBytes = runtimeConfigService.previewContainerMemoryBytes(task.projectId());
String containerId = dockerService.createAndStartContainer(
ContainerRole.PREVIEW,
task.ownerUserId(),
sessionId,
task.projectId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.example.dvely.preview.infrastructure.persistence.repository.SpringDataPreviewSessionRepository;
import com.example.dvely.project.domain.model.Project;
import com.example.dvely.project.domain.repository.ProjectRepository;
import com.example.dvely.agent.infrastructure.docker.ContainerRole;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
Expand Down Expand Up @@ -131,6 +132,7 @@ public ProvisionOutcome provision(Long projectId, Long ownerUserId, boolean forc
try {
long memoryBytes = runtimeConfigService.previewContainerMemoryBytes(projectId);
containerId = dockerService.createAndStartContainer(
ContainerRole.PREVIEW,
ownerUserId, sessionId, projectId, null, null, memoryBytes);
hostPort = dockerService.getMappedPort(containerId);
} catch (RuntimeException exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.dvely.project.domain.model.Project;
import com.example.dvely.project.domain.repository.ProjectRepository;
import com.example.dvely.provisioning.infrastructure.EcrImageRegistry.EcrAuth;
import com.example.dvely.agent.infrastructure.docker.ContainerRole;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -73,6 +74,7 @@ private Path prepareContextTar(Long ownerUserId, Long projectId) {
}
String sessionId = "img-" + projectId + "-" + System.currentTimeMillis();
String containerId = dockerService.createAndStartContainer(
ContainerRole.BUILD,
ownerUserId, sessionId, projectId, null, null);
try {
sourceClone.cloneInto(containerId, ownerUserId, sourceRepo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.example.dvely.deployment.application.port.out.FrontendStaticHostingPort;
import com.example.dvely.project.domain.repository.ProjectCloudConnectionSettingRepository;
import com.example.dvely.provisioning.infrastructure.S3StaticSiteStore;
import com.example.dvely.agent.infrastructure.docker.ContainerRole;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -45,6 +46,7 @@ public String publishToS3(PublishRequest request) {
String sessionId = "site-build-" + request.projectId() + "-" + System.currentTimeMillis();
// 프론트 번들러(vite/webpack)는 메모리를 꽤 쓴다 — 1GiB 기본으로는 큰 앱이 OOM 날 수 있어 2GiB.
String containerId = dockerService.createAndStartContainer(
ContainerRole.BUILD,
request.ownerUserId(), sessionId, request.projectId(), null, null,
DockerContainerService.JAVA_MEMORY_LIMIT_BYTES);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.dvely.agent.infrastructure.docker.DockerContainerService.ExecResult;
import com.example.dvely.project.domain.model.Project;
import com.example.dvely.project.domain.repository.ProjectRepository;
import com.example.dvely.agent.infrastructure.docker.ContainerRole;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -51,6 +52,7 @@ public NativeArtifact build(Long ownerUserId, Long projectId) {

String sessionId = "build-" + projectId + "-" + System.currentTimeMillis();
String containerId = dockerService.createAndStartContainer(
ContainerRole.BUILD,
ownerUserId, sessionId, projectId, null, null,
DockerContainerService.JAVA_MEMORY_LIMIT_BYTES);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.dvely.project.domain.model.Project;
import com.example.dvely.project.domain.repository.ProjectRepository;
import com.example.dvely.provisioning.infrastructure.EcrImageRegistry.EcrAuth;
import com.example.dvely.agent.infrastructure.docker.ContainerRole;
import java.nio.file.Path;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -78,6 +79,7 @@ private Path prepareWebContextTar(Long ownerUserId, Long projectId, String front
}
String sessionId = "web-" + projectId + "-" + System.currentTimeMillis();
String containerId = dockerService.createAndStartContainer(
ContainerRole.BUILD,
ownerUserId, sessionId, projectId, null, null);
try {
sourceClone.cloneInto(containerId, ownerUserId, repo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ void tearDown() {

@Test
void createAndStartContainerPublishesHostPortOnLoopbackOnly() {
containerId = service.createAndStartContainer(
containerId = service.createAndStartContainer(ContainerRole.PREVIEW,
999_000L, "it-session-" + System.nanoTime(), 1L, 1L, "it-task-" + System.nanoTime());

Ports.Binding[] bindings = inspectPortBindings(containerId);
Expand Down Expand Up @@ -101,7 +101,7 @@ void createAndStartContainerPublishesHostPortOnLoopbackOnly() {
// is HostIp.
@Test
void restartContainerKeepsHostPortOnLoopbackAfterReallocation() {
containerId = service.createAndStartContainer(
containerId = service.createAndStartContainer(ContainerRole.PREVIEW,
999_001L, "it-session-restart-" + System.nanoTime(), 1L, 1L,
"it-task-restart-" + System.nanoTime());
int portBeforeRestart = service.getMappedPort(containerId);
Expand Down
Loading
Loading