Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/ecs/deploy.py
1567 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
14
import hashlib
15
import json
16
import os
17
import sys
18
19
from botocore import compat, config
20
from botocore.exceptions import ClientError
21
from awscli.compat import compat_open
22
from awscli.customizations.ecs import exceptions, filehelpers
23
from awscli.customizations.commands import BasicCommand
24
25
TIMEOUT_BUFFER_MIN = 10
26
DEFAULT_DELAY_SEC = 15
27
MAX_WAIT_MIN = 360 # 6 hours
28
29
30
class ECSDeploy(BasicCommand):
31
NAME = 'deploy'
32
33
DESCRIPTION = (
34
"Deploys a new task definition to the specified ECS service. "
35
"Only services that use CodeDeploy for deployments are supported. "
36
"This command will register a new task definition, update the "
37
"CodeDeploy appspec with the new task definition revision, create a "
38
"CodeDeploy deployment, and wait for the deployment to successfully "
39
"complete. This command will exit with a return code of 255 if the "
40
"deployment does not succeed within 30 minutes by default or "
41
"up to 10 minutes more than your deployment group's configured wait "
42
"time (max of 6 hours)."
43
)
44
45
ARG_TABLE = [
46
{
47
'name': 'service',
48
'help_text': ("The short name or full Amazon Resource Name "
49
"(ARN) of the service to update"),
50
'required': True
51
},
52
{
53
'name': 'task-definition',
54
'help_text': ("The file path where your task definition file is "
55
"located. The format of the file must be the same "
56
"as the JSON output of: <codeblock>aws ecs "
57
"register-task-definition "
58
"--generate-cli-skeleton</codeblock>"),
59
'required': True
60
},
61
{
62
'name': 'codedeploy-appspec',
63
'help_text': ("The file path where your AWS CodeDeploy appspec "
64
"file is located. The appspec file may be in JSON "
65
"or YAML format. The <code>TaskDefinition</code> "
66
"property will be updated within the appspec with "
67
"the newly registered task definition ARN, "
68
"overwriting any placeholder values in the file."),
69
'required': True
70
},
71
{
72
'name': 'cluster',
73
'help_text': ("The short name or full Amazon Resource Name "
74
"(ARN) of the cluster that your service is "
75
"running within. If you do not specify a "
76
"cluster, the \"default\" cluster is assumed."),
77
'required': False
78
},
79
{
80
'name': 'codedeploy-application',
81
'help_text': ("The name of the AWS CodeDeploy application "
82
"to use for the deployment. The specified "
83
"application must use the 'ECS' compute "
84
"platform. If you do not specify an "
85
"application, the application name "
86
"<code>AppECS-[CLUSTER_NAME]-[SERVICE_NAME]</code> "
87
"is assumed."),
88
'required': False
89
},
90
{
91
'name': 'codedeploy-deployment-group',
92
'help_text': ("The name of the AWS CodeDeploy deployment "
93
"group to use for the deployment. The "
94
"specified deployment group must be associated "
95
"with the specified ECS service and cluster. "
96
"If you do not specify a deployment group, "
97
"the deployment group name "
98
"<code>DgpECS-[CLUSTER_NAME]-[SERVICE_NAME]</code> "
99
"is assumed."),
100
'required': False
101
}
102
]
103
104
MSG_TASK_DEF_REGISTERED = \
105
"Successfully registered new ECS task definition {arn}\n"
106
107
MSG_CREATED_DEPLOYMENT = "Successfully created deployment {id}\n"
108
109
MSG_SUCCESS = ("Successfully deployed {task_def} to "
110
"service '{service}'\n")
111
112
USER_AGENT_EXTRA = 'customization/ecs-deploy'
113
114
def _run_main(self, parsed_args, parsed_globals):
115
116
register_task_def_kwargs, appspec_obj = \
117
self._load_file_args(parsed_args.task_definition,
118
parsed_args.codedeploy_appspec)
119
120
ecs_client_wrapper = ECSClient(
121
self._session, parsed_args, parsed_globals, self.USER_AGENT_EXTRA)
122
123
self.resources = self._get_resource_names(
124
parsed_args, ecs_client_wrapper)
125
126
codedeploy_client = self._session.create_client(
127
'codedeploy',
128
region_name=parsed_globals.region,
129
verify=parsed_globals.verify_ssl,
130
config=config.Config(user_agent_extra=self.USER_AGENT_EXTRA))
131
132
self._validate_code_deploy_resources(codedeploy_client)
133
134
self.wait_time = self._cd_validator.get_deployment_wait_time()
135
136
self.task_def_arn = self._register_task_def(
137
register_task_def_kwargs, ecs_client_wrapper)
138
139
self._create_and_wait_for_deployment(codedeploy_client, appspec_obj)
140
141
def _create_and_wait_for_deployment(self, client, appspec):
142
deployer = CodeDeployer(client, appspec)
143
deployer.update_task_def_arn(self.task_def_arn)
144
deployment_id = deployer.create_deployment(
145
self.resources['app_name'],
146
self.resources['deployment_group_name'])
147
148
sys.stdout.write(self.MSG_CREATED_DEPLOYMENT.format(
149
id=deployment_id))
150
151
deployer.wait_for_deploy_success(deployment_id, self.wait_time)
152
service_name = self.resources['service']
153
154
sys.stdout.write(
155
self.MSG_SUCCESS.format(
156
task_def=self.task_def_arn, service=service_name))
157
sys.stdout.flush()
158
159
def _get_file_contents(self, file_path):
160
full_path = os.path.expandvars(os.path.expanduser(file_path))
161
try:
162
with compat_open(full_path) as f:
163
return f.read()
164
except (OSError, IOError, UnicodeDecodeError) as e:
165
raise exceptions.FileLoadError(
166
file_path=file_path, error=e)
167
168
def _get_resource_names(self, args, ecs_client):
169
service_details = ecs_client.get_service_details()
170
service_name = service_details['service_name']
171
cluster_name = service_details['cluster_name']
172
173
application_name = filehelpers.get_app_name(
174
service_name, cluster_name, args.codedeploy_application)
175
deployment_group_name = filehelpers.get_deploy_group_name(
176
service_name, cluster_name, args.codedeploy_deployment_group)
177
178
return {
179
'service': service_name,
180
'service_arn': service_details['service_arn'],
181
'cluster': cluster_name,
182
'cluster_arn': service_details['cluster_arn'],
183
'app_name': application_name,
184
'deployment_group_name': deployment_group_name
185
}
186
187
def _load_file_args(self, task_def_arg, appspec_arg):
188
task_def_string = self._get_file_contents(task_def_arg)
189
register_task_def_kwargs = json.loads(task_def_string)
190
191
appspec_string = self._get_file_contents(appspec_arg)
192
appspec_obj = filehelpers.parse_appspec(appspec_string)
193
194
return register_task_def_kwargs, appspec_obj
195
196
def _register_task_def(self, task_def_kwargs, ecs_client):
197
response = ecs_client.register_task_definition(task_def_kwargs)
198
199
task_def_arn = response['taskDefinition']['taskDefinitionArn']
200
201
sys.stdout.write(self.MSG_TASK_DEF_REGISTERED.format(
202
arn=task_def_arn))
203
sys.stdout.flush()
204
205
return task_def_arn
206
207
def _validate_code_deploy_resources(self, client):
208
validator = CodeDeployValidator(client, self.resources)
209
validator.describe_cd_resources()
210
validator.validate_all()
211
self._cd_validator = validator
212
213
214
class CodeDeployer():
215
216
MSG_WAITING = ("Waiting for {deployment_id} to succeed "
217
"(will wait up to {wait} minutes)...\n")
218
219
def __init__(self, cd_client, appspec_dict):
220
self._client = cd_client
221
self._appspec_dict = appspec_dict
222
223
def create_deployment(self, app_name, deploy_grp_name):
224
request_obj = self._get_create_deploy_request(
225
app_name, deploy_grp_name)
226
227
try:
228
response = self._client.create_deployment(**request_obj)
229
except ClientError as e:
230
raise exceptions.ServiceClientError(
231
action='create deployment', error=e)
232
233
return response['deploymentId']
234
235
def _get_appspec_hash(self):
236
appspec_str = json.dumps(self._appspec_dict)
237
appspec_encoded = compat.ensure_bytes(appspec_str)
238
return hashlib.sha256(appspec_encoded).hexdigest()
239
240
def _get_create_deploy_request(self, app_name, deploy_grp_name):
241
return {
242
"applicationName": app_name,
243
"deploymentGroupName": deploy_grp_name,
244
"revision": {
245
"revisionType": "AppSpecContent",
246
"appSpecContent": {
247
"content": json.dumps(self._appspec_dict),
248
"sha256": self._get_appspec_hash()
249
}
250
}
251
}
252
253
def update_task_def_arn(self, new_arn):
254
"""
255
Inserts the ARN of the previously created ECS task definition
256
into the provided appspec.
257
258
Expected format of ECS appspec (YAML) is:
259
version: 0.0
260
resources:
261
- <service-name>:
262
type: AWS::ECS::Service
263
properties:
264
taskDefinition: <value> # replace this
265
loadBalancerInfo:
266
containerName: <value>
267
containerPort: <value>
268
"""
269
appspec_obj = self._appspec_dict
270
271
resources_key = filehelpers.find_required_key(
272
'codedeploy-appspec', appspec_obj, 'resources')
273
updated_resources = []
274
275
# 'resources' is a list of string:obj dictionaries
276
for resource in appspec_obj[resources_key]:
277
for name in resource:
278
# get content of resource
279
resource_content = resource[name]
280
# get resource properties
281
properties_key = filehelpers.find_required_key(
282
name, resource_content, 'properties')
283
properties_content = resource_content[properties_key]
284
# find task definition property
285
task_def_key = filehelpers.find_required_key(
286
properties_key, properties_content, 'taskDefinition')
287
288
# insert new task def ARN into resource
289
properties_content[task_def_key] = new_arn
290
291
updated_resources.append(resource)
292
293
appspec_obj[resources_key] = updated_resources
294
self._appspec_dict = appspec_obj
295
296
def wait_for_deploy_success(self, id, wait_min):
297
waiter = self._client.get_waiter("deployment_successful")
298
299
if wait_min is not None and wait_min > MAX_WAIT_MIN:
300
wait_min = MAX_WAIT_MIN
301
302
elif wait_min is None or wait_min < 30:
303
wait_min = 30
304
305
delay_sec = DEFAULT_DELAY_SEC
306
max_attempts = (wait_min * 60) / delay_sec
307
config = {
308
'Delay': delay_sec,
309
'MaxAttempts': max_attempts
310
}
311
312
self._show_deploy_wait_msg(id, wait_min)
313
waiter.wait(deploymentId=id, WaiterConfig=config)
314
315
def _show_deploy_wait_msg(self, id, wait_min):
316
sys.stdout.write(
317
self.MSG_WAITING.format(deployment_id=id,
318
wait=wait_min))
319
sys.stdout.flush()
320
321
322
class CodeDeployValidator():
323
def __init__(self, cd_client, resources):
324
self._client = cd_client
325
self._resource_names = resources
326
327
def describe_cd_resources(self):
328
try:
329
self.app_details = self._client.get_application(
330
applicationName=self._resource_names['app_name'])
331
except ClientError as e:
332
raise exceptions.ServiceClientError(
333
action='describe Code Deploy application', error=e)
334
335
try:
336
dgp = self._resource_names['deployment_group_name']
337
app = self._resource_names['app_name']
338
self.deployment_group_details = self._client.get_deployment_group(
339
applicationName=app, deploymentGroupName=dgp)
340
except ClientError as e:
341
raise exceptions.ServiceClientError(
342
action='describe Code Deploy deployment group', error=e)
343
344
def get_deployment_wait_time(self):
345
346
if (not hasattr(self, 'deployment_group_details') or
347
self.deployment_group_details is None):
348
return None
349
else:
350
dgp_info = self.deployment_group_details['deploymentGroupInfo']
351
blue_green_info = dgp_info['blueGreenDeploymentConfiguration']
352
353
deploy_ready_wait_min = \
354
blue_green_info['deploymentReadyOption']['waitTimeInMinutes']
355
356
terminate_key = 'terminateBlueInstancesOnDeploymentSuccess'
357
termination_wait_min = \
358
blue_green_info[terminate_key]['terminationWaitTimeInMinutes']
359
360
configured_wait = deploy_ready_wait_min + termination_wait_min
361
362
return configured_wait + TIMEOUT_BUFFER_MIN
363
364
def validate_all(self):
365
self.validate_application()
366
self.validate_deployment_group()
367
368
def validate_application(self):
369
app_name = self._resource_names['app_name']
370
if self.app_details['application']['computePlatform'] != 'ECS':
371
raise exceptions.InvalidPlatformError(
372
resource='Application', name=app_name)
373
374
def validate_deployment_group(self):
375
dgp = self._resource_names['deployment_group_name']
376
service = self._resource_names['service']
377
service_arn = self._resource_names['service_arn']
378
cluster = self._resource_names['cluster']
379
cluster_arn = self._resource_names['cluster_arn']
380
381
grp_info = self.deployment_group_details['deploymentGroupInfo']
382
compute_platform = grp_info['computePlatform']
383
384
if compute_platform != 'ECS':
385
raise exceptions.InvalidPlatformError(
386
resource='Deployment Group', name=dgp)
387
388
target_services = \
389
self.deployment_group_details['deploymentGroupInfo']['ecsServices']
390
391
# either ECS resource names or ARNs can be stored, so check both
392
for target in target_services:
393
target_serv = target['serviceName']
394
if target_serv != service and target_serv != service_arn:
395
raise exceptions.InvalidProperyError(
396
dg_name=dgp, resource='service', resource_name=service)
397
398
target_cluster = target['clusterName']
399
if target_cluster != cluster and target_cluster != cluster_arn:
400
raise exceptions.InvalidProperyError(
401
dg_name=dgp, resource='cluster', resource_name=cluster)
402
403
404
class ECSClient():
405
406
def __init__(self, session, parsed_args, parsed_globals, user_agent_extra):
407
self._args = parsed_args
408
self._custom_config = config.Config(user_agent_extra=user_agent_extra)
409
self._client = session.create_client(
410
'ecs',
411
region_name=parsed_globals.region,
412
endpoint_url=parsed_globals.endpoint_url,
413
verify=parsed_globals.verify_ssl,
414
config=self._custom_config)
415
416
def get_service_details(self):
417
cluster = self._args.cluster
418
419
if cluster is None or '':
420
cluster = 'default'
421
422
try:
423
service_response = self._client.describe_services(
424
cluster=cluster, services=[self._args.service])
425
except ClientError as e:
426
raise exceptions.ServiceClientError(
427
action='describe ECS service', error=e)
428
429
if len(service_response['services']) == 0:
430
raise exceptions.InvalidServiceError(
431
service=self._args.service, cluster=cluster)
432
433
service_details = service_response['services'][0]
434
cluster_name = \
435
filehelpers.get_cluster_name_from_arn(
436
service_details['clusterArn'])
437
438
return {
439
'service_arn': service_details['serviceArn'],
440
'service_name': service_details['serviceName'],
441
'cluster_arn': service_details['clusterArn'],
442
'cluster_name': cluster_name
443
}
444
445
def register_task_definition(self, kwargs):
446
try:
447
response = \
448
self._client.register_task_definition(**kwargs)
449
except ClientError as e:
450
raise exceptions.ServiceClientError(
451
action='register ECS task definition', error=e)
452
453
return response
454
455