OCI Registry — Store more than Container Images
The swift progress of container technologies necessitates standardized practices, notably guided by the Open Container Initiative (OCI).
This article examines the new OCI specifications crucial for standardizing container images and artifacts’ operation, structure, and distribution. The practical application of these standards in a registry to store artifacts is presented.
Introduction to OCI Specifications
The Open Container Initiative (OCI) specifications are defined for standardizing container technologies. There are 3 specifications.
First, the Runtime Specification defines the environment where containers operate. Next, the Image Specification defines how container images are organized. Finally, the Distribution Specs defines how to serve these images.
💡 OCI Specifications
- Runtime Specifications — latest release v1.2.0 — Feb 13th, 2024
- Image Specifications — latest release v1.1.0 — Feb 15th, 2024
- Distribution Specifications — latest release v1.1.0 — Feb 15th, 2024
Together, the Image and Distribution Specs have been updated to support more than container images. Before going to the new features, let’s focus first on image format and storage.
First, the registry is a service that implements the required HTTP API endpoints to push and pull images.
Then, a repository is a collection of related images, as different versions of the same application, stored within the registry. It represents a namespace for grouping images.
Then, the reference is a generic pointer to an image within a repository. It can be a tag, a human readable string, or a digest providing a unique hash for the image content, ensuring immutability and verification.
Usually, the registry clients support both references simultaneously: tag and digest for human readability and unicity, but, in that case, only the digest is sent to registry API.
The subsequent paragraphs provide explanations of the diagram presented below.
Tags Management
The registry implements a tags list endpoint /v2/<name>/tags/list
to retrieve the related tags from the <name>
repository. The response returns the list of stored tags on this repository with the following format.
$ curl https://registry.my.labs/v2/kube-apiserver-amd64/tags/list
{
"name": "kube-apiserver-amd64",
"tags": [
"v1.28.0",
"v1.28.1",
"v1.28.2",
"v1.28.3",
"v1.28.4",
"v1.28.5",
"v1.28.6",
"v1.28.7",
"v1.29.1",
"v1.29.2"
]
}
So, the tags are named-pointers to a manifest in a repository. They are designed to be human-readable.
It’s possible to assign multiple tags, each pointing to distinct manifests, allowing the management of different image versions. Multiple tags can be assigned to the same manifest: for instance, the latest
tag. And a tag can be reassigned to a different manifest.
Manifests Management
Image Manifest
To access the manifest pointed by a tag, the endpoint is the following for a given <name>
repository and <tag>
tag: /v2/<name>/manifests/<tag>
.
A manifest is a JSON document that describes an object in the given repository.
$ curl https://registry.my.labs/v2/kube-apiserver-amd64/manifests/v1.29.2
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha26:786295faae…",
"size": 2564
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:aba5379b9c…",
"size": 84572
},
…
]
}
In this case, the manifest is an image manifest as described in mediaType key,
Then, the config part describes the image configuration. It references a blob by its digest within this repository.
Blobs are the binary form of the content that is stored by a registry, addressable by its digest.
So, the config blob defines the image’s runtime configuration, including environment variables, default command to run, and other settings coming from the tool used to build the image.
The next part are the layers of the image. Each layer references a blob by its digest.
The layers blobs are the file system layers. Each layer is built upon the previous one, creating a full filesystem of the image when combined.
Image Index
$ curl https://registry.my.labs/v2/opentelemetry-operator/manifests/latest
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:7db3ebbdd5480eb18035d1ef8f8dd5604923bc0796d6ff80ac6025365fed214e",
"size": 673,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:97bcecfaefc88fa27b1a81f9ec3bc18ee2fd50bd7163bf8bb8448ba0f7e6f307",
"size": 673,
"platform": {
"architecture": "arm64",
"os": "linux"
}
}
]
}
Another type of manifest is image index that points to multiple image manifests as denoted by the mediaType key and the manifests list. Here, both manifests point to the same image version but with different architecture.
The pointed image manifests are the same as explained above, except that the image manifest is stored as a blob.
So, the usual use of OCI Registry is for image storage, let’s see now its expanded use to store artifacts.
OCI Artifacts
OCI artifacts storage uses the same principles explained before using an image manifest metadata to identify the artifacts and using the layers blobs to store their contents.
For several years, some projects have been coming up with different ways to specify artifacts in the registry.
First by storing content on the same repository as the image, such as signatures and software bill of materials. The usual implementation uses a well-known tag to identify the artifact.
Another method relied on using a dedicated repository to store content.
This is the method used by tools such as Helm, FluxCD, Open Policy Agent bundles…
In the new OCI release, the use of image manifest metadata and layers to store contents
has been finally codified as a valid way with some adjustments.
For new clients pushing artifacts, there is a new top-level key called artifactType which can be used to denote a custom artifact.
A new key called subject can now be included on manifests which points to another object in the registry. This feature can establish relationships for linking objects in the registry (image and/or artifacts). To discover these relationships, a new endpoint called referrers has been implemented.
ORAS implements these new specifications. ORAS means OCI Registry As Storage.
To demonstrate these changes, ORAS CLI tool is used in the following paragraphs to achieve the process represented in the following diagram.
Push Content
First, let’s create a resource file, basically, push a test namespace.
$ cat namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: test
$ oras push registry.my.labs/oras/test:v1 \
--artifact-type application/vnd.example.manifests.v1 \
namespace.yaml:application/yaml
Uploading d6f2a1f4c156 namespace.yaml
Uploaded d6f2a1f4c156 namespace.yaml
Pushed [registry] registry.my.labs/oras/test:v1
Digest: sha256:ba4a52f4c16afeb6af9e5fa7c833ea4d9e806bf032e9f10e4ea337ca6809469e
Let’s use oras push
command to push this YAML resource to the local registry: oras/test:v1
. Define the artifact type: example vendor of Kubernetes manifests. For each pushed file, define the filename with its type: namespace.yaml
and application/yaml
. After pushing the artifact, you get the digests of all uploaded files and the related image manifest.
Let’s see the created manifest.
$ curl https://registry.my.labs/v2/oras/test/manifests/v1 | jq .
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.example.manifests.v1",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3…",
"size": 2
},
"layers": [
{
"mediaType": "application/yaml",
"digest": "sha256:d6f2a1f4c156…",
"size": 54,
"annotations": {
"org.opencontainers.image.title": "namespace.yaml"
}
}...
It’s still a JSON file representing an image manifest:
- the same mediaType as for an image
- the new artifactType and the referenced layer are those passed to the ORAS CLI just before
To pull the YAML resource from the registry, use oras pull
command using the same URL as the push command.
$ oras pull registry.my.labs/oras/test:v1
Downloading d6f2a1f4c156 namespace.yaml
Downloaded d6f2a1f4c156 namespace.yaml
Pulled [registry] registry.my.labs/oras/test:v1
Digest: sha256:ba4a52f4c16afeb6af9e5fa7c833ea4d9e806bf032e9f10e4ea337ca6809469e
$ cat namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: test
So, ORAS download and extract the namespace.yaml
file.
So, it’s straightforward to push and pull artifacts to the OCI registry.
Sign Content
Now, let’s sign the just pushed resource and push & attach the signature to the content.
$ cat signature.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
"signature": "sestegra"
}
$ oras attach registry.my.labs/oras/test:v1 \
--artifact-type=application/vnd.example.signature.v1+json \
signature.json:application/json
Uploading 2dc8164f1420 signature.json
Uploaded 2dc8164f1420 signature.json
Attached to [registry] registry.my.labs/oras/test@sha256:ba4a52f4c16a…
Digest: sha256:73499a9600d96c6bc46671affd5b58559a1bb1b99644d8638fbb563bee7a3982
Let’s assume the given JSON is a valid signature.
Use the oras attach
command, to push the signature artifact to the same repository as the YAML resource and to attach the signature to the YAML resource
As before, define the artifact type pushed to the registry: example vendor of signature. And define the filename with its type: signature.json
and application/json
. After pushing and attached to the YAML resource, the digest of the related image manifest is returned.
Let’s see the created manifest.
$ curl https://registry.my.labs/v2/oras/test/manifests/sha256:73499a9... | jq .
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.example.signature.v1+json",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3…",
"size": 2
},
"layers": [
{
"mediaType": "application/json",
"digest": "sha256:2dc8164f1420…",
"size": 153,
"annotations": {
"org.opencontainers.image.title": "signature.json"
}
}
],
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:ba4a52f4c16a…",
"size": 572
},
"annotations": {
"org.opencontainers.image.created": "2024-03-17T08:42:37Z"
}
}
It’s still a JSON file representing an image manifest:
- the same mediaType as for an image
- the new artifactType and the referenced layer are those passed to the ORAS CLI just before
Then, extra information for relationships are present. The subject field points to the image manifest of the YAML resource (see sha256:ba4a52f4c16a…
when pushing this content to the local registry).
To discover all relationships of an artifact, the oras discover
command can be used.
$ oras discover registry.my.labs/oras/test:v1
Discovered 1 artifact referencing v1
Digest: sha256:ba4a52f4c16afeb6af9e5fa7c833ea4d9e806bf032e9f10e4ea337ca6809469e
Artifact Type Digest
application/vnd.example.signature.v1+json sha256:73499a9600d9…
$ oras pull registry.my.labs/oras/test@sha256:73499a9600d9…
Downloading 2dc8164f1420 signature.json
Downloaded 2dc8164f1420 signature.json
Pulled [registry] registry.my.labs/oras/test:v1@sha256:73499a9600d9…
Digest: sha256:73499a9600d96c6bc46671affd5b58559a1bb1b99644d8638fbb563bee7a3982
$ cat signature.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
"signature": "sestegra"
}
So, the list of attached artifacts is returned, here the signature (see attached digest sha256:73499a9600d9…
when pushing the content’s signature to the local registry).
Now, let’s pull the signature from the registry with oras pull
command as before using the returned digest.
So, ORAS download and extract the signature.
So, it’s straightforward to get the attached signature from the tag of the YAML resource.
Create and Attach the SBOM
Let’s continue to push stuff on OCI registry, let’s create a software bill of materials and push and attach it the resource.
$ cat sbom.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
”components": […]
}
$ oras attach registry.my.labs/oras/test:v1 \
--artifact-type=application/vnd.example.sbom.v1+json \
sbom.json:application/json
Uploading adebfc263c9c sbom.json
Uploaded adebfc263c9c sbom.json
Attached to [registry] registry.my.labs/oras/test@sha256:ba4a52f4c16afeb...
Digest: sha256:e3ef102e5c87576a7cc66885bf6d50608c86469f42da10f877f75c71184ad008
A SBOM consists of the list of the used components. Let’s assume the given JSON is a valid SBOM.
Use oras attach
command to push the SBOM artifact to the same repository as the YAML resource and to attach the SBOM to the YAML resource.
As before, define the artifact type pushed to the registry: example vendor of SBOM. As before, define the filename with its type: sbom.json
and application/json
. After pushing, and attached to the artifact, the digest of the related image manifest is returned.
The content of the created image manifest is similar than before.
So, where we are in the global process?
- Create an artifact containing YAML resources
- Sign and attach the signature to the YAML resources
- Create SBOM and attach it to the YAML resources
Sign SBOM, Push & Attach its Signature
Now, let’s sign the SBOM, and attach the SBOM signature to the SBOM artifact.
$ cat sbom-signature.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
"signature": "sestegra"
}
$ oras attach registry.my.labs/oras/test@sha256:e3ef102e5c87... \
--artifact-type=application/vnd.example.signature.v1+json \
sbom-signature.json:application/json
Uploading 1743cdc854d6 sbom-signature.json
Uploaded 1743cdc854d6 sbom-signature.json
Attached to [registry] registry.my.labs/oras/test@sha256:e3ef102e5c87576a...
Digest: sha256:bae78f64dc90d9e0d135342cd4ce938b746ca9d5e194a21640475fe7d492f1df
As before, let’s assume this is a valid signature represented by a JSON file.
Use oras attach
command to push the SBOM signature artifact to the same repository as the initial artifact, and to attach the SBOM signature to the SBOM.
As before, define the artifact type pushed to the registry: example vendor of signature As before, define the filename with its type: sbom-signature.json
and application/json
. After pushing, and attached to the SBOM artifact, the digest of the related image manifest is returned.
Discover Referrers
Now, to discover the relationships just created, use the oras discover
command using the repository oras/test
and v1
tag with the tree
option to get the hierarchy of all pushed artifacts.
$ oras discover -o tree registry.my.labs/oras/test:v1
registry.my.labs/oras/test@sha256:ba4a52f4c16afeb6af9e5fa7c83...
├── application/vnd.example.signature.v1+json
│ └── sha256:73499a9600d96c6bc46671affd5b58559a1bb1b99644d8638fbb563bee7a3982
└── application/vnd.example.sbom.v1+json
└── sha256:e3ef102e5c87576a7cc66885bf6d50608c86469f42da10f877f75c71184ad008
└── application/vnd.example.signature.v1+json
└── sha256:bae78f64dc90d9e0d135342cd4ce938b746ca...
The hierarchy is the expected as pushed artifacts.
- First the YAML resource on top
- With the attached signature
- With the attached SBOM
- And the signature of the SBOM attached to the SBOM
So, it’s straightforward to discover and pull all artifacts in these relationships by only knowing the initial pushed content.
Conclusion
The OCI specifications provide a consistent and reliable framework for containers images and artifacts across various environments. They standardize runtime, image, and distribution, ensuring predictable, secure container operations, irrespective of the infrastructure.
This standardization promotes innovation, collaboration, and scalability in container images and artifacts deployments.
References
- Open Container Initiative (OCI)
https://opencontainers.org/ - OCI Runtime Specifications
https://github.com/opencontainers/runtime-spec - OCI Image Specifications
https://github.com/opencontainers/image-spec - OCI Distribution Specifications
https://github.com/opencontainers/distribution-spec - ORAS — OCI Registry As Storage
https://oras.land/