├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.py ├── cdk.json ├── cloudformation ├── README-zh_cn.md ├── README.md └── resource-tagging-automation.yaml ├── deploy.sh ├── docs └── architecture.png ├── lambda └── lambda-handler.py ├── package-lock.json ├── package.json ├── requirements.txt └── resourcetagging ├── __init__.py └── tagging_stack.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .venv 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | .cdk_output 8 | .cfn_outputs 9 | resourcetagging/__pycache__ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Guide to Resource Tagging Automation 2 | This is a Lambda function that can auto tagging newly created AWS resources. It is triggered by EventBridge events from CloudTrail logs. Currently it supports tagging EC2, S3, DynamoDB, RDS, Lambda, EFS, EBS, ELB, OpenSearch, SNS, SQS, ElastiCache and KMS. 3 | 4 | The solution will Tag those supported resources with pre-defined tags and also (optionally) the identity used to deploy them for easier ownership and cost allocation. 5 | 6 | Notice that the solution can only tag newly created resources, if you want to tag resources already created, please go to AWS Tag Editor Console, https://console.aws.amazon.com/resource-groups/tag-editor/find-resources. 7 | 8 | If you want to deploy by CloudFormation, please refer https://github.com/aws-samples/resource-tagging-automation/tree/main/cloudformation. 9 | 10 | 11 | ### Project Architecture 12 | ![ProjectArchitecture](docs/architecture.png) 13 | 14 | 15 | ### Prerequisite 16 | 1. A Linux machine to deploy CDK codes, with AWS IAM user's AK/SK configured. 17 | 18 | 2. Python3.8 and NodeJS are installed on the Linux machine. 19 | 20 | 21 | ### To deploy 22 | 1. Ensure CDK is installed 23 | ``` 24 | $ npm install -g aws-cdk 25 | ``` 26 | 27 | 2. Create a Python virtual environment 28 | ``` 29 | $ python3 -m venv .venv 30 | ``` 31 | 32 | 3. Activate virtual environment 33 | 34 | _On MacOS or Linux_ 35 | ``` 36 | $ source .venv/bin/activate 37 | ``` 38 | 39 | _On Windows_ 40 | ``` 41 | % .venv\Scripts\activate.bat 42 | ``` 43 | 44 | 4. Install the required dependencies. 45 | 46 | ``` 47 | $ pip install -r requirements.txt 48 | ``` 49 | 50 | 5. Bootstrapping cdk environment. 51 | 52 | ``` 53 | $ cdk bootstrap 54 | ``` 55 | 56 | 6. Synthesize (`cdk synth`) or deploy (`cdk deploy`) the example, use --parameters to pass additional parameters, use --require-approval to run without being prompted for approval, Please replace your own tags to the tags parameter. 57 | 58 | ``` 59 | $ cdk deploy --require-approval never --parameters tags='{"TagName1": "TagValue1","TagName2": "TagValue2"}' 60 | ``` 61 | 62 | (Optional) You can also choose to disable the Identity Recording feature by definening the `identityRecording` parameter to false. This feature is enabled by default. 63 | 64 | ``` 65 | $ cdk deploy --require-approval never --parameters tags='{"TagName1": "TagValue1","TagName2": "TagValue2"}' --parameters identityRecording='false' 66 | ``` 67 | 68 | ### To check 69 | 1. Configure AWS CLI, please refer https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config 70 | 71 | 2. Ensure the Lambda function is successfully created. 72 | 73 | ``` 74 | $ aws lambda list-functions | grep FunctionName | grep resource-tagging-automation-function 75 | ``` 76 | 77 | 3. Lambda function will be invoked when EventBridge rule triggered, such as EC2 Runinstances event, and the resource will be tagged by the Lambda function. 78 | 79 | 80 | ### To clean up afterwards: 81 | Notice this operation will destroy the Lambda function and all other resources created by CDK. 82 | 83 | ``` 84 | $ cdk destroy 85 | ``` 86 | 87 | ### Support more resources: 88 | 1. Go to AWS Console and navigate to Amazon EventBridge --> Rules, find and select rule 'resource-tagging-automation-awstaggingautomationruleXXXXXX', click 'Edit' button. 89 | 2. Go to step 2 and find 'Event pattern' chapter, add new resource in eventSource, eventName and source. 90 | 3. Keep clicking 'Next' button until click 'Update rule' in step 5. 91 | 4. Go to AWS Console and navigate to Lambda --> Functions, find out function 'resource-tagging-automation-function'. 92 | 5. Edit the function code, add a method to get newly created resources' ARN list from EventBridge rule event. 93 | 6. The method example is as below. 94 | ``` 95 | def aws_elasticloadbalancing(event): 96 | arnList = [] 97 | if event['detail']['eventName'] == 'CreateLoadBalancer': 98 | print("tagging for new LoadBalancer...") 99 | lbs = event['detail']['responseElements'] 100 | for lb in lbs['loadBalancers']: 101 | arnList.append(lb['loadBalancerArn']) 102 | return arnList 103 | ``` 104 | 105 | ### Troubleshooting 106 | If your account permissions aren't set up correctly, or if you stop a deployment, you may have to manually clean up deployed resources. Navigate to the CloudFormation console, select the stack that needs to be cleaned up, and click the Delete button to destroy the stack manually and start over. 107 | 108 | 109 | ### Security 110 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 111 | 112 | 113 | ### License 114 | This library is licensed under the MIT-0 License. See the LICENSE file. 115 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from aws_cdk import App 4 | 5 | from resourcetagging.tagging_stack import ResourceTaggingStack 6 | 7 | app = App() 8 | ResourceTaggingStack(app, "resource-tagging-automation") 9 | 10 | app.synth() 11 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py" 3 | } 4 | -------------------------------------------------------------------------------- /cloudformation/README-zh_cn.md: -------------------------------------------------------------------------------- 1 | ### 准备: 2 | 1. 下载CloudFormation模版 resource-tagging-automation.yaml 3 | 2. AWS 账号 4 | 5 | ### 架构图: 6 | ![ProjectArchitecture](../docs/architecture.png) 7 | 8 | ### 部署: 9 | 1. 打开AWS Console, 导航到CloudFormation, 10 | 2. 点击Create Stack, 11 | 3. 点击“Upload a template file”,选择下载到本地的CloudFormation 模版, 12 | 4. 在“Stack name”输入名称, 13 | 5. 参数部分设置, 14 | A. AutomationTags : 以json的形式输入自动为每个资源打的Tag, 如: {"tag1": "test1","tag2": "test2"} 15 | B. EventBridgeRuleName : 输入EventBridge Rule的名称, 默认: resource-tagging-automation-rules 16 | C. IAMAutoTaggingPolicyName :输入创建的IAM customed managed policy的名称, 默认: resource-tagging-automation-policy 17 | D. IAMAutoTaggingRoleName :输入为Lambda创建的角色名称, 默认: resource-tagging-automation-role 18 | E. LambdaAutoTaggingFunctionName :输入lambda函数的名称, 默认: resource-tagging-automation-function 19 | 6. 点击Next, 勾选“I acknowledge that AWS CloudFormation might create IAM resources.”, 点击“Create stack”, 20 | 7. Stackset需要3-5分钟完成。 21 | 22 | ### 修改tag标签: 23 | 1. 到Lambda 控制界面, 选择Lambda函数, 默认: resource-tagging-automation-function, 24 | 2. 到Configuration Tab, 选择 Environment variables, 点击Edit 修改tags标签的值, 25 | 3. 修改值 (json格式), 点击Save。 那么之后资源被创建的时候会被打上新的tag。 26 | -------------------------------------------------------------------------------- /cloudformation/README.md: -------------------------------------------------------------------------------- 1 | ### Prepare: 2 | 1. Download the CloudFormation template resource-tagging-automation.yaml 3 | 2. An AWS account 4 | 5 | ### Architecture: 6 | ![ProjectArchitecture](../docs/architecture.png) 7 | 8 | ### Deployment: 9 | 1. Open the AWS Console, navigate to CloudFormation, 10 | 2. Click Create Stack, 11 | 3. Click "Upload a template file", select the CloudFormation template downloaded to the local, 12 | 4. Enter a name in "Stack name", 13 | 5. Parameters section settings, 14 | A. AutomationTags : Enter the tags that are automatically printed for each resource in the form of json, such as: {"TagName1": "TagValue1","TagName2": "TagValue2"} 15 | B. EventBridgeRuleName : Enter the name of the EventBridge Rule, default value: resource-tagging-automation-rules 16 | C. IAMAutoTaggingPolicyName : Enter the name of the created IAM custom managed policy, default value: resource-tagging-automation-policy 17 | D. IAMAutoTaggingRoleName : Enter the role name created for Lambda, default value: resource-tagging-automation-role 18 | E. LambdaAutoTaggingFunctionName : Enter the name of the lambda function, default value: resource-tagging-automation-function 19 | 6. Click Next, check "I acknowledge that AWS CloudFormation might create IAM resources.", click "Create stack", 20 | 7. StackSet takes 3-5 minutes to finish creation. 21 | 22 | ### Modify tag: 23 | 1. Go to the Lambda control interface, select the Lambda function, default value: resource-tagging-automation-function, 24 | 2. Go to the Configuration Tab, select Environment variables, click Edit to modify the value of the tags tag, 25 | 3. Modify the value (json format) and click Save. When the resource is created later, it will be marked with a new tag. -------------------------------------------------------------------------------- /cloudformation/resource-tagging-automation.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | AWSTemplateFormatVersion: "2010-09-09" 18 | Description: AWS auto tagging solution for EC2, ELB, EFS, EBS, S3, RDS, DynamoDB, Lambda, OpenSearch, ElastiCache, Redshift, SageMaker, SNS, SQS, KMS, MQ, MSK and ECS. 19 | 20 | Parameters: 21 | AutomationTags: 22 | Type: String 23 | Default: '' 24 | Description: 'Sample: {"TagName1": "TagValue1","TagName2": "TagValue2"}' 25 | 26 | LambdaAutoTaggingFunctionName: 27 | Type: String 28 | Description: Name of the lambda-Ayto-Tagging-Function-Name 29 | Default: resource-tagging-automation-function 30 | 31 | EventBridgeRuleName: 32 | Type: String 33 | Description: Name of the EventBridge Rules 34 | Default: resource-tagging-automation-rules 35 | 36 | IAMAutoTaggingRoleName: 37 | Type: String 38 | Description: IAM role name for lambda 39 | Default: resource-tagging-automation-role 40 | 41 | IAMAutoTaggingPolicyName: 42 | Type: String 43 | Description: IAM customed managed policy 44 | Default: resource-tagging-automation-policy 45 | 46 | TrailName: 47 | Type: String 48 | Description: Name of the Cloudtrail to record events 49 | Default: resource-tagging-automation-trail 50 | 51 | Resources: 52 | # S3 Bucket for Cloudtrail 53 | TrailBucket: 54 | Type: 'AWS::S3::Bucket' 55 | DeletionPolicy: Retain 56 | Properties: 57 | BucketName: !Join [ "-", ["tagging-log", !Select [2, !Split [ "/", !Ref 'AWS::StackId']]]] 58 | 59 | # S3 Bucket policy 60 | TrailBucketPolicy: 61 | DependsOn: TrailBucket 62 | Type: AWS::S3::BucketPolicy 63 | Properties: 64 | Bucket: !Ref TrailBucket 65 | PolicyDocument: 66 | Version: '2012-10-17' 67 | Statement: 68 | - Sid: AWSCloudTrailAclCheck 69 | Effect: Allow 70 | Principal: 71 | Service: 'cloudtrail.amazonaws.com' 72 | Action: 's3:GetBucketAcl' 73 | Resource: !Sub 'arn:aws:s3:::${TrailBucket}' 74 | - Sid: AWSCloudTrailWrite 75 | Effect: Allow 76 | Principal: 77 | Service: 'cloudtrail.amazonaws.com' 78 | Action: 's3:PutObject' 79 | Resource: !Sub 'arn:aws:s3:::${TrailBucket}/AWSLogs/${AWS::AccountId}/*' 80 | Condition: 81 | StringEquals: 82 | 's3:x-amz-acl': 'bucket-owner-full-control' 83 | 84 | # Cloudtrail resource 85 | Trail: 86 | DependsOn: TrailBucketPolicy 87 | Type: AWS::CloudTrail::Trail 88 | Properties: 89 | S3BucketName: !Ref TrailBucket 90 | IsLogging: true 91 | TrailName: !Ref TrailName 92 | EnableLogFileValidation: false 93 | IncludeGlobalServiceEvents: true 94 | IsMultiRegionTrail: true 95 | 96 | # Lambda function resource 97 | LambdaExecutionRole: 98 | Type: AWS::IAM::Role 99 | Properties: 100 | AssumeRolePolicyDocument: 101 | Version: '2012-10-17' 102 | Statement: 103 | - Effect: Allow 104 | Principal: 105 | Service: 106 | - lambda.amazonaws.com 107 | Action: 108 | - sts:AssumeRole 109 | Policies: 110 | - PolicyName: !Ref IAMAutoTaggingPolicyName 111 | PolicyDocument: 112 | Version: '2012-10-17' 113 | Statement: 114 | - Effect: Allow 115 | Action: 116 | # Rules for DynamoDB 117 | - 'dynamodb:TagResource' 118 | - 'dynamodb:DescribeTable' 119 | 120 | # Rules for Lambdas 121 | - 'lambda:TagResource' 122 | - 'lambda:ListTags' 123 | 124 | # Rules for S3 125 | - 's3:GetBucketTagging' 126 | - 's3:PutBucketTagging' 127 | 128 | # Rules for EC2 129 | - 'ec2:CreateTags' 130 | - 'ec2:DescribeNatGateways' 131 | - 'ec2:DescribeInternetGateways' 132 | - 'ec2:DescribeVolumes' 133 | 134 | # Rules for RDS 135 | - 'rds:AddTagsToResource' 136 | - 'rds:DescribeDBInstances' 137 | 138 | # Rules for SNS 139 | - 'sns:TagResource' 140 | 141 | # Rules for SQS 142 | - 'sqs:ListQueueTags' 143 | - 'sqs:TagQueue' 144 | 145 | # Rules for OpenSearch 146 | - 'es:AddTags' 147 | 148 | # Rules for KMS 149 | - 'kms:ListResourceTags' 150 | - 'kms:TagResource' 151 | 152 | # Rules for EFS 153 | - 'elasticfilesystem:TagResource' 154 | - 'elasticfilesystem:CreateTags' 155 | - 'elasticfilesystem:DescribeTags' 156 | 157 | # Rules for ELB 158 | - 'elasticloadbalancing:AddTags' 159 | 160 | # Rules for CloudWatch 161 | - 'logs:CreateLogGroup' 162 | - 'logs:CreateLogStream' 163 | - 'logs:PutLogEvents' 164 | 165 | # Rules for Redshift 166 | - 'redshift:CreateTags' 167 | - 'redshift-serverless:TagResource' 168 | 169 | # Rules for Sagemaker 170 | - 'sagemaker:AddTags' 171 | 172 | # Rules for ECS 173 | - 'ecs:TagResource' 174 | 175 | # Rules for MSK 176 | - 'kafka:TagResource' 177 | 178 | # Rules for CloudWatch logs and alarms 179 | - 'logs:TagLogGroup' 180 | - 'cloudwatch:TagResource' 181 | 182 | # Rules for Amazon MQ 183 | - 'mq:CreateTags' 184 | 185 | # Rules for Resource Group Tag Editor 186 | - 'tag:getResources' 187 | - 'tag:getTagKeys' 188 | - 'tag:getTagValues' 189 | - 'tag:TagResources' 190 | - 'tag:UntagResources' 191 | - 'cloudformation:DescribeStacks' 192 | - 'cloudformation:ListStackResources' 193 | - 'resource-groups:*' 194 | Resource: '*' 195 | 196 | LambdaAutoTagging: 197 | Type: AWS::Lambda::Function 198 | Properties: 199 | Role: !GetAtt LambdaExecutionRole.Arn 200 | Code: 201 | ZipFile: | 202 | import boto3 203 | import os 204 | import json 205 | 206 | def aws_ec2(event): 207 | arnList = [] 208 | _account = event['account'] 209 | _region = event['region'] 210 | ec2ArnTemplate = 'arn:aws:ec2:@region@:@account@:instance/@instanceId@' 211 | volumeArnTemplate = 'arn:aws:ec2:@region@:@account@:volume/@volumeId@' 212 | ec2_resource = boto3.resource('ec2') 213 | if event['detail']['eventName'] == 'RunInstances': 214 | print("tagging for new EC2...") 215 | for item in event['detail']['responseElements']['instancesSet']['items']: 216 | _instanceId = item['instanceId'] 217 | arnList.append(ec2ArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@instanceId@', _instanceId)) 218 | 219 | _instance = ec2_resource.Instance(_instanceId) 220 | for volume in _instance.volumes.all(): 221 | arnList.append(volumeArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@volumeId@', volume.id)) 222 | 223 | elif event['detail']['eventName'] == 'CreateVolume': 224 | print("tagging for new EBS...") 225 | _volumeId = event['detail']['responseElements']['volumeId'] 226 | arnList.append(volumeArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@volumeId@', _volumeId)) 227 | 228 | elif event['detail']['eventName'] == 'CreateInternetGateway': 229 | print("tagging for new IGW...") 230 | 231 | elif event['detail']['eventName'] == 'CreateNatGateway': 232 | print("tagging for new Nat Gateway...") 233 | 234 | elif event['detail']['eventName'] == 'AllocateAddress': 235 | print("tagging for new EIP...") 236 | arnList.append(event['detail']['responseElements']['allocationId']) 237 | 238 | elif event['detail']['eventName'] == 'CreateVpcEndpoint': 239 | print("tagging for new VPC Endpoint...") 240 | 241 | elif event['detail']['eventName'] == 'CreateTransitGateway': 242 | print("tagging for new Transit Gateway...") 243 | 244 | return arnList 245 | 246 | def aws_elasticloadbalancing(event): 247 | arnList = [] 248 | if event['detail']['eventName'] == 'CreateLoadBalancer': 249 | print("tagging for new LoadBalancer...") 250 | lbs = event['detail']['responseElements'] 251 | for lb in lbs['loadBalancers']: 252 | arnList.append(lb['loadBalancerArn']) 253 | return arnList 254 | 255 | def aws_rds(event): 256 | arnList = [] 257 | if event['detail']['eventName'] == 'CreateDBInstance': 258 | print("tagging for new RDS...") 259 | #db_instance_id = event['detail']['requestParameters']['dBInstanceIdentifier'] 260 | #waiter = boto3.client('rds').get_waiter('db_instance_available') 261 | #waiter.wait( 262 | # DBInstanceIdentifier = db_instance_id 263 | #) 264 | arnList.append(event['detail']['responseElements']['dBInstanceArn']) 265 | return arnList 266 | 267 | def aws_s3(event): 268 | arnList = [] 269 | if event['detail']['eventName'] == 'CreateBucket': 270 | print("tagging for new S3...") 271 | _bkcuetName = event['detail']['requestParameters']['bucketName'] 272 | arnList.append('arn:aws:s3:::' + _bkcuetName) 273 | return arnList 274 | 275 | def aws_lambda(event): 276 | arnList = [] 277 | _exist1 = event['detail']['responseElements'] 278 | _exist2 = event['detail']['eventName'] == 'CreateFunction20150331' 279 | if _exist1!= None and _exist2: 280 | function_name = event['detail']['responseElements']['functionName'] 281 | print('Functin name is :', function_name) 282 | arnList.append(event['detail']['responseElements']['functionArn']) 283 | return arnList 284 | 285 | def aws_dynamodb(event): 286 | arnList = [] 287 | if event['detail']['eventName'] == 'CreateTable': 288 | table_name = event['detail']['responseElements']['tableDescription']['tableName'] 289 | waiter = boto3.client('dynamodb').get_waiter('table_exists') 290 | waiter.wait( 291 | TableName=table_name, 292 | WaiterConfig={ 293 | 'Delay': 123, 294 | 'MaxAttempts': 123 295 | } 296 | ) 297 | arnList.append(event['detail']['responseElements']['tableDescription']['tableArn']) 298 | return arnList 299 | 300 | def aws_kms(event): 301 | arnList = [] 302 | if event['detail']['eventName'] == 'CreateKey': 303 | arnList.append(event['detail']['responseElements']['keyMetadata']['arn']) 304 | return arnList 305 | 306 | def aws_sns(event): 307 | arnList = [] 308 | _account = event['account'] 309 | _region = event['region'] 310 | snsArnTemplate = 'arn:aws:sns:@region@:@account@:@topicName@' 311 | if event['detail']['eventName'] == 'CreateTopic': 312 | print("tagging for new SNS...") 313 | _topicName = event['detail']['requestParameters']['name'] 314 | arnList.append(snsArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@topicName@', _topicName)) 315 | return arnList 316 | 317 | def aws_sqs(event): 318 | arnList = [] 319 | _account = event['account'] 320 | _region = event['region'] 321 | sqsArnTemplate = 'arn:aws:sqs:@region@:@account@:@queueName@' 322 | if event['detail']['eventName'] == 'CreateQueue': 323 | print("tagging for new SQS...") 324 | _queueName = event['detail']['requestParameters']['queueName'] 325 | arnList.append(sqsArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@queueName@', _queueName)) 326 | return arnList 327 | 328 | def aws_elasticfilesystem(event): 329 | arnList = [] 330 | _account = event['account'] 331 | _region = event['region'] 332 | efsArnTemplate = 'arn:aws:elasticfilesystem:@region@:@account@:file-system/@fileSystemId@' 333 | if event['detail']['eventName'] == 'CreateMountTarget': 334 | print("tagging for new efs...") 335 | _efsId = event['detail']['responseElements']['fileSystemId'] 336 | arnList.append(efsArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@fileSystemId@', _efsId)) 337 | return arnList 338 | 339 | def aws_es(event): 340 | arnList = [] 341 | if event['detail']['eventName'] == 'CreateDomain': 342 | print("tagging for new open search...") 343 | arnList.append(event['detail']['responseElements']['domainStatus']['aRN']) 344 | return arnList 345 | 346 | def aws_kafka(event): 347 | arnList = [] 348 | if event['detail']['eventName'] == 'CreateClusterV2': 349 | print("tagging for new msk...") 350 | arnList.append(event['detail']['responseElements']['clusterArn']) 351 | return arnList 352 | 353 | def aws_ecs(event): 354 | arnList = [] 355 | if event['detail']['eventName'] == 'CreateCluster': 356 | print("tagging for new ecs...") 357 | arnList.append(event['detail']['responseElements']['cluster']['clusterArn']) 358 | return arnList 359 | 360 | def aws_logs(event): 361 | arnList = [] 362 | _account = event['account'] 363 | _region = event['region'] 364 | logsArnTemplate = 'arn:aws:logs:@region@:@account@:log-group:@logName@' 365 | if event['detail']['eventName'] == 'CreateLogGroup': 366 | print("tagging for new cloudwatch logs...") 367 | _logName = event['detail']['requestParameters']['logGroupName'] 368 | arnList.append(logsArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@logName@', _logName)) 369 | return arnList 370 | 371 | def aws_monitoring(event): 372 | arnList = [] 373 | _account = event['account'] 374 | _region = event['region'] 375 | alarmArnTemplate = 'arn:aws:cloudwatch:@region@:@account@:alarm:@alarmName@' 376 | if event['detail']['eventName'] == 'PutMetricAlarm': 377 | print("tagging for new Cloudwatch alarm...") 378 | _alarmName = event['detail']['requestParameters']['alarmName'] 379 | arnList.append(alarmArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@alarmName@', _alarmName)) 380 | return arnList 381 | 382 | def aws_amazonmq(event): 383 | arnList = [] 384 | if event['detail']['eventName'] == 'CreateBroker': 385 | print("tagging for new Amazon MQ...") 386 | arnList.append(event['detail']['responseElements']['brokerArn']) 387 | return arnList 388 | 389 | def aws_redshift(event): 390 | arnList = [] 391 | _account = event['account'] 392 | _region = event['region'] 393 | redshiftTemplate = 'arn:aws:redshift:@region@:@account@:cluster:@clusterID@' 394 | if event['detail']['eventName'] == 'CreateCluster': 395 | print("tagging for new Redshift...") 396 | _redshiftID = event['detail']['requestParameters']['clusterIdentifier'] 397 | arnList.append(redshiftTemplate.replace('@region@', _region).replace('@account@', _account).replace('@clusterID@', _redshiftID)) 398 | return arnList 399 | 400 | def aws_redshift_serverless(event): 401 | arnList = [] 402 | #redshift_serverless need native tag method 403 | client = boto3.client('redshift-serverless') 404 | tag = os.environ['tags'].replace('"', "") 405 | tag_o = tag[1:-1] 406 | tags = tag_o.split(',') 407 | tag_arr = [] 408 | for t in tags: 409 | d = {} 410 | key = t.split(':')[0] 411 | value = t.split(':')[1] 412 | d['key'] = key 413 | d['value'] = value 414 | tag_arr.append(d) 415 | print(tag_arr) 416 | 417 | if event['detail']['eventName'] == 'CreateWorkgroup': 418 | print("tagging for new redshift serverless workgroup...") 419 | _workgroupArn = event['detail']['responseElements']['workgroup']['workgroupArn'] 420 | client.tag_resource(resourceArn = _workgroupArn,tags = tag_arr) 421 | arnList.append(_workgroupArn) 422 | return arnList 423 | elif event['detail']['eventName'] == 'CreateNamespace': 424 | print("tagging for new redshift serverless namespace...") 425 | _namespaceArn = event['detail']['responseElements']['namespace']['namespaceArn'] 426 | client.tag_resource(resourceArn = _namespaceArn,tags = tag_arr) 427 | arnList.append(_namespaceArn) 428 | return arnList 429 | 430 | def aws_sagemaker(event): 431 | arnList = [] 432 | if event['detail']['eventName'] == 'CreateNotebookInstance': 433 | print("tagging for new notebook instance...") 434 | _instance = event['detail']['responseElements']['notebookInstanceArn'] 435 | arnList.append(_instance) 436 | return arnList 437 | elif event['detail']['eventName'] == 'CreateProcessingJob': 438 | print("tagging for new processing job ....") 439 | _job = event['detail']['responseElements']['processingJobArn'] 440 | arnList.append(_job) 441 | return arnList 442 | elif event['detail']['eventName'] == 'CreateEndpoint': 443 | print("tagging for new endpoint ...") 444 | _endpoint = event['detail']['responseElements']['endpointArn'] 445 | arnList.append(_endpoint) 446 | return arnList 447 | elif event['detail']['eventName'] == 'CreateDomain': 448 | print("tagging for new CreateDomain ...") 449 | _domain = event['detail']['responseElements']['domainArn'] 450 | arnList.append(_domain) 451 | return arnList 452 | elif event['detail']['eventName'] == 'CreateModel': 453 | print("tagging for new CreateModel ...") 454 | _model = event['detail']['responseElements']['modelArn'] 455 | arnList.append(_model) 456 | return arnList 457 | elif event['detail']['eventName'] == 'CreateLabelingJob': 458 | print("tagging for new CreateLabelingJob ...") 459 | _labelingJob = event['detail']['responseElements']['labelingJobArn'] 460 | arnList.append(_labelingJob) 461 | return arnList 462 | elif event['detail']['eventName'] == 'CreateTrainingJob': 463 | print("tagging for new CreateTrainingJob ...") 464 | _trainingJobArn = event['detail']['responseElements']['trainingJobArn'] 465 | arnList.append(_trainingJobArn) 466 | return arnList 467 | elif event['detail']['eventName'] == 'CreateTransformJob': 468 | print("tagging for new CreateTransformJob ...") 469 | _transformJobArn = event['detail']['responseElements']['transformJobArn'] 470 | arnList.append(_transformJobArn) 471 | return arnList 472 | elif event['detail']['eventName'] == 'CreateUserProfile': 473 | print("tagging for new CreateUserProfile ...") 474 | _userProfileArn = event['detail']['responseElements']['userProfileArn'] 475 | arnList.append(_userProfileArn) 476 | return arnList 477 | elif event['detail']['eventName'] == 'CreateWorkteam': 478 | print("tagging for new CreateWorkteam ...") 479 | _workteamArn = event['detail']['responseElements']['workteamArn'] 480 | arnList.append(_workteamArn) 481 | return arnList 482 | 483 | def transfromArn4CN(event, resARNs): 484 | _region = event['region'] 485 | if _region == 'cn-north-1' or _region == 'cn-northwest-1': 486 | _resARNs = [] 487 | # deal with china arns 488 | for _arn in resARNs: 489 | arn = _arn.replace("arn:aws:", "arn:aws-cn:", 1) 490 | _resARNs.append(arn) 491 | resARNs = _resARNs 492 | return resARNs 493 | 494 | 495 | def main(event, context): 496 | print("input event is: ", event) 497 | print("new source is " + event['source']) 498 | _method = event['source'].replace('.', "_").replace('-', "_") 499 | 500 | resARNs = globals()[_method](event) 501 | resARNs = transfromArn4CN(event, resARNs) 502 | print("resource arn is: ", resARNs) 503 | 504 | res_tags = json.loads(os.environ['tags']) 505 | boto3.client('resourcegroupstaggingapi').tag_resources( 506 | ResourceARNList=resARNs, 507 | Tags=res_tags 508 | ) 509 | 510 | return { 511 | 'statusCode': 200, 512 | 'body': json.dumps('Finished map tagging with ' + event['source']) 513 | } 514 | FunctionName: !Ref LambdaAutoTaggingFunctionName 515 | Handler: index.main 516 | Runtime: python3.10 517 | Timeout: 300 518 | Environment: 519 | Variables: 520 | tags: !Ref AutomationTags 521 | 522 | EventBridgeRule: 523 | Type: AWS::Events::Rule 524 | Properties: 525 | Description: Rules to filtering events 526 | Name: !Ref EventBridgeRuleName 527 | EventPattern: '{ 528 | "detail": { 529 | "eventSource": ["ec2.amazonaws.com", "elasticloadbalancing.amazonaws.com", "s3.amazonaws.com", "rds.amazonaws.com", "lambda.amazonaws.com", "dynamodb.amazonaws.com", "elasticfilesystem.amazonaws.com", "es.amazonaws.com", "sqs.amazonaws.com", "sns.amazonaws.com", "kms.amazonaws.com","redshift.amazonaws.com", "redshift-serverless.amazonaws.com", "sagemaker.amazonaws.com", "ecs.amazonaws.com", "monitoring.amazonaws.com", "logs.amazonaws.com", "kafka.amazonaws.com", "amazonmq.amazonaws.com"], 530 | "eventName": ["RunInstances", "CreateFunction20150331", "CreateBucket", "CreateDBInstance", "CreateTable", "CreateVolume", "CreateLoadBalancer", "CreateMountTarget", "CreateDomain", "CreateQueue", "CreateTopic", "CreateKey", "CreateCluster", "CreateNamespace", "CreateWorkgroup", "CreateNotebookInstance", "CreateProcessingJob", "CreateEndpoint", "CreateDomain", "CreateModel", "CreateLabelingJob", "CreateTrainingJob", "CreateTransformJob", "CreateUserProfile", "CreateWorkteam", "PutMetricAlarm", "CreateLogGroup", "CreateClusterV2", "CreateBroker"] 531 | }, 532 | "detail-type": ["AWS API Call via CloudTrail"], 533 | "source": ["aws.ec2", "aws.elasticloadbalancing", "aws.rds", "aws.lambda", "aws.s3", "aws.dynamodb", "aws.elasticfilesystem", "aws.es", "aws.sqs", "aws.sns", "aws.kms", "aws.ecs", "aws.redshift", "aws.redshift-serverless", "aws.sagemaker", "aws.monitoring", "aws.logs", "aws.kafka", "aws.amazonmq"] 534 | }' 535 | Targets: 536 | - Arn: !GetAtt LambdaAutoTagging.Arn 537 | Id: !Ref LambdaAutoTaggingFunctionName 538 | 539 | PermissionForEventsToInvokeLambda: 540 | Type: AWS::Lambda::Permission 541 | Properties: 542 | FunctionName: !Ref "LambdaAutoTaggingFunctionName" 543 | Action: "lambda:InvokeFunction" 544 | Principal: "events.amazonaws.com" 545 | SourceArn: !GetAtt EventBridgeRule.Arn -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | # software and associated documentation files (the "Software"), to deal in the Software 7 | # without restriction, including without limitation the rights to use, copy, modify, 8 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | npm install -g aws-cdk 19 | 20 | python3 -m venv .venv 21 | 22 | case "$OSTYPE" in 23 | darwin*) source .venv/bin/activate ;; 24 | linux*) source .venv/bin/activate ;; 25 | msys*) .venv\Scripts\activate.bat ;; 26 | cygwin*) .venv\Scripts\activate.bat ;; 27 | *) echo "unknown: $OSTYPE" ;; 28 | esac 29 | 30 | pip3 install -r requirements.txt 31 | 32 | cdk bootstrap 33 | 34 | cdk destroy --force 35 | 36 | cdk deploy --require-approval never \ 37 | --parameters tags='{"TagName1": "TagValue1","TagName2": "TagValue2"}' 38 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/resource-tagging-automation/61c832fb41418f24194921f6185aeb7fc0c916ad/docs/architecture.png -------------------------------------------------------------------------------- /lambda/lambda-handler.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import boto3 5 | import os 6 | import json 7 | 8 | def aws_ec2(event): 9 | arnList = [] 10 | _account = event['account'] 11 | _region = event['region'] 12 | ec2ArnTemplate = 'arn:aws:ec2:@region@:@account@:instance/@instanceId@' 13 | volumeArnTemplate = 'arn:aws:ec2:@region@:@account@:volume/@volumeId@' 14 | ec2_resource = boto3.resource('ec2') 15 | if event['detail']['eventName'] == 'RunInstances': 16 | print("tagging for new EC2...") 17 | for item in event['detail']['responseElements']['instancesSet']['items']: 18 | _instanceId = item['instanceId'] 19 | arnList.append(ec2ArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@instanceId@', _instanceId)) 20 | 21 | _instance = ec2_resource.Instance(_instanceId) 22 | for volume in _instance.volumes.all(): 23 | arnList.append(volumeArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@volumeId@', volume.id)) 24 | 25 | elif event['detail']['eventName'] == 'CreateVolume': 26 | print("tagging for new EBS...") 27 | _volumeId = event['detail']['responseElements']['volumeId'] 28 | arnList.append(volumeArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@volumeId@', _volumeId)) 29 | 30 | elif event['detail']['eventName'] == 'CreateInternetGateway': 31 | print("tagging for new IGW...") 32 | 33 | elif event['detail']['eventName'] == 'CreateNatGateway': 34 | print("tagging for new Nat Gateway...") 35 | 36 | elif event['detail']['eventName'] == 'AllocateAddress': 37 | print("tagging for new EIP...") 38 | arnList.append(event['detail']['responseElements']['allocationId']) 39 | 40 | elif event['detail']['eventName'] == 'CreateVpcEndpoint': 41 | print("tagging for new VPC Endpoint...") 42 | 43 | elif event['detail']['eventName'] == 'CreateTransitGateway': 44 | print("tagging for new Transit Gateway...") 45 | 46 | return arnList 47 | 48 | def aws_elasticloadbalancing(event): 49 | arnList = [] 50 | if event['detail']['eventName'] == 'CreateLoadBalancer': 51 | print("tagging for new LoadBalancer...") 52 | lbs = event['detail']['responseElements'] 53 | for lb in lbs['loadBalancers']: 54 | arnList.append(lb['loadBalancerArn']) 55 | return arnList 56 | 57 | def aws_rds(event): 58 | arnList = [] 59 | if event['detail']['eventName'] == 'CreateDBInstance': 60 | print("tagging for new RDS...") 61 | #db_instance_id = event['detail']['requestParameters']['dBInstanceIdentifier'] 62 | #waiter = boto3.client('rds').get_waiter('db_instance_available') 63 | #waiter.wait( 64 | # DBInstanceIdentifier = db_instance_id 65 | #) 66 | arnList.append(event['detail']['responseElements']['dBInstanceArn']) 67 | return arnList 68 | 69 | def aws_s3(event): 70 | arnList = [] 71 | if event['detail']['eventName'] == 'CreateBucket': 72 | print("tagging for new S3...") 73 | _bkcuetName = event['detail']['requestParameters']['bucketName'] 74 | arnList.append('arn:aws:s3:::' + _bkcuetName) 75 | return arnList 76 | 77 | def aws_lambda(event): 78 | arnList = [] 79 | _exist1 = event['detail']['responseElements'] 80 | _exist2 = event['detail']['eventName'] == 'CreateFunction20150331' 81 | if _exist1!= None and _exist2: 82 | function_name = event['detail']['responseElements']['functionName'] 83 | print('Functin name is :', function_name) 84 | arnList.append(event['detail']['responseElements']['functionArn']) 85 | return arnList 86 | 87 | def aws_dynamodb(event): 88 | arnList = [] 89 | if event['detail']['eventName'] == 'CreateTable': 90 | table_name = event['detail']['responseElements']['tableDescription']['tableName'] 91 | waiter = boto3.client('dynamodb').get_waiter('table_exists') 92 | waiter.wait( 93 | TableName=table_name, 94 | WaiterConfig={ 95 | 'Delay': 123, 96 | 'MaxAttempts': 123 97 | } 98 | ) 99 | arnList.append(event['detail']['responseElements']['tableDescription']['tableArn']) 100 | return arnList 101 | 102 | def aws_kms(event): 103 | arnList = [] 104 | if event['detail']['eventName'] == 'CreateKey': 105 | arnList.append(event['detail']['responseElements']['keyMetadata']['arn']) 106 | return arnList 107 | 108 | def aws_sns(event): 109 | arnList = [] 110 | _account = event['account'] 111 | _region = event['region'] 112 | snsArnTemplate = 'arn:aws:sns:@region@:@account@:@topicName@' 113 | if event['detail']['eventName'] == 'CreateTopic': 114 | print("tagging for new SNS...") 115 | _topicName = event['detail']['requestParameters']['name'] 116 | arnList.append(snsArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@topicName@', _topicName)) 117 | return arnList 118 | 119 | def aws_sqs(event): 120 | arnList = [] 121 | _account = event['account'] 122 | _region = event['region'] 123 | sqsArnTemplate = 'arn:aws:sqs:@region@:@account@:@queueName@' 124 | if event['detail']['eventName'] == 'CreateQueue': 125 | print("tagging for new SQS...") 126 | _queueName = event['detail']['requestParameters']['queueName'] 127 | arnList.append(sqsArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@queueName@', _queueName)) 128 | return arnList 129 | 130 | def aws_elasticfilesystem(event): 131 | arnList = [] 132 | _account = event['account'] 133 | _region = event['region'] 134 | efsArnTemplate = 'arn:aws:elasticfilesystem:@region@:@account@:file-system/@fileSystemId@' 135 | if event['detail']['eventName'] == 'CreateMountTarget': 136 | print("tagging for new efs...") 137 | _efsId = event['detail']['responseElements']['fileSystemId'] 138 | arnList.append(efsArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@fileSystemId@', _efsId)) 139 | return arnList 140 | 141 | def aws_es(event): 142 | arnList = [] 143 | if event['detail']['eventName'] == 'CreateDomain': 144 | print("tagging for new open search...") 145 | arnList.append(event['detail']['responseElements']['domainStatus']['aRN']) 146 | return arnList 147 | 148 | def aws_elasticache(event): 149 | arnList = [] 150 | _account = event['account'] 151 | _region = event['region'] 152 | ecArnTemplate = 'arn:aws:elasticache:@region@:@account@:cluster:@ecId@' 153 | 154 | if event['detail']['eventName'] == 'CreateReplicationGroup' or event['detail']['eventName'] == 'ModifyReplicationGroupShardConfiguration': 155 | print("tagging for new ElastiCache cluster...") 156 | _replicationGroupId = event['detail']['requestParameters']['replicationGroupId'] 157 | waiter = boto3.client('elasticache').get_waiter('replication_group_available') 158 | waiter.wait( 159 | ReplicationGroupId = _replicationGroupId, 160 | WaiterConfig={ 161 | 'Delay': 123, 162 | 'MaxAttempts': 123 163 | } 164 | ) 165 | _clusters = event['detail']['responseElements']['memberClusters'] 166 | for _ec in _clusters: 167 | arnList.append(ecArnTemplate.replace('@region@', _region).replace('@account@', _account).replace('@ecId@', _ec)) 168 | 169 | elif event['detail']['eventName'] == 'CreateCacheCluster': 170 | print("tagging for new ElastiCache node...") 171 | _cacheClusterId = event['detail']['responseElements']['cacheClusterId'] 172 | waiter = boto3.client('elasticache').get_waiter('cache_cluster_available') 173 | waiter.wait( 174 | CacheClusterId = _cacheClusterId, 175 | WaiterConfig={ 176 | 'Delay': 123, 177 | 'MaxAttempts': 123 178 | } 179 | ) 180 | arnList.append(event['detail']['responseElements']['aRN']) 181 | 182 | return arnList 183 | 184 | def get_identity(event): 185 | print("getting user Identity...") 186 | _userId = event['detail']['userIdentity']['arn'].split('/')[-1] 187 | 188 | if event['detail']['userIdentity']['type'] == 'AssumedRole': 189 | _roleId = event['detail']['userIdentity']['arn'].split('/')[-2] 190 | return _userId, _roleId 191 | return _userId 192 | 193 | def main(event, context): 194 | print(f"input event is: {event}") 195 | print("new source is ", event['source']) 196 | _method = event['source'].replace('.', "_") 197 | 198 | resARNs = globals()[_method](event) 199 | print("resource arn is: ", resARNs) 200 | 201 | _res_tags = json.loads(os.environ['tags']) 202 | _identity_recording = os.environ['identityRecording'] 203 | 204 | if _identity_recording == 'true': 205 | if event['detail']['userIdentity']['type'] == 'AssumedRole': 206 | _userId, _roleId = get_identity(event) 207 | _res_tags['roleId'] = _roleId 208 | else: 209 | _userId = get_identity(event) 210 | 211 | _res_tags['userId'] = _userId 212 | 213 | print(_res_tags) 214 | boto3.client('resourcegroupstaggingapi').tag_resources( 215 | ResourceARNList=resARNs, 216 | Tags=_res_tags 217 | ) 218 | 219 | return { 220 | 'statusCode': 200, 221 | 'body': json.dumps('Finished tagging with ' + event['source']) 222 | } 223 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-tagging-automation", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib>=2.72.0 2 | constructs>=10.0.0 3 | boto3>=1.20.0 -------------------------------------------------------------------------------- /resourcetagging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/resource-tagging-automation/61c832fb41418f24194921f6185aeb7fc0c916ad/resourcetagging/__init__.py -------------------------------------------------------------------------------- /resourcetagging/tagging_stack.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from aws_cdk import ( 5 | CfnParameter, 6 | Duration, 7 | Stack, 8 | aws_iam as _iam, 9 | aws_events as _events, 10 | aws_events_targets as _targets, 11 | aws_lambda as _lambda 12 | ) 13 | from constructs import Construct 14 | 15 | class ResourceTaggingStack(Stack): 16 | 17 | def __init__(self, scope: Construct, id: str, **kwargs) -> None: 18 | super().__init__(scope, id, **kwargs) 19 | 20 | # set parameters 21 | tags = CfnParameter(self, "tags", type="String", description="tag name and value with json format.") 22 | identityRecording = CfnParameter(self, "identityRecording", type="String", default="true", description="Defines if the tool records the requester identity as a tag.") 23 | 24 | # create role for lambda function 25 | lambda_role = _iam.Role(self, "lambda_role", 26 | role_name = "resource-tagging-role", 27 | assumed_by=_iam.ServicePrincipal("lambda.amazonaws.com")) 28 | 29 | lambda_role.add_managed_policy(_iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole")) 30 | lambda_role.add_to_policy(_iam.PolicyStatement( 31 | effect=_iam.Effect.ALLOW, 32 | resources=["*"], 33 | actions=["dynamodb:TagResource", "dynamodb:DescribeTable", "lambda:TagResource", "lambda:ListTags", "s3:GetBucketTagging", "s3:PutBucketTagging", 34 | "ec2:CreateTags", "ec2:DescribeNatGateways", "ec2:DescribeInternetGateways", "ec2:DescribeVolumes", "rds:AddTagsToResource", "rds:DescribeDBInstances", 35 | "sns:TagResource", "sqs:ListQueueTags", "sqs:TagQueue", "es:AddTags", "kms:ListResourceTags", "kms:TagResource", "elasticfilesystem:TagResource", 36 | "elasticfilesystem:CreateTags", "elasticfilesystem:DescribeTags", "elasticloadbalancing:AddTags", "logs:CreateLogGroup", "logs:CreateLogStream", 37 | "logs:PutLogEvents", "tag:getResources", "tag:getTagKeys", "tag:getTagValues", "tag:TagResources", "tag:UntagResources", "cloudformation:DescribeStacks", 38 | "cloudformation:ListStackResources", "elasticache:DescribeReplicationGroups", "elasticache:DescribeCacheClusters", "elasticache:AddTagsToResource","resource-groups:*"] 39 | )) 40 | 41 | # create lambda function 42 | tagging_function = _lambda.Function(self, "resource_tagging_automation_function", 43 | runtime=_lambda.Runtime.PYTHON_3_10, 44 | memory_size=128, 45 | timeout=Duration.seconds(600), 46 | handler="lambda-handler.main", 47 | code=_lambda.Code.from_asset("./lambda"), 48 | function_name="resource-tagging-automation-function", 49 | role=lambda_role, 50 | environment={ 51 | "tags": tags.value_as_string, 52 | "identityRecording": identityRecording.value_as_string 53 | } 54 | ) 55 | 56 | _eventRule = _events.Rule(self, "resource-tagging-automation-rule", 57 | event_pattern=_events.EventPattern( 58 | source=["aws.ec2", "aws.elasticloadbalancing", "aws.rds", "aws.lambda", "aws.s3", "aws.dynamodb", "aws.elasticfilesystem", "aws.es", "aws.sqs", "aws.sns", "aws.kms", "aws.elasticache"], 59 | detail_type=["AWS API Call via CloudTrail"], 60 | detail={ 61 | "eventSource": ["ec2.amazonaws.com", "elasticloadbalancing.amazonaws.com", "s3.amazonaws.com", "rds.amazonaws.com", "lambda.amazonaws.com", "dynamodb.amazonaws.com", "elasticfilesystem.amazonaws.com", "es.amazonaws.com", "sqs.amazonaws.com", "sns.amazonaws.com", "kms.amazonaws.com", "elasticache.amazonaws.com"], 62 | "eventName": ["RunInstances", "CreateFunction20150331", "CreateBucket", "CreateDBInstance", "CreateTable", "CreateVolume", "CreateLoadBalancer", "CreateMountTarget", "CreateDomain", "CreateQueue", "CreateTopic", "CreateKey", "CreateReplicationGroup", "CreateCacheCluster", "ModifyReplicationGroupShardConfiguration"] 63 | } 64 | ) 65 | ) 66 | 67 | _eventRule.add_target(_targets.LambdaFunction(tagging_function, retry_attempts=2) 68 | ) --------------------------------------------------------------------------------