Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DeployBlueGreenAction class to handle CodeDeploy deployments #121

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions ecs_deploy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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})
Expand Down Expand Up @@ -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/'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joaoricardo000 Will it always be us-east-1? Or should it be the same region as you are deploying to?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this needs to be configurable, like the regions are in the deploy, scale, etc. commands.
In addition this should be configured and falling back to a default inside the Action, not in the CLI controller, please.

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(
Expand Down
86 changes: 85 additions & 1 deletion ecs_deploy/ecs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
import time
import json
import re

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this like makes the class to a "codedeploy-deploy" tool, rather then an "ecs-deploy" too.
I'm still thinking, if this use-case might be a bit out of the scope of the project.

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'],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this imply, that a task definition does not have more than one containers?

'ContainerPort': int(self.service['loadBalancers'][0]['containerPort'])
}
}
}
}]
})


class ScaleAction(EcsAction):
def scale(self, desired_count):
try:
Expand Down