πŸ“Œ OCI CLI Automated Compute Shutdown & Startup Utility for Non-Business Hours

8/1/2020

☁️

An enterprise-grade, secure Bash solution for Oracle Cloud Infrastructure (OCI) designed to run via cron from a Master Management Node to automatically shut down development, testing, and staging instances during non-business hours (and restart them in the morning).


πŸ“Œ 1. Description & Overview

In cloud environments, non-production instances (Development, Staging, QA) left running 24/7 consume unnecessary compute budget. Shutting down non-production VMs outside of standard business hours (e.g., stopping at 7:00 PM and starting at 7:00 AM Monday through Friday) can reduce monthly compute billing by ~65% to 70%.

a) Enterprise Authentication via Instance Principals: Eliminates storing API private key files on disk by using OCI Dynamic Groups (MasterManagementNodeGroup) and IAM policy statements:

Allow dynamic-group MasterManagementNodeGroup to inspect instances in compartment DevCompartment
Allow dynamic-group MasterManagementNodeGroup to use instance-family in compartment DevCompartment where any {request.operation = 'InstanceAction'}

b) Graceful ACPI Shutdown (SOFTSTOP): Uses –action SOFTSTOP instead of raw –action STOP to send an ACPI signal to the guest Linux/Windows OS, allowing databases (Oracle DB, PostgreSQL, MySQL) and applications to flush state to disk cleanly before powering down.

c) Cron-Safe Environment Handling: Explicitly exports system PATH=β€œ/usr/local/bin:/usr/bin:/bin:${PATH}” to prevent cron execution failures (oci: command not found).

d) Flexible Targeting: Supports filtering by display name pattern (e.g. dev) AND/OR freeform tag matching (AutoShutdown: true).

e) Dry-Run Testing Mode: Supports setting DRY_RUN=true to test and list targets without executing real instance state changes.

Solution Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             OCI Master Management Node                 β”‚
β”‚  (Cron Scheduler + OCI CLI + Instance Principals)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
             Executes Cron at 19:00 Mon-Fri
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             Oracle Cloud Infrastructure (OCI)          β”‚
β”‚               dev-compartment (Dev VMs)                β”‚
β”‚  [VM-Dev-01: SOFTSTOP] [VM-Dev-02: SOFTSTOP] ...      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”‘ 2. IAM Policies & Authentication Setup

To run OCI CLI commands securely from a Master VM without storing private API keys or credentials on disk, use Instance Principals with Dynamic Groups.

Step 2.1: Create a Dynamic Group

In the OCI Console, navigate to Identity & Security -> Dynamic Groups and create a group:

  • Name: MasterManagementNodeGroup
  • Matching Rule: Add the OCID of your Master Management VM:
    ANY {instance.id = 'ocid1.instance.oc1.iad.example_master_vm_ocid'}

Step 2.2: Create IAM Policy Statements

In Identity & Security -> Policies, create a policy scoped to your target development compartment:

  • Policy Name: DevComputeAutoShutdownPolicy
  • Statements:
    Allow dynamic-group MasterManagementNodeGroup to inspect instances in compartment DevCompartment
    Allow dynamic-group MasterManagementNodeGroup to use instance-family in compartment DevCompartment where any {request.operation = 'InstanceAction'}

[!TIP] Using request.operation = 'InstanceAction' restricts the master node so it can only issue lifecycle actions (START, STOP, SOFTSTOP, RESET) without permission to delete, modify, or terminate resources.


πŸ› οΈ 3. Prerequisites & Master Node Setup

On your Master Management VM, ensure the following prerequisites are installed:

  1. OCI CLI Installation:
    sudo bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"
  2. Verify Installation:
    /usr/local/bin/oci --version
  3. Verify Instance Principal Auth:
    oci compute instance list --auth instance_principal --compartment-id <YOUR_COMPARTMENT_OCID> --limit 1

πŸ“œ 4. Production Scripts

Script 1: Timer Shutdown (oci_dev_Timer_shutdown.sh)

