Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fcf17fc
feat: Rdbms 관리를 위한 Command 모델 정의
hyobin-yang Dec 18, 2025
10d1487
feat: Rdbms 관리를 위한 Port 인터페이스 정의
hyobin-yang Dec 18, 2025
09cd588
chore: AWS RDS SDK 의존성 추가
hyobin-yang Dec 18, 2025
971d3ad
feat: AWS RDS 클라이언트 설정 클래스 구현
hyobin-yang Dec 18, 2025
3ea303b
feat: AWS RDS 매퍼 구현
hyobin-yang Dec 18, 2025
a39963b
feat: AWS RDS 관리 어댑터 구현
hyobin-yang Dec 18, 2025
983c4e1
feat: RDBMS 요청/응답 DTO 클래스 구현
hyobin-yang Dec 18, 2025
648cdfb
refactor: AwsS3Config를 JIT 세션 관리 패턴으로 리팩토링
hyobin-yang Dec 18, 2025
eba964d
refactor(audit): 통일된 CLOUD_PROVIDER 리소스 타입 사용
hyobin-yang Dec 18, 2025
8eed9e4
refactor(audit): ObjectStorageController 리소스 타입을 CLOUD_PROVIDER로 변경
hyobin-yang Dec 18, 2025
52f6315
feat: RDS Capability 등록
hyobin-yang Dec 18, 2025
1287577
refactor: RDS 생명주기 관리 로직 재사용성 확보
hyobin-yang Dec 18, 2025
1dca93a
feat: RDBMS 관리 컨트롤러 및 라우터 구현
hyobin-yang Dec 18, 2025
c250e03
feat: RDBMS 관리 유스케이스 구현
hyobin-yang Dec 18, 2025
7271b70
feat: adminPassword 암호화/복호화 로직 구현
hyobin-yang Dec 18, 2025
750796a
feat: CSP 암호화/복호화 관련 에러코드 추가
hyobin-yang Dec 18, 2025
3e79b11
refactor: CloudErrorTranslator가 커스텀 예외를 처리하도록 수정
hyobin-yang Dec 18, 2025
2ddd74c
test: 암호화/복호화 관련 테스트 코드 추가
hyobin-yang Dec 18, 2025
cbf595f
refactor: Rdbms 관련 DB 로직 추가
hyobin-yang Dec 18, 2025
2d188d1
test: 유스케이스 및 DB 동기화 테스트 추가
hyobin-yang Dec 18, 2025
c915f67
fix: 필요한 stubbing 추가
hyobin-yang Dec 18, 2025
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
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@
<artifactId>resourcegroupstaggingapi</artifactId>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>rds</artifactId>
</dependency>

