Nexus

이 문서는 tirosh-home site의 Nexus Repository Manager를 package registry로 운영하는 방법을 설명합니다.

Nexus Kubernetes 리소스는 tirosh-gitops가 관리합니다. Nexus 내부의 repository, privilege, role, user 같은 애플리케이션 운영 상태는 tirosh-infra에서 REST API로 관리합니다.

1. 운영 개요

1-1. Nexus가 맡는 일

Nexus는 package manager나 artifact client가 repository protocol을 기대하는 생태계에 사용합니다. Python은 PyPI, frontend는 npm, JVM은 Maven, Kubernetes chart는 Helm, Rust는 Cargo, Go는 Go repository, C/C++ library는 Conan repository로 운영합니다.

Raw repository는 embedded firmware와 board artifact처럼 file repository가 자연스러운 대상에 한정합니다. AI model, checkpoint, dataset 같은 큰 blob artifact는 기본적으로 S3/MinIO 계열 object storage를 우선하고, Hugging Face client 호환성이 필요해질 때 별도 repository로 판단합니다.

1-2. 관리 경계

Nexus 운영은 GitOps 리소스와 Nexus 애플리케이션 상태로 나뉩니다.

저장소 책임
tirosh-gitops Nexus namespace, Deployment/StatefulSet, PVC, Service, Ingress, backup CronJob
tirosh-infra Nexus repository, privilege, role, user, GitHub Actions package publishing settings, 수동 restore Job 생성

profile.toml은 Nexus desired state를 직접 담지 않고, site별 vars 파일 경로만 가리킵니다. Nexus repository catalog와 role/user desired state는 sites/<site_id>/nexus/vars.yml이 SoT입니다.

1-3. 기본 운영 원칙

  • consume endpoint는 group repository를 사용합니다. 예를 들어 Python install은 pypi를 봅니다.
  • publish endpoint는 hosted repository를 사용합니다. 예를 들어 Python publish는 pypi-hosted로 보냅니다.
  • guide/sample artifact는 *-guide-hosted repository로 publish합니다. 기존 group repository에는 guide hosted를 member로 포함해 consume endpoint를 유지합니다.
  • proxy repository는 외부 공식 registry cache입니다.
  • publish credential과 developer credential은 분리합니다.
  • 운영 restore는 GitOps에 영구 Job으로 두지 않고, 필요할 때만 one-off Job으로 생성합니다.

Conan repository를 사용할 때는 Nexus의 Conan Bearer Token Realm이 활성화되어 있어야 합니다. tirosh-infra는 site vars의 nexus_required_active_realms를 기준으로 ConanToken realm을 함께 수렴시킵니다.

2. 빠른 적용

2-1. 전체 순서

처음 구성할 때는 로컬 secret을 준비하고, Nexus 내부 상태를 적용한 뒤, GitHub Actions variables/secrets를 등록합니다.

cp .env.example .env
$EDITOR .env

make nexus/doctor SITE=tirosh-home
make nexus/apply SITE=tirosh-home
make nexus/github/apply

2-2. Local secrets 준비

.env.gitignore에 포함되어 있으므로 git에 커밋하지 않습니다.

NEXUS_ADMIN_USERNAME=admin
NEXUS_ADMIN_PASSWORD=<nexus-admin-password>
NEXUS_CI_PASSWORD=<new-ci-user-password>

NEXUS_BASE_URL=https://nexus.internal.tirosh.ai
NEXUS_PYPI_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/pypi-hosted/
NEXUS_PYPI_INDEX_URL=https://nexus.internal.tirosh.ai/repository/pypi/simple/
NEXUS_NPM_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/npm-hosted/
NEXUS_NPM_REGISTRY_URL=https://nexus.internal.tirosh.ai/repository/npm/
NEXUS_USERNAME=github-actions
NEXUS_PASSWORD=<NEXUS_CI_PASSWORD>
변수 의미
NEXUS_ADMIN_USERNAME Nexus REST API를 호출할 관리자 계정
NEXUS_ADMIN_PASSWORD 관리자 계정 비밀번호
NEXUS_CI_PASSWORD 생성 또는 갱신할 Nexus CI user의 비밀번호. 최소 8자 이상
NEXUS_BASE_URL GitHub Actions endpoint 변수를 자동 생성할 Nexus base URL
NEXUS_<ECOSYSTEM>_*_URL ecosystem별 endpoint override. 비어 있으면 NEXUS_BASE_URL 기준 기본값 사용
NEXUS_USERNAME GitHub Actions가 package install/publish에 사용할 Nexus user
NEXUS_PASSWORD GitHub Actions secret으로 등록할 Nexus password

NEXUS_PASSWORD가 비어 있거나 change-me이면 GitHub Actions 등록 target은 NEXUS_CI_PASSWORD를 대신 사용합니다.

2-3. Nexus state 적용

아래 target은 Nexus 내부 repository, privilege, role, user를 원하는 상태로 수렴시킵니다.

make nexus/doctor SITE=tirosh-home
make nexus/apply SITE=tirosh-home

내부적으로 실행되는 playbook은 다음입니다.

infra/ansible/playbooks/nexus/provision-repositories.yml

nexus/apply는 v2 public interface이고, 기존 nexus/repositories/apply는 같은 작업을 수행하는 상세 target으로 남아 있습니다.

CI user password를 강제로 다시 설정하려면 site vars에서 아래 값을 임시로 켭니다.

nexus_ci_user:
  reset_password: true

password reset 후에는 다시 false로 돌립니다.

2-4. GitHub Actions 설정 적용

Nexus CI user가 준비되면 GitHub repository에 package publish 설정을 등록합니다.

make nexus/github/apply

