1#!/usr/bin/env bash
2set -euo pipefail
3
4# Deployment Automation Script
5# Supports rolling deployments, rollback, and health checks
6# Can deploy to staging or production environments
7
8# ============================================================================
9# Configuration
10# ============================================================================
11
12SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
14# Default configuration (can be overridden by deploy.conf or environment)
15APP_NAME="${APP_NAME:-myapp}"
16DEPLOY_USER="${DEPLOY_USER:-deploy}"
17DEPLOY_PATH="${DEPLOY_PATH:-/var/www/apps}"
18KEEP_RELEASES="${KEEP_RELEASES:-5}"
19HEALTH_CHECK_TIMEOUT="${HEALTH_CHECK_TIMEOUT:-30}"
20HEALTH_CHECK_PATH="${HEALTH_CHECK_PATH:-/health}"
21
22# Environment-specific server lists
23STAGING_SERVERS="${STAGING_SERVERS:-staging1.example.com}"
24PRODUCTION_SERVERS="${PRODUCTION_SERVERS:-prod1.example.com,prod2.example.com,prod3.example.com}"
25
26# Colors
27RED='\033[0;31m'
28GREEN='\033[0;32m'
29YELLOW='\033[1;33m'
30BLUE='\033[0;34m'
31CYAN='\033[0;36m'
32BOLD='\033[1m'
33NC='\033[0m'
34
35# ============================================================================
36# Utility Functions
37# ============================================================================
38
39log_info() {
40 echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $*"
41}
42
43log_success() {
44 echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} ✓ $*"
45}
46
47log_error() {
48 echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} ✗ $*" >&2
49}
50
51log_warning() {
52 echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} ⚠ $*"
53}
54
55log_section() {
56 echo
57 echo -e "${CYAN}${BOLD}==> $*${NC}"
58}
59
60# ============================================================================
61# Configuration Loading
62# ============================================================================
63
64load_config() {
65 local config_file="$SCRIPT_DIR/deploy.conf"
66
67 if [[ -f "$config_file" ]]; then
68 log_info "Loading configuration from $config_file"
69 # shellcheck disable=SC1090
70 source "$config_file"
71 fi
72}
73
74get_servers() {
75 local env=$1
76
77 case "$env" in
78 staging)
79 echo "$STAGING_SERVERS"
80 ;;
81 production)
82 echo "$PRODUCTION_SERVERS"
83 ;;
84 *)
85 log_error "Invalid environment: $env"
86 exit 1
87 ;;
88 esac
89}
90
91# ============================================================================
92# Remote Execution Functions
93# ============================================================================
94
95remote_exec() {
96 local server=$1
97 shift
98 local command="$*"
99
100 ssh -o StrictHostKeyChecking=no \
101 -o ConnectTimeout=10 \
102 "${DEPLOY_USER}@${server}" \
103 "$command"
104}
105
106sync_files() {
107 local server=$1
108 local source=$2
109 local dest=$3
110
111 rsync -avz --delete \
112 -e "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
113 "$source" \
114 "${DEPLOY_USER}@${server}:${dest}"
115}
116
117# ============================================================================
118# Deployment Functions
119# ============================================================================
120
121prepare_release() {
122 local release_name
123 release_name=$(date +%Y%m%d-%H%M%S)
124 echo "$release_name"
125}
126
127create_release_structure() {
128 local server=$1
129 local release=$2
130
131 log_info "Creating release structure on $server"
132
133 remote_exec "$server" "
134 mkdir -p ${DEPLOY_PATH}/${APP_NAME}/{releases,shared}
135 mkdir -p ${DEPLOY_PATH}/${APP_NAME}/releases/${release}
136 "
137}
138
139upload_release() {
140 local server=$1
141 local release=$2
142 local source_dir=$3
143
144 log_info "Uploading release to $server"
145
146 # In real scenario, this would upload actual built artifacts
147 # For demo, we'll create a simple structure
148 remote_exec "$server" "
149 cat > ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/app.sh << 'EOF'
150#!/bin/bash
151echo 'Application version: ${release}'
152echo 'Server: \$(hostname)'
153echo 'Status: Running'
154EOF
155 chmod +x ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/app.sh
156 "
157}
158
159link_shared_resources() {
160 local server=$1
161 local release=$2
162
163 log_info "Linking shared resources on $server"
164
165 remote_exec "$server" "
166 # Create shared directories if they don't exist
167 mkdir -p ${DEPLOY_PATH}/${APP_NAME}/shared/{config,logs,data}
168
169 # Link shared resources into release
170 ln -sf ${DEPLOY_PATH}/${APP_NAME}/shared/config \
171 ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/config
172 ln -sf ${DEPLOY_PATH}/${APP_NAME}/shared/logs \
173 ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/logs
174 ln -sf ${DEPLOY_PATH}/${APP_NAME}/shared/data \
175 ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/data
176 "
177}
178
179switch_current_release() {
180 local server=$1
181 local release=$2
182
183 log_info "Switching current release on $server"
184
185 remote_exec "$server" "
186 ln -sfn ${DEPLOY_PATH}/${APP_NAME}/releases/${release} \
187 ${DEPLOY_PATH}/${APP_NAME}/current
188 "
189}
190
191health_check() {
192 local server=$1
193
194 log_info "Running health check on $server"
195
196 # In real scenario, this would check HTTP endpoint
197 # For demo, we'll check if the symlink exists and app runs
198 if remote_exec "$server" "
199 [[ -L ${DEPLOY_PATH}/${APP_NAME}/current ]] && \
200 ${DEPLOY_PATH}/${APP_NAME}/current/app.sh > /dev/null 2>&1
201 "; then
202 log_success "Health check passed on $server"
203 return 0
204 else
205 log_error "Health check failed on $server"
206 return 1
207 fi
208}
209
210cleanup_old_releases() {
211 local server=$1
212
213 log_info "Cleaning up old releases on $server"
214
215 remote_exec "$server" "
216 cd ${DEPLOY_PATH}/${APP_NAME}/releases
217 ls -t | tail -n +$((KEEP_RELEASES + 1)) | xargs -r rm -rf
218 "
219}
220
221# ============================================================================
222# Deployment Commands
223# ============================================================================
224
225deploy() {
226 local env=$1
227 local servers
228
229 IFS=',' read -ra servers <<< "$(get_servers "$env")"
230
231 log_section "Starting deployment to $env environment"
232 log_info "Servers: ${servers[*]}"
233
234 local release
235 release=$(prepare_release)
236 log_info "Release: $release"
237
238 # Deploy to each server in rolling fashion
239 for server in "${servers[@]}"; do
240 log_section "Deploying to $server"
241
242 # Create structure
243 create_release_structure "$server" "$release"
244
245 # Upload files
246 upload_release "$server" "$release" "."
247
248 # Link shared resources
249 link_shared_resources "$server" "$release"
250
251 # Switch to new release
252 switch_current_release "$server" "$release"
253
254 # Health check
255 if ! health_check "$server"; then
256 log_error "Deployment failed on $server"
257 log_warning "Consider rolling back"
258 exit 1
259 fi
260
261 # Cleanup old releases
262 cleanup_old_releases "$server"
263
264 log_success "Deployment successful on $server"
265
266 # Wait between servers in production for rolling deployment
267 if [[ "$env" == "production" ]] && [[ "$server" != "${servers[-1]}" ]]; then
268 log_info "Waiting 10 seconds before next server..."
269 sleep 10
270 fi
271 done
272
273 log_section "Deployment Complete"
274 log_success "Successfully deployed $release to $env"
275}
276
277rollback() {
278 local env=$1
279 local servers
280
281 IFS=',' read -ra servers <<< "$(get_servers "$env")"
282
283 log_section "Starting rollback on $env environment"
284
285 for server in "${servers[@]}"; do
286 log_info "Rolling back on $server"
287
288 # Get previous release
289 local previous_release
290 previous_release=$(remote_exec "$server" "
291 cd ${DEPLOY_PATH}/${APP_NAME}/releases
292 current=\$(readlink ${DEPLOY_PATH}/${APP_NAME}/current | xargs basename)
293 ls -t | grep -v \"\$current\" | head -n1
294 ")
295
296 if [[ -z "$previous_release" ]]; then
297 log_error "No previous release found on $server"
298 continue
299 fi
300
301 log_info "Previous release: $previous_release"
302
303 # Switch to previous release
304 switch_current_release "$server" "$previous_release"
305
306 # Health check
307 if health_check "$server"; then
308 log_success "Rollback successful on $server"
309 else
310 log_error "Rollback failed on $server"
311 fi
312 done
313
314 log_section "Rollback Complete"
315}
316
317status() {
318 local env=$1
319 local servers
320
321 IFS=',' read -ra servers <<< "$(get_servers "$env")"
322
323 log_section "Status for $env environment"
324
325 for server in "${servers[@]}"; do
326 echo
327 echo -e "${BOLD}Server: $server${NC}"
328
329 # Get current release
330 local current
331 current=$(remote_exec "$server" "
332 if [[ -L ${DEPLOY_PATH}/${APP_NAME}/current ]]; then
333 readlink ${DEPLOY_PATH}/${APP_NAME}/current | xargs basename
334 else
335 echo 'not deployed'
336 fi
337 ")
338
339 echo " Current release: $current"
340
341 # List available releases
342 echo " Available releases:"
343 remote_exec "$server" "
344 cd ${DEPLOY_PATH}/${APP_NAME}/releases 2>/dev/null && ls -t | head -n5 || echo ' (none)'
345 " | sed 's/^/ /'
346
347 # Health check
348 if health_check "$server" > /dev/null 2>&1; then
349 echo -e " Health: ${GREEN}OK${NC}"
350 else
351 echo -e " Health: ${RED}FAILED${NC}"
352 fi
353 done
354
355 echo
356}
357
358# ============================================================================
359# Usage
360# ============================================================================
361
362show_usage() {
363 cat << EOF
364${BOLD}Deployment Automation Script${NC}
365
366${BOLD}Usage:${NC}
367 $0 <command> [options]
368
369${BOLD}Commands:${NC}
370 deploy --env <staging|production> Deploy new release
371 rollback --env <staging|production> Rollback to previous release
372 status --env <staging|production> Show deployment status
373
374${BOLD}Examples:${NC}
375 $0 deploy --env staging
376 $0 deploy --env production
377 $0 rollback --env production
378 $0 status --env staging
379
380EOF
381}
382
383# ============================================================================
384# Main Entry Point
385# ============================================================================
386
387main() {
388 load_config
389
390 local command="${1:-}"
391 local env="staging"
392
393 # Parse arguments
394 shift || true
395 while [[ $# -gt 0 ]]; do
396 case "$1" in
397 --env)
398 env="$2"
399 shift 2
400 ;;
401 *)
402 log_error "Unknown option: $1"
403 show_usage
404 exit 1
405 ;;
406 esac
407 done
408
409 # Validate environment
410 if [[ "$env" != "staging" ]] && [[ "$env" != "production" ]]; then
411 log_error "Invalid environment: $env"
412 show_usage
413 exit 1
414 fi
415
416 # Execute command
417 case "$command" in
418 deploy)
419 deploy "$env"
420 ;;
421 rollback)
422 rollback "$env"
423 ;;
424 status)
425 status "$env"
426 ;;
427 *)
428 log_error "Unknown command: $command"
429 show_usage
430 exit 1
431 ;;
432 esac
433}
434
435main "$@"