<!-- AWS SDK v2 for Java - EC2 only -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ public enum AuditResourceType {
CLOUD_ACCOUNT("클라우드계정"),
TARGETING_RULE("타겟팅규칙"),
PLATFORM_CONFIG("플랫폼설정"),
S3_BUCKET("S3버킷"),
OBJECT_STORAGE_CONTAINER("오브젝트스토리지컨테이너"),
UI("사용자인터페이스");

private final String description;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,26 @@ public void initializeS3BucketCapabilities() {

log.info("AWS S3 Bucket capabilities registered successfully - AWS|S3|BUCKET");
}

@PostConstruct
public void initializeRdsCapabilities() {
log.info("Registering AWS RDS capabilities...");

CspCapability awsRdsCapability = CspCapability.builder()
.supportsStart(true) // RDS 인스턴스 시작 지원 (일부 엔진만 지원)
.supportsStop(true) // RDS 인스턴스 중지 지원 (일부 엔진만 지원)
.supportsTerminate(true) // RDS 인스턴스 삭제 지원
.supportsTagging(true) // AWS RDS 인스턴스 태그 지원
.supportsListByTag(true) // 태그 기반 목록 조회 지원
.build();

capabilityRegistry.register(
CloudProvider.ProviderType.AWS,
"RDS", // 서비스 타입
"DATABASE", // 리소스 타입
awsRdsCapability
);

log.info("AWS RDS capabilities registered successfully - AWS|RDS|DATABASE");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.agenticcp.core.domain.cloud.adapter.outbound.aws.config;

import com.agenticcp.core.common.exception.BusinessException;
import com.agenticcp.core.domain.cloud.adapter.outbound.aws.account.AwsSessionCredential;
import com.agenticcp.core.domain.cloud.exception.CloudErrorCode;
import com.agenticcp.core.domain.cloud.port.model.account.CloudSessionCredential;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.rds.RdsClient;

/**
* AWS RDS 설정 클래스
*
* 세션 자격증명 기반으로 RdsClient를 생성합니다.
*
* @author AgenticCP Team
* @version 1.0.0
*/
@Slf4j
@Configuration
@ConditionalOnProperty(name = "aws.enabled", havingValue = "true", matchIfMissing = true)
public class AwsRdsConfig {

/**
* 세션 자격증명으로 RDS Client를 생성합니다.
*
* @param session AWS 세션 자격증명
* @param region AWS 리전 (RDS는 리전이 중요하므로 명시적 전달 권장, null이면 세션 리전 사용)
* @return RdsClient 인스턴스
*/
public RdsClient createRdsClient(CloudSessionCredential session, String region) {
AwsSessionCredential awsSession = validateAndCastSession(session);
String targetRegion = region != null ? region : awsSession.getRegion();

return RdsClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(toSdkCredentials(awsSession)))
.region(Region.of(resolveRegion(targetRegion)))
.build();
}

/**
* 리전 기본값 처리
*
* @param region AWS 리전 (null이거나 빈 문자열이면 us-east-1 반환)
* @return 처리된 리전 문자열
*/
private String resolveRegion(String region) {
return (region != null && !region.isBlank()) ? region : "us-east-1";
}

/**
* 세션 검증 및 AWS 세션으로 캐스팅
*
* @param session 도메인 세션 자격증명
* @return AWS 세션 자격증명
* @throws BusinessException 세션이 유효하지 않거나 AWS 세션이 아닌 경우
*/
private AwsSessionCredential validateAndCastSession(CloudSessionCredential session) {
if (session == null) {
throw new BusinessException(CloudErrorCode.CLOUD_CONNECTION_FAILED, "세션 자격증명이 필요합니다.");
}
if (!(session instanceof AwsSessionCredential awsSession)) {
throw new BusinessException(CloudErrorCode.CLOUD_CONNECTION_FAILED,
"AWS 세션 자격증명이 필요합니다. 제공된 타입: " + session.getClass().getSimpleName());
}
if (!awsSession.isValid()) {
throw new BusinessException(CloudErrorCode.CLOUD_CONNECTION_FAILED,
"세션이 만료되었습니다. expiresAt: " + awsSession.getExpiresAt());
}
return awsSession;
}

/**
* 도메인 세션 객체를 AWS SDK 세션 객체로 변환
*
* @param session AWS 도메인 세션 자격증명
* @return AWS SDK 세션 자격증명
*/
private AwsSessionCredentials toSdkCredentials(AwsSessionCredential session) {
return AwsSessionCredentials.create(
session.getAccessKeyId(),
session.getSecretAccessKey(),
session.getSessionToken()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,37 @@

import com.agenticcp.core.common.exception.BusinessException;
import com.agenticcp.core.domain.cloud.adapter.outbound.aws.account.AwsSessionCredential;
import com.agenticcp.core.domain.cloud.adapter.outbound.common.ThreadLocalCredentialCache;
import com.agenticcp.core.common.context.TenantContextHolder;
import com.agenticcp.core.domain.cloud.exception.CloudErrorCode;
import com.agenticcp.core.domain.cloud.port.model.account.CloudSessionCredential;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClient;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.sts.StsClient;

import java.net.URI;

/**
* AWS S3 설정 클래스
*
* S3Client, ResourceGroupsTaggingApiClient, StsClient Bean을 생성합니다.
* ThreadLocal 기반 자격증명을 사용하여 멀티 테넌트 환경을 지원합니다.
* 세션 자격증명 기반으로 S3Client, ResourceGroupsTaggingApiClient를 생성합니다.
* StsClient Bean을 생성합니다.
*
* @author AgenticCP Team
* @version 1.0.0
*/
@Slf4j
@Configuration
@ConditionalOnProperty(name = "aws.enabled", havingValue = "true", matchIfMissing = true)
public class AwsS3Config {

@Value("${aws.s3.region:us-east-1}")
private String defaultRegion;

@Value("${aws.s3.endpoint:}")
private String endpoint;

/**
* S3Client Bean을 생성합니다.
* ThreadLocal 기반 자격증명을 사용하여 동적 자격증명을 지원합니다.
*
* @return S3Client 인스턴스
*/
@Bean
public S3Client s3Client() {
var builder = S3Client.builder()
.region(Region.of(resolveRegion(defaultRegion)))
.credentialsProvider(createThreadLocalCredentialsProvider());

// 커스텀 엔드포인트 설정 (LocalStack 등 테스트 환경용)
if (endpoint != null && !endpoint.isEmpty()) {
builder.endpointOverride(URI.create(endpoint));
}
return builder.build();
}

/**
* 세션 자격증명으로 S3 Client를 생성합니다.
*
Expand All @@ -75,26 +50,6 @@ public S3Client createS3Client(CloudSessionCredential session, String region) {
.build();
}

/**
* ResourceGroupsTaggingApiClient Bean을 생성합니다.
* 태그 기반 리소스 조회에 사용됩니다.
* ThreadLocal 기반 자격증명을 사용하여 동적 자격증명을 지원합니다.
*
* @return ResourceGroupsTaggingApiClient 인스턴스
*/
@Bean
public ResourceGroupsTaggingApiClient resourceGroupsTaggingApiClient() {
var builder = ResourceGroupsTaggingApiClient.builder()
.region(Region.of(resolveRegion(defaultRegion)))
.credentialsProvider(createThreadLocalCredentialsProvider());

// 커스텀 엔드포인트 설정 (LocalStack 등 테스트 환경용)
if (endpoint != null && !endpoint.isEmpty()) {
builder.endpointOverride(URI.create(endpoint));
}
return builder.build();
}

/**
* 세션 자격증명으로 ResourceGroupsTaggingApiClient를 생성합니다.
* 태그 기반 리소스 조회에 사용됩니다.
Expand Down Expand Up @@ -135,35 +90,6 @@ public StsClient stsClient() {
private String resolveRegion(String region) {
return (region != null && !region.isBlank()) ? region : "us-east-1";
}

/**
* ThreadLocal 기반 자격증명 제공자 생성
*
* 현재 스레드의 ThreadLocal 캐시에서 자격증명을 조회하여 제공합니다.
* 멀티 테넌트 환경에서 각 요청별로 다른 자격증명을 사용할 수 있도록 합니다.
*/
private AwsCredentialsProvider createThreadLocalCredentialsProvider() {
return () -> {
try {
// 현재 테넌트 키 조회
String tenantKey = TenantContextHolder.getCurrentTenantKey();

if (tenantKey == null) {
log.warn("테넌트 컨텍스트가 설정되지 않음 - 기본 자격증명 사용");
return null; // 기본 자격증명 사용
}

// ThreadLocal 캐시에서 자격증명 조회
// 현재는 첫 번째 캐시된 자격증명을 반환
// TODO: 실제로는 요청 컨텍스트에서 프로바이더 타입과 계정 스코프를 가져와야 함
return ThreadLocalCredentialCache.getFirstCredentials();

} catch (Exception e) {
log.error("ThreadLocal 자격증명 조회 실패: {}", e.getMessage(), e);
return null; // 기본 자격증명 사용
}
};
}

/**
* 세션 검증 및 AWS 세션으로 캐스팅
Expand Down
Loading