diff --git a/ecs_deploy/cli.py b/ecs_deploy/cli.py index 800c019..520694b 100644 --- a/ecs_deploy/cli.py +++ b/ecs_deploy/cli.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta from ecs_deploy import VERSION -from ecs_deploy.ecs import DeployAction, ScaleAction, RunAction, EcsClient, DiffAction, \ +from ecs_deploy.ecs import DeployAction, DeployBlueGreenAction, ScaleAction, RunAction, EcsClient, DiffAction, \ TaskPlacementError, EcsError, UpdateAction, LAUNCH_TYPE_EC2, LAUNCH_TYPE_FARGATE from ecs_deploy.newrelic import Deployment, NewRelicException from ecs_deploy.slack import SlackNotification @@ -55,7 +55,8 @@ def get_client(access_key_id, secret_access_key, region, profile): @click.option('--sleep-time', default=1, type=int, help='Amount of seconds to wait between each check of the service (default: 1)') @click.option('--slack-url', required=False, help='Webhook URL of the Slack integration. Can also be defined via environment variable SLACK_URL') @click.option('--slack-service-match', default=".*", required=False, help='A regular expression for defining, which services should be notified. (default: .* =all). Can also be defined via environment variable SLACK_SERVICE_MATCH') -def deploy(cluster, service, tag, image, command, env, secret, role, execution_role, task, region, access_key_id, secret_access_key, profile, timeout, newrelic_apikey, newrelic_appid, newrelic_region, comment, user, ignore_warnings, diff, deregister, rollback, exclusive_env, exclusive_secrets, sleep_time, slack_url, slack_service_match='.*'): +@click.option('--cd-application-name', required=False, help='CodeDeploy Application name from Blue/Green deployment') +def deploy(cluster, service, tag, image, command, env, secret, role, execution_role, task, region, access_key_id, secret_access_key, profile, timeout, newrelic_apikey, newrelic_appid, newrelic_region, comment, user, ignore_warnings, diff, deregister, rollback, exclusive_env, exclusive_secrets, sleep_time, slack_url, slack_service_match='.*', cd_application_name=None): """ Redeploy or modify a service. @@ -70,7 +71,10 @@ def deploy(cluster, service, tag, image, command, env, secret, role, execution_r try: client = get_client(access_key_id, secret_access_key, region, profile) - deployment = DeployAction(client, cluster, service) + if cd_application_name: + deployment = DeployBlueGreenAction(client, cluster, service, cd_application_name=cd_application_name) + else: + deployment = DeployAction(client, cluster, service) td = get_task_definition(deployment, task) td.set_images(tag, **{key: value for (key, value) in image}) @@ -437,13 +441,18 @@ def deploy_task_definition(deployment, task_definition, title, success_message, failure_message, timeout, deregister, previous_task_definition, ignore_warnings, sleep_time): click.secho('Updating service') - deployment.deploy(task_definition) + deploy_response = deployment.deploy(task_definition) message = 'Successfully changed task definition to: %s:%s\n' % ( task_definition.family, task_definition.revision ) + if type(deployment) == DeployBlueGreenAction: + _cd_deploy_url = 'https://us-east-1.console.aws.amazon.com/codesuite/codedeploy/deployments/' + click.secho('\nDeployment created: %s' % deploy_response, fg='green') + click.secho('\t%s%s\n' % (_cd_deploy_url, deploy_response), fg='yellow') + click.secho(message, fg='green') wait_for_finish( diff --git a/ecs_deploy/ecs.py b/ecs_deploy/ecs.py index 6e99d91..df067e4 100644 --- a/ecs_deploy/ecs.py +++ b/ecs_deploy/ecs.py @@ -1,4 +1,5 @@ from datetime import datetime +import time import json import re @@ -30,6 +31,7 @@ def __init__(self, access_key_id=None, secret_access_key=None, profile_name=profile) self.boto = session.client(u'ecs') self.events = session.client(u'events') + self.codedeploy = session.client(u'codedeploy') def describe_services(self, cluster_name, service_name): return self.boto.describe_services( @@ -541,7 +543,7 @@ def _get_secrets_diffs(container, secrets, old_secrets): class EcsAction(object): - def __init__(self, client, cluster_name, service_name): + def __init__(self, client, cluster_name, service_name, **kwargs): self._client = client self._cluster_name = cluster_name self._service_name = service_name @@ -663,6 +665,88 @@ def deploy(self, task_definition): raise EcsError(str(e)) +class DeployBlueGreenAction(EcsAction): + def __init__(self, *args, **kwargs): + super(DeployBlueGreenAction, self).__init__(*args, **kwargs) + self.cd_application_name = kwargs['cd_application_name'] + self._cd_group_name = None + self._deployment_target_id = None + self._deployment_id = None + self._traffic_health_percentage = 90 + + @property + def deployment_id(self): + if not self._deployment_id: + raise EcsError('CodeDeploy deployment not defined') + return self._deployment_id + + @property + def cd_group_name(self): + if not self._cd_group_name: + self._cd_group_name = self.client.codedeploy.list_deployment_groups( + applicationName=self.cd_application_name, + )['deploymentGroups'][0] + return self._cd_group_name + + @property + def deployment_target_id(self): + while not self._deployment_target_id: + try: + self._deployment_target_id = self.client.codedeploy.list_deployment_targets( + deploymentId=self.deployment_id + )['targetIds'][0] + except ClientError as e: + if e.response['Error']['Code'] == 'DeploymentNotStartedException': + time.sleep(1) + continue + raise e + return self._deployment_target_id + + def deploy(self, task_definition): + response = self.client.codedeploy.create_deployment( + applicationName=self.cd_application_name, + deploymentGroupName=self.cd_group_name, + revision={ + 'revisionType': 'String', + 'string': { + 'content': self._get_revision_content(task_definition) + } + } + ) + self._deployment_id = response['deploymentId'] + return self._deployment_id + + def is_deployed(self, service): + deployment_target = self.client.codedeploy.get_deployment_target( + deploymentId=self.deployment_id, + targetId=self.deployment_target_id + )['deploymentTarget'] + + if deployment_target['ecsTarget']['status'] == 'Failed': + raise EcsError('CodeDeploy Deployment Failed.') + + for task_set in deployment_target['ecsTarget']['taskSetsInfo']: + if task_set['taskSetLabel'] == 'Green': + return task_set["trafficWeight"] > self._traffic_health_percentage + + def _get_revision_content(self, task_definition): + return json.dumps({ + 'version': 1, + 'Resources': [{ + 'TargetService': { + 'Type': 'AWS::ECS::Service', + 'Properties': { + 'TaskDefinition': task_definition.arn, + 'LoadBalancerInfo': { + 'ContainerName': self.service['loadBalancers'][0]['containerName'], + 'ContainerPort': int(self.service['loadBalancers'][0]['containerPort']) + } + } + } + }] + }) + + class ScaleAction(EcsAction): def scale(self, desired_count): try: