Skip to content

[FEATURE] AWS Lambda(Serverless Function) 관리 기능 구현 #182

@hyobin-yang

Description

@hyobin-yang

AWS Serverless Function(Lambda) 자원 관리 기능 구현 계획

개요

AWS Lambda, Azure Functions, GCP Cloud Functions를 헥사고날 아키텍처로 구현합니다.
도메인 추상화명은 FUNCTION(Serverless Function)을 사용합니다.

추상화명

  • FUNCTION
    • Serverless Function의 일반적인 용어
    • AWS Lambda, Azure Functions, GCP Cloud Functions 모두를 포괄하는 추상화명
    • 다른 CSP에서도 자연스럽게 매핑 가능

아키텍처 구조

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

domain/cloud/
├── adapter/outbound/aws/function/      # AWS Lambda 전용 어댑터
│   ├── AwsFunctionManagementAdapter.java   # 함수 생명주기 관리
│   ├── AwsFunctionDiscoveryAdapter.java    # 함수 조회
│   ├── AwsFunctionInvocationAdapter.java   # 함수 실행
│   ├── AwsFunctionMapper.java              # AWS SDK ↔ CloudResource 변환
│   └── ...
├── adapter/outbound/aws/config/
│   └── AwsFunctionConfig.java              # Lambda 클라이언트 설정
├── controller/
│   └── FunctionController.java             # REST API (CSP 독립적)
├── dto/
│   ├── FunctionCreateRequest.java
│   ├── FunctionUpdateRequest.java
│   ├── FunctionDeleteRequest.java
│   ├── FunctionQueryRequest.java
│   ├── FunctionInvokeRequest.java
│   └── FunctionResponse.java
├── port/model/function/                  # Command 객체
│   ├── FunctionCreateCommand.java
│   ├── FunctionUpdateCommand.java
│   ├── FunctionDeleteCommand.java
│   ├── FunctionQuery.java
│   ├── FunctionInvokeCommand.java
│   └── GetFunctionCommand.java
├── port/outbound/function/               # 포트 인터페이스
│   ├── FunctionManagementPort.java       # CRUD 작업
│   ├── FunctionDiscoveryPort.java        # 조회 작업
│   └── FunctionInvocationPort.java       # 함수 실행
├── service/function/
│   ├── FunctionUseCaseService.java       # 유스케이스 서비스 (CSP 독립적)
│   └── FunctionPortRouter.java           # 포트 라우터
└── repository/
    └── CloudResourceRepository.java      # 기존 리포지토리 활용

구현 단계

Phase 1: 포트 및 모델 정의

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

FunctionCreateCommand.java

/**
 * Serverless Function 생성 도메인 커맨드 (CSP 중립적)
 * 
 * UseCase Service에서 Adapter로 전달되는 내부 명령 모델입니다.
 * CSP 중립적인 필드를 사용하며, 각 CSP Adapter의 Mapper에서 CSP 특화 요청으로 변환합니다.
 * 
 * 필드 매핑 예시:
 * - functionName: AWS(FunctionName), Azure(FunctionName), GCP(name)
 * - runtime: AWS(Runtime), Azure(runtime), GCP(runtime)
 * - handler: AWS(Handler), Azure(scriptFile.entryPoint), GCP(entryPoint)
 * - memorySize: AWS(MemorySize), Azure(functionAppConfig), GCP(availableMemoryMb)
 * - timeout: AWS(Timeout), Azure(functionTimeout), GCP(timeout)
 */
@Builder
public record FunctionCreateCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String serviceKey,           // "LAMBDA", "AZURE_FUNCTIONS", "CLOUD_FUNCTIONS"
    String resourceType,         // "FUNCTION"
    String functionName,         // CSP 중립적: AWS(FunctionName), Azure(FunctionName), GCP(name)
    String runtime,              // CSP 중립적: "nodejs18.x", "python3.11", "java17", "go1.x"
    String handler,              // CSP 중립적: AWS(Handler), Azure(scriptFile.entryPoint), GCP(entryPoint)
    Integer memorySize,          // MB (모든 CSP 공통)
    Integer timeout,             // 초 (모든 CSP 공통, 최대값 CSP별로 다를 수 있음)
    String roleArn,              // CSP 중립적: AWS(Role ARN), Azure(identity), GCP(serviceAccountEmail)
    Map<String, String> environmentVariables,  // 환경 변수
    String description,          // 함수 설명
    String vpcId,                // VPC 연결 (선택적, CSP별 구현 다를 수 있음)
    String codeUri,              // 코드 URI (S3, Blob Storage, GCS 등)
    byte[] codeZip,              // 코드 ZIP 바이너리 (선택적, URI 대신 사용)
    Map<String, String> tags,
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 설정
    CloudSessionCredential session
) {}