다른 GitHub repository에 등록할 때는 gh가 지원하는 GH_REPO를 지정합니다.

GH_REPO=tirosh-chain/tirosh-dev-guide make nexus/github/apply

등록되는 값은 다음입니다.

이름 GitHub 저장 위치
NEXUS_BASE_URL Variable https://nexus.internal.tirosh.ai
NEXUS_PYPI_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/pypi-hosted/
NEXUS_PYPI_INDEX_URL Variable https://nexus.internal.tirosh.ai/repository/pypi/simple/
NEXUS_NPM_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/npm-hosted/
NEXUS_NPM_REGISTRY_URL Variable https://nexus.internal.tirosh.ai/repository/npm/
NEXUS_NPM_REGISTRY Variable https://nexus.internal.tirosh.ai/repository/npm/
NEXUS_NPM_PUBLISH_REGISTRY Variable https://nexus.internal.tirosh.ai/repository/npm-hosted/
NEXUS_MAVEN_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/maven-hosted/
NEXUS_MAVEN_REPOSITORY_URL Variable https://nexus.internal.tirosh.ai/repository/maven/
NEXUS_HELM_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/helm-hosted/
NEXUS_HELM_REPOSITORY_URL Variable https://nexus.internal.tirosh.ai/repository/helm/
NEXUS_CARGO_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/cargo-hosted/
NEXUS_CARGO_REGISTRY_URL Variable https://nexus.internal.tirosh.ai/repository/cargo/
NEXUS_CARGO_PROXY_REGISTRY Variable sparse+https://nexus.internal.tirosh.ai/repository/cargo/
NEXUS_CARGO_REGISTRY_INDEX Variable sparse+https://nexus.internal.tirosh.ai/repository/cargo-hosted/
NEXUS_GO_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/go-hosted/
NEXUS_GO_PROXY_URL Variable https://nexus.internal.tirosh.ai/repository/go/
NEXUS_CONAN_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/conan-hosted/
NEXUS_CONAN_REMOTE_URL Variable https://nexus.internal.tirosh.ai/repository/conan/
NEXUS_CONAN_PUBLISH_REMOTE_URL Variable https://nexus.internal.tirosh.ai/repository/conan-hosted/
NEXUS_RAW_EMBEDDED_PUBLISH_URL Variable https://nexus.internal.tirosh.ai/repository/raw-embedded-hosted/
NEXUS_RAW_FIRMWARE_URL Variable https://nexus.internal.tirosh.ai/repository/raw-embedded-hosted
NEXUS_USERNAME Variable github-actions
NEXUS_PASSWORD Secret Nexus CI user password

tirosh-dev-guide처럼 guide artifact를 publish하는 repository는 GitHub Actions variable을 guide hosted endpoint로 override합니다. 이 값은 Nexus repository 생성이 아니라 GitHub repository 설정입니다.

GH_REPO=tirosh-chain/tirosh-dev-guide \
NEXUS_PYPI_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/pypi-guide-hosted/ \
NEXUS_NPM_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/npm-guide-hosted/ \
NEXUS_CARGO_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/cargo-guide-hosted/ \
NEXUS_CARGO_REGISTRY_INDEX=sparse+https://nexus.internal.tirosh.ai/repository/cargo-guide-hosted/ \
NEXUS_CONAN_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/conan-guide-hosted/ \
NEXUS_RAW_EMBEDDED_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/raw-embedded-guide-hosted/ \
NEXUS_RAW_FIRMWARE_URL=https://nexus.internal.tirosh.ai/repository/raw-embedded-guide-hosted \
  make nexus/github/apply

3. Desired State 구성

3-1. 설정 파일

Nexus desired state는 site profile, site vars, local env 세 곳으로 나뉩니다. 저장소에 남겨도 되는 값은 YAML/TOML에 두고, password와 token은 .env에 둡니다.

sites/<site_id>/profile.toml은 site별 vars 파일 경로만 가리킵니다.

[nexus]
vars = "sites/tirosh-home/nexus/vars.yml"

sites/tirosh-home/nexus/vars.yml은 Nexus repository catalog와 aggregate role/user 설정을 담습니다.

nexus_url: "https://nexus.internal.tirosh.ai"
nexus_privilege_name_prefix: "tirosh-nexus"

nexus_ci_user:
  id: "github-actions"
  first_name: "GitHub Actions"
  last_name: "Nexus CI"
  email: "github-actions@tirosh.local"
  reset_password: false
  roles:
    - "tirosh-nexus-publisher"
    - "tirosh-nexus-reader"

nexus_roles:
  publisher:
    id: "tirosh-nexus-publisher"
    name: "Tirosh Nexus Publisher"
    description: "Publish packages and artifacts to Tirosh Nexus repositories."
    actions: [BROWSE, READ, ADD]
  reader:
    id: "tirosh-nexus-reader"
    name: "Tirosh Nexus Reader"
    description: "Read packages and artifacts from Tirosh Nexus repositories."
    actions: [BROWSE, READ]

nexus_repository_defaults:
  hosted:
    blob_store: "default"
    write_policy: "ALLOW_ONCE"
    strict_content_type_validation: true
  proxy:
    blob_store: "default"
    strict_content_type_validation: true
    content_max_age: 1440
    metadata_max_age: 1440
  group:
    blob_store: "default"
    strict_content_type_validation: true

nexus_required_active_realms:
  - "ConanToken"

nexus_repositories:
  - format: "pypi"
    description: "Python packages"
    hosted:
      name: "pypi-hosted"
    proxy:
      name: "pypi-proxy"
      remote_url: "https://pypi.org/"
      pypi:
        index_path: "/simple"
    group:
      name: "pypi"
  - format: "npm"
    description: "Frontend npm packages"
    hosted:
      name: "npm-hosted"
    proxy:
      name: "npm-proxy"
      remote_url: "https://registry.npmjs.org"
    group:
      name: "npm"
  - format: "helm"
    description: "Kubernetes Helm charts"
    hosted:
      name: "helm-hosted"
    group:
      name: "helm"
  - format: "conan"
    description: "C and C++ Conan packages"
    hosted:
      name: "conan-hosted"
    proxy:
      name: "conan-proxy"
      remote_url: "https://center2.conan.io"
      conan:
        version: "V2"
    group:
      name: "conan"

Helm은 조직마다 upstream chart repository가 다르므로 기본 catalog에서는 helm-hosted만 group member로 둡니다.

3-2. Repository 이름

client가 바라보는 endpoint는 생태계 이름 그대로 둡니다.

종류 이름 규칙 용도
hosted <ecosystem>-hosted 내부 package publish pypi-hosted, npm-hosted
guide hosted <ecosystem>-guide-hosted guide/sample artifact publish pypi-guide-hosted, npm-guide-hosted
proxy <ecosystem>-proxy 또는 upstream 이름 포함 외부 공식 registry cache pypi-proxy, maven-central-proxy
group <ecosystem> developer/CI consume endpoint pypi, npm, maven

publish는 hosted repository로 보냅니다. install, restore, dependency resolution은 group repository를 봅니다. group repository는 내부 hosted, guide hosted, 외부 proxy를 한 endpoint로 묶습니다.

3-3. 생성되는 리소스

현재 catalog가 만드는 Nexus 상태는 다음입니다.

생태계 hosted proxy group
PyPI pypi-hosted, pypi-guide-hosted pypi-proxy pypi
npm npm-hosted, npm-guide-hosted npm-proxy npm
Maven maven-hosted maven-central-proxy maven
Helm helm-hosted 없음 helm
Cargo cargo-hosted, cargo-guide-hosted cargo-proxy cargo
Go go-hosted go-proxy go
Conan conan-hosted, conan-guide-hosted conan-proxy conan
Raw embedded raw-embedded-hosted, raw-embedded-guide-hosted 없음 없음

그 외에 아래 보안 리소스도 생성 또는 갱신합니다.

대상
Publisher role tirosh-nexus-publisher
Reader role tirosh-nexus-reader
CI user github-actions

repository write policy는 기본적으로 ALLOW_ONCE입니다. 같은 version artifact를 덮어쓰지 않도록 하기 위한 설정입니다.

Go hosted repository는 Nexus Repository 3.93.0부터 지원됩니다. Terraform hosted repository는 Nexus에서 지원하지만 GPG signing key와 passphrase가 필수이므로 별도 secret 설계가 준비될 때 추가합니다. Docker image는 현재 GHCR을 기준 registry로 사용하므로 Nexus 기본 catalog에 넣지 않습니다.

Conan 2 repository는 Nexus Repository 3.76.0 이상에서 proxy, hosted, group 구성을 지원합니다. C/C++ library package는 Conan hosted repository에 publish하고, .elf, .hex, .bin, .map 같은 firmware release artifact는 Raw repository에 보관합니다.

3-4. 권한 모델

Nexus credential은 consume과 publish 책임을 분리합니다. 개발자는 dependency를 내려받을 수 있어야 하지만 release artifact를 올리거나 수정할 필요는 없습니다. CI만 hosted repository에 publish하고, 사람 계정은 기본적으로 group repository를 read-only로 사용합니다.

주체 role actions 용도
GitHub Actions tirosh-nexus-publisher, tirosh-nexus-reader repository별 publisher actions, BROWSE, READ package install and publish
개발자 tirosh-nexus-reader BROWSE, READ package install

github-actions 계정은 GitHub Actions secret 전용입니다. CI는 build 중 dependency를 group repository에서 내려받고, release 때 hosted repository에 publish하므로 reader role과 publisher role을 모두 가집니다. publish 권한이 있으므로 사내 개발자에게 공유하지 않습니다.

package registry마다 publisher actions가 다른 이유는 publish API가 서로 다르기 때문입니다. Nexus의 repository-view privilege는 BROWSE, READ, ADD, EDIT, DELETE 같은 공통 action을 제공하지만, 각 package client가 publish 중 호출하는 HTTP/API 동작은 format마다 다릅니다.

단순 파일 업로드형 repository는 새 artifact 추가 권한인 ADD가 중심입니다. 반면 Cargo와 Conan처럼 archive upload와 registry metadata 갱신을 함께 수행하는 repository는 content 수정에 해당하는 EDIT도 필요할 수 있습니다. 이 차이는 guide repository 여부가 아니라 registry protocol의 차이입니다. 따라서 cargo-hostedcargo-guide-hosted는 같은 publisher actions를 사용합니다.

repository 유형 publisher actions 이유
기본 hosted package repository BROWSE, READ, ADD 새 artifact를 추가합니다. repository write policy는 ALLOW_ONCE이므로 같은 version overwrite는 막습니다.
Cargo hosted, Cargo guide hosted BROWSE, READ, ADD, EDIT cargo publish는 crate archive upload와 registry metadata 갱신을 함께 수행합니다.
Conan hosted, Conan guide hosted BROWSE, READ, ADD, EDIT Conan package publish는 recipe/package revision metadata 갱신을 포함합니다.
Raw embedded hosted BROWSE, READ, ADD firmware binary와 manifest를 append-only artifact로 보관합니다.
proxy/group repository reader actions only dependency consume endpoint입니다. CI와 개발자는 group/proxy에서 read만 수행합니다.

기업 운영에서는 보통 다음 기준을 적용합니다.

