Build gitlab-rails before waiting for the assets image
What does this MR do?
Splits the Debian gitlab-rails-* build so that only a short final step needs the assets image, when that image doesn't exist yet. This is step 2 of #2441.
Most of the rails build (bundle install, gettext:compile, bootsnap) doesn't use the assets. But the assets image is a FROM stage, so none of it can start until that image exists. Since gitlab-org/gitlab!263017 (merged), E2E on CNG starts gitlab-rails-ee before the assets image is built, so in cold MR pipelines the job waits for it. With this MR, that work overlaps with the wait.
In paired test pipelines (below), the split got the rails job done 1.9–3.6 minutes earlier in pairs 1, 3, 5 and 6, about the time the control spent waiting. The split adds no measurable build time. In production, cold MR pipelines wait a median of 2 minutes (90th percentile 3.5, longest 13.5), so I expect about 2 minutes saved per cold pipeline, and up to about 6 minutes when the assets build is slow.
gitlab-rails/Dockerfile.erb
-
The runtime stage is now named
rails-no-assets. It has every runtime layer except the assets. -
The
assetsstage is declared at the end and used only by a new last stage,final, which is still the default target:FROM rails-no-assets AS final COPY --chown=git --from=assets --exclude=webpack/**/*.map assets /srv/gitlab/public/assets/ -
--excludereplaces the oldfind … -name '*.map' -delete, so the maps are never added to a layer. WithCOMPILE_ASSETS=truethefindstays inbuilderand there's no assets stage. -
The published image has the same files, ownership and modes. The assets are now their own last layer instead of being inside the
/srv/gitlabcopy.
build-scripts/build.sh
build_target_only <stage> [args]runsbuild_if_neededwith--target. It skips the tag, push,--metadata-file, zstd output,skopeo copy, signing and artifact files.- It keeps the same
--cache-to/--cache-fromrefs, so the final build reuses its layers from the sticky BuildKit node or from the registry cache. - It skips the build when
needs_buildis false.
- It keeps the same
rails_split_assets_buildreadsRAILS_SPLIT_ASSETS_BUILD:- It is off by default.
RAILS_SPLIT_ASSETS_BUILD=trueturns it on. - It always stays off for UBI, FIPS, nix,
COMPILE_ASSETS, stable branches, tags and auto-deploy, and when the build cache is disabled (DISABLE_DOCKER_BUILD_CACHE,CACHE_DOCKER_BUILD=false). Phase B only reuses Phase A through the cache.
- It is off by default.
rails_split_assets_build_nowisrails_split_assets_build, but only while the assets image is missing (assets_image_availablefrom !3173 (merged)). Once the image exists, a single build is faster, since there's nothing left to overlap. It logs which path it takes.build_if_neededno longer copiesshared/toshared/sharedon a second build in the same job.
.gitlab/ci/common.gitlab-ci.yml (.gitlab-rails-ee, .gitlab-rails-ce)
- The build args are collected once in
rails_build_args, so both phases use exactly the same args, which keeps the cache hits. - When the split is on:
download_gitlab, then the image check. If the image is still missing: Phase A (build_target_only rails-no-assets). Thenfetch_assets, Phase B (build_final_image),push_tagsand the vendor bundle extraction.- The check runs after
download_gitlab, which doesn't need the assets, so the image has the most time to appear. In one test run the download took more than 6 minutes and the image appeared meanwhile, so the job built in one step.
- The check runs after
- When it's off, the order is the same as today:
fetch_assetsfirst, then the single build. UBI/FIPS keepfetch_assetsbeforebuild_asset_image. ci_files/final_images.ymland the downstream images are unchanged. Webservice, sidekiq, toolbox and workhorse still use the full rails image.
When to turn it on
The split only saves time when gitlab-rails-* starts before the assets image exists, for example in pipelines that gitlab-org/gitlab!263017 (merged) triggers as soon as the assets hash is known. It's opt-in: a pipeline that wants it passes RAILS_SPLIT_ASSETS_BUILD=true. gitlab-org/gitlab already sends true from E2E on CNG.
The wait-for-assets-* gate from !3173 (merged) skips pipelines that set RAILS_SPLIT_ASSETS_BUILD=true, so the rails job can start early.
How was it tested?
Paired pipelines
Each pair ran two gitlab-org/gitlab MR pipelines at the same time, each with a cold assets compile (a different comment in main.js). Run A used this branch, run B used the CNG master commit it's based on (8b57dab5), and both set RAILS_SPLIT_ASSETS_BUILD=true. Times are seconds within gitlab-rails-ee, and minutes from pipeline start for when it finished.
| Pair | Path in A | B waited for the image | A: Phase A + Phase B | B: single build | Rails job done, A / B |
|---|---|---|---|---|---|
| 1 | split | 180s | 618 + 55 = 674s | 682s | 18.1 / 21.6 min (−3.5) |
| 2 | split, checked at job start (old version) | 240s | 413 + 63 = 476s | 386s | 21.1 / 18.6 min (+2.5) |
| 3 | split | 150s | 385 + 63 = 448s | 439s | 15.3 / 17.2 min (−1.9) |
| 4 | one step: the image existed after a 387s download | 690s | 422s (single) | 451s | not comparable: B's assets image was 9 min late |
| 5 | split | 270s | 330 + 53 = 382s | 454s | 15.9 / 19.5 min (−3.6, flatters A: B's assets image was 1.9 min later) |
| 6 | split | 210s | 399 + 61 = 460s | 432s | 14.2 / 17.5 min (−3.3) |
- A split job never waited for the image: Phase A always outlasted the wait.
- Phase B takes 53–63 seconds, but Phase A is shorter than a full single build. The split's total build time differed from B's single build by −72 to +90 seconds, about +10 on average, which is within the run-to-run noise.
- So the split saves about what B spent waiting.
- Pair 2 used the earlier version, which checked for the image at the start of the job. Its download took 319 seconds, the image appeared meanwhile, and the job paid for the split without overlapping anything. That's why the check now runs after
download_gitlab, which pair 4 confirms. - With the image already there (a warm pipeline), the job builds in one step, as before.
Image
- In pair 2, E2E on CNG deployed the split image with the Helm chart and passed: 26 of 27 jobs, the 27th failed on a DNS error in the test runner before any test ran. Webservice, sidekiq, toolbox and workhorse built on top of it.
Local and unit tests
I used BuildKit moby/buildkit:v0.26.3, the same as BUILDKIT_IMAGE in CI:
--target rails-no-assetssucceeds whenASSETS_IMAGEdoesn't exist. BuildKit doesn't resolve the unused stage, so no fallback Dockerfile is needed.COPY --exclude=webpack/**/*.mapworks with the built-in frontend (no# syntax=line). It removes maps at every depth underwebpack/and keeps other files.- In a second build with the default target, every Phase A step is
CACHED. Only the assetsCOPYruns. buildx build --checkon the rendered EE andCOMPILE_ASSETSDockerfiles gives the same warnings asmaster.build-scripts/tests/rails_split_assets_build.batscovers the opt-in, the builds that never split, and that the split only happens while the assets image is missing (stubbedskopeo).
Still to check:
- The UBI/FIPS pipeline (
fips_pipeline) is unchanged: https://gitlab.com/gitlab-org/build/CNG/-/pipelines/2996725833. - CE builds:
gitlab-rails-cein https://gitlab.com/gitlab-org/build/CNG/-/pipelines/2996723077. -
COMPILE_ASSETS=truestill works: checked in review rather than in a pipeline (!3180 (comment 4143011016)). The rendered Dockerfile only differs frommasterin comments, blank lines and the stage name, and neither split check returns true or callsskopeo, so the job runsfetch_assetsand a single build as before.
Related
- !3179 (merged) (merged): time limit for the assets image wait in
fetch_assets - gitlab-org/gitlab!263017 (merged) (merged): triggers CNG as soon as the assets hash is known, and sets
RAILS_SPLIT_ASSETS_BUILD=true - !3173 (merged) (merged):
wait-for-assets-*gate jobs, skipped whenRAILS_SPLIT_ASSETS_BUILD=true - gitlab-org/gitlab#630697 (closed): measurements
- Epic: gitlab-org#22918