paint-brush
A DevOps Approach to AEM Packages: Automating Creation, Configuration, and Moreby@realgpp
252 reads

A DevOps Approach to AEM Packages: Automating Creation, Configuration, and More

by Giuseppe BaglioFebruary 16th, 2025
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

The `create-remote-aem-pkg.sh script automates interactions with AEM’s Package Manager API, offering a structured approach to package creation, configuration, and distribution. Designed for developers and administrators, it replaces manual workflows with a command-line-driven process that emphasizes consistency and reliability.
featured image - A DevOps Approach to AEM Packages: Automating Creation, Configuration, and More
Giuseppe Baglio HackerNoon profile picture

Adobe Experience Manager (AEM) packages are the unsung heroes of content management — powerful containers that bundle everything from code and configurations to critical content. But let’s face it: manually creating, configuring, and downloading these packages can feel like a tedious dance of clicks.


What if you could automate this process with a few keystrokes, ensuring consistency, speed, reliability, and less heavy lifting?


I will show you a Bash script that flips the script (pun intended!) on how AEM developers and admins work with the Package Manager API. Think about crafting packages in seconds, tailoring filters on the fly, and snagging backups with surgical precision — all before your coffee cools to that perfect sipping temperature. ☕


Before we dive in, a quick note: This article is a deep dive, meticulously detailed and unapologetically technical. We’ll dissect the script’s logic, explore AEM API intricacies, and troubleshoot edge cases. For developers eager to jump straight into the code, you can jump to the bottom of the article. But if you’re here to understand the how and why behind the automation, strap in — we’re going all the way down the rabbit hole. 🕳️

1. Script Overview

The create-remote-aem-pkg.sh script automates interactions with AEM’s Package Manager API, offering a structured approach to package creation, configuration, and distribution. Designed for developers and administrators, it replaces manual workflows with a command-line-driven process that emphasizes consistency and reliability.

1.1 Core Functionalities

  • Package Validation: Checks for existing packages to avoid redundancy before initiating creation.
  • Dynamic Filter Injection: Programmatically defines content paths (e.g., /content/dam, /apps) to include in the package.
  • Build Automation: Triggers package compilation and downloads the output to a specified directory, appending a timestamp to filenames for version control.
  • Error Handling: Validates HTTP responses, folder paths, and authentication to provide actionable feedback during failures.
  • Authentication: Supports basic¹ credential-based authentication via curl.

1.2 Key Benefits

  • Efficiency: Reduces manual steps required for package creation, configuration, and download.
  • Consistency: Ensures uniform package structures and naming conventions across environments.
  • Traceability: Detailed logging at each stage (creation, filtering, building, downloading) aids in auditing and troubleshooting.

1.3 Practical Applications

  • Scheduled Backups: Integrate with cron jobs to regularly archive critical content paths.
  • Environment Synchronization: Replicate configurations or content between AEM instances during deployments.
  • Pre-Update Snapshots: Capture stable states of /etc or /apps before applying system updates.

1.4 Prerequisites

  • Access to an AEM instance (credentials, server, port).
  • Basic familiarity with Bash scripting and AEM’s Package Manager.
  • Permission to create and download packages via the AEM API.

1.5 Example Usage

./create-remote-aem-pkg.sh admin securepass123 localhost 4502 backup-group "Content Backup" /backups /content/dam /etc/clientlibs


This command creates a package named “Content Backup” under the group backup-group, including /content/dam and /etc/clientlibs, and saves the output to the /backups directory.

2. Script Breakdown

Let’s dissect the create-remote-aem-pkg.sh script (you can find it at the bottom of the article) to understand how it orchestrates AEM package management. We’ll focus on its structure, key functions, and workflow logic—ideal for developers looking to customize or debug the tool.

2.1 Core Functions

  • _log(): A utility function that prefixes messages with timestamps for clear audit trails.
_log () {
  echo "[$(date +%Y.%m.%d-%H:%M:%S)] $1"
}


Why it matters: Ensures every action (e.g., “Package built”) is logged with context, simplifying troubleshooting.

  • check_last_exec(): Validates the success of prior commands by checking exit codes and API responses.
check_last_exec () {
  # Checks $? (exit status) and $CURL_OUTPUT for errors
  if [ "$status" -ne 0 ] || [[ $output =~ .*success\":false* ]]; then
    _log "Error detected!";
    exit 1;
  fi
}

Why it matters: Prevents silent failures by halting execution on critical errors like authentication issues or invalid paths.

2.2 Input Parameters

The script accepts seven positional arguments followed by dynamic filters:

USR="$1" # AEM username
PWD="$2" # AEM password
SVR="$3" # Server host (e.g., localhost)
PORT="$4" # Port (e.g., 4502)
PKG_GROUP="$5" # Package group (e.g., "backups")
PKG_NAME="$6" # Package name (e.g., "dam-backup")
BK_FOLDER="$7" # Backup directory (e.g., "/backups")
shift 7 # Remaining arguments become filters (e.g., "/content/dam")


Positional arguments ensure simplicity, while shift handles variable filter paths flexibly.

2.3 Package Validation & Creation

  • Sanitize Names: Replaces spaces in PKG_NAME with underscores to avoid URL issues.
PKG_NAME=${PKG_NAME// /_}
  • Check Existing Packages: Uses curl to list packages via AEM’s API, avoiding redundant creations.
if [ $(curl ... | grep "$PKG_NAME.zip" | wc -l) -eq 1 ]; then
  _log "Package exists—skipping creation."
else
  curl -X POST ... # Creates the package
fi

2.4 Dynamic Filter Configuration

Constructs a JSON array of filters from input paths:

FILTERS_PARAM=""
for i in "${!FILTERS[@]}"; do
  FILTERS_PARAM+="{\"root\": \"${FILTERS[$i]}\", \"rules\": []}"
  # Adds commas between entries, but not after the last
done


Example output:

[{"root": "/content/dam"}, {"root": "/apps"}]

This JSON is injected into the package definition via AEM’s /crx/packmgr/update.jsp endpoint.

2.5 Build & Download Workflow

  • Build the Package: Triggers compilation using AEM’s build command:

curl -X POST … -F "cmd=build"


Note: The script waits for the build to complete before proceeding.

  • Download: Uses curl to fetch the .zip and save it with a timestamped filename:
BK_FILE="$PKG_NAME-$(date +%Y%m%d-%H%M%S).zip"
curl -o "$BK_FOLDER/$BK_FILE" ...

3. Error Handling, Security Notes, & Logging

Robust error handling and logging are critical for unattended scripts like create-remote-aem-pkg.sh, ensuring failures are caught early and logged clearly. Here’s how the script safeguards against unexpected issues and provides actionable insights.

3.1 Logging Mechanism

  • Timestamped Logs: The _log function prefixes every message with a [YYYY.MM.DD-HH:MM:SS] timestamp, creating an audit trail for debugging:

_log "Starting backup process..." # Output: [2023.10.25-14:30:45] Starting backup process...


Why it matters: Timestamps help correlate script activity with AEM server logs or external events (e.g., cron job schedules).

  • Verbose Output: Critical steps, like package creation, filter updates, and downloads, are explicitly logged to track progress.

3.2 Error Validation Workflow

Pre-Flight Checks:

  • Validates the existence of the backup folder (BK_FOLDER) before proceeding:
if [ ! -d "$BK_FOLDER" ]; then  
  _log "Backup folder '$BK_FOLDER' does not exist!" && exit 1  
fi  
  • Sanitizes PKG_NAME to avoid URL issues (e.g., spaces replaced with underscores).


API Response Validation:

The check_last_exec function examines both shell exit codes ($?) and AEM API responses:

check_last_exec "Error message" "$CURL_OUTPUT" $CURL_STATUS

  • Exit Codes: Non-zero values (e.g., curl network failures) trigger immediate exits.


  • API Errors: Detects success\":false JSON responses or "HTTP ERROR" strings in AEM output.


3.3 HTTP Status Verification: When downloading the package, the script checks for a 200 status code:

if [ "$(curl -w "%{http_code}" ...)" -eq "200" ]; then  
  # Proceed if download succeeds  
else  
  _log "Error downloading the package!" && exit 1  
fi  

3.4 Common Failure Scenarios

  • Invalid credentials: check_last_exec catches 401 Unauthorized responses and exits with a clear error message.
  • Invalid filter path: AEM API returns success:false, the script logs "Error adding filters" and terminates.
  • Disk full: Fails to write BK_FILE, checks file size with -s flag and alerts before exiting.
  • AEM instance unreachable:curl exits with a non-zero code, the script logs "Error building the package".

3.5 Security Considerations

  • SSL Certificate Bypass: The script uses curl -k for simplicity, which skips SSL verification. Recommendation for Production: Replace with --cacert to specify a CA bundle.


  • Plaintext Passwords: Credentials are passed as arguments, which may appear in process logs. Mitigation: Use environment variables or a secrets vault (e.g., $AEM_PASSWORD).

3.6 Debugging Tips

  • Enable Verbose Output: Temporarily add set -x at the script’s start to print executed commands.
  • Test API Calls Manually: Isolate issues by running critical curl commands outside the script
  • Inspect Logs: Redirect script output to a file for later analysis:

./create-remote-aem-pkg.sh ... >> /var/log/aem_backup.log 2>&1

4. Tailoring the Tool to Your Workflow

The create-remote-aem-pkg.sh script is designed to be a starting point—a foundation you can modify to align with your team’s needs. Below are common customizations, along with implementation guidance, to extend its functionality or adapt it to specific use cases.

4.1 Adjusting the Backup Filename Format

The default filename uses a timestamp ($PKG_NAME-$(date +%Y%m%d-%H%M%S).zip). Modify this to include environment names, project IDs, or semantic versioning:

# Example: Include environment (e.g., "dev", "prod")  
BK_FILE="${PKG_NAME}-${ENV}-$(date +%Y%m%d).zip"  

# Example: Add Git commit SHA for traceability  
COMMIT_SHA=$(git rev-parse --short HEAD)  
BK_FILE="${PKG_NAME}-${COMMIT_SHA}.zip"  

Tip: Ensure date/time formats avoid characters forbidden in filenames (e.g., colons : on Windows).

4.2 Expanding or Modifying Filters

The script accepts dynamic paths as filters but you can also hardcode frequently used paths or add exclusions:

# Hardcode essential paths (e.g., "/var/audit")  
DEFAULT_FILTERS=("/content/dam" "/apps" "/var/audit")  
FILTERS=("${DEFAULT_FILTERS[@]}" "${@}")  # Merge with command-line inputs  

# Add exclusion rules (requires AEM API support)  
FILTERS_PARAM+="{\"root\": \"${FILTERS[$i]}\", \"rules\": [{\"modifier\": \"exclude\", \"pattern\": \".*/test/*\"}]}"  

4.3 Enhancing Security


Avoid Plaintext Passwords:

Use environment variables or a secrets manager to inject credentials:

# Fetch password from environment variable  
PWD="$AEM_PASSWORD"  

# Use AWS Secrets Manager (example)  
PWD=$(aws secretsmanager get-secret-value --secret-id aem/prod/password --query SecretString --output text)  


Enforce SSL Validation:
Replacecurl -k (insecure) with a trusted CA certificate:

curl --cacert /path/to/ca-bundle.crt -u "$USR":"$PWD" ...

4.4 Adding Post-Build Actions

Extend the script to trigger downstream processes after a successful download:

# Example: Upload to cloud storage  
aws s3 cp "$BK_FOLDER/$BK_FILE" s3://my-backup-bucket/  

# Example: Validate package integrity  
CHECKSUM=$(sha256sum "$BK_FOLDER/$BK_FILE" | cut -d ' ' -f 1)  
_log "SHA-256 checksum: $CHECKSUM"  

# Example: Clean up old backups (retain last 7 days)  
find "$BK_FOLDER" -name "*.zip" -mtime +7 -exec rm {} \;  

4.5 Adding Notification Alerts

Notify teams of success/failure via Slack, email, or monitoring tools:

# Post to Slack on failure  
curl -X POST -H 'Content-type: application/json' \  
--data "{\"text\":\"🚨 AEM backup failed: $(hostname)\"}" \  
https://hooks.slack.com/services/YOUR/WEBHOOK/URL  

# Send email via sendmail  
if [ $? -ne 0 ]; then  
  echo "Subject: Backup Failed" | sendmail [email protected]  
fi  

5. Conclusion

Managing AEM packages doesn’t have to be a manual, error-prone chore. With the create-remote-aem-pkg.sh script, you can transform package creation, filtering, and distribution into a streamlined, repeatable process. This tool isn’t just about saving time, it’s about enabling consistency, reliability, and scalability in your AEM operations.

Key Takeaways

  1. Automation Wins: By eliminating repetitive GUI interactions, the script reduces human error and frees teams to focus on higher-value tasks.


  2. Flexibility Matters: Whether backing up critical content, syncing environments, or preparing for updates, the script adapts to diverse use cases with minimal tweaking.


  3. Resilience is Key: Built-in logging, error checks, and security considerations ensure the script behaves predictably, even when things go sideways.


Great tools are born from real-world challenges. This script is a starting point; think of it as a foundation to build upon as your team’s needs grow. Whether you’re a solo developer or part of a large DevOps team, automation like this exemplifies how small investments in code can yield outsized returns in productivity and peace of mind.


Ready to take the next step?

  • 🛠️ Customize: Tailor the script using Section 6 as your guide.
  • 🔍 Audit: Review your existing AEM workflows for automation opportunities.
  • 🤝 Share: Mentor your team or write a blog post about your modifications.


Thank you for following along — now go forth and automate! 🚀

Appendix

Complete Code

#!/bin/bash
set -eo pipefail

# The script will create a package thought the package manager api:
# - The package is created, if not already present
# - Package filters are populated accordingly to specified paths
# - Package is builded
# - Package is download to the specified folder

_log () {
  echo "[$(date +%Y.%m.%d-%H:%M:%S)] $1"
}

check_last_exec () {
    local message="$1"
    local output="$2"
    local status=$3

    if [ "$status" -ne 0 ]; then
        echo && echo "$message" && echo
        exit 1
    fi

    if [[ $output =~ .*success\":false* ]] || [[ $output =~ .*"HTTP ERROR"* ]]; then
        _log "$message"
        exit 1
    fi
}

USR="$1"
PWD="$2"
SVR="$3"
PORT="$4"
PKG_GROUP="$5"
PKG_NAME="$6"
BK_FOLDER="$7"

shift 7
# The following paths will be included in the package
FILTERS=($@)
BK_FILE=$PKG_NAME"-"$(date +%Y%m%d-%H%M%S).zip

_log "Starting backup process..."
echo "AEM instance: '$SVR':'$PORT'
AEM User: '$USR'
Package group: $PKG_GROUP
Package name: '$PKG_NAME'
Destination folder: $BK_FOLDER
Destination file: '$BK_FILE'
Filter paths: "
printf '\t%s\n\n' "${FILTERS[@]}"

if [ ! -d "$BK_FOLDER" ]; then
  _log "Backup folder '$BK_FOLDER' does not exist!" && echo
  exit 1
fi

PKG_NAME=${PKG_NAME// /_}
check_last_exec "Error replacing white space chars from package name!" "" $? || exit 1
_log "Removed whitespaces from package name: '$PKG_NAME'"
BK_FILE=$PKG_NAME.zip
_log "Backup file: '$BK_FILE'"

_log "Creating the package..."
if [ $(curl -k -u "$USR":"$PWD" "$SVR:$PORT/crx/packmgr/service.jsp?cmd=ls" 2>/dev/null | grep "$PKG_NAME.zip" | wc -l) -eq 1 ]; then
  _log " Package '$PKG_GROUP/$PKG_NAME' is already present: skipping creation."
else
  curl -k --silent -u "$USR":"$PWD" -X POST \
  "$SVR:$PORT/crx/packmgr/service/.json/etc/packages/$PKG_GROUP/$PKG_NAME?cmd=create" \
  -d packageName="$PKG_NAME" -d groupName="$PKG_GROUP"

  check_last_exec "  Error creating the package!" "" $?
  _log " Package created"
fi

# create filters variable
FILTERS_PARAM=""
ARR_LEN="${#FILTERS[@]}"
for i in "${!FILTERS[@]}"; do

  FILTERS_PARAM=$FILTERS_PARAM"{\"root\": \"${FILTERS[$i]}\", \"rules\": []}"

  T=$((i+1))
  if [ $T -ne $ARR_LEN ]; then
   FILTERS_PARAM=$FILTERS_PARAM", "
  fi
done

# add filters
_log "Adding filters to the package..."
CURL_OUTPUT=$(curl -k --silent -u "$USR":"$PWD" -X POST "$SVR:$PORT/crx/packmgr/update.jsp" \
-F path=/etc/packages/"$PKG_GROUP"/"$PKG_NAME".zip -F packageName="$PKG_NAME" \
-F groupName="$PKG_GROUP" \
-F filter="[$FILTERS_PARAM]" \
-F "_charset_=UTF-8")

CURL_STATUS=$?

# Pass the status to the check_last_exec function
check_last_exec "Error adding filters to the package!" "$CURL_OUTPUT" $CURL_STATUS
_log "  Package filters updated successfully."

# build package
_log "Building the package..."
CURL_OUTPUT=$(curl -k -u "$USR":"$PWD" -X POST \
  "$SVR:$PORT/crx/packmgr/service/script.html/etc/packages/$PKG_GROUP/$PKG_NAME.zip" \
  -F "cmd=build")

check_last_exec " Error building the package!" "$CURL_OUTPUT" $?
_log "  Package built."

# download package
_log "Downloading the package..."
if [ "$(curl -w "%{http_code}" -o "$BK_FOLDER/$BK_FILE" -k --silent -u "$USR":"$PWD" "$SVR:$PORT/etc/packages/$PKG_GROUP/$PKG_NAME.zip")" -eq "200" ]; then
  if [ -f "$BK_FOLDER/$BK_FILE" ] && [ -s "$BK_FOLDER/$BK_FILE" ]; then
    _log "  Package $BK_FILE downloaded in $BK_FOLDER."
    exit 0
  fi
fi

_log "  Error downloading the package!"
exit 1


References

[¹] Skipping SSL verification with curl -k is handy for testing, but you’ll want something sturdier in production (for example --cacert)!


[²] AEM Package Manager Official Documentation