Глобальные таблицы AWS CDK и Amazon DynamoDB

Amazon DynamoDB — это база данных для бессерверных приложений AWS. Ее модель подключения на основе HTTP позволяет легко интегрироваться с сервисами бессерверных вычислений, такими как AWS Lambda. Одной из дополнительных возможностей Amazon DynamoDB являются глобальные таблицы, которые позволяют запускать копии экземпляров базы данных в других регионах.

В этой статье блога я расскажу о глобальных таблицах Amazon DynamoDB в контексте фреймворка AWS CDK — есть несколько нюансов с огромными последствиями, о которых вы должны знать.

Давайте погрузимся в тему.

Традиционный способ создания глобальных таблиц

Существует два известных мне способа создания глобальных таблиц с помощью AWS CDK, причем один из них, по моему скромному мнению, гораздо лучше для удобства обслуживания вашего приложения.

Давайте начнем с «традиционного» способа создания глобальных таблиц Amazon DynamoDB с помощью AWS CDK — используя конструкцию aws_dynamodb.Table и указывая свойство replicationRegions.

const globalTable = new dynamodb.Table(this, "Table", {
  partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
  replicationRegions: ["us-east-1", "us-east-2", "us-west-2"],
  billingMode: dynamodb.BillingMode.PROVISIONED
});
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь я определил таблицу с тремя регионами репликации. Если вы развернете этот фрагмент кода в веб-консоли AWS DynamoDB, вы увидите, что таблица имеет три реплики — по одной для каждого региона.

На первый взгляд, все выглядит хорошо и работает, как ожидалось. Но как только вы заглянете «под занавес» конструкции, вы увидите нечто неожиданное — развертывание «Таблицы» создало пользовательский ресурс AWS CloudFormation наряду с ресурсом DynamoDB.

Проблема с пользовательским ресурсом

Код, который использует данный пользовательский ресурс, можно найти здесь.

Пользовательский ресурс отвечает за создание таблиц-реплик. Он использует API AWS SDK updateTable и предоставляет соответствующее значение для свойства ReplicaUpdates. На первый взгляд, это звучит неплохо, но, создавая реплики через вызов AWS SDK, мы фактически отделяем эти ресурсы от шаблона AWS CloudFormation и кода AWS CDK.

Последствия такой архитектуры огромны с точки зрения удобства сопровождения. Вот два случая, которые привели меня к путанице и трудностям, с которыми мне пришлось столкнуться несколько раз при работе с глобальными таблицами, развернутыми с помощью свойства replicationRegions.

Атрибут DeletionPolicy.

Если вы работаете с производственной нагрузкой, считается хорошей практикой (я бы сказал, обязательным условием) указывать DeletionPolicy (в AWS CDK это называется removalPolicy) RETAIN для ресурсов, которые являются государственными и содержат производственные данные.

Если вы удалите стек CloudFormation, CloudFormation не будет удалять ресурсы, когда мы установим DeletionPolicy в RETAIN. Как вы можете себе представить, это может спасти вас от катастрофы.

Обычно в AWS CDK это делается с помощью метода applyRemovalPolicy на данной конструкции.

const globalTable = new cdk.aws_dynamodb.Table(this, "Table", {
  // ...
});
globalTable.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
Вход в полноэкранный режим Выход из полноэкранного режима

Проблема заключается в том, что реплики, созданные с помощью пользовательского ресурса, не наследуют эту removalPolicy. Эта настройка будет применяться только к «корневой» таблице.

Таким образом, если я удалю стек CloudFormation, CloudFormation сохранит «корневую» таблицу, но удалит пользовательский ресурс. Поскольку пользовательский ресурс сработал с событием Delete, он выдаст действие Delete для всех таблиц реплики. В результате все таблицы-реплики исчезнут.

Чтобы убедиться, что событие Delete никогда не попадет на пользовательский ресурс, используемый AWS CDK, необходимо установить DeletionPolicy на самом пользовательском ресурсе. Это не так просто, поскольку вам придется «найти» узел пользовательского ресурса в дереве AWS CDK и переопределить его атрибут DeletionPolicy. Вот как я бы это сделал.

const globalTable = new cdk.aws_dynamodb.Table(this, id, {
  // ...
});

/**
 * This only applies to the "root table" and NOT THE REPLICAS!
 */
globalTable.applyRemovalPolicy(removalPolicy);

/**
 * Make sure we do not remove the custom resource
 */
const customReplicaResource = globalTable.node.children.find(child => {
  return (child as any).resource?.cfnResourceType === "Custom::DynamoDBReplica";
}) as cdk.CfnResource;

customReplicaResource.applyRemovalPolicy(removalPolicy);
Войти в полноэкранный режим Выход из полноэкранного режима

Включение восстановления по точке во времени для таблиц-реплик

Для производственного использования включение восстановления по точкам во времени (PITR) является обязательным условием. Оно полезно не только в сценариях повреждения данных, но и для аварийного восстановления. В CDK применение PITR на глобальной таблице (созданной с помощью свойства replicationRegions) сначала может показаться простым.

