├── .gitignore ├── LICENSE ├── README.md ├── client.py ├── requirements.txt ├── tag_config.yaml └── tagger.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Bahr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Tagger for AWS 2 | 3 | [Read the companion article](https://bahr.dev/2020/01/03/efficient-resource-tagging/) 4 | 5 | ## :exclamation: Supported Services 6 | 7 | * `lambda` 8 | * `cloudwatchlogs` 9 | * `cloudfront` 10 | 11 | Let us know which services you are interested in! 12 | 13 | ## :factory: Install dependencies 14 | 15 | `pip install -r requirements.txt` 16 | 17 | ## :triangular_ruler: Config structure 18 | 19 | The yaml config specifies which tags and which functions should get which tag values. 20 | 21 | ### Structure 22 | ```yaml 23 | target_tag: 24 | tag_value: 25 | - arn_part 26 | ``` 27 | 28 | The `target_tag` specifies the key of the tag. The `tag_value` specifies the value of the tag. This tag is applied to every resource of the given service whose ARN contains the `arn_part`. 29 | 30 | ### Example 31 | ```yaml 32 | department: 33 | research: 34 | - aws-scheduler 35 | - stock-watch 36 | - bottleneck-testing 37 | - testing-mail 38 | business: 39 | - market-watch 40 | - contracts-appraisal 41 | project: 42 | scheduler: 43 | - aws-scheduler 44 | - bottleneck-testing 45 | stock-watch: 46 | - stock-watch 47 | mail: 48 | - testing-mail 49 | market-watch: 50 | - market-watch 51 | contracts-appraisal: 52 | - contracts-appraisal 53 | ``` 54 | 55 | ## :rocket: Run 56 | 57 | * Show the help: `python tagger.py --help` 58 | * Show existing tags: `python tagger.py lambda TAG_1,TAG_2,TAG_N` 59 | * Use the region `eu-central-1` instead of the default `us-east-1`: `python tagger.py lambda TAG --region eu-central-1` 60 | * Do a dry run for writing new tags: `python tagger.py lambda TAG --write --dry-run` 61 | * Use a different yaml file than `tag_config.yarml`: `python tagger.py lambda TAG --write --file my_config.yaml` 62 | * Overwrite existing tags: `python tagger.py lambda TAG --write --overwrite` 63 | 64 | ## :wrench: Contributions 65 | 66 | Yes please! Open a ticket or send a pull request. 67 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | CLOUDFRONT = 'cloudfront' 4 | CLOUDWATCHLOGS = 'cloudwatchlogs' 5 | LAMBDA = 'lambda' 6 | 7 | 8 | class Client: 9 | 10 | def __init__(self, service, region): 11 | self.service = service 12 | self.region = region 13 | 14 | if self.service == LAMBDA: 15 | self.client = boto3.client('lambda', self.region) 16 | elif self.service == CLOUDWATCHLOGS: 17 | self.client = boto3.client('logs', self.region) 18 | elif self.service == CLOUDFRONT: 19 | self.client = boto3.client('cloudfront', self.region) 20 | else: 21 | raise Exception(f'Service {self.service} is not yet supported.') 22 | 23 | def get_resources(self): 24 | resources = [] 25 | if self.service == LAMBDA: 26 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Paginator.ListFunctions 27 | for page in self.client.get_paginator('list_functions').paginate(): 28 | resources.extend(page.get('Functions')) 29 | resources.sort(key=lambda x: x['FunctionArn']) 30 | for r in resources: 31 | r['tagger_id'] = r['FunctionArn'] 32 | elif self.service == CLOUDWATCHLOGS: 33 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/logs.html#CloudWatchLogs.Paginator.DescribeLogGroups 34 | for page in self.client.get_paginator('describe_log_groups').paginate(): 35 | resources.extend(page.get('logGroups')) 36 | resources.sort(key=lambda x: x['logGroupName']) 37 | for r in resources: 38 | r['tagger_id'] = r['logGroupName'] 39 | elif self.service == CLOUDFRONT: 40 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudfront.html#CloudFront.Client.list_distributions 41 | for page in self.client.get_paginator('list_distributions').paginate(): 42 | resources.extend(page.get('DistributionList', {}).get('Items', [])) 43 | resources.sort(key=lambda x: x['ARN']) 44 | for r in resources: 45 | r['tagger_id'] = r['ARN'] 46 | return resources 47 | 48 | def get_tags(self, tagger_id): 49 | if self.service == LAMBDA: 50 | return self.client.list_tags(Resource=tagger_id).get('Tags', []) 51 | elif self.service == CLOUDWATCHLOGS: 52 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/logs.html#CloudWatchLogs.Client.list_tags_log_group 53 | return self.client.list_tags_log_group(logGroupName=tagger_id).get('tags', []) 54 | elif self.service == CLOUDFRONT: 55 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudfront.html#CloudFront.Client.list_tags_for_resource 56 | result = {} 57 | for item in self.client.list_tags_for_resource(Resource=tagger_id).get('Tags', {}).get('Items', []): 58 | result[item['Key']] = item['Value'] 59 | return result 60 | 61 | def write_tags(self, tagger_id, new_tags): 62 | if self.service == LAMBDA: 63 | self.client.tag_resource(Resource=tagger_id, Tags=new_tags) 64 | elif self.service == CLOUDWATCHLOGS: 65 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/logs.html#CloudWatchLogs.Client.tag_log_group 66 | self.client.tag_log_group(logGroupName=tagger_id, tags=new_tags) 67 | elif self.service == CLOUDFRONT: 68 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudfront.html#CloudFront.Client.tag_resource 69 | items = [] 70 | for key, value in new_tags.items(): 71 | items.append({ 72 | 'Key': key, 73 | 'Value': value 74 | }) 75 | self.client.tag_resource(Resource=tagger_id, Tags={'Items': items}) 76 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | argparse 3 | pyyaml -------------------------------------------------------------------------------- /tag_config.yaml: -------------------------------------------------------------------------------- 1 | department: 2 | research: 3 | - aws-scheduler 4 | - vocabulary 5 | - stock-watch 6 | - bottleneck-testing 7 | - testing-mail 8 | eve: 9 | - market-watch 10 | - eve-military 11 | - contracts-appraisal 12 | - eve-market-capturer 13 | - camper-api 14 | project: -------------------------------------------------------------------------------- /tagger.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import yaml 3 | 4 | from client import Client 5 | 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument("service", help="Specify which AWS service to use. Currently supported: lambda, cloudwatchlogs, cloudfront") 8 | parser.add_argument("tags", help="Specify which comma separated tags to scan for.") 9 | parser.add_argument("-r", "--region", help="Specify AWS region. Default is us-east-1.", default="us-east-1") 10 | parser.add_argument("-w", "--write", help="Write tags to the service. Default is false.", action="store_true") 11 | parser.add_argument("-f", "--file", help="file which holds the mappings to write", default="tag_config.yaml") 12 | parser.add_argument("-d", "--dry-run", help="simulate a dry run", action="store_true") 13 | parser.add_argument("-o", "--overwrite", help="write the tag even if it is already set", action="store_true") 14 | args = parser.parse_args() 15 | 16 | target_tags = args.tags.split(",") 17 | target_tags.sort() 18 | 19 | client = Client(args.service, args.region) 20 | print(f"Loading resources for service {args.service} and region '{args.region}'") 21 | resources = client.get_resources() 22 | print(f"Loaded {len(resources)} resources.") 23 | 24 | if args.write: 25 | with open(args.file) as file: 26 | tags_config = yaml.load(file) 27 | for target_tag in target_tags: 28 | if target_tag not in tags_config: 29 | print(f"'{target_tag}' is missing from the yaml file. Please add it to the file or omit the tag. Aborting.") 30 | exit() 31 | 32 | untagged = [] 33 | for resource in resources: 34 | print('-----') 35 | print(f"Loading tags for resource {resource['tagger_id']}") 36 | resource_tags = client.get_tags(resource['tagger_id']) 37 | print(f"Found {len(resource_tags)} tags: {resource_tags}") 38 | 39 | for target_tag in target_tags: 40 | print(f"Processing tag '{target_tag}'.") 41 | if target_tag not in resource_tags or args.overwrite: 42 | if not args.overwrite: 43 | print(f"Resource {resource['tagger_id']} is missing tag '{target_tag}'.") 44 | 45 | if args.write: 46 | new_tags = {} 47 | if target_tag in tags_config and tags_config[target_tag] is not None: 48 | for tag_value, arn_parts in tags_config[target_tag].items(): 49 | if arn_parts is None: 50 | print(f"The tag '{target_tag}'s value {tag_value} has no arn_parts in the yaml file.") 51 | continue 52 | arn_parts.sort() 53 | for arn_part in arn_parts: 54 | if arn_part in resource['tagger_id']: 55 | new_tags[target_tag] = tag_value 56 | else: 57 | print(f"Tag '{target_tag}' has no values in the yaml file.") 58 | 59 | if len(new_tags) > 0: 60 | print(f"Adding tags to resource {resource['tagger_id']}: {new_tags}") 61 | if not args.dry_run: 62 | client.write_tags(resource['tagger_id'], new_tags) 63 | else: 64 | print(f"No new tags for resource {resource['tagger_id']}.") 65 | else: 66 | print("Write is disabled. Tags are not updated. Use --write to activate it. Use --write AND --dry-run for a dry run.") 67 | untagged.append(resource['tagger_id']) 68 | else: 69 | print(f"Function already has the tag '{target_tag}'.") 70 | 71 | print('--- DONE ---') 72 | if len(untagged) > 0: 73 | print(f"{len(untagged)} resources remain untagged: {untagged}") --------------------------------------------------------------------------------