Skip to main content

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:tag
  • oci://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 fieldWhere 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 AcrPull on 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.yaml files.

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 AzureWebApp or AwsAppRunner tomorrow 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 via valueFrom.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.