Skip to content

[FEATURE] AWS Route53(DNS) 관리 기능 구현 #181

@hyobin-yang

Description

@hyobin-yang

AWS DNS(Route53) 자원 관리 기능 구현 계획

개요

AWS Route53을 헥사고날 아키텍처로 구현합니다.
도메인 추상화명은 DNS(Domain Name System)를 사용합니다.

추상화명

  • DNS
    • 도메인 이름 시스템의 일반적인 용어
    • AWS Route53, Azure DNS, GCP Cloud DNS 모두를 포괄하는 추상화명
    • 다른 CSP에서도 자연스럽게 매핑 가능

아키텍처 구조

헥사고날 아키텍처를 준수하여 다음과 같이 구성합니다:

domain/cloud/
├── adapter/outbound/aws/dns/          # AWS Route53 전용 어댑터
│   ├── AwsDnsManagementAdapter.java   # 호스팅 존 생명주기 관리
│   ├── AwsDnsDiscoveryAdapter.java    # 호스팅 존 조회
│   ├── AwsDnsMapper.java              # AWS SDK ↔ CloudResource 변환
│   └── ...
├── adapter/outbound/aws/config/
│   └── AwsDnsConfig.java              # Route53 클라이언트 설정
├── controller/
│   └── DnsController.java             # REST API (CSP 독립적)
├── dto/
│   ├── DnsCreateRequest.java
│   ├── DnsUpdateRequest.java
│   ├── DnsDeleteRequest.java
│   ├── DnsQueryRequest.java
│   └── DnsResponse.java
├── port/model/dns/                    # Command 객체
│   ├── DnsCreateCommand.java
│   ├── DnsUpdateCommand.java
│   ├── DnsDeleteCommand.java
│   ├── DnsQuery.java
│   └── GetDnsCommand.java
├── port/outbound/dns/                 # 포트 인터페이스
│   ├── DnsManagementPort.java         # CRUD 작업
│   ├── DnsDiscoveryPort.java          # 조회 작업
│   └── DnsRecordManagementPort.java   # DNS 레코드 관리 (Phase 2)
├── service/dns/
│   ├── DnsUseCaseService.java         # 유스케이스 서비스 (CSP 독립적)
│   └── DnsPortRouter.java             # 포트 라우터
└── repository/
    └── CloudResourceRepository.java   # 기존 리포지토리 활용

구현 단계

Phase 1: 호스팅 존 관리

1.1 Command 모델 정의 (port/model/dns/)

DnsCreateCommand.java

/**
 * DNS 호스팅 존 생성 도메인 커맨드 (CSP 중립적)
 * 
 * UseCase Service에서 Adapter로 전달되는 내부 명령 모델입니다.
 * CSP 중립적인 필드를 사용하며, 각 CSP Adapter의 Mapper에서 CSP 특화 요청으로 변환합니다.
 * 
 * 필드 매핑 예시:
 * - zoneName: AWS(hostedZoneName), Azure(zoneName), GCP(dnsName)
 * - zoneType: AWS(hostedZoneConfig), Azure(zoneType), GCP(visibility)
 * - vpcId: AWS(VPC ID for Private Zone), Azure(virtualNetworkId), GCP(managedZoneVisibility)
 */
@Builder
public record DnsCreateCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String serviceKey,           // "ROUTE53", "AZURE_DNS", "CLOUD_DNS"
    String resourceType,         // "DNS_ZONE"
    String zoneName,             // CSP 중립적: AWS(hostedZoneName), Azure(zoneName), GCP(dnsName)
    String zoneType,             // "PUBLIC", "PRIVATE" (CSP 중립적)
    String vpcId,                // Private Zone인 경우 VPC ID (선택적)
    String comment,              // 호스팅 존 설명
    Map<String, String> tags,
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 설정
    CloudSessionCredential session
) {}

DnsUpdateCommand.java

/**
 * DNS 호스팅 존 수정 도메인 커맨드 (CSP 중립적)
 */
@Builder
public record DnsUpdateCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,   // 호스팅 존 ID (CSP별 형식 다를 수 있음)
    String comment,              // 호스팅 존 설명 변경
    Map<String, String> tags,
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 설정
    CloudSessionCredential session
) {}

DnsDeleteCommand.java

/**
 * DNS 호스팅 존 삭제 도메인 커맨드 (CSP 중립적)
 */
