AWS : automatisierter shutdown der aws instanzen

Problem : Man hat ein paar Instanzen in der AWS installiert, diese werden aber nur für die Entwicklung bzw. Tests benötigt. Um zusätzliche Kosten zu sparen kann man diese automatisiert außerhalb der benötigten Zeiten herunterfahren.

Lösung : Man lässt dieses Skript auf einer Maschine/Instanz laufen auf der die aws cli konfiguriert ist. Das Beispiel hier läuft selbst eine AWS Instanz. Aus diesem Grund wird mit
OWN_INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
die eigene ID ermittelt.
Das Skript fährt alle Instanzen herunter außer diese die in DO_NOT_SHUTDOWN definiert sind.

#!/bin/bash
# ----------------------------------
set -o nounset
set -o pipefail
# ----------------------------------
#
# Script fährt alle Instanzen herunter bis auf die unter DO_NOT_SHUTDOWN definierten
# die eigene Maschine bleibt natürlich auch online
# PFAD zu awscli
AWS_PATH="/root/.local/bin/"
OWN_INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
# -----------------------------------------------------------
# Definieren der Maschinen die NICHT heruntergefahren werden
# -----------------------------------------------------------
# wenn die eigene Instanz geschützt werden soll diese mit ins Array aufnehmen. Nur so ist gewährleistet das alles abgearbeitet wird
# z.B. declare -a DO_NOT_SHUTDOWN=("${OWN_INSTANCE_ID}" "i-xxxxxxxxxxxxx" "i-xxxxxxxxxxxxx" "i-xxxxxxxxxxxxx")
#
declare -a DO_NOT_SHUTDOWN=("${OWN_INSTANCE_ID}")
# -----------------------------------------------------------
# nichts mehr ändern
# -----------------------------------------------------------
LOG_PATH="$(dirname $(readlink -f $0))/logs"
LOG_FILE="$(date +%Y%m%d)-awscli.log"
# -----------------------------------------------------------
# Funktionen
# -----------------------------------------------------------
function is_inList ()
{
  SERVER=${1}
  max=${#DO_NOT_SHUTDOWN[@]}
  RET=1

  for ((i = 0 ; i < max ; i++ )); do
    if [ "${DO_NOT_SHUTDOWN[i]}" = "${SERVER}" ]; then
      RET=0
      break
    fi
  done

  return $RET
}
function logger () {
        mkdir -p ${LOG_PATH}
        if [ -z ${2} ]; then
                STATI="INFO"
        else
                STATI="${2}"
        fi
        echo "$(date +%Y-%m-%d) | $(date +%R) | ${STATI} | ${1}" >> ${LOG_PATH}/${LOG_FILE}
}
# -----------------------------------------------------------
# Prüfen ob OWN_INSTANCE_ID einen Wert enthält
# -----------------------------------------------------------
logger "Starte -> ${0##*/}" "INFO"
if [ -z ${OWN_INSTANCE_ID} ]; then
        logger "OWN_INSTANCE_ID ist leer Skript wird nicht gestartet" "ERROR"
        logger "Beende AWS Script" "ERROR"
        exit 99
fi

for item in $(${AWS_PATH}aws ec2 describe-instances --query 'Reservations[].Instances[][InstanceId,Tags[?Key==`Name`].Value | [0],LaunchTime,State.Name]' --filters "Name=instance-state-name,Values=running" --output text | awk '{print $1}')
do
        is_inList ${item}
        RET=$?
        if [ $RET -eq 0 ]; then
                logger "${item} wird nicht heruntergefahren definiert in OWN_INSTANCE_ID oder DO_NOT_SHUTDOWN" "INFO"
                continue
        else
                logger "${item} wird heruntergerfahren" "INFO"
                #----  aws cli fährt kiste herunter
                ${AWS_PATH}aws ec2 stop-instances --instance-ids ${item} > /dev/null 2>&1
        fi
done
logger "Beende -> ${0##*/}" "INFO"
exit 0


Bash : Funktion um Files zu laden

Funktion um Dateien aus dem Internet zu laden. Prüft ob curl vorhanden ist , wenn nicht wird wget versucht. Wenn gar nichts von beiden gefunden wird wird das Skript beendet.

function get_remote_file () {
        # ------------------------- get_remote_file -------------------------------------------------------------------
        # download an file to local storage
        #
        # need 2 parameters : get_remote_file [URL_TO_FILE] [LOCAL_PATH]
        # -----------------------------------------------------------------------------------------------------------------

        if [[ ! -z "${1}" || ! -z "${2}" ]]; then
                bin_dl=""
                # check Local Path
                if [ ! -d "${2}" ]; then
                        mkdir "${2}"
                fi
                # check bins
                # using command instead of which to be posix comp.
		
		# ------ check curl
                command -v curl >/dev/null 2>&1
                if [ $? -eq 0 ]; then
                        bin_dl="curl -s -O "
                fi

		# ------ check wget
                command -v wget >/dev/null 2>&1
                if [[ ${bin_dl} = "" && $? -eq 0 ]]; then
                        bin_dl="wget -q "
                fi

		# ------ if emtpy curl and wget not found
                if [ ${bin_dl} = "" ]; then
                        echo "need curl or wget for work please install one of them"
                        exit 98
                fi

                # download file
                if [[ "${1}" =~ http:// || "${1}" =~ https:// || "${1}" =~ ftp:// ]]; then
                        # ${1} is an remote file will be downloaded to ${2}
                        cd "${2}" && { ${bin_dl} "${1}" ; cd -; }
                else
                        # ${1} is not an remote file EXIT !
                        exit 98
                fi
        else
                echo "check parameters for function #> get_remote_file"
                exit 9
        fi
}

Bash : Inhalt von ZIP Dateien vergleichen

Problem : Man möchte über Bash nur den Inhalt einer Zip Datei vergleichen. Die Zip Datei wird aber automatisiert auf einem Server über cron erstellt, was zur Folge hatte das der Zeitstempel und somit auch die md5 Summen unterschiedlich sind.

Lösung : Die Lösung ist mit unzip in die Datei zu schauen und diesen Output mit diff zu verleichen.
function check_files_in_zip () {
	# ------------------------ check_files_in_zip ---------------------------------------
	# compare the content of two zipfiles if equal the function return 0 otherwise 1 
	#
	# need 2 parameters : check_files_in_zip [NAME_OF_OLD_ZIPFILE] [NAME_OF_NEW_ZIPFILE]
	# -----------------------------------------------------------------------------------

	if [[ ! -z "${1}" || ! -z "${2}"  ]]; then
        	diff <(unzip -v -l "${1}" | awk '! /Archiv/ && /[0-9]/ { print $1,$5,$6,$7,$8 }' | sed '$d') <(unzip -v -l "${2}" | awk '! /Archiv/ && /[0-9]/ { print $1,$5,$6,$7,$8 }' | sed '$d') 1>/dev/null 2>&1
        	if [ $? -eq 0 ]; then
                	return 0
        	else
                	return 1
       		fi
	else
		echo "check parameters for function #> check_files_in_zip"
                exit 9
        fi
}

BASH: Nur eine Instanz eines Bash Skriptes starten

In einigen Fällen darf ein Skript nur eine Instanz starten, z.B. Aufbereitung für Backup. Für diesen Zweck hab ich hier das kleine Bespiel eingestellt.

Sollten mehrere Benutzer das Skript starten können ist natürlich darauf zu achten das sie alle Schreibrechte auf das PID File haben.
PIDFILE="$(dirname "$(readlink -f "$0")")/$(basename "${0}").pid"
Und hier das komplette Beispiel :
#!/bin/bash

function check_process () {
        # ------------------------- check_process -----------------------------
        # create an pid file for an bash script and check is it already running
        #
        # need 2 parameters : check_process [NAME_OF_PIDFILE] [NAME_OF_SCRIPT]
        # ---------------------------------------------------------------------

        cp_PID_FILE="${1}"
        cp_SCR_NAME="${2}"
        cp_RET=1
        if [[ -n "${cp_PID_FILE}" && -n "${cp_SCR_NAME}" ]]; then
                if [ -f "${cp_PID_FILE}" ]; then
                        # Pid File auslesen
                        pid=$(cat "${cp_PID_FILE}")
                        chkpid=$(ps -ax | grep "/bin/bash" | grep "/${cp_SCR_NAME}" | grep "${pid}" | grep -v grep)
                        if [ $? -ne 0 ]; then
                                rm "${cp_PID_FILE}"
                                cp_RET=0
                        else
                                # Skript läuft noch -> keine doppelte ausführung wird beendet
                                # Direkter Abbruch (auskommentieren)
                                # echo "the script is already running -> PID:${pid}"
                                # exit 1
                                # Rückgabewert
                                cp_RET=1

                        fi
                fi
        else
                echo "check parameters for function #> check_process"
                exit 9
        fi
        if [ ${cp_RET} -eq 0 ]; then
                echo $$ > "${cp_PID_FILE}"
        fi
        return ${cp_RET}
}
# --------------------------------------------------------------------
clear
PIDFILE="$(dirname "$(readlink -f "$0")")/$(basename "${0}").pid"
check_process "${PIDFILE}" "$(basename "${0}")"
if [ $? -eq 1 ]; then
        echo "Script läuft bereits"
        exit 99
fi
echo "readlink : " $(readlink -f "${0}")
echo "basename : " $(basename "${0}")
echo "pidfile  : " ${PIDFILE}
echo "---------------------------------"
# endlosschleife - debug
while true; do
printf "."
sleep 2
done


Vielleicht hilft das dem einen oder anderen ;-)

Bash : Shell Skripte überprüfen

Eine einfach Möglichkeit um Shell Skripte zu überprüfen bietet shellcheck.net an. Es lässt sich auch lokal installieren und liefert auch dort Informationen zu Bugs & Verbesserungen. Bei mir immer am Anfang einer Neuinstallation dabei ;-)

Installation unter debian :
apt-get install shellcheck


Es ist manchmal ganz interresant vorhandene Skript testen zu lassen. Unterstützt werden bash, ksh , sh (POSIX kompatibel) und zsh.

shellcheck output
shellcheck output
Shellcheck on Github
shellcheck - wiki - Fehlercodes
“Das einzig sichere System müsste ausgeschaltet, in einem versiegelten und von Stahlbeton ummantelten Raum und von bewaffneten Schutztruppen umstellt sein.”
Gene Spafford (Sicherheitsexperte)