-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
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와 연결되어야 함
DnsCreateCommand의vpcId필드로 VPC 지정- AWS의 경우
Vpc.vpcId()와Vpc.vpcRegion()모두 필요
8-4. 네임서버 정보
- 호스팅 존 생성 시 네임서버 정보 제공
DnsResponse에nameServers필드로 포함configurationJSON에도 저장
8-5. Caller Reference
- Route53은 중복 생성 방지를 위해
callerReference필수 - UUID 생성하여 사용
9. CSP 중립성 보장
- ✅ 필드명 중립화:
hostedZoneName→zoneName(모든 CSP 공통)hostedZoneConfig.privateZone→zoneType("PUBLIC", "PRIVATE")vpcId→vpcId(Private Zone 공통)
- ✅ CSP 특화 필드 제거:
delegationSetId(AWS 전용) →providerSpecificConfig로 이동resourceGroupName(Azure 전용) →providerSpecificConfig로 이동
- ✅ 확장성:
providerSpecificConfig로 CSP별 특화 설정 지원- AWS:
delegationSetId,hostedZoneConfig등 - Azure:
resourceGroupName,zoneType,registrationVirtualNetworkIds등 - GCP:
description,visibility,privateVisibilityConfig등
- AWS:
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
DnsCreateRequestDTO 생성DnsUpdateRequestDTO 생성DnsDeleteRequestDTO 생성DnsQueryRequestDTO 생성DnsResponseDTO 생성DnsController구현- API 문서화 (Swagger)
Phase 5: Capability 및 설정
- CapabilityRegistry에 DNS 등록
- 설정 파일 업데이트
- Bean 등록 확인
Phase 6: 테스트
- 단위 테스트 작성
- 통합 테스트 작성
- API 테스트
참고 자료
- 헥사고날 아키텍처 가이드
- RDS 구현 예시 (Issue [FEATURE] AWS RDS 자원 관리 기능 구현 #179)
- VM 구현 예시
- VPC 구현 예시
- AWS SDK Route53 문서
- Route53 API 참조
Phase 2 예정: DNS 레코드 관리
DNS 호스팅 존 관리 구현 후, DNS 레코드 관리 기능을 Phase 2로 구현합니다.
Phase 2 주요 기능
-
DNS 레코드 생성
- A, AAAA, CNAME, MX, TXT 등 다양한 레코드 타입 지원
- TTL 설정
- 라운드 로빈 지원 (여러 값)
-
DNS 레코드 조회
- 호스팅 존별 레코드 목록 조회
- 레코드 타입별 필터링
- 레코드 이름별 필터링
-
DNS 레코드 수정
- 레코드 값 변경
- TTL 변경
-
DNS 레코드 삭제
- 특정 레코드 삭제