This cheat sheet is for scripts that run from CI, cron or an operations host, where nobody is watching the output. The basics (--all, --query, --raw-output) are in the daily operations cheat sheet. This one covers what breaks when the same script runs across 20 regions and 300 compartments.
Pick the auth mode for where the script runs
| Where it runs | Auth | How |
|---|---|---|
| Laptop, human operator | Session token | oci session authenticate, then --auth security_token |
| OCI compute (cron, ops host) | Instance principal | --auth instance_principal or OCI_CLI_AUTH=instance_principal |
| OCI Functions, Resource Manager, Cloud Shell-style services | Resource principal | --auth resource_principal |
| External CI without federation | API key | Profile in ~/.oci/config |
# Browser login, short-lived token, no long-lived key on disk
oci session authenticate --region eu-frankfurt-1 --profile-name OPS
oci session validate --profile OPS
oci session refresh --profile OPS # extend before it expires
oci os ns get --profile OPS --auth security_token
Tip
Set
OCI_CLI_AUTH,OCI_CLI_PROFILEandOCI_CLI_REGIONat the top of the script so every command uses the same identity and region unless you override it. A script that silently falls back to the operator's DEFAULT profile is a common cause of changes landing in the wrong tenancy.
Loop over every subscribed region
TENANCY=ocid1.tenancy.oc1..aaaa # pass in from the pipeline, don't discover it
for r in $(oci iam region-subscription list --tenancy-id "$TENANCY" \
--query 'data[?status==`READY`]."region-name"' | jq -r '.[]'); do
echo "== $r"
oci search resource structured-search --region "$r" \
--query-text "query instance resources where lifecycleState = 'RUNNING'" \
--query 'data.items[].{name:"display-name",ocid:identifier}' --output table
done
With an API key profile, the tenancy OCID is also in ~/.oci/config (tenancy=). Search is regional, so a tenancy-wide inventory needs the region loop.
Build complex requests from JSON
# Full skeleton for every parameter
oci compute instance launch --generate-full-command-json-input > launch.json
# Skeleton for one complex parameter
oci compute instance launch --generate-param-json-input shape-config
# Run from the file; command-line flags override values in the file
oci compute instance launch --from-json file://launch.json \
--display-name "app-$(date +%s)" --wait-for-state RUNNING
Keep launch.json in Git without secrets. Generate it per environment with jq:
jq --arg s "$SUBNET" '.createVnicDetails.subnetId=$s' launch.json > launch.prod.json
Waiters and work requests
| Flag | Use |
|---|---|
--wait-for-state STATE | Repeatable; returns when the resource reaches any listed state |
--max-wait-seconds N | Stop waiting after N seconds (the command fails, the operation continues) |
--wait-interval-seconds N | Poll interval; raise it in large loops to cut API calls |
oci compute instance action --instance-id $ID --action SOFTSTOP \
--wait-for-state STOPPED --max-wait-seconds 900 --wait-interval-seconds 15
Some asynchronous operations return a work request. On these, --wait-for-state waits for the work request (for example SUCCEEDED) rather than the resource:
WR=$(oci work-requests work-request list -c $C --resource-id $ID \
--query 'data[0].id' --raw-output)
oci work-requests work-request get --work-request-id $WR --query 'data.status' --raw-output
oci work-requests work-request-error list --work-request-id $WR --all
oci work-requests work-request-log-entry list --work-request-id $WR --all
Service-specific work request commands exist too (for example under oci ce, oci db and oci lb). Names vary by service; check oci <service> --help.
Warning
A waiter timeout is not a rollback. If
--max-wait-secondsexpires, the create, resize or delete keeps running. The script must re-check state before retrying, or it can launch duplicates.
Retries, timeouts and throttling
The CLI retries throttled (429) and some transient failures by default.
oci compute instance list -c $C --all --no-retry # fail fast
oci compute instance list -c $C --all --max-retries 8 # more patience
oci os object list -bn big --all --connection-timeout 10 --read-timeout 120
Check oci --help on your build for the global retry and timeout options it supports.
For parallel work, cap concurrency instead of firing hundreds of calls at once:
oci compute instance list -c $C --all --lifecycle-state STOPPED \
--query 'data[].id' | jq -r '.[]' \
| xargs -P 4 -I{} oci compute instance action --instance-id {} --action START
Safe concurrent updates with ETags
Read the ETag, then update only if nothing changed in between:
ETAG=$(oci network nsg get --nsg-id $NSG --query 'etag' --raw-output)
oci network nsg update --nsg-id $NSG --display-name nsg-app-prod --if-match "$ETAG"
# 412 PreconditionFailed = someone else changed it; re-read and decide
This matters for anything that replaces a full list in one call, such as security list rules, route table rules and policy statements.
Canned queries and defaults in oci_cli_rc
# ~/.oci/oci_cli_rc
[DEFAULT]
compartment-id = ocid1.compartment.oc1..aaaa
[OCI_CLI_CANNED_QUERIES]
inst = data[].{name:"display-name",state:"lifecycle-state",shape:shape,ad:"availability-domain"}
ids = data[].id
oci compute instance list --all --query query://inst --output table
Defaults in oci_cli_rc apply to every command that accepts the parameter. Don't set compartment-id there on a shared automation host; pass it explicitly.
raw-request for APIs the CLI doesn't wrap yet
oci raw-request --http-method GET \
--target-uri "https://iaas.eu-frankfurt-1.oraclecloud.com/20160918/instances?compartmentId=$C&limit=5"
# <service-host> and <api-version>/<resource> are placeholders from the service's API reference
oci raw-request --http-method POST \
--target-uri "https://<service-host>/<api-version>/<resource>" \
--request-body file://body.json \
--request-headers '{"opc-retry-token":"run-42"}'
raw-request signs the call with your configured auth, supports JSON payloads only and returns data, headers and status. Endpoint hostnames and API versions vary by service; take them from the service's API reference.
JMESPath patterns that hold up at scale
# Hyphenated keys must be double-quoted inside the query
--query 'data[?"lifecycle-state"==`RUNNING`].{n:"display-name",id:id}'
# Tag lookups
--query 'data[?"freeform-tags".env==`prod`].id'
--query 'data[?"defined-tags".Ops.Owner==`team-a`].id'
# Count without jq
--query 'length(data)'
# Sort and take the newest
--query 'sort_by(data, &"time-created")[-1].id' --raw-output
Without --all, --query only sees the first page, so counts and filters are silently wrong on large compartments.
Troubleshooting automation runs
| Symptom | Likely cause | Check |
|---|---|---|
Works for a human, 404 NotAuthorizedOrNotFound in cron | Different profile or principal; policy missing for the dynamic group | oci iam region list --debug 2>&1 | grep -i auth |
| Results differ between runs | Missing --all, or region not set | Echo OCI_CLI_REGION and add --all |
| 429 TooManyRequests storms | Unbounded parallelism | xargs -P cap; longer --wait-interval-seconds |
| Duplicate resources | Retry after waiter timeout | Re-check state; idempotent display names |
security_token expired mid-run | Long job with session auth | Use instance principal for long jobs |
Prevent it next time
- One auth mode per environment, set explicitly at the top of every script.
- Always
--allon list calls that feed logic, and always an explicit--region. - Use
--if-matchfor updates that replace lists. - Log the
opc-request-idfrom--debugon failures so Oracle Support can trace the call.