Skip to content

Reconciling settings

What happens when you run an apply: how state is compared, what --check and --diff really report, what makes a run idempotent, and which settings behave differently because GitLab's API constrains them.

What a run does

  1. Plan. Your desired state is normalized into ordered resource operations. This is controller-side and costs nothing.
  2. Route. Each operation is bound to the first capable, ready transport in your preference. See Backends and transports.
  3. Authorize. The registry checks the declared credential against each operation before any request is built — family, authority boundary, endpoint acceptance. A refusal here costs no network call.
  4. Reconcile. Each operation reads current state, compares it, and writes only what differs.
  5. Report. A coverage manifest binds every planned operation to the backend that processed it or to its skip reason.

Partial-declarative, by design

The role converges the settings you declare. It does not prune resources you did not mention. Declared containers that do not exist are created: groups, projects, user accounts, and instance runners are all created from their declarations (identity derived from the declared path or name), and each supports state: absent for deletion — a container deletion removes everything inside it.

To remove something, say so:

gitlab_settings_groups:
  - path: platform
    hooks:
      - url: https://old.example.com/hook
        state: absent

Members are never pruned

Memberships are managed declaratively — the role converges exactly the ones you declare — but it never removes memberships you did not mention, even with a full declaration. Use state: absent for a specific member.

Scoping a run

gitlab_settings_resource narrows what is reconciled:

Value Reconciles
all (default) Everything declared, in the order instance, users, groups, projects, runners — users plan before groups so a membership can reference an account created in the same run. Empty scopes cost nothing.
instance Instance-level settings only
users User account entries only
groups Group entries only
projects Project entries only
runners Instance runner entries only

Narrow it only for deliberately phased orchestration — for example: apply instance settings, create groups by some other means, then apply group settings. The default is not slow for undeclared scopes.

Check mode and diff

REST and GraphQL operations read live state in check mode and report only real drift. A check run is not a guess about what might change; it is a comparison against what is actually there — with one deliberate exception: when the planner proves a parent container is created earlier in the same run, its children cannot be read yet, so check mode reports the planned write as a prediction (still validating the would-be request against the committed contracts). A real apply still fails loudly if the prediction was wrong.

ansible-playbook -i localhost, playbook.yml --check --diff

--diff returns before and after for managed, non-secret fields.

A Rails partition cannot be previewed

Rails cannot prove drift without booting Rails, which is a real action on the instance. In strict check mode a Rails partition fails the run; in compatible check mode each Rails operation is recorded as an explicit structured skip. See Rails execution.

Credential maintenance never happens in ordinary check mode either — see check mode.

Idempotence

A second identical run reports no change. Where that is not achievable, the role prefers reporting no change over issuing a write it cannot verify — and this page names every such case, because a silent exception would be worse than a documented one.

Write-only values

CI/CD variable values, hook secrets, integration credentials, license keys, schedule variable values, Pages certificates, user passwords, external status check shared secrets, and audit-destination secret tokens are write-only: GitLab masks them in responses, so they can never be compared or displayed. One write-only secret flows the other way: a runner's authentication token is returned exactly once at registration and captured to a declared token_file on the controller (0600) — see instance runners.

They are carried on every write, because several GitLab endpoints require them on update. They are never rendered in a diff.

To force a rotation when nothing else drifted:

    force_secret_update: true      # licenses use force_update

Mirror URLs are also write-only, but rotating their credentials needs an explicit state: absent apply followed by creation with the new URL. force_secret_update: true is rejected for mirrors rather than silently doing nothing.

Settings that behave differently

These are GitLab API constraints surfaced honestly, not role limitations that will quietly resolve themselves.

Setting Behaviour To change one
Hook url_variables and custom_headers Values are masked in responses, so they reconcile by key presence. A listed key is treated as in sync; value drift is not detectable. Remove the key and re-declare it.
Protected-ref access arrays (allowed_to_push and friends) Request and response shapes differ, so they do not round-trip. Applied on create, excluded from drift detection and updates. Remove and re-declare the protected ref.
Protected-environment deploy_access_levels, approval_rules Same round-trip problem. Applied on create, excluded from drift detection — idempotence is chosen over update coverage. Remove and re-declare the protected environment.
Project feature-flag strategies The nested rollout configuration is applied on create but not reconciled on update. The scalar active and description do reconcile. Remove and re-declare the flag.
Protected tags, member roles, SSH certificates, LDAP/SAML links, shares, job token allowlists, custom emoji, on-call rotations, audit event streaming destinations GitLab has no update verb. Drift on an existing entry is reported through strict/compatible handling. Remove and re-declare the entry.
Environments Must be stopped before state: absent can delete them. GitLab's error is surfaced verbatim. Stop the environment first.

Declared state wins

A feature-flag run overwrites live toggles. That is deliberate: the owning team opts into declarative management knowingly. Group push rules, by contrast, are fully reconciled through the push_rules singleton on group entries.

Recording the result

gitlab_settings_output_file writes the structured apply result — including the coverage manifest — to a path on the controller:

gitlab_settings_output_file: /var/log/gitlab-settings/last-apply.json

Useful as a GitOps audit artifact, and as the record of what was skipped in compatible mode.

Failure handling

gitlab_settings_mode governs operations that cannot be applied — unknown fields, missing groups or projects, capabilities absent on the target, or an unroutable transport:

  • strict (default) — the run fails, naming the operation and reason.
  • compatible — the operation is recorded as a skip with an actionable reason and the run continues.

Credential problems are separate and always fail, in both modes: an unsupported credential family, an out-of-boundary token, or an unproven authority is not something to skip past. See Credential operations.