Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 74 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ jobs:
printf '%s' "$OCI_API_KEY_PEM" > ~/.oci/oci_api_key.pem
chmod 600 ~/.oci/oci_api_key.pem
cat > ~/.oci/config <<EOF
[syscode-homelab]
[DEFAULT]
user=$OCI_USER_OCID
fingerprint=$OCI_FINGERPRINT
tenancy=$OCI_TENANCY_OCID
region=uk-london-1
region=$OCI_REGION
key_file=$HOME/.oci/oci_api_key.pem
EOF
chmod 600 ~/.oci/config
Expand All @@ -62,12 +62,78 @@ jobs:
OCI_FINGERPRINT: ${{ secrets.OCI_FINGERPRINT }}
OCI_TENANCY_OCID: ${{ secrets.OCI_TENANCY_OCID }}
OCI_API_KEY_PEM: ${{ secrets.OCI_API_KEY_PEM }}
OCI_REGION: ${{ secrets.OCI_REGION }}

- name: Write backend config
run: printf '%s' "$TF_BACKEND_CONFIG" > tofu/oci/backend-config.tfvars
env:
TF_BACKEND_CONFIG: ${{ secrets.TF_BACKEND_CONFIG }}

- name: Install OCI CLI
run: pip install oci-cli --quiet

- name: Check free tier capacity
env:
OCI_COMPARTMENT_OCID: ${{ secrets.OCI_COMPARTMENT_OCID }}
run: |
echo "Querying live OCI state for compartment ${OCI_COMPARTMENT_OCID}"

INSTANCES=$(oci compute instance list \
--compartment-id "$OCI_COMPARTMENT_OCID" \
--all --output json 2>/dev/null || echo '{"data":[]}')

LIVE_STATES='.["lifecycle-state"] != "TERMINATING" and .["lifecycle-state"] != "TERMINATED"'
A1_FILTER="select(.shape==\"VM.Standard.A1.Flex\") | select($LIVE_STATES)"
MICRO_FILTER="select(.shape==\"VM.Standard.E2.1.Micro\") | select($LIVE_STATES)"

CURRENT_OCPUS=$(echo "$INSTANCES" | \
jq "[.data[] | $A1_FILTER | (.\"shape-config\".ocpus // 0)] | add // 0")
CURRENT_RAM=$(echo "$INSTANCES" | \
jq "[.data[] | $A1_FILTER | (.\"shape-config\".\"memory-in-gbs\" // 0)] | add // 0")
CURRENT_MICRO=$(echo "$INSTANCES" | \
jq "[.data[] | $MICRO_FILTER] | length")

REQUESTED_OCPUS=$(grep -oE 'ocpus\s*=\s*[0-9]+' tofu/oci/terraform.tfvars \
| awk -F'=' '{s+=int($2)} END {print s+0}')
REQUESTED_RAM=$(grep -oE 'memory_gb\s*=\s*[0-9]+' tofu/oci/terraform.tfvars \
| awk -F'=' '{s+=int($2)} END {print s+0}')
REQUESTED_MICRO=$(grep -c 'micro_nodes' tofu/oci/terraform.tfvars || echo 0)

MAX_AMPERE_OCPUS=4
MAX_AMPERE_RAM_GB=24
MAX_MICRO_INSTANCES=1

echo "A1 live: ${CURRENT_OCPUS}/${MAX_AMPERE_OCPUS} OCPU, ${CURRENT_RAM}/${MAX_AMPERE_RAM_GB} GB"
echo "A1 tfvars: ${REQUESTED_OCPUS} OCPU, ${REQUESTED_RAM} GB"
echo "Micro: live=${CURRENT_MICRO}, tfvars=${REQUESTED_MICRO}, limit=${MAX_MICRO_INSTANCES}"

FAIL=0
if [ "$(echo "$REQUESTED_OCPUS > $MAX_AMPERE_OCPUS" | bc)" = "1" ]; then
echo "ERROR: tfvars requests ${REQUESTED_OCPUS} A1 OCPU but limit is ${MAX_AMPERE_OCPUS}"
FAIL=1
fi
if [ "$(echo "$CURRENT_OCPUS > $MAX_AMPERE_OCPUS" | bc)" = "1" ]; then
echo "ERROR: live A1 OCPU=${CURRENT_OCPUS} already exceeds limit=${MAX_AMPERE_OCPUS} — drift detected"
FAIL=1
fi
if [ "$(echo "$REQUESTED_RAM > $MAX_AMPERE_RAM_GB" | bc)" = "1" ]; then
echo "ERROR: tfvars requests ${REQUESTED_RAM} GB A1 RAM but limit is ${MAX_AMPERE_RAM_GB} GB"
FAIL=1
fi
if [ "$(echo "$CURRENT_RAM > $MAX_AMPERE_RAM_GB" | bc)" = "1" ]; then
echo "ERROR: live A1 RAM=${CURRENT_RAM} GB already exceeds limit=${MAX_AMPERE_RAM_GB} GB — drift detected"
FAIL=1
fi
if [ "$REQUESTED_MICRO" -gt "$MAX_MICRO_INSTANCES" ]; then
echo "ERROR: tfvars requests ${REQUESTED_MICRO} Micro but limit is ${MAX_MICRO_INSTANCES}"
FAIL=1
fi
if [ "$CURRENT_MICRO" -gt "$MAX_MICRO_INSTANCES" ]; then
echo "ERROR: live Micro=${CURRENT_MICRO} exceeds limit=${MAX_MICRO_INSTANCES} — drift"
FAIL=1
fi
exit $FAIL

- name: Setup OpenTofu
uses: opentofu/setup-opentofu@v2.0.0
with:
Expand All @@ -86,13 +152,16 @@ jobs:
-var="compartment_ocid=$OCI_COMPARTMENT_OCID" \
-var="talos_image_ocid=${{ steps.talos_image.outputs.ocid }}" \
-var="omni_join_token=$OMNI_JOIN_TOKEN" \
-var="omni_endpoint=$OMNI_ENDPOINT" \
-var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" \
-var="oci_config_profile=DEFAULT" \
-var-file=terraform.tfvars \
-out=tfplan
env:
OCI_TENANCY_OCID: ${{ secrets.OCI_TENANCY_OCID }}
OCI_COMPARTMENT_OCID: ${{ secrets.OCI_COMPARTMENT_OCID }}
OMNI_JOIN_TOKEN: ${{ secrets.OMNI_JOIN_TOKEN }}
OMNI_ENDPOINT: ${{ secrets.OMNI_ENDPOINT }}
TAILSCALE_AUTH_KEY: ${{ secrets.TAILSCALE_AUTH_KEY }}
TF_LOG: WARN

Expand Down Expand Up @@ -120,11 +189,11 @@ jobs:
printf '%s' "$OCI_API_KEY_PEM" > ~/.oci/oci_api_key.pem
chmod 600 ~/.oci/oci_api_key.pem
cat > ~/.oci/config <<EOF
[syscode-homelab]
[DEFAULT]
user=$OCI_USER_OCID
fingerprint=$OCI_FINGERPRINT
tenancy=$OCI_TENANCY_OCID
region=uk-london-1
region=$OCI_REGION
key_file=$HOME/.oci/oci_api_key.pem
EOF
chmod 600 ~/.oci/config
Expand All @@ -133,6 +202,7 @@ jobs:
OCI_FINGERPRINT: ${{ secrets.OCI_FINGERPRINT }}
OCI_TENANCY_OCID: ${{ secrets.OCI_TENANCY_OCID }}
OCI_API_KEY_PEM: ${{ secrets.OCI_API_KEY_PEM }}
OCI_REGION: ${{ secrets.OCI_REGION }}

- name: Write backend config
run: printf '%s' "$TF_BACKEND_CONFIG" > tofu/oci/backend-config.tfvars
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ flake.lock
.dagger/
dagger/__pycache__/

# AI tooling
.specstory/
docs/plans/
.claude/

# Artifacts
artifacts/
*.qcow2
Expand Down
169 changes: 169 additions & 0 deletions FREE_TIER_RESOURCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# OCI Always Free Resources — Complete Reference

> Last verified: March 2026 against OCI service limits documentation and API.
> Covers two OCI account types with different Always Free behaviours.

---

## Two Types of OCI Free Tier

OCI has two distinct account types, both offering "Always Free" resources, but with
differences in how some limits are applied:

| Feature | Always Free Account | PAYG Account (with Always Free) |
|---------|--------------------|---------------------------------|
| Account type | Dedicated free-tier account | Pay-as-you-go with free allowances |
| E2.1.Micro (x86 bastion) | ✅ Available (up to 2) | ✅ Available (up to 2) |
| A1.Flex OCPU granularity | Integer (1, 2, 3…) | Integer (1, 2, 3…) |
| A1.Flex total allowance | 4 OCPUs / 24 GB RAM | 4 OCPUs / 24 GB RAM |
| Load Balancer (10 Mbps) | ✅ 1 × Always Free | ✅ 1 × Always Free |
| Network Load Balancer | ✅ 1 × Always Free | ✅ 1 × Always Free |
| Block Storage | 200 GB total | 200 GB total |
| Object Storage | 20 GB | 20 GB |

