From 339cbe353b898fab1d819644871e11e11335bf41 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 23 Feb 2023 21:09:30 +0100 Subject: [PATCH] ECS Policies (#264) * Policies for ecs_service, ecs_cluster, ecs_taskdefinition, ecs_task Signed-off-by: Alina Buzachis * Abstract List and Describe operations Signed-off-by: Alina Buzachis * Add extra black line Signed-off-by: Alina Buzachis * Update resources Signed-off-by: Alina Buzachis * Remove bracket Signed-off-by: Alina Buzachis * Update for dependant resources Signed-off-by: Alina Buzachis * Refactor Signed-off-by: Alina Buzachis * Remove extra space Signed-off-by: Alina Buzachis * apply suggestion Signed-off-by: Alina Buzachis * Linting Signed-off-by: Alina Buzachis * Move ecs policies and terminator classes into paas files Signed-off-by: Alina Buzachis * Apply suggestion Signed-off-by: Alina Buzachis * Apply suggestion Signed-off-by: Alina Buzachis * Fix rebase Signed-off-by: Alina Buzachis * remove trailing spaces Signed-off-by: Alina Buzachis * More ECS related permissions * requested changes --------- Signed-off-by: Alina Buzachis Co-authored-by: Alina Buzachis --- aws/policy/paas.yaml | 35 ++++++++ aws/policy/security-services.yaml | 32 ++------ aws/terminator/paas.py | 130 +++++++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 26 deletions(-) diff --git a/aws/policy/paas.yaml b/aws/policy/paas.yaml index 305e910e..8fdea1f6 100644 --- a/aws/policy/paas.yaml +++ b/aws/policy/paas.yaml @@ -147,3 +147,38 @@ Statement: StringLike: lambda:FunctionArn: - arn:aws:lambda:{{ aws_region }}:{{ aws_account_id }}:function:* + + - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees + Effect: Allow + Action: + - ecs:CreateCluster + Resource: "*" + + - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees + Effect: Allow + Action: + - ecs:Describe* + - ecs:List* + - ecs:TagResource + - ecs:UntagResource + - ecs:PutAccountSetting + - ecs:RegisterTaskDefinition + - ecs:DeregisterTaskDefinition + Resource: + - "*" + + - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees + Effect: Allow + Action: + - ecs:RunTask + - ecs:StartTask + - ecs:StopTask + - ecs:DeleteCluster + - ecs:CreateService + - ecs:DeleteService + - ecs:UpdateService + - ecs:UpdateCluster + - ecs:*CapacityProvider + - ecs:PutClusterCapacityProviders + Resource: + - 'arn:aws:ecs:{{ aws_region }}:{{ aws_account_id }}:*' diff --git a/aws/policy/security-services.yaml b/aws/policy/security-services.yaml index 9d07e30f..3f413719 100644 --- a/aws/policy/security-services.yaml +++ b/aws/policy/security-services.yaml @@ -31,22 +31,11 @@ Statement: - 'arn:aws:iam::aws:policy/service-role/AmazonDMSVPCManagementRole' - 'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole' - 'arn:aws:iam::aws:policy/service-role/AWSServiceRoleForVPCTransitGateway' - - # Legacy - We need to backport ansible-collections/community.aws/63 or - # wait until community.aws drops CI support for Ansible 2.9 - - Sid: AllowPassRole - Effect: Allow - Action: - - iam:PassRole - Resource: - - 'arn:aws:iam::{{ aws_account_id }}:role/ansible_lambda_role' + - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' - Sid: AllowRegionalUnrestrictedResourceActionsWhichIncurNoFees Effect: Allow Action: - - iam:ListAccountAliases - - iam:ListPolicies - - iam:ListInstanceProfiles - iam:GetUser - acm:ListCertificates - acm:ListTagsForCertificate @@ -73,12 +62,7 @@ Statement: Action: - access-analyzer:ValidatePolicy - iam:GetRole - - iam:ListAttachedRolePolicies - - iam:ListRoles - - iam:ListRolePolicies - - iam:ListRoleTags - - iam:ListSAMLProviders - - iam:ListServerCertificates + - iam:List* - iam:TagRole - iam:UntagRole - kms:CreateAlias @@ -92,12 +76,7 @@ Statement: - kms:GetKeyPolicy - kms:GetKeyRotationStatus - kms:GetPublicKey - - kms:ListAliases - - kms:ListGrants - - kms:ListKeyPolicies - - kms:ListKeys - - kms:ListResourceTags - - kms:ListRetirableGrants + - kms:List* - kms:PutKeyPolicy - kms:RetireGrant - kms:ScheduleKeyDeletion @@ -152,7 +131,6 @@ Statement: - iam:GetInstanceProfile - iam:GetSAMLProvider - iam:GetServerCertificate - - iam:ListInstanceProfilesForRole - iam:PassRole - iam:RemoveRoleFromInstanceProfile - iam:UpdateSAMLProvider @@ -181,6 +159,7 @@ Statement: - 'arn:aws:iam::{{ aws_account_id }}:role/rds_export_task' - 'arn:aws:logs:{{ aws_region }}:{{ aws_account_id }}:log-group:*' - 'arn:aws:logs:{{ aws_region }}:{{ aws_account_id }}:log-group:ansible-test*' + - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS' # This allows AWS Services to autmatically create their Default Service Linked Roles # These have fixed policies and can only be assumed by the service itself. @@ -195,6 +174,7 @@ Statement: - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/eks-nodegroup.amazonaws.com/*' - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/transitgateway.amazonaws.com/*' - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/network-firewall.amazonaws.com/*' + - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/ecs.amazonaws.com/*' Condition: ForAnyValue:StringEquals: iam:AWSServiceName: @@ -204,3 +184,5 @@ Statement: - 'eks-nodegroup.amazonaws.com' - 'transitgateway.amazonaws.com' - 'network-firewall.amazonaws.com' + - 'ecs.amazonaws.com' + - 'ecs-test.amazonaws.com' diff --git a/aws/terminator/paas.py b/aws/terminator/paas.py index 5df85809..02053ba3 100644 --- a/aws/terminator/paas.py +++ b/aws/terminator/paas.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from . import DbTerminator, Terminator @@ -117,3 +117,131 @@ def terminate(self): else: # delete streaming distribution self.client.delete_streaming_distribution(Id=self.Id, IfMatch=ETag) + + +class Ecs(DbTerminator): + @property + def age_limit(self): + return datetime.timedelta(minutes=20) + + @property + def name(self): + return self.instance['clusterName'] + + @staticmethod + def create(credentials): + def _paginate_cluster_results(client): + names = client.get_paginator('list_clusters').paginate( + PaginationConfig={ + 'PageSize': 100, + } + ).build_full_result()['clusterArns'] + + if not names: + return [] + + return client.describe_clusters(clusters=names)['clusters'] + + return Terminator._create(credentials, Ecs, 'ecs', _paginate_cluster_results) + + def terminate(self): + def _paginate_task_results(container_instance=None): + params = { + 'cluster': self.name, + 'PaginationConfig': { + 'PageSize': 100, + } + } + + if container_instance: + params['containerInstance'] = container_instance + + names = self.client.get_paginator('list_tasks').paginate( + **params + ).build_full_result()['taskArns'] + + return [] if not names else names + + def _paginate_task_definition_results(): + names = self.client.get_paginator('list_task_definitions').paginate( + PaginationConfig={ + 'PageSize': 100, + } + ).build_full_result()['taskDefinitionArns'] + + return [] if not names else names + + def _paginate_container_instance_results(): + names = self.client.get_paginator('list_container_instances').paginate( + cluster=self.name, + PaginationConfig={ + 'PageSize': 100, + } + ).build_full_result()['containerInstanceArns'] + + return [] if not names else names + + def _paginate_service_results(): + names = self.client.get_paginator('list_services').paginate( + cluster=self.name, + PaginationConfig={ + 'PageSize': 100, + } + ).build_full_result()['serviceArns'] + + return [] if not names else names + + # If there are running services, delete them first + services = _paginate_service_results() + for each in services: + self.client.delete_service(cluster=self.name, service=each, force=True) + + # Deregister container instances and stop any running task + container_instances = _paginate_container_instance_results() + for each in container_instances: + self.client.deregister_container_instance(containerInstance=each['containerInstanceArn'], force=True) + + # Deregister task definitions + task_definitions = _paginate_task_definition_results() + for each in task_definitions: + self.client.deregister_task_definition(taskDefinition=each) + + # Stop all the tasks + tasks = _paginate_task_results() + for each in tasks: + self.client.stop_task(cluster=self.name, task=each) + + # Delete cluster + try: + self.client.delete_cluster(cluster=self.name) + except (self.client.exceptions.ClusterContainsServicesException, self.client.exceptions.ClusterContainsTasksException): + pass + + +class EcsCluster(DbTerminator): + @property + def age_limit(self): + return timedelta(minutes=30) + + @property + def name(self): + return self.instance['clusterName'] + + @staticmethod + def create(credentials): + def _paginate_cluster_results(client): + names = client.get_paginator('list_clusters').paginate( + PaginationConfig={ + 'PageSize': 100, + } + ).build_full_result()['clusterArns'] + + if not names: + return [] + + return client.describe_clusters(clusters=names)['clusters'] + + return Terminator._create(credentials, EcsCluster, 'ecs', _paginate_cluster_results) + + def terminate(self): + self.client.delete_cluster(cluster=self.name)