CI/CD Variables, Predefined Variables, File Variables, Masking, Protection, Expansion, and Precedence: Configuration, Design Choices, and Tradeoffs
Variable design is configuration architecture. This lesson compares repository YAML, project/group/instance settings, pipeline inputs/variables, ordinary versus file variables, protected versus unprotected access, expansion versus literal values, and convenience versus auditability. The goal is to choose the narrowest data channel whose ownership and precedence are obvious.
Learning objectives
- Choose repository YAML for non-sensitive versioned configuration and settings/external secret systems for sensitive or centrally governed values.
- Choose file variables when a tool requires a file path, rather than encoding multiline material into command arguments or logs.
- Use protected variables only where protected refs and reviewer trust actually match the deployment/security boundary.
- Prefer pipeline inputs over ad-hoc pipeline variables for newer parameterized designs when the feature fits the use case and current GitLab guidance.
- Control expansion intentionally and separate GitLab expansion, Runner expansion, and shell expansion when predicting values.
1. YAML variables versus project/group/instance settings
Repository YAML is ideal for non-sensitive configuration that should
change with code and be reviewed in merge requests: feature modes,
test paths, synthetic build labels, or tool flags. Settings
variables are appropriate when the value is centrally administered,
sensitive, environment-scoped, or intentionally decoupled from a
commit. In either design, preserve the immutable
CI_COMMIT_SHA with pipeline source/ref evidence so the
effective configuration can be tied to the exact revision.
| Choice | Strengths | Risks / tradeoffs | Production fit |
|---|---|---|---|
| YAML variable | Versioned, reviewable, reproducible with the commit. | Visible to repository readers; lower precedence than project/group/pipeline sources. | Non-sensitive build/test configuration. |
| Project variable | Central project control; masking/protection/file type available. | Can silently override YAML unless ownership is documented. | Project-specific config or credentials pending stronger secret system. |
| Group variable | Shared governance across projects/subgroups. | Wide blast radius; closest subgroup precedence can surprise teams. | Standardized non-secret config or centrally governed credentials with careful scope. |
| Instance variable | Central Self-Managed/Dedicated control. | Very broad scope and admin ownership. | Rare platform-wide defaults with clear governance. |
2. Pipeline inputs versus pipeline variables
Pipeline variables are high-precedence values supplied when a pipeline is started, scheduled, triggered, or forwarded. That power is useful but can override project/group/YAML behavior. Current GitLab documentation recommends pipeline inputs over pipeline variables for newer designs where inputs fit the need, because inputs define an explicit configuration interface and can be validated/interpolated at pipeline creation.
Treat this as an API-design question. If callers are choosing a
documented parameter such as target_environment from an
allowed set, an input is clearer than an arbitrary high-precedence
variable. Keep pipeline variables restricted when they are not
needed.
3. Ordinary versus file variables
Use an ordinary variable when the consumer expects a small scalar in the environment. Use a file variable when a tool expects a file path: certificate bundles, synthetic config files, kubeconfig-like data, or other multiline material. The job still has access to the underlying data; the file form simply matches the consumer interface and reduces risky command-line interpolation.
| Consumer interface | Preferred form | Why |
|---|---|---|
--endpoint "$URL" |
Ordinary variable | The tool expects a scalar string. |
--certificate-authority FILE |
File variable | The tool expects a filesystem path. |
| Many structured non-secret settings | Versioned config file in repository | Review/diff semantics are clearer than dozens of variables. |
| High-value credential | External secrets provider where supported | Shorter exposure/stronger lifecycle can be preferable to long-lived settings variables. |
4. Protected versus unprotected values
Protection should mirror the code trust boundary. A production deployment credential should not be available to arbitrary feature branches merely because masking is enabled. Protected variables limit availability to protected refs, but this only helps if the protected ref itself is governed: restricted push/merge rights, review, trustworthy runner selection, and controlled CI configuration.
Do not “fix” an absent protected variable by clearing the protection checkbox. First ask whether the current ref is supposed to have that capability. Absence can be the correct secure result.
5. Visible, masked, hidden: use the strongest appropriate UI control but know the boundary
For sensitive values stored in GitLab settings, use masking and hiding where current GitLab supports them. Hidden values cannot be revealed again through the settings UI after creation. However, both masked and hidden variables must still be supplied to authorized jobs, so malicious or compromised job code can use them.
Masking also has formatting constraints and can fail if output transforms the value. Therefore the production pattern is layered: review CI code, restrict refs, restrict runners, minimize scopes, prefer ephemeral/federated identity where possible, and never depend on redaction alone.
6. Expanded versus literal values
Expansion is useful for composing non-sensitive configuration, but
it increases coupling. A variable value such as
$BASE_URL/api changes meaning if
BASE_URL changes or is overridden by a
higher-precedence source. Literal values are easier to audit when
exact strings matter.
| Need | Recommendation | Reason |
|---|---|---|
| Compose non-secret path from reviewed values | Use controlled expansion and document dependencies. | Readable when provenance is stable. |
Store text containing literal $NAME |
Disable expansion. | Prevents accidental substitution. |
| Secret value | Avoid variable-reference expansion. | Masking/hidden constraints and unexpected substitution increase risk. |
| Runtime shell composition | Quote variables in script and validate untrusted input. | Shell is a separate expansion layer with command-injection concerns. |
7. Precedence is part of your public configuration contract
If developers expect a YAML setting to control test mode while the platform team defines a group variable with the same key, the system has an undocumented override channel. That is a design bug even when GitLab behaves exactly as documented.
Use distinct names for values with different ownership, document intentional override points, and minimize duplicate keys across scopes. For critical deployment controls, consider rejecting unexpected values in job code or constraining caller inputs rather than allowing silent arbitrary override.
validate_target:
script:
- case "$DEPLOY_TARGET" in
review|staging) printf 'validated target=%s\n' "$DEPLOY_TARGET" ;;
*) printf 'unsupported DEPLOY_TARGET\n' >&2; exit 2 ;;
esac
The example validates the effective value after precedence resolution. It does not trust a variable merely because GitLab supplied it.
8. Environment scope can reduce blast radius, but avoid premature coupling
Project/group variables can be scoped to environments where supported. This can reduce which deployment jobs receive a value, but environment-scoped variables can be awkward during pipeline validation because some environment information is not available early enough for every rule/include decision.
Do not use environment-scoped variables as a workaround for unclear pipeline architecture. Keep pipeline creation deterministic, then use environment scope at the deployment boundary where it is actually meaningful.
9. Decision table: choose a variable channel from the requirement
| Requirement | Recommended channel | Trust / tier note | Evidence |
|---|---|---|---|
| Reviewed non-secret test flag | YAML variable | All tiers; repository readers can see it. | Commit SHA + merged config + effective value. |
| Synthetic certificate-like lab file | Project File variable | Requires project variable management role. | Type metadata + temporary path/hash; no content log. |
| Production credential | External secret provider or tightly governed settings variable | Provider/tier varies; restrict ref/runner. | Secret reference + authorization result, never secret value. |
| Caller-selectable release mode | Typed pipeline input where supported | Current GitLab recommends inputs for newer parameterization. | Input definition + interpolated configuration + pipeline source/SHA. |
| Shared organization default | Group variable with explicit owner | Group scope can override project YAML. | Group path/scope + project inheritance + effective value. |
| Protected deployment capability | Protected value plus protected ref and trusted runner | Protection is one layer, not complete trust. | Protected-ref state + presence test + runner identity. |
10. Governance checklist
- Every high-impact variable has a documented owner, scope, purpose, rotation/change process, and expected consumers.
- Duplicate keys across group/project/YAML scopes are intentional and documented.
- Sensitive values are never committed to YAML or emitted to artifacts/logs.
- Protected variables align with protected-ref and runner trust policy.
- Pipeline-variable use is restricted; new parameter interfaces prefer inputs when appropriate.
- Expansion is explicit, minimal, and never relied on for hiding secrets.
- Evidence records source/ref/SHA and effective non-secret values without exporting the whole environment.
Knowledge check
Why can a group variable be operationally dangerous even if its value is harmless?
Its scope can affect many projects and it can override lower-precedence configuration, creating a wide and possibly undocumented behavior change.
When should a file variable be preferred?
When the consumer expects a file path or multiline file-like data; it aligns the interface without putting the raw value in command arguments.
Why is “protected variable” not enough for a production secret?
The protected ref, CI code, runner, token scopes, and external target all remain part of the trust boundary.
What is the design advantage of pipeline inputs over arbitrary pipeline variables?
Inputs define an explicit parameter interface and are interpolated at pipeline creation rather than acting as another unrestricted high-precedence variable namespace.
What should you do if two teams intentionally need different ownership of similar settings?
Prefer distinct, documented keys/scopes rather than relying on surprise precedence collisions.
Official references and version notes
- GitLab CI/CD variables — project/group/instance variables, visibility, masking, hiding, protection, file variables, expansion, precedence, pipeline-variable restrictions, and security guidance.
- Predefined CI/CD variables reference — current pre-pipeline, pipeline, and job-only availability phases and variable definitions.
- Where variables can be used — GitLab, GitLab Runner, and execution-shell expansion mechanisms and keyword-specific behavior.
-
CI/CD YAML syntax reference
—
variables, job variables,variables:expand,rules:variables,workflow:rules:variables, and inheritance semantics. - CI/CD inputs — typed/configuration inputs, interpolation, and the current recommendation to prefer inputs over ad-hoc pipeline variables for newer parameterized pipelines where applicable.
- Protected branches — current protected-ref behavior that underpins protected-variable availability.
Variable behavior in this chapter was rechecked against current primary GitLab documentation on 2026-09-11. The current documentation orders policy variables above manual-job and pipeline variables, then project/group/instance values, dotenv values, YAML variables, deployment variables, and most predefined variables. It also documents project/group variable visibility as Masked by default since GitLab 18.3, UI variable-reference expansion disabled by default since GitLab 18.6, and pipeline inputs as the preferred parameter mechanism over pipeline variables for newer designs where applicable. These are version-sensitive details: verify them again before publishing or automating production policy.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.