운영 기준 권장 방식 이유
사람 계정과 CI 계정 분리 developer는 reader role, CI는 publisher role 실수나 로컬 환경 유출로 release artifact가 변경되는 일을 줄입니다.
hosted와 group/proxy 분리 publish는 hosted, consume은 group dependency 소비자는 stable endpoint 하나만 사용하고, publish 권한은 hosted에만 부여합니다.
repository별 privilege 생성 tirosh-nexus-<repository>-publisher, tirosh-nexus-<repository>-reader registry별 권한 차이를 코드로 추적할 수 있습니다.
delete 권한 제한 기본 publisher role에는 DELETE를 넣지 않음 release artifact는 감사와 재현성 대상입니다. 삭제는 운영자 또는 break-glass 절차로 제한합니다.
overwrite 방지 hosted write policy는 ALLOW_ONCE 같은 version을 다시 publish하지 못하게 하여 build reproducibility를 지킵니다.
권한 코드화 Nexus UI 수동 변경 대신 Ansible vars로 관리 권한 변경 이유와 diff를 PR에서 검토할 수 있습니다.

참고 문서:

주제 레퍼런스 이 문서에서 사용하는 판단
Nexus repository-view privilege actions Sonatype Nexus Repository - Privileges ADD는 content 추가, EDIT은 repository content 수정 동작에 해당합니다.
Cargo registry publish/auth The Cargo Book - Registries, Registry Authentication Cargo publish는 registry endpoint와 token credential provider 구성이 필요합니다.
Conan revisions Conan 2 - Revisions Conan package publish는 recipe/package revision metadata를 다룹니다.

4. 적용 후 확인

4-1. Make output

Make target이 성공하면 마지막에 적용된 repository 목록이 출력됩니다.

Repositories: pypi-hosted, pypi-guide-hosted, npm-hosted, npm-guide-hosted, maven-hosted, helm-hosted, cargo-hosted, cargo-guide-hosted, go-hosted, conan-hosted, conan-guide-hosted, raw-embedded-hosted, raw-embedded-guide-hosted, pypi-proxy, npm-proxy, maven-central-proxy, cargo-proxy, go-proxy, conan-proxy, pypi, npm, maven, helm, cargo, go, conan

4-2. Nexus UI

화면 확인할 값
Repositories pypi-hosted, pypi-guide-hosted, npm-hosted, npm-guide-hosted, maven-hosted, helm-hosted, cargo-hosted, cargo-guide-hosted, go-hosted, conan-hosted, conan-guide-hosted, raw-embedded-hosted, raw-embedded-guide-hosted
Repositories pypi-proxy, npm-proxy, maven-central-proxy, cargo-proxy, go-proxy, conan-proxy
Repositories pypi, npm, maven, helm, cargo, go, conan
Security > Realms Conan Bearer Token Realm
Security > Privileges tirosh-nexus-<repository>-publisher, tirosh-nexus-<repository>-reader
Security > Roles tirosh-nexus-publisher, tirosh-nexus-reader
Security > Users github-actions

4-3. REST API

set -a
. ./.env
set +a

curl -fsS \
  -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
  https://nexus.internal.tirosh.ai/service/rest/v1/repositories/pypi/hosted/pypi-hosted

curl -fsS \
  -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
  https://nexus.internal.tirosh.ai/service/rest/v1/repositories/pypi/proxy/pypi-proxy

curl -fsS \
  -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
  https://nexus.internal.tirosh.ai/service/rest/v1/repositories/pypi/group/pypi

5. Package 사용

사내 개발자는 CI credential을 사용하지 않습니다. 개인 Nexus 계정 또는 SSO/LDAP group을 통해 필요한 reader role만 부여받습니다.

5-1. PyPI with uv

package를 사용하는 프로젝트에서는 Nexus group index를 pyproject.toml에 정의합니다.

[project]
dependencies = [
  "infra-tools",
  "object-store-cli",
]

[tool.uv.sources]
infra-tools = { index = "tirosh-pypi" }
object-store-cli = { index = "tirosh-pypi" }

[[tool.uv.index]]
name = "tirosh-pypi"
url = "https://nexus.internal.tirosh.ai/repository/pypi/simple/"
explicit = true
authenticate = "always"

이 repo는 root pyproject.toml에 같은 Nexus index를 이미 정의합니다.

explicit = true를 사용하면 [tool.uv.sources]에 명시한 package만 Nexus index에서 가져옵니다. 내부 package 이름과 같은 package가 외부 PyPI에 생기는 dependency confusion 위험을 줄이기 위해 이 방식을 기본으로 사용합니다.

credential은 pyproject.toml에 넣지 않습니다.

export UV_INDEX_TIROSH_PYPI_USERNAME="<nexus-user>"
export UV_INDEX_TIROSH_PYPI_PASSWORD="<nexus-password-or-token>"

uv lock
uv sync

환경 변수 이름은 index 이름 tirosh-pypi를 대문자로 바꾸고 구분자를 underscore로 바꾼 TIROSH_PYPI를 사용합니다.

5-2. PyPI with pip

pip만 쓰는 프로젝트에서는 pip 설정 또는 환경 변수를 사용합니다.

python -m pip install \
  --index-url https://nexus.internal.tirosh.ai/repository/pypi/simple/ \
  object-store-cli

pip URL에 credential을 직접 넣지 않는 편이 좋습니다. 필요한 경우 pip.conf, keyring, .netrc, CI secret 같은 별도 credential store를 사용합니다.

5-3. npm

frontend package install은 npm group registry를 사용하고, publish는 npm-hosted registry를 사용합니다.

npm config set @tirosh:registry https://nexus.internal.tirosh.ai/repository/npm/
npm publish --registry https://nexus.internal.tirosh.ai/repository/npm-hosted/

