Przejdź do treści

⚙️ Shell in Ansible

Leveraging shell scripting within Ansible playbooks and roles for maximum flexibility while maintaining Ansible's declarative benefits.


🎯 Ansible Shell Modules

Command Module

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# playbook.yml - Command module (no shell interpretation)
---
- name: System Information Gathering
  hosts: all
  tasks:
    - name: Get system uptime
      command: uptime
      register: system_uptime

    - name: Display uptime
      debug:
        msg: "System uptime: {{ system_uptime.stdout }}"

    - name: Check disk space
      command: df -h /
      register: disk_space

    - name: Verify sufficient disk space
      assert:
        that:
          - "disk_space.stdout_lines[1].split()[4].rstrip('%') | int < 80"
        fail_msg: "Disk usage exceeds 80%"

    - name: Get process count
      command: ps aux | wc -l
      args:
        stdin: "{{ lookup('pipe', 'ps aux') }}"
      register: process_count

Shell Module

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
# shell-playbook.yml - Shell module (full shell interpretation)
---
- name: Advanced System Configuration
  hosts: webservers
  become: yes
  tasks:
    - name: Configure complex nginx setup
      shell: |
        #!/bin/bash
        set -euo pipefail

        # Create directories with proper permissions
        mkdir -p /var/www/{html,logs,cache}
        chown -R www-data:www-data /var/www

        # Generate nginx configuration
        cat > /etc/nginx/sites-available/default << 'EOF'
        server {
            listen 80;
            server_name {{ inventory_hostname }};

            location / {
                proxy_pass http://localhost:8080;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
            }

            location /static/ {
                alias /var/www/html/static/;
                expires 1y;
                add_header Cache-Control "public, immutable";
            }
        }
        EOF

        # Test configuration
        nginx -t

        # Reload service
        systemctl reload nginx

        # Verify service status
        systemctl is-active nginx

      environment:
        NGINX_VERSION: "{{ nginx_version | default('latest') }}"

    - name: Deploy application with complex logic
      shell: |
        #!/bin/bash
        set -euo pipefail

        # Multi-step deployment with rollback capability
        DEPLOY_DIR="/var/www/app"
        BACKUP_DIR="/var/backups/app-$(date +%Y%m%d-%H%M%S)"

        # Create backup
        if [ -d "$DEPLOY_DIR" ]; then
            echo "Creating backup..."
            cp -r "$DEPLOY_DIR" "$BACKUP_DIR"
        fi

        # Deploy new version
        echo "Deploying new version..."
        rm -rf "$DEPLOY_DIR"
        mkdir -p "$DEPLOY_DIR"

        # Extract application
        tar -xzf /tmp/app.tar.gz -C "$DEPLOY_DIR"

        # Run database migrations
        if [ -f "$DEPLOY_DIR/migrate.sh" ]; then
            echo "Running database migrations..."
            "$DEPLOY_DIR/migrate.sh"
        fi

        # Set permissions
        chown -R www-data:www-data "$DEPLOY_DIR"
        find "$DEPLOY_DIR" -type f -exec chmod 644 {} \;
        find "$DEPLOY_DIR" -type d -exec chmod 755 {} \;

        # Warm up cache
        curl -s http://localhost/warmup > /dev/null

        echo "Deployment completed successfully"

      args:
        creates: "/var/www/app/deploy-complete"

    - name: Verify deployment
      shell: |
        #!/bin/bash
        set -e

        # Health checks
        if ! curl -sf http://localhost/health; then
            echo "Health check failed" >&2
            exit 1
        fi

        # Performance check
        RESPONSE_TIME=$(curl -w "%{time_total}" -o /dev/null -s http://localhost/)
        if (( $(echo "$RESPONSE_TIME > 2.0" | bc -l) )); then
            echo "Response time too slow: ${RESPONSE_TIME}s" >&2
            exit 1
        fi

        # Mark deployment as complete
        touch /var/www/app/deploy-complete

        echo "Deployment verification passed"

🛠️ Advanced Shell Integration Patterns

Dynamic Variable Generation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# dynamic-vars.yml - Generate variables with shell scripts
---
- name: Dynamic Variable Generation
  hosts: all
  tasks:
    - name: Generate system fingerprint
      shell: |
        #!/bin/bash
        set -e

        # Generate unique system fingerprint
        echo "{"
        echo "  \"hostname\": \"$(hostname)\","
        echo "  \"uuid\": \"$(cat /etc/machine-id 2>/dev/null || uuidgen)\","
        echo "  \"os\": \"$(uname -s)\","
        echo "  \"arch\": \"$(uname -m)\","
        echo "  \"kernel\": \"$(uname -r)\","
        echo "  \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
        echo "}"
      register: system_fingerprint

    - name: Set facts from JSON output
      set_fact:
        system_info: "{{ system_fingerprint.stdout | from_json }}"

    - name: Display system information
      debug:
        msg: |
          System Information:
            Hostname: {{ system_info.hostname }}
            UUID: {{ system_info.uuid }}
            OS: {{ system_info.os }}
            Architecture: {{ system_info.arch }}
            Kernel: {{ system_info.kernel }}

Conditional Logic with Shell

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# conditional-logic.yml - Complex conditional logic
---
- name: Conditional System Configuration
  hosts: all
  tasks:
    - name: Determine system type and configure accordingly
      shell: |
        #!/bin/bash
        set -e

        # Complex logic to determine system configuration
        SYSTEM_TYPE="unknown"
        CONFIG_PROFILE="default"

        # Check if this is a container
        if [ -f /.dockerenv ] || [ -f /run/.containerenv ]; then
            SYSTEM_TYPE="container"
            CONFIG_PROFILE="container"
        elif [ -d /proc/vz ] && [ ! -d /proc/bc ]; then
            SYSTEM_TYPE="openvz"
            CONFIG_PROFILE="virtual"
        elif systemd-detect-virt --quiet; then
            SYSTEM_TYPE="vm"
            CONFIG_PROFILE="virtual"
        else
            SYSTEM_TYPE="baremetal"
            CONFIG_PROFILE="physical"
        fi

        # Additional checks
        MEMORY_GB=$(free -g | awk '/^Mem:/{print $2}')
        CPU_CORES=$(nproc)

        if [ "$MEMORY_GB" -gt 16 ] && [ "$CPU_CORES" -gt 8 ]; then
            CONFIG_PROFILE="${CONFIG_PROFILE}-highperf"
        elif [ "$MEMORY_GB" -lt 4 ] || [ "$CPU_CORES" -lt 2 ]; then
            CONFIG_PROFILE="${CONFIG_PROFILE}-lowres"
        fi

        echo "{"
        echo "  \"system_type\": \"$SYSTEM_TYPE\","
        echo "  \"config_profile\": \"$CONFIG_PROFILE\","
        echo "  \"memory_gb\": $MEMORY_GB,"
        echo "  \"cpu_cores\": $CPU_CORES"
        echo "}"
      register: system_classification

    - name: Set system classification facts
      set_fact:
        classification: "{{ system_classification.stdout | from_json }}"

    - name: Apply configuration based on system type
      include_tasks: "profiles/{{ classification.config_profile }}.yml"
      when: classification.config_profile != "default"

Error Handling and Retry Logic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# error-handling.yml - Robust error handling with retries
---
- name: Robust System Operations
  hosts: all
  tasks:
    - name: Install package with retry logic
      shell: |
        #!/bin/bash
        set -e

        PACKAGE_NAME="{{ package_name }}"
        MAX_ATTEMPTS=3
        ATTEMPT=1

        while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
            echo "Attempt $ATTEMPT/$MAX_ATTEMPTS: Installing $PACKAGE_NAME"

            if apt-get update && apt-get install -y "$PACKAGE_NAME"; then
                echo "Package installed successfully"
                exit 0
            else
                EXIT_CODE=$?
                echo "Installation failed with exit code $EXIT_CODE"

                if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
                    echo "Max attempts reached, giving up"
                    exit $EXIT_CODE
                fi

                # Wait before retry with exponential backoff
                WAIT_TIME=$((2 ** ATTEMPT))
                echo "Waiting $WAIT_TIME seconds before retry..."
                sleep $WAIT_TIME

                ATTEMPT=$((ATTEMPT + 1))
            fi
        done
      register: package_install_result
      ignore_errors: yes

    - name: Handle installation result
      block:
        - name: Check if installation succeeded
          assert:
            that:
              - package_install_result.rc == 0
            fail_msg: "Package installation failed after all retries"

      rescue:
        - name: Log failure and attempt alternative installation
          shell: |
            #!/bin/bash
            echo "Primary installation failed, trying alternative method..."
            # Alternative installation logic here
            echo "Alternative installation attempted"
          delegate_to: localhost

        - name: Notify administrators
          mail:
            host: localhost
            port: 25
            subject: "Package Installation Failed - {{ inventory_hostname }}"
            body: |
              Package installation for {{ package_name }} failed on {{ inventory_hostname }}.
              Please investigate and resolve manually.
            to: admin@example.com

📋 Shell Script Best Practices in Ansible

Secure Script Handling

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# secure-scripts.yml - Secure shell script management
---
- name: Secure Script Execution
  hosts: all
  become: yes
  tasks:
    - name: Create secure temporary directory
      tempfile:
        state: directory
        suffix: ansible-secure
      register: secure_temp

    - name: Copy secure script
      copy:
        content: |
          #!/bin/bash
          set -euo pipefail

          # Secure script with proper error handling
          SECURE_TEMP="{{ secure_temp.path }}"

          # Validate inputs
          validate_required() {
              local var_name="$1"
              local var_value="${!var_name:-}"

              if [ -z "$var_value" ]; then
                  echo "Error: Required variable $var_name is not set" >&2
                  return 1
              fi
          }

          # Secure credential handling
          if [ -f "/run/secrets/{{ secret_name }}" ]; then
              SECRET_VALUE=$(cat "/run/secrets/{{ secret_name }}")
          else
              echo "Error: Secret {{ secret_name }} not found" >&2
              exit 1
          fi

          # Main logic with proper cleanup
          trap 'rm -rf "$SECURE_TEMP"' EXIT

          # Your secure logic here
          echo "Executing secure operation..."

        dest: "{{ secure_temp.path }}/secure-script.sh"
        mode: '0700'

    - name: Execute secure script
      shell: "{{ secure_temp.path }}/secure-script.sh"
      environment:
        SECRET_NAME: "{{ secret_name }}"
        SECURE_TEMP: "{{ secure_temp.path }}"

    - name: Clean up temporary directory
      file:
        path: "{{ secure_temp.path }}"
        state: absent
      when: secure_temp.path is defined

Environment-Aware Scripts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# environment-aware.yml - Environment-specific shell integration
---
- name: Environment-Aware Configuration
  hosts: all
  vars:
    environment_configs:
      development:
        script: scripts/dev-setup.sh
        vars:
          LOG_LEVEL: debug
          DB_HOST: localhost
      staging:
        script: scripts/staging-setup.sh
        vars:
          LOG_LEVEL: info
          DB_HOST: staging-db.example.com
      production:
        script: scripts/prod-setup.sh
        vars:
          LOG_LEVEL: warn
          DB_HOST: prod-db.example.com

    current_config: "{{ environment_configs[environment | default('development')] }}"

  tasks:
    - name: Execute environment-specific script
      shell: |
        #!/bin/bash
        set -euo pipefail

        # Source environment variables
        export LOG_LEVEL="{{ current_config.vars.LOG_LEVEL }}"
        export DB_HOST="{{ current_config.vars.DB_HOST }}"
        export ENVIRONMENT="{{ environment | default('development') }}"
        export TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

        # Execute environment-specific setup
        {{ current_config.script }}

      environment:
        LOG_LEVEL: "{{ current_config.vars.LOG_LEVEL }}"
        DB_HOST: "{{ current_config.vars.DB_HOST }}"
        ENVIRONMENT: "{{ environment | default('development') }}"
        TIMESTAMP: "{{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}"

      register: environment_setup_result

    - name: Verify environment setup
      assert:
        that:
          - environment_setup_result.rc == 0
        fail_msg: "Environment setup failed: {{ environment_setup_result.stderr }}"

🎯 Real-World Examples

Example 1: Database Migration with Rollback

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# database-migration.yml - Complex database migration
---
- name: Database Migration with Rollback
  hosts: database_servers
  become: yes
  vars:
    backup_dir: "/var/backups/db-{{ ansible_date_time.iso8601_basic_short }}"

  tasks:
    - name: Create database backup
      shell: |
        #!/bin/bash
        set -euo pipefail

        BACKUP_DIR="{{ backup_dir }}"
        DB_NAME="{{ db_name }}"

        echo "Creating database backup..."
        mkdir -p "$BACKUP_DIR"

        # Create backup with timestamp
        BACKUP_FILE="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).sql"

        if pg_dump "$DB_NAME" > "$BACKUP_FILE"; then
            echo "Backup created: $BACKUP_FILE"

            # Compress backup
            gzip "$BACKUP_FILE"
            echo "Backup compressed: ${BACKUP_FILE}.gz"

            # Record backup info
            echo "{"
            echo "  \"backup_file\": \"${BACKUP_FILE}.gz\","
            echo "  \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
            echo "  \"size_bytes\": $(stat -c%s "${BACKUP_FILE}.gz")"
            echo "}" > "$BACKUP_DIR/backup-info.json"
        else
            echo "Backup failed" >&2
            exit 1
        fi

      register: db_backup_result

    - name: Run database migrations
      shell: |
        #!/bin/bash
        set -euo pipefail

        MIGRATION_DIR="/opt/app/migrations"
        DB_NAME="{{ db_name }}"

        echo "Running database migrations..."

        # Check current schema version
        CURRENT_VERSION=$(psql -d "$DB_NAME" -t -c "SELECT version FROM schema_migrations ORDER BY applied_at DESC LIMIT 1;" 2>/dev/null || echo "0")

        # Apply migrations in order
        find "$MIGRATION_DIR" -name "*.sql" -type f | sort | while read -r migration; do
            MIGRATION_VERSION=$(basename "$migration" | cut -d'_' -f1)

            if [ "$MIGRATION_VERSION" -gt "$CURRENT_VERSION" ]; then
                echo "Applying migration $MIGRATION_VERSION..."

                if psql -d "$DB_NAME" -f "$migration"; then
                    # Record successful migration
                    psql -d "$DB_NAME" -c "INSERT INTO schema_migrations (version, applied_at) VALUES ('$MIGRATION_VERSION', NOW());"
                    echo "Migration $MIGRATION_VERSION applied successfully"
                else
                    echo "Migration $MIGRATION_VERSION failed" >&2
                    exit 1
                fi
            fi
        done

        echo "All migrations completed successfully"

      register: db_migration_result
      ignore_errors: yes

    - name: Handle migration failure with rollback
      block: []
      rescue:
        - name: Restore database from backup
          shell: |
            #!/bin/bash
            set -e

            BACKUP_DIR="{{ backup_dir }}"
            DB_NAME="{{ db_name }}"

            # Find latest backup
            LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/backup-*.sql.gz | head -1)

            if [ -n "$LATEST_BACKUP" ]; then
                echo "Restoring database from backup: $LATEST_BACKUP"

                # Restore database
                gunzip -c "$LATEST_BACKUP" | psql "$DB_NAME"
                echo "Database restored successfully"
            else
                echo "No backup found for rollback" >&2
                exit 1
            fi

          when: db_backup_result is succeeded

        - name: Notify team of rollback
          mail:
            host: localhost
            subject: "Database Migration Rollback - {{ inventory_hostname }}"
            body: |
              Database migration failed on {{ inventory_hostname }}.
              Automatic rollback has been performed.
              Please investigate the issue.
            to: devops-team@example.com

