Manifests as OCI Artifacts
When a Workload needs Kubernetes shapes that the SDK doesn't generate — StatefulSet, DaemonSet, init containers, sidecars, custom annotations, NetworkPolicies — the dev ships a manifest bundle in their repo's .fractal/ directory and publishes it to the container registry as an OCI artifact. The agent fetches it at deploy time, applies the manifests, and overlays the SDK-derived fields on top.
This is the "escape hatch" for the rare cases where the typed SDK isn't enough. The standard SDK path remains the default — most workloads need none of this.
Repo layout
<repo-root>/
.fractal/
<componentId>-fdeploy.yaml # the workload's manifests
fractal-parameters.yml # per-environment substitution values
One <componentId>-fdeploy.yaml per Workload component. The filename's prefix matches the component's id exactly. A repo containing two workloads looks like:
.fractal/
producer-fdeploy.yaml
dashboard-fdeploy.yaml
fractal-parameters.yml
The manifest file may declare any K8s objects the workload needs — Deployment, StatefulSet, DaemonSet, Service, ConfigMap, Mapping, NetworkPolicy, etc. The agent applies all of them.
Parameter substitution
fractal-parameters.yml defines per-environment values. The agent matches the entry whose name equals the live system's environment short-name, then expands every $VAR placeholder in the workload's manifest with the matching key.
environments:
- name: dev
parameters:
NAMESPACE: my-app
IMAGE: registry.example.com/my-app:v1
HOSTNAME: dev.example.com
- name: prod
parameters:
NAMESPACE: my-app
IMAGE: registry.example.com/my-app:v1.0
HOSTNAME: app.example.com
Substitution rule: regex \$VAR over the raw manifest text. Unknown placeholders are left as-is so typos surface as visible artifacts in the applied resource rather than empty strings. Allowed characters in VAR: letters, digits, underscore.
Publishing the bundle
The dev's CI bundles .fractal/ as a tar and pushes it as an OCI artifact to the same registries already used for container images. ORAS is the canonical client.
tar -cf manifests.tar .fractal
oras push \
"$REGISTRY/<your-app>-manifests:${{ github.sha }}" \
--artifact-type "application/vnd.fractal.workload-manifests.v1+tar" \
manifests.tar:application/vnd.fractal.workload-manifests.v1+tar
oras tag "$REGISTRY/<your-app>-manifests:${{ github.sha }}" latest
Push to whichever registries the target clouds need — typically both ACR and the Aruba container registry for orgs with both targets.
There is intentionally no default location. The dev names the artifact however they want — repo-scoped, environment-tagged, digest-pinned. The live-system layer pins the explicit URI.
A complete example workflow lives in cloud-conf-demo/.github/workflows/fractal-manifests.yml.
Pinning from the live system
The OCI URI is set on the live-system K8s satisfier:
import {CaaSK8sWorkload} from '@fractal_cloud/sdk';
CaaSK8sWorkload.satisfy(blueprint.dashboard)
.withReplicas(2)
.withManifestUri(
'oci://crisiswatchacr.azurecr.io/cloud-conf-demo-manifests:latest',
)
.build();
Both URI shapes are accepted:
oci://host/repo:tagoci://host/repo@sha256:<digest>(recommended for production immutability)
The oci:// prefix is optional; a bare host/repo:tag works too.
When withManifestUri is not called, the agent generates a Deployment from SDK params alone. There is no convention-based default fallback.
SDK overlay precedence
When a manifest bundle is in play, the agent applies it first, then overwrites a fixed set of fields on whichever doc is the workload-shaped resource (Deployment / StatefulSet / DaemonSet whose name matches the component ID):
| SDK field | Where it overlays |
|---|---|
withReplicas(n) | spec.replicas (Deployment, StatefulSet — ignored on DaemonSet) |
withContainerImage(s) | spec.template.spec.containers[0].image |
withContainerPort(n) | spec.template.spec.containers[0].ports[0] |
withCpu(s) / withMemory(s) | spec.template.spec.containers[0].resources.{requests,limits} |
withResourceRequests({...}) | spec.template.spec.containers[0].resources.requests |
withResourceLimits({...}) | spec.template.spec.containers[0].resources.limits |
Anything else in the bundle — sidecars at containers[1+], init containers, podSecurityContext, topologySpreadConstraints, serviceAccountName, ConfigMaps, Services, Mappings, NetworkPolicies — flows through untouched.
The rule, succinctly: YAML defines the shape; SDK params define the workload-level knobs.
Auth
The agent fetches the OCI artifact through the same auth the rest of caas-k8s uses:
- Aruba KaaS: registry credentials from Vault (env-init service writes them at
fractal_cloud/environments/<env>/aruba/container-registry). - AKS: Azure AD workload identity federation. No static credentials needed; the agent's pod has a federated SA mapped to a role with
AcrPullon the target registry.
No SSH keys, no Git access, no per-repo credentials.
When to use this
Reach for withManifestUri only when:
- The workload needs a K8s shape the SDK doesn't directly model —
StatefulSet,DaemonSet,CronJob,Job. - The workload has features the SDK can't express — sidecar containers, init containers, custom security contexts, projected volumes,
topologySpreadConstraints, NetworkPolicies, custom annotations consumed by an operator, etc. - Migrating from a legacy gitops setup that already had
.fractal/<name>-fdeploy.yamlfiles.
Stay on the typed SDK builder when:
- The workload is a stateless replicated service.
- The set of knobs you need maps to existing SDK methods (image, env, port, replicas, resources, ingress link, secret mounts).
- You want type-checked, autocompletable cross-cloud portability — a SDK Workload satisfier could become an
AzureWebApporAwsAppRunnertomorrow without touching the fractal definition.
Anti-patterns
- Don't use the bundle path to work around bugs in the SDK builders. Open an issue instead — the SDK should grow.
- Don't put cleartext secrets in
fractal-parameters.yml. Substitution is plain text — values land in your bundle and your registry. Sensitive values should live in Kubernetes Secrets provisioned by the env-init service or by a link, and referenced viavalueFrom.secretKeyRef. - Don't depend on substitution for runtime values that change frequently. Each substitution requires a re-publish of the bundle. For values that vary per LiveSystem instance (dynamic endpoints, generated credentials), use the link framework — those values resolve at reconcile time without rebuilding the bundle.