@Builder
public record DnsDeleteCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,
    Boolean forceDelete,         // 레코드가 있어도 강제 삭제 여부
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 삭제 옵션
    CloudSessionCredential session
) {}

DnsQuery.java

/**
 * DNS 호스팅 존 조회 쿼리 (CSP 중립적)
 */
@Builder
public record DnsQuery(
    ProviderType providerType,
    String accountScope,
    Set<String> regions,
    String zoneName,             // 호스팅 존 이름으로 필터링 (CSP 중립적)
    String zoneType,             // "PUBLIC", "PRIVATE"로 필터링
    String vpcId,                // Private Zone인 경우 VPC ID로 필터링
    Map<String, String> tagsEquals,  // 태그로 필터링
    int page,
    int size
) {}

GetDnsCommand.java

@Builder
public record GetDnsCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,
    String serviceKey,
    String resourceType,
    CloudSessionCredential session
) {}

1.2 포트 인터페이스 정의 (port/outbound/dns/)

DnsManagementPort.java

public interface DnsManagementPort {
    /**
     * DNS 호스팅 존 생성
     */
    CloudResource createHostedZone(DnsCreateCommand command);
    
    /**
     * DNS 호스팅 존 수정
     */
    CloudResource updateHostedZone(DnsUpdateCommand command);
    
    /**
     * DNS 호스팅 존 삭제
     */
    void deleteHostedZone(DnsDeleteCommand command);
}

DnsDiscoveryPort.java

public interface DnsDiscoveryPort {
    /**
     * DNS 호스팅 존 목록 조회
     */
    Page<CloudResource> listHostedZones(DnsQuery query, CloudSessionCredential session);
    
    /**
     * 특정 DNS 호스팅 존 조회
     */
    Optional<CloudResource> getHostedZone(String zoneId, CloudSessionCredential session);
    
    /**
     * DNS 호스팅 존 상태 조회
     */
    String getZoneStatus(String zoneId, CloudSessionCredential session);
}

Phase 2: AWS 어댑터 구현

2.1 Config 클래스 (adapter/outbound/aws/config/AwsDnsConfig.java)

@Slf4j
@Configuration
@ConditionalOnProperty(name = "aws.enabled", havingValue = "true", matchIfMissing = true)
public class AwsDnsConfig {
    
    /**
     * 세션 자격증명으로 Route53 Client를 생성합니다.
     * 
     * Route53은 글로벌 서비스이므로 리전이 중요하지 않지만,
     * 일관성을 위해 세션의 리전을 사용합니다.
     * 
     * @param session AWS 세션 자격증명
     * @param region AWS 리전 (Route53은 글로벌 서비스이므로 무시될 수 있음)
     * @return Route53Client 인스턴스
     */
    public Route53Client createRoute53Client(CloudSessionCredential session, String region) {
        AwsSessionCredential awsSession = validateAndCastSession(session);
        
        return Route53Client.builder()
                .credentialsProvider(StaticCredentialsProvider.create(toSdkCredentials(awsSession)))
                .region(Region.AWS_GLOBAL)  // Route53은 글로벌 서비스
                .build();
    }
    
    // 기타 메서드들은 AwsRdsConfig와 유사한 패턴
}

2.2 Mapper 클래스 (adapter/outbound/aws/dns/AwsDnsMapper.java)

@Component
public class AwsDnsMapper {
    
    /**
     * AWS HostedZone → CloudResource 변환
     */
    public CloudResource toCloudResource(HostedZone hostedZone, CloudProvider provider, CloudRegion region, CloudService service) {
        return CloudResource.builder()
            .resourceId(hostedZone.id())
            .resourceName(extractZoneName(hostedZone.name()))  // trailing dot 제거
            .displayName(extractZoneName(hostedZone.name()))
            .provider(provider)
            .region(region)
            .service(service)
            .resourceType(CloudResource.ResourceType.DNS_ZONE)
            .lifecycleState(mapLifecycleState(hostedZone))
            .tags(convertTagsToJson(hostedZone))
            .configuration(convertConfigurationToJson(hostedZone))
            .status(mapStatus(hostedZone))
            .createdInCloud(hostedZone.installedFrom() != null ? 
                Instant.from(hostedZone.installedFrom()) : null)
            .lastSync(LocalDateTime.now())
            .build();
    }
    
