Skip to content

Instantly share code, notes, and snippets.

@wbyoung
Last active September 12, 2017 02:26
Show Gist options
  • Save wbyoung/20a39f508c813362bcb7e5a724c6f352 to your computer and use it in GitHub Desktop.
Save wbyoung/20a39f508c813362bcb7e5a724c6f352 to your computer and use it in GitHub Desktop.
#!/bin/bash
set -e
JQ="jq --raw-output --exit-status"
escape-json() {
python -c "import sys; import json; sys.stdout.write(json.dumps(sys.stdin.read().strip())[1:-1])"
}
escape-subst() {
# adapted to read from stdin from: https://stackoverflow.com/a/29613573/98069
IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g')
printf %s "${REPLY%$'\n'}"
}
# @env account_id
# @env region
# @env repository
# @env commit_sha
# @env service
# @returns via stderr
container-tag() {
echo "$account_id.dkr.ecr.$region.amazonaws.com/$repository:$service-${commit_sha:0:9}" >&2
}
# @arg tag
# @env region
# @env container
push-container() {
echo 'Logging in...'
eval $(aws ecr get-login --no-include-email --region "$region")
echo 'Pushing container...'
docker tag "$container" "$1"
docker push "$1"
}
# @arg tag
# @env account_id
# @env region
# @env commit_sha
# @env task_family
# @env task_definition_template
# @env config_keys
# @env config_value
# @returns via stderr
register-task() {
local escaped_tag=$(echo "$tag" | escape-subst)
local definition=$(
sed \
-e "s/\${IMAGE_TAG}/$escaped_tag/g" \
-e "s/\${AWS_ACCOUNT_ID}/$account_id/g" \
-e "s/\${COMMIT_SHA}/$commit_sha/g" \
"$task_definition_template"
)
for i in "${!config_keys[@]}"; do
local key="${config_keys[$i]}"
local value="${config_values[$i]}"
local escaped_value=$(echo "$value" | escape-json | escape-subst)
definition=$(
echo "$definition" | sed -e "s/\${$key}/$escaped_value/g"
)
done
local active_arns=$(
aws ecs list-task-definitions \
--output json \
--region "$region" \
--status ACTIVE \
--family-prefix "$task_family" |
$JQ '.taskDefinitionArns[]'
)
local arn=$(
aws ecs register-task-definition \
--output json \
--region "$region" \
--family "$task_family" \
--cli-input-json "$definition" |
$JQ '.taskDefinition.taskDefinitionArn'
)
# deregister all active task definitions
for revision in $active_arns; do
aws ecs deregister-task-definition \
--region "$region" \
--task-definition $revision > /dev/null
done
echo "$arn" >&2
echo "Revision: $arn"
}
# @arg arn
# @env region
# @env service
# @env ?service_update_timeout_exit_status
update-service() {
if [[ $(
aws ecs update-service \
--output json \
--region "$region" \
--cluster default \
--service "$service" \
--task-definition $1 |
$JQ '.service.taskDefinition'
) != $1 ]]; then
echo "Error updating service."
return 1
fi
for attempt in {1..30}; do
if stale=$(
aws ecs describe-services \
--output json \
--region "$region" \
--cluster default \
--services "$service" |
$JQ ".services[0].deployments | .[] | select(.taskDefinition != \"$1\") | .taskDefinition"); then
echo "Waiting for stale deployments:"
echo "$stale"
sleep 5
else
echo "Deployed!"
return 0
fi
done
echo "Service update took too long."
return "${service_update_timeout_exit_status-1}"
}
usage() {
if [ $# -gt 1 ]; then
echo $@
echo
fi
echo 'aws-deploy'
echo ' --account-id <value>'
echo ' --pre-deploy <value>'
echo ' --service <value>'
echo ' --task-family <value>'
echo ' --repository <value>'
echo ' --region <value>'
echo ' --container <value>'
echo ' --task-definition-template <value>'
echo ' [--config <key> <value>]'
echo
echo 'OPTIONS'
echo
echo ' --account-id (string)'
echo ' The AWS account ID'
echo
echo ' --pre-deploy (string)'
echo ' The name of a command to execute after the task definition has '
echo ' been created and before the service is updated/deployed.'
echo
echo ' --service (string)'
echo ' The name of the service'
echo
echo ' --task-family (string)'
echo ' The task family under which to register'
echo
echo ' --repository (string)'
echo ' The name of the repository to push to'
echo
echo ' --region (string)'
echo ' The region'
echo
echo ' --container (string)'
echo ' The container tag name to push'
echo
echo ' --task-definition-template (path) the task definition template'
echo ' The template may include the following placeholders in the style ${VAR}:'
echo
echo ' AWS_ACCOUNT_ID the account id'
echo ' COMMIT_SHA the git commit SHA'
echo ' IMAGE_TAG the generated image tag'
echo
echo ' Additionally, keys may be provided via the config option.'
echo
echo ' [--config <key> <value>]'
echo ' Add a substitution value for the template file, i.e.'
echo
echo ' aws-deploy --config MY_VAR true'
echo
echo ' Would replace occurrences of ${MY_VAR} with true in the template. This '
echo ' option can be repeated for multiple config variables.'
echo
echo ' [--soft-fail]'
echo ' Do not fail if service update takes too long. A log message will'
echo ' still be generated, but the exit code of the program will not be'
echo ' affected'
echo
}
aws-deploy() {
local config_keys=()
local config_values=()
while [ $# -ne 0 ]; do
case "$1" in
--account-id)
shift; local account_id="$1"
shift
;;
--pre-deploy)
shift; local pre_deploy="$1"
shift
;;
--service)
shift; local service="$1"
shift
;;
--repository)
shift; local repository="$1"
shift
;;
--task-family)
shift; local task_family="$1"
shift
;;
--region)
shift; local region="$1"
shift
;;
--container)
shift; local container="$1"
shift
;;
--task-definition-template)
shift; local task_definition_template="$1"
shift
;;
-c|--config)
shift; config_keys+=("$1")
shift; config_values+=("$1")
shift
;;
--soft-fail)
shift; local service_update_timeout_exit_status="0"
;;
-h|--help)
usage
exit 0
;;
*)
usage unknown option "$1"
exit 1
;;
esac
done
[ -n "$account_id" ] || { usage missing --account-id; exit 1; }
[ -n "$service" ] || { usage missing --service; exit 1; }
[ -n "$task_family" ] || { usage missing --task-family; exit 1; }
[ -n "$repository" ] || { usage missing --repository; exit 1; }
[ -n "$region" ] || { usage missing --region; exit 1; }
[ -n "$container" ] || { usage missing --container; exit 1; }
[ -n "$task_definition_template" ] || {
usage missing --task-definition-template; exit 1;
}
for key in "${config_keys[@]}"; do
[ -n "$key" ] || { usage missing --config key; exit 1; }
done
for value in "${config_values[@]}"; do
[ -n "$value" ] || { usage missing --config value; exit 1; }
done
local commit_sha=$(git rev-parse HEAD)
local tag=$(container-tag 3>&2 2>&1 1>&3)
push-container "$tag"
local arn=$(register-task "$tag" 3>&2 2>&1 1>&3)
if [ -n "$pre_deploy" ]; then
"$pre_deploy"
fi
update-service $arn
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
aws-deploy "$@"
fi
@wbyoung
Copy link
Author

wbyoung commented Jun 21, 2017

Example usage:

#!/bin/bash

set -e

AWS_DEPLOY="/tmp/aws-deploy.sh"
AWS_DEPLOY_URL="https://gist.githubusercontent.com/wbyoung/20a39f508c813362bcb7e5a724c6f352/raw/4ba98ce7333cb84fd59e42873c73a866a563a9b4/aws-deploy.sh"
AWS_DEPLOY_SHA1="b522289bade32dd9514d18ee2811e4bdeac011e936ad7fb383890a11b9ebe4f7"

curl --silent -o "$AWS_DEPLOY" "$AWS_DEPLOY_URL"
echo "$AWS_DEPLOY_SHA1 $AWS_DEPLOY" | gsha256sum -c - && . "$AWS_DEPLOY" && \
  aws-deploy \
    --account-id $AWS_ACCOUNT_ID \
    --service my_app \
    --task-family my_app \
    --repository namespace/my_app \
    --region us-east-2 \
    --container my_app:latest \
    --task-definition-template deployment/task-definition.json.tmpl \
    --config MY_APP_CONFIG "$MY_APP_CONFIG"
rm "$AWS_DEPLOY"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment