π 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:
- OCI CLI Installation:
sudo bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)" - Verify Installation:
/usr/local/bin/oci --version - 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
-
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 -
Configure
crontabforroot(or service user):sudo crontab -e -
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
cronruns with a bare-bones environment (PATH=/usr/bin:/bin). If OCI CLI is installed under/usr/local/bin/ocior~/.local/bin/oci,cronwill fail withcommand not foundunlessexport PATHis 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