diff --git a/local-app/python-tools/aws-service-linked-roles/README.md b/local-app/python-tools/aws-service-linked-roles/README.md index e69e33e4..2c244b49 100644 --- a/local-app/python-tools/aws-service-linked-roles/README.md +++ b/local-app/python-tools/aws-service-linked-roles/README.md @@ -76,7 +76,15 @@ resource "aws_iam_service_linked_role" "roles" { } ``` -## Changelog +## CHANGELOG + +### **[1.0.6]**: +* **Updated Ignore List**: Added `trustedadvisor` and `support` to the default organization-managed exclusion list. +* **Variable Persistence**: The script now includes services in the `service_linked_roles` list even if they have the `boc:created_by: terraform` tag. This ensures that a `terraform apply` doesn't attempt to delete existing managed roles just because they weren't part of the current "import" batch. +* **Logic Refinement**: + * **Ignore List**: If a service is in this list, it is excluded from **both** the `import.tf` and the `.tfvars`. + * **Terraform Tag**: If a service has the tag, it is excluded from the `import.tf` (to avoid state conflicts) but **included** in the `.tfvars` (to maintain the resource in the state). + ### [1.0.5] - 2026-04-14 * **Added**: Built-in ignore list for organization-managed services (e.g., GuardDuty, Security Hub, SSO). @@ -105,4 +113,3 @@ resource "aws_iam_service_linked_role" "roles" { ### [1.0.0] - 2026-04-14 - **Initial Release**: Basic SLR scraping and file generation. - diff --git a/local-app/python-tools/aws-service-linked-roles/aws-slr-generate.py b/local-app/python-tools/aws-service-linked-roles/aws-slr-generate.py index f51d59b3..34f74ed5 100755 --- a/local-app/python-tools/aws-service-linked-roles/aws-slr-generate.py +++ b/local-app/python-tools/aws-service-linked-roles/aws-slr-generate.py @@ -4,14 +4,14 @@ import csv from botocore.exceptions import ClientError, ProfileNotFound -VERSION = "1.0.5" +VERSION = "1.0.6" # Default list of organization-managed services to ignore DEFAULT_IGNORE_SERVICES = [ "access-analyzer", "guardduty", "inspector2", "acm", "cloudtrail", "compute-optimizer", "member.org.stacksets.cloudformation", "config-multiaccountsetup", "fms", "ipam", "organizations", - "securityhub", "sso" + "securityhub", "sso", "trustedadvisor", "support" ] def get_session_info(session): @@ -51,10 +51,13 @@ def generate_slr_terraform(): account_id, partition = get_session_info(session) ignored_services = load_ignored_services(args.organization_services) - service_names_to_import = [] + # This list will contain ALL services that should be in the TF state, + # including those already managed by Terraform. + all_managed_service_names = [] + import_content = [f"# Generated by aws-slr-generate v{VERSION}\n"] slr_found_count = 0 - imported_count = 0 + imports_staged_count = 0 paginator = iam.get_paginator('list_roles') for page in paginator.paginate(PathPrefix='/aws-service-role/'): @@ -63,7 +66,7 @@ def generate_slr_terraform(): role_name = role['RoleName'] role_arn = role['Arn'] - # 1. Check Trust Policy for Service Principal + # Extract Service Principal trust_policy = role.get('AssumeRolePolicyDocument', {}) service_principals = [] for statement in trust_policy.get('Statement', []): @@ -77,36 +80,37 @@ def generate_slr_terraform(): if not service_principals: continue - # Using the first service principal as the primary key primary_service = service_principals[0] - # 2. Check for Tags (boc:created_by == terraform) + # Check Tags try: tags_response = iam.list_role_tags(RoleName=role_name) role_tags = {tag['Key']: tag['Value'] for tag in tags_response.get('Tags', [])} except ClientError: role_tags = {} - # 3. Filtering Logic is_ignored = any(ignored in primary_service for ignored in ignored_services) is_managed_by_tf = role_tags.get('boc:created_by') == 'terraform' if is_ignored: import_content.append(f"# Skipping {primary_service}: Organization-managed service\n") - elif is_managed_by_tf: - import_content.append(f"# Skipping {primary_service}: Role '{role_name}' already managed by terraform (tag detected)\n") else: - import_content.append(f'import {{\n') - import_content.append(f' to = aws_iam_service_linked_role.roles["{primary_service}"]\n') - import_content.append(f' id = "{role_arn}"\n') - import_content.append(f'}}\n\n') - service_names_to_import.append(primary_service) - imported_count += 1 + # If not in the ignore list, it BELONGS in the tfvars list + all_managed_service_names.append(primary_service) + + if is_managed_by_tf: + import_content.append(f"# Skipping {primary_service}: Role '{role_name}' already managed by terraform (tag detected)\n") + else: + import_content.append(f'import {{\n') + import_content.append(f' to = aws_iam_service_linked_role.roles["{primary_service}"]\n') + import_content.append(f' id = "{role_arn}"\n') + import_content.append(f'}}\n\n') + imports_staged_count += 1 - # Prepare Vars Content - service_names_to_import = sorted(list(set(service_names_to_import))) + # Prepare Vars Content (All non-ignored roles) + all_managed_service_names = sorted(list(set(all_managed_service_names))) vars_content = [f"# Generated by aws-slr-generate v{VERSION}\n", "service_linked_roles = [\n"] - for service in service_names_to_import: + for service in all_managed_service_names: vars_content.append(f' "{service}",\n') vars_content.append("]\n") @@ -127,8 +131,8 @@ def generate_slr_terraform(): print(f"AWS Partition: {partition}") print(f"AWS Account: {account_id}") print(f"Total SLRs Found: {slr_found_count}") - print(f"Imports Created: {imported_count}") - print(f"Ignored/Skipped: {slr_found_count - imported_count}") + print(f"Imports Staged: {imports_staged_count}") + print(f"Total in Vars: {len(all_managed_service_names)}") print("-" * 45) except (ProfileNotFound, ClientError) as e: