📌 Host Files Backups to OCI Object Storage using Resource Principal
Automating backups from a Compute Virtual Machine (VM) to Oracle Cloud Infrastructure (OCI) Object Storage typically requires managing API keys or configuration files. Storing credentials locally on a VM increases the security blast radius.
In this article, we showcase a fail-proof Bash script that uses OCI Resource Principal authentication—removing the need for local credentials entirely—and details how to configure OCI and automate the process.
Table of Contents
- Introduction
- Key Features of the Script
- OCI IAM Configuration Guide
- Prerequisites
- Download the Backup Script
- Usage & Configuration
- Automating Backups with Cron
- Full Script Source Code
Introduction
Resource Principal is an IAM feature in Oracle Cloud Infrastructure that allows resources, such as Compute instances, to make API calls to other OCI services. By matching your instances inside a Dynamic Group and applying policies, the OCI CLI automatically retrieves short-lived session tokens from the local instance metadata service (IMDS). This makes cloud uploads secure and compliant with security best practices.
Key Features of the Script
- Zero Credentials on Disk: Uses
export OCI_CLI_AUTH="resource_principal"for all OCI API transactions. - Pre-flight Connectivity Validation: Verifies both OCI CLI namespace access and target bucket write permissions before creating zip archives to prevent unnecessary processor load.
- Traceable Error Trapping: Utilizes strict Bash settings (
set -euo pipefail) along with customERRandEXITtraps to output the exact line number (LINENO) and command (BASH_COMMAND) if anything fails. - Warning Tolerance (Active Logs): Handles active files (like logs) that grow or shrink during compression by mapping Info-ZIP warning code
12as a warning instead of a fatal crash. - Self-cleaning Workspace: Registers cleanups so that temporary backup files in
/tmpare removed on successful exit or failure. - Cron-Friendly Output: Automatically detects interactive/non-interactive terminals and suppresses progress bar carriage returns (
\r) to ensure cron log files don’t bloat.
OCI IAM Configuration Guide
To enable Resource Principal authentication, you must configure a Dynamic Group and an IAM Policy in the OCI Console.
Step 1: Create a Dynamic Group
- Go to Identity & Security -> Dynamic Groups in the OCI Console.
- Click Create Dynamic Group.
- Name it (e.g.,
Backup_Compute_Group). - Define the matching rule to identify your VM instance:
- Rule for a single instance:
Any {instance.id = 'ocid1.instance.oc1.iad.anuw...'} - Rule for all instances in a compartment:
instance.compartment.id = 'ocid1.compartment.oc1..aaaaaaa...'
- Rule for a single instance:
Step 2: Create an IAM Policy
- Navigate to Identity & Security -> Policies.
- Click Create Policy.
- Define the policy statement allowing the group write privileges:
Note: UsingAllow dynamic-group Backup_Compute_Group to manage objects in compartment Target_Compartment where target.bucket.name = 'Target_Bucket_Name'manage objectsallows the script to check bucket properties to ensure it exists before starting the compression.
Prerequisites
Make sure the following tools are installed on the local virtual machine:
- OCI CLI: Required for OCI API communication. (Installation Docs)
- zip: Utilized to package the directory.
# Debian/Ubuntu sudo apt-get install zip -y # RHEL/CentOS/Oracle Linux sudo yum install zip -y
Download the Backup Script
You can download the script directly or host it in your website’s static files.
📥 Download oci_backup.sh (Right-click and select “Save Link As…”)
Usage & Configuration
Give the script execute permissions and run it:
chmod +x oci_backup.sh
./oci_backup.sh -a /path/to/app -b my-target-bucket
Script Arguments
| Parameter | Required/Optional | Description |
|---|---|---|
-a |
Required | Path to the application directory to zip and upload. |
-b |
Required | Target OCI Object Storage bucket. |
-n |
Optional | OCI Namespace (autodetected if omitted). |
-l |
Optional | Custom log file path (falls back to ./oci_backup.log). |
-t |
Optional | Custom temporary directory for building the zip (Default: /tmp). |
-o |
Optional | Custom name for the object in Object Storage. |
Automating Backups with Cron
To automate backups (e.g., every night at 2:00 AM), edit the user’s crontab:
crontab -e
Add the following cron configuration, adjusting paths:
PATH=/usr/local/bin:/usr/bin:/bin
0 2 * * * /usr/local/bin/oci_backup.sh -a /var/www/my-app -b backup-bucket >> /var/log/backup_cron.log 2>&1
Full Script Source Code
Below is the complete implementation of oci_backup.sh including strict error handling, dependency validation, and logging utilities:
#!/usr/bin/env bash
#
# ==============================================================================
# OCI Backup Script using Resource Principal
# ==============================================================================
# Strict error handling
set -euo pipefail
# Global Configuration / Variables Defaults
APP_DIR=""
BUCKET_NAME=""
OCI_NAMESPACE=""
LOG_FILE=""
TEMP_DIR="/tmp"
OBJECT_NAME=""
TEMP_ZIP_PATH=""
# Helper to log messages to the console and log file
log() {
local level="$1"
local msg="$2"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local log_line="[$timestamp] [$level] $msg"
if [[ -n "${LOG_FILE:-}" ]]; then
echo "${log_line}" >> "${LOG_FILE}"
fi
if [[ "${level}" == "ERROR" ]]; then
echo "${log_line}" >&2
else
echo "${log_line}"
fi
}
# Print usage instructions
usage() {
cat <<EOF
Usage: $(basename "$0") -a <app_dir> -b <bucket_name> [options]
Required Arguments:
-a <app_dir> Path to the application directory to backup.
-b <bucket_name> Name of the target OCI Object Storage bucket.
Optional Arguments:
-n <namespace> OCI Object Storage Namespace. (Autodetected if omitted)
-l <log_file> Path to the script's log file. (Default: /var/log/oci_backup.log or ./oci_backup.log)
-t <temp_dir> Path to temporary directory for zip creation. (Default: /tmp)
-o <object_name> Custom name for the uploaded object in Object Storage.
-h Show this help message.
EOF
exit 1
}
# Cleanup handler to remove temporary zip files on exit
cleanup() {
local exit_code=$?
if [[ -n "${TEMP_ZIP_PATH:-}" ]] && [[ -f "${TEMP_ZIP_PATH}" ]]; then
log "INFO" "Cleaning up temporary archive: ${TEMP_ZIP_PATH}"
rm -f "${TEMP_ZIP_PATH}"
fi
if [ "${exit_code}" -eq 0 ]; then
log "INFO" "Backup process completed successfully."
else
log "ERROR" "Backup process terminated unexpectedly with exit code ${exit_code}."
fi
}
# Detailed error handler for tracking line number of failure
error_handler() {
local line_no=$1
local last_command=$2
local exit_code=$3
log "ERROR" "Command '${last_command}' failed on line ${line_no} with exit status ${exit_code}."
}
# Register traps
trap cleanup EXIT
trap 'error_handler ${LINENO} "${BASH_COMMAND}" $?' ERR
# Argument Parsing
while getopts "a:b:n:l:t:o:h" opt; do
case "${opt}" in
a) APP_DIR="${OPTARG}" ;;
b) BUCKET_NAME="${OPTARG}" ;;
n) OCI_NAMESPACE="${OPTARG}" ;;
l) LOG_FILE="${OPTARG}" ;;
t) TEMP_DIR="${OPTARG}" ;;
o) OBJECT_NAME="${OPTARG}" ;;
h) usage ;;
*) usage ;;
esac
done
# Logging Initialization
if [[ -z "${LOG_FILE}" ]]; then
if [[ -w "/var/log" ]]; then
LOG_FILE="/var/log/oci_backup.log"
else
LOG_FILE="./oci_backup.log"
fi
fi
LOG_DIR=$(dirname "${LOG_FILE}")
if [[ ! -d "${LOG_DIR}" ]]; then
mkdir -p "${LOG_DIR}"
fi
log "INFO" "Starting backup process..."
# Input validations
if [[ -z "${APP_DIR}" ]] || [[ -z "${BUCKET_NAME}" ]]; then
log "ERROR" "Missing required arguments: -a <app_dir> and -b <bucket_name> are required."
usage
fi
if ! command -v oci >/dev/null 2>&1; then
log "ERROR" "OCI CLI is not installed or not present in system PATH."
exit 1
fi
if ! command -v zip >/dev/null 2>&1; then
log "ERROR" "The 'zip' utility is not installed. Please install it to proceed."
exit 1
fi
if [[ ! -d "${APP_DIR}" ]] || [[ ! -r "${APP_DIR}" ]]; then
log "ERROR" "Target application directory does not exist or is not readable: ${APP_DIR}"
exit 1
fi
# Configure OCI CLI & Resource Principal Authentication
export OCI_CLI_AUTH="resource_principal"
# Verify OCI CLI and fetch namespace
if [[ -z "${OCI_NAMESPACE}" ]]; then
log "INFO" "OCI Namespace not provided. Querying metadata..."
local ns_output
local ns_exit_code=0
ns_output=$(oci os ns get --query "data" --output text 2>&1) || ns_exit_code=$?
if [[ "${ns_exit_code}" -ne 0 ]]; then
log "ERROR" "Failed to retrieve OCI Namespace. Verify Dynamic Group and IAM Policies."
log "ERROR" "OCI CLI Error details: ${ns_output}"
exit 1
fi
OCI_NAMESPACE="${ns_output}"
fi
log "INFO" "Resolved OCI Namespace: ${OCI_NAMESPACE}"
# Check bucket accessibility
log "INFO" "Checking access to bucket '${BUCKET_NAME}'..."
local bucket_check
local check_exit_code=0
bucket_check=$(oci os bucket get -ns "${OCI_NAMESPACE}" -bn "${BUCKET_NAME}" --query "data.name" --output text 2>&1) || check_exit_code=$?
if [[ "${check_exit_code}" -ne 0 ]]; then
log "ERROR" "Failed to access bucket '${BUCKET_NAME}'. OCI CLI Error: ${bucket_check}"
exit 1
fi
log "INFO" "Bucket verification succeeded. Access granted."
# Create Zip Archive
APP_DIR_ABS=$(cd "${APP_DIR}" && pwd)
APP_DIR_NAME=$(basename "${APP_DIR_ABS}")
PARENT_DIR=$(dirname "${APP_DIR_ABS}")
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
TEMP_ZIP_PATH="${TEMP_DIR}/${APP_DIR_NAME}_backup_${TIMESTAMP}.zip"
if [[ -z "${OBJECT_NAME}" ]]; then
OBJECT_NAME="${APP_DIR_NAME}_backup_${TIMESTAMP}.zip"
fi
log "INFO" "Archiving application directory: ${APP_DIR_ABS}"
local zip_output
local zip_exit_code=0
zip_output=$( (cd "${PARENT_DIR}" && zip -r "${TEMP_ZIP_PATH}" "${APP_DIR_NAME}") 2>&1 ) || zip_exit_code=$?
if [[ "${zip_exit_code}" -ne 0 ]] && [[ "${zip_exit_code}" -ne 12 ]]; then
log "ERROR" "Failed to create zip archive. Exit code: ${zip_exit_code}."
log "ERROR" "Zip CLI details: ${zip_output}"
exit 2
elif [[ "${zip_exit_code}" -eq 12 ]]; then
log "WARNING" "Zip completed with warning (some files changed during compression, e.g. active logs). Continuing backup."
else
log "INFO" "Successfully created zip archive."
fi
# Upload to OCI Object Storage
log "INFO" "Uploading backup archive to OCI Object Storage bucket: ${BUCKET_NAME}..."
local upload_output
local upload_exit_code=0
upload_output=$(oci os object put \
-ns "${OCI_NAMESPACE}" \
-bn "${BUCKET_NAME}" \
--file "${TEMP_ZIP_PATH}" \
--name "${OBJECT_NAME}" 2>&1) || upload_exit_code=$?
if [[ "${upload_exit_code}" -ne 0 ]]; then
log "ERROR" "Failed to upload archive to OCI Object Storage. Exit code: ${upload_exit_code}"
log "ERROR" "OCI CLI Output: ${upload_output}"
exit 3
fi
log "INFO" "Backup upload completed successfully."
log "INFO" "Backup size: $(du -sh "${TEMP_ZIP_PATH}" | cut -f1)"