FunctionUpdateCommand.java

/**
 * Serverless Function 수정 도메인 커맨드 (CSP 중립적)
 */
@Builder
public record FunctionUpdateCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,   // 함수 ARN/ID (CSP별 형식 다를 수 있음)
    String runtime,              // 런타임 변경
    String handler,              // 핸들러 변경
    Integer memorySize,          // 메모리 크기 변경 (MB)
    Integer timeout,             // 타임아웃 변경 (초)
    String roleArn,              // 실행 역할 변경
    Map<String, String> environmentVariables,  // 환경 변수 변경
    String description,          // 설명 변경
    String codeUri,              // 코드 업데이트 URI
    byte[] codeZip,              // 코드 ZIP 바이너리
    Map<String, String> tags,
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 설정
    CloudSessionCredential session
) {}

FunctionDeleteCommand.java

/**
 * Serverless Function 삭제 도메인 커맨드 (CSP 중립적)
 */
@Builder
public record FunctionDeleteCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 삭제 옵션
    CloudSessionCredential session
) {}

FunctionQuery.java

/**
 * Serverless Function 조회 쿼리 (CSP 중립적)
 */
@Builder
public record FunctionQuery(
    ProviderType providerType,
    String accountScope,
    Set<String> regions,
    String functionName,         // 함수 이름으로 필터링 (CSP 중립적)
    String runtime,              // 런타임으로 필터링
    String vpcId,                // VPC ID로 필터링
    Map<String, String> tagsEquals,  // 태그로 필터링
    int page,
    int size
) {}

FunctionInvokeCommand.java

/**
 * Serverless Function 실행 커맨드 (CSP 중립적)
 */
@Builder
public record FunctionInvokeCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,   // 함수 ARN/ID
    String invocationType,       // "RequestResponse", "Event", "DryRun"
    String payload,              // JSON 문자열 페이로드
    String qualifier,            // 함수 버전/별칭 (선택적)
    Map<String, String> context,  // 추가 컨텍스트 정보
    String tenantKey,
    CloudSessionCredential session
) {}

GetFunctionCommand.java

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

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

FunctionManagementPort.java

public interface FunctionManagementPort {
    /**
     * Serverless Function 생성
     */
    CloudResource createFunction(FunctionCreateCommand command);
    
    /**
     * Serverless Function 수정
     */
    CloudResource updateFunction(FunctionUpdateCommand command);
    
    /**
     * Serverless Function 삭제
     */
    void deleteFunction(FunctionDeleteCommand command);
}

FunctionDiscoveryPort.java

public interface FunctionDiscoveryPort {
    /**
     * Serverless Function 목록 조회
     */
    Page<CloudResource> listFunctions(FunctionQuery query, CloudSessionCredential session);
    
    /**
     * 특정 Serverless Function 조회
     */
    Optional<CloudResource> getFunction(String functionId, CloudSessionCredential session);
    
    /**
     * Serverless Function 상태 조회
     */
    String getFunctionStatus(String functionId, CloudSessionCredential session);
    
    /**
     * Serverless Function 코드 조회
     */
    byte[] getFunctionCode(String functionId, CloudSessionCredential session);
}

FunctionInvocationPort.java

public interface FunctionInvocationPort {
    /**
     * Serverless Function 실행
     * 
     * @param command 실행 커맨드
     * @return 실행 결과 (JSON 문자열)
     */
    String invokeFunction(FunctionInvokeCommand command);
    
    /**
     * Serverless Function 비동기 실행
     * 
     * @param command 실행 커맨드
     * @return 요청 ID (추적용)
     */
    String invokeFunctionAsync(FunctionInvokeCommand command);
}

Phase 2: AWS 어댑터 구현

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

@Slf4j
@Configuration
@ConditionalOnProperty(name = "aws.enabled", havingValue = "true", matchIfMissing = true)
public class AwsFunctionConfig {
    
    /**
     * 세션 자격증명으로 Lambda Client를 생성합니다.
     * 
     * @param session AWS 세션 자격증명
     * @param region AWS 리전 (Lambda는 리전별로 관리)
     * @return LambdaClient 인스턴스
     */
    public LambdaClient createLambdaClient(CloudSessionCredential session, String region) {
        AwsSessionCredential awsSession = validateAndCastSession(session);
        String targetRegion = region != null ? region : awsSession.getRegion();

        return LambdaClient.builder()
                .credentialsProvider(StaticCredentialsProvider.create(toSdkCredentials(awsSession)))
                .region(Region.of(resolveRegion(targetRegion)))
                .build();
    }
    
    // 기타 메서드들은 AwsRdsConfig와 유사한 패턴
}

