· 9 years ago · Sep 21, 2016, 10:24 PM
1#!/usr/bin/env bash
2set -e
3
4function usage() {
5 set -e
6 cat <<EOM
7 ##### ecs-deploy #####
8 Simple script for triggering blue/green deployments on Amazon Elastic Container Service
9 https://github.com/silinternational/ecs-deploy
10
11 One of the following is required:
12 -n | --service-name Name of service to deploy
13 -d | --task-definition Name of task definition to deploy
14
15 Required arguments:
16 -k | --aws-access-key AWS Access Key ID. May also be set as environment variable AWS_ACCESS_KEY_ID
17 -s | --aws-secret-key AWS Secret Access Key. May also be set as environment variable AWS_SECRET_ACCESS_KEY
18 -r | --region AWS Region Name. May also be set as environment variable AWS_DEFAULT_REGION
19 -p | --profile AWS Profile to use - If you set this aws-access-key, aws-secret-key and region are needed
20 -c | --cluster Name of ECS cluster
21 -i | --image Name of Docker image to run, ex: repo/image:latest
22 Format: [domain][:port][/repo][/][image][:tag]
23 Examples: mariadb, mariadb:latest, silintl/mariadb,
24 silintl/mariadb:latest, private.registry.com:8000/repo/image:tag
25 --aws-instance-profile Use the IAM role associated with this instance
26
27 Optional arguments:
28 -m | --min minumumHealthyPercent: The lower limit on the number of running tasks during a deployment.
29 -M | --max maximumPercent: The upper limit on the number of running tasks during a deployment.
30 -t | --timeout Default is 90s. Script monitors ECS Service for new task definition to be running.
31 -e | --tag-env-var Get image tag name from environment variable. If provided this will override value specified in image name argument.
32 -v | --verbose Verbose output
33
34 Examples:
35 Simple deployment of a service (Using env vars for AWS settings):
36
37 ecs-deploy -c production1 -n doorman-service -i docker.repo.com/doorman:latest
38
39 All options:
40
41 ecs-deploy -k ABC123 -s SECRETKEY -r us-east-1 -c production1 -n doorman-service -i docker.repo.com/doorman -t 240 -e CI_TIMESTAMP -v
42
43 Updating a task definition with a new image:
44
45 ecs-deploy -d open-door-task -i docker.repo.com/doorman:17
46
47 Using profiles (for STS delegated credentials, for instance):
48
49 ecs-deploy -p PROFILE -c production1 -n doorman-service -i docker.repo.com/doorman -t 240 -e CI_TIMESTAMP -v
50
51 Notes:
52 - If a tag is not found in image and an ENV var is not used via -e, it will default the tag to "latest"
53EOM
54
55 exit 2
56}
57if [ $# == 0 ]; then usage; fi
58
59# Setup default values for variables
60CLUSTER=false
61SERVICE=false
62TASK_DEFINITION=false
63IMAGE=false
64MIN=false
65MAX=false
66TIMEOUT=90
67VERBOSE=false
68TAGVAR=false
69AWS_CLI=$(which aws)
70AWS_ECS="$AWS_CLI --output json ecs"
71
72# Loop through arguments, two at a time for key and value
73while [[ $# > 0 ]]
74do
75 key="$1"
76
77 case $key in
78 -k|--aws-access-key)
79 AWS_ACCESS_KEY_ID="$2"
80 shift # past argument
81 ;;
82 -s|--aws-secret-key)
83 AWS_SECRET_ACCESS_KEY="$2"
84 shift # past argument
85 ;;
86 -r|--region)
87 AWS_DEFAULT_REGION="$2"
88 shift # past argument
89 ;;
90 -p|--profile)
91 AWS_PROFILE="$2"
92 shift # past argument
93 ;;
94 --aws-instance-profile)
95 AWS_IAM_ROLE=true
96 ;;
97 -c|--cluster)
98 CLUSTER="$2"
99 shift # past argument
100 ;;
101 -n|--service-name)
102 SERVICE="$2"
103 shift # past argument
104 ;;
105 -d|--task-definition)
106 TASK_DEFINITION="$2"
107 shift
108 ;;
109 -i|--image)
110 IMAGE="$2"
111 shift
112 ;;
113 -t|--timeout)
114 TIMEOUT="$2"
115 shift
116 ;;
117 -m|--min)
118 MIN="$2"
119 shift
120 ;;
121 -M|--max)
122 MAX="$2"
123 shift
124 ;;
125 -e|--tag-env-var)
126 TAGVAR="$2"
127 shift
128 ;;
129 -v|--verbose)
130 VERBOSE=true
131 ;;
132 *)
133 usage
134 exit 2
135 ;;
136 esac
137 shift # past argument or value
138done
139
140# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION and AWS_PROFILE can be set as environment variables
141if [ -z ${AWS_ACCESS_KEY_ID+x} ]; then unset AWS_ACCESS_KEY_ID; fi
142if [ -z ${AWS_SECRET_ACCESS_KEY+x} ]; then unset AWS_SECRET_ACCESS_KEY; fi
143if [ -z ${AWS_DEFAULT_REGION+x} ];
144 then unset AWS_DEFAULT_REGION
145 else
146 AWS_ECS="$AWS_ECS --region $AWS_DEFAULT_REGION"
147fi
148if [ -z ${AWS_PROFILE+x} ];
149 then unset AWS_PROFILE
150 else
151 AWS_ECS="$AWS_ECS --profile $AWS_PROFILE"
152fi
153
154if [ $VERBOSE == true ]; then
155 set -x
156fi
157
158# Make sure we have all the variables needed: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, CLUSTER, SERVICE, IMAGE
159if [ -z $AWS_IAM_ROLE ] && [ -z $AWS_ACCESS_KEY_ID ] && [ -z $AWS_PROFILE ]; then
160 echo "AWS_ACCESS_KEY_ID is required. You can set it as an environment variable or pass the value using -k or --aws-access-key"
161 exit 1
162fi
163if [ -z $AWS_IAM_ROLE ] && [ -z $AWS_SECRET_ACCESS_KEY ] && [ -z $AWS_PROFILE ]; then
164 echo "AWS_SECRET_ACCESS_KEY is required. You can set it as an environment variable or pass the value using -s or --aws-secret-key"
165 exit 1
166fi
167if [ -z $AWS_IAM_ROLE ] && [ -z $AWS_DEFAULT_REGION ] && [ -z $AWS_PROFILE ]; then
168 echo "AWS_DEFAULT_REGION is required. You can set it as an environment variable or pass the value using -r or --region"
169 exit 1
170fi
171if [ $SERVICE == false ] && [ $TASK_DEFINITION == false ]; then
172 echo "One of SERVICE or TASK DEFINITON is required. You can pass the value using -n / --service-name for a service, or -d / --task-definiton for a task"
173 exit 1
174fi
175if [ $SERVICE != false ] && [ $TASK_DEFINITION != false ]; then
176 echo "Only one of SERVICE or TASK DEFINITON may be specified, but you supplied both"
177 exit 1
178fi
179if [ $SERVICE != false ] && [ $CLUSTER == false ]; then
180 echo "CLUSTER is required. You can pass the value using -c or --cluster"
181 exit 1
182fi
183if [ $IMAGE == false ]; then
184 echo "IMAGE is required. You can pass the value using -i or --image"
185 exit 1
186fi
187
188# Define regex for image name
189# This regex will create groups for:
190# - domain
191# - port
192# - repo
193# - image
194# - tag
195# If a group is missing it will be an empty string
196imageRegex="^([a-zA-Z0-9.\-]+):?([0-9]+)?/([a-zA-Z0-9_-]+)/?([a-zA-Z0-9_-]+)?:?([a-zA-Z0-9\._-]+)?$"
197
198if [[ $IMAGE =~ $imageRegex ]]; then
199 # Define variables from matching groups
200 domain=${BASH_REMATCH[1]}
201 port=${BASH_REMATCH[2]}
202 repo=${BASH_REMATCH[3]}
203 img=${BASH_REMATCH[4]}
204 tag=${BASH_REMATCH[5]}
205
206 # Validate what we received to make sure we have the pieces needed
207 if [[ "x$domain" == "x" ]]; then
208 echo "Image name does not contain a domain or repo as expected. See usage for supported formats." && exit 1;
209 fi
210 if [[ "x$repo" == "x" ]]; then
211 echo "Image name is missing the actual image name. See usage for supported formats." && exit 1;
212 fi
213
214 # When a match for image is not found, the image name was picked up by the repo group, so reset variables
215 if [[ "x$img" == "x" ]]; then
216 img=$repo
217 repo=""
218 fi
219
220else
221 # check if using root level repo with format like mariadb or mariadb:latest
222 rootRepoRegex="^([a-zA-Z0-9\-]+):?([a-zA-Z0-9\.\-]+)?$"
223 if [[ $IMAGE =~ $rootRepoRegex ]]; then
224 img=${BASH_REMATCH[1]}
225 if [[ "x$img" == "x" ]]; then
226 echo "Invalid image name. See usage for supported formats." && exit 1
227 fi
228 tag=${BASH_REMATCH[2]}
229 else
230 echo "Unable to parse image name: $IMAGE, check the format and try again" && exit 1
231 fi
232fi
233
234# If tag is missing make sure we can get it from env var, or use latest as default
235if [[ "x$tag" == "x" ]]; then
236 if [[ $TAGVAR == false ]]; then
237 tag="latest"
238 else
239 tag=${!TAGVAR}
240 if [[ "x$tag" == "x" ]]; then
241 tag="latest"
242 fi
243 fi
244fi
245
246# Reassemble image name
247useImage=""
248if [[ "x$domain" != "x" ]]; then
249 useImage="$domain"
250fi
251if [[ "x$port" != "x" ]]; then
252 useImage="$useImage:$port"
253fi
254if [[ "x$repo" != "x" ]]; then
255 useImage="$useImage/$repo"
256fi
257if [[ "x$img" != "x" ]]; then
258 if [[ "x$useImage" == "x" ]]; then
259 useImage="$img"
260 else
261 useImage="$useImage/$img"
262 fi
263fi
264imageWithoutTag="$useImage"
265if [[ "x$tag" != "x" ]]; then
266 useImage="$useImage:$tag"
267fi
268
269echo "Using image name: $useImage"
270
271if [ $SERVICE != false ]; then
272 # Get current task definition name from service
273 TASK_DEFINITION=`$AWS_ECS describe-services --services $SERVICE --cluster $CLUSTER | jq -r .services[0].taskDefinition`
274fi
275
276echo "Current task definition: $TASK_DEFINITION";
277
278# Get a JSON representation of the current task definition
279# + Update definition to use new image name
280# + Filter the def
281DEF=$( $AWS_ECS describe-task-definition --task-def $TASK_DEFINITION \
282 | sed -e "s|\"image\": \"${imageWithoutTag}.*\"|\"image\": \"${useImage}\"|" \
283 | jq '.taskDefinition|{family: .family, volumes: .volumes, containerDefinitions: .containerDefinitions}' )
284
285# Register the new task definition, and store its ARN
286NEW_TASKDEF=`$AWS_ECS register-task-definition --cli-input-json "$DEF" | jq -r .taskDefinition.taskDefinitionArn`
287echo "New task definition: $NEW_TASKDEF";
288
289if [ $SERVICE == false ]; then
290 echo "Task definition updated successfully"
291else
292 DEPLOYMENT_CONFIG=""
293 if [ $MAX != false ]; then
294 DEPLOYMENT_CONFIG=",maximumPercent=$MAX"
295 fi
296 if [ $MIN != false ]; then
297 DEPLOYMENT_CONFIG="$DEPLOYMENT_CONFIG,minimumHealthyPercent=$MIN"
298 fi
299 if [ ! -z "$DEPLOYMENT_CONFIG" ]; then
300 DEPLOYMENT_CONFIG="--deployment-configuration ${DEPLOYMENT_CONFIG:1}"
301 fi
302
303 # Update the service
304 UPDATE=`$AWS_ECS update-service --cluster $CLUSTER --service $SERVICE --task-definition $NEW_TASKDEF $DEPLOYMENT_CONFIG`
305
306 # See if the service is able to come up again
307 every=10
308 i=0
309 while [ $i -lt $TIMEOUT ]
310 do
311 # Scan the list of running tasks for that service, and see if one of them is the
312 # new version of the task definition
313
314 RUNNING=$($AWS_ECS list-tasks --cluster $CLUSTER --service-name $SERVICE --desired-status RUNNING \
315 | jq -r '.taskArns[]' \
316 | xargs -I{} $AWS_ECS describe-tasks --cluster $CLUSTER --tasks {} \
317 | jq ".tasks[]| if .taskDefinitionArn == \"$NEW_TASKDEF\" then . else empty end|.lastStatus" \
318 | grep -e "RUNNING" || : )
319
320 if [ "$RUNNING" ]; then
321 echo "Service updated successfully, new task definition running.";
322 exit 0
323 fi
324
325 sleep $every
326 i=$(( $i + $every ))
327 done
328
329 # Timeout
330 echo "ERROR: New task definition not running within $TIMEOUT seconds"
331 exit 1
332fi