Skip to content

Commit bbb0894

Browse files
committed
Add extra episode on building a container image in GitLab CI/CD
1 parent 74c43fc commit bbb0894

2 files changed

Lines changed: 324 additions & 1 deletion

File tree

config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ episodes:
7272
- 05-gitlabci-vomsproxy.md
7373
- 06-catservices.md
7474
- 07-final-run.md
75-
# - A-extra-building-image.md
75+
- A-extra-building-image.md
7676

7777
# Information for Learners
7878
learners:

episodes/A-extra-building-image.md

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
---
2+
title: "EXTRA: Building a Container Image in GitLab CI/CD"
3+
teaching: 10
4+
exercises: 10
5+
---
6+
7+
::::::::: questions
8+
9+
- How can I build a container image with my own code in a GitLab CI/CD pipeline?
10+
- How can I reuse the same container building configuration across multiple jobs?
11+
12+
:::::::::
13+
14+
::::::::: objectives
15+
16+
- Build and push a container image to the GitLab container registry using buildah.
17+
- Create a reusable `.buildah` template for container building.
18+
- Use container images built in CI/CD pipelines in your analysis workflows.
19+
20+
:::::::::
21+
22+
## Building Container Images with Buildah
23+
24+
[Buildah](https://buildah.io/) is a container build tool that works well in CI/CD environments without requiring Docker. In this lesson, you'll learn to build and store container images in the GitLab container registry within your CI/CD pipeline.
25+
26+
### What You'll Need
27+
28+
- A `Dockerfile` in your repository that defines your container image.
29+
- If unfamiliar with Docker, see the [HSF Docker tutorial](https://hsf-training.github.io/hsf-training-docker/index.html).
30+
31+
---
32+
33+
## Creating a Reusable Buildah Template
34+
35+
Instead of repeating buildah commands in every job, define a reusable `.buildah` template that other jobs can extend:
36+
37+
```yaml
38+
.buildah:
39+
stage: build
40+
image: quay.io/buildah/stable
41+
variables:
42+
DOCKER_FILE_NAME: "Dockerfile"
43+
REGISTRY_IMAGE_PATH: ${CI_REGISTRY_IMAGE}:latest
44+
EXTRA_TAGS: ""
45+
script:
46+
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
47+
- export BUILDAH_FORMAT=docker
48+
- export STORAGE_DRIVER=vfs
49+
- buildah build --storage-driver=${STORAGE_DRIVER} -f ${DOCKER_FILE_NAME} -t ${REGISTRY_IMAGE_PATH}
50+
- |
51+
if [ -n "${EXTRA_TAGS}" ]; then
52+
for tag in ${EXTRA_TAGS}; do
53+
buildah tag ${REGISTRY_IMAGE_PATH} ${CI_REGISTRY_IMAGE}:${tag}
54+
done
55+
fi
56+
- buildah push --storage-driver=${STORAGE_DRIVER} ${REGISTRY_IMAGE_PATH}
57+
- |
58+
if [ -n "${EXTRA_TAGS}" ]; then
59+
for tag in ${EXTRA_TAGS}; do
60+
buildah push --storage-driver=${STORAGE_DRIVER} ${CI_REGISTRY_IMAGE}:${tag}
61+
done
62+
fi
63+
```
64+
65+
**Template variables:**
66+
67+
- **`DOCKER_FILE_NAME`:** Path to your Dockerfile (default: `Dockerfile`).
68+
- **`REGISTRY_IMAGE_PATH`:** Full image path and tag (e.g., `registry.example.com/user/repo:tag`).
69+
- **`EXTRA_TAGS`:** Space-separated list of additional tags (optional, e.g., `"latest v1.0"`).
70+
71+
---
72+
73+
## Example 1: Simple Image Build and Push
74+
75+
Extend the `.buildah` template to build an image tagged with your commit hash:
76+
77+
```yaml
78+
stages:
79+
- build
80+
81+
build_image:
82+
extends: .buildah
83+
variables:
84+
DOCKER_FILE_NAME: "Dockerfile"
85+
REGISTRY_IMAGE_PATH: "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
86+
EXTRA_TAGS: "latest"
87+
```
88+
89+
**What happens:**
90+
91+
1. The image is tagged with your commit hash (e.g., `a1b2c3d`).
92+
2. It's also tagged as `latest` via `EXTRA_TAGS`.
93+
3. Both tags are pushed to your GitLab container registry.
94+
95+
Find your image at **Deploy > Container registry** in your GitLab project.
96+
97+
---
98+
99+
## Example 2: Conditional Builds with Rules
100+
101+
Build the container image only when the Dockerfile changes or on a schedule:
102+
103+
```yaml
104+
stages:
105+
- build
106+
107+
build_image:
108+
extends: .buildah
109+
variables:
110+
DOCKER_FILE_NAME: "Dockerfile"
111+
REGISTRY_IMAGE_PATH: "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
112+
EXTRA_TAGS: "latest"
113+
rules:
114+
- if: $CI_PIPELINE_SOURCE == "schedule"
115+
variables:
116+
EXTRA_TAGS: "latest scheduled"
117+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
118+
changes:
119+
- Dockerfile
120+
- if: $CI_COMMIT_BRANCH == "main"
121+
changes:
122+
- Dockerfile
123+
```
124+
125+
This ensures containers are only rebuilt when necessary, saving CI/CD time.
126+
127+
---
128+
129+
## Using Your Built Container Image
130+
131+
Once the image is pushed to the registry, use it in subsequent CI jobs or elsewhere:
132+
133+
### In GitLab CI/CD
134+
135+
```yaml
136+
test_with_image:
137+
stage: test
138+
image: ${CI_REGISTRY_IMAGE}:latest
139+
script:
140+
- echo "Running tests in the custom container"
141+
- # your test commands here
142+
```
143+
144+
### Locally (on LXPLUS or your machine)
145+
146+
```bash
147+
apptainer shell docker://${CI_REGISTRY_IMAGE}:latest
148+
```
149+
150+
Or with Docker:
151+
152+
```bash
153+
docker run ${CI_REGISTRY_IMAGE}:latest
154+
```
155+
156+
Replace `${CI_REGISTRY_IMAGE}` with your actual registry path (e.g., `gitlab-registry.cern.ch/username/repo`).
157+
158+
---
159+
160+
:::::: challenge
161+
162+
### Challenge: Build Your Own Container
163+
164+
1. Create a simple `Dockerfile` in your repository that compiles `CMSSW` like the previous example.
165+
2. Add a `build_image` job to your `.gitlab-ci.yml` that extends `.buildah`.
166+
3. Push your code and check **CI/CD > Pipelines** to watch the build.
167+
4. Find your image in **Deploy > Container registry**.
168+
5. Use it in a downstream CI job or locally with apptainer.
169+
170+
::::::
171+
172+
---
173+
174+
:::::: callout
175+
176+
**Tip:** The commit hash tag (e.g., `a1b2c3d`) ensures a one-to-one correspondence between your code and the built image, making it easy to reproduce analysis runs from a specific commit.
177+
178+
::::::
179+
180+
## Build an image that includes the compiled CMSSW area (from `cmssw_compile`)
181+
182+
If you already compile CMSSW in a `cmssw_compile` job and publish the full `${CMSSW_RELEASE}` as an artifact, you can bake that compiled area into a reusable runtime image. This lets downstream jobs (or local users) run `cmsRun` without rebuilding.
183+
184+
### 1) Ensure `cmssw_compile` exports the area as an artifact
185+
186+
```yaml
187+
variables:
188+
CMS_PATH: /cvmfs/cms.cern.ch
189+
CMSSW_RELEASE: CMSSW_10_6_30
190+
SCRAM_ARCH: slc7_amd64_gcc700
191+
192+
cmssw_compile:
193+
image: registry.cern.ch/docker.io/cmssw/el7:x86_64
194+
stage: compile
195+
tags: [k8s-cvmfs]
196+
script:
197+
- set +u && source ${CMS_PATH}/cmsset_default.sh; set -u
198+
- export SCRAM_ARCH=${SCRAM_ARCH}
199+
- cmsrel ${CMSSW_RELEASE}
200+
- cd ${CMSSW_RELEASE}/src && cmsenv
201+
- mkdir -p AnalysisCode && cp -r $CI_PROJECT_DIR/ZPeakAnalysis AnalysisCode/
202+
- cd ${CMSSW_RELEASE}/src && scram b -j 4
203+
artifacts:
204+
untracked: true
205+
expire_in: 1 hour
206+
paths:
207+
- ${CMSSW_RELEASE}
208+
```
209+
210+
### 2) Add a Dockerfile that copies the compiled area
211+
212+
Create `Dockerfile` in your repository:
213+
214+
```Dockerfile
215+
# Dockerfile
216+
FROM registry.cern.ch/docker.io/cmssw/el7:x86_64
217+
218+
ARG CMSSW_RELEASE
219+
ENV CMS_PATH=/cvmfs/cms.cern.ch \
220+
SCRAM_ARCH=slc7_amd64_gcc700 \
221+
CMSSW_RELEASE=${CMSSW_RELEASE} \
222+
CMSSW_BASE=/opt/${CMSSW_RELEASE}
223+
224+
# Copy the compiled CMSSW area from the CI workspace (artifact) into the image
225+
COPY ${CMSSW_RELEASE} /opt/${CMSSW_RELEASE}
226+
227+
SHELL ["/bin/bash", "-lc"]
228+
RUN echo 'source ${CMS_PATH}/cmsset_default.sh && cd ${CMSSW_BASE}/src && cmsenv' >> /etc/profile.d/cmssw.sh
229+
WORKDIR /opt/${CMSSW_RELEASE}/src
230+
```
231+
232+
Notes:
233+
234+
- The `COPY ${CMSSW_RELEASE} ...` works because the build job (below) uses `needs: cmssw_compile` with `artifacts: true`, so the compiled directory is present in the build context.
235+
- This image contains your compiled code, so downstream jobs can run `cmsRun` directly.
236+
237+
### 3) Build the image by extending the reusable `.buildah` template
238+
239+
Assuming you already defined the reusable `.buildah` template:
240+
241+
```yaml
242+
.buildah:
243+
stage: build
244+
image: quay.io/buildah/stable
245+
variables:
246+
DOCKER_FILE_NAME: "Dockerfile"
247+
REGISTRY_IMAGE_PATH: ${CI_REGISTRY_IMAGE}:latest
248+
EXTRA_TAGS: ""
249+
script:
250+
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
251+
- export BUILDAH_FORMAT=docker
252+
- export STORAGE_DRIVER=vfs
253+
- buildah build --storage-driver=${STORAGE_DRIVER} -f ${DOCKER_FILE_NAME} -t ${REGISTRY_IMAGE_PATH}
254+
- |
255+
if [ -n "${EXTRA_TAGS}" ]; then
256+
for tag in ${EXTRA_TAGS}; do
257+
buildah tag ${REGISTRY_IMAGE_PATH} ${CI_REGISTRY_IMAGE}:${tag}
258+
done
259+
fi
260+
- buildah push --storage-driver=${STORAGE_DRIVER} ${REGISTRY_IMAGE_PATH}
261+
- |
262+
if [ -n "${EXTRA_TAGS}" ]; then
263+
for tag in ${EXTRA_TAGS}; do
264+
buildah push --storage-driver=${STORAGE_DRIVER} ${CI_REGISTRY_IMAGE}:${tag}
265+
done
266+
fi
267+
```
268+
269+
Then add the image build job:
270+
271+
```yaml
272+
stages: [compile, build]
273+
274+
build_cmssw_image:
275+
stage: build
276+
extends: .buildah
277+
needs:
278+
- job: cmssw_compile
279+
artifacts: true
280+
variables:
281+
DOCKER_FILE_NAME: "Dockerfile"
282+
REGISTRY_IMAGE_PATH: "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
283+
EXTRA_TAGS: "latest"
284+
# Optional: pass release as build-arg
285+
BUILDAH_ARGS: "--build-arg CMSSW_RELEASE=${CMSSW_RELEASE}"
286+
before_script:
287+
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
288+
- export BUILDAH_FORMAT=docker
289+
- export STORAGE_DRIVER=vfs
290+
script:
291+
- buildah build --storage-driver=${STORAGE_DRIVER} ${BUILDAH_ARGS} -f ${DOCKER_FILE_NAME} -t ${REGISTRY_IMAGE_PATH}
292+
- |
293+
if [ -n "${EXTRA_TAGS}" ]; then
294+
for tag in ${EXTRA_TAGS}; do
295+
buildah tag ${REGISTRY_IMAGE_PATH} ${CI_REGISTRY_IMAGE}:${tag}
296+
done
297+
fi
298+
- buildah push --storage-driver=${STORAGE_DRIVER} ${REGISTRY_IMAGE_PATH}
299+
- |
300+
if [ -n "${EXTRA_TAGS}" ]; then
301+
for tag in ${EXTRA_TAGS}; do
302+
buildah push --storage-driver=${STORAGE_DRIVER} ${CI_REGISTRY_IMAGE}:${tag}
303+
done
304+
fi
305+
```
306+
307+
You can now use the image `${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}` (or `:latest`) in later jobs:
308+
309+
```yaml
310+
run_with_baked_image:
311+
stage: test
312+
image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
313+
tags: [k8s-cvmfs]
314+
script:
315+
- source /cvmfs/cms.cern.ch/cmsset_default.sh
316+
- cd /opt/${CMSSW_RELEASE}/src && cmsenv
317+
- cmsRun AnalysisCode/ZPeakAnalysis/test/MyZPeak_cfg.py
318+
```
319+
320+
Tips:
321+
322+
- Artifacts are read-only in downstream jobs, but Buildah only needs to read them (no extra `chmod` is required).
323+
- Tag images by commit for exact reproducibility; add `latest` for a moving pointer.

0 commit comments

Comments
 (0)