|
| 1 | +""" |
| 2 | +-*- coding: utf-8 -*- |
| 3 | +======================== |
| 4 | +AWS Lambda |
| 5 | +======================== |
| 6 | +Contributor: Chirag Rathod (Srce Cde) |
| 7 | +======================== |
| 8 | +""" |
| 9 | + |
| 10 | +import os |
| 11 | +import sys |
| 12 | +import traceback |
| 13 | +import logging |
| 14 | +import ast |
| 15 | +import json |
| 16 | +import boto3 |
| 17 | + |
| 18 | +logger = logging.getLogger() |
| 19 | +logger.setLevel(logging.INFO) |
| 20 | + |
| 21 | +# retrieve list of instance ids from ENV variable to skip stopping |
| 22 | +INSTANCE_IDS_TO_IGNORE_STOP = ast.literal_eval( |
| 23 | + os.environ.get("INSTANCE_IDS_TO_IGNORE_STOP", "[]") |
| 24 | +) |
| 25 | + |
| 26 | + |
| 27 | +def process_error() -> dict: |
| 28 | + ex_type, ex_value, ex_traceback = sys.exc_info() |
| 29 | + traceback_string = traceback.format_exception(ex_type, ex_value, ex_traceback) |
| 30 | + error_msg = json.dumps( |
| 31 | + { |
| 32 | + "errorType": ex_type.__name__, |
| 33 | + "errorMessage": str(ex_value), |
| 34 | + "stackTrace": traceback_string, |
| 35 | + } |
| 36 | + ) |
| 37 | + return error_msg |
| 38 | + |
| 39 | + |
| 40 | +def fetch_regions() -> list: |
| 41 | + """ |
| 42 | + Helper function to retrieve regions |
| 43 | +
|
| 44 | + Returns: |
| 45 | + -------- |
| 46 | + regions: list of AWS regions |
| 47 | + """ |
| 48 | + ec2_client = boto3.client("ec2") |
| 49 | + try: |
| 50 | + regions = ec2_client.describe_regions() |
| 51 | + except: |
| 52 | + error_msg = process_error() |
| 53 | + logger.error(error_msg) |
| 54 | + return regions["Regions"] |
| 55 | + |
| 56 | + |
| 57 | +def stop_instances(ec2_client: object, instance_ids: list) -> None: |
| 58 | + """ |
| 59 | + Helper function to stop the instances |
| 60 | +
|
| 61 | + Parameters: |
| 62 | + ----------- |
| 63 | + ec2_client: boto3 region specific object |
| 64 | + instance_ids: list of instance ids to stop |
| 65 | + """ |
| 66 | + try: |
| 67 | + response = ec2_client.stop_instances(InstanceIds=instance_ids, Force=True) |
| 68 | + except: |
| 69 | + error_msg = process_error() |
| 70 | + logger.error(error_msg) |
| 71 | + |
| 72 | + |
| 73 | +def start_instances(ec2_client: object, instance_ids: list) -> None: |
| 74 | + """ |
| 75 | + Helper function to stop the instances |
| 76 | +
|
| 77 | + Parameters: |
| 78 | + ----------- |
| 79 | + ec2_client: boto3 region specific object |
| 80 | + instance_ids: list of instance ids to start |
| 81 | + """ |
| 82 | + try: |
| 83 | + response = ec2_client.start_instances(InstanceIds=instance_ids) |
| 84 | + except: |
| 85 | + error_msg = process_error() |
| 86 | + logger.error(error_msg) |
| 87 | + |
| 88 | + |
| 89 | +def get_instance_ids(response: dict, STOP: bool) -> list: |
| 90 | + """ |
| 91 | + Parse the instance IDs from response |
| 92 | +
|
| 93 | + Parameters: |
| 94 | + ----------- |
| 95 | + response: boto3 describe_instances response |
| 96 | + STOP: Flag to decide type of instance Ids to fetch |
| 97 | +
|
| 98 | + Returns: |
| 99 | + -------- |
| 100 | + instance_ids: list of instance ids to stop |
| 101 | + """ |
| 102 | + if STOP: |
| 103 | + instance_ids_to_stop = [] |
| 104 | + for resp in response.get("Reservations"): |
| 105 | + for instance in resp.get("Instances"): |
| 106 | + if ( |
| 107 | + instance.get("State").get("Name") in ["pending", "running"] |
| 108 | + and instance["InstanceId"] not in INSTANCE_IDS_TO_IGNORE_STOP |
| 109 | + ): |
| 110 | + instance_ids_to_stop.append(instance["InstanceId"]) |
| 111 | + return instance_ids_to_stop |
| 112 | + else: |
| 113 | + instance_ids_to_start = [] |
| 114 | + for resp in response.get("Reservations"): |
| 115 | + for instance in resp.get("Instances"): |
| 116 | + if instance.get("State").get("Name") in ["stopped", "stopping"]: |
| 117 | + instance_ids_to_start.append(instance["InstanceId"]) |
| 118 | + return instance_ids_to_start |
| 119 | + |
| 120 | + |
| 121 | +def start_stop_instances_across_region(regions: list, STOP=True) -> None: |
| 122 | + """ |
| 123 | + Start / Stop the instances across regions |
| 124 | +
|
| 125 | + Parameters: |
| 126 | + ----------- |
| 127 | + regions: list of regions to analyze |
| 128 | + STOP: Flag to decide whether to start or stop the instances |
| 129 | +
|
| 130 | + """ |
| 131 | + try: |
| 132 | + for region in regions: |
| 133 | + ec2_client = boto3.client("ec2", region_name=region["RegionName"]) |
| 134 | + response = ec2_client.describe_instances() |
| 135 | + |
| 136 | + instance_ids = get_instance_ids(response, STOP) |
| 137 | + if instance_ids and STOP: |
| 138 | + stop_instances(ec2_client, instance_ids) |
| 139 | + |
| 140 | + if instance_ids and not STOP: |
| 141 | + start_instances(ec2_client, instance_ids) |
| 142 | + |
| 143 | + while "NextToken" in response: |
| 144 | + response = ec2_client.describe_instances( |
| 145 | + NextToken=response["NextToken"] |
| 146 | + ) |
| 147 | + instance_ids = get_instance_ids(response, STOP) |
| 148 | + if instance_ids and STOP: |
| 149 | + stop_instances(ec2_client, instance_ids) |
| 150 | + |
| 151 | + if instance_ids and not STOP: |
| 152 | + start_instances(ec2_client, instance_ids) |
| 153 | + |
| 154 | + except: |
| 155 | + error_msg = process_error() |
| 156 | + logger.error(error_msg) |
| 157 | + |
| 158 | + |
| 159 | +def lambda_handler(event, context): |
| 160 | + """ |
| 161 | + Main handler |
| 162 | + """ |
| 163 | + logging.info(event) |
| 164 | + # retrieve regions |
| 165 | + regions = fetch_regions() |
| 166 | + |
| 167 | + try: |
| 168 | + rule_type = event["resources"][0].split("/")[-1] |
| 169 | + |
| 170 | + # checking which rule triggered the lambda function |
| 171 | + if "ScheduledEC2StopRule" in rule_type: |
| 172 | + start_stop_instances_across_region(regions, STOP=True) |
| 173 | + |
| 174 | + if "ScheduledEC2StartRule" in rule_type: |
| 175 | + start_stop_instances_across_region(regions, STOP=False) |
| 176 | + except: |
| 177 | + error_msg = process_error() |
| 178 | + logger.error(error_msg) |
| 179 | + |
| 180 | + return {"statusCode": 200, "body": json.dumps("Thanks from Srce Cde (Chirag)!")} |
0 commit comments