    /**
     * DnsCreateCommand → CreateHostedZoneRequest 변환
     * CSP 중립적 Command를 AWS SDK 요청으로 변환
     */
    public CreateHostedZoneRequest toCreateRequest(DnsCreateCommand command) {
        var builder = CreateHostedZoneRequest.builder()
            .name(command.zoneName())
            .callerReference(UUID.randomUUID().toString());  // Route53 요구사항
        
        // Private Zone인 경우 VPC 설정
        if ("PRIVATE".equals(command.zoneType()) && command.vpcId() != null) {
            builder.hostedZoneConfig(HostedZoneConfig.builder()
                .privateZone(true)
                .comment(command.comment())
                .build());
            
            builder.vpc(Vpc.builder()
                .vpcId(command.vpcId())
                .vpcRegion(command.region())
                .build());
        } else {
            // Public Zone
            builder.hostedZoneConfig(HostedZoneConfig.builder()
                .privateZone(false)
                .comment(command.comment())
                .build());
        }
        
        return builder.build();
    }
    
    private String extractZoneName(String zoneName) {
        // Route53은 zone name 끝에 trailing dot(.)을 포함
        return zoneName.endsWith(".") ? zoneName.substring(0, zoneName.length() - 1) : zoneName;
    }
    
    // 기타 변환 메서드들...
}

