# Configure media storage

Select a media storage backend on `spec.media.storage` so the agent's facade accepts file uploads.

## Prerequisites

- An AgentRuntime. `type: none` is the default and disables media storage.
- For `local`: a ReadWriteMany PersistentVolumeClaim in the agent's namespace. The agent containers have a read-only root filesystem, so `local` needs a mounted volume.
- For `s3`, `gcs` or `azure`: a bucket or container, and either a workload identity that can reach it or credentials in a Secret.

| `type` | Use case | Credentials |
|--------|----------|-------------|
| `local` | Development, single-replica deployments | None |
| `s3` | AWS, or S3-compatible services such as MinIO | IRSA, EC2 instance profile, or a Secret |
| `gcs` | Google Cloud | Workload Identity (Application Default Credentials) |
| `azure` | Azure | An account key in a Secret |

## Steps

1. Give the agent pod access to the backend. Skip this step for `local`.

   Annotate the ServiceAccount the pod runs as. To use your own ServiceAccount, set `spec.podOverrides.serviceAccountName`.

   EKS (IRSA):

   ```yaml
   apiVersion: v1
   kind: ServiceAccount
   metadata:
     name: omnia-agent
     annotations:
       eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/omnia-media-role
   ```

   The role needs `s3:PutObject`, `s3:GetObject`, `s3:DeleteObject` and `s3:ListBucket` on the bucket (`arn:aws:s3:::my-media-bucket`) and its objects (`arn:aws:s3:::my-media-bucket/*`).

   GKE (Workload Identity):

   ```yaml
   apiVersion: v1
   kind: ServiceAccount
   metadata:
     name: omnia-agent
     annotations:
       iam.gke.io/gcp-service-account: omnia-media@my-project.iam.gserviceaccount.com
   ```

   Grant the Google service account `roles/storage.objectAdmin` on the bucket.

   To use explicit credentials instead, create a Secret and reference it in step 2. An S3 Secret holds the keys `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. An Azure Secret holds the key `AZURE_ACCOUNT_KEY`:

   ```yaml
   apiVersion: v1
   kind: Secret
   metadata:
     name: azure-storage-key
   type: Opaque
   stringData:
     AZURE_ACCOUNT_KEY: <your-storage-account-key>
   ```

   Azure always needs the account key. Upload and download URLs are SAS URLs signed with that key, so workload identity alone cannot produce them, even when the pod also uses workload identity.

2. Set `spec.media.storage`. Use the block for your backend.

   Local, with a PVC mounted at `basePath` in the facade and runtime containers:

   ```yaml
   apiVersion: omnia.altairalabs.ai/v1alpha1
   kind: AgentRuntime
   metadata:
     name: my-agent
   spec:
     media:
       storage:
         type: local
         local:
           basePath: /var/lib/omnia/media
           volumeClaim: omnia-media
   ```

   S3, keyless. `region` is required: the API server rejects an `s3` backend without one. An S3-compatible service that ignores regions still needs a value, such as `us-east-1`.

   ```yaml
   spec:
     media:
       storage:
         type: s3
         s3:
           bucket: my-media-bucket
           region: us-west-2
           prefix: omnia/media/   # optional
   ```

   For MinIO or another S3-compatible service, add `endpoint` (this also forces path-style addressing). For explicit credentials, add `secretRef`:

   ```yaml
   spec:
     media:
       storage:
         type: s3
         s3:
           bucket: my-bucket
           region: us-east-1
           endpoint: http://minio.minio-system:9000
         secretRef:
           name: my-s3-credentials
   ```

   GCS:

   ```yaml
   spec:
     media:
       storage:
         type: gcs
         gcs:
           bucket: my-media-bucket
           prefix: omnia/media/   # optional
   ```

   Azure:

   ```yaml
   spec:
     media:
       storage:
         type: azure
         azure:
           account: mystorageaccount
           container: media
         secretRef:
           name: azure-storage-key
   ```

3. Optionally set limits next to `type`: `maxFileSizeBytes`, `defaultTTL`, `uploadURLTTL` and `downloadURLTTL`. See [AgentRuntime `media.storage`](/reference/core/agentruntime/#mediastorage) for the fields and defaults.

4. Apply the AgentRuntime.

   ```bash
   kubectl apply -f agentruntime.yaml
   ```

## Verify

The operator renders the settings into `OMNIA_MEDIA_STORAGE_*` environment variables on the facade container. List their names:

```bash
kubectl get deployment my-agent \
  -o jsonpath='{.spec.template.spec.containers[?(@.name=="facade")].env[*].name}'
```

The output includes `OMNIA_MEDIA_STORAGE_TYPE`. Then confirm the facade started the backend:

```bash
kubectl logs deployment/my-agent -c facade
```

The log contains a `media storage enabled` entry with the backend type. To upload a file, follow [Upload media to an agent](/how-to/agents/upload-media-to-an-agent/).

## Troubleshooting

### The AgentRuntime is rejected with "type s3 requires spec.media.storage.s3.region"

The facade needs both a bucket and a region for `s3`, so the CRD refuses an `s3` backend without `s3.region`. Set it.

### Upload fails with "upload not found or expired"

The upload URL expired or was already used. The default `uploadURLTTL` is 15 minutes. Request a new upload URL, or raise `uploadURLTTL`.

### S3 "access denied"

1. Check that the IAM role grants the four actions in step 1.
2. Check the IRSA annotation on the ServiceAccount the pod runs as.
3. Check that the bucket policy allows the role.

### GCS "permission denied"

1. Check that Workload Identity is configured for the pod's ServiceAccount.
2. Check that the Google service account has `roles/storage.objectAdmin`.
3. Check the bucket IAM bindings.

### Azure "SAS URL generation requires shared key credential"

The facade has no account key. Add `secretRef` to `spec.media.storage` that points at a Secret with the key `AZURE_ACCOUNT_KEY`.

## Related pages

- [AgentRuntime reference: `media.storage`](/reference/core/agentruntime/#mediastorage)
- [Media metrics](/reference/platform/media-metrics/)
- [Upload media to an agent](/how-to/agents/upload-media-to-an-agent/)
- [WebSocket flows](/explanation/platform/websocket-flows/#file-upload) for the upload sequence