2.2 Mapper 클래스 (adapter/outbound/aws/function/AwsFunctionMapper.java)

@Component
public class AwsFunctionMapper {
    
    /**
     * AWS FunctionConfiguration → CloudResource 변환
     */
    public CloudResource toCloudResource(FunctionConfiguration functionConfig, CloudProvider provider, CloudRegion region, CloudService service) {
        return CloudResource.builder()
            .resourceId(functionConfig.functionArn())
            .resourceName(functionConfig.functionName())
            .displayName(functionConfig.functionName())
            .provider(provider)
            .region(region)
            .service(service)
            .resourceType(CloudResource.ResourceType.FUNCTION)
            .lifecycleState(mapLifecycleState(functionConfig.state()))
            .memoryGb(functionConfig.memorySize() != null ? functionConfig.memorySize() / 1024 : null)  // MB → GB
            .instanceType(functionConfig.runtime())  // 런타임을 instanceType에 저장
            .tags(convertTagsToJson(functionConfig.tags()))
            .configuration(convertConfigurationToJson(functionConfig))
            .status(mapStatus(functionConfig.state()))
            .createdInCloud(functionConfig.lastModified() != null ? 
                Instant.from(functionConfig.lastModified()) : null)
            .lastSync(LocalDateTime.now())
            .build();
    }
    
    /**
     * FunctionCreateCommand → CreateFunctionRequest 변환
     * CSP 중립적 Command를 AWS SDK 요청으로 변환
     */
    public CreateFunctionRequest toCreateRequest(FunctionCreateCommand command) {
        var builder = CreateFunctionRequest.builder()
            .functionName(command.functionName())
            .runtime(command.runtime())
            .handler(command.handler())
            .memorySize(command.memorySize())
            .timeout(command.timeout())
            .role(command.roleArn())
            .description(command.description());
        
        // 환경 변수 설정
        if (command.environmentVariables() != null && !command.environmentVariables().isEmpty()) {
            builder.environment(Environment.builder()
                .variables(command.environmentVariables())
                .build());
        }
        
        // VPC 설정
        if (command.vpcId() != null) {
            // providerSpecificConfig에서 VPC 세부 정보 추출
            VpcConfig vpcConfig = getVpcConfig(command);
            builder.vpcConfig(vpcConfig);
        }
        
        // 코드 설정
        if (command.codeZip() != null) {
            builder.code(FunctionCode.builder()
                .zipFile(SdkBytes.fromByteArray(command.codeZip()))
                .build());
        } else if (command.codeUri() != null) {
            // S3 URI 파싱
            S3CodeLocation s3Location = parseS3Uri(command.codeUri());
            builder.code(FunctionCode.builder()
                .s3Bucket(s3Location.bucket())
                .s3Key(s3Location.key())
                .build());
        }
        
        // 태그 설정
        if (command.tags() != null && !command.tags().isEmpty()) {
            builder.tags(command.tags());
        }
        
        return builder.build();
    }
    
    private VpcConfig getVpcConfig(FunctionCreateCommand command) {
        // providerSpecificConfig에서 VPC 설정 추출
        // AWS의 경우: subnetIds, securityGroupIds 필요
        if (command.providerSpecificConfig() != null) {
            @SuppressWarnings("unchecked")
            List<String> subnetIds = (List<String>) command.providerSpecificConfig().get("subnetIds");
            @SuppressWarnings("unchecked")
            List<String> securityGroupIds = (List<String>) command.providerSpecificConfig().get("securityGroupIds");
            
            if (subnetIds != null || securityGroupIds != null) {
                return VpcConfig.builder()
                    .subnetIds(subnetIds != null ? subnetIds : List.of())
                    .securityGroupIds(securityGroupIds != null ? securityGroupIds : List.of())
                    .build();
            }
        }
        return null;
    }
    
    // 기타 변환 메서드들...
}

