Skip to content

Instantly share code, notes, and snippets.

@stevenringo
Created September 11, 2017 00:13
Show Gist options
  • Save stevenringo/2aa8485706677c0a92320f17c1efb3a8 to your computer and use it in GitHub Desktop.
Save stevenringo/2aa8485706677c0a92320f17c1efb3a8 to your computer and use it in GitHub Desktop.

Revisions

  1. stevenringo created this gist Sep 11, 2017.
    209 changes: 209 additions & 0 deletions deploy_stack.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,209 @@
    set -o pipefail

    _exit_error() {
    message=$1
    code=$2
    echo "$message" >&2
    exit $code
    }

    _exit_ok() {
    message=$1
    echo "$message"
    exit 0
    }

    get-stack-state() {
    local stack_name=$1
    local region=$2
    local stack_state
    stack_state=$(aws cloudformation describe-stacks --region $region \
    --stack-name $stack_name \
    --query "Stacks[0].StackStatus" \
    --output text 2>&1)
    retcode="$?"
    if <<<"${stack_state}" grep -q "does not exist"; then
    _exit_ok DOES_NOT_EXIST
    fi
    if [ $retcode -ne 0 ]; then
    _exit_error "$stack_state" $retcode
    fi
    _exit_ok "$stack_state"
    }

    get-stack-action() {
    local stack_state=$1
    local stack_action
    case $stack_state in
    *_IN_PROGRESS) # busy
    _exit_error "Stack is currently in $stack_state state and cannot be updated" 127
    ;;
    *_FAILED) # errored
    _exit_error "Stack is currently in $stack_state state and cannot be updated" 127
    ;;
    ROLLBACK_COMPLETE) # ok
    stack_action="delete-then-create"
    ;;
    *_COMPLETE) # ok
    stack_action="update"
    ;;
    DOES_NOT_EXIST) # nope
    stack_action="create"
    ;;
    *) # unknown
    ;;
    esac
    echo "$stack_action"
    }

    upsert-stack() {
    local stack_required_action=$1
    local stack_name=$2
    local stack_template=$3
    local region=$4
    local upsert_result
    local retcode
    echo "Attempting to $stack_required_action stack: $stack_name..." >&2
    upsert_result=$(aws cloudformation ${stack_required_action}-stack --region $region \
    --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
    --stack-name $stack_name \
    --template-body file://./cloudformation/$stack_template.yml \
    --parameters file://./cloudformation/.tmp/cloudformation_parameters-$stack_template.json \
    --query StackId \
    --output text 2>&1)
    retcode="$?"
    if <<<"${upsert_result}" grep -q "No updates are to be performed"; then
    _exit_ok "No updates are to be performed"
    fi
    if [ $retcode -ne 0 ]; then
    _exit_error "$upsert_result" $retcode
    fi
    echo "$upsert_result"
    }

    tail_stack() {
    local stack_name=$1
    local region=$2
    local current
    local final_line
    local output
    local output_result
    local previous
    local stack_events
    local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    local headings="| Time | Resource | Status | Message |"
    local markline="+--------------+--------------------------------+--------------------------------+--------------------------------------------------------------+"
    local header="$markline\n$headings\n$markline\n"
    local colour_reset='\033[0m'
    local colour_red='\033[31m'
    local colour_green='\033[32m'
    local colour_yellow='\033[33m'
    local colour_blue='\033[34m'
    local colour_magenta='\033[35m'
    local colour_cyan='\033[36m'
    local colour_white='\033[97m'

    while true; do
    stack_events=$(aws cloudformation describe-stack-events --region $region \
    --stack-name $stack_name \
    --output text \
    --query "sort_by(StackEvents, &Timestamp)[?Timestamp>='$now'].[
    Timestamp,
    LogicalResourceId,
    ResourceStatus,
    ResourceStatusReason,
    ResourceType
    ]" 2>&1)
    retcode="$?"
    echo "$stack_events" | egrep -q "does not exist" && break
    if [ $retcode -ne 0 ]; then
    _exit_error "$stack_events" $retcode
    fi
    if [ -z "$stack_events" ]; then
    sleep 1
    continue
    fi
    stack_events_table=$(
    while IFS=$'\t' read timestamp resource status message rtype; do
    # ResourceType (rtype) not used, included for completeness
    printf "| %b%-12s%b | %b%-30s%b | %b%-30s%b | %b%-60s%b |\n" \
    "$colour_green" "${timestamp:11:8} UTC" "$colour_reset" \
    "$colour_red" "$(echo $resource | awk -v len=30 '{ if(length($0)>len) print "…" substr($0,length($0)-len+2,length($0)); else print;}')" "$colour_reset" \
    "$colour_cyan" "$(echo $status | awk -v len=30 '{ if(length($0)>len) print "…" substr($0,length($0)-len+2,length($0)); else print;}')" "$colour_reset" \
    "$colour_blue" "$(echo $message | awk -v len=60 '{ if(length($0)>len) print "…" substr($0,length($0)-len+2,length($0)); else print;}')" "$colour_reset"
    done <<< "$stack_events"
    )
    current=$(echo -e "$header$stack_events_table")
    if [ -z "$previous" ]; then
    echo "$current"
    elif [ "$current" != "$previous" ]; then
    comm -13 <(echo "$previous") <(echo "$current")
    fi
    previous="$current"
    stack_state=$(aws cloudformation describe-stacks --region $region \
    --stack-name $stack_name \
    --query "Stacks[0].StackStatus" \
    --output text 2>&1)
    retcode="$?"
    echo "$stack_state" | egrep -q ".*_(COMPLETE|FAILED)$" && break
    [ $retcode -ne 0 ] && break
    sleep 1
    done
    echo $markline
    }

    deploy_stack() {
    local application=$1
    local environment=$2
    local stack_template=$3
    local region=$4
    local stack_name
    local stack_required_action
    stack_name=$(echo "$application-$environment-$stack_template" | tr '_' '-')

    echo "Checking state of stack: $stack_name..."
    stack_state=$(get-stack-state $stack_name $region) || exit $?
    stack_action=$(get-stack-action $stack_state) || exit $?
    if [[ $stack_action == "delete-then-create" ]]; then
    echo "Stack exists in non-updatable state. Deleting..." >&2
    drop_stack $application $environment $stack_template $region
    stack_action="create"
    fi
    upsert_result=$(upsert-stack $stack_action $stack_name $stack_template $region) || exit $?
    if <<<"${upsert_result}" grep -q "No updates are to be performed"; then
    _exit_ok "$upsert_result"
    fi
    echo "Stack with id $upsert_result is being ${stack_action}d"
    tail_stack $stack_name $region
    }

    delete-stack() {
    local stack_name=$1
    local region=$2
    local delete_result
    local retcode
    echo "Attempting to delete stack: $stack_name..." >&2
    delete_result=$(aws cloudformation delete-stack --stack-name $stack_name --region $region 2>&1)
    retcode="$?"
    if [ $retcode -ne 0 ]; then
    _exit_error "$delete_result" $retcode
    fi
    # delete_result is empty if there is no error, so no return here
    }

    drop_stack() {
    local environment=$1
    local application=$2
    local stack_template=$3
    local region=$4
    local stack_name
    local stack_required_action
    stack_name=$(echo "$environment-$application-$stack_template" | tr '_' '-')
    echo "Checking state of stack: $stack_name..."
    stack_state=$(get-stack-state $stack_name $region) || exit $?
    if <<<"${stack_state}" egrep -q "DOES_NOT_EXIST"; then
    _exit_error "The stack $stack_name does not exist"
    fi
    delete_result=$(delete-stack $stack_name $region) || exit $?
    tail_stack $stack_name $region
    }