From 1fce04bbf9426a2457e3446288af744c0265d957 Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Tue, 25 Feb 2025 11:18:16 +0530 Subject: [PATCH 01/11] Setup the sample application --- .github/workflows/ci.yml | 58 ++++++++ .gitignore | 5 + LICENSE | 201 +++++++++++++++++++++++++++ Makefile | 60 ++++++++ README.md | 93 ++++++++++++- app.py | 11 ++ cdk.json | 69 +++++++++ dms_sample/__init__.py | 0 dms_sample/stack.py | 292 +++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 35 +++++ lib/query.py | 85 ++++++++++++ requirements.txt | 4 + run.py | 262 +++++++++++++++++++++++++++++++++++ 13 files changed, 1173 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 app.py create mode 100644 cdk.json create mode 100644 dms_sample/__init__.py create mode 100644 dms_sample/stack.py create mode 100644 docker-compose.yml create mode 100644 lib/query.py create mode 100644 requirements.txt create mode 100644 run.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b786a9c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: Deploy on LocalStack + +on: + push: + paths-ignore: + - 'README.md' + branches: + - main + pull_request: + branches: + - main + schedule: + # “At 00:00 on Sunday.” + - cron: "0 0 * * 0" + workflow_dispatch: + +jobs: + cdk: + name: Setup infrastructure using CDK + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install CDK + run: | + npm install -g aws-cdk-local aws-cdk + cdklocal --version + + - name: Install dependencies + run: | + make install + + - name: Start LocalStack + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + run: | + export LOCALSTACK_AUTH_TOKEN=$LOCALSTACK_AUTH_TOKEN + make start + sleep 30 + + - name: Deploy the infrastructure + run: | + make deploy + + - name: Run tests + run: | + make run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae34260 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +.venv/ +volume/ +.DS_Store +cdk.out/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4498737 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +VENV_BIN ?= python3 -m venv +VENV_DIR ?= .venv +PIP_CMD ?= pip3 + +USERNAME ?= admin123 +DB_NAME ?= testdb +USERPWD ?= mysecretpassword +STACK_NAME ?= DMsSampleSetupStack +DB_ENDPOINT ?= postgres_server +DB_PORT ?= 5432 +ENDPOINT_URL = http://localhost.localstack.cloud:4566 +export AWS_ACCESS_KEY_ID ?= test +export AWS_SECRET_ACCESS_KEY ?= test +export AWS_DEFAULT_REGION ?= us-east-1 + +VENV_RUN = . $(VENV_ACTIVATE) + +CLOUD_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) +LOCAL_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) DB_ENDPOINT=$(DB_ENDPOINT) DB_PORT=$(DB_PORT) ENDPOINT_URL=$(ENDPOINT_URL) + +ifeq ($(OS), Windows_NT) + VENV_ACTIVATE = $(VENV_DIR)/Scripts/activate +else + VENV_ACTIVATE = $(VENV_DIR)/bin/activate +endif + +usage: ## Show this help + @grep -Fh "##" $(MAKEFILE_LIST) | grep -Fv fgrep | sed -e 's/:.*##\s*/##/g' | awk -F'##' '{ printf "%-25s %s\n", $$1, $$2 }' + +$(VENV_ACTIVATE): + test -d $(VENV_DIR) || $(VENV_BIN) $(VENV_DIR) + $(VENV_RUN); touch $(VENV_ACTIVATE) + +venv: $(VENV_ACTIVATE) ## Create a new (empty) virtual environment + +start: + $(LOCAL_ENV) docker compose up --build --detach --wait + +install: venv + $(VENV_RUN); $(PIP_CMD) install -r requirements.txt + +deploy: + $(VENV_RUN); $(LOCAL_ENV) cdklocal bootstrap --output ./cdk.local.out + $(VENV_RUN); $(LOCAL_ENV) cdklocal deploy --require-approval never --output ./cdk.local.out + +deploy-aws: + $(VENV_RUN); $(CLOUD_ENV) cdk bootstrap + $(VENV_RUN); $(CLOUD_ENV) cdk deploy --require-approval never + +destroy: + docker-compose down + +destroy-aws: venv + $(VENV_RUN); $(CLOUD_ENV) cdk destroy --require-approval never + +run: + $(VENV_RUN); $(LOCAL_ENV) python run.py + +run-aws: + $(VENV_RUN); $(CLOUD_ENV) python run.py diff --git a/README.md b/README.md index 3db4c27..9dfb825 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,91 @@ -# sample-dms-aurora-postgresql-kinesis -[WIP] Sample Application showcasing how to use Aurora PostgreSQL as a source and kinesis as a target to create full-load and cdc tasks using the CDK in Python +# Sample Application showcasing how to use DMS to create CDC with Aurora PostgreSQL source and kinesis target + +## Introduction + +This scenario demonstrates how to use Database Migration Service (DMS) to create change data capture (CDC) tasks using the Cloud Development Kit in Python. It is a self-contained setup that will create a VPC to host the DMS replication instance, a database, a Kinesis stream, and a replication task. + +![dms-postgres-to-kinesis](./dms-postgres-to-kinesis.jpg) + +## Pre-requisites + +- [LocalStack Auth Token](https://docs.localstack.cloud/getting-started/auth-token/) +- [Python 3.10](https://www.python.org/downloads/) & `pip` +- [Docker Compose](https://docs.docker.com/compose/install/) +- [CDK](https://docs.localstack.cloud/user-guide/integrations/aws-cdk/) with the [`cdklocal`](https://github.com/localstack/aws-cdk-local) wrapper. + + +Start LocalStack Pro with the `LOCALSTACK_AUTH_TOKEN` pre-configured: + +```bash +export LOCALSTACK_AUTH_TOKEN= +docker-compose up +``` + +The Docker Compose file will start LocalStack Pro container and a Postgres container. The Postgres container will be used to showcase how to reach a database external to LocalStack. + +## Instructions + +### Install the dependencies + +Install all the dependencies by running the following command: + +```bash +make install +``` + +### Creating the infrastructure + +To deploy the infrastructure, you can run the following command: + +```bash +make deploy +``` + +After successful deployment, you will see the following output: + +```bash +Outputs: +DMsSampleSetupStack.cdcTask = arn:aws:dms:us-east-1:000000000000:task:F6V3I917K2919C2HGVXCKE8O8AY19SX7M4TZH2U +DMsSampleSetupStack.dbSecret = arn:aws:secretsmanager:us-east-1:000000000000:secret:DMsSampleSetupStack-postgressecret-cb6c3bd1-vgGron +DMsSampleSetupStack.kinesisStream = arn:aws:kinesis:us-east-1:000000000000:stream/DMsSampleSetupStack-TargetStream3B4B2880-1d69ef19 +Stack ARN: +arn:aws:cloudformation:us-east-1:000000000000:stack/DMsSampleSetupStack/8f4fb494 + +✨ Total time: 45.06s +``` + +### Running the tasks + +You can run the tasks by executing the following command: + +```bash +make run +``` + +## Developer Notes + +A replication task gets deployed with the stack: + +A CDC replication task runs against the RDS database: + +- Creates three tables: `authors`, `accounts`, `books` +- Starts CDC replication task +- Captures and logs 3 Kinesis events: 1 for `awsdms_apply_exceptions` table, 3 for our tables +- Makes 3 inserts +- Captures and logs 3 Kinesis events +- Makes 3 table alterations, 1 per table +- Captures and logs 3 Kinesis events +- Logs `table_statistics` for the task + +## Deploying on AWS + +You can deploy and run the stack on AWS by running the following commands: + +```bash +make deploy-aws +make run-aws +``` + +## License + +This project is licensed under the Apache 2.0 License. diff --git a/app.py b/app.py new file mode 100644 index 0000000..ec65d78 --- /dev/null +++ b/app.py @@ -0,0 +1,11 @@ +import os +import aws_cdk as cdk + +from dms_sample.stack import DmsSampleStack + +STACK_NAME = os.getenv("STACK_NAME", "DMSPostgresKinesis") + +app = cdk.App() +DmsSampleStack(app, STACK_NAME) + +app.synth() diff --git a/cdk.json b/cdk.json new file mode 100644 index 0000000..20c5a8f --- /dev/null +++ b/cdk.json @@ -0,0 +1,69 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false + } +} diff --git a/dms_sample/__init__.py b/dms_sample/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dms_sample/stack.py b/dms_sample/stack.py new file mode 100644 index 0000000..6b120fb --- /dev/null +++ b/dms_sample/stack.py @@ -0,0 +1,292 @@ +import json +import os + +import aws_cdk as cdk +from aws_cdk import SecretValue, Stack +from aws_cdk import aws_dms as dms +from aws_cdk import aws_ec2 as ec2 +from aws_cdk import aws_iam as iam +from aws_cdk import aws_kinesis as kinesis +from aws_cdk import aws_rds as rds +from aws_cdk import aws_secretsmanager as secretsmanager +from constructs import Construct + + +USERNAME = os.getenv("USERNAME", "") +USER_PWD = os.getenv("USERPWD", "") +DB_NAME = os.getenv("DB_NAME", "") +SCHEMA_NAME = "public" + + +class DmsSampleStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + # VPC and Security Group + vpc = ec2.Vpc( + self, + "vpc", + max_azs=2, + nat_gateways=0, + enable_dns_support=True, + enable_dns_hostnames=True, + subnet_configuration=[ + ec2.SubnetConfiguration( + name="public", + cidr_mask=24, + subnet_type=ec2.SubnetType.PUBLIC, + ), + ], + ) + security_group = create_security_group(self, vpc) + + # IAM Role for DMS + dms_assume_role = iam.Role( + self, "SuperRole", assumed_by=iam.ServicePrincipal("dms.amazonaws.com") + ) + + # Aurora PostgreSQL Cluster + engine = rds.DatabaseClusterEngine.aurora_postgres( + version=rds.AuroraPostgresEngineVersion.VER_15_3 + ) + + parameter_group = rds.ParameterGroup( + self, + "parameterGroup", + engine=engine, + parameters={"rds.logical_replication": "1"}, + ) + + credentials = rds.Credentials.from_password( + username=USERNAME, + password=SecretValue.unsafe_plain_text(USER_PWD), + ) + + aurora_cluster = rds.DatabaseCluster( + self, + "auroraCluster", + engine=engine, + parameter_group=parameter_group, + vpc=vpc, + security_groups=[security_group], + writer=rds.ClusterInstance.serverless_v2("writer"), + readers=[rds.ClusterInstance.serverless_v2("reader", scale_with_writer=True)], + serverless_v2_min_capacity=0.5, + serverless_v2_max_capacity=1, + credentials=credentials, + removal_policy=cdk.RemovalPolicy.DESTROY, + default_database_name=DB_NAME, + vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), + ) + + # Secrets Manager for DB Access + db_port = cdk.Token.as_number(aurora_cluster.cluster_endpoint.port) + + allow_from_port(security_group, db_port) + + postgres_secret = create_secret(self, aurora_cluster.cluster_endpoint.hostname, db_port) + + postgres_access_role = create_postgres_access_role(self, postgres_secret) + + # Source Endpoint + source_endpoint = create_source_endpoint(self, postgres_access_role, postgres_secret) + + # Kinesis Stream + target_stream = create_kinesis_stream(self, dms_assume_role) + + # Target Endpoint + target_endpoint = create_kinesis_target_endpoint(self, target_stream, dms_assume_role) + + # DMS Replication Instance + replication_instance = create_replication_instance(self, vpc, security_group) + + # Create CDC replication task + cdc_task = create_replication_task( + self, + "cdc-task", + replication_instance=replication_instance, + source=source_endpoint, + target=target_endpoint, + migration_type="cdc", + ) + + # Outputs + cdk.CfnOutput(self, "kinesisStream", value=target_stream.stream_arn) + cdk.CfnOutput(self, "cdcTask", value=cdc_task.ref) + cdk.CfnOutput(self, "dbSecret", value=postgres_secret.ref) + + +def allow_from_port(security_group: ec2.SecurityGroup, port: int): + security_group.connections.allow_from( + port_range=ec2.Port.tcp_range(port, port), + other=ec2.Peer.any_ipv4(), + ) + + +def create_replication_task( + stack: Stack, + id: str, + replication_instance: dms.CfnReplicationInstance, + source: dms.CfnEndpoint, + target: dms.CfnEndpoint, + migration_type: str = "cdc", + replication_task_settings: dict = None, +) -> dms.CfnReplicationTask: + table_mappings = { + "rules": [ + { + "rule-type": "selection", + "rule-id": "1", + "rule-name": "rule1", + "object-locator": {"schema-name": "public", "table-name": "%"}, + "rule-action": "include", + } + ] + } + replication_task_settings = {"Logging": {"EnableLogging": True}} + + return dms.CfnReplicationTask( + stack, + id, + replication_task_identifier=id, + migration_type=migration_type, + replication_instance_arn=replication_instance.ref, + source_endpoint_arn=source.ref, + target_endpoint_arn=target.ref, + table_mappings=json.dumps(table_mappings), + replication_task_settings=json.dumps(replication_task_settings), + ) + + +def create_source_endpoint(stack: Stack, role: iam.Role, secret: secretsmanager.CfnSecret) -> dms.CfnEndpoint: + return dms.CfnEndpoint( + stack, + "source-postgres", + endpoint_type="source", + engine_name="aurora-postgresql", + database_name=DB_NAME, + postgre_sql_settings=dms.CfnEndpoint.PostgreSqlSettingsProperty( + secrets_manager_access_role_arn=role.role_arn, + secrets_manager_secret_id=secret.ref, + ), + ) + + +def create_secret(stack: Stack, host: str, db_port: int) -> secretsmanager.CfnSecret: + return secretsmanager.CfnSecret( + stack, + "postgres-secret", + secret_string=json.dumps( + { + "username": USERNAME, + "password": USER_PWD, + "host": host, + "port": db_port, + "dbname": DB_NAME, + } + ), + ) + + +def create_security_group(stack: Stack, vpc: ec2.Vpc) -> ec2.SecurityGroup: + return ec2.SecurityGroup( + stack, + "sg", + vpc=vpc, + description="Security group for DMS sample", + allow_all_outbound=True, + ) + + +def create_kinesis_stream(stack: Stack, dms_assume_role: iam.Role) -> kinesis.Stream: + stream = kinesis.Stream( + stack, "TargetStream", shard_count=1, retention_period=cdk.Duration.hours(24) + ) + stream.grant_read_write(dms_assume_role) + stream.apply_removal_policy(cdk.RemovalPolicy.DESTROY) + return stream + + +def create_postgres_access_role(stack: Stack, postgres_secret: secretsmanager.CfnSecret) -> iam.Role: + return iam.Role( + stack, + "postgres-access-role", + assumed_by=iam.ServicePrincipal(f"dms.{stack.region}.amazonaws.com"), + inline_policies={ + "AllowSecrets": iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + actions=["secretsmanager:GetSecretValue"], + effect=iam.Effect.ALLOW, + resources=[postgres_secret.ref], + ) + ] + ) + }, + ) + + +def create_kinesis_target_endpoint(stack: Stack, target_stream: kinesis.Stream, dms_assume_role: iam.Role) -> dms.CfnEndpoint: + return dms.CfnEndpoint( + stack, + "target", + endpoint_type="target", + engine_name="kinesis", + kinesis_settings=dms.CfnEndpoint.KinesisSettingsProperty( + stream_arn=target_stream.stream_arn, + message_format="json", + service_access_role_arn=dms_assume_role.role_arn, + include_control_details=True, + include_null_and_empty=True, + include_partition_value=True, + include_table_alter_operations=True, + include_transaction_details=True, + partition_include_schema_table=True, + ), + ) + + +def create_replication_instance( + stack: Stack, vpc: ec2.Vpc, security_group: ec2.SecurityGroup +) -> dms.CfnReplicationInstance: + # Role definitions + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "dms.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + + iam.CfnRole( + stack, + "DmsVpcRole", + managed_policy_arns=[ + "arn:aws:iam::aws:policy/service-role/AmazonDMSVPCManagementRole", + ], + assume_role_policy_document=assume_role_policy_document, + role_name="dms-vpc-role", # this exact name needs to be set + ) + replication_subnet_group = dms.CfnReplicationSubnetGroup( + stack, + "ReplSubnetGroup", + replication_subnet_group_description="Replication Subnet Group for DMS test", + subnet_ids=[subnet.subnet_id for subnet in vpc.public_subnets], + ) + + return dms.CfnReplicationInstance( + stack, + "replication-instance", + replication_instance_class="dms.t3.micro", + allocated_storage=5, + replication_subnet_group_identifier=replication_subnet_group.ref, + allow_major_version_upgrade=False, + auto_minor_version_upgrade=False, + multi_az=False, + publicly_accessible=True, + vpc_security_group_ids=[security_group.security_group_id], + availability_zone=vpc.public_subnets[0].availability_zone, + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e5823cd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3.8" + +services: + localstack: + container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}" + image: localstack/localstack-pro:latest # required for Pro + pull_policy: always + ports: + - "127.0.0.1:4566:4566" # LocalStack Gateway + - "127.0.0.1:4510-4559:4510-4559" # external services port range + - "127.0.0.1:443:443" # LocalStack HTTPS Gateway (Pro) + environment: + # Activate LocalStack Pro: https://docs.localstack.cloud/getting-started/auth-token/ + - LOCALSTACK_AUTH_TOKEN=${LOCALSTACK_AUTH_TOKEN:?} # required for Pro + - ENABLE_DMS=1 + volumes: + - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" + - "/var/run/docker.sock:/var/run/docker.sock" + healthcheck: + test: curl --fail localhost.localstack.cloud:4566/_localstack/health || exit 1 + interval: 5s + timeout: 2s + retries: 5 + start_period: 10s + postgres_server: + container_name: dms-sample-postgres + image: postgres + ports: + - "127.0.0.1:5432:5432" + restart: always + command: -c 'wal_level=logical' -c 'max_replication_slots=10' -c 'max_wal_senders=10' + environment: + - POSTGRES_DB=${DB_NAME:-testdb} + - POSTGRES_USER=${USERNAME:-admin123} + - POSTGRES_PASSWORD=${USERPWD:-mysecretpassword} \ No newline at end of file diff --git a/lib/query.py b/lib/query.py new file mode 100644 index 0000000..22ea9f5 --- /dev/null +++ b/lib/query.py @@ -0,0 +1,85 @@ +PSQL_CREATE_ACCOUNTS_TABLE = """CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + age SMALLINT, + birth_date DATE, + account_balance NUMERIC(10, 2), + is_active BOOLEAN, + signup_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP, + bio TEXT, + profile_picture BYTEA, + favorite_color TEXT CHECK (favorite_color IN ('red', 'green', 'blue')), + height REAL, + weight DOUBLE PRECISION + );""" +PSQL_INSERT_ACCOUNTS_SAMPLE_DATA = """INSERT INTO accounts +(name, age, birth_date, account_balance, is_active, signup_time, last_login, bio, profile_picture, favorite_color, height, weight) +VALUES +('Alice', 30, '1991-05-21', 1500.00, TRUE, '2021-01-08 09:00:00', '2021-03-10 08:00:00', 'Bio of Alice', NULL, 'red', 1.70, 60.5);""" + +PSQL_CREATE_AUTHORS_TABLE = """CREATE TABLE authors ( + author_id SERIAL PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + date_of_birth DATE, + nationality VARCHAR(50), + biography TEXT, + email VARCHAR(255), + phone_number VARCHAR(20), + active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +);""" + +PSQL_INSERT_AUTHORS_SAMPLE_DATA = """INSERT INTO authors (first_name, last_name, date_of_birth, nationality, biography, email, phone_number) +VALUES +('John', 'Doe', '1980-01-01', 'American', 'Biography of John Doe.', 'john.doe@example.com', '123-456-7890');""" + +PSQL_CREATE_BOOKS_TABLE = """CREATE TABLE books ( + book_id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + author_id INT, + publish_date DATE, + isbn VARCHAR(20), + genre VARCHAR(100), + page_count INT, + publisher VARCHAR(100), + language VARCHAR(50), + available_copies INT, + total_copies INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (author_id) REFERENCES authors(author_id) +); +""" +PSQL_INSERT_BOOKS_SAMPLE_DATA = """INSERT INTO books (title, author_id, publish_date, isbn, genre, page_count, publisher, language, available_copies, total_copies) +VALUES +('The Great Adventure', 1, '2020-06-01', '978-3-16-148410-0', 'Adventure', 300, 'Adventure Press', 'English', 10, 20);""" + +ALTER_TABLES = [ + # control: column-type-change -> books + "ALTER TABLE books ALTER COLUMN isbn TYPE VARCHAR(30);", + + # control: drop-column -> accounts + "ALTER TABLE accounts DROP COLUMN profile_picture;", + + # control: add-column with default value -> authors + "ALTER TABLE authors ADD COLUMN is_available BOOLEAN DEFAULT TRUE;", +] + +CREATE_TABLES = [ + PSQL_CREATE_AUTHORS_TABLE, + PSQL_CREATE_ACCOUNTS_TABLE, + PSQL_CREATE_BOOKS_TABLE, +] + +DROP_TABLES = [ + "DROP TABLE IF EXISTS books;", + "DROP TABLE IF EXISTS accounts;", + "DROP TABLE IF EXISTS authors;", +] + +PRESEED_DATA = [ + PSQL_INSERT_AUTHORS_SAMPLE_DATA, + PSQL_INSERT_ACCOUNTS_SAMPLE_DATA, + PSQL_INSERT_BOOKS_SAMPLE_DATA, +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6c7ae16 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +aws-cdk-lib==2.138.0 +boto3==1.34.96 +constructs>=10.0.0,<11.0.0 +psycopg2-binary==2.9.10 diff --git a/run.py b/run.py new file mode 100644 index 0000000..87a3188 --- /dev/null +++ b/run.py @@ -0,0 +1,262 @@ +import json +import os +import time +from pprint import pprint +from time import sleep +from typing import Callable, TypedDict, TypeVar + +import psycopg2 +from psycopg2.extras import RealDictCursor +from boto3 import client + +from lib import query as q + +STACK_NAME = os.getenv("STACK_NAME", "DMSPostgresKinesis") + +ENDPOINT_URL = os.getenv("ENDPOINT_URL") + +cfn = client("cloudformation", endpoint_url=ENDPOINT_URL) +dms = client("dms", endpoint_url=ENDPOINT_URL) +kinesis = client("kinesis", endpoint_url=ENDPOINT_URL) +secretsmanager = client("secretsmanager", endpoint_url=ENDPOINT_URL) + + +retries = 100 if not ENDPOINT_URL else 10 +retry_sleep = 5 if not ENDPOINT_URL else 1 + + +class CfnOutput(TypedDict): + cdcTask: str + kinesisStream: str + dbSecret: str + + +class Credentials(TypedDict): + host: str + dbname: str + username: str + password: str + port: int + + +def get_cfn_output(): + stacks = cfn.describe_stacks()["Stacks"] + stack = None + for s in stacks: + if s["StackName"] == STACK_NAME: + stack = s + break + if not stack: + raise Exception(f"Stack {STACK_NAME} Not found") + + outputs = stack["Outputs"] + cfn_output = CfnOutput() + for output in outputs: + cfn_output[output["OutputKey"]] = output["OutputValue"] + return cfn_output + + +def get_credentials(secret_arn: str) -> Credentials: + secret_value = secretsmanager.get_secret_value(SecretId=secret_arn) + credentials = Credentials(**json.loads(secret_value["SecretString"])) + if credentials["host"] == "postgres_server": + credentials["host"] = "localhost" + return credentials + + +T = TypeVar("T") + + +def retry( + function: Callable[..., T], retries=retries, sleep=retry_sleep, **kwargs +) -> T: + raise_error = None + retries = int(retries) + for i in range(0, retries + 1): + try: + return function(**kwargs) + except Exception as error: + raise_error = error + time.sleep(sleep) + raise raise_error + + +def run_queries_on_postgres( + credentials: Credentials, + queries: list[str], +): + cursor = None + cnx = None + try: + cnx = psycopg2.connect( + user=credentials["username"], + password=credentials["password"], + host=credentials["host"], + dbname=credentials["dbname"], + cursor_factory=RealDictCursor, + port=int(credentials["port"]), + ) + cursor = cnx.cursor() + for query in queries: + cursor.execute(query) + cnx.commit() + finally: + if cursor: + cursor.close() + if cnx: + cnx.close() + + +def get_query_result( + credentials: Credentials, + query: str, +): + cursor = None + cnx = None + try: + cnx = psycopg2.connect( + user=credentials["username"], + password=credentials["password"], + host=credentials["host"], + dbname=credentials["dbname"], + cursor_factory=RealDictCursor, + port=int(credentials["port"]), + ) + cursor = cnx.cursor() + cursor.execute(query) + return cursor.fetchall() + finally: + if cursor: + cursor.close() + if cnx: + cnx.close() + +def start_task(task: str): + response = dms.start_replication_task( + ReplicationTaskArn=task, StartReplicationTaskType="start-replication" + ) + status = response["ReplicationTask"].get("Status") + print(f"Replication Task {task} status: {status}") + + +def stop_task(task: str): + response = dms.stop_replication_task(ReplicationTaskArn=task) + status = response["ReplicationTask"].get("Status") + print(f"\n Replication Task {task} status: {status}") + + +def wait_for_task_status(task: str, expected_status: str): + print(f"Waiting for task status {expected_status}") + + def _wait_for_status(): + status = dms.describe_replication_tasks( + Filters=[{"Name": "replication-task-arn", "Values": [task]}], + WithoutSettings=True, + )["ReplicationTasks"][0].get("Status") + print(f"{task=} {status=}") + assert status == expected_status + + retry(_wait_for_status) + + +def wait_for_kinesis(stream: str, expected_count: int, threshold_timestamp: int): + print("\n\tKinesis events\n") + print("fetching Kinesis event") + + shard_id = kinesis.describe_stream(StreamARN=stream)["StreamDescription"]["Shards"][ + 0 + ]["ShardId"] + shard_iterator = kinesis.get_shard_iterator( + StreamARN=stream, + ShardId=shard_id, + ShardIteratorType="TRIM_HORIZON", + ) + shard_iter = shard_iterator["ShardIterator"] + all_records = [] + while shard_iter is not None: + res = kinesis.get_records(ShardIterator=shard_iter, Limit=50) + shard_iter = res["NextShardIterator"] + records = res["Records"] + for r in records: + if r["ApproximateArrivalTimestamp"].timestamp() > threshold_timestamp: + all_records.append(r) + if len(all_records) >= expected_count: + break + print(f"found {len(all_records)}, {expected_count=}") + sleep(retry_sleep) + print(f"Received: {len(all_records)} events") + pprint( + [ + {**json.loads(record["Data"]), "partition_key": record["PartitionKey"]} + for record in all_records + ] + ) + + +def describe_table_statistics(task_arn: str): + res = dms.describe_table_statistics( + ReplicationTaskArn=task_arn, + ) + res["TableStatistics"] = sorted( + res["TableStatistics"], key=lambda x: (x["SchemaName"], x["TableName"]) + ) + return res + + +def execute_cdc(cfn_output: CfnOutput): + # CDC Flow + credentials = get_credentials(cfn_output["dbSecret"]) + task = cfn_output["cdcTask"] + stream = cfn_output["kinesisStream"] + print("") + print("*" * 12) + print("STARTING CDC FLOW") + print("*" * 12) + print(f"db endpoint: {credentials['host']}:{credentials['port']}\n") + + run_queries_on_postgres(credentials, q.DROP_TABLES) + print("\tCreating tables") + run_queries_on_postgres(credentials, q.CREATE_TABLES) + + threshold_timestamp = int(time.time()) + print("Starting CDC task") + start_task(task) + wait_for_task_status(task, "running") + + print("\n****Create table events****\n") + # 1 create apply_dms_exception, 3 create + wait_for_kinesis(stream, 4, threshold_timestamp) + print("\n****End create table events****\n") + + print("\n****INSERT events****\n") + sleep(1) + threshold_timestamp = int(time.time()) + sleep(1) + run_queries_on_postgres(credentials, q.PRESEED_DATA) + # 1 authors, 1 accounts, 1 books + wait_for_kinesis(stream, 3, threshold_timestamp) + print("\n****End of INSERT events****\n") + + print("\n****ALTER tables events****\n") + sleep(1) + threshold_timestamp = int(time.time()) + sleep(1) + run_queries_on_postgres(credentials, q.ALTER_TABLES) + wait_for_kinesis(stream, 3, threshold_timestamp) + print("\n****End of ALTER tables events****\n") + + print("\n****Table Statistics****\n") + print("\tTable Statistics tasks") + pprint(describe_table_statistics(task)) + + stop_task(task) + wait_for_task_status(task, "stopped") + + print("\n\tDrop tables") + run_queries_on_postgres(credentials, q.DROP_TABLES) + + +if __name__ == "__main__": + cfn_output = get_cfn_output() + + execute_cdc(cfn_output) From 60360305000c3732a47f77248443d983ea45ae1b Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Tue, 25 Feb 2025 14:09:12 +0530 Subject: [PATCH 02/11] change the replication instance class --- dms_sample/stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dms_sample/stack.py b/dms_sample/stack.py index 6b120fb..70c3ea9 100644 --- a/dms_sample/stack.py +++ b/dms_sample/stack.py @@ -280,7 +280,7 @@ def create_replication_instance( return dms.CfnReplicationInstance( stack, "replication-instance", - replication_instance_class="dms.t3.micro", + replication_instance_class="dms.t2.micro", allocated_storage=5, replication_subnet_group_identifier=replication_subnet_group.ref, allow_major_version_upgrade=False, From e3700df472a78b859ec79be32a3dbc8d01ce3556 Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Tue, 25 Feb 2025 14:25:15 +0530 Subject: [PATCH 03/11] add arch diagram --- dms-postgres-to-kinesis.jpg | Bin 0 -> 102541 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 dms-postgres-to-kinesis.jpg diff --git a/dms-postgres-to-kinesis.jpg b/dms-postgres-to-kinesis.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ba93c0de776e9c2acf901b8758f7931dab2837a6 GIT binary patch literal 102541 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 +S(dqW$cNc~!hc$FGf~&3GEW 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⪼Jd6;Bnr9#j_!Edj*IvcjVJgvZj|1!IH*}ShI{} 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 literal 0 HcmV?d00001 From 087e9bc9ff76775846654df25e8f9ea29c6e4edf Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Tue, 25 Feb 2025 14:52:17 +0530 Subject: [PATCH 04/11] fix lint --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index e5823cd..d563a8a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,4 +32,4 @@ services: environment: - POSTGRES_DB=${DB_NAME:-testdb} - POSTGRES_USER=${USERNAME:-admin123} - - POSTGRES_PASSWORD=${USERPWD:-mysecretpassword} \ No newline at end of file + - POSTGRES_PASSWORD=${USERPWD:-mysecretpassword} From 5e9b79a36d9d99822801374e6a3528bb9f5e6e66 Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Wed, 26 Feb 2025 10:50:34 +0530 Subject: [PATCH 05/11] fix number of events --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9dfb825..469a56f 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ A CDC replication task runs against the RDS database: - Creates three tables: `authors`, `accounts`, `books` - Starts CDC replication task -- Captures and logs 3 Kinesis events: 1 for `awsdms_apply_exceptions` table, 3 for our tables +- Captures and logs 4 Kinesis events: 1 for `awsdms_apply_exceptions` table, 3 for our tables - Makes 3 inserts - Captures and logs 3 Kinesis events - Makes 3 table alterations, 1 per table From 7a1d4ef51a70ad514428c8ee649130992d53290e Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Wed, 26 Feb 2025 12:28:40 +0530 Subject: [PATCH 06/11] non default values for kinesis --- README.md | 2 +- dms_sample/stack.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 469a56f..78f823e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Introduction -This scenario demonstrates how to use Database Migration Service (DMS) to create change data capture (CDC) tasks using the Cloud Development Kit in Python. It is a self-contained setup that will create a VPC to host the DMS replication instance, a database, a Kinesis stream, and a replication task. +This scenario demonstrates how to use Database Migration Service (DMS) to create change data capture (CDC) tasks using the Cloud Development Kit in Python. It is a self-contained setup that will create a VPC to host the DMS replication instance, a database, a Kinesis stream with non-default settings, and a replication task. ![dms-postgres-to-kinesis](./dms-postgres-to-kinesis.jpg) diff --git a/dms_sample/stack.py b/dms_sample/stack.py index 70c3ea9..b767301 100644 --- a/dms_sample/stack.py +++ b/dms_sample/stack.py @@ -240,7 +240,7 @@ def create_kinesis_target_endpoint(stack: Stack, target_stream: kinesis.Stream, include_null_and_empty=True, include_partition_value=True, include_table_alter_operations=True, - include_transaction_details=True, + include_transaction_details=False, partition_include_schema_table=True, ), ) From 17b94d890a4c10ee37e5edb29576290c319d6645 Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Wed, 26 Feb 2025 12:41:35 +0530 Subject: [PATCH 07/11] postgres-server remove --- Makefile | 3 +-- docker-compose.yml | 11 ----------- run.py | 2 -- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 4498737..3cb12dd 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,6 @@ USERNAME ?= admin123 DB_NAME ?= testdb USERPWD ?= mysecretpassword STACK_NAME ?= DMsSampleSetupStack -DB_ENDPOINT ?= postgres_server DB_PORT ?= 5432 ENDPOINT_URL = http://localhost.localstack.cloud:4566 export AWS_ACCESS_KEY_ID ?= test @@ -16,7 +15,7 @@ export AWS_DEFAULT_REGION ?= us-east-1 VENV_RUN = . $(VENV_ACTIVATE) CLOUD_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) -LOCAL_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) DB_ENDPOINT=$(DB_ENDPOINT) DB_PORT=$(DB_PORT) ENDPOINT_URL=$(ENDPOINT_URL) +LOCAL_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) DB_PORT=$(DB_PORT) ENDPOINT_URL=$(ENDPOINT_URL) ifeq ($(OS), Windows_NT) VENV_ACTIVATE = $(VENV_DIR)/Scripts/activate diff --git a/docker-compose.yml b/docker-compose.yml index d563a8a..259db3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,14 +22,3 @@ services: timeout: 2s retries: 5 start_period: 10s - postgres_server: - container_name: dms-sample-postgres - image: postgres - ports: - - "127.0.0.1:5432:5432" - restart: always - command: -c 'wal_level=logical' -c 'max_replication_slots=10' -c 'max_wal_senders=10' - environment: - - POSTGRES_DB=${DB_NAME:-testdb} - - POSTGRES_USER=${USERNAME:-admin123} - - POSTGRES_PASSWORD=${USERPWD:-mysecretpassword} diff --git a/run.py b/run.py index 87a3188..43556b5 100644 --- a/run.py +++ b/run.py @@ -59,8 +59,6 @@ def get_cfn_output(): def get_credentials(secret_arn: str) -> Credentials: secret_value = secretsmanager.get_secret_value(SecretId=secret_arn) credentials = Credentials(**json.loads(secret_value["SecretString"])) - if credentials["host"] == "postgres_server": - credentials["host"] = "localhost" return credentials From 77e2c274d6ce3fe4affbd0ec6669d32b697a3f95 Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Wed, 26 Feb 2025 13:00:31 +0530 Subject: [PATCH 08/11] fix reviews --- README.md | 5 ++++- dms_sample/stack.py | 46 +++++++++++++++++++++++++++++---------------- run.py | 6 +++++- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 78f823e..245c68e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Introduction -This scenario demonstrates how to use Database Migration Service (DMS) to create change data capture (CDC) tasks using the Cloud Development Kit in Python. It is a self-contained setup that will create a VPC to host the DMS replication instance, a database, a Kinesis stream with non-default settings, and a replication task. +This scenario demonstrates how to use Database Migration Service (DMS) to create change data capture (CDC) tasks using the Cloud Development Kit in Python. It is a self-contained setup that will create a VPC to host the DMS replication instance, a database, a Kinesis stream, and a replication task. ![dms-postgres-to-kinesis](./dms-postgres-to-kinesis.jpg) @@ -41,6 +41,9 @@ To deploy the infrastructure, you can run the following command: make deploy ``` +> NOTE: By default we create kinesis target endpoint with default settings. +> In order to create the target with non-default values set the environment to `KINESIS_TARGET=non-default`. + After successful deployment, you will see the following output: ```bash diff --git a/dms_sample/stack.py b/dms_sample/stack.py index b767301..af3788c 100644 --- a/dms_sample/stack.py +++ b/dms_sample/stack.py @@ -15,6 +15,7 @@ USERNAME = os.getenv("USERNAME", "") USER_PWD = os.getenv("USERPWD", "") DB_NAME = os.getenv("DB_NAME", "") +KINESIS_TARGET = os.getenv("KINESIS_TARGET", "default") SCHEMA_NAME = "public" @@ -227,23 +228,36 @@ def create_postgres_access_role(stack: Stack, postgres_secret: secretsmanager.Cf def create_kinesis_target_endpoint(stack: Stack, target_stream: kinesis.Stream, dms_assume_role: iam.Role) -> dms.CfnEndpoint: + if str.lower(KINESIS_TARGET) == "non-default": + return dms.CfnEndpoint( + stack, + "target", + endpoint_type="target", + engine_name="kinesis", + kinesis_settings=dms.CfnEndpoint.KinesisSettingsProperty( + stream_arn=target_stream.stream_arn, + message_format="json", + service_access_role_arn=dms_assume_role.role_arn, + include_control_details=True, + include_null_and_empty=True, + include_partition_value=True, + include_table_alter_operations=True, + include_transaction_details=False, + partition_include_schema_table=True, + ), + ) + return dms.CfnEndpoint( - stack, - "target", - endpoint_type="target", - engine_name="kinesis", - kinesis_settings=dms.CfnEndpoint.KinesisSettingsProperty( - stream_arn=target_stream.stream_arn, - message_format="json", - service_access_role_arn=dms_assume_role.role_arn, - include_control_details=True, - include_null_and_empty=True, - include_partition_value=True, - include_table_alter_operations=True, - include_transaction_details=False, - partition_include_schema_table=True, - ), - ) + stack, + "target", + endpoint_type="target", + engine_name="kinesis", + kinesis_settings=dms.CfnEndpoint.KinesisSettingsProperty( + stream_arn=target_stream.stream_arn, + message_format="json", + service_access_role_arn=dms_assume_role.role_arn, + ), + ) def create_replication_instance( diff --git a/run.py b/run.py index 43556b5..33b4f17 100644 --- a/run.py +++ b/run.py @@ -14,6 +14,7 @@ STACK_NAME = os.getenv("STACK_NAME", "DMSPostgresKinesis") ENDPOINT_URL = os.getenv("ENDPOINT_URL") +KINESIS_TARGET = os.getenv("KINESIS_TARGET", "default") cfn = client("cloudformation", endpoint_url=ENDPOINT_URL) dms = client("dms", endpoint_url=ENDPOINT_URL) @@ -240,7 +241,10 @@ def execute_cdc(cfn_output: CfnOutput): threshold_timestamp = int(time.time()) sleep(1) run_queries_on_postgres(credentials, q.ALTER_TABLES) - wait_for_kinesis(stream, 3, threshold_timestamp) + if str.lower(KINESIS_TARGET) == "non-default": + wait_for_kinesis(stream, 3, threshold_timestamp) + else: + wait_for_kinesis(stream, 0, threshold_timestamp) print("\n****End of ALTER tables events****\n") print("\n****Table Statistics****\n") From 1dda3e45b4988c6536c0c8dde95e81df5dd93380 Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Wed, 26 Feb 2025 13:34:20 +0530 Subject: [PATCH 09/11] fix readme for non-default event stats --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 245c68e..2546989 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ A CDC replication task runs against the RDS database: - Makes 3 inserts - Captures and logs 3 Kinesis events - Makes 3 table alterations, 1 per table -- Captures and logs 3 Kinesis events +- Captures and logs 3 Kinesis events for non-default settings else 0 - Logs `table_statistics` for the task ## Deploying on AWS From 512f8ebff0d23116497954da4ed4cbc171fe59ce Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Wed, 26 Feb 2025 16:59:22 +0530 Subject: [PATCH 10/11] fix Makefile for db_port --- Makefile | 3 +-- README.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3cb12dd..ede5d7e 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,6 @@ USERNAME ?= admin123 DB_NAME ?= testdb USERPWD ?= mysecretpassword STACK_NAME ?= DMsSampleSetupStack -DB_PORT ?= 5432 ENDPOINT_URL = http://localhost.localstack.cloud:4566 export AWS_ACCESS_KEY_ID ?= test export AWS_SECRET_ACCESS_KEY ?= test @@ -15,7 +14,7 @@ export AWS_DEFAULT_REGION ?= us-east-1 VENV_RUN = . $(VENV_ACTIVATE) CLOUD_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) -LOCAL_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) DB_PORT=$(DB_PORT) ENDPOINT_URL=$(ENDPOINT_URL) +LOCAL_ENV = USERNAME=$(USERNAME) DB_NAME=$(DB_NAME) USERPWD=$(USERPWD) STACK_NAME=$(STACK_NAME) ENDPOINT_URL=$(ENDPOINT_URL) ifeq ($(OS), Windows_NT) VENV_ACTIVATE = $(VENV_DIR)/Scripts/activate diff --git a/README.md b/README.md index 2546989..ec80d32 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ To deploy the infrastructure, you can run the following command: make deploy ``` -> NOTE: By default we create kinesis target endpoint with default settings. +> NOTE: By default we create the kinesis target endpoint with default settings. > In order to create the target with non-default values set the environment to `KINESIS_TARGET=non-default`. After successful deployment, you will see the following output: From 7c0636d78701505d96a88bd95dfa19c6b1449c54 Mon Sep 17 00:00:00 2001 From: sannya-singal Date: Wed, 26 Feb 2025 17:11:42 +0530 Subject: [PATCH 11/11] fix reviews --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ec80d32..aef0c37 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,10 @@ make deploy ``` > NOTE: By default we create the kinesis target endpoint with default settings. +> For non-default settings we enable capturing events related to DDL operations and +> include `NULL` and empty column values from the events. > In order to create the target with non-default values set the environment to `KINESIS_TARGET=non-default`. +> To know more about these settings, checkout the [official AWS documentation](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.Kinesis.html#:~:text=Kinesis%20Data%20Streams%20endpoint%20settings). After successful deployment, you will see the following output: