Files
linux-scripts/lambda-deployer.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

580 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
#########################################################################################
#### lambda-deployer.sh — Package, deploy, and schedule Python Lambda functions ####
#### Supports dependency bundling, EventBridge scheduling, invocation, and log tail ####
#### Requires: bash 4+, aws-cli v2, jq, zip, pip3 ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.01 ####
#### ####
#### Usage: ####
#### export AWS_PROFILE="production" ####
#### ./lambda-deployer.sh --deploy --function-name my-func --role-arn <ARN> ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
AWS_REGION="${AWS_REGION:-}"
FUNCTION_NAME="${FUNCTION_NAME:-}"
LAMBDA_RUNTIME="${LAMBDA_RUNTIME:-python3.12}"
LAMBDA_HANDLER="${LAMBDA_HANDLER:-lambda_function.lambda_handler}"
LAMBDA_ROLE_ARN="${LAMBDA_ROLE_ARN:-}"
LAMBDA_TIMEOUT="${LAMBDA_TIMEOUT:-30}"
LAMBDA_MEMORY="${LAMBDA_MEMORY:-128}"
LAMBDA_ENV_VARS="${LAMBDA_ENV_VARS:-}"
LAMBDA_LAYERS="${LAMBDA_LAYERS:-}"
SOURCE_DIR="${SOURCE_DIR:-.}"
SCHEDULE_EXPRESSION="${SCHEDULE_EXPRESSION:-}"
PAYLOAD="${PAYLOAD:-}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
RUN_MODE=""
ZIP_FILE=""
START_TIME=""
TEMP_DIR=""
# ── Colors ────────────────────────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }
die() {
err "$@"
exit 1
}
# ── Cleanup ───────────────────────────────────────────────────────────
cleanup() {
if [[ -n "$TEMP_DIR" ]] && [[ -d "$TEMP_DIR" ]]; then
verbose "Cleaning up temp directory: $TEMP_DIR"
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
# ── AWS CLI wrapper ───────────────────────────────────────────────────
aws_cmd() {
local args=("$@")
[[ -n "$AWS_REGION" ]] && args+=(--region "$AWS_REGION")
verbose "aws ${args[*]}"
aws "${args[@]}"
}
# ── Resolve region ────────────────────────────────────────────────────
resolve_region() {
[[ -z "$AWS_REGION" ]] && AWS_REGION="$(aws configure get region 2>/dev/null || true)"
[[ -z "$AWS_REGION" ]] && die "Cannot determine AWS region. Set AWS_REGION or configure aws-cli."
verbose "Region: $AWS_REGION"
}
# ── Dependency checks ────────────────────────────────────────────────
check_dependencies() {
local missing=()
for cmd in aws jq zip pip3; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
[[ ${#missing[@]} -gt 0 ]] && die "Missing required tools: ${missing[*]}"
[[ "${BASH_VERSINFO[0]}" -lt 4 ]] && die "Bash 4+ required (found ${BASH_VERSION})"
}
# ── Print header ──────────────────────────────────────────────────────
print_header() {
echo -e "${BOLD}Lambda Deployer${RESET}"
echo "Region: $AWS_REGION"
echo "Mode: $RUN_MODE"
echo "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
}
# ── Elapsed time ──────────────────────────────────────────────────────
elapsed() {
local end
end=$(date +%s)
echo $(( end - START_TIME ))
}
# ── Build environment variables JSON ─────────────────────────────────
build_env_vars_json() {
local env_str="$1"
[[ -z "$env_str" ]] && return
local json="{"
local first=true
IFS=',' read -ra pairs <<< "$env_str"
for pair in "${pairs[@]}"; do
[[ "$first" == "true" ]] && first=false || json+=","
json+="\"${pair%%=*}\":\"${pair#*=}\""
done
echo "{\"Variables\":${json}}}"
}
# ── Build layers array ───────────────────────────────────────────────
build_layers_args() {
[[ -z "$1" ]] && return
echo "${1//,/ }"
}
# ── Package mode ──────────────────────────────────────────────────────
do_package() {
[[ -z "$FUNCTION_NAME" ]] && die "--function-name is required for package mode"
local src_dir
src_dir="$(realpath "$SOURCE_DIR")"
[[ -d "$src_dir" ]] || die "Source directory not found: $src_dir"
log "Packaging function ${BOLD}$FUNCTION_NAME${RESET}..."
TEMP_DIR="$(mktemp -d)"
local pkg_dir="$TEMP_DIR/package"
mkdir -p "$pkg_dir"
# Install dependencies
local req_file="$src_dir/requirements.txt"
if [[ -f "$req_file" ]]; then
log "Installing dependencies from requirements.txt..."
pip3 install -r "$req_file" -t "$pkg_dir" --quiet --disable-pip-version-check 2>/dev/null \
|| die "pip3 install failed"
# Clean up pip metadata to reduce zip size
find "$pkg_dir" -type d \( -name "__pycache__" -o -name "*.dist-info" -o -name "*.egg-info" \) \
-exec rm -rf {} + 2>/dev/null || true
local pkg_count
pkg_count=$(grep -cE '^[^#[:space:]]' "$req_file" || echo "0")
log "Collected $pkg_count package(s)"
else
warn "No requirements.txt found — packaging handler only"
fi
# Copy handler code
local py_count=0
while IFS= read -r -d '' f; do
cp "$f" "$pkg_dir/"
((py_count++)) || true
done < <(find "$src_dir" -maxdepth 1 -name "*.py" -print0)
if [[ $py_count -eq 0 ]]; then
die "No .py files found in $src_dir"
fi
verbose "Copied $py_count Python file(s)"
# Create zip
ZIP_FILE="/tmp/lambda-${FUNCTION_NAME}.zip"
(cd "$pkg_dir" && zip -r -q "$ZIP_FILE" .) \
|| die "Failed to create zip"
local size
size=$(du -h "$ZIP_FILE" | cut -f1)
log "Created deployment package: ${BOLD}$ZIP_FILE${RESET} ($size)"
}
# ── Check if function exists ─────────────────────────────────────────
function_exists() {
local name="$1"
aws_cmd lambda get-function --function-name "$name" &>/dev/null
}
# ── Deploy mode ───────────────────────────────────────────────────────
do_deploy() {
[[ -z "$FUNCTION_NAME" ]] && die "--function-name is required for deploy mode"
# Package first if zip doesn't exist
ZIP_FILE="/tmp/lambda-${FUNCTION_NAME}.zip"
if [[ ! -f "$ZIP_FILE" ]]; then
do_package
fi
if function_exists "$FUNCTION_NAME"; then
log "Function ${BOLD}$FUNCTION_NAME${RESET} exists — updating..."
aws_cmd lambda update-function-code \
--function-name "$FUNCTION_NAME" \
--zip-file "fileb://$ZIP_FILE" \
--output text --query 'FunctionArn' >/dev/null \
|| die "Failed to update function code"
aws_cmd lambda wait function-updated \
--function-name "$FUNCTION_NAME" 2>/dev/null || true
local config_args=(
lambda update-function-configuration
--function-name "$FUNCTION_NAME"
--runtime "$LAMBDA_RUNTIME"
--handler "$LAMBDA_HANDLER"
--timeout "$LAMBDA_TIMEOUT"
--memory-size "$LAMBDA_MEMORY"
)
if [[ -n "$LAMBDA_ENV_VARS" ]]; then
local env_json
env_json=$(build_env_vars_json "$LAMBDA_ENV_VARS")
config_args+=(--environment "$env_json")
fi
if [[ -n "$LAMBDA_LAYERS" ]]; then
local layers
layers=$(build_layers_args "$LAMBDA_LAYERS")
# shellcheck disable=SC2086,SC2206
config_args+=(--layers $layers)
fi
local fn_arn
fn_arn=$(aws_cmd "${config_args[@]}" --output text --query 'FunctionArn') \
|| die "Failed to update function configuration"
echo -e " ${GREEN}${RESET} Function updated: $fn_arn"
else
[[ -z "$LAMBDA_ROLE_ARN" ]] && die "--role-arn is required to create a new function"
log "Function ${BOLD}$FUNCTION_NAME${RESET} does not exist — creating..."
log "Creating function $FUNCTION_NAME ($LAMBDA_RUNTIME, $LAMBDA_MEMORY MB, ${LAMBDA_TIMEOUT}s timeout)"
local create_args=(
lambda create-function
--function-name "$FUNCTION_NAME"
--runtime "$LAMBDA_RUNTIME"
--handler "$LAMBDA_HANDLER"
--role "$LAMBDA_ROLE_ARN"
--timeout "$LAMBDA_TIMEOUT"
--memory-size "$LAMBDA_MEMORY"
--zip-file "fileb://$ZIP_FILE"
)
if [[ -n "$LAMBDA_ENV_VARS" ]]; then
local env_json
env_json=$(build_env_vars_json "$LAMBDA_ENV_VARS")
create_args+=(--environment "$env_json")
fi
if [[ -n "$LAMBDA_LAYERS" ]]; then
local layers
layers=$(build_layers_args "$LAMBDA_LAYERS")
# shellcheck disable=SC2086,SC2206
create_args+=(--layers $layers)
fi
local fn_arn
fn_arn=$(aws_cmd "${create_args[@]}" --output text --query 'FunctionArn') \
|| die "Failed to create function"
# Wait for function to become active
verbose "Waiting for function to become active..."
aws_cmd lambda wait function-active-v2 \
--function-name "$FUNCTION_NAME" 2>/dev/null || true
echo -e " ${GREEN}${RESET} Function created: $fn_arn"
fi
}
# ── Schedule mode ─────────────────────────────────────────────────────
do_schedule() {
[[ -z "$FUNCTION_NAME" ]] && die "--function-name is required for schedule mode"
[[ -z "$SCHEDULE_EXPRESSION" ]] && die "--schedule-expression is required for schedule mode"
log "Configuring EventBridge schedule for ${BOLD}$FUNCTION_NAME${RESET}..."
local fn_arn
fn_arn=$(aws_cmd lambda get-function \
--function-name "$FUNCTION_NAME" \
--output text --query 'Configuration.FunctionArn' 2>/dev/null) \
|| die "Function $FUNCTION_NAME not found — deploy it first"
local rule_name="lambda-deployer-${FUNCTION_NAME}"
local rule_arn
rule_arn=$(aws_cmd events put-rule \
--name "$rule_name" \
--schedule-expression "$SCHEDULE_EXPRESSION" \
--state ENABLED \
--description "Scheduled trigger for $FUNCTION_NAME (managed by lambda-deployer)" \
--output text --query 'RuleArn') \
|| die "Failed to create EventBridge rule"
echo -e " ${GREEN}${RESET} Rule: $rule_name ($SCHEDULE_EXPRESSION)"
aws_cmd events put-targets \
--rule "$rule_name" \
--targets "[{\"Id\":\"${FUNCTION_NAME}-target\",\"Arn\":\"${fn_arn}\"}]" \
--output text >/dev/null \
|| die "Failed to add Lambda target to rule"
local stmt_id="lambda-deployer-${FUNCTION_NAME}-invoke"
aws_cmd lambda remove-permission \
--function-name "$FUNCTION_NAME" \
--statement-id "$stmt_id" 2>/dev/null || true
aws_cmd lambda add-permission \
--function-name "$FUNCTION_NAME" \
--statement-id "$stmt_id" \
--action "lambda:InvokeFunction" \
--principal "events.amazonaws.com" \
--source-arn "$rule_arn" \
--output text >/dev/null \
|| die "Failed to add invoke permission"
echo -e " ${GREEN}${RESET} Permission granted for EventBridge to invoke $FUNCTION_NAME"
log "Schedule configured: $SCHEDULE_EXPRESSION"
}
# ── Invoke mode ───────────────────────────────────────────────────────
do_invoke() {
[[ -z "$FUNCTION_NAME" ]] && die "--function-name is required for invoke mode"
log "Invoking ${BOLD}$FUNCTION_NAME${RESET}..."
local invoke_args=(
lambda invoke
--function-name "$FUNCTION_NAME"
--log-type Tail
)
if [[ -n "$PAYLOAD" ]]; then
invoke_args+=(--payload "$PAYLOAD")
verbose "Payload: $PAYLOAD"
fi
local output_file
output_file="$(mktemp)"
local response
response=$(aws_cmd "${invoke_args[@]}" "$output_file" 2>&1) \
|| die "Invoke failed: $response"
local func_error
func_error=$(echo "$response" | jq -r '.FunctionError // empty' 2>/dev/null || echo "")
if [[ -n "$func_error" ]]; then
echo -e " ${RED}${RESET} Function error: $func_error"
else
echo -e " ${GREEN}${RESET} Status: $(echo "$response" | jq -r '.StatusCode // 200' 2>/dev/null)"
fi
echo ""
echo -e "${BOLD}Response:${RESET}"
jq '.' "$output_file" 2>/dev/null || cat "$output_file"
echo ""
local log_result
log_result=$(echo "$response" | jq -r '.LogResult // empty' 2>/dev/null || echo "")
if [[ -n "$log_result" ]]; then
echo -e "${BOLD}Execution Log:${RESET}"
echo "$log_result" | base64 --decode 2>/dev/null || true
fi
rm -f "$output_file"
}
# ── Logs mode ─────────────────────────────────────────────────────────
do_logs() {
[[ -z "$FUNCTION_NAME" ]] && die "--function-name is required for logs mode"
local log_group="/aws/lambda/$FUNCTION_NAME"
local exists
exists=$(aws_cmd logs describe-log-groups \
--log-group-name-prefix "$log_group" \
--query "logGroups[?logGroupName=='$log_group'].logGroupName" \
--output text 2>/dev/null || echo "")
[[ -z "$exists" ]] && die "Log group $log_group not found — has $FUNCTION_NAME been invoked?"
log "Tailing logs for ${BOLD}$FUNCTION_NAME${RESET} (Ctrl+C to stop)..."
echo ""
local start_time next_token=""
start_time=$(( $(date +%s) * 1000 - 300000 ))
while true; do
local filter_args=(logs filter-log-events --log-group-name "$log_group"
--start-time "$start_time" --interleaved)
[[ -n "$next_token" ]] && filter_args+=(--next-token "$next_token")
local result
result=$(aws_cmd "${filter_args[@]}" --output json 2>/dev/null || echo "{}")
local events
events=$(echo "$result" | jq -r \
'.events[]? | "\(.timestamp | . / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")) \(.message)"' \
2>/dev/null || echo "")
[[ -n "$events" ]] && echo "$events"
local new_token
new_token=$(echo "$result" | jq -r '.nextToken // empty' 2>/dev/null || echo "")
if [[ -n "$new_token" ]]; then
next_token="$new_token"
else
next_token=""
local last_ts
last_ts=$(echo "$result" | jq -r '.events[-1]?.timestamp // empty' 2>/dev/null || echo "")
[[ -n "$last_ts" ]] && start_time=$(( last_ts + 1 ))
fi
sleep 2
done
}
# ── List mode ─────────────────────────────────────────────────────────
do_list() {
log "Listing Lambda functions in ${BOLD}$AWS_REGION${RESET}..."
echo ""
local functions
functions=$(aws_cmd lambda list-functions \
--query 'Functions[*].[FunctionName,Runtime,CodeSize,LastModified]' \
--output json 2>/dev/null) \
|| die "Failed to list functions"
local count
count=$(echo "$functions" | jq 'length' 2>/dev/null || echo "0")
if [[ "$count" -eq 0 ]]; then
log "No Lambda functions found in $AWS_REGION"
return
fi
printf " ${BOLD}%-25s %-14s %-10s %s${RESET}\n" "FUNCTION" "RUNTIME" "SIZE" "LAST MODIFIED"
echo " ─────────────────────────────────────────────────────────────────"
echo "$functions" | jq -r '.[] | @tsv' 2>/dev/null | while IFS=$'\t' read -r name runtime size modified; do
# Convert bytes to human-readable
local human_size
if [[ "$size" -ge 1048576 ]]; then
human_size="$(awk "BEGIN{printf \"%.1f MB\", $size/1048576}")"
elif [[ "$size" -ge 1024 ]]; then
human_size="$(awk "BEGIN{printf \"%.1f KB\", $size/1024}")"
else
human_size="${size} B"
fi
# Trim the modified timestamp
local short_modified
short_modified="${modified%%+*}"
short_modified="${short_modified%.*}"
printf " %-25s %-14s %-10s %s\n" "$name" "$runtime" "$human_size" "$short_modified"
done
echo ""
log "Total: $count function(s)"
}
# ── Usage / help ──────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: $SCRIPT_NAME <mode> [options]
Modes:
--package Package handler + deps into zip --deploy Create/update function
--schedule EventBridge scheduled rule --invoke Invoke function
--logs Tail CloudWatch Logs --list List functions
Options:
--function-name NAME Function name --runtime RUNTIME (python3.12)
--handler HANDLER Entry point --role-arn ARN Exec role
--timeout N Seconds (30) --memory N MB (128)
--env-vars K=V,... Env variables --layers ARN,... Layer ARNs
--schedule-expression E Cron/rate expr --payload JSON Invoke payload
--source-dir DIR Source dir (.) --verbose Debug output
--no-color No ANSI colors -h, --help This help
EOF
}
# ── Parse arguments ───────────────────────────────────────────────────
parse_args() {
if [[ $# -eq 0 ]]; then
usage
exit 0
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--package|--deploy|--schedule|--invoke|--logs|--list)
RUN_MODE="${1#--}"; shift ;;
--function-name) [[ $# -lt 2 ]] && die "$1 requires a value"; FUNCTION_NAME="$2"; shift 2 ;;
--runtime) [[ $# -lt 2 ]] && die "$1 requires a value"; LAMBDA_RUNTIME="$2"; shift 2 ;;
--handler) [[ $# -lt 2 ]] && die "$1 requires a value"; LAMBDA_HANDLER="$2"; shift 2 ;;
--role-arn) [[ $# -lt 2 ]] && die "$1 requires a value"; LAMBDA_ROLE_ARN="$2"; shift 2 ;;
--timeout) [[ $# -lt 2 ]] && die "$1 requires a value"; LAMBDA_TIMEOUT="$2"; shift 2 ;;
--memory) [[ $# -lt 2 ]] && die "$1 requires a value"; LAMBDA_MEMORY="$2"; shift 2 ;;
--env-vars) [[ $# -lt 2 ]] && die "$1 requires a value"; LAMBDA_ENV_VARS="$2"; shift 2 ;;
--layers) [[ $# -lt 2 ]] && die "$1 requires a value"; LAMBDA_LAYERS="$2"; shift 2 ;;
--schedule-expression) [[ $# -lt 2 ]] && die "$1 requires a value"; SCHEDULE_EXPRESSION="$2"; shift 2 ;;
--payload) [[ $# -lt 2 ]] && die "$1 requires a value"; PAYLOAD="$2"; shift 2 ;;
--source-dir) [[ $# -lt 2 ]] && die "$1 requires a value"; SOURCE_DIR="$2"; shift 2 ;;
--verbose) VERBOSE="true"; shift ;;
--no-color) COLOR="never"; shift ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown option: $1 (see --help)" ;;
esac
done
if [[ -z "$RUN_MODE" ]]; then err "No mode specified"; echo ""; usage; exit 1; fi
}
# ── Main ──────────────────────────────────────────────────────────────
main() {
parse_args "$@"
setup_colors
check_dependencies
resolve_region
START_TIME=$(date +%s)
print_header
case "$RUN_MODE" in
package)
do_package
;;
deploy)
do_deploy
;;
schedule)
do_schedule
;;
invoke)
do_invoke
;;
logs)
do_logs
;;
list)
do_list
;;
*)
die "Unknown mode: $RUN_MODE"
;;
esac
if [[ "$RUN_MODE" != "logs" ]]; then
log "Completed in $(elapsed)s"
fi
}
main "$@"