rem ========================================================= rem rem Creates Lambda function to update child resources from rem parent resource tags based on sync_tag_keys array. rem rem Supports ec2 volume, snapshot, eni, eip rem rem Cloudwatch Event Rule is created to trigger function when rem ec2 tag updates occur. rem rem SNS topic is created to send email notification on tag rem update. rem rem ========================================================= rem ========================================================= rem Update the following variables to match your environment rem ========================================================= set AWS_PROFILE=MY-AWS-PROFILE set AWS_DEFAULT_REGION=us-east-1 set aws_accountid=000000000000 set aws_nameprefix=synctags set aws_synctagkeys=[\"Name\", \"BillingCode\", \"Application\", \"Environment\"] set aws_notifyemail=ME@EMAIL.COM set aws_functiontest_instanceid=i-00000000000000000 rem ========================================================= rem Create function iam role and policy rem ========================================================= aws iam create-role --role-name %aws_nameprefix%-role --path "/service-role/" --assume-role-policy-document "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" aws iam put-role-policy --role-name %aws_nameprefix%-role --policy-name %aws_nameprefix%-policy --policy-document "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\",\"Action\": \"logs:CreateLogGroup\",\"Resource\": \"arn:aws:logs:%aws_default_region%:%aws_accountid%:*\"},{\"Effect\": \"Allow\",\"Action\": [\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\": [\"arn:aws:logs:%aws_default_region%:%aws_accountid%:log-group:/aws/lambda/%aws_nameprefix%-event-handler:*\"]},{\"Effect\": \"Allow\",\"Action\":[\"ec2:DescribeInstances\",\"ec2:DescribeVolumes\",\"ec2:DescribeSnapshots\",\"ec2:DescribeAddresses\",\"ec2:CreateTags\",\"ec2:DeleteTags\",\"ec2:DescribeNetworkInterfaces\",\"ec2:DescribeTags\"],\"Resource\": \"*\"},{\"Effect\": \"Allow\",\"Action\": [\"sns:Publish\",\"sns:Subscribe\"],\"Resource\": \"arn:aws:sns:%aws_default_region%:%aws_accountid%:%aws_nameprefix%-topic\"},{\"Effect\": \"Allow\",\"Action\": [\"iam:ListAccountAliases\"],\"Resource\": \"*\"}]}" rem ========================================================= rem Create notification topic and subscription for email rem notifications rem ========================================================= aws sns create-topic --name %aws_nameprefix%-topic --out text --query "TopicArn" aws sns subscribe --topic-arn arn:aws:sns:%AWS_DEFAULT_REGION%:%aws_accountid%:%aws_nameprefix%-topic --protocol email --notification-endpoint %aws_notifyemail% rem Confirm topic subscription email in order to receive notifications on tag updates rem ========================================================= rem Create function code file rem ========================================================= echo import boto3 > %aws_nameprefix%.py echo import os >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo def lambda_handler(event, context): >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo accountname = boto3.client('iam').list_account_aliases()['AccountAliases'][0] >> %aws_nameprefix%.py echo topicarn = os.environ["NotifyTopicArn"] >> %aws_nameprefix%.py echo sync_tag_keys = os.environ["SyncTagKeys"] >> %aws_nameprefix%.py echo instancename = '' >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo ec2_resource = boto3.resource('ec2') >> %aws_nameprefix%.py echo ec2_client = boto3.client('ec2') >> %aws_nameprefix%.py echo sns_client = boto3.client('sns') >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo try: >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo eventname = event['detail-type'] >> %aws_nameprefix%.py echo detail = event['detail'] >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo if eventname == 'Tag Change on Resource': >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo if detail['service'] == 'ec2' and detail['resource-type'] == 'instance': >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo changedtags = detail['changed-tag-keys'] >> %aws_nameprefix%.py echo resources = event['resources'][0].split('/') >> %aws_nameprefix%.py echo instanceid = resources[len(resources) - 1] >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo synctags = [t for t in changedtags or [] if t in sync_tag_keys] >> %aws_nameprefix%.py echo if not synctags: >> %aws_nameprefix%.py echo print("info: no matching changed tags") >> %aws_nameprefix%.py echo return >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo instance = ec2_resource.Instance(instanceid) >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo tags = [t for t in instance.tags or [] if t['Key'] in sync_tag_keys] >> %aws_nameprefix%.py echo if not tags: >> %aws_nameprefix%.py echo print("info: no matching instance tags") >> %aws_nameprefix%.py echo return >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo for vol in instance.volumes.all(): >> %aws_nameprefix%.py echo vol.create_tags(Tags=tags) >> %aws_nameprefix%.py echo snapshots = ec2_client.describe_snapshots(Filters=[{'Name':'volume-id','Values':[vol.id]}])['Snapshots'] >> %aws_nameprefix%.py echo for snap in snapshots: >> %aws_nameprefix%.py echo snapshot = ec2_resource.Snapshot(snap['SnapshotId']) >> %aws_nameprefix%.py echo snapshot.create_tags(Tags=tags) >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo for eni in instance.network_interfaces: >> %aws_nameprefix%.py echo eni.create_tags(Tags=tags) >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo addresses = ec2_client.describe_addresses(Filters=[{'Name':'instance-id','Values':[instanceid]}])['Addresses'] >> %aws_nameprefix%.py echo for addr in addresses: >> %aws_nameprefix%.py echo address = ec2_resource.VpcAddress(addr['AllocationId']) >> %aws_nameprefix%.py echo ec2_client.create_tags(Resources=[addr['AllocationId']],Tags=tags) >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo for tag in instance.tags: >> %aws_nameprefix%.py echo if 'Name' in tag['Key']: >> %aws_nameprefix%.py echo instancename = tag['Value'] >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo print('success: updated child resource tags for instance ' + instancename) >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo subject = 'Tags Update Event - ' + accountname + ' - ' + instancename >> %aws_nameprefix%.py echo message = 'Instance Name: ' + instancename + '\r\rTags:\r' + str(instance.tags) + '\r\rEvent Detail:\r' + str(event) >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo response = sns_client.publish( >> %aws_nameprefix%.py echo TopicArn=topicarn,Message=message,Subject=subject >> %aws_nameprefix%.py echo ) >> %aws_nameprefix%.py echo: >> %aws_nameprefix%.py echo return True >> %aws_nameprefix%.py echo except Exception as e: >> %aws_nameprefix%.py echo print(e) >> %aws_nameprefix%.py echo return False >> %aws_nameprefix%.py rem ========================================================= rem Create function zip with code, using ps on windows rem ========================================================= powershell -ExecutionPolicy ByPass -Command "& Compress-Archive .\%aws_nameprefix%.py .\%aws_nameprefix%.zip -Force" rem ========================================================= rem Create function and assign invoke to events rule rem ========================================================= aws lambda create-function --function-name %aws_nameprefix%-event-handler --zip-file fileb://%aws_nameprefix%.zip --handler %aws_nameprefix%.lambda_handler --runtime python3.7 --role arn:aws:iam::%aws_accountid%:role/service-role/%aws_nameprefix%-role --environment Variables="{NotifyTopicArn=arn:aws:sns:%aws_default_region%:%aws_accountid%:%aws_nameprefix%-topic,SyncTagKeys='%aws_synctagkeys%'}" --timeout 30 aws lambda add-permission --function-name %aws_nameprefix%-event-handler --action lambda:InvokeFunction --statement-id events --principal events.amazonaws.com --source-arn arn:aws:events:%aws_default_region%:%aws_accountid%:rule/%aws_nameprefix%-event-rule rem ========================================================= rem Call function with test event to est match and no rem match conditions. Review Cloutwatch LogGroup\LogStream rem ========================================================= rem NoMatch - should receive "info: no matching changed tags" in log stream aws lambda invoke --function-name %aws_nameprefix%-event-handler --payload "{ \"version\": \"0\", \"id\": \"bddcf1d6-0251-35a1-aab0-adc1fb47c11c\", \"detail-type\": \"Tag Change on Resource\", \"source\": \"aws.tag\", \"account\": \"%aws_accountid%\", \"time\": \"2018-09-18T20:41:38Z\", \"region\": \"%aws_default_region%\", \"resources\": [ \"arn:aws:ec2:%aws_default_region%:%aws_accountid%:instance/%aws_functiontest_instanceid%\" ], \"detail\": { \"changed-tag-keys\": [ \"a-new-key\", \"an-updated-key\", \"a-deleted-key\" ], \"service\": \"ec2\", \"resource-type\": \"instance\", \"version\": 3, \"tags\": { \"a-new-key\": \"tag-value-on-new-key-just-added\", \"an-updated-key\": \"tag-value-was-just-changed\", \"an-unchanged-key\": \"tag-value-still-the-same\" } }}" out rem Match - will receive notification email and "success: updated child resource tags for instance" in log stream aws lambda invoke --function-name %aws_nameprefix%-event-handler --payload "{ \"version\": \"0\", \"id\": \"bddcf1d6-0251-35a1-aab0-adc1fb47c11c\", \"detail-type\": \"Tag Change on Resource\", \"source\": \"aws.tag\", \"account\": \"%aws_accountid%\", \"time\": \"2018-09-18T20:41:38Z\", \"region\": \"%aws_default_region%\", \"resources\": [ \"arn:aws:ec2:%aws_default_region%:%aws_accountid%:instance/%aws_functiontest_instanceid%\" ], \"detail\": { \"changed-tag-keys\": [ \"BillingCode\", \"an-updated-key\", \"a-deleted-key\" ], \"service\": \"ec2\", \"resource-type\": \"instance\", \"version\": 3, \"tags\": { \"a-new-key\": \"tag-value-on-new-key-just-added\", \"an-updated-key\": \"tag-value-was-just-changed\", \"an-unchanged-key\": \"tag-value-still-the-same\" } }}" out rem ========================================================= rem Create cloudwatch event rule to trigger function rem on ec2 tag updates rem ========================================================= aws events put-rule --name %aws_nameprefix%-event-rule --event-pattern "{\"source\":[\"aws.tag\"],\"detail-type\":[\"Tag Change on Resource\"],\"detail\":{\"service\":[\"ec2\"],\"resource-type\":[\"instance\"]}}" aws events put-targets --rule %aws_nameprefix%-event-rule --targets "Id"="1","Arn"="arn:aws:lambda:%AWS_DEFAULT_REGION%:%aws_accountid%:function:%aws_nameprefix%-event-handler" rem ========================================================= rem Create test events for console testing of Lambda function rem https://aws.amazon.com/blogs/compute/improved-testing-on-the-aws-lambda-console/ rem ========================================================= rem MatchTestEvent echo { > matchtestevent.tmp echo "version": "0", >> matchtestevent.tmp echo "id": "bddcf1d6-0251-35a1-aab0-adc1fb47c11c", >> matchtestevent.tmp echo "detail-type": "Tag Change on Resource", >> matchtestevent.tmp echo "source": "aws.tag", >> matchtestevent.tmp echo "account": "%aws_accountid%", >> matchtestevent.tmp echo "time": "2018-09-18T20:41:38Z", >> matchtestevent.tmp echo "region": "%AWS_DEFAULT_REGION%", >> matchtestevent.tmp echo "resources": ["arn:aws:ec2:%AWS_DEFAULT_REGION%:%aws_accountid%:instance/%aws_functiontest_instanceid%" ], >> matchtestevent.tmp echo "detail": { >> matchtestevent.tmp echo "changed-tag-keys": [ "BillingCode", "an-updated-key", "a-deleted-key" ], >> matchtestevent.tmp echo "service": "ec2", >> matchtestevent.tmp echo "resource-type": "instance", >> matchtestevent.tmp echo "version": 3, >> matchtestevent.tmp echo "tags": { "a-new-key": "tag-value-on-new-key-just-added", "an-updated-key": "tag-value-was-just-changed", "an-unchanged-key": "tag-value-still-the-same" } >> matchtestevent.tmp echo } >> matchtestevent.tmp echo } >> matchtestevent.tmp notepad matchtestevent.tmp rem NoMatchTestEvent echo { > nomatchtestevent.tmp echo "version": "0", >> nomatchtestevent.tmp echo "id": "bddcf1d6-0251-35a1-aab0-adc1fb47c11c", >> nomatchtestevent.tmp echo "detail-type": "Tag Change on Resource", >> nomatchtestevent.tmp echo "source": "aws.tag", >> nomatchtestevent.tmp echo "account": "%aws_accountid%", >> nomatchtestevent.tmp echo "time": "2018-09-18T20:41:38Z", >> nomatchtestevent.tmp echo "region": "%AWS_DEFAULT_REGION%", >> nomatchtestevent.tmp echo "resources": [ "arn:aws:ec2:%AWS_DEFAULT_REGION%:%aws_accountid%:instance/%aws_functiontest_instanceid%" ], >> nomatchtestevent.tmp echo "detail": { >> nomatchtestevent.tmp echo "changed-tag-keys": ["a-new-key", "an-updated-key", "a-deleted-key" ], >> nomatchtestevent.tmp echo "service": "ec2", >> nomatchtestevent.tmp echo "resource-type": "instance", >> nomatchtestevent.tmp echo "version": 3, >> nomatchtestevent.tmp echo "tags": { "a-new-key": "tag-value-on-new-key-just-added", "an-updated-key": "tag-value-was-just-changed", "an-unchanged-key": "tag-value-still-the-same" } >> nomatchtestevent.tmp echo } >> nomatchtestevent.tmp echo } >> nomatchtestevent.tmp notepad nomatchtestevent.tmp rem ========================================================= rem Remove all resources rem ========================================================= aws events remove-targets --rule %aws_nameprefix%-event-rule --ids 1 aws events delete-rule --name %aws_nameprefix%-event-rule aws lambda delete-function --function-name %aws_nameprefix%-event-handler aws logs delete-log-group --log-group-name /aws/lambda/%aws_nameprefix%-event-handler aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:%AWS_DEFAULT_REGION%:%aws_accountid%:%aws_nameprefix%-topic --no-paginate --output text --query "Subscriptions[]["SubscriptionArn"]" > snstopicsubarn.tmp set /p snstopicsubarn=