2.3 Management Adapter (adapter/outbound/aws/function/AwsFunctionManagementAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsFunctionManagementAdapter implements FunctionManagementPort, ProviderScoped {
    
    private final AwsFunctionConfig functionConfig;
    private final AwsFunctionMapper mapper;
    private final CloudErrorTranslator errorTranslator;
    
    @Override
    public CloudResource createFunction(FunctionCreateCommand command) {
        LambdaClient lambdaClient = null;
        try {
            lambdaClient = functionConfig.createLambdaClient(command.session(), command.region());
            
            CreateFunctionRequest request = mapper.toCreateRequest(command);
            CreateFunctionResponse response = lambdaClient.createFunction(request);
            
            // 생성된 함수 조회
            FunctionConfiguration functionConfig = response.configuration();
            
            return mapper.toCloudResource(functionConfig, ...);
        } catch (Throwable t) {
            throw errorTranslator.translate(t);
        } finally {
            if (lambdaClient != null) {
                lambdaClient.close();
            }
        }
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

2.4 Discovery Adapter (adapter/outbound/aws/function/AwsFunctionDiscoveryAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsFunctionDiscoveryAdapter implements FunctionDiscoveryPort, ProviderScoped {
    
    private final AwsFunctionConfig functionConfig;
    private final AwsFunctionMapper mapper;
    private final CloudErrorTranslator errorTranslator;
    
    @Override
    public Page<CloudResource> listFunctions(FunctionQuery query, CloudSessionCredential session) {
        LambdaClient lambdaClient = null;
        try {
            lambdaClient = functionConfig.createLambdaClient(session, query.regions().iterator().next());
            
            // AWS Lambda ListFunctions 호출
            ListFunctionsRequest.Builder requestBuilder = ListFunctionsRequest.builder();
            
            // 필터링 로직
            if (query.functionName() != null) {
                // AWS는 prefix 기반 필터링만 지원
                requestBuilder.functionVersion("ALL");
            }
            
            ListFunctionsResponse response = lambdaClient.listFunctions(requestBuilder.build());
            
            // 페이징 처리
            List<CloudResource> resources = response.functions().stream()
                .filter(function -> matchesQuery(function, query))
                .map(function -> mapper.toCloudResource(function, ...))
                .collect(Collectors.toList());
            
            return new PageImpl<>(resources, PageRequest.of(query.page(), query.size()), resources.size());
        } catch (Throwable t) {
            throw errorTranslator.translate(t);
        } finally {
            if (lambdaClient != null) {
                lambdaClient.close();
            }
        }
    }
    
    private boolean matchesQuery(FunctionConfiguration function, FunctionQuery query) {
        // 쿼리 조건에 맞는지 필터링
        if (query.functionName() != null && !function.functionName().startsWith(query.functionName())) {
            return false;
        }
        if (query.runtime() != null && !query.runtime().equals(function.runtime())) {
            return false;
        }
        return true;
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

2.5 Invocation Adapter (adapter/outbound/aws/function/AwsFunctionInvocationAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsFunctionInvocationAdapter implements FunctionInvocationPort, ProviderScoped {
    
    private final AwsFunctionConfig functionConfig;
    private final CloudErrorTranslator errorTranslator;
    
    @Override
    public String invokeFunction(FunctionInvokeCommand command) {
        LambdaClient lambdaClient = null;
        try {
            lambdaClient = functionConfig.createLambdaClient(command.session(), command.region());
            
            InvokeRequest.Builder requestBuilder = InvokeRequest.builder()
                .functionName(command.providerResourceId())
                .payload(SdkBytes.fromUtf8String(command.payload()));
            
            // InvocationType 설정
            if ("Event".equals(command.invocationType())) {
                requestBuilder.invocationType(InvocationType.EVENT);
            } else if ("DryRun".equals(command.invocationType())) {
                requestBuilder.invocationType(InvocationType.DRY_RUN);
            } else {
                requestBuilder.invocationType(InvocationType.REQUEST_RESPONSE);
            }
            
            // Qualifier 설정 (버전/별칭)
            if (command.qualifier() != null) {
                requestBuilder.qualifier(command.qualifier());
            }
            
            InvokeResponse response = lambdaClient.invoke(requestBuilder.build());
            
            return response.payload().asUtf8String();
        } catch (Throwable t) {
            throw errorTranslator.translate(t);
        } finally {
            if (lambdaClient != null) {
                lambdaClient.close();
            }
        }
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

Phase 3: Service 계층 구현

3.1 Port Router (service/function/FunctionPortRouter.java)

@Component
@RequiredArgsConstructor
public class FunctionPortRouter {
    
    private final Map<ProviderType, FunctionManagementPort> managementMap;
    private final Map<ProviderType, FunctionDiscoveryPort> discoveryMap;
    private final Map<ProviderType, FunctionInvocationPort> invocationMap;
    
    public FunctionManagementPort management(ProviderType type) {
        return require(managementMap, type);
    }
    
    public FunctionDiscoveryPort discovery(ProviderType type) {
        return require(discoveryMap, type);
    }
    
    public FunctionInvocationPort invocation(ProviderType type) {
        return require(invocationMap, 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/function/FunctionUseCaseService.java)

@Slf4j
@Service
@RequiredArgsConstructor
public class FunctionUseCaseService {
    
    private static final String RESOURCE_TYPE = "FUNCTION";
    
    private final FunctionPortRouter portRouter;
    private final CapabilityGuard capabilityGuard;
    private final AccountCredentialManagementPort credentialPort;
    private final CloudResourceRepository resourceRepository;
    private final CloudResourceManagementHelper resourceHelper;
    
    /**
     * Serverless Function 생성
     */
    @Transactional
    public CloudResource createFunction(FunctionCreateRequest 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 변환
        FunctionCreateCommand command = toCreateCommand(request, session);
        
        // 어댑터 호출
        CloudResource resource = portRouter.management(providerType).createFunction(command);
        
        // DB에 CloudResource 저장
        CloudResource savedResource = resourceHelper.registerResource(
            ResourceRegistrationRequest.builder()
                .resource(resource)
                .tenantKey(TenantContextHolder.getCurrentTenantKeyOrThrow())
                .accountScope(accountScope)
                .build()
        );
        
        return savedResource;
    }
    
    /**
     * Serverless Function 목록 조회
     */
    @Transactional(readOnly = true)
    public Page<CloudResource> listFunctions(ProviderType providerType, String accountScope, FunctionQueryRequest request) {
        CloudSessionCredential session = getSession(providerType, accountScope);
        FunctionQuery query = toQuery(providerType, accountScope, request);
        
        return portRouter.discovery(providerType).listFunctions(query, session);
    }
    
    /**
     * Serverless Function 실행
     */
    @Transactional
    public String invokeFunction(ProviderType providerType, String accountScope, FunctionInvokeRequest request) {
        CloudSessionCredential session = getSession(providerType, accountScope);
        FunctionInvokeCommand command = toInvokeCommand(providerType, accountScope, request, session);
        
        return portRouter.invocation(providerType).invokeFunction(command);
    }
    
    // 기타 메서드들...
    
    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 -> "LAMBDA";
            case AZURE -> "AZURE_FUNCTIONS";
            case GCP -> "CLOUD_FUNCTIONS";
            default -> throw new BusinessException(CloudErrorCode.CLOUD_PROVIDER_NOT_SUPPORTED,
                "지원하지 않는 프로바이더입니다: " + providerType);
        };
    }
}

Phase 4: Controller 및 DTO 구현

4.1 DTO 클래스 (dto/)

FunctionCreateRequest.java

/**
 * Serverless Function 생성 요청 DTO (CSP 중립적)
 * 
 * Controller에서 받는 요청 객체로, CSP 중립적인 필드만 포함합니다.
 * CSP 특화 설정은 providerSpecificConfig에 포함됩니다.
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionCreateRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String region;
    
    @NotBlank
    @Pattern(regexp = "^[a-zA-Z0-9-_]{1,64}$", message = "함수 이름은 1-64자의 영문, 숫자, 하이픈, 언더스코어만 사용 가능합니다")
    private String functionName;  // CSP 중립적
    
    @NotBlank
    private String runtime;  // CSP 중립적: "nodejs18.x", "python3.11", "java17", "go1.x"
    
    @NotBlank
    private String handler;  // CSP 중립적: AWS(Handler), Azure(scriptFile.entryPoint), GCP(entryPoint)
    
    @Min(128)
    @Max(10240)
    private Integer memorySize;  // MB
    
    @Min(1)
    @Max(900)  // AWS 최대값, Azure/GCP는 다를 수 있음
    private Integer timeout;  // 초
    
    @NotBlank
    private String roleArn;  // CSP 중립적: 실행 역할
    
    private Map<String, String> environmentVariables;  // 환경 변수
    
    private String description;  // 함수 설명
    
    private String vpcId;  // VPC 연결 (선택적)
    
    @NotBlank
    private String codeUri;  // 코드 URI (S3, Blob Storage, GCS 등)
    
    private Map<String, String> tags;
    
    /**
     * CSP별 특화 설정
     * 
     * AWS 예시:
     *   - subnetIds: ["subnet-12345", "subnet-67890"] (VPC 연결 시)
     *   - securityGroupIds: ["sg-12345"] (VPC 연결 시)
     *   - layers: ["arn:aws:lambda:region:account:layer:layer-name:1"]
     *   - reservedConcurrentExecutions: 10
     *   - deadLetterQueueTargetArn: "arn:aws:sqs:region:account:dlq"
     * 
     * Azure 예시:
     *   - hostingPlan: "Consumption" | "Premium" | "Dedicated"
     *   - appServicePlanId: "/subscriptions/.../resourceGroups/.../providers/..."
     *   - bindings: [{"type": "httpTrigger", "direction": "in", "authLevel": "function"}]
     * 
     * GCP 예시:
     *   - serviceAccountEmail: "[email protected]"
     *   - vpcConnector: "projects/project/locations/region/connectors/connector"
     *   - maxInstances: 10
     *   - minInstances: 0
     *   - ingressSettings: "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB"
     */
    private Map<String, Object> providerSpecificConfig;
}

FunctionUpdateRequest.java

/**
 * Serverless Function 수정 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionUpdateRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String functionId;  // 수정할 함수 ID/ARN
    
    private String runtime;  // 런타임 변경
    
    private String handler;  // 핸들러 변경
    
    @Min(128)
    @Max(10240)
    private Integer memorySize;  // 메모리 크기 변경 (MB)
    
    @Min(1)
    @Max(900)
    private Integer timeout;  // 타임아웃 변경 (초)
    
    private String roleArn;  // 실행 역할 변경
    
    private Map<String, String> environmentVariables;  // 환경 변수 변경
    
    private String description;  // 설명 변경
    
    private String codeUri;  // 코드 업데이트 URI
    
    private Map<String, String> tagsToAdd;  // 추가할 태그
    
    private Map<String, String> tagsToRemove;  // 제거할 태그
    
    /**
     * CSP별 특화 설정
     */
    private Map<String, Object> providerSpecificConfig;
}

FunctionDeleteRequest.java

/**
 * Serverless Function 삭제 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionDeleteRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String functionId;  // 삭제할 함수 ID/ARN
    
    private String reason;  // 삭제 이유 (감사 로그용)
    
    /**
     * CSP별 특화 삭제 옵션
     */
    private Map<String, Object> providerSpecificConfig;
    
    /**
     * 기본 삭제 요청 생성
     */
    public static FunctionDeleteRequest basic(String functionId) {
        return FunctionDeleteRequest.builder()
            .functionId(functionId)
            .build();
    }
}

FunctionQueryRequest.java

/**
 * Serverless Function 조회 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionQueryRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    private Set<String> regions;  // 조회할 리전 목록
    
    private String functionName;  // 함수 이름으로 필터링
    
    private String runtime;  // 런타임으로 필터링
    
    private String vpcId;  // 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;
}

FunctionInvokeRequest.java

/**
 * Serverless Function 실행 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionInvokeRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String functionId;  // 실행할 함수 ID/ARN
    
    @NotBlank
    private String region;
    
    @Pattern(regexp = "RequestResponse|Event|DryRun", message = "RequestResponse, Event, DryRun 중 하나여야 합니다")
    @Builder.Default
    private String invocationType = "RequestResponse";  // 기본값: 동기 실행
    
    @NotBlank
    private String payload;  // JSON 문자열 페이로드
    
    private String qualifier;  // 함수 버전/별칭 (선택적)
    
    private Map<String, String> context;  // 추가 컨텍스트 정보
}

FunctionResponse.java

/**
 * Serverless Function 응답 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionResponse {
    
    private Long id;
    private String resourceId;
    private String resourceName;  // function name
    private ProviderType providerType;
    private String region;
    private String runtime;  // 런타임
    private String handler;  // 핸들러
    private Integer memorySize;  // MB
    private Integer timeout;  // 초
    private String roleArn;  // 실행 역할
    private Map<String, String> environmentVariables;  // 환경 변수
    private String description;  // 함수 설명
    private String vpcId;  // VPC ID
    private String status;  // 상태
    private String lifecycleState;  // 생명주기 상태
    private String lastModified;  // 마지막 수정 시간
    private Map<String, String> tags;
    private LocalDateTime createdAt;
    private LocalDateTime lastSync;
    
    /**
     * CloudResource → FunctionResponse 변환
     */
    public static FunctionResponse from(CloudResource resource) {
        // CloudResource의 configuration JSON에서 추가 정보 추출
        return FunctionResponse.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/FunctionController.java)

@Slf4j
@RestController
@RequestMapping("/api/v1/cloud/providers/{provider}/accounts/{accountScope}/functions")
@RequiredArgsConstructor
@Tag(name = "Serverless Function Management", description = "Serverless Function 관리 API (멀티 클라우드 지원)")
public class FunctionController {
    
    private final FunctionUseCaseService functionUseCaseService;
    
    @PostMapping
    @Operation(summary = "Serverless Function 생성")
    public ResponseEntity<ApiResponse<FunctionResponse>> createFunction(
            @PathVariable ProviderType provider,
            @PathVariable String accountScope,
            @Valid @RequestBody FunctionCreateRequest request) {
        
        request.setProviderType(provider);
        request.setAccountScope(accountScope);
        
        CloudResource resource = functionUseCaseService.createFunction(request);
        FunctionResponse response = FunctionResponse.from(resource);
        
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(ApiResponse.success(response, "Serverless Function 생성에 성공했습니다."));
    }
    
    @GetMapping
    @Operation(summary = "Serverless Function 목록 조회")
    public ResponseEntity<ApiResponse<Page<FunctionResponse>>> listFunctions(
            @PathVariable ProviderType provider,
            @PathVariable String accountScope,
            @Valid FunctionQueryRequest request) {
        
        // 구현...
    }
    
    @PostMapping("/{functionId}/invoke")
    @Operation(summary = "Serverless Function 실행")
    public ResponseEntity<ApiResponse<String>> invokeFunction(
            @PathVariable ProviderType provider,
            @PathVariable String accountScope,
            @PathVariable String functionId,
            @Valid @RequestBody FunctionInvokeRequest request) {
        
        request.setProviderType(provider);
        request.setAccountScope(accountScope);
        request.setFunctionId(functionId);
        
        String result = functionUseCaseService.invokeFunction(provider, accountScope, request);
        
        return ResponseEntity.ok(ApiResponse.success(result, "Serverless Function 실행에 성공했습니다."));
    }
    
    // 기타 엔드포인트들...
}

Phase 5: Capability 등록

CapabilityRegistry에 Function 기능 등록

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

고려사항

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

  • 포트 인터페이스: CSP 독립적인 비즈니스 계약 정의
  • 어댑터 분리: AWS 전용 로직은 adapter/outbound/aws/function/에만 존재
  • 의존성 방향: 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에 Function 관련 에러 코드 추가

6. 트랜잭션 관리

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

7. Capability 검증

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

8. Serverless Function 특화 고려사항

8-1. 코드 배포 방식

  • URI 방식: S3, Blob Storage, GCS 등에서 코드 다운로드
  • 직접 업로드: ZIP 바이너리를 직접 전달 (작은 코드에 적합)
  • 코드 크기 제한: CSP별로 최대 크기 제한 다름
    • AWS Lambda: 50MB (직접 업로드), 250MB (압축), S3 무제한
    • Azure Functions: 100MB (Consumption Plan)
    • GCP Cloud Functions: 100MB (압축), 500MB (S3)

8-2. 런타임 지원

  • CSP별로 지원하는 런타임과 버전이 다름
  • 런타임 추상화 전략:
    • 공통 런타임 식별자 사용: nodejs18.x, python3.11, java17, go1.x
    • 매퍼에서 CSP별 실제 런타임 값으로 변환

8-3. 핸들러/엔트리포인트

  • AWS Lambda: package.Class::method 또는 filename.handler
  • Azure Functions: scriptFile.entryPoint (예: index.handler)
  • GCP Cloud Functions: entryPoint 함수 이름
  • CSP 중립적으로 handler 필드로 통일

8-4. 메모리 및 타임아웃

  • 메모리: MB 단위 (모든 CSP 공통)
  • 타임아웃: 초 단위 (최대값 CSP별로 다름)
    • AWS Lambda: 최대 900초 (15분)
    • Azure Functions: 최대 600초 (10분, Consumption Plan)
    • GCP Cloud Functions: 최대 540초 (9분, 1세대), 3600초 (1시간, 2세대)

8-5. 실행 역할(Role)

  • AWS Lambda: IAM Role ARN
  • Azure Functions: Managed Identity 또는 App Service Identity
  • GCP Cloud Functions: Service Account Email
  • CSP 중립적으로 roleArn 필드로 통일 (실제 값 형식은 CSP별로 다름)

8-6. VPC 연결

  • AWS Lambda: VPC Config (Subnet IDs, Security Group IDs)
  • Azure Functions: Virtual Network Integration
  • GCP Cloud Functions: VPC Connector
  • CSP 중립적으로 vpcId 필드로 시작하되, 세부 설정은 providerSpecificConfig

8-7. 환경 변수

  • 모든 CSP에서 키-값 쌍으로 환경 변수 지원
  • 민감한 정보는 CSP별 Secrets Manager와 연동 권장

8-8. 함수 실행

  • 동기 실행 (RequestResponse): 결과를 즉시 반환
  • 비동기 실행 (Event): 이벤트로 큐에 넣고 즉시 반환
  • Dry Run: 실제 실행 없이 검증만 수행

9. CSP 중립성 보장

  • 필드명 중립화:
    • FunctionNamefunctionName (모든 CSP 공통)
    • Runtimeruntime (모든 CSP 공통)
    • Handlerhandler (모든 CSP 공통)
    • MemorySizememorySize (MB, 모든 CSP 공통)
    • Timeouttimeout (초, 모든 CSP 공통)
    • RoleroleArn (실행 역할, 모든 CSP 공통)
  • CSP 특화 필드 제거:
    • subnetIds, securityGroupIds (AWS 전용) → providerSpecificConfig로 이동
    • hostingPlan (Azure 전용) → providerSpecificConfig로 이동
    • vpcConnector (GCP 전용) → providerSpecificConfig로 이동
  • 확장성: providerSpecificConfig로 CSP별 특화 설정 지원
    • AWS: layers, reservedConcurrentExecutions, deadLetterQueueTargetArn
    • Azure: hostingPlan, appServicePlanId, bindings
    • GCP: serviceAccountEmail, vpcConnector, maxInstances, minInstances

10. 코드 업로드 및 배포

10-1. 코드 배포 전략

  • Phase 1: URI 기반 배포만 지원 (S3, Blob Storage, GCS)
  • Phase 2: 직접 ZIP 업로드 지원 고려
  • 코드 크기 제한 검증 필요

10-2. 코드 업데이트

  • 함수 코드만 업데이트 (코드 업데이트)
  • 함수 설정만 업데이트 (설정 업데이트)
  • 코드와 설정 모두 업데이트

11. 트리거/이벤트 소스 (Phase 2)

Phase 1에서는 함수 생성/수정/삭제/조회에 집중하고, 트리거 관리는 Phase 2에서 구현합니다.

Phase 2 예정 기능:

  • HTTP 트리거
  • 이벤트 소스 연결 (S3, SQS, EventBridge 등)
  • 스케줄 트리거 (Cron)

12. 테스트 전략

  • 단위 테스트: 각 Adapter, Service, Controller 테스트
  • 통합 테스트: LocalStack을 활용한 AWS 통합 테스트
  • Mock 테스트: Port 인터페이스 Mock으로 Service 테스트
  • 코드 실행 테스트: 실제 Lambda 함수 실행 테스트

13. 성능 고려사항

  • 페이징: 대량 조회 시 페이징 처리
  • 비동기 실행: 장시간 작업은 비동기 실행 지원
  • 코드 캐싱: 자주 사용되는 함수 코드 캐싱 고려

14. 모니터링 및 로깅

  • 구조화된 로깅: 모든 레이어에서 일관된 로깅
  • 실행 로그: 함수 실행 결과 로깅
  • 메트릭: 함수 실행 횟수, 성공/실패, 실행 시간 등 메트릭 수집
  • 감사 로그: 함수 생성/삭제 등 중요 작업 감사 로깅

15. 보안 고려사항

  • 역할 기반 접근: 함수 실행 역할 검증
  • VPC 격리: Private 함수의 경우 VPC 연결 검증
  • 환경 변수 암호화: 민감한 환경 변수는 암호화하여 저장
  • 코드 검증: 업로드된 코드의 보안 검증 (선택적)

구현 체크리스트

Phase 1: 포트 및 모델

  • FunctionCreateCommand 정의
  • FunctionUpdateCommand 정의
  • FunctionDeleteCommand 정의
  • FunctionQuery 정의
  • FunctionInvokeCommand 정의
  • GetFunctionCommand 정의
  • FunctionManagementPort 인터페이스 정의
  • FunctionDiscoveryPort 인터페이스 정의
  • FunctionInvocationPort 인터페이스 정의

Phase 2: AWS 어댑터

  • AwsFunctionConfig 클래스 생성
  • AwsFunctionMapper 클래스 생성
  • AwsFunctionManagementAdapter 구현
  • AwsFunctionDiscoveryAdapter 구현
  • AwsFunctionInvocationAdapter 구현
  • ProviderScoped 인터페이스 구현

Phase 3: Service 계층

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

Phase 4: Controller 및 DTO

  • FunctionCreateRequest DTO 생성
  • FunctionUpdateRequest DTO 생성
  • FunctionDeleteRequest DTO 생성
  • FunctionQueryRequest DTO 생성
  • FunctionInvokeRequest DTO 생성
  • FunctionResponse DTO 생성
  • FunctionController 구현
  • API 문서화 (Swagger)

Phase 5: Capability 및 설정

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

Phase 6: 테스트

  • 단위 테스트 작성
  • 통합 테스트 작성
  • API 테스트
  • 함수 실행 테스트

참고 자료

Phase 2 예정: 트리거 및 이벤트 소스 관리

Serverless Function 기본 기능 구현 후, 트리거 및 이벤트 소스 관리 기능을 Phase 2로 구현합니다.

Phase 2 주요 기능

  1. HTTP 트리거

    • API Gateway 연동 (AWS)
    • Function App HTTP 트리거 (Azure)
    • Cloud Functions HTTP 트리거 (GCP)
  2. 이벤트 소스 연결

    • S3, SQS, EventBridge (AWS)
    • Event Grid, Service Bus (Azure)
    • Pub/Sub, Cloud Storage (GCP)
  3. 스케줄 트리거

    • EventBridge Rules (AWS)
    • Timer Trigger (Azure)
    • Cloud Scheduler (GCP)

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions