====== StepFunctions and State Machine ====== ===== List state machines ===== $ aws stepfunctions list-state-machines --profile nonprod_admin | jq .[][].name | grep -i 'dev\|test\|nonprod' "dev-approval-workflow" "dev-get-accounts" "ami-lifecycle-dev-ami-statemachine" ===== Core Step Function ===== {{:aws:screen_shot_2019-03-04_at_17.35.04.png?400|}} Code for the Lifecycle checking step function { "Comment": "StateMachine Framework to get lifecycle code working in states.", "StartAt": "StartState", "States": { "StartState":{ "Comment": "Passes input data", "Type": "Pass", "Result": { "AMIage": "30" }, "ResultPath": "$", "Next": "AMIolderThanXdays" }, "AMIolderThanXdays": { "Comment": "Local State comment", "Type": "Task", "Resource": "arn:aws:lambda:eu-west-1:05772692xxxx:function:AJS-test-statemachine-AMIolderThanX", "ResultPath": "$.AMIolderThanX", "Next": "AmisStillToProcess_Choice" }, "AmisStillToProcess_Choice": { "Type": "Choice", "Choices": [ { "Variable": "$.AMIolderThanX.continue", "BooleanEquals": true, "Next": "AMIgetAccounts" } ], "Default": "Done" }, "AMIgetAccounts": { "Type": "Task", "Resource": "arn:aws:lambda:eu-west-1:05772692xxxx:function:AJS-testStateMachine-getAccounts", "InputPath": "$.AMIolderThanX", "ResultPath": "$.Accounts", "Next": "AccountsStillToProcess_Choice" }, "AccountsStillToProcess_Choice": { "Type": "Choice", "Choices": [ { "Variable": "$.Accounts.continueAcc", "BooleanEquals": true, "Next": "AMIgetUsage" } ], "Default": "NextAmi" }, "AMIgetUsage": { "Type": "Task", "Resource": "arn:aws:lambda:eu-west-1:05772692xxxx:function:AJS-test-StateMachine-AMIgetUsage", "InputPath": "$.Accounts", "ResultPath": "$.Accounts", "Next": "AMIusageAddToNotifier" }, "AMIusageAddToNotifier":{ "Type": "Task", "Resource": "arn:aws:lambda:eu-west-1:05772692xxxx:function:AJS-test-StateMachine-AMI_Account_to_Notifier", "InputPath": "$.Accounts", "ResultPath": "$.Accounts", "Next": "AccountsStillToProcess_Choice" }, "NextAmi": { "Type": "Task", "Resource": "arn:aws:lambda:eu-west-1:05772692xxxx:function:AJS-test-statemachine-iterator", "InputPath": "$.AMIolderThanX", "ResultPath": "$.AMIolderThanX", "Next": "AmisStillToProcess_Choice" }, "Done": { "Type": "Pass", "End": true } } } ===== IAM policy ===== AndrewLambdaTestPolicy { "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "states:ListStateMachines", "ec2:DescribeInstances", "states:ListActivities", "states:CreateActivity", "autoscaling:DescribeLaunchConfigurations", "states:StopExecution", "ec2:DescribeImages", "states:SendTaskSuccess", "states:SendTaskFailure", "autoscaling:DescribeAutoScalingGroups", "ec2:DescribeImageAttribute", "states:SendTaskHeartbeat", "states:CreateStateMachine", "sts:AssumeRole" ], "Resource": "*" }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": "states:*", "Resource": [ "arn:aws:states:*:*:activity:*", "arn:aws:states:*:*:execution:*:*", "arn:aws:states:*:*:stateMachine:*" ] } ] } AmazonSQSFullAccess { "Version": "2012-10-17", "Statement": [ { "Action": [ "sqs:*" ], "Effect": "Allow", "Resource": "*" } ] } AWSLambdaBasicExecutionRole { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" } ] } AWSLambdaRole { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": [ "*" ] } ] } ===== Lambda Functions ===== AJS-test-statemachine-AMIolderThanX import os import boto3 import datetime from datetime import date from datetime import datetime # Simple Lambda to return ami list to emulate ami older than X days def checkAMIinUse(AMIage, context): region = os.environ['REGION'] ec2 = boto3.client('ec2',region) AGEthreshold = int(os.environ['AGEthreshold']) Regex = os.environ['REGEX'] AMIlist = [] # print (AGEthreshold, Regex) # get list of all AMI owned by this account, matching the regex AMIResponse = ec2.describe_images(Filters=[{'Name': 'name', 'Values': [Regex]}, ], Owners=['self']) for i in AMIResponse['Images']: AMIname = i['Name'] AMIimageID = i['ImageId'] AMIcreationDate = i['CreationDate'] # convert unicode string to timedate object AMICreationDateTime = datetime.strptime((i['CreationDate']), '%Y-%m-%dT%H:%M:%S.%fZ') # only needs days not time, so use date method, likewise now is just days AMICreationDate = AMICreationDateTime.date() now = date.today() # Get age of ami and convert timedelta to contain only days AMIage = (now - AMICreationDate).days if AMIage >= AGEthreshold: olderThanThreshold = True AMIlist.append(AMIimageID) else: olderThanThreshold = False # print ("AMI", AMIimageID, "Age Threshold ", AGEthreshold, "AMI age ",AMIage, "olderthanthres ", olderThanThreshold) # print (AMIlist) # resultDict = {"OlderThanThreshold":olderThanThreshold, "AMIage":AMIage} # return (AMIlist) index = 0 count = len(AMIlist) result = { "ami_ids": AMIlist, "index": index, "count": count, "continue": index < count } return (result) AJS-testStateMachine-getAccounts # Simple Lambda to return account numbers for ami shared with. # AccoutID == UserID import boto3 import sys def GetAMIaccounts(AMIinfo, context): ec2 = boto3.client('ec2') AMIlist = (AMIinfo['ami_ids']) Index = int(AMIinfo['index']) image = AMIlist[Index] # aws ec2 describe-image-attribute --image-id ami-07cf57ebf50e78466 --attribute launchPermission --profile nonprod_admin #try: response = ec2.describe_image_attribute(ImageId=image, Attribute='launchPermission') Accounts = response['LaunchPermissions'] accountlist = [] for account in Accounts: accountlist.append(account['UserId']) # print (AMIimageID['ImageID'], accountlist) indexAcc = 0 countAcc = len(accountlist) if countAcc == 0: continueAcc = False else: continueAcc = True response = {} response['ami_id'] = image response['account_id'] = accountlist response['indexAcc'] = indexAcc response['countAcc'] = countAcc response['continueAcc'] = continueAcc print (response) return (response) AJS-test-statemachine-iterator def lambda_handler(event, context): index = event['index'] count = event['count'] index += 1 event['index'] = index # "index < count" evalutes to either true or false # to control the step loop. event['continue'] = index < count return event AJS-test-StateMachine-AMIgetUsage import os import boto3 def lambda_handler(event, context): sts_client = boto3.client('sts') print (event) index = int(event['indexAcc']) count = int(event['countAcc']) ami_id = event['ami_id'] accountToAssume = event['account_id'][index] roleToAssume = os.environ['roleToAssume'] # print (index, type(index)) # print ("Account is ",accountToAssume, "roleToAssume", roleToAssume, "AMI id", ami_id) assumed_role_object=sts_client.assume_role( RoleArn="arn:aws:iam::"+accountToAssume+":role/"+roleToAssume, RoleSessionName="AMI_Lifecycle_StateMachine" ) # print ("RoleArnTest", RoleArnTest ) credentials=assumed_role_object['Credentials'] # print (credentials) ec2client = boto3.client( 'ec2', aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'] ) autoscaling_client = boto3.client( 'autoscaling', aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'] ) # Start of real code, we have already selected the correct account from the list of accounts to switch to, # but there is only one AMI being passed in. # aws ec2 describe-instances --filters "Name=image-id, Values=ami-0e12cbde3e77cbb98" --query 'Reservations[*].Instances[*].[InstanceId]' --profile nonprod_admin #EC2InUseResponse = ec2client.describe_instances(Filters=[{'Name': 'image-id', 'Values': [ami_id['ImageID']]}]) EC2InUseResponse = ec2client.describe_instances(Filters=[{'Name': 'image-id', 'Values': [ami_id]}]) # list comprehension, returns list of instances EC2instance_ids = [ i["InstanceId"] for r in EC2InUseResponse["Reservations"] for i in r["Instances"] ] # print ('checkAMIinUse: Instances built from ',ami_id, ' are ', EC2instance_ids) if len(EC2instance_ids) == 0: EC2AMIinUse = False else: EC2AMIinUse = True # print ('checkAMIinUse: Instances ', EC2AMIinUse) # End of ec2 instance detection # print ( '=' * 10 ) # get all ASG and for ech, get the Launch config. # check each LC for the AMI in AMIimageID # this gets all asg but have to match lc passed to function within it. # aws autoscaling describe-auto-scaling-groups --profile nonprod_admin --auto-scaling-group-names "AJS asg1" # print ('checkAMIinUse: ASG Check if AMI is in use by ASG') ASGresponse = autoscaling_client.describe_auto_scaling_groups() ASGAMIinUse = False ASGlist = [] for i in ASGresponse['AutoScalingGroups']: # get launch config from ASG LC_Name = i['LaunchConfigurationName'] ASG_Name = i['AutoScalingGroupName'] # print ('LC_Name is:-', LC_Name, 'ASG_Name is:- ', ASG_Name) # get all launch conf info and select ImageID LCResponse = autoscaling_client.describe_launch_configurations(LaunchConfigurationNames=[LC_Name, ], ) #LCResponse = getAllLaunchConf(LC_Name) LC_object = LCResponse['LaunchConfigurations'] for item in LC_object: # Check if imagesIDs are the same , if they are, add LaunchConf to list # print ('Testing for AMIimageID ', AMIimageID, ' in ', item['ImageId']) #if AMIimageID['ImageID'] == item['ImageId']: if ami_id == item['ImageId']: # print('True') ASGlist.append(item['LaunchConfigurationName']) ASGAMIinUse = True # This gets the EC2 instances fired up as part of autoscaling # $ aws autoscaling describe-auto-scaling-groups --profile nonprod_admin --auto-scaling-group-name 'AJS-asg1' --query 'AutoScalingGroups[*].Instances[*].InstanceId' # Create an empty list and populate with all the instances built as part of an asg ASGinstances = [] instanceInASG = autoscaling_client.describe_auto_scaling_groups(AutoScalingGroupNames=[ASG_Name]) for i in instanceInASG['AutoScalingGroups']: for inst in (i['Instances']): # print (inst['InstanceId']) ASGinstances.append(inst['InstanceId']) #print ('All EC2 instances ', EC2instance_ids) #print ('ASGinstances:- ', ASGinstances) for instASG in ASGinstances: # print (instASG) # remove asg drived instance from list of EC2 instances EC2instance_ids.remove(instASG) break break # print ('checkAMIinUse: ASG Ami InUse:- ', ASGAMIinUse, ASGlist ) if EC2AMIinUse or ASGAMIinUse is True: AMIinUse = True else: AMIinUse = False # print ( '+' * 10 ) # Finish off, inc index and write info back to state machine index += 1 event['indexAcc'] = index # "index < count" evalutes to either true or false # to control the step loop. event['continueAcc'] = index < count event['AMIusage'] = {'accountID':[accountToAssume], 'InstanceID':EC2instance_ids, 'ASG_ID':ASGlist} return (event) # return (AMIinUse, EC2instance_ids, ASGlist) """ { "ami_id": "ami-07cf57ebf50e78466", "account_id": [ "532982424333", "057726927330", "1234567890" ], "indexAcc": 1, "countAcc": 3, "continueAcc": true, "AMIusage": { "accountID": [ "532982424333" ], "InstanceID": [ "inst123456", "inst654321" ], "ASG_ID": [ "asg098765", "asg456789" ] } } """ ^Env variables ^ | roleToAssume | AMI-lifecycle-usage | Role needs autoscaling and ec2 access in each account to assume AJS-test-StateMachine-AMI_Account_to_Notifier import os import boto3 def lambda_handler(event, context): sqs = boto3.client('sqs') queue_url = os.environ['SQS_Queue'] MessageBody = os.environ['MessageBody'] index = event['indexAcc'] count = event['countAcc'] ami = event['ami_id'] """ "AMIusage": { "accountID": [ "532982424333" ], "InstanceID": [ "inst123456", "inst654321" ], "ASG_ID": [ "asg098765", "asg456789" ] } """ usage = event['AMIusage'] usage.update({'AMI_ID':ami}) account = usage['accountID'] instances = usage['InstanceID'] autoScaling = usage['ASG_ID'] AMIage = '30' # BUG!! don't send message if instances AND asg are both empty. print (instances, len(instances), autoScaling, len(autoScaling)) if len(instances) == 0 and len(autoScaling) == 0: print ('Quitting early') return (event) # Send message to SQS queue response = sqs.send_message( QueueUrl=queue_url, DelaySeconds=0, MessageAttributes={ 'AMIage': { 'DataType': 'Number', 'StringValue': AMIage }, 'AMIimageID': { 'DataType': 'String', 'StringValue': ami }, 'accountID': { 'DataType': 'String', 'StringValue': str(account) }, 'instances': { 'DataType': 'String', 'StringValue': str(instances) }, 'autoScaling': { 'DataType': 'String', 'StringValue': str(autoScaling) } }, MessageBody = MessageBody ) return (event) ^Env variables^ | MessageBody | Your account references an AMI older than the threshold in either EC2 instances or AutoScaling Launch Configurations. Please see Message Attributes for details. | | SQS_Queue | https://sqs.eu-west-1.amazonaws.com/057726927330/AJS-testQueue |