diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..513c9f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.idea/ +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +.Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# poetry dependency management +poetry.lock diff --git a/Constants.py b/Constants.py new file mode 100755 index 0000000..168cb67 --- /dev/null +++ b/Constants.py @@ -0,0 +1,25 @@ +from enum import Enum + + +class Prefix(Enum): + ALLOWED = ['*'] + RESTRICTED = ['prod'] + + +class StatusCodes(Enum): + SUCCESS_STATUS_CODE = 200 + ERROR_STATUS_CODE = 500 + + +class ServiceState(Enum): + RUNNING = "Running" + PENDING = "Pending" + CHECKING = "Checking" + SERVICE_NOT_FOUND = "Service not found" + + +class Operations(Enum): + GET_STATUS = "status" + UPDATE_SERVICE = "update" + + diff --git a/EcsClusterProcessor.py b/EcsClusterProcessor.py new file mode 100755 index 0000000..53e6219 --- /dev/null +++ b/EcsClusterProcessor.py @@ -0,0 +1,29 @@ +from EcsManage import ECSManager +from Constants import Operations +from utils import build_response +from Constants import StatusCodes + + +class ClusterProcessor: + def __init__(self, ecs_manager): + self.ecs_manager:ECSManager = ECSManager() + + def process_clusters(self, cluster_names, environments, desired_tasks,operation:Operations=Operations.GET_STATUS): + """Process ECS clusters.""" + cluster_arns = self.ecs_manager.get_all_clusters_arns() + response = [] + for cluster_name in cluster_names: + for cluster_arn in cluster_arns: + if self.should_process_cluster(cluster_name, cluster_arn, environments): + if operation == Operations.UPDATE_SERVICE: + self.ecs_manager.update_cluster_services(cluster_arn, desired_tasks) + response = build_response(StatusCodes.SUCCESS_STATUS_CODE,f"Successfully update {cluster_name}") + elif operation == Operations.GET_STATUS: + response.append(self.ecs_manager.check_status(cluster_arn)) + return response + + @staticmethod + def should_process_cluster(cluster_name, cluster_arn, environments): + """Check if the cluster should be processed.""" + return (cluster_name == "all" or cluster_name in cluster_arn) and any(env in cluster_arn for env in environments) and cluster_name and cluster_arn + diff --git a/EcsManage.py b/EcsManage.py new file mode 100755 index 0000000..767ba5b --- /dev/null +++ b/EcsManage.py @@ -0,0 +1,80 @@ +import json +import os +import boto3 +from Constants import (Prefix) +from utils import (build_response, + get_name_from_arn, + build_service_status) + + +class ECSManager: + def __init__(self): + self.ecs_client = self._get_ecs_client() + + @staticmethod + def _get_ecs_client(): + region = os.environ.get("AWS_REGION") + return boto3.client('ecs', region_name=region) + + @staticmethod + def _contains_restricted(arn): + for restricted in Prefix.RESTRICTED.value: + return restricted.lower() in arn.lower() + + def remove_restricted_arns(self, arn_list): + return [arn for arn in arn_list if not self._contains_restricted(arn)] + + def get_all_clusters_arns(self): + try: + response = self.ecs_client.list_clusters() + return self.remove_restricted_arns(response["clusterArns"]) + except Exception as e: + print(e) + return self._error_response(str(e)) + + def get_all_services_arns(self, cluster): + try: + response = self.ecs_client.list_services(cluster=cluster) + return response["serviceArns"] + except Exception as e: + print(e) + return self._error_response(str(e)) + + def update_ecs_service(self, desired_task, cluster, service): + if self._contains_restricted(cluster): + raise Exception("Operation on this clusters is restricted") + self.ecs_client.update_service(cluster=cluster, service=service, desiredCount=int(desired_task)) + cluster_name = get_name_from_arn(cluster) + service_name = get_name_from_arn(service) + print("Service updated successfully:", {"cluster": cluster_name, "service": service_name}) + + def update_cluster_services(self, cluster_arn, desired_tasks): + """Update services in the cluster.""" + service_arns = self.get_all_services_arns(cluster=cluster_arn) + for service_arn in service_arns: + self.update_ecs_service(desired_task=desired_tasks, cluster=cluster_arn, service=service_arn) + + def check_status(self,cluster): + services = self.get_all_services_arns(cluster) + response = self.ecs_client.describe_services( + cluster=cluster, + services=services + ) + cluster_name = get_name_from_arn(cluster) + cluster_status = {"cluster": cluster_name, "services":[]} + if 'services' in response and len(response['services']) > 0: + for service in response['services']: + service_name = service.get('serviceName','') + running_count = service.get('runningCount', 0) + pending_count = service.get('pendingCount', 0) + cluster_status['services'].append(build_service_status( + service=service_name, + running=running_count, + pending=pending_count + )) + print("serivce name: ",service_name,", running: ",running_count,", pending: ",pending_count) + return cluster_status + + @staticmethod + def _error_response(message): + return build_response(500, message) \ No newline at end of file diff --git a/EcsUpdate.py b/EcsUpdate.py deleted file mode 100644 index 3a93e39..0000000 --- a/EcsUpdate.py +++ /dev/null @@ -1,93 +0,0 @@ -import json -import os -import boto3 - -# Constants -SUCCESS_STATUS_CODE = 200 -ERROR_STATUS_CODE = 500 - -# AWS ECS Client -def get_ecs_client(): - region = os.environ.get("AWS_REGION") - return boto3.client("ecs", region_name=region) - -# Response helpers -def create_response(status, message): - return {"statusCode": status, "body": json.dumps({"message": message})} - -def success_response(message): - return create_response(SUCCESS_STATUS_CODE, message) - -def error_response(message): - return create_response(ERROR_STATUS_CODE, message) - -# ECS Functions -def contains_prod(arn): - return "prod" in arn.lower() - -def remove_prod_arns(arn_list): - return [arn for arn in arn_list if not contains_prod(arn)] - -def get_all_clusters_arns(): - try: - ecs = get_ecs_client() - response = ecs.list_clusters() - return remove_prod_arns(response["clusterArns"]) - except Exception as e: - print(e) - return error_response(str(e)) - -def get_all_services_arns(cluster): - try: - ecs = get_ecs_client() - response = ecs.list_services(cluster=cluster) - return response["serviceArns"] - except Exception as e: - print(e) - return error_response(str(e)) - -def update_ecs_service(desired_task, cluster, service): - if "prod" in cluster.lower(): - raise Exception("Operation on production clusters is restricted") - ecs = get_ecs_client() - ecs.update_service(cluster=cluster, service=service, desiredCount=int(desired_task)) - print("Service updated successfully:", {"cluster": cluster, "service": service}) - -# Request processing functions -def parse_request_body(event): - try: - body = json.loads(event["body"]) - return body - except: - return event - -def extract_clusters(body): - return body["cluster_names"].split(',') - -def extract_environments(body): - return body['environment'].split(',') - -def extract_desired_tasks(body): - return body['desired_tasks'] - -# Lambda Handler -def lambda_handler(event, context): - try: - body = parse_request_body(event) - environments = extract_environments(body) - cluster_names = extract_clusters(body) - desired_tasks = extract_desired_tasks(body) - - cluster_arns = get_all_clusters_arns() - - for cluster_name in cluster_names: - for cluster_arn in cluster_arns: - if (cluster_name == "all" or cluster_name in cluster_arn) and any(env in cluster_arn for env in environments): - service_arns = get_all_services_arns(cluster=cluster_arn) - for service_arn in service_arns: - update_ecs_service(desired_task=desired_tasks, cluster=cluster_arn, service=service_arn) - - return success_response("Successfully updated your ECS clusters") - except Exception as e: - print(e) - return error_response(str(e)) diff --git a/EcsUpdateClass.py b/EcsUpdateClass.py deleted file mode 100644 index bddb95e..0000000 --- a/EcsUpdateClass.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -import os -import boto3 - -class ECSManager: - def __init__(self): - self.ecs_client = self._get_ecs_client() - - def _get_ecs_client(self): - region = os.environ.get("AWS_REGION") - return boto3.client("ecs", region_name=region) - - def _contains_prod(self, arn): - return "prod" in arn.lower() - - def remove_prod_arns(self, arn_list): - return [arn for arn in arn_list if not self._contains_prod(arn)] - - def get_all_clusters_arns(self): - try: - response = self.ecs_client.list_clusters() - return self.remove_prod_arns(response["clusterArns"]) - except Exception as e: - print(e) - return self._error_response(str(e)) - - def get_all_services_arns(self, cluster): - try: - response = self.ecs_client.list_services(cluster=cluster) - return response["serviceArns"] - except Exception as e: - print(e) - return self._error_response(str(e)) - - def update_ecs_service(self, desired_task, cluster, service): - if "prod" in cluster.lower(): - raise Exception("Operation on production clusters is restricted") - self.ecs_client.update_service(cluster=cluster, service=service, desiredCount=int(desired_task)) - print("Service updated successfully:", {"cluster": cluster, "service": service}) - - @staticmethod - def _error_response(message): - return {"statusCode": 500, "body": json.dumps({"message": message})} - - -class LambdaHandler: - def __init__(self, event): - self.event = event - self.ecs_manager = ECSManager() - - def _parse_request_body(self): - """Parse the request body.""" - try: - body = json.loads(self.event["body"]) - return body - except Exception as e: - print(e) - return self.event - - def extract_clusters(self, body): - """Extract cluster names from the request body.""" - return body.get("cluster_names", "").split(',') - - def extract_environments(self, body): - """Extract environments from the request body.""" - return body.get("environment", "").split(',') - - def extract_desired_tasks(self, body): - """Extract desired tasks from the request body.""" - return body.get("desired_tasks", "") - - def handle_request(self): - """Handle Lambda request.""" - try: - body = self._parse_request_body() - environments = self.extract_environments(body) - cluster_names = self.extract_clusters(body) - desired_tasks = self.extract_desired_tasks(body) - - cluster_arns = self.ecs_manager.get_all_clusters_arns() - - for cluster_name in cluster_names: - for cluster_arn in cluster_arns: - if (cluster_name == "all" or cluster_name in cluster_arn) and any(env in cluster_arn for env in environments): - service_arns = self.ecs_manager.get_all_services_arns(cluster=cluster_arn) - for service_arn in service_arns: - self.ecs_manager.update_ecs_service(desired_task=desired_tasks, cluster=cluster_arn, service=service_arn) - - return {"statusCode": 200, "body": json.dumps({"message": "Successfully updated your ECS clusters"})} - except Exception as e: - print(e) - return {"statusCode": 500, "body": json.dumps({"message": str(e)})} - -def lambda_handler(event, context): - """Lambda handler function.""" - handler = LambdaHandler(event) - return handler.handle_request() - diff --git a/EcsUpdateClass_1.1.py b/EcsUpdateClass_1.1.py deleted file mode 100644 index cf3d8fe..0000000 --- a/EcsUpdateClass_1.1.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import os -import boto3 - -class ECSManager: - def __init__(self): - self.ecs_client = self._get_ecs_client() - - def _get_ecs_client(self): - region = os.environ.get("AWS_REGION") - return boto3.client("ecs", region_name=region) - - def _contains_prod(self, arn): - return "prod" in arn.lower() - - def remove_prod_arns(self, arn_list): - return [arn for arn in arn_list if not self._contains_prod(arn)] - - def get_all_clusters_arns(self): - try: - response = self.ecs_client.list_clusters() - return self.remove_prod_arns(response["clusterArns"]) - except Exception as e: - print(e) - return self._error_response(str(e)) - - def get_all_services_arns(self, cluster): - try: - response = self.ecs_client.list_services(cluster=cluster) - return response["serviceArns"] - except Exception as e: - print(e) - return self._error_response(str(e)) - - def update_ecs_service(self, desired_task, cluster, service): - if "prod" in cluster.lower(): - raise Exception("Operation on production clusters is restricted") - self.ecs_client.update_service(cluster=cluster, service=service, desiredCount=int(desired_task)) - print("Service updated successfully:", {"cluster": cluster, "service": service}) - - @staticmethod - def _error_response(message): - return {"statusCode": 500, "body": json.dumps({"message": message})} - - -class LambdaHandler: - def __init__(self, event): - self.event = event - self.ecs_manager = ECSManager() - - def _parse_request_body(self): - """Parse the request body.""" - try: - body = json.loads(self.event["body"]) - return body - except Exception as e: - print(e) - return self.event - - def extract_clusters(self, body): - """Extract cluster names from the request body.""" - return body.get("cluster_names", "").split(',') - - def extract_environments(self, body): - """Extract environments from the request body.""" - return body.get("environment", "").split(',') - - def extract_desired_tasks(self, body): - """Extract desired tasks from the request body.""" - return body.get("desired_tasks", "") - - def handle_request(self): - """Handle Lambda request.""" - try: - body = self._parse_request_body() - environments = self.extract_environments(body) - cluster_names = self.extract_clusters(body) - desired_tasks = self.extract_desired_tasks(body) - - self.process_clusters(cluster_names, environments, desired_tasks) - - return {"statusCode": 200, "body": json.dumps({"message": "Successfully updated your ECS clusters"})} - except Exception as e: - print(e) - return {"statusCode": 500, "body": json.dumps({"message": str(e)})} - - def process_clusters(self, cluster_names, environments, desired_tasks): - """Process ECS clusters.""" - cluster_arns = self.ecs_manager.get_all_clusters_arns() - for cluster_name in cluster_names: - for cluster_arn in cluster_arns: - if self.should_process_cluster(cluster_name, cluster_arn, environments): - self.update_cluster_services(cluster_arn, desired_tasks) - - def should_process_cluster(self, cluster_name, cluster_arn, environments): - """Check if the cluster should be processed.""" - return (cluster_name == "all" or cluster_name in cluster_arn) and any(env in cluster_arn for env in environments) - - def update_cluster_services(self, cluster_arn, desired_tasks): - """Update services in the cluster.""" - service_arns = self.ecs_manager.get_all_services_arns(cluster=cluster_arn) - for service_arn in service_arns: - self.ecs_manager.update_ecs_service(desired_task=desired_tasks, cluster=cluster_arn, service=service_arn) - -def lambda_handler(event, context): - """Lambda handler function.""" - handler = LambdaHandler(event) - return handler.handle_request() \ No newline at end of file diff --git a/EcsUpdateClass_1.2.py b/EcsUpdateClass_1.2.py deleted file mode 100644 index 243e28e..0000000 --- a/EcsUpdateClass_1.2.py +++ /dev/null @@ -1,112 +0,0 @@ -import json -import os -import boto3 - -class ECSManager: - def __init__(self): - self.ecs_client = self._get_ecs_client() - - def _get_ecs_client(self): - region = os.environ.get("AWS_REGION") - return boto3.client("ecs", region_name=region) - - def _contains_prod(self, arn): - return "prod" in arn.lower() - - def remove_prod_arns(self, arn_list): - return [arn for arn in arn_list if not self._contains_prod(arn)] - - def get_all_clusters_arns(self): - try: - response = self.ecs_client.list_clusters() - return self.remove_prod_arns(response["clusterArns"]) - except Exception as e: - print(e) - return self._error_response(str(e)) - - def get_all_services_arns(self, cluster): - try: - response = self.ecs_client.list_services(cluster=cluster) - return response["serviceArns"] - except Exception as e: - print(e) - return self._error_response(str(e)) - - def update_ecs_service(self, desired_task, cluster, service): - if "prod" in cluster.lower(): - raise Exception("Operation on production clusters is restricted") - self.ecs_client.update_service(cluster=cluster, service=service, desiredCount=int(desired_task)) - print("Service updated successfully:", {"cluster": cluster, "service": service}) - - @staticmethod - def _error_response(message): - return {"statusCode": 500, "body": json.dumps({"message": message})} - -class ClusterProcessor: - def __init__(self, ecs_manager): - self.ecs_manager = ecs_manager - - def process_clusters(self, cluster_names, environments, desired_tasks): - """Process ECS clusters.""" - cluster_arns = self.ecs_manager.get_all_clusters_arns() - for cluster_name in cluster_names: - for cluster_arn in cluster_arns: - if self.should_process_cluster(cluster_name, cluster_arn, environments): - self.update_cluster_services(cluster_arn, desired_tasks) - - def should_process_cluster(self, cluster_name, cluster_arn, environments): - """Check if the cluster should be processed.""" - return (cluster_name == "all" or cluster_name in cluster_arn) and any(env in cluster_arn for env in environments) - - def update_cluster_services(self, cluster_arn, desired_tasks): - """Update services in the cluster.""" - service_arns = self.ecs_manager.get_all_services_arns(cluster=cluster_arn) - for service_arn in service_arns: - self.ecs_manager.update_ecs_service(desired_task=desired_tasks, cluster=cluster_arn, service=service_arn) - -class LambdaHandler: - def __init__(self, event): - self.event = event - self.ecs_manager = ECSManager() - self.cluster_processor = ClusterProcessor(self.ecs_manager) - - def _parse_request_body(self): - """Parse the request body.""" - try: - body = json.loads(self.event["body"]) - return body - except Exception as e: - print(e) - return self.event - - def extract_clusters(self, body): - """Extract cluster names from the request body.""" - return body.get("cluster_names", "").split(',') - - def extract_environments(self, body): - """Extract environments from the request body.""" - return body.get("environment", "").split(',') - - def extract_desired_tasks(self, body): - """Extract desired tasks from the request body.""" - return body.get("desired_tasks", "") - - def handle_request(self): - """Handle Lambda request.""" - try: - body = self._parse_request_body() - environments = self.extract_environments(body) - cluster_names = self.extract_clusters(body) - desired_tasks = self.extract_desired_tasks(body) - - self.cluster_processor.process_clusters(cluster_names, environments, desired_tasks) - - return {"statusCode": 200, "body": json.dumps({"message": "Successfully updated your ECS clusters"})} - except Exception as e: - print(e) - return {"statusCode": 500, "body": json.dumps({"message": str(e)})} - -def lambda_handler(event, context): - """Lambda handler function.""" - handler = LambdaHandler(event) - return handler.handle_request() diff --git a/EcsUpdateClass_1.3.py b/EcsUpdateClass_1.3.py deleted file mode 100644 index e3dcfce..0000000 --- a/EcsUpdateClass_1.3.py +++ /dev/null @@ -1,97 +0,0 @@ -import json -import os -import boto3 - -def build_response(status_code, message, exception=None): - """Build response object.""" - response_body = {"message": message} - if exception: - response_body["error"] = str(exception) - - return {"statusCode": status_code, "body": json.dumps(response_body)} - -class ECSManager: - def __init__(self): - self.ecs_client = self._get_ecs_client() - - def _get_ecs_client(self): - region = os.environ.get("AWS_REGION") - return boto3.client("ecs", region_name=region) - - def _contains_prod(self, arn): - return "prod" in arn.lower() - - def remove_prod_arns(self, arn_list): - return [arn for arn in arn_list if not self._contains_prod(arn)] - - def get_all_clusters_arns(self): - try: - response = self.ecs_client.list_clusters() - return self.remove_prod_arns(response["clusterArns"]) - except Exception as e: - print(e) - return self._error_response(str(e)) - - def get_all_services_arns(self, cluster): - try: - response = self.ecs_client.list_services(cluster=cluster) - return response["serviceArns"] - except Exception as e: - print(e) - return self._error_response(str(e)) - - def update_ecs_service(self, desired_task, cluster, service): - if "prod" in cluster.lower(): - raise Exception("Operation on production clusters is restricted") - self.ecs_client.update_service(cluster=cluster, service=service, desiredCount=int(desired_task)) - print("Service updated successfully:", {"cluster": cluster, "service": service}) - - def _error_response(self, message): - return build_response(500, message) - -class LambdaHandler: - def __init__(self, event): - self.event = event - self.ecs_manager = ECSManager() - self.cluster_processor = ClusterProcessor(self.ecs_manager) - - def _parse_request_body(self): - """Parse the request body.""" - try: - body = json.loads(self.event["body"]) - return body - except Exception as e: - print(e) - return self.event - - def extract_clusters(self, body): - """Extract cluster names from the request body.""" - return body.get("cluster_names", "").split(',') - - def extract_environments(self, body): - """Extract environments from the request body.""" - return body.get("environment", "").split(',') - - def extract_desired_tasks(self, body): - """Extract desired tasks from the request body.""" - return body.get("desired_tasks", "") - - def handle_request(self): - """Handle Lambda request.""" - try: - body = self._parse_request_body() - environments = self.extract_environments(body) - cluster_names = self.extract_clusters(body) - desired_tasks = self.extract_desired_tasks(body) - - self.cluster_processor.process_clusters(cluster_names, environments, desired_tasks) - - return build_response(200, "Successfully updated your ECS clusters") - except Exception as e: - print(e) - return build_response(500, "Internal Server Error", e) - -def lambda_handler(event, context): - """Lambda handler function.""" - handler = LambdaHandler(event) - return handler.handle_request() diff --git a/LambdaHandler.py b/LambdaHandler.py new file mode 100755 index 0000000..d7167ca --- /dev/null +++ b/LambdaHandler.py @@ -0,0 +1,54 @@ +from utils import build_response +from Constants import (StatusCodes,Operations) +from EcsManage import ECSManager +import json +from EcsClusterProcessor import ClusterProcessor + + +class LambdaHandler: + def __init__(self, event): + self.event = event + self.ecs_manager = ECSManager() + self.cluster_processor = ClusterProcessor(self.ecs_manager) + + def _parse_request_body(self): + """Parse the request body.""" + try: + body = json.loads(self.event["body"]) + return body + except Exception as e: + return self.event["body"] + + @staticmethod + def extract_clusters(body): + """Extract cluster names from the request body.""" + return body.get("cluster_names", "").split(',') + + @staticmethod + def extract_environments( body): + """Extract environments from the request body.""" + return body.get("environment", "").split(',') + + @staticmethod + def extract_desired_tasks( body): + """Extract desired tasks from the request body.""" + return body.get("desired_tasks", "") + + def handle_request(self,operation:Operations = Operations.GET_STATUS): + """Handle Lambda request.""" + try: + body = self._parse_request_body() + environments = self.extract_environments(body) + cluster_names = self.extract_clusters(body) + desired_tasks = self.extract_desired_tasks(body) + process = self.cluster_processor.process_clusters( + cluster_names=cluster_names, + environments=environments, + desired_tasks= desired_tasks, + operation=operation + ) + + return process + except Exception as e: + print(e) + return build_response(StatusCodes.ERROR_STATUS_CODE, "Internal Server Error", e) \ No newline at end of file diff --git a/README.md b/README.md index a9e44d1..3310afe 100644 --- a/README.md +++ b/README.md @@ -1 +1,87 @@ -# EcsService \ No newline at end of file +# ECS Infrastructure Scaling Utility + +This Python utility is designed to upscale or downscale ECS (Amazon Elastic Container Service) services, providing functionality to manage containerized applications efficiently. It utilizes Boto3, the AWS SDK for Python, along with AWS Lambda functions. + +## Key Features + +1. **Upscale and Downscale ECS Services**: Automatically adjust the desired task count for ECS services based on specified criteria such as CPU or memory utilization. + +2. **Retrieve ECS Service Information**: Obtain detailed information about ECS services including the number of tasks pending and running, aiding in monitoring and management. + +3. **Production Environment Protection**: Implements safeguards to restrict updates to ECS services in production environments, minimizing risks and ensuring stability. + +## Benefits + +1. **Cost Optimization**: Efficiently manage ECS resources to optimize costs by scaling up or down based on demand, avoiding unnecessary expenses. + +2. **On-Demand Development and Staging Environments**: Enable on-demand scaling for development and staging environments, allowing flexibility and agility in testing and development workflows. + +## Usage + +There are two way to use this use this utility + + 1. Clone the repository and execute the utility as a standard Python project, allowing for customization and direct usage. + 2. Configuration with AWS Lambda Function: + + + +### Prerequisites for Lambda functoin + +- AWS account with appropriate permissions to access ECS services. +- Lambda function with the appropriate IAM role. +- Postman for testing API endpoints. + +### Configuration with Lambda function + +- Set up a Lambda function with the provided IAM role. +```commandline +{ + "Effect": "Allow", + "Action": [ + "ecs:ListServices", + "ecs:ListTasks", + "ecs:ListContainerInstances", + "ecs:ListTaskDefinitions", + "ecs:ListClusters", + "ecs:DescribeClusters", + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:DescribeTasks", + "ecs:UpdateService" + ], + "Resource": "*" +} +``` + +- Attach an API Gateway to the Lambda function for external access. Consider implementing additional security measures such as API keys or IAM access. +- Utilize HTTP POST/GET requests to interact with the API Gateway endpoint. +- Provide the request body with parameters such as desired task count, cluster names, and environment details. + +Example Request Body for Postman: +```commandline +Use body object in case of Post man +{ + "desired_tasks": "0", + "cluster_names": "all", + "environment" : "dev,stage" +} + + +``` + +## Production Resources Safty: +To ensure safety and prevent unintended actions, the Prefix class has been implemented with specific keywords designated for different access levels. + +**ALLOWED**: This includes a list containing only the wildcard '*', which essentially allows unrestricted access to all resources. + +**RESTRICTED**: This list contains 'prod', indicating restricted access specifically to production resources. +```commandline +to change the Constants.py you can chage safeguard + +class Prefix(Enum): + ALLOWED = ['*'] + RESTRICTED = ['prod'] +``` +## Summary +The ECS Infrastructure Scaling Utility is a Python tool for automating ECS service scaling based on resource utilization. It offers monitoring features and safeguards for production environments, optimizing costs and enhancing flexibility. + diff --git a/iam.json b/iam.json new file mode 100644 index 0000000..cf14f8a --- /dev/null +++ b/iam.json @@ -0,0 +1,16 @@ +{ + "Effect": "Allow", + "Action": [ + "ecs:ListServices", + "ecs:ListTasks", + "ecs:ListContainerInstances", + "ecs:ListTaskDefinitions", + "ecs:ListClusters", + "ecs:DescribeClusters", + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:DescribeTasks", + "ecs:UpdateService" + ], + "Resource": "*" +} \ No newline at end of file diff --git a/lambda_function.py b/lambda_function.py new file mode 100755 index 0000000..d5ad70e --- /dev/null +++ b/lambda_function.py @@ -0,0 +1,31 @@ +from LambdaHandler import LambdaHandler +from utils import build_response +from Constants import Operations + + +def lambda_handler(event, context): + if 'httpMethod' in event: + http_method = event['httpMethod'] + lambda_handler_ecs: LambdaHandler = LambdaHandler(event) + if http_method == 'GET': + print("______________________get request__________________________") + response = lambda_handler_ecs.handle_request( + operation=Operations.GET_STATUS + ) + print("Cluster_status: ", response) + return build_response(200, response) + # Handle GET request + elif http_method == 'POST': + print("_______________________post request________________________") + request = lambda_handler_ecs.handle_request( + operation=Operations.UPDATE_SERVICE + ) + # Handle POST request + return build_response(200, "Services updated successfully!") + else: + # Handle other HTTP methods + return build_response(405, 'Unsupported HTTP method') + else: + # Handle case where HTTP method is not provided + return build_response(400, 'No HTTP method provided') + diff --git a/run.py b/run.py new file mode 100755 index 0000000..cba3348 --- /dev/null +++ b/run.py @@ -0,0 +1,11 @@ +from lambda_function import lambda_handler + +Event = { + "httpMethod": "GET", + "body": { + "desired_tasks": "0", + "cluster_names": "all", + "environment" : "dev,stage" + } +} +lambda_handler(event=Event,context="Hello World") \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..6126f46 --- /dev/null +++ b/utils.py @@ -0,0 +1,22 @@ +import json + + +def build_response(status_code, message, exception=None): + """Build response object.""" + response_body = {"message": message} + if exception: + response_body["error"] = str(exception) + return {"statusCode": status_code, "body": json.dumps(response_body)} + + +def build_cluster_status(cluster,services:[]): + return {"cluster": cluster, services:services} + + +def build_service_status(service,running,pending): + return {"service":service,"running":running,"pending":pending} + + +def get_name_from_arn(arn): + name = arn.split('/')[-1] + return name \ No newline at end of file