이 블로그 게시물 시리즈에서는 AWS 에서 다중 테넌트 서비스를 구축하기 위한 모범 사례에 대해 논의하고 싶습니다. 다중 테넌트 서비스를 구축하는 방법에 대한 기존 문헌은 일반적으로 수백 명의 고객이 있는 SaaS 애플리케이션을 대상으로 합니다(예: AWS 서버리스 서비스를 사용하여 다중 테넌트 SaaS 솔루션 구축 ).
이 시리즈의 주요 근거는 AWS 계정에 모두 배포되는 더 적은 수의 클라이언트를 사용하여 사용 사례에 대한 다중 테넌트 서비스를 구축하는 데 중점을 두는 것입니다. 일반적으로 이는 내부용 다중 테넌트 서비스를 구축하는 시나리오에 적용됩니다.
일련의 블로그 게시물을 각 서비스 간 통합 유형에 대해 동기식, 비동기식, 일괄 통합이라는 세 부분으로 나누겠습니다.
1부에서는 API Gateway와 AppSync라는 두 가지 AWS 서비스에 대한 다중 테넌트 아키텍처에 대해 설명합니다. 기사 전반에 걸쳐 Typescript 및 AWS CDK에서 이 기사를 위해 구축된 샘플 애플리케이션 앱의 코드( https://github.com/filletofish/aws-cdk-multi-tenant-api-example/tree/main )를 참조합니다.
내부 서비스를 위한 다중 테넌시
1.1. 테넌트 격리
1.2. 다중 테넌트 모니터링
1.3. 스케일링
내부 서비스를 위한 다중 테넌시
2.1. 테넌트 격리 - 액세스 제어
2.2 테넌트 격리 - 시끄러운 이웃 문제
2.3 다중 테넌트 모니터링
2.4 지표, 경보, 대시보드
2.5 API 클라이언트 온보딩 및 오프보딩
AWS AppSync를 통한 멀티 테넌시
결론
멀티 테넌시는 소프트웨어의 단일 인스턴스로 여러 고객이나 테넌트에게 서비스를 제공하는 소프트웨어 기능입니다.
둘 이상의 팀이 서비스 API를 호출하도록 허용하면 서비스가 다중 테넌트가 됩니다. 다중 테넌트 아키텍처는 테넌트 격리, 테넌트 수준 모니터링, 확장 등 서비스에 추가적인 복잡성을 가져옵니다.
일반적으로 테넌트 격리는 테넌트가 다른 테넌트의 리소스에 액세스하지 못하도록 하여 보안 문제를 해결합니다. 또한 한 테넌트로 인한 오류가 서비스의 다른 테넌트에 영향을 미치지 않도록 테넌트 격리가 구현됩니다. 시끄러운 이웃 문제라고도 합니다. 테넌트 격리 전략에 대한 AWS 백서 (https://d1.awsstatic.com/whitepapers/saas-tenant-isolation-strategies.pdf )에서 자세한 내용을 확인하세요.
여러 테넌트가 인프라 리소스를 공유하기 시작하면 각 테넌트가 시스템을 어떻게 사용하는지 모니터링해야 합니다. 이는 일반적으로 테넌트 이름 또는 식별자가 로그, 지표 및 대시보드에 있어야 함을 의미합니다. 다중 테넌트 모니터링은 다음과 같은 여러 가지 이유로 유용할 수 있습니다.
다중 테넌트 서비스는 단일 테넌트 서비스보다 확장 문제에 더 많이 노출될 가능성이 높습니다. 그러나 확장성은 매우 중요한 주제이므로 이 블로그 게시물에서는 다루지 않겠습니다.
AWS에서 REST , HTTP 또는 WebSocket API를 사용하여 AWS 웹 서비스를 구축하는 경우 API 게이트웨이를 사용할 가능성이 높습니다.
AWS에서는 서비스의 리소스와 데이터를 격리하고, 비용 관리를 더 쉽게 하며, 테스트 환경과 프로덕션 환경을 분리하기 위해 각 서비스를 자체 AWS 계정에 배포할 것을 권장합니다(자세한 내용은 AWS 백서 여러 계정을 사용하여 AWS 환경 구성 참조).
회사 서비스가 AWS에 배포된 경우 API 게이트웨이에 대한 액세스를 관리하기 위한 가장 확실한 솔루션은 AWS IAM입니다. AWS Cognito는 다중 테넌트 API에 대한 액세스를 관리하기 위한 또 다른 옵션입니다( API Gateway를 사용하여 대규모 계층형 다중 테넌트 REST API 조절 , Amazon Cognito에 대한 사례 및 반대 사례 참조).
AWS IAM과 AWS Cognito의 비교는 별도로 심층 분석할 가치가 있습니다. 하지만 이 기사에서는 회사 서비스가 AWS에 있을 때 액세스를 관리하는 가장 간단한 방법인 AWS IAM을 고수하겠습니다.
API 게이트웨이 메서드에 대한 AWS IAM 인증을 활성화하면( CFN 참조), 이 메서드에 대한 모든 API 요청은 API 게이트웨이를 호출할 수 있는 IAM 자격 증명의 자격 증명으로 서명되어야 합니다.
기본적으로 AWS 계정 간 액세스는 허용되지 않습니다. 예를 들어, 다른 AWS 계정의 자격 증명을 사용하여 API 게이트웨이를 호출하면 실패합니다. 고객을 API와 통합하려면 교차 계정 액세스를 설정해야 합니다. API 게이트웨이에 대한 교차 계정 액세스 권한을 부여하려면 리소스 기반 인증(API 게이트웨이 HTTP API에는 사용할 수 없음)과 자격 증명 기반 인증(자세한 내용은 https://repost.aws/knowledge-center/ 참조)이라는 두 가지 방법을 사용할 수 있습니다. 액세스-API-게이트웨이-계정 ):
리소스 기반 인증을 사용하여 클라이언트 온보딩 . 리소스 기반 액세스의 경우 API 게이트웨이 리소스 정책을 업데이트하고 클라이언트의 AWS 계정을 추가해야 합니다. 이 방법의 가장 큰 단점은 리소스 정책을 업데이트한 후 변경 사항을 적용하려면 API 게이트웨이 단계를 다시 배포해야 한다는 것입니다(AWS 문서 [1] 및 [2] 참조). 그러나 CDK를 사용하면 새로운 단계의 배포를 자동화할 수 있습니다( Api 게이트웨이에 대한 AWS CDK 문서 참조). 또 다른 단점은 리소스 정책의 최대 길이에 대한 제한입니다.
신원 기반 인증을 사용하여 클라이언트 온보딩 . 자격 증명 기반 액세스 제어의 경우 클라이언트에 대한 IAM 역할을 생성하고 역할의 리소스 정책(신뢰할 수 있는 관계)을 업데이트하여 클라이언트가 이를 맡을 수 있도록 해야 합니다. IAM 사용자를 사용할 수 있지만 보안 관점에서 IAM 역할이 더 좋습니다. 역할은 임시 자격 증명을 사용한 인증을 허용하며 IAM 사용자 자격 증명을 저장할 필요가 없습니다. 계정당 역할은 1,000개로 제한되지만 이 제한은 조정 가능합니다. 또한 API에 대한 교차 계정 액세스를 얻기 위한 역할 기반 방법의 또 다른 단점은 모든 새 API 클라이언트에 대해 IAM 역할을 생성해야 한다는 것입니다. 그러나 역할 관리는 CDK를 사용하여 자동화할 수 있습니다( 제공된 CDK 앱의 코드 샘플 참조).
AWS IAM 인증을 사용하면 API 게이트웨이에 대한 액세스만 제어할 수 있습니다(IAM 정책을 사용하면 어떤 AWS 계정이 어떤 API 게이트웨이 엔드포인트를 호출할 수 있는지 지정할 수 있습니다). 서비스의 데이터 및 기타 기본 리소스에 대한 액세스 제어를 구현하는 것은 귀하의 책임입니다. 서비스 내에서 추가 액세스 제어를 위해 API 게이트웨이 요청과 함께 전달된 호출자의 AWS IAM ARN을 사용할 수 있습니다.
export const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => { // IAM Principal ARN of the api caller const callerArn = event.requestContext.identity.userArn!; // .. business logic based on caller return { statusCode: 200, body: JSON.stringify({ message: `Received API Call from ${callerArn}`, }) }; };
기본 API 게이트웨이 한도는 10,000TPS( API 게이트웨이 할당량 및 한도 )입니다. 그러나 다운스트림 종속성으로 인해 서비스에 더 낮은 TPS 제한이 필요할 수 있습니다. 전체 시스템의 가용성에 영향을 미치는 단일 테넌트의 API 요청 과부하를 방지하려면 테넌트별 API 속도 제한("제한" 또는 "허용 제어"라고도 함)을 구현해야 합니다.
API Gateway API 사용 계획 및 키를 사용하여 각 클라이언트에 대한 제한을 개별적으로 구성할 수 있습니다(자세한 내용은 AWS 설명서 [1], [2] 및 [3] 참조).
API Gateway에는 두 가지 유형의 로그가 있습니다.
API 게이트웨이 실행 로그: 요청 또는 응답 매개변수 값, 필요한 API 키, 사용 계획 활성화 여부 등과 같은 데이터가 포함됩니다. 기본적으로 활성화되어 있지 않지만 구성할 수 있습니다.
API 게이트웨이 액세스 로그 기능: API에 액세스한 사람, 액세스한 방법, 액세스한 엔드포인트 및 API 호출 결과를 기록할 수 있습니다. 로그 형식을 제공하고 컨텍스트 변수를 사용하여 기록할 내용을 선택할 수 있습니다(CDK의 문서 참조).
API 클라이언트의 요청을 모니터링하려면 액세스 로깅을 활성화하는 것이 좋습니다. 최소한 호출자의 AWS IAM ARN( $context.identity.userArn
), 요청 경로( $context.path
), 서비스 응답 상태 코드 $context.status
및 API 호출 대기 시간( $context.responseLatency
)을 기록할 수 있습니다. .
개인적으로 AWS IAM 인증 및 Lambda 함수를 컴퓨팅으로 사용하는 서비스의 경우 이 API 게이트웨이 액세스 로깅 구성이 유용하다고 생각했습니다.
const formatObject = { requestId: '$context.requestId', extendedRequestId: '$context.extendedRequestId', apiId: '$context.apiId', resourceId: '$context.resourceId', domainName: '$context.domainName', stage: '$context.stage', path: '$context.path', resourcePath: '$context.resourcePath', httpMethod: '$context.httpMethod', protocol: '$context.protocol', accountId: '$context.identity.accountId', sourceIp: '$context.identity.sourceIp', user: '$context.identity.user', userAgent: '$context.identity.userAgent', userArn: '$context.identity.userArn', caller: '$context.identity.caller', cognitoIdentityId: '$context.identity.cognitoIdentityId', status: '$context.status', integration: { // The status code returned from an integration. For Lambda proxy integrations, this is the status code that your Lambda function code returns. status: '$context.integration.status', // For Lambda proxy integration, the status code returned from AWS Lambda, not from the backend Lambda function code. integrationStatus: '$context.integration.integrationStatus', // The error message returned from an integration // A string that contains an integration error message. error: '$context.integration.error', latency: '$context.integration.latency', }, error: { responseType: '$context.error.responseType', message: '$context.error.message', }, requestTime: '$context.requestTime', responseLength: '$context.responseLength', responseLatency: '$context.responseLatency', }; const accessLogFormatString = JSON.stringify(formatObject); const accessLogFormat = apigw.AccessLogFormat.custom(accessLogFormatString);
로깅이 활성화되면 CloudWatch Insights를 사용하여 다음을 통해 선택한 API 클라이언트로부터 최신 호출을 쉽게 얻을 수 있습니다.
fields @timestamp, path, status, responseLatency, userArn | sort @timestamp desc | filter userArn like 'payment-service' | limit 20
기본적으로 API Gateway에서 지원하는 CloudWatch 지표는 모든 요청에 대해 집계됩니다. 그러나 API 게이트웨이 액세스 로그를 구문 분석하여 클라이언트 이름의 추가 차원으로 사용자 지정 CloudWatch 지표를 게시하면 API의 클라이언트(테넌트) 사용량을 모니터링할 수 있습니다. 최소한 클라이언트별 CloudWatch 지표 Count, 4xx, 5xx, Latency Split by Dimension=${Client}
게시하는 것이 좋습니다. 상태 코드 및 API 경로와 같은 차원을 추가할 수도 있습니다.
2.4.1. 클라이언트별 지표 게시를 위해 지표 로그 필터 사용
CloudWatch 지표 로그 필터(문서 참조)를 사용하면 사용자 지정 필터를 제공하고 API Gateway 액세스 로그에서 지표 값을 추출할 수 있습니다(아래 예 참조). 지표 로그 필터를 사용하면 로그에서 사용자 정의 지표 차원에 대한 값을 추출할 수도 있습니다. 다중 테넌시 모니터링의 경우 클라이언트 차원은 호출자의 IAM ARN이 될 수 있습니다.
지표 로그 필터의 주요 장점은 (1) 관리할 컴퓨팅이 필요하지 않다는 것입니다. (2) 간단하고 저렴합니다. 그러나 데이터 수정(예: IAM ARN 대신 더 읽기 쉬운 클라이언트 이름 설정)을 수행할 수 없으며 단일 로그 그룹(문서)당 지표 필터가 100개로 제한됩니다.
Client
및 Path
차원을 사용하여 Count
게시하기 위한 CloudWatch 지표 로그 필터의 예
new logs.MetricFilter(this, 'MultiTenantApiCountMetricFilter', { logGroup: accessLogsGroup, filterPattern: logs.FilterPattern.exists('$.userArn'), metricNamespace: metricNamespace, metricName: 'Count', metricValue: '1', unit: cloudwatch.Unit.COUNT, dimensions: { client: '$.userArn', method: '$.httpMethod', path: '$.path',},}); });
제공된 샘플 CDK 애플리케이션에서 4xx, 5xx 오류 및 대기 시간 지표에 대한 모든 지표 필터를 확인하세요.
2.4.2. 클라이언트별 지표 게시를 위해 Lambda 함수 사용
대체 옵션은 로그를 구문 분석하고 지표를 추출하여 게시하는 Lambda 함수를 생성하는 것입니다. 이를 통해 알 수 없는 클라이언트를 필터링하거나 userArn에서 클라이언트 이름을 추출하는 등 더 많은 사용자 정의 작업을 수행할 수 있습니다.
몇 줄의 CDK 코드만으로 Lambda 함수를 API 게이트웨이 액세스 로그에 구독할 수 있습니다.
const logProcessingFunction = new lambda.NodejsFunction( this, 'log-processor-function', { functionName: 'multi-tenant-api-log-processor-function', } ); new logs.SubscriptionFilter(this, 'MultiTenantApiLogSubscriptionFilter', { logGroup: accessLogsGroup, destination: new logsd.LambdaDestination(logProcessingFunction), filterPattern: logs.FilterPattern.allEvents(), });
코드 의 전체 예와 로그 프로세서 Lambda 함수 구현을 확인하세요.
클라이언트별로 분할된 API Gateway 지표 게시를 시작하면 이제 각 클라이언트에 대해 CloudWatch 대시보드 및 CloudWatch 경보를 별도로 생성할 수 있습니다.
CDK 앱은 클라이언트 이름, 해당 AWS 계정, 요청된 TPS 제한 및 기타 메타데이터가 포함된 구성을 저장하는 쉬운 솔루션이 될 수 있습니다. 새 API 클라이언트를 온보딩하려면 코드에서 관리되는 구성에 이를 추가해야 합니다.
interface ApiClientConfig { name: string; awsAccounts: string[]; rateLimit: number; burstLimit: number; } const apiClients: ApiClientConfig[] = [ { name: 'payment-service', awsAccounts: ['111122223333','444455556666'], rateLimit: 10, burstLimit: 2, }, { name: 'order-service', awsAccounts: ['777788889999'], rateLimit: 1, burstLimit: 1, }, ];
이 구성을 사용하면 CDK 앱은 IAM 역할, API 게이트웨이 사용 키를 생성하고 액세스 로그를 구문 분석하는 Lambda 함수에 클라이언트 이름을 전달할 수 있습니다(샘플 애플리케이션 코드에서 참조).
서비스에 GraphQL API가 있는 경우 AppSync를 사용할 가능성이 높습니다. API Gateway와 마찬가지로 IAM 인증을 사용하여 AppSync 요청을 승인할 수 있습니다. AppSync에는 리소스 정책( GH 문제 참조)이 없으므로 AppSync API에 대한 액세스 제어를 설정하는 데 역할 기반 인증만 사용할 수 있습니다. API 게이트웨이와 마찬가지로 서비스의 모든 새 테넌트에 대해 별도의 IAM 역할을 생성합니다.
안타깝게도 AppSync는 테넌트 격리 및 모니터링에 필요한 클라이언트별 조절을 제한적으로 지원합니다. WAF를 사용하여 AppSync에 대한 TPS 제한을 설정할 수 있지만 서비스 테넌트를 격리하기 위해 별도의 클라이언트별 제한을 생성할 수는 없습니다. 마찬가지로 AppSync는 API Gateway처럼 액세스 로그를 제공하지 않습니다.
해결책? API Gateway를 AppSync에 프록시로 추가하고 위에서 설명한 모든 API Gateway 기능을 사용하여 테넌트 격리 및 모니터링과 같은 다중 테넌시 요구 사항을 구현할 수 있습니다. 또한 아직 AppSync에 없는 Lambda 권한 부여자, 사용자 지정 도메인, API 수명 주기 관리와 같은 다른 API 게이트웨이 기능을 사용할 수 있습니다. 단점은 요청에 대한 약간의 추가 대기 시간입니다.
그게 다야. 질문이나 아이디어가 있으시면 댓글로 알려주시거나 저에게 직접 연락해주세요. 이 시리즈의 다음 부분에서는 AWS Event Bridge 및 AWS SQS/SNS와의 비동기식 내부 통합에 대한 모범 사례를 검토하겠습니다.
AWS를 기반으로 다중 테넌트 서비스를 구축하는 주제에 대해 자세히 알아보고 싶다면 다음 리소스가 유용하다고 생각합니다.