#!/usr/bin/env bash
# ==============================================================================
# Script: oci_dev_Timer_shutdown.sh
# Purpose: Oracle Cloud Infrastructure (OCI) Compute Instance Auto-Shutdown
#          Designed for cron execution on a Master/Management VM during
#          non-business hours to reduce cloud compute costs.
# ==============================================================================

set -euo pipefail

# ------------------------------------------------------------------------------
# ENVIRONMENT & CRON PATH SETUP
# ------------------------------------------------------------------------------
# Cron runs in a stripped environment. Explicitly export system paths.
export PATH="/usr/local/bin:/usr/bin:/bin:${PATH:-}"

# ------------------------------------------------------------------------------
# CONFIGURATION & PARAMETERS
# ------------------------------------------------------------------------------
COMPARTMENT_OCID="${COMPARTMENT_OCID:-ocid1.compartment.oc1..example_ocid_here}"
LOG_FILE="${LOG_FILE:-/var/log/oci_dev_shutdown.log}"
NAME_PATTERN="${NAME_PATTERN:-dev}"   # Case-insensitive substring in display_name
TAG_KEY="${TAG_KEY:-AutoShutdown}"     # Optional Freeform Tag key
TAG_VALUE="${TAG_VALUE:-true}"        # Optional Freeform Tag value

# OCI Authentication mode:
# Set to "instance_principal" if master VM uses Dynamic Groups (Recommended)
# Set to "api_key" (or leave empty) to use standard ~/.oci/config
OCI_AUTH_TYPE="${OCI_AUTH_TYPE:-instance_principal}"

# Action: SOFTSTOP (Graceful ACPI shutdown) vs STOP (Power off)
STOP_ACTION="${STOP_ACTION:-SOFTSTOP}"

# Operational Flags
DRY_RUN="${DRY_RUN:-false}"   # Set to "true" for testing without stopping VMs

# ------------------------------------------------------------------------------
# LOGGING HELPER
# ------------------------------------------------------------------------------
log() {
    local msg="$1"
    local timestamp
    timestamp=$(date "+%Y-%m-%d %H:%M:%S %Z")
    echo "[${timestamp}] ${msg}" | tee -a "${LOG_FILE}" 2>/dev/null || echo "[${timestamp}] ${msg}"
}

log "====== Starting OCI Development Instance Timer Shutdown ======"

# ------------------------------------------------------------------------------
# PRE-FLIGHT CHECKS
# ------------------------------------------------------------------------------
if ! command -v oci &>/dev/null; then
    log "[-] ERROR: OCI CLI executable ('oci') not found in PATH (${PATH}). Exiting."
    exit 1
fi

if [[ "${COMPARTMENT_OCID}" == *"example_ocid_here"* ]]; then
    log "[-] ERROR: Please set COMPARTMENT_OCID to a valid OCI Compartment OCID."
    exit 1
fi

AUTH_FLAG=""
if [ "${OCI_AUTH_TYPE}" == "instance_principal" ]; then
    AUTH_FLAG="--auth instance_principal"
fi

# ------------------------------------------------------------------------------
# 1. FETCH RUNNING INSTANCES
# ------------------------------------------------------------------------------
log "[+] Querying OCI Compute instances in compartment: ${COMPARTMENT_OCID}..."
log "[+] Filtering rules: State=RUNNING | Name contains '${NAME_PATTERN}' OR Tag '${TAG_KEY}=${TAG_VALUE}'"

# JMESPath Query:
# Filters instances in RUNNING state where display_name contains 'dev'
# OR freeform_tags contains 'AutoShutdown: true'.
JMES_QUERY="data[?lifecycle_state=='RUNNING' && (contains(not_null(display_name, ''), '${NAME_PATTERN}') || freeform_tags.\"${TAG_KEY}\" == '${TAG_VALUE}')].[id, display_name, shape]"

RAW_TARGETS=$(oci compute instance list \
    ${AUTH_FLAG} \
    --compartment-id "${COMPARTMENT_OCID}" \
    --all \
    --query "${JMES_QUERY}" \
    --output json 2>/dev/null || echo "[]")