> **How to identify free resources in the API**: OCI's service limit API marks Always
> Free resources with descriptions containing "Always Free" (e.g. `lb-10mbps-micro-count`
> = "10Mbps **Always Free** Load Balancer Count"). Some Always Free resources (e.g. the
> Network Load Balancer) do not carry this marker in the API response but are still free
> — cross-reference with the [official Always Free documentation](#references).

---

## Compute

### Ampere A1.Flex (ARM64)

- **Shape**: `VM.Standard.A1.Flex`
- **Billing type**: `LIMITED_FREE` — reported by OCI shape API
- **Total allowance**: **4 OCPUs and 24 GB RAM** across all instances in the tenancy
- **OCPU granularity**: Integer values only (1, 2, 3, 4)
- **Architecture**: ARM64 (Ampere Altra)

**Always Free configurations**:

| Instances | OCPUs each | RAM each | Total OCPUs | Total RAM |
|-----------|-----------|---------|------------|----------|
| 4 | 1 | 6 GB | 4 | 24 GB ✅ |
| 3 | 1 | 8 GB | 3 | 24 GB (1 OCPU unused) |
| 2 | 2 | 12 GB | 4 | 24 GB ✅ |
| 1 | 4 | 24 GB | 4 | 24 GB ✅ |

> To maximise both OCPUs and RAM: **4 × (1 OCPU / 6 GB)**

### VM.Standard.E2.1.Micro (x86, AMD)

- **Shape**: `VM.Standard.E2.1.Micro` (fixed shape, not Flex)
- **Count**: Up to **2 instances**
- **CPU**: 1/8 OCPU per instance
- **RAM**: 1 GB per instance
- **Architecture**: x86_64 (AMD EPYC)
- **Available in both Always Free and PAYG accounts**

---

## Storage

### Block Volume Storage

| Limit | Value | Source |
|-------|-------|--------|
| Total free storage | **200 GB** total per tenancy | `total-free-storage-gb` = "Free Volume Size (GB)" |
| Free backups | **5** | `free-backup-count` = "Free Backup Counts" |
| Minimum boot volume | 47 GB per instance | OCI minimum |

**Storage planning** (boot volumes count toward the 200 GB total):

| Config | Boot volumes | Remaining for data |
|--------|-------------|-------------------|
| 4 × A1.Flex + 2 × Micro | 4×47 + 2×47 = 282 GB | ❌ exceeds 200 GB |
| 4 × A1.Flex only | 4×47 = 188 GB | 12 GB data |
| 3 × A1.Flex only | 3×50 = 150 GB | 50 GB data |
| 2 × A1.Flex + 1 × Micro | 2×47 + 47 = 141 GB | 59 GB data |

> Boot volumes are included in the 200 GB total. Plan storage allocations carefully.

### Object Storage

- **Capacity**: 20 GB standard storage
- **API Requests**: 50,000/month (10,000 PUT, 50,000 GET)
- **No free archive tier** beyond standard limits

---

## Networking

### Virtual Cloud Networks (VCN)

- 2 VCNs, unlimited subnets per VCN
- Internet Gateway, NAT Gateway (1 per VCN), Service Gateway: free

### Load Balancer

| Type | Free? | API identifier | Notes |
|------|-------|----------------|-------|
| **Flexible LB (10 Mbps)** | ✅ **Always Free** | `lb-10mbps-micro-count` | 1 instance, L4+L7 |
| Flexible LB (>10 Mbps) | ❌ Paid | `lb-flexible-count` | Pay by bandwidth |
| **Network LB** | ✅ **Always Free** | `max-nlb-flexible-count` | 1 instance, L4 only |

> **Kubernetes (OCI CCM)**: To provision the free 10 Mbps LB instead of a paid
> flexible LB, annotate your Service with
> `service.beta.kubernetes.io/oci-load-balancer-shape: "10Mbps"`.
> Without this annotation the CCM defaults to a paid flexible shape.

### Public IP Addresses

- **Reserved IPs**: 2 reserved public IPv4 addresses (free)
- **Ephemeral IPs**: Assigned to instances at no cost

### Data Transfer

- **Outbound**: 10 TB/month free
- **Inbound**: Always free

---

## Database

### Autonomous Database

- 2 databases, 1 OCPU each, 20 GB storage each
- Types: ATP (OLTP), ADW (analytics), JSON

### NoSQL Database

- 3 tables, 25 GB per table, 133M reads + 133M writes/month

---

## Service Limits Quick Reference

| Resource | Always Free Amount |
|----------|-------------------|
| A1.Flex OCPUs | **4 total** |
| A1.Flex RAM | **24 GB total** |
| E2.1.Micro instances | **2** (both account types) |
| Block Storage | **200 GB total** |
| Block Storage Backups | **5** |
| Object Storage | **20 GB** |
| Load Balancer (10 Mbps) | **1** |
| Network Load Balancer | **1** |
| Reserved Public IPs | **2** |
| VCNs | **2** |
| Outbound Transfer | **10 TB/month** |
| Autonomous Databases | **2** (1 OCPU, 20 GB each) |
| NoSQL Tables | **3** (25 GB each) |
| Functions | **2M invocations/month** |
| API Gateway | **1M requests/month** |
| Monitoring | **500M datapoints/month** |

---

## References

- [OCI Always Free Documentation](https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm)
- [OCI Service Limits](https://docs.oracle.com/en-us/iaas/Content/General/Concepts/servicelimits.htm)
- [OCI Pricing](https://www.oracle.com/cloud/price-list.html)

---

**Last verified:** March 2026 — OCI service limits documentation and API
Loading
Loading