PowerShell 7+ retry engine that keeps calling oci compute instance launch
until Oracle Cloud has Always Free ARM (VM.Standard.A1.Flex) capacity,
rotating through every availability domain in the compartment, then waits
for the instance to boot and reports its public IP.
-
OCI CLI installed and already configured (
oci setup configalready run,oci iam region listalready works). -
PowerShell 7+ (
pwsh). The script enforces this with#Requires -Version 7.0. -
An SSH key pair at
$HOME\.ssh\oracle_ssh/$HOME\.ssh\oracle_ssh.pub. Generate one if it doesn't exist:ssh-keygen -t ed25519 -f "$HOME\.ssh\oracle_ssh"
Start-Retry.ps1 Entry point / retry loop
config.example.ps1 Template config, tracked in git -- placeholders only
config.local.ps1 Your real config -- git-ignored, never committed
OracleCli.ps1 oci CLI wrapper, launch-request builder, error classifier
Logger.ps1 Timestamped logging to logs/*.log
LaunchTemplate.json Skeleton instance-launch request body
logs/ retry.log, error.log, success.log
output/ launch.json (generated per attempt), last-success.json
Start-Retry.ps1 loads config.local.ps1, which is git-ignored so your real
OCIDs never get committed. Set it up once:
Copy-Item config.example.ps1 config.local.ps1
notepad config.local.ps1If config.local.ps1 is missing, the script fails immediately with a message
pointing you at this step -- it won't run against config.example.ps1's
placeholders. In config.local.ps1, set:
CompartmentId,SubnetId-- required, no default will work for your tenancy.ImageId-- leave blank to auto-resolve the newest matching image viaoci compute image listat startup (usesOperatingSystem/OperatingSystemVersion), or pin a specific image id for reliability.Shape,Ocpus,MemoryInGBs,BootVolumeSizeInGBs-- Always Free ARM ceiling is 4 OCPUs / 24 GB total across all A1 instances in a tenancy.RetryIntervalSeconds,RateLimitBackoffSeconds-- pacing between attempts.SshPublicKeyPath/SshPrivateKeyPath-- default tooracle_ssh(.pub).
Availability domains are not hardcoded: they're fetched at startup via
oci iam availability-domain list, so the same script works in any region
regardless of how many ADs it has.
pwsh -File .\Start-Retry.ps1The script retries forever (Ctrl+C to stop) until it successfully launches
and the instance reaches RUNNING with a public IP, then prints:
==================== INSTANCE READY ====================
Instance Name : portfolio-vm-20260719-141502
Instance OCID : ocid1.instance.oc1...
Public IP : 130.x.x.x
Private IP : 10.x.x.x
SSH Command : ssh -i "C:\Users\you\.ssh\oracle_ssh" ubuntu@130.x.x.x
==========================================================
The original PowerShell attempt passed --shape-config as an inline JSON
string on the command line, escaped with backticks:
--shape-config "{`"ocpus`":$Ocpus,`"memoryInGBs`":$Memory}"This works in Bash but is unreliable on Windows: PowerShell and the Win32
process-creation layer both re-quote arguments before an external executable
sees them, and that re-quoting can strip or corrupt embedded " characters,
so the CLI sometimes receives malformed JSON instead of the intended object.
To avoid that entire bug class, this project never puts JSON on the command
line. Every launch attempt builds a complete request object in memory from
LaunchTemplate.json (field names/casing match
oci compute instance launch --generate-full-command-json-input), writes it
to output\launch.json via ConvertTo-Json (which handles all escaping),
and submits it with:
oci compute instance launch --from-json file://<absolute path to launch.json> --output jsonOnly a plain file path ever crosses the command-line boundary.
Two related things discovered while validating this end-to-end against a
real tenancy, both handled in OracleCli.ps1:
--from-jsonstill needs the top-level convenience keys, not just the nested complex objects.oci compute instance launch --generate-full-command-json-inputshows both a nestedcreateVnicDetails.subnetIdand a flat top-levelsubnetIdkey. Only the top-level one satisfies the CLI's own required-option check --createVnicDetails.subnetIdalone still fails withMissing option(s) --subnet-id.LaunchTemplate.jsonsets both to be safe.--no-retryis passed on every invocation. The OCI Python SDK has its own built-in retry strategy for retryable 5xx errors -- which includes "Out of host capacity", the exact routine case this whole engine exists to handle -- and without--no-retryit silently retries inside a single CLI call for 60-100+ seconds before ever returning. That fights this script's own AD-rotation retry loop and makesRetryIntervalSecondsmeaningless.Invoke-OciCliis the single call site for theociCLI, so this is applied centrally rather than repeated at every call site.- Every
ociCLI call also gets a hard local timeout (Config.OciCommandTimeoutSeconds, default 120s) viaSystem.Diagnostics.Processwith async stream reads, so a single stuck invocation can never block the infinite retry loop forever. - On Windows, the CLI additionally shells out to a nested PowerShell process
to
Get-Aclthe~/.oci/configfile as a permissions check on every invocation.Invoke-OciClisetsOCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=Trueon the child process to skip that (it only silences an informational warning, it doesn't touch actual credential/config file security) -- and, defensively,Remove-OciCliNoisestrips that PowerShell error scaffolding from any output that does slip through before it's classified, since generic tokens in it (e.g.CommandNotFoundException) would otherwise be misread as a genuine OCI "NotFound" API error and wrongly treated as fatal.
- Availability domains are tried round-robin, one attempt per AD per cycle.
- Every
ociCLI invocation's combined stdout+stderr is classified:- Capacity ("Out of host capacity") -- log and rotate to the next AD.
- RateLimit (
TooManyRequests) -- back off forRateLimitBackoffSeconds. - Network (connection/timeout errors) -- log and retry.
- Auth (authentication/authorization failures) -- fatal, stops the script (retrying with the same broken credentials can't help).
- InvalidJson / InvalidConfig (malformed request, bad/missing parameter, 404 on a referenced resource) -- fatal, stops the script, since retrying a structurally broken request forever is pointless.
- Anything else -- logged as an unknown CLI failure and retried.
- If OCI accepts the launch but the instance later lands in
TERMINATED(an asynchronous capacity fault), the loop treats that the same as a capacity error and retries with the next AD. - On success, the script polls until the instance is
RUNNINGand has a public IP before printing results and exiting 0.
logs\retry.log-- everything, timestamped.logs\error.log-- ERROR-level lines only.logs\success.log-- SUCCESS-level lines only.output\launch.json-- the exact request body sent on the most recent attempt.output\last-success.json-- instance id/IPs from the last successful run.
The current version lives in the VERSION file and is logged on every run
(Starting OCI Always Free ARM retry engine vX.Y.Z.). This project follows
Semantic Versioning; notable changes are tracked in
CHANGELOG.md (Keep a Changelog
format).
To cut a release:
-
Bump
VERSIONand move the relevant[Unreleased]entries inCHANGELOG.mdunder a new dated## [X.Y.Z] - YYYY-MM-DDsection. -
Commit and push that to
main. -
Tag and push the tag:
git tag vX.Y.Z git push origin vX.Y.Z
.github/workflows/release.yml then publishes a GitHub Release
automatically, using that CHANGELOG.md section as the release notes. It
fails loudly if the tag doesn't match VERSION or if no matching changelog
section is found -- both catch a forgotten bump before a broken release goes out.
See CONTRIBUTING.md for dev setup, code style, and how to submit a PR.