📌 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

  1. Introduction
  2. Key Features of the Script
  3. OCI IAM Configuration Guide
  4. Prerequisites
  5. Download the Backup Script
  6. Usage & Configuration
  7. Automating Backups with Cron
  8. 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 custom ERR and EXIT traps 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 12 as a warning instead of a fatal crash.
  • Self-cleaning Workspace: Registers cleanups so that temporary backup files in /tmp are 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

  1. Go to Identity & Security -> Dynamic Groups in the OCI Console.
  2. Click Create Dynamic Group.
  3. Name it (e.g., Backup_Compute_Group).
  4. 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...'

Step 2: Create an IAM Policy

  1. Navigate to Identity & Security -> Policies.
  2. Click Create Policy.
  3. Define the policy statement allowing the group write privileges:
    Allow dynamic-group Backup_Compute_Group to manage objects in compartment Target_Compartment where target.bucket.name = 'Target_Bucket_Name'
    Note: Using manage objects allows 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:

  1. OCI CLI: Required for OCI API communication. (Installation Docs)
  2. 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)"