LabHub
学习 学习路径 课程

CBA — Backstage 认证助理

用生产配置启动的门户后端一直没有就绪

在 LabHub 中继续学习

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

목표

진짜 Backstage 백엔드를 운영처럼 띄웁니다. 설정 파일 두 개를 --config 로 겹치고, 비밀은 환경변수로 넣고, 브라우저 화면이 부를 수 있게 CORS 를 열고, 재시작해도 데이터가 남게 DB 를 옮깁니다. 판정은 파일이 아니라 떠 있는 프로세스의 동작(듣는 포트, 준비 상태, 401/200, 응답 헤더, 재시작 뒤 남은 데이터)으로 합니다.

왜 중요한가

로컬에서는 되던 백엔드가 운영에 올리면 엉뚱한 포트에서 듣거나, 떠 있는데 준비가 안 되거나, 화면에서만 API 호출이 막히거나, 재시작할 때마다 등록한 것이 사라집니다. 원인은 대부분 코드가 아니라 설정이 겹치는 순서, 빠진 환경변수, 출처가 다른 클라이언트와 서버, 메모리 DB 입니다. 병합 규칙을 머리로 아는 것과, 떠 있는 프로세스에서 그 결과를 재어 보는 것은 다릅니다.

처음 VM 이 뜨는 데 4분쯤 걸리고, 1단계 설치는 인터넷에서 약 30초 걸립니다. 이 VM 에는 docker·podman 이 없어 컨테이너 이미지 빌드는 하지 않습니다(command -v docker 가 비어 있습니다). 프런트엔드 앱도 빌드하지 않고, 브라우저 대신 Origin 헤더를 붙인 curl 로 CORS 응답을 확인합니다.

단계

  1. /usr/local/cba-prod 에 백엔드 패키지를 고정 버전으로 설치합니다.
  2. 기본·운영 설정 두 파일을 --config 로 겹쳐 띄우고, 순서를 뒤집으면 무엇이 달라지는지 봅니다.
  3. ${PORTAL_API_TOKEN} 없이 뜬 백엔드가 준비되지 않는 것을 보고, 비밀을 환경변수로 넣습니다.
  4. backend.cors.origin 으로 포털 화면의 출처만 허용합니다.
  5. APP_CONFIG_ 환경변수로 파일을 고치지 않고 허용 출처를 넓힙니다.
  6. :memory: DB 에서 재시작하면 등록한 location 이 사라지는 것을 기록합니다.
  7. DB 를 디렉터리로 옮겨 재시작 뒤에도 남게 합니다.
  8. 지금 프로세스에서 잰 값으로 운영 설정 점검표를 씁니다.

참고

운영 설정을 시험할 백엔드를 고정 버전으로 받는다

/usr/local/cba-prod 에 npm 프로젝트(package.json 포함)를 만들고 세 패키지를 범위 기호 없이 정확한 버전으로 설치하세요. npm 캐시는 npm_config_cache=/usr/local/cba-prod/.npmcache 로 둡니다. @backstage/backend-defaults@0.17.8, @backstage/plugin-catalog-backend@3.9.1, better-sqlite3@12.4.1.

npm init -ynpm install --save-exact .... package.json 은 의존성 기록만이 아니라, 백엔드가 기동할 때 프로젝트 루트를 찾는 기준이기도 합니다(없으면 기동이 NoPkgJsonFound 로 죽습니다).

운영 파일을 얹었더니 다른 포트에서 듣는다

/usr/local/cba-prod/app-config.yaml(기본)과 /usr/local/cba-prod/app-config.production.yaml(운영)을 만드세요. 기본: app.baseUrl: http://localhost:3000, backend.baseUrl: http://localhost:7007, backend.listen.port: 7007, DB better-sqlite3·':memory:', catalog.rules: [{allow: [Component, Location]}], location file·./catalog/portal.yaml(Component portal-web). 운영: backend.baseUrl: http://localhost:7300, backend.listen.port: 7300 만. 카탈로그 플러그인만 든 index.js 와, 백엔드를 NODE_ENV=production 으로 node index.js --config app-config.yaml --config app-config.production.yaml 로 다시 띄우는 /usr/local/cba-prod/start.sh 를 만들고(출력은 /usr/local/cba-prod/backend.log 에 이어 쓰기) 실행하세요. 비교를 위해 한 번은 --config 순서를 뒤집어 띄워 듣는 포트를 보고, /root/cba-prod/order.txtreversed_listen_port=<그때 포트> 한 줄을 남긴 뒤 start.sh 로 되돌립니다.

--config 를 하나라도 주면 기본 파일 자동 로드는 꺼지고, 준 순서대로 얹혀 뒤가 이깁니다. 로그 첫 줄 Loading config from MergedConfigSource{...} 에서 실제로 읽은 파일과 순서가 보입니다. ss -ltnp 로 듣는 포트를 확인하세요. start.sh 는 셸이 끝나도 살도록 setsid nohup ... >> backend.log 2>&1 < /dev/null & 로 띄웁니다.

비밀이 빠진 채 뜬 백엔드는 준비되지 않는다

운영 파일에 backend.auth.externalAccess 로 type static, token ${PORTAL_API_TOKEN}, subject ops-cli 를 추가하세요. 먼저 환경변수 없이 띄워 /.backstage/health/v1/readiness 가 무엇을 돌려주는지 보고, 로그에서 Missing required config value 가 든 메시지를 /root/cba-prod/missing-env.txt 에 저장하세요. 그다음 /root/cba-prod/secrets.envPORTAL_API_TOKEN=<직접 만든 24자 이상 무작위 값> 을 권한 600 으로 두고, start.sh 가 그 파일을 읽어 환경변수로 넘기게 고쳐 다시 띄우세요. 토큰 없이 /api/catalog/entities 는 401, Authorization: Bearer <그 값> 이면 200 이어야 하고, 토큰 값은 어떤 YAML 에도 적히면 안 됩니다.

${VAR} 치환에 쓰인 환경변수가 없으면 그 값은 통째로 없는 것이 되고, 필수 값을 읽는 서비스가 시작하다 실패합니다. 프로세스가 죽지 않고 포트는 열려 있어 '떠 있는데 안 되는' 상태가 됩니다. NODE_ENV=production 로그는 JSON 이니 jq -r .message 로 읽기 쉽습니다. 값은 openssl rand -hex 16 이나 node -p "require('crypto').randomBytes(24).toString('base64')" 로 만드세요.

브라우저의 포털 화면만 백엔드를 부를 수 있다

운영 파일에 app.baseUrl: http://portal.example.test:3000backend.cors.origin: http://portal.example.test:3000 를 추가하고 start.sh 로 다시 띄우세요. Origin: http://portal.example.test:3000 헤더로 요청하면 응답에 같은 값의 Access-Control-Allow-Origin 이 오고, Origin: http://evil.example.test 로는 그 헤더가 없어야 합니다. OPTIONS 사전 요청(Access-Control-Request-Method: GET)도 한 번 보내 응답 코드를 보세요.

프런트엔드 앱은 브라우저에서 app.baseUrl 로 뜨고 backend.baseUrl 의 API 를 부릅니다. 둘의 출처(스킴·호스트·포트)가 다르면 브라우저가 백엔드의 CORS 응답 헤더를 보고 허용 여부를 정합니다. curl 은 CORS 를 강제하지 않으니 헤더가 오는지로 확인하세요: curl -s -D - -o /dev/null -H 'Origin: ...' URL.

파일을 고치지 않고 이 배포에서만 Origin 을 넓힌다

로컬 개발용 화면 http://localhost:3000 도 이 백엔드를 부를 수 있게 하되 YAML 은 고치지 마세요. start.sh 에서 APP_CONFIG_backend_cors_origin 환경변수에 JSON 배열 ["http://portal.example.test:3000","http://localhost:3000"] 를 넣어 다시 띄웁니다. 두 Origin 모두 Access-Control-Allow-Origin 을 받고 http://evil.example.test 는 여전히 못 받아야 합니다.

APP_CONFIG_ 뒤의 이름에서 _. 로 바뀌어 설정 키가 되고, 값은 JSON 으로 먼저 해석됩니다. 환경변수는 모든 설정 파일보다 우선합니다. 로그 첫 줄의 EnvConfigSource{count=...} 가 몇으로 바뀌는지 보세요. 셸 따옴표 안에 JSON 큰따옴표가 살아 있어야 합니다.

재시작했더니 등록한 location 이 사라졌다

토큰으로 POST /api/catalog/locations{"type":"url","target":"https://git.example.test/portal/catalog-info.yaml"} 를 보내 등록하고(201), GET /api/catalog/locations 개수를 센 뒤 start.sh 로 재시작해 다시 세세요. /root/cba-prod/memory.txtid=<등록 응답의 location.id>, before_restart=<개수>, after_restart=<개수> 세 줄을 남깁니다.

등록 응답이 201 이어도 그 url 을 실제로 읽는 것은 나중의 처리 단계입니다(이 호스트는 없는 주소라 읽기는 실패합니다 — 여기서는 등록 기록이 남는지만 봅니다). DB 설정이 무엇이었는지 기억하세요.

DB 를 디스크로 옮기면 재시작해도 남는다

운영 파일에 backend.database.connection.directory: /usr/local/cba-prod/db 를 추가하세요(client 는 기본 파일의 better-sqlite3 가 병합됩니다). start.sh 로 다시 띄우고 같은 url 을 다시 등록해 그 POST 응답 JSON 을 /root/cba-prod/persist.json 에 저장한 뒤, 한 번 더 재시작GET /api/catalog/locations/<id> 가 200 인 것을 확인하세요.

객체는 키 단위로 깊게 병합되므로 운영 파일에는 connection 만 적으면 됩니다. directory 를 주면 플러그인마다 SQLite 파일이 생깁니다 — ls 로 어떤 파일이 생겼는지 보세요. 운영에서는 보통 PostgreSQL 을 씁니다.

운영 설정 점검표를 지금 프로세스로 채운다

/root/cba-prod/report.md 첫 다섯 줄에 지금 떠 있는 백엔드에서 잰 값을 적으세요 — listen_port=(node 가 듣는 포트), base_port_open=(7007 에 연결되는지 yes/no), evil_origin_allowed=(http://evil.example.test 가 ACAO 를 받는지 yes/no), env_config_count=(마지막 기동 로그의 EnvConfigSource count), locations_now=(GET /api/catalog/locations 개수). 그 아래에 브라우저 화면(app.baseUrl)과 백엔드(backend.baseUrl)가 어떻게 통신하는지, NODE_ENV=production 에서 로그에 나온 경고, 이 VM 에서 Docker 이미지 빌드를 하지 않은 이유를 쓰세요.

숫자와 yes/no 는 기억이 아니라 지금 잰 값입니다. ss -ltnp, curl, grep 'Loading config from' backend.log | tail -1. 경고는 JSON 로그에서 "level":"warn" 인 줄을 찾으세요. command -v docker 로 도구가 있는지 확인하세요.