В этой серии публикаций в блоге я хотел бы обсудить лучшие практики создания мультитенантных сервисов в AWS . Существующая литература о том, как создавать мультитенантные сервисы, обычно ориентирована на приложения SaaS с сотнями клиентов (например , Создание мультитенантного SaaS-решения с использованием бессерверных сервисов AWS ).
Основная цель этой серии статей — сосредоточиться на создании мультитенантных сервисов для сценариев использования с меньшим количеством клиентов, которые все развернуты в учетных записях AWS. Обычно это применимо к сценариям, когда вы создаете мультитенантную службу для внутреннего использования.
Я разделю серию сообщений в блоге на три части для каждого типа интеграции между сервисами: синхронная, асинхронная и пакетная интеграция.
В части 1 будет обсуждаться мультитенантная архитектура для двух сервисов AWS: API Gateway и AppSync. На протяжении всей статьи я ссылаюсь на код из примера приложения, созданного для этой статьи в 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 Gateway.
AWS рекомендует развертывать каждый сервис в отдельных учетных записях AWS, чтобы изолировать ресурсы и данные сервиса, упростить управление расходами и разделить тестовую и производственную среды (подробности см. в Техническом документе AWS «Организация среды AWS с использованием нескольких учетных записей »).
Если сервисы вашей компании развернуты в AWS, то наиболее очевидным решением для управления доступом к вашему шлюзу API является AWS IAM. AWS Cognito — еще один вариант управления доступом к мультитенантному API (см. раздел «Регулирование многоуровневого мультитенантного REST API в масштабе с помощью API Gateway» , Аргументы за и против Amazon Cognito ).
Сравнение AWS IAM и AWS Cognito заслуживает отдельного подробного рассмотрения. Но в этой статье я бы остановился на AWS IAM, поскольку это самый простой способ управления доступом, когда сервисы вашей компании находятся в AWS.
После включения авторизации AWS IAM для метода шлюза API (см. CFN ) все запросы API для этого метода должны быть подписаны учетными данными идентификатора IAM, которым разрешено вызывать ваш шлюз API.
По умолчанию доступ между учетными записями AWS запрещен. Например, вызов API-шлюза с учетными данными другой учетной записи AWS не удастся. Чтобы интегрировать своих клиентов с вашим API, вам необходимо настроить доступ к нескольким аккаунтам. Для предоставления межаккаунтного доступа к вашему API-шлюзу вы можете использовать два метода: авторизацию на основе ресурсов (недоступно для HTTP API API-шлюза) и авторизацию на основе личности (подробнее см. на https://repost.aws/knowledge-center/ ). доступ-api-шлюз-аккаунт ):
Подключение клиента с авторизацией на основе ресурсов . Для доступа на основе ресурсов вам необходимо обновить политику ресурсов шлюза API и добавить учетную запись AWS вашего клиента. Основным недостатком этого метода является то, что после обновления политики ресурсов необходимо повторно развернуть этап API Gateway, чтобы изменения вступили в силу (см. документы AWS [1] и [2] ). Однако если вы используете CDK, вы можете автоматизировать развертывание новых этапов (см. Документацию AWS CDK для Api Gateway ). Еще одним недостатком является ограничение максимальной длины ресурсной политики.
Подключение клиента с авторизацией на основе личности . Для управления доступом на основе удостоверений вам необходимо создать роль IAM для клиента и разрешить клиенту взять на себя ее использование, обновив политику ресурсов роли (доверенные отношения). Вы можете использовать пользователей IAM, но роли IAM лучше с точки зрения безопасности. Роли допускают аутентификацию с использованием временных учетных данных и не требуют хранения учетных данных пользователя IAM. Существует ограничение в 1000 ролей на одну учетную запись, но это ограничение можно изменить. Кроме того, еще одним недостатком ролевого метода получения доступа к вашему API между учетными записями является то, что вам необходимо создавать роль IAM для каждого нового клиента API. Однако управление ролями можно автоматизировать с помощью CDK (см. пример кода из прилагаемого приложения CDK ).
Авторизация AWS IAM позволяет вам контролировать доступ только к шлюзу API (с помощью политики IAM вы можете указать, какая учетная запись AWS может вызывать те или иные конечные точки шлюза API). Вы несете ответственность за реализацию контроля доступа к данным и другим базовым ресурсам вашего сервиса. В рамках вашего сервиса вы можете использовать AWS IAM ARN вызывающего абонента, который передается с запросом шлюза API, для дальнейшего контроля доступа:
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 000 TPS ( квоты и ограничения шлюза API ). Однако из-за ваших нисходящих зависимостей вашему сервису может потребоваться более низкий предел TPS. Чтобы избежать перегрузки запросов API от одного клиента, которая повлияет на доступность всей системы, вам следует реализовать ограничение скорости API для каждого клиента (также называемое «регулированием» или «контролем доступа»).
Вы можете использовать планы и ключи использования API шлюза 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 Auth и функцией 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
Метрики CloudWatch, поддерживаемые API Gateway, по умолчанию агрегируются для всех запросов. Но вы можете проанализировать журналы доступа к API-шлюзу, чтобы опубликовать пользовательские метрики CloudWatch с дополнительным измерением имени вашего клиента, чтобы иметь возможность отслеживать использование клиентом (арендатором) вашего API. Как минимум я бы рекомендовал публиковать метрики CloudWatch для каждого клиента Count, 4xx, 5xx, Latency, разделенные по Dimension=${Client}
. Вы также можете добавить такие измерения, как код состояния и путь к API.
2.4.1. Использование фильтров журнала метрик для публикации метрик по каждому клиенту.
Фильтры журналов метрик CloudWatch (см. документацию) позволяют вам предоставить собственный фильтр и извлекать значения метрик из журналов доступа к шлюзу API (см. пример ниже). Фильтры журналов метрик также позволяют извлекать значения для пользовательских измерений метрик из журналов. Для мониторинга с несколькими арендаторами измерение «Клиент» может быть IAM ARN вызывающего абонента.
Основными преимуществами фильтров метрических журналов являются (1) отсутствие вычислительных затрат для управления (2) это просто и дешево. Но вы не можете вносить какие-либо изменения в данные (например, устанавливать более читаемые имена клиентов вместо IAM ARN), и существует ограничение в 100 фильтров метрик на одну группу журналов (документы).
Пример фильтра журнала метрик CloudWatch для Count
публикаций с измерением Client
и Path
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',},}); });
См . все фильтры метрик для метрик ошибок 4xx, 5xx и задержки в предоставленном образце приложения CDK .
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(), });
См. полный пример кода , а также реализацию лямбда-функции процессора журнала .
После того как вы начали публиковать метрики шлюза API, разделенные по клиентам, вы теперь можете создавать панели мониторинга 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, которая анализирует журналы доступа (см. это в примере кода приложения).
Если у вашего сервиса есть API GraphQL , вы, вероятно, используете AppSync. Как и в случае с API-шлюзом, вы можете использовать IAM Auth для авторизации запросов AppSync. У AppSync нет политики ресурсов (см. проблему GH ), поэтому для настройки контроля доступа к AppSync API можно использовать только авторизацию на основе ролей. Как и в случае с API Gateway, вам потребуется создать отдельную роль IAM для каждого нового клиента вашего сервиса.
К сожалению, AppSync имеет ограниченную поддержку регулирования для каждого клиента, которая нам необходима для изоляции и мониторинга клиентов. Хотя вы можете настроить ограничения TPS для AppSync с помощью WAF, вы не можете создавать отдельные ограничения для каждого клиента, чтобы изолировать ваших клиентов службы. Аналогично, AppSync не предоставляет журналы доступа, как это делает API Gateway.
Решение? Вы можете добавить API-шлюз в качестве прокси-сервера в AppSync и использовать все описанные выше функции API-шлюза для реализации требований мультитенантности, таких как изоляция и мониторинг клиентов. Кроме того, вы можете использовать другие функции API-шлюза, такие как авторизаторы Lambda, собственный домен и управление жизненным циклом API, которых еще нет в AppSync. Недостатком является небольшая дополнительная задержка для ваших запросов.
Вот и все. Если у вас есть какие-либо вопросы или идеи, дайте мне знать в комментариях или свяжитесь со мной напрямую. В следующей части этой серии я рассмотрю лучшие практики асинхронной внутренней интеграции с AWS Event Bridge и AWS SQS/SNS.
Если вы хотите углубиться в тему создания мультитенантных сервисов на базе AWS, я нашел эти ресурсы полезными:
Также опубликовано здесь.