Files
yucca/ansible/ceph/docs/s3-integration.md
T
Antoine Lecompte c6985d902c feat(all): introduce partition/region/ceph-cluster model across the stack (#222)
* feat: introduce partition/region/ceph-cluster model across the stack

Formalize partition -> region -> {one k8s cluster, many ceph clusters} and
thread it through every layer plus a new yuctl ops CLI.

- tf: deployment/<partition>/<region>/<stack> layout; terragrunt path-parse +
  state key yucca/<partition>/<region>/<stack>; per-region region.hcl (role,
  site_id, datacenter, provider_code, domain); env->partition / site->region
  renames (NetBird object names byte-identical); standardized per-stack
  `discovery` output contract (secrets as op:// refs).
- k8s: clusters/<partition>/<region>/ (staging/austin, prod/htz-fsn1, dev/local);
  role-based kustomize components (primary/secondary); hybrid cluster-settings
  (TF-rendered identity + human fragment); dev-mirror folded into dev/local;
  charts regrouped into charts/{apps,platform,lib,dev}.
- ci: infra.yml partition/region discovery matrix; partition-keyed path filters;
  <partition>-<region> environment gates; image-versions path moves.
- ansible: inventories under <partition>-<region>/<cluster>.
- yuctl: Go/cobra CLI reading the discovery contract from TF state.
- Retire the sietch-talos libvirt VM cluster (dev@local is the k3d cluster);
  ceph inventory_dirname -> <partition>-<region>/<cluster>.

Verified: mise k8s:validate green (3 clusters); yuctl go build/vet; tofu
validate pre-merge (all 9 stacks). Live-staging state migration NOT run.

* fix typo

* commit
2026-06-29 08:40:29 -04:00

7.4 KiB

S3 Integration Guide

Audience: Application developers (Yucca, Immich, Restic, internal tooling).

Endpoints

The cluster runs Ceph RGW (RADOS Gateway) on every node behind a self-signed wildcard TLS certificate on port 443.

Style URL
Path-style https://s3.staging.austin.int.futo.cloud/<bucket>/<key>
Virtual-hosted https://<bucket>.s3.staging.austin.int.futo.cloud/<key>
Direct (per-node) https://10.10.10.90:443, https://10.10.10.91:443, https://10.10.10.92:443

Region: us-east-1

Path-style is recommended for simplicity. Virtual-hosted requires wildcard DNS (see DNS section below).

Getting credentials

Option A: 1Password (preferred)

S3 credentials for the svc-yucca-restic service account are stored in 1Password after initial deployment. Ask the infrastructure team for access to the "Ceph S3" vault entry.

Option B: radosgw-admin (infra operators only)

SSH to the bootstrap node (sietch-ceph-laurel) and run:

radosgw-admin user info --uid=svc-yucca-restic

The keys[0].access_key and keys[0].secret_key fields contain the credentials.

To create a new service account:

radosgw-admin user create \
  --uid=svc-myapp \
  --display-name='myapp service account' \
  --max-buckets=100

Self-signed certificate handling

The cluster uses a self-signed wildcard certificate. Every client must either trust the CA or disable TLS verification.

Copy the cert from the bootstrap node:

scp ansible-iac@10.10.10.90:/etc/ceph/rgw-ssl.crt ./rgw-ssl.crt

Then pass it to your client (examples below).

Disabling verification (quick testing only)

Pass --no-verify-ssl (AWS CLI) or verify=False (boto3). Fine for benchmarking, not for production.

AWS CLI configuration

~/.aws/credentials

[sietch]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY

~/.aws/config

[profile sietch]
region = us-east-1
endpoint_url = https://s3.staging.austin.int.futo.cloud
s3 =
    signature_version = s3v4
    addressing_style = path

Basic operations

# List buckets
aws --profile sietch --no-verify-ssl s3 ls

# Create a bucket
aws --profile sietch --no-verify-ssl s3 mb s3://my-bucket

# Upload a file
aws --profile sietch --no-verify-ssl s3 cp ./file.txt s3://my-bucket/file.txt

# List objects
aws --profile sietch --no-verify-ssl s3 ls s3://my-bucket/

# Download
aws --profile sietch --no-verify-ssl s3 cp s3://my-bucket/file.txt ./downloaded.txt

# Using the CA bundle instead of --no-verify-ssl
aws --profile sietch --ca-bundle ./rgw-ssl.crt s3 ls

boto3 (Python)

import boto3
import botocore
import urllib3

# Suppress InsecureRequestWarning when verify=False
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.staging.austin.int.futo.cloud",
    aws_access_key_id="YOUR_ACCESS_KEY",
    aws_secret_access_key="YOUR_SECRET_KEY",
    region_name="us-east-1",
    verify=False,  # or path to rgw-ssl.crt
    config=botocore.config.Config(
        signature_version="s3v4",
        s3={"addressing_style": "path"},
    ),
)

# Create a bucket
s3.create_bucket(Bucket="my-bucket")

# Upload
s3.put_object(Bucket="my-bucket", Key="hello.txt", Body=b"hello world")

# Download
obj = s3.get_object(Bucket="my-bucket", Key="hello.txt")
data = obj["Body"].read()

# List objects
response = s3.list_objects_v2(Bucket="my-bucket")
for item in response.get("Contents", []):
    print(item["Key"], item["Size"])

To use the CA bundle instead of disabling verification:

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.staging.austin.int.futo.cloud",
    aws_access_key_id="YOUR_ACCESS_KEY",
    aws_secret_access_key="YOUR_SECRET_KEY",
    region_name="us-east-1",
    verify="/path/to/rgw-ssl.crt",
    config=botocore.config.Config(
        signature_version="s3v4",
        s3={"addressing_style": "path"},
    ),
)