INSTANCE_COUNT=$(echo "${RAW_TARGETS}" | grep -c '"id"' || echo 0)

if [ "${INSTANCE_COUNT}" -eq 0 ]; then
    log "[*] No active running development instances found matching criteria. Operations complete."
    log "====== Shutdown Process Finished ======"
    exit 0
fi

log "[+] Found ${INSTANCE_COUNT} running target instance(s)."

# ------------------------------------------------------------------------------
# 2. EXECUTE SOFTSTOP / STOP ON EACH INSTANCE
# ------------------------------------------------------------------------------
python3 -c '
import sys, json
try:
    data = json.loads(sys.argv[1])
    for item in data:
        print(f"{item[0]}|{item[1]}|{item[2]}")
except Exception:
    pass
' "${RAW_TARGETS}" | while IFS='|' read -r inst_id inst_name inst_shape; do

    if [ "${DRY_RUN}" == "true" ]; then
        log "[DRY-RUN] Would issue ${STOP_ACTION} to instance '${inst_name}' (${inst_id}, Shape: ${inst_shape})"
    else
        log "[+] Issuing ${STOP_ACTION} command to instance '${inst_name}' (${inst_id})..."
        
        if oci compute instance action \
            ${AUTH_FLAG} \
            --instance-id "${inst_id}" \
            --action "${STOP_ACTION}" \
            --output table &>/dev/null; then
            log "    -> Successfully sent ${STOP_ACTION} signal to '${inst_name}'."
        else
            log "    [-] ERROR: Failed to issue ${STOP_ACTION} to '${inst_name}'."
        fi
    fi
done

log "====== Shutdown Process Completed Successfully ======"

Script 2: Morning Startup (oci_dev_morning_startup.sh)

#!/usr/bin/env bash
# ==============================================================================
# Script: oci_dev_morning_startup.sh
# Purpose: Oracle Cloud Infrastructure (OCI) Compute Instance Auto-Startup
#          Partner script to start stopped dev instances at 7:00 AM Mon-Fri.
# ==============================================================================

set -euo pipefail

export PATH="/usr/local/bin:/usr/bin:/bin:${PATH:-}"

COMPARTMENT_OCID="${COMPARTMENT_OCID:-ocid1.compartment.oc1..example_ocid_here}"
LOG_FILE="${LOG_FILE:-/var/log/oci_dev_startup.log}"
NAME_PATTERN="${NAME_PATTERN:-dev}"
TAG_KEY="${TAG_KEY:-AutoShutdown}"
TAG_VALUE="${TAG_VALUE:-true}"
OCI_AUTH_TYPE="${OCI_AUTH_TYPE:-instance_principal}"
DRY_RUN="${DRY_RUN:-false}"

log() {
    local msg="$1"
    local timestamp
    timestamp=$(date "+%Y-%m-%d %H:%M:%S %Z")
    echo "[${timestamp}] ${msg}" | tee -a "${LOG_FILE}" 2>/dev/null || echo "[${timestamp}] ${msg}"
}

log "====== Starting OCI Development Instance Morning Startup ======"

AUTH_FLAG=""
if [ "${OCI_AUTH_TYPE}" == "instance_principal" ]; then
    AUTH_FLAG="--auth instance_principal"
fi

JMES_QUERY="data[?lifecycle_state=='STOPPED' && (contains(not_null(display_name, ''), '${NAME_PATTERN}') || freeform_tags.\"${TAG_KEY}\" == '${TAG_VALUE}')].[id, display_name, shape]"

RAW_TARGETS=$(oci compute instance list \
    ${AUTH_FLAG} \
    --compartment-id "${COMPARTMENT_OCID}" \
    --all \
    --query "${JMES_QUERY}" \
    --output json 2>/dev/null || echo "[]")

INSTANCE_COUNT=$(echo "${RAW_TARGETS}" | grep -c '"id"' || echo 0)

if [ "${INSTANCE_COUNT}" -eq 0 ]; then
    log "[*] No stopped development instances found matching criteria. Operations complete."
    log "====== Startup Process Finished ======"
    exit 0
