CI/CD YAML, Scripts, Images, Services, before_script, after_script, and Defaults: Configuration, Design Choices, and Tradeoffs
Choose maintainable job defaults, trusted images, service patterns, and shell portability deliberately, balancing reproducibility, isolation, compatibility, reliability, performance, governance, and cost.
Learning objectives
- Choose between defaults and explicit per-job configuration based on change radius and clarity.
- Evaluate minimal trusted images versus convenience-heavy images using provenance, attack surface, compatibility, and maintenance criteria.
- Choose service sidecars versus external test dependencies without confusing network convenience with production equivalence.
- Design shell commands for portability or deliberately constrain runners when platform-specific behavior is required.
- Use a decision table that includes maintainability, security, governance, reliability, compatibility, performance, and cost.
default,
image, services, before_script,
script, and after_script—are part of core
GitLab CI/CD and are available on Free, Premium, and Ultimate across
GitLab.com, Self-Managed, and Dedicated. Runtime behavior still
depends on the runner executor. In particular, container
image/services semantics require a
compatible container-capable executor; a Shell executor runs commands
directly on the runner host and has materially different isolation and
dependency assumptions. Hosted-compute quotas and registry
availability can change, so all mandatory learning also has a CI
Lint/log-fixture or local-container fallback.
1. Defaults reduce duplication but increase change radius
default is valuable when many jobs intentionally share
a contract: image, runner tags, retry policy, or setup/cleanup
hooks. The risk is hidden coupling. A one-line change to a default
may alter every job that does not override that key, including jobs
in included configuration.
A useful design question is: if this value changes, should every inheriting job change together? If yes, a default expresses policy. If no, explicit job configuration is usually clearer.
| Choice | Good fit | Risk |
|---|---|---|
| default:image | Most jobs share one reviewed runtime. | One image update changes many jobs simultaneously. |
| job image | A job needs a specialized runtime/toolchain. | Duplication and inconsistent update cadence. |
| default:before_script | Setup is truly common and cheap. | Hidden work, network calls, or package installs on every job. |
| job before_script | Setup belongs only to one job class. | Repeated boilerplate if overused. |
2. Minimal trusted image versus convenience-heavy image
A tiny image can reduce attack surface, pull time, and accidental tool availability, but it may increase setup complexity or expose differences such as BusyBox versus GNU utilities. A large “CI toolbox” image improves convenience but becomes a high-value supply-chain dependency with more packages, patching work, and hidden capabilities.
Production policy should define who builds/approves CI images, where they are hosted, how they are scanned, how digests are recorded, and how updates are promoted. A digest pins identity; it does not prove the image is trustworthy. Trust comes from provenance, review, build process, vulnerability context, and controlled promotion.
3. Service sidecar versus external integration dependency
A service sidecar is ideal for disposable tests that need an isolated database, cache, or HTTP endpoint. Its lifecycle is coupled to the job and its data can be synthetic. An external test environment may better represent shared integration behavior, but it introduces network availability, credentials, concurrency, cleanup, data governance, and potentially billing.
| Dimension | Service sidecar | External dependency |
|---|---|---|
| Isolation | Ephemeral per job/pipeline when executor supports it. | Shared unless separately provisioned. |
| Credentials | Often synthetic/local. | Usually requires managed authentication. |
| Fidelity | Good for protocol/component tests; not identical to production topology. | Can approximate real integration more closely. |
| Failure modes | Image pull, startup, readiness, alias/port. | DNS/TLS, auth, quotas, outages, stale shared state. |
| Cost | Runner compute and pulls. | Potential service/cloud/network cost. |
4. Portable shell or constrained runner: make the choice explicit
Portable scripts favor common POSIX constructs and tools that are guaranteed by the selected image. Executor-specific optimization deliberately relies on Bash, PowerShell, Windows paths, Docker socket access, GPU devices, or other platform characteristics. Both can be valid; the failure is leaving the dependency implicit.
If a job requires Bash, use an image/runner where Bash is present
and document that requirement. If it requires PowerShell, constrain
the runner appropriately and write PowerShell syntax. Do not assume
/bin/sh, Bash arrays, PowerShell cmdlets, and Windows
path quoting are interchangeable.
5. Defaults and includes create a configuration dependency graph
Included files can contribute defaults that affect jobs declared elsewhere. This is powerful for standardization but can surprise maintainers when they inspect only the local YAML. Review the merged configuration in the Pipeline Editor/CI Lint when behavior seems inconsistent with the file in front of you.
For production includes, pin the source/ref where practical, review changes, and treat remote templates/components as supply-chain dependencies. Chapter 20 goes deeper into reusable pipeline architecture; here the key principle is that resolved configuration—not one physical YAML file—is what GitLab executes.
6. Worked scenario: choose a runtime policy
A team has 20 jobs: 14 use the same static-analysis image, 4 need a database for tests, and 2 build Windows binaries. Which design is preferable?
| Decision | Choice | Reasoning |
|---|---|---|
| Default image | Reviewed Linux analysis image for the 14 common jobs. | Reduces duplication while matching the dominant shared runtime. |
| DB tests | Job-level image plus disposable DB service with alias/readiness. | Keeps database dependency scoped to four jobs. |
| Windows jobs | Explicit job tags/runtime and PowerShell scripts. | Makes executor/platform dependency visible rather than pretending Linux portability. |
| Image identity | Version tag in beginner lab; verified digest/internal promotion in production. | Balances usability with stronger production reproducibility. |
| Common setup | Only deterministic, cheap setup in default; specialized setup stays job-local. | Avoids hidden network/install cost across all jobs. |
7. Decision table: production dimensions
| Dimension | Questions before approving the design |
|---|---|
| Maintainability | Can a reviewer understand inheritance and update scope from merged configuration? |
| Security | Who controls the image/service; is provenance reviewed; does the executor isolate untrusted jobs? |
| Governance | Are shared defaults/templates owned and versioned; are exceptions explicit? |
| Reliability | Are image pulls, service readiness, network dependencies, and shell behavior deterministic? |
| Compatibility | Does every eligible runner provide the required executor, architecture, shell, and tools? |
| Performance | Are heavy images/services pulled unnecessarily; does default setup repeat expensive work? |
| Cost | Does extra runner time, image transfer, external service use, or hosted compute materially increase spend? |
8. Common design mistakes
- Install the world in before_script: every job pays network/time risk. Prefer a reviewed purpose-built image when setup is stable.
- Use latest everywhere: no repository diff is required for behavior to change.
- Assume services equal production: sidecars are test dependencies, not topology proof.
- Hide platform needs: Bash/PowerShell/Docker requirements should constrain runner selection visibly.
- Copy giant defaults into each job: defeats centralized policy and creates drift.
- Put secrets in image arguments or service commands: keep secret handling in the appropriate protected CI/CD secret mechanism; Chapter 13 covers it deeply.
Knowledge check
When is default:image a good design?
When a meaningful set of jobs intentionally share one reviewed runtime and should move together when that runtime changes.
Does pinning an image digest prove the image is secure?
No. It proves exact content identity; trust still requires provenance, review, scanning/context, and controlled promotion.
Why might a service sidecar be preferable to a shared external database for unit/integration tests?
It can provide disposable synthetic state with less credential and concurrency coupling, assuming the executor supports it.
What should you do when a job genuinely needs PowerShell-specific behavior?
Make the dependency explicit with an appropriate runner/platform constraint and write/test PowerShell rather than pretending the script is portable.
Why inspect merged configuration when includes are used?
Because defaults and other resolved settings may originate outside the local file and materially change the job GitLab creates.
Summary
Good CI YAML makes coupling explicit. Use defaults for intentional shared policy, job overrides for scoped differences, reviewed and identifiable images for runtime reproducibility, services for bounded test dependencies, and explicit runner/shell constraints for platform-specific work. Optimize only after you can explain the resolved configuration and trust boundary.
Official references
- GitLab Docs — CI/CD YAML syntax reference
- GitLab Docs — Scripts and job logs
- GitLab Docs — Deprecated CI/CD keywords
- GitLab Docs — Use CI/CD configuration from other files
- GitLab Docs — Run CI/CD jobs in Docker containers
- GitLab Docs — Services
- GitLab Docs — Docker executor
- GitLab Docs — Shell executor
- GitLab Docs — Runner executors
- GitLab Docs — Pipeline editor
- GitLab Docs — CI Lint
- GitLab Docs — Predefined CI/CD variables
- GitLab Docs — Runner security
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.