From b640b6c4f5d781d5ee8fcabcacd9f57a82cfde04 Mon Sep 17 00:00:00 2001 From: Raymond Eah Date: Wed, 11 Feb 2026 15:16:18 -0500 Subject: [PATCH 1/3] scope IAM policy names to role name to prevent EntityAlreadyExists on re-creation The Lambda that attaches resource collection permissions used hardcoded policy names (e.g. datadog-aws-integration-resource-collection-permissions-1) which are global per AWS account. When a stack is deleted and re-created with the same role name, cleanup may fail to delete the old policies (e.g. still attached), causing create_policy to fail with EntityAlreadyExists. Fix: include the IAM role name in policy names so they are unique per integration role. The existing wildcard ARN in the Lambda execution role already covers any suffix pattern. Co-Authored-By: Claude Opus 4.6 --- .../attach_integration_permissions.py | 46 +++++++++---------- aws_quickstart/datadog_integration_role.yaml | 46 +++++++++---------- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/aws_quickstart/attach_integration_permissions.py b/aws_quickstart/attach_integration_permissions.py index 16bcc76..977cb4a 100644 --- a/aws_quickstart/attach_integration_permissions.py +++ b/aws_quickstart/attach_integration_permissions.py @@ -35,31 +35,30 @@ def fetch_permissions_from_datadog(api_url): json_response = json.loads(response.read()) return json_response["data"]["attributes"]["permissions"] +def _detach_and_delete_policy(iam_client, role_name, policy_arn, policy_name): + """Detach a managed policy from a role and delete it. Ignores missing entities.""" + try: + iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn) + except iam_client.exceptions.NoSuchEntityException: + pass + except Exception as e: + LOGGER.error(f"Error detaching policy {policy_name}: {str(e)}") + + try: + iam_client.delete_policy(PolicyArn=policy_arn) + except iam_client.exceptions.NoSuchEntityException: + pass + except iam_client.exceptions.DeleteConflictException: + LOGGER.warning(f"Policy {policy_name} still attached, skipping delete") + except Exception as e: + LOGGER.error(f"Error deleting policy {policy_name}: {str(e)}") + def cleanup_existing_policies(iam_client, role_name, account_id, partition, max_policies=10): - # Remove resource collection permissions + # Remove role-scoped resource collection policies for i in range(max_policies): - policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{i+1}" + policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{role_name}-{i+1}" policy_arn = f"arn:{partition}:iam::{account_id}:policy/{policy_name}" - try: - iam_client.detach_role_policy( - RoleName=role_name, - PolicyArn=policy_arn - ) - except iam_client.exceptions.NoSuchEntityException: - pass - except Exception as e: - LOGGER.error(f"Error detaching policy {policy_name}: {str(e)}") - - try: - iam_client.delete_policy( - PolicyArn=policy_arn - ) - except iam_client.exceptions.NoSuchEntityException: - pass - except iam_client.exceptions.DeleteConflictException: - LOGGER.warning(f"Policy {policy_name} still attached, skipping delete") - except Exception as e: - LOGGER.error(f"Error deleting policy {policy_name}: {str(e)}") + _detach_and_delete_policy(iam_client, role_name, policy_arn, policy_name) # Remove standard permissions try: @@ -96,8 +95,7 @@ def attach_resource_collection_permissions(iam_client, role_name): # Create and attach new policies for i, chunk in enumerate(permission_chunks): - # Create policy - policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{i+1}" + policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{role_name}-{i+1}" policy_document = { "Version": "2012-10-17", "Statement": [ diff --git a/aws_quickstart/datadog_integration_role.yaml b/aws_quickstart/datadog_integration_role.yaml index 17fb792..6bd37ba 100644 --- a/aws_quickstart/datadog_integration_role.yaml +++ b/aws_quickstart/datadog_integration_role.yaml @@ -143,31 +143,30 @@ Resources: json_response = json.loads(response.read()) return json_response["data"]["attributes"]["permissions"] + def _detach_and_delete_policy(iam_client, role_name, policy_arn, policy_name): + """Detach a managed policy from a role and delete it. Ignores missing entities.""" + try: + iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn) + except iam_client.exceptions.NoSuchEntityException: + pass + except Exception as e: + LOGGER.error(f"Error detaching policy {policy_name}: {str(e)}") + + try: + iam_client.delete_policy(PolicyArn=policy_arn) + except iam_client.exceptions.NoSuchEntityException: + pass + except iam_client.exceptions.DeleteConflictException: + LOGGER.warning(f"Policy {policy_name} still attached, skipping delete") + except Exception as e: + LOGGER.error(f"Error deleting policy {policy_name}: {str(e)}") + def cleanup_existing_policies(iam_client, role_name, account_id, partition, max_policies=10): - # Remove resource collection permissions + # Remove role-scoped resource collection policies for i in range(max_policies): - policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{i+1}" + policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{role_name}-{i+1}" policy_arn = f"arn:{partition}:iam::{account_id}:policy/{policy_name}" - try: - iam_client.detach_role_policy( - RoleName=role_name, - PolicyArn=policy_arn - ) - except iam_client.exceptions.NoSuchEntityException: - pass - except Exception as e: - LOGGER.error(f"Error detaching policy {policy_name}: {str(e)}") - - try: - iam_client.delete_policy( - PolicyArn=policy_arn - ) - except iam_client.exceptions.NoSuchEntityException: - pass - except iam_client.exceptions.DeleteConflictException: - LOGGER.warning(f"Policy {policy_name} still attached, skipping delete") - except Exception as e: - LOGGER.error(f"Error deleting policy {policy_name}: {str(e)}") + _detach_and_delete_policy(iam_client, role_name, policy_arn, policy_name) # Remove standard permissions try: @@ -204,8 +203,7 @@ Resources: # Create and attach new policies for i, chunk in enumerate(permission_chunks): - # Create policy - policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{i+1}" + policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{role_name}-{i+1}" policy_document = { "Version": "2012-10-17", "Statement": [ From e4e9d980310eda34d6e519c21c9f26180fa6c3a9 Mon Sep 17 00:00:00 2001 From: Raymond Eah Date: Wed, 11 Feb 2026 15:30:06 -0500 Subject: [PATCH 2/3] bump changelog and version.txt --- aws_quickstart/CHANGELOG.md | 8 ++++++++ aws_quickstart/version.txt | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/aws_quickstart/CHANGELOG.md b/aws_quickstart/CHANGELOG.md index ab5ae8c..00dcd42 100644 --- a/aws_quickstart/CHANGELOG.md +++ b/aws_quickstart/CHANGELOG.md @@ -1,3 +1,11 @@ +# 4.5.1 (February 11, 2026) + +- Scope IAM policy names to role name to prevent `EntityAlreadyExists` errors on stack re-creation + +# 4.5.0 (February 11, 2026) + +- Add `main_extended_workflow.yaml` CloudFormation template for CSM onboarding with workflow status reporting + # 4.4.0 (February 9, 2026) - Remove all enumerated IAM permissions from the datadog_integration_role stack diff --git a/aws_quickstart/version.txt b/aws_quickstart/version.txt index c2d2cb0..af6f175 100644 --- a/aws_quickstart/version.txt +++ b/aws_quickstart/version.txt @@ -1 +1 @@ -v4.5.0 +v4.5.1 From dc265ab11f407164de1d6c7c439344222ced875f Mon Sep 17 00:00:00 2001 From: Raymond Eah Date: Thu, 12 Feb 2026 13:59:49 -0500 Subject: [PATCH 3/3] adjust language in the changelog --- aws_quickstart/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_quickstart/CHANGELOG.md b/aws_quickstart/CHANGELOG.md index 00dcd42..77cc9f7 100644 --- a/aws_quickstart/CHANGELOG.md +++ b/aws_quickstart/CHANGELOG.md @@ -4,7 +4,7 @@ # 4.5.0 (February 11, 2026) -- Add `main_extended_workflow.yaml` CloudFormation template for CSM onboarding with workflow status reporting +- Add `main_extended_workflow.yaml` CloudFormation template for full AWS Integration onboarding with workflow status reporting including Cloud Security product configuration. # 4.4.0 (February 9, 2026)