Automates getting and keeping an Oracle Cloud Infrastructure (OCI) "Always
Free" compute instance, for any Always Free-eligible shape you configure
(AMD VM.Standard.E2.1.Micro, Ampere VM.Standard.A1.Flex, or anything
else your tenancy allows).
Today that's the whole tool - obtain and keep alive one Always Free
instance. The name and the underlying design (wrap the official oci CLI
rather than reimplement its API) are deliberately not scoped to just
capacity/free-tier, so the same tool can grow into other OCI-CLI-driven
automation over time without a rename.
It does two jobs today:
- Obtain - repeatedly try to launch the shape you configured, cycling through every availability domain (AD) in the region between attempts, until one succeeds. OCI's Always Free capacity (especially A1/ARM) is frequently exhausted, and capacity is AD-specific, so rotating through every AD maximizes your odds.
- Monitor - once an instance is running, periodically confirm it's still there. If OCI ever reclaims it (Always Free instances can be reclaimed for prolonged inactivity, among other reasons), automatically switch back into obtain mode.
The transition between the two modes is automatic - you don't run anything by hand after the initial setup.
Out of scope: this tool's job ends the moment the compute instance
exists and is RUNNING. It does not touch the instance's OS, networking,
or any software on it.
- How it works
- Requirements
- Install
- Config reference
- Testing without a live OCI account
- Troubleshooting
- Contributing
- License
Everything talks to OCI exclusively through the official oci CLI
(oci compute instance launch, oci iam availability-domain list,
oci compute instance get), invoked as subprocesses. This tool never reads,
stores, generates, or transmits OCI credentials - if oci already works
when you run it by hand (~/.oci/config, or OCI_CLI_* environment
variables), it works here with zero extra setup.
Two systemd units carry out the two modes:
oci-shepherd-obtain.service- runsoci_shepherd.py obtain. Internally loops through your configured availability domains, sleeping and retrying, until a launch succeeds. On success it writes the new instance's OCID to a state file and runssystemctl start oci-shepherd-monitor.service, then exits.oci-shepherd-monitor.service- runsoci_shepherd.py monitor. Polls the instance recorded in the state file. If it ever finds the instance gone (terminated/reclaimed), it clears the state file, runssystemctl start oci-shepherd-obtain.service, and exits.
Only oci-shepherd-obtain.service needs to be enabled at boot. It checks
its state file on every start (including after a host reboot): if that
file already points at a live instance, it hands off straight to the
monitor unit instead of launching a redundant one.
Every attempt - success or failure, in either mode - is appended to a JSON Lines log file, one JSON object per line, so you have a durable, greppable record instead of just console output that scrolls away.
- Python 3.8+
- PyYAML:
python3 -m pip install pyyaml - The OCI CLI,
already authenticated (
oci setup config, orOCI_CLI_*env vars). Confirm it works before touching this tool:oci iam availability-domain list --compartment-id <your compartment OCID>
- A Linux host with systemd, for the packaged units (the Python script itself has no Linux-specific dependency, but the unit files do).
- An existing VCN + subnet in the target region/compartment (this tool does not create networking - Always Free includes 2 VCNs to use).
- An SSH key pair (only the public key is needed by this tool).
sudo mkdir -p /opt/oci-shepherd /etc/oci-shepherd
sudo cp oci_shepherd.py /opt/oci-shepherd/
sudo cp config.example.yaml /etc/oci-shepherd/config.yaml
sudo nano /etc/oci-shepherd/config.yaml # fill in your values - see Config reference below
sudo cp systemd/oci-shepherd-obtain.service /etc/systemd/system/
sudo cp systemd/oci-shepherd-monitor.service /etc/systemd/system/
sudo systemctl daemon-reload
# Only enable/start the obtain unit - it starts the monitor unit for you
# on success, and hands off back and forth automatically after that.
sudo systemctl enable --now oci-shepherd-obtain.serviceWatch progress:
journalctl -u oci-shepherd-obtain.service -f
tail -f /var/log/oci-shepherd/activity.jsonlOnce obtained, systemctl status oci-shepherd-monitor.service should show
it active, and oci-shepherd-obtain.service will show as inactive (exited
cleanly) until it's needed again.
The unit files default to root, mainly so the tool can call systemctl start on the other unit without extra permission plumbing, and so it
picks up /root/.oci/config. If you'd rather not run as root, both unit
files have commented-out User=/Environment=HOME= lines - uncomment
them, point HOME at that user's home directory (so ~/.oci/config
resolves correctly), and grant that user passwordless systemctl start/stop on both units (via a polkit rule or a narrow sudoers entry).
Without that grant, the automatic handoff between obtain and monitor will
fail (harmlessly logged as systemd_handoff_failed) and you'll need to
start the other unit yourself.
The config file is YAML. See config.example.yaml
for a fully-commented version with fake placeholder values. Every field
below is exactly what oci_shepherd.py's load_config() reads -
if you add a field here, add it there too, and vice versa.
| Field | Required | Description |
|---|---|---|
region |
yes | OCI region, e.g. us-ashburn-1. |
profile |
no | OCI CLI profile name from ~/.oci/config. Default profile if unset. |
compartment_id |
yes | Compartment OCID to launch into. |
shape |
yes | Compute shape, e.g. VM.Standard.A1.Flex or VM.Standard.E2.1.Micro. |
shape_config.ocpus / shape_config.memory_in_gbs |
required for .Flex shapes |
OCPU/RAM sizing; stay within your tenancy's Always Free limits. |
availability_domains |
yes | ["all"] to auto-discover every AD in the region, or an explicit list of AD names. |
subnet_id |
yes | Subnet OCID for the instance's VNIC. |
assign_public_ip |
no (default true) |
Assign an ephemeral public IP. |
image_id |
one of this or the two below | Exact image OCID to launch. |
image_operating_system / image_operating_system_version |
one of this pair or image_id |
Resolved to the newest matching image if image_id is unset. |
ssh_public_key_path |
one of this or ssh_public_key |
Path to an SSH public key file. |
ssh_public_key |
one of this or ssh_public_key_path |
SSH public key, inline. |
display_name |
yes | Instance display name. |
boot_volume_size_in_gbs |
yes | Boot volume size in GB. |
obtain_retry_interval_seconds |
yes | Delay after a full pass through all ADs before starting another pass. |
obtain_inter_ad_delay_seconds |
no (default 5) |
Delay between trying each AD within one pass. |
wait_for_running_seconds |
no (default 600) |
How long to wait for a launched instance to reach RUNNING before treating the attempt as failed. |
monitor_check_interval_seconds |
yes | How often monitor mode polls the instance's state. |
rate_limit_backoff_base_seconds |
no (default 30) |
Starting backoff delay on HTTP 429. |
rate_limit_backoff_max_seconds |
no (default 900) |
Backoff delay cap. |
rate_limit_max_consecutive_retries |
no (default 8) |
Consecutive 429s tolerated on one AD before moving to the next AD. |
log_file |
yes | Path to the JSON Lines activity log (parent dir auto-created). |
state_file |
yes | Path where the obtained instance's OCID is recorded (parent dir auto-created). |
obtain_systemd_unit |
no (default oci-shepherd-obtain.service) |
Unit name monitor mode starts when the instance is lost. |
monitor_systemd_unit |
no (default oci-shepherd-monitor.service) |
Unit name obtain mode starts on success. |
python3 oci_shepherd.py selftest --log-file ./selftest.jsonlThis exercises the outcome-classification and logging code paths with
canned "capacity error", "rate limited", and "success" responses - no oci
CLI or network access required - and prints/writes the resulting log
lines so you can see exactly what each outcome looks like.
For a fuller dry run that exercises the actual obtain_loop/monitor_loop
functions (AD resolution, image resolution, launch, state file handoff,
systemd handoff) against a scripted fake instead of the real OCI CLI, see
test/run_local_test.py and
test/fake_oci (a minimal fake oci CLI you can put
ahead of the real one on PATH for manual testing on a Linux box).
This is expected, not a bug. Always Free Ampere A1 capacity is well known in the OCI community to be scarce and highly region/AD-dependent - reports of retry tools like this one running anywhere from a few minutes to several weeks (occasionally longer) before landing capacity are common. See hitrov/oci-arm-host-capacity and its issue tracker for a large collection of real-world timing reports across regions. There is no way to promise a completion time - the tool's job is to keep trying efficiently (every AD, with sane backoff) for as long as it takes, not to guarantee a specific wait.
Things that actually help:
- Set
availability_domains: ["all"]so every AD in the region is tried, not just one. - Consider trying multiple regions (run a separate instance of this tool, with a separate config/log/state file, per region) if your workload doesn't require a specific one.
- The AMD
VM.Standard.E2.1.Microshape is generally far less contested thanVM.Standard.A1.Flex- if ARM isn't a hard requirement, it will typically succeed much faster.
OCI's API rate-limits aggressive retry loops, which a tool like this one will inevitably run into over a long obtain phase. When that happens you'll see log lines like:
{"event": "rate_limited", "availability_domain": "...", "detail": "{\"code\": \"TooManyRequests\", ...}", "backoff_seconds": 41.7, "consecutive_rate_limits": 2, ...}This is expected and handled: the tool backs off exponentially (with
jitter) starting at rate_limit_backoff_base_seconds, capped at
rate_limit_backoff_max_seconds, and retries the same AD rather than
failing outright. A tight loop with no backoff is a known way to make rate
limiting worse and get temporarily throttled harder - that's specifically
what this backoff avoids. If you see rate_limit_giving_up_on_ad after
rate_limit_max_consecutive_retries consecutive 429s, the tool has moved
on to the next AD rather than waiting forever on one; it will come back to
that AD on the next pass.
Install the OCI CLI and confirm oci --version works for the same user/
environment this tool runs as (root, by default, per the systemd units).
Your ~/.oci/config (or OCI_CLI_* environment variables) aren't set up,
or are set up for a different user than the one running the systemd unit.
Confirm manually first:
oci iam availability-domain list --compartment-id <compartment OCID>If that fails, fix it with oci setup config before touching this tool -
this tool has no credential-handling of its own to debug.
load_config() validates the config file before doing anything else and
fails with a specific message (missing file, invalid YAML, missing
required field, malformed-looking OCID, missing SSH key, etc.) rather
than a raw stack trace. If you see a Python traceback instead, please
treat it as a bug in this tool and open an issue/PR.
Check journalctl -u oci-shepherd-obtain.service for a fatal_error log
line - this means the tool hit something retrying won't fix (bad auth,
missing config field, etc.), exited non-zero, and systemd restarted it per
Restart=on-failure. Fix the underlying config/auth issue; the loop is
intentional insurance against transient crashes, not the tool's normal
retry mechanism (normal capacity retries are logged as launch_failed /
obtain_pass_complete and do not exit the process).
Bug reports, PRs, and questions are welcome - see CONTRIBUTING.md for how to run this tool's tests without needing a live OCI account, and CODE_OF_CONDUCT.md for community expectations. Found a security issue? See SECURITY.md instead of a public issue.
Copyright (C) 2026 the oci-shepherd contributors.
Licensed under the GNU Affero General Public License, version 3 or (at your option) any later version - see LICENSE for the full text. In short: you're free to use, modify, and redistribute this tool, including running a modified version as a network service, provided that a modified network-service version also makes its source available to the users interacting with it over the network (the "AGPL" difference from plain GPL). This tool ships with zero credential-handling code of its own by design (see "How it works" above) - that stays true regardless of how you deploy or modify it.