const globalTable = new cdk.aws_dynamodb.Table(this, id, {
  pointInTimeRecovery: true
  // ...
});
Вход в полноэкранный режим Выход из полноэкранного режима

Проблема в том, что, как и в случае с DeletionPolicy, который мы рассматривали ранее, свойство pointInTimeRecovery применяется только к «корневой» таблице. Оно НЕ будет «перенаправлено» в таблицы-реплики. В лучшем случае инструмент аудита поймает это для вас. В худшем случае вы не сможете восстановить таблицы-реплики, если что-то пойдет не так.

Однако надежда еще не потеряна. Вы также можете использовать обходной путь, чтобы убедиться, что таблицы реплики имеют те же настройки PITR, что и ваша «корневая» таблица. Для этого нужно самостоятельно вызвать updateContinuousBackups через конструкцию AwsCustomResource для каждого региона, в котором находится реплика.

const replicationRegions = ["eu-center-1", "eu-west-1"];

const globalTable = new cdk.aws_dynamodb.Table(this, id, {
  pointInTimeRecovery: true,
  replicationRegions
  // ...
});

for (const replicationRegion of replicationRegions) {
  new cdk.custom_resources.AwsCustomResource(this, id, {
    onUpdate: {
      service: "DynamoDB",
      action: "updateContinuousBackups",
      parameters: {
        TableName: globalTable.tableName,
        PointInTimeRecoverySpecification: {
          PointInTimeRecoveryEnabled: true
        }
      },
      region: replicationRegion,
      physicalResourceId: cdk.custom_resources.PhysicalResourceId.of(id)
    },
    policy: cdk.custom_resources.AwsCustomResourcePolicy.fromSdkCalls({
      resources: [globalTable.tableArn]
    })
  });
}
Вход в полноэкранный режим Выход из полноэкранного режима

Другой способ создания глобальных таблиц

Вместо использования конструкции aws_dynamodb.Table, рассмотрите возможность использования конструкции aws_dynamodb.CfnGlobalTable. Насколько мне известно, этот ресурс был предоставлен нам в мае 2021 года, и я бы очень рекомендовал вам использовать его по умолчанию, даже если вы не планируете использовать таблицы реплик в ближайшем будущем в своей архитектуре.

Если вы хотите углубиться в технические подробности о ресурсе, которому соответствует CfnGlobalTable, обратите внимание на эту замечательную статью в блоге.

Я выступаю за использование CfnGlobalTable, потому что к этой конструкции не применимы проблемы, упомянутые в предыдущем разделе. Например, чтобы задать политику DeletionPolicy, которая будет применяться ко всем таблицам, даже репликам, достаточно использовать метод applyRemovalPolicy, доступный в этой конструкции.

const globalTable = new cdk.aws_dynamodb.CfnGlobalTable(this, id, {
  // ...
});
globalTable.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
Вход в полноэкранный режим Выход из полноэкранного режима

Что насчет ПИТР? Чтобы включить его на всех таблицах (репликах и «корневой» таблице), не нужно возиться с вызовами AWS SDK через пользовательский ресурс (или каким-то другим способом). Вы можете установить PITR для «корневой» таблицы и таблиц-реплик.

const globalTable = new cdk.aws_dynamodb.CfnGlobalTable(this, id, {
  // ...
  replicas: [
    {
      region: "eu-center-1",
      pointInTimeRecoverySpecification: {
        pointInTimeRecoveryEnabled: true
      }
    },
    // Let us say that, the root table lives in "eu-west-1"
    {
      region: "eu-west-1",
      pointInTimeRecoverySpecification: {
        pointInTimeRecoveryEnabled: true
      }
    }
  ]
});
Вход в полноэкранный режим Выход из полноэкранного режима

Еще одна важная причина использования CfnGlobalTable заключается в том, что этот ресурс использует более новую версию глобальных таблиц Amazon DynamoDB. Вы можете прочитать о различных версиях глобальных таблиц здесь.

Миграция

Я еще не готов опубликовать здесь статью о миграции (я все еще оцениваю различные способы сделать это), поэтому, пожалуйста, следите за моей следующей статьей. Если вы хотите выполнить миграцию сейчас, я бы основывал необходимые шаги на этой и этой статьях.

Заключительные слова

Я надеюсь, что информация в этой статье избавит вас от некоторых трудностей и разочарований, через которые мне пришлось пройти при работе с глобальными таблицами, созданными с помощью свойства replicationRegions в CDK.

Как я уже говорил, CfnGlobalTable объективно лучше подходит для этой работы. Да, вы можете потерять некоторые тонкости конструкции L2 (L3?) CDK, но использование более новой версии глобальных таблиц Amazon DynamoDB принесет дивиденды в долгосрочной перспективе.

Для получения более подробной информации подобного содержания, пожалуйста, следите за мной в Twitter — @wm_matuszewski.

Спасибо за ваше время.

Оцените статью
devanswers.ru
Добавить комментарий