CBA — Backstage 인증 어소시에이트 · 설정 계층과 app-config.yaml · 이론
설정이 한 파일이 아닌 이유와 병합 규칙
한 줄 요약
Backstage 의 설정은 app-config.yaml 한 장이 아니라 여러 장을 순서대로 쌓아 올린 결과입니다. 매핑은 깊게 합쳐지고 리스트는 통째로 교체되며, 뒤에 오는 파일이 이깁니다. 이 규칙 하나를 정확히 알면 "왜 내가 고친 값이 안 먹히는가" 의 대부분이 풀립니다.
왜 이게 필요했나
같은 코드가 네 곳에서 돕니다. 개발자 노트북, CI, 스테이징, 운영입니다. 그런데 이 넷은 거의 모든 값이 달라야 합니다.
- 주소가 다릅니다. 노트북은
localhost:3000, 운영은 회사 도메인입니다. - 데이터베이스가 다릅니다. 노트북은 파일 하나짜리 SQLite, 운영은 PostgreSQL 입니다.
- 인증이 다릅니다. 노트북은 게스트 로그인으로 충분하지만 운영은 회사 신원 제공자를 붙여야 합니다.
- 문서 빌드 방식이 다릅니다. 노트북은 포털이 직접 빌드해도 되지만 운영에서는 CI 가 빌드해 올린 것을 서빙합니다.
값을 한 파일에 몰아 두면 누군가 자기 환경에 맞게 고쳐 커밋할 때마다 다른 사람의 환경이 깨집니다. 게다가 토큰과 비밀번호는 애초에 파일에 둘 수 없습니다. 그래서 Backstage 는 설정을 여러 장으로 나누고 순서대로 겹쳐 읽는 방식을 택했습니다.
어떻게 동작하나
파일 여러 장이 순서대로 쌓인다
관례로 쓰는 파일이 셋입니다.
| 파일 | 어디에 사나 | 무엇을 담나 |
| --- | --- | --- |
| app-config.yaml | 저장소에 커밋한다 | 모든 환경의 공통 기본값 |
| app-config.local.yaml | 개인 노트북. git 에서 제외한다 | 그 사람만의 덮어쓰기 |
| app-config.production.yaml | 컨테이너 이미지에 함께 실린다 | 운영에서만 다른 값 |
기동할 때 --config 로 나열한 순서대로 읽고, 뒤에 온 것이 앞의 것을 덮습니다. 파일 이름이 특별한 것이 아니라 순서가 전부 입니다.
병합 규칙은 딱 두 줄이다
매핑(map) : 키 단위로 깊게 합친다. 뒤에 없는 키는 앞의 값이 그대로 남는다리스트(list): 합치지 않는다. 뒤에 있으면 통째로 교체된다리스트 규칙이 사고의 원천입니다. 기본 파일에 catalog.locations 를 세 건 적어 두고 로컬 파일에 한 건만 적으면, 결과는 네 건이 아니라 한 건 입니다. 나머지 두 건은 조용히 사라지고 아무 경고도 나오지 않습니다.
그리고 매핑 쪽에도 함정이 하나 있습니다. 값의 종류가 바뀌면 깊게 합칠 대상이 없으므로 그냥 교체됩니다. connection 이 기본 파일에서는 문자열 하나였는데 오버라이드에서 매핑이 되면, 문자열은 사라지고 매핑만 남습니다.
비밀은 파일이 아니라 환경에서 온다
값 자리에 ${GITHUB_TOKEN} 처럼 쓰면 기동할 때 환경변수로 치환됩니다. 이 형식을 쓰는 이유는 편의가 아니라 설정 파일이 저장소에 커밋되고 컨테이너 이미지에 구워지기 때문 입니다. 값을 직접 적는 순간 그 비밀은 git 이력과 이미지 레이어에 영원히 남습니다.
반대 방향도 있습니다. APP_CONFIG_ 로 시작하는 환경변수는 설정의 특정 경로 하나를 덮어씁니다. 경로의 점을 밑줄로 바꾼 이름을 씁니다.
APP_CONFIG_app_baseUrl=https://portal.example.com → app.baseUrl 을 덮는다APP_CONFIG_backend_listen_port=7007 → backend.listen.port 를 덮는다이미지를 다시 굽지 않고 배포 매니페스트만 고쳐 값을 바꿀 수 있어서, 쿠버네티스에 올릴 때 특히 요긴합니다.
무엇이 브라우저까지 가는가
설정 값에는 가시성(visibility)이 있습니다. 기본은 백엔드 전용이고, 스키마에서 frontend 로 표시한 값만 프런트엔드 번들에 들어갑니다. 이 구분이 없으면 integrations.github[0].token 같은 값이 브라우저 소스에 그대로 실립니다. 시험에서 "설정에 적은 값은 모두 프런트엔드에서 읽을 수 있는가" 를 물으면 답은 아니오입니다.
자주 쓰는 구획
| 키 | 하는 일 |
| --- | --- |
| app · organization | 브라우저가 쓰는 주소와 이름 |
| backend | 리슨 주소와 포트, CORS, 데이터베이스 |
| integrations | GitHub·GitLab 같은 SCM 자격증명 |
| proxy | 브라우저 대신 백엔드가 외부 API 를 부르는 통로 |
| catalog | 허용 kind(rules), 손으로 적는 진입점(locations), 디스커버리(providers) |
| auth | 로그인 제공자와 신원 결정 |
| techdocs | 문서를 누가 빌드하고 어디에 두는가 |
proxy 가 왜 따로 있는지가 시험 포인트입니다. 프런트엔드가 외부 API 를 직접 부르면 CORS 에 막히고, 막히지 않더라도 자격증명을 브라우저에 둬야 합니다. 그래서 백엔드가 대신 부르고 프런트엔드는 자기 백엔드의 경로만 호출합니다.
catalog.providers 의 schedule 도 눈여겨볼 자리입니다. 주기를 짧게 잡으면 최신성은 좋아지지만 SCM API 한도에 걸리고, 길게 잡으면 사람들이 포털의 정보를 믿지 않게 됩니다.
현장에서 만나는 모습
저자의 홈랩 클러스터에서 정확히 같은 성격의 사고가 두 번 났습니다.
첫 번째는 containerd 였습니다. /etc/containerd/conf.d/ 아래 드롭인 파일 여러 개가 같은 플러그인을 건드리면, 필드가 합쳐지는 것이 아니라 이름순 마지막 파일이 그 플러그인 설정을 통째로 가져갑니다. 앞 파일이 적어 둔 값은 남지 않고, 남지 않은 자리는 앞 파일 값이 아니라 기본값으로 떨어집니다. "한 줄만 추가" 하려고 파일을 하나 더 놓았다가 노드의 런타임 설정이 전부 기본값이 됐습니다. Backstage 의 리스트 교체 규칙이 이것과 같은 성격입니다.
두 번째는 게이트웨이였습니다. HTTPS 리다이렉트를 켜자 인증서 갱신이 막혔는데, /.well-known/acme-challenge/ 를 더 구체적으로 적어도 소용이 없었습니다. Cilium 은 경로 구체성을 우선하지 않기 때문입니다. 여기서 얻은 감각이 설정 계층에도 그대로 갑니다. 더 자세히 적었으니 이기겠지 는 통하지 않습니다. 이기는 것은 순서입니다.
그리고 이 클러스터의 반복된 교훈인 "상태가 Ready 인 것과 실제로 동작하는 것은 다른 명제" 도 설정에 그대로 적용됩니다. 포털이 떠 있다고 해서 여러분이 의도한 값으로 떠 있는 것은 아닙니다. 병합 결과를 눈으로 확인하는 습관이 필요합니다.
다음 실습에서 할 것
/root/cba-config/ 에 기본 설정, 로컬 오버라이드, 운영 설정 세 장을 직접 작성합니다. 그다음 두 장을 겹쳤을 때의 병합 결과를 손으로 예측해 적고, 채점기가 같은 규칙으로 다시 계산한 값과 대조합니다. 마지막에 운영 설정을 ConfigMap 으로 클러스터에 올리고 APP_CONFIG_ 환경변수로 한 줄을 덮어 봅니다.