ADR-010: Migrate Plane file storage from bundled MinIO to AWS S3
Context
Plane CE is the central workflow hub for Aucert, with all task management and agent orchestration workflows running through it. File uploads (attachments, images, cover photos) currently live on a bundled MinIO pod with a 5Gi PVC inside AKS — no backup, no encryption at rest, default credentials (admin/password), HTTP-only transport. Data loss is unrecoverable.
Two migration attempts were made and failed:
Azure Blob Storage (tested 2026-04-03): Standard StorageV2 accounts have zero S3 API compatibility. boto3 presigned URLs rejected with AuthenticationFailed. S3 compat only available on Data Lake Gen2 (HNS enabled).
Cloudflare R2 (tested 2026-04-04): R2 is S3-compatible for most operations (GET, PUT, LIST) but does not support S3 POST Object (form-based uploads) — returns HTTP 501. Plane CE v1.2.0 uses generate_presigned_post() for all file uploads, making R2 incompatible without patching Plane's source code.
Key technical finding from R2 attempt: Plane's S3Storage class (/code/plane/settings/storage.py) hardcodes AWS_QUERYSTRING_AUTH=False and AWS_DEFAULT_ACL=public-read via django-storages inheritance. The class has custom generate_presigned_post() and generate_presigned_url() methods that use boto3 directly. The url() method returns raw object names (not presigned URLs). These are not configurable via environment variables.
Decision
Migrate to AWS S3 — the only tested-compatible option that supports the full S3 API including POST Object.
Status: proposed — pending AWS account setup and IAM credential creation.
Alternatives considered
| Option | Pros | Cons |
|---|---|---|
| AWS S3 (proposed) | Full S3 API including POST Object, industry standard, aligns with potential AWS infrastructure migration | Egress fees (negligible for internal tool), cross-cloud traffic from Azure AKS |
| Keep bundled MinIO | No migration effort | No backup, no encryption, data loss on cluster failure — unacceptable |
| Azure Blob Storage | Same cloud provider | Tested, failed — zero S3 API compatibility |
| Cloudflare R2 | Zero egress, S3-compatible for GET/PUT | Tested, failed — no POST Object support (501), incompatible with Plane's upload flow |
| Azure Data Lake Gen2 (HNS) | S3-compatible | Requires new storage account, overcomplicated |
| Patch Plane source code for R2 | Makes R2 work | Fragile, breaks on Plane upgrades, significant effort |
Consequences
What becomes easier
- Durable storage with 11 nines availability
- Encryption at rest (SSE-S3 or SSE-KMS)
- Scoped IAM credentials per bucket
- Aligns with potential AWS infrastructure migration
What becomes harder
- Cross-cloud traffic (Azure AKS → AWS S3) adds latency
- Two cloud providers to manage credentials for
- S3 egress costs (minimal for internal tool with small team)
Risks
- If AWS infrastructure migration doesn't happen, S3 becomes a permanent cross-cloud dependency. Mitigation: egress costs are negligible for an internal tool, and S3 is the most portable storage API.