LabHub
学习 学习路径 课程

CBA — Backstage 认证助理

新加的后端插件只返回 401

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

VM 안에 진짜 Backstage 백엔드 프로세스를 띄우고, 백엔드 플러그인을 직접 만들어 붙입니다. 새 경로가 왜 401 을 돌려주는지, 공개 경로는 어떻게 여는지, 설정·다른 플러그인·확장점을 플러그인 코드에서 어떻게 쓰는지를 HTTP 응답과 로그로 확인합니다.

왜 중요한가

Backstage 를 고친다는 것은 대부분 플러그인을 더하거나 기존 플러그인을 넓히는 일입니다. 새 백엔드 플러그인을 붙였더니 모든 요청이 401 이면, 코드 버그를 찾느라 시간을 쓰기 쉽습니다. 실제로는 기본 인증 정책이 라우터보다 앞에서 막고 있습니다. 같은 이유로 플러그인이 카탈로그를 부를 때도 토큰이 필요하고, 카탈로그 동작을 바꿀 때는 그 패키지를 고치지 않고 모듈을 더합니다.

이 실습은 백엔드만 다룹니다. 프런트엔드 플러그인(React·Material UI)은 앱 번들을 빌드해야 하는데 이 VM 에서는 그 빌드를 하지 않으므로 직접 확인하지 않습니다. 보고서에서 둘의 차이를 정리합니다.

처음 VM 이 뜨는 데 4분쯤 걸리고, 1단계 설치는 인터넷에서 약 30초 걸립니다. 코드는 TypeScript 가 아니라 CommonJS 자바스크립트 한 파일(index.js)로 씁니다.

단계

  1. /usr/local/cba-plugin 에 백엔드 패키지 여섯 개를 정확한 버전으로 설치합니다.
  2. 카탈로그 플러그인만 든 백엔드를 7007 포트에 띄웁니다.
  3. oncall 플러그인을 붙이고 인증 없이 부른 결과(401)를 기록합니다.
  4. addAuthPolicy/ping 한 경로만 엽니다.
  5. rootConfig 로 설정값을 요청마다 읽고, 재시작 없이 설정을 바꿔 응답 변화를 기록합니다.
  6. auth·discovery 서비스로 카탈로그 API 를 서비스 간 호출합니다.
  7. 카탈로그 모듈(createBackendModule)로 엔티티 공급자를 더합니다.
  8. 401·404 가 어느 층에서 나오는지와 백엔드·프런트엔드 플러그인 차이를 보고합니다.

참고

플러그인을 올릴 백엔드 재료를 정확한 버전으로 받는다

/usr/local/cba-plugin 에 npm 프로젝트를 만들고 아래 여섯 패키지를 범위 기호 없이 정확한 버전으로 설치하세요. package.json 의 dependencies 에도 ^ 없이 그 버전이 적혀야 합니다. 루트 디스크가 작으니 npm 캐시는 npm_config_cache=/usr/local/cba-plugin/.npmcache 로 스크래치 디스크에 둡니다. @backstage/backend-defaults@0.17.8, @backstage/backend-plugin-api@1.10.0, @backstage/plugin-catalog-backend@3.9.1, @backstage/plugin-catalog-node@2.2.4, better-sqlite3@12.4.1, express@4.22.3.

npm install --save-exact 패키지@버전 ... 은 package.json 에 범위 없이 적습니다. better-sqlite3 는 네이티브 모듈이라 최신 13.x 는 이 Node 20 용 미리 빌드한 바이너리가 없어 컴파일을 시도하다 실패합니다 — backend-defaults 가 요구하는 12.x 에서 고정 버전을 고르는 이유입니다. 설치가 끝나면 npm ls --depth=0 으로 버전을 확인하세요.

카탈로그만 든 백엔드를 띄운다

/usr/local/cba-plugin/app-config.yaml/usr/local/cba-plugin/catalog/org.yaml, /usr/local/cba-plugin/index.js 를 만들어 카탈로그 플러그인만 든 백엔드를 7007 포트에 띄우세요. 설정: backend.baseUrl: http://localhost:7007, backend.listen.port: 7007, backend.databaseclient: better-sqlite3·connection: ':memory:', catalog.rules[{allow: [Component, Group, Location]}], catalog.locationstype: file·target: ./catalog/org.yaml. org.yaml 에는 Group team-payments 와 그 팀이 소유한 Component payments-api·refund-worker 를 넣습니다. index.jscreateBackend()@backstage/plugin-catalog-backend 를 add 하고 start 합니다. 프로세스는 /usr/local/cba-plugin 에서 node index.js 로 띄우고 출력을 /usr/local/cba-plugin/backend.log 로 보내세요. /.backstage/health/v1/readiness 가 200 이면 준비된 것입니다.

셸을 닫아도 살아 있어야 하므로 setsid nohup node index.js > backend.log 2>&1 < /dev/null & 처럼 표준 입출력을 모두 떼어 내세요(떼지 않으면 명령이 끝나지 않고 매달립니다). 기동 로그에서 Plugin initialization complete 줄을 찾고, 인증 없이 /api/catalog/entities 를 부르면 무엇이 돌아오는지도 봐 두세요.

새 플러그인의 경로가 전부 401 이다

index.jscreateBackendPlugin 으로 pluginId oncall 플러그인을 만들어 add 하세요. coreServices.httpRouter 로 express 라우터를 등록하고 GET /ping(→ {"ok":true})과 GET /roster(당번 JSON) 두 경로를 둡니다. 인증 정책은 아직 추가하지 않습니다. 재시작한 뒤 인증 없이 http://127.0.0.1:7007/api/oncall/ping 을 부른 결과를 curl -s -i 출력 그대로 /root/cba-plugin/before-policy.txt 에 저장하세요.

플러그인 라우터는 /api/<pluginId> 아래에 붙습니다. 코드에 버그가 없어도 401 이 나오는 이유를 문서의 기본 인증 정책에서 찾아보세요. 비교를 위해 /api/oncall/없는경로/api/없는플러그인/x 도 불러 보면 401 이 어느 층에서 나오는지 보입니다.

상태 확인 경로 하나만 인증 없이 연다

oncall 플러그인 init 에서 http.addAuthPolicy/ping 한 경로만 allow: 'unauthenticated' 로 여세요. 재시작 뒤 인증 없이 /api/oncall/ping 은 200 {"ok":true}, /api/oncall/roster 는 여전히 401 이어야 합니다.

정책의 path 는 라우터에 쓴 경로와 같은 형식(플러그인 접두사 없이)입니다. 플러그인 전체를 여는 설정(backend.auth.dangerouslyDisableDefaultAuthPolicy)은 모든 플러그인을 무인증으로 만드니 쓰지 마세요.

당번 채널을 코드가 아니라 설정에서 읽는다

app-config.yamloncall.channel: '#payments-oncall' 을 넣고, oncall 플러그인에 coreServices.rootConfig 를 주입받아 요청을 받을 때마다 config.getString('oncall.channel') 을 돌려주는 GET /channel(→ {"channel":"..."}, 인증 없이 허용)을 추가한 뒤 재시작하세요. 응답을 확인한 다음 재시작하지 않고 설정 파일의 값을 '#platform-oncall' 로 고치고, 응답이 바뀌는 것을 확인하세요. /root/cba-plugin/channel.txtbefore=<처음 응답의 channel>after=<바뀐 응답의 channel> 두 줄을 남깁니다.

설정 파일은 백엔드가 지켜보고 있다가 바뀌면 다시 읽습니다(로그에 Found 0 new secrets in config 가 다시 찍힙니다). 그래도 값이 안 바뀐다면 init 에서 한 번 읽어 변수에 담아 두지 않았는지 보세요. 응답이 바뀔 때까지 몇 초 폴링하세요.

플러그인이 카탈로그를 부를 때도 토큰이 필요하다

oncall 플러그인에 GET /services(인증 없이 허용)를 추가하세요. 이 핸들러는 coreServices.authgetPluginRequestToken(onBehalfOf 는 auth.getOwnServiceCredentials(), targetPluginId 는 catalog)으로 토큰을 받고, coreServices.discoverygetBaseUrl('catalog') 로 주소를 얻어 /entities?filter=kind=componentAuthorization: Bearer 헤더로 호출한 뒤 {"catalogStatus": <카탈로그 응답 코드>, "names": [Component 이름 정렬]} 을 돌려줍니다. 재시작 뒤 응답의 names 에 payments-apirefund-worker 가 있어야 합니다. 5단계에서 바꾼 채널 설정은 그대로 둡니다.

플러그인끼리는 코드로 서로를 부를 수 없고 HTTP 로만 통신합니다. 그래서 같은 프로세스 안이라도 카탈로그의 기본 인증 정책을 통과할 토큰이 필요합니다. 기동 직후에는 카탈로그가 파일을 아직 처리하지 못해 목록이 빌 수 있으니 몇 초 뒤 다시 부르세요. 백엔드 로그에서 /api/catalog/entities 요청의 User-Agent 가 무엇으로 찍히는지도 보세요.

카탈로그를 고치지 않고 모듈로 엔티티를 밀어 넣는다

createBackendModule 로 pluginId catalog, moduleId pager-provider 모듈을 만들어 add 하세요. @backstage/plugin-catalog-nodecatalogProcessingExtensionPoint 를 주입받아 addEntityProvider 로 provider 이름 pager-provider 를 등록하고, connect 에서 applyMutation({type: 'full', ...}) 으로 Component pager-bridge(owner team-payments, backstage.io/managed-by-location·backstage.io/managed-by-origin-location 애노테이션 포함)를 넣습니다. org.yaml 에는 넣지 않습니다. 재시작 뒤 /api/oncall/services 의 names 에 pager-bridgepayments-api·refund-worker 와 함께 나와야 합니다.

모듈은 대상 플러그인이 공개한 확장점으로만 그 플러그인을 넓힙니다. 카탈로그 패키지 자체가 아니라 -node 라이브러리 패키지에서 확장점을 가져오는 이유를 모듈 문서에서 확인하세요. provider 가 넣는 엔티티도 location 애노테이션이 없으면 처리 단계에서 걸러집니다.

401 과 404 가 어느 층에서 나오는지 보고한다

/root/cba-plugin/report.md 첫 다섯 줄에 지금 떠 있는 백엔드에서 직접 확인한 값키=값 으로 적으세요 — mount_path=(oncall 플러그인 라우터가 붙은 경로), protected_route_status=(인증 없이 /roster), unknown_route_status=(인증 없이 oncall 플러그인 아래 없는 경로), unknown_plugin_status=(등록되지 않은 pluginId 아래 경로), catalog_direct_status=(인증 없이 /api/catalog/entities). 그 아래에 백엔드 플러그인과 프런트엔드 플러그인이 각각 어디서 돌고 어떻게 연결되는지 설명을 쓰세요.

숫자는 기억이 아니라 curl 로 지금 잰 값을 옮기세요. 없는 경로가 404 가 아닌 이유는 인증 검사가 라우터보다 앞에 있기 때문입니다. 설명에는 프런트엔드 플러그인이 브라우저에서 돌며 backend.baseUrl 의 /api/<pluginId> 를 부른다는 점을 넣으세요.