Example 2: Multi-Stage Application Deployment

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# application-deployment.yml - Multi-stage deployment
---
- name: Multi-Stage Application Deployment
  hosts: application_servers
  become: yes
  vars:
    app_version: "{{ app_version | default('latest') }}"
    deploy_dir: "/var/www/app"
    backup_dir: "/var/backups/app-{{ ansible_date_time.iso8601_basic_short }}"

  tasks:
    - name: Stage 1 - Prepare Environment
      shell: |
        #!/bin/bash
        set -euo pipefail

        DEPLOY_DIR="{{ deploy_dir }}"
        BACKUP_DIR="{{ backup_dir }}"

        echo "=== Stage 1: Prepare Environment ==="

        # Create deployment directories
        mkdir -p "$DEPLOY_DIR" "$BACKUP_DIR"

        # Create backup of current deployment
        if [ -d "$DEPLOY_DIR/current" ]; then
            echo "Creating backup of current deployment..."
            cp -r "$DEPLOY_DIR/current" "$BACKUP_DIR/current-$(date +%Y%m%d-%H%M%S)"
        fi

        # Prepare staging directory
        STAGING_DIR="$DEPLOY_DIR/staging"
        rm -rf "$STAGING_DIR"
        mkdir -p "$STAGING_DIR"

        echo "Environment prepared successfully"

    - name: Stage 2 - Deploy Application
      shell: |
        #!/bin/bash
        set -euo pipefail

        DEPLOY_DIR="{{ deploy_dir }}"
        APP_VERSION="{{ app_version }}"
        STAGING_DIR="$DEPLOY_DIR/staging"

        echo "=== Stage 2: Deploy Application ==="

        # Download application
        echo "Downloading application version $APP_VERSION..."
        curl -L "https://artifacts.example.com/app-$APP_VERSION.tar.gz" | tar -xz -C "$STAGING_DIR"

        # Install dependencies
        if [ -f "$STAGING_DIR/requirements.txt" ]; then
            echo "Installing Python dependencies..."
            pip3 install -r "$STAGING_DIR/requirements.txt"
        fi

        # Set permissions
        chown -R www-data:www-data "$STAGING_DIR"
        find "$STAGING_DIR" -type f -exec chmod 644 {} \;
        find "$STAGING_DIR" -type d -exec chmod 755 {} \;

        # Create symlinks
        ln -sf "$STAGING_DIR" "$DEPLOY_DIR/new"

        echo "Application deployed successfully"

    - name: Stage 3 - Test and Validate
      shell: |
        #!/bin/bash
        set -e

        DEPLOY_DIR="{{ deploy_dir }}"
        NEW_VERSION_DIR="$DEPLOY_DIR/new"

        echo "=== Stage 3: Test and Validate ==="

        # Run unit tests
        if [ -f "$NEW_VERSION_DIR/tests/run-tests.sh" ]; then
            echo "Running unit tests..."
            "$NEW_VERSION_DIR/tests/run-tests.sh"
        fi

        # Health check
        echo "Performing health check..."
        timeout 30 bash -c 'until curl -sf http://localhost/health; do sleep 2; done'

        # Performance test
        RESPONSE_TIME=$(curl -w "%{time_total}" -o /dev/null -s http://localhost/)
        if (( $(echo "$RESPONSE_TIME > 5.0" | bc -l) )); then
            echo "Performance test failed: ${RESPONSE_TIME}s" >&2
            exit 1
        fi

        echo "Validation completed successfully"

      register: validation_result
      ignore_errors: yes

    - name: Stage 4 - Activate Deployment
      shell: |
        #!/bin/bash
        set -euo pipefail

        DEPLOY_DIR="{{ deploy_dir }}"
        NEW_VERSION_DIR="$DEPLOY_DIR/new"

        echo "=== Stage 4: Activate Deployment ==="

        # Atomic switch
        mv "$NEW_VERSION_DIR" "$DEPLOY_DIR/current-tmp"
        ln -sfn "$DEPLOY_DIR/current-tmp" "$DEPLOY_DIR/current"
        rm -rf "$DEPLOY_DIR/current-tmp"

        # Restart services
        systemctl restart nginx
        systemctl restart app

        # Verify services
        systemctl is-active nginx
        systemctl is-active app

        echo "Deployment activated successfully"

      when: validation_result is succeeded

    - name: Stage 5 - Cleanup and Notification
      shell: |
        #!/bin/bash
        set -e

        DEPLOY_DIR="{{ deploy_dir }}"
        BACKUP_DIR="{{ backup_dir }}"

        echo "=== Stage 5: Cleanup and Notification ==="

        # Cleanup old backups (keep last 5)
        ls -td "$BACKUP_DIR"/*/ 2>/dev/null | tail -n +6 | xargs rm -rf

        # Send notification
        curl -X POST "{{ slack_webhook }}" \
          -H "Content-Type: application/json" \
          -d "{
            \"text\": \"Application deployment completed successfully\",
            \"attachments\": [{
              \"color\": \"good\",
              \"fields\": [
                {\"title\": \"Host\", \"value\": \"{{ inventory_hostname }}\", \"short\": true},
                {\"title\": \"Version\", \"value\": \"{{ app_version }}\", \"short\": true},
                {\"title\": \"Timestamp\", \"value\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}
              ]
            }]
          }"

        echo "Cleanup and notification completed"

🧪 Testing and Validation

Unit Testing Shell Scripts in Ansible

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/bin/bash
# test-ansible-scripts.sh - Test framework for Ansible shell scripts

# Setup test environment
setup_test_environment() {
    TEST_DIR=$(mktemp -d)
    cd "$TEST_DIR"

    # Create mock directories
    mkdir -p {/var/www,/var/backups,/opt/app/migrations}

    # Create mock files
    echo "SELECT version FROM schema_migrations ORDER BY applied_at DESC LIMIT 1;" > mock-query.sql
    echo "Mock migration content" > /opt/app/migrations/001_initial.sql

    # Export test variables
    export TEST_DIR
    export MOCK_DB_HOST="localhost"
    export MOCK_DB_NAME="testdb"
}

# Test script execution
test_script_execution() {
    local script_content="$1"
    local expected_exit_code="${2:-0}"

    echo "Testing script execution..."

    # Create temporary script
    local temp_script
    temp_script=$(mktemp)
    echo "$script_content" > "$temp_script"
    chmod +x "$temp_script"

    # Execute script
    local exit_code=0
    "$temp_script" || exit_code=$?

    if [ "$exit_code" -eq "$expected_exit_code" ]; then
        echo "✅ Script execution test passed"
    else
        echo "❌ Script execution failed with exit code $exit_code (expected $expected_exit_code)" >&2
        return 1
    fi

    rm -f "$temp_script"
}

# Test environment variable handling
test_environment_variables() {
    local script_content="$1"
    local required_vars=("$@")

    echo "Testing environment variable handling..."

    # Test with missing variables
    for var in "${required_vars[@]}"; do
        # Temporarily unset variable
        local original_value="${!var}"
        unset "$var"

        # Test script behavior
        if echo "$script_content" | bash >/dev/null 2>&1; then
            echo "❌ Script should fail when $var is missing" >&2
            return 1
        fi

        # Restore variable
        if [ -n "$original_value" ]; then
            export "$var"="$original_value"
        fi
    done

    echo "✅ Environment variable handling test passed"
}

# Run all tests
run_all_tests() {
    setup_test_environment

    # Test basic script execution
    local test_script='#!/bin/bash
echo "Hello, World!"
exit 0'

    test_script_execution "$test_script" 0

    # Test error handling
    local error_script='#!/bin/bash
echo "This will fail" >&2
exit 1'

    test_script_execution "$error_script" 1

    # Test environment variables
    local env_script='#!/bin/bash
if [ -z "$REQUIRED_VAR" ]; then
    echo "REQUIRED_VAR is not set" >&2
    exit 1
fi
echo "REQUIRED_VAR is set to: $REQUIRED_VAR"'

    export REQUIRED_VAR="test_value"
    test_script_execution "$env_script" 0

    unset REQUIRED_VAR
    test_script_execution "$env_script" 1

    echo "All tests completed successfully"
}

# Execute tests
run_all_tests

🧾 Summary

Command module: For simple, secure commands without shell interpretation ✅ Shell module: For complex logic with full shell features ✅ Dynamic variables: Generate variables using shell scripts ✅ Error handling: Robust retry logic and failure management ✅ Security: Secure credential and temporary file handling ✅ Environment awareness: Context-specific configuration ✅ Testing: Comprehensive test frameworks for shell scripts


🧾 See Also