이 블로그 게시물 시리즈에서는 AWS 에서 다중 테넌트 서비스를 구축하기 위한 모범 사례에 대해 논의하고 싶습니다. 다중 테넌트 서비스를 구축하는 방법에 대한 기존 문헌은 일반적으로 수백 명의 고객이 있는 SaaS 애플리케이션을 대상으로 합니다(예: ).
일련의 블로그 게시물을 각 서비스 간 통합 유형에 대해 동기식, 비동기식, 일괄 통합이라는 세 부분으로 나누겠습니다.
내부 서비스를 위한 다중 테넌시
1.1. 테넌트 격리 1.2. 다중 테넌트 모니터링 1.3. 스케일링내부 서비스를 위한 다중 테넌시
2.1. 테넌트 격리 - 액세스 제어 2.2 테넌트 격리 - 시끄러운 이웃 문제 2.3 다중 테넌트 모니터링 2.4 지표, 경보, 대시보드 2.5 API 클라이언트 온보딩 및 오프보딩AWS AppSync를 통한 멀티 테넌시
결론
멀티 테넌시는 소프트웨어의 단일 인스턴스로 여러 고객이나 테넌트에게 서비스를 제공하는 소프트웨어 기능입니다.
둘 이상의 팀이 서비스 API를 호출하도록 허용하면 서비스가 다중 테넌트가 됩니다. 다중 테넌트 아키텍처는 테넌트 격리, 테넌트 수준 모니터링, 확장 등 서비스에 추가적인 복잡성을 가져옵니다.
AWS에서 REST , HTTP 또는 WebSocket API를 사용하여 AWS 웹 서비스를 구축하는 경우 API 게이트웨이를 사용할 가능성이 높습니다.
리소스 기반 인증을 사용하여 클라이언트 온보딩 . 리소스 기반 액세스의 경우 API 게이트웨이 리소스 정책을 업데이트하고 클라이언트의 AWS 계정을 추가해야 합니다. 이 방법의 가장 큰 단점은 리소스 정책을 업데이트한 후 변경 사항을 적용하려면 API 게이트웨이 단계를 다시 배포해야 한다는 것입니다(AWS 문서 및 참조). 그러나 CDK를 사용하면 새로운 단계의 배포를 자동화할 수 있습니다( 참조). 또 다른 단점은 리소스 정책의 최대 길이에 대한 제한입니다.
신원 기반 인증을 사용하여 클라이언트 온보딩 . 자격 증명 기반 액세스 제어의 경우 클라이언트에 대한 IAM 역할을 생성하고 역할의 리소스 정책(신뢰할 수 있는 관계)을 업데이트하여 클라이언트가 이를 맡을 수 있도록 해야 합니다. IAM 사용자를 사용할 수 있지만 보안 관점에서 IAM 역할이 더 좋습니다. 역할은 임시 자격 증명을 사용한 인증을 허용하며 IAM 사용자 자격 증명을 저장할 필요가 없습니다. 계정당 역할은 1,000개로 제한되지만 이 제한은 조정 가능합니다. 또한 API에 대한 교차 계정 액세스를 얻기 위한 역할 기반 방법의 또 다른 단점은 모든 새 API 클라이언트에 대해 IAM 역할을 생성해야 한다는 것입니다. 그러나 역할 관리는 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 Gateway에는 두 가지 유형의 로그가 있습니다.
API 클라이언트의 요청을 모니터링하려면 액세스 로깅을 활성화하는 것이 좋습니다. 최소한 호출자의 AWS IAM ARN( $context.identity.userArn
), 요청 경로( $context.path
), 서비스 응답 상태 코드 $context.status
및 API 호출 대기 시간( $context.responseLatency
)을 기록할 수 있습니다. .
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);
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. 클라이언트별 지표 게시를 위해 지표 로그 필터 사용
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',},}); });
2.4.2. 클라이언트별 지표 게시를 위해 Lambda 함수 사용
대체 옵션은 로그를 구문 분석하고 지표를 추출하여 게시하는 Lambda 함수를 생성하는 것입니다. 이를 통해 알 수 없는 클라이언트를 필터링하거나 userArn에서 클라이언트 이름을 추출하는 등 더 많은 사용자 정의 작업을 수행할 수 있습니다.
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(), });
interface ApiClientConfig { name: string; awsAccounts: string[]; rateLimit: number; burstLimit: number; } const apiClients: ApiClientConfig[] = [ { name: 'payment-service', awsAccounts: ['3','444455556666'], rateLimit: 10, burstLimit: 2, }, { name: 'order-service', awsAccounts: ['777788889999'], rateLimit: 1, burstLimit: 1, }, ];
서비스에 GraphQL API가 있는 경우 AppSync를 사용할 가능성이 높습니다. API Gateway와 마찬가지로 IAM 인증을 사용하여 AppSync 요청을 승인할 수 있습니다. AppSync에는 리소스 정책( 참조)이 없으므로 AppSync API에 대한 액세스 제어를 설정하는 데 역할 기반 인증만 사용할 수 있습니다. API 게이트웨이와 마찬가지로 서비스의 모든 새 테넌트에 대해 별도의 IAM 역할을 생성합니다.