2.3 Management Adapter (adapter/outbound/aws/dns/AwsDnsManagementAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsDnsManagementAdapter implements DnsManagementPort, ProviderScoped {
    
    private final AwsDnsConfig dnsConfig;
    private final AwsDnsMapper mapper;
    private final CloudErrorTranslator errorTranslator;
    
    @Override
    public CloudResource createHostedZone(DnsCreateCommand command) {
        Route53Client route53Client = null;
        try {
            route53Client = dnsConfig.createRoute53Client(command.session(), command.region());
            
            CreateHostedZoneRequest request = mapper.toCreateRequest(command);
            CreateHostedZoneResponse response = route53Client.createHostedZone(request);
            
            // 생성된 호스팅 존 조회
            HostedZone hostedZone = response.hostedZone();
            
            return mapper.toCloudResource(hostedZone, ...);
        } catch (Throwable t) {
            throw errorTranslator.translate(t);
        } finally {
            if (route53Client != null) {
                route53Client.close();
            }
        }
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

2.4 Discovery Adapter (adapter/outbound/aws/dns/AwsDnsDiscoveryAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsDnsDiscoveryAdapter implements DnsDiscoveryPort, ProviderScoped {
    
    private final AwsDnsConfig dnsConfig;
    private final AwsDnsMapper mapper;
    
    @Override
    public Page<CloudResource> listHostedZones(DnsQuery query, CloudSessionCredential session) {
        Route53Client route53Client = null;
        try {
            route53Client = dnsConfig.createRoute53Client(session, null);
            
            // AWS Route53 ListHostedZones 호출
            ListHostedZonesRequest.Builder requestBuilder = ListHostedZonesRequest.builder();
            
            // 필터링 로직 (AWS Route53은 제한적인 필터링 지원)
            if (query.zoneName() != null) {
                // AWS는 prefix 기반 필터링만 지원
                // zoneName으로 시작하는 존만 조회
            }
            
            ListHostedZonesResponse response = route53Client.listHostedZones(requestBuilder.build());
            
            // 페이징 처리
            List<CloudResource> resources = response.hostedZones().stream()
                .map(zone -> mapper.toCloudResource(zone, ...))
                .collect(Collectors.toList());
            
            return new PageImpl<>(resources, PageRequest.of(query.page(), query.size()), response.hostedZones().size());
        } catch (Throwable t) {
            throw errorTranslator.translate(t);
        } finally {
            if (route53Client != null) {
                route53Client.close();
            }
        }
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

Phase 3: Service 계층 구현

3.1 Port Router (service/dns/DnsPortRouter.java)

@Component
@RequiredArgsConstructor
public class DnsPortRouter {
    
    private final Map<ProviderType, DnsManagementPort> managementMap;
    private final Map<ProviderType, DnsDiscoveryPort> discoveryMap;
    
    public DnsManagementPort management(ProviderType type) {
        return require(managementMap, type);
    }
    
    public DnsDiscoveryPort discovery(ProviderType type) {
        return require(discoveryMap, type);
    }
    
    private <T> T require(Map<ProviderType, T> map, ProviderType type) {
        T port = map.get(type);
        if (port == null) {
            throw new BusinessException(CloudErrorCode.CLOUD_PROVIDER_NOT_SUPPORTED,
                "지원하지 않는 프로바이더입니다: " + type);
        }
        return port;
    }
}

3.2 UseCase Service (service/dns/DnsUseCaseService.java)

@Slf4j
@Service
@RequiredArgsConstructor
public class DnsUseCaseService {
    
    private static final String RESOURCE_TYPE = "DNS_ZONE";
    
    private final DnsPortRouter portRouter;
    private final CapabilityGuard capabilityGuard;
    private final AccountCredentialManagementPort credentialPort;
    private final CloudResourceRepository resourceRepository;
    private final CloudResourceManagementHelper resourceHelper;
    
    /**
     * DNS 호스팅 존 생성
     */
    @Transactional
    public CloudResource createHostedZone(DnsCreateRequest request) {
        ProviderType providerType = request.getProviderType();
        String accountScope = request.getAccountScope();
        
        // Capability 검증
        String serviceKey = getServiceKeyForProvider(providerType);
        capabilityGuard.ensureSupported(providerType, serviceKey, RESOURCE_TYPE, Operation.CREATE);
        
        // 세션 획득
        CloudSessionCredential session = getSession(providerType, accountScope);
        
        // Command 변환
        DnsCreateCommand command = toCreateCommand(request, session);
        
        // 어댑터 호출
        CloudResource resource = portRouter.management(providerType).createHostedZone(command);
        
        // DB에 CloudResource 저장
        CloudResource savedResource = resourceHelper.registerResource(
            ResourceRegistrationRequest.builder()
                .resource(resource)
                .tenantKey(TenantContextHolder.getCurrentTenantKeyOrThrow())
                .accountScope(accountScope)
                .build()
        );
        
        return savedResource;
    }
    
    /**
     * DNS 호스팅 존 목록 조회
     */
    @Transactional(readOnly = true)
    public Page<CloudResource> listHostedZones(ProviderType providerType, String accountScope, DnsQueryRequest request) {
        CloudSessionCredential session = getSession(providerType, accountScope);
        DnsQuery query = toQuery(providerType, accountScope, request);
        
        return portRouter.discovery(providerType).listHostedZones(query, session);
    }
    
    // 기타 메서드들...
    
    private CloudSessionCredential getSession(ProviderType providerType, String accountScope) {
        String tenantKey = TenantContextHolder.getCurrentTenantKeyOrThrow();
        return credentialPort.getSession(tenantKey, accountScope, providerType);
    }
    
    private String getServiceKeyForProvider(ProviderType providerType) {
        return switch (providerType) {
            case AWS -> "ROUTE53";
            case AZURE -> "AZURE_DNS";
            case GCP -> "CLOUD_DNS";
            default -> throw new BusinessException(CloudErrorCode.CLOUD_PROVIDER_NOT_SUPPORTED,
                "지원하지 않는 프로바이더입니다: " + providerType);
        };
    }
}

Phase 4: Controller 및 DTO 구현

4.1 DTO 클래스 (dto/)

DnsCreateRequest.java

/**
 * DNS 호스팅 존 생성 요청 DTO (CSP 중립적)
 * 
 * Controller에서 받는 요청 객체로, CSP 중립적인 필드만 포함합니다.
 * CSP 특화 설정은 providerSpecificConfig에 포함됩니다.
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DnsCreateRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String region;
    
    @NotBlank
    @Pattern(regexp = "^[a-z0-9]([a-z0-9\\-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9\\-]{0,61}[a-z0-9])?)*$", 
             message = "유효한 도메인 이름 형식이 아닙니다")
    private String zoneName;  // CSP 중립적: example.com
    
    @NotNull
    @Pattern(regexp = "PUBLIC|PRIVATE", message = "PUBLIC 또는 PRIVATE이어야 합니다")
    private String zoneType;  // CSP 중립적: "PUBLIC", "PRIVATE"
    
    private String vpcId;  // Private Zone인 경우 VPC ID (선택적)
    
    private String comment;  // 호스팅 존 설명
    
    private Map<String, String> tags;
    
    /**
     * CSP별 특화 설정
     * 
     * AWS 예시:
     *   - delegationSetId: "N1234567890" (재사용 가능한 위임 집합 ID)
     * 
     * Azure 예시:
     *   - resourceGroupName: "my-resource-group"
     *   - zoneType: "Public" | "Private"
     *   - registrationVirtualNetworkIds: ["vnet-id-1", "vnet-id-2"]
     * 
     * GCP 예시:
     *   - description: "Managed zone description"
     *   - dnsName: "example.com."
     *   - visibility: "public" | "private"
     *   - privateVisibilityConfig: { networks: [...] }
     */
    private Map<String, Object> providerSpecificConfig;
}

DnsUpdateRequest.java

/**
 * DNS 호스팅 존 수정 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DnsUpdateRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String zoneId;  // 수정할 호스팅 존 ID
    
    private String comment;  // 호스팅 존 설명 변경
    
    private Map<String, String> tagsToAdd;  // 추가할 태그
    
    private Map<String, String> tagsToRemove;  // 제거할 태그
    
    /**
     * CSP별 특화 설정
     */
    private Map<String, Object> providerSpecificConfig;
}

DnsDeleteRequest.java

/**
 * DNS 호스팅 존 삭제 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DnsDeleteRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String zoneId;  // 삭제할 호스팅 존 ID
    
    @Builder.Default
    private Boolean forceDelete = false;  // 레코드가 있어도 강제 삭제 여부
    
    private String reason;  // 삭제 이유 (감사 로그용)
    
    /**
     * CSP별 특화 삭제 옵션
     */
    private Map<String, Object> providerSpecificConfig;
    
    /**
     * 기본 삭제 요청 생성
     */
    public static DnsDeleteRequest basic(String zoneId) {
        return DnsDeleteRequest.builder()
            .zoneId(zoneId)
            .forceDelete(false)
            .build();
    }
}

DnsQueryRequest.java

/**
 * DNS 호스팅 존 조회 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DnsQueryRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    private Set<String> regions;  // 조회할 리전 목록 (Route53은 글로벌 서비스이므로 무시될 수 있음)
    
    private String zoneName;  // 호스팅 존 이름으로 필터링
    
    private String zoneType;  // "PUBLIC", "PRIVATE"로 필터링
    
    private String vpcId;  // Private Zone인 경우 VPC ID로 필터링
    
    private Map<String, String> tags;  // 태그로 필터링
    
    @Min(0)
    @Builder.Default
    private int page = 0;
    
    @Min(1)
    @Max(100)
    @Builder.Default
    private int size = 20;
}

DnsResponse.java

/**
 * DNS 호스팅 존 응답 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DnsResponse {
    
    private Long id;
    private String resourceId;
    private String resourceName;  // zone name (example.com)
    private ProviderType providerType;
    private String region;
    private String zoneType;  // "PUBLIC", "PRIVATE"
    private String vpcId;  // Private Zone인 경우 VPC ID
    private String nameServers;  // 네임서버 목록 (JSON 배열 문자열)
    private String status;
    private String lifecycleState;
    private String comment;  // 호스팅 존 설명
    private Map<String, String> tags;
    private LocalDateTime createdAt;
    private LocalDateTime lastSync;
    
    /**
     * CloudResource → DnsResponse 변환
     */
    public static DnsResponse from(CloudResource resource) {
        // CloudResource의 configuration JSON에서 추가 정보 추출
        return DnsResponse.builder()
            .id(resource.getId())
            .resourceId(resource.getResourceId())
            .resourceName(resource.getResourceName())
            .providerType(resource.getProvider().getProviderType())
            .region(resource.getRegion() != null ? resource.getRegion().getRegionKey() : null)
            .status(resource.getStatus() != null ? resource.getStatus().name() : null)
            .lifecycleState(resource.getLifecycleState() != null ? resource.getLifecycleState().name() : null)
            .tags(parseTags(resource.getTags()))
            .createdAt(resource.getCreatedAt())
            .lastSync(resource.getLastSync())
            .build();
    }
    
    private static Map<String, String> parseTags(String tagsJson) {
        // JSON 문자열을 Map으로 변환
        // 구현 생략
        return Map.of();
    }
}

4.2 Controller (controller/DnsController.java)

@Slf4j
@RestController
@RequestMapping("/api/v1/cloud/providers/{provider}/accounts/{accountScope}/dns/zones")
@RequiredArgsConstructor
@Tag(name = "DNS Management", description = "DNS 호스팅 존 관리 API (멀티 클라우드 지원)")
public class DnsController {
    
    private final DnsUseCaseService dnsUseCaseService;
    
    @PostMapping
    @Operation(summary = "DNS 호스팅 존 생성")
    public ResponseEntity<ApiResponse<DnsResponse>> createHostedZone(
            @PathVariable ProviderType provider,
            @PathVariable String accountScope,
            @Valid @RequestBody DnsCreateRequest request) {
        
        request.setProviderType(provider);
        request.setAccountScope(accountScope);
        
        CloudResource resource = dnsUseCaseService.createHostedZone(request);
        DnsResponse response = DnsResponse.from(resource);
        
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(ApiResponse.success(response, "DNS 호스팅 존 생성에 성공했습니다."));
    }
    
    @GetMapping
    @Operation(summary = "DNS 호스팅 존 목록 조회")
    public ResponseEntity<ApiResponse<Page<DnsResponse>>> listHostedZones(
            @PathVariable ProviderType provider,
            @PathVariable String accountScope,
            @Valid DnsQueryRequest request) {
        
        // 구현...
    }
    
    // 기타 엔드포인트들...
}

Phase 5: Capability 등록

CapabilityRegistry에 DNS 기능 등록

// AwsCapabilityConfig.java에 추가
@Bean
public CspCapability awsDnsCapability() {
    return CspCapability.builder()
        .providerType(ProviderType.AWS)
        .serviceKey("ROUTE53")
        .resourceType("DNS_ZONE")
        .supportsCreate(true)
        .supportsRead(true)
        .supportsUpdate(true)
        .supportsDelete(true)
        .build();
}

고려사항

1. 헥사고날 아키텍처 준수

  • 포트 인터페이스: CSP 독립적인 비즈니스 계약 정의
  • 어댑터 분리: AWS 전용 로직은 adapter/outbound/aws/dns/에만 존재
  • 의존성 방향: Service → Port → Adapter (단방향)
  • 도메인 모델: CloudResource 엔티티 활용

2. CSP 독립성 보장

  • Controller: 특정 CSP에 종속되지 않는 공통 API만 제공
  • Service: CSP별 차이는 Port 인터페이스로 추상화
  • Command: CSP 중립적인 필드만 포함, providerSpecificConfig로 확장 가능

3. 보안 및 자격증명 관리

  • JIT 세션 관리: Service 레벨에서 세션 획득 및 Port에 전달
  • ThreadLocal 자격증명: 멀티 테넌트 환경 지원

4. 리소스 동기화

  • CloudResource 저장: 생성/수정 시 CloudResourceRepository에 저장
  • 동기화 전략:
    • 생성/수정 시 즉시 저장
    • 조회 시 최신 상태 반영 (lastSync 필드 활용)
    • 주기적 동기화 작업 고려 (별도 스케줄러)

5. 에러 처리

  • CloudErrorTranslator: AWS SDK 예외를 도메인 예외로 변환
  • BusinessException: 비즈니스 로직 예외 처리
  • 에러 코드: CloudErrorCode에 DNS 관련 에러 코드 추가

6. 트랜잭션 관리

  • @transactional: Service 메서드에 적절한 트랜잭션 설정
  • 읽기 전용: 조회 메서드는 @Transactional(readOnly = true)

7. Capability 검증

  • CapabilityGuard: 작업 전 CSP 지원 여부 검증
  • 동적 검증: 런타임에 Capability 확인

8. DNS 특화 고려사항

8-1. Route53은 글로벌 서비스

  • Route53은 리전 개념이 없지만, 일관성을 위해 리전 파라미터는 유지
  • AwsDnsConfig에서 Region.AWS_GLOBAL 사용
  • VPC 연결 시에만 리전이 필요

8-2. 호스팅 존 이름 형식

  • Route53은 zone name 끝에 trailing dot(.)을 포함 (예: example.com.)
  • 매퍼에서 trailing dot 제거하여 저장 (CSP 중립적)
  • 표시 시에는 trailing dot 없이 표시

8-3. Private Zone과 VPC 연결

  • Private Zone은 VPC와 연결되어야 함
  • DnsCreateCommandvpcId 필드로 VPC 지정
  • AWS의 경우 Vpc.vpcId()Vpc.vpcRegion() 모두 필요

8-4. 네임서버 정보

  • 호스팅 존 생성 시 네임서버 정보 제공
  • DnsResponsenameServers 필드로 포함
  • configuration JSON에도 저장

8-5. Caller Reference

  • Route53은 중복 생성 방지를 위해 callerReference 필수
  • UUID 생성하여 사용

9. CSP 중립성 보장

  • 필드명 중립화:
    • hostedZoneNamezoneName (모든 CSP 공통)
    • hostedZoneConfig.privateZonezoneType ("PUBLIC", "PRIVATE")
    • vpcIdvpcId (Private Zone 공통)
  • CSP 특화 필드 제거:
    • delegationSetId (AWS 전용) → providerSpecificConfig로 이동
    • resourceGroupName (Azure 전용) → providerSpecificConfig로 이동
  • 확장성: providerSpecificConfig로 CSP별 특화 설정 지원
    • AWS: delegationSetId, hostedZoneConfig
    • Azure: resourceGroupName, zoneType, registrationVirtualNetworkIds
    • GCP: description, visibility, privateVisibilityConfig

10. DNS 레코드 관리 (Phase 2)

Phase 1에서는 호스팅 존 관리에 집중하고, DNS 레코드 관리는 Phase 2에서 구현합니다.

DNS 레코드 타입:

  • A (IPv4 주소)
  • AAAA (IPv6 주소)
  • CNAME (별칭)
  • MX (메일 교환)
  • TXT (텍스트)
  • NS (네임서버)
  • SRV (서비스)
  • PTR (포인터)
  • 기타 레코드 타입

11. 테스트 전략

  • 단위 테스트: 각 Adapter, Service, Controller 테스트
  • 통합 테스트: LocalStack을 활용한 AWS 통합 테스트 (Route53 지원 여부 확인 필요)
  • Mock 테스트: Port 인터페이스 Mock으로 Service 테스트

12. 성능 고려사항

  • 페이징: 대량 조회 시 페이징 처리
  • 비동기 처리: Route53 작업은 대부분 동기이지만, 대량 작업 시 비동기 고려
  • 캐싱: 자주 조회되는 호스팅 존 정보 캐싱 고려

13. 모니터링 및 로깅

  • 구조화된 로깅: 모든 레이어에서 일관된 로깅
  • 메트릭: DNS 작업 성공/실패 메트릭 수집
  • 감사 로그: DNS 존 생성/삭제 등 중요 작업 감사 로깅

구현 체크리스트

Phase 1: 포트 및 모델

  • DnsCreateCommand 정의
  • DnsUpdateCommand 정의
  • DnsDeleteCommand 정의
  • DnsQuery 정의
  • GetDnsCommand 정의
  • DnsManagementPort 인터페이스 정의
  • DnsDiscoveryPort 인터페이스 정의

Phase 2: AWS 어댑터

  • AwsDnsConfig 클래스 생성
  • AwsDnsMapper 클래스 생성
  • AwsDnsManagementAdapter 구현
  • AwsDnsDiscoveryAdapter 구현
  • ProviderScoped 인터페이스 구현

Phase 3: Service 계층

  • DnsPortRouter 생성
  • DnsUseCaseService 구현
  • Capability 검증 로직 추가
  • JIT 세션 관리 구현
  • 리소스 저장 로직 구현

Phase 4: Controller 및 DTO

  • DnsCreateRequest DTO 생성
  • DnsUpdateRequest DTO 생성
  • DnsDeleteRequest DTO 생성
  • DnsQueryRequest DTO 생성
  • DnsResponse DTO 생성
  • DnsController 구현
  • API 문서화 (Swagger)

Phase 5: Capability 및 설정

  • CapabilityRegistry에 DNS 등록
  • 설정 파일 업데이트
  • Bean 등록 확인

Phase 6: 테스트

  • 단위 테스트 작성
  • 통합 테스트 작성
  • API 테스트

참고 자료

Phase 2 예정: DNS 레코드 관리

DNS 호스팅 존 관리 구현 후, DNS 레코드 관리 기능을 Phase 2로 구현합니다.

Phase 2 주요 기능

  1. DNS 레코드 생성

    • A, AAAA, CNAME, MX, TXT 등 다양한 레코드 타입 지원
    • TTL 설정
    • 라운드 로빈 지원 (여러 값)
  2. DNS 레코드 조회

    • 호스팅 존별 레코드 목록 조회
    • 레코드 타입별 필터링
    • 레코드 이름별 필터링
  3. DNS 레코드 수정

    • 레코드 값 변경
    • TTL 변경
  4. DNS 레코드 삭제

    • 특정 레코드 삭제

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions