TT Lab
Get started
Learn Learning paths Courses

CBA — Backstage Associate

Why Configuration Is Not One File, and the Merge Rules

Continue in TT Lab

In one line

Backstage's configuration is not a single app-config.yaml but the result of stacking several files in order. Mappings are merged deeply, lists are replaced wholesale, and the later file wins. If you know this one rule exactly, most of "why does my edited value not take effect?" is solved.

Why this was needed

The same code runs in four places: a developer laptop, CI, staging, and production. And nearly every value must differ among these four.

If you pile all the values into one file, every time someone edits and commits it to fit their own environment, someone else's environment breaks. On top of that, tokens and passwords cannot be kept in a file in the first place. So Backstage chose to split configuration into several files and read them layered in order.

How it works

Several files stack in order

There are three files used by convention.

File Where it lives What it holds
app-config.yaml Committed to the repository Common defaults for all environments
app-config.local.yaml A personal laptop. Excluded from git That person's own overrides
app-config.production.yaml Shipped in the container image Values that differ only in production

At startup, files are read in the order listed with --config, and the later one overrides the earlier one. The file names are not special; the order is everything.

The merge rules are just two lines

매핑(map)  : 키 단위로 깊게 합친다. 뒤에 없는 키는 앞의 값이 그대로 남는다
리스트(list): 합치지 않는다. 뒤에 있으면 통째로 교체된다

The list rule is the source of accidents. If you write three catalog.locations in the base file and only one in the local file, the result is not four but one. The other two quietly disappear, and no warning appears.

And the mapping side has a trap too. When the kind of value changes, there is nothing to merge deeply, so it is simply replaced. If connection was a single string in the base file and becomes a mapping in the override, the string disappears and only the mapping remains.

Secrets come from the environment, not from files

If you write something like ${GITHUB_TOKEN} in a value position, it is substituted with the environment variable at startup. The reason for using this form is not convenience but that configuration files are committed to the repository and baked into container images. The moment you write the value directly, that secret stays forever in git history and image layers.

There is the reverse direction too. An environment variable that starts with APP_CONFIG_ overrides one specific path in the configuration. You use a name with the dots of the path replaced by underscores.

APP_CONFIG_app_baseUrl=https://portal.example.com   →  app.baseUrl 을 덮는다
APP_CONFIG_backend_listen_port=7007                 →  backend.listen.port 를 덮는다

It is especially handy when deploying to Kubernetes, because you can change a value by editing only the deployment manifest, without rebuilding the image.

What goes all the way to the browser

Configuration values have visibility. The default is backend-only, and only values marked frontend in the schema go into the frontend bundle. Without this distinction, a value like integrations.github[0].token would be shipped as-is in the browser source. If the exam asks "Can every value written in the configuration be read from the frontend?", the answer is no.

Commonly used sections

Key What it does
app · organization The address and name the browser uses
backend The listen address and port, CORS, the database
integrations SCM credentials such as GitHub and GitLab
proxy The channel through which the backend, instead of the browser, calls external APIs
catalog Allowed kinds (rules), hand-written entry points (locations), discovery (providers)
auth Login providers and identity resolution
techdocs Who builds the documentation and where it goes

Why proxy exists separately is an exam point. If the frontend calls an external API directly, it is blocked by CORS, and even if it is not blocked, credentials must be placed in the browser. So the backend calls on its behalf, and the frontend calls only its own backend's path.

The schedule of catalog.providers is also a spot worth noting. A short interval improves freshness but hits the SCM API limit, and a long one makes people stop trusting the portal's information.

What it looks like in the field

An incident of exactly the same nature happened twice in the author's homelab cluster.

The first was containerd. When several drop-in files under /etc/containerd/conf.d/ touch the same plugin, the fields are not merged but the last file in name order takes the whole plugin configuration. Values the earlier files wrote do not remain, and the places that do not remain fall to the defaults, not to the earlier files' values. Someone placed one more file to "add just one line," and the node's runtime configuration all became defaults. Backstage's list replacement rule is of the same nature.

The second was the gateway. When HTTPS redirect was turned on, certificate renewal was blocked, and writing /.well-known/acme-challenge/ more specifically did not help. That is because Cilium does not give priority to path specificity. The sense gained here carries straight over to configuration layers. "I wrote it in more detail, so it will win" does not work. What wins is the order.

And this cluster's repeated lesson, "a state of Ready and actually working are different claims," applies to configuration as it is. A portal being up does not mean it is up with the values you intended. You need the habit of checking the merge result with your own eyes.

What you will do in the next lab

In /root/cba-config/, you write three configuration files yourself: the base configuration, a local override, and the production configuration. Then you predict by hand, and write down, the merge result when two files are layered, and compare it against the values the grader recomputes with the same rules. Finally you put the production configuration on the cluster as a ConfigMap and override one line with an APP_CONFIG_ environment variable.