인증 정보는 project-local .npmrc에 직접 커밋하지 않고 개발자 환경이나 CI secret에서 주입합니다.

5-4. Other ecosystems

각 ecosystem의 표준 client가 Nexus repository URL을 바라보도록 설정합니다.

생태계 consume repository publish repository 기본 client
JVM maven maven-hosted Maven, Gradle
Kubernetes charts helm helm-hosted Helm
Rust cargo cargo-hosted Cargo
Go go go-hosted Go module tooling
C/C++ conan conan-hosted Conan 2

Go hosted repository는 Nexus 3.93.0 이상에서만 사용할 수 있습니다. Cargo는 Nexus 문서 기준 cargo publish를 통한 hosted repository publish를 지원합니다.

6. GitHub Actions Publishing

6-1. Workflow 대상

.github/workflows/python-packages.yml은 Python package를 검증하고, main branch push 또는 package release tag에서 Nexus PyPI hosted repository로 publish합니다.

make nexus/github/apply는 PyPI뿐 아니라 npm, Maven, Helm, Cargo, Go, Conan, Raw endpoint 변수도 함께 등록합니다. 현재 이 repo의 publish workflow는 Python package만 사용하므로 NEXUS_PYPI_*와 shared credential만 참조합니다. 다른 ecosystem workflow를 추가할 때는 이미 등록된 ecosystem별 NEXUS_*_URL 변수를 사용합니다.

package path release tag prefix
infra-tools packages/infra-tools infra-tools-v
object-store-cli packages/object-store-cli object-store-cli-v

PR에서는 package별로 아래 작업을 수행합니다.

uv sync
ruff check
pytest
uv build

tests 디렉터리가 없는 package는 test step에서 건너뜁니다.

6-2. Publish endpoint

publish는 hosted repository로 보내고, install/resolve는 group repository를 봅니다. make nexus/github/apply는 아래 endpoint를 GitHub Actions variable로 등록합니다.

생태계 publish variable consume variable
Python/PyPI NEXUS_PYPI_PUBLISH_URL NEXUS_PYPI_INDEX_URL
npm NEXUS_NPM_PUBLISH_URL, NEXUS_NPM_PUBLISH_REGISTRY NEXUS_NPM_REGISTRY_URL, NEXUS_NPM_REGISTRY
Maven NEXUS_MAVEN_PUBLISH_URL NEXUS_MAVEN_REPOSITORY_URL
Helm NEXUS_HELM_PUBLISH_URL NEXUS_HELM_REPOSITORY_URL
Cargo NEXUS_CARGO_PUBLISH_URL, NEXUS_CARGO_REGISTRY_INDEX NEXUS_CARGO_REGISTRY_URL, NEXUS_CARGO_PROXY_REGISTRY
Go NEXUS_GO_PUBLISH_URL NEXUS_GO_PROXY_URL
Conan NEXUS_CONAN_PUBLISH_URL, NEXUS_CONAN_PUBLISH_REMOTE_URL NEXUS_CONAN_REMOTE_URL
Raw embedded NEXUS_RAW_EMBEDDED_PUBLISH_URL, NEXUS_RAW_FIRMWARE_URL 직접 artifact URL

현재 tirosh-infra 자체 publish workflow는 Python package인 infra-tools, object-store-cli만 Nexus에 올립니다. 하지만 Nexus 운영 기준과 GitHub Actions variable catalog는 PyPI에 한정하지 않습니다. tirosh-dev-guide 같은 source repo가 npm, Cargo, Conan, firmware artifact workflow를 추가하면 같은 Nexus endpoint catalog를 사용합니다.

Nexus hosted repository는 같은 version 또는 같은 artifact path를 다시 올리는 일을 허용하지 않는 방향으로 운영합니다. 따라서 같은 commit의 publish job을 재실행하면 ecosystem에 따라 중복 version, 중복 package, 중복 path 오류가 날 수 있습니다.

6-3. Versioning

Version은 Nexus가 새로 정하지 않습니다. Nexus는 각 ecosystem의 package metadata와 artifact path를 저장하고 제공하며, version 계산은 source repo CI와 package manager가 맡습니다.

기본 정책은 release tag가 있으면 고정 release version을 쓰고, release tag가 없으면 추적 가능한 dev version을 쓰는 것입니다. Git tag를 매 commit마다 만들지 않고 CI run 안에서 dev version을 계산하는 이유는 tag 폭증을 막으면서도 artifact가 어느 source revision에서 왔는지 남기기 위해서입니다.

생태계 release 예시 dev 예시 version을 반영하는 곳
Python/PyPI 0.1.0 0.2.0.dev37 pyproject.toml, wheel/sdist metadata
npm 0.1.0 0.2.0-dev.37 package.json
Maven 0.1.0 0.2.0-dev.37 또는 0.2.0-SNAPSHOT pom.xml, Gradle project version
Helm 0.1.0 0.2.0-dev.37 Chart.yamlversion
Cargo 0.1.0 0.2.0-dev.37 Cargo.toml
Go v0.1.0 pseudo-version 또는 module-path dev tag module path와 Git tag
Conan 0.1.0 0.2.0-dev.37 Conan package reference
Raw embedded 0.1.0 0.2.0-dev.37 artifact path, filename, manifest

Python만 0.2.0.dev37처럼 점(.)을 사용합니다. PEP 440이 SemVer prerelease 문자열인 0.2.0-dev.37을 그대로 쓰지 않기 때문입니다. npm, Cargo, Helm, Conan은 SemVer prerelease 표기를 자연스럽게 사용합니다.

Version을 package metadata에 반영할 때는 생태계별 표준 도구를 사용합니다. 직접 sed로 파일을 고치지 않습니다.