fi

log "[+] Found ${INSTANCE_COUNT} stopped instance(s) to power ON."

python3 -c '
import sys, json
try:
    data = json.loads(sys.argv[1])
    for item in data:
        print(f"{item[0]}|{item[1]}|{item[2]}")
except Exception:
    pass
' "${RAW_TARGETS}" | while IFS='|' read -r inst_id inst_name inst_shape; do

    if [ "${DRY_RUN}" == "true" ]; then
        log "[DRY-RUN] Would issue START to instance '${inst_name}' (${inst_id})"
    else
        log "[+] Issuing START command to instance '${inst_name}' (${inst_id})..."
        if oci compute instance action ${AUTH_FLAG} --instance-id "${inst_id}" --action START --output table &>/dev/null; then
            log "    -> Successfully sent START signal to '${inst_name}'."
        else
            log "    [-] ERROR: Failed to issue START to '${inst_name}'."
        fi
    fi
done

log "====== Startup Process Completed Successfully ======"

⏰ 5. Cron Job Setup & Scheduling Guide

  1. Make both scripts executable and place them in /opt/scripts/:

    sudo mkdir -p /opt/scripts
    sudo cp oci_dev_Timer_shutdown.sh oci_dev_morning_startup.sh /opt/scripts/
    sudo chmod +x /opt/scripts/oci_dev_*.sh
  2. Configure crontab for root (or service user):

    sudo crontab -e
  3. Add the schedule entries:

    # Environment Variables for Cron
    COMPARTMENT_OCID="ocid1.compartment.oc1..oc1_your_real_compartment_ocid"
    OCI_AUTH_TYPE="instance_principal"
    STOP_ACTION="SOFTSTOP"
    
    # 1. Timer Shutdown: Stop Dev Instances at 7:00 PM (19:00) Monday through Friday
    0 19 * * 1-5 /opt/scripts/oci_dev_Timer_shutdown.sh >> /var/log/oci_shutdown_cron.log 2>&1
    
    # 2. Morning Startup: Start Dev Instances at 7:00 AM (07:00) Monday through Friday
    0 7 * * 1-5 /opt/scripts/oci_dev_morning_startup.sh >> /var/log/oci_startup_cron.log 2>&1

⚠️ 6. Precautions & Important OCI Billing Caveats

[!WARNING]

1. Graceful ACPI Shutdown (SOFTSTOP) vs Power Off (STOP)

  • SOFTSTOP (Recommended): Sends an ACPI shutdown signal to the guest OS. Allows Linux/Windows services, databases (e.g. Oracle DB, PostgreSQL, MySQL), and applications to flush data to disk cleanly before powering down.
  • STOP: Immediately cuts power to the VM. Can cause database corruption or filesystem dirty states if databases are running.

[!IMPORTANT]

2. Storage & IP Billing Remains Active During Shutdown

Stopping a Compute Instance stops Compute CPU/RAM OCPU charges, but does NOT eliminate all costs:

  • Boot Volumes & Block Volumes: Boot volumes (e.g., 50GB–100GB) and attached Block Volumes remain provisioned and continue to incur block storage charges ($0.0255/GB/month).
  • Reserved Public IPs: Unattached Reserved Public IPs may incur a nominal fee if left unattached depending on region policies.
  • Ephemeral Public IPs: If an instance uses an Ephemeral Public IP, restarting the instance will assign a new public IP address. Use OCI DNS or Reserved Public IPs for persistent endpoints.

[!NOTE]

3. OCI CLI Path in Cron

cron runs with a bare-bones environment (PATH=/usr/bin:/bin). If OCI CLI is installed under /usr/local/bin/oci or ~/.local/bin/oci, cron will fail with command not found unless export PATH is explicitly defined inside the script (as implemented above).

[!TIP]

4. Dry-Run Verification

Test your script before scheduling by running in dry-run mode:

DRY_RUN=true COMPARTMENT_OCID="ocid1.compartment..." ./oci_dev_Timer_shutdown.sh