Restic

export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"
export RESTIC_REPOSITORY="s3:https://s3.staging.austin.int.futo.cloud/restic-backups"

# Init (first time)
restic init --option s3.region=us-east-1

# Backup
restic backup /data --option s3.region=us-east-1

Note: Restic uses the Go AWS SDK. For self-signed certs, set AWS_CA_BUNDLE=/path/to/rgw-ssl.crt or use the system trust store.

Bucket creation

Buckets are created via any S3 client. The svc-yucca-restic service account has a limit of 100 buckets (configurable via radosgw-admin user modify --max-buckets).

# AWS CLI
aws --profile sietch --no-verify-ssl s3 mb s3://my-new-bucket

# boto3
s3.create_bucket(Bucket="my-new-bucket")

Bucket data lands in the EC data pool (dev-z1.rgw.buckets.data). Index metadata goes to a separate replicated pool. No pool-level configuration is needed from the application side.

DNS setup for virtual-hosted buckets

Virtual-hosted bucket addressing (e.g., https://my-bucket.s3.staging.austin.int.futo.cloud) requires two DNS records:

s3.staging.austin.int.futo.cloud.        A  10.10.10.90
s3.staging.austin.int.futo.cloud.        A  10.10.10.91
s3.staging.austin.int.futo.cloud.        A  10.10.10.92
*.s3.staging.austin.int.futo.cloud.      A  10.10.10.90
*.s3.staging.austin.int.futo.cloud.      A  10.10.10.91
*.s3.staging.austin.int.futo.cloud.      A  10.10.10.92

Round-robin A records across all three nodes.

These records are live and managed in this repo: tf/deployment/dev/dns/ (Cloudflare, futo.cloud zone). They resolve publicly but point at the management VLAN, so they are only routable from networks that reach 10.10.10.0/24. To change them, edit records.auto.tfvars and TF_STACK_DIR=tf/deployment/dev/dns mise run tf:apply.

Performance characteristics

Property Value
Data pool Erasure coded, k=8 m=3, failure domain=OSD
Index pool Replicated, size=2, min_size=1
Storage media HDD-backed (HGST 6 TB SAS drives)
Block.db SSD-backed (Micron 5100 3.8 TB)
Nodes 3 (Dell R730xd)
RGW daemons 1 per node
TLS Self-signed wildcard, 10-year validity
Network 10 GbE bonded active-backup (no LACP)

What to expect

  • Throughput: HDD-bound for large objects. A single node can sustain roughly 500-800 MiB/s aggregate reads from its HDDs. With 3 nodes and EC 8+3, expect 300-600 MiB/s aggregate for large sequential workloads depending on concurrency and object size.
  • Latency: Higher than SSD or cloud S3. Small object PUTs (< 1 MiB) will see 10-50 ms latency due to HDD seeks. Use concurrency to amortize.
  • IOPS: Low single-drive IOPS (100-200 per HDD). Use larger objects (16+ MiB) to maximize throughput.
  • EC overhead: Usable capacity is raw * k/(k+m) = raw * 8/11 = ~72.7% of raw HDD capacity.
  • Single network: Public and cluster traffic share the same 10 GbE bond. Recovery/rebalancing events will compete with client I/O.

Benchmarking

The cluster includes a benchmark tool at roles/s3_bench/files/s3bench.py:

python3 s3bench.py \
  --endpoint https://10.10.10.90:443 \
  --access-key KEY \
  --secret-key SECRET \
  --bucket s3bench \
  --num-objects 100 \
  --object-size-mb 16 \
  --concurrency 8 \
  --ops put \
  --log /tmp/bench.jsonl

Supports put, get, delete, and mixed (70/20/10 split) operations. Results include throughput (MiB/s), IOPS, and p50/p95/p99 latencies.