#!/usr/bin/env bash ### # The script uses CFN to provision a Target Group in the app-dev ALB, and creates # an instance behind that target group, after which it attaches two host-header # based rules to the HTTP/HTTPS listeners which target the newly created target group. # After the stack is up, it installs the relevant Strigo app, ssc and job-worker builds # on it for the relevant branch. # # NOTE: AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set prior to running this. ### set -euo pipefail declare -r APP_DEV_LISTENER_ARN='arn:aws:elasticloadbalancing:...' function log() { message="$1" echo -e "\e[32m## ${message}\e[0m" >&2 } function error_exit() { ####################################### # Print a title in red # TODO: echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2 # Arguments: # The message to be printed ####################################### local message="$1" echo "\e[91m${message}" >&2 exit 1 } function install_python_dependency() { ####################################### # Install dependencies required for stack provisioning ####################################### local package="$1" if ! is_python_package_installed ${package}; then log "Installing ${package}..." sudo pip install ${package} --upgrade fi } function is_python_package_installed() { local package_name="$1" log "Verifying ${package_name} is installed..." if pip freeze | grep ${package_name} >/dev/null; then return 0 fi return 1 } function get_alb_listener_rule_priority() { ####################################### # Get the required priority (1, 2, etc..) for the rule. # Each rule must be given a priority when it is created. ####################################### log "Retrieving listener rule priority..." local priority=$(aws elbv2 describe-rules --region ${AWS_DEFAULT_REGION} --listener-arn ${APP_DEV_LISTENER_ARN} | jq '[.Rules[].Priority][:-1]' | jq '[.[] | tonumber] | max + 1') log "Priority for ${APP_DEV_LISTENER_ARN} is: ${priority}" echo ${priority} } function is_stack_exists() { ####################################### # Check whether the stack `stack_name` exists. # Note that this will not check if the stack exists and was created successfully. ####################################### local stack_name="$1" if aws cloudformation describe-stacks | jq ".Stacks[].StackName | contains(\"${stack_name}\")" | grep "true" >/dev/null; then log "Stack already exists" return 0 fi log "Stack does not exist" return 1 } function create_stack() { ####################################### # Idempotently create a stack ####################################### local branch="$1" local priority="$2" log "Creating stack for branch: ${branch}..." aws cloudformation create-stack --region ${AWS_DEFAULT_REGION} --stack-name "${branch}" --template-body "$(cat dev/cfn.yaml)" --parameters ParameterKey=Branch,ParameterValue="${branch}" ParameterKey=LoadBalancerPriority,ParameterValue=$priority log "Waiting for creation to complete..." aws cloudformation wait stack-create-complete --region ${AWS_DEFAULT_REGION} --stack-name "${branch}" log "Done" } function delete_stack() { ####################################### # Idempotently create a stack ####################################### local branch="$1" log "Deleting stack for branch: ${branch}..." aws cloudformation delete-stack --region ${AWS_DEFAULT_REGION} --stack-name "${branch}" log "Waiting for deletion to complete..." aws cloudformation wait stack-delete-complete --region ${AWS_DEFAULT_REGION} --stack-name "${branch}" log "Done" } function delete_resources() { ####################################### # Idempotently create a stack ####################################### local branch="$1" # Making sure we don't accidentally pass an empty branch name in which case # aws s3 rm will delete the entire bucket's contents. if ! is_var_set ${branch}; then return fi log "Deleting resources for branch: ${branch}..." aws s3 rm --region ${AWS_DEFAULT_REGION} s3://strigo-deploy-dev/${branch} --recursive log "Done" } function get_instance_id() { local branch="$1" log "Retrieving instance id for stack: ${branch}..." local instance_id=$(aws cloudformation describe-stack-resources --region ${AWS_DEFAULT_REGION} --stack-name "${branch}" | jq -r .StackResources[0].PhysicalResourceId) log "Instance ID is: ${instance_id}" echo ${instance_id} } function get_instance_ip() { local intsance_id="$1" log "Retrieving instance ip for instance id: ${instance_id}..." local instance_ip=$(aws ec2 describe-instances --region ${AWS_DEFAULT_REGION} --instance-id ${instance_id} | jq -r .Reservations[].Instances[].PrivateIpAddress) log "Instance IP is: ${instance_ip}" echo ${instance_ip} } function is_dir() { local dir="$1" if [[ -d ${dir} ]]; then return 0 fi return 1 } function is_var_set() { local var="$1" if [[ -z ${var} || -R ${var} ]]; then return 1 fi return 0 } function edit_hosts_file() { ####################################### # Place instance ip in hosts file # Used by ansible to provision the machine ####################################### local instance_ip="$1" local hosts_file_path="$2" log "Editing hosts file: ${hosts_file_path}..." sed -i "s/{{INSTANCE_IP}}/${instance_ip}/g" ${hosts_file_path} log "Hosts file is:" echo "$(cat ${hosts_file_path})" >&2 } function clone_ansible_source() { ####################################### # Clone or pull strigo-ops repo. ####################################### local branch="$1" local repo_name="strigo-ops" local repo_slug="strigo/${repo_name}" local clone=false if ! is_dir ${repo_name}; then clone=true log "Cloning ${repo_slug}..." git clone git@github.com:${repo_slug}.git fi pushd ${repo_name} >/dev/null if is_var_set ${branch}; then log "Checking out ${repo_slug}/${branch}..." git checkout ${branch} fi if ! [ ${clone} = true ]; then log "Pulling branch..." git pull fi popd >/dev/null } function set_ssh_key() { ####################################### # Write and add strigo's ssh key to the agent # for ansible to be able to provision the instance ####################################### local key="$1" local key_file="${2:-private_key}" log "Writing instance ssh key to: ${key_file}..." echo -e "$key" > ${key_file} chmod 600 ${key_file} log "Adding instance ssh key agent..." ssh-add ${key_file} ssh-add -l } function ansible() { ####################################### # Deploy strigo-app, strigo-job-worker and strigo-ssc # On the provisioned instance (via ansible) ####################################### local working_dir="$1" local branch="$2" local hosts_file="$3" local strigo_app_version="$4" local stack_created_now="$5" local strigo_ssc_version="${6:-0.0.5}" local strigo_job_worker_version="${7:-0.0.3}" local vars="hosts=strigo-instance env=dev branch=\"${branch}\" aws_access_key=\"${AWS_ACCESS_KEY_ID}\" aws_secret_key=\"${AWS_SECRET_ACCESS_KEY}\"" local args="-i ${hosts_file} -vv" if ${stack_created_now}; then local strigo_app_tags='--skip-tags datadog' local ansible_tags='--skip-tags common,install_python,datadog' else local strigo_app_tags='--tags deploy' local ansible_tags='--tags deploy' fi pushd ${working_dir} >/dev/null log "Running ansible..." log "args: ${args}" log "vars: ${vars}" log "Hosts file: ${hosts_file}" log "Branch: ${branch}" log "strigo-app version: ${strigo_app_version}" ansible-playbook ${args} strigo-app.yml -e "${vars} deb_dir=${branch}/strigo-app" -e "{'strigo_app': {'version':'$strigo_app_version'}}" ${strigo_app_tags} ansible-playbook ${args} strigo-job-worker.yml -e "${vars}" -e "{'strigo_job_worker': {'version':'$strigo_job_worker_version'}}" ${ansible_tags} ansible-playbook ${args} strigo-ssc.yml -e "${vars}" -e "{'strigo_ssc': {'version':'$strigo_ssc_version'}}" ${ansible_tags} ansible-playbook ${args} strigo-mongo-dev.yml -e "${vars}" ${ansible_tags} popd >/dev/null } function assert_required_vars_set() { local required_unset=false # TODO: Nicefy if ! is_var_set ${AWS_ACCESS_KEY_ID}; then echo "AWS_ACCESS_KEY_ID must be set!" required_unset=true fi if ! is_var_set ${AWS_SECRET_ACCESS_KEY}; then echo "AWS_SECRET_ACCESS_KEY must be set!" required_unset=true fi if ! is_var_set ${AWS_DEFAULT_REGION}; then echo "AWS_DEFAULT_REGION must be set!" required_unset=true fi if ${required_unset}; then error_exit "Some required environment variables are not set!" fi } function main() { # TODO: Add normal CLI local branch="$1" local instance_ssh_key="$2" local strigo_app_version="$3" local commit_message="$4" local strigo_ops_branch="${5:-master}" local hosts_file="${6:-dev/hosts}" assert_required_vars_set install_python_dependency "awscli" local stack_created_now=false local stack_exists=true if ! is_stack_exists "${branch}"; then stack_exists=false fi # Only want to create the stack if commit message is `make_env` and it doesn't exist. if [[ "${commit_message}" == 'make_env' ]] && ! ${stack_exists}; then log "Stack creation required." local priority=$(get_alb_listener_rule_priority) create_stack "${branch}" "${priority}" stack_created_now=true stack_exists=true elif [[ "${commit_message}" == 'destroy_env' ]] && ${stack_exists}; then log "Stack deletion required." delete_stack "${branch}" delete_resources "${branch}" exit 0 fi # Only want to try and deploy if the stack exists. if ${stack_exists}; then install_python_dependency "ansible==2.3.0.0" log "Deployment required." local instance_id=$(get_instance_id "${branch}") local instance_ip=$(get_instance_ip "${instance_id}") edit_hosts_file "${instance_ip}" "${hosts_file}" clone_ansible_source "${strigo_ops_branch}" set_ssh_key "${instance_ssh_key}" ansible "strigo-ops/ansible" "${branch}" "../../${hosts_file}" "${strigo_app_version}" "${stack_created_now}" else log "Skipping deploy." fi } main "$@"