생태계 CI에서 쓰는 도구 이유
Python/uv uv version --package ... --frozen 또는 dynamic version backend PEP 440 version을 표준 metadata에 반영합니다.
npm npm version --no-git-tag-version --allow-same-version package.json과 npm metadata를 표준 방식으로 갱신합니다.
Maven/Gradle Maven Versions Plugin, Gradle property/task JVM build tool이 metadata와 publish coordinate를 관리합니다.
Helm chart packaging step에서 Chart.yaml version 반영 chart archive 이름과 index metadata가 같은 version을 봅니다.
Cargo cargo set-version Cargo.toml 갱신은 Cargo 생태계 도구에 맡깁니다.
Go module-path semver tag 또는 pseudo-version Go는 registry upload보다 VCS tag와 module proxy가 배포 단위입니다.
Conan conan create --version ... recipe invocation에서 package version을 명시합니다.
Raw embedded CI가 filename/path/manifest 생성 package metadata가 없으므로 artifact 경로와 manifest가 release identity입니다.

이 repo의 packages/ 아래 Python package는 pyproject.toml에 version을 직접 쓰지 않고 hatch-vcs 기반 dynamic version을 사용합니다. build 시점에 Git tag와 현재 commit 사이의 거리를 읽어 wheel/sdist version을 계산합니다.

tirosh-infra는 monorepo이므로 Python package마다 자기 tag만 봅니다. infra-toolsinfra-tools-v*, object-store-cliobject-store-cli-v* tag만 version source로 사용합니다.

package tag pattern fallback
infra-tools infra-tools-v<version> 0.1.0.dev0
object-store-cli object-store-cli-v<version> 0.1.0.dev0
git tag infra-tools-v0.1.0
git push origin infra-tools-v0.1.0

git tag object-store-cli-v0.1.0
git push origin object-store-cli-v0.1.0

버전이 계산되는 방식은 아래처럼 읽으면 됩니다.

상황 예시 의미
matching release tag가 아직 없음 0.1.0.dev0 첫 release 전 fallback version입니다.
현재 commit이 infra-tools-v0.1.0 tag와 정확히 일치 0.1.0 Nexus에 올리는 고정 release version입니다.
infra-tools-v0.1.0 이후 7번째 commit 0.1.1.dev7 다음 patch release 후보를 향한 dev build입니다.
현재 commit이 object-store-cli-v0.2.0 tag와 정확히 일치 0.2.0 object-store-cli만의 release입니다. infra-tools version에는 영향을 주지 않습니다.

local_scheme = "no-local-version"을 사용하므로 build 산출물 version에 +g<sha> 같은 local version segment를 붙이지 않습니다. Nexus/PyPI 계열 repository에서는 같은 version을 다시 업로드할 수 없기 때문에, 같은 commit의 publish job을 재실행하면 중복 version으로 실패할 수 있습니다.

workflow는 fetch-depth: 0으로 checkout합니다. shallow checkout이면 tag와 commit distance를 알 수 없어 dynamic version 계산이 흔들릴 수 있으므로, package publish workflow에서는 전체 Git history와 tag를 가져와야 합니다.

7. Backup과 Restore

Nexus backup/restore는 GitOps, infra-tools, backup image가 역할을 나눠서 처리합니다. 어디서 무엇을 책임지는지 먼저 정리하고, 그 다음에 rehearsal과 운영 restore 절차를 봅니다.

7-1. 전체 운영 모델

Nexus의 실제 데이터는 nexus namespace의 nexus-data PVC에 있습니다. backup은 이 PVC를 archive해서 object storage에 올리고, restore는 선택한 backup id를 내려받아 같은 PVC를 덮어씁니다.

Nexus Deployment
  -> nexus-data PVC
  -> CronJob/nexus-backup
  -> object storage: nexus/backups/<backup-id>/

restore는 반대 방향으로 진행합니다.

object storage backup id
  -> infra-tools nexus-restore
  -> one-off Kubernetes Job
  -> existing nexus-data PVC overwrite
  -> Nexus ready, API, package serving 확인

운영 원칙은 세 가지입니다.

원칙 설명
정기 backup은 GitOps가 관리 CronJob, RBAC, ConfigMap, PVC mount는 tirosh-gitops에 둡니다.
restore 실행은 infra-tools가 생성 live CronJob의 image, env, volume, service account를 읽어서 one-off Job을 만듭니다.
운영 restore는 PVC overwrite 새 PVC로 복원하는 rehearsal이 아니라, 기존 nexus-data PVC를 덮어쓰는 destructive 작업입니다.

7-2. GitOps가 맡는 리소스

이 단계에서는 infra 도구로 만든 backup/restore 흐름을 클러스터의 상시 리소스로 연결합니다. 독자가 바로 찾아볼 수 있도록 실제 위치를 기준으로 확인합니다.

리소스 위치 역할
backup CronJob tirosh-gitops/catalog/platform/nexus/base/backup-cronjob.yaml 매일 03:00 Asia/Seoul에 backup image를 실행합니다.
backup RBAC tirosh-gitops/catalog/platform/nexus/base/backup-rbac.yaml Nexus deployment scale/get/watch, Pod 조회, PVC 접근에 필요한 권한을 제공합니다.
restore Job 예시 tirosh-gitops/catalog/platform/nexus/base/backup-restore-job.example.yaml 운영에 상시 적용하지 않는 참고용 Job 형태입니다.
object storage ConfigMap tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml endpoint, region, bucket, prefix를 제공합니다.
object storage Secret tirosh-infra/sites/tirosh-home/secrets.toml local에서 infra-tools로 Kubernetes Secret을 생성합니다. 값은 GitOps에 commit하지 않습니다.
GHCR image pull Secret tirosh-infra/sites/tirosh-home/secrets.toml private GHCR image pull용 kubernetes.io/dockerconfigjson Secret을 만듭니다.

GitOps의 CronJob은 아래 흐름을 기준으로 구성됩니다.

schedule: "0 3 * * *"
timeZone: "Asia/Seoul"
containers:
  - name: nexus-backup
    image: ghcr.io/tirosh-chain/nexus-backup:latest
    args:
      - --namespace
      - nexus
      - --deployment
      - nexus
      - --selector
      - app.kubernetes.io/name=nexus
      - --data-path
      - /nexus-data
      - --bucket
      - $(OBJECT_STORAGE_BUCKET)
      - --prefix
      - $(OBJECT_STORAGE_PREFIX)
      - backup

여기서 중요한 점은 restore가 별도의 설정 파일을 다시 해석하지 않는다는 것입니다. infra-tools nexus-restore는 live CronJob/nexus-backup을 읽고, 그 CronJob의 image, env, volume, service account 설정을 복사해서 restore Job을 만듭니다. 그래서 backup과 restore는 같은 bucket, prefix, credential, PVC mount를 사용합니다.

7-3. Backup image

Nexus /nexus-data 백업/복구 도구는 tools/nexus-backup에 둡니다. object storage 접근은 packages/object-store-cli를 사용하고, 운영용 container image는 GHCR에 publish합니다.

대상
CLI package tools/nexus-backup
object storage package packages/object-store-cli
image ghcr.io/tirosh-chain/nexus-backup
workflow .github/workflows/nexus-backup-image.yml
self-hosted validation .github/workflows/nexus-backup-self-hosted-validation.yml

image workflow는 package test, lint, wheel build, Docker build를 검증하고 main 또는 nexus-backup-v* tag에서 image를 publish합니다. workflow는 tirosh-ubuntu-latest label을 가진 self-hosted runner pool에서 실행합니다.

tirosh-home runner에서만 검증해야 하는 경우 self-hosted validation workflow를 수동 실행합니다.

GitHub Actions
  -> nexus-backup-self-hosted-validation
  -> Run workflow

현재 runner pool이 한 대뿐이면 image build와 frontend E2E가 동시에 밀릴 수 있으므로, GitHub CI 문서의 runner pool 확장 절차에 따라 github-ci-02 이후 VM을 추가합니다.

7-4. Object storage와 Secret

object storage 설정은 공개 가능한 연결 정보와 비밀값으로 나눕니다.

위치 관리 방식
OBJECT_STORAGE_ENDPOINT_URL tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml GitOps ConfigMap
OBJECT_STORAGE_REGION tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml GitOps ConfigMap
OBJECT_STORAGE_BUCKET tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml GitOps ConfigMap
OBJECT_STORAGE_PREFIX tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml GitOps ConfigMap
OBJECT_STORAGE_ACCESS_KEY_ID tirosh-infra/sites/tirosh-home/secrets.tomlnexus-backup-object-store entry local env에서 Kubernetes Secret 생성
OBJECT_STORAGE_SECRET_ACCESS_KEY tirosh-infra/sites/tirosh-home/secrets.tomlnexus-backup-object-store entry local env에서 Kubernetes Secret 생성

Object storage는 AWS/NCloud/MinIO 같은 S3-compatible provider를 같은 방식으로 다루기 위해 OBJECT_STORAGE_* env contract를 유지합니다. 로컬 검증에서만 AWS profile을 쓰고 싶다면 OBJECT_STORAGE_PROFILE 또는 object-store --profile <profile>을 사용할 수 있지만, Kubernetes Secret으로 전달되는 운영 credential은 provider-neutral env 이름을 기준으로 관리합니다.

inventory.toml이나 site profile에는 bucket/prefix를 넣지 않습니다. 이 값은 Kubernetes에서 CronJob이 읽는 런타임 설정이고, infra-tools는 live CronJob을 기준으로 restore Job을 만들기 때문입니다.

GHCR package가 private이면 backup CronJob이 image를 pull할 수 있도록 image pull Secret도 먼저 등록합니다. token은 GitHub classic PAT이고 scope는 read:packages만 사용합니다.

organization 자체 token이 아니라, 개인 계정 또는 machine user에서 만든 token을 씁니다. Kubernetes Secret과 imagePullSecrets는 namespace-scoped이므로, 현재는 nexus-backup CronJob이 실행되는 nexus namespace에 Secret을 생성합니다.

export GHCR_USERNAME="<github-user-or-machine-user>"
export GHCR_PULL_TOKEN="<classic-pat-with-read-packages>"

make kubernetes/image-pull-secret/plan SITE=tirosh-home
make kubernetes/image-pull-secret/apply SITE=tirosh-home

unset GHCR_USERNAME GHCR_PULL_TOKEN

plan은 실제 token 값을 출력하지 않는 redacted manifest 미리보기입니다. applysites/tirosh-home/secrets.tomlghcr-pull entry를 읽고, kubernetes.io/dockerconfigjson Secret을 nexus namespace에 적용합니다. GitOps 쪽 ServiceAccount 또는 Pod spec은 이 Secret을 imagePullSecrets로 참조해야 합니다.

7-5. Backup 확인

nexus-backup backuprestore는 실제 Nexus deployment replica를 조정하고 /nexus-data를 archive 또는 overwrite합니다. 운영 namespace에서 dry-run처럼 실행하지 않습니다.

먼저 bucket, prefix, credential이 맞는지 object-store dry-run으로 확인합니다.

export OBJECT_STORAGE_ENDPOINT_URL="<object-storage-endpoint>"
export OBJECT_STORAGE_REGION="<region>"
export OBJECT_STORAGE_ACCESS_KEY_ID="<access-key>"
export OBJECT_STORAGE_SECRET_ACCESS_KEY="<secret-key>"
export OBJECT_STORAGE_BUCKET="<backup-bucket>"
export OBJECT_STORAGE_PREFIX="nexus/backups/rehearsal"

tmpdir="$(mktemp -d)"
printf 'nexus-backup rehearsal\n' > "$tmpdir/probe.txt"

object-store \
  --bucket "$OBJECT_STORAGE_BUCKET" \
  --prefix "$OBJECT_STORAGE_PREFIX/" \
  --dry-run \
  upload --source "$tmpdir"

object-store \
  --bucket "$OBJECT_STORAGE_BUCKET" \
  --prefix "nexus/backups/" \
  list --json

GitOps가 적용된 뒤에는 CronJob/nexus-backup이 하루 한 번 실행됩니다. 수동 확인이 필요하면 마지막 Job의 성공 여부와 object storage prefix를 함께 봅니다.

kubectl -n nexus get cronjob nexus-backup
kubectl -n nexus get jobs -l app.kubernetes.io/name=nexus-backup

7-6. Restore rehearsal

restore rehearsal의 목표는 단순히 Pod가 ready가 되는지 보는 것이 아닙니다. 복구된 Nexus가 backup 시점의 repository, component, artifact를 실제로 제공하는지 확인해야 합니다.

권장 방식은 임시 namespace와 임시 PVC에 restore한 뒤, restored Nexus를 별도로 띄워 비교하는 것입니다. make nexus/restore/*는 운영 PVC overwrite용 명령이므로 rehearsal에서는 바로 사용하지 않습니다.

rehearsal에서 확인할 항목은 다음입니다.

확인 기준
backup id nexus/backups/<backup-id>/ prefix 생성
archive nexus-data.tar.gz 존재
manifest manifest.json 존재, deployment/image/namespace가 기대값
checksum sha256sum.txt 존재, restore 전 검증 통과
ownership 필요 시 --restore-owner 200:200으로 /nexus-data 권한 보정
restore Nexus deployment가 다시 ready 상태
repository API restored Nexus의 repository 목록과 component count가 기준 Nexus와 일치
package serving 대표 hosted repository의 index와 artifact download가 기준 Nexus와 일치

deployment ready는 restore의 최소 조건입니다. rehearsal에서는 port-forward로 기준 Nexus와 restored Nexus를 동시에 열고, hosted repository의 목록과 대표 artifact를 비교합니다.

curl -fsS -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
  http://127.0.0.1:18081/service/rest/v1/repositories

curl -fsS -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
  http://127.0.0.1:18081/repository/pypi-hosted/simple/

curl -fsS -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
  http://127.0.0.1:18081/repository/pypi-hosted/packages/<package>/<version>/<file> \
  -o /tmp/restored-artifact

기준 Nexus에서 받은 같은 artifact와 checksum 또는 byte 비교가 일치하면, 복구된 Nexus가 backup 시점의 package를 실제로 serve한다고 볼 수 있습니다.

7-7. 운영 Restore 실행

운영 restore는 기존 nexus-data PVC를 덮어쓰는 operating PVC overwrite restore입니다. GitOps에 영구 Job으로 두지 않고, 필요할 때만 infra-tools로 one-off Job을 생성합니다.

먼저 manifest를 확인합니다.

make nexus/restore/plan \
  SITE=tirosh-home \
  BACKUP_ID=2026-06-19T092915Z

실제 restore는 명시 확인을 붙여 실행합니다. CONFIRM_RESTORE=true를 넘겨도 바로 진행하지 않고, 실행 중 경고 메시지에서 정확히 yes를 입력해야 합니다.

make nexus/restore/apply \
  SITE=tirosh-home \
  BACKUP_ID=2026-06-19T092915Z \
  CONFIRM_RESTORE=true
WARNING: this is an operating PVC overwrite restore.
Type exactly 'yes' to continue.
Restore confirmation:

기본 restore owner는 200:200입니다. 필요하면 NEXUS_RESTORE_OWNER=<uid:gid>로 바꿉니다. restore가 끝나면 7-6의 API와 artifact serving 확인을 운영 Nexus에도 적용합니다.

8. 문제 확인

문제가 생기면 local secret, Nexus API, GitHub Actions 설정, 개발자 credential 순서로 확인합니다.

8-1. Local secret 확인

credential이 없으면 Make target이 실행 전에 멈춥니다.

error: NEXUS_ADMIN_USERNAME is required
hint: set it in .env or export it

이 경우 .env에 필요한 key가 있는지 확인합니다. 값 자체는 출력하지 않는 편이 안전합니다.

awk -F= '/^[A-Za-z_][A-Za-z0-9_]*=/ { print $1 }' .env

8-2. Nexus API

Nexus에 접속할 수 있는지 확인합니다.

curl -fsS https://nexus.internal.tirosh.ai/service/rest/swagger.json >/dev/null

API 인증이 실패하면 admin credential을 확인합니다.

set -a
. ./.env
set +a

curl -fsS \
  -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
  https://nexus.internal.tirosh.ai/service/rest/v1/security/users?userId=github-actions

8-3. GitHub Actions

publish workflow가 Missing credentials로 실패하면 NEXUS_PYPI_PUBLISH_URL, NEXUS_PYPI_INDEX_URL, NEXUS_USERNAME, NEXUS_PASSWORD가 등록되어 있는지 확인합니다.

gh variable list
gh secret list

8-4. Developer auth

개발자가 uv sync에서 인증 실패를 보면 reader role과 환경 변수 이름을 확인합니다.

echo "$UV_INDEX_TIROSH_PYPI_USERNAME"
test -n "$UV_INDEX_TIROSH_PYPI_PASSWORD"