base-jobs/doc/source/docker-image.rst
Clark Boylan eb8826b605 Replace blockdiag and seqdiag with graphviz in docs
blockdiag, seqdiag and their associated sphinx extension are no longer
maintained. This wasn't an issue until the version of Pillow we need for
blockdiag/seqdiag stopped having wheels built for python3.12 and we
moved our default build env to python3.12. The good news is that sphinx
has a built in graphviz extension which should be well maintained that
we can switch to.

To make that switch we do need to install graphviz for the `dot` command
instead of libjpeg-dev since graphviz isn't a native python tool. But
this tradeoff seems reasonable since this software is maintained.

Change-Id: I60ba6235fcfc28508ce10bb60854c0dc62705e0c
2024-09-23 18:30:26 -07:00

11 KiB

Container Images

The jobs described in this section all work together to handle the full gating process for continuously deployed container images. They can be used to build or test images which rely on other images using the full power of Zuul's speculative execution.

There are a few key concepts to keep in mind:

A buildset is a group of jobs all running on the same change.

A buildset registry is a container image registry which is used to store speculatively built images for the use of jobs in a single buildset. It holds the differences between the current state of the world and the future state if the change in question (and all of its dependent changes) were to merge. It must be started by one of the jobs in a buildset, and it ceases to exist once that job is complete.

An intermediate registry is a long-running registry that is used to store images created for unmerged changes for use by other unmerged changes. It is not publicly accessible and is intended only to be used by Zuul in order to transfer artifacts from one buildset to another. OpenDev maintains such a registry.

With these concepts in mind, the jobs described below implement the following workflow for a single change:

digraph image_transfer {

splines=false nodesep=1

// Set things up like a spreadsheet grid as I found that simplifies // remembering which nodes have edges between them. ir_start [label="Intermediate Registry"] ir_end [style=invis] ir_0 [label="" shape=point height=.005] ir_1 [label="" shape=point height=.005] ir_2 [label="" shape=point height=.005] ir_3 [label="" shape=point height=.005] ir_4 [label="" shape=point height=.005] ir_5 [label="" shape=point height=.005] ir_start -> ir_0 -> ir_1 -> ir_2 -> ir_3 -> ir_4 -> ir_5 -> ir_end [arrowhead="none" style="bold"]

br_start [label="Buildset Registry"] br_end [style=invis] br_0 [label="" shape=point height=.005] br_1 [label="" shape=point height=.005] br_2 [label="" shape=point height=.005] br_3 [label="" shape=point height=.005] br_4 [label="" shape=point height=.005] br_5 [label="" shape=point height=.005] br_start -> br_0 -> br_1 -> br_2 -> br_3 -> br_4 -> br_5 [arrowhead="none" style="bold"] br_5 -> br_end [arrowhead="none" style="dashed"]

ij_start [label="Image Build Job"] ij_end [style=invis] ij_0 [label="" shape=point height=.005] ij_1 [label="" shape=point height=.005] ij_2 [label="" shape=point height=.005] ij_3 [label="" shape=point height=.005] ij_4 [label="" shape=point height=.005] ij_5 [label="" shape=point height=.005] ij_start -> ij_0 -> ij_1 [arrowhead="none" style="dashed"] ij_1 -> ij_2 [arrowhead="none" style="bold"] ij_2 -> ij_3 -> ij_4 -> ij_5 -> ij_end [arrowhead="none" style="dashed"]

tj_start [label="Deployment Test Job"] tj_end [style=invis] tj_0 [label="" shape=point height=.005] tj_1 [label="" shape=point height=.005] tj_2 [label="" shape=point height=.005] tj_3 [label="" shape=point height=.005] tj_4 [label="" shape=point height=.005] tj_5 [label="" shape=point height=.005] tj_start -> tj_0 -> tj_1 -> tj_2 -> tj_3 -> tj_4 [arrowhead="none" style="dashed"] tj_4 -> tj_5 [arrowhead="none" style="bold"] tj_5 -> tj_end [arrowhead="none" style="dashed"]

{rank=same;ir_start;br_start;ij_start;tj_start} {rank=same;ir_0;br_0;ij_0;tj_0} {rank=same;ir_1;br_1;ij_1;tj_1} {rank=same;ir_2;br_2;ij_2;tj_2} {rank=same;ir_3;br_3;ij_3;tj_3} {rank=same;ir_4;br_4;ij_4;tj_4} {rank=same;ir_5;br_5;ij_5;tj_5} {rank=same;ir_end;br_end;ij_end;tj_end}

// Flows between first and second column ir_0 -> br_0 [weight=0 label="Images from previous changes"] br_3 -> ir_3 [weight=0 label="Current image"] ir_end -> br_end [weight=0 style=invis]

// Flows between second and third column br_1 -> ij_1 [weight=0 label="Images from previous changes"] ij_2 -> br_2 [weight=0 label="Current image"] br_end -> ij_end [weight=0 style=invis]

// Flows between second and fourth column ij_0 -> tj_0 [style=invis] ij_4 -> tj_4 [weight=0 label="Current and previous images"] br_4 -> ij_4 [weight=0 arrowhead="none"] ij_end -> tj_end [weight=0 style=invis]

}

The intermediate registry is always running and the buildset registry is started by a job running on a change. The "Image Build" and "Deployment Test" jobs are example jobs which might be running on a change. Essentially, these are image producer or consumer jobs respectively.

There are two ways to use the jobs described below:

A Repository with Producers and Consumers

The first is in a repository where images are both produced and consumed. In this case, we can expect that there will be at least one image build job, and at least one job which uses that image (for example, by performing a test deployment of the image). In this case we need to construct a job graph with dependencies as follows:

digraph inheritance {

rankdir="LR"; node [shape=box]; "opendev-buildset-registry" -> "build-image" [dir=back]; "build-image" -> "test-image" [dir=back];

}

The :zuulopendev-buildset-registry job will run first and automatically start a buildset registry populated with images built from any changes which appear ahead of the current change. It will then return its connection information to Zuul and pause and continue running until the completion of the build and test jobs.

The build-image job should inherit from :zuulopendev-build-docker-image, which will ensure that it is automatically configured to use the buildset registry.

The test-image job is something that you will create yourself. There is no standard way to test or deploy an image, that depends on your application. However, there is one thing you will need to do in your job to take advantage of the buildset registry. In a pre-run playbook, use the use-buildset-registry role:

- hosts: all
  roles:
    - use-buildset-registry

That will configure the docker daemon on the host to use the buildset registry so that it will use the newly built version of any required images.

A Repository with Only Producers

The second way to use these jobs is in a repository where an image is merely built, but not deployed. In this case, there are no consumers of the buildset registry other than the image build job, and so the registry can be run on the job itself. In this case, you may omit the :zuulopendev-buildset-registry job and run only the :zuulopendev-build-docker-image job.

Publishing an Image

So far we've covered the image building process. This system also provides two more jobs that are used in publishing images to Docker Hub.

The :zuulopendev-upload-docker-image job does everything the :zuulopendev-build-docker-image job does, but it also uploads the built image to Docker Hub using an automatically-generated and temporary tag. The "build" job is designed to be used in the check pipeline, while the "upload" job is designed to take its place in the gate pipeline. By front-loading the upload to Docker Hub, we reduce the chance that a credential or network error will prevent us from publishing an image after a change lands.

The :zuulopendev-promote-docker-image jobs is designed to be used in the promote pipeline and simply re-tags the image on Docker Hub after the change lands.

Keeping in mind that everything described above in buildset_image_transfer applies to the :zuulopendev-upload-docker-image job, the following illustrates the additional tasks performed by the "upload" and "promote" jobs:

digraph image_transfer {

splines=false nodesep=1

// Set things up like a spreadsheet grid as I found that simplifies // remembering which nodes have edges between them. dh_start [label="Docker Hub"] dh_end [style=invis] dh_0 [label="" shape=point height=.005] dh_1 [label="" shape=point height=.005] dh_2 [label="" shape=point height=.005] dh_start -> dh_0 -> dh_1 -> dh_2 -> dh_end [arrowhead="none" style="bold"]

ui_start [label="upload-image"] ui_end [style=invis] ui_0 [label="" shape=point height=.005] ui_1 [label="" shape=point height=.005] ui_2 [label="" shape=point height=.005] ui_start -> ui_0 [arrowhead="none" style="bold"] ui_0 -> ui_1 -> ui_2 -> ui_end [arrowhead="none" style="dashed"]

pi_start [label="promote-image"] pi_end [style=invis] pi_0 [label="" shape=point height=.005] pi_1 [label="" shape=point height=.005] pi_2 [label="" shape=point height=.005] pi_start -> pi_0 -> pi_1 [arrowhead="none" style="dashed"] pi_1 -> pi_2 [arrowhead="none" style="bold"] pi_2 -> pi_end [arrowhead="none" style="dashed"]

{rank=same;dh_start;ui_start;pi_start} {rank=same;dh_0;ui_0;pi_0} {rank=same;dh_1;ui_1;pi_1} {rank=same;dh_2;ui_2;pi_2} {rank=same;dh_end;ui_end;pi_end}

// Flows between first and second column ui_0 -> dh_0 [weight=0 label="Current Image with Temporary Tag"] dh_end -> ui_end [weight=0 style=invis]

// Flows between first and third column dh_1 -> ui_1 [weight=0 arrowhead="none"] ui_1 -> pi_1 [weight=0 label="Current Image Manifest with Temporary Tag"] pi_1 [xlabel="Only the manifest is transferred,nnot the actual image layers"] pi_2 -> ui_2 [weight=0 label="Current Image Manifest with Final Tag" arrowhead="none"] ui_2 -> dh_2 [weight=0 label=""] dh_end -> pi_end [weight=